From 95d15725fc730144ba0e11bb726c45759af2f090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Fri, 12 Nov 2010 18:45:29 +0100 Subject: [PATCH] Use the old dash code to implement the view selector The view selector is a tabbed interface with a search entry. Starting a search switches focus to the results' tab, ending a search moves the focus back to the previously selected tab. Activating a normal tab while a search is active cancels the search. https://bugzilla.gnome.org/show_bug.cgi?id=634948 --- data/theme/gnome-shell.css | 128 ++++---- js/Makefile.am | 1 + js/ui/dash.js | 589 ------------------------------------- js/ui/viewSelector.js | 517 ++++++++++++++++++++++++++++++++ po/POTFILES.in | 1 + 5 files changed, 583 insertions(+), 653 deletions(-) create mode 100644 js/ui/viewSelector.js diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index 2d48ac59b..f54ed521a 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -382,90 +382,92 @@ StTooltip StLabel { spacing: 12px; } +#viewSelector { + spacing: 16px; +} + +#searchArea { + padding: 0px 12px; +} + #searchEntry { - padding: 4px; - border-radius: 4px; - color: #a8a8a8; - border: 1px solid #565656; - background-color: #404040; - caret-color: #fff; + padding: 4px 8px; + border-radius: 12px; + color: rgb(128, 128, 128); + border: 2px solid rgba(128, 128, 128, 0.4); + background-gradient-start: rgba(0, 0, 0, 0.2); + background-gradient-end: rgba(128, 128, 128, 0.2); + background-gradient-direction: vertical; + caret-color: rgb(128, 128, 128); caret-size: 1px; height: 16px; + width: 250px; transition-duration: 300; } #searchEntry:focus { - color: #545454; - border: 1px solid #3a3a3a; - background-color: #e8e8e8; - caret-color: #545454; + border: 2px solid #ffffff; + background-gradient-start: rgba(0, 0, 0, 0.2); + background-gradient-end: #ffffff; + background-gradient-direction: vertical; + color: rgb(64, 64, 64); + font-weight: bold; -st-shadow: 0px 0px 6px 2px rgba(255,255,255,0.9); transition-duration: 0; } #searchEntry:hover { - color: #a8a8a8; - border: 1px solid #4d4d4d; - background-color: #e8e8e8; + border: 2px solid #e8e8e8; caret-color: #545454; transition-duration: 500; } -.dash-section { +.view-tab-title { + color: #888a85; + font-weight: bold; + padding: 0px 12px; +} + +.view-tab-title:selected { + color: white; +} + +.view-tab-boxpointer { + -arrow-border-radius: 9px; + -arrow-background-color: rgba(0,0,0,0.5); + -arrow-border-width: 2px; + -arrow-border-color: rgba(255,255,255,0.5); + -arrow-base: 30px; + -arrow-rise: 15px; +} + +#searchResults { + padding: 20px 10px 10px 10px; +} + +#searchResultsContent { + padding: 0 10px; spacing: 8px; } -.section-header { -} - -.section-header-inner { - spacing: 4px; -} - -.section-text-content { - padding: 4px 0px; -} - -.dash-section-content { - color: #ffffff; - spacing: 8px; -} - -.more-link { -} - -.more-link-expander { - background-image: url("section-more.svg"); - width: 9px; - height: 9px; -} - -.more-link-expander.open { - background-image: url("section-more-open.svg"); - width: 9px; - height: 9px; -} - -.dash-pane { - border-radius: 10px; - background-color: #111111; - border: 2px solid #868686; - color: #ffffff; - padding: 30px 10px 10px 20px; -} - -#dashAppSearchResults { - padding: 8px 0px; -} - .search-statustext, .search-section-header { - padding: 4px 0px; + padding: 4px 12px; spacing: 4px; + color: #6f6f6f; +} + +.search-section { + background-color: rgba(128, 128, 128, .1); + border: 1px solid rgba(50, 50, 50, .4); + border-radius: 10px; } .search-section-results { color: #ffffff; + border-radius: 10px; + border: 1px solid rgba(50, 50, 50, .4); + padding: 6px; } .search-section-list-results { @@ -473,17 +475,15 @@ StTooltip StLabel { } .search-result-content { - padding: 3px; + padding: 4px; } .search-result-content:selected { - padding: 2px; - border: 1px solid #5c5c5c; - border-radius: 2px; - background-color: #1e1e1e; + border-radius: 4px; + background: rgba(255,255,255,0.33); } -.dash-results-container { +.results-container { spacing: 4px; } diff --git a/js/Makefile.am b/js/Makefile.am index 52eb09077..14c75e5fd 100644 --- a/js/Makefile.am +++ b/js/Makefile.am @@ -48,6 +48,7 @@ nobase_dist_js_DATA = \ ui/status/volume.js \ ui/telepathyClient.js \ ui/tweener.js \ + ui/viewSelector.js \ ui/windowAttentionHandler.js \ ui/windowManager.js \ ui/workspace.js \ diff --git a/js/ui/dash.js b/js/ui/dash.js index 6c52b7612..40bf445b7 100644 --- a/js/ui/dash.js +++ b/js/ui/dash.js @@ -1,6 +1,5 @@ /* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ -const Clutter = imports.gi.Clutter; const Mainloop = imports.mainloop; const Signals = imports.signals; const Lang = imports.lang; @@ -12,597 +11,9 @@ const _ = Gettext.gettext; const AppDisplay = imports.ui.appDisplay; const AppFavorites = imports.ui.appFavorites; const DND = imports.ui.dnd; -const DocDisplay = imports.ui.docDisplay; -const PlaceDisplay = imports.ui.placeDisplay; const Main = imports.ui.main; -const Overview = imports.ui.overview; -const Search = imports.ui.search; -const SearchDisplay = imports.ui.searchDisplay; -const Tweener = imports.ui.tweener; const Workspace = imports.ui.workspace; -/* - * Returns the index in an array of a given length that is obtained - * if the provided index is incremented by an increment and the array - * is wrapped in if necessary. - * - * index: prior index, expects 0 <= index < length - * increment: the change in index, expects abs(increment) <= length - * length: the length of the array - */ -function _getIndexWrapped(index, increment, length) { - return (index + increment + length) % length; -} - -function Pane() { - this._init(); -} - -Pane.prototype = { - _init: function () { - this._open = false; - - this.actor = new St.BoxLayout({ style_class: 'dash-pane', - vertical: true, - reactive: true }); - this.actor.connect('button-press-event', Lang.bind(this, function (a, e) { - // Eat button press events so they don't go through and close the pane - return true; - })); - - // Hidden by default - this.actor.hide(); - }, - - open: function () { - if (this._open) - return; - this._open = true; - this.emit('open-state-changed', this._open); - this.actor.opacity = 0; - this.actor.show(); - Tweener.addTween(this.actor, - { opacity: 255, - time: Overview.PANE_FADE_TIME, - transition: 'easeOutQuad' - }); - }, - - close: function () { - if (!this._open) - return; - this._open = false; - Tweener.addTween(this.actor, - { opacity: 0, - time: Overview.PANE_FADE_TIME, - transition: 'easeOutQuad', - onComplete: Lang.bind(this, function() { - this.actor.hide(); - this.emit('open-state-changed', this._open); - }) - }); - }, - - destroyContent: function() { - let children = this.actor.get_children(); - for (let i = 0; i < children.length; i++) { - children[i].destroy(); - } - }, - - toggle: function () { - if (this._open) - this.close(); - else - this.open(); - } -}; -Signals.addSignalMethods(Pane.prototype); - -function ResultArea() { - this._init(); -} - -ResultArea.prototype = { - _init : function() { - this.actor = new St.BoxLayout({ vertical: true }); - this.resultsContainer = new St.BoxLayout({ style_class: 'dash-results-container' }); - this.actor.add(this.resultsContainer, { expand: true }); - - this.display = new DocDisplay.DocDisplay(); - this.resultsContainer.add(this.display.actor, { expand: true }); - this.display.load(); - } -}; - -function ResultPane(dash) { - this._init(dash); -} - -ResultPane.prototype = { - __proto__: Pane.prototype, - - _init: function(dash) { - Pane.prototype._init.call(this); - - let resultArea = new ResultArea(); - this.actor.add(resultArea.actor, { expand: true }); - this.connect('open-state-changed', Lang.bind(this, function(pane, isOpen) { - resultArea.display.resetState(); - })); - } -}; - -function SearchEntry() { - this._init(); -} - -SearchEntry.prototype = { - _init : function() { - this.actor = new St.Entry({ name: 'searchEntry', - hint_text: _("Find") }); - this.entry = this.actor.clutter_text; - - this.actor.clutter_text.connect('text-changed', Lang.bind(this, - function() { - if (this.isActive()) - this.actor.set_secondary_icon_from_file(global.imagedir + - 'close-black.svg'); - else - this.actor.set_secondary_icon_from_file(null); - })); - this.actor.connect('secondary-icon-clicked', Lang.bind(this, - function() { - this.reset(); - })); - this.actor.connect('destroy', Lang.bind(this, this._onDestroy)); - - global.stage.connect('notify::key-focus', Lang.bind(this, this._updateCursorVisibility)); - - this.pane = null; - - this._capturedEventId = 0; - }, - - _updateCursorVisibility: function() { - let focus = global.stage.get_key_focus(); - if (focus == global.stage || focus == this.entry) - this.entry.set_cursor_visible(true); - else - this.entry.set_cursor_visible(false); - }, - - show: function() { - if (this._capturedEventId == 0) - this._capturedEventId = global.stage.connect('captured-event', - Lang.bind(this, this._onCapturedEvent)); - this.entry.set_cursor_visible(true); - this.entry.set_selection(0, 0); - }, - - hide: function() { - if (this.isActive()) - this.reset(); - if (this._capturedEventId > 0) { - global.stage.disconnect(this._capturedEventId); - this._capturedEventId = 0; - } - }, - - reset: function () { - let [x, y, mask] = global.get_pointer(); - let actor = global.stage.get_actor_at_pos (Clutter.PickMode.REACTIVE, - x, y); - // this.actor is never hovered directly, only its clutter_text and icon - let hovered = this.actor == actor.get_parent(); - - this.actor.set_hover(hovered); - - this.entry.text = ''; - global.stage.set_key_focus(null); - this.entry.set_cursor_visible(true); - this.entry.set_selection(0, 0); - }, - - getText: function () { - return this.entry.get_text().replace(/^\s+/g, '').replace(/\s+$/g, ''); - }, - - // some search term has been entered - isActive: function() { - return this.actor.get_text() != ''; - }, - - // the entry does not show the hint - _isActivated: function() { - return this.entry.text == this.actor.get_text(); - }, - - _onCapturedEvent: function(actor, event) { - let source = event.get_source(); - let panelEvent = source && Main.panel.actor.contains(source); - - switch (event.type()) { - case Clutter.EventType.BUTTON_PRESS: - // the user clicked outside after activating the entry, but - // with no search term entered - cancel the search - if (source != this.entry && this.entry.text == '') { - this.reset(); - // allow only panel events to continue - return !panelEvent; - } - return false; - case Clutter.EventType.KEY_PRESS: - // If neither the stage nor our entry have key focus, some - // "special" actor grabbed the focus (run dialog, looking - // glass); we don't want to interfere with that - let focus = global.stage.get_key_focus(); - if (focus != global.stage && focus != this.entry) - return false; - - let sym = event.get_key_symbol(); - - // If we have an active search, Escape cancels it - if we - // haven't, the key is ignored - if (sym == Clutter.Escape) - if (this._isActivated()) { - this.reset(); - return true; - } else { - return false; - } - - // Ignore non-printable keys - if (!Clutter.keysym_to_unicode(sym)) - return false; - - // Search started - move the key focus to the entry and - // "repeat" the event - if (!this._isActivated()) { - global.stage.set_key_focus(this.entry); - this.entry.event(event, false); - } - - return false; - default: - // Suppress all other events outside the panel while the entry - // is activated and no search has been entered - any click - // outside the entry will cancel the search - return (this.entry.text == '' && !panelEvent); - } - }, - - _onDestroy: function() { - if (this._capturedEventId > 0) { - global.stage.disconnect(this._capturedEventId); - this._capturedEventId = 0; - } - } -}; -Signals.addSignalMethods(SearchEntry.prototype); - - -function MoreLink() { - this._init(); -} - -MoreLink.prototype = { - _init : function () { - this.actor = new St.BoxLayout({ style_class: 'more-link', - reactive: true }); - this.pane = null; - - this._expander = new St.Bin({ style_class: 'more-link-expander' }); - this.actor.add(this._expander, { expand: true, y_fill: false }); - }, - - activate: function() { - if (!this.actor.visible) - return true; // If the link isn't visible we don't want the header to react - // to clicks - if (this.pane == null) { - // Ensure the pane is created; the activated handler will call setPane - this.emit('activated'); - } - this._pane.toggle(); - return true; - }, - - setPane: function (pane) { - this._pane = pane; - this._pane.connect('open-state-changed', Lang.bind(this, function(pane, isOpen) { - if (isOpen) - this._expander.add_style_class_name('open'); - else - this._expander.remove_style_class_name('open'); - })); - } -}; - -Signals.addSignalMethods(MoreLink.prototype); - -function SectionHeader(title, suppressBrowse) { - this._init(title, suppressBrowse); -} - -SectionHeader.prototype = { - _init : function (title, suppressBrowse) { - this.actor = new St.Bin({ style_class: 'section-header', - x_align: St.Align.START, - x_fill: true, - y_fill: true, - reactive: !suppressBrowse }); - this._innerBox = new St.BoxLayout({ style_class: 'section-header-inner' }); - this.actor.set_child(this._innerBox); - - let textBox = new St.BoxLayout({ style_class: 'section-text-content' }); - this.text = new St.Label({ style_class: 'section-title', - text: title }); - textBox.add(this.text, { x_align: St.Align.START }); - - this._innerBox.add(textBox, { expand: true }); - - if (!suppressBrowse) { - this.moreLink = new MoreLink(); - this._innerBox.add(this.moreLink.actor, { x_align: St.Align.END }); - this.actor.connect('button-press-event', Lang.bind(this, this._onButtonPress)); - } - }, - - _onButtonPress: function() { - this.moreLink.activate(); - }, - - setMoreLinkVisible : function(visible) { - if (visible) - this.moreLink.actor.show(); - else - this.moreLink.actor.hide(); - } -}; - -Signals.addSignalMethods(SectionHeader.prototype); - - -function Section(titleString, suppressBrowse) { - this._init(titleString, suppressBrowse); -} - -Section.prototype = { - _init: function(titleString, suppressBrowse) { - this.actor = new St.BoxLayout({ style_class: 'dash-section', - vertical: true }); - this.header = new SectionHeader(titleString, suppressBrowse); - this.actor.add(this.header.actor); - this.content = new St.BoxLayout({ style_class: 'dash-section-content', - vertical: true }); - this.actor.add(this.content); - } -}; - -function OldDash() { - this._init(); -} - -OldDash.prototype = { - _init : function() { - // dash and the popup panes need to be reactive so that the clicks in unoccupied places on them - // are not passed to the transparent background underneath them. This background is used for the workspaces area when - // the additional dash panes are being shown and it handles clicks by closing the additional panes, so that the user - // can interact with the workspaces. However, this behavior is not desirable when the click is actually over a pane. - // - // We have to make the individual panes reactive instead of making the whole dash actor reactive because the width - // of the Group actor ends up including the width of its hidden children, so we were getting a reactive object as - // wide as the details pane that was blocking the clicks to the workspaces underneath it even when the details pane - // was actually hidden. - this.actor = new St.BoxLayout({ name: 'dash', - vertical: true, - reactive: true }); - - // The searchArea just holds the entry - this.searchArea = new St.BoxLayout({ name: 'dashSearchArea', - vertical: true }); - this.sectionArea = new St.BoxLayout({ name: 'dashSections', - vertical: true }); - - this.actor.add(this.searchArea); - this.actor.add(this.sectionArea); - - // The currently active popup display - this._activePane = null; - - /***** Search *****/ - - this._searchActive = false; - this._searchPending = false; - this._searchEntry = new SearchEntry(); - this.searchArea.add(this._searchEntry.actor, { y_fill: false, expand: true }); - - this._searchSystem = new Search.SearchSystem(); - this._searchSystem.registerProvider(new AppDisplay.AppSearchProvider()); - this._searchSystem.registerProvider(new AppDisplay.PrefsSearchProvider()); - this._searchSystem.registerProvider(new PlaceDisplay.PlaceSearchProvider()); - this._searchSystem.registerProvider(new DocDisplay.DocSearchProvider()); - - this.searchResults = new SearchDisplay.SearchResults(this._searchSystem); - this.actor.add(this.searchResults.actor); - this.searchResults.actor.hide(); - - this._keyPressId = 0; - this._searchTimeoutId = 0; - this._searchEntry.entry.connect('text-changed', Lang.bind(this, function (se, prop) { - let searchPreviouslyActive = this._searchActive; - this._searchActive = this._searchEntry.isActive(); - this._searchPending = this._searchActive && !searchPreviouslyActive; - if (this._searchPending) { - this.searchResults.startingSearch(); - } - if (this._searchActive) { - this.searchResults.actor.show(); - this.sectionArea.hide(); - } else { - this.searchResults.actor.hide(); - this.sectionArea.show(); - } - if (!this._searchActive) { - 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)); - })); - this._searchEntry.entry.connect('activate', Lang.bind(this, function (se) { - if (this._searchTimeoutId > 0) { - Mainloop.source_remove(this._searchTimeoutId); - this._doSearch(); - } - this.searchResults.activateSelected(); - return true; - })); - - /***** Applications *****/ - - this._appsSection = new Section(_("APPLICATIONS")); - let appWell = new AppDisplay.AppWell(); - this._appsSection.content.add(appWell.actor, { expand: true }); - - this._allApps = null; - this._appsSection.header.moreLink.connect('activated', Lang.bind(this, function (link) { - if (this._allApps == null) { - this._allApps = new AppDisplay.AllAppDisplay(); - this._addPane(this._allApps, St.Align.START); - link.setPane(this._allApps); - } - })); - - this.sectionArea.add(this._appsSection.actor); - - /***** Places *****/ - - /* Translators: This is in the sense of locations for documents, - network locations, etc. */ - this._placesSection = new Section(_("PLACES & DEVICES"), true); - let placesDisplay = new PlaceDisplay.DashPlaceDisplay(); - this._placesSection.content.add(placesDisplay.actor, { expand: true }); - this.sectionArea.add(this._placesSection.actor); - - /***** Documents *****/ - - this._docsSection = new Section(_("RECENT ITEMS")); - - this._docDisplay = new DocDisplay.DashDocDisplay(); - this._docsSection.content.add(this._docDisplay.actor, { expand: true }); - - this._moreDocsPane = null; - this._docsSection.header.moreLink.connect('activated', Lang.bind(this, function (link) { - if (this._moreDocsPane == null) { - this._moreDocsPane = new ResultPane(this); - this._addPane(this._moreDocsPane, St.Align.END); - link.setPane(this._moreDocsPane); - } - })); - - this._docDisplay.connect('changed', Lang.bind(this, function () { - this._docsSection.header.setMoreLinkVisible( - this._docDisplay.actor.get_children().length > 0); - })); - this._docDisplay.emit('changed'); - - this.sectionArea.add(this._docsSection.actor, { expand: true }); - }, - - _onKeyPress: function(stage, event) { - // If neither the stage nor the search entry have key focus, some - // "special" actor grabbed the focus (run dialog, looking glass); - // we don't want to interfere with that - let focus = stage.get_key_focus(); - if (focus != stage && focus != this._searchEntry.entry) - return false; - - let symbol = event.get_key_symbol(); - if (symbol == Clutter.Escape) { - // If we're in one of the "more" modes or showing the - // details pane, close them - if (this._activePane != null) - this._activePane.close(); - // Otherwise, just close the Overview entirely - else - Main.overview.hide(); - return true; - } else if (symbol == Clutter.Up) { - if (!this._searchActive) - return true; - this.searchResults.selectUp(false); - - return true; - } else if (symbol == Clutter.Down) { - if (!this._searchActive) - return true; - - this.searchResults.selectDown(false); - return true; - } - return false; - }, - - _doSearch: function () { - this._searchTimeoutId = 0; - let text = this._searchEntry.getText(); - this.searchResults.updateSearch(text); - - return false; - }, - - addSearchProvider: function(provider) { - //Add a new search provider to the dash. - - this._searchSystem.registerProvider(provider); - this.searchResults.createProviderMeta(provider); - }, - - show: function() { - this._searchEntry.show(); - if (this._keyPressId == 0) - this._keyPressId = global.stage.connect('key-press-event', - Lang.bind(this, this._onKeyPress)); - }, - - hide: function() { - this._firstSelectAfterOverlayShow = true; - this._searchEntry.hide(); - if (this._activePane != null) - this._activePane.close(); - if (this._keyPressId > 0) { - global.stage.disconnect(this._keyPressId); - this._keyPressId = 0; - } - }, - - closePanes: function () { - if (this._activePane != null) - this._activePane.close(); - }, - - _addPane: function(pane, align) { - pane.connect('open-state-changed', Lang.bind(this, function (pane, isOpen) { - if (isOpen) { - if (pane != this._activePane && this._activePane != null) { - this._activePane.close(); - } - this._activePane = pane; - } else if (pane == this._activePane) { - this._activePane = null; - } - })); - Main.overview.addPane(pane, align); - } -}; -Signals.addSignalMethods(Dash.prototype); - function Dash() { this._init(); diff --git a/js/ui/viewSelector.js b/js/ui/viewSelector.js new file mode 100644 index 000000000..20c640ee0 --- /dev/null +++ b/js/ui/viewSelector.js @@ -0,0 +1,517 @@ +/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ + +const Clutter = imports.gi.Clutter; +const Mainloop = imports.mainloop; +const Meta = imports.gi.Meta; +const Signals = imports.signals; +const Lang = imports.lang; +const Shell = imports.gi.Shell; +const St = imports.gi.St; +const Gettext = imports.gettext.domain('gnome-shell'); +const _ = Gettext.gettext; + +const Main = imports.ui.main; +const Search = imports.ui.search; +const SearchDisplay = imports.ui.searchDisplay; +const Tweener = imports.ui.tweener; + + +function SearchEntry() { + this._init(); +} + +SearchEntry.prototype = { + _init : function() { + this.actor = new St.Entry({ name: 'searchEntry', + hint_text: _("Search your computer") }); + this.entry = this.actor.clutter_text; + + this.actor.clutter_text.connect('text-changed', Lang.bind(this, + function() { + if (this.isActive()) + this.actor.set_secondary_icon_from_file(global.imagedir + + 'close-black.svg'); + else + this.actor.set_secondary_icon_from_file(null); + })); + this.actor.connect('secondary-icon-clicked', Lang.bind(this, + function() { + this.reset(); + })); + this.actor.connect('destroy', Lang.bind(this, this._onDestroy)); + + global.stage.connect('notify::key-focus', Lang.bind(this, this._updateCursorVisibility)); + + this.pane = null; + + this._capturedEventId = 0; + }, + + _updateCursorVisibility: function() { + let focus = global.stage.get_key_focus(); + if (focus == global.stage || focus == this.entry) + this.entry.set_cursor_visible(true); + else + this.entry.set_cursor_visible(false); + }, + + show: function() { + if (this._capturedEventId == 0) + this._capturedEventId = global.stage.connect('captured-event', + Lang.bind(this, this._onCapturedEvent)); + this.entry.set_cursor_visible(true); + this.entry.set_selection(0, 0); + }, + + hide: function() { + if (this.isActive()) + this.reset(); + if (this._capturedEventId > 0) { + global.stage.disconnect(this._capturedEventId); + this._capturedEventId = 0; + } + }, + + reset: function () { + let [x, y, mask] = global.get_pointer(); + let actor = global.stage.get_actor_at_pos (Clutter.PickMode.REACTIVE, + x, y); + // this.actor is never hovered directly, only its clutter_text and icon + let hovered = this.actor == actor.get_parent(); + + this.actor.set_hover(hovered); + + this.entry.text = ''; + this.entry.set_cursor_visible(true); + this.entry.set_selection(0, 0); + }, + + getText: function () { + return this.entry.get_text().replace(/^\s+/g, '').replace(/\s+$/g, ''); + }, + + // some search term has been entered + isActive: function() { + return this.actor.get_text() != ''; + }, + + // the entry does not show the hint + _isActivated: function() { + return this.entry.text == this.actor.get_text(); + }, + + _onCapturedEvent: function(actor, event) { + let source = event.get_source(); + let panelEvent = source && Main.panel.actor.contains(source); + + switch (event.type()) { + case Clutter.EventType.BUTTON_PRESS: + // the user clicked outside after activating the entry, but + // with no search term entered - cancel the search + if (source != this.entry && this.entry.text == '') { + this.reset(); + // allow only panel events to continue + return !panelEvent; + } + return false; + case Clutter.EventType.KEY_PRESS: + // If neither the stage nor our entry have key focus, some + // "special" actor grabbed the focus (run dialog, looking + // glass); we don't want to interfere with that + let focus = global.stage.get_key_focus(); + if (focus != global.stage && focus != this.entry) + return false; + + let sym = event.get_key_symbol(); + + // If we have an active search, Escape cancels it - if we + // haven't, the key is ignored + if (sym == Clutter.Escape) + if (this._isActivated()) { + this.reset(); + return true; + } else { + return false; + } + + // Ignore non-printable keys + if (!Clutter.keysym_to_unicode(sym)) + return false; + + // Search started - move the key focus to the entry and + // "repeat" the event + if (!this._isActivated()) { + global.stage.set_key_focus(this.entry); + this.entry.event(event, false); + } + + return false; + default: + // Suppress all other events outside the panel while the entry + // is activated and no search has been entered - any click + // outside the entry will cancel the search + return (this.entry.text == '' && !panelEvent); + } + }, + + _onDestroy: function() { + if (this._capturedEventId > 0) { + global.stage.disconnect(this._capturedEventId); + this._capturedEventId = 0; + } + } +}; +Signals.addSignalMethods(SearchEntry.prototype); + + +function BaseTab(titleActor, pageActor) { + this._init(titleActor, pageActor); +} + +BaseTab.prototype = { + _init: function(titleActor, pageActor) { + this.title = titleActor; + this.page = new St.Bin({ child: pageActor, + x_align: St.Align.START, + y_align: St.Align.START, + x_fill: true, + y_fill: true, + style_class: 'view-tab-page' }); + + this.visible = false; + }, + + show: function() { + this.visible = true; + this.page.opacity = 0; + this.page.show(); + + Tweener.addTween(this.page, + { opacity: 255, + time: 0.1, + transition: 'easeOutQuad' }); + }, + + hide: function() { + this.visible = false; + Tweener.addTween(this.page, + { opacity: 0, + time: 0.1, + transition: 'easeOutQuad', + onComplete: Lang.bind(this, + function() { + this.page.hide(); + }) + }); + }, + + _activate: function() { + this.emit('activated'); + } +}; +Signals.addSignalMethods(BaseTab.prototype); + + +function ViewTab(label, pageActor) { + this._init(label, pageActor); +} + +ViewTab.prototype = { + __proto__: BaseTab.prototype; + + _init: function(label, pageActor) { + let titleActor = new St.Button({ label: label, + style_class: 'view-tab-title' }); + titleActor.connect('clicked', Lang.bind(this, this._activate)); + + BaseTab.prototype._init.call(this, titleActor, pageActor); + } +}; + + +function SearchTab(entryActor, pageActor) { + this._init(entryActor, pageActor); +} + +SearchTab.prototype = { + __proto__: BaseTab.prototype +}; + + +function ViewSelector() { + this._init(); +} + +ViewSelector.prototype = { + _init : function() { + this.actor = new St.BoxLayout({ name: 'viewSelector', + vertical: true }); + + // The tab bar is located at the top of the view selector and + // holds both "normal" tab labels and the search entry. The former + // is left aligned, the latter right aligned - unless the text + // direction is RTL, in which case the order is reversed. + this._tabBar = new Shell.GenericContainer(); + this._tabBar.connect('get-preferred-width', + Lang.bind(this, this._getPreferredTabBarWidth)); + this._tabBar.connect('get-preferred-height', + Lang.bind(this, this._getPreferredTabBarHeight)); + this._tabBar.connect('allocate', + Lang.bind(this, this._allocateTabBar)); + this.actor.add(this._tabBar); + + // Box to hold "normal" tab labels + this._tabBox = new St.BoxLayout({ name: 'viewSelectorTabBar' }); + this._tabBar.add_actor(this._tabBox); + + // The searchArea just holds the entry + this._searchArea = new St.Bin({ name: 'searchArea' }); + this._tabBar.add_actor(this._searchArea); + + // The page area holds the tab pages. Every page is given the + // area's full allocation, so that the pages would appear on top + // of each other if the inactive ones weren't hidden. + this._pageArea = new Shell.Stack(); + this.actor.add(this._pageArea, { x_fill: true, + y_fill: true, + expand: true }); + + this._tabs = []; + this._activeTab = null; + + this._itemDragBeginId = 0; + this._overviewHidingId = 0; + + // Public constraints which may be used to tie actors' height or + // vertical position to the current tab's content; as the content's + // height and position depend on the view selector's style properties + // (e.g. font size, padding, spacing, ...) it would be extremely hard + // and ugly to get these from the outside. While it would be possible + // to use position and height properties directly, outside code would + // need to ensure that the content is properly allocated before + // accessing the properties. + this.constrainY = new Clutter.BindConstraint({ source: this._pageArea, + coordinate: Clutter.BindCoordinate.Y }); + this.constrainHeight = new Clutter.BindConstraint({ source: this._pageArea, + coordinate: Clutter.BindCoordinate.HEIGHT }); + + /***** Search *****/ + this._searchActive = false; + this._searchPending = false; + this._searchEntry = new SearchEntry(); + this._searchArea.set_child(this._searchEntry.actor); + + this._searchSystem = new Search.SearchSystem(); + + this.searchResults = new SearchDisplay.SearchResults(this._searchSystem); + this._searchTab = new SearchTab(this._searchArea, + this.searchResults.actor); + this._pageArea.add_actor(this._searchTab.page); + this._searchTab.hide(); + + this._keyPressId = 0; + this._searchTimeoutId = 0; + this._searchEntry.entry.connect('text-changed', Lang.bind(this, function (se, prop) { + let searchPreviouslyActive = this._searchActive; + this._searchActive = this._searchEntry.isActive(); + this._searchPending = this._searchActive && !searchPreviouslyActive; + if (this._searchPending) { + this.searchResults.startingSearch(); + } + if (this._searchActive) { + this._switchTab(this._searchTab); + } else { + this._switchTab(this._activeTab); + } + if (!this._searchActive) { + 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)); + })); + this._searchEntry.entry.connect('activate', Lang.bind(this, function (se) { + if (this._searchTimeoutId > 0) { + Mainloop.source_remove(this._searchTimeoutId); + this._doSearch(); + } + this.searchResults.activateSelected(); + return true; + })); + }, + + addViewTab: function(title, pageActor) { + let viewTab = new ViewTab(title, pageActor); + this._tabs.push(viewTab); + this._tabBox.add(viewTab.title); + this._pageArea.add_actor(viewTab.page); + viewTab.page.hide(); + + viewTab.connect('activated', Lang.bind(this, + function(tab) { + this._switchTab(tab); + })); + }, + + _switchTab: function(tab) { + if (this._activeTab && this._activeTab.visible) { + if (this._activeTab == tab) + return; + this._activeTab.title.remove_style_pseudo_class('selected'); + this._activeTab.hide(); + } + + if (tab != this._searchTab) { + tab.title.add_style_pseudo_class('selected'); + this._activeTab = tab; + if (this._searchTab.visible) { + this._searchTab.hide(); + this._searchEntry.reset(); + } + } + + if (!tab.visible) + tab.show(); + }, + + _switchDefaultTab: function() { + if (this._tabs.length > 0) + this._switchTab(this._tabs[0]); + }, + + _getPreferredTabBarWidth: function(box, forHeight, alloc) { + let children = box.get_children(); + for (let i = 0; i < children.length; i++) { + let [childMin, childNat] = children[i].get_preferred_width(forHeight); + alloc.min_size += childMin; + alloc.natural_size += childNat; + } + }, + + _getPreferredTabBarHeight: function(box, forWidth, alloc) { + let children = box.get_children(); + for (let i = 0; i < children.length; i++) { + let [childMin, childNatural] = children[i].get_preferred_height(forWidth); + if (childMin > alloc.min_size) + alloc.min_size = childMin; + if (childNatural > alloc.natural_size) + alloc.natural_size = childNatural; + } + }, + + _allocateTabBar: function(container, box, flags) { + let allocWidth = box.x2 - box.x1; + let allocHeight = box.y2 - box.y1; + + let [searchMinWidth, searchNatWidth] = this._searchArea.get_preferred_width(-1); + let [barMinWidth, barNatWidth] = this._tabBox.get_preferred_width(-1); + let childBox = new Clutter.ActorBox(); + childBox.y1 = 0; + childBox.y2 = allocHeight; + if (this.actor.get_direction() == St.TextDirection.RTL) { + childBox.x1 = allocWidth - barNatWidth; + childBox.x2 = allocWidth; + } else { + childBox.x1 = 0; + childBox.x2 = barNatWidth; + } + this._tabBox.allocate(childBox, flags); + + if (this.actor.get_direction() == St.TextDirection.RTL) { + childBox.x1 = 0; + childBox.x2 = searchNatWidth; + } else { + childBox.x1 = allocWidth - searchNatWidth; + childBox.x2 = allocWidth; + } + this._searchArea.allocate(childBox, flags); + + Meta.later_add(Meta.LaterType.BEFORE_REDRAW, Lang.bind(this, + function() { + this.constrainY.offset = this.actor.y; + })); + }, + + _onKeyPress: function(stage, event) { + // If neither the stage nor the search entry have key focus, some + // "special" actor grabbed the focus (run dialog, looking glass); + // we don't want to interfere with that + let focus = stage.get_key_focus(); + if (focus != stage && focus != this._searchEntry.entry) + return false; + + let symbol = event.get_key_symbol(); + if (symbol == Clutter.Escape) { + Main.overview.hide(); + return true; + } else if (symbol == Clutter.Up) { + if (!this._searchActive) + return true; + this.searchResults.selectUp(false); + + return true; + } else if (symbol == Clutter.Down) { + if (!this._searchActive) + return true; + + this.searchResults.selectDown(false); + return true; + } + return false; + }, + + _doSearch: function () { + this._searchTimeoutId = 0; + let text = this._searchEntry.getText(); + this.searchResults.updateSearch(text); + + return false; + }, + + addSearchProvider: function(provider) { + this._searchSystem.registerProvider(provider); + this.searchResults.createProviderMeta(provider); + }, + + show: function() { + // Connect type-as-you-find listener + this._searchEntry.show(); + + if (this._itemDragBeginId == 0) + this._itemDragBeginId = Main.overview.connect('item-drag-begin', + Lang.bind(this, this._switchDefaultTab)); + if (this._overviewHidingId == 0) + this._overviewHidingId = Main.overview.connect('hiding', + Lang.bind(this, this._switchDefaultTab)); + if (this._keyPressId == 0) + this._keyPressId = global.stage.connect('key-press-event', + Lang.bind(this, this._onKeyPress)); + + this._switchDefaultTab(); + }, + + hide: function() { + // Disconnect type-as-you-find listener + this._searchEntry.hide(); + + if (this._keyPressId > 0) { + global.stage.disconnect(this._keyPressId); + this._keyPressId = 0; + } + + if (this._itemDragBeginId > 0) { + Main.overview.disconnect(this._itemDragBeginId); + this._itemDragBeginId = 0; + } + + if (this._overviewHidingId > 0) { + Main.overview.disconnect(this._overviewHidingId); + this._overviewHidingId = 0; + } + } +}; +Signals.addSignalMethods(ViewSelector.prototype); diff --git a/po/POTFILES.in b/po/POTFILES.in index a23f9bf51..b438a10e2 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -15,6 +15,7 @@ js/ui/popupMenu.js js/ui/runDialog.js js/ui/statusMenu.js js/ui/status/accessibility.js +js/ui/viewSelector.js js/ui/windowAttentionHandler.js js/ui/workspacesView.js src/gvc/gvc-mixer-control.c