[gnome-shell] overview: highlight windows on launcher hover



commit 76229a3601267830be0332f58f82ec16bc77a125
Author: StÃphane DÃmurget <stephane demurget free fr>
Date:   Sat Nov 3 22:18:42 2012 +0100

    overview: highlight windows on launcher hover
    
    When in the overview, if you move the mouse cursor over one of the
    application launchers in the dash, all the unrelated windows are dimmed
    both both in the window view and in the workspace view.
    
    It helps to easily understand whether or not there are already opened
    windows for this application, and where they are. It can also help in
    differentiating the windows in the overview (sometimes the thumbnails
    aren't precise enough to easily know which thumbnail belongs to which
    application).
    
    https://bugzilla.gnome.org/show_bug.cgi?id=657315

 js/ui/dash.js               |  115 ++++++++++++++++++++++++++++++++-----------
 js/ui/overview.js           |   11 ++++
 js/ui/workspace.js          |   36 +++++++++++++
 js/ui/workspaceThumbnail.js |   37 ++++++++++++++
 4 files changed, 170 insertions(+), 29 deletions(-)
---
diff --git a/js/ui/dash.js b/js/ui/dash.js
index d367aaa..d56913e 100644
--- a/js/ui/dash.js
+++ b/js/ui/dash.js
@@ -20,6 +20,7 @@ const DASH_ANIMATION_TIME = 0.2;
 const DASH_ITEM_LABEL_SHOW_TIME = 0.15;
 const DASH_ITEM_LABEL_HIDE_TIME = 0.1;
 const DASH_ITEM_HOVER_TIMEOUT = 300;
