[gnome-shell/overview-relayout: 8/20] Use the old dash code to implement the view selector



commit 17cc2539a8ac2454048b379f55d416246339916f
Author: Florian Müllner <fmuellner gnome org>
Date:   Fri Nov 12 18:45:29 2010 +0100

    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 |   85 ++++-
 js/Makefile.am             |    1 +
 js/ui/dash.js              |  903 +-------------------------------------------
 js/ui/viewSelector.js      |  810 +++++++++++++++++++++++++++++++++++++++
 po/POTFILES.in             |    1 +
 5 files changed, 877 insertions(+), 923 deletions(-)
---
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index 7c70f69..4fdee54 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -375,35 +375,65 @@ StTooltip {
     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;
 }
 
+.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;
+}
+
 .dash-section {
     spacing: 8px;
 }
@@ -447,18 +477,33 @@ StTooltip {
     padding: 30px 10px 10px 20px;
 }
 
-#dashAppSearchResults {
-    padding: 8px 0px;
+#SearchResults {
+    padding: 20px 10px 10px 10px;
+}
+
+#SearchResultsContent {
+    padding: 0 10px;
+    spacing: 8px;
 }
 
 .dash-search-statustext,
 .dash-search-section-header {
-    padding: 4px 0px;
+    padding: 4px 12px;
     spacing: 4px;
+    color: #6f6f6f;
+}
+
+.dash-search-section {
+    background-color: rgba(128, 128, 128, .1);
+    border: 1px solid rgba(50, 50, 50, .4);
+    border-radius: 10px;
 }
 
 .dash-search-section-results {
     color: #ffffff;
+    border-radius: 10px;
+    border: 1px solid rgba(50, 50, 50, .4);
+    padding: 6px;
 }
 
 .dash-search-section-list-results {
@@ -466,14 +511,12 @@ StTooltip {
 }
 
 .dash-search-result-content {
-    padding: 3px;
+    padding: 4px;
 }
 
 .dash-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 {
diff --git a/js/Makefile.am b/js/Makefile.am
index eca1726..fcca708 100644
--- a/js/Makefile.am
+++ b/js/Makefile.am
@@ -46,6 +46,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 7d61838..cdeb770 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;
@@ -11,911 +10,11 @@ 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 Tweener = imports.ui.tweener;
 const Workspace = imports.ui.workspace;
 
-// 25 search results (per result type) should be enough for everyone
-const MAX_RENDERED_SEARCH_RESULTS = 25;
-
-/*
- * 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 SearchResult(provider, metaInfo, terms) {
-    this._init(provider, metaInfo, terms);
-}
-
-SearchResult.prototype = {
-    _init: function(provider, metaInfo, terms) {
-        this.provider = provider;
-        this.metaInfo = metaInfo;
-        this.actor = new St.Clickable({ style_class: 'dash-search-result',
-                                        reactive: true,
-                                        x_align: St.Align.START,
-                                        x_fill: true,
-                                        y_fill: true });
-        this.actor._delegate = this;
-
-        let content = provider.createResultActor(metaInfo, terms);
-        if (content == null) {
-            content = new St.BoxLayout({ style_class: 'dash-search-result-content' });
-            let title = new St.Label({ text: this.metaInfo['name'] });
-            let icon = this.metaInfo['icon'];
-            content.add(icon, { y_fill: false });
-            content.add(title, { expand: true, y_fill: false });
-        }
-        this._content = content;
-        this.actor.set_child(content);
-
-        this.actor.connect('clicked', Lang.bind(this, this._onResultClicked));
-
-        let draggable = DND.makeDraggable(this.actor);
-        draggable.connect('drag-begin',
-                          Lang.bind(this, function() {
-                              Main.overview.beginItemDrag(this);
-                          }));
-        draggable.connect('drag-end',
-                          Lang.bind(this, function() {
-                              Main.overview.endItemDrag(this);
-                          }));
-    },
-
-    setSelected: function(selected) {
-        if (selected)
-            this._content.add_style_pseudo_class('selected');
-        else
-            this._content.remove_style_pseudo_class('selected');
-    },
-
-    activate: function() {
-        this.provider.activateResult(this.metaInfo.id);
-        Main.overview.toggle();
-    },
-
-    _onResultClicked: function(actor, event) {
-        this.activate();
-    },
-
-    getDragActorSource: function() {
-        return this.metaInfo['icon'];
-    },
-
-    getDragActor: function(stageX, stageY) {
-        return new Clutter.Clone({ source: this.metaInfo['icon'] });
-    },
-
-    shellWorkspaceLaunch: function() {
-        if (this.provider.dragActivateResult)
-            this.provider.dragActivateResult(this.metaInfo.id);
-        else
-            this.provider.activateResult(this.metaInfo.id);
-    }
-};
-
-function OverflowSearchResults(provider) {
-    this._init(provider);
-}
-
-OverflowSearchResults.prototype = {
-    __proto__: Search.SearchResultDisplay.prototype,
-
-    _init: function(provider) {
-        Search.SearchResultDisplay.prototype._init.call(this, provider);
-        this.actor = new St.OverflowBox({ style_class: 'dash-search-section-list-results' });
-    },
-
-    getVisibleResultCount: function() {
-        return this.actor.get_n_visible();
-    },
-
-    renderResults: function(results, terms) {
-        for (let i = 0; i < results.length && i < MAX_RENDERED_SEARCH_RESULTS; i++) {
-            let result = results[i];
-            let meta = this.provider.getResultMeta(result);
-            let display = new SearchResult(this.provider, meta, terms);
-            this.actor.add_actor(display.actor);
-        }
-    },
-
-    selectIndex: function(index) {
-        let nVisible = this.actor.get_n_visible();
-        let children = this.actor.get_children();
-        if (this.selectionIndex >= 0) {
-            let prevActor = children[this.selectionIndex];
-            prevActor._delegate.setSelected(false);
-        }
-        this.selectionIndex = -1;
-        if (index >= nVisible)
-            return false;
-        else if (index < 0)
-            return false;
-        let targetActor = children[index];
-        targetActor._delegate.setSelected(true);
-        this.selectionIndex = index;
-        return true;
-    },
-
-    activateSelected: function() {
-        let children = this.actor.get_children();
-        let targetActor = children[this.selectionIndex];
-        targetActor._delegate.activate();
-    }
-};
-
-function SearchResults(searchSystem) {
-    this._init(searchSystem);
-}
-
-SearchResults.prototype = {
-    _init: function(searchSystem) {
-        this._searchSystem = searchSystem;
-
-        this.actor = new St.BoxLayout({ name: 'dashSearchResults',
-                                        vertical: true });
-        this._statusText = new St.Label({ style_class: 'dash-search-statustext' });
-        this.actor.add(this._statusText);
-        this._selectedProvider = -1;
-        this._providers = this._searchSystem.getProviders();
-        this._providerMeta = [];
-        for (let i = 0; i < this._providers.length; i++)
-            this.createProviderMeta(this._providers[i]);
-    },
-
-    createProviderMeta: function(provider) {
-        let providerBox = new St.BoxLayout({ style_class: 'dash-search-section',
-                                             vertical: true });
-        let titleButton = new St.Button({ style_class: 'dash-search-section-header',
-                                          reactive: true,
-                                          x_fill: true,
-                                          y_fill: true });
-        titleButton.connect('clicked', Lang.bind(this, function () { this._onHeaderClicked(provider); }));
-        providerBox.add(titleButton);
-        let titleBox = new St.BoxLayout();
-        titleButton.set_child(titleBox);
-        let title = new St.Label({ text: provider.title });
-        let count = new St.Label();
-        titleBox.add(title, { expand: true });
-        titleBox.add(count);
-
-        let resultDisplayBin = new St.Bin({ style_class: 'dash-search-section-results',
-                                            x_fill: true,
-                                            y_fill: true });
-        providerBox.add(resultDisplayBin, { expand: true });
-        let resultDisplay = provider.createResultContainerActor();
-        if (resultDisplay == null) {
-            resultDisplay = new OverflowSearchResults(provider);
-        }
-        resultDisplayBin.set_child(resultDisplay.actor);
-
-        this._providerMeta.push({ actor: providerBox,
-                                  resultDisplay: resultDisplay,
-                                  count: count });
-        this.actor.add(providerBox);
-    },
-
-    _clearDisplay: function() {
-        this._selectedProvider = -1;
-        this._visibleResultsCount = 0;
-        for (let i = 0; i < this._providerMeta.length; i++) {
-            let meta = this._providerMeta[i];
-            meta.resultDisplay.clear();
-            meta.actor.hide();
-        }
-    },
-
-    reset: function() {
-        this._searchSystem.reset();
-        this._statusText.hide();
-        this._clearDisplay();
-    },
-
-    startingSearch: function() {
-        this.reset();
-        this._statusText.set_text(_("Searching..."));
-        this._statusText.show();
-    },
-
-    _metaForProvider: function(provider) {
-        return this._providerMeta[this._providers.indexOf(provider)];
-    },
-
-    updateSearch: function (searchString) {
-        let results = this._searchSystem.updateSearch(searchString);
-
-        this._clearDisplay();
-
-        if (results.length == 0) {
-            this._statusText.set_text(_("No matching results."));
-            this._statusText.show();
-            return true;
-        } else {
-            this._statusText.hide();
-        }
-
-        let terms = this._searchSystem.getTerms();
-
-        for (let i = 0; i < results.length; i++) {
-            let [provider, providerResults] = results[i];
-            let meta = this._metaForProvider(provider);
-            meta.actor.show();
-            meta.resultDisplay.renderResults(providerResults, terms);
-            meta.count.set_text('' + providerResults.length);
-        }
-
-        this.selectDown(false);
-
-        return true;
-    },
-
-    _onHeaderClicked: function(provider) {
-        provider.expandSearch(this._searchSystem.getTerms());
-    },
-
-    _modifyActorSelection: function(resultDisplay, up) {
-        let success;
-        let index = resultDisplay.getSelectionIndex();
-        if (up && index == -1)
-            index = resultDisplay.getVisibleResultCount() - 1;
-        else if (up)
-            index = index - 1;
-        else
-            index = index + 1;
-        return resultDisplay.selectIndex(index);
-    },
-
-    selectUp: function(recursing) {
-        for (let i = this._selectedProvider; i >= 0; i--) {
-            let meta = this._providerMeta[i];
-            if (!meta.actor.visible)
-                continue;
-            let success = this._modifyActorSelection(meta.resultDisplay, true);
-            if (success) {
-                this._selectedProvider = i;
-                return;
-            }
-        }
-        if (this._providerMeta.length > 0 && !recursing) {
-            this._selectedProvider = this._providerMeta.length - 1;
-            this.selectUp(true);
-        }
-    },
-
-    selectDown: function(recursing) {
-        let current = this._selectedProvider;
-        if (current == -1)
-            current = 0;
-        for (let i = current; i < this._providerMeta.length; i++) {
-            let meta = this._providerMeta[i];
-            if (!meta.actor.visible)
-                continue;
-            let success = this._modifyActorSelection(meta.resultDisplay, false);
-            if (success) {
-                this._selectedProvider = i;
-                return;
-            }
-        }
-        if (this._providerMeta.length > 0 && !recursing) {
-            this._selectedProvider = 0;
-            this.selectDown(true);
-        }
-    },
-
-    activateSelected: function() {
-        let current = this._selectedProvider;
-        if (current < 0)
-            return;
-        let meta = this._providerMeta[current];
-        let resultDisplay = meta.resultDisplay;
-        resultDisplay.activateSelected();
-        Main.overview.hide();
-    }
-};
-
-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 SearchSectionHeader(title, onClick) {
-    this._init(title, onClick);
-}
-
-SearchSectionHeader.prototype = {
-    _init : function(title, onClick) {
-        this.actor = new St.Button({ style_class: 'dash-search-section-header',
-                                      x_fill: true,
-                                      y_fill: true });
-        let box = new St.BoxLayout();
-        this.actor.set_child(box);
-        let titleText = new St.Label({ style_class: 'dash-search-section-title',
-                                        text: title });
-        this.countText = new St.Label({ style_class: 'dash-search-section-count' });
-
-        box.add(titleText);
-        box.add(this.countText, { expand: true, x_fill: false, x_align: St.Align.END });
-
-        this.actor.connect('clicked', onClick);
-    }
-};
-
-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 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 0000000..74fc368
--- /dev/null
+++ b/js/ui/viewSelector.js
@@ -0,0 +1,810 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+const Clutter = imports.gi.Clutter;
+const Gtk = imports.gi.Gtk;
+const Mainloop = imports.mainloop;
+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 DND = imports.ui.dnd;
+const Main = imports.ui.main;
+const Overview = imports.ui.overview;
+const Search = imports.ui.search;
+const Tweener = imports.ui.tweener;
+
+// 25 search results (per result type) should be enough for everyone
+const MAX_RENDERED_SEARCH_RESULTS = 25;
+
+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 = '';
+        // make sure the entry gets a key-focus-out signal and sets the hint
+        global.stage.set_key_focus(this.entry);
+        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 SearchResult(provider, metaInfo, terms) {
+    this._init(provider, metaInfo, terms);
+}
+
+SearchResult.prototype = {
+    _init: function(provider, metaInfo, terms) {
+        this.provider = provider;
+        this.metaInfo = metaInfo;
+        this.actor = new St.Clickable({ style_class: 'dash-search-result',
+                                        reactive: true,
+                                        x_align: St.Align.START,
+                                        x_fill: true,
+                                        y_fill: true });
+        this.actor._delegate = this;
+
+        let content = provider.createResultActor(metaInfo, terms);
+        if (content == null) {
+            content = new St.BoxLayout({ style_class: 'dash-search-result-content' });
+            let title = new St.Label({ text: this.metaInfo['name'] });
+            let icon = this.metaInfo['icon'];
+            content.add(icon, { y_fill: false });
+            content.add(title, { expand: true, y_fill: false });
+        }
+        this._content = content;
+        this.actor.set_child(content);
+
+        this.actor.connect('clicked', Lang.bind(this, this._onResultClicked));
+
+        let draggable = DND.makeDraggable(this.actor);
+        draggable.connect('drag-begin',
+                          Lang.bind(this, function() {
+                              Main.overview.beginItemDrag(this);
+                          }));
+        draggable.connect('drag-end',
+                          Lang.bind(this, function() {
+                              Main.overview.endItemDrag(this);
+                          }));
+    },
+
+    setSelected: function(selected) {
+        if (selected)
+            this._content.add_style_pseudo_class('selected');
+        else
+            this._content.remove_style_pseudo_class('selected');
+    },
+
+    activate: function() {
+        this.provider.activateResult(this.metaInfo.id);
+        Main.overview.toggle();
+    },
+
+    _onResultClicked: function(actor, event) {
+        this.activate();
+    },
+
+    getDragActorSource: function() {
+        return this.metaInfo['icon'];
+    },
+
+    getDragActor: function(stageX, stageY) {
+        return new Clutter.Clone({ source: this.metaInfo['icon'] });
+    },
+
+    shellWorkspaceLaunch: function() {
+        if (this.provider.dragActivateResult)
+            this.provider.dragActivateResult(this.metaInfo.id);
+        else
+            this.provider.activateResult(this.metaInfo.id);
+    }
+};
+
+function OverflowSearchResults(provider) {
+    this._init(provider);
+}
+
+OverflowSearchResults.prototype = {
+    __proto__: Search.SearchResultDisplay.prototype,
+
+    _init: function(provider) {
+        Search.SearchResultDisplay.prototype._init.call(this, provider);
+        this.actor = new St.OverflowBox({ style_class: 'dash-search-section-list-results' });
+    },
+
+    getVisibleResultCount: function() {
+        return this.actor.get_n_visible();
+    },
+
+    renderResults: function(results, terms) {
+        for (let i = 0; i < results.length && i < MAX_RENDERED_SEARCH_RESULTS; i++) {
+            let result = results[i];
+            let meta = this.provider.getResultMeta(result);
+            let display = new SearchResult(this.provider, meta, terms);
+            this.actor.add_actor(display.actor);
+        }
+    },
+
+    selectIndex: function(index) {
+        let nVisible = this.actor.get_n_visible();
+        let children = this.actor.get_children();
+        if (this.selectionIndex >= 0) {
+            let prevActor = children[this.selectionIndex];
+            prevActor._delegate.setSelected(false);
+        }
+        this.selectionIndex = -1;
+        if (index >= nVisible)
+            return false;
+        else if (index < 0)
+            return false;
+        let targetActor = children[index];
+        targetActor._delegate.setSelected(true);
+        this.selectionIndex = index;
+        return true;
+    },
+
+    activateSelected: function() {
+        let children = this.actor.get_children();
+        let targetActor = children[this.selectionIndex];
+        targetActor._delegate.activate();
+    }
+};
+
+function SearchResults(searchSystem) {
+    this._init(searchSystem);
+}
+
+SearchResults.prototype = {
+    _init: function(searchSystem) {
+        this._searchSystem = searchSystem;
+
+        this.actor = new St.Bin({ name: 'SearchResults',
+                                  y_align: St.Align.START,
+                                  x_align: St.Align.START,
+                                  x_fill: true });
+        this._content = new St.BoxLayout({ name: 'SearchResultsContent',
+                                           vertical: true });
+
+        let scrollView = new St.ScrollView({ x_fill: true,
+                                             y_fill: false,
+                                             vshadows: true });
+        scrollView.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
+        scrollView.add_actor(this._content);
+
+        this.actor.set_child(scrollView);
+
+        this._statusText = new St.Label({ style_class: 'dash-search-statustext' });
+        this._content.add(this._statusText);
+        this._selectedProvider = -1;
+        this._providers = this._searchSystem.getProviders();
+        this._providerMeta = [];
+        for (let i = 0; i < this._providers.length; i++)
+            this.createProviderMeta(this._providers[i]);
+    },
+
+    createProviderMeta: function(provider) {
+        let providerBox = new St.BoxLayout({ style_class: 'dash-search-section',
+                                             vertical: true });
+        let titleButton = new St.Button({ style_class: 'dash-search-section-header',
+                                          reactive: true,
+                                          x_fill: true,
+                                          y_fill: true });
+        titleButton.connect('clicked', Lang.bind(this, function () { this._onHeaderClicked(provider); }));
+        providerBox.add(titleButton);
+        let titleBox = new St.BoxLayout();
+        titleButton.set_child(titleBox);
+        let title = new St.Label({ text: provider.title });
+        let count = new St.Label();
+        titleBox.add(title, { expand: true });
+        titleBox.add(count);
+
+        let resultDisplayBin = new St.Bin({ style_class: 'dash-search-section-results',
+                                            x_fill: true,
+                                            y_fill: true });
+        providerBox.add(resultDisplayBin, { expand: true });
+        let resultDisplay = provider.createResultContainerActor();
+        if (resultDisplay == null) {
+            resultDisplay = new OverflowSearchResults(provider);
+        }
+        resultDisplayBin.set_child(resultDisplay.actor);
+
+        this._providerMeta.push({ actor: providerBox,
+                                  resultDisplay: resultDisplay,
+                                  count: count });
+        this._content.add(providerBox);
+    },
+
+    _clearDisplay: function() {
+        this._selectedProvider = -1;
+        this._visibleResultsCount = 0;
+        for (let i = 0; i < this._providerMeta.length; i++) {
+            let meta = this._providerMeta[i];
+            meta.resultDisplay.clear();
+            meta.actor.hide();
+        }
+    },
+
+    reset: function() {
+        this._searchSystem.reset();
+        this._statusText.hide();
+        this._clearDisplay();
+    },
+
+    startingSearch: function() {
+        this.reset();
+        this._statusText.set_text(_("Searching..."));
+        this._statusText.show();
+    },
+
+    _metaForProvider: function(provider) {
+        return this._providerMeta[this._providers.indexOf(provider)];
+    },
+
+    updateSearch: function (searchString) {
+        let results = this._searchSystem.updateSearch(searchString);
+
+        this._clearDisplay();
+
+        if (results.length == 0) {
+            this._statusText.set_text(_("No matching results."));
+            this._statusText.show();
+            return true;
+        } else {
+            this._statusText.hide();
+        }
+
+        let terms = this._searchSystem.getTerms();
+
+        for (let i = 0; i < results.length; i++) {
+            let [provider, providerResults] = results[i];
+            let meta = this._metaForProvider(provider);
+            meta.actor.show();
+            meta.resultDisplay.renderResults(providerResults, terms);
+            meta.count.set_text('' + providerResults.length);
+        }
+
+        this.selectDown(false);
+
+        return true;
+    },
+
+    _onHeaderClicked: function(provider) {
+        provider.expandSearch(this._searchSystem.getTerms());
+    },
+
+    _modifyActorSelection: function(resultDisplay, up) {
+        let success;
+        let index = resultDisplay.getSelectionIndex();
+        if (up && index == -1)
+            index = resultDisplay.getVisibleResultCount() - 1;
+        else if (up)
+            index = index - 1;
+        else
+            index = index + 1;
+        return resultDisplay.selectIndex(index);
+    },
+
+    selectUp: function(recursing) {
+        for (let i = this._selectedProvider; i >= 0; i--) {
+            let meta = this._providerMeta[i];
+            if (!meta.actor.visible)
+                continue;
+            let success = this._modifyActorSelection(meta.resultDisplay, true);
+            if (success) {
+                this._selectedProvider = i;
+                return;
+            }
+        }
+        if (this._providerMeta.length > 0 && !recursing) {
+            this._selectedProvider = this._providerMeta.length - 1;
+            this.selectUp(true);
+        }
+    },
+
+    selectDown: function(recursing) {
+        let current = this._selectedProvider;
+        if (current == -1)
+            current = 0;
+        for (let i = current; i < this._providerMeta.length; i++) {
+            let meta = this._providerMeta[i];
+            if (!meta.actor.visible)
+                continue;
+            let success = this._modifyActorSelection(meta.resultDisplay, false);
+            if (success) {
+                this._selectedProvider = i;
+                return;
+            }
+        }
+        if (this._providerMeta.length > 0 && !recursing) {
+            this._selectedProvider = 0;
+            this.selectDown(true);
+        }
+    },
+
+    activateSelected: function() {
+        let current = this._selectedProvider;
+        if (current < 0)
+            return;
+        let meta = this._providerMeta[current];
+        let resultDisplay = meta.resultDisplay;
+        resultDisplay.activateSelected();
+        Main.overview.hide();
+    }
+};
+
+
+function ViewTab(label, pageActor) {
+    this._init(label, pageActor);
+}
+
+ViewTab.prototype = {
+    _init: function(label, pageActor) {
+        this.title = new St.Button({ label: label,
+                                     style_class: 'view-tab-title' });
+        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.title.connect('clicked', Lang.bind(this, this._activate));
+
+        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(ViewTab.prototype);
+
+
+function SearchTab(entryActor, pageActor) {
+    this._init(entryActor, pageActor);
+}
+
+SearchTab.prototype = {
+    __proto__: ViewTab.prototype,
+
+    _init: function(entryActor, pageActor) {
+        ViewTab.prototype._init.call(this, 'search', pageActor);
+        this.title.destroy();
+        this.title = entryActor;
+    }
+};
+
+function ViewSelector() {
+    this._init();
+}
+
+ViewSelector.prototype = {
+    _init : function() {
+        this.actor = new St.BoxLayout({ name: 'ViewSelector',
+                                        vertical: true });
+
+        this._boxContainer = new Shell.GenericContainer();
+        this.actor.add(this._boxContainer);
+
+        this._boxContainer.connect('get-preferred-width',
+                                   Lang.bind(this, this._getPreferredWidth));
+        this._boxContainer.connect('get-preferred-height',
+                                   Lang.bind(this, this._getPreferredHeight));
+        this._boxContainer.connect('allocate',
+                                   Lang.bind(this, this._allocate));
+
+        this._tabBar = new St.BoxLayout({ name: 'ViewSelectorTabBar' });
+        this._boxContainer.add_actor(this._tabBar);
+
+        // The searchArea just holds the entry
+        this.searchArea = new St.Bin({ name: 'SearchArea' });
+        this._boxContainer.add_actor(this.searchArea);
+
+        this._contentArea = new Shell.GenericContainer();
+        this._contentArea.connect('allocate', Lang.bind(this,
+            function(container, box, flags) {
+                let children = container.get_children();
+                let childBox = new Clutter.ActorBox();
+                childBox.x1 = childBox.y1 = 0;
+                childBox.x2 = box.x2 - box.x1;
+                childBox.y2 = box.y2 - box.y1;
+                for (let i = 0; i < children.length; i++)
+                    children[i].allocate(childBox, flags);
+            }));
+        this.actor.add(this._contentArea, { x_fill: true,
+                                            y_fill: true,
+                                            expand: true });
+
+        this._tabs = [];
+        this._activeTab = null;
+
+        let workId = Main.initializeDeferredWork(this.actor,
+            Lang.bind(this, function() {
+                Main.overview.connect('item-drag-begin',
+                                      Lang.bind(this, this._switchDefaultTab));
+                Main.overview.connect('hiding',
+                                      Lang.bind(this, this._switchDefaultTab));
+            }));
+
+        this.constraintY = new Clutter.BindConstraint({ coordinate: Clutter.BindCoordinate.Y });
+        this.constraintHeight = new Clutter.BindConstraint({ coordinate: Clutter.BindCoordinate.HEIGHT });
+        this._constraintOffsetId = Main.initializeDeferredWork(this.actor,
+            Lang.bind(this, function() {
+                this.constraintY.offset = this._constraintOffset;
+            }));
+        this._constraintSourceId = Main.initializeDeferredWork(this.actor,
+            Lang.bind(this, function() {
+                if (this._activeTab) {
+                    this.constraintY.set_source(this._activeTab.page);
+                    this.constraintHeight.set_source(this._activeTab.page);
+                }
+        }));
+
+        /***** 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 SearchResults(this._searchSystem);
+        this._searchTab = new SearchTab(this.searchArea,
+                                        this.searchResults.actor);
+        this._contentArea.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._tabBar.add(viewTab.title);
+        this._contentArea.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();
+        }
+
+        Main.queueDeferredWork(this._constraintSourceId);
+
+        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]);
+    },
+
+    _getPreferredWidth: 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;
+        }
+    },
+
+    _getPreferredHeight: 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;
+        }
+    },
+
+    _allocate: 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._tabBar.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._tabBar.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);
+
+        let spacing = this.actor.get_theme_node().get_length('spacing');
+        this._constraintOffset = this.actor.y + box.y2 + spacing;
+        Main.queueDeferredWork(this._constraintOffsetId);
+    },
+
+    _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) {
+        //Add a new search provider to the dash.
+
+        this._searchSystem.registerProvider(provider);
+        this.searchResults.createProviderMeta(provider);
+    },
+
+    show: function() {
+        this._switchDefaultTab();
+
+        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._keyPressId > 0) {
+            global.stage.disconnect(this._keyPressId);
+            this._keyPressId = 0;
+        }
+    }
+};
+Signals.addSignalMethods(ViewSelector.prototype);
diff --git a/po/POTFILES.in b/po/POTFILES.in
index a23f9bf..b438a10 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



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]