diff --git a/js/ui/dash.js b/js/ui/dash.js index d367aaac9..d56913ebd 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 658cb81a6..8369c023a 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 3005c8dd7..ba240111f 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 c6b5bdbf9..1b4c5b823 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) {