+const HOVERED_APP_NOTIFICATION_TIMEOUT = 20;
 
 function getAppFromSource(source) {
     if (source instanceof AppDisplay.AppWellIcon) {
@@ -57,6 +58,7 @@ const DashItemContainer = new Lang.Class({
         this._childScale = 1;
         this._childOpacity = 255;
         this.animatingOut = false;
+        this.appIcon = null;
     },
 
     _allocate: function(actor, box, flags) {
@@ -375,9 +377,12 @@ const Dash = new Lang.Class({
         this._dragPlaceholder = null;
         this._dragPlaceholderPos = -1;
         this._animatingPlaceholdersCount = 0;
-        this._showLabelTimeoutId = 0;
+        this._setHoverTimeoutId = 0;
         this._resetHoverTimeoutId = 0;
-        this._labelShowing = false;
+        this._userAlreadyHovering = false;
+        this._hoveredItem = null;
+        this._hoveredAppTimeoutId = 0;
+        this._primaryAction = true;
 
         this._container = new DashActor();
         this._box = new St.BoxLayout({ vertical: true,
@@ -387,7 +392,7 @@ const Dash = new Lang.Class({
 
         this._showAppsIcon = new ShowAppsIcon();
         this._showAppsIcon.icon.setIconSize(this.iconSize);
-        this._hookUpLabel(this._showAppsIcon);
+        this._hookUpItem(this._showAppsIcon);
 
         this.showAppsButton = this._showAppsIcon.toggleButton;
 
@@ -421,6 +426,19 @@ const Dash = new Lang.Class({
                               Lang.bind(this, this._onDragCancelled));
         Main.overview.connect('window-drag-end',
                               Lang.bind(this, this._onDragEnd));
+
+        Main.overview.connect('hiding',
+                               Lang.bind(this, function() {
+                                                   this._userAlreadyHovering = false;
+
+                                                   if (this._hoveredItem)
+                                                       this._hoveredItem.hideLabel();
+
+                                                   this._setHoveredItem(null, this._primaryAction);
+                                               }));
+
+        global.stage.connect('captured-event',
+                              Lang.bind(this, this._onCapturedEvent));
     },
 
     _onDragBegin: function() {
@@ -468,26 +486,14 @@ const Dash = new Lang.Class({
         return DND.DragMotionResult.CONTINUE;
     },
 
-    _appIdListToHash: function(apps) {
-        let ids = {};
-        for (let i = 0; i < apps.length; i++)
-            ids[apps[i].get_id()] = apps[i];
-        return ids;
-    },
-
     _queueRedisplay: function () {
         Main.queueDeferredWork(this._workId);
     },
 
-    _hookUpLabel: function(item) {
+    _hookUpItem: function(item) {
         item.child.connect('notify::hover', Lang.bind(this, function() {
             this._onHover(item);
         }));
-
-        Main.overview.connect('hiding', Lang.bind(this, function() {
-            this._labelShowing = false;
-            item.hideLabel();
-        }));
     },
 
     _createAppItem: function(app) {
@@ -508,6 +514,7 @@ const Dash = new Lang.Class({
                         }));
 
         let item = new DashItemContainer();
+        item.appIcon = appIcon;
         item.setChild(appIcon.actor);
 
         // Override default AppWellIcon label_actor, now the
@@ -516,7 +523,7 @@ const Dash = new Lang.Class({
         item.setLabelText(app.get_name());
 
         appIcon.icon.setIconSize(this.iconSize);
-        this._hookUpLabel(item);
+        this._hookUpItem(item);
 
         return item;
     },
@@ -525,9 +532,9 @@ const Dash = new Lang.Class({
         // When the menu closes, it calls sync_hover, which means
         // that the notify::hover handler does everything we need to.
         if (opened) {
-            if (this._showLabelTimeoutId > 0) {
-                Mainloop.source_remove(this._showLabelTimeoutId);
-                this._showLabelTimeoutId = 0;
+            if (this._setHoverTimeoutId > 0) {
+                Mainloop.source_remove(this._setHoverTimeoutId);
+                this._setHoverTimeoutId = 0;
             }
 
             item.hideLabel();
@@ -536,32 +543,82 @@ const Dash = new Lang.Class({
 
     _onHover: function (item) {
         if (item.child.get_hover()) {
-            if (this._showLabelTimeoutId == 0) {
-                let timeout = this._labelShowing ? 0 : DASH_ITEM_HOVER_TIMEOUT;
-                this._showLabelTimeoutId = Mainloop.timeout_add(timeout,
+            if (this._setHoverTimeoutId == 0) {
+                let timeout = this._userAlreadyHovering ? 0 : DASH_ITEM_HOVER_TIMEOUT;
+
+                this._setHoverTimeoutId = Mainloop.timeout_add(timeout,
                     Lang.bind(this, function() {
-                        this._labelShowing = true;
                         item.showLabel();
+                        this._setHoveredItem(item, this._primaryAction);
+                        this._userAlreadyHovering = true;
                         return false;
                     }));
+
                 if (this._resetHoverTimeoutId > 0) {
                     Mainloop.source_remove(this._resetHoverTimeoutId);
                     this._resetHoverTimeoutId = 0;
                 }
             }
         } else {
-            if (this._showLabelTimeoutId > 0)
-                Mainloop.source_remove(this._showLabelTimeoutId);
-            this._showLabelTimeoutId = 0;
+            if (this._setHoverTimeoutId > 0)
+                Mainloop.source_remove(this._setHoverTimeoutId);
+            this._setHoverTimeoutId = 0;
+
             item.hideLabel();
-            if (this._labelShowing) {
+            this._setHoveredItem(null, this._primaryAction);
+
+            if (this._userAlreadyHovering)
                 this._resetHoverTimeoutId = Mainloop.timeout_add(DASH_ITEM_HOVER_TIMEOUT,
                     Lang.bind(this, function() {
-                        this._labelShowing = false;
+                        this._userAlreadyHovering = false;
                         return false;
                     }));
             }
+    },
+
+    _setHoveredItem: function(item, primaryAction) {
+        if (this._hoveredItem == item && this._primaryAction == primaryAction)
+            return;
+
+        this._hoveredItem = item;
+        this._primaryAction = primaryAction;
+
+        let app = null;
+
+        if (item != null && item.appIcon != null)
+            app = getAppFromSource(item.appIcon);
+
+        // The leave and enter events will both be dispatched before we tick into the next
+        // frame, so the _setHoverItem(null) should have no immediate effect.
+        if (this._hoveredAppTimeoutId > 0)
+            Mainloop.source_remove(this._hoveredAppTimeoutId);
+
+        this._hoveredAppTimeoutId = Mainloop.timeout_add(HOVERED_APP_NOTIFICATION_TIMEOUT,
+                                                         Lang.bind(this, function() {
+                                                             this._hoveredAppTimeoutId = 0;
+                                                             this.emit('hovered-app-changed', app, primaryAction);
+                                                             return false;
+                                                         }));
+    },
+
+    _onCapturedEvent: function(actor, event) {
+        let keyPress = (event.type() == Clutter.EventType.KEY_PRESS);
+        let keyRelease = (event.type() == Clutter.EventType.KEY_RELEASE);
+
+        if (!keyPress && !keyRelease)
+            return false;
+
+        let key = event.get_key_symbol();
+
+        if (key == Clutter.KEY_Alt_L || key == Clutter.KEY_Alt_R) {
+            let primaryAction = !keyPress;
+
+            if (this._primaryAction != primaryAction) {
+                this._setHoveredItem(this._hoveredItem, primaryAction);
+            }
         }
+
+        return false;
     },
 
     _adjustIconSize: function() {
diff --git a/js/ui/overview.js b/js/ui/overview.js
index 658cb81..8369c02 100644
--- a/js/ui/overview.js
+++ b/js/ui/overview.js
@@ -176,6 +176,8 @@ const Overview = new Lang.Class({
         this._modal = false;            // have a modal grab
         this.animationInProgress = false;
         this._hideInProgress = false;
+        this.hoveredApp = null;
+        this.primaryAction = false;
 
         // During transitions, we raise this to the top to avoid having the overview
         // area be reactive; it causes too many issues such as double clicks on
@@ -232,6 +234,9 @@ const Overview = new Lang.Class({
         this._group.add_actor(this._searchEntry);
 
         this._dash = new Dash.Dash();
+        this._dash.connect('hovered-app-changed',
+                            Lang.bind(this, this._hoveredAppChanged));
+
         this._viewSelector = new ViewSelector.ViewSelector(this._searchEntry,
                                                            this._dash.showAppsButton);
         this._group.add_actor(this._viewSelector.actor);
@@ -556,6 +561,12 @@ const Overview = new Lang.Class({
         this._viewSelector.actor.set_size(viewWidth, viewHeight);
     },
 
+    _hoveredAppChanged: function(dash, app, primaryAction) {
+        this.hoveredApp = app;
+        this.primaryAction = primaryAction;
+        this.emit('hovered-app-changed', app, primaryAction);
+    },
+
     //// Public methods ////
 
     beginItemDrag: function(source) {
diff --git a/js/ui/workspace.js b/js/ui/workspace.js
index 3005c8d..ba24011 100644
--- a/js/ui/workspace.js
+++ b/js/ui/workspace.js
@@ -27,8 +27,11 @@ const WINDOW_CLONE_MAXIMUM_SCALE = 0.7;
 
 const LIGHTBOX_FADE_TIME = 0.1;
 const CLOSE_BUTTON_FADE_TIME = 0.1;
+const DIMMED_WINDOW_FADE_IN_TIME = 0.2;
+const DIMMED_WINDOW_FADE_OUT_TIME = 0.15;
 
 const DRAGGING_WINDOW_OPACITY = 100;
+const DIMMED_WINDOW_OPACITY = 100;
 
 const BUTTON_LAYOUT_SCHEMA = 'org.gnome.shell.overrides';
 const BUTTON_LAYOUT_KEY = 'button-layout';
@@ -192,6 +195,16 @@ const WindowClone = new Lang.Class({
         this.actor.destroy();
     },
 
+    setDimmed: function(dimmed, withAnimation) {
+        let opacity = dimmed ? DIMMED_WINDOW_OPACITY : 255;
+        let time = dimmed ? DIMMED_WINDOW_FADE_IN_TIME : DIMMED_WINDOW_FADE_OUT_TIME;
+
+        Tweener.addTween(this.actor,
+                         { opacity: opacity,
+                           time: withAnimation ? time : 0,
+                           transition: 'easeOutQuad' });
+    },
+
     zoomFromOverview: function() {
         if (this._zooming) {
             // If the user clicked on the zoomed window, or we are
@@ -1001,6 +1014,9 @@ const Workspace = new Lang.Class({
             }
         }
 
+        this._hoveredAppChangedId = Main.overview.connect('hovered-app-changed',
+                                                          Lang.bind(this, this._hoveredAppChanged));
+
         // Track window changes
         if (this.metaWorkspace) {
             this._windowAddedId = this.metaWorkspace.connect('window-added',
@@ -1346,6 +1362,8 @@ const Workspace = new Lang.Class({
 
         this._currentLayout = null;
         this.positionWindows(WindowPositionFlags.ANIMATE);
+
+        this._updateCloneDimmed(clone, Main.overview.hoveredApp, Main.overview.primaryAction, false);
     },
 
     _windowAdded : function(metaWorkspace, metaWin) {
@@ -1456,6 +1474,8 @@ const Workspace = new Lang.Class({
         }
         Tweener.removeTweens(actor);
 
+        Main.overview.disconnect(this._hoveredAppChangedId);
+
         if (this.metaWorkspace) {
             this.metaWorkspace.disconnect(this._windowAddedId);
             this.metaWorkspace.disconnect(this._windowRemovedId);
@@ -1659,6 +1679,22 @@ const Workspace = new Lang.Class({
         Main.activateWindow(clone.metaWindow, time, wsIndex);
     },
 
+    _hoveredAppChanged: function(overview, hoveredApp, primaryAction) {
+        for (let i = 0; i < this._windows.length; i++) {
+            this._updateCloneDimmed(this._windows[i], hoveredApp, primaryAction, true);
+        }
+    },
+
+    _updateCloneDimmed: function(clone, hoveredApp, primaryAction, withAnimation) {
+        let app = Shell.WindowTracker.get_default().get_window_app(clone.metaWindow);
+        let dimmed = (hoveredApp != null && app != hoveredApp);
+
+        if (primaryAction)
+            dimmed = dimmed && (hoveredApp.state != Shell.AppState.STOPPED);
+
+        clone.setDimmed(dimmed, withAnimation);
+    },
+
     // Draggable target interface
     handleDragOver : function(source, actor, x, y, time) {
         if (source.realWindow && !this._isMyWindow(source.realWindow))
diff --git a/js/ui/workspaceThumbnail.js b/js/ui/workspaceThumbnail.js
index c6b5bdb..1b4c5b8 100644
--- a/js/ui/workspaceThumbnail.js
+++ b/js/ui/workspaceThumbnail.js
@@ -20,6 +20,9 @@ let MAX_THUMBNAIL_SCALE = 1/8.;
 const RESCALE_ANIMATION_TIME = 0.2;
 const SLIDE_ANIMATION_TIME = 0.2;
 
+// the window opacity is very low as windows can be layered, contrary to the view selector
+const DIMMED_WINDOW_OPACITY = 50;
+
 // When we create workspaces by dragging, we add a "cut" into the top and
 // bottom of each workspace so that the user doesn't have to hit the
 // placeholder exactly.
@@ -58,6 +61,16 @@ const WindowClone = new Lang.Class({
         this.inDrag = false;
     },
 
+    setDimmed: function(dimmed, withAnimation) {
+        let opacity = dimmed ? DIMMED_WINDOW_OPACITY : 255;
+        let time = dimmed ? Workspace.DIMMED_WINDOW_FADE_IN_TIME : Workspace.DIMMED_WINDOW_FADE_OUT_TIME;
+
+        Tweener.addTween(this.actor,
+                         { opacity: opacity,
+                           time: withAnimation ? time : 0,
+                           transition: 'easeOutQuad' });
+    },
+
     setStackAbove: function (actor) {
         this._stackAbove = actor;
         if (this._stackAbove == null)
@@ -192,6 +205,9 @@ const WorkspaceThumbnail = new Lang.Class({
             }
         }
 
+        this._hoveredAppChangedId = Main.overview.connect('hovered-app-changed',
+                                                          Lang.bind(this, this._hoveredAppChanged));
+
         // Track window changes
         this._windowAddedId = this.metaWorkspace.connect('window-added',
                                                           Lang.bind(this, this._windowAdded));
@@ -312,6 +328,8 @@ const WorkspaceThumbnail = new Lang.Class({
             return;
 
         let clone = this._addWindowClone(win);
+
+        this._updateCloneDimmed(clone, Main.overview.hoveredApp, Main.overview.primaryAction, false);
     },
 
     _windowAdded : function(metaWorkspace, metaWin) {
@@ -368,6 +386,8 @@ const WorkspaceThumbnail = new Lang.Class({
     },
 
     _onDestroy: function(actor) {
+        Main.overview.disconnect(this._hoveredAppChangedId);
+
         this.workspaceRemoved();
 
         this._windows = [];
@@ -435,6 +455,23 @@ const WorkspaceThumbnail = new Lang.Class({
             this.metaWorkspace.activate(time);
     },
 
+    _hoveredAppChanged: function(overview, hoveredApp, primaryAction) {
+        for (let i = 0; i < this._windows.length; i++) {
+            this._updateCloneDimmed(this._windows[i], hoveredApp, primaryAction, true);
+        }
+    },
+
+    _updateCloneDimmed: function(clone, hoveredApp, primaryAction, withAnimation) {
+        let app = Shell.WindowTracker.get_default().get_window_app(clone.metaWindow);
+        let dimmed = (hoveredApp != null && app != hoveredApp);
+
+        if (primaryAction)
+            dimmed = dimmed && (hoveredApp.state != Shell.AppState.STOPPED);
+
+        clone.setDimmed(dimmed, withAnimation);
+    },
+
+
     // Draggable target interface used only by ThumbnailsBox
     handleDragOverInternal : function(source, time) {
         if (source == Main.xdndHandler) {



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