From c267a7a7f9c7c340e60c63aad1bf42923f19f292 Mon Sep 17 00:00:00 2001 From: Joost Verdoorn Date: Fri, 22 Jun 2012 00:52:56 +0200 Subject: [PATCH] viewSelector: Merge SearchTab into the ViewSelector Tabs used to provide an abstraction for a page and the control used to activate it. As the latter has now been replaced with external controls handled directly in the viewSelector, the abstraction itself doesn't make much sense anymore. In preparation of replacing it, move the search handling provided by the SearchTab directly in the viewSelector. https://bugzilla.gnome.org/show_bug.cgi?id=682109 --- js/ui/viewSelector.js | 484 ++++++++++++++++++++---------------------- 1 file changed, 226 insertions(+), 258 deletions(-) diff --git a/js/ui/viewSelector.js b/js/ui/viewSelector.js index 38326a59b..0d382a21e 100644 --- a/js/ui/viewSelector.js +++ b/js/ui/viewSelector.js @@ -98,250 +98,6 @@ const ViewTab = new Lang.Class({ }); -const SearchTab = new Lang.Class({ - Name: 'SearchTab', - Extends: BaseTab, - - _init: function(searchEntry) { - this.active = false; - this._searchPending = false; - this._searchTimeoutId = 0; - - this._searchSystem = new Search.SearchSystem(); - - this._entry = searchEntry; - ShellEntry.addContextMenu(this._entry); - this._text = this._entry.clutter_text; - this._text.connect('key-press-event', Lang.bind(this, this._onKeyPress)); - - this._inactiveIcon = new St.Icon({ style_class: 'search-entry-icon', - icon_name: 'edit-find', - icon_type: St.IconType.SYMBOLIC }); - this._activeIcon = new St.Icon({ style_class: 'search-entry-icon', - icon_name: 'edit-clear', - icon_type: St.IconType.SYMBOLIC }); - this._entry.set_secondary_icon(this._inactiveIcon); - - this._iconClickedId = 0; - - this._searchResults = new SearchDisplay.SearchResults(this._searchSystem); - this.parent(new St.Bin() /* Dummy */, this._searchResults.actor, _("Search"), 'edit-find'); - - this._text.connect('text-changed', Lang.bind(this, this._onTextChanged)); - this._text.connect('key-press-event', Lang.bind(this, function (o, e) { - // We can't connect to 'activate' here because search providers - // might want to do something with the modifiers in activateDefault. - let symbol = e.get_key_symbol(); - if (symbol == Clutter.Return || symbol == Clutter.KP_Enter) { - if (this._searchTimeoutId > 0) { - Mainloop.source_remove(this._searchTimeoutId); - this._doSearch(); - } - this._searchResults.activateDefault(); - return true; - } - return false; - })); - - this._entry.connect('notify::mapped', Lang.bind(this, this._onMapped)); - - global.stage.connect('notify::key-focus', Lang.bind(this, this._onStageKeyFocusChanged)); - - this._capturedEventId = 0; - - this._text.connect('key-focus-in', Lang.bind(this, function() { - this._searchResults.highlightDefault(true); - })); - this._text.connect('key-focus-out', Lang.bind(this, function() { - this._searchResults.highlightDefault(false); - })); - - // 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 St.Bin({ can_focus: true }); - this._focusTrap.connect('key-focus-in', Lang.bind(this, function() { - this._entry.grab_key_focus(); - })); - // ... but make it unfocusable using arrow keys keynav by making its - // bounding box always contain the possible focus source's bounding - // box since StWidget's keynav logic won't ever select it as a target - // in that case. - this._focusTrap.add_constraint(new Clutter.BindConstraint({ source: this._searchResults.actor, - coordinate: Clutter.BindCoordinate.ALL })); - this._searchResults.actor.add_actor(this._focusTrap); - - global.focus_manager.add_group(this._searchResults.actor); - }, - - hide: function() { - this.parent(); - - // 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: function () { - global.stage.set_key_focus(null); - - this._entry.text = ''; - - this._text.set_cursor_visible(true); - this._text.set_selection(0, 0); - }, - - _onStageKeyFocusChanged: function() { - let focus = global.stage.get_key_focus(); - let appearFocused = (this._entry.contains(focus) || - this._searchResults.actor.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: function() { - if (this._entry.mapped) { - // Enable 'find-as-you-type' - this._capturedEventId = global.stage.connect('captured-event', - Lang.bind(this, this._onCapturedEvent)); - 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; - } - }, - - addSearchProvider: function(provider) { - this._searchSystem.registerProvider(provider); - this._searchResults.createProviderMeta(provider); - }, - - removeSearchProvider: function(provider) { - this._searchSystem.unregisterProvider(provider); - this._searchResults.destroyProviderMeta(provider); - }, - - startSearch: function(event) { - global.stage.set_key_focus(this._text); - this._text.event(event, false); - }, - - // the entry does not show the hint - _isActivated: function() { - return this._text.text == this._entry.get_text(); - }, - - _onTextChanged: function (se, prop) { - let searchPreviouslyActive = this.active; - this.active = this._entry.get_text() != ''; - this._searchPending = this.active && !searchPreviouslyActive; - if (this._searchPending) { - this._searchResults.startingSearch(); - } - if (this.active) { - this._entry.set_secondary_icon(this._activeIcon); - - if (this._iconClickedId == 0) { - this._iconClickedId = this._entry.connect('secondary-icon-clicked', - Lang.bind(this, function() { - this.reset(); - })); - } - this._activate(); - } else { - if (this._iconClickedId > 0) - this._entry.disconnect(this._iconClickedId); - this._iconClickedId = 0; - - this._entry.set_secondary_icon(this._inactiveIcon); - this.emit('search-cancelled'); - } - if (!this.active) { - if (this._searchTimeoutId > 0) { - Mainloop.source_remove(this._searchTimeoutId); - this._searchTimeoutId = 0; - } - return; - } - if (this._searchTimeoutId > 0) - return; - this._searchTimeoutId = Mainloop.timeout_add(150, Lang.bind(this, this._doSearch)); - }, - - _onKeyPress: function(entry, event) { - let symbol = event.get_key_symbol(); - if (symbol == Clutter.Escape) { - if (this._isActivated()) { - this.reset(); - return true; - } - } else if (this.active) { - let arrowNext, nextDirection; - if (entry.get_text_direction() == Clutter.TextDirection.RTL) { - arrowNext = Clutter.Left; - nextDirection = Gtk.DirectionType.LEFT; - } else { - arrowNext = Clutter.Right; - nextDirection = Gtk.DirectionType.RIGHT; - } - - if (symbol == Clutter.Tab) { - this._searchResults.navigateFocus(Gtk.DirectionType.TAB_FORWARD); - return true; - } else if (symbol == Clutter.ISO_Left_Tab) { - this._focusTrap.can_focus = false; - this._searchResults.navigateFocus(Gtk.DirectionType.TAB_BACKWARD); - this._focusTrap.can_focus = true; - return true; - } else if (symbol == Clutter.Down) { - this._searchResults.navigateFocus(Gtk.DirectionType.DOWN); - return true; - } else if (symbol == arrowNext && this._text.position == -1) { - this._searchResults.navigateFocus(nextDirection); - return true; - } - } - return false; - }, - - _onCapturedEvent: function(actor, event) { - if (event.type() == Clutter.EventType.BUTTON_PRESS) { - let source = event.get_source(); - if (source != this._text && this._text.text == '' && - !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 false; - }, - - _doSearch: function () { - this._searchTimeoutId = 0; - let text = this._text.get_text().replace(/^\s+/g, '').replace(/\s+$/g, ''); - this._searchResults.doSearch(text); - - return false; - } -}); - - const ViewSelector = new Lang.Class({ Name: 'ViewSelector', @@ -369,13 +125,37 @@ const ViewSelector = new Lang.Class({ this._tabs = []; this._activeTab = null; - this._searchTab = new SearchTab(searchEntry); - this._addTab(this._searchTab); + this.active = false; + this._searchPending = false; + this._searchTimeoutId = 0; - this._searchTab.connect('search-cancelled', Lang.bind(this, - function() { - this._switchTab(this._activeTab); - })); + this._searchSystem = new Search.SearchSystem(); + + this._entry = searchEntry; + ShellEntry.addContextMenu(this._entry); + + this._text = this._entry.clutter_text; + this._text.connect('text-changed', Lang.bind(this, this._onTextChanged)); + this._text.connect('key-press-event', Lang.bind(this, this._onKeyPress)); + this._text.connect('key-focus-in', Lang.bind(this, function() { + this._searchResults.highlightDefault(true); + })); + this._text.connect('key-focus-out', Lang.bind(this, function() { + this._searchResults.highlightDefault(false); + })); + this._entry.connect('notify::mapped', Lang.bind(this, this._onMapped)); + global.stage.connect('notify::key-focus', Lang.bind(this, this._onStageKeyFocusChanged)); + + this._inactiveIcon = new St.Icon({ style_class: 'search-entry-icon', + icon_name: 'edit-find', + icon_type: St.IconType.SYMBOLIC }); + this._activeIcon = new St.Icon({ style_class: 'search-entry-icon', + icon_name: 'edit-clear', + icon_type: St.IconType.SYMBOLIC }); + this._entry.set_secondary_icon(this._inactiveIcon); + + this._iconClickedId = 0; + this._capturedEventId = 0; this._workspacesDisplay = new WorkspacesView.WorkspacesDisplay(); this._windowsTab = new ViewTab('windows', _("Windows"), this._workspacesDisplay.actor, 'text-x-generic'); @@ -385,16 +165,37 @@ const ViewSelector = new Lang.Class({ this._appsTab = new ViewTab('applications', _("Applications"), appView.actor, 'system-run'); this._addViewTab(this._appsTab); + this._searchResults = new SearchDisplay.SearchResults(this._searchSystem); + this._searchTab = new BaseTab(new St.Bin(), this._searchResults.actor, _("Search"), 'edit-find'); + this._addTab(this._searchTab); + // Default search providers // Wanda comes obviously first this.addSearchProvider(new Wanda.WandaSearchProvider()); this.addSearchProvider(new AppDisplay.AppSearchProvider()); this.addSearchProvider(new AppDisplay.SettingsSearchProvider()); this.addSearchProvider(new PlaceDisplay.PlaceSearchProvider()); - + // Load remote search providers provided by applications RemoteSearch.loadRemoteSearchProviders(Lang.bind(this, this.addSearchProvider)); + // 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 St.Bin({ can_focus: true }); + this._focusTrap.connect('key-focus-in', Lang.bind(this, function() { + this._entry.grab_key_focus(); + })); + // ... but make it unfocusable using arrow keys keynav by making its + // bounding box always contain the possible focus source's bounding + // box since StWidget's keynav logic won't ever select it as a target + // in that case. + this._focusTrap.add_constraint(new Clutter.BindConstraint({ source: this._searchResults.actor, + coordinate: Clutter.BindCoordinate.ALL })); + this._searchResults.actor.add_actor(this._focusTrap); + + global.focus_manager.add_group(this._searchResults.actor); + Main.overview.connect('item-drag-begin', Lang.bind(this, this._resetShowAppsButton)); @@ -496,15 +297,15 @@ const ViewSelector = new Lang.Class({ let symbol = event.get_key_symbol(); if (symbol == Clutter.Escape) { - if (this._searchTab.active) - this._searchTab.reset(); + if (this.active) + this.reset(); else Main.overview.hide(); return true; } else if (Clutter.keysym_to_unicode(symbol) || - (symbol == Clutter.BackSpace && this._searchTab.active)) { - this._searchTab.startSearch(event); - } else if (!this._searchTab.active) { + (symbol == Clutter.BackSpace && this.active)) { + this.startSearch(event); + } else if (!this.active) { if (symbol == Clutter.Tab) { this._activeTab.page.navigate_focus(null, Gtk.DirectionType.TAB_FORWARD, false); return true; @@ -516,12 +317,179 @@ const ViewSelector = new Lang.Class({ return false; }, + _searchCancelled: function() { + this._switchTab(this._activeTab); + + // 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: function () { + global.stage.set_key_focus(null); + + this._entry.text = ''; + + this._text.set_cursor_visible(true); + this._text.set_selection(0, 0); + }, + + _onStageKeyFocusChanged: function() { + let focus = global.stage.get_key_focus(); + let appearFocused = (this._entry.contains(focus) || + this._searchResults.actor.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: function() { + if (this._entry.mapped) { + // Enable 'find-as-you-type' + this._capturedEventId = global.stage.connect('captured-event', + Lang.bind(this, this._onCapturedEvent)); + 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; + } + }, + + startSearch: function(event) { + global.stage.set_key_focus(this._text); + this._text.event(event, false); + }, + + // the entry does not show the hint + _isActivated: function() { + return this._text.text == this._entry.get_text(); + }, + + _onTextChanged: function (se, prop) { + let searchPreviouslyActive = this.active; + this.active = this._entry.get_text() != ''; + this._searchPending = this.active && !searchPreviouslyActive; + if (this._searchPending) { + this._searchResults.startingSearch(); + } + if (this.active) { + this._entry.set_secondary_icon(this._activeIcon); + + if (this._iconClickedId == 0) { + this._iconClickedId = this._entry.connect('secondary-icon-clicked', + Lang.bind(this, function() { + this.reset(); + })); + } + this._switchTab(this._searchTab); + } else { + if (this._iconClickedId > 0) + this._entry.disconnect(this._iconClickedId); + this._iconClickedId = 0; + + this._entry.set_secondary_icon(this._inactiveIcon); + this._searchCancelled(); + } + if (!this.active) { + if (this._searchTimeoutId > 0) { + Mainloop.source_remove(this._searchTimeoutId); + this._searchTimeoutId = 0; + } + return; + } + if (this._searchTimeoutId > 0) + return; + this._searchTimeoutId = Mainloop.timeout_add(150, Lang.bind(this, this._doSearch)); + }, + + _onKeyPress: function(entry, event) { + let symbol = event.get_key_symbol(); + if (symbol == Clutter.Escape) { + if (this._isActivated()) { + this.reset(); + return true; + } + } else if (symbol == Clutter.Return || symbol == Clutter.KP_Enter) { + // We can't connect to 'activate' here because search providers + // might want to do something with the modifiers in activateDefault. + if (this._searchTimeoutId > 0) { + Mainloop.source_remove(this._searchTimeoutId); + this._doSearch(); + } + this._searchResults.activateDefault(); + return true; + } else if (this.active) { + let arrowNext, nextDirection; + if (entry.get_text_direction() == Clutter.TextDirection.RTL) { + arrowNext = Clutter.Left; + nextDirection = Gtk.DirectionType.LEFT; + } else { + arrowNext = Clutter.Right; + nextDirection = Gtk.DirectionType.RIGHT; + } + + if (symbol == Clutter.Tab) { + this._searchResults.navigateFocus(Gtk.DirectionType.TAB_FORWARD); + return true; + } else if (symbol == Clutter.ISO_Left_Tab) { + this._focusTrap.can_focus = false; + this._searchResults.navigateFocus(Gtk.DirectionType.TAB_BACKWARD); + this._focusTrap.can_focus = true; + return true; + } else if (symbol == Clutter.Down) { + this._searchResults.navigateFocus(Gtk.DirectionType.DOWN); + return true; + } else if (symbol == arrowNext && this._text.position == -1) { + this._searchResults.navigateFocus(nextDirection); + return true; + } + } + return false; + }, + + _onCapturedEvent: function(actor, event) { + if (event.type() == Clutter.EventType.BUTTON_PRESS) { + let source = event.get_source(); + if (source != this._text && this._text.text == '' && + !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 false; + }, + + _doSearch: function () { + this._searchTimeoutId = 0; + let text = this._text.get_text().replace(/^\s+/g, '').replace(/\s+$/g, ''); + this._searchResults.doSearch(text); + + return false; + }, + addSearchProvider: function(provider) { - this._searchTab.addSearchProvider(provider); + this._searchSystem.registerProvider(provider); + this._searchResults.createProviderMeta(provider); }, removeSearchProvider: function(provider) { - this._searchTab.removeSearchProvider(provider); + this._searchSystem.unregisterProvider(provider); + this._searchResults.destroyProviderMeta(provider); } }); Signals.addSignalMethods(ViewSelector.prototype);