diff --git a/Settings.ui b/Settings.ui
index 2b164a8b8..a78c92fe5 100644
--- a/Settings.ui
+++ b/Settings.ui
@@ -1719,6 +1719,79 @@
3
+
+
+
+ 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,