diff options
Diffstat (limited to 'extensions/47/vertical-workspaces/lib/search.js')
-rw-r--r-- | extensions/47/vertical-workspaces/lib/search.js | 474 |
1 files changed, 474 insertions, 0 deletions
diff --git a/extensions/47/vertical-workspaces/lib/search.js b/extensions/47/vertical-workspaces/lib/search.js new file mode 100644 index 0000000..47198a7 --- /dev/null +++ b/extensions/47/vertical-workspaces/lib/search.js @@ -0,0 +1,474 @@ +/** + * V-Shell (Vertical Workspaces) + * search.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2024 + * @license GPL-3.0 + * + */ + +'use strict'; + +import GLib from 'gi://GLib'; +import Clutter from 'gi://Clutter'; +import St from 'gi://St'; +import Shell from 'gi://Shell'; +import GObject from 'gi://GObject'; + +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import * as Search from 'resource:///org/gnome/shell/ui/search.js'; +import * as AppDisplay from 'resource:///org/gnome/shell/ui/appDisplay.js'; + +import * as SystemActions from 'resource:///org/gnome/shell/misc/systemActions.js'; +import { Highlighter } from 'resource:///org/gnome/shell/misc/util.js'; + +let Me; +// gettext +let _; +let opt; + +const SEARCH_MAX_WIDTH = 1092; + +export const SearchModule = class { + constructor(me) { + Me = me; + opt = Me.opt; + _ = Me.gettext; + + this._firstActivation = true; + this.moduleEnabled = false; + this._overrides = null; + } + + cleanGlobals() { + Me = null; + opt = null; + _ = null; + } + + update(reset) { + this.moduleEnabled = opt.get('searchModule'); + const conflict = false; + + reset = reset || !this.moduleEnabled || conflict; + + // don't touch the original code if module disabled + if (reset && !this._firstActivation) { + this._disableModule(); + } else if (!reset) { + this._firstActivation = false; + this._activateModule(); + } + if (reset && this._firstActivation) + console.debug(' SearchModule - Keeping untouched'); + } + + _activateModule() { + this._updateSearchViewWidth(); + + if (!this._overrides) + this._overrides = new Me.Util.Overrides(); + + this._overrides.addOverride('AppSearchProvider', AppDisplay.AppSearchProvider.prototype, AppSearchProvider); + this._overrides.addOverride('SearchResult', Search.SearchResult.prototype, SearchResult); + this._overrides.addOverride('SearchResultsView', Search.SearchResultsView.prototype, SearchResultsView); + this._overrides.addOverride('ListSearchResults', Search.ListSearchResults.prototype, ListSearchResults); + this._overrides.addOverride('ListSearchResult', Search.ListSearchResult.prototype, ListSearchResultOverride); + this._overrides.addOverride('Highlighter', Highlighter.prototype, HighlighterOverride); + + // Don't expand the search view vertically and align it to the top + // this is important in the static workspace mode when the search view bg is not transparent + // also the "Searching..." and "No Results" notifications will be closer to the search entry, with the distance given by margin-top in the stylesheet + Main.overview.searchController.y_align = Clutter.ActorAlign.START; + // Increase the maxResults for app search so that it can show more results in case the user decreases the size of the result icon + const appSearchDisplay = Main.overview.searchController._searchResults._providers.filter(p => p.id === 'applications')[0]?.display; + if (appSearchDisplay) + appSearchDisplay._maxResults = 12; + console.debug(' SearchModule - Activated'); + } + + _disableModule() { + const reset = true; + + const searchResults = Main.overview.searchController._searchResults; + if (searchResults?._searchTimeoutId) { + GLib.source_remove(searchResults._searchTimeoutId); + searchResults._searchTimeoutId = 0; + } + + this._updateSearchViewWidth(reset); + + if (this._overrides) + this._overrides.removeAll(); + this._overrides = null; + + Main.overview.searchController.y_align = Clutter.ActorAlign.FILL; + + console.debug(' WorkspaceSwitcherPopupModule - Disabled'); + } + + _updateSearchViewWidth(reset = false) { + const searchContent = Main.overview.searchController._searchResults._content; + + if (reset) { + searchContent.set_style(''); + } else { + let width = SEARCH_MAX_WIDTH; + if (Me.Util.monitorHasLowResolution()) + width = Math.round(width * 0.8); + width = Math.round(width * opt.SEARCH_VIEW_SCALE); + searchContent.set_style(`max-width: ${width}px;`); + } + } +}; + +const ListSearchResults = { + _getMaxDisplayedResults() { + return opt.SEARCH_MAX_ROWS; + }, +}; + +// AppDisplay.AppSearchProvider +const AppSearchProvider = { + getInitialResultSet(terms, cancellable) { + // Defer until the parental controls manager is initialized, so the + // results can be filtered correctly. + if (!this._parentalControlsManager.initialized) { + return new Promise(resolve => { + let initializedId = this._parentalControlsManager.connect('app-filter-changed', async () => { + if (this._parentalControlsManager.initialized) { + this._parentalControlsManager.disconnect(initializedId); + resolve(await this.getInitialResultSet(terms, cancellable)); + } + }); + }); + } + + const pattern = terms.join(' '); + + let appInfoList = Shell.AppSystem.get_default().get_installed(); + + let weightList = {}; + appInfoList = appInfoList.filter(appInfo => { + try { + appInfo.get_id(); // catch invalid file encodings + } catch (e) { + return false; + } + + let string = ''; + let name; + let shouldShow = false; + if (appInfo.get_display_name) { + // show only launchers that should be visible in this DE + shouldShow = appInfo.should_show() && this._parentalControlsManager.shouldShowApp(appInfo); + + if (shouldShow) { + let id = appInfo.get_id().split('.'); + id = id[id.length - 2] || ''; + let baseName = appInfo.get_string('Name') || ''; + let dispName = appInfo.get_display_name() || ''; + let gName = appInfo.get_generic_name() || ''; + let description = appInfo.get_description() || ''; + let categories = appInfo.get_string('Categories')?.replace(/;/g, ' ') || ''; + let keywords = appInfo.get_string('Keywords')?.replace(/;/g, ' ') || ''; + name = `${dispName} ${id}`; + string = `${dispName} ${gName} ${baseName} ${description} ${categories} ${keywords} ${id}`; + } + } + + let m = -1; + if (shouldShow && opt.SEARCH_FUZZY) { + m = Me.Util.fuzzyMatch(pattern, name); + m = (m + Me.Util.strictMatch(pattern, string)) / 2; + } else if (shouldShow) { + m = Me.Util.strictMatch(pattern, string); + } + + if (m !== -1) + weightList[appInfo.get_id()] = m; + + return shouldShow && (m !== -1); + }); + + appInfoList.sort((a, b) => weightList[a.get_id()] > weightList[b.get_id()]); + + const usage = Shell.AppUsage.get_default(); + // sort apps by usage list + appInfoList.sort((a, b) => usage.compare(a.get_id(), b.get_id())); + // prefer apps where any word in their name starts with the pattern + appInfoList.sort((a, b) => Me.Util.isMoreRelevant(a.get_display_name(), b.get_display_name(), pattern)); + + let results = appInfoList.map(app => app.get_id()); + + if (opt.SEARCH_APP_GRID_MODE && Main.overview.dash.showAppsButton.checked) + this._filterAppGrid(results); + + results = results.concat(this._systemActions.getMatchingActions(terms)); + + return new Promise(resolve => resolve(results)); + }, + + _filterAppGrid(results) { + const icons = Main.overview._overview.controls._appDisplay._orderedItems; + icons.forEach(icon => { + icon.visible = results.includes(icon.id); + }); + }, + + // App search result size + createResultObject(resultMeta) { + let iconSize = opt.SEARCH_ICON_SIZE; + if (!iconSize) { + iconSize = Me.Util.monitorHasLowResolution() + ? 64 + : 96; + } + + if (resultMeta.id.endsWith('.desktop')) { + const icon = new AppDisplay.AppIcon(this._appSys.lookup_app(resultMeta['id']), { + expandTitleOnHover: false, + }); + icon.icon.setIconSize(iconSize); + return icon; + } else { + this._iconSize = iconSize; + return new SystemActionIcon(this, resultMeta); + } + }, +}; + +const SystemActionIcon = GObject.registerClass({ + // Registered name should be unique + GTypeName: `SystemAction${Math.floor(Math.random() * 1000)}`, +}, class SystemActionIcon extends Search.GridSearchResult { + _init(provider, metaInfo, resultsView) { + super._init(provider, metaInfo, resultsView); + if (!Clutter.Container) + this.add_style_class_name('grid-search-result-46'); + this.icon._setSizeManually = true; + this.icon.setIconSize(provider._iconSize); + } + + activate() { + SystemActions.getDefault().activateAction(this.metaInfo['id']); + Main.overview.hide(); + } +}); + +const SearchResult = { + activate() { + this.provider.activateResult(this.metaInfo.id, this._resultsView.terms); + + if (this.metaInfo.clipboardText) { + St.Clipboard.get_default().set_text( + St.ClipboardType.CLIPBOARD, this.metaInfo.clipboardText); + } + // don't close overview if Shift key is pressed - Shift moves windows to the workspace + if (!Me.Util.isShiftPressed()) + Main.overview.toggle(); + }, +}; + +const SearchResultsView = { + 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._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) + this._searchTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, opt.SEARCH_DELAY, this._onSearchTimeout.bind(this)); + + this._highlighter = new Highlighter(this._terms); + + this.emit('terms-changed'); + }, + + _doSearch() { + this._startingSearch = false; + + let previousResults = this._results; + this._results = {}; + + const term0 = this._terms[0]; + const onlySupportedProviders = term0.startsWith(Me.WSP_PREFIX) || term0.startsWith(Me.ESP_PREFIX) || term0.startsWith(Me.RFSP_PREFIX); + + this._providers.forEach(provider => { + const supportedProvider = ['open-windows', 'extensions', 'recent-files'].includes(provider.id); + if (!onlySupportedProviders || (onlySupportedProviders && supportedProvider)) { + let previousProviderResults = previousResults[provider.id]; + this._doProviderSearch(provider, previousProviderResults); + } else { + // hide unwanted providers, they will show() automatically when needed + provider.display.visible = false; + } + }); + + this._updateSearchProgress(); + this._clearSearchTimeout(); + }, + + _updateSearchProgress() { + let haveResults = this._providers.some(provider => { + let display = provider.display; + return display.getFirstResult() !== null; + }); + + this._scrollView.visible = haveResults; + this._statusBin.visible = !haveResults; + + if (!haveResults) { + if (this.searchInProgress) + this._statusText.set_text(_('Searching…')); + else + this._statusText.set_text(_('No results.')); + } + }, + + _highlightFirstVisibleAppGridIcon() { + const appDisplay = Main.overview._overview.controls._appDisplay; + // appDisplay.grab_key_focus(); + for (const icon of appDisplay._orderedItems) { + if (icon.visible) { + appDisplay.selectApp(icon.id); + break; + } + } + }, + + _maybeSetInitialSelection() { + if (opt.SEARCH_APP_GRID_MODE && Main.overview.dash.showAppsButton.checked) { + this._highlightFirstVisibleAppGridIcon(); + return; + } + + 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; + } + }, + + highlightDefault(highlight) { + if (opt.SEARCH_APP_GRID_MODE && Main.overview.dash.showAppsButton.checked) { + if (highlight) + this._highlightFirstVisibleAppGridIcon(); + } else { + this._highlightDefault = highlight; + this._setSelected(this._defaultResult, highlight); + } + }, +}; + +// Add highlighting of the "name" part of the result for all providers +const ListSearchResultOverride = { + _highlightTerms() { + let markup = this._resultsView.highlightTerms(this.metaInfo['name']); + this.label_actor.clutter_text.set_markup(markup); + markup = this._resultsView.highlightTerms(this.metaInfo['description'].split('\n')[0]); + this._descriptionLabel.clutter_text.set_markup(markup); + }, +}; + +const HighlighterOverride = { + /** + * @param {?string[]} terms - list of terms to highlight + */ + /* constructor(terms) { + if (!terms) + return; + + const escapedTerms = terms + .map(term => Shell.util_regex_escape(term)) + .filter(term => term.length > 0); + + if (escapedTerms.length === 0) + return; + + this._highlightRegex = new RegExp( + `(${escapedTerms.join('|')})`, 'gi'); + },*/ + + /** + * Highlight all occurences of the terms defined for this + * highlighter in the provided text using markup. + * + * @param {string} text - text to highlight the defined terms in + * @returns {string} + */ + highlight(text, options) { + if (!this._highlightRegex) + return GLib.markup_escape_text(text, -1); + + // force use local settings if the class is overridden by another extension (WSP, ESP) + const o = options || opt; + let escaped = []; + let lastMatchEnd = 0; + let match; + let style = ['', '']; + if (o.HIGHLIGHT_DEFAULT) + style = ['<b>', '</b>']; + // The default highlighting by the bold style causes text to be "randomly" ellipsized in cases where it's not necessary + // and also blurry + // Underscore doesn't affect label size and all looks better + else if (o.HIGHLIGHT_UNDERLINE) + style = ['<u>', '</u>']; + + 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(`${style[0]}${matched}${style[1]}`); + lastMatchEnd = match.index + match[0].length; + } + let unmatched = GLib.markup_escape_text( + text.slice(lastMatchEnd), -1); + escaped.push(unmatched); + return escaped.join(''); + }, +}; |