[gnome-shell/T27795: 112/138] Implement the desktop search's results panel



commit 4d9f8c16001da9f4dbd2946cc82af233267c94a0
Author: Mario Sanchez Prada <mario endlessm com>
Date:   Wed Feb 7 15:45:18 2018 +0000

    Implement the desktop search's results panel
    
    This commit implements the panel, with the following considerations:
    
      - Display a max of 8 results in a grid result
      - Show separator between search results (but not at the end)
      - Center search results horizontally and vertically
      - Add a close button in the top right corner to close the panel
      - Adapt style of icons, text... according to Endless' design specs
    
    https://phabricator.endlessm.com/T4606
    https://phabricator.endlessm.com/T17659
    
     * 13-03-2019: Squashed with 38090bc34
        viewSelector: Don't set the spinner on when there are nothing to search
    
        The spinner is being set whenever the search-progress-updated signal is
        received, and then uses the .searchInProgress property to determine whether
        to spin or not, but the problem is that this property is being set from
        multiple places, so sometimes we can end in a race condition that ends up
        with the callback being called with no text in the entry but still the
        searchResults object reporting that it's searching.
    
        When this happens, the spinner enters a state where it's constantly
        spinning, consuming lots of CPU for no reason unless some user initiated
        action takes it out of that state.
    
        Probably not a real fix, but let's at least prevent this situation from
        happening by double checking that there's something to search before
        setting the spinner on fire.
    
        https://phabricator.endlessm.com/T17973
    
     * 13-03-2019: Squashed with 38090bc34
        shellEntry: ensure that search spinner stops
    
        Make sure the spinner is stopped whenever the search is stopped.
        Previously, it was easy to get into a state where the spinner
        would keep running and cause CPU churn.
    
        https://phabricator.endlessm.com/T17855

 data/theme/gnome-shell-sass/_endless.scss | 176 ++++++++++++++++++++++++++++++
 js/misc/util.js                           |  10 ++
 js/ui/appDisplay.js                       |  13 ++-
 js/ui/search.js                           | 156 ++++++++++++++++++++------
 js/ui/shellEntry.js                       |   1 +
 js/ui/viewSelector.js                     |   7 +-
 6 files changed, 325 insertions(+), 38 deletions(-)
---
diff --git a/data/theme/gnome-shell-sass/_endless.scss b/data/theme/gnome-shell-sass/_endless.scss
index 140e0af7d8..59ac53229a 100644
--- a/data/theme/gnome-shell-sass/_endless.scss
+++ b/data/theme/gnome-shell-sass/_endless.scss
@@ -463,6 +463,182 @@ popup-separator-menu-item {
     }
 }
 
