/* * ArcMenu - A traditional application menu for GNOME 3 * * ArcMenu Lead Developer and Maintainer * Andrew Zaech https://gitlab.com/AndrewZaech * * ArcMenu Founder, Former Maintainer, and Former Graphic Designer * LinxGem33 https://gitlab.com/LinxGem33 - (No Longer Active) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * Credits: This file leverages the work from GNOME Shell search.js file * (https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/master/js/ui/search.js) */ const Me = imports.misc.extensionUtils.getCurrentExtension(); const {Clutter, Gio, GLib, GObject, Shell, St } = imports.gi; const AppDisplay = imports.ui.appDisplay; const appSys = Shell.AppSystem.get_default(); const Constants = Me.imports.constants; const Gettext = imports.gettext.domain(Me.metadata['gettext-domain']); const MW = Me.imports.menuWidgets; const PopupMenu = imports.ui.popupMenu; const { RecentFilesManager } = Me.imports.recentFilesManager; const RemoteSearch = imports.ui.remoteSearch; const Utils = Me.imports.utils; const _ = Gettext.gettext; const { OpenWindowSearchProvider } = Me.imports.searchProviders.openWindows; const { RecentFilesSearchProvider } = Me.imports.searchProviders.recentFiles; const SEARCH_PROVIDERS_SCHEMA = 'org.gnome.desktop.search-providers'; var ListSearchResult = GObject.registerClass(class Arc_Menu_ListSearchResult extends MW.ApplicationMenuItem{ _init(provider, metaInfo, resultsView) { let menulayout = resultsView._menuLayout; let app = appSys.lookup_app(metaInfo['id']); super._init(menulayout, app, Constants.DisplayType.LIST, metaInfo) this.app = app; let layoutProperties = this._menuLayout.layoutProperties; this.searchType = layoutProperties.SearchDisplayType; this.metaInfo = metaInfo; this.provider = provider; this._settings = this._menuLayout._settings; this.resultsView = resultsView; this.layout = this._settings.get_enum('menu-layout'); if(this.provider.id === 'org.gnome.Nautilus.desktop' || this.provider.id === 'arcmenu.recent-files') this._path = this.metaInfo['description']; let highlightSearchResultTerms = this._settings.get_boolean('highlight-search-result-terms'); if(highlightSearchResultTerms){ this._termsChangedId = this.resultsView.connect('terms-changed', this._highlightTerms.bind(this)); this._highlightTerms(); } let showSearchResultDescriptions = this._settings.get_boolean("show-search-result-details"); if(this.metaInfo['description'] && this.provider.appInfo.get_id() === 'org.gnome.Calculator.desktop' && !showSearchResultDescriptions) this.label.text = this.metaInfo['name'] + " " + this.metaInfo['description']; if(!this.app && this.metaInfo['description']) this.description = this.metaInfo['description'].split('\n')[0]; this.connect('destroy', this._onDestroy.bind(this)); } _onDestroy() { if (this._termsChangedId) { this.resultsView.disconnect(this._termsChangedId); this._termsChangedId = null; } } _highlightTerms() { let showSearchResultDescriptions = this._settings.get_boolean("show-search-result-details"); if(this.descriptionLabel && showSearchResultDescriptions){ let descriptionMarkup = this.resultsView.highlightTerms(this.metaInfo['description'].split('\n')[0]); this.descriptionLabel.clutter_text.set_markup(descriptionMarkup); } let labelMarkup = this.resultsView.highlightTerms(this.label.text.split('\n')[0]); this.label.clutter_text.set_markup(labelMarkup); } }); var AppSearchResult = GObject.registerClass(class Arc_Menu_AppSearchResult extends MW.ApplicationMenuItem{ _init(provider, metaInfo, resultsView) { let menulayout = resultsView._menuLayout; let app = appSys.lookup_app(metaInfo['id']) || appSys.lookup_app(provider.id); let displayType = menulayout.layoutProperties.SearchDisplayType; super._init(menulayout, app, displayType, metaInfo); this.app = app; this.provider = provider; this.metaInfo = metaInfo; this.resultsView = resultsView; if(!this.app && this.metaInfo['description']) this.description = this.metaInfo['description'].split('\n')[0]; let highlightSearchResultTerms = this._settings.get_boolean('highlight-search-result-terms'); if(highlightSearchResultTerms){ this._termsChangedId = this.resultsView.connect('terms-changed', this._highlightTerms.bind(this)); this._highlightTerms(); } this.connect('destroy', this._onDestroy.bind(this)); } _onDestroy() { if (this._termsChangedId) { this.resultsView.disconnect(this._termsChangedId); this._termsChangedId = null; } } _highlightTerms() { let showSearchResultDescriptions = this._settings.get_boolean("show-search-result-details"); if(this.descriptionLabel && showSearchResultDescriptions){ let descriptionMarkup = this.resultsView.highlightTerms(this.descriptionLabel.text.split('\n')[0]); this.descriptionLabel.clutter_text.set_markup(descriptionMarkup); } let labelMarkup = this.resultsView.highlightTerms(this.label.text.split('\n')[0]); this.label.clutter_text.set_markup(labelMarkup); } }); var SearchResultsBase = GObject.registerClass({ Signals: { 'terms-changed': {}, 'no-results': {} }, }, class ArcMenu_SearchResultsBase extends St.BoxLayout { _init(provider, resultsView) { super._init({ vertical: true }); this.provider = provider; this.resultsView = resultsView; this._menuLayout = resultsView._menuLayout; this._terms = []; this._resultDisplayBin = new St.Bin({ x_expand: true, y_expand: true }); this.add_child(this._resultDisplayBin); this._resultDisplays = {}; this._clipboard = St.Clipboard.get_default(); this._cancellable = new Gio.Cancellable(); this.connect('destroy', this._onDestroy.bind(this)); } _onDestroy() { this._terms = []; } _createResultDisplay(meta) { if (this.provider.createResultObject) return this.provider.createResultObject(meta, this.resultsView); return null; } clear() { this._cancellable.cancel(); for (let resultId in this._resultDisplays) this._resultDisplays[resultId].destroy(); this._resultDisplays = {}; this._clearResultDisplay(); this.hide(); } _setMoreCount(count) { } _ensureResultActors(results, callback) { let metasNeeded = results.filter( resultId => this._resultDisplays[resultId] === undefined ); if (metasNeeded.length === 0) { callback(true); } else { this._cancellable.cancel(); this._cancellable.reset(); this.provider.getResultMetas(metasNeeded, metas => { if (this._cancellable.is_cancelled()) { if (metas.length > 0) log(`Search provider ${this.provider.id} returned results after the request was canceled`); callback(false); return; } if (metas.length != metasNeeded.length) { log('Wrong number of result metas returned by search provider ' + this.provider.id + ': expected ' + metasNeeded.length + ' but got ' + metas.length); callback(false); return; } if (metas.some(meta => !meta.name || !meta.id)) { log('Invalid result meta returned from search provider ' + this.provider.id); callback(false); return; } metasNeeded.forEach((resultId, i) => { let meta = metas[i]; let display = this._createResultDisplay(meta); this._resultDisplays[resultId] = display; }); callback(true); }, this._cancellable); } } updateSearch(providerResults, terms, callback) { this._terms = terms; if (providerResults.length == 0) { this._clearResultDisplay(); this.hide(); callback(); } else { let maxResults = this._getMaxDisplayedResults(); let results = this.provider.filterResults(providerResults, maxResults); let moreCount = Math.max(providerResults.length - results.length, 0); this._ensureResultActors(results, successful => { if (!successful) { this._clearResultDisplay(); callback(); return; } // To avoid CSS transitions causing flickering when // the first search result stays the same, we hide the // content while filling in the results. this.hide(); this._clearResultDisplay(); results.forEach(resultId => { this._addItem(this._resultDisplays[resultId]); }); this._setMoreCount(this.provider.canLaunchSearch ? moreCount : 0); this.show(); callback(); }); } } }); var ListSearchResults = GObject.registerClass( class ArcMenu_ListSearchResults extends SearchResultsBase { _init(provider, resultsView) { super._init(provider, resultsView); this._menuLayout = resultsView._menuLayout; this.searchType = this._menuLayout.layoutProperties.SearchDisplayType; this._settings = this._menuLayout._settings; this.layout = this._settings.get_enum('menu-layout'); this._container = new St.BoxLayout({ vertical: true, x_align: Clutter.ActorAlign.FILL, y_align: Clutter.ActorAlign.FILL, x_expand: true, y_expand: true, }); if(this.searchType === Constants.DisplayType.GRID){ this.add_style_class_name('margin-box'); } this.providerInfo = new ArcSearchProviderInfo(provider, this._menuLayout); this.providerInfo.connect('activate', () => { if (provider.canLaunchSearch) { provider.launchSearch(this._terms); this._menuLayout.arcMenu.toggle(); } }); this._container.add_child(this.providerInfo); this._content = new St.BoxLayout({ vertical: true, x_expand: true, y_expand: true, x_align: Clutter.ActorAlign.FILL }); this._container.add_child(this._content); this._resultDisplayBin.set_child(this._container); } _setMoreCount(count) { this.providerInfo.setMoreCount(count); } _getMaxDisplayedResults() { return this._settings.get_int('max-search-results'); } _clearResultDisplay() { this._content.remove_all_children(); } _createResultDisplay(meta) { return super._createResultDisplay(meta, this.resultsView) || new ListSearchResult(this.provider, meta, this.resultsView); } _addItem(display) { if(display.get_parent()) display.get_parent().remove_child(display); this._content.add_child(display); } getFirstResult() { if (this._content.get_n_children() > 0) return this._content.get_child_at_index(0)._delegate; else return null; } }); var AppSearchResults = GObject.registerClass( class ArcMenu_AppSearchResults extends SearchResultsBase { _init(provider, resultsView) { super._init(provider, resultsView); this._parentContainer = resultsView; this._menuLayout = resultsView._menuLayout; this._settings = this._menuLayout._settings; this.layoutProperties = this._menuLayout.layoutProperties; this.searchType = this.layoutProperties.SearchDisplayType; this.layout = this._menuLayout._settings.get_enum('menu-layout'); this.itemCount = 0; this.gridTop = -1; this.gridLeft = 0; this.rtl = this._menuLayout.mainBox.get_text_direction() == Clutter.TextDirection.RTL; let layout = new Clutter.GridLayout({ orientation: Clutter.Orientation.VERTICAL, column_spacing: this.searchType === Constants.DisplayType.GRID ? this.layoutProperties.ColumnSpacing : 0, row_spacing: this.searchType === Constants.DisplayType.GRID ? this.layoutProperties.RowSpacing : 0, }); this._grid = new St.Widget({ x_expand: true, x_align: this.searchType === Constants.DisplayType.LIST ? Clutter.ActorAlign.FILL : Clutter.ActorAlign.CENTER, layout_manager: layout }); layout.hookup_style(this._grid); if(this.searchType === Constants.DisplayType.GRID){ let spacing = this.layoutProperties.ColumnSpacing; this._grid.style = "padding: 0px 0px 10px 0px; spacing: " + spacing + "px;"; this._resultDisplayBin.x_align = Clutter.ActorAlign.CENTER; } this._resultDisplayBin.set_child(this._grid); } _getMaxDisplayedResults() { let maxDisplayedResults; if(this.searchType === Constants.DisplayType.GRID) maxDisplayedResults = this._menuLayout.getColumnsFromGridIconSizeSetting(); else maxDisplayedResults = this._settings.get_int('max-search-results'); return maxDisplayedResults; } _clearResultDisplay() { this.itemCount = 0; this.gridTop = -1; this.gridLeft = 0; this._grid.remove_all_children(); } _createResultDisplay(meta) { return new AppSearchResult(this.provider, meta, this.resultsView); } _addItem(display) { const GridColumns = this.searchType === Constants.DisplayType.LIST ? 1 : this._menuLayout.getColumnsFromGridIconSizeSetting(); if(!this.rtl && (this.itemCount % GridColumns === 0)){ this.gridTop++; this.gridLeft = 0; } else if(this.rtl && (this.gridLeft === 0)){ this.gridTop++; this.gridLeft = GridColumns; } this._grid.layout_manager.attach(display, this.gridLeft, this.gridTop, 1, 1); display.gridLocation = [this.gridLeft, this.gridTop]; if(!this.rtl) this.gridLeft++; else if(this.rtl) this.gridLeft--; this.itemCount++; } getFirstResult() { if (this._grid.get_n_children() > 0) return this._grid.get_child_at_index(0)._delegate; else return null; } }); var SearchResults = GObject.registerClass({ Signals: { 'terms-changed': {}, 'have-results': {}, 'no-results': {} }, }, class ArcMenu_SearchResults extends St.BoxLayout { _init(menuLayout) { super._init({ vertical: true, y_expand: true, x_expand: true, x_align: Clutter.ActorAlign.FILL }); this._menuLayout = menuLayout; this.layoutProperties = this._menuLayout.layoutProperties; this.searchType = this.layoutProperties.SearchDisplayType; this._settings = this._menuLayout._settings; this.layout = this._settings.get_enum('menu-layout'); this._content = new St.BoxLayout({ vertical: true, x_align: Clutter.ActorAlign.FILL }); this.add_child(this._content); this._statusText = new St.Label(); this._statusBin = new St.Bin({ x_align: Clutter.ActorAlign.CENTER, y_align: Clutter.ActorAlign.CENTER, x_expand: true, y_expand: true }); if(menuLayout._settings.get_boolean('enable-custom-arc-menu')) this._statusText.style_class = 'arc-menu-status-text'; else this._statusText.style_class = ''; this.add_child(this._statusBin); this._statusBin.set_child(this._statusText); this._highlightDefault = true; this._defaultResult = null; this._startingSearch = false; this._terms = []; this._results = {}; this._providers = []; this._highlightRegex = null; this.recentFilesManager = new RecentFilesManager(); this._searchSettings = new Gio.Settings({ schema_id: SEARCH_PROVIDERS_SCHEMA }); this.disabledID = this._searchSettings.connect('changed::disabled', this._reloadRemoteProviders.bind(this)); this.enabledID = this._searchSettings.connect('changed::enabled', this._reloadRemoteProviders.bind(this)); this.disablExternalID = this._searchSettings.connect('changed::disable-external', this._reloadRemoteProviders.bind(this)); this.sortOrderID = this._searchSettings.connect('changed::sort-order', this._reloadRemoteProviders.bind(this)); this._searchTimeoutId = null; this._cancellable = new Gio.Cancellable(); this._registerProvider(new AppDisplay.AppSearchProvider()); this.installChangedID = appSys.connect('installed-changed', this._reloadRemoteProviders.bind(this)); this._reloadRemoteProviders(); this.connect('destroy', this._onDestroy.bind(this)); } get terms() { return this._terms; } setStyle(style){ if(this._statusText){ this._statusText.style_class = style; } } _onDestroy(){ this._terms = []; this._results = {}; this._clearDisplay(); this._clearSearchTimeout(); this._defaultResult = null; this._startingSearch = false; if(this.disabledID){ this._searchSettings.disconnect(this.disabledID); this.disabledID = null; } if(this.enabledID){ this._searchSettings.disconnect(this.enabledID); this.enabledID = null; } if(this.disablExternalID){ this._searchSettings.disconnect(this.disablExternalID); this.disablExternalID = null; } if(this.sortOrderID){ this._searchSettings.disconnect(this.sortOrderID); this.sortOrderID = null; } if(this.installChangedID){ appSys.disconnect(this.installChangedID); this.installChangedID = null; } let remoteProviders = this._providers.filter(p => p.isRemoteProvider); remoteProviders.forEach(provider => { this._unregisterProvider(provider); }); this.recentFilesManager.destroy(); this.recentFilesManager = null; this._content.destroy_all_children(); } _reloadRemoteProviders() { let currentTerms = this._terms; //cancel any active search if (this._terms.length !== 0) this._reset(); this._oldProviders = null; let remoteProviders = this._providers.filter(p => p.isRemoteProvider); remoteProviders.forEach(provider => { this._unregisterProvider(provider); }); if(this._settings.get_boolean('search-provider-open-windows')) this._registerProvider(new OpenWindowSearchProvider()); if(this._settings.get_boolean('search-provider-recent-files')) this._registerProvider(new RecentFilesSearchProvider(this.recentFilesManager)); RemoteSearch.loadRemoteSearchProviders(this._searchSettings, providers => { providers.forEach(this._registerProvider.bind(this)); }); //restart any active search if(currentTerms.length > 0) this.setTerms(currentTerms); } _registerProvider(provider) { provider.searchInProgress = false; this._providers.push(provider); this._ensureProviderDisplay(provider); } _unregisterProvider(provider) { let index = this._providers.indexOf(provider); this._providers.splice(index, 1); if (provider.display){ provider.display.destroy(); } } _gotResults(results, provider) { this._results[provider.id] = results; this._updateResults(provider, results); } _clearSearchTimeout() { if (this._searchTimeoutId) { GLib.source_remove(this._searchTimeoutId); this._searchTimeoutId = null; } } _reset() { this._terms = []; this._results = {}; this._clearDisplay(); this._clearSearchTimeout(); this._defaultResult = null; this._startingSearch = false; this._updateSearchProgress(); } _doSearch() { this._startingSearch = false; let previousResults = this._results; this._results = {}; this._providers.forEach(provider => { provider.searchInProgress = true; let previousProviderResults = previousResults[provider.id]; if (this._isSubSearch && previousProviderResults) provider.getSubsearchResultSet(previousProviderResults, this._terms, results => { this._gotResults(results, provider); }, this._cancellable); else provider.getInitialResultSet(this._terms, results => { this._gotResults(results, provider); }, this._cancellable); }); this._updateSearchProgress(); this._clearSearchTimeout(); } _onSearchTimeout() { this._searchTimeoutId = null; this._doSearch(); return GLib.SOURCE_REMOVE; } setTerms(terms) { // Check for the case of making a duplicate previous search before // setting state of the current search or cancelling the search. // This will prevent incorrect state being as a result of a duplicate // search while the previous search is still active. let searchString = terms.join(' '); let previousSearchString = this._terms.join(' '); if (searchString == previousSearchString) return; this._startingSearch = true; this.recentFilesManager.cancelCurrentQueries(); this._cancellable.cancel(); this._cancellable.reset(); if (terms.length == 0) { this._reset(); return; } let isSubSearch = false; if (this._terms.length > 0) isSubSearch = searchString.indexOf(previousSearchString) == 0; this._terms = terms; this._isSubSearch = isSubSearch; this._updateSearchProgress(); if (this._searchTimeoutId === null) this._searchTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 150, this._onSearchTimeout.bind(this)); const escapedTerms = terms .map(term => Shell.util_regex_escape(term)) .filter(term => term.length > 0); if (escapedTerms.length === 0) return; this._highlightRegex = new RegExp('(%s)'.format( escapedTerms.join('|')), 'gi'); this.emit('terms-changed'); } _ensureProviderDisplay(provider) { if (provider.display) return; let providerDisplay; if (provider.appInfo) providerDisplay = new ListSearchResults(provider, this); else providerDisplay = new AppSearchResults(provider, this); providerDisplay.hide(); this._content.add_child(providerDisplay); provider.display = providerDisplay; } _clearDisplay() { this._providers.forEach(provider => { provider.display.clear(); }); } _maybeSetInitialSelection() { let newDefaultResult = null; let providers = this._providers; for (let i = 0; i < providers.length; i++) { let provider = providers[i]; let display = provider.display; if (!display.visible) continue; let firstResult = display.getFirstResult(); if (firstResult) { newDefaultResult = firstResult; break; // select this one! } } if (newDefaultResult !== this._defaultResult) { this._setSelected(this._defaultResult, false); this._setSelected(newDefaultResult, this._highlightDefault); this._defaultResult = newDefaultResult; } } get searchInProgress() { if (this._startingSearch) return true; return this._providers.some(p => p.searchInProgress); } _updateSearchProgress() { let haveResults = this._providers.some(provider => { let display = provider.display; return (display.getFirstResult() != null); }); this._statusBin.visible = !haveResults; if (haveResults) this.emit("have-results") else if (!haveResults) { if (this.searchInProgress) this._statusText.set_text(_("Searching...")); else this._statusText.set_text(_("No results.")); this.emit("no-results") } } _updateResults(provider, results) { let terms = this._terms; let display = provider.display; display.updateSearch(results, terms, () => { provider.searchInProgress = false; this._maybeSetInitialSelection(); this._updateSearchProgress(); }); } highlightDefault(highlight) { this._highlightDefault = highlight; this._setSelected(this._defaultResult, highlight); } getTopResult(){ return this._defaultResult; } getProviders(){ return this._providers; } setProvider(providerID){ if(!this._oldProviders) this._oldProviders = this._providers; this._clearDisplay(); this._providers = this._oldProviders; if(providerID === Constants.CategoryType.ALL_PROGRAMS){ this._providers = this._providers.filter(p => (p.appInfo ? false : true)); } else if(providerID !== Constants.CategoryType.SEARCH_RESULTS){ this._providers = this._providers.filter(p => (p.appInfo ? p.appInfo.get_id() : p) === providerID); } } _setSelected(result, selected) { if(!result) return; if(selected) result.add_style_pseudo_class('active'); else result.remove_style_pseudo_class('active'); } hasActiveResult(){ return (this._defaultResult ? true : false) && this._highlightDefault; } highlightTerms(text) { if (!this._highlightRegex) return GLib.markup_escape_text(text, -1); let escaped = []; let lastMatchEnd = 0; let match; while ((match = this._highlightRegex.exec(text))) { if (match.index > lastMatchEnd) { let unmatched = GLib.markup_escape_text( text.slice(lastMatchEnd, match.index), -1); escaped.push(unmatched); } let matched = GLib.markup_escape_text(match[0], -1); escaped.push('%s'.format(matched)); lastMatchEnd = match.index + match[0].length; } let unmatched = GLib.markup_escape_text( text.slice(lastMatchEnd), -1); escaped.push(unmatched); return escaped.join(''); } }); var ArcSearchProviderInfo = GObject.registerClass(class Arc_Menu_ArcSearchProviderInfo extends MW.ArcMenuPopupBaseMenuItem{ _init(provider, menuLayout) { super._init(menuLayout); this.provider = provider; this._menuLayout = menuLayout; this._settings = this._menuLayout._settings; this.description = this.provider.appInfo.get_description(); if(this.description) this.description = this.description.split('\n')[0]; this.label = new St.Label({ text: provider.appInfo.get_name(), x_align: Clutter.ActorAlign.START, y_align: Clutter.ActorAlign.CENTER, style: 'text-align: left;' }); this.label.style = 'font-weight: bold;'; this.style = "padding: 10px 0px;"; this.add_child(this.label); this._moreText = ""; } setMoreCount(count) { this._moreText = ngettext("%d more", "%d more", count).format(count); if(count > 0) this.label.text = this.provider.appInfo.get_name() + " (" + this._moreText + ")"; else this.label.text = this.provider.appInfo.get_name(); } });