From ef4ced7c19941f1a6e368ec6b859cdc918d28426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Wed, 20 May 2015 17:44:50 +0200 Subject: [PATCH 1/7] Add top-icons extension --- extensions/top-icons/extension.js | 225 ++++++++++++++++++++++++++ extensions/top-icons/meson.build | 5 + extensions/top-icons/metadata.json.in | 10 ++ extensions/top-icons/stylesheet.css | 1 + meson.build | 1 + 5 files changed, 242 insertions(+) create mode 100644 extensions/top-icons/extension.js create mode 100644 extensions/top-icons/meson.build create mode 100644 extensions/top-icons/metadata.json.in create mode 100644 extensions/top-icons/stylesheet.css diff --git a/extensions/top-icons/extension.js b/extensions/top-icons/extension.js new file mode 100644 index 0000000..7312a26 --- /dev/null +++ b/extensions/top-icons/extension.js @@ -0,0 +1,225 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Clutter = imports.gi.Clutter; +const Shell = imports.gi.Shell; +const St = imports.gi.St; +const Main = imports.ui.main; +const GLib = imports.gi.GLib; +const Lang = imports.lang; +const Panel = imports.ui.panel; +const PanelMenu = imports.ui.panelMenu; +const Meta = imports.gi.Meta; +const Mainloop = imports.mainloop; +const NotificationDaemon = imports.ui.notificationDaemon; +const System = imports.system; + +let trayAddedId = 0; +let trayRemovedId = 0; +let getSource = null; +let icons = []; +let notificationDaemon = null; +let sysTray = null; + +const PANEL_ICON_SIZE = 24; + +function init() { + if (Main.legacyTray) { + notificationDaemon = Main.legacyTray; + NotificationDaemon.STANDARD_TRAY_ICON_IMPLEMENTATIONS = imports.ui.legacyTray.STANDARD_TRAY_ICON_IMPLEMENTATIONS; + } + else if (Main.notificationDaemon._fdoNotificationDaemon && + Main.notificationDaemon._fdoNotificationDaemon._trayManager) { + notificationDaemon = Main.notificationDaemon._fdoNotificationDaemon; + getSource = Lang.bind(notificationDaemon, NotificationDaemon.FdoNotificationDaemon.prototype._getSource); + } + else if (Main.notificationDaemon._trayManager) { + notificationDaemon = Main.notificationDaemon; + getSource = Lang.bind(notificationDaemon, NotificationDaemon.NotificationDaemon.prototype._getSource); + } + else { + NotificationDaemon.STANDARD_TRAY_ICON_IMPLEMENTATIONS = { + 'bluetooth-applet': 1, 'gnome-sound-applet': 1, 'nm-applet': 1, + 'gnome-power-manager': 1, 'keyboard': 1, 'a11y-keyboard': 1, + 'kbd-scrolllock': 1, 'kbd-numlock': 1, 'kbd-capslock': 1, 'ibus-ui-gtk': 1 + }; + } +} + +function enable() { + if (notificationDaemon) + GLib.idle_add(GLib.PRIORITY_LOW, moveToTop); + else + createTray(); +} + +function createSource (title, pid, ndata, sender, trayIcon) { + if (trayIcon) { + onTrayIconAdded(this, trayIcon, title); + return null; + } + + return getSource(title, pid, ndata, sender, trayIcon); +}; + +function onTrayIconAdded(o, icon, role) { + let wmClass = icon.wm_class ? icon.wm_class.toLowerCase() : ''; + if (NotificationDaemon.STANDARD_TRAY_ICON_IMPLEMENTATIONS[wmClass] !== undefined) + return; + + let buttonBox = new PanelMenu.ButtonBox(); + let box = buttonBox.actor; + let parent = box.get_parent(); + + let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; + let iconSize = PANEL_ICON_SIZE * scaleFactor; + + icon.set_size(iconSize, iconSize); + box.add_actor(icon); + + icon.reactive = true; + + if (parent) + parent.remove_actor(box); + + icons.push(icon); + Main.panel._rightBox.insert_child_at_index(box, 0); + + let clickProxy = new St.Bin({ width: iconSize, height: iconSize }); + clickProxy.reactive = true; + Main.uiGroup.add_actor(clickProxy); + + icon._proxyAlloc = Main.panel._rightBox.connect('allocation-changed', function() { + Meta.later_add(Meta.LaterType.BEFORE_REDRAW, function() { + let [x, y] = icon.get_transformed_position(); + clickProxy.set_position(x, y); + }); + }); + + icon.connect("destroy", function() { + Main.panel._rightBox.disconnect(icon._proxyAlloc); + clickProxy.destroy(); + }); + + clickProxy.connect('button-release-event', function(actor, event) { + icon.click(event); + }); + + icon._clickProxy = clickProxy; + + /* Fixme: HACK */ + Meta.later_add(Meta.LaterType.BEFORE_REDRAW, function() { + let [x, y] = icon.get_transformed_position(); + clickProxy.set_position(x, y); + return false; + }); + let timerId = 0; + let i = 0; + timerId = Mainloop.timeout_add(500, function() { + icon.set_size(icon.width == iconSize ? iconSize - 1 : iconSize, + icon.width == iconSize ? iconSize - 1 : iconSize); + i++; + if (i == 2) + Mainloop.source_remove(timerId); + }); +} + +function onTrayIconRemoved(o, icon) { + let parent = icon.get_parent(); + parent.destroy(); + icon.destroy(); + icons.splice(icons.indexOf(icon), 1); +} + +function createTray() { + sysTray = new Shell.TrayManager(); + sysTray.connect('tray-icon-added', onTrayIconAdded); + sysTray.connect('tray-icon-removed', onTrayIconRemoved); + sysTray.manage_screen(global.screen, Main.panel.actor); +} + +function destroyTray() { + icons.forEach(icon => { icon.get_parent().destroy(); }); + icons = []; + + sysTray = null; + System.gc(); // force finalizing tray to unmanage screen +} + +function moveToTop() { + notificationDaemon._trayManager.disconnect(notificationDaemon._trayIconAddedId); + notificationDaemon._trayManager.disconnect(notificationDaemon._trayIconRemovedId); + trayAddedId = notificationDaemon._trayManager.connect('tray-icon-added', onTrayIconAdded); + trayRemovedId = notificationDaemon._trayManager.connect('tray-icon-removed', onTrayIconRemoved); + + notificationDaemon._getSource = createSource; + + let toDestroy = []; + if (notificationDaemon._sources) { + for (let i = 0; i < notificationDaemon._sources.length; i++) { + let source = notificationDaemon._sources[i]; + if (!source.trayIcon) + continue; + let parent = source.trayIcon.get_parent(); + parent.remove_actor(source.trayIcon); + onTrayIconAdded(this, source.trayIcon, source.initialTitle); + toDestroy.push(source); + } + } + else { + for (let i = 0; i < notificationDaemon._iconBox.get_n_children(); i++) { + let button = notificationDaemon._iconBox.get_child_at_index(i); + let icon = button.child; + button.remove_actor(icon); + onTrayIconAdded(this, icon, ''); + toDestroy.push(button); + } + } + + for (let i = 0; i < toDestroy.length; i++) { + toDestroy[i].destroy(); + } +} + +function moveToTray() { + if (trayAddedId != 0) { + notificationDaemon._trayManager.disconnect(trayAddedId); + trayAddedId = 0; + } + + if (trayRemovedId != 0) { + notificationDaemon._trayManager.disconnect(trayRemovedId); + trayRemovedId = 0; + } + + notificationDaemon._trayIconAddedId = notificationDaemon._trayManager.connect('tray-icon-added', + Lang.bind(notificationDaemon, notificationDaemon._onTrayIconAdded)); + notificationDaemon._trayIconRemovedId = notificationDaemon._trayManager.connect('tray-icon-removed', + Lang.bind(notificationDaemon, notificationDaemon._onTrayIconRemoved)); + + notificationDaemon._getSource = getSource; + + for (let i = 0; i < icons.length; i++) { + let icon = icons[i]; + let parent = icon.get_parent(); + if (icon._clicked) { + icon.disconnect(icon._clicked); + } + icon._clicked = undefined; + if (icon._proxyAlloc) { + Main.panel._rightBox.disconnect(icon._proxyAlloc); + } + icon._clickProxy.destroy(); + parent.remove_actor(icon); + parent.destroy(); + notificationDaemon._onTrayIconAdded(notificationDaemon, icon); + } + + icons = []; +} + +function disable() { + if (notificationDaemon) + moveToTray(); + else + destroyTray(); +} diff --git a/extensions/top-icons/meson.build b/extensions/top-icons/meson.build new file mode 100644 index 0000000..48504f6 --- /dev/null +++ b/extensions/top-icons/meson.build @@ -0,0 +1,5 @@ +extension_data += configure_file( + input: metadata_name + '.in', + output: metadata_name, + configuration: metadata_conf +) diff --git a/extensions/top-icons/metadata.json.in b/extensions/top-icons/metadata.json.in new file mode 100644 index 0000000..f1e2436 --- /dev/null +++ b/extensions/top-icons/metadata.json.in @@ -0,0 +1,10 @@ +{ +"extension-id": "@extension_id@", +"uuid": "@uuid@", +"settings-schema": "@gschemaname@", +"gettext-domain": "@gettext_domain@", +"name": "Top Icons", +"description": "Shows legacy tray icons on top", +"shell-version": [ "@shell_current@" ], +"url": "http://94.247.144.115/repo/topicons/" +} diff --git a/extensions/top-icons/stylesheet.css b/extensions/top-icons/stylesheet.css new file mode 100644 index 0000000..25134b6 --- /dev/null +++ b/extensions/top-icons/stylesheet.css @@ -0,0 +1 @@ +/* This extensions requires no special styling */ diff --git a/meson.build b/meson.build index 40a8275..c16bde1 100644 --- a/meson.build +++ b/meson.build @@ -54,6 +54,7 @@ all_extensions += [ 'auto-move-windows', 'example', 'native-window-placement', + 'top-icons', 'user-theme' ] -- 2.21.0 From a219c4f34806b6642f0cfc5f890662d599bfd1a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Wed, 20 May 2015 18:05:41 +0200 Subject: [PATCH 2/7] Add dash-to-dock extension --- extensions/dash-to-dock/Settings.ui | 3332 +++++++++++++++++ extensions/dash-to-dock/appIconIndicators.js | 1124 ++++++ extensions/dash-to-dock/appIcons.js | 1171 ++++++ extensions/dash-to-dock/convenience.js | 74 + extensions/dash-to-dock/dash.js | 1175 ++++++ extensions/dash-to-dock/docking.js | 1925 ++++++++++ extensions/dash-to-dock/extension.js | 23 + extensions/dash-to-dock/intellihide.js | 323 ++ extensions/dash-to-dock/launcherAPI.js | 244 ++ extensions/dash-to-dock/media/glossy.svg | 139 + extensions/dash-to-dock/media/logo.svg | 528 +++ extensions/dash-to-dock/meson.build | 23 + extensions/dash-to-dock/metadata.json.in | 12 + ....shell.extensions.dash-to-dock.gschema.xml | 540 +++ extensions/dash-to-dock/prefs.js | 868 +++++ extensions/dash-to-dock/stylesheet.css | 109 + extensions/dash-to-dock/theming.js | 672 ++++ extensions/dash-to-dock/utils.js | 255 ++ extensions/dash-to-dock/windowPreview.js | 630 ++++ meson.build | 1 + 20 files changed, 13168 insertions(+) create mode 100644 extensions/dash-to-dock/Settings.ui create mode 100644 extensions/dash-to-dock/appIconIndicators.js create mode 100644 extensions/dash-to-dock/appIcons.js create mode 100644 extensions/dash-to-dock/convenience.js create mode 100644 extensions/dash-to-dock/dash.js create mode 100644 extensions/dash-to-dock/docking.js create mode 100644 extensions/dash-to-dock/extension.js create mode 100644 extensions/dash-to-dock/intellihide.js create mode 100644 extensions/dash-to-dock/launcherAPI.js create mode 100644 extensions/dash-to-dock/media/glossy.svg create mode 100644 extensions/dash-to-dock/media/logo.svg create mode 100644 extensions/dash-to-dock/meson.build create mode 100644 extensions/dash-to-dock/metadata.json.in create mode 100644 extensions/dash-to-dock/org.gnome.shell.extensions.dash-to-dock.gschema.xml create mode 100644 extensions/dash-to-dock/prefs.js create mode 100644 extensions/dash-to-dock/stylesheet.css create mode 100644 extensions/dash-to-dock/theming.js create mode 100644 extensions/dash-to-dock/utils.js create mode 100644 extensions/dash-to-dock/windowPreview.js diff --git a/extensions/dash-to-dock/Settings.ui b/extensions/dash-to-dock/Settings.ui new file mode 100644 index 0000000..2b164a8 --- /dev/null +++ b/extensions/dash-to-dock/Settings.ui @@ -0,0 +1,3332 @@ + + + + + + 1 + 0.050000000000000003 + 0.25 + + + True + False + 12 + 12 + 12 + 12 + vertical + + + True + False + 0 + in + + + True + False + none + + + 100 + 80 + True + True + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + False + True + When set to minimize, double clicking minimizes all the windows of the application. + True + 40 + 0 + + + + 0 + 1 + + + + + True + False + True + Shift+Click action + 0 + + + 0 + 0 + + + + + True + False + center + + Raise window + Minimize window + Launch new instance + Cycle through windows + Minimize or overview + Show window previews + Minimize or show previews + Quit + + + + 1 + 0 + 2 + + + + + + + + + 100 + 80 + True + True + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + False + True + Behavior for Middle-Click. + True + 40 + 0 + + + + 0 + 1 + + + + + True + False + True + Middle-Click action + 0 + + + 0 + 0 + + + + + True + False + center + + Raise window + Minimize window + Launch new instance + Cycle through windows + Minimize or overview + Show window previews + Minimize or show previews + Quit + + + + 1 + 0 + 2 + + + + + + + + + 100 + 80 + True + True + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + False + True + Behavior for Shift+Middle-Click. + True + 40 + 0 + + + + 0 + 1 + + + + + True + False + True + Shift+Middle-Click action + 0 + + + 0 + 0 + + + + + True + False + center + + Raise window + Minimize window + Launch new instance + Cycle through windows + Minimize or overview + Show window previews + Minimize or show previews + Quit + + + + 1 + 0 + 2 + + + + + + + + + + + + + + False + True + 0 + + + + + 1 + 0.01 + 0.10000000000000001 + + + 0.33000000000000002 + 1 + 0.01 + 0.10000000000000001 + + + 10 + 1 + 5 + + + True + False + 12 + 12 + 12 + 12 + vertical + + + True + False + 0 + in + + + True + False + none + + + 100 + 80 + True + True + + + True + False + 12 + 12 + 12 + 12 + vertical + 12 + + + True + False + 32 + + + True + False + center + Enable Unity7 like glossy backlit items + 0 + + + True + True + 0 + + + + + True + True + center + + + False + True + 1 + + + + + False + True + 0 + + + + + True + False + + + True + False + start + Use dominant color + + + True + True + 0 + + + + + True + True + + + False + True + 1 + + + + + False + True + 1 + + + + + True + False + 32 + + + True + True + + + 1 + 0 + + + + + True + False + True + Customize indicator style + fill + 0 + + + 0 + 0 + + + + + False + True + 2 + + + + + True + False + 1 + vertical + 12 + + + True + False + 32 + + + True + False + Color + 0 + + + True + True + 0 + + + + + True + True + True + + + False + True + 1 + + + + + False + True + 0 + + + + + True + False + 32 + + + True + False + Border color + 0 + + + True + True + 0 + + + + + True + True + True + + + False + True + 1 + + + + + False + True + 1 + + + + + True + False + 32 + + + True + False + Border width + 0 + + + True + True + 0 + + + + + True + True + dot_border_width_adjustment + + + False + True + 1 + + + + + False + True + 2 + + + + + False + True + 3 + + + + + + + + + + + + + + False + True + 0 + + + + + 1 + 0.050000000000000003 + 0.25 + + + 16 + 128 + 1 + 10 + + + True + True + 6 + 6 + 6 + 6 + + + True + False + 24 + 24 + 24 + 24 + vertical + 24 + + + True + False + 0 + in + + + True + False + none + + + True + True + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + False + True + Show the dock on + 0 + + + 0 + 0 + + + + + True + False + center + + + + 1 + 0 + + + + + Show on all monitors. + True + True + False + 12 + 0 + True + + + 0 + 2 + 2 + + + + + + + + + + + + + + + 100 + True + True + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + False + True + Position on screen + 0 + + + False + True + 0 + + + + + True + False + 32 + + + Left + True + True + False + end + center + 0 + True + + + + False + True + 0 + + + + + Bottom + True + True + False + center + 0 + True + position_left_button + + + + False + True + 1 + + + + + Top + True + True + False + center + 0 + bottom + True + position_left_button + + + + False + True + 2 + + + + + Right + True + True + False + center + 0 + True + position_left_button + + + + False + True + 3 + + + + + False + True + 1 + + + + + + + + + + + + + + False + True + 0 + + + + + True + False + 0 + in + + + True + False + none + + + 100 + 80 + True + True + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + False + True + Hide the dock when it obstructs a window of the current application. More refined settings are available. + True + 0 + + + + 0 + 1 + + + + + True + False + True + Intelligent autohide + 0 + + + 0 + 0 + + + + + True + False + 6 + + + True + True + True + center + center + 0.46000000834465027 + + + True + False + emblem-system-symbolic + + + + + + False + True + 0 + + + + + True + True + end + center + + + False + True + 1 + + + + + 1 + 0 + 2 + + + + + + + + + + + + + + False + True + 1 + + + + + True + False + 0 + in + + + True + False + none + + + 100 + 80 + True + True + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + False + Dock size limit + 0 + + + 0 + 0 + + + + + True + True + baseline + True + dock_size_adjustment + 0 + 2 + right + + + + + 1 + 0 + + + + + Panel mode: extend to the screen edge + True + True + False + 12 + 0 + True + + + 0 + 1 + 2 + + + + + + + + + True + True + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + False + Icon size limit + 0 + + + 0 + 0 + + + + + True + True + baseline + True + icon_size_adjustment + 1 + 0 + right + + + + + 1 + 0 + + + + + Fixed icon size: scroll to reveal other icons + True + True + False + 12 + 0 + True + + + 0 + 1 + 2 + + + + + + + + + + + + + + False + True + 2 + + + + + + + True + False + Position and size + + + False + + + + + True + False + 24 + 24 + 24 + 24 + vertical + 24 + + + True + False + 0 + in + + + True + False + none + + + True + True + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + True + end + center + + + 1 + 0 + + + + + True + False + True + Show favorite applications + 0 + + + 0 + 0 + + + + + + + + + True + True + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + True + end + center + + + 1 + 0 + + + + + True + False + True + Show running applications + 0 + + + 0 + 0 + + + + + Isolate workspaces. + True + True + False + 12 + 0 + True + + + 0 + 2 + 2 + + + + + Isolate monitors. + True + True + False + 12 + 0 + True + + + 0 + 3 + 2 + + + + + True + True + False + 3 + 0 + True + + + True + False + Show open windows previews. + True + + + + + 0 + 1 + 2 + + + + + + + + + True + True + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + True + end + center + + + 1 + 0 + 2 + + + + + True + False + True + If disabled, these settings are accessible from gnome-tweak-tool or the extension website. + True + 0 + + + + 0 + 1 + + + + + True + False + True + Show <i>Applications</i> icon + True + 0 + + + 0 + 0 + + + + + Move the applications button at the beginning of the dock. + True + True + False + 12 + 0 + True + + + 0 + 2 + 2 + + + + + True + True + False + 3 + 0 + 0.43000000715255737 + True + + + True + False + Animate <i>Show Applications</i>. + True + + + + + 0 + 3 + 2 + + + + + + + + + + + + + + False + True + 0 + + + + + 1 + + + + + True + False + Launchers + + + 1 + False + + + + + True + False + 24 + 24 + 24 + 24 + vertical + 24 + + + True + False + 0 + in + + + True + False + none + + + True + True + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + False + True + Enable Super+(0-9) as shortcuts to activate apps. It can also be used together with Shift and Ctrl. + True + True + 0 + + + + 0 + 1 + + + + + True + False + True + Use keyboard shortcuts to activate apps + 0 + + + 0 + 0 + + + + + True + False + 6 + + + True + True + True + center + center + 0.46000000834465027 + + + True + False + emblem-system-symbolic + + + + + + False + True + 0 + + + + + True + True + end + center + + + False + True + 1 + + + + + 1 + 0 + 2 + + + + + + + + + + + + + + False + True + 1 + + + + + True + False + 0 + in + + + True + False + none + + + True + True + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + False + True + Behaviour when clicking on the icon of a running application. + True + 0 + + + + 0 + 1 + + + + + True + False + True + Click action + 0 + + + 0 + 0 + + + + + True + False + 6 + + + True + True + True + center + + + True + False + emblem-system-symbolic + + + + + + False + True + 0 + + + + + True + False + center + + Raise window + Minimize + Launch new instance + Cycle through windows + Minimize or overview + Show window previews + Minimize or show previews + + + + False + True + 1 + + + + + 1 + 0 + 2 + + + + + + + + + + + + + + False + True + 2 + + + + + True + False + 0 + in + + + True + False + none + + + True + True + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + False + True + Behaviour when scrolling on the icon of an application. + True + 0 + + + + 0 + 1 + + + + + True + False + True + Scroll action + 0 + + + 0 + 0 + + + + + True + False + 6 + + + True + False + center + + Do nothing + Cycle through windows + Switch workspace + + + + False + True + 1 + + + + + 1 + 0 + 2 + + + + + + + + + + + + + + False + True + 3 + + + + + 2 + + + + + True + False + Behavior + + + 2 + False + + + + + True + False + 24 + 24 + 24 + 24 + vertical + 24 + + + True + False + 0 + in + + + True + True + none + + + True + True + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + False + True + Few customizations meant to integrate the dock with the default GNOME theme. Alternatively, specific options can be enabled below. + True + 0 + + + + 0 + 1 + + + + + True + False + True + Use built-in theme + 0 + + + 0 + 0 + + + + + True + True + end + center + + + 1 + 0 + 2 + + + + + + + + + + + + + + False + True + 0 + + + + + True + False + 0 + in + + + True + False + none + + + True + True + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + True + end + center + + + 1 + 0 + 2 + + + + + True + False + True + Save space reducing padding and border radius. + 0 + + + + 0 + 1 + + + + + True + False + True + Shrink the dash + 0 + + + 0 + 0 + + + + + + + + + True + True + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + False + True + Customize windows counter indicators + 0 + + + 0 + 0 + + + + + True + False + 6 + + + True + True + True + center + center + 0.46000000834465027 + + + True + False + emblem-system-symbolic + + + + + + False + True + 0 + + + + + True + False + + Default + Dots + Squares + Dashes + Segmented + Solid + Ciliora + Metro + + + + False + True + 1 + + + + + 1 + 0 + 2 + + + + + + + + + + + + True + True + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + False + True + Set the background color for the dash. + True + 0 + + + + 0 + 1 + + + + + True + False + True + Customize the dash color + 0 + + + 0 + 0 + + + + + True + False + 6 + + + True + True + True + center + center + 0.46000000834465027 + + + False + True + 1 + + + + + True + True + end + center + + + False + True + 1 + + + + + 1 + 0 + 2 + + + + + + + + + True + True + + + True + False + vertical + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + False + True + Tune the dash background opacity. + 0 + + + + 0 + 1 + + + + + True + False + True + Customize opacity + 0 + + + 0 + 0 + + + + + True + False + 6 + + + True + True + True + center + center + + + True + False + emblem-system-symbolic + + + + + + False + True + 0 + + + + + True + False + center + + Default + Fixed + Adaptive + Dynamic + + + + False + True + 1 + + + + + 1 + 0 + 2 + + + + + False + True + 0 + + + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + False + Opacity + + + False + True + 0 + + + + + True + True + custom_opacity_adjustement + on + False + 0 + 0 + 2 + right + + + + + True + True + 1 + + + + + False + True + 1 + + + + + + + + + 100 + 80 + True + True + + + True + False + 32 + + + True + False + center + 12 + Force straight corner + + 0 + + + True + True + 0 + + + + + True + True + center + 3 + + + False + True + 12 + 1 + + + + + + + + + + + + + + False + True + 1 + + + + + 3 + + + + + True + False + Appearance + + + 3 + False + + + + + False + 24 + 24 + True + True + vertical + 5 + + + + False + True + 10 + 0 + + + + + True + False + <b>Dash to Dock</b> + True + + + False + True + 1 + + + + + True + False + center + + + True + False + end + version: + + + False + True + 0 + + + + + True + False + start + ... + + + False + True + 1 + + + + + False + True + 2 + + + + + True + False + Moves the dash out of the overview transforming it in a dock + center + True + + + False + True + 3 + + + + + True + False + center + 5 + + + True + False + Created by + + + False + True + 0 + + + + + True + True + Michele (<a href="mailto:micxgx@gmail.com">micxgx@gmail.com</a>) + True + + + False + True + 1 + + + + + False + True + 4 + + + + + Webpage + True + True + True + + center + none + https://micheleg.github.io/dash-to-dock/ + + + False + True + 5 + + + + + True + True + end + <span size="small">This program comes with ABSOLUTELY NO WARRANTY. +See the <a href="https://www.gnu.org/licenses/old-licenses/gpl-2.0.html">GNU General Public License, version 2 or later</a> for details.</span> + True + center + True + + + True + True + 6 + + + + + 4 + + + + + True + False + About + + + 4 + False + + + + + 1 + 0.01 + 0.10000000000000001 + + + 1 + 0.01 + 0.10000000000000001 + + + True + False + 12 + 12 + 12 + 12 + vertical + + + True + False + 0 + in + + + True + False + none + + + 100 + 80 + True + True + + + True + False + 12 + 12 + 12 + 12 + vertical + 12 + + + True + False + 32 + + + True + True + + + 1 + 0 + + + + + True + False + True + Customize minimum and maximum opacity values + fill + 0 + + + 0 + 0 + + + + + False + True + 0 + + + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + False + Minimum opacity + + + False + True + 0 + + + + + True + True + min_opacity_adjustement + on + False + 0 + 0 + 2 + right + + + + + True + True + 1 + + + + + False + True + 1 + + + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + False + Maximum opacity + + + False + True + 0 + + + + + True + True + max_opacity_adjustement + on + False + 0 + 0 + 2 + right + + + + + True + True + 1 + + + + + False + True + 1 + + + + + + + + + + + + + + False + True + 0 + + + + + 1000 + 50 + 250 + + + 10 + 0.25 + 1 + + + True + False + 12 + 12 + 12 + 12 + vertical + + + True + False + 0 + in + + + True + False + none + + + 100 + 80 + True + True + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + True + end + center + + + 1 + 0 + 2 + + + + + True + False + True + Number overlay + 0 + + + 0 + 0 + + + + + True + False + Temporarily show the application numbers over the icons, corresponding to the shortcut. + True + 40 + 0 + + + + 0 + 1 + + + + + + + + + 100 + 80 + True + True + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + True + end + center + + + 1 + 0 + 2 + + + + + True + False + True + Show the dock if it is hidden + 0 + + + 0 + 0 + + + + + True + False + If using autohide, the dock will appear for a short time when triggering the shortcut. + True + 40 + 0 + + + + 0 + 1 + + + + + + + + + 100 + 80 + True + True + + + True + False + 12 + 12 + 12 + 12 + 32 + + + False + center + 12 + + + 1 + 0 + + + + + True + False + True + Shortcut for the options above + 0 + + + 0 + 0 + + + + + True + False + Syntax: <Shift>, <Ctrl>, <Alt>, <Super> + True + 40 + 0 + + + + 0 + 1 + + + + + + + + + + + + True + True + + + True + False + 12 + 12 + 12 + 12 + True + 6 + 32 + + + True + True + end + shortcut_time_adjustment + 3 + + + 1 + 0 + + + + + True + False + True + Hide timeout (s) + 0 + + + 0 + 0 + + + + + + + + + + + + + + False + True + 0 + + + + + 1 + 0.050000000000000003 + 0.25 + + + True + False + 12 + 12 + 12 + 12 + vertical + + + True + False + 0 + in + + + True + False + none + + + True + True + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + False + True + Show the dock by mouse hover on the screen edge. + True + 0 + + + + 0 + 1 + + + + + True + False + True + Autohide + 0 + + + 0 + 0 + + + + + True + True + end + center + + + 1 + 0 + 2 + + + + + Push to show: require pressure to show the dock + True + True + False + 0 + True + + + 0 + 3 + 2 + + + + + Enable in fullscreen mode + True + True + False + 12 + 0 + True + + + 0 + 2 + 2 + + + + + + + + + True + True + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + False + True + Show the dock when it doesn't obstruct application windows. + True + 0 + + + + 0 + 1 + + + + + True + False + True + Dodge windows + 0 + + + 0 + 0 + + + + + True + True + end + center + + + 1 + 0 + 2 + + + + + True + False + vertical + + + All windows + True + True + False + 12 + 0 + True + True + + + + False + True + 0 + + + + + Only focused application's windows + True + True + False + 0 + True + True + all_windows_radio_button + + + + False + True + 1 + + + + + Only maximized windows + True + True + False + 0 + True + True + all_windows_radio_button + + + + False + True + 2 + + + + + 0 + 2 + 2 + + + + + + + + + True + True + + + True + False + 12 + 12 + 12 + 12 + True + 6 + 32 + + + True + True + end + animation_time_adjustment + 3 + + + 1 + 0 + + + + + True + False + True + Animation duration (s) + 0 + + + 0 + 0 + + + + + True + True + end + hide_timeout_adjustment + 3 + + + 1 + 1 + + + + + True + True + end + show_timeout_adjustment + 3 + + + 1 + 2 + + + + + True + True + 0.000 + pressure_threshold_adjustment + + + 1 + 3 + + + + + True + False + True + Hide timeout (s) + 0 + + + 0 + 1 + + + + + True + False + True + Show timeout (s) + 0 + + + 0 + 2 + + + + + True + False + True + Pressure threshold + 0 + + + 0 + 3 + + + + + + + + + + + + + + False + True + 0 + + + + diff --git a/extensions/dash-to-dock/appIconIndicators.js b/extensions/dash-to-dock/appIconIndicators.js new file mode 100644 index 0000000..6eb0706 --- /dev/null +++ b/extensions/dash-to-dock/appIconIndicators.js @@ -0,0 +1,1124 @@ +const Cogl = imports.gi.Cogl; +const Cairo = imports.cairo; +const Clutter = imports.gi.Clutter; +const GdkPixbuf = imports.gi.GdkPixbuf +const Gio = imports.gi.Gio; +const Gtk = imports.gi.Gtk; +const Lang = imports.lang; +const Pango = imports.gi.Pango; +const Shell = imports.gi.Shell; +const St = imports.gi.St; + +const Util = imports.misc.util; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Utils = Me.imports.utils; + +let tracker = Shell.WindowTracker.get_default(); + +const RunningIndicatorStyle = { + DEFAULT: 0, + DOTS: 1, + SQUARES: 2, + DASHES: 3, + SEGMENTED: 4, + SOLID: 5, + CILIORA: 6, + METRO: 7 +}; + +const MAX_WINDOWS_CLASSES = 4; + + +/* + * This is the main indicator class to be used. The desired bahviour is + * obtained by composing the desired classes below based on the settings. + * + */ +var AppIconIndicator = new Lang.Class({ + + Name: 'DashToDock.AppIconIndicator', + + _init: function(source, settings) { + this._indicators = []; + + // Unity indicators always enabled for now + let unityIndicator = new UnityIndicator(source, settings); + this._indicators.push(unityIndicator); + + // Choose the style for the running indicators + let runningIndicator = null; + let runningIndicatorStyle; + + if (settings.get_boolean('apply-custom-theme' )) { + runningIndicatorStyle = RunningIndicatorStyle.DOTS; + } else { + runningIndicatorStyle = settings.get_enum('running-indicator-style'); + } + + switch (runningIndicatorStyle) { + case RunningIndicatorStyle.DEFAULT: + runningIndicator = new RunningIndicatorBase(source, settings); + break; + + case RunningIndicatorStyle.DOTS: + runningIndicator = new RunningIndicatorDots(source, settings); + break; + + case RunningIndicatorStyle.SQUARES: + runningIndicator = new RunningIndicatorSquares(source, settings); + break; + + case RunningIndicatorStyle.DASHES: + runningIndicator = new RunningIndicatorDashes(source, settings); + break; + + case RunningIndicatorStyle.SEGMENTED: + runningIndicator = new RunningIndicatorSegmented(source, settings); + break; + + case RunningIndicatorStyle.SOLID: + runningIndicator = new RunningIndicatorSolid(source, settings); + break; + + case RunningIndicatorStyle.CILIORA: + runningIndicator = new RunningIndicatorCiliora(source, settings); + break; + + case RunningIndicatorStyle.METRO: + runningIndicator = new RunningIndicatorMetro(source, settings); + break; + + default: + runningIndicator = new RunningIndicatorBase(source, settings); + } + + this._indicators.push(runningIndicator); + }, + + update: function() { + for (let i=0; i 0) + this._isFocused = true; + else + this._isFocused = false; + + // In the case of workspace isolation, we need to hide the dots of apps with + // no windows in the current workspace + if (this._source.app.state != Shell.AppState.STOPPED && this._nWindows > 0) + this._isRunning = true; + else + this._isRunning = false; + + this._updateCounterClass(); + this._updateFocusClass(); + this._updateDefaultDot(); + }, + + _updateCounterClass: function() { + for (let i = 1; i <= MAX_WINDOWS_CLASSES; i++) { + let className = 'running' + i; + if (i != this._nWindows) + this._source.actor.remove_style_class_name(className); + else + this._source.actor.add_style_class_name(className); + } + }, + + _updateFocusClass: function() { + if (this._isFocused) + this._source.actor.add_style_class_name('focused'); + else + this._source.actor.remove_style_class_name('focused'); + }, + + _updateDefaultDot: function() { + if (this._isRunning) + this._source._dot.show(); + else + this._source._dot.hide(); + }, + + _hideDefaultDot: function() { + // I use opacity to hide the default dot because the show/hide function + // are used by the parent class. + this._source._dot.opacity = 0; + }, + + _restoreDefaultDot: function() { + this._source._dot.opacity = 255; + }, + + _enableBacklight: function() { + + let colorPalette = this._dominantColorExtractor._getColorPalette(); + + // Fallback + if (colorPalette === null) { + this._source._iconContainer.set_style( + 'border-radius: 5px;' + + 'background-gradient-direction: vertical;' + + 'background-gradient-start: #e0e0e0;' + + 'background-gradient-end: darkgray;' + ); + + return; + } + + this._source._iconContainer.set_style( + 'border-radius: 5px;' + + 'background-gradient-direction: vertical;' + + 'background-gradient-start: ' + colorPalette.original + ';' + + 'background-gradient-end: ' + colorPalette.darker + ';' + ); + + }, + + _disableBacklight: function() { + this._source._iconContainer.set_style(null); + }, + + destroy: function() { + this.parent(); + this._disableBacklight(); + // Remove glossy background if the children still exists + if (this._source._iconContainer.get_children().length > 1) + this._source._iconContainer.get_children()[1].set_style(null); + this._restoreDefaultDot(); + } +}); + + +const RunningIndicatorDots = new Lang.Class({ + + Name: 'DashToDock.RunningIndicatorDots', + Extends: RunningIndicatorBase, + + _init: function(source, settings) { + + this.parent(source, settings) + + this._hideDefaultDot(); + + this._area = new St.DrawingArea({x_expand: true, y_expand: true}); + + // We draw for the bottom case and rotate the canvas for other placements + //set center of rotatoins to the center + this._area.set_pivot_point(0.5, 0.5); + // prepare transformation matrix + let m = new Cogl.Matrix(); + m.init_identity(); + + switch (this._side) { + case St.Side.TOP: + m.xx = -1; + m.rotate(180, 0, 0, 1); + break + + case St.Side.BOTTOM: + // nothing + break; + + case St.Side.LEFT: + m.yy = -1; + m.rotate(90, 0, 0, 1); + break; + + case St.Side.RIGHT: + m.rotate(-90, 0, 0, 1); + break + } + + this._area.set_transform(m); + + this._area.connect('repaint', Lang.bind(this, this._updateIndicator)); + this._source._iconContainer.add_child(this._area); + + let keys = ['custom-theme-running-dots-color', + 'custom-theme-running-dots-border-color', + 'custom-theme-running-dots-border-width', + 'custom-theme-customize-running-dots', + 'unity-backlit-items', + 'running-indicator-dominant-color']; + + keys.forEach(function(key) { + this._signalsHandler.add([ + this._settings, + 'changed::' + key, + Lang.bind(this, this.update) + ]); + }, this); + + // Apply glossy background + // TODO: move to enable/disableBacklit to apply itonly to the running apps? + // TODO: move to css class for theming support + let path = imports.misc.extensionUtils.getCurrentExtension().path; + this._glossyBackgroundStyle = 'background-image: url(\'' + path + '/media/glossy.svg\');' + + 'background-size: contain;'; + + }, + + update: function() { + this.parent(); + + // Enable / Disable the backlight of running apps + if (!this._settings.get_boolean('apply-custom-theme') && this._settings.get_boolean('unity-backlit-items')) { + this._source._iconContainer.get_children()[1].set_style(this._glossyBackgroundStyle); + if (this._isRunning) + this._enableBacklight(); + else + this._disableBacklight(); + } else { + this._disableBacklight(); + this._source._iconContainer.get_children()[1].set_style(null); + } + + if (this._area) + this._area.queue_repaint(); + }, + + _computeStyle: function() { + + let [width, height] = this._area.get_surface_size(); + this._width = height; + this._height = width; + + // By defaut re-use the style - background color, and border width and color - + // of the default dot + let themeNode = this._source._dot.get_theme_node(); + this._borderColor = themeNode.get_border_color(this._side); + this._borderWidth = themeNode.get_border_width(this._side); + this._bodyColor = themeNode.get_background_color(); + + if (!this._settings.get_boolean('apply-custom-theme')) { + // Adjust for the backlit case + if (this._settings.get_boolean('unity-backlit-items')) { + // Use dominant color for dots too if the backlit is enables + let colorPalette = this._dominantColorExtractor._getColorPalette(); + + // Slightly adjust the styling + this._borderWidth = 2; + + if (colorPalette !== null) { + this._borderColor = Clutter.color_from_string(colorPalette.lighter)[1] ; + this._bodyColor = Clutter.color_from_string(colorPalette.darker)[1]; + } else { + // Fallback + this._borderColor = Clutter.color_from_string('white')[1]; + this._bodyColor = Clutter.color_from_string('gray')[1]; + } + } + + // Apply dominant color if requested + if (this._settings.get_boolean('running-indicator-dominant-color')) { + let colorPalette = this._dominantColorExtractor._getColorPalette(); + if (colorPalette !== null) { + this._bodyColor = Clutter.color_from_string(colorPalette.original)[1]; + } + } + + // Finally, use customize style if requested + if (this._settings.get_boolean('custom-theme-customize-running-dots')) { + this._borderColor = Clutter.color_from_string(this._settings.get_string('custom-theme-running-dots-border-color'))[1]; + this._borderWidth = this._settings.get_int('custom-theme-running-dots-border-width'); + this._bodyColor = Clutter.color_from_string(this._settings.get_string('custom-theme-running-dots-color'))[1]; + } + } + + // Define the radius as an arbitrary size, but keep large enough to account + // for the drawing of the border. + this._radius = Math.max(this._width/22, this._borderWidth/2); + this._padding = 0; // distance from the margin + this._spacing = this._radius + this._borderWidth; // separation between the dots + }, + + _updateIndicator: function() { + + let area = this._area; + let cr = this._area.get_context(); + + this._computeStyle(); + this._drawIndicator(cr); + cr.$dispose(); + }, + + _drawIndicator: function(cr) { + // Draw the required numbers of dots + let n = this._nWindows; + + cr.setLineWidth(this._borderWidth); + Clutter.cairo_set_source_color(cr, this._borderColor); + + // draw for the bottom case: + cr.translate((this._width - (2*n)*this._radius - (n-1)*this._spacing)/2, this._height - this._padding); + for (let i = 0; i < n; i++) { + cr.newSubPath(); + cr.arc((2*i+1)*this._radius + i*this._spacing, -this._radius - this._borderWidth/2, this._radius, 0, 2*Math.PI); + } + + cr.strokePreserve(); + Clutter.cairo_set_source_color(cr, this._bodyColor); + cr.fill(); + }, + + destroy: function() { + this.parent(); + this._area.destroy(); + } + +}); + +// Adapted from dash-to-panel by Jason DeRose +// https://github.com/jderose9/dash-to-panel +const RunningIndicatorCiliora = new Lang.Class({ + + Name: 'DashToDock.RunningIndicatorCiliora', + Extends: RunningIndicatorDots, + + _drawIndicator: function(cr) { + if (this._isRunning) { + + let size = Math.max(this._width/20, this._borderWidth); + let spacing = size; // separation between the dots + let lineLength = this._width - (size*(this._nWindows-1)) - (spacing*(this._nWindows-1)); + let padding = this._borderWidth; + // For the backlit case here we don't want the outer border visible + if (this._settings.get_boolean('unity-backlit-items') && !this._settings.get_boolean('custom-theme-customize-running-dots')) + padding = 0; + let yOffset = this._height - padding - size; + + cr.setLineWidth(this._borderWidth); + Clutter.cairo_set_source_color(cr, this._borderColor); + + cr.translate(0, yOffset); + cr.newSubPath(); + cr.rectangle(0, 0, lineLength, size); + for (let i = 1; i < this._nWindows; i++) { + cr.newSubPath(); + cr.rectangle(lineLength + (i*spacing) + ((i-1)*size), 0, size, size); + } + + cr.strokePreserve(); + Clutter.cairo_set_source_color(cr, this._bodyColor); + cr.fill(); + } + } +}); + +// Adapted from dash-to-panel by Jason DeRose +// https://github.com/jderose9/dash-to-panel +const RunningIndicatorSegmented = new Lang.Class({ + + Name: 'DashToDock.RunningIndicatorSegmented', + Extends: RunningIndicatorDots, + + _drawIndicator: function(cr) { + if (this._isRunning) { + let size = Math.max(this._width/20, this._borderWidth); + let spacing = Math.ceil(this._width/18); // separation between the dots + let dashLength = Math.ceil((this._width - ((this._nWindows-1)*spacing))/this._nWindows); + let lineLength = this._width - (size*(this._nWindows-1)) - (spacing*(this._nWindows-1)); + let padding = this._borderWidth; + // For the backlit case here we don't want the outer border visible + if (this._settings.get_boolean('unity-backlit-items') && !this._settings.get_boolean('custom-theme-customize-running-dots')) + padding = 0; + let yOffset = this._height - padding - size; + + cr.setLineWidth(this._borderWidth); + Clutter.cairo_set_source_color(cr, this._borderColor); + + cr.translate(0, yOffset); + for (let i = 0; i < this._nWindows; i++) { + cr.newSubPath(); + cr.rectangle(i*dashLength + i*spacing, 0, dashLength, size); + } + + cr.strokePreserve(); + Clutter.cairo_set_source_color(cr, this._bodyColor); + cr.fill() + } + } +}); + +// Adapted from dash-to-panel by Jason DeRose +// https://github.com/jderose9/dash-to-panel +const RunningIndicatorSolid = new Lang.Class({ + + Name: 'DashToDock.RunningIndicatorSolid', + Extends: RunningIndicatorDots, + + _drawIndicator: function(cr) { + if (this._isRunning) { + + let size = Math.max(this._width/20, this._borderWidth); + let padding = this._borderWidth; + // For the backlit case here we don't want the outer border visible + if (this._settings.get_boolean('unity-backlit-items') && !this._settings.get_boolean('custom-theme-customize-running-dots')) + padding = 0; + let yOffset = this._height - padding - size; + + cr.setLineWidth(this._borderWidth); + Clutter.cairo_set_source_color(cr, this._borderColor); + + cr.translate(0, yOffset); + cr.newSubPath(); + cr.rectangle(0, 0, this._width, size); + + cr.strokePreserve(); + Clutter.cairo_set_source_color(cr, this._bodyColor); + cr.fill(); + + } + } +}); + +// Adapted from dash-to-panel by Jason DeRose +// https://github.com/jderose9/dash-to-panel +const RunningIndicatorSquares = new Lang.Class({ + + Name: 'DashToDock.RunningIndicatorSquares', + Extends: RunningIndicatorDots, + + _drawIndicator: function(cr) { + if (this._isRunning) { + let size = Math.max(this._width/11, this._borderWidth); + let padding = this._borderWidth; + let spacing = Math.ceil(this._width/18); // separation between the dots + let yOffset = this._height - padding - size; + + cr.setLineWidth(this._borderWidth); + Clutter.cairo_set_source_color(cr, this._borderColor); + + cr.translate(Math.floor((this._width - this._nWindows*size - (this._nWindows-1)*spacing)/2), yOffset); + for (let i = 0; i < this._nWindows; i++) { + cr.newSubPath(); + cr.rectangle(i*size + i*spacing, 0, size, size); + } + cr.strokePreserve(); + Clutter.cairo_set_source_color(cr, this._bodyColor); + cr.fill(); + } + } +}); + +// Adapted from dash-to-panel by Jason DeRose +// https://github.com/jderose9/dash-to-panel +const RunningIndicatorDashes = new Lang.Class({ + + Name: 'DashToDock.RunningIndicatorDashes', + Extends: RunningIndicatorDots, + + _drawIndicator: function(cr) { + if (this._isRunning) { + let size = Math.max(this._width/20, this._borderWidth); + let padding = this._borderWidth; + let spacing = Math.ceil(this._width/18); // separation between the dots + let dashLength = Math.floor(this._width/4) - spacing; + let yOffset = this._height - padding - size; + + cr.setLineWidth(this._borderWidth); + Clutter.cairo_set_source_color(cr, this._borderColor); + + cr.translate(Math.floor((this._width - this._nWindows*dashLength - (this._nWindows-1)*spacing)/2), yOffset); + for (let i = 0; i < this._nWindows; i++) { + cr.newSubPath(); + cr.rectangle(i*dashLength + i*spacing, 0, dashLength, size); + } + + cr.strokePreserve(); + Clutter.cairo_set_source_color(cr, this._bodyColor); + cr.fill(); + } + } +}); + +// Adapted from dash-to-panel by Jason DeRose +// https://github.com/jderose9/dash-to-panel +const RunningIndicatorMetro = new Lang.Class({ + + Name: 'DashToDock.RunningIndicatorMetro', + Extends: RunningIndicatorDots, + + _init: function(source, settings) { + this.parent(source, settings); + this._source.actor.add_style_class_name('metro'); + }, + + _drawIndicator: function(cr) { + if (this._isRunning) { + let size = Math.max(this._width/20, this._borderWidth); + let padding = 0; + // For the backlit case here we don't want the outer border visible + if (this._settings.get_boolean('unity-backlit-items') && !this._settings.get_boolean('custom-theme-customize-running-dots')) + padding = 0; + let yOffset = this._height - padding - size; + + let n = this._nWindows; + if(n <= 1) { + cr.translate(0, yOffset); + Clutter.cairo_set_source_color(cr, this._bodyColor); + cr.newSubPath(); + cr.rectangle(0, 0, this._width, size); + cr.fill(); + } else { + let blackenedLength = (1/48)*this._width; // need to scale with the SVG for the stacked highlight + let darkenedLength = this._isFocused ? (2/48)*this._width : (10/48)*this._width; + let blackenedColor = this._bodyColor.shade(.3); + let darkenedColor = this._bodyColor.shade(.7); + + cr.translate(0, yOffset); + + Clutter.cairo_set_source_color(cr, this._bodyColor); + cr.newSubPath(); + cr.rectangle(0, 0, this._width - darkenedLength - blackenedLength, size); + cr.fill(); + Clutter.cairo_set_source_color(cr, blackenedColor); + cr.newSubPath(); + cr.rectangle(this._width - darkenedLength - blackenedLength, 0, 1, size); + cr.fill(); + Clutter.cairo_set_source_color(cr, darkenedColor); + cr.newSubPath(); + cr.rectangle(this._width - darkenedLength, 0, darkenedLength, size); + cr.fill(); + } + } + }, + + destroy: function() { + this.parent(); + this._source.actor.remove_style_class_name('metro'); + } +}); + +/* + * Unity like notification and progress indicators + */ +const UnityIndicator = new Lang.Class({ + Name: 'DashToDock.UnityIndicator', + Extends: IndicatorBase, + + _init: function(source, settings) { + + this.parent(source, settings); + + this._notificationBadgeLabel = new St.Label(); + this._notificationBadgeBin = new St.Bin({ + child: this._notificationBadgeLabel, + x_align: St.Align.END, y_align: St.Align.START, + x_expand: true, y_expand: true + }); + this._notificationBadgeLabel.add_style_class_name('notification-badge'); + this._notificationBadgeCount = 0; + this._notificationBadgeBin.hide(); + + this._source._iconContainer.add_child(this._notificationBadgeBin); + this._source._iconContainer.connect('allocation-changed', Lang.bind(this, this.updateNotificationBadge)); + + this._remoteEntries = []; + this._source.remoteModel.lookupById(this._source.app.id).forEach( + Lang.bind(this, function(entry) { + this.insertEntryRemote(entry); + }) + ); + + this._signalsHandler.add([ + this._source.remoteModel, + 'entry-added', + Lang.bind(this, this._onLauncherEntryRemoteAdded) + ], [ + this._source.remoteModel, + 'entry-removed', + Lang.bind(this, this._onLauncherEntryRemoteRemoved) + ]) + }, + + _onLauncherEntryRemoteAdded: function(remoteModel, entry) { + if (!entry || !entry.appId()) + return; + if (this._source && this._source.app && this._source.app.id == entry.appId()) { + this.insertEntryRemote(entry); + } + }, + + _onLauncherEntryRemoteRemoved: function(remoteModel, entry) { + if (!entry || !entry.appId()) + return; + + if (this._source && this._source.app && this._source.app.id == entry.appId()) { + this.removeEntryRemote(entry); + } + }, + + updateNotificationBadge: function() { + let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; + let [minWidth, natWidth] = this._source._iconContainer.get_preferred_width(-1); + let logicalNatWidth = natWidth / scaleFactor; + let font_size = Math.max(10, Math.round(logicalNatWidth / 5)); + let margin_left = Math.round(logicalNatWidth / 4); + + this._notificationBadgeLabel.set_style( + 'font-size: ' + font_size + 'px;' + + 'margin-left: ' + margin_left + 'px;' + ); + + this._notificationBadgeBin.width = Math.round(logicalNatWidth - margin_left); + this._notificationBadgeLabel.clutter_text.ellipsize = Pango.EllipsizeMode.MIDDLE; + }, + + _notificationBadgeCountToText: function(count) { + if (count <= 9999) { + return count.toString(); + } else if (count < 1e5) { + let thousands = count / 1e3; + return thousands.toFixed(1).toString() + "k"; + } else if (count < 1e6) { + let thousands = count / 1e3; + return thousands.toFixed(0).toString() + "k"; + } else if (count < 1e8) { + let millions = count / 1e6; + return millions.toFixed(1).toString() + "M"; + } else if (count < 1e9) { + let millions = count / 1e6; + return millions.toFixed(0).toString() + "M"; + } else { + let billions = count / 1e9; + return billions.toFixed(1).toString() + "B"; + } + }, + + setNotificationBadge: function(count) { + this._notificationBadgeCount = count; + let text = this._notificationBadgeCountToText(count); + this._notificationBadgeLabel.set_text(text); + }, + + toggleNotificationBadge: function(activate) { + if (activate && this._notificationBadgeCount > 0) { + this.updateNotificationBadge(); + this._notificationBadgeBin.show(); + } + else + this._notificationBadgeBin.hide(); + }, + + _showProgressOverlay: function() { + if (this._progressOverlayArea) { + this._updateProgressOverlay(); + return; + } + + this._progressOverlayArea = new St.DrawingArea({x_expand: true, y_expand: true}); + this._progressOverlayArea.connect('repaint', Lang.bind(this, function() { + this._drawProgressOverlay(this._progressOverlayArea); + })); + + this._source._iconContainer.add_child(this._progressOverlayArea); + this._updateProgressOverlay(); + }, + + _hideProgressOverlay: function() { + if (this._progressOverlayArea) + this._progressOverlayArea.destroy(); + this._progressOverlayArea = null; + }, + + _updateProgressOverlay: function() { + if (this._progressOverlayArea) + this._progressOverlayArea.queue_repaint(); + }, + + _drawProgressOverlay: function(area) { + let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; + let [surfaceWidth, surfaceHeight] = area.get_surface_size(); + let cr = area.get_context(); + + let iconSize = this._source.icon.iconSize * scaleFactor; + + let x = Math.floor((surfaceWidth - iconSize) / 2); + let y = Math.floor((surfaceHeight - iconSize) / 2); + + let lineWidth = Math.floor(1.0 * scaleFactor); + let padding = Math.floor(iconSize * 0.05); + let width = iconSize - 2.0*padding; + let height = Math.floor(Math.min(18.0*scaleFactor, 0.20*iconSize)); + x += padding; + y += iconSize - height - padding; + + cr.setLineWidth(lineWidth); + + // Draw the outer stroke + let stroke = new Cairo.LinearGradient(0, y, 0, y + height); + let fill = null; + stroke.addColorStopRGBA(0.5, 0.5, 0.5, 0.5, 0.1); + stroke.addColorStopRGBA(0.9, 0.8, 0.8, 0.8, 0.4); + Utils.drawRoundedLine(cr, x + lineWidth/2.0, y + lineWidth/2.0, width, height, true, true, stroke, fill); + + // Draw the background + x += lineWidth; + y += lineWidth; + width -= 2.0*lineWidth; + height -= 2.0*lineWidth; + + stroke = Cairo.SolidPattern.createRGBA(0.20, 0.20, 0.20, 0.9); + fill = new Cairo.LinearGradient(0, y, 0, y + height); + fill.addColorStopRGBA(0.4, 0.25, 0.25, 0.25, 1.0); + fill.addColorStopRGBA(0.9, 0.35, 0.35, 0.35, 1.0); + Utils.drawRoundedLine(cr, x + lineWidth/2.0, y + lineWidth/2.0, width, height, true, true, stroke, fill); + + // Draw the finished bar + x += lineWidth; + y += lineWidth; + width -= 2.0*lineWidth; + height -= 2.0*lineWidth; + + let finishedWidth = Math.ceil(this._progress * width); + stroke = Cairo.SolidPattern.createRGBA(0.8, 0.8, 0.8, 1.0); + fill = Cairo.SolidPattern.createRGBA(0.9, 0.9, 0.9, 1.0); + + if (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL) + Utils.drawRoundedLine(cr, x + lineWidth/2.0 + width - finishedWidth, y + lineWidth/2.0, finishedWidth, height, true, true, stroke, fill); + else + Utils.drawRoundedLine(cr, x + lineWidth/2.0, y + lineWidth/2.0, finishedWidth, height, true, true, stroke, fill); + + cr.$dispose(); + }, + + setProgress: function(progress) { + this._progress = Math.min(Math.max(progress, 0.0), 1.0); + this._updateProgressOverlay(); + }, + + toggleProgressOverlay: function(activate) { + if (activate) { + this._showProgressOverlay(); + } + else { + this._hideProgressOverlay(); + } + }, + + insertEntryRemote: function(remote) { + if (!remote || this._remoteEntries.indexOf(remote) !== -1) + return; + + this._remoteEntries.push(remote); + this._selectEntryRemote(remote); + }, + + removeEntryRemote: function(remote) { + if (!remote || this._remoteEntries.indexOf(remote) == -1) + return; + + this._remoteEntries.splice(this._remoteEntries.indexOf(remote), 1); + + if (this._remoteEntries.length > 0) { + this._selectEntryRemote(this._remoteEntries[this._remoteEntries.length-1]); + } else { + this.setNotificationBadge(0); + this.toggleNotificationBadge(false); + this.setProgress(0); + this.toggleProgressOverlay(false); + } + }, + + _selectEntryRemote: function(remote) { + if (!remote) + return; + + this._signalsHandler.removeWithLabel('entry-remotes'); + + this._signalsHandler.addWithLabel('entry-remotes', + [ + remote, + 'count-changed', + Lang.bind(this, (remote, value) => { + this.setNotificationBadge(value); + }) + ], [ + remote, + 'count-visible-changed', + Lang.bind(this, (remote, value) => { + this.toggleNotificationBadge(value); + }) + ], [ + remote, + 'progress-changed', + Lang.bind(this, (remote, value) => { + this.setProgress(value); + }) + ], [ + remote, + 'progress-visible-changed', + Lang.bind(this, (remote, value) => { + this.toggleProgressOverlay(value); + }) + ]); + + this.setNotificationBadge(remote.count()); + this.toggleNotificationBadge(remote.countVisible()); + this.setProgress(remote.progress()); + this.toggleProgressOverlay(remote.progressVisible()); + } +}); + + +// We need an icons theme object, this is the only way I managed to get +// pixel buffers that can be used for calculating the backlight color +let themeLoader = null; + +// Global icon cache. Used for Unity7 styling. +let iconCacheMap = new Map(); +// Max number of items to store +// We don't expect to ever reach this number, but let's put an hard limit to avoid +// even the remote possibility of the cached items to grow indefinitely. +const MAX_CACHED_ITEMS = 1000; +// When the size exceed it, the oldest 'n' ones are deleted +const BATCH_SIZE_TO_DELETE = 50; +// The icon size used to extract the dominant color +const DOMINANT_COLOR_ICON_SIZE = 64; + +// Compute dominant color frim the app icon. +// The color is cached for efficiency. +const DominantColorExtractor = new Lang.Class({ + Name: 'DashToDock.DominantColorExtractor', + + _init: function(app) { + this._app = app; + }, + + /** + * Try to get the pixel buffer for the current icon, if not fail gracefully + */ + _getIconPixBuf: function() { + let iconTexture = this._app.create_icon_texture(16); + + if (themeLoader === null) { + let ifaceSettings = new Gio.Settings({ schema: "org.gnome.desktop.interface" }); + + themeLoader = new Gtk.IconTheme(), + themeLoader.set_custom_theme(ifaceSettings.get_string('icon-theme')); // Make sure the correct theme is loaded + } + + // Unable to load the icon texture, use fallback + if (iconTexture instanceof St.Icon === false) { + return null; + } + + iconTexture = iconTexture.get_gicon(); + + // Unable to load the icon texture, use fallback + if (iconTexture === null) { + return null; + } + + if (iconTexture instanceof Gio.FileIcon) { + // Use GdkPixBuf to load the pixel buffer from the provided file path + return GdkPixbuf.Pixbuf.new_from_file(iconTexture.get_file().get_path()); + } + + // Get the pixel buffer from the icon theme + let icon_info = themeLoader.lookup_icon(iconTexture.get_names()[0], DOMINANT_COLOR_ICON_SIZE, 0); + if (icon_info !== null) + return icon_info.load_icon(); + else + return null; + }, + + /** + * The backlight color choosing algorithm was mostly ported to javascript from the + * Unity7 C++ source of Canonicals: + * https://bazaar.launchpad.net/~unity-team/unity/trunk/view/head:/launcher/LauncherIcon.cpp + * so it more or less works the same way. + */ + _getColorPalette: function() { + if (iconCacheMap.get(this._app.get_id())) { + // We already know the answer + return iconCacheMap.get(this._app.get_id()); + } + + let pixBuf = this._getIconPixBuf(); + if (pixBuf == null) + return null; + + let pixels = pixBuf.get_pixels(), + offset = 0; + + let total = 0, + rTotal = 0, + gTotal = 0, + bTotal = 0; + + let resample_y = 1, + resample_x = 1; + + // Resampling of large icons + // We resample icons larger than twice the desired size, as the resampling + // to a size s + // DOMINANT_COLOR_ICON_SIZE < s < 2*DOMINANT_COLOR_ICON_SIZE, + // most of the case exactly DOMINANT_COLOR_ICON_SIZE as the icon size is tipycally + // a multiple of it. + let width = pixBuf.get_width(); + let height = pixBuf.get_height(); + + // Resample + if (height >= 2* DOMINANT_COLOR_ICON_SIZE) + resample_y = Math.floor(height/DOMINANT_COLOR_ICON_SIZE); + + if (width >= 2* DOMINANT_COLOR_ICON_SIZE) + resample_x = Math.floor(width/DOMINANT_COLOR_ICON_SIZE); + + if (resample_x !==1 || resample_y !== 1) + pixels = this._resamplePixels(pixels, resample_x, resample_y); + + // computing the limit outside the for (where it would be repeated at each iteration) + // for performance reasons + let limit = pixels.length; + for (let offset = 0; offset < limit; offset+=4) { + let r = pixels[offset], + g = pixels[offset + 1], + b = pixels[offset + 2], + a = pixels[offset + 3]; + + let saturation = (Math.max(r,g, b) - Math.min(r,g, b)); + let relevance = 0.1 * 255 * 255 + 0.9 * a * saturation; + + rTotal += r * relevance; + gTotal += g * relevance; + bTotal += b * relevance; + + total += relevance; + } + + total = total * 255; + + let r = rTotal / total, + g = gTotal / total, + b = bTotal / total; + + let hsv = Utils.ColorUtils.RGBtoHSV(r * 255, g * 255, b * 255); + + if (hsv.s > 0.15) + hsv.s = 0.65; + hsv.v = 0.90; + + let rgb = Utils.ColorUtils.HSVtoRGB(hsv.h, hsv.s, hsv.v); + + // Cache the result. + let backgroundColor = { + lighter: Utils.ColorUtils.ColorLuminance(rgb.r, rgb.g, rgb.b, 0.2), + original: Utils.ColorUtils.ColorLuminance(rgb.r, rgb.g, rgb.b, 0), + darker: Utils.ColorUtils.ColorLuminance(rgb.r, rgb.g, rgb.b, -0.5) + }; + + if (iconCacheMap.size >= MAX_CACHED_ITEMS) { + //delete oldest cached values (which are in order of insertions) + let ctr=0; + for (let key of iconCacheMap.keys()) { + if (++ctr > BATCH_SIZE_TO_DELETE) + break; + iconCacheMap.delete(key); + } + } + + iconCacheMap.set(this._app.get_id(), backgroundColor); + + return backgroundColor; + }, + + /** + * Downsample large icons before scanning for the backlight color to + * improve performance. + * + * @param pixBuf + * @param pixels + * @param resampleX + * @param resampleY + * + * @return []; + */ + _resamplePixels: function (pixels, resampleX, resampleY) { + let resampledPixels = []; + // computing the limit outside the for (where it would be repeated at each iteration) + // for performance reasons + let limit = pixels.length / (resampleX * resampleY) / 4; + for (let i = 0; i < limit; i++) { + let pixel = i * resampleX * resampleY; + + resampledPixels.push(pixels[pixel * 4]); + resampledPixels.push(pixels[pixel * 4 + 1]); + resampledPixels.push(pixels[pixel * 4 + 2]); + resampledPixels.push(pixels[pixel * 4 + 3]); + } + + return resampledPixels; + }, +}) diff --git a/extensions/dash-to-dock/appIcons.js b/extensions/dash-to-dock/appIcons.js new file mode 100644 index 0000000..ef9c6e1 --- /dev/null +++ b/extensions/dash-to-dock/appIcons.js @@ -0,0 +1,1171 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Clutter = imports.gi.Clutter; +const GdkPixbuf = imports.gi.GdkPixbuf +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Gtk = imports.gi.Gtk; +const Signals = imports.signals; +const Lang = imports.lang; +const Meta = imports.gi.Meta; +const Shell = imports.gi.Shell; +const St = imports.gi.St; +const Mainloop = imports.mainloop; + +// Use __ () and N__() for the extension gettext domain, and reuse +// the shell domain with the default _() and N_() +const Gettext = imports.gettext.domain('dashtodock'); +const __ = Gettext.gettext; +const N__ = function(e) { return e }; + +const AppDisplay = imports.ui.appDisplay; +const AppFavorites = imports.ui.appFavorites; +const Dash = imports.ui.dash; +const DND = imports.ui.dnd; +const IconGrid = imports.ui.iconGrid; +const Main = imports.ui.main; +const PopupMenu = imports.ui.popupMenu; +const Tweener = imports.ui.tweener; +const Util = imports.misc.util; +const Workspace = imports.ui.workspace; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Utils = Me.imports.utils; +const WindowPreview = Me.imports.windowPreview; +const AppIconIndicators = Me.imports.appIconIndicators; + +let tracker = Shell.WindowTracker.get_default(); + +let DASH_ITEM_LABEL_SHOW_TIME = Dash.DASH_ITEM_LABEL_SHOW_TIME; + +const clickAction = { + SKIP: 0, + MINIMIZE: 1, + LAUNCH: 2, + CYCLE_WINDOWS: 3, + MINIMIZE_OR_OVERVIEW: 4, + PREVIEWS: 5, + MINIMIZE_OR_PREVIEWS: 6, + QUIT: 7 +}; + +const scrollAction = { + DO_NOTHING: 0, + CYCLE_WINDOWS: 1, + SWITCH_WORKSPACE: 2 +}; + +let recentlyClickedAppLoopId = 0; +let recentlyClickedApp = null; +let recentlyClickedAppWindows = null; +let recentlyClickedAppIndex = 0; +let recentlyClickedAppMonitor = -1; + +/** + * Extend AppIcon + * + * - Pass settings to the constructor and bind settings changes + * - Apply a css class based on the number of windows of each application (#N); + * - Customized indicators for running applications in place of the default "dot" style which is hidden (#N); + * a class of the form "running#N" is applied to the AppWellIcon actor. + * like the original .running one. + * - Add a .focused style to the focused app + * - Customize click actions. + * - Update minimization animation target + * - Update menu if open on windows change + */ +var MyAppIcon = new Lang.Class({ + Name: 'DashToDock.AppIcon', + Extends: AppDisplay.AppIcon, + + // settings are required inside. + _init: function(settings, remoteModel, app, monitorIndex, iconParams) { + // a prefix is required to avoid conflicting with the parent class variable + this._dtdSettings = settings; + this.monitorIndex = monitorIndex; + this._signalsHandler = new Utils.GlobalSignalsHandler(); + this.remoteModel = remoteModel; + this._indicator = null; + + this.parent(app, iconParams); + + this._updateIndicatorStyle(); + + // Monitor windows-changes instead of app state. + // Keep using the same Id and function callback (that is extended) + if (this._stateChangedId > 0) { + this.app.disconnect(this._stateChangedId); + this._stateChangedId = 0; + } + + this._windowsChangedId = this.app.connect('windows-changed', + Lang.bind(this, + this.onWindowsChanged)); + this._focusAppChangeId = tracker.connect('notify::focus-app', + Lang.bind(this, + this._onFocusAppChanged)); + + // In Wayland sessions, this signal is needed to track the state of windows dragged + // from one monitor to another. As this is triggered quite often (whenever a new winow + // of any application opened or moved to a different desktop), + // we restrict this signal to the case when 'isolate-monitors' is true, + // and if there are at least 2 monitors. + if (this._dtdSettings.get_boolean('isolate-monitors') && + Main.layoutManager.monitors.length > 1) { + this._signalsHandler.removeWithLabel('isolate-monitors'); + this._signalsHandler.addWithLabel('isolate-monitors', [ + global.screen, + 'window-entered-monitor', + Lang.bind(this, this._onWindowEntered) + ]); + } + + this._progressOverlayArea = null; + this._progress = 0; + + let keys = ['apply-custom-theme', + 'running-indicator-style', + ]; + + keys.forEach(function(key) { + this._signalsHandler.add([ + this._dtdSettings, + 'changed::' + key, + Lang.bind(this, this._updateIndicatorStyle) + ]); + }, this); + + this._dtdSettings.connect('changed::scroll-action', Lang.bind(this, function() { + this._optionalScrollCycleWindows(); + })); + this._optionalScrollCycleWindows(); + + this._numberOverlay(); + + this._previewMenuManager = null; + this._previewMenu = null; + }, + + _onDestroy: function() { + this.parent(); + + // This is necessary due to an upstream bug + // https://bugzilla.gnome.org/show_bug.cgi?id=757556 + // It can be safely removed once it get solved upstrea. + if (this._menu) + this._menu.close(false); + + // Disconect global signals + + if (this._windowsChangedId > 0) + this.app.disconnect(this._windowsChangedId); + this._windowsChangedId = 0; + + if (this._focusAppChangeId > 0) { + tracker.disconnect(this._focusAppChangeId); + this._focusAppChangeId = 0; + } + + this._signalsHandler.destroy(); + + if (this._scrollEventHandler) + this.actor.disconnect(this._scrollEventHandler); + }, + + // TOOD Rename this function + _updateIndicatorStyle: function() { + + if (this._indicator !== null) { + this._indicator.destroy(); + this._indicator = null; + } + this._indicator = new AppIconIndicators.AppIconIndicator(this, this._dtdSettings); + this._indicator.update(); + }, + + _onWindowEntered: function(metaScreen, monitorIndex, metaWin) { + let app = Shell.WindowTracker.get_default().get_window_app(metaWin); + if (app && app.get_id() == this.app.get_id()) + this.onWindowsChanged(); + }, + + _optionalScrollCycleWindows: function() { + if (this._scrollEventHandler) { + this.actor.disconnect(this._scrollEventHandler); + this._scrollEventHandler = 0; + } + + let isEnabled = this._dtdSettings.get_enum('scroll-action') === scrollAction.CYCLE_WINDOWS; + if (!isEnabled) return; + this._scrollEventHandler = this.actor.connect('scroll-event', Lang.bind(this, + this.onScrollEvent)); + }, + + onScrollEvent: function(actor, event) { + + // We only activate windows of running applications, i.e. we never open new windows + // We check if the app is running, and that the # of windows is > 0 in + // case we use workspace isolation, + let appIsRunning = this.app.state == Shell.AppState.RUNNING + && this.getInterestingWindows().length > 0; + + if (!appIsRunning) + return false + + if (this._optionalScrollCycleWindowsDeadTimeId > 0) + return false; + else + this._optionalScrollCycleWindowsDeadTimeId = Mainloop.timeout_add(250, Lang.bind(this, function() { + this._optionalScrollCycleWindowsDeadTimeId = 0; + })); + + let direction = null; + + switch (event.get_scroll_direction()) { + case Clutter.ScrollDirection.UP: + direction = Meta.MotionDirection.UP; + break; + case Clutter.ScrollDirection.DOWN: + direction = Meta.MotionDirection.DOWN; + break; + case Clutter.ScrollDirection.SMOOTH: + let [dx, dy] = event.get_scroll_delta(); + if (dy < 0) + direction = Meta.MotionDirection.UP; + else if (dy > 0) + direction = Meta.MotionDirection.DOWN; + break; + } + + let focusedApp = tracker.focus_app; + if (!Main.overview._shown) { + let reversed = direction === Meta.MotionDirection.UP; + if (this.app == focusedApp) + this._cycleThroughWindows(reversed); + else { + // Activate the first window + let windows = this.getInterestingWindows(); + if (windows.length > 0) { + let w = windows[0]; + Main.activateWindow(w); + } + } + } + else + this.app.activate(); + return true; + }, + + onWindowsChanged: function() { + + if (this._menu && this._menu.isOpen) + this._menu.update(); + + this._indicator.update(); + this.updateIconGeometry(); + }, + + /** + * Update taraget for minimization animation + */ + updateIconGeometry: function() { + // If (for unknown reason) the actor is not on the stage the reported size + // and position are random values, which might exceeds the integer range + // resulting in an error when assigned to the a rect. This is a more like + // a workaround to prevent flooding the system with errors. + if (this.actor.get_stage() == null) + return; + + let rect = new Meta.Rectangle(); + + [rect.x, rect.y] = this.actor.get_transformed_position(); + [rect.width, rect.height] = this.actor.get_transformed_size(); + + let windows = this.app.get_windows(); + if (this._dtdSettings.get_boolean('multi-monitor')){ + let monitorIndex = this.monitorIndex; + windows = windows.filter(function(w) { + return w.get_monitor() == monitorIndex; + }); + } + windows.forEach(function(w) { + w.set_icon_geometry(rect); + }); + }, + + _updateRunningStyle: function() { + // The logic originally in this function has been moved to + // AppIconIndicatorBase._updateDefaultDot(). However it cannot be removed as + // it called by the parent constructor. + }, + + popupMenu: function() { + this._removeMenuTimeout(); + this.actor.fake_release(); + this._draggable.fakeRelease(); + + if (!this._menu) { + this._menu = new MyAppIconMenu(this, this._dtdSettings); + this._menu.connect('activate-window', Lang.bind(this, function(menu, window) { + this.activateWindow(window); + })); + this._menu.connect('open-state-changed', Lang.bind(this, function(menu, isPoppedUp) { + if (!isPoppedUp) + this._onMenuPoppedDown(); + 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 position = Utils.getPosition(this._dtdSettings); + this._isHorizontal = ( position == St.Side.TOP || + position == St.Side.BOTTOM); + // If horizontal also remove the height of the dash + 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 + this._menu.actor.style = ('max-height: ' + Math.round(workArea.height - additional_margin - verticalMargins) + 'px;' + + 'max-width: 400px'); + } + })); + let id = Main.overview.connect('hiding', Lang.bind(this, function() { + this._menu.close(); + })); + this._menu.actor.connect('destroy', function() { + Main.overview.disconnect(id); + }); + + this._menuManager.addMenu(this._menu); + } + + this.emit('menu-state-changed', true); + + this.actor.set_hover(true); + this._menu.popup(); + this._menuManager.ignoreRelease(); + this.emit('sync-tooltip'); + + return false; + }, + + _onFocusAppChanged: function() { + this._indicator.update(); + }, + + activate: function(button) { + let event = Clutter.get_current_event(); + let modifiers = event ? event.get_state() : 0; + let focusedApp = tracker.focus_app; + + // Only consider SHIFT and CONTROL as modifiers (exclude SUPER, CAPS-LOCK, etc.) + modifiers = modifiers & (Clutter.ModifierType.SHIFT_MASK | Clutter.ModifierType.CONTROL_MASK); + + // We don't change the CTRL-click behaviour: in such case we just chain + // up the parent method and return. + if (modifiers & Clutter.ModifierType.CONTROL_MASK) { + // Keep default behaviour: launch new window + // By calling the parent method I make it compatible + // with other extensions tweaking ctrl + click + this.parent(button); + return; + } + + // We check what type of click we have and if the modifier SHIFT is + // being used. We then define what buttonAction should be for this + // event. + let buttonAction = 0; + if (button && button == 2 ) { + if (modifiers & Clutter.ModifierType.SHIFT_MASK) + buttonAction = this._dtdSettings.get_enum('shift-middle-click-action'); + else + buttonAction = this._dtdSettings.get_enum('middle-click-action'); + } + else if (button && button == 1) { + if (modifiers & Clutter.ModifierType.SHIFT_MASK) + buttonAction = this._dtdSettings.get_enum('shift-click-action'); + else + buttonAction = this._dtdSettings.get_enum('click-action'); + } + + // We check if the app is running, and that the # of windows is > 0 in + // case we use workspace isolation. + let windows = this.getInterestingWindows(); + let appIsRunning = this.app.state == Shell.AppState.RUNNING + && windows.length > 0; + + // Some action modes (e.g. MINIMIZE_OR_OVERVIEW) require overview to remain open + // This variable keeps track of this + let shouldHideOverview = true; + + // We customize the action only when the application is already running + if (appIsRunning) { + switch (buttonAction) { + case clickAction.MINIMIZE: + // In overview just activate the app, unless the acion is explicitely + // requested with a keyboard modifier + if (!Main.overview._shown || modifiers){ + // If we have button=2 or a modifier, allow minimization even if + // the app is not focused + if (this.app == focusedApp || button == 2 || modifiers & Clutter.ModifierType.SHIFT_MASK) { + // minimize all windows on double click and always in the case of primary click without + // additional modifiers + let click_count = 0; + if (Clutter.EventType.CLUTTER_BUTTON_PRESS) + click_count = event.get_click_count(); + let all_windows = (button == 1 && ! modifiers) || click_count > 1; + this._minimizeWindow(all_windows); + } + else + this._activateAllWindows(); + } + else { + let w = windows[0]; + Main.activateWindow(w); + } + break; + + case clickAction.MINIMIZE_OR_OVERVIEW: + // When a single window is present, toggle minimization + // If only one windows is present toggle minimization, but only when trigggered with the + // simple click action (no modifiers, no middle click). + if (windows.length == 1 && !modifiers && button == 1) { + let w = windows[0]; + if (this.app == focusedApp) { + // Window is raised, minimize it + this._minimizeWindow(w); + } else { + // Window is minimized, raise it + Main.activateWindow(w); + } + // Launch overview when multiple windows are present + // TODO: only show current app windows when gnome shell API will allow it + } else { + shouldHideOverview = false; + Main.overview.toggle(); + } + break; + + case clickAction.CYCLE_WINDOWS: + if (!Main.overview._shown){ + if (this.app == focusedApp) + this._cycleThroughWindows(); + else { + // Activate the first window + let w = windows[0]; + Main.activateWindow(w); + } + } + else + this.app.activate(); + break; + + case clickAction.LAUNCH: + this.launchNewWindow(); + break; + + case clickAction.PREVIEWS: + if (!Main.overview._shown) { + // If only one windows is present just switch to it, but only when trigggered with the + // simple click action (no modifiers, no middle click). + if (windows.length == 1 && !modifiers && button == 1) { + let w = windows[0]; + Main.activateWindow(w); + } else + this._windowPreviews(); + } + else { + this.app.activate(); + } + break; + + case clickAction.MINIMIZE_OR_PREVIEWS: + // When a single window is present, toggle minimization + // If only one windows is present toggle minimization, but only when trigggered with the + // simple click action (no modifiers, no middle click). + if (!Main.overview._shown){ + if (windows.length == 1 && !modifiers && button == 1) { + let w = windows[0]; + if (this.app == focusedApp) { + // Window is raised, minimize it + this._minimizeWindow(w); + } else { + // Window is minimized, raise it + Main.activateWindow(w); + } + } else { + // Launch previews when multiple windows are present + this._windowPreviews(); + } + } else { + this.app.activate(); + } + break; + + case clickAction.QUIT: + this.closeAllWindows(); + break; + + case clickAction.SKIP: + let w = windows[0]; + Main.activateWindow(w); + break; + } + } + else { + this.launchNewWindow(); + } + + // Hide overview except when action mode requires it + if(shouldHideOverview) { + Main.overview.hide(); + } + }, + + shouldShowTooltip: function() { + return this.actor.hover && (!this._menu || !this._menu.isOpen) && + (!this._previewMenu || !this._previewMenu.isOpen); + }, + + _windowPreviews: function() { + if (!this._previewMenu) { + this._previewMenuManager = new PopupMenu.PopupMenuManager(this); + + this._previewMenu = new WindowPreview.WindowPreviewMenu(this, this._dtdSettings); + + this._previewMenuManager.addMenu(this._previewMenu); + + 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); + }); + + } + + if (this._previewMenu.isOpen) + this._previewMenu.close(); + else + this._previewMenu.popup(); + + return false; + }, + + // 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. + launchNewWindow: function(p) { + let appInfo = this.app.get_app_info(); + let actions = appInfo.list_actions(); + if (this.app.can_open_new_window()) { + this.animateLaunch(); + // This is used as a workaround for a bug resulting in no new windows being opened + // for certain running applications when calling open_new_window(). + // + // https://bugzilla.gnome.org/show_bug.cgi?id=756844 + // + // Similar to what done when generating the popupMenu entries, if the application provides + // a "New Window" action, use it instead of directly requesting a new window with + // open_new_window(), which fails for certain application, notably Nautilus. + if (actions.indexOf('new-window') == -1) { + this.app.open_new_window(-1); + } + else { + let i = actions.indexOf('new-window'); + if (i !== -1) + this.app.launch_action(actions[i], global.get_current_time(), -1); + } + } + else { + // Try to manually activate the first window. Otherwise, when the app is activated by + // switching to a different workspace, a launch spinning icon is shown and disappers only + // after a timeout. + let windows = this.app.get_windows(); + if (windows.length > 0) + Main.activateWindow(windows[0]) + else + this.app.activate(); + } + }, + + _numberOverlay: function() { + // Add label for a Hot-Key visual aid + this._numberOverlayLabel = new St.Label(); + this._numberOverlayBin = new St.Bin({ + child: this._numberOverlayLabel, + x_align: St.Align.START, y_align: St.Align.START, + x_expand: true, y_expand: true + }); + this._numberOverlayLabel.add_style_class_name('number-overlay'); + this._numberOverlayOrder = -1; + this._numberOverlayBin.hide(); + + this._iconContainer.add_child(this._numberOverlayBin); + + }, + + updateNumberOverlay: function() { + // We apply an overall scale factor that might come from a HiDPI monitor. + // Clutter dimensions are in physical pixels, but CSS measures are in logical + // pixels, so make sure to consider the scale. + let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; + // Set the font size to something smaller than the whole icon so it is + // still visible. The border radius is large to make the shape circular + let [minWidth, natWidth] = this._iconContainer.get_preferred_width(-1); + let font_size = Math.round(Math.max(12, 0.3*natWidth) / scaleFactor); + let size = Math.round(font_size*1.2); + this._numberOverlayLabel.set_style( + 'font-size: ' + font_size + 'px;' + + 'border-radius: ' + this.icon.iconSize + 'px;' + + 'width: ' + size + 'px; height: ' + size +'px;' + ); + }, + + setNumberOverlay: function(number) { + this._numberOverlayOrder = number; + this._numberOverlayLabel.set_text(number.toString()); + }, + + toggleNumberOverlay: function(activate) { + if (activate && this._numberOverlayOrder > -1) { + this.updateNumberOverlay(); + this._numberOverlayBin.show(); + } + else + this._numberOverlayBin.hide(); + }, + + _minimizeWindow: function(param) { + // Param true make all app windows minimize + let windows = this.getInterestingWindows(); + let current_workspace = global.screen.get_active_workspace(); + for (let i = 0; i < windows.length; i++) { + let w = windows[i]; + if (w.get_workspace() == current_workspace && w.showing_on_its_workspace()) { + w.minimize(); + // Just minimize one window. By specification it should be the + // focused window on the current workspace. + if(!param) + break; + } + } + }, + + // By default only non minimized windows are activated. + // This activates all windows in the current workspace. + _activateAllWindows: function() { + // First activate first window so workspace is switched if needed. + // We don't do this if isolation is on! + if (!this._dtdSettings.get_boolean('isolate-workspaces') && + !this._dtdSettings.get_boolean('isolate-monitors')) + this.app.activate(); + + // then activate all other app windows in the current workspace + let windows = this.getInterestingWindows(); + let activeWorkspace = global.screen.get_active_workspace_index(); + + if (windows.length <= 0) + return; + + let activatedWindows = 0; + + for (let i = windows.length - 1; i >= 0; i--) { + if (windows[i].get_workspace().index() == activeWorkspace) { + Main.activateWindow(windows[i]); + activatedWindows++; + } + } + }, + + //This closes all windows of the app. + closeAllWindows: function() { + let windows = this.getInterestingWindows(); + for (let i = 0; i < windows.length; i++) + windows[i].delete(global.get_current_time()); + }, + + _cycleThroughWindows: function(reversed) { + // Store for a little amount of time last clicked app and its windows + // since the order changes upon window interaction + let MEMORY_TIME=3000; + + let app_windows = this.getInterestingWindows(); + + if (app_windows.length <1) + return + + if (recentlyClickedAppLoopId > 0) + Mainloop.source_remove(recentlyClickedAppLoopId); + recentlyClickedAppLoopId = Mainloop.timeout_add(MEMORY_TIME, this._resetRecentlyClickedApp); + + // If there isn't already a list of windows for the current app, + // or the stored list is outdated, use the current windows list. + let monitorIsolation = this._dtdSettings.get_boolean('isolate-monitors'); + if (!recentlyClickedApp || + recentlyClickedApp.get_id() != this.app.get_id() || + recentlyClickedAppWindows.length != app_windows.length || + (recentlyClickedAppMonitor != this.monitorIndex && monitorIsolation)) { + recentlyClickedApp = this.app; + recentlyClickedAppWindows = app_windows; + recentlyClickedAppMonitor = this.monitorIndex; + recentlyClickedAppIndex = 0; + } + + if (reversed) { + recentlyClickedAppIndex--; + if (recentlyClickedAppIndex < 0) recentlyClickedAppIndex = recentlyClickedAppWindows.length - 1; + } else { + recentlyClickedAppIndex++; + } + let index = recentlyClickedAppIndex % recentlyClickedAppWindows.length; + let window = recentlyClickedAppWindows[index]; + + Main.activateWindow(window); + }, + + _resetRecentlyClickedApp: function() { + if (recentlyClickedAppLoopId > 0) + Mainloop.source_remove(recentlyClickedAppLoopId); + recentlyClickedAppLoopId=0; + recentlyClickedApp =null; + recentlyClickedAppWindows = null; + recentlyClickedAppIndex = 0; + recentlyClickedAppMonitor = -1; + + return false; + }, + + // Filter out unnecessary windows, for instance + // nautilus desktop window. + getInterestingWindows: function() { + return getInterestingWindows(this.app, this._dtdSettings, this.monitorIndex); + } +}); +/** + * Extend AppIconMenu + * + * - Pass settings to the constructor + * - set popup arrow side based on dash orientation + * - Add close windows option based on quitfromdash extension + * (https://github.com/deuill/shell-extension-quitfromdash) + * - Add open windows thumbnails instead of list + * - update menu when application windows change + */ +const MyAppIconMenu = new Lang.Class({ + Name: 'DashToDock.MyAppIconMenu', + Extends: AppDisplay.AppIconMenu, + + _init: function(source, settings) { + let side = Utils.getPosition(settings); + + // Damm it, there has to be a proper way of doing this... + // As I can't call the parent parent constructor (?) passing the side + // parameter, I overwite what I need later + this.parent(source); + + // Change the initialized side where required. + this._arrowSide = side; + this._boxPointer._arrowSide = side; + this._boxPointer._userArrowSide = side; + + this._dtdSettings = settings; + }, + + _redisplay: function() { + this.removeAll(); + + if (this._dtdSettings.get_boolean('show-windows-preview')) { + // Display the app windows menu items and the separator between windows + // of the current desktop and other windows. + + this._allWindowsMenuItem = new PopupMenu.PopupSubMenuMenuItem(__('All Windows'), false); + this._allWindowsMenuItem.actor.hide(); + this.addMenuItem(this._allWindowsMenuItem); + + if (!this._source.app.is_window_backed()) { + this._appendSeparator(); + + let appInfo = this._source.app.get_app_info(); + let actions = appInfo.list_actions(); + if (this._source.app.can_open_new_window() && + actions.indexOf('new-window') == -1) { + this._newWindowMenuItem = this._appendMenuItem(_("New Window")); + this._newWindowMenuItem.connect('activate', Lang.bind(this, function() { + if (this._source.app.state == Shell.AppState.STOPPED) + this._source.animateLaunch(); + + this._source.app.open_new_window(-1); + this.emit('activate-window', null); + })); + this._appendSeparator(); + } + + + if (AppDisplay.discreteGpuAvailable && + this._source.app.state == Shell.AppState.STOPPED && + actions.indexOf('activate-discrete-gpu') == -1) { + this._onDiscreteGpuMenuItem = this._appendMenuItem(_("Launch using Dedicated Graphics Card")); + this._onDiscreteGpuMenuItem.connect('activate', Lang.bind(this, function() { + if (this._source.app.state == Shell.AppState.STOPPED) + this._source.animateLaunch(); + + this._source.app.launch(0, -1, true); + this.emit('activate-window', null); + })); + } + + for (let i = 0; i < actions.length; i++) { + let action = actions[i]; + let item = this._appendMenuItem(appInfo.get_action_name(action)); + item.connect('activate', Lang.bind(this, function(emitter, event) { + this._source.app.launch_action(action, event.get_time(), -1); + this.emit('activate-window', null); + })); + } + + let canFavorite = global.settings.is_writable('favorite-apps'); + + if (canFavorite) { + this._appendSeparator(); + + let isFavorite = AppFavorites.getAppFavorites().isFavorite(this._source.app.get_id()); + + if (isFavorite) { + let item = this._appendMenuItem(_("Remove from Favorites")); + item.connect('activate', Lang.bind(this, function() { + let favs = AppFavorites.getAppFavorites(); + favs.removeFavorite(this._source.app.get_id()); + })); + } else { + let item = this._appendMenuItem(_("Add to Favorites")); + item.connect('activate', Lang.bind(this, function() { + let favs = AppFavorites.getAppFavorites(); + favs.addFavorite(this._source.app.get_id()); + })); + } + } + + if (Shell.AppSystem.get_default().lookup_app('org.gnome.Software.desktop')) { + this._appendSeparator(); + let item = this._appendMenuItem(_("Show Details")); + item.connect('activate', Lang.bind(this, function() { + let id = this._source.app.get_id(); + let args = GLib.Variant.new('(ss)', [id, '']); + Gio.DBus.get(Gio.BusType.SESSION, null, + function(o, res) { + let bus = Gio.DBus.get_finish(res); + bus.call('org.gnome.Software', + '/org/gnome/Software', + 'org.gtk.Actions', 'Activate', + GLib.Variant.new('(sava{sv})', + ['details', [args], null]), + null, 0, -1, null, null); + Main.overview.hide(); + }); + })); + } + } + + } else { + this.parent(); + } + + // quit menu + this._appendSeparator(); + this._quitfromDashMenuItem = this._appendMenuItem(_("Quit")); + this._quitfromDashMenuItem.connect('activate', Lang.bind(this, function() { + this._source.closeAllWindows(); + })); + + this.update(); + }, + + // update menu content when application windows change. This is desirable as actions + // acting on windows (closing) are performed while the menu is shown. + update: function() { + + if(this._dtdSettings.get_boolean('show-windows-preview')){ + + let windows = this._source.getInterestingWindows(); + + // update, show or hide the quit menu + if ( windows.length > 0) { + let quitFromDashMenuText = ""; + if (windows.length == 1) + this._quitfromDashMenuItem.label.set_text(_("Quit")); + else + this._quitfromDashMenuItem.label.set_text(_("Quit") + ' ' + windows.length + ' ' + _("Windows")); + + this._quitfromDashMenuItem.actor.show(); + + } else { + this._quitfromDashMenuItem.actor.hide(); + } + + // update, show, or hide the allWindows menu + // Check if there are new windows not already displayed. In such case, repopulate the allWindows + // menu. Windows removal is already handled by each preview being connected to the destroy signal + let old_windows = this._allWindowsMenuItem.menu._getMenuItems().map(function(item){ + return item._window; + }); + + let new_windows = windows.filter(function(w) {return old_windows.indexOf(w) < 0;}); + if (new_windows.length > 0) { + this._populateAllWindowMenu(windows); + + // Try to set the width to that of the submenu. + // TODO: can't get the actual size, getting a bit less. + // Temporary workaround: add 15px to compensate + this._allWindowsMenuItem.actor.width = this._allWindowsMenuItem.menu.actor.width + 15; + + } + + // The menu is created hidden and never hidded after being shown. Instead, a singlal + // connected to its items destroy will set is insensitive if no more windows preview are shown. + if (windows.length > 0){ + this._allWindowsMenuItem.actor.show(); + this._allWindowsMenuItem.setSensitive(true); + } + + // Update separators + this._getMenuItems().forEach(Lang.bind(this, this._updateSeparatorVisibility)); + } + + + }, + + _populateAllWindowMenu: function(windows) { + + this._allWindowsMenuItem.menu.removeAll(); + + if (windows.length > 0) { + + let activeWorkspace = global.screen.get_active_workspace(); + let separatorShown = windows[0].get_workspace() != activeWorkspace; + + for (let i = 0; i < windows.length; i++) { + let window = windows[i]; + if (!separatorShown && window.get_workspace() != activeWorkspace) { + this._allWindowsMenuItem.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + separatorShown = true; + } + + let item = new WindowPreview.WindowPreviewMenuItem(window); + this._allWindowsMenuItem.menu.addMenuItem(item); + item.connect('activate', Lang.bind(this, function() { + this.emit('activate-window', window); + })); + + // This is to achieve a more gracefull transition when the last windows is closed. + item.connect('destroy', Lang.bind(this, function() { + if(this._allWindowsMenuItem.menu._getMenuItems().length == 1) // It's still counting the item just going to be destroyed + this._allWindowsMenuItem.setSensitive(false); + })); + } + } + }, +}); +Signals.addSignalMethods(MyAppIconMenu.prototype); + +// Filter out unnecessary windows, for instance +// nautilus desktop window. +function getInterestingWindows(app, settings, monitorIndex) { + let windows = app.get_windows().filter(function(w) { + return !w.skip_taskbar; + }); + + // When using workspace isolation, we filter out windows + // that are not in the current workspace + if (settings.get_boolean('isolate-workspaces')) + windows = windows.filter(function(w) { + return w.get_workspace().index() == global.screen.get_active_workspace_index(); + }); + + if (settings.get_boolean('isolate-monitors')) + windows = windows.filter(function(w) { + return w.get_monitor() == monitorIndex; + }); + + return windows; +} + +/** + * A wrapper class around the ShowAppsIcon class. + * + * - Pass settings to the constructor + * - set label position based on dash orientation (Note, I am reusing most machinery of the appIcon class) + * - implement a popupMenu based on the AppIcon code (Note, I am reusing most machinery of the appIcon class) + * + * I can't subclass the original object because of this: https://bugzilla.gnome.org/show_bug.cgi?id=688973. + * thus use this pattern where the real showAppsIcon object is encaptulated, and a reference to it will be properly wired upon + * use of this class in place of the original showAppsButton. + * + */ + + var ShowAppsIconWrapper = new Lang.Class({ + Name: 'DashToDock.ShowAppsIconWrapper', + + _init: function(settings) { + this._dtdSettings = settings; + this.realShowAppsIcon = new Dash.ShowAppsIcon(); + + /* the variable equivalent to toggleButton has a different name in the appIcon class + (actor): duplicate reference to easily reuse appIcon methods */ + this.actor = this.realShowAppsIcon.toggleButton; + + // Re-use appIcon methods + this._removeMenuTimeout = AppDisplay.AppIcon.prototype._removeMenuTimeout; + this._setPopupTimeout = AppDisplay.AppIcon.prototype._setPopupTimeout; + this._onButtonPress = AppDisplay.AppIcon.prototype._onButtonPress; + this._onKeyboardPopupMenu = AppDisplay.AppIcon.prototype._onKeyboardPopupMenu; + this._onLeaveEvent = AppDisplay.AppIcon.prototype._onLeaveEvent; + this._onTouchEvent = AppDisplay.AppIcon.prototype._onTouchEvent; + this._onMenuPoppedDown = AppDisplay.AppIcon.prototype._onMenuPoppedDown; + + // No action on clicked (showing of the appsview is controlled elsewhere) + this._onClicked = Lang.bind(this, function(actor, button) { + this._removeMenuTimeout(); + }); + + this.actor.connect('leave-event', Lang.bind(this, this._onLeaveEvent)); + this.actor.connect('button-press-event', Lang.bind(this, this._onButtonPress)); + this.actor.connect('touch-event', Lang.bind(this, this._onTouchEvent)); + this.actor.connect('clicked', Lang.bind(this, this._onClicked)); + this.actor.connect('popup-menu', Lang.bind(this, this._onKeyboardPopupMenu)); + + this._menu = null; + this._menuManager = new PopupMenu.PopupMenuManager(this); + this._menuTimeoutId = 0; + + this.showLabel = itemShowLabel; + }, + + popupMenu: function() { + this._removeMenuTimeout(); + this.actor.fake_release(); + + if (!this._menu) { + this._menu = new MyShowAppsIconMenu(this, this._dtdSettings); + this._menu.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._menu.close(); + })); + this._menu.actor.connect('destroy', function() { + Main.overview.disconnect(id); + }); + this._menuManager.addMenu(this._menu); + } + + //this.emit('menu-state-changed', true); + + this.actor.set_hover(true); + this._menu.popup(); + this._menuManager.ignoreRelease(); + this.emit('sync-tooltip'); + + return false; + } +}); +Signals.addSignalMethods(ShowAppsIconWrapper.prototype); + + +/** + * A menu for the showAppsIcon + */ +const MyShowAppsIconMenu = new Lang.Class({ + Name: 'DashToDock.ShowAppsIconMenu', + Extends: MyAppIconMenu, + + _redisplay: function() { + this.removeAll(); + + /* Translators: %s is "Settings", which is automatically translated. You + can also translate the full message if this fits better your language. */ + let name = __('Dash to Dock %s').format(_('Settings')) + let item = this._appendMenuItem(name); + + item.connect('activate', function () { + Util.spawn(["gnome-shell-extension-prefs", Me.metadata.uuid]); + }); + } +}); + +/** + * This function is used for both extendShowAppsIcon and extendDashItemContainer + */ +function itemShowLabel() { + // Check if the label is still present at all. When switching workpaces, the + // item might have been destroyed in between. + if (!this._labelText || this.label.get_stage() == null) + return; + + this.label.set_text(this._labelText); + this.label.opacity = 0; + this.label.show(); + + let [stageX, stageY] = this.get_transformed_position(); + let node = this.label.get_theme_node(); + + let itemWidth = this.allocation.x2 - this.allocation.x1; + let itemHeight = this.allocation.y2 - this.allocation.y1; + + let labelWidth = this.label.get_width(); + let labelHeight = this.label.get_height(); + + let x, y, xOffset, yOffset; + + let position = Utils.getPosition(this._dtdSettings); + this._isHorizontal = ((position == St.Side.TOP) || (position == St.Side.BOTTOM)); + let labelOffset = node.get_length('-x-offset'); + + switch (position) { + case St.Side.LEFT: + yOffset = Math.floor((itemHeight - labelHeight) / 2); + y = stageY + yOffset; + xOffset = labelOffset; + x = stageX + this.get_width() + xOffset; + break; + case St.Side.RIGHT: + yOffset = Math.floor((itemHeight - labelHeight) / 2); + y = stageY + yOffset; + xOffset = labelOffset; + x = Math.round(stageX) - labelWidth - xOffset; + break; + case St.Side.TOP: + y = stageY + labelOffset + itemHeight; + xOffset = Math.floor((itemWidth - labelWidth) / 2); + x = stageX + xOffset; + break; + case St.Side.BOTTOM: + yOffset = labelOffset; + y = stageY - labelHeight - yOffset; + xOffset = Math.floor((itemWidth - labelWidth) / 2); + x = stageX + xOffset; + break; + } + + // keep the label inside the screen border + // Only needed fot the x coordinate. + + // Leave a few pixel gap + let gap = 5; + let monitor = Main.layoutManager.findMonitorForActor(this); + if (x - monitor.x < gap) + x += monitor.x - x + labelOffset; + else if (x + labelWidth > monitor.x + monitor.width - gap) + x -= x + labelWidth - (monitor.x + monitor.width) + gap; + + this.label.set_position(x, y); + Tweener.addTween(this.label, { + opacity: 255, + time: DASH_ITEM_LABEL_SHOW_TIME, + transition: 'easeOutQuad', + }); +} diff --git a/extensions/dash-to-dock/convenience.js b/extensions/dash-to-dock/convenience.js new file mode 100644 index 0000000..bc50419 --- /dev/null +++ b/extensions/dash-to-dock/convenience.js @@ -0,0 +1,74 @@ +/* -*- mode: js; js-basic-offset: 4; indent-tabs-mode: nil -*- */ + +/* + * Part of this file comes from gnome-shell-extensions: + * https://gitlab.gnome.org/GNOME/gnome-shell-extensions/ + */ + +const Gettext = imports.gettext; +const Gio = imports.gi.Gio; + +const Config = imports.misc.config; +const ExtensionUtils = imports.misc.extensionUtils; + +/** + * initTranslations: + * @domain: (optional): the gettext domain to use + * + * Initialize Gettext to load translations from extensionsdir/locale. + * If @domain is not provided, it will be taken from metadata['gettext-domain'] + */ +function initTranslations(domain) { + let extension = ExtensionUtils.getCurrentExtension(); + + domain = domain || extension.metadata['gettext-domain']; + + // Check if this extension was built with "make zip-file", and thus + // has the locale files in a subfolder + // otherwise assume that extension has been installed in the + // same prefix as gnome-shell + let localeDir = extension.dir.get_child('locale'); + if (localeDir.query_exists(null)) + Gettext.bindtextdomain(domain, localeDir.get_path()); + else + Gettext.bindtextdomain(domain, Config.LOCALEDIR); +} + +/** + * getSettings: + * @schema: (optional): the GSettings schema id + * + * Builds and return a GSettings schema for @schema, using schema files + * in extensionsdir/schemas. If @schema is not provided, it is taken from + * metadata['settings-schema']. + */ +function getSettings(schema) { + let extension = ExtensionUtils.getCurrentExtension(); + + schema = schema || extension.metadata['settings-schema']; + + const GioSSS = Gio.SettingsSchemaSource; + + // Check if this extension was built with "make zip-file", and thus + // has the schema files in a subfolder + // otherwise assume that extension has been installed in the + // same prefix as gnome-shell (and therefore schemas are available + // in the standard folders) + let schemaDir = extension.dir.get_child('schemas'); + let schemaSource; + if (schemaDir.query_exists(null)) + schemaSource = GioSSS.new_from_directory(schemaDir.get_path(), + GioSSS.get_default(), + false); + else + schemaSource = GioSSS.get_default(); + + let schemaObj = schemaSource.lookup(schema, true); + if (!schemaObj) + throw new Error('Schema ' + schema + ' could not be found for extension ' + + extension.metadata.uuid + '. Please check your installation.'); + + return new Gio.Settings({ + settings_schema: schemaObj + }); +} diff --git a/extensions/dash-to-dock/dash.js b/extensions/dash-to-dock/dash.js new file mode 100644 index 0000000..4cf5aa2 --- /dev/null +++ b/extensions/dash-to-dock/dash.js @@ -0,0 +1,1175 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Clutter = imports.gi.Clutter; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Gtk = imports.gi.Gtk; +const Signals = imports.signals; +const Lang = imports.lang; +const Meta = imports.gi.Meta; +const Shell = imports.gi.Shell; +const St = imports.gi.St; +const Mainloop = imports.mainloop; + +const AppDisplay = imports.ui.appDisplay; +const AppFavorites = imports.ui.appFavorites; +const Dash = imports.ui.dash; +const DND = imports.ui.dnd; +const IconGrid = imports.ui.iconGrid; +const Main = imports.ui.main; +const PopupMenu = imports.ui.popupMenu; +const Tweener = imports.ui.tweener; +const Util = imports.misc.util; +const Workspace = imports.ui.workspace; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Utils = Me.imports.utils; +const AppIcons = Me.imports.appIcons; + +let DASH_ANIMATION_TIME = Dash.DASH_ANIMATION_TIME; +let DASH_ITEM_LABEL_HIDE_TIME = Dash.DASH_ITEM_LABEL_HIDE_TIME; +let DASH_ITEM_HOVER_TIMEOUT = Dash.DASH_ITEM_HOVER_TIMEOUT; + +/** + * Extend DashItemContainer + * + * - Pass settings to the constructor + * - set label position based on dash orientation + * + * I can't subclass the original object because of this: https://bugzilla.gnome.org/show_bug.cgi?id=688973. + * thus use this ugly pattern. + */ +function extendDashItemContainer(dashItemContainer, settings) { + dashItemContainer._dtdSettings = settings; + dashItemContainer.showLabel = AppIcons.itemShowLabel; +} + +/** + * This class is a fork of the upstream DashActor class (ui.dash.js) + * + * Summary of changes: + * - passed settings to class as parameter + * - modified chldBox calculations for when 'show-apps-at-top' option is checked + * - handle horizontal dash + */ +const MyDashActor = new Lang.Class({ + Name: 'DashToDock.MyDashActor', + + _init: function(settings) { + // a prefix is required to avoid conflicting with the parent class variable + this._dtdSettings = settings; + this._rtl = (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL); + + this._position = Utils.getPosition(settings); + this._isHorizontal = ((this._position == St.Side.TOP) || + (this._position == St.Side.BOTTOM)); + + let layout = new Clutter.BoxLayout({ + orientation: this._isHorizontal ? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL + }); + + this.actor = new Shell.GenericContainer({ + name: 'dash', + layout_manager: layout, + clip_to_allocation: true + }); + this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth)); + this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight)); + this.actor.connect('allocate', Lang.bind(this, this._allocate)); + + this.actor._delegate = this; + }, + + _allocate: function(actor, box, flags) { + let contentBox = box; + let availWidth = contentBox.x2 - contentBox.x1; + let availHeight = contentBox.y2 - contentBox.y1; + + let [appIcons, showAppsButton] = actor.get_children(); + let [showAppsMinHeight, showAppsNatHeight] = showAppsButton.get_preferred_height(availWidth); + let [showAppsMinWidth, showAppsNatWidth] = showAppsButton.get_preferred_width(availHeight); + + let offset_x = this._isHorizontal?showAppsNatWidth:0; + let offset_y = this._isHorizontal?0:showAppsNatHeight; + + let childBox = new Clutter.ActorBox(); + if ((this._dtdSettings.get_boolean('show-apps-at-top') && !this._isHorizontal) + || (this._dtdSettings.get_boolean('show-apps-at-top') && !this._rtl) + || (!this._dtdSettings.get_boolean('show-apps-at-top') && this._isHorizontal && this._rtl)) { + childBox.x1 = contentBox.x1 + offset_x; + childBox.y1 = contentBox.y1 + offset_y; + childBox.x2 = contentBox.x2; + childBox.y2 = contentBox.y2; + appIcons.allocate(childBox, flags); + + childBox.y1 = contentBox.y1; + childBox.x1 = contentBox.x1; + childBox.x2 = contentBox.x1 + showAppsNatWidth; + childBox.y2 = contentBox.y1 + showAppsNatHeight; + showAppsButton.allocate(childBox, flags); + } + else { + childBox.x1 = contentBox.x1; + childBox.y1 = contentBox.y1; + childBox.x2 = contentBox.x2 - offset_x; + childBox.y2 = contentBox.y2 - offset_y; + appIcons.allocate(childBox, flags); + + childBox.x2 = contentBox.x2; + childBox.y2 = contentBox.y2; + childBox.x1 = contentBox.x2 - showAppsNatWidth; + childBox.y1 = contentBox.y2 - showAppsNatHeight; + showAppsButton.allocate(childBox, flags); + } + }, + + _getPreferredWidth: function(actor, forHeight, alloc) { + // We want to request the natural height of all our children + // as our natural height, so we chain up to StWidget (which + // then calls BoxLayout), but we only request the showApps + // button as the minimum size + + let [, natWidth] = this.actor.layout_manager.get_preferred_width(this.actor, forHeight); + + let themeNode = this.actor.get_theme_node(); + let [, showAppsButton] = this.actor.get_children(); + let [minWidth, ] = showAppsButton.get_preferred_height(forHeight); + + alloc.min_size = minWidth; + alloc.natural_size = natWidth; + + }, + + _getPreferredHeight: function(actor, forWidth, alloc) { + // We want to request the natural height of all our children + // as our natural height, so we chain up to StWidget (which + // then calls BoxLayout), but we only request the showApps + // button as the minimum size + + let [, natHeight] = this.actor.layout_manager.get_preferred_height(this.actor, forWidth); + + let themeNode = this.actor.get_theme_node(); + let [, showAppsButton] = this.actor.get_children(); + let [minHeight, ] = showAppsButton.get_preferred_height(forWidth); + + alloc.min_size = minHeight; + alloc.natural_size = natHeight; + } +}); + +const baseIconSizes = [16, 22, 24, 32, 48, 64, 96, 128]; + +/** + * This class is a fork of the upstream dash class (ui.dash.js) + * + * Summary of changes: + * - disconnect global signals adding a destroy method; + * - play animations even when not in overview mode + * - set a maximum icon size + * - show running and/or favorite applications + * - emit a custom signal when an app icon is added + * - hide showApps label when the custom menu is shown. + * - add scrollview + * ensure actor is visible on keyfocus inseid the scrollview + * - add 128px icon size, might be usefull for hidpi display + * - sync minimization application target position. + * - keep running apps ordered. + */ +var MyDash = new Lang.Class({ + Name: 'DashToDock.MyDash', + + _init: function(settings, remoteModel, monitorIndex) { + this._dtdSettings = settings; + + // Initialize icon variables and size + this._maxHeight = -1; + this.iconSize = this._dtdSettings.get_int('dash-max-icon-size'); + this._availableIconSizes = baseIconSizes; + this._shownInitially = false; + this._initializeIconSize(this.iconSize); + + this._remoteModel = remoteModel; + this._monitorIndex = monitorIndex; + this._position = Utils.getPosition(settings); + this._isHorizontal = ((this._position == St.Side.TOP) || + (this._position == St.Side.BOTTOM)); + this._signalsHandler = new Utils.GlobalSignalsHandler(); + + this._dragPlaceholder = null; + this._dragPlaceholderPos = -1; + this._animatingPlaceholdersCount = 0; + this._showLabelTimeoutId = 0; + this._resetHoverTimeoutId = 0; + this._ensureAppIconVisibilityTimeoutId = 0; + this._labelShowing = false; + + this._containerObject = new MyDashActor(settings); + this._container = this._containerObject.actor; + this._scrollView = new St.ScrollView({ + name: 'dashtodockDashScrollview', + hscrollbar_policy: Gtk.PolicyType.NEVER, + vscrollbar_policy: Gtk.PolicyType.NEVER, + enable_mouse_scrolling: false + }); + + this._scrollView.connect('scroll-event', Lang.bind(this, this._onScrollEvent)); + + this._box = new St.BoxLayout({ + vertical: !this._isHorizontal, + clip_to_allocation: false, + x_align: Clutter.ActorAlign.START, + y_align: Clutter.ActorAlign.START + }); + this._box._delegate = this; + this._container.add_actor(this._scrollView); + this._scrollView.add_actor(this._box); + + // Create a wrapper around the real showAppsIcon in order to add a popupMenu. + let showAppsIconWrapper = new AppIcons.ShowAppsIconWrapper(this._dtdSettings); + showAppsIconWrapper.connect('menu-state-changed', Lang.bind(this, function(showAppsIconWrapper, opened) { + this._itemMenuStateChanged(showAppsIconWrapper, opened); + })); + // an instance of the showAppsIcon class is encapsulated in the wrapper + this._showAppsIcon = showAppsIconWrapper.realShowAppsIcon; + + this._showAppsIcon.childScale = 1; + this._showAppsIcon.childOpacity = 255; + this._showAppsIcon.icon.setIconSize(this.iconSize); + this._hookUpLabel(this._showAppsIcon); + + this.showAppsButton = this._showAppsIcon.toggleButton; + + this._container.add_actor(this._showAppsIcon); + + let rtl = Clutter.get_default_text_direction() == Clutter.TextDirection.RTL; + this.actor = new St.Bin({ + child: this._container, + y_align: St.Align.START, + x_align: rtl ? St.Align.END : St.Align.START + }); + + if (this._isHorizontal) { + this.actor.connect('notify::width', Lang.bind(this, function() { + if (this._maxHeight != this.actor.width) + this._queueRedisplay(); + this._maxHeight = this.actor.width; + })); + } + else { + this.actor.connect('notify::height', Lang.bind(this, function() { + if (this._maxHeight != this.actor.height) + this._queueRedisplay(); + this._maxHeight = this.actor.height; + })); + } + + // Update minimization animation target position on allocation of the + // container and on scrollview change. + this._box.connect('notify::allocation', Lang.bind(this, this._updateAppsIconGeometry)); + let scrollViewAdjustment = this._isHorizontal ? this._scrollView.hscroll.adjustment : this._scrollView.vscroll.adjustment; + scrollViewAdjustment.connect('notify::value', Lang.bind(this, this._updateAppsIconGeometry)); + + this._workId = Main.initializeDeferredWork(this._box, Lang.bind(this, this._redisplay)); + + this._settings = new Gio.Settings({ + schema_id: 'org.gnome.shell' + }); + + this._appSystem = Shell.AppSystem.get_default(); + + this._signalsHandler.add([ + this._appSystem, + 'installed-changed', + Lang.bind(this, function() { + AppFavorites.getAppFavorites().reload(); + this._queueRedisplay(); + }) + ], [ + AppFavorites.getAppFavorites(), + 'changed', + Lang.bind(this, this._queueRedisplay) + ], [ + this._appSystem, + 'app-state-changed', + Lang.bind(this, this._queueRedisplay) + ], [ + Main.overview, + 'item-drag-begin', + Lang.bind(this, this._onDragBegin) + ], [ + Main.overview, + 'item-drag-end', + Lang.bind(this, this._onDragEnd) + ], [ + Main.overview, + 'item-drag-cancelled', + Lang.bind(this, this._onDragCancelled) + ]); + }, + + destroy: function() { + this._signalsHandler.destroy(); + }, + + _onScrollEvent: function(actor, event) { + // If scroll is not used because the icon is resized, let the scroll event propagate. + if (!this._dtdSettings.get_boolean('icon-size-fixed')) + return Clutter.EVENT_PROPAGATE; + + // reset timeout to avid conflicts with the mousehover event + if (this._ensureAppIconVisibilityTimeoutId > 0) { + Mainloop.source_remove(this._ensureAppIconVisibilityTimeoutId); + this._ensureAppIconVisibilityTimeoutId = 0; + } + + // Skip to avoid double events mouse + if (event.is_pointer_emulated()) + return Clutter.EVENT_STOP; + + let adjustment, delta; + + if (this._isHorizontal) + adjustment = this._scrollView.get_hscroll_bar().get_adjustment(); + else + adjustment = this._scrollView.get_vscroll_bar().get_adjustment(); + + let increment = adjustment.step_increment; + + switch (event.get_scroll_direction()) { + case Clutter.ScrollDirection.UP: + delta = -increment; + break; + case Clutter.ScrollDirection.DOWN: + delta = +increment; + break; + case Clutter.ScrollDirection.SMOOTH: + let [dx, dy] = event.get_scroll_delta(); + delta = dy * increment; + // Also consider horizontal component, for instance touchpad + if (this._isHorizontal) + delta += dx * increment; + break; + } + + adjustment.set_value(adjustment.get_value() + delta); + + return Clutter.EVENT_STOP; + }, + + _onDragBegin: function() { + this._dragCancelled = false; + this._dragMonitor = { + dragMotion: Lang.bind(this, this._onDragMotion) + }; + DND.addDragMonitor(this._dragMonitor); + + if (this._box.get_n_children() == 0) { + this._emptyDropTarget = new Dash.EmptyDropTargetItem(); + this._box.insert_child_at_index(this._emptyDropTarget, 0); + this._emptyDropTarget.show(true); + } + }, + + _onDragCancelled: function() { + this._dragCancelled = true; + this._endDrag(); + }, + + _onDragEnd: function() { + if (this._dragCancelled) + return; + + this._endDrag(); + }, + + _endDrag: function() { + this._clearDragPlaceholder(); + this._clearEmptyDropTarget(); + this._showAppsIcon.setDragApp(null); + DND.removeDragMonitor(this._dragMonitor); + }, + + _onDragMotion: function(dragEvent) { + let app = Dash.getAppFromSource(dragEvent.source); + if (app == null) + return DND.DragMotionResult.CONTINUE; + + let showAppsHovered = this._showAppsIcon.contains(dragEvent.targetActor); + + if (!this._box.contains(dragEvent.targetActor) || showAppsHovered) + this._clearDragPlaceholder(); + + if (showAppsHovered) + this._showAppsIcon.setDragApp(app); + else + this._showAppsIcon.setDragApp(null); + + 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, appIcon) { + item.child.connect('notify::hover', Lang.bind(this, function() { + this._syncLabel(item, appIcon); + })); + + let id = Main.overview.connect('hiding', Lang.bind(this, function() { + this._labelShowing = false; + item.hideLabel(); + })); + item.child.connect('destroy', function() { + Main.overview.disconnect(id); + }); + + if (appIcon) { + appIcon.connect('sync-tooltip', Lang.bind(this, function() { + this._syncLabel(item, appIcon); + })); + } + }, + + _createAppItem: function(app) { + let appIcon = new AppIcons.MyAppIcon(this._dtdSettings, this._remoteModel, app, this._monitorIndex, + { setSizeManually: true, + showLabel: false }); + + if (appIcon._draggable) { + appIcon._draggable.connect('drag-begin', Lang.bind(this, function() { + appIcon.actor.opacity = 50; + })); + appIcon._draggable.connect('drag-end', Lang.bind(this, function() { + appIcon.actor.opacity = 255; + })); + } + + appIcon.connect('menu-state-changed', Lang.bind(this, function(appIcon, opened) { + this._itemMenuStateChanged(item, opened); + })); + + let item = new Dash.DashItemContainer(); + + extendDashItemContainer(item, this._dtdSettings); + item.setChild(appIcon.actor); + + appIcon.actor.connect('notify::hover', Lang.bind(this, function() { + if (appIcon.actor.hover) { + this._ensureAppIconVisibilityTimeoutId = Mainloop.timeout_add(100, Lang.bind(this, function() { + ensureActorVisibleInScrollView(this._scrollView, appIcon.actor); + this._ensureAppIconVisibilityTimeoutId = 0; + return GLib.SOURCE_REMOVE; + })); + } + else { + if (this._ensureAppIconVisibilityTimeoutId > 0) { + Mainloop.source_remove(this._ensureAppIconVisibilityTimeoutId); + this._ensureAppIconVisibilityTimeoutId = 0; + } + } + })); + + appIcon.actor.connect('clicked', Lang.bind(this, function(actor) { + ensureActorVisibleInScrollView(this._scrollView, actor); + })); + + appIcon.actor.connect('key-focus-in', Lang.bind(this, function(actor) { + let [x_shift, y_shift] = ensureActorVisibleInScrollView(this._scrollView, actor); + + // This signal is triggered also by mouse click. The popup menu is opened at the original + // coordinates. Thus correct for the shift which is going to be applied to the scrollview. + if (appIcon._menu) { + appIcon._menu._boxPointer.xOffset = -x_shift; + appIcon._menu._boxPointer.yOffset = -y_shift; + } + })); + + // Override default AppIcon label_actor, now the + // accessible_name is set at DashItemContainer.setLabelText + appIcon.actor.label_actor = null; + item.setLabelText(app.get_name()); + + appIcon.icon.setIconSize(this.iconSize); + this._hookUpLabel(item, appIcon); + + return item; + }, + + /** + * Return an array with the "proper" appIcons currently in the dash + */ + getAppIcons: function() { + // Only consider children which are "proper" + // icons (i.e. ignoring drag placeholders) and which are not + // animating out (which means they will be destroyed at the end of + // the animation) + let iconChildren = this._box.get_children().filter(function(actor) { + return actor.child && + actor.child._delegate && + actor.child._delegate.icon && + !actor.animatingOut; + }); + + let appIcons = iconChildren.map(function(actor) { + return actor.child._delegate; + }); + + return appIcons; + }, + + _updateAppsIconGeometry: function() { + let appIcons = this.getAppIcons(); + appIcons.forEach(function(icon) { + icon.updateIconGeometry(); + }); + }, + + _itemMenuStateChanged: function(item, opened) { + // 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; + } + + item.hideLabel(); + } + else { + // I want to listen from outside when a menu is closed. I used to + // add a custom signal to the appIcon, since gnome 3.8 the signal + // calling this callback was added upstream. + this.emit('menu-closed'); + } + }, + + _syncLabel: function(item, appIcon) { + let shouldShow = appIcon ? appIcon.shouldShowTooltip() : item.child.get_hover(); + + if (shouldShow) { + if (this._showLabelTimeoutId == 0) { + let timeout = this._labelShowing ? 0 : DASH_ITEM_HOVER_TIMEOUT; + this._showLabelTimeoutId = Mainloop.timeout_add(timeout, Lang.bind(this, function() { + this._labelShowing = true; + item.showLabel(); + this._showLabelTimeoutId = 0; + return GLib.SOURCE_REMOVE; + })); + GLib.Source.set_name_by_id(this._showLabelTimeoutId, '[gnome-shell] item.showLabel'); + 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; + item.hideLabel(); + if (this._labelShowing) { + this._resetHoverTimeoutId = Mainloop.timeout_add(DASH_ITEM_HOVER_TIMEOUT, Lang.bind(this, function() { + this._labelShowing = false; + this._resetHoverTimeoutId = 0; + return GLib.SOURCE_REMOVE; + })); + GLib.Source.set_name_by_id(this._resetHoverTimeoutId, '[gnome-shell] this._labelShowing'); + } + } + }, + + _adjustIconSize: function() { + // For the icon size, we only consider children which are "proper" + // icons (i.e. ignoring drag placeholders) and which are not + // animating out (which means they will be destroyed at the end of + // the animation) + let iconChildren = this._box.get_children().filter(function(actor) { + return actor.child && + actor.child._delegate && + actor.child._delegate.icon && + !actor.animatingOut; + }); + + iconChildren.push(this._showAppsIcon); + + if (this._maxHeight == -1) + return; + + // Check if the container is present in the stage. This avoids critical + // errors when unlocking the screen + if (!this._container.get_stage()) + return; + + let themeNode = this._container.get_theme_node(); + let maxAllocation = new Clutter.ActorBox({ + x1: 0, + y1: 0, + x2: this._isHorizontal ? this._maxHeight : 42 /* whatever */, + y2: this._isHorizontal ? 42 : this._maxHeight + }); + let maxContent = themeNode.get_content_box(maxAllocation); + let availHeight; + if (this._isHorizontal) + availHeight = maxContent.x2 - maxContent.x1; + else + availHeight = maxContent.y2 - maxContent.y1; + let spacing = themeNode.get_length('spacing'); + + let firstButton = iconChildren[0].child; + let firstIcon = firstButton._delegate.icon; + + let minHeight, natHeight, minWidth, natWidth; + + // Enforce the current icon size during the size request + firstIcon.setIconSize(this.iconSize); + [minHeight, natHeight] = firstButton.get_preferred_height(-1); + [minWidth, natWidth] = firstButton.get_preferred_width(-1); + + let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; + let iconSizes = this._availableIconSizes.map(function(s) { + return s * scaleFactor; + }); + + // Subtract icon padding and box spacing from the available height + if (this._isHorizontal) + availHeight -= iconChildren.length * (natWidth - this.iconSize * scaleFactor) + + (iconChildren.length - 1) * spacing; + else + availHeight -= iconChildren.length * (natHeight - this.iconSize * scaleFactor) + + (iconChildren.length - 1) * spacing; + + let availSize = availHeight / iconChildren.length; + + + let newIconSize = this._availableIconSizes[0]; + for (let i = 0; i < iconSizes.length; i++) { + if (iconSizes[i] < availSize) + newIconSize = this._availableIconSizes[i]; + } + + if (newIconSize == this.iconSize) + return; + + let oldIconSize = this.iconSize; + this.iconSize = newIconSize; + this.emit('icon-size-changed'); + + let scale = oldIconSize / newIconSize; + for (let i = 0; i < iconChildren.length; i++) { + let icon = iconChildren[i].child._delegate.icon; + + // Set the new size immediately, to keep the icons' sizes + // in sync with this.iconSize + icon.setIconSize(this.iconSize); + + // Don't animate the icon size change when the overview + // is transitioning, or when initially filling + // the dash + if (Main.overview.animationInProgress || + !this._shownInitially) + continue; + + let [targetWidth, targetHeight] = icon.icon.get_size(); + + // Scale the icon's texture to the previous size and + // tween to the new size + icon.icon.set_size(icon.icon.width * scale, + icon.icon.height * scale); + + Tweener.addTween(icon.icon, + { width: targetWidth, + height: targetHeight, + time: DASH_ANIMATION_TIME, + transition: 'easeOutQuad', + }); + } + }, + + _redisplay: function() { + let favorites = AppFavorites.getAppFavorites().getFavoriteMap(); + + let running = this._appSystem.get_running(); + if (this._dtdSettings.get_boolean('isolate-workspaces') || + this._dtdSettings.get_boolean('isolate-monitors')) { + // When using isolation, we filter out apps that have no windows in + // the current workspace + let settings = this._dtdSettings; + let monitorIndex = this._monitorIndex; + running = running.filter(function(_app) { + return AppIcons.getInterestingWindows(_app, settings, monitorIndex).length != 0; + }); + } + + let children = this._box.get_children().filter(function(actor) { + return actor.child && + actor.child._delegate && + actor.child._delegate.app; + }); + // Apps currently in the dash + let oldApps = children.map(function(actor) { + return actor.child._delegate.app; + }); + // Apps supposed to be in the dash + let newApps = []; + + if (this._dtdSettings.get_boolean('show-favorites')) { + for (let id in favorites) + newApps.push(favorites[id]); + } + + // We reorder the running apps so that they don't change position on the + // dash with every redisplay() call + if (this._dtdSettings.get_boolean('show-running')) { + // First: add the apps from the oldApps list that are still running + for (let i = 0; i < oldApps.length; i++) { + let index = running.indexOf(oldApps[i]); + if (index > -1) { + let app = running.splice(index, 1)[0]; + if (this._dtdSettings.get_boolean('show-favorites') && (app.get_id() in favorites)) + continue; + newApps.push(app); + } + } + // Second: add the new apps + for (let i = 0; i < running.length; i++) { + let app = running[i]; + if (this._dtdSettings.get_boolean('show-favorites') && (app.get_id() in favorites)) + continue; + newApps.push(app); + } + } + + // Figure out the actual changes to the list of items; we iterate + // over both the list of items currently in the dash and the list + // of items expected there, and collect additions and removals. + // Moves are both an addition and a removal, where the order of + // the operations depends on whether we encounter the position + // where the item has been added first or the one from where it + // was removed. + // There is an assumption that only one item is moved at a given + // time; when moving several items at once, everything will still + // end up at the right position, but there might be additional + // additions/removals (e.g. it might remove all the launchers + // and add them back in the new order even if a smaller set of + // additions and removals is possible). + // If above assumptions turns out to be a problem, we might need + // to use a more sophisticated algorithm, e.g. Longest Common + // Subsequence as used by diff. + + let addedItems = []; + let removedActors = []; + + let newIndex = 0; + let oldIndex = 0; + while ((newIndex < newApps.length) || (oldIndex < oldApps.length)) { + // No change at oldIndex/newIndex + if (oldApps[oldIndex] && oldApps[oldIndex] == newApps[newIndex]) { + oldIndex++; + newIndex++; + continue; + } + + // App removed at oldIndex + if (oldApps[oldIndex] && (newApps.indexOf(oldApps[oldIndex]) == -1)) { + removedActors.push(children[oldIndex]); + oldIndex++; + continue; + } + + // App added at newIndex + if (newApps[newIndex] && (oldApps.indexOf(newApps[newIndex]) == -1)) { + let newItem = this._createAppItem(newApps[newIndex]); + addedItems.push({ app: newApps[newIndex], + item: newItem, + pos: newIndex }); + newIndex++; + continue; + } + + // App moved + let insertHere = newApps[newIndex + 1] && (newApps[newIndex + 1] == oldApps[oldIndex]); + let alreadyRemoved = removedActors.reduce(function(result, actor) { + let removedApp = actor.child._delegate.app; + return result || removedApp == newApps[newIndex]; + }, false); + + if (insertHere || alreadyRemoved) { + let newItem = this._createAppItem(newApps[newIndex]); + addedItems.push({ + app: newApps[newIndex], + item: newItem, + pos: newIndex + removedActors.length + }); + newIndex++; + } + else { + removedActors.push(children[oldIndex]); + oldIndex++; + } + } + + for (let i = 0; i < addedItems.length; i++) + this._box.insert_child_at_index(addedItems[i].item, + addedItems[i].pos); + + for (let i = 0; i < removedActors.length; i++) { + let item = removedActors[i]; + + // Don't animate item removal when the overview is transitioning + if (!Main.overview.animationInProgress) + item.animateOutAndDestroy(); + else + item.destroy(); + } + + this._adjustIconSize(); + + for (let i = 0; i < addedItems.length; i++) + // Emit a custom signal notifying that a new item has been added + this.emit('item-added', addedItems[i]); + + // Skip animations on first run when adding the initial set + // of items, to avoid all items zooming in at once + + let animate = this._shownInitially && + !Main.overview.animationInProgress; + + if (!this._shownInitially) + this._shownInitially = true; + + for (let i = 0; i < addedItems.length; i++) + addedItems[i].item.show(animate); + + // Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=692744 + // Without it, StBoxLayout may use a stale size cache + this._box.queue_relayout(); + + // This is required for icon reordering when the scrollview is used. + this._updateAppsIconGeometry(); + + // This will update the size, and the corresponding number for each icon + this._updateNumberOverlay(); + }, + + _updateNumberOverlay: function() { + let appIcons = this.getAppIcons(); + let counter = 1; + appIcons.forEach(function(icon) { + if (counter < 10){ + icon.setNumberOverlay(counter); + counter++; + } + else if (counter == 10) { + icon.setNumberOverlay(0); + counter++; + } + else { + // No overlay after 10 + icon.setNumberOverlay(-1); + } + icon.updateNumberOverlay(); + }); + + }, + + toggleNumberOverlay: function(activate) { + let appIcons = this.getAppIcons(); + appIcons.forEach(function(icon) { + icon.toggleNumberOverlay(activate); + }); + }, + + _initializeIconSize: function(max_size) { + let max_allowed = baseIconSizes[baseIconSizes.length-1]; + max_size = Math.min(max_size, max_allowed); + + if (this._dtdSettings.get_boolean('icon-size-fixed')) + this._availableIconSizes = [max_size]; + else { + this._availableIconSizes = baseIconSizes.filter(function(val) { + return (val numChildren) + pos = numChildren; + } + else + pos = 0; // always insert at the top when dash is empty + + // Take into account childredn position in rtl + if (this._isHorizontal && (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL)) + pos = numChildren - pos; + + if ((pos != this._dragPlaceholderPos) && (pos <= numFavorites) && (this._animatingPlaceholdersCount == 0)) { + this._dragPlaceholderPos = pos; + + // Don't allow positioning before or after self + if ((favPos != -1) && (pos == favPos || pos == favPos + 1)) { + this._clearDragPlaceholder(); + return DND.DragMotionResult.CONTINUE; + } + + // If the placeholder already exists, we just move + // it, but if we are adding it, expand its size in + // an animation + let fadeIn; + if (this._dragPlaceholder) { + this._dragPlaceholder.destroy(); + fadeIn = false; + } + else + fadeIn = true; + + this._dragPlaceholder = new Dash.DragPlaceholderItem(); + this._dragPlaceholder.child.set_width (this.iconSize); + this._dragPlaceholder.child.set_height (this.iconSize / 2); + this._box.insert_child_at_index(this._dragPlaceholder, + this._dragPlaceholderPos); + this._dragPlaceholder.show(fadeIn); + // Ensure the next and previous icon are visible when moving the placeholder + // (I assume there's room for both of them) + if (this._dragPlaceholderPos > 1) + ensureActorVisibleInScrollView(this._scrollView, this._box.get_children()[this._dragPlaceholderPos-1]); + if (this._dragPlaceholderPos < this._box.get_children().length-1) + ensureActorVisibleInScrollView(this._scrollView, this._box.get_children()[this._dragPlaceholderPos+1]); + } + + // Remove the drag placeholder if we are not in the + // "favorites zone" + if (pos > numFavorites) + this._clearDragPlaceholder(); + + if (!this._dragPlaceholder) + return DND.DragMotionResult.NO_DROP; + + let srcIsFavorite = (favPos != -1); + + if (srcIsFavorite) + return DND.DragMotionResult.MOVE_DROP; + + return DND.DragMotionResult.COPY_DROP; + }, + + /** + * Draggable target interface + */ + acceptDrop: function(source, actor, x, y, time) { + let app = Dash.getAppFromSource(source); + + // Don't allow favoriting of transient apps + if (app == null || app.is_window_backed()) + return false; + + if (!this._settings.is_writable('favorite-apps') || !this._dtdSettings.get_boolean('show-favorites')) + return false; + + let id = app.get_id(); + + let favorites = AppFavorites.getAppFavorites().getFavoriteMap(); + + let srcIsFavorite = (id in favorites); + + let favPos = 0; + let children = this._box.get_children(); + for (let i = 0; i < this._dragPlaceholderPos; i++) { + if (this._dragPlaceholder && (children[i] == this._dragPlaceholder)) + continue; + + let childId = children[i].child._delegate.app.get_id(); + if (childId == id) + continue; + if (childId in favorites) + favPos++; + } + + // No drag placeholder means we don't wan't to favorite the app + // and we are dragging it to its original position + if (!this._dragPlaceholder) + return true; + + Meta.later_add(Meta.LaterType.BEFORE_REDRAW, Lang.bind(this, function() { + let appFavorites = AppFavorites.getAppFavorites(); + if (srcIsFavorite) + appFavorites.moveFavoriteToPos(id, favPos); + else + appFavorites.addFavoriteAtPos(id, favPos); + return false; + })); + + return true; + }, + + showShowAppsButton: function() { + this.showAppsButton.visible = true + this.showAppsButton.set_width(-1) + this.showAppsButton.set_height(-1) + }, + + hideShowAppsButton: function() { + this.showAppsButton.hide() + this.showAppsButton.set_width(0) + this.showAppsButton.set_height(0) + } +}); + +Signals.addSignalMethods(MyDash.prototype); + +/** + * This is a copy of the same function in utils.js, but also adjust horizontal scrolling + * and perform few further cheks on the current value to avoid changing the values when + * it would be clamp to the current one in any case. + * Return the amount of shift applied + */ +function ensureActorVisibleInScrollView(scrollView, actor) { + let adjust_v = true; + let adjust_h = true; + + let vadjustment = scrollView.vscroll.adjustment; + let hadjustment = scrollView.hscroll.adjustment; + let [vvalue, vlower, vupper, vstepIncrement, vpageIncrement, vpageSize] = vadjustment.get_values(); + let [hvalue, hlower, hupper, hstepIncrement, hpageIncrement, hpageSize] = hadjustment.get_values(); + + let [hvalue0, vvalue0] = [hvalue, vvalue]; + + let voffset = 0; + let hoffset = 0; + let fade = scrollView.get_effect('fade'); + if (fade) { + voffset = fade.vfade_offset; + hoffset = fade.hfade_offset; + } + + let box = actor.get_allocation_box(); + let y1 = box.y1, y2 = box.y2, x1 = box.x1, x2 = box.x2; + + let parent = actor.get_parent(); + while (parent != scrollView) { + if (!parent) + throw new Error('Actor not in scroll view'); + + let box = parent.get_allocation_box(); + y1 += box.y1; + y2 += box.y1; + x1 += box.x1; + x2 += box.x1; + parent = parent.get_parent(); + } + + if (y1 < vvalue + voffset) + vvalue = Math.max(0, y1 - voffset); + else if (vvalue < vupper - vpageSize && y2 > vvalue + vpageSize - voffset) + vvalue = Math.min(vupper -vpageSize, y2 + voffset - vpageSize); + + if (x1 < hvalue + hoffset) + hvalue = Math.max(0, x1 - hoffset); + else if (hvalue < hupper - hpageSize && x2 > hvalue + hpageSize - hoffset) + hvalue = Math.min(hupper - hpageSize, x2 + hoffset - hpageSize); + + if (vvalue !== vvalue0) { + Tweener.addTween(vadjustment, { value: vvalue, + time: Util.SCROLL_TIME, + transition: 'easeOutQuad' + }); + } + + if (hvalue !== hvalue0) { + Tweener.addTween(hadjustment, + { value: hvalue, + time: Util.SCROLL_TIME, + transition: 'easeOutQuad' }); + } + + return [hvalue- hvalue0, vvalue - vvalue0]; +} diff --git a/extensions/dash-to-dock/docking.js b/extensions/dash-to-dock/docking.js new file mode 100644 index 0000000..11810a1 --- /dev/null +++ b/extensions/dash-to-dock/docking.js @@ -0,0 +1,1925 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Clutter = imports.gi.Clutter; +const GLib = imports.gi.GLib; +const Gtk = imports.gi.Gtk; +const Lang = imports.lang; +const Meta = imports.gi.Meta; +const Shell = imports.gi.Shell; +const St = imports.gi.St; +const Mainloop = imports.mainloop; +const Params = imports.misc.params; + +const Main = imports.ui.main; +const Dash = imports.ui.dash; +const IconGrid = imports.ui.iconGrid; +const Overview = imports.ui.overview; +const OverviewControls = imports.ui.overviewControls; +const PointerWatcher = imports.ui.pointerWatcher; +const Tweener = imports.ui.tweener; +const Signals = imports.signals; +const ViewSelector = imports.ui.viewSelector; +const WorkspaceSwitcherPopup= imports.ui.workspaceSwitcherPopup; +const Layout = imports.ui.layout; +const LayoutManager = imports.ui.main.layoutManager; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Convenience = Me.imports.convenience; +const Utils = Me.imports.utils; +const Intellihide = Me.imports.intellihide; +const Theming = Me.imports.theming; +const MyDash = Me.imports.dash; +const LauncherAPI = Me.imports.launcherAPI; + +const DOCK_DWELL_CHECK_INTERVAL = 100; + +var State = { + HIDDEN: 0, + SHOWING: 1, + SHOWN: 2, + HIDING: 3 +}; + +const scrollAction = { + DO_NOTHING: 0, + CYCLE_WINDOWS: 1, + SWITCH_WORKSPACE: 2 +}; + +/** + * A simple St.Widget with one child whose allocation takes into account the + * slide out of its child via the _slidex parameter ([0:1]). + * + * Required since I want to track the input region of this container which is + * based on its allocation even if the child overlows the parent actor. By doing + * this the region of the dash that is slideout is not steling anymore the input + * regions making the extesion usable when the primary monitor is the right one. + * + * The slidex parameter can be used to directly animate the sliding. The parent + * must have a WEST (SOUTH) anchor_point to achieve the sliding to the RIGHT (BOTTOM) + * side. + * + * It can't be an extended object because of this: https://bugzilla.gnome.org/show_bug.cgi?id=688973. + * thus use the Shell.GenericContainer pattern. +*/ +const DashSlideContainer = new Lang.Class({ + Name: 'DashToDock.DashSlideContainer', + + _init: function(params) { + // Default local params + let localDefaults = { + side: St.Side.LEFT, + initialSlideValue: 1 + } + + let localParams = Params.parse(params, localDefaults, true); + + if (params) { + // Remove local params before passing the params to the parent + // constructor to avoid errors. + let prop; + for (prop in localDefaults) { + if ((prop in params)) + delete params[prop]; + } + } + + this.actor = new Shell.GenericContainer(params); + this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth)); + this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight)); + this.actor.connect('allocate', Lang.bind(this, this._allocate)); + + this.actor._delegate = this; + + this._child = null; + + // slide parameter: 1 = visible, 0 = hidden. + this._slidex = localParams.initialSlideValue; + this._side = localParams.side; + this._slideoutSize = 0; // minimum size when slided out + }, + + _allocate: function(actor, box, flags) { + if (this._child == null) + return; + + let availWidth = box.x2 - box.x1; + let availHeight = box.y2 - box.y1; + let [minChildWidth, minChildHeight, natChildWidth, natChildHeight] = + this._child.get_preferred_size(); + + let childWidth = natChildWidth; + let childHeight = natChildHeight; + + let childBox = new Clutter.ActorBox(); + + let slideoutSize = this._slideoutSize; + + if (this._side == St.Side.LEFT) { + childBox.x1 = (this._slidex -1) * (childWidth - slideoutSize); + childBox.x2 = slideoutSize + this._slidex*(childWidth - slideoutSize); + childBox.y1 = 0; + childBox.y2 = childBox.y1 + childHeight; + } + else if ((this._side == St.Side.RIGHT) || (this._side == St.Side.BOTTOM)) { + childBox.x1 = 0; + childBox.x2 = childWidth; + childBox.y1 = 0; + childBox.y2 = childBox.y1 + childHeight; + } + else if (this._side == St.Side.TOP) { + childBox.x1 = 0; + childBox.x2 = childWidth; + childBox.y1 = (this._slidex -1) * (childHeight - slideoutSize); + childBox.y2 = slideoutSize + this._slidex * (childHeight - slideoutSize); + } + + this._child.allocate(childBox, flags); + this._child.set_clip(-childBox.x1, -childBox.y1, + -childBox.x1+availWidth, -childBox.y1 + availHeight); + }, + + /** + * Just the child width but taking into account the slided out part + */ + _getPreferredWidth: function(actor, forHeight, alloc) { + let [minWidth, natWidth] = this._child.get_preferred_width(forHeight); + if ((this._side == St.Side.LEFT) || (this._side == St.Side.RIGHT)) { + minWidth = (minWidth - this._slideoutSize) * this._slidex + this._slideoutSize; + natWidth = (natWidth - this._slideoutSize) * this._slidex + this._slideoutSize; + } + + alloc.min_size = minWidth; + alloc.natural_size = natWidth; + }, + + /** + * Just the child height but taking into account the slided out part + */ + _getPreferredHeight: function(actor, forWidth, alloc) { + let [minHeight, natHeight] = this._child.get_preferred_height(forWidth); + if ((this._side == St.Side.TOP) || (this._side == St.Side.BOTTOM)) { + minHeight = (minHeight - this._slideoutSize) * this._slidex + this._slideoutSize; + natHeight = (natHeight - this._slideoutSize) * this._slidex + this._slideoutSize; + } + alloc.min_size = minHeight; + alloc.natural_size = natHeight; + }, + + /** + * I was expecting it to be a virtual function... stil I don't understand + * how things work. + */ + add_child: function(actor) { + // I'm supposed to have only on child + if (this._child !== null) + this.actor.remove_child(actor); + + this._child = actor; + this.actor.add_child(actor); + }, + + set slidex(value) { + this._slidex = value; + this._child.queue_relayout(); + }, + + get slidex() { + return this._slidex; + } +}); + +const DockedDash = new Lang.Class({ + Name: 'DashToDock.DockedDash', + + _init: function(settings, remoteModel, monitorIndex) { + this._rtl = (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL); + + // Load settings + this._settings = settings; + this._remoteModel = remoteModel; + this._monitorIndex = monitorIndex; + // Connect global signals + this._signalsHandler = new Utils.GlobalSignalsHandler(); + + this._bindSettingsChanges(); + + this._position = Utils.getPosition(settings); + this._isHorizontal = ((this._position == St.Side.TOP) || (this._position == St.Side.BOTTOM)); + + // Temporary ignore hover events linked to autohide for whatever reason + this._ignoreHover = false; + this._oldignoreHover = null; + // This variables are linked to the settings regardles of autohide or intellihide + // being temporary disable. Get set by _updateVisibilityMode; + this._autohideIsEnabled = null; + this._intellihideIsEnabled = null; + this._fixedIsEnabled = null; + + // Create intellihide object to monitor windows overlapping + this._intellihide = new Intellihide.Intellihide(this._settings, this._monitorIndex); + + // initialize dock state + this._dockState = State.HIDDEN; + + // Put dock on the required monitor + this._monitor = Main.layoutManager.monitors[this._monitorIndex]; + + // this store size and the position where the dash is shown; + // used by intellihide module to check window overlap. + this.staticBox = new Clutter.ActorBox(); + + // Initialize pressure barrier variables + this._canUsePressure = false; + this._pressureBarrier = null; + this._barrier = null; + this._removeBarrierTimeoutId = 0; + + // Initialize dwelling system variables + this._dockDwelling = false; + this._dockWatch = null; + this._dockDwellUserTime = 0; + this._dockDwellTimeoutId = 0 + + // Create a new dash object + this.dash = new MyDash.MyDash(this._settings, this._remoteModel, this._monitorIndex); + + if (!this._settings.get_boolean('show-show-apps-button')) + this.dash.hideShowAppsButton(); + + // Create the main actor and the containers for sliding in and out and + // centering, turn on track hover + + let positionStyleClass = ['top', 'right', 'bottom', 'left']; + // This is the centering actor + this.actor = new St.Bin({ + name: 'dashtodockContainer', + reactive: false, + style_class: positionStyleClass[this._position], + x_align: this._isHorizontal?St.Align.MIDDLE:St.Align.START, + y_align: this._isHorizontal?St.Align.START:St.Align.MIDDLE + }); + this.actor._delegate = this; + + // This is the sliding actor whose allocation is to be tracked for input regions + this._slider = new DashSlideContainer({ + side: this._position, + initialSlideValue: 0 + }); + + // This is the actor whose hover status us tracked for autohide + this._box = new St.BoxLayout({ + name: 'dashtodockBox', + reactive: true, + track_hover: true + }); + this._box.connect('notify::hover', Lang.bind(this, this._hoverChanged)); + + // Create and apply height constraint to the dash. It's controlled by this.actor height + this.constrainSize = new Clutter.BindConstraint({ + source: this.actor, + coordinate: this._isHorizontal?Clutter.BindCoordinate.WIDTH:Clutter.BindCoordinate.HEIGHT + }); + this.dash.actor.add_constraint(this.constrainSize); + + this._signalsHandler.add([ + Main.overview, + 'item-drag-begin', + Lang.bind(this, this._onDragStart) + ], [ + Main.overview, + 'item-drag-end', + Lang.bind(this, this._onDragEnd) + ], [ + Main.overview, + 'item-drag-cancelled', + Lang.bind(this, this._onDragEnd) + ], [ + // update when workarea changes, for instance if other extensions modify the struts + //(like moving th panel at the bottom) + global.screen, + 'workareas-changed', + Lang.bind(this, this._resetPosition) + ], [ + Main.overview, + 'showing', + Lang.bind(this, this._onOverviewShowing) + ], [ + Main.overview, + 'hiding', + Lang.bind(this, this._onOverviewHiding) + ], [ + // Hide on appview + Main.overview.viewSelector, + 'page-changed', + Lang.bind(this, this._pageChanged) + ], [ + Main.overview.viewSelector, + 'page-empty', + Lang.bind(this, this._onPageEmpty) + ], [ + // Ensure the ShowAppsButton status is kept in sync + Main.overview.viewSelector._showAppsButton, + 'notify::checked', + Lang.bind(this, this._syncShowAppsButtonToggled) + ], [ + global.screen, + 'in-fullscreen-changed', + Lang.bind(this, this._updateBarrier) + ], [ + // Monitor windows overlapping + this._intellihide, + 'status-changed', + Lang.bind(this, this._updateDashVisibility) + ], [ + // Keep dragged icon consistent in size with this dash + this.dash, + 'icon-size-changed', + Lang.bind(this, function() { + Main.overview.dashIconSize = this.dash.iconSize; + }) + ], [ + // This duplicate the similar signal which is in owerview.js. + // Being connected and thus executed later this effectively + // overwrite any attempt to use the size of the default dash + //which given the customization is usually much smaller. + // I can't easily disconnect the original signal + Main.overview._controls.dash, + 'icon-size-changed', + Lang.bind(this, function() { + Main.overview.dashIconSize = this.dash.iconSize; + }) + ]); + + this._injectionsHandler = new Utils.InjectionsHandler(); + this._themeManager = new Theming.ThemeManager(this._settings, this); + + // Since the actor is not a topLevel child and its parent is now not added to the Chrome, + // the allocation change of the parent container (slide in and slideout) doesn't trigger + // anymore an update of the input regions. Force the update manually. + this.actor.connect('notify::allocation', + Lang.bind(Main.layoutManager, Main.layoutManager._queueUpdateRegions)); + + this.dash._container.connect('allocation-changed', Lang.bind(this, this._updateStaticBox)); + this._slider.actor.connect(this._isHorizontal ? 'notify::x' : 'notify::y', Lang.bind(this, this._updateStaticBox)); + + // sync hover after a popupmenu is closed + this.dash.connect('menu-closed', Lang.bind(this, function() { + this._box.sync_hover(); + })); + + // Load optional features that need to be activated for one dock only + if (this._monitorIndex == this._settings.get_int('preferred-monitor')) + this._enableExtraFeatures(); + // Load optional features that need to be activated once per dock + this._optionalScrollWorkspaceSwitch(); + + // Delay operations that require the shell to be fully loaded and with + // user theme applied. + + this._paintId = this.actor.connect('paint', Lang.bind(this, this._initialize)); + + // Manage the which is used to reserve space in the overview for the dock + // Add and additional dashSpacer positioned according to the dash positioning. + // It gets restored on extension unload. + this._dashSpacer = new OverviewControls.DashSpacer(); + this._dashSpacer.setDashActor(this._box); + + if (this._position == St.Side.LEFT) + Main.overview._controls._group.insert_child_at_index(this._dashSpacer, this._rtl ? -1 : 0); // insert on first + else if (this._position == St.Side.RIGHT) + Main.overview._controls._group.insert_child_at_index(this._dashSpacer, this._rtl ? 0 : -1); // insert on last + else if (this._position == St.Side.TOP) + Main.overview._overview.insert_child_at_index(this._dashSpacer, 0); + else if (this._position == St.Side.BOTTOM) + Main.overview._overview.insert_child_at_index(this._dashSpacer, -1); + + // Add dash container actor and the container to the Chrome. + this.actor.set_child(this._slider.actor); + this._slider.add_child(this._box); + this._box.add_actor(this.dash.actor); + + // Add aligning container without tracking it for input region + Main.uiGroup.add_child(this.actor); + + if (this._settings.get_boolean('dock-fixed')) { + // Note: tracking the fullscreen directly on the slider actor causes some hiccups when fullscreening + // windows of certain applications + Main.layoutManager._trackActor(this.actor, {affectsInputRegion: false, trackFullscreen: true}); + Main.layoutManager._trackActor(this._slider.actor, {affectsStruts: true}); + } + else + Main.layoutManager._trackActor(this._slider.actor); + + // Set initial position + this._resetDepth(); + this._resetPosition(); + }, + + _initialize: function() { + if (this._paintId > 0) { + this.actor.disconnect(this._paintId); + this._paintId=0; + } + + // Apply custome css class according to the settings + this._themeManager.updateCustomTheme(); + + // Since Gnome 3.8 dragging an app without having opened the overview before cause the attemp to + //animate a null target since some variables are not initialized when the viewSelector is created + if (Main.overview.viewSelector._activePage == null) + Main.overview.viewSelector._activePage = Main.overview.viewSelector._workspacesPage; + + this._updateVisibilityMode(); + + // In case we are already inside the overview when the extension is loaded, + // for instance on unlocking the screen if it was locked with the overview open. + if (Main.overview.visibleTarget) { + this._onOverviewShowing(); + this._pageChanged(); + } + + // Setup pressure barrier (GS38+ only) + this._updatePressureBarrier(); + this._updateBarrier(); + + // setup dwelling system if pressure barriers are not available + this._setupDockDwellIfNeeded(); + }, + + destroy: function() { + // Disconnect global signals + this._signalsHandler.destroy(); + // The dash, intellihide and themeManager have global signals as well internally + this.dash.destroy(); + this._intellihide.destroy(); + this._themeManager.destroy(); + + this._injectionsHandler.destroy(); + + // Destroy main clutter actor: this should be sufficient removing it and + // destroying all its children + this.actor.destroy(); + + // Remove barrier timeout + if (this._removeBarrierTimeoutId > 0) + Mainloop.source_remove(this._removeBarrierTimeoutId); + + // Remove existing barrier + this._removeBarrier(); + + // Remove pointer watcher + if (this._dockWatch) { + PointerWatcher.getPointerWatcher()._removeWatch(this._dockWatch); + this._dockWatch = null; + } + + // Remove the dashSpacer + this._dashSpacer.destroy(); + + // Restore legacyTray position + this._resetLegacyTray(); + + }, + + _bindSettingsChanges: function() { + this._signalsHandler.add([ + this._settings, + 'changed::scroll-action', + Lang.bind(this, function() { + this._optionalScrollWorkspaceSwitch(); + }) + ], [ + this._settings, + 'changed::dash-max-icon-size', + Lang.bind(this, function() { + this.dash.setIconSize(this._settings.get_int('dash-max-icon-size')); + }) + ], [ + this._settings, + 'changed::icon-size-fixed', + Lang.bind(this, function() { + this.dash.setIconSize(this._settings.get_int('dash-max-icon-size')); + }) + ], [ + this._settings, + 'changed::show-favorites', + Lang.bind(this, function() { + this.dash.resetAppIcons(); + }) + ], [ + this._settings, + 'changed::show-running', + Lang.bind(this, function() { + this.dash.resetAppIcons(); + }) + ], [ + this._settings, + 'changed::show-apps-at-top', + Lang.bind(this, function() { + this.dash.resetAppIcons(); + }) + ], [ + this._settings, + 'changed::show-show-apps-button', + Lang.bind(this, function() { + if (this._settings.get_boolean('show-show-apps-button')) + this.dash.showShowAppsButton(); + else + this.dash.hideShowAppsButton(); + }) + ], [ + this._settings, + 'changed::dock-fixed', + Lang.bind(this, function() { + if (this._settings.get_boolean('dock-fixed')) { + Main.layoutManager._untrackActor(this.actor); + Main.layoutManager._trackActor(this.actor, {affectsInputRegion: false, trackFullscreen: true}); + Main.layoutManager._untrackActor(this._slider.actor); + Main.layoutManager._trackActor(this._slider.actor, {affectsStruts: true}); + } else { + Main.layoutManager._untrackActor(this.actor); + Main.layoutManager._untrackActor(this._slider.actor); + Main.layoutManager._trackActor(this._slider.actor); + } + + this._resetPosition(); + + // Add or remove barrier depending on if dock-fixed + this._updateBarrier(); + + this._updateVisibilityMode(); + }) + ], [ + this._settings, + 'changed::intellihide', + Lang.bind(this, this._updateVisibilityMode) + ], [ + this._settings, + 'changed::intellihide-mode', + Lang.bind(this, function() { + this._intellihide.forceUpdate(); + }) + ], [ + this._settings, + 'changed::autohide', + Lang.bind(this, function() { + this._updateVisibilityMode(); + this._updateBarrier(); + }) + ], [ + this._settings, + 'changed::autohide-in-fullscreen', + Lang.bind(this, this._updateBarrier) + ], + [ + this._settings, + 'changed::extend-height', + Lang.bind(this, this._resetPosition) + ], [ + this._settings, + 'changed::height-fraction', + Lang.bind(this, this._resetPosition) + ], [ + this._settings, + 'changed::require-pressure-to-show', + Lang.bind(this, function() { + // Remove pointer watcher + if (this._dockWatch) { + PointerWatcher.getPointerWatcher()._removeWatch(this._dockWatch); + this._dockWatch = null; + } + this._setupDockDwellIfNeeded(); + this._updateBarrier(); + }) + ], [ + this._settings, + 'changed::pressure-threshold', + Lang.bind(this, function() { + this._updatePressureBarrier(); + this._updateBarrier(); + }) + ]); + + }, + + /** + * This is call when visibility settings change + */ + _updateVisibilityMode: function() { + if (this._settings.get_boolean('dock-fixed')) { + this._fixedIsEnabled = true; + this._autohideIsEnabled = false; + this._intellihideIsEnabled = false; + } + else { + this._fixedIsEnabled = false; + this._autohideIsEnabled = this._settings.get_boolean('autohide') + this._intellihideIsEnabled = this._settings.get_boolean('intellihide') + } + + if (this._intellihideIsEnabled) + this._intellihide.enable(); + else + this._intellihide.disable(); + + this._updateDashVisibility(); + }, + + /** + * Show/hide dash based on, in order of priority: + * overview visibility + * fixed mode + * intellihide + * autohide + * overview visibility + */ + _updateDashVisibility: function() { + if (Main.overview.visibleTarget) + return; + + if (this._fixedIsEnabled) { + this._removeAnimations(); + this._animateIn(this._settings.get_double('animation-time'), 0); + } + else if (this._intellihideIsEnabled) { + if (this._intellihide.getOverlapStatus()) { + this._ignoreHover = false; + // Do not hide if autohide is enabled and mouse is hover + if (!this._box.hover || !this._autohideIsEnabled) + this._animateOut(this._settings.get_double('animation-time'), 0); + } + else { + this._ignoreHover = true; + this._removeAnimations(); + this._animateIn(this._settings.get_double('animation-time'), 0); + } + } + else { + if (this._autohideIsEnabled) { + this._ignoreHover = false; + global.sync_pointer(); + + if (this._box.hover) + this._animateIn(this._settings.get_double('animation-time'), 0); + else + this._animateOut(this._settings.get_double('animation-time'), 0); + } + else + this._animateOut(this._settings.get_double('animation-time'), 0); + } + }, + + _onOverviewShowing: function() { + this._ignoreHover = true; + this._intellihide.disable(); + this._removeAnimations(); + this._animateIn(this._settings.get_double('animation-time'), 0); + }, + + _onOverviewHiding: function() { + this._ignoreHover = false; + this._intellihide.enable(); + this._updateDashVisibility(); + }, + + _hoverChanged: function() { + if (!this._ignoreHover) { + // Skip if dock is not in autohide mode for instance because it is shown + // by intellihide. + if (this._autohideIsEnabled) { + if (this._box.hover) + this._show(); + else + this._hide(); + } + } + }, + + getDockState: function() { + return this._dockState; + }, + + _show: function() { + if ((this._dockState == State.HIDDEN) || (this._dockState == State.HIDING)) { + if (this._dockState == State.HIDING) + // suppress all potential queued hiding animations - i.e. added to Tweener but not started, + // always give priority to show + this._removeAnimations(); + + this.emit('showing'); + this._animateIn(this._settings.get_double('animation-time'), 0); + } + }, + + _hide: function() { + // If no hiding animation is running or queued + if ((this._dockState == State.SHOWN) || (this._dockState == State.SHOWING)) { + let delay; + + if (this._dockState == State.SHOWING) + //if a show already started, let it finish; queue hide without removing the show. + // to obtain this I increase the delay to avoid the overlap and interference + // between the animations + delay = this._settings.get_double('hide-delay') + this._settings.get_double('animation-time'); + else + delay = this._settings.get_double('hide-delay'); + + this.emit('hiding'); + this._animateOut(this._settings.get_double('animation-time'), delay); + } + }, + + _animateIn: function(time, delay) { + this._dockState = State.SHOWING; + + Tweener.addTween(this._slider, { + slidex: 1, + time: time, + delay: delay, + transition: 'easeOutQuad', + onComplete: Lang.bind(this, function() { + this._dockState = State.SHOWN; + // Remove barrier so that mouse pointer is released and can access monitors on other side of dock + // NOTE: Delay needed to keep mouse from moving past dock and re-hiding dock immediately. This + // gives users an opportunity to hover over the dock + if (this._removeBarrierTimeoutId > 0) + Mainloop.source_remove(this._removeBarrierTimeoutId); + this._removeBarrierTimeoutId = Mainloop.timeout_add(100, Lang.bind(this, this._removeBarrier)); + }) + }); + }, + + _animateOut: function(time, delay) { + this._dockState = State.HIDING; + Tweener.addTween(this._slider, { + slidex: 0, + time: time, + delay: delay , + transition: 'easeOutQuad', + onComplete: Lang.bind(this, function() { + this._dockState = State.HIDDEN; + // Remove queued barried removal if any + if (this._removeBarrierTimeoutId > 0) + Mainloop.source_remove(this._removeBarrierTimeoutId); + this._updateBarrier(); + }) + }); + }, + + /** + * Dwelling system based on the GNOME Shell 3.14 messageTray code. + */ + _setupDockDwellIfNeeded: function() { + // If we don't have extended barrier features, then we need + // to support the old tray dwelling mechanism. + if (!global.display.supports_extended_barriers() || !this._settings.get_boolean('require-pressure-to-show')) { + let pointerWatcher = PointerWatcher.getPointerWatcher(); + this._dockWatch = pointerWatcher.addWatch(DOCK_DWELL_CHECK_INTERVAL, Lang.bind(this, this._checkDockDwell)); + this._dockDwelling = false; + this._dockDwellUserTime = 0; + } + }, + + _checkDockDwell: function(x, y) { + + let workArea = Main.layoutManager.getWorkAreaForMonitor(this._monitor.index) + let shouldDwell; + // Check for the correct screen edge, extending the sensitive area to the whole workarea, + // minus 1 px to avoid conflicting with other active corners. + if (this._position == St.Side.LEFT) + shouldDwell = (x == this._monitor.x) && (y > workArea.y) && (y < workArea.y + workArea.height); + else if (this._position == St.Side.RIGHT) + shouldDwell = (x == this._monitor.x + this._monitor.width - 1) && (y > workArea.y) && (y < workArea.y + workArea.height); + else if (this._position == St.Side.TOP) + shouldDwell = (y == this._monitor.y) && (x > workArea.x) && (x < workArea.x + workArea.width); + else if (this._position == St.Side.BOTTOM) + shouldDwell = (y == this._monitor.y + this._monitor.height - 1) && (x > workArea.x) && (x < workArea.x + workArea.width); + + if (shouldDwell) { + // We only set up dwell timeout when the user is not hovering over the dock + // already (!this._box.hover). + // The _dockDwelling variable is used so that we only try to + // fire off one dock dwell - if it fails (because, say, the user has the mouse down), + // we don't try again until the user moves the mouse up and down again. + if (!this._dockDwelling && !this._box.hover && (this._dockDwellTimeoutId == 0)) { + // Save the interaction timestamp so we can detect user input + let focusWindow = global.display.focus_window; + this._dockDwellUserTime = focusWindow ? focusWindow.user_time : 0; + + this._dockDwellTimeoutId = Mainloop.timeout_add(this._settings.get_double('show-delay') * 1000, + Lang.bind(this, this._dockDwellTimeout)); + GLib.Source.set_name_by_id(this._dockDwellTimeoutId, '[dash-to-dock] this._dockDwellTimeout'); + } + this._dockDwelling = true; + } + else { + this._cancelDockDwell(); + this._dockDwelling = false; + } + }, + + _cancelDockDwell: function() { + if (this._dockDwellTimeoutId != 0) { + Mainloop.source_remove(this._dockDwellTimeoutId); + this._dockDwellTimeoutId = 0; + } + }, + + _dockDwellTimeout: function() { + this._dockDwellTimeoutId = 0; + + if (!this._settings.get_boolean('autohide-in-fullscreen') && this._monitor.inFullscreen) + return GLib.SOURCE_REMOVE; + + // We don't want to open the tray when a modal dialog + // is up, so we check the modal count for that. When we are in the + // overview we have to take the overview's modal push into account + if (Main.modalCount > (Main.overview.visible ? 1 : 0)) + return GLib.SOURCE_REMOVE; + + // If the user interacted with the focus window since we started the tray + // dwell (by clicking or typing), don't activate the message tray + let focusWindow = global.display.focus_window; + let currentUserTime = focusWindow ? focusWindow.user_time : 0; + if (currentUserTime != this._dockDwellUserTime) + return GLib.SOURCE_REMOVE; + + // Reuse the pressure version function, the logic is the same + this._onPressureSensed(); + return GLib.SOURCE_REMOVE; + }, + + _updatePressureBarrier: function() { + this._canUsePressure = global.display.supports_extended_barriers(); + let pressureThreshold = this._settings.get_double('pressure-threshold'); + + // Remove existing pressure barrier + if (this._pressureBarrier) { + this._pressureBarrier.destroy(); + this._pressureBarrier = null; + } + + if (this._barrier) { + this._barrier.destroy(); + this._barrier = null; + } + + // Create new pressure barrier based on pressure threshold setting + if (this._canUsePressure) { + this._pressureBarrier = new Layout.PressureBarrier(pressureThreshold, this._settings.get_double('show-delay')*1000, + Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW); + this._pressureBarrier.connect('trigger', Lang.bind(this, function(barrier) { + if (!this._settings.get_boolean('autohide-in-fullscreen') && this._monitor.inFullscreen) + return; + this._onPressureSensed(); + })); + } + }, + + /** + * handler for mouse pressure sensed + */ + _onPressureSensed: function() { + if (Main.overview.visibleTarget) + return; + + // In case the mouse move away from the dock area before hovering it, in such case the leave event + // would never be triggered and the dock would stay visible forever. + let triggerTimeoutId = Mainloop.timeout_add(250, Lang.bind(this, function() { + triggerTimeoutId = 0; + + let [x, y, mods] = global.get_pointer(); + let shouldHide = true; + switch (this._position) { + case St.Side.LEFT: + if (x <= this.staticBox.x2 && + x >= this._monitor.x && + y >= this._monitor.y && + y <= this._monitor.y + this._monitor.height) { + shouldHide = false; + } + break; + case St.Side.RIGHT: + if (x >= this.staticBox.x1 && + x <= this._monitor.x + this._monitor.width && + y >= this._monitor.y && + y <= this._monitor.y + this._monitor.height) { + shouldHide = false; + } + break; + case St.Side.TOP: + if (x >= this._monitor.x && + x <= this._monitor.x + this._monitor.width && + y <= this.staticBox.y2 && + y >= this._monitor.y) { + shouldHide = false; + } + break; + case St.Side.BOTTOM: + if (x >= this._monitor.x && + x <= this._monitor.x + this._monitor.width && + y >= this.staticBox.y1 && + y <= this._monitor.y + this._monitor.height) { + shouldHide = false; + } + } + if (shouldHide) { + this._hoverChanged(); + return GLib.SOURCE_REMOVE; + } + else { + return GLib.SOURCE_CONTINUE; + } + + })); + + this._show(); + }, + + /** + * Remove pressure barrier + */ + _removeBarrier: function() { + if (this._barrier) { + if (this._pressureBarrier) + this._pressureBarrier.removeBarrier(this._barrier); + this._barrier.destroy(); + this._barrier = null; + } + this._removeBarrierTimeoutId = 0; + return false; + }, + + /** + * Update pressure barrier size + */ + _updateBarrier: function() { + // Remove existing barrier + this._removeBarrier(); + + // The barrier needs to be removed in fullscreen with autohide disabled, otherwise the mouse can + // get trapped on monitor. + if (this._monitor.inFullscreen && !this._settings.get_boolean('autohide-in-fullscreen')) + return + + // Manually reset pressure barrier + // This is necessary because we remove the pressure barrier when it is triggered to show the dock + if (this._pressureBarrier) { + this._pressureBarrier._reset(); + this._pressureBarrier._isTriggered = false; + } + + // Create new barrier + // The barrier extends to the whole workarea, minus 1 px to avoid conflicting with other active corners + // Note: dash in fixed position doesn't use pressure barrier. + if (this._canUsePressure && this._autohideIsEnabled && this._settings.get_boolean('require-pressure-to-show')) { + let x1, x2, y1, y2, direction; + let workArea = Main.layoutManager.getWorkAreaForMonitor(this._monitor.index) + + if (this._position == St.Side.LEFT) { + x1 = this._monitor.x + 1; + x2 = x1; + y1 = workArea.y + 1; + y2 = workArea.y + workArea.height - 1; + direction = Meta.BarrierDirection.POSITIVE_X; + } + else if (this._position == St.Side.RIGHT) { + x1 = this._monitor.x + this._monitor.width - 1; + x2 = x1; + y1 = workArea.y + 1; + y2 = workArea.y + workArea.height - 1; + direction = Meta.BarrierDirection.NEGATIVE_X; + } + else if (this._position == St.Side.TOP) { + x1 = workArea.x + 1; + x2 = workArea.x + workArea.width - 1; + y1 = this._monitor.y; + y2 = y1; + direction = Meta.BarrierDirection.POSITIVE_Y; + } + else if (this._position == St.Side.BOTTOM) { + x1 = workArea.x + 1; + x2 = workArea.x + workArea.width - 1; + y1 = this._monitor.y + this._monitor.height; + y2 = y1; + direction = Meta.BarrierDirection.NEGATIVE_Y; + } + + this._barrier = new Meta.Barrier({ + display: global.display, + x1: x1, + x2: x2, + y1: y1, + y2: y2, + directions: direction + }); + if (this._pressureBarrier) + this._pressureBarrier.addBarrier(this._barrier); + } + }, + + _isPrimaryMonitor: function() { + return (this._monitorIndex == Main.layoutManager.primaryIndex); + }, + + _resetPosition: function() { + // Ensure variables linked to settings are updated. + this._updateVisibilityMode(); + + let extendHeight = this._settings.get_boolean('extend-height'); + + // Note: do not use the workarea coordinates in the direction on which the dock is placed, + // to avoid a loop [position change -> workArea change -> position change] with + // fixed dock. + let workArea = Main.layoutManager.getWorkAreaForMonitor(this._monitorIndex); + + // Reserve space for the dash on the overview + // if the dock is on the primary monitor + if (this._isPrimaryMonitor()) + this._dashSpacer.show(); + else + // No space is required in the overview of the dash + this._dashSpacer.hide(); + + let fraction = this._settings.get_double('height-fraction'); + + if (extendHeight) + fraction = 1; + else if ((fraction < 0) || (fraction > 1)) + fraction = 0.95; + + let anchor_point; + + if (this._isHorizontal) { + this.actor.width = Math.round( fraction * workArea.width); + + let pos_y; + if (this._position == St.Side.BOTTOM) { + pos_y = this._monitor.y + this._monitor.height; + anchor_point = Clutter.Gravity.SOUTH_WEST; + } + else { + pos_y = this._monitor.y; + anchor_point = Clutter.Gravity.NORTH_WEST; + } + + this.actor.move_anchor_point_from_gravity(anchor_point); + this.actor.x = workArea.x + Math.round((1 - fraction) / 2 * workArea.width); + this.actor.y = pos_y; + + if (extendHeight) { + this.dash._container.set_width(this.actor.width); + this.actor.add_style_class_name('extended'); + } + else { + this.dash._container.set_width(-1); + this.actor.remove_style_class_name('extended'); + } + } + else { + this.actor.height = Math.round(fraction * workArea.height); + + let pos_x; + if (this._position == St.Side.RIGHT) { + pos_x = this._monitor.x + this._monitor.width; + anchor_point = Clutter.Gravity.NORTH_EAST; + } + else { + pos_x = this._monitor.x; + anchor_point = Clutter.Gravity.NORTH_WEST; + } + + this.actor.move_anchor_point_from_gravity(anchor_point); + this.actor.x = pos_x; + this.actor.y = workArea.y + Math.round((1 - fraction) / 2 * workArea.height); + + if (extendHeight) { + this.dash._container.set_height(this.actor.height); + this.actor.add_style_class_name('extended'); + } + else { + this.dash._container.set_height(-1); + this.actor.remove_style_class_name('extended'); + } + } + + this._y0 = this.actor.y; + + this._adjustLegacyTray(); + }, + + // Set the dash at the correct depth in z + _resetDepth: function() { + // Keep the dash below the modalDialogGroup and the legacyTray + if (Main.legacyTray && Main.legacyTray.actor) + Main.layoutManager.uiGroup.set_child_below_sibling(this.actor, Main.legacyTray.actor); + else + Main.layoutManager.uiGroup.set_child_below_sibling(this.actor, Main.layoutManager.modalDialogGroup); + }, + + _adjustLegacyTray: function() { + // The legacyTray has been removed in GNOME Shell 3.26. + // Once we drop support for previous releases this fuction can be dropped too. + if (!Main.legacyTray) + return; + + let use_work_area = true; + + if (this._fixedIsEnabled && !this._settings.get_boolean('extend-height') + && this._isPrimaryMonitor() + && ((this._position == St.Side.BOTTOM) || (this._position == St.Side.LEFT))) + use_work_area = false; + + Main.legacyTray.actor.clear_constraints(); + let constraint = new Layout.MonitorConstraint({ + primary: true, + work_area: use_work_area + }); + Main.legacyTray.actor.add_constraint(constraint); + }, + + _resetLegacyTray: function() { + // The legacyTray has been removed in GNOME Shell 3.26. + // Once we drop support for previous releases this fuction can be dropped too. + if (!Main.legacyTray) + return; + Main.legacyTray.actor.clear_constraints(); + let constraint = new Layout.MonitorConstraint({ + primary: true, + work_area: true + }); + Main.legacyTray.actor.add_constraint(constraint); + }, + + _updateStaticBox: function() { + this.staticBox.init_rect( + this.actor.x + this._slider.actor.x - (this._position == St.Side.RIGHT ? this._box.width : 0), + this.actor.y + this._slider.actor.y - (this._position == St.Side.BOTTOM ? this._box.height : 0), + this._box.width, + this._box.height + ); + + this._intellihide.updateTargetBox(this.staticBox); + }, + + _removeAnimations: function() { + Tweener.removeTweens(this._slider); + }, + + _onDragStart: function() { + // The dash need to be above the top_window_group, otherwise it doesn't + // accept dnd of app icons when not in overiew mode. + Main.layoutManager.uiGroup.set_child_above_sibling(this.actor, global.top_window_group); + this._oldignoreHover = this._ignoreHover; + this._ignoreHover = true; + this._animateIn(this._settings.get_double('animation-time'), 0); + }, + + _onDragEnd: function() { + // Restore drag default dash stack order + this._resetDepth(); + if (this._oldignoreHover !== null) + this._ignoreHover = this._oldignoreHover; + this._oldignoreHover = null; + this._box.sync_hover(); + if (Main.overview._shown) + this._pageChanged(); + }, + + _pageChanged: function() { + let activePage = Main.overview.viewSelector.getActivePage(); + let dashVisible = (activePage == ViewSelector.ViewPage.WINDOWS || + activePage == ViewSelector.ViewPage.APPS); + + if (dashVisible) + this._animateIn(this._settings.get_double('animation-time'), 0); + else + this._animateOut(this._settings.get_double('animation-time'), 0); + }, + + _onPageEmpty: function() { + /* The dash spacer is required only in the WINDOWS view if in the default position. + * The 'page-empty' signal is emitted in between a change of view, + * signalling the spacer can be added and removed without visible effect, + * as it's done for the upstream dashSpacer. + * + * Moreover, hiding the spacer ensure the appGrid allocaton is triggered. + * This matter as the appview spring animation is triggered by to first reallocaton of the appGrid, + * (See appDisplay.js, line 202 on GNOME Shell 3.14: + * this._grid.actor.connect('notify::allocation', ...) + * which in turn seems to be triggered by changes in the other actors in the overview. + * Normally, as far as I could understand, either the dashSpacer being hidden or the workspacesThumbnails + * sliding out would trigger the allocation. However, with no stock dash + * and no thumbnails, which happen if the user configured only 1 and static workspace, + * the animation out of icons is not played. + */ + + let activePage = Main.overview.viewSelector.getActivePage(); + this._dashSpacer.visible = (this._isHorizontal || activePage == ViewSelector.ViewPage.WINDOWS); + }, + + /** + * Show dock and give key focus to it + */ + _onAccessibilityFocus: function() { + this._box.navigate_focus(null, Gtk.DirectionType.TAB_FORWARD, false); + this._animateIn(this._settings.get_double('animation-time'), 0); + }, + + /** + * Keep ShowAppsButton status in sync with the overview status + */ + _syncShowAppsButtonToggled: function() { + let status = Main.overview.viewSelector._showAppsButton.checked; + if (this.dash.showAppsButton.checked !== status) + this.dash.showAppsButton.checked = status; + }, + + // Optional features to be enabled only for the main Dock + _enableExtraFeatures: function() { + // Restore dash accessibility + Main.ctrlAltTabManager.addGroup( + this.dash.actor, _('Dash'), 'user-bookmarks-symbolic', + {focusCallback: Lang.bind(this, this._onAccessibilityFocus)}); + }, + + /** + * Switch workspace by scrolling over the dock + */ + _optionalScrollWorkspaceSwitch: function() { + let label = 'optionalScrollWorkspaceSwitch'; + + function isEnabled() { + return this._settings.get_enum('scroll-action') === scrollAction.SWITCH_WORKSPACE; + } + + this._settings.connect('changed::scroll-action', Lang.bind(this, function() { + if (Lang.bind(this, isEnabled)()) + Lang.bind(this, enable)(); + else + Lang.bind(this, disable)(); + })); + + if (Lang.bind(this, isEnabled)()) + Lang.bind(this, enable)(); + + function enable() { + this._signalsHandler.removeWithLabel(label); + + this._signalsHandler.addWithLabel(label, [ + this._box, + 'scroll-event', + Lang.bind(this, onScrollEvent) + ]); + + this._optionalScrollWorkspaceSwitchDeadTimeId = 0; + } + + function disable() { + this._signalsHandler.removeWithLabel(label); + + if (this._optionalScrollWorkspaceSwitchDeadTimeId > 0) { + Mainloop.source_remove(this._optionalScrollWorkspaceSwitchDeadTimeId); + this._optionalScrollWorkspaceSwitchDeadTimeId = 0; + } + } + + // This was inspired to desktop-scroller@obsidien.github.com + function onScrollEvent(actor, event) { + // When in overview change workscape only in windows view + if (Main.overview.visible && Main.overview.viewSelector.getActivePage() !== ViewSelector.ViewPage.WINDOWS) + return false; + + let activeWs = global.screen.get_active_workspace(); + let direction = null; + + switch (event.get_scroll_direction()) { + case Clutter.ScrollDirection.UP: + direction = Meta.MotionDirection.UP; + break; + case Clutter.ScrollDirection.DOWN: + direction = Meta.MotionDirection.DOWN; + break; + case Clutter.ScrollDirection.SMOOTH: + let [dx, dy] = event.get_scroll_delta(); + if (dy < 0) + direction = Meta.MotionDirection.UP; + else if (dy > 0) + direction = Meta.MotionDirection.DOWN; + break; + } + + if (direction !== null) { + // Prevent scroll events from triggering too many workspace switches + // by adding a 250ms deadtime between each scroll event. + // Usefull on laptops when using a touchpad. + + // During the deadtime do nothing + if (this._optionalScrollWorkspaceSwitchDeadTimeId > 0) + return false; + else + this._optionalScrollWorkspaceSwitchDeadTimeId = Mainloop.timeout_add(250, Lang.bind(this, function() { + this._optionalScrollWorkspaceSwitchDeadTimeId = 0; + })); + + let ws; + + ws = activeWs.get_neighbor(direction) + + if (Main.wm._workspaceSwitcherPopup == null) + Main.wm._workspaceSwitcherPopup = new WorkspaceSwitcherPopup.WorkspaceSwitcherPopup(); + // Set the actor non reactive, so that it doesn't prevent the + // clicks events from reaching the dash actor. I can't see a reason + // why it should be reactive. + Main.wm._workspaceSwitcherPopup.actor.reactive = false; + Main.wm._workspaceSwitcherPopup.connect('destroy', function() { + Main.wm._workspaceSwitcherPopup = null; + }); + + // Do not show wokspaceSwithcer in overview + if (!Main.overview.visible) + Main.wm._workspaceSwitcherPopup.display(direction, ws.index()); + Main.wm.actionMoveWorkspace(ws); + + return true; + } + else + return false; + } + }, + + _activateApp: function(appIndex) { + let children = this.dash._box.get_children().filter(function(actor) { + return actor.child && + actor.child._delegate && + actor.child._delegate.app; + }); + + // Apps currently in the dash + let apps = children.map(function(actor) { + return actor.child._delegate; + }); + + // Activate with button = 1, i.e. same as left click + let button = 1; + if (appIndex < apps.length) + apps[appIndex].activate(button); + } + +}); + +Signals.addSignalMethods(DockedDash.prototype); + +/* + * Handle keybaord shortcuts + */ +const KeyboardShortcuts = new Lang.Class({ + + Name: 'DashToDock.KeyboardShortcuts', + + _numHotkeys: 10, + + _init: function(settings, allDocks){ + + this._settings = settings; + this._allDocks = allDocks; + this._signalsHandler = new Utils.GlobalSignalsHandler(); + + this._hotKeysEnabled = false; + if (this._settings.get_boolean('hot-keys')) + this._enableHotKeys(); + + this._signalsHandler.add([ + this._settings, + 'changed::hot-keys', + Lang.bind(this, function() { + if (this._settings.get_boolean('hot-keys')) + Lang.bind(this, this._enableHotKeys)(); + else + Lang.bind(this, this._disableHotKeys)(); + }) + ]); + + this._optionalNumberOverlay(); + }, + + destroy: function (){ + // Remove keybindings + this._disableHotKeys(); + this._disableExtraShortcut(); + this._signalsHandler.destroy(); + }, + + _enableHotKeys: function() { + if (this._hotKeysEnabled) + return; + + // Setup keyboard bindings for dash elements + let keys = ['app-hotkey-', 'app-shift-hotkey-', 'app-ctrl-hotkey-']; + keys.forEach( function(key) { + for (let i = 0; i < this._numHotkeys; i++) { + let appNum = i; + Main.wm.addKeybinding(key + (i + 1), this._settings, + Meta.KeyBindingFlags.NONE, + Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW, + Lang.bind(this, function() { + this._allDocks[0]._activateApp(appNum); + this._showOverlay(); + })); + } + }, this); + + this._hotKeysEnabled = true; + }, + + _disableHotKeys: function() { + if (!this._hotKeysEnabled) + return; + + let keys = ['app-hotkey-', 'app-shift-hotkey-', 'app-ctrl-hotkey-']; + keys.forEach( function(key) { + for (let i = 0; i < this._numHotkeys; i++) + Main.wm.removeKeybinding(key + (i + 1)); + }, this); + + this._hotKeysEnabled = false; + }, + + _optionalNumberOverlay: function() { + this._shortcutIsSet = false; + // Enable extra shortcut if either 'overlay' or 'show-dock' are true + if (this._settings.get_boolean('hot-keys') && + (this._settings.get_boolean('hotkeys-overlay') || this._settings.get_boolean('hotkeys-show-dock'))) + this._enableExtraShortcut(); + + this._signalsHandler.add([ + this._settings, + 'changed::hot-keys', + Lang.bind(this, this._checkHotkeysOptions) + ], [ + this._settings, + 'changed::hotkeys-overlay', + Lang.bind(this, this._checkHotkeysOptions) + ], [ + this._settings, + 'changed::hotkeys-show-dock', + Lang.bind(this, this._checkHotkeysOptions) + ]); + }, + + _checkHotkeysOptions: function() { + if (this._settings.get_boolean('hot-keys') && + (this._settings.get_boolean('hotkeys-overlay') || this._settings.get_boolean('hotkeys-show-dock'))) + this._enableExtraShortcut(); + else + this._disableExtraShortcut(); + }, + + _enableExtraShortcut: function() { + if (!this._shortcutIsSet) { + Main.wm.addKeybinding('shortcut', this._settings, + Meta.KeyBindingFlags.NONE, + Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW, + Lang.bind(this, this._showOverlay)); + this._shortcutIsSet = true; + } + }, + + _disableExtraShortcut: function() { + if (this._shortcutIsSet) { + Main.wm.removeKeybinding('shortcut'); + this._shortcutIsSet = false; + } + }, + + _showOverlay: function() { + for (let i = 0; i < this._allDocks.length; i++) { + let dock = this._allDocks[i]; + if (dock._settings.get_boolean('hotkeys-overlay')) + dock.dash.toggleNumberOverlay(true); + + // Restart the counting if the shortcut is pressed again + if (dock._numberOverlayTimeoutId) { + Mainloop.source_remove(dock._numberOverlayTimeoutId); + dock._numberOverlayTimeoutId = 0; + } + + // Hide the overlay/dock after the timeout + let timeout = dock._settings.get_double('shortcut-timeout') * 1000; + dock._numberOverlayTimeoutId = Mainloop.timeout_add(timeout, Lang.bind(dock, function() { + dock._numberOverlayTimeoutId = 0; + dock.dash.toggleNumberOverlay(false); + // Hide the dock again if necessary + dock._updateDashVisibility(); + })); + + // Show the dock if it is hidden + if (dock._settings.get_boolean('hotkeys-show-dock')) { + let showDock = (dock._intellihideIsEnabled || dock._autohideIsEnabled); + if (showDock) + dock._show(); + } + } + } + +}); + +/** + * Isolate overview to open new windows for inactive apps + * Note: the future implementaion is not fully contained here. Some bits are around in other methods of other classes. + * This class just take care of enabling/disabling the option. + */ +const WorkspaceIsolation = new Lang.Class({ + + Name: 'DashToDock.WorkspaceIsolation', + + _init: function(settings, allDocks) { + + this._settings = settings; + this._allDocks = allDocks; + + this._signalsHandler = new Utils.GlobalSignalsHandler(); + this._injectionsHandler = new Utils.InjectionsHandler(); + + this._signalsHandler.add([ + this._settings, + 'changed::isolate-workspaces', + Lang.bind(this, function() { + this._allDocks.forEach(function(dock) { + dock.dash.resetAppIcons(); + }); + if (this._settings.get_boolean('isolate-workspaces') || + this._settings.get_boolean('isolate-monitors')) + Lang.bind(this, this._enable)(); + else + Lang.bind(this, this._disable)(); + }) + ],[ + this._settings, + 'changed::isolate-monitors', + Lang.bind(this, function() { + this._allDocks.forEach(function(dock) { + dock.dash.resetAppIcons(); + }); + if (this._settings.get_boolean('isolate-workspaces') || + this._settings.get_boolean('isolate-monitors')) + Lang.bind(this, this._enable)(); + else + Lang.bind(this, this._disable)(); + }) + ]); + + if (this._settings.get_boolean('isolate-workspaces') || + this._settings.get_boolean('isolate-monitors')) + this._enable(); + + }, + + _enable: function() { + + // ensure I never double-register/inject + // although it should never happen + this._disable(); + + this._allDocks.forEach(function(dock) { + this._signalsHandler.addWithLabel('isolation', [ + global.screen, + 'restacked', + Lang.bind(dock.dash, dock.dash._queueRedisplay) + ], [ + global.window_manager, + 'switch-workspace', + Lang.bind(dock.dash, dock.dash._queueRedisplay) + ]); + + // This last signal is only needed for monitor isolation, as windows + // might migrate from one monitor to another without triggering 'restacked' + if (this._settings.get_boolean('isolate-monitors')) + this._signalsHandler.addWithLabel('isolation', [ + global.screen, + 'window-entered-monitor', + Lang.bind(dock.dash, dock.dash._queueRedisplay) + ]); + + }, this); + + // here this is the Shell.App + function IsolatedOverview() { + // These lines take care of Nautilus for icons on Desktop + let windows = this.get_windows().filter(function(w) { + return w.get_workspace().index() == global.screen.get_active_workspace_index(); + }); + if (windows.length == 1) + if (windows[0].skip_taskbar) + return this.open_new_window(-1); + + if (this.is_on_workspace(global.screen.get_active_workspace())) + return Main.activateWindow(windows[0]); + return this.open_new_window(-1); + } + + this._injectionsHandler.addWithLabel('isolation', [ + Shell.App.prototype, + 'activate', + IsolatedOverview + ]); + }, + + _disable: function () { + this._signalsHandler.removeWithLabel('isolation'); + this._injectionsHandler.removeWithLabel('isolation'); + }, + + destroy: function() { + this._signalsHandler.destroy(); + this._injectionsHandler.destroy(); + } + +}); + + +var DockManager = new Lang.Class({ + Name: 'DashToDock.DockManager', + + _init: function() { + this._remoteModel = new LauncherAPI.LauncherEntryRemoteModel(); + this._settings = Convenience.getSettings('org.gnome.shell.extensions.dash-to-dock'); + this._oldDash = Main.overview._dash; + /* Array of all the docks created */ + this._allDocks = []; + this._createDocks(); + + // status variable: true when the overview is shown through the dash + // applications button. + this._forcedOverview = false; + + // Connect relevant signals to the toggling function + this._bindSettingsChanges(); + }, + + _toggle: function() { + this._deleteDocks(); + this._createDocks(); + this.emit('toggled'); + }, + + _bindSettingsChanges: function() { + // Connect relevant signals to the toggling function + this._signalsHandler = new Utils.GlobalSignalsHandler(); + this._signalsHandler.add([ + global.screen, + 'monitors-changed', + Lang.bind(this, this._toggle) + ], [ + this._settings, + 'changed::multi-monitor', + Lang.bind(this, this._toggle) + ], [ + this._settings, + 'changed::preferred-monitor', + Lang.bind(this, this._toggle) + ], [ + this._settings, + 'changed::dock-position', + Lang.bind(this, this._toggle) + ], [ + this._settings, + 'changed::extend-height', + Lang.bind(this, this._adjustPanelCorners) + ], [ + this._settings, + 'changed::dock-fixed', + Lang.bind(this, this._adjustPanelCorners) + ]); + }, + + _createDocks: function() { + + this._preferredMonitorIndex = this._settings.get_int('preferred-monitor'); + // In case of multi-monitor, we consider the dock on the primary monitor to be the preferred (main) one + // regardless of the settings + // The dock goes on the primary monitor also if the settings are incosistent (e.g. desired monitor not connected). + if (this._settings.get_boolean('multi-monitor') || + this._preferredMonitorIndex < 0 || this._preferredMonitorIndex > Main.layoutManager.monitors.length - 1 + ) { + this._preferredMonitorIndex = Main.layoutManager.primaryIndex; + } else { + // Gdk and shell monitors numbering differ at least under wayland: + // While the primary monitor appears to be always index 0 in Gdk, + // the shell can assign a different number (Main.layoutManager.primaryMonitor) + // This ensure the indexing in the settings (Gdk) and in the shell are matched, + // i.e. that we start counting from the primaryMonitorIndex + this._preferredMonitorIndex = (Main.layoutManager.primaryIndex + this._preferredMonitorIndex) % Main.layoutManager.monitors.length ; + } + + // First we create the main Dock, to get the extra features to bind to this one + let dock = new DockedDash(this._settings, this._remoteModel, this._preferredMonitorIndex); + this._mainShowAppsButton = dock.dash.showAppsButton; + this._allDocks.push(dock); + + // connect app icon into the view selector + dock.dash.showAppsButton.connect('notify::checked', Lang.bind(this, this._onShowAppsButtonToggled)); + + // Make the necessary changes to Main.overview._dash + this._prepareMainDash(); + + // Adjust corners if necessary + this._adjustPanelCorners(); + + if (this._settings.get_boolean('multi-monitor')) { + let nMon = Main.layoutManager.monitors.length; + for (let iMon = 0; iMon < nMon; iMon++) { + if (iMon == this._preferredMonitorIndex) + continue; + let dock = new DockedDash(this._settings, this._remoteModel, iMon); + this._allDocks.push(dock); + // connect app icon into the view selector + dock.dash.showAppsButton.connect('notify::checked', Lang.bind(this, this._onShowAppsButtonToggled)); + } + } + + // Load optional features. We load *after* the docks are created, since + // we need to connect the signals to all dock instances. + this._workspaceIsolation = new WorkspaceIsolation(this._settings, this._allDocks); + this._keyboardShortcuts = new KeyboardShortcuts(this._settings, this._allDocks); + }, + + _prepareMainDash: function() { + // Pretend I'm the dash: meant to make appgrd swarm animation come from the + // right position of the appShowButton. + Main.overview._dash = this._allDocks[0].dash; + + // set stored icon size to the new dash + Main.overview.dashIconSize = this._allDocks[0].dash.iconSize; + + // Hide usual Dash + Main.overview._controls.dash.actor.hide(); + + // Also set dash width to 1, so it's almost not taken into account by code + // calculaing the reserved space in the overview. The reason to keep it at 1 is + // to allow its visibility change to trigger an allocaion of the appGrid which + // in turn is triggergin the appsIcon spring animation, required when no other + // actors has this effect, i.e in horizontal mode and without the workspaceThumnails + // 1 static workspace only) + Main.overview._controls.dash.actor.set_width(1); + }, + + _deleteDocks: function() { + // Remove extra features + this._workspaceIsolation.destroy(); + this._keyboardShortcuts.destroy(); + + // Delete all docks + let nDocks = this._allDocks.length; + for (let i = nDocks-1; i >= 0; i--) { + this._allDocks[i].destroy(); + this._allDocks.pop(); + } + }, + + _restoreDash: function() { + Main.overview._controls.dash.actor.show(); + Main.overview._controls.dash.actor.set_width(-1); //reset default dash size + // This force the recalculation of the icon size + Main.overview._controls.dash._maxHeight = -1; + + // reset stored icon size to the default dash + Main.overview.dashIconSize = Main.overview._controls.dash.iconSize; + + Main.overview._dash = this._oldDash; + }, + + _onShowAppsButtonToggled: function(button) { + // Sync the status of the default appButtons. Only if the two statuses are + // different, that means the user interacted with the extension provided + // application button, cutomize the behaviour. Otherwise the shell has changed the + // status (due to the _syncShowAppsButtonToggled function below) and it + // has already performed the desired action. + + let animate = this._settings.get_boolean('animate-show-apps'); + let selector = Main.overview.viewSelector; + + if (selector._showAppsButton.checked !== button.checked) { + // find visible view + let visibleView; + Main.overview.viewSelector.appDisplay._views.every(function(v, index) { + if (v.view.actor.visible) { + visibleView = index; + return false; + } + else + return true; + }); + + if (button.checked) { + // force spring animation triggering.By default the animation only + // runs if we are already inside the overview. + if (!Main.overview._shown) { + this._forcedOverview = true; + let view = Main.overview.viewSelector.appDisplay._views[visibleView].view; + let grid = view._grid; + if (animate) { + // Animate in the the appview, hide the appGrid to avoiud flashing + // Go to the appView before entering the overview, skipping the workspaces. + // Do this manually avoiding opacity in transitions so that the setting of the opacity + // to 0 doesn't get overwritten. + Main.overview.viewSelector._activePage.opacity = 0; + Main.overview.viewSelector._activePage.hide(); + Main.overview.viewSelector._activePage = Main.overview.viewSelector._appsPage; + Main.overview.viewSelector._activePage.show(); + grid.actor.opacity = 0; + + // The animation has to be trigered manually because the AppDisplay.animate + // method is waiting for an allocation not happening, as we skip the workspace view + // and the appgrid could already be allocated from previous shown. + // It has to be triggered after the overview is shown as wrong coordinates are obtained + // otherwise. + let overviewShownId = Main.overview.connect('shown', Lang.bind(this, function() { + Main.overview.disconnect(overviewShownId); + Meta.later_add(Meta.LaterType.BEFORE_REDRAW, Lang.bind(this, function() { + grid.actor.opacity = 255; + grid.animateSpring(IconGrid.AnimationDirection.IN, this._allDocks[0].dash.showAppsButton); + })); + })); + } + else { + Main.overview.viewSelector._activePage = Main.overview.viewSelector._appsPage; + Main.overview.viewSelector._activePage.show(); + grid.actor.opacity = 255; + } + + } + + // Finally show the overview + selector._showAppsButton.checked = true; + Main.overview.show(); + } + else { + if (this._forcedOverview) { + // force exiting overview if needed + + if (animate) { + // Manually trigger springout animation without activating the + // workspaceView to avoid the zoomout animation. Hide the appPage + // onComplete to avoid ugly flashing of original icons. + let view = Main.overview.viewSelector.appDisplay._views[visibleView].view; + let grid = view._grid; + view.animate(IconGrid.AnimationDirection.OUT, Lang.bind(this, function() { + Main.overview.viewSelector._appsPage.hide(); + Main.overview.hide(); + selector._showAppsButton.checked = false; + this._forcedOverview = false; + })); + } + else { + Main.overview.hide(); + this._forcedOverview = false; + } + } + else { + selector._showAppsButton.checked = false; + this._forcedOverview = false; + } + } + } + + // whenever the button is unactivated even if not by the user still reset the + // forcedOverview flag + if (button.checked == false) + this._forcedOverview = false; + }, + + destroy: function() { + this._signalsHandler.destroy(); + this._deleteDocks(); + this._revertPanelCorners(); + this._restoreDash(); + this._remoteModel.destroy(); + }, + + /** + * Adjust Panel corners + */ + _adjustPanelCorners: function() { + let position = Utils.getPosition(this._settings); + let isHorizontal = ((position == St.Side.TOP) || (position == St.Side.BOTTOM)); + let extendHeight = this._settings.get_boolean('extend-height'); + let fixedIsEnabled = this._settings.get_boolean('dock-fixed'); + let dockOnPrimary = this._settings.get_boolean('multi-monitor') || + this._preferredMonitorIndex == Main.layoutManager.primaryIndex; + + if (!isHorizontal && dockOnPrimary && extendHeight && fixedIsEnabled) { + Main.panel._rightCorner.actor.hide(); + Main.panel._leftCorner.actor.hide(); + } + else + this._revertPanelCorners(); + }, + + _revertPanelCorners: function() { + Main.panel._leftCorner.actor.show(); + Main.panel._rightCorner.actor.show(); + } +}); +Signals.addSignalMethods(DockManager.prototype); diff --git a/extensions/dash-to-dock/extension.js b/extensions/dash-to-dock/extension.js new file mode 100644 index 0000000..97c1dbb --- /dev/null +++ b/extensions/dash-to-dock/extension.js @@ -0,0 +1,23 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Docking = Me.imports.docking; +const Convenience = Me.imports.convenience; + +// We declare this with var so it can be accessed by other extensions in +// GNOME Shell 3.26+ (mozjs52+). +var dockManager; + +function init() { + Convenience.initTranslations('dashtodock'); +} + +function enable() { + dockManager = new Docking.DockManager(); +} + +function disable() { + dockManager.destroy(); + + dockManager=null; +} diff --git a/extensions/dash-to-dock/intellihide.js b/extensions/dash-to-dock/intellihide.js new file mode 100644 index 0000000..1fd2699 --- /dev/null +++ b/extensions/dash-to-dock/intellihide.js @@ -0,0 +1,323 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const GLib = imports.gi.GLib; +const Lang = imports.lang; +const Mainloop = imports.mainloop; +const Meta = imports.gi.Meta; +const Shell = imports.gi.Shell; + +const Main = imports.ui.main; +const Signals = imports.signals; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Utils = Me.imports.utils; + +// A good compromise between reactivity and efficiency; to be tuned. +const INTELLIHIDE_CHECK_INTERVAL = 100; + +const OverlapStatus = { + UNDEFINED: -1, + FALSE: 0, + TRUE: 1 +}; + +const IntellihideMode = { + ALL_WINDOWS: 0, + FOCUS_APPLICATION_WINDOWS: 1, + MAXIMIZED_WINDOWS : 2 +}; + +// List of windows type taken into account. Order is important (keep the original +// enum order). +const handledWindowTypes = [ + Meta.WindowType.NORMAL, + Meta.WindowType.DOCK, + Meta.WindowType.DIALOG, + Meta.WindowType.MODAL_DIALOG, + Meta.WindowType.TOOLBAR, + Meta.WindowType.MENU, + Meta.WindowType.UTILITY, + Meta.WindowType.SPLASHSCREEN +]; + +/** + * A rough and ugly implementation of the intellihide behaviour. + * Intallihide object: emit 'status-changed' signal when the overlap of windows + * with the provided targetBoxClutter.ActorBox changes; + */ +var Intellihide = new Lang.Class({ + Name: 'DashToDock.Intellihide', + + _init: function(settings, monitorIndex) { + // Load settings + this._settings = settings; + this._monitorIndex = monitorIndex; + + this._signalsHandler = new Utils.GlobalSignalsHandler(); + this._tracker = Shell.WindowTracker.get_default(); + this._focusApp = null; // The application whose window is focused. + this._topApp = null; // The application whose window is on top on the monitor with the dock. + + this._isEnabled = false; + this.status = OverlapStatus.UNDEFINED; + this._targetBox = null; + + this._checkOverlapTimeoutContinue = false; + this._checkOverlapTimeoutId = 0; + + this._trackedWindows = new Map(); + + // Connect global signals + this._signalsHandler.add([ + // Add signals on windows created from now on + global.display, + 'window-created', + Lang.bind(this, this._windowCreated) + ], [ + // triggered for instance when the window list order changes, + // included when the workspace is switched + global.screen, + 'restacked', + Lang.bind(this, this._checkOverlap) + ], [ + // when windows are alwasy on top, the focus window can change + // without the windows being restacked. Thus monitor window focus change. + this._tracker, + 'notify::focus-app', + Lang.bind(this, this._checkOverlap) + ], [ + // update wne monitor changes, for instance in multimonitor when monitor are attached + global.screen, + 'monitors-changed', + Lang.bind(this, this._checkOverlap ) + ]); + }, + + destroy: function() { + // Disconnect global signals + this._signalsHandler.destroy(); + + // Remove residual windows signals + this.disable(); + }, + + enable: function() { + this._isEnabled = true; + this._status = OverlapStatus.UNDEFINED; + global.get_window_actors().forEach(function(wa) { + this._addWindowSignals(wa); + }, this); + this._doCheckOverlap(); + }, + + disable: function() { + this._isEnabled = false; + + for (let wa of this._trackedWindows.keys()) { + this._removeWindowSignals(wa); + } + this._trackedWindows.clear(); + + if (this._checkOverlapTimeoutId > 0) { + Mainloop.source_remove(this._checkOverlapTimeoutId); + this._checkOverlapTimeoutId = 0; + } + }, + + _windowCreated: function(display, metaWindow) { + this._addWindowSignals(metaWindow.get_compositor_private()); + }, + + _addWindowSignals: function(wa) { + if (!this._handledWindow(wa)) + return; + let signalId = wa.connect('allocation-changed', Lang.bind(this, this._checkOverlap, wa.get_meta_window())); + this._trackedWindows.set(wa, signalId); + wa.connect('destroy', Lang.bind(this, this._removeWindowSignals)); + }, + + _removeWindowSignals: function(wa) { + if (this._trackedWindows.get(wa)) { + wa.disconnect(this._trackedWindows.get(wa)); + this._trackedWindows.delete(wa); + } + + }, + + updateTargetBox: function(box) { + this._targetBox = box; + this._checkOverlap(); + }, + + forceUpdate: function() { + this._status = OverlapStatus.UNDEFINED; + this._doCheckOverlap(); + }, + + getOverlapStatus: function() { + return (this._status == OverlapStatus.TRUE); + }, + + _checkOverlap: function() { + if (!this._isEnabled || (this._targetBox == null)) + return; + + /* Limit the number of calls to the doCheckOverlap function */ + if (this._checkOverlapTimeoutId) { + this._checkOverlapTimeoutContinue = true; + return + } + + this._doCheckOverlap(); + + this._checkOverlapTimeoutId = Mainloop.timeout_add(INTELLIHIDE_CHECK_INTERVAL, Lang.bind(this, function() { + this._doCheckOverlap(); + if (this._checkOverlapTimeoutContinue) { + this._checkOverlapTimeoutContinue = false; + return GLib.SOURCE_CONTINUE; + } else { + this._checkOverlapTimeoutId = 0; + return GLib.SOURCE_REMOVE; + } + })); + }, + + _doCheckOverlap: function() { + + if (!this._isEnabled || (this._targetBox == null)) + return; + + let overlaps = OverlapStatus.FALSE; + let windows = global.get_window_actors(); + + if (windows.length > 0) { + /* + * Get the top window on the monitor where the dock is placed. + * The idea is that we dont want to overlap with the windows of the topmost application, + * event is it's not the focused app -- for instance because in multimonitor the user + * select a window in the secondary monitor. + */ + + let topWindow = null; + for (let i = windows.length - 1; i >= 0; i--) { + let meta_win = windows[i].get_meta_window(); + if (this._handledWindow(windows[i]) && (meta_win.get_monitor() == this._monitorIndex)) { + topWindow = meta_win; + break; + } + } + + if (topWindow !== null) { + this._topApp = this._tracker.get_window_app(topWindow); + // If there isn't a focused app, use that of the window on top + this._focusApp = this._tracker.focus_app || this._topApp + + windows = windows.filter(this._intellihideFilterInteresting, this); + + for (let i = 0; i < windows.length; i++) { + let win = windows[i].get_meta_window(); + + if (win) { + let rect = win.get_frame_rect(); + + let test = (rect.x < this._targetBox.x2) && + (rect.x + rect.width > this._targetBox.x1) && + (rect.y < this._targetBox.y2) && + (rect.y + rect.height > this._targetBox.y1); + + if (test) { + overlaps = OverlapStatus.TRUE; + break; + } + } + } + } + } + + if (this._status !== overlaps) { + this._status = overlaps; + this.emit('status-changed', this._status); + } + + }, + + // Filter interesting windows to be considered for intellihide. + // Consider all windows visible on the current workspace. + // Optionally skip windows of other applications + _intellihideFilterInteresting: function(wa) { + let meta_win = wa.get_meta_window(); + if (!this._handledWindow(wa)) + return false; + + let currentWorkspace = global.screen.get_active_workspace_index(); + let wksp = meta_win.get_workspace(); + let wksp_index = wksp.index(); + + // Depending on the intellihide mode, exclude non-relevent windows + switch (this._settings.get_enum('intellihide-mode')) { + case IntellihideMode.ALL_WINDOWS: + // Do nothing + break; + + case IntellihideMode.FOCUS_APPLICATION_WINDOWS: + // Skip windows of other apps + if (this._focusApp) { + // The DropDownTerminal extension is not an application per se + // so we match its window by wm class instead + if (meta_win.get_wm_class() == 'DropDownTerminalWindow') + return true; + + let currentApp = this._tracker.get_window_app(meta_win); + let focusWindow = global.display.get_focus_window() + + // Consider half maximized windows side by side + // and windows which are alwayson top + if((currentApp != this._focusApp) && (currentApp != this._topApp) + && !((focusWindow && focusWindow.maximized_vertically && !focusWindow.maximized_horizontally) + && (meta_win.maximized_vertically && !meta_win.maximized_horizontally) + && meta_win.get_monitor() == focusWindow.get_monitor()) + && !meta_win.is_above()) + return false; + } + break; + + case IntellihideMode.MAXIMIZED_WINDOWS: + // Skip unmaximized windows + if (!meta_win.maximized_vertically && !meta_win.maximized_horizontally) + return false; + break; + } + + if ( wksp_index == currentWorkspace && meta_win.showing_on_its_workspace() ) + return true; + else + return false; + + }, + + // Filter windows by type + // inspired by Opacify@gnome-shell.localdomain.pl + _handledWindow: function(wa) { + let metaWindow = wa.get_meta_window(); + + if (!metaWindow) + return false; + + // The DropDownTerminal extension uses the POPUP_MENU window type hint + // so we match its window by wm class instead + if (metaWindow.get_wm_class() == 'DropDownTerminalWindow') + return true; + + let wtype = metaWindow.get_window_type(); + for (let i = 0; i < handledWindowTypes.length; i++) { + var hwtype = handledWindowTypes[i]; + if (hwtype == wtype) + return true; + else if (hwtype > wtype) + return false; + } + return false; + } +}); + +Signals.addSignalMethods(Intellihide.prototype); diff --git a/extensions/dash-to-dock/launcherAPI.js b/extensions/dash-to-dock/launcherAPI.js new file mode 100644 index 0000000..d051a70 --- /dev/null +++ b/extensions/dash-to-dock/launcherAPI.js @@ -0,0 +1,244 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Gio = imports.gi.Gio; +const Lang = imports.lang; +const Signals = imports.signals; + +var LauncherEntryRemoteModel = new Lang.Class({ + Name: 'DashToDock.LauncherEntryRemoteModel', + + _init: function () { + this._entriesByDBusName = {}; + + this._launcher_entry_dbus_signal_id = + Gio.DBus.session.signal_subscribe(null, // sender + 'com.canonical.Unity.LauncherEntry', // iface + null, // member + null, // path + null, // arg0 + Gio.DBusSignalFlags.NONE, + Lang.bind(this, this._onEntrySignalReceived)); + + this._dbus_name_owner_changed_signal_id = + Gio.DBus.session.signal_subscribe('org.freedesktop.DBus', // sender + 'org.freedesktop.DBus', // interface + 'NameOwnerChanged', // member + '/org/freedesktop/DBus', // path + null, // arg0 + Gio.DBusSignalFlags.NONE, + Lang.bind(this, this._onDBusNameOwnerChanged)); + + this._acquireUnityDBus(); + }, + + destroy: function () { + if (this._launcher_entry_dbus_signal_id) { + Gio.DBus.session.signal_unsubscribe(this._launcher_entry_dbus_signal_id); + } + + if (this._dbus_name_owner_changed_signal_id) { + Gio.DBus.session.signal_unsubscribe(this._dbus_name_owner_changed_signal_id); + } + + this._releaseUnityDBus(); + }, + + size: function () { + return Object.keys(this._entriesByDBusName).length; + }, + + lookupByDBusName: function (dbusName) { + return this._entriesByDBusName.hasOwnProperty(dbusName) ? this._entriesByDBusName[dbusName] : null; + }, + + lookupById: function (appId) { + let ret = []; + for (let dbusName in this._entriesByDBusName) { + let entry = this._entriesByDBusName[dbusName]; + if (entry && entry.appId() == appId) { + ret.push(entry); + } + } + + return ret; + }, + + addEntry: function (entry) { + let existingEntry = this.lookupByDBusName(entry.dbusName()); + if (existingEntry) { + existingEntry.update(entry); + } else { + this._entriesByDBusName[entry.dbusName()] = entry; + this.emit('entry-added', entry); + } + }, + + removeEntry: function (entry) { + delete this._entriesByDBusName[entry.dbusName()] + this.emit('entry-removed', entry); + }, + + _acquireUnityDBus: function () { + if (!this._unity_bus_id) { + Gio.DBus.session.own_name('com.canonical.Unity', + Gio.BusNameOwnerFlags.ALLOW_REPLACEMENT, null, null); + } + }, + + _releaseUnityDBus: function () { + if (this._unity_bus_id) { + Gio.DBus.session.unown_name(this._unity_bus_id); + this._unity_bus_id = 0; + } + }, + + _onEntrySignalReceived: function (connection, sender_name, object_path, + interface_name, signal_name, parameters, user_data) { + if (!parameters || !signal_name) + return; + + if (signal_name == 'Update') { + if (!sender_name) { + return; + } + + this._handleUpdateRequest(sender_name, parameters); + } + }, + + _onDBusNameOwnerChanged: function (connection, sender_name, object_path, + interface_name, signal_name, parameters, user_data) { + if (!parameters || !this.size()) + return; + + let [name, before, after] = parameters.deep_unpack(); + + if (!after) { + if (this._entriesByDBusName.hasOwnProperty(before)) { + this.removeEntry(this._entriesByDBusName[before]); + } + } + }, + + _handleUpdateRequest: function (senderName, parameters) { + if (!senderName || !parameters) { + return; + } + + let [appUri, properties] = parameters.deep_unpack(); + let appId = appUri.replace(/(^\w+:|^)\/\//, ''); + let entry = this.lookupByDBusName(senderName); + + if (entry) { + entry.setDBusName(senderName); + entry.update(properties); + } else { + let entry = new LauncherEntryRemote(senderName, appId, properties); + this.addEntry(entry); + } + }, +}); + +Signals.addSignalMethods(LauncherEntryRemoteModel.prototype); + +var LauncherEntryRemote = new Lang.Class({ + Name: 'DashToDock.LauncherEntryRemote', + + _init: function (dbusName, appId, properties) { + this._dbusName = dbusName; + this._appId = appId; + this._count = 0; + this._countVisible = false; + this._progress = 0.0; + this._progressVisible = false; + this.update(properties); + }, + + appId: function () { + return this._appId; + }, + + dbusName: function () { + return this._dbusName; + }, + + count: function () { + return this._count; + }, + + setCount: function (count) { + if (this._count != count) { + this._count = count; + this.emit('count-changed', this._count); + } + }, + + countVisible: function () { + return this._countVisible; + }, + + setCountVisible: function (countVisible) { + if (this._countVisible != countVisible) { + this._countVisible = countVisible; + this.emit('count-visible-changed', this._countVisible); + } + }, + + progress: function () { + return this._progress; + }, + + setProgress: function (progress) { + if (this._progress != progress) { + this._progress = progress; + this.emit('progress-changed', this._progress); + } + }, + + progressVisible: function () { + return this._progressVisible; + }, + + setProgressVisible: function (progressVisible) { + if (this._progressVisible != progressVisible) { + this._progressVisible = progressVisible; + this.emit('progress-visible-changed', this._progressVisible); + } + }, + + setDBusName: function (dbusName) { + if (this._dbusName != dbusName) { + let oldName = this._dbusName; + this._dbusName = dbusName; + this.emit('dbus-name-changed', oldName); + } + }, + + update: function (other) { + if (other instanceof LauncherEntryRemote) { + this.setDBusName(other.dbusName()) + this.setCount(other.count()); + this.setCountVisible(other.countVisible()); + this.setProgress(other.progress()); + this.setProgressVisible(other.progressVisible()) + } else { + for (let property in other) { + if (other.hasOwnProperty(property)) { + if (property == 'count') { + this.setCount(other[property].get_int64()); + } else if (property == 'count-visible') { + this.setCountVisible(other[property].get_boolean()); + } if (property == 'progress') { + this.setProgress(other[property].get_double()); + } else if (property == 'progress-visible') { + this.setProgressVisible(other[property].get_boolean()); + } else { + // Not implemented yet + } + } + } + } + }, +}); + +Signals.addSignalMethods(LauncherEntryRemote.prototype); diff --git a/extensions/dash-to-dock/media/glossy.svg b/extensions/dash-to-dock/media/glossy.svg new file mode 100644 index 0000000..55b71ba --- /dev/null +++ b/extensions/dash-to-dock/media/glossy.svg @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/extensions/dash-to-dock/media/logo.svg b/extensions/dash-to-dock/media/logo.svg new file mode 100644 index 0000000..eebd0b1 --- /dev/null +++ b/extensions/dash-to-dock/media/logo.svg @@ -0,0 +1,528 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Dash to Dock + Michele + + + + + + + + + + + + + + + + + + + + + + diff --git a/extensions/dash-to-dock/meson.build b/extensions/dash-to-dock/meson.build new file mode 100644 index 0000000..e0906fa --- /dev/null +++ b/extensions/dash-to-dock/meson.build @@ -0,0 +1,23 @@ +extension_data += configure_file( + input: metadata_name + '.in', + output: metadata_name, + configuration: metadata_conf +) + +extension_sources += files( + 'appIconIndicators.js', + 'appIcons.js', + 'convenience.js', + 'dash.js', + 'docking.js', + 'intellihide.js', + 'launcherAPI.js', + 'prefs.js', + 'Settings.ui', + 'theming.js', + 'utils.js', + 'windowPreview.js' +) +extension_schemas += files(metadata_conf.get('gschemaname') + '.gschema.xml') + +install_data(['media/logo.svg', 'media/glossy.svg'], install_dir: join_paths(extensiondir, uuid, 'media')) diff --git a/extensions/dash-to-dock/metadata.json.in b/extensions/dash-to-dock/metadata.json.in new file mode 100644 index 0000000..90eddb5 --- /dev/null +++ b/extensions/dash-to-dock/metadata.json.in @@ -0,0 +1,12 @@ +{ +"extension-id": "@extension_id@", +"uuid": "@uuid@", +"settings-schema": "@gschemaname@", +"gettext-domain": "@gettext_domain@", +"original-author": "micxgx@gmail.com", +"name": "Dash to Dock", +"description": "A dock for the Gnome Shell. This extension moves the dash out of the overview transforming it in a dock for an easier launching of applications and a faster switching between windows and desktops. Side and bottom placement options are available.", +"shell-version": [ "@shell_current@" ], +"version": 45, +"url": "https://micheleg.github.io/dash-to-dock/" +} diff --git a/extensions/dash-to-dock/org.gnome.shell.extensions.dash-to-dock.gschema.xml b/extensions/dash-to-dock/org.gnome.shell.extensions.dash-to-dock.gschema.xml new file mode 100644 index 0000000..3e4f68a --- /dev/null +++ b/extensions/dash-to-dock/org.gnome.shell.extensions.dash-to-dock.gschema.xml @@ -0,0 +1,540 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 'LEFT' + Dock position + Dock is shown on the Left, Right, Top or Bottom side of the screen. + + + 0.2 + Animation time + Sets the time duration of the autohide effect. + + + 0.25 + Show delay + Sets the delay after the mouse reaches the screen border before showing the dock. + + + 0.20 + Show delay + Sets the delay after the mouse left the dock before hiding it. + + + false + Set a custom dash background background color + Sets the color for the dash background. + + + "#ffffff" + Dash background color. + Customize the background color of the dash. + + + 'DEFAULT' + Transparency mode for the dock + FIXED: constant transparency. ADAPTIVE: lock state with the top panel when not hidden. DYNAMIC: dock takes the opaque style only when windows are close to it. + + + 'DEFAULT' + ... + DEFAULT: .... DOTS: .... + + + false + Use application icon dominant color for the indicator color + + + + false + Manually set the min and max opacity + For Adaptive and Dynamic modes, the min/max opacity values will be given by 'min-alpha' and 'max-alpha'. + + + 0.2 + Opacity of the dash background when free-floating + Sets the opacity of the dash background when no windows are close. + + + 0.8 + Opacity of the dash background when windows are close. + Sets the opacity of the dash background when windows are close. + + + 0.8 + Opacity of the dash background + Sets the opacity of the dash background when in autohide mode. + + + true + Dock dodges windows + Enable or disable intellihide mode + + + 'FOCUS_APPLICATION_WINDOWS' + Define which windows are considered for intellihide. + + + + true + Dock shown on mouse over + Enable or disable autohide mode + + + true + Require pressure to show dash + Enable or disable requiring pressure to show the dash + + + 100 + Pressure threshold + Sets how much pressure is needed to show the dash. + + + false + Enable autohide in fullscreen mode. + Enable autohide in fullscreen mode. + + + false + Dock always visible + Dock is always visible + + + true + Switch workspace by scrolling over the dock + Add the possibility to switch workspace by mouse scrolling over the dock. + + + 48 + Maximum dash icon size + Set the allowed maximum dash icon size. Allowed range: 16..64. + + + false + Fixed icon size + Keep the icon size fived by scrolling the dock. + + + false + Apply custom theme + Apply customization to the dash appearance + + + false + TODO + TODO + + + false + Customize the style of the running application indicators. + Customize the style of the running application indicators. + + + "#ffffff" + Running application indicators color + Customize the color of the running application indicators. + + + "#ffffff" + Running application indicators border color. + Customize the border color of the running application indicators. + + + 0 + Running application indicators border width. + Customize the border width of the running application indicators. + + + true + Show running apps + Show or hide running appplications icons in the dash + + + false + Provide workspace isolation + Dash shows only windows from the currentworkspace + + + false + Provide monitor isolation + Dash shows only windows from the monitor + + + true + Show preview of the open windows + Replace open windows list with windows previews + + + true + Show favorites apps + Show or hide favorite appplications icons in the dash + + + true + Show applications button + Show appplications button in the dash + + + false + Show application button at top + Show appplication button at top of the dash + + + true + Animate Show Applications from the desktop + Animate Show Applications from the desktop + + + true + Basic compatibility with bolt extensions + Make the extension work properly when bolt extensions is enabled + + + 0.90 + Dock max height (fraction of available space) + + + false + Extend the dock container to all the available height + + + -1 + Monitor on which putting the dock + Set on which monitor to put the dock, use -1 for the primary one + + + false + Enable multi-monitor docks + Show a dock on every monitor + + + true + Minimize on shift+click + + + true + Activate only one window + + + 'cycle-windows' + Action when clicking on a running app + Set the action that is executed when clicking on the icon of a running application + + + 'do-nothing' + Action when scrolling app + Set the action that is executed when scrolling on the application icon + + + 'minimize' + Action when shit+clicking on a running app + Set the action that is executed when shift+clicking on the icon of a running application + + + 'launch' + Action when clicking on a running app + Set the action that is executed when middle-clicking on the icon of a running application + + + 'launch' + Action when clicking on a running app + Set the action that is executed when shift+middle-clicking on the icon of a running application + + + true + Super Hot-Keys + Launch and switch between dash items using Super+(0-9) + + + true + Show the dock when using the hotkeys + The dock will be quickly shown so that the number-overlay is visible and app activation is easier + + + "<Super>q" + Keybinding to show the dock and the number overlay. + Behavior depends on hotkeys-show-dock and hotkeys-overlay. + + + q']]]> + Keybinding to show the dock and the number overlay. + Behavior depends on hotkeys-show-dock and hotkeys-overlay. + + + 2 + Timeout to hide the dock + Sets the time duration before the dock is hidden again. + + + true + Show the dock when using the hotkeys + The dock will be quickly shown so that the number-overlay is visible and app activation is easier + + + 1']]]> + Keybinding to launch 1st dash app + + Keybinding to launch 1st app. + + + + 2']]]> + Keybinding to launch 2nd dash app + + Keybinding to launch 2nd app. + + + + 3']]]> + Keybinding to launch 3rd dash app + + Keybinding to launch 3rd app. + + + + 4']]]> + Keybinding to launch 4th dash app + + Keybinding to launch 4th app. + + + + 5']]]> + Keybinding to launch 5th dash app + + Keybinding to launch 5th app. + + + + 6']]]> + Keybinding to launch 6th dash app + + Keybinding to launch 6th app. + + + + 7']]]> + Keybinding to launch 7th dash app + + Keybinding to launch 7th app. + + + + 8']]]> + Keybinding to launch 8th dash app + + Keybinding to launch 8th app. + + + + 9']]]> + Keybinding to launch 9th dash app + + Keybinding to launch 9th app. + + + + 0']]]> + Keybinding to launch 10th dash app + + Keybinding to launch 10th app. + + + + 1']]]> + Keybinding to trigger 1st dash app with shift behavior + + Keybinding to trigger 1st app with shift behavior. + + + + 2']]]> + Keybinding to trigger 2nd dash app with shift behavior + + Keybinding to trigger 2nd app with shift behavior. + + + + 3']]]> + Keybinding to trigger 3rd dash app with shift behavior + + Keybinding to trigger 3rd app with shift behavior. + + + + 4']]]> + Keybinding to trigger 4th dash app with shift behavior + + Keybinding to trigger 4th app with shift behavior. + + + + 5']]]> + Keybinding to trigger 5th dash app with shift behavior + + Keybinding to trigger 5th app with shift behavior. + + + + 6']]]> + Keybinding to trigger 6th dash app with shift behavior + + Keybinding to trigger 6th app with shift behavior. + + + + 7']]]> + Keybinding to trigger 7th dash app with shift behavior + + Keybinding to trigger 7th app with shift behavior. + + + + 8']]]> + Keybinding to trigger 8th dash app with shift behavior + + Keybinding to trigger 8th app with shift behavior. + + + + 9']]]> + Keybinding to trigger 9th dash app with shift behavior + + Keybinding to trigger 9th app with shift behavior. + + + + 0']]]> + Keybinding to trigger 10th dash app with shift behavior + + Keybinding to trigger 10th app with shift behavior. + + + + 1']]]> + Keybinding to trigger 1st dash app + + Keybinding to either show or launch the 1st application in the dash. + + + + 2']]]> + Keybinding to trigger 2nd dash app + + Keybinding to either show or launch the 2nd application in the dash. + + + + 3']]]> + Keybinding to trigger 3rd dash app + + Keybinding to either show or launch the 3rd application in the dash. + + + + 4']]]> + Keybinding to trigger 4th dash app + + Keybinding to either show or launch the 4th application in the dash. + + + + 5']]]> + Keybinding to trigger 5th dash app + + Keybinding to either show or launch the 5th application in the dash. + + + + 6']]]> + Keybinding to trigger 6th dash app + + Keybinding to either show or launch the 6th application in the dash. + + + + 7']]]> + Keybinding to trigger 7th dash app + + Keybinding to either show or launch the 7th application in the dash. + + + + 8']]]> + Keybinding to trigger 8th dash app + + Keybinding to either show or launch the 8th application in the dash. + + + + 9']]]> + Keybinding to trigger 9th dash app + + Keybinding to either show or launch the 9th application in the dash. + + + + 0']]]> + Keybinding to trigger 10th dash app + + Keybinding to either show or launch the 10th application in the dash. + + + + false + Force straight corners in dash + Make the borders in the dash non rounded + + + false + Enable unity7 like glossy backlit items + Emulate the unity7 backlit glossy items behaviour + + + diff --git a/extensions/dash-to-dock/prefs.js b/extensions/dash-to-dock/prefs.js new file mode 100644 index 0000000..d8d8b94 --- /dev/null +++ b/extensions/dash-to-dock/prefs.js @@ -0,0 +1,868 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; +const Gdk = imports.gi.Gdk; +const Lang = imports.lang; +const Mainloop = imports.mainloop; + +// Use __ () and N__() for the extension gettext domain, and reuse +// the shell domain with the default _() and N_() +const Gettext = imports.gettext.domain('dashtodock'); +const __ = Gettext.gettext; +const N__ = function(e) { return e }; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Convenience = Me.imports.convenience; + +const SCALE_UPDATE_TIMEOUT = 500; +const DEFAULT_ICONS_SIZES = [ 128, 96, 64, 48, 32, 24, 16 ]; + +const TransparencyMode = { + DEFAULT: 0, + FIXED: 1, + ADAPTIVE: 2, + DYNAMIC: 3 +}; + +const RunningIndicatorStyle = { + DEFAULT: 0, + DOTS: 1, + SQUARES: 2, + DASHES: 3, + SEGMENTED: 4, + SOLID: 5, + CILIORA: 6, + METRO: 7 +}; + +/** + * This function was copied from the activities-config extension + * https://github.com/nls1729/acme-code/tree/master/activities-config + * by Norman L. Smith. + */ +function cssHexString(css) { + let rrggbb = '#'; + let start; + for (let loop = 0; loop < 3; loop++) { + let end = 0; + let xx = ''; + for (let loop = 0; loop < 2; loop++) { + while (true) { + let x = css.slice(end, end + 1); + if ((x == '(') || (x == ',') || (x == ')')) + break; + end++; + } + if (loop == 0) { + end++; + start = end; + } + } + xx = parseInt(css.slice(start, end)).toString(16); + if (xx.length == 1) + xx = '0' + xx; + rrggbb += xx; + css = css.slice(end); + } + return rrggbb; +} + +function setShortcut(settings) { + let shortcut_text = settings.get_string('shortcut-text'); + let [key, mods] = Gtk.accelerator_parse(shortcut_text); + + if (Gtk.accelerator_valid(key, mods)) { + let shortcut = Gtk.accelerator_name(key, mods); + settings.set_strv('shortcut', [shortcut]); + } + else { + settings.set_strv('shortcut', []); + } +} + +const Settings = new Lang.Class({ + Name: 'DashToDock.Settings', + + _init: function() { + this._settings = Convenience.getSettings('org.gnome.shell.extensions.dash-to-dock'); + + this._rtl = (Gtk.Widget.get_default_direction() == Gtk.TextDirection.RTL); + + this._builder = new Gtk.Builder(); + this._builder.set_translation_domain(Me.metadata['gettext-domain']); + this._builder.add_from_file(Me.path + '/Settings.ui'); + + this.widget = new Gtk.ScrolledWindow({ hscrollbar_policy: Gtk.PolicyType.NEVER }); + this._notebook = this._builder.get_object('settings_notebook'); + this.widget.add(this._notebook); + + // Set a reasonable initial window height + this.widget.connect('realize', Lang.bind(this, function() { + let window = this.widget.get_toplevel(); + let [default_width, default_height] = window.get_default_size(); + window.resize(default_width, 650); + })); + + // Timeout to delay the update of the settings + this._dock_size_timeout = 0; + this._icon_size_timeout = 0; + this._opacity_timeout = 0; + + this._bindSettings(); + + this._builder.connect_signals_full(Lang.bind(this, this._connector)); + }, + + /** + * Connect signals + */ + _connector: function(builder, object, signal, handler) { + object.connect(signal, Lang.bind(this, this._SignalHandler[handler])); + }, + + _bindSettings: function() { + // Position and size panel + + // Monitor options + + this._monitors = []; + // Build options based on the number of monitors and the current settings. + let n_monitors = Gdk.Screen.get_default().get_n_monitors(); + let primary_monitor = Gdk.Screen.get_default().get_primary_monitor(); + + let monitor = this._settings.get_int('preferred-monitor'); + + // Add primary monitor with index 0, because in GNOME Shell the primary monitor is always 0 + this._builder.get_object('dock_monitor_combo').append_text(__('Primary monitor')); + this._monitors.push(0); + + // Add connected monitors + let ctr = 0; + for (let i = 0; i < n_monitors; i++) { + if (i !== primary_monitor) { + ctr++; + this._monitors.push(ctr); + this._builder.get_object('dock_monitor_combo').append_text(__('Secondary monitor ') + ctr); + } + } + + // If one of the external monitor is set as preferred, show it even if not attached + if ((monitor >= n_monitors) && (monitor !== primary_monitor)) { + this._monitors.push(monitor) + this._builder.get_object('dock_monitor_combo').append_text(__('Secondary monitor ') + ++ctr); + } + + this._builder.get_object('dock_monitor_combo').set_active(this._monitors.indexOf(monitor)); + + // Position option + let position = this._settings.get_enum('dock-position'); + + switch (position) { + case 0: + this._builder.get_object('position_top_button').set_active(true); + break; + case 1: + this._builder.get_object('position_right_button').set_active(true); + break; + case 2: + this._builder.get_object('position_bottom_button').set_active(true); + break; + case 3: + this._builder.get_object('position_left_button').set_active(true); + break; + } + + if (this._rtl) { + /* Left is Right in rtl as a setting */ + this._builder.get_object('position_left_button').set_label(__('Right')); + this._builder.get_object('position_right_button').set_label(__('Left')); + } + + // Intelligent autohide options + this._settings.bind('dock-fixed', + this._builder.get_object('intelligent_autohide_switch'), + 'active', + Gio.SettingsBindFlags.INVERT_BOOLEAN); + this._settings.bind('dock-fixed', + this._builder.get_object('intelligent_autohide_button'), + 'sensitive', + Gio.SettingsBindFlags.INVERT_BOOLEAN); + this._settings.bind('autohide', + this._builder.get_object('autohide_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('autohide-in-fullscreen', + this._builder.get_object('autohide_enable_in_fullscreen_checkbutton'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('require-pressure-to-show', + this._builder.get_object('require_pressure_checkbutton'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('intellihide', + this._builder.get_object('intellihide_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('animation-time', + this._builder.get_object('animation_duration_spinbutton'), + 'value', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('hide-delay', + this._builder.get_object('hide_timeout_spinbutton'), + 'value', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('show-delay', + this._builder.get_object('show_timeout_spinbutton'), + 'value', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('pressure-threshold', + this._builder.get_object('pressure_threshold_spinbutton'), + 'value', + Gio.SettingsBindFlags.DEFAULT); + + //this._builder.get_object('animation_duration_spinbutton').set_value(this._settings.get_double('animation-time')); + + // Create dialog for intelligent autohide advanced settings + this._builder.get_object('intelligent_autohide_button').connect('clicked', Lang.bind(this, function() { + + let dialog = new Gtk.Dialog({ title: __('Intelligent autohide customization'), + transient_for: this.widget.get_toplevel(), + use_header_bar: true, + modal: true }); + + // GTK+ leaves positive values for application-defined response ids. + // Use +1 for the reset action + dialog.add_button(__('Reset to defaults'), 1); + + let box = this._builder.get_object('intelligent_autohide_advanced_settings_box'); + dialog.get_content_area().add(box); + + this._settings.bind('intellihide', + this._builder.get_object('intellihide_mode_box'), + 'sensitive', + Gio.SettingsBindFlags.GET); + + // intellihide mode + + let intellihideModeRadioButtons = [ + this._builder.get_object('all_windows_radio_button'), + this._builder.get_object('focus_application_windows_radio_button'), + this._builder.get_object('maximized_windows_radio_button') + ]; + + intellihideModeRadioButtons[this._settings.get_enum('intellihide-mode')].set_active(true); + + this._settings.bind('autohide', + this._builder.get_object('require_pressure_checkbutton'), + 'sensitive', + Gio.SettingsBindFlags.GET); + + this._settings.bind('autohide', + this._builder.get_object('autohide_enable_in_fullscreen_checkbutton'), + 'sensitive', + Gio.SettingsBindFlags.GET); + + this._settings.bind('require-pressure-to-show', + this._builder.get_object('show_timeout_spinbutton'), + 'sensitive', + Gio.SettingsBindFlags.INVERT_BOOLEAN); + this._settings.bind('require-pressure-to-show', + this._builder.get_object('show_timeout_label'), + 'sensitive', + Gio.SettingsBindFlags.INVERT_BOOLEAN); + this._settings.bind('require-pressure-to-show', + this._builder.get_object('pressure_threshold_spinbutton'), + 'sensitive', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('require-pressure-to-show', + this._builder.get_object('pressure_threshold_label'), + 'sensitive', + Gio.SettingsBindFlags.DEFAULT); + + dialog.connect('response', Lang.bind(this, function(dialog, id) { + if (id == 1) { + // restore default settings for the relevant keys + let keys = ['intellihide', 'autohide', 'intellihide-mode', 'autohide-in-fullscreen', 'require-pressure-to-show', + 'animation-time', 'show-delay', 'hide-delay', 'pressure-threshold']; + keys.forEach(function(val) { + this._settings.set_value(val, this._settings.get_default_value(val)); + }, this); + intellihideModeRadioButtons[this._settings.get_enum('intellihide-mode')].set_active(true); + } else { + // remove the settings box so it doesn't get destroyed; + dialog.get_content_area().remove(box); + dialog.destroy(); + } + return; + })); + + dialog.show_all(); + + })); + + // size options + this._builder.get_object('dock_size_scale').set_value(this._settings.get_double('height-fraction')); + this._builder.get_object('dock_size_scale').add_mark(0.9, Gtk.PositionType.TOP, null); + let icon_size_scale = this._builder.get_object('icon_size_scale'); + icon_size_scale.set_range(8, DEFAULT_ICONS_SIZES[0]); + icon_size_scale.set_value(this._settings.get_int('dash-max-icon-size')); + DEFAULT_ICONS_SIZES.forEach(function(val) { + icon_size_scale.add_mark(val, Gtk.PositionType.TOP, val.toString()); + }); + + // Corrent for rtl languages + if (this._rtl) { + // Flip value position: this is not done automatically + this._builder.get_object('dock_size_scale').set_value_pos(Gtk.PositionType.LEFT); + icon_size_scale.set_value_pos(Gtk.PositionType.LEFT); + // I suppose due to a bug, having a more than one mark and one above a value of 100 + // makes the rendering of the marks wrong in rtl. This doesn't happen setting the scale as not flippable + // and then manually inverting it + icon_size_scale.set_flippable(false); + icon_size_scale.set_inverted(true); + } + + this._settings.bind('icon-size-fixed', this._builder.get_object('icon_size_fixed_checkbutton'), 'active', Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('extend-height', this._builder.get_object('dock_size_extend_checkbutton'), 'active', Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('extend-height', this._builder.get_object('dock_size_scale'), 'sensitive', Gio.SettingsBindFlags.INVERT_BOOLEAN); + + + // Apps panel + + this._settings.bind('show-running', + this._builder.get_object('show_running_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('isolate-workspaces', + this._builder.get_object('application_button_isolation_button'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('isolate-monitors', + this._builder.get_object('application_button_monitor_isolation_button'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('show-windows-preview', + this._builder.get_object('windows_preview_button'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('multi-monitor', + this._builder.get_object('multi_monitor_button'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('show-favorites', + this._builder.get_object('show_favorite_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('show-show-apps-button', + this._builder.get_object('show_applications_button_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('show-apps-at-top', + this._builder.get_object('application_button_first_button'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('show-show-apps-button', + this._builder.get_object('application_button_first_button'), + 'sensitive', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('animate-show-apps', + this._builder.get_object('application_button_animation_button'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('show-show-apps-button', + this._builder.get_object('application_button_animation_button'), + 'sensitive', + Gio.SettingsBindFlags.DEFAULT); + + + // Behavior panel + + this._settings.bind('hot-keys', + this._builder.get_object('hot_keys_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('hot-keys', + this._builder.get_object('overlay_button'), + 'sensitive', + Gio.SettingsBindFlags.DEFAULT); + + this._builder.get_object('click_action_combo').set_active(this._settings.get_enum('click-action')); + this._builder.get_object('click_action_combo').connect('changed', Lang.bind (this, function(widget) { + this._settings.set_enum('click-action', widget.get_active()); + })); + + this._builder.get_object('scroll_action_combo').set_active(this._settings.get_enum('scroll-action')); + this._builder.get_object('scroll_action_combo').connect('changed', Lang.bind (this, function(widget) { + this._settings.set_enum('scroll-action', widget.get_active()); + })); + + this._builder.get_object('shift_click_action_combo').connect('changed', Lang.bind (this, function(widget) { + this._settings.set_enum('shift-click-action', widget.get_active()); + })); + + this._builder.get_object('middle_click_action_combo').connect('changed', Lang.bind (this, function(widget) { + this._settings.set_enum('middle-click-action', widget.get_active()); + })); + this._builder.get_object('shift_middle_click_action_combo').connect('changed', Lang.bind (this, function(widget) { + this._settings.set_enum('shift-middle-click-action', widget.get_active()); + })); + + // Create dialog for number overlay options + this._builder.get_object('overlay_button').connect('clicked', Lang.bind(this, function() { + + let dialog = new Gtk.Dialog({ title: __('Show dock and application numbers'), + transient_for: this.widget.get_toplevel(), + use_header_bar: true, + modal: true }); + + // GTK+ leaves positive values for application-defined response ids. + // Use +1 for the reset action + dialog.add_button(__('Reset to defaults'), 1); + + let box = this._builder.get_object('box_overlay_shortcut'); + dialog.get_content_area().add(box); + + this._builder.get_object('overlay_switch').set_active(this._settings.get_boolean('hotkeys-overlay')); + this._builder.get_object('show_dock_switch').set_active(this._settings.get_boolean('hotkeys-show-dock')); + + // We need to update the shortcut 'strv' when the text is modified + this._settings.connect('changed::shortcut-text', Lang.bind(this, function() {setShortcut(this._settings);})); + this._settings.bind('shortcut-text', + this._builder.get_object('shortcut_entry'), + 'text', + Gio.SettingsBindFlags.DEFAULT); + + this._settings.bind('hotkeys-overlay', + this._builder.get_object('overlay_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('hotkeys-show-dock', + this._builder.get_object('show_dock_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('shortcut-timeout', + this._builder.get_object('timeout_spinbutton'), + 'value', + Gio.SettingsBindFlags.DEFAULT); + + dialog.connect('response', Lang.bind(this, function(dialog, id) { + if (id == 1) { + // restore default settings for the relevant keys + let keys = ['shortcut-text', 'hotkeys-overlay', 'hotkeys-show-dock', 'shortcut-timeout']; + keys.forEach(function(val) { + this._settings.set_value(val, this._settings.get_default_value(val)); + }, this); + } else { + // remove the settings box so it doesn't get destroyed; + dialog.get_content_area().remove(box); + dialog.destroy(); + } + return; + })); + + dialog.show_all(); + + })); + + // Create dialog for middle-click options + this._builder.get_object('middle_click_options_button').connect('clicked', Lang.bind(this, function() { + + let dialog = new Gtk.Dialog({ title: __('Customize middle-click behavior'), + transient_for: this.widget.get_toplevel(), + use_header_bar: true, + modal: true }); + + // GTK+ leaves positive values for application-defined response ids. + // Use +1 for the reset action + dialog.add_button(__('Reset to defaults'), 1); + + let box = this._builder.get_object('box_middle_click_options'); + dialog.get_content_area().add(box); + + this._builder.get_object('shift_click_action_combo').set_active(this._settings.get_enum('shift-click-action')); + + this._builder.get_object('middle_click_action_combo').set_active(this._settings.get_enum('middle-click-action')); + + this._builder.get_object('shift_middle_click_action_combo').set_active(this._settings.get_enum('shift-middle-click-action')); + + this._settings.bind('shift-click-action', + this._builder.get_object('shift_click_action_combo'), + 'active-id', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('middle-click-action', + this._builder.get_object('middle_click_action_combo'), + 'active-id', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('shift-middle-click-action', + this._builder.get_object('shift_middle_click_action_combo'), + 'active-id', + Gio.SettingsBindFlags.DEFAULT); + + dialog.connect('response', Lang.bind(this, function(dialog, id) { + if (id == 1) { + // restore default settings for the relevant keys + let keys = ['shift-click-action', 'middle-click-action', 'shift-middle-click-action']; + keys.forEach(function(val) { + this._settings.set_value(val, this._settings.get_default_value(val)); + }, this); + this._builder.get_object('shift_click_action_combo').set_active(this._settings.get_enum('shift-click-action')); + this._builder.get_object('middle_click_action_combo').set_active(this._settings.get_enum('middle-click-action')); + this._builder.get_object('shift_middle_click_action_combo').set_active(this._settings.get_enum('shift-middle-click-action')); + } else { + // remove the settings box so it doesn't get destroyed; + dialog.get_content_area().remove(box); + dialog.destroy(); + } + return; + })); + + dialog.show_all(); + + })); + + // Appearance Panel + + this._settings.bind('apply-custom-theme', this._builder.get_object('customize_theme'), 'sensitive', Gio.SettingsBindFlags.INVERT_BOOLEAN | Gio.SettingsBindFlags.GET); + this._settings.bind('apply-custom-theme', this._builder.get_object('builtin_theme_switch'), 'active', Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('custom-theme-shrink', this._builder.get_object('shrink_dash_switch'), 'active', Gio.SettingsBindFlags.DEFAULT); + + // Running indicators + this._builder.get_object('running_indicators_combo').set_active( + this._settings.get_enum('running-indicator-style') + ); + this._builder.get_object('running_indicators_combo').connect( + 'changed', + Lang.bind (this, function(widget) { + this._settings.set_enum('running-indicator-style', widget.get_active()); + }) + ); + + if (this._settings.get_enum('running-indicator-style') == RunningIndicatorStyle.DEFAULT) + this._builder.get_object('running_indicators_advance_settings_button').set_sensitive(false); + + this._settings.connect('changed::running-indicator-style', Lang.bind(this, function() { + if (this._settings.get_enum('running-indicator-style') == RunningIndicatorStyle.DEFAULT) + this._builder.get_object('running_indicators_advance_settings_button').set_sensitive(false); + else + this._builder.get_object('running_indicators_advance_settings_button').set_sensitive(true); + })); + + // Create dialog for running indicators advanced settings + this._builder.get_object('running_indicators_advance_settings_button').connect('clicked', Lang.bind(this, function() { + + let dialog = new Gtk.Dialog({ title: __('Customize running indicators'), + transient_for: this.widget.get_toplevel(), + use_header_bar: true, + modal: true }); + + let box = this._builder.get_object('running_dots_advance_settings_box'); + dialog.get_content_area().add(box); + + this._settings.bind('running-indicator-dominant-color', + this._builder.get_object('dominant_color_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + + this._settings.bind('custom-theme-customize-running-dots', + this._builder.get_object('dot_style_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('custom-theme-customize-running-dots', + this._builder.get_object('dot_style_settings_box'), + 'sensitive', Gio.SettingsBindFlags.DEFAULT); + + let rgba = new Gdk.RGBA(); + rgba.parse(this._settings.get_string('custom-theme-running-dots-color')); + this._builder.get_object('dot_color_colorbutton').set_rgba(rgba); + + this._builder.get_object('dot_color_colorbutton').connect('notify::color', Lang.bind(this, function(button) { + let rgba = button.get_rgba(); + let css = rgba.to_string(); + let hexString = cssHexString(css); + this._settings.set_string('custom-theme-running-dots-color', hexString); + })); + + rgba.parse(this._settings.get_string('custom-theme-running-dots-border-color')); + this._builder.get_object('dot_border_color_colorbutton').set_rgba(rgba); + + this._builder.get_object('dot_border_color_colorbutton').connect('notify::color', Lang.bind(this, function(button) { + let rgba = button.get_rgba(); + let css = rgba.to_string(); + let hexString = cssHexString(css); + this._settings.set_string('custom-theme-running-dots-border-color', hexString); + })); + + this._settings.bind('custom-theme-running-dots-border-width', + this._builder.get_object('dot_border_width_spin_button'), + 'value', + Gio.SettingsBindFlags.DEFAULT); + + + dialog.connect('response', Lang.bind(this, function(dialog, id) { + // remove the settings box so it doesn't get destroyed; + dialog.get_content_area().remove(box); + dialog.destroy(); + return; + })); + + dialog.show_all(); + + })); + + this._settings.bind('custom-background-color', this._builder.get_object('custom_background_color_switch'), 'active', Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('custom-background-color', this._builder.get_object('custom_background_color'), 'sensitive', Gio.SettingsBindFlags.DEFAULT); + + let rgba = new Gdk.RGBA(); + rgba.parse(this._settings.get_string('background-color')); + this._builder.get_object('custom_background_color').set_rgba(rgba); + + this._builder.get_object('custom_background_color').connect('notify::color', Lang.bind(this, function(button) { + let rgba = button.get_rgba(); + let css = rgba.to_string(); + let hexString = cssHexString(css); + this._settings.set_string('background-color', hexString); + })); + + // Opacity + this._builder.get_object('customize_opacity_combo').set_active( + this._settings.get_enum('transparency-mode') + ); + this._builder.get_object('customize_opacity_combo').connect( + 'changed', + Lang.bind (this, function(widget) { + this._settings.set_enum('transparency-mode', widget.get_active()); + }) + ); + + this._builder.get_object('custom_opacity_scale').set_value(this._settings.get_double('background-opacity')); + + if (this._settings.get_enum('transparency-mode') !== TransparencyMode.FIXED) + this._builder.get_object('custom_opacity_scale').set_sensitive(false); + + this._settings.connect('changed::transparency-mode', Lang.bind(this, function() { + if (this._settings.get_enum('transparency-mode') !== TransparencyMode.FIXED) + this._builder.get_object('custom_opacity_scale').set_sensitive(false); + else + this._builder.get_object('custom_opacity_scale').set_sensitive(true); + })); + + if (this._settings.get_enum('transparency-mode') !== TransparencyMode.ADAPTIVE && + this._settings.get_enum('transparency-mode') !== TransparencyMode.DYNAMIC) { + this._builder.get_object('dynamic_opacity_button').set_sensitive(false); + } + + this._settings.connect('changed::transparency-mode', Lang.bind(this, function() { + if (this._settings.get_enum('transparency-mode') !== TransparencyMode.ADAPTIVE && + this._settings.get_enum('transparency-mode') !== TransparencyMode.DYNAMIC) { + this._builder.get_object('dynamic_opacity_button').set_sensitive(false); + } + else { + this._builder.get_object('dynamic_opacity_button').set_sensitive(true); + } + })); + + // Create dialog for transparency advanced settings + this._builder.get_object('dynamic_opacity_button').connect('clicked', Lang.bind(this, function() { + + let dialog = new Gtk.Dialog({ title: __('Cutomize opacity'), + transient_for: this.widget.get_toplevel(), + use_header_bar: true, + modal: true }); + + let box = this._builder.get_object('advanced_transparency_dialog'); + dialog.get_content_area().add(box); + + this._settings.bind( + 'customize-alphas', + this._builder.get_object('customize_alphas_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT + ); + this._settings.bind( + 'customize-alphas', + this._builder.get_object('min_alpha_scale'), + 'sensitive', + Gio.SettingsBindFlags.DEFAULT + ); + this._settings.bind( + 'customize-alphas', + this._builder.get_object('max_alpha_scale'), + 'sensitive', + Gio.SettingsBindFlags.DEFAULT + ); + + this._builder.get_object('min_alpha_scale').set_value( + this._settings.get_double('min-alpha') + ); + this._builder.get_object('max_alpha_scale').set_value( + this._settings.get_double('max-alpha') + ); + + dialog.connect('response', Lang.bind(this, function(dialog, id) { + // remove the settings box so it doesn't get destroyed; + dialog.get_content_area().remove(box); + dialog.destroy(); + return; + })); + + dialog.show_all(); + })); + + + this._settings.bind('unity-backlit-items', + this._builder.get_object('unity_backlit_items_switch'), + 'active', Gio.SettingsBindFlags.DEFAULT + ); + + this._settings.bind('force-straight-corner', + this._builder.get_object('force_straight_corner_switch'), + 'active', Gio.SettingsBindFlags.DEFAULT); + + // About Panel + + this._builder.get_object('extension_version').set_label(Me.metadata.version.toString()); + }, + + /** + * Object containing all signals defined in the glade file + */ + _SignalHandler: { + dock_display_combo_changed_cb: function(combo) { + this._settings.set_int('preferred-monitor', this._monitors[combo.get_active()]); + }, + + position_top_button_toggled_cb: function(button) { + if (button.get_active()) + this._settings.set_enum('dock-position', 0); + }, + + position_right_button_toggled_cb: function(button) { + if (button.get_active()) + this._settings.set_enum('dock-position', 1); + }, + + position_bottom_button_toggled_cb: function(button) { + if (button.get_active()) + this._settings.set_enum('dock-position', 2); + }, + + position_left_button_toggled_cb: function(button) { + if (button.get_active()) + this._settings.set_enum('dock-position', 3); + }, + + icon_size_combo_changed_cb: function(combo) { + this._settings.set_int('dash-max-icon-size', this._allIconSizes[combo.get_active()]); + }, + + dock_size_scale_format_value_cb: function(scale, value) { + return Math.round(value*100)+ ' %'; + }, + + dock_size_scale_value_changed_cb: function(scale) { + // Avoid settings the size consinuosly + if (this._dock_size_timeout > 0) + Mainloop.source_remove(this._dock_size_timeout); + + this._dock_size_timeout = Mainloop.timeout_add(SCALE_UPDATE_TIMEOUT, Lang.bind(this, function() { + this._settings.set_double('height-fraction', scale.get_value()); + this._dock_size_timeout = 0; + return GLib.SOURCE_REMOVE; + })); + }, + + icon_size_scale_format_value_cb: function(scale, value) { + return value+ ' px'; + }, + + icon_size_scale_value_changed_cb: function(scale) { + // Avoid settings the size consinuosly + if (this._icon_size_timeout > 0) + Mainloop.source_remove(this._icon_size_timeout); + + this._icon_size_timeout = Mainloop.timeout_add(SCALE_UPDATE_TIMEOUT, Lang.bind(this, function() { + this._settings.set_int('dash-max-icon-size', scale.get_value()); + this._icon_size_timeout = 0; + return GLib.SOURCE_REMOVE; + })); + }, + + custom_opacity_scale_value_changed_cb: function(scale) { + // Avoid settings the opacity consinuosly as it's change is animated + if (this._opacity_timeout > 0) + Mainloop.source_remove(this._opacity_timeout); + + this._opacity_timeout = Mainloop.timeout_add(SCALE_UPDATE_TIMEOUT, Lang.bind(this, function() { + this._settings.set_double('background-opacity', scale.get_value()); + this._opacity_timeout = 0; + return GLib.SOURCE_REMOVE; + })); + }, + + min_opacity_scale_value_changed_cb: function(scale) { + // Avoid settings the opacity consinuosly as it's change is animated + if (this._opacity_timeout > 0) + Mainloop.source_remove(this._opacity_timeout); + + this._opacity_timeout = Mainloop.timeout_add(SCALE_UPDATE_TIMEOUT, Lang.bind(this, function() { + this._settings.set_double('min-alpha', scale.get_value()); + this._opacity_timeout = 0; + return GLib.SOURCE_REMOVE; + })); + }, + + max_opacity_scale_value_changed_cb: function(scale) { + // Avoid settings the opacity consinuosly as it's change is animated + if (this._opacity_timeout > 0) + Mainloop.source_remove(this._opacity_timeout); + + this._opacity_timeout = Mainloop.timeout_add(SCALE_UPDATE_TIMEOUT, Lang.bind(this, function() { + this._settings.set_double('max-alpha', scale.get_value()); + this._opacity_timeout = 0; + return GLib.SOURCE_REMOVE; + })); + }, + + custom_opacity_scale_format_value_cb: function(scale, value) { + return Math.round(value*100) + ' %'; + }, + + min_opacity_scale_format_value_cb: function(scale, value) { + return Math.round(value*100) + ' %'; + }, + + max_opacity_scale_format_value_cb: function(scale, value) { + return Math.round(value*100) + ' %'; + }, + + all_windows_radio_button_toggled_cb: function(button) { + if (button.get_active()) + this._settings.set_enum('intellihide-mode', 0); + }, + + focus_application_windows_radio_button_toggled_cb: function(button) { + if (button.get_active()) + this._settings.set_enum('intellihide-mode', 1); + }, + + maximized_windows_radio_button_toggled_cb: function(button) { + if (button.get_active()) + this._settings.set_enum('intellihide-mode', 2); + } + } +}); + +function init() { + Convenience.initTranslations(); +} + +function buildPrefsWidget() { + let settings = new Settings(); + let widget = settings.widget; + widget.show_all(); + return widget; +} diff --git a/extensions/dash-to-dock/stylesheet.css b/extensions/dash-to-dock/stylesheet.css new file mode 100644 index 0000000..6e9bf38 --- /dev/null +++ b/extensions/dash-to-dock/stylesheet.css @@ -0,0 +1,109 @@ +/* Shrink the dash by reducing padding and border radius */ +#dashtodockContainer.shrink #dash, +#dashtodockContainer.dashtodock #dash { + border:1px; + padding:0px; +} + +#dashtodockContainer.shrink.left #dash, +#dashtodockContainer.dashtodock.left #dash { + border-left: 0px; + border-radius: 0px 9px 9px 0px; +} + + +#dashtodockContainer.shrink.right #dash, +#dashtodockContainer.dashtodock.right #dash { + border-right: 0px; + border-radius: 9px 0px 0px 9px; +} + + +#dashtodockContainer.shrink.top #dash, +#dashtodockContainer.dashtodock.top #dash { + border-top: 0px; + border-radius: 0px 0px 9px 9px; +} + +#dashtodockContainer.shrink.bottom #dash, +#dashtodockContainer.dashtodock.bottom #dash { + border-bottom: 0px; + border-radius: 9px 9px 0px 0px; +} + +#dashtodockContainer.straight-corner #dash, +#dashtodockContainer.shrink.straight-corner #dash { + border-radius: 0px; +} + +/* Scrollview style */ +.bottom #dashtodockDashScrollview, +.top #dashtodockDashScrollview { + -st-hfade-offset: 24px; +} + +.left #dashtodockDashScrollview, +.right #dashtodockDashScrollview { + -st-vfade-offset: 24px; +} + +#dashtodockContainer.running-dots .dash-item-container > StButton, +#dashtodockContainer.dashtodock .dash-item-container > StButton { + transition-duration: 250; + background-size: contain; +} + +#dashtodockContainer.shrink .dash-item-container > StButton, +#dashtodockContainer.dashtodock .dash-item-container > StButton { + padding: 1px 2px; +} + +/* Dash height extended to the whole available vertical space */ +#dashtodockContainer.extended.top #dash, +#dashtodockContainer.extended.right #dash, +#dashtodockContainer.extended.bottom #dash, +#dashtodockContainer.extended.left #dash { + border-radius: 0; +} + +#dashtodockContainer.extended.top #dash, +#dashtodockContainer.extended.bottom #dash { + border-left:0px; + border-right:0px; +} + +#dashtodockContainer.extended.right #dash, +#dashtodockContainer.extended.left #dash { + border-top:0px; + border-bottom:0px; +} + +/* Running and focused application style */ + +#dashtodockContainer.running-dots .app-well-app.running > .overview-icon, +#dashtodockContainer.dashtodock .app-well-app.running > .overview-icon { + background-image:none; +} + + +#dashtodockContainer.running-dots .app-well-app.focused .overview-icon, +#dashtodockContainer.dashtodock .app-well-app.focused .overview-icon { + background-color: rgba(238, 238, 236, 0.1); +} + +#dashtodockContainer.dashtodock #dash { + background: #2e3436; +} + +#dashtodockContainer .number-overlay { + color: rgba(255,255,255,1); + background-color: rgba(0,0,0,0.8); + text-align: center; +} + +#dashtodockPreviewSeparator.popup-separator-menu-item-horizontal { + width: 1px; + height: auto; + border-right-width: 1px; + margin: 32px 0px; +} diff --git a/extensions/dash-to-dock/theming.js b/extensions/dash-to-dock/theming.js new file mode 100644 index 0000000..4b18d1a --- /dev/null +++ b/extensions/dash-to-dock/theming.js @@ -0,0 +1,672 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Clutter = imports.gi.Clutter; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Gtk = imports.gi.Gtk; +const Signals = imports.signals; +const Lang = imports.lang; +const Meta = imports.gi.Meta; +const Shell = imports.gi.Shell; +const St = imports.gi.St; +const Mainloop = imports.mainloop; + +const AppDisplay = imports.ui.appDisplay; +const AppFavorites = imports.ui.appFavorites; +const Dash = imports.ui.dash; +const DND = imports.ui.dnd; +const IconGrid = imports.ui.iconGrid; +const Main = imports.ui.main; +const PopupMenu = imports.ui.popupMenu; +const Tweener = imports.ui.tweener; +const Util = imports.misc.util; +const Workspace = imports.ui.workspace; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Dock = Me.imports.docking; +const Utils = Me.imports.utils; + +/* + * DEFAULT: transparency given by theme + * FIXED: constant transparency chosen by user + * ADAPTIVE: apply 'transparent' style to dock AND panel when + * no windows are close to the dock OR panel. + * When dock is hidden, the dock 'transparent' style only + * apply to itself. + * DYNAMIC: apply 'transparent' style when no windows are close to the dock + * */ +const TransparencyMode = { + DEFAULT: 0, + FIXED: 1, + ADAPTIVE: 2, + DYNAMIC: 3 +}; + +/** + * Manage theme customization and custom theme support + */ +var ThemeManager = new Lang.Class({ + Name: 'DashToDock.ThemeManager', + + _init: function(settings, dock) { + this._settings = settings; + this._signalsHandler = new Utils.GlobalSignalsHandler(); + this._bindSettingsChanges(); + this._actor = dock.actor; + this._dash = dock.dash; + + // initialize colors with generic values + this._customizedBackground = {red: 0, green: 0, blue: 0, alpha: 0}; + this._customizedBorder = {red: 0, green: 0, blue: 0, alpha: 0}; + this._transparency = new Transparency(this._settings, dock); + + this._signalsHandler.add([ + // When theme changes re-obtain default background color + St.ThemeContext.get_for_stage (global.stage), + 'changed', + Lang.bind(this, this.updateCustomTheme) + ], [ + // update :overview pseudoclass + Main.overview, + 'showing', + Lang.bind(this, this._onOverviewShowing) + ], [ + Main.overview, + 'hiding', + Lang.bind(this, this._onOverviewHiding) + ]); + + this._updateCustomStyleClasses(); + + // destroy themeManager when the managed actor is destroyed (e.g. extension unload) + // in order to disconnect signals + this._actor.connect('destroy', Lang.bind(this, this.destroy)); + + }, + + destroy: function() { + this._signalsHandler.destroy(); + this._transparency.destroy(); + }, + + _onOverviewShowing: function() { + this._actor.add_style_pseudo_class('overview'); + }, + + _onOverviewHiding: function() { + this._actor.remove_style_pseudo_class('overview'); + }, + + _updateDashOpacity: function() { + let newAlpha = this._settings.get_double('background-opacity'); + + let [backgroundColor, borderColor] = this._getDefaultColors(); + + if (backgroundColor==null) + return; + + // Get the background and border alphas. We check the background alpha + // for a minimum of .001 to prevent division by 0 errors + let backgroundAlpha = Math.max(Math.round(backgroundColor.alpha/2.55)/100, .001); + let borderAlpha = Math.round(borderColor.alpha/2.55)/100; + + // The border and background alphas should remain in sync + // We also limit the borderAlpha to a maximum of 1 (full opacity) + borderAlpha = Math.min((borderAlpha/backgroundAlpha)*newAlpha, 1); + + this._customizedBackground = 'rgba(' + + backgroundColor.red + ',' + + backgroundColor.green + ',' + + backgroundColor.blue + ',' + + newAlpha + ')'; + + this._customizedBorder = 'rgba(' + + borderColor.red + ',' + + borderColor.green + ',' + + borderColor.blue + ',' + + borderAlpha + ')'; + + }, + + _getDefaultColors: function() { + // Prevent shell crash if the actor is not on the stage. + // It happens enabling/disabling repeatedly the extension + if (!this._dash._container.get_stage()) + return [null, null]; + + // Remove custom style + let oldStyle = this._dash._container.get_style(); + this._dash._container.set_style(null); + + let themeNode = this._dash._container.get_theme_node(); + this._dash._container.set_style(oldStyle); + + let backgroundColor = themeNode.get_background_color(); + + // Just in case the theme has different border colors .. + // We want to find the inside border-color of the dock because it is + // the side most visible to the user. We do this by finding the side + // opposite the position + let position = Utils.getPosition(this._settings); + let side = position + 2; + if (side > 3) + side = Math.abs(side - 4); + + let borderColor = themeNode.get_border_color(side); + + return [backgroundColor, borderColor]; + }, + + _updateDashColor: function() { + // Retrieve the color. If needed we will adjust it before passing it to + // this._transparency. + let [backgroundColor, borderColor] = this._getDefaultColors(); + + if (backgroundColor==null) + return; + + if (this._settings.get_boolean('custom-background-color')) { + // When applying a custom color, we need to check the alpha value, + // if not the opacity will always be overridden by the color below. + // Note that if using 'adaptive' or 'dynamic' transparency modes, + // the opacity will be set by the opaque/transparent styles anyway. + let newAlpha = Math.round(backgroundColor.alpha/2.55)/100; + if (this._settings.get_enum('transparency-mode') == TransparencyMode.FIXED) + newAlpha = this._settings.get_double('background-opacity'); + + backgroundColor = Clutter.color_from_string(this._settings.get_string('background-color'))[1]; + this._customizedBackground = 'rgba(' + + backgroundColor.red + ',' + + backgroundColor.green + ',' + + backgroundColor.blue + ',' + + newAlpha + ')'; + + this._customizedBorder = this._customizedBackground; + } + this._transparency.setColor(backgroundColor); + }, + + _updateCustomStyleClasses: function() { + if (this._settings.get_boolean('apply-custom-theme')) + this._actor.add_style_class_name('dashtodock'); + else + this._actor.remove_style_class_name('dashtodock'); + + if (this._settings.get_boolean('custom-theme-shrink')) + this._actor.add_style_class_name('shrink'); + else + this._actor.remove_style_class_name('shrink'); + + if (this._settings.get_enum('running-indicator-style') !== 0) + this._actor.add_style_class_name('running-dots'); + else + this._actor.remove_style_class_name('running-dots'); + + // If not the built-in theme option is not selected + if (!this._settings.get_boolean('apply-custom-theme')) { + if (this._settings.get_boolean('force-straight-corner')) + this._actor.add_style_class_name('straight-corner'); + else + this._actor.remove_style_class_name('straight-corner'); + } else { + this._actor.remove_style_class_name('straight-corner'); + } + }, + + updateCustomTheme: function() { + this._updateCustomStyleClasses(); + this._updateDashOpacity(); + this._updateDashColor(); + this._adjustTheme(); + this._dash._redisplay(); + }, + + /** + * Reimported back and adapted from atomdock + */ + _adjustTheme: function() { + // Prevent shell crash if the actor is not on the stage. + // It happens enabling/disabling repeatedly the extension + if (!this._dash._container.get_stage()) + return; + + // Remove prior style edits + this._dash._container.set_style(null); + this._transparency.disable(); + + // If built-in theme is enabled do nothing else + if (this._settings.get_boolean('apply-custom-theme')) + return; + + let newStyle = ''; + let position = Utils.getPosition(this._settings); + + if (!this._settings.get_boolean('custom-theme-shrink')) { + // obtain theme border settings + let themeNode = this._dash._container.get_theme_node(); + let borderColor = themeNode.get_border_color(St.Side.TOP); + let borderWidth = themeNode.get_border_width(St.Side.TOP); + let borderRadius = themeNode.get_border_radius(St.Corner.TOPRIGHT); + + // We're copying border and corner styles to left border and top-left + // corner, also removing bottom border and bottom-right corner styles + let borderInner = ''; + let borderRadiusValue = ''; + let borderMissingStyle = ''; + + if (this._rtl && (position != St.Side.RIGHT)) + borderMissingStyle = 'border-right: ' + borderWidth + 'px solid ' + + borderColor.to_string() + ';'; + else if (!this._rtl && (position != St.Side.LEFT)) + borderMissingStyle = 'border-left: ' + borderWidth + 'px solid ' + + borderColor.to_string() + ';'; + + switch (position) { + case St.Side.LEFT: + borderInner = 'border-left'; + borderRadiusValue = '0 ' + borderRadius + 'px ' + borderRadius + 'px 0;'; + break; + case St.Side.RIGHT: + borderInner = 'border-right'; + borderRadiusValue = borderRadius + 'px 0 0 ' + borderRadius + 'px;'; + break; + case St.Side.TOP: + borderInner = 'border-top'; + borderRadiusValue = '0 0 ' + borderRadius + 'px ' + borderRadius + 'px;'; + break; + case St.Side.BOTTOM: + borderInner = 'border-bottom'; + borderRadiusValue = borderRadius + 'px ' + borderRadius + 'px 0 0;'; + break; + } + + newStyle = borderInner + ': none;' + + 'border-radius: ' + borderRadiusValue + + borderMissingStyle; + + // I do call set_style possibly twice so that only the background gets the transition. + // The transition-property css rules seems to be unsupported + this._dash._container.set_style(newStyle); + } + + // Customize background + let fixedTransparency = this._settings.get_enum('transparency-mode') == TransparencyMode.FIXED; + let defaultTransparency = this._settings.get_enum('transparency-mode') == TransparencyMode.DEFAULT; + if (!defaultTransparency && !fixedTransparency) { + this._transparency.enable(); + } + else if (!defaultTransparency || this._settings.get_boolean('custom-background-color')) { + newStyle = newStyle + 'background-color:'+ this._customizedBackground + '; ' + + 'border-color:'+ this._customizedBorder + '; ' + + 'transition-delay: 0s; transition-duration: 0.250s;'; + this._dash._container.set_style(newStyle); + } + }, + + _bindSettingsChanges: function() { + let keys = ['transparency-mode', + 'customize-alphas', + 'min-alpha', + 'max-alpha', + 'background-opacity', + 'custom-background-color', + 'background-color', + 'apply-custom-theme', + 'custom-theme-shrink', + 'custom-theme-running-dots', + 'extend-height', + 'force-straight-corner']; + + keys.forEach(function(key) { + this._signalsHandler.add([ + this._settings, + 'changed::' + key, + Lang.bind(this, this.updateCustomTheme) + ]); + }, this); + } +}); + +/** + * The following class is based on the following upstream commit: + * https://git.gnome.org/browse/gnome-shell/commit/?id=447bf55e45b00426ed908b1b1035f472c2466956 + * Transparency when free-floating + */ +const Transparency = new Lang.Class({ + Name: 'DashToDock.Transparency', + + _init: function(settings, dock) { + this._settings = settings; + this._dash = dock.dash; + this._actor = this._dash._container; + this._dockActor = dock.actor; + this._dock = dock; + this._panel = Main.panel; + this._position = Utils.getPosition(this._settings); + + this._backgroundColor = '0,0,0'; + this._transparentAlpha = '0.2'; + this._opaqueAlpha = '1'; + this._transparentAlphaBorder = '0.1'; + this._opaqueAlphaBorder = '0.5'; + this._transparentTransition = '0ms'; + this._opaqueTransition = '0ms'; + + this._updateStyles(); + + this._signalsHandler = new Utils.GlobalSignalsHandler(); + this._injectionsHandler = new Utils.InjectionsHandler(); + this._trackedWindows = new Map(); + }, + + enable: function() { + // ensure I never double-register/inject + // although it should never happen + this.disable(); + + this._signalsHandler.addWithLabel('transparency', [ + global.window_group, + 'actor-added', + Lang.bind(this, this._onWindowActorAdded) + ], [ + global.window_group, + 'actor-removed', + Lang.bind(this, this._onWindowActorRemoved) + ], [ + global.window_manager, + 'switch-workspace', + Lang.bind(this, this._updateSolidStyle) + ], [ + Main.overview, + 'hiding', + Lang.bind(this, this._updateSolidStyle) + ], [ + Main.overview, + 'showing', + Lang.bind(this, this._updateSolidStyle) + ]); + + // Window signals + global.get_window_actors().forEach(function(win) { + // An irrelevant window actor ('Gnome-shell') produces an error when the signals are + // disconnected, therefore do not add signals to it. + if (win.get_meta_window().get_wm_class() !== 'Gnome-shell') + this._onWindowActorAdded(null, win); + }, this); + + if (this._settings.get_enum('transparency-mode') === TransparencyMode.ADAPTIVE) + this._enableAdaptive(); + + if (this._actor.get_stage()) + this._updateSolidStyle(); + + this.emit('transparency-enabled'); + }, + + disable: function() { + this._disableAdaptive(); + + // ensure I never double-register/inject + // although it should never happen + this._signalsHandler.removeWithLabel('transparency'); + + for (let key of this._trackedWindows.keys()) + this._trackedWindows.get(key).forEach(id => { + key.disconnect(id); + }); + this._trackedWindows.clear(); + + this.emit('transparency-disabled'); + }, + + destroy: function() { + this.disable(); + this._signalsHandler.destroy(); + this._injectionsHandler.destroy(); + }, + + _onWindowActorAdded: function(container, metaWindowActor) { + let signalIds = []; + ['allocation-changed', 'notify::visible'].forEach(s => { + signalIds.push(metaWindowActor.connect(s, Lang.bind(this, this._updateSolidStyle))); + }); + this._trackedWindows.set(metaWindowActor, signalIds); + }, + + _onWindowActorRemoved: function(container, metaWindowActor) { + if (!this._trackedWindows.get(metaWindowActor)) + return; + + this._trackedWindows.get(metaWindowActor).forEach(id => { + metaWindowActor.disconnect(id); + }); + this._trackedWindows.delete(metaWindowActor); + this._updateSolidStyle(); + }, + + _updateSolidStyle: function() { + let isNear = this._dockIsNear() || this._panelIsNear(); + if (isNear) { + this._actor.set_style(this._opaque_style); + if (this._panel._updateSolidStyle && this._adaptiveEnabled) { + if (this._settings.get_boolean('dock-fixed') || this._panelIsNear()) + this._panel._addStyleClassName('solid'); + else + this._panel._removeStyleClassName('solid'); + } + } + else { + this._actor.set_style(this._transparent_style); + if (this._panel._updateSolidStyle && this._adaptiveEnabled) + this._panel._removeStyleClassName('solid'); + } + + this.emit('solid-style-updated', isNear); + }, + + _dockIsNear: function() { + if (this._dockActor.has_style_pseudo_class('overview')) + return false; + /* Get all the windows in the active workspace that are in the primary monitor and visible */ + let activeWorkspace = global.screen.get_active_workspace(); + let dash = this._dash; + let windows = activeWorkspace.list_windows().filter(function(metaWindow) { + return metaWindow.get_monitor() === dash._monitorIndex && + metaWindow.showing_on_its_workspace() && + metaWindow.get_window_type() != Meta.WindowType.DESKTOP; + }); + + /* Check if at least one window is near enough to the panel. + * If the dock is hidden, we need to account for the space it would take + * up when it slides out. This is avoid an ugly transition. + * */ + let factor = 0; + if (!this._settings.get_boolean('dock-fixed') && + this._dock.getDockState() == Dock.State.HIDDEN) + factor = 1; + let [leftCoord, topCoord] = this._actor.get_transformed_position(); + let threshold; + if (this._position === St.Side.LEFT) + threshold = leftCoord + this._actor.get_width() * (factor + 1); + else if (this._position === St.Side.RIGHT) + threshold = leftCoord - this._actor.get_width() * factor; + else if (this._position === St.Side.TOP) + threshold = topCoord + this._actor.get_height() * (factor + 1); + else + threshold = topCoord - this._actor.get_height() * factor; + + let scale = St.ThemeContext.get_for_stage(global.stage).scale_factor; + let isNearEnough = windows.some(Lang.bind(this, function(metaWindow) { + let coord; + if (this._position === St.Side.LEFT) { + coord = metaWindow.get_frame_rect().x; + return coord < threshold + 5 * scale; + } + else if (this._position === St.Side.RIGHT) { + coord = metaWindow.get_frame_rect().x + metaWindow.get_frame_rect().width; + return coord > threshold - 5 * scale; + } + else if (this._position === St.Side.TOP) { + coord = metaWindow.get_frame_rect().y; + return coord < threshold + 5 * scale; + } + else { + coord = metaWindow.get_frame_rect().y + metaWindow.get_frame_rect().height; + return coord > threshold - 5 * scale; + } + })); + + return isNearEnough; + }, + + _panelIsNear: function() { + if (!this._panel._updateSolidStyle || + this._settings.get_enum('transparency-mode') !== TransparencyMode.ADAPTIVE) + return false; + + if (this._panel.actor.has_style_pseudo_class('overview') || !Main.sessionMode.hasWindows) { + this._panel._removeStyleClassName('solid'); + return false; + } + + /* Get all the windows in the active workspace that are in the + * primary monitor and visible */ + let activeWorkspace = global.screen.get_active_workspace(); + let windows = activeWorkspace.list_windows().filter(function(metaWindow) { + return metaWindow.is_on_primary_monitor() && + metaWindow.showing_on_its_workspace() && + metaWindow.get_window_type() != Meta.WindowType.DESKTOP; + }); + + /* Check if at least one window is near enough to the panel */ + let [, panelTop] = this._panel.actor.get_transformed_position(); + let panelBottom = panelTop + this._panel.actor.get_height(); + let scale = St.ThemeContext.get_for_stage(global.stage).scale_factor; + let isNearEnough = windows.some(Lang.bind(this._panel, function(metaWindow) { + let verticalPosition = metaWindow.get_frame_rect().y; + return verticalPosition < panelBottom + 5 * scale; + })); + + return isNearEnough; + }, + + _updateStyles: function() { + this._getAlphas(); + + this._transparent_style = + 'background-color: rgba(' + + this._backgroundColor + ', ' + this._transparentAlpha + ');' + + 'border-color: rgba(' + + this._backgroundColor + ', ' + this._transparentAlphaBorder + ');' + + 'transition-duration: ' + this._transparentTransition + 'ms;'; + + this._opaque_style = + 'background-color: rgba(' + + this._backgroundColor + ', ' + this._opaqueAlpha + ');' + + 'border-color: rgba(' + + this._backgroundColor + ',' + this._opaqueAlphaBorder + ');' + + 'transition-duration: ' + this._opaqueTransition + 'ms;'; + + this.emit('styles-updated'); + }, + + setColor: function(color) { + this._backgroundColor = color.red + ',' + color.green + ',' + color.blue; + this._updateStyles(); + }, + + _getAlphas: function() { + // Create dummy object and add to the uiGroup to get it to the stage + let dummyObject = new St.Bin({ + name: 'dashtodockContainer', + }); + Main.uiGroup.add_child(dummyObject); + + dummyObject.add_style_class_name('opaque'); + let themeNode = dummyObject.get_theme_node(); + this._opaqueAlpha = themeNode.get_background_color().alpha / 255; + this._opaqueAlphaBorder = themeNode.get_border_color(0).alpha / 255; + this._opaqueTransition = themeNode.get_transition_duration(); + + dummyObject.add_style_class_name('transparent'); + themeNode = dummyObject.get_theme_node(); + this._transparentAlpha = themeNode.get_background_color().alpha / 255; + this._transparentAlphaBorder = themeNode.get_border_color(0).alpha / 255; + this._transparentTransition = themeNode.get_transition_duration(); + + Main.uiGroup.remove_child(dummyObject); + + if (this._settings.get_boolean('customize-alphas')) { + this._opaqueAlpha = this._settings.get_double('max-alpha'); + this._opaqueAlphaBorder = this._opaqueAlpha / 2; + this._transparentAlpha = this._settings.get_double('min-alpha'); + this._transparentAlphaBorder = this._transparentAlpha / 2; + } + + if (this._settings.get_enum('transparency-mode') === TransparencyMode.ADAPTIVE && + this._panel._updateSolidStyle) { + themeNode = this._panel.actor.get_theme_node(); + if (this._panel.actor.has_style_class_name('solid')) { + this._opaqueTransition = themeNode.get_transition_duration(); + this._panel._removeStyleClassName('solid'); + themeNode = this._panel.actor.get_theme_node(); + this._transparentTransition = themeNode.get_transition_duration(); + this._panel._addStyleClassName('solid'); + } + else { + this._transparentTransition = themeNode.get_transition_duration(); + this._panel._addStyleClassName('solid'); + themeNode = this._panel.actor.get_theme_node(); + this._opaqueTransition = themeNode.get_transition_duration(); + this._panel._removeStyleClassName('solid'); + } + } + }, + + _enableAdaptive: function() { + if (!this._panel._updateSolidStyle || + this._dash._monitorIndex !== Main.layoutManager.primaryIndex) + return; + + this._adaptiveEnabled = true; + + function UpdateSolidStyle() { + return; + } + + this._injectionsHandler.addWithLabel('adaptive', [ + this._panel, + '_updateSolidStyle', + UpdateSolidStyle + ]); + + // Once we injected the new function, we need to disconnect and + // reconnect all window signals. + for (let key of this._panel._trackedWindows.keys()) + this._panel._trackedWindows.get(key).forEach(id => { + key.disconnect(id); + }); + + for (let win of this._panel._trackedWindows.keys()) + this._panel._onWindowActorAdded(null, win); + }, + + _disableAdaptive: function() { + if (!this._adaptiveEnabled) + return; + + this._injectionsHandler.removeWithLabel('adaptive'); + this._adaptiveEnabled = false; + + // Once we removed the injection, we need to disconnect and + // reconnect all window signals. + for (let key of this._panel._trackedWindows.keys()) + this._panel._trackedWindows.get(key).forEach(id => { + key.disconnect(id); + }); + + for (let win of this._panel._trackedWindows.keys()) + this._panel._onWindowActorAdded(null, win); + } +}); +Signals.addSignalMethods(Transparency.prototype); diff --git a/extensions/dash-to-dock/utils.js b/extensions/dash-to-dock/utils.js new file mode 100644 index 0000000..6514649 --- /dev/null +++ b/extensions/dash-to-dock/utils.js @@ -0,0 +1,255 @@ +const Clutter = imports.gi.Clutter; +const Lang = imports.lang; +const St = imports.gi.St; + +/** + * Simplify global signals and function injections handling + * abstract class + */ +const BasicHandler = new Lang.Class({ + Name: 'DashToDock.BasicHandler', + + _init: function() { + this._storage = new Object(); + }, + + add: function(/* unlimited 3-long array arguments */) { + // Convert arguments object to array, concatenate with generic + let args = Array.concat('generic', Array.slice(arguments)); + // Call addWithLabel with ags as if they were passed arguments + this.addWithLabel.apply(this, args); + }, + + destroy: function() { + for( let label in this._storage ) + this.removeWithLabel(label); + }, + + addWithLabel: function(label /* plus unlimited 3-long array arguments*/) { + if (this._storage[label] == undefined) + this._storage[label] = new Array(); + + // Skip first element of the arguments + for (let i = 1; i < arguments.length; i++) { + let item = this._storage[label]; + item.push(this._create(arguments[i])); + } + }, + + removeWithLabel: function(label) { + if (this._storage[label]) { + for (let i = 0; i < this._storage[label].length; i++) + this._remove(this._storage[label][i]); + + delete this._storage[label]; + } + }, + + // Virtual methods to be implemented by subclass + + /** + * Create single element to be stored in the storage structure + */ + _create: function(item) { + throw new Error('no implementation of _create in ' + this); + }, + + /** + * Correctly delete single element + */ + _remove: function(item) { + throw new Error('no implementation of _remove in ' + this); + } +}); + +/** + * Manage global signals + */ +var GlobalSignalsHandler = new Lang.Class({ + Name: 'DashToDock.GlobalSignalHandler', + Extends: BasicHandler, + + _create: function(item) { + let object = item[0]; + let event = item[1]; + let callback = item[2] + let id = object.connect(event, callback); + + return [object, id]; + }, + + _remove: function(item) { + item[0].disconnect(item[1]); + } +}); + +/** + * Color manipulation utilities + */ +var ColorUtils = { + + // Darken or brigthen color by a fraction dlum + // Each rgb value is modified by the same fraction. + // Return "#rrggbb" string + ColorLuminance: function(r, g, b, dlum) { + let rgbString = '#'; + + rgbString += Math.round(Math.min(Math.max(r*(1+dlum), 0), 255)).toString(16); + rgbString += Math.round(Math.min(Math.max(g*(1+dlum), 0), 255)).toString(16); + rgbString += Math.round(Math.min(Math.max(b*(1+dlum), 0), 255)).toString(16); + + return rgbString; + }, + + // Convert hsv ([0-1, 0-1, 0-1]) to rgb ([0-255, 0-255, 0-255]). + // Following algorithm in https://en.wikipedia.org/wiki/HSL_and_HSV + // here with h = [0,1] instead of [0, 360] + // Accept either (h,s,v) independently or {h:h, s:s, v:v} object. + // Return {r:r, g:g, b:b} object. + HSVtoRGB: function(h, s, v) { + if (arguments.length === 1) { + s = h.s; + v = h.v; + h = h.h; + } + + let r,g,b; + let c = v*s; + let h1 = h*6; + let x = c*(1 - Math.abs(h1 % 2 - 1)); + let m = v - c; + + if (h1 <=1) + r = c + m, g = x + m, b = m; + else if (h1 <=2) + r = x + m, g = c + m, b = m; + else if (h1 <=3) + r = m, g = c + m, b = x + m; + else if (h1 <=4) + r = m, g = x + m, b = c + m; + else if (h1 <=5) + r = x + m, g = m, b = c + m; + else + r = c + m, g = m, b = x + m; + + return { + r: Math.round(r * 255), + g: Math.round(g * 255), + b: Math.round(b * 255) + }; + }, + + // Convert rgb ([0-255, 0-255, 0-255]) to hsv ([0-1, 0-1, 0-1]). + // Following algorithm in https://en.wikipedia.org/wiki/HSL_and_HSV + // here with h = [0,1] instead of [0, 360] + // Accept either (r,g,b) independently or {r:r, g:g, b:b} object. + // Return {h:h, s:s, v:v} object. + RGBtoHSV: function (r, g, b) { + if (arguments.length === 1) { + r = r.r; + g = r.g; + b = r.b; + } + + let h,s,v; + + let M = Math.max(r, g, b); + let m = Math.min(r, g, b); + let c = M - m; + + if (c == 0) + h = 0; + else if (M == r) + h = ((g-b)/c) % 6; + else if (M == g) + h = (b-r)/c + 2; + else + h = (r-g)/c + 4; + + h = h/6; + v = M/255; + if (M !== 0) + s = c/M; + else + s = 0; + + return { + h: h, + s: s, + v: v + }; + } +}; + +/** + * Manage function injection: both instances and prototype can be overridden + * and restored + */ +var InjectionsHandler = new Lang.Class({ + Name: 'DashToDock.InjectionsHandler', + Extends: BasicHandler, + + _create: function(item) { + let object = item[0]; + let name = item[1]; + let injectedFunction = item[2]; + let original = object[name]; + + object[name] = injectedFunction; + return [object, name, injectedFunction, original]; + }, + + _remove: function(item) { + let object = item[0]; + let name = item[1]; + let original = item[3]; + object[name] = original; + } +}); + +/** + * Return the actual position reverseing left and right in rtl + */ +function getPosition(settings) { + let position = settings.get_enum('dock-position'); + if (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL) { + if (position == St.Side.LEFT) + position = St.Side.RIGHT; + else if (position == St.Side.RIGHT) + position = St.Side.LEFT; + } + return position; +} + +function drawRoundedLine(cr, x, y, width, height, isRoundLeft, isRoundRight, stroke, fill) { + if (height > width) { + y += Math.floor((height - width) / 2.0); + height = width; + } + + height = 2.0 * Math.floor(height / 2.0); + + var leftRadius = isRoundLeft ? height / 2.0 : 0.0; + var rightRadius = isRoundRight ? height / 2.0 : 0.0; + + cr.moveTo(x + width - rightRadius, y); + cr.lineTo(x + leftRadius, y); + if (isRoundLeft) + cr.arcNegative(x + leftRadius, y + leftRadius, leftRadius, -Math.PI/2, Math.PI/2); + else + cr.lineTo(x, y + height); + cr.lineTo(x + width - rightRadius, y + height); + if (isRoundRight) + cr.arcNegative(x + width - rightRadius, y + rightRadius, rightRadius, Math.PI/2, -Math.PI/2); + else + cr.lineTo(x + width, y); + cr.closePath(); + + if (fill != null) { + cr.setSource(fill); + cr.fillPreserve(); + } + if (stroke != null) + cr.setSource(stroke); + cr.stroke(); +} diff --git a/extensions/dash-to-dock/windowPreview.js b/extensions/dash-to-dock/windowPreview.js new file mode 100644 index 0000000..4b99aa8 --- /dev/null +++ b/extensions/dash-to-dock/windowPreview.js @@ -0,0 +1,630 @@ +/* + * Credits: + * This file is based on code from the Dash to Panel extension by Jason DeRose + * and code from the Taskbar extension by Zorin OS + * Some code was also adapted from the upstream Gnome Shell source code. + */ +const Clutter = imports.gi.Clutter; +const GLib = imports.gi.GLib; +const Lang = imports.lang; +const St = imports.gi.St; +const Mainloop = imports.mainloop; +const Main = imports.ui.main; +const Gtk = imports.gi.Gtk; + +const Params = imports.misc.params; +const PopupMenu = imports.ui.popupMenu; +const Tweener = imports.ui.tweener; +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; + +const WindowPreviewMenu = new Lang.Class({ + Name: 'WindowPreviewMenu', + Extends: PopupMenu.PopupMenu, + + _init: function(source, settings) { + this._dtdSettings = settings; + + let side = Utils.getPosition(settings); + + 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; + + this.actor.add_style_class_name('app-well-menu'); + this.actor.set_style('max-width: ' + (Main.layoutManager.monitors[monitorIndex].width - 22) + 'px; ' + + 'max-height: ' + (Main.layoutManager.monitors[monitorIndex].height - 22) + 'px;'); + this.actor.hide(); + + // Chain our visibility and lifecycle to that of the source + this._mappedId = this._source.actor.connect('notify::mapped', Lang.bind(this, function () { + if (!this._source.actor.mapped) + this.close(); + })); + this._destroyId = this._source.actor.connect('destroy', Lang.bind(this, this.destroy)); + + Main.uiGroup.add_actor(this.actor); + + // Change the initialized side where required. + this._arrowSide = side; + this._boxPointer._arrowSide = side; + this._boxPointer._userArrowSide = side; + + this._previewBox = new WindowPreviewList(this._source, this._dtdSettings); + this.addMenuItem(this._previewBox); + }, + + _redisplay: function() { + this._previewBox._shownInitially = false; + this._previewBox._redisplay(); + }, + + popup: function() { + let windows = this._source.getInterestingWindows(); + 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 () { + if (this._mappedId) + this._source.actor.disconnect(this._mappedId); + + if (this._destroyId) + this._source.actor.disconnect(this._destroyId); + + this.parent(); + } + +}); + +const WindowPreviewList = new Lang.Class({ + Name: 'WindowPreviewMenuSection', + Extends: PopupMenu.PopupMenuSection, + + _init: function(source, settings) { + this._dtdSettings = settings; + + this.parent(); + + this.actor = new St.ScrollView({ name: 'dashtodockWindowScrollview', + hscrollbar_policy: Gtk.PolicyType.NEVER, + vscrollbar_policy: Gtk.PolicyType.NEVER, + enable_mouse_scrolling: true }); + + this.actor.connect('scroll-event', Lang.bind(this, this._onScrollEvent )); + + let position = Utils.getPosition(this._dtdSettings); + this.isHorizontal = position == St.Side.BOTTOM || position == St.Side.TOP; + this.box.set_vertical(!this.isHorizontal); + this.box.set_name('dashtodockWindowList'); + this.actor.add_actor(this.box); + this.actor._delegate = this; + + this._shownInitially = false; + + this._source = source; + this.app = source.app; + + this._redisplayId = Main.initializeDeferredWork(this.actor, Lang.bind(this, this._redisplay)); + + this.actor.connect('destroy', Lang.bind(this, this._onDestroy)); + this._stateChangedId = this.app.connect('windows-changed', + Lang.bind(this, + this._queueRedisplay)); + }, + + _queueRedisplay: function () { + Main.queueDeferredWork(this._redisplayId); + }, + + _onScrollEvent: function(actor, event) { + // Event coordinates are relative to the stage but can be transformed + // as the actor will only receive events within his bounds. + let stage_x, stage_y, ok, event_x, event_y, actor_w, actor_h; + [stage_x, stage_y] = event.get_coords(); + [ok, event_x, event_y] = actor.transform_stage_point(stage_x, stage_y); + [actor_w, actor_h] = actor.get_size(); + + // If the scroll event is within a 1px margin from + // the relevant edge of the actor, let the event propagate. + if (event_y >= actor_h - 2) + return Clutter.EVENT_PROPAGATE; + + // Skip to avoid double events mouse + if (event.is_pointer_emulated()) + return Clutter.EVENT_STOP; + + let adjustment, delta; + + if (this.isHorizontal) + adjustment = this.actor.get_hscroll_bar().get_adjustment(); + else + adjustment = this.actor.get_vscroll_bar().get_adjustment(); + + let increment = adjustment.step_increment; + + switch ( event.get_scroll_direction() ) { + case Clutter.ScrollDirection.UP: + delta = -increment; + break; + case Clutter.ScrollDirection.DOWN: + delta = +increment; + break; + case Clutter.ScrollDirection.SMOOTH: + let [dx, dy] = event.get_scroll_delta(); + delta = dy*increment; + delta += dx*increment; + break; + + } + + adjustment.set_value(adjustment.get_value() + delta); + + return Clutter.EVENT_STOP; + }, + + _onDestroy: function() { + this.app.disconnect(this._stateChangedId); + this._stateChangedId = 0; + }, + + _createPreviewItem: function(window) { + let preview = new WindowPreviewMenuItem(window); + return preview; + }, + + _redisplay: function () { + // Remove separator + let nonWinItem = this._getMenuItems().filter(function(actor) { + return !actor._window; + }); + for (let i = 0; i < nonWinItem.length; i++) { + let item = nonWinItem[i]; + item.destroy(); + } + + let children = this._getMenuItems().filter(function(actor) { + return actor._window; + }); + + // Windows currently on the menu + let oldWin = children.map(function(actor) { + return actor._window; + }); + + // All app windows + let newWin = this._source.getInterestingWindows().sort(this.sortWindowsCompareFunction); + + let addedItems = []; + let removedActors = []; + + let newIndex = 0; + let oldIndex = 0; + + while (newIndex < newWin.length || oldIndex < oldWin.length) { + // No change at oldIndex/newIndex + if (oldWin[oldIndex] && + oldWin[oldIndex] == newWin[newIndex]) { + oldIndex++; + newIndex++; + continue; + } + + // Window removed at oldIndex + if (oldWin[oldIndex] && + newWin.indexOf(oldWin[oldIndex]) == -1) { + removedActors.push(children[oldIndex]); + oldIndex++; + continue; + } + + // Window added at newIndex + if (newWin[newIndex] && + oldWin.indexOf(newWin[newIndex]) == -1) { + addedItems.push({ item: this._createPreviewItem(newWin[newIndex]), + pos: newIndex }); + newIndex++; + continue; + } + + // Window moved + let insertHere = newWin[newIndex + 1] && + newWin[newIndex + 1] == oldWin[oldIndex]; + let alreadyRemoved = removedActors.reduce(function(result, actor) { + let removedWin = actor._window; + return result || removedWin == newWin[newIndex]; + }, false); + + if (insertHere || alreadyRemoved) { + addedItems.push({ item: this._createPreviewItem(newWin[newIndex]), + pos: newIndex + removedActors.length }); + newIndex++; + } else { + removedActors.push(children[oldIndex]); + oldIndex++; + } + } + + for (let i = 0; i < addedItems.length; i++) + this.addMenuItem(addedItems[i].item, + addedItems[i].pos); + + for (let i = 0; i < removedActors.length; i++) { + let item = removedActors[i]; + if (this._shownInitially) + item._animateOutAndDestroy(); + else + item.actor.destroy(); + } + + // Separate windows from other workspaces + let ws_index = global.screen.get_active_workspace_index(); + let separator_index = 0; + for (let i = 0; i < newWin.length; i++) + if (newWin[i].get_workspace().index() == ws_index) + separator_index++; + + if (separator_index > 0 && separator_index !== newWin.length) { + let separatorItem = new PopupMenu.PopupSeparatorMenuItem(); + if (this.isHorizontal) { + separatorItem._separator.set_x_expand(false); + separatorItem._separator.set_y_expand(true); + separatorItem._separator.set_name('dashtodockPreviewSeparator'); + separatorItem._separator.add_style_class_name('popup-separator-menu-item-horizontal'); + separatorItem._separator.set_x_align(Clutter.ActorAlign.CENTER); + separatorItem._separator.set_y_align(Clutter.ActorAlign.FILL); + } + this.addMenuItem(separatorItem, separator_index); + } + + // Skip animations on first run when adding the initial set + // of items, to avoid all items zooming in at once + let animate = this._shownInitially; + + if (!this._shownInitially) + this._shownInitially = true; + + for (let i = 0; i < addedItems.length; i++) + addedItems[i].item.show(animate); + + // Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=692744 + // Without it, StBoxLayout may use a stale size cache + this.box.queue_relayout(); + + if (newWin.length < 1) + this._getTopMenu().close(~0); + + // As for upstream: + // St.ScrollView always requests space horizontally for a possible vertical + // scrollbar if in AUTOMATIC mode. Doing better would require implementation + // of width-for-height in St.BoxLayout and St.ScrollView. This looks bad + // when we *don't* need it, so turn off the scrollbar when that's true. + // Dynamic changes in whether we need it aren't handled properly. + let needsScrollbar = this._needsScrollbar(); + let scrollbar_policy = needsScrollbar ? Gtk.PolicyType.AUTOMATIC : Gtk.PolicyType.NEVER; + if (this.isHorizontal) + this.actor.hscrollbar_policy = scrollbar_policy; + else + this.actor.vscrollbar_policy = scrollbar_policy; + + if (needsScrollbar) + this.actor.add_style_pseudo_class('scrolled'); + else + this.actor.remove_style_pseudo_class('scrolled'); + }, + + _needsScrollbar: function() { + let topMenu = this._getTopMenu(); + let topThemeNode = topMenu.actor.get_theme_node(); + if (this.isHorizontal) { + let [topMinWidth, topNaturalWidth] = topMenu.actor.get_preferred_width(-1); + let topMaxWidth = topThemeNode.get_max_width(); + return topMaxWidth >= 0 && topNaturalWidth >= topMaxWidth; + } else { + let [topMinHeight, topNaturalHeight] = topMenu.actor.get_preferred_height(-1); + let topMaxHeight = topThemeNode.get_max_height(); + return topMaxHeight >= 0 && topNaturalHeight >= topMaxHeight; + } + + }, + + isAnimatingOut: function() { + return this.actor.get_children().reduce(function(result, actor) { + return result || actor.animatingOut; + }, false); + }, + + sortWindowsCompareFunction: function(windowA, windowB) { + let ws_index = global.screen.get_active_workspace_index(); + let winA_inActiveWS = windowA.get_workspace().index() == ws_index; + let winB_inActiveWS = windowB.get_workspace().index() == ws_index; + + // Only change the order if winA is not in the current WS, while winB is + if (!winA_inActiveWS && winB_inActiveWS) + return 1; + + return 0; + } +}); + +const WindowPreviewMenuItem = new Lang.Class({ + Name: 'WindowPreviewMenuItem', + Extends: PopupMenu.PopupBaseMenuItem, + + _init: function(window, params) { + this._window = window; + this._destroyId = 0; + this._windowAddedId = 0; + this.parent(params); + + // We don't want this: it adds spacing on the left of the item. + this.actor.remove_child(this._ornamentLabel); + this.actor.add_style_class_name('dashtodock-app-well-preview-menu-item'); + + this._cloneBin = new St.Bin(); + this._cloneBin.set_size(PREVIEW_MAX_WIDTH, PREVIEW_MAX_HEIGHT); + + // TODO: improve the way the closebutton is layout. Just use some padding + // for the moment. + this._cloneBin.set_style('padding-bottom: 0.5em'); + + this.closeButton = new St.Button({ style_class: 'window-close', + x_expand: true, + y_expand: true}); + this.closeButton.set_x_align(Clutter.ActorAlign.END); + this.closeButton.set_y_align(Clutter.ActorAlign.START); + + + this.closeButton.opacity = 0; + this.closeButton.connect('clicked', Lang.bind(this, this._closeWindow)); + + let overlayGroup = new Clutter.Actor({layout_manager: new Clutter.BinLayout() }); + + overlayGroup.add_actor(this._cloneBin); + overlayGroup.add_actor(this.closeButton); + + let label = new St.Label({ text: window.get_title()}); + label.set_style('max-width: '+PREVIEW_MAX_WIDTH +'px'); + let labelBin = new St.Bin({ child: label, + x_align: St.Align.MIDDLE}); + + this._windowTitleId = this._window.connect('notify::title', Lang.bind(this, function() { + label.set_text(this._window.get_title()); + })); + + let box = new St.BoxLayout({ vertical: true, + reactive:true, + x_expand:true }); + box.add(overlayGroup); + box.add(labelBin); + this.actor.add_actor(box); + + this.actor.connect('enter-event', + Lang.bind(this, this._onEnter)); + this.actor.connect('leave-event', + Lang.bind(this, this._onLeave)); + this.actor.connect('key-focus-in', + Lang.bind(this, this._onEnter)); + this.actor.connect('key-focus-out', + Lang.bind(this, this._onLeave)); + + this._cloneTexture(window); + + }, + + _cloneTexture: function(metaWin){ + + let mutterWindow = metaWin.get_compositor_private(); + + // Newly-created windows are added to a workspace before + // the compositor finds out about them... + // Moreover sometimes they return an empty texture, thus as a workarounf also check for it size + if (!mutterWindow || !mutterWindow.get_texture() || !mutterWindow.get_texture().get_size()[0]) { + let id = Mainloop.idle_add(Lang.bind(this, + function () { + // Check if there's still a point in getting the texture, + // otherwise this could go on indefinitely + if (this.actor && metaWin.get_workspace()) + this._cloneTexture(metaWin); + return GLib.SOURCE_REMOVE; + })); + GLib.Source.set_name_by_id(id, '[dash-to-dock] this._cloneTexture'); + return; + } + + 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 clone = new Clutter.Clone ({ source: windowTexture, + reactive: true, + width: width * scale, + height: height * scale }); + + // when the source actor is destroyed, i.e. the window closed, first destroy the clone + // and then destroy the menu item (do this animating out) + this._destroyId = mutterWindow.connect('destroy', Lang.bind(this, function() { + clone.destroy(); + this._destroyId = 0; // avoid to try to disconnect this signal from mutterWindow in _onDestroy(), + // as the object was just destroyed + this._animateOutAndDestroy(); + })); + + this._clone = clone; + this._mutterWindow = mutterWindow; + this._cloneBin.set_child(this._clone); + }, + + _windowCanClose: function() { + return this._window.can_close() && + !this._hasAttachedDialogs(); + }, + + _closeWindow: function(actor) { + this._workspace = this._window.get_workspace(); + + // This mechanism is copied from the workspace.js upstream code + // It forces window activation if the windows don't get closed, + // for instance because asking user confirmation, by monitoring the opening of + // such additional confirmation window + this._windowAddedId = this._workspace.connect('window-added', + Lang.bind(this, + this._onWindowAdded)); + + this.deleteAllWindows(); + }, + + deleteAllWindows: function() { + // Delete all windows, starting from the bottom-most (most-modal) one + //let windows = this._window.get_compositor_private().get_children(); + let windows = this._clone.get_children(); + for (let i = windows.length - 1; i >= 1; i--) { + let realWindow = windows[i].source; + let metaWindow = realWindow.meta_window; + + metaWindow.delete(global.get_current_time()); + } + + this._window.delete(global.get_current_time()); + }, + + _onWindowAdded: function(workspace, win) { + let metaWindow = this._window; + + if (win.get_transient_for() == metaWindow) { + workspace.disconnect(this._windowAddedId); + this._windowAddedId = 0; + + // use an idle handler to avoid mapping problems - + // see comment in Workspace._windowAdded + let id = Mainloop.idle_add(Lang.bind(this, + function() { + this.emit('activate'); + return GLib.SOURCE_REMOVE; + })); + GLib.Source.set_name_by_id(id, '[dash-to-dock] this.emit'); + } + }, + + _hasAttachedDialogs: function() { + // count trasient windows + let n=0; + this._window.foreach_transient(function(){n++;}); + return n>0; + }, + + _onEnter: function() { + this._showCloseButton(); + return Clutter.EVENT_PROPAGATE; + }, + + _onLeave: function() { + if (!this._cloneBin.has_pointer && + !this.closeButton.has_pointer) + this._hideCloseButton(); + + return Clutter.EVENT_PROPAGATE; + }, + + _idleToggleCloseButton: function() { + this._idleToggleCloseId = 0; + + if (!this._cloneBin.has_pointer && + !this.closeButton.has_pointer) + this._hideCloseButton(); + + return GLib.SOURCE_REMOVE; + }, + + _showCloseButton: function() { + + if (this._windowCanClose()) { + this.closeButton.show(); + Tweener.addTween(this.closeButton, + { opacity: 255, + time: Workspace.CLOSE_BUTTON_FADE_TIME, + transition: 'easeOutQuad' }); + } + }, + + _hideCloseButton: function() { + Tweener.addTween(this.closeButton, + { opacity: 0, + time: Workspace.CLOSE_BUTTON_FADE_TIME, + transition: 'easeInQuad' }); + }, + + show: function(animate) { + let fullWidth = this.actor.get_width(); + + this.actor.opacity = 0; + this.actor.set_width(0); + + let time = animate ? 0.25 : 0; + Tweener.addTween(this.actor, + { opacity: 255, + width: fullWidth, + time: time, + transition: 'easeInOutQuad' + }); + }, + + _animateOutAndDestroy: function() { + Tweener.addTween(this.actor, + { opacity: 0, + time: 0.25, + }); + + Tweener.addTween(this.actor, + { height: 0, + width: 0, + time: 0.25, + delay: 0.25, + onCompleteScope: this, + onComplete: function() { + this.actor.destroy(); + } + }); + }, + + activate: function() { + this._getTopMenu().close(); + Main.activateWindow(this._window); + }, + + _onDestroy: function() { + + this.parent(); + + if (this._windowAddedId > 0) { + this._workspace.disconnect(this._windowAddedId); + this._windowAddedId = 0; + } + + if (this._destroyId > 0) { + this._mutterWindow.disconnect(this._destroyId); + this._destroyId = 0; + } + + if (this._windowTitleId > 0) { + this._window.disconnect(this._windowTitleId); + this._windowTitleId = 0; + } + } + +}); diff --git a/meson.build b/meson.build index c16bde1..f9b56cf 100644 --- a/meson.build +++ b/meson.build @@ -52,6 +52,7 @@ default_extensions += [ all_extensions = default_extensions all_extensions += [ 'auto-move-windows', + 'dash-to-dock', 'example', 'native-window-placement', 'top-icons', -- 2.21.0 From 8175177aa1a3fd8a40d41df1f23beee1f99d06b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Wed, 20 May 2015 18:55:47 +0200 Subject: [PATCH 3/7] Add panel-favorites extension --- extensions/panel-favorites/extension.js | 267 ++++++++++++++++++++ extensions/panel-favorites/meson.build | 5 + extensions/panel-favorites/metadata.json.in | 10 + extensions/panel-favorites/stylesheet.css | 14 + meson.build | 1 + 5 files changed, 297 insertions(+) create mode 100644 extensions/panel-favorites/extension.js create mode 100644 extensions/panel-favorites/meson.build create mode 100644 extensions/panel-favorites/metadata.json.in create mode 100644 extensions/panel-favorites/stylesheet.css diff --git a/extensions/panel-favorites/extension.js b/extensions/panel-favorites/extension.js new file mode 100644 index 0000000..b817dbb --- /dev/null +++ b/extensions/panel-favorites/extension.js @@ -0,0 +1,267 @@ +// Copyright (C) 2011-2013 R M Yorston +// Licence: GPLv2+ + +const Clutter = imports.gi.Clutter; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Lang = imports.lang; +const Shell = imports.gi.Shell; +const Signals = imports.signals; +const St = imports.gi.St; +const Mainloop = imports.mainloop; + +const AppFavorites = imports.ui.appFavorites; +const Main = imports.ui.main; +const Panel = imports.ui.panel; +const Tweener = imports.ui.tweener; + +const PANEL_LAUNCHER_LABEL_SHOW_TIME = 0.15; +const PANEL_LAUNCHER_LABEL_HIDE_TIME = 0.1; +const PANEL_LAUNCHER_HOVER_TIMEOUT = 300; + +const PanelLauncher = new Lang.Class({ + Name: 'PanelLauncher', + + _init: function(app) { + this.actor = new St.Button({ style_class: 'panel-button', + reactive: true }); + this.iconSize = 24; + let icon = app.create_icon_texture(this.iconSize); + this.actor.set_child(icon); + this.actor._delegate = this; + let text = app.get_name(); + if ( app.get_description() ) { + text += '\n' + app.get_description(); + } + + this.label = new St.Label({ style_class: 'panel-launcher-label'}); + this.label.set_text(text); + Main.layoutManager.addChrome(this.label); + this.label.hide(); + this.actor.label_actor = this.label; + + this._app = app; + this.actor.connect('clicked', Lang.bind(this, function() { + this._app.open_new_window(-1); + })); + this.actor.connect('notify::hover', + Lang.bind(this, this._onHoverChanged)); + this.actor.opacity = 207; + + this.actor.connect('notify::allocation', Lang.bind(this, this._alloc)); + }, + + _onHoverChanged: function(actor) { + actor.opacity = actor.hover ? 255 : 207; + }, + + _alloc: function() { + let size = this.actor.allocation.y2 - this.actor.allocation.y1 - 3; + if ( size >= 24 && size != this.iconSize ) { + this.actor.get_child().destroy(); + this.iconSize = size; + let icon = this._app.create_icon_texture(this.iconSize); + this.actor.set_child(icon); + } + }, + + showLabel: function() { + this.label.opacity = 0; + this.label.show(); + + let [stageX, stageY] = this.actor.get_transformed_position(); + + let itemHeight = this.actor.allocation.y2 - this.actor.allocation.y1; + let itemWidth = this.actor.allocation.x2 - this.actor.allocation.x1; + let labelWidth = this.label.get_width(); + + let node = this.label.get_theme_node(); + let yOffset = node.get_length('-y-offset'); + + let y = stageY + itemHeight + yOffset; + let x = Math.floor(stageX + itemWidth/2 - labelWidth/2); + + let parent = this.label.get_parent(); + let parentWidth = parent.allocation.x2 - parent.allocation.x1; + + if ( Clutter.get_default_text_direction() == Clutter.TextDirection.LTR ) { + // stop long tooltips falling off the right of the screen + x = Math.min(x, parentWidth-labelWidth-6); + // but whatever happens don't let them fall of the left + x = Math.max(x, 6); + } + else { + x = Math.max(x, 6); + x = Math.min(x, parentWidth-labelWidth-6); + } + + this.label.set_position(x, y); + Tweener.addTween(this.label, + { opacity: 255, + time: PANEL_LAUNCHER_LABEL_SHOW_TIME, + transition: 'easeOutQuad', + }); + }, + + hideLabel: function() { + this.label.opacity = 255; + Tweener.addTween(this.label, + { opacity: 0, + time: PANEL_LAUNCHER_LABEL_HIDE_TIME, + transition: 'easeOutQuad', + onComplete: Lang.bind(this, function() { + this.label.hide(); + }) + }); + }, + + destroy: function() { + this.label.destroy(); + this.actor.destroy(); + } +}); + +const PanelFavorites = new Lang.Class({ + Name: 'PanelFavorites', + + _init: function() { + this._showLabelTimeoutId = 0; + this._resetHoverTimeoutId = 0; + this._labelShowing = false; + + this.actor = new St.BoxLayout({ name: 'panelFavorites', + x_expand: true, y_expand: true, + style_class: 'panel-favorites' }); + this._display(); + + this.container = new St.Bin({ y_fill: true, + x_fill: true, + child: this.actor }); + + this.actor.connect('destroy', Lang.bind(this, this._onDestroy)); + this._installChangedId = Shell.AppSystem.get_default().connect('installed-changed', Lang.bind(this, this._redisplay)); + this._changedId = AppFavorites.getAppFavorites().connect('changed', Lang.bind(this, this._redisplay)); + }, + + _redisplay: function() { + for ( let i=0; i 0) { + Mainloop.source_remove(this._resetHoverTimeoutId); + this._resetHoverTimeoutId = 0; + } + } + } else { + if (this._showLabelTimeoutId > 0) { + Mainloop.source_remove(this._showLabelTimeoutId); + this._showLabelTimeoutId = 0; + } + launcher.hideLabel(); + if (this._labelShowing) { + this._resetHoverTimeoutId = Mainloop.timeout_add( + PANEL_LAUNCHER_HOVER_TIMEOUT, + Lang.bind(this, function() { + this._labelShowing = false; + this._resetHoverTimeoutId = 0; + return GLib.SOURCE_REMOVE; + })); + } + } + }, + + _onDestroy: function() { + if ( this._installChangedId != 0 ) { + Shell.AppSystem.get_default().disconnect(this._installChangedId); + this._installChangedId = 0; + } + + if ( this._changedId != 0 ) { + AppFavorites.getAppFavorites().disconnect(this._changedId); + this._changedId = 0; + } + } +}); +Signals.addSignalMethods(PanelFavorites.prototype); + +let myAddToStatusArea; +let panelFavorites; + +function enable() { + Panel.Panel.prototype.myAddToStatusArea = myAddToStatusArea; + + // place panel to left of app menu, or failing that at right end of box + let siblings = Main.panel._leftBox.get_children(); + let appMenu = Main.panel.statusArea['appMenu']; + let pos = appMenu ? siblings.indexOf(appMenu.container) : siblings.length; + + panelFavorites = new PanelFavorites(); + Main.panel.myAddToStatusArea('panel-favorites', panelFavorites, + pos, 'left'); +} + +function disable() { + delete Panel.Panel.prototype.myAddToStatusArea; + + panelFavorites.actor.destroy(); + panelFavorites.emit('destroy'); + panelFavorites = null; +} + +function init() { + myAddToStatusArea = function(role, indicator, position, box) { + if (this.statusArea[role]) + throw new Error('Extension point conflict: there is already a status indicator for role ' + role); + + position = position || 0; + let boxes = { + left: this._leftBox, + center: this._centerBox, + right: this._rightBox + }; + let boxContainer = boxes[box] || this._rightBox; + this.statusArea[role] = indicator; + this._addToPanelBox(role, indicator, position, boxContainer); + return indicator; + }; +} diff --git a/extensions/panel-favorites/meson.build b/extensions/panel-favorites/meson.build new file mode 100644 index 0000000..48504f6 --- /dev/null +++ b/extensions/panel-favorites/meson.build @@ -0,0 +1,5 @@ +extension_data += configure_file( + input: metadata_name + '.in', + output: metadata_name, + configuration: metadata_conf +) diff --git a/extensions/panel-favorites/metadata.json.in b/extensions/panel-favorites/metadata.json.in new file mode 100644 index 0000000..037f281 --- /dev/null +++ b/extensions/panel-favorites/metadata.json.in @@ -0,0 +1,10 @@ +{ +"extension-id": "@extension_id@", +"uuid": "@uuid@", +"settings-schema": "@gschemaname@", +"gettext-domain": "@gettext_domain@", +"name": "Frippery Panel Favorites", +"description": "Add launchers for Favorites to the panel", +"shell-version": [ "@shell_current@" ], +"url": "http://intgat.tigress.co.uk/rmy/extensions/index.html" +} diff --git a/extensions/panel-favorites/stylesheet.css b/extensions/panel-favorites/stylesheet.css new file mode 100644 index 0000000..120adac --- /dev/null +++ b/extensions/panel-favorites/stylesheet.css @@ -0,0 +1,14 @@ +.panel-favorites { + spacing: 6px; +} + +.panel-launcher-label { + border-radius: 7px; + padding: 4px 12px; + background-color: rgba(0,0,0,0.9); + color: white; + text-align: center; + font-size: 9pt; + font-weight: bold; + -y-offset: 6px; +} diff --git a/meson.build b/meson.build index f9b56cf..3451585 100644 --- a/meson.build +++ b/meson.build @@ -55,6 +55,7 @@ all_extensions += [ 'dash-to-dock', 'example', 'native-window-placement', + 'panel-favorites', 'top-icons', 'user-theme' ] -- 2.21.0 From 0bec6fd48fc857c54ef4917b1648f507c8142270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Fri, 4 Mar 2016 17:07:21 +0100 Subject: [PATCH 4/7] Add updates-dialog extension --- extensions/updates-dialog/extension.js | 490 ++++++++++++++++++ extensions/updates-dialog/meson.build | 7 + extensions/updates-dialog/metadata.json.in | 10 + ...hell.extensions.updates-dialog.gschema.xml | 30 ++ extensions/updates-dialog/stylesheet.css | 1 + meson.build | 1 + po/POTFILES.in | 2 + 7 files changed, 541 insertions(+) create mode 100644 extensions/updates-dialog/extension.js create mode 100644 extensions/updates-dialog/meson.build create mode 100644 extensions/updates-dialog/metadata.json.in create mode 100644 extensions/updates-dialog/org.gnome.shell.extensions.updates-dialog.gschema.xml create mode 100644 extensions/updates-dialog/stylesheet.css diff --git a/extensions/updates-dialog/extension.js b/extensions/updates-dialog/extension.js new file mode 100644 index 0000000..2fa62a5 --- /dev/null +++ b/extensions/updates-dialog/extension.js @@ -0,0 +1,490 @@ +/* + * Copyright (c) 2015 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +const Clutter = imports.gi.Clutter; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Lang = imports.lang; +const Pango = imports.gi.Pango; +const PkgKit = imports.gi.PackageKitGlib; +const Polkit = imports.gi.Polkit; +const Signals = imports.signals; +const St = imports.gi.St; + +const EndSessionDialog = imports.ui.endSessionDialog; +const Main = imports.ui.main; +const ModalDialog = imports.ui.modalDialog; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Convenience = Me.imports.convenience; + +const PkIface = ' \ + \ + \ + \ + \ + \ + \ +'; + +const PkOfflineIface = ' \ + \ + \ + \ + \ + \ + \ + \ + \ +'; + +const PkTransactionIface = ' \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +'; + +const LoginManagerIface = ' \ + \ + \ + \ + \ + \ + \ + \ + \ +'; + +const PkProxy = Gio.DBusProxy.makeProxyWrapper(PkIface); +const PkOfflineProxy = Gio.DBusProxy.makeProxyWrapper(PkOfflineIface); +const PkTransactionProxy = Gio.DBusProxy.makeProxyWrapper(PkTransactionIface); +const LoginManagerProxy = Gio.DBusProxy.makeProxyWrapper(LoginManagerIface); + +let pkProxy = null; +let pkOfflineProxy = null; +let loginManagerProxy = null; +let updatesDialog = null; +let extensionSettings = null; +let cancellable = null; + +let updatesCheckInProgress = false; +let updatesCheckRequested = false; +let securityUpdates = []; + +function getDetailText(period) { + let text = _("Important security updates need to be installed.\n"); + if (period < 60) + text += ngettext("You can close this dialog and get %d minute to finish your work.", + "You can close this dialog and get %d minutes to finish your work.", + period).format(period); + else + text += ngettext("You can close this dialog and get %d hour to finish your work.", + "You can close this dialog and get %d hours to finish your work.", + Math.floor(period / 60)).format(Math.floor(period / 60)); + return text; +} + +const UpdatesDialog = new Lang.Class({ + Name: 'UpdatesDialog', + Extends: ModalDialog.ModalDialog, + + _init: function(settings) { + this.parent({ styleClass: 'end-session-dialog', + destroyOnClose: false }); + + this._gracePeriod = settings.get_uint('grace-period'); + this._gracePeriod = Math.min(Math.max(10, this._gracePeriod), 24*60); + this._lastWarningPeriod = settings.get_uint('last-warning-period'); + this._lastWarningPeriod = Math.min(Math.max(1, this._lastWarningPeriod), this._gracePeriod - 1); + this._lastWarnings = settings.get_uint('last-warnings'); + this._lastWarnings = Math.min(Math.max(1, this._lastWarnings), + Math.floor((this._gracePeriod - 1) / this._lastWarningPeriod)); + + let messageLayout = new St.BoxLayout({ vertical: true, + style_class: 'end-session-dialog-layout' }); + this.contentLayout.add(messageLayout, + { x_fill: true, + y_fill: true, + y_expand: true }); + + let subjectLabel = new St.Label({ style_class: 'end-session-dialog-subject', + style: 'padding-bottom: 1em;', + text: _("Important security updates") }); + messageLayout.add(subjectLabel, + { x_fill: false, + y_fill: false, + x_align: St.Align.START, + y_align: St.Align.START }); + + this._detailLabel = new St.Label({ style_class: 'end-session-dialog-description', + style: 'padding-bottom: 0em;', + text: getDetailText(this._gracePeriod) }); + this._detailLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; + this._detailLabel.clutter_text.line_wrap = true; + + messageLayout.add(this._detailLabel, + { y_fill: true, + y_align: St.Align.START }); + + let buttons = [{ action: Lang.bind(this, this.close), + label: _("Close"), + key: Clutter.Escape }, + { action: Lang.bind(this, this._done), + label: _("Restart & Install") }]; + + this.setButtons(buttons); + + this._openTimeoutId = 0; + this.connect('destroy', Lang.bind(this, this._clearOpenTimeout)); + + this._startTimer(); + }, + + _clearOpenTimeout: function() { + if (this._openTimeoutId > 0) { + GLib.source_remove(this._openTimeoutId); + this._openTimeoutId = 0; + } + }, + + tryOpen: function() { + if (this._openTimeoutId > 0 || this.open()) + return; + + this._openTimeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, + Lang.bind(this, function() { + if (!this.open()) + return GLib.SOURCE_CONTINUE; + + this._clearOpenTimeout(); + return GLib.SOURCE_REMOVE; + })); + }, + + _startTimer: function() { + this._secondsLeft = this._gracePeriod*60; + + this._timerId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, Lang.bind(this, + function() { + this._secondsLeft -= 1; + let minutesLeft = this._secondsLeft / 60; + let periodLeft = Math.floor(minutesLeft); + + if (this._secondsLeft == 60 || + (periodLeft > 0 && periodLeft <= this._lastWarningPeriod * this._lastWarnings && + minutesLeft % this._lastWarningPeriod == 0)) { + this.tryOpen(); + this._detailLabel.text = getDetailText(periodLeft); + } + + if (this._secondsLeft > 0) { + if (this._secondsLeft < 60) { + let seconds = EndSessionDialog._roundSecondsToInterval(this._gracePeriod*60, this._secondsLeft, 10); + this._detailLabel.text = + _("Important security updates need to be installed now.\n") + + ngettext("This computer will restart in %d second.", + "This computer will restart in %d seconds.", + seconds).format(seconds); + } + return GLib.SOURCE_CONTINUE; + } + + this._done(); + return GLib.SOURCE_REMOVE; + })); + this.connect('destroy', Lang.bind(this, function() { + if (this._timerId > 0) { + GLib.source_remove(this._timerId); + this._timerId = 0; + } + })); + }, + + _done: function() { + this.emit('done'); + this.destroy(); + }, + + getState: function() { + return [this._gracePeriod, this._lastWarningPeriod, this._lastWarnings, this._secondsLeft]; + }, + + setState: function(state) { + [this._gracePeriod, this._lastWarningPeriod, this._lastWarnings, this._secondsLeft] = state; + }, +}); +Signals.addSignalMethods(UpdatesDialog.prototype); + +function showDialog() { + if (updatesDialog) + return; + + updatesDialog = new UpdatesDialog(extensionSettings); + updatesDialog.tryOpen(); + updatesDialog.connect('destroy', function() { updatesDialog = null; }); + updatesDialog.connect('done', function() { + if (pkOfflineProxy.TriggerAction == 'power-off' || + pkOfflineProxy.TriggerAction == 'reboot') { + loginManagerProxy.RebootRemote(false); + } else { + pkOfflineProxy.TriggerRemote('reboot', function(result, error) { + if (!error) + loginManagerProxy.RebootRemote(false); + else + log('Failed to trigger offline update: %s'.format(error.message)); + }); + } + }); +} + +function cancelDialog(save) { + if (!updatesDialog) + return; + + if (save) { + let state = GLib.Variant.new('(uuuu)', updatesDialog.getState()); + global.set_runtime_state(Me.uuid, state); + } + updatesDialog.destroy(); +} + +function restoreExistingState() { + let state = global.get_runtime_state('(uuuu)', Me.uuid); + if (state === null) + return false; + + global.set_runtime_state(Me.uuid, null); + showDialog(); + updatesDialog.setState(state.deep_unpack()); + return true; +} + +function syncState() { + if (!pkOfflineProxy || !loginManagerProxy) + return; + + if (restoreExistingState()) + return; + + if (!updatesCheckInProgress && + securityUpdates.length > 0 && + pkOfflineProxy.UpdatePrepared) + showDialog(); + else + cancelDialog(); +} + +function doPkTransaction(callback) { + if (!pkProxy) + return; + + pkProxy.CreateTransactionRemote(function(result, error) { + if (error) { + log('Error creating PackageKit transaction: %s'.format(error.message)); + checkUpdatesDone(); + return; + } + + new PkTransactionProxy(Gio.DBus.system, + 'org.freedesktop.PackageKit', + String(result), + function(proxy, error) { + if (!error) { + proxy.SetHintsRemote( + ['background=true', 'interactive=false'], + function(result, error) { + if (error) { + log('Error connecting to PackageKit: %s'.format(error.message)); + checkUpdatesDone(); + return; + } + callback(proxy); + }); + } else { + log('Error connecting to PackageKit: %s'.format(error.message)); + } + }); + }); +} + +function pkUpdatePackages(proxy) { + proxy.connectSignal('Finished', function(p, e, params) { + let [exit, runtime] = params; + + if (exit == PkgKit.ExitEnum.CANCELLED_PRIORITY) { + // try again + checkUpdates(); + } else if (exit != PkgKit.ExitEnum.SUCCESS) { + log('UpdatePackages failed: %s'.format(PkgKit.ExitEnum.to_string(exit))); + } + + checkUpdatesDone(); + }); + proxy.UpdatePackagesRemote(1 << PkgKit.TransactionFlagEnum.ONLY_DOWNLOAD, securityUpdates); +} + +function pkGetUpdates(proxy) { + proxy.connectSignal('Package', function(p, e, params) { + let [info, packageId, summary] = params; + + if (info == PkgKit.InfoEnum.SECURITY) + securityUpdates.push(packageId); + }); + proxy.connectSignal('Finished', function(p, e, params) { + let [exit, runtime] = params; + + if (exit == PkgKit.ExitEnum.SUCCESS) { + if (securityUpdates.length > 0) { + doPkTransaction(pkUpdatePackages); + return; + } + } else if (exit == PkgKit.ExitEnum.CANCELLED_PRIORITY) { + // try again + checkUpdates(); + } else { + log('GetUpdates failed: %s'.format(PkgKit.ExitEnum.to_string(exit))); + } + + checkUpdatesDone(); + }); + proxy.GetUpdatesRemote(0); +} + +function checkUpdatesDone() { + updatesCheckInProgress = false; + if (updatesCheckRequested) { + updatesCheckRequested = false; + checkUpdates(); + } else { + syncState(); + } +} + +function checkUpdates() { + if (updatesCheckInProgress) { + updatesCheckRequested = true; + return; + } + updatesCheckInProgress = true; + securityUpdates = []; + doPkTransaction(pkGetUpdates); +} + +function initSystemProxies() { + new PkProxy(Gio.DBus.system, + 'org.freedesktop.PackageKit', + '/org/freedesktop/PackageKit', + function(proxy, error) { + if (!error) { + pkProxy = proxy; + let id = pkProxy.connectSignal('UpdatesChanged', checkUpdates); + pkProxy._signalId = id; + checkUpdates(); + } else { + log('Error connecting to PackageKit: %s'.format(error.message)); + } + }, + cancellable); + new PkOfflineProxy(Gio.DBus.system, + 'org.freedesktop.PackageKit', + '/org/freedesktop/PackageKit', + function(proxy, error) { + if (!error) { + pkOfflineProxy = proxy; + let id = pkOfflineProxy.connect('g-properties-changed', syncState); + pkOfflineProxy._signalId = id; + syncState(); + } else { + log('Error connecting to PackageKit: %s'.format(error.message)); + } + }, + cancellable); + new LoginManagerProxy(Gio.DBus.system, + 'org.freedesktop.login1', + '/org/freedesktop/login1', + function(proxy, error) { + if (!error) { + proxy.CanRebootRemote(cancellable, function(result, error) { + if (!error && result == 'yes') { + loginManagerProxy = proxy; + syncState(); + } else { + log('Reboot is not available'); + } + }); + } else { + log('Error connecting to Login manager: %s'.format(error.message)); + } + }, + cancellable); +} + +function init(metadata) { +} + +function enable() { + cancellable = new Gio.Cancellable(); + extensionSettings = Convenience.getSettings(); + Polkit.Permission.new("org.freedesktop.packagekit.trigger-offline-update", + null, cancellable, function(p, result) { + try { + let permission = Polkit.Permission.new_finish(result); + if (permission && permission.allowed) + initSystemProxies(); + else + throw(new Error('not allowed')); + } catch(e) { + log('No permission to trigger offline updates: %s'.format(e.toString())); + } + }); +} + +function disable() { + cancelDialog(true); + cancellable.cancel(); + cancellable = null; + extensionSettings = null; + updatesDialog = null; + loginManagerProxy = null; + if (pkOfflineProxy) { + pkOfflineProxy.disconnect(pkOfflineProxy._signalId); + pkOfflineProxy = null; + } + if (pkProxy) { + pkProxy.disconnectSignal(pkProxy._signalId); + pkProxy = null; + } +} diff --git a/extensions/updates-dialog/meson.build b/extensions/updates-dialog/meson.build new file mode 100644 index 0000000..585c02d --- /dev/null +++ b/extensions/updates-dialog/meson.build @@ -0,0 +1,7 @@ +extension_data += configure_file( + input: metadata_name + '.in', + output: metadata_name, + configuration: metadata_conf +) + +extension_schemas += files(metadata_conf.get('gschemaname') + '.gschema.xml') diff --git a/extensions/updates-dialog/metadata.json.in b/extensions/updates-dialog/metadata.json.in new file mode 100644 index 0000000..9946abb --- /dev/null +++ b/extensions/updates-dialog/metadata.json.in @@ -0,0 +1,10 @@ +{ +"extension-id": "@extension_id@", +"uuid": "@uuid@", +"settings-schema": "@gschemaname@", +"gettext-domain": "@gettext_domain@", +"name": "Updates Dialog", +"description": "Shows a modal dialog when there are software updates.", +"shell-version": [ "@shell_current@" ], +"url": "http://rtcm.fedorapeople.org/updates-dialog" +} diff --git a/extensions/updates-dialog/org.gnome.shell.extensions.updates-dialog.gschema.xml b/extensions/updates-dialog/org.gnome.shell.extensions.updates-dialog.gschema.xml new file mode 100644 index 0000000..c08d33c --- /dev/null +++ b/extensions/updates-dialog/org.gnome.shell.extensions.updates-dialog.gschema.xml @@ -0,0 +1,30 @@ + + + + + 300 + Grace period in minutes + + When the grace period is over, the computer will automatically + reboot and install security updates. + + + + 10 + Last warning dialog period + + A last warning dialog is displayed this many minutes before + the automatic reboot. + + + + 1 + Number of last warning dialogs + + How many warning dialogs are displayed. Each is displayed at + 'last-warning-period' minute intervals. + + + + diff --git a/extensions/updates-dialog/stylesheet.css b/extensions/updates-dialog/stylesheet.css new file mode 100644 index 0000000..25134b6 --- /dev/null +++ b/extensions/updates-dialog/stylesheet.css @@ -0,0 +1 @@ +/* This extensions requires no special styling */ diff --git a/meson.build b/meson.build index 3451585..08a243e 100644 --- a/meson.build +++ b/meson.build @@ -57,6 +57,7 @@ all_extensions += [ 'native-window-placement', 'panel-favorites', 'top-icons', + 'updates-dialog', 'user-theme' ] diff --git a/po/POTFILES.in b/po/POTFILES.in index d98ca1b..43a817f 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -15,6 +15,8 @@ extensions/native-window-placement/org.gnome.shell.extensions.native-window-plac extensions/places-menu/extension.js extensions/places-menu/placeDisplay.js extensions/screenshot-window-sizer/org.gnome.shell.extensions.screenshot-window-sizer.gschema.xml +extensions/updates-dialog/extension.js +extensions/updates-dialog/org.gnome.shell.extensions.updates-dialog.gschema.xml extensions/user-theme/extension.js extensions/user-theme/org.gnome.shell.extensions.user-theme.gschema.xml extensions/window-list/extension.js -- 2.21.0 From c86f37e39adb0d2e1fce01cb68be47c675a9ace9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Thu, 1 Jun 2017 23:57:14 +0200 Subject: [PATCH 5/7] Add no-hot-corner extension --- extensions/no-hot-corner/extension.js | 31 +++++++++++++++++++++++ extensions/no-hot-corner/meson.build | 5 ++++ extensions/no-hot-corner/metadata.json.in | 9 +++++++ extensions/no-hot-corner/stylesheet.css | 1 + meson.build | 1 + 5 files changed, 47 insertions(+) create mode 100644 extensions/no-hot-corner/extension.js create mode 100644 extensions/no-hot-corner/meson.build create mode 100644 extensions/no-hot-corner/metadata.json.in create mode 100644 extensions/no-hot-corner/stylesheet.css diff --git a/extensions/no-hot-corner/extension.js b/extensions/no-hot-corner/extension.js new file mode 100644 index 0000000..e7a0d63 --- /dev/null +++ b/extensions/no-hot-corner/extension.js @@ -0,0 +1,31 @@ +const Main = imports.ui.main; + +let _id; + +function _disableHotCorners() { + // Disables all hot corners + Main.layoutManager.hotCorners.forEach(function(hotCorner) { + if (!hotCorner) { + return; + } + + hotCorner._toggleOverview = function() {}; + hotCorner._pressureBarrier._trigger = function() {}; + }); +} + +function init() { +} + +function enable() { + _disableHotCorners(); + // Hot corners may be re-created afterwards (for example, If there's a monitor change). + // So we catch all changes. + _id = Main.layoutManager.connect('hot-corners-changed', _disableHotCorners); +} + +function disable() { + // Disconnects the callback and re-creates the hot corners + Main.layoutManager.disconnect(_id); + Main.layoutManager._updateHotCorners(); +} diff --git a/extensions/no-hot-corner/meson.build b/extensions/no-hot-corner/meson.build new file mode 100644 index 0000000..48504f6 --- /dev/null +++ b/extensions/no-hot-corner/meson.build @@ -0,0 +1,5 @@ +extension_data += configure_file( + input: metadata_name + '.in', + output: metadata_name, + configuration: metadata_conf +) diff --git a/extensions/no-hot-corner/metadata.json.in b/extensions/no-hot-corner/metadata.json.in new file mode 100644 index 0000000..406d83b --- /dev/null +++ b/extensions/no-hot-corner/metadata.json.in @@ -0,0 +1,9 @@ +{ +"extension-id": "@extension_id@", +"uuid": "@uuid@", +"name": "No Topleft Hot Corner", +"description": "Disable the hot corner in the top left; you can still reach the overview by clicking the Activities button or pressing the dedicated key.", +"shell-version": [ "@shell_current@" ], +"url": "https://github.com/HROMANO/nohotcorner/", +"version": 15 +} diff --git a/extensions/no-hot-corner/stylesheet.css b/extensions/no-hot-corner/stylesheet.css new file mode 100644 index 0000000..25134b6 --- /dev/null +++ b/extensions/no-hot-corner/stylesheet.css @@ -0,0 +1 @@ +/* This extensions requires no special styling */ diff --git a/meson.build b/meson.build index 08a243e..201c484 100644 --- a/meson.build +++ b/meson.build @@ -55,6 +55,7 @@ all_extensions += [ 'dash-to-dock', 'example', 'native-window-placement', + 'no-hot-corner', 'panel-favorites', 'top-icons', 'updates-dialog', -- 2.21.0 From 9749175fac05702dbed39942b4e5ffe889a84f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Tue, 26 Mar 2019 19:44:43 +0100 Subject: [PATCH 6/7] Add window-grouper extension --- extensions/window-grouper/extension.js | 109 ++++++++++ extensions/window-grouper/meson.build | 8 + extensions/window-grouper/metadata.json.in | 11 + ...hell.extensions.window-grouper.gschema.xml | 9 + extensions/window-grouper/prefs.js | 194 ++++++++++++++++++ extensions/window-grouper/stylesheet.css | 1 + meson.build | 3 +- 7 files changed, 334 insertions(+), 1 deletion(-) create mode 100644 extensions/window-grouper/extension.js create mode 100644 extensions/window-grouper/meson.build create mode 100644 extensions/window-grouper/metadata.json.in create mode 100644 extensions/window-grouper/org.gnome.shell.extensions.window-grouper.gschema.xml create mode 100644 extensions/window-grouper/prefs.js create mode 100644 extensions/window-grouper/stylesheet.css diff --git a/extensions/window-grouper/extension.js b/extensions/window-grouper/extension.js new file mode 100644 index 0000000..e67e634 --- /dev/null +++ b/extensions/window-grouper/extension.js @@ -0,0 +1,109 @@ +// -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- +// Start apps on custom workspaces + +const Shell = imports.gi.Shell; + +const Main = imports.ui.main; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Convenience = Me.imports.convenience; + +class WindowMover { + constructor() { + this._settings = Convenience.getSettings(); + this._appSystem = Shell.AppSystem.get_default(); + this._appConfigs = new Set(); + this._appData = new Map(); + + this._appsChangedId = + this._appSystem.connect('installed-changed', + this._updateAppData.bind(this)); + + this._settings.connect('changed', this._updateAppConfigs.bind(this)); + this._updateAppConfigs(); + } + + _updateAppConfigs() { + this._appConfigs.clear(); + + this._settings.get_strv('application-list').forEach(appId => { + this._appConfigs.add(appId); + }); + + this._updateAppData(); + } + + _updateAppData() { + let ids = [...this._appConfigs.values()]; + let removedApps = [...this._appData.keys()].filter( + a => !ids.includes(a.id) + ); + removedApps.forEach(app => { + app.disconnect(this._appData.get(app).windowsChangedId); + this._appData.delete(app); + }); + + let addedApps = ids.map(id => this._appSystem.lookup_app(id)).filter( + app => app != null && !this._appData.has(app) + ); + addedApps.forEach(app => { + let data = { + windows: app.get_windows(), + windowsChangedId: app.connect('windows-changed', + this._appWindowsChanged.bind(this)) + } + this._appData.set(app, data); + }); + } + + destroy() { + if (this._appsChangedId) { + this._appSystem.disconnect(this._appsChangedId); + this._appsChangedId = 0; + } + + if (this._settings) { + this._settings.run_dispose(); + this._settings = null; + } + + this._appConfigs.clear(); + this._updateAppData(); + } + + _appWindowsChanged(app) { + let data = this._appData.get(app); + let windows = app.get_windows(); + + // If get_compositor_private() returns non-NULL on a removed windows, + // the window still exists and is just moved to a different workspace + // or something; assume it'll be added back immediately, so keep it + // to avoid moving it again + windows.push(...data.windows.filter( + w => !windows.includes(w) && w.get_compositor_private() != null + )); + + windows.filter(w => !data.windows.includes(w)).forEach(window => { + let leader = data.windows.find(w => w.get_pid() == window.get_pid()); + if (leader) + window.change_workspace(leader.get_workspace()); + }); + data.windows = windows; + } +}; + +let prevCheckWorkspaces; +let winMover; + +function init() { + Convenience.initTranslations(); +} + +function enable() { + winMover = new WindowMover(); +} + +function disable() { + winMover.destroy(); +} diff --git a/extensions/window-grouper/meson.build b/extensions/window-grouper/meson.build new file mode 100644 index 0000000..c55a783 --- /dev/null +++ b/extensions/window-grouper/meson.build @@ -0,0 +1,8 @@ +extension_data += configure_file( + input: metadata_name + '.in', + output: metadata_name, + configuration: metadata_conf +) + +extension_sources += files('prefs.js') +extension_schemas += files(metadata_conf.get('gschemaname') + '.gschema.xml') diff --git a/extensions/window-grouper/metadata.json.in b/extensions/window-grouper/metadata.json.in new file mode 100644 index 0000000..aa202c8 --- /dev/null +++ b/extensions/window-grouper/metadata.json.in @@ -0,0 +1,11 @@ +{ + "extension-id": "@extension_id@", + "uuid": "@uuid@", + "settings-schema": "@gschemaname@", + "gettext-domain": "@gettext_domain@", + "name": "Window grouper", + "description": "Keep windows that belong to the same process on the same workspace.", + "shell-version": [ "@shell_current@" ], + "original-authors": [ "fmuellner@redhat.com" ], + "url": "@url@" +} diff --git a/extensions/window-grouper/org.gnome.shell.extensions.window-grouper.gschema.xml b/extensions/window-grouper/org.gnome.shell.extensions.window-grouper.gschema.xml new file mode 100644 index 0000000..ee052a6 --- /dev/null +++ b/extensions/window-grouper/org.gnome.shell.extensions.window-grouper.gschema.xml @@ -0,0 +1,9 @@ + + + + [ ] + Application that should be grouped + A list of application ids + + + diff --git a/extensions/window-grouper/prefs.js b/extensions/window-grouper/prefs.js new file mode 100644 index 0000000..27fbcca --- /dev/null +++ b/extensions/window-grouper/prefs.js @@ -0,0 +1,194 @@ +// -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- +// Start apps on custom workspaces + +const { Gio, GObject, Gtk } = imports.gi; + +const Gettext = imports.gettext.domain('gnome-shell-extensions'); +const _ = Gettext.gettext; +const N_ = e => e; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Convenience = Me.imports.convenience; + +const SETTINGS_KEY = 'application-list'; + +const Columns = { + APPINFO: 0, + DISPLAY_NAME: 1, + ICON: 2 +}; + +const Widget = GObject.registerClass({ + GTypeName: 'WindowGrouperPrefsWidget', +}, class Widget extends Gtk.Grid { + _init(params) { + super._init(params); + this.set_orientation(Gtk.Orientation.VERTICAL); + + this._settings = Convenience.getSettings(); + this._settings.connect('changed', this._refresh.bind(this)); + this._changedPermitted = false; + + this._store = new Gtk.ListStore(); + this._store.set_column_types([Gio.AppInfo, GObject.TYPE_STRING, Gio.Icon]); + + let scrolled = new Gtk.ScrolledWindow({ shadow_type: Gtk.ShadowType.IN}); + scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC); + this.add(scrolled); + + + this._treeView = new Gtk.TreeView({ + model: this._store, + headers_visible: false, + hexpand: true, + vexpand: true + }); + this._treeView.get_selection().set_mode(Gtk.SelectionMode.SINGLE); + + let appColumn = new Gtk.TreeViewColumn({ + sort_column_id: Columns.DISPLAY_NAME, + spacing: 12 + }); + let iconRenderer = new Gtk.CellRendererPixbuf({ + stock_size: Gtk.IconSize.DIALOG, + xpad: 12, + ypad: 12 + }); + appColumn.pack_start(iconRenderer, false); + appColumn.add_attribute(iconRenderer, "gicon", Columns.ICON); + let nameRenderer = new Gtk.CellRendererText(); + appColumn.pack_start(nameRenderer, true); + appColumn.add_attribute(nameRenderer, "text", Columns.DISPLAY_NAME); + this._treeView.append_column(appColumn); + + scrolled.add(this._treeView); + + let toolbar = new Gtk.Toolbar({ icon_size: Gtk.IconSize.SMALL_TOOLBAR }); + toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_INLINE_TOOLBAR); + this.add(toolbar); + + let newButton = new Gtk.ToolButton({ + icon_name: 'list-add-symbolic' + }); + newButton.connect('clicked', this._createNew.bind(this)); + toolbar.add(newButton); + + let delButton = new Gtk.ToolButton({ + icon_name: 'list-remove-symbolic' + }); + delButton.connect('clicked', this._deleteSelected.bind(this)); + toolbar.add(delButton); + + let selection = this._treeView.get_selection(); + selection.connect('changed', () => { + delButton.sensitive = selection.count_selected_rows() > 0; + }); + delButton.sensitive = selection.count_selected_rows() > 0; + + this._changedPermitted = true; + this._refresh(); + } + + _createNew() { + let dialog = new Gtk.AppChooserDialog({ + heading: _("Select an application for which grouping should apply"), + transient_for: this.get_toplevel(), + modal: true + }); + + dialog.get_widget().show_all = true; + + dialog.connect('response', (dialog, id) => { + if (id != Gtk.ResponseType.OK) { + dialog.destroy(); + return; + } + + let appInfo = dialog.get_app_info(); + if (!appInfo) { + dialog.destroy(); + return; + } + + this._changedPermitted = false; + this._appendItem(appInfo.get_id()); + this._changedPermitted = true; + + let iter = this._store.append(); + this._store.set(iter, + [Columns.APPINFO, Columns.ICON, Columns.DISPLAY_NAME], + [appInfo, appInfo.get_icon(), appInfo.get_display_name()]); + + dialog.destroy(); + }); + dialog.show_all(); + } + + _deleteSelected() { + let [any, model, iter] = this._treeView.get_selection().get_selected(); + + if (any) { + let appInfo = this._store.get_value(iter, Columns.APPINFO); + + this._changedPermitted = false; + this._removeItem(appInfo.get_id()); + this._changedPermitted = true; + this._store.remove(iter); + } + } + + _refresh() { + if (!this._changedPermitted) + // Ignore this notification, model is being modified outside + return; + + this._store.clear(); + + let currentItems = this._settings.get_strv(SETTINGS_KEY); + let validItems = [ ]; + for (let i = 0; i < currentItems.length; i++) { + let id = currentItems[i]; + let appInfo = Gio.DesktopAppInfo.new(id); + if (!appInfo) + continue; + validItems.push(currentItems[i]); + + let iter = this._store.append(); + this._store.set(iter, + [Columns.APPINFO, Columns.ICON, Columns.DISPLAY_NAME], + [appInfo, appInfo.get_icon(), appInfo.get_display_name()]); + } + + if (validItems.length != currentItems.length) // some items were filtered out + this._settings.set_strv(SETTINGS_KEY, validItems); + } + + _appendItem(id) { + let currentItems = this._settings.get_strv(SETTINGS_KEY); + currentItems.push(id); + this._settings.set_strv(SETTINGS_KEY, currentItems); + } + + _removeItem(id) { + let currentItems = this._settings.get_strv(SETTINGS_KEY); + let index = currentItems.indexOf(id); + + if (index < 0) + return; + currentItems.splice(index, 1); + this._settings.set_strv(SETTINGS_KEY, currentItems); + } +}); + + +function init() { + Convenience.initTranslations(); +} + +function buildPrefsWidget() { + let widget = new Widget({ margin: 12 }); + widget.show_all(); + + return widget; +} diff --git a/extensions/window-grouper/stylesheet.css b/extensions/window-grouper/stylesheet.css new file mode 100644 index 0000000..25134b6 --- /dev/null +++ b/extensions/window-grouper/stylesheet.css @@ -0,0 +1 @@ +/* This extensions requires no special styling */ diff --git a/meson.build b/meson.build index 201c484..6a4d1bf 100644 --- a/meson.build +++ b/meson.build @@ -59,7 +59,8 @@ all_extensions += [ 'panel-favorites', 'top-icons', 'updates-dialog', - 'user-theme' + 'user-theme', + 'window-grouper' ] enabled_extensions = get_option('enable_extensions') -- 2.21.0 From a2e46a1c2ee8308133703a9ba7951fdc4621100a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Tue, 26 Mar 2019 21:32:09 +0100 Subject: [PATCH 7/7] Add disable-screenshield extension --- extensions/disable-screenshield/extension.js | 32 +++++++++++++++++++ extensions/disable-screenshield/meson.build | 5 +++ .../disable-screenshield/metadata.json.in | 9 ++++++ .../disable-screenshield/stylesheet.css | 1 + meson.build | 1 + 5 files changed, 48 insertions(+) create mode 100644 extensions/disable-screenshield/extension.js create mode 100644 extensions/disable-screenshield/meson.build create mode 100644 extensions/disable-screenshield/metadata.json.in create mode 100644 extensions/disable-screenshield/stylesheet.css diff --git a/extensions/disable-screenshield/extension.js b/extensions/disable-screenshield/extension.js new file mode 100644 index 0000000..4f80f3c --- /dev/null +++ b/extensions/disable-screenshield/extension.js @@ -0,0 +1,32 @@ +const ScreenShield = imports.ui.screenShield; + +let _onUserBecameActiveOrig; + +function _onUserBecameActiveInjected() +{ + this.idleMonitor.remove_watch(this._becameActiveId); + this._becameActiveId = 0; + + this._longLightbox.hide(); + this._shortLightbox.hide(); + + this.deactivate(false); +} + +function init() +{ +} + +function enable() +{ + _onUserBecameActiveOrig = + ScreenShield.ScreenShield.prototype._onUserBecameActive; + ScreenShield.ScreenShield.prototype._onUserBecameActive = + _onUserBecameActiveInjected; +} + +function disable() +{ + ScreenShield.ScreenShield.prototype._onUserBecameActive = + _onUserBecameActiveOrig; +} diff --git a/extensions/disable-screenshield/meson.build b/extensions/disable-screenshield/meson.build new file mode 100644 index 0000000..48504f6 --- /dev/null +++ b/extensions/disable-screenshield/meson.build @@ -0,0 +1,5 @@ +extension_data += configure_file( + input: metadata_name + '.in', + output: metadata_name, + configuration: metadata_conf +) diff --git a/extensions/disable-screenshield/metadata.json.in b/extensions/disable-screenshield/metadata.json.in new file mode 100644 index 0000000..074429f --- /dev/null +++ b/extensions/disable-screenshield/metadata.json.in @@ -0,0 +1,9 @@ +{ + "extension-id": "@extension_id@", + "uuid": "@uuid@", + "name": "Disable Screen Shield", + "description": "Disable screen shield when screen lock is disabled", + "shell-version": [ "@shell_current@" ], + "original-authors": [ "lgpasquale@gmail.com" ], + "url": "@url@" +} diff --git a/extensions/disable-screenshield/stylesheet.css b/extensions/disable-screenshield/stylesheet.css new file mode 100644 index 0000000..25134b6 --- /dev/null +++ b/extensions/disable-screenshield/stylesheet.css @@ -0,0 +1 @@ +/* This extensions requires no special styling */ diff --git a/meson.build b/meson.build index 6a4d1bf..7562eb1 100644 --- a/meson.build +++ b/meson.build @@ -53,6 +53,7 @@ all_extensions = default_extensions all_extensions += [ 'auto-move-windows', 'dash-to-dock', + 'disable-screenshield', 'example', 'native-window-placement', 'no-hot-corner', -- 2.21.0