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);