[gnome-shell/T27795: 91/138] viewSelector: Add the search entry and results widgets to ViewsDisplay
- From: Georges Basile Stavracas Neto <gbsneto src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-shell/T27795: 91/138] viewSelector: Add the search entry and results widgets to ViewsDisplay
- Date: Tue, 1 Oct 2019 23:36:58 +0000 (UTC)
commit 200373ecf734b027e2c698fc4ed01903344ff792
Author: Mario Sanchez Prada <mario endlessm com>
Date: Thu Jun 15 15:04:08 2017 -0700
viewSelector: Add the search entry and results widgets to ViewsDisplay
Add the relevant elements to ViewsDisplay and ViewsDisplayContainer, and
override the vfunc_allocate() function in the custom layout manager to
properly assign the right allocation to every actor in the desktop: the
icon grid, the search entry and the search results panel.
js/ui/viewSelector.js | 251 ++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 234 insertions(+), 17 deletions(-)
---
diff --git a/js/ui/viewSelector.js b/js/ui/viewSelector.js
index c2783d2532..d507aadabd 100644
--- a/js/ui/viewSelector.js
+++ b/js/ui/viewSelector.js
@@ -1,7 +1,7 @@
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/* exported ViewSelector */
-const { Clutter, Gio, GObject, Meta, Shell, St } = imports.gi;
+const { Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi;
const Signals = imports.signals;
const AppDisplay = imports.ui.appDisplay;
@@ -19,13 +19,16 @@ const IconGrid = imports.ui.iconGrid;
const SHELL_KEYBINDINGS_SCHEMA = 'org.gnome.shell.keybindings';
var PINCH_GESTURE_THRESHOLD = 0.7;
+const SEARCH_ACTIVATION_TIMEOUT = 50;
+
var ViewPage = {
WINDOWS: 1,
APPS: 2
};
const ViewsDisplayPage = {
- APP_GRID: 1
+ APP_GRID: 1,
+ SEARCH: 2
};
var FocusTrap = GObject.registerClass(
@@ -134,37 +137,143 @@ var ViewsDisplayLayout = GObject.registerClass({
param_types: [GObject.TYPE_INT, GObject.TYPE_INT]
},
},
+ Properties: {
+ 'expansion': GObject.ParamSpec.double(
+ 'expansion',
+ 'expansion',
+ 'expansion',
+ GObject.ParamFlags.READWRITE,
+ 0, 1, 0)
+ },
}, class ViewsDisplayLayout extends Clutter.BinLayout {
- _init(appDisplayActor) {
+ _init(entry, gridContainerActor, searchResultsActor) {
super._init();
- this._appDisplayActor = appDisplayActor;
- this._appDisplayActor.connect('style-changed', this._onStyleChanged.bind(this));
+ this._entry = entry;
+ this._gridContainerActor = gridContainerActor;
+ this._searchResultsActor = searchResultsActor;
+
+ this._entry.connect('style-changed', this._onStyleChanged.bind(this));
+ this._gridContainerActor.connect('style-changed', this._onStyleChanged.bind(this));
+
+ this._heightAboveEntry = 0;
+ this.expansion = 0;
+ this._lowResolutionMode = false;
}
_onStyleChanged() {
this.layout_changed();
}
+ _centeredHeightAbove(height, availHeight) {
+ return Math.max(0, Math.floor((availHeight - height) / 2));
+ }
+
+ _computeGridContainerPlacement(viewHeight, entryHeight, availHeight) {
+ // If we have the space for it, we add some padding to the top of the
+ // all view when calculating its centered position. This is to offset
+ // the icon labels at the bottom of the icon grid, so the icons
+ // themselves appears centered.
+ let themeNode = this._gridContainerActor.get_theme_node();
+ let topPadding = themeNode.get_length('-natural-padding-top');
+ let heightAbove = this._centeredHeightAbove(viewHeight + topPadding, availHeight);
+ let leftover = Math.max(availHeight - viewHeight - heightAbove, 0);
+ heightAbove += Math.min(topPadding, leftover);
+ // Always leave enough room for the search entry at the top
+ heightAbove = Math.max(entryHeight, heightAbove);
+ return heightAbove;
+ }
+
+ _computeChildrenAllocation(allocation) {
+ let availWidth = allocation.x2 - allocation.x1;
+ let availHeight = allocation.y2 - allocation.y1;
+
+ // Entry height
+ let entryHeight = this._entry.get_preferred_height(availWidth)[1];
+ let themeNode = this._entry.get_theme_node();
+ let entryMinPadding = themeNode.get_length('-minimum-vpadding');
+ let entryTopMargin = themeNode.get_length('margin-top');
+ entryHeight += entryMinPadding * 2;
+
+ // GridContainer height
+ let gridContainerHeight = this._gridContainerActor.get_preferred_height(availWidth)[1];
+ let heightAboveGrid = this._computeGridContainerPlacement(gridContainerHeight, entryHeight,
availHeight);
+ this._heightAboveEntry = this._centeredHeightAbove(entryHeight, heightAboveGrid);
+
+ let entryBox = allocation.copy();
+ entryBox.y1 = this._heightAboveEntry + entryTopMargin;
+ entryBox.y2 = entryBox.y1 + entryHeight;
+
+ let gridContainerBox = allocation.copy();
+ // The grid container box should have the dimensions of this container but start
+ // after the search entry and according to the calculated xplacement policies
+ gridContainerBox.y1 = this._computeGridContainerPlacement(gridContainerHeight, entryHeight,
availHeight);
+
+ let searchResultsBox = allocation.copy();
+
+ // The views clone does not have a searchResultsActor
+ if (this._searchResultsActor) {
+ let searchResultsHeight = availHeight - entryHeight;
+ searchResultsBox.x1 = allocation.x1;
+ searchResultsBox.x2 = allocation.x2;
+ searchResultsBox.y1 = entryBox.y2;
+ searchResultsBox.y2 = searchResultsBox.y1 + searchResultsHeight;
+ }
+
+ return [entryBox, gridContainerBox, searchResultsBox];
+ }
+
vfunc_allocate(actor, box, flags) {
- let availWidth = box.x2 - box.x1;
- let availHeight = box.y2 - box.y1;
+ let [entryBox, gridContainerBox, searchResultsBox] = this._computeChildrenAllocation(box);
// We want to emit the signal BEFORE any allocation has happened since the
// icon grid will need to precompute certain values before being able to
// report a sensible preferred height for the specified width.
- this.emit('grid-available-size-changed', availWidth, availHeight);
- super.vfunc_allocate(actor, box, flags);
+ this.emit( 'grid-available-size-changed', box.x2 - box.x1, box.y2 - box.y1);
+
+ this._entry.allocate(entryBox, flags);
+ this._gridContainerActor.allocate(gridContainerBox, flags);
+ if (this._searchResultsActor)
+ this._searchResultsActor.allocate(searchResultsBox, flags);
+ }
+
+ set expansion(v) {
+ if (v == this._expansion || this._searchResultsActor == null)
+ return;
+
+ this._gridContainerActor.visible = v != 1;
+ this._searchResultsActor.visible = v != 0;
+
+ this._gridContainerActor.opacity = (1 - v) * 255;
+ this._searchResultsActor.opacity = v * 255;
+
+ let entryTranslation = - this._heightAboveEntry * v;
+ this._entry.translation_y = entryTranslation;
+
+ this._searchResultsActor.translation_y = entryTranslation;
+
+ this._expansion = v;
+ this.notify('expansion')
+ }
+
+ get expansion() {
+ return this._expansion;
}
});
var ViewsDisplayContainer = GObject.registerClass(
class ViewsDisplayContainer extends St.Widget {
- _init(appDisplay) {
- this._appDisplay = appDisplay;
+ _init(entry, gridContainer, searchResults) {
+ this._entry = entry;
+ this._gridContainer = gridContainer;
+ this._searchResults = searchResults;
+
this._activePage = ViewsDisplayPage.APP_GRID;
- let layoutManager = new ViewsDisplayLayout(this._appDisplay.actor);
+ let layoutManager = new ViewsDisplayLayout(
+ entry,
+ gridContainer.actor,
+ searchResults.actor);
super._init({
layout_manager: layoutManager,
x_expand: true,
@@ -173,7 +282,9 @@ class ViewsDisplayContainer extends St.Widget {
layoutManager.connect('grid-available-size-changed', this._onGridAvailableSizeChanged.bind(this));
- this.add_actor(this._appDisplay.actor);
+ this.add_child(this._entry);
+ this.add_child(this._gridContainer.actor);
+ this.add_child(this._searchResults.actor);
}
_onGridAvailableSizeChanged(actor, width, height) {
@@ -181,18 +292,30 @@ class ViewsDisplayContainer extends St.Widget {
box.x1 = box.y1 = 0;
box.x2 = width;
box.y2 = height;
- box = this._appDisplay.actor.get_theme_node().get_content_box(box);
+ box = this._gridContainer.actor.get_theme_node().get_content_box(box);
let availWidth = box.x2 - box.x1;
let availHeight = box.y2 - box.y1;
- this._appDisplay.adaptToSize(availWidth, availHeight);
+ this._gridContainer.adaptToSize(availWidth, availHeight);
}
- showPage(page) {
+ showPage(page, doAnimation) {
if (this._activePage === page)
return;
this._activePage = page;
+
+ let tweenTarget = page == ViewsDisplayPage.SEARCH ? 1 : 0;
+ if (doAnimation) {
+ this._searchResults.isAnimating = true;
+ this.ease_property('@layout.expansion', tweenTarget, {
+ duration: 250,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => this._searchResults.isAnimating = false,
+ });
+ } else {
+ this.layout_manager.expansion = tweenTarget;
+ }
}
getActivePage() {
@@ -202,9 +325,103 @@ class ViewsDisplayContainer extends St.Widget {
var ViewsDisplay = class {
constructor() {
+ this._enterSearchTimeoutId = 0;
+
this._appDisplay = new AppDisplay.AppDisplay()
- this.actor = new ViewsDisplayContainer(this._appDisplay);
+ this._searchResults = new Search.SearchResults();
+ this._searchResults.connect('search-progress-updated', this._updateSpinner.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
+ // include the entry in the keynav tab path
+ this._focusTrap = new FocusTrap({ can_focus: true });
+ this._focusTrap.connect('key-focus-in', () => {
+ this.entry.grab_key_focus();
+ });
+ this._searchResults.actor.add_actor(this._focusTrap);
+
+ global.focus_manager.add_group(this._searchResults.actor);
+
+ this.entry = new ShellEntry.OverviewEntry();
+ this.entry.connect('search-activated', this._onSearchActivated.bind(this));
+ this.entry.connect('search-active-changed', this._onSearchActiveChanged.bind(this));
+ this.entry.connect('search-navigate-focus', this._onSearchNavigateFocus.bind(this));
+ this.entry.connect('search-terms-changed', this._onSearchTermsChanged.bind(this));
+
+ this.entry.clutter_text.connect('key-focus-in', () => {
+ this._searchResults.highlightDefault(true);
+ });
+ this.entry.clutter_text.connect('key-focus-out', () => {
+ this._searchResults.highlightDefault(false);
+ });
+
+ // Clicking on any empty area should exit search and get back to the desktop.
+ let clickAction = new Clutter.ClickAction();
+ clickAction.connect('clicked', this._resetSearch.bind(this));
+ Main.overview.addAction(clickAction, false);
+ this._searchResults.actor.bind_property('mapped', clickAction, 'enabled',
GObject.BindingFlags.SYNC_CREATE);
+
+ this.actor = new ViewsDisplayContainer(this.entry, this._appDisplay, this._searchResults);
+ }
+
+ _updateSpinner() {
+ this.entry.setSpinning(this._searchResults.searchInProgress);
+ }
+
+ _enterSearch() {
+ if (this._enterSearchTimeoutId > 0)
+ return;
+
+ // We give a very short time for search results to populate before
+ // triggering the animation, unless an animation is already in progress
+ if (this._searchResults.isAnimating) {
+ this.actor.showPage(ViewsDisplayPage.SEARCH, true);
+ return;
+ }
+
+ this._enterSearchTimeoutId = GLib.timeout_add(
+ GLib.PRIORITY_DEFAULT,
+ SEARCH_ACTIVATION_TIMEOUT, () => {
+ this._enterSearchTimeoutId = 0;
+ this.actor.showPage(ViewsDisplayPage.SEARCH, true);
+
+ return GLib.SOURCE_REMOVE;
+ }
+ );
+ }
+
+ _leaveSearch() {
+ if (this._enterSearchTimeoutId > 0) {
+ GLib.source_remove(this._enterSearchTimeoutId);
+ this._enterSearchTimeoutId = 0;
+ }
+ this.actor.showPage(ViewsDisplayPage.APP_GRID, true);
+ }
+
+ _onSearchActivated() {
+ this._searchResults.activateDefault();
+ this._resetSearch();
+ }
+
+ _onSearchActiveChanged() {
+ if (this.entry.active)
+ this._enterSearch();
+ else
+ this._leaveSearch();
+ }
+
+ _onSearchNavigateFocus(entry, direction) {
+ this._searchResults.navigateFocus(direction);
+ }
+
+ _onSearchTermsChanged() {
+ let terms = this.entry.getSearchTerms();
+ this._searchResults.setTerms(terms);
+ }
+
+ _resetSearch() {
+ this.entry.resetSearch();
}
get appDisplay() {
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]