summaryrefslogtreecommitdiffstats
path: root/js/ui/viewSelector.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/ui/viewSelector.js')
-rw-r--r--js/ui/viewSelector.js609
1 files changed, 609 insertions, 0 deletions
diff --git a/js/ui/viewSelector.js b/js/ui/viewSelector.js
new file mode 100644
index 0000000..bfb02a5
--- /dev/null
+++ b/js/ui/viewSelector.js
@@ -0,0 +1,609 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported ViewSelector */
+
+const { Clutter, Gio, GObject, Meta, Shell, St } = imports.gi;
+const Signals = imports.signals;
+
+const AppDisplay = imports.ui.appDisplay;
+const Main = imports.ui.main;
+const OverviewControls = imports.ui.overviewControls;
+const Params = imports.misc.params;
+const Search = imports.ui.search;
+const ShellEntry = imports.ui.shellEntry;
+const WorkspacesView = imports.ui.workspacesView;
+const EdgeDragAction = imports.ui.edgeDragAction;
+const IconGrid = imports.ui.iconGrid;
+
+const SHELL_KEYBINDINGS_SCHEMA = 'org.gnome.shell.keybindings';
+var PINCH_GESTURE_THRESHOLD = 0.7;
+
+var ViewPage = {
+ WINDOWS: 1,
+ APPS: 2,
+ SEARCH: 3,
+};
+
+var FocusTrap = GObject.registerClass(
+class FocusTrap extends St.Widget {
+ vfunc_navigate_focus(from, direction) {
+ if (direction == St.DirectionType.TAB_FORWARD ||
+ direction == St.DirectionType.TAB_BACKWARD)
+ return super.vfunc_navigate_focus(from, direction);
+ return false;
+ }
+});
+
+function getTermsForSearchString(searchString) {
+ searchString = searchString.replace(/^\s+/g, '').replace(/\s+$/g, '');
+ if (searchString == '')
+ return [];
+
+ let terms = searchString.split(/\s+/);
+ return terms;
+}
+
+var TouchpadShowOverviewAction = class {
+ constructor(actor) {
+ actor.connect('captured-event::touchpad', this._handleEvent.bind(this));
+ }
+
+ _handleEvent(actor, event) {
+ if (event.type() != Clutter.EventType.TOUCHPAD_PINCH)
+ return Clutter.EVENT_PROPAGATE;
+
+ if (event.get_touchpad_gesture_finger_count() != 3)
+ return Clutter.EVENT_PROPAGATE;
+
+ if (event.get_gesture_phase() == Clutter.TouchpadGesturePhase.END)
+ this.emit('activated', event.get_gesture_pinch_scale());
+
+ return Clutter.EVENT_STOP;
+ }
+};
+Signals.addSignalMethods(TouchpadShowOverviewAction.prototype);
+
+var ShowOverviewAction = GObject.registerClass({
+ Signals: { 'activated': { param_types: [GObject.TYPE_DOUBLE] } },
+}, class ShowOverviewAction extends Clutter.GestureAction {
+ _init() {
+ super._init();
+ this.set_n_touch_points(3);
+
+ global.display.connect('grab-op-begin', () => {
+ this.cancel();
+ });
+ }
+
+ vfunc_gesture_prepare(_actor) {
+ return Main.actionMode == Shell.ActionMode.NORMAL &&
+ this.get_n_current_points() == this.get_n_touch_points();
+ }
+
+ _getBoundingRect(motion) {
+ let minX, minY, maxX, maxY;
+
+ for (let i = 0; i < this.get_n_current_points(); i++) {
+ let x, y;
+
+ if (motion == true)
+ [x, y] = this.get_motion_coords(i);
+ else
+ [x, y] = this.get_press_coords(i);
+
+ if (i == 0) {
+ minX = maxX = x;
+ minY = maxY = y;
+ } else {
+ minX = Math.min(minX, x);
+ minY = Math.min(minY, y);
+ maxX = Math.max(maxX, x);
+ maxY = Math.max(maxY, y);
+ }
+ }
+
+ return new Meta.Rectangle({ x: minX,
+ y: minY,
+ width: maxX - minX,
+ height: maxY - minY });
+ }
+
+ vfunc_gesture_begin(_actor) {
+ this._initialRect = this._getBoundingRect(false);
+ return true;
+ }
+
+ vfunc_gesture_end(_actor) {
+ let rect = this._getBoundingRect(true);
+ let oldArea = this._initialRect.width * this._initialRect.height;
+ let newArea = rect.width * rect.height;
+ let areaDiff = newArea / oldArea;
+
+ this.emit('activated', areaDiff);
+ }
+});
+
+var ViewSelector = GObject.registerClass({
+ Signals: {
+ 'page-changed': {},
+ 'page-empty': {},
+ },
+}, class ViewSelector extends Shell.Stack {
+ _init(searchEntry, workspaceAdjustment, showAppsButton) {
+ super._init({
+ name: 'viewSelector',
+ x_expand: true,
+ visible: false,
+ });
+
+ this._showAppsButton = showAppsButton;
+ this._showAppsButton.connect('notify::checked', this._onShowAppsButtonToggled.bind(this));
+
+ this._activePage = null;
+
+ this._searchActive = false;
+
+ this._entry = searchEntry;
+ ShellEntry.addContextMenu(this._entry);
+
+ this._text = this._entry.clutter_text;
+ this._text.connect('text-changed', this._onTextChanged.bind(this));
+ this._text.connect('key-press-event', this._onKeyPress.bind(this));
+ this._text.connect('key-focus-in', () => {
+ this._searchResults.highlightDefault(true);
+ });
+ this._text.connect('key-focus-out', () => {
+ this._searchResults.highlightDefault(false);
+ });
+ this._entry.connect('popup-menu', () => {
+ if (!this._searchActive)
+ return;
+
+ this._entry.menu.close();
+ this._searchResults.popupMenuDefault();
+ });
+ this._entry.connect('notify::mapped', this._onMapped.bind(this));
+ global.stage.connect('notify::key-focus', this._onStageKeyFocusChanged.bind(this));
+
+ this._entry.set_primary_icon(new St.Icon({ style_class: 'search-entry-icon',
+ icon_name: 'edit-find-symbolic' }));
+ this._clearIcon = new St.Icon({ style_class: 'search-entry-icon',
+ icon_name: 'edit-clear-symbolic' });
+
+ this._iconClickedId = 0;
+ this._capturedEventId = 0;
+
+ this._workspacesDisplay =
+ new WorkspacesView.WorkspacesDisplay(workspaceAdjustment);
+ this._workspacesPage = this._addPage(this._workspacesDisplay,
+ _("Windows"), 'focus-windows-symbolic');
+
+ this.appDisplay = new AppDisplay.AppDisplay();
+ this._appsPage = this._addPage(this.appDisplay,
+ _("Applications"), 'view-app-grid-symbolic');
+
+ this._searchResults = new Search.SearchResultsView();
+ this._searchPage = this._addPage(this._searchResults,
+ _("Search"), 'edit-find-symbolic',
+ { a11yFocus: this._entry });
+
+ // 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.add_actor(this._focusTrap);
+
+ global.focus_manager.add_group(this._searchResults);
+
+ this._stageKeyPressId = 0;
+ Main.overview.connect('showing', () => {
+ this._stageKeyPressId = global.stage.connect('key-press-event',
+ this._onStageKeyPress.bind(this));
+ });
+ Main.overview.connect('hiding', () => {
+ if (this._stageKeyPressId != 0) {
+ global.stage.disconnect(this._stageKeyPressId);
+ this._stageKeyPressId = 0;
+ }
+ });
+ Main.overview.connect('shown', () => {
+ // If we were animating from the desktop view to the
+ // apps page the workspace page was visible, allowing
+ // the windows to animate, but now we no longer want to
+ // show it given that we are now on the apps page or
+ // search page.
+ if (this._activePage != this._workspacesPage) {
+ this._workspacesPage.opacity = 0;
+ this._workspacesPage.hide();
+ }
+ });
+
+ Main.wm.addKeybinding('toggle-application-view',
+ new Gio.Settings({ schema_id: SHELL_KEYBINDINGS_SCHEMA }),
+ Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
+ Shell.ActionMode.NORMAL |
+ Shell.ActionMode.OVERVIEW,
+ this._toggleAppsPage.bind(this));
+
+ Main.wm.addKeybinding('toggle-overview',
+ new Gio.Settings({ schema_id: SHELL_KEYBINDINGS_SCHEMA }),
+ Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
+ Shell.ActionMode.NORMAL |
+ Shell.ActionMode.OVERVIEW,
+ Main.overview.toggle.bind(Main.overview));
+
+ let side;
+ if (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL)
+ side = St.Side.RIGHT;
+ else
+ side = St.Side.LEFT;
+ let gesture = new EdgeDragAction.EdgeDragAction(side,
+ Shell.ActionMode.NORMAL);
+ gesture.connect('activated', () => {
+ if (Main.overview.visible)
+ Main.overview.hide();
+ else
+ this.showApps();
+ });
+ global.stage.add_action(gesture);
+
+ gesture = new ShowOverviewAction();
+ gesture.connect('activated', this._pinchGestureActivated.bind(this));
+ global.stage.add_action(gesture);
+
+ gesture = new TouchpadShowOverviewAction(global.stage);
+ gesture.connect('activated', this._pinchGestureActivated.bind(this));
+ }
+
+ _pinchGestureActivated(action, scale) {
+ if (scale < PINCH_GESTURE_THRESHOLD)
+ Main.overview.show();
+ }
+
+ _toggleAppsPage() {
+ this._showAppsButton.checked = !this._showAppsButton.checked;
+ Main.overview.show();
+ }
+
+ showApps() {
+ this._showAppsButton.checked = true;
+ Main.overview.show();
+ }
+
+ animateToOverview() {
+ this.show();
+ this.reset();
+ this._workspacesDisplay.animateToOverview(this._showAppsButton.checked);
+ this._activePage = null;
+ if (this._showAppsButton.checked)
+ this._showPage(this._appsPage);
+ else
+ this._showPage(this._workspacesPage);
+
+ if (!this._workspacesDisplay.activeWorkspaceHasMaximizedWindows())
+ Main.overview.fadeOutDesktop();
+ }
+
+ animateFromOverview() {
+ // Make sure workspace page is fully visible to allow
+ // workspace.js do the animation of the windows
+ this._workspacesPage.opacity = 255;
+
+ this._workspacesDisplay.animateFromOverview(this._activePage != this._workspacesPage);
+
+ this._showAppsButton.checked = false;
+
+ if (!this._workspacesDisplay.activeWorkspaceHasMaximizedWindows())
+ Main.overview.fadeInDesktop();
+ }
+
+ vfunc_hide() {
+ this.reset();
+ this._workspacesDisplay.hide();
+
+ super.vfunc_hide();
+ }
+
+ _addPage(actor, name, a11yIcon, params) {
+ params = Params.parse(params, { a11yFocus: null });
+
+ let page = new St.Bin({ child: actor });
+
+ if (params.a11yFocus) {
+ Main.ctrlAltTabManager.addGroup(params.a11yFocus, name, a11yIcon);
+ } else {
+ Main.ctrlAltTabManager.addGroup(actor, name, a11yIcon, {
+ proxy: this,
+ focusCallback: () => this._a11yFocusPage(page),
+ });
+ }
+ page.hide();
+ this.add_actor(page);
+ return page;
+ }
+
+ _fadePageIn() {
+ this._activePage.ease({
+ opacity: 255,
+ duration: OverviewControls.SIDE_CONTROLS_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+ }
+
+ _fadePageOut(page) {
+ let oldPage = page;
+ page.ease({
+ opacity: 0,
+ duration: OverviewControls.SIDE_CONTROLS_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onStopped: () => this._animateIn(oldPage),
+ });
+ }
+
+ _animateIn(oldPage) {
+ if (oldPage)
+ oldPage.hide();
+
+ this.emit('page-empty');
+
+ this._activePage.show();
+
+ if (this._activePage == this._appsPage && oldPage == this._workspacesPage) {
+ // Restore opacity, in case we animated via _fadePageOut
+ this._activePage.opacity = 255;
+ this.appDisplay.animate(IconGrid.AnimationDirection.IN);
+ } else {
+ this._fadePageIn();
+ }
+ }
+
+ _animateOut(page) {
+ let oldPage = page;
+ if (page == this._appsPage &&
+ this._activePage == this._workspacesPage &&
+ !Main.overview.animationInProgress) {
+ this.appDisplay.animate(IconGrid.AnimationDirection.OUT, () => {
+ this._animateIn(oldPage);
+ });
+ } else {
+ this._fadePageOut(page);
+ }
+ }
+
+ _showPage(page) {
+ if (!Main.overview.visible)
+ return;
+
+ if (page == this._activePage)
+ return;
+
+ let oldPage = this._activePage;
+ this._activePage = page;
+ this.emit('page-changed');
+
+ if (oldPage)
+ this._animateOut(oldPage);
+ else
+ this._animateIn();
+ }
+
+ _a11yFocusPage(page) {
+ this._showAppsButton.checked = page == this._appsPage;
+ page.navigate_focus(null, St.DirectionType.TAB_FORWARD, false);
+ }
+
+ _onShowAppsButtonToggled() {
+ this._showPage(this._showAppsButton.checked
+ ? this._appsPage : this._workspacesPage);
+ }
+
+ _onStageKeyPress(actor, event) {
+ // Ignore events while anything but the overview has
+ // pushed a modal (system modals, looking glass, ...)
+ if (Main.modalCount > 1)
+ return Clutter.EVENT_PROPAGATE;
+
+ let symbol = event.get_key_symbol();
+
+ if (symbol === Clutter.KEY_Escape) {
+ if (this._searchActive)
+ this.reset();
+ else if (this._showAppsButton.checked)
+ this._showAppsButton.checked = false;
+ else
+ Main.overview.hide();
+ return Clutter.EVENT_STOP;
+ } else if (this._shouldTriggerSearch(symbol)) {
+ this.startSearch(event);
+ } else if (!this._searchActive && !global.stage.key_focus) {
+ if (symbol === Clutter.KEY_Tab || symbol === Clutter.KEY_Down) {
+ this._activePage.navigate_focus(null, St.DirectionType.TAB_FORWARD, false);
+ return Clutter.EVENT_STOP;
+ } else if (symbol === Clutter.KEY_ISO_Left_Tab) {
+ this._activePage.navigate_focus(null, St.DirectionType.TAB_BACKWARD, false);
+ return Clutter.EVENT_STOP;
+ }
+ }
+ return Clutter.EVENT_PROPAGATE;
+ }
+
+ _searchCancelled() {
+ this._showPage(this._showAppsButton.checked
+ ? this._appsPage
+ : this._workspacesPage);
+
+ // Leave the entry focused when it doesn't have any text;
+ // when replacing a selected search term, Clutter emits
+ // two 'text-changed' signals, one for deleting the previous
+ // text and one for the new one - the second one is handled
+ // incorrectly when we remove focus
+ // (https://bugzilla.gnome.org/show_bug.cgi?id=636341) */
+ if (this._text.text != '')
+ this.reset();
+ }
+
+ reset() {
+ // Don't drop the key focus on Clutter's side if anything but the
+ // overview has pushed a modal (e.g. system modals when activated using
+ // the overview).
+ if (Main.modalCount <= 1)
+ global.stage.set_key_focus(null);
+
+ this._entry.text = '';
+
+ this._text.set_cursor_visible(true);
+ this._text.set_selection(0, 0);
+ }
+
+ _onStageKeyFocusChanged() {
+ let focus = global.stage.get_key_focus();
+ let appearFocused = this._entry.contains(focus) ||
+ this._searchResults.contains(focus);
+
+ this._text.set_cursor_visible(appearFocused);
+
+ if (appearFocused)
+ this._entry.add_style_pseudo_class('focus');
+ else
+ this._entry.remove_style_pseudo_class('focus');
+ }
+
+ _onMapped() {
+ if (this._entry.mapped) {
+ // Enable 'find-as-you-type'
+ this._capturedEventId = global.stage.connect('captured-event',
+ this._onCapturedEvent.bind(this));
+ this._text.set_cursor_visible(true);
+ this._text.set_selection(0, 0);
+ } else {
+ // Disable 'find-as-you-type'
+ if (this._capturedEventId > 0)
+ global.stage.disconnect(this._capturedEventId);
+ this._capturedEventId = 0;
+ }
+ }
+
+ _shouldTriggerSearch(symbol) {
+ if (symbol === Clutter.KEY_Multi_key)
+ return true;
+
+ if (symbol === Clutter.KEY_BackSpace && this._searchActive)
+ return true;
+
+ let unicode = Clutter.keysym_to_unicode(symbol);
+ if (unicode == 0)
+ return false;
+
+ if (getTermsForSearchString(String.fromCharCode(unicode)).length > 0)
+ return true;
+
+ return false;
+ }
+
+ startSearch(event) {
+ global.stage.set_key_focus(this._text);
+
+ let synthEvent = event.copy();
+ synthEvent.set_source(this._text);
+ this._text.event(synthEvent, false);
+ }
+
+ // the entry does not show the hint
+ _isActivated() {
+ return this._text.text == this._entry.get_text();
+ }
+
+ _onTextChanged() {
+ let terms = getTermsForSearchString(this._entry.get_text());
+
+ this._searchActive = terms.length > 0;
+ this._searchResults.setTerms(terms);
+
+ if (this._searchActive) {
+ this._showPage(this._searchPage);
+
+ this._entry.set_secondary_icon(this._clearIcon);
+
+ if (this._iconClickedId == 0) {
+ this._iconClickedId = this._entry.connect('secondary-icon-clicked',
+ this.reset.bind(this));
+ }
+ } else {
+ if (this._iconClickedId > 0) {
+ this._entry.disconnect(this._iconClickedId);
+ this._iconClickedId = 0;
+ }
+
+ this._entry.set_secondary_icon(null);
+ this._searchCancelled();
+ }
+ }
+
+ _onKeyPress(entry, event) {
+ let symbol = event.get_key_symbol();
+ if (symbol === Clutter.KEY_Escape) {
+ if (this._isActivated()) {
+ this.reset();
+ return Clutter.EVENT_STOP;
+ }
+ } else if (this._searchActive) {
+ let arrowNext, nextDirection;
+ if (entry.get_text_direction() == Clutter.TextDirection.RTL) {
+ arrowNext = Clutter.KEY_Left;
+ nextDirection = St.DirectionType.LEFT;
+ } else {
+ arrowNext = Clutter.KEY_Right;
+ nextDirection = St.DirectionType.RIGHT;
+ }
+
+ if (symbol === Clutter.KEY_Tab) {
+ this._searchResults.navigateFocus(St.DirectionType.TAB_FORWARD);
+ return Clutter.EVENT_STOP;
+ } else if (symbol === Clutter.KEY_ISO_Left_Tab) {
+ this._focusTrap.can_focus = false;
+ this._searchResults.navigateFocus(St.DirectionType.TAB_BACKWARD);
+ this._focusTrap.can_focus = true;
+ return Clutter.EVENT_STOP;
+ } else if (symbol === Clutter.KEY_Down) {
+ this._searchResults.navigateFocus(St.DirectionType.DOWN);
+ return Clutter.EVENT_STOP;
+ } else if (symbol == arrowNext && this._text.position == -1) {
+ this._searchResults.navigateFocus(nextDirection);
+ return Clutter.EVENT_STOP;
+ } else if (symbol === Clutter.KEY_Return || symbol === Clutter.KEY_KP_Enter) {
+ this._searchResults.activateDefault();
+ return Clutter.EVENT_STOP;
+ }
+ }
+ return Clutter.EVENT_PROPAGATE;
+ }
+
+ _onCapturedEvent(actor, event) {
+ if (event.type() == Clutter.EventType.BUTTON_PRESS) {
+ let source = event.get_source();
+ if (source != this._text &&
+ this._text.has_key_focus() &&
+ this._text.text == '' &&
+ !this._text.has_preedit() &&
+ !Main.layoutManager.keyboardBox.contains(source)) {
+ // the user clicked outside after activating the entry, but
+ // with no search term entered and no keyboard button pressed
+ // - cancel the search
+ this.reset();
+ }
+ }
+
+ return Clutter.EVENT_PROPAGATE;
+ }
+
+ getActivePage() {
+ if (this._activePage == this._workspacesPage)
+ return ViewPage.WINDOWS;
+ else if (this._activePage == this._appsPage)
+ return ViewPage.APPS;
+ else
+ return ViewPage.SEARCH;
+ }
+});