diff --git a/Settings.ui b/Settings.ui index 2b164a8b8..a78c92fe5 100644 --- a/Settings.ui +++ b/Settings.ui @@ -1719,6 +1719,79 @@ 3 + + + True + False + 0 + in + + + True + False + none + + + True + True + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + False + True + 0 + Show window previews on mouse hover + + + 0 + 0 + + + + + True + False + 6 + + + True + True + end + center + + + + + 1 + 0 + 2 + + + + + + + + + + + + + + False + True + 4 + + 2 diff --git a/appIcons.js b/appIcons.js index 53c633ec6..75177aa5a 100644 --- a/appIcons.js +++ b/appIcons.js @@ -61,6 +61,9 @@ let recentlyClickedAppWindows = null; let recentlyClickedAppIndex = 0; let recentlyClickedAppMonitor = -1; +// Icon list might change over time, so we keep a global variable +let appIconsHoverList = null; + /** * Extend AppIcon * @@ -315,8 +318,7 @@ var MyAppIcon = new Lang.Class({ else { // Setting the max-height is s useful if part of the menu is // scrollable so the minimum height is smaller than the natural height. - let monitor_index = Main.layoutManager.findIndexForActor(this.actor); - let workArea = Main.layoutManager.getWorkAreaForMonitor(monitor_index); + let workArea = Main.layoutManager.getWorkAreaForMonitor(this.monitorIndex); let position = Utils.getPosition(this._dtdSettings); this._isHorizontal = ( position == St.Side.TOP || position == St.Side.BOTTOM); @@ -324,8 +326,17 @@ var MyAppIcon = new Lang.Class({ let additional_margin = this._isHorizontal && !this._dtdSettings.get_boolean('dock-fixed') ? Main.overview._dash.actor.height : 0; let verticalMargins = this._menu.actor.margin_top + this._menu.actor.margin_bottom; // Also set a max width to the menu, so long labels (long windows title) get truncated + let monitor = Main.layoutManager.monitors[this.monitorIndex]; + let max_width = Math.round(monitor.width / 3); this._menu.actor.style = ('max-height: ' + Math.round(workArea.height - additional_margin - verticalMargins) + 'px;' + - 'max-width: 400px'); + 'max-width: ' + max_width + 'px'); + } + + // Close the window previews + if (this._previewMenu) { + this._previewMenu.cancelOpen(); + if (this._previewMenu.isOpen) + this._previewMenu.close(~0); } })); let id = Main.overview.connect('hiding', Lang.bind(this, function() { @@ -397,6 +408,8 @@ var MyAppIcon = new Lang.Class({ // This variable keeps track of this let shouldHideOverview = true; + let shouldClosePreview = true; + // We customize the action only when the application is already running if (appIsRunning) { switch (buttonAction) { @@ -470,8 +483,11 @@ var MyAppIcon = new Lang.Class({ if (windows.length == 1 && !modifiers && button == 1) { let w = windows[0]; Main.activateWindow(w); - } else + } + else { this._windowPreviews(); + shouldClosePreview = false; + } } else { this.app.activate(); @@ -515,6 +531,9 @@ var MyAppIcon = new Lang.Class({ this.launchNewWindow(); } + if (this._previewMenu && this._previewMenu.isOpen && shouldClosePreview) + this._previewMenu.hoverClose(~0); + // Hide overview except when action mode requires it if(shouldHideOverview) { Main.overview.hide(); @@ -526,35 +545,115 @@ var MyAppIcon = new Lang.Class({ (!this._previewMenu || !this._previewMenu.isOpen); }, - _windowPreviews: function() { - if (!this._previewMenu) { - this._previewMenuManager = new PopupMenu.PopupMenuManager(this); + _createPreviewMenus: function() { + this._previewMenuManager = new PopupMenu.PopupMenuManager(this); - this._previewMenu = new WindowPreview.WindowPreviewMenu(this, this._dtdSettings); + this._previewMenu = new WindowPreview.WindowPreviewMenu(this, this._dtdSettings); - this._previewMenuManager.addMenu(this._previewMenu); + this._previewMenuManager.addMenu(this._previewMenu); + this._previewMenu.connect('open-state-changed', Lang.bind(this, function(menu, isPoppedUp) { + if (!isPoppedUp) + this._onMenuPoppedDown(); + })); - this._previewMenu.connect('open-state-changed', Lang.bind(this, function(menu, isPoppedUp) { - if (!isPoppedUp) - this._onMenuPoppedDown(); - })); - let id = Main.overview.connect('hiding', Lang.bind(this, function() { - this._previewMenu.close(); - })); - this._previewMenu.actor.connect('destroy', function() { - Main.overview.disconnect(id); - }); + let id = Main.overview.connect('hiding', Lang.bind(this, function() { + this._previewMenu.close(); + })); + this._previewMenu.actor.connect('destroy', function() { + Main.overview.disconnect(id); + }); + }, - } + _windowPreviews: function() { + if (!this._previewMenu) + this._createPreviewMenus(); - if (this._previewMenu.isOpen) + if (this._previewMenu.isOpen) { this._previewMenu.close(); - else + } + else { + this._previewMenu.fromHover = false; this._previewMenu.popup(); + this._previewMenu.actor.navigate_focus(null, Gtk.DirectionType.TAB_FORWARD, false); + } return false; }, + enableHover: function(appIcons) { + appIconsHoverList = appIcons; + if (this._hoverIsEnabled) + return; + this._hoverIsEnabled = true; + + if (!this._previewMenu) + this._createPreviewMenus(); + + this._signalsHandler.addWithLabel('preview-hover', [ + this._previewMenu, + 'menu-closed', + function(menu) { + // enter-event doesn't fire on an app icon when the popup menu from a previously + // hovered app icon is still open, so when a preview menu closes we need to + // see if a new app icon is hovered and open its preview menu now. + // also, for some reason actor doesn't report being hovered by get_hover() + // if the hover started when a popup was opened. So, look for the actor by mouse position. + let [x, y,] = global.get_pointer(); + let hoveredActor = global.stage.get_actor_at_pos(Clutter.PickMode.REACTIVE, x, y); + let appIconToOpen; + appIconsHoverList.forEach(function (appIcon) { + if(appIcon.actor == hoveredActor) { + appIconToOpen = appIcon; + } else if(appIcon._previewMenu && appIcon._previewMenu.isOpen) { + appIcon._previewMenu.close(); + } + }); + + if (appIconToOpen) { + appIconToOpen.actor.sync_hover(); + if (appIconToOpen._previewMenu && appIconToOpen._previewMenu != menu) + appIconToOpen._previewMenu._onEnter(); + } + return GLib.SOURCE_REMOVE; + } + ]); + + let windowPreviewMenuData = this._previewMenuManager._menus[this._previewMenuManager._findMenu(this._previewMenu)]; + this._previewMenu.disconnect(windowPreviewMenuData.openStateChangeId); + windowPreviewMenuData.openStateChangeId = this._previewMenu.connect( + 'open-state-changed', + Lang.bind(this._previewMenuManager, function(menu, open) { + if (menu.fromHover) { + if (open) { + if (this.activeMenu) + this.activeMenu.close(BoxPointer.PopupAnimation.FADE); + + // don't grab here, we are grabbing in onLeave in windowPreview.js + // this._grabHelper.grab({ + // actor: menu.actor, + // focus: menu.sourceActor, + // onUngrab: Lang.bind(this, this._closeMenu, menu) + // }); + } else { + this._grabHelper.ungrab({ actor: menu.actor }); + } + } + else + this._onMenuOpenState(menu, open); + }) + ); + + this._previewMenu.enableHover(); + }, + + disableHover: function() { + this._hoverIsEnabled = false; + + this._signalsHandler.removeWithLabel('preview-hover'); + if (this._previewMenu) + this._previewMenu.disableHover(); + }, + // Try to do the right thing when attempting to launch a new window of an app. In // particular, if the application doens't allow to launch a new window, activate // the existing window instead. @@ -954,7 +1053,7 @@ const MyAppIconMenu = new Lang.Class({ separatorShown = true; } - let item = new WindowPreview.WindowPreviewMenuItem(window); + let item = new WindowPreview.WindowPreviewMenuItem(window, this._source); this._allWindowsMenuItem.menu.addMenuItem(item); item.connect('activate', Lang.bind(this, function() { this.emit('activate-window', window); diff --git a/dash.js b/dash.js index 4cf5aa2a5..c0d2c2677 100644 --- a/dash.js +++ b/dash.js @@ -304,6 +304,10 @@ var MyDash = new Lang.Class({ Main.overview, 'item-drag-cancelled', Lang.bind(this, this._onDragCancelled) + ], [ + this._dtdSettings, + 'changed::show-previews-hover', + Lang.bind(this, this._togglePreviewHover) ]); }, @@ -503,6 +507,27 @@ var MyDash = new Lang.Class({ return item; }, + _togglePreviewHover: function() { + if (this._dtdSettings.get_boolean('show-previews-hover')) + this._enableHover(); + else + this._disableHover(); + }, + + _enableHover: function() { + let appIcons = this.getAppIcons(); + appIcons.forEach(function (appIcon) { + appIcon.enableHover(appIcons); + }); + }, + + _disableHover: function() { + let appIcons = this.getAppIcons(); + appIcons.forEach(function (appIcon) { + appIcon.disableHover(); + }); + }, + /** * Return an array with the "proper" appIcons currently in the dash */ @@ -857,6 +882,9 @@ var MyDash = new Lang.Class({ // This will update the size, and the corresponding number for each icon this._updateNumberOverlay(); + + // Connect windows previews to hover events + this._togglePreviewHover(); }, _updateNumberOverlay: function() { diff --git a/docking.js b/docking.js index 2e0c2b4c9..719e575b9 100644 --- a/docking.js +++ b/docking.js @@ -366,6 +366,7 @@ const DockedDash = new Lang.Class({ // sync hover after a popupmenu is closed this.dash.connect('menu-closed', Lang.bind(this, function() { this._box.sync_hover(); + this._hoverChanged(); })); // Load optional features that need to be activated for one dock only @@ -684,7 +685,13 @@ const DockedDash = new Lang.Class({ }, _hoverChanged: function() { - if (!this._ignoreHover) { + let dontClose = false; + this.dash.getAppIcons().forEach(function(appIcon) { + if (appIcon._previewMenu && appIcon._previewMenu.isOpen) + dontClose = true; + }); + + if (!this._ignoreHover && !dontClose) { // Skip if dock is not in autohide mode for instance because it is shown // by intellihide. if (this._autohideIsEnabled) { diff --git a/prefs.js b/prefs.js index d8d8b9418..9167dbd86 100644 --- a/prefs.js +++ b/prefs.js @@ -524,6 +524,11 @@ const Settings = new Lang.Class({ })); + this._settings.bind('show-previews-hover', + this._builder.get_object('preview_hover_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + // Appearance Panel this._settings.bind('apply-custom-theme', this._builder.get_object('customize_theme'), 'sensitive', Gio.SettingsBindFlags.INVERT_BOOLEAN | Gio.SettingsBindFlags.GET); diff --git a/schemas/org.gnome.shell.extensions.dash-to-dock.gschema.xml b/schemas/org.gnome.shell.extensions.dash-to-dock.gschema.xml index 765a397f1..a452bdbe2 100644 --- a/schemas/org.gnome.shell.extensions.dash-to-dock.gschema.xml +++ b/schemas/org.gnome.shell.extensions.dash-to-dock.gschema.xml @@ -536,5 +536,10 @@ Enable unity7 like glossy backlit items Emulate the unity7 backlit glossy items behaviour + + false + Show window previews on mouse hover + + diff --git a/windowPreview.js b/windowPreview.js index 09253b9b3..f05364165 100644 --- a/windowPreview.js +++ b/windowPreview.js @@ -20,10 +20,14 @@ const Workspace = imports.ui.workspace; const Me = imports.misc.extensionUtils.getCurrentExtension(); const Utils = Me.imports.utils; -const PREVIEW_MAX_WIDTH = 250; -const PREVIEW_MAX_HEIGHT = 150; +/* + * Timeouts for the hovering events + */ +const HOVER_ENTER_TIMEOUT = 100; +const HOVER_LEAVE_TIMEOUT = 100; +const HOVER_MENU_LEAVE_TIMEOUT = 500; -const WindowPreviewMenu = new Lang.Class({ +var WindowPreviewMenu = new Lang.Class({ Name: 'WindowPreviewMenu', Extends: PopupMenu.PopupMenu, @@ -34,9 +38,6 @@ const WindowPreviewMenu = new Lang.Class({ this.parent(source.actor, 0.5, side); - // We want to keep the item hovered while the menu is up - this.blockSourceEvents = true; - this._source = source; this._app = this._source.app; let monitorIndex = this._source.monitorIndex; @@ -62,6 +63,8 @@ const WindowPreviewMenu = new Lang.Class({ this._previewBox = new WindowPreviewList(this._source, this._dtdSettings); this.addMenuItem(this._previewBox); + + this.fromHover = false; }, _redisplay: function() { @@ -74,12 +77,13 @@ const WindowPreviewMenu = new Lang.Class({ if (windows.length > 0) { this._redisplay(); this.open(); - this.actor.navigate_focus(null, Gtk.DirectionType.TAB_FORWARD, false); this._source.emit('sync-tooltip'); } }, destroy: function () { + this.disableHover(); + if (this._mappedId) this._source.actor.disconnect(this._mappedId); @@ -87,6 +91,122 @@ const WindowPreviewMenu = new Lang.Class({ this._source.actor.disconnect(this._destroyId); this.parent(); + }, + + enableHover: function() { + // Show window previews on mouse hover + this._enterSourceId = this._source.actor.connect('enter-event', Lang.bind(this, this._onEnter)); + this._leaveSourceId = this._source.actor.connect('leave-event', Lang.bind(this, this._onLeave)); + + this._enterMenuId = this.actor.connect('enter-event', Lang.bind(this, this._onMenuEnter)); + this._leaveMenuId = this.actor.connect('leave-event', Lang.bind(this, this._onMenuLeave)); + }, + + disableHover: function() { + if (this._enterSourceId) { + this._source.actor.disconnect(this._enterSourceId); + this._enterSourceId = 0; + } + if (this._leaveSourceId) { + this._source.actor.disconnect(this._leaveSourceId); + this._leaveSourceId = 0; + } + + if (this._enterMenuId) { + this.actor.disconnect(this._enterMenuId); + this._enterMenuId = 0; + } + if (this._leaveMenuId) { + this.actor.disconnect(this._leaveMenuId); + this._leaveMenuId = 0; + } + }, + + _onEnter: function () { + this.cancelOpen(); + this.cancelClose(); + + this._hoverOpenTimeoutId = Mainloop.timeout_add( + HOVER_ENTER_TIMEOUT, + Lang.bind(this, this.hoverOpen) + ); + }, + + _onLeave: function () { + this.cancelOpen(); + this.cancelClose(); + + // grabHelper.grab() is usually called when the menu is opened. However, there seems to be a bug in the + // underlying gnome-shell that causes all window contents to freeze if the grab and ungrab occur + // in quick succession in timeouts from the Mainloop (for example, clicking the icon as the preview window is opening) + // So, instead wait until the mouse is leaving the icon (and might be moving toward the open window) to trigger the grab + if (this.isOpen) { + this._source._previewMenuManager._grabHelper.grab({ + actor: this.actor, + focus: this.sourceActor, + onUngrab: Lang.bind(this, function() { + this.close(~0); + }) + }); + } + + this._hoverCloseTimeoutId = Mainloop.timeout_add( + HOVER_LEAVE_TIMEOUT, + Lang.bind(this, this.hoverClose) + ); + }, + + cancelOpen: function () { + if (this._hoverOpenTimeoutId) { + Mainloop.source_remove(this._hoverOpenTimeoutId); + this._hoverOpenTimeoutId = null; + } + }, + + cancelClose: function () { + if (this._hoverCloseTimeoutId) { + Mainloop.source_remove(this._hoverCloseTimeoutId); + this._hoverCloseTimeoutId = null; + } + }, + + hoverOpen: function () { + this._hoverOpenTimeoutId = null; + if (!this.isOpen) { + this.fromHover = true; + this.popup(); + } + }, + + hoverClose: function () { + this._hoverCloseTimeoutId = null; + + if (!this.fromHover) + return; + + if (this.isOpen) + this.close(~0); + this.fromHover = false; + }, + + _onMenuEnter: function () { + if (!this.fromHover) + return; + + this.cancelClose(); + }, + + _onMenuLeave: function () { + if (!this.fromHover) + return; + + this.cancelOpen(); + this.cancelClose(); + + this._hoverCloseTimeoutId = Mainloop.timeout_add( + HOVER_MENU_LEAVE_TIMEOUT, + Lang.bind(this, this.hoverClose) + ); } }); @@ -183,7 +303,7 @@ const WindowPreviewList = new Lang.Class({ }, _createPreviewItem: function(window) { - let preview = new WindowPreviewMenuItem(window); + let preview = new WindowPreviewMenuItem(window, this._source); return preview; }, @@ -365,7 +485,7 @@ const WindowPreviewMenuItem = new Lang.Class({ Name: 'WindowPreviewMenuItem', Extends: PopupMenu.PopupBaseMenuItem, - _init: function(window, params) { + _init: function(window, source, params) { this._window = window; this._destroyId = 0; this._windowAddedId = 0; @@ -375,8 +495,13 @@ const WindowPreviewMenuItem = new Lang.Class({ this.actor.remove_child(this._ornamentLabel); this.actor.add_style_class_name('dashtodock-app-well-preview-menu-item'); + let monitorIndex = source.monitorIndex; + let monitor = Main.layoutManager.monitors[monitorIndex]; + this._preview_max_width = Math.round(monitor.width / 5); + this._preview_max_height = Math.round(monitor.height / 5); + this._cloneBin = new St.Bin(); - this._cloneBin.set_size(PREVIEW_MAX_WIDTH, PREVIEW_MAX_HEIGHT); + this._cloneBin.set_size(this._preview_max_width, this._preview_max_height); // TODO: improve the way the closebutton is layout. Just use some padding // for the moment. @@ -398,7 +523,7 @@ const WindowPreviewMenuItem = new Lang.Class({ overlayGroup.add_actor(this.closeButton); let label = new St.Label({ text: window.get_title()}); - label.set_style('max-width: '+PREVIEW_MAX_WIDTH +'px'); + label.set_style('max-width: ' + this._preview_max_width + 'px'); let labelBin = new St.Bin({ child: label, x_align: St.Align.MIDDLE}); @@ -449,7 +574,7 @@ const WindowPreviewMenuItem = new Lang.Class({ let windowTexture = mutterWindow.get_texture(); let [width, height] = windowTexture.get_size(); - let scale = Math.min(1.0, PREVIEW_MAX_WIDTH/width, PREVIEW_MAX_HEIGHT/height); + let scale = Math.min(1.0, this._preview_max_width / width, this._preview_max_height / height); let clone = new Clutter.Clone ({ source: windowTexture, reactive: true,