+// Desktop Search (results)
+
+%search-label-main {
+    font-size: 11pt;
+    font-weight: bold;
+}
+
+%search-label-color {
+    color: #444444;
+}
+
+#searchResults {
+    background-color: rgba(255,255,255,0.92);
+    border-radius: 4px;
+    padding: 10px;
+    max-width: 875px;
+    margin-left: 50px;
+    margin-right: 50px;
+    margin-bottom: 10px;
+
+    .overview-icon {
+        icon-size: 64px;
+        spacing: 8px;
+        text-align: center;
+    }
+
+    .overview-icon-label {
+        @extend %search-label-main;
+        @extend %search-label-color;
+        text-shadow: none;
+    }
+
+    #searchResultsContent {
+        spacing: 0px;
+
+        .search-section {
+            padding: 0px 10px;
+            spacing: 0px;
+
+            .search-provider-icon-label {
+                @extend %search-label-main;
+                @extend %search-label-color;
+                text-shadow: none;
+                width: 100px;
+            }
+
+            .search-provider-icon-label {
+                margin-top: 6px;
+            }
+
+            .search-provider-icon {
+                border-radius: 4px;
+                transition-duration: 100ms;
+                width: 100px;
+            }
+
+            .search-section-separator {
+                -gradient-height: 1px;
+                -gradient-start: rgba(208,208,208,0.9);
+                -gradient-end: rgba(208,208,208,0.9);
+                -margin-horizontal: 0em;
+                height: 1px;
+            }
+
+            .search-section-content {
+                padding-top: 5px;
+                padding-bottom: 10px;
+                spacing: 25px; /* space between provider icon and results container */
+            }
+
+            .icon-grid {
+                spacing: 20px;
+                padding-top: 10px;
+                padding-bottom: 15px;
+                -shell-grid-horizontal-item-size: 104px;
+                -shell-grid-vertical-item-size: 104px;
+            }
+        }
+
+        .list-search-results {
+            spacing: 0px;
+
+            .list-search-result {
+                padding: 5px 10px 10px 10px;
+                transition-duration: 100ms;
+
+                .list-search-result-content {
+                    spacing: 10px;
+                    padding: 0px;
+                    min-height: 32px;
+
+                    .list-search-result-title {
+                        @extend %search-label-main;
+                        @extend %search-label-color;
+                        font-size: 13pt;
+                    }
+
+                    .list-search-result-description {
+                        @extend %search-label-color;
+                    }
+
+                    .list-search-result-arrow-icon {
+                        icon-size: 16px;
+                        color: transparent;
+                    }
+                }
+
+                &:first-child {
+                    border-radius-topright: 4px;
+                    border-radius-topleft: 4px;
+                }
+
+                &:last-child {
+                    border-radius-bottomright: 4px;
+                    border-radius-bottomleft: 4px;
+                }
+
+                &:hover, &:focus, &:selected {
+                    .list-search-result-arrow-icon { color: #444444; }
+                }
+            }
+        }
+    }
+
+    #searchResultsCloseButton StIcon {
+        @extend %search-label-color;
+        icon-size: 14px;
+    }
+
+    #searchResultsCloseButton:hover StIcon {
+        color: #888888;
+    }
+
+    #searchResultsCloseButton:active StIcon {
+        @extend %search-label-color;
+    }
+
+    .search-display > StBoxLayout {
+        padding: 0px;
+    }
+
+    .search-statustext {
+        color: #242424;
+        font-size: 2em;
+        font-weight: bold;
+    }
+}
+
+%search-result-hover { background-color: rgba(226,226,226,0.9); }
+%search-result-selected { background-color: rgba(208,208,208,0.9); }
+%search-result-active { background-color: rgba(192,192,192,0.9); }
+
+.list-search-result {
+    &:hover { @extend %search-result-hover; }
+    &:focus, &:selected { @extend %search-result-selected; }
+    &:active { @extend %search-result-active; }
+}
+
+.grid-search-result {
+    border-radius: 4px;
+    transition-duration: 100ms;
+
+    &:hover { @extend %search-result-hover; }
+    &:focus, &:selected { @extend %search-result-selected; }
+    &:active {
+        @extend %search-result-active;
+        .overview-icon { box-shadow: none; }
+    }
+}
+
+.search-provider-icon {
+    &:hover { @extend %search-result-hover; }
+    &:focus, &:selected { @extend %search-result-selected; }
+    &:active { @extend %search-result-active; }
+}
+
 // Discovery Feed
 
 .discovery-feed-bar-icon {
diff --git a/js/misc/util.js b/js/misc/util.js
index 05dcb4f2fa..0c986658de 100644
--- a/js/misc/util.js
+++ b/js/misc/util.js
@@ -511,6 +511,16 @@ function getBrowserApp() {
     return browserApp;
 }
 
+function blockClickEventsOnActor(actor) {
+    actor.reactive = true;
+    actor.connect('button-press-event', function (actor, event) {
+        return true;
+    });
+    actor.connect('button-release-event', function (actor, event) {
+        return true;
+    });
+}
+
 function _getJsonSearchEngine(folder) {
     let path = GLib.build_filenamev([GLib.get_user_config_dir(), folder, 'Default', 'Preferences']);
     let parser = new Json.Parser();
diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js
index 1387644fb5..fcb65a7c7a 100644
--- a/js/ui/appDisplay.js
+++ b/js/ui/appDisplay.js
@@ -1050,10 +1050,17 @@ var AppSearchProvider = class AppSearchProvider {
         this.getInitialResultSet(terms, callback, cancellable);
     }
 
+    activateResult(appId) {
+        let event = Clutter.get_current_event();
+        let app = this._appSys.lookup_app(appId);
+        let activationContext = new AppActivation.AppActivationContext(app);
+        activationContext.activate(event);
+    }
+
     createResultObject(resultMeta) {
-        if (resultMeta.id.endsWith('.desktop'))
-            return new AppIcon(this._appSys.lookup_app(resultMeta['id']));
-        else
+        // We only use this code path for SystemActions which, from the point
+        // of view of this method, are those NOT referenced with desktop IDs.
+        if (!resultMeta.id.endsWith('.desktop'))
             return new SystemActionIcon(this, resultMeta);
     }
 };
diff --git a/js/ui/search.js b/js/ui/search.js
index 794507b0b1..2660b01e26 100644
--- a/js/ui/search.js
+++ b/js/ui/search.js
@@ -8,12 +8,14 @@ const IconGrid = imports.ui.iconGrid;
 const InternetSearch = imports.ui.internetSearch;
 const Main = imports.ui.main;
 const RemoteSearch = imports.ui.remoteSearch;
+const Separator = imports.ui.separator;
 const Util = imports.misc.util;
 
 const SEARCH_PROVIDERS_SCHEMA = 'org.gnome.desktop.search-providers';
 
 var MAX_LIST_SEARCH_RESULTS_ROWS = 5;
 var MAX_GRID_SEARCH_RESULTS_ROWS = 1;
+var MAX_GRID_SEARCH_RESULTS_COLS = 8;
 
 var MaxWidthBox = GObject.registerClass(
 class MaxWidthBox extends St.BoxLayout {
@@ -55,6 +57,25 @@ var SearchResult = class {
 };
 Signals.addSignalMethods(SearchResult.prototype);
 
+var ListDescriptionBox = GObject.registerClass(
+class ListDescriptionBox extends St.BoxLayout {
+    vfunc_get_preferred_height(forWidth) {
+        // This container requests space for the title and description
+        // regardless of visibility, but allocates normally to visible actors.
+        // This allows us have a constant sized box, but still center the title
+        // label when the description is not present.
+        let min = 0, nat = 0;
+        let children = this.get_children();
+        for (let i = 0; i < children.length; i++) {
+            let child = children[i];
+            let [childMin, childNat] = child.get_preferred_height(forWidth);
+            min += childMin;
+            nat += childNat;
+        }
+        return [min, nat];
+    }
+});
+
 var ListSearchResult = class extends SearchResult {
 
     constructor(provider, metaInfo, resultsView) {
@@ -69,33 +90,32 @@ var ListSearchResult = class extends SearchResult {
 
         this._termsChangedId = 0;
 
-        let titleBox = new St.BoxLayout({ style_class: 'list-search-result-title' });
-
-        content.add(titleBox, { x_fill: true,
-                                y_fill: false,
-                                x_align: St.Align.START,
-                                y_align: St.Align.MIDDLE });
-
         // An icon for, or thumbnail of, content
         let icon = this.metaInfo['createIcon'](this.ICON_SIZE);
         if (icon) {
-            titleBox.add(icon);
+            content.add(icon);
         }
 
-        let title = new St.Label({ text: this.metaInfo['name'] });
-        titleBox.add(title, { x_fill: false,
-                              y_fill: false,
-                              x_align: St.Align.START,
-                              y_align: St.Align.MIDDLE });
-
+        let details = new ListDescriptionBox({ vertical: true });
+        content.add(details, { x_fill: true,
+                               y_fill: false,
+                               x_align: St.Align.START,
+                               y_align: St.Align.MIDDLE });
+
+        let title = new St.Label({ style_class: 'list-search-result-title',
+                                   text: this.metaInfo['name'] })
+        details.add(title, { x_fill: false,
+                             y_fill: false,
+                             x_align: St.Align.START,
+                             y_align: St.Align.START });
         this.actor.label_actor = title;
 
         if (this.metaInfo['description']) {
             this._descriptionLabel = new St.Label({ style_class: 'list-search-result-description' });
-            content.add(this._descriptionLabel, { x_fill: false,
+            details.add(this._descriptionLabel, { x_fill: false,
                                                   y_fill: false,
                                                   x_align: St.Align.START,
-                                                  y_align: St.Align.MIDDLE });
+                                                  y_align: St.Align.END });
 
             this._termsChangedId =
                 this._resultsView.connect('terms-changed',
@@ -104,6 +124,12 @@ var ListSearchResult = class extends SearchResult {
             this._highlightTerms();
         }
 
+        let hoverIcon = new St.Icon({ style_class: 'list-search-result-arrow-icon',
+                                      icon_name: 'go-next-symbolic' });
+        content.add(hoverIcon, { x_fill: false,
+                                 x_align: St.Align.END,
+                                 expand: true });
+
         this.actor.connect('destroy', this._onDestroy.bind(this));
     }
 
@@ -151,8 +177,8 @@ var SearchResultsBase = class {
                                               y_fill: true });
         this.actor.add(this._resultDisplayBin, { expand: true });
 
-        let separator = new St.Widget({ style_class: 'search-section-separator' });
-        this.actor.add(separator);
+        let separator = new Separator.HorizontalSeparator({ style_class: 'search-section-separator' });
+        this.actor.add(separator.actor);
 
         this._resultDisplays = {};
 
@@ -195,7 +221,7 @@ var SearchResultsBase = class {
         this.provider.activateResult(id, this._terms);
         if (result.metaInfo.clipboardText)
             this._clipboard.set_text(St.ClipboardType.CLIPBOARD, result.metaInfo.clipboardText);
-        Main.overview.toggle();
+        Main.overview.hide();
     }
 
     _setMoreCount(_count) {
@@ -299,7 +325,9 @@ var ListSearchResults = class extends SearchResultsBase {
 
         this._content = new St.BoxLayout({ style_class: 'list-search-results',
                                            vertical: true });
-        this._container.add(this._content, { expand: true });
+        this._container.add(this._content, { expand: true,
+                                             y_fill: false,
+                                             y_align: St.Align.MIDDLE });
 
         this._resultDisplayBin.set_child(this._container);
     }
@@ -322,6 +350,10 @@ var ListSearchResults = class extends SearchResultsBase {
     }
 
     _addItem(display) {
+        if (this._content.get_n_children() > 0) {
+            display.separator = new Separator.HorizontalSeparator({ style_class: 'search-section-separator' 
});
+            this._content.add(display.separator.actor);
+        }
         this._content.add_actor(display.actor);
     }
 
@@ -339,7 +371,7 @@ var GridSearchResults = class extends SearchResultsBase {
         super(provider, resultsView);
 
         this._grid = new IconGrid.IconGrid({ rowLimit: MAX_GRID_SEARCH_RESULTS_ROWS,
-                                             xAlign: St.Align.START });
+                                             xAlign: St.Align.MIDDLE });
 
         this._bin = new St.Bin({ x_align: St.Align.MIDDLE });
         this._bin.set_child(this._grid);
@@ -380,12 +412,7 @@ var GridSearchResults = class extends SearchResultsBase {
     }
 
     _getMaxDisplayedResults() {
-        let width = this.actor.allocation.x2 - this.actor.allocation.x1;
-        if (width == 0)
-            return -1;
-
-        let nCols = this._grid.columnsForWidth(width);
-        return nCols * this._grid.getRowLimit();
+        return MAX_GRID_SEARCH_RESULTS_ROWS * MAX_GRID_SEARCH_RESULTS_COLS;
     }
 
     _clearResultDisplay() {
@@ -410,10 +437,46 @@ var GridSearchResults = class extends SearchResultsBase {
 };
 Signals.addSignalMethods(GridSearchResults.prototype);
 
+var SearchResultsBin = GObject.registerClass(
+class SearchResultsBin extends St.BoxLayout {
+    vfunc_allocate(box, flags) {
+        let themeNode = this.get_theme_node();
+        let maxWidth = themeNode.get_max_width();
+        let availWidth = box.x2 - box.x1;
+        let adjustedBox = box;
+
+        if (availWidth > maxWidth) {
+            let excessWidth = availWidth - maxWidth;
+            adjustedBox.x1 += Math.floor(excessWidth / 2);
+            adjustedBox.x2 -= Math.floor(excessWidth / 2);
+        }
+
+        super.vfunc_allocate(adjustedBox, flags);
+    }
+});
+
 var SearchResults = class {
     constructor() {
-        this.actor = new St.BoxLayout({ name: 'searchResults',
-                                        vertical: true });
+        this.actor = new SearchResultsBin({ name: 'searchResults',
+                                            vertical: true });
+        Util.blockClickEventsOnActor(this.actor);
+
+        let closeIcon = new St.Icon({ icon_name: 'window-close-symbolic' });
+        let closeButton = new St.Button({ name: 'searchResultsCloseButton',
+                                          child: closeIcon,
+                                          x_expand: true,
+                                          y_expand: false });
+        // We need to set the ClutterActor align, not St.Bin
+        closeButton.set_x_align(Clutter.ActorAlign.END);
+        closeButton.set_y_align(Clutter.ActorAlign.START);
+        closeButton.connect('clicked', () => {
+            this.emit('search-close-clicked');
+        });
+
+        let topBin = new St.Widget({ layout_manager: new Clutter.BinLayout() });
+        topBin.add_actor(closeButton);
+
+        this.actor.add_child(topBin);
 
         this._content = new MaxWidthBox({ name: 'searchResultsContent',
                                           vertical: true });
@@ -680,6 +743,24 @@ var SearchResults = class {
         }
     }
 
+    _syncSeparatorVisibility() {
+        let lastVisibleDisplay;
+        for (let i = 0; i < this._providers.length; i++) {
+            let provider = this._providers[i];
+            let display = provider.display;
+
+            if (!display.separator)
+                continue;
+
+            display.separator.actor.show();
+            if (display.actor.visible)
+                lastVisibleDisplay = display;
+        }
+
+        if (lastVisibleDisplay)
+            lastVisibleDisplay.separator.actor.hide();
+    }
+
     _updateSearchProgress() {
         let haveResults = this._providers.some(provider => {
             let display = provider.display;
@@ -687,6 +768,7 @@ var SearchResults = class {
         });
         let showStatus = !haveResults && !this.isAnimating;
 
+        this._syncSeparatorVisibility();
         this._scrollView.visible = haveResults;
         this._statusBin.visible = showStatus;
 
@@ -789,10 +871,6 @@ class ProviderInfo extends St.Button {
                       accessible_name: provider.appInfo.get_name(),
                       track_hover: true });
 
-        this._content = new St.BoxLayout({ vertical: false,
-                                           style_class: 'list-search-provider-content' });
-        this.set_child(this._content);
-
         let icon = new St.Icon({ icon_size: this.PROVIDER_ICON_SIZE,
                                  gicon: provider.appInfo.get_icon() });
 
@@ -809,12 +887,22 @@ class ProviderInfo extends St.Button {
         detailsBox.add_actor(this._moreLabel);
 
 
+        this._content = new St.Widget({ layout_manager: new Clutter.BinLayout() });
         this._content.add_actor(icon);
         this._content.add_actor(detailsBox);
+
+        let box = new St.BoxLayout({ vertical: true, x_expand: false });
+        this.set_child(box);
+
+        box.add_actor(this._content);
+
+        let label = new St.Label({ text: provider.appInfo.get_name(),
+                                   style_class: 'search-provider-icon-label' });
+        box.add_actor(label);
     }
 
     get PROVIDER_ICON_SIZE() {
-        return 32;
+        return 64;
     }
 
     animateLaunch() {
diff --git a/js/ui/shellEntry.js b/js/ui/shellEntry.js
index 1f410d3428..5ea85db490 100644
--- a/js/ui/shellEntry.js
+++ b/js/ui/shellEntry.js
@@ -331,6 +331,7 @@ var OverviewEntry = GObject.registerClass({
 
     _stopSearch() {
         global.stage.set_key_focus(null);
+        this.setSpinning(false);
     }
 
     _startSearch(event) {
diff --git a/js/ui/viewSelector.js b/js/ui/viewSelector.js
index ab46649d69..75d5cb15c6 100644
--- a/js/ui/viewSelector.js
+++ b/js/ui/viewSelector.js
@@ -370,6 +370,7 @@ var ViewsDisplay = class {
 
         this._searchResults = new Search.SearchResults();
         this._searchResults.connect('search-progress-updated', this._updateSpinner.bind(this));
+        this._searchResults.connect('search-close-clicked', this._resetSearch.bind(this));
 
         // Since the entry isn't inside the results container we install this
         // dummy widget as the last results container child so that we can
@@ -414,7 +415,11 @@ var ViewsDisplay = class {
     }
 
     _updateSpinner() {
-        this.entry.setSpinning(this._searchResults.searchInProgress);
+        // Make sure we never set the spinner on when there's nothing to search,
+        // regardless of the reported current state, as it can be out of date.
+        let searchTerms = this.entry.text.trim();
+        let spinning = (searchTerms.length > 0) && this._searchResults.searchInProgress;
+        this.entry.setSpinning(spinning);
     }
 
     _enterSearch() {


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]