From 3ce61087bd6777c450690f5f10e7d5a689f8d08c 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/6] 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.20.1 From 6bbc576ed2c697b9da688fe19febff9c6ac7163f 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/6] 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.20.1 From b60180a015533e1f56cde52b853c9384f5532f53 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/6] 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.20.1 From 8f79f11a30d3e1bcbb441b50ac1ef0ea3ed2dc47 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/6] 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.20.1 From 3602940b74b9b138d9eb51b4ad2fe6cdacacbfd0 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/6] 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.20.1 From 74d8ce6acd4c1a61de25d796f63e4f1f79180ed4 Mon Sep 17 00:00:00 2001 From: Carlos Soriano Date: Mon, 13 Aug 2018 17:28:41 +0200 Subject: [PATCH 6/6] Add desktop icons extension --- extensions/desktop-icons/createThumbnail.js | 35 + extensions/desktop-icons/dbusUtils.js | 74 ++ extensions/desktop-icons/desktopGrid.js | 664 +++++++++++++++ extensions/desktop-icons/desktopIconsUtil.js | 60 ++ extensions/desktop-icons/desktopManager.js | 701 ++++++++++++++++ extensions/desktop-icons/extension.js | 71 ++ extensions/desktop-icons/fileItem.js | 771 ++++++++++++++++++ extensions/desktop-icons/meson.build | 18 + extensions/desktop-icons/metadata.json.in | 11 + extensions/desktop-icons/po/LINGUAS | 12 + extensions/desktop-icons/po/POTFILES.in | 4 + extensions/desktop-icons/po/cs.po | 136 +++ extensions/desktop-icons/po/da.po | 136 +++ extensions/desktop-icons/po/de.po | 136 +++ extensions/desktop-icons/po/es.po | 186 +++++ extensions/desktop-icons/po/fi.po | 136 +++ extensions/desktop-icons/po/fr.po | 164 ++++ extensions/desktop-icons/po/id.po | 135 +++ extensions/desktop-icons/po/it.po | 152 ++++ extensions/desktop-icons/po/meson.build | 1 + extensions/desktop-icons/po/pl.po | 157 ++++ extensions/desktop-icons/po/pt_BR.po | 163 ++++ extensions/desktop-icons/po/ru.po | 153 ++++ extensions/desktop-icons/po/zh_TW.po | 135 +++ extensions/desktop-icons/prefs.js | 159 ++++ extensions/desktop-icons/schemas/meson.build | 6 + ...shell.extensions.desktop-icons.gschema.xml | 25 + extensions/desktop-icons/stylesheet.css | 33 + meson.build | 1 + po/cs.po | 161 ++++ po/da.po | 161 ++++ po/de.po | 161 ++++ po/es.po | 222 ++++- po/fi.po | 167 +++- po/fr.po | 190 +++++ po/id.po | 159 ++++ po/it.po | 177 ++++ po/pl.po | 183 +++++ po/pt_BR.po | 197 ++++- po/ru.po | 179 ++++ po/zh_TW.po | 165 +++- 41 files changed, 6527 insertions(+), 30 deletions(-) create mode 100755 extensions/desktop-icons/createThumbnail.js create mode 100644 extensions/desktop-icons/dbusUtils.js create mode 100644 extensions/desktop-icons/desktopGrid.js create mode 100644 extensions/desktop-icons/desktopIconsUtil.js create mode 100644 extensions/desktop-icons/desktopManager.js create mode 100644 extensions/desktop-icons/extension.js create mode 100644 extensions/desktop-icons/fileItem.js create mode 100644 extensions/desktop-icons/meson.build create mode 100644 extensions/desktop-icons/metadata.json.in create mode 100644 extensions/desktop-icons/po/LINGUAS create mode 100644 extensions/desktop-icons/po/POTFILES.in create mode 100644 extensions/desktop-icons/po/cs.po create mode 100644 extensions/desktop-icons/po/da.po create mode 100644 extensions/desktop-icons/po/de.po create mode 100644 extensions/desktop-icons/po/es.po create mode 100644 extensions/desktop-icons/po/fi.po create mode 100644 extensions/desktop-icons/po/fr.po create mode 100644 extensions/desktop-icons/po/id.po create mode 100644 extensions/desktop-icons/po/it.po create mode 100644 extensions/desktop-icons/po/meson.build create mode 100644 extensions/desktop-icons/po/pl.po create mode 100644 extensions/desktop-icons/po/pt_BR.po create mode 100644 extensions/desktop-icons/po/ru.po create mode 100644 extensions/desktop-icons/po/zh_TW.po create mode 100644 extensions/desktop-icons/prefs.js create mode 100644 extensions/desktop-icons/schemas/meson.build create mode 100644 extensions/desktop-icons/schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml create mode 100644 extensions/desktop-icons/stylesheet.css diff --git a/extensions/desktop-icons/createThumbnail.js b/extensions/desktop-icons/createThumbnail.js new file mode 100755 index 0000000..212f6b7 --- /dev/null +++ b/extensions/desktop-icons/createThumbnail.js @@ -0,0 +1,35 @@ +#!/usr/bin/gjs + +/* Desktop Icons GNOME Shell extension + * + * Copyright (C) 2018 Sergio Costas + * + * 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 3 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 GnomeDesktop = imports.gi.GnomeDesktop; +const Gio = imports.gi.Gio; + +let thumbnailFactory = GnomeDesktop.DesktopThumbnailFactory.new(GnomeDesktop.DesktopThumbnailSize.LARGE); + +let file = Gio.File.new_for_path(ARGV[0]); +let fileUri = file.get_uri(); + +let fileInfo = file.query_info('standard::content-type,time::modified', Gio.FileQueryInfoFlags.NONE, null); +let modifiedTime = fileInfo.get_attribute_uint64('time::modified'); +let thumbnailPixbuf = thumbnailFactory.generate_thumbnail(fileUri, fileInfo.get_content_type()); +if (thumbnailPixbuf == null) + thumbnailFactory.create_failed_thumbnail(fileUri, modifiedTime); +else + thumbnailFactory.save_thumbnail(thumbnailPixbuf, fileUri, modifiedTime); diff --git a/extensions/desktop-icons/dbusUtils.js b/extensions/desktop-icons/dbusUtils.js new file mode 100644 index 0000000..ba9e912 --- /dev/null +++ b/extensions/desktop-icons/dbusUtils.js @@ -0,0 +1,74 @@ +const Gio = imports.gi.Gio; +var NautilusFileOperationsProxy; +var FreeDesktopFileManagerProxy; + +const NautilusFileOperationsInterface = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +const NautilusFileOperationsProxyInterface = Gio.DBusProxy.makeProxyWrapper(NautilusFileOperationsInterface); + +const FreeDesktopFileManagerInterface = ` + + + + + + + + + + +`; + +const FreeDesktopFileManagerProxyInterface = Gio.DBusProxy.makeProxyWrapper(FreeDesktopFileManagerInterface); + +function init() { + NautilusFileOperationsProxy = new NautilusFileOperationsProxyInterface( + Gio.DBus.session, + 'org.gnome.Nautilus', + '/org/gnome/Nautilus', + (proxy, error) => { + if (error) { + log('Error connecting to Nautilus'); + } + } + ); + + FreeDesktopFileManagerProxy = new FreeDesktopFileManagerProxyInterface( + Gio.DBus.session, + 'org.freedesktop.FileManager1', + '/org/freedesktop/FileManager1', + (proxy, error) => { + if (error) { + log('Error connecting to Nautilus'); + } + } + ); +} diff --git a/extensions/desktop-icons/desktopGrid.js b/extensions/desktop-icons/desktopGrid.js new file mode 100644 index 0000000..83ca2cc --- /dev/null +++ b/extensions/desktop-icons/desktopGrid.js @@ -0,0 +1,664 @@ +/* Desktop Icons GNOME Shell extension + * + * Copyright (C) 2017 Carlos Soriano + * + * 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 3 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 Gtk = imports.gi.Gtk; +const Clutter = imports.gi.Clutter; +const Lang = imports.lang; +const St = imports.gi.St; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Shell = imports.gi.Shell; + +const Signals = imports.signals; + +const Layout = imports.ui.layout; +const Main = imports.ui.main; +const BoxPointer = imports.ui.boxpointer; +const PopupMenu = imports.ui.popupMenu; +const GrabHelper = imports.ui.grabHelper; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Extension = Me.imports.extension; +const FileItem = Me.imports.fileItem; +const Prefs = Me.imports.prefs; +const DBusUtils = Me.imports.dbusUtils; +const DesktopIconsUtil = Me.imports.desktopIconsUtil; +const Util = imports.misc.util; + +const Clipboard = St.Clipboard.get_default(); +const CLIPBOARD_TYPE = St.ClipboardType.CLIPBOARD; +const Gettext = imports.gettext.domain('desktop-icons'); + +const _ = Gettext.gettext; + + +/* From NautilusFileUndoManagerState */ +var UndoStatus = { + NONE: 0, + UNDO: 1, + REDO: 2, +}; + +var StoredCoordinates = { + PRESERVE: 0, + OVERWRITE:1, +}; + +class Placeholder extends St.Bin { + constructor() { + super(); + } +} + +var DesktopGrid = class { + + constructor(bgManager) { + this._bgManager = bgManager; + + this._fileItemHandlers = new Map(); + this._fileItems = []; + + this.layout = new Clutter.GridLayout({ + orientation: Clutter.Orientation.VERTICAL, + column_homogeneous: true, + row_homogeneous: true + }); + + this.actor = new St.Widget(); + this.actor._delegate = this; + + this._grid = new St.Widget({ + name: 'DesktopGrid', + layout_manager: this.layout, + reactive: true, + x_expand: true, + y_expand: true, + can_focus: true, + opacity: 255 + }); + this.actor.add_child(this._grid); + + this._renamePopup = new RenamePopup(this); + this.actor.add_child(this._renamePopup.actor); + + this._bgManager._container.add_child(this.actor); + + this.actor.connect('destroy', () => this._onDestroy()); + + let monitorIndex = bgManager._monitorIndex; + this._monitorConstraint = new Layout.MonitorConstraint({ + index: monitorIndex, + work_area: true + }); + this._grid.add_constraint(this._monitorConstraint); + + this._addDesktopBackgroundMenu(); + + this._bgDestroyedId = bgManager.backgroundActor.connect('destroy', + () => this._backgroundDestroyed()); + + this._grid.connect('button-press-event', (actor, event) => this._onPressButton(actor, event)); + + this._grid.connect('key-press-event', this._onKeyPress.bind(this)); + + this._grid.connect('allocation-changed', () => Extension.desktopManager.scheduleReLayoutChildren()); + } + + _onKeyPress(actor, event) { + if (global.stage.get_key_focus() != actor) + return Clutter.EVENT_PROPAGATE; + + let symbol = event.get_key_symbol(); + let isCtrl = (event.get_state() & Clutter.ModifierType.CONTROL_MASK) != 0; + let isShift = (event.get_state() & Clutter.ModifierType.SHIFT_MASK) != 0; + if (isCtrl && isShift && [Clutter.Z, Clutter.z].indexOf(symbol) > -1) { + this._doRedo(); + return Clutter.EVENT_STOP; + } + else if (isCtrl && [Clutter.Z, Clutter.z].indexOf(symbol) > -1) { + this._doUndo(); + return Clutter.EVENT_STOP; + } + else if (isCtrl && [Clutter.C, Clutter.c].indexOf(symbol) > -1) { + Extension.desktopManager.doCopy(); + return Clutter.EVENT_STOP; + } + else if (isCtrl && [Clutter.X, Clutter.x].indexOf(symbol) > -1) { + Extension.desktopManager.doCut(); + return Clutter.EVENT_STOP; + } + else if (isCtrl && [Clutter.V, Clutter.v].indexOf(symbol) > -1) { + this._doPaste(); + return Clutter.EVENT_STOP; + } + else if (symbol == Clutter.Return) { + Extension.desktopManager.doOpen(); + return Clutter.EVENT_STOP; + } + else if (symbol == Clutter.Delete) { + Extension.desktopManager.doTrash(); + return Clutter.EVENT_STOP; + } else if (symbol == Clutter.F2) { + // Support renaming other grids file items. + Extension.desktopManager.doRename(); + return Clutter.EVENT_STOP; + } + + return Clutter.EVENT_PROPAGATE; + } + + _backgroundDestroyed() { + this._bgDestroyedId = 0; + if (this._bgManager == null) + return; + + if (this._bgManager._backgroundSource) { + this._bgDestroyedId = this._bgManager.backgroundActor.connect('destroy', + () => this._backgroundDestroyed()); + } else { + this.actor.destroy(); + } + } + + _onDestroy() { + if (this._bgDestroyedId && this._bgManager.backgroundActor != null) + this._bgManager.backgroundActor.disconnect(this._bgDestroyedId); + this._bgDestroyedId = 0; + this._bgManager = null; + } + + _omNewFolderClicked() { + let dir = DesktopIconsUtil.getDesktopDir().get_child(_('New Folder')); + DBusUtils.NautilusFileOperationsProxy.CreateFolderRemote(dir.get_uri(), + (result, error) => { + if (error) + throw new Error('Error creating new folder: ' + error.message); + } + ); + } + + _parseClipboardText(text) { + let lines = text.split('\n'); + let [mime, action, ...files] = lines; + + if (mime != 'x-special/nautilus-clipboard') + return [false, false, null]; + + if (!(['copy', 'cut'].includes(action))) + return [false, false, null]; + let isCut = action == 'cut'; + + /* Last line is empty due to the split */ + if (files.length <= 1) + return [false, false, null]; + /* Remove last line */ + files.pop(); + + return [true, isCut, files]; + } + + _doPaste() { + Clipboard.get_text(CLIPBOARD_TYPE, + (clipboard, text) => { + let [valid, is_cut, files] = this._parseClipboardText(text); + if (!valid) + return; + + let desktopDir = `${DesktopIconsUtil.getDesktopDir().get_uri()}`; + if (is_cut) { + DBusUtils.NautilusFileOperationsProxy.MoveURIsRemote(files, desktopDir, + (result, error) => { + if (error) + throw new Error('Error moving files: ' + error.message); + } + ); + } else { + DBusUtils.NautilusFileOperationsProxy.CopyURIsRemote(files, desktopDir, + (result, error) => { + if (error) + throw new Error('Error copying files: ' + error.message); + } + ); + } + } + ); + } + + _onPasteClicked() { + this._doPaste(); + } + + _doUndo() { + DBusUtils.NautilusFileOperationsProxy.UndoRemote( + (result, error) => { + if (error) + throw new Error('Error performing undo: ' + error.message); + } + ); + } + + _onUndoClicked() { + this._doUndo(); + } + + _doRedo() { + DBusUtils.NautilusFileOperationsProxy.RedoRemote( + (result, error) => { + if (error) + throw new Error('Error performing redo: ' + error.message); + } + ); + } + + _onRedoClicked() { + this._doRedo(); + } + + _onOpenDesktopInFilesClicked() { + Gio.AppInfo.launch_default_for_uri_async(DesktopIconsUtil.getDesktopDir().get_uri(), + null, null, + (source, res) => { + try { + Gio.AppInfo.launch_default_for_uri_finish(res); + } catch (e) { + log('Error opening Desktop in Files: ' + e.message); + } + } + ); + } + + _onOpenTerminalClicked() { + let desktopPath = DesktopIconsUtil.getDesktopDir().get_path(); + let command = DesktopIconsUtil.getTerminalCommand(desktopPath); + + Util.spawnCommandLine(command); + } + + _syncUndoRedo() { + this._undoMenuItem.actor.visible = DBusUtils.NautilusFileOperationsProxy.UndoStatus == UndoStatus.UNDO; + this._redoMenuItem.actor.visible = DBusUtils.NautilusFileOperationsProxy.UndoStatus == UndoStatus.REDO; + } + + _undoStatusChanged(proxy, properties, test) { + if ('UndoStatus' in properties.deep_unpack()) + this._syncUndoRedo(); + } + + _createDesktopBackgroundMenu() { + let menu = new PopupMenu.PopupMenu(Main.layoutManager.dummyCursor, + 0, St.Side.TOP); + menu.addAction(_("New Folder"), () => this._omNewFolderClicked()); + menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + this._pasteMenuItem = menu.addAction(_("Paste"), () => this._onPasteClicked()); + this._undoMenuItem = menu.addAction(_("Undo"), () => this._onUndoClicked()); + this._redoMenuItem = menu.addAction(_("Redo"), () => this._onRedoClicked()); + menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + menu.addAction(_("Show Desktop in Files"), () => this._onOpenDesktopInFilesClicked()); + menu.addAction(_("Open in Terminal"), () => this._onOpenTerminalClicked()); + menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + menu.addSettingsAction(_("Change Background…"), 'gnome-background-panel.desktop'); + menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + menu.addSettingsAction(_("Display Settings"), 'gnome-display-panel.desktop'); + menu.addSettingsAction(_("Settings"), 'gnome-control-center.desktop'); + + menu.actor.add_style_class_name('background-menu'); + + Main.layoutManager.uiGroup.add_child(menu.actor); + menu.actor.hide(); + + menu._propertiesChangedId = DBusUtils.NautilusFileOperationsProxy.connect('g-properties-changed', + this._undoStatusChanged.bind(this)); + this._syncUndoRedo(); + + menu.connect('destroy', + () => DBusUtils.NautilusFileOperationsProxy.disconnect(menu._propertiesChangedId)); + menu.connect('open-state-changed', + (popupm, isOpen) => { + if (isOpen) { + Clipboard.get_text(CLIPBOARD_TYPE, + (clipBoard, text) => { + let [valid, is_cut, files] = this._parseClipboardText(text); + this._pasteMenuItem.actor.visible = valid; + } + ); + } + } + ); + + return menu; + } + + _openMenu(x, y) { + Main.layoutManager.setDummyCursorGeometry(x, y, 0, 0); + this.actor._desktopBackgroundMenu.open(BoxPointer.PopupAnimation.NONE); + /* Since the handler is in the press event it needs to ignore the release event + * to not immediately close the menu on release + */ + this.actor._desktopBackgroundManager.ignoreRelease(); + } + + _addFileItemTo(fileItem, column, row, overwriteCoordinates) { + let placeholder = this.layout.get_child_at(column, row); + placeholder.child = fileItem.actor; + this._fileItems.push(fileItem); + let selectedId = fileItem.connect('selected', this._onFileItemSelected.bind(this)); + let renameId = fileItem.connect('rename-clicked', this.doRename.bind(this)); + this._fileItemHandlers.set(fileItem, [selectedId, renameId]); + + /* If this file is new in the Desktop and hasn't yet + * fixed coordinates, store the new possition to ensure + * that the next time it will be shown in the same possition. + * Also store the new possition if it has been moved by the user, + * and not triggered by a screen change. + */ + if ((fileItem.savedCoordinates == null) || (overwriteCoordinates == StoredCoordinates.OVERWRITE)) { + let [fileX, fileY] = placeholder.get_transformed_position(); + fileItem.savedCoordinates = [Math.round(fileX), Math.round(fileY)]; + } + } + + addFileItemCloseTo(fileItem, x, y, overwriteCoordinates) { + let [column, row] = this._getEmptyPlaceClosestTo(x, y); + this._addFileItemTo(fileItem, column, row, overwriteCoordinates); + } + + _getEmptyPlaceClosestTo(x, y) { + let maxColumns = this._getMaxColumns(); + let maxRows = this._getMaxRows(); + + let [actorX, actorY] = this._grid.get_transformed_position(); + let actorWidth = this._grid.allocation.x2 - this._grid.allocation.x1; + let actorHeight = this._grid.allocation.y2 - this._grid.allocation.y1; + let placeX = Math.round((x - actorX) * maxColumns / actorWidth); + let placeY = Math.round((y - actorY) * maxRows / actorHeight); + + placeX = DesktopIconsUtil.clamp(placeX, 0, maxColumns - 1); + placeY = DesktopIconsUtil.clamp(placeY, 0, maxRows - 1); + if (this.layout.get_child_at(placeX, placeY).child == null) + return [placeX, placeY]; + let found = false; + let resColumn = null; + let resRow = null; + let minDistance = Infinity; + for (let column = 0; column < maxColumns; column++) { + for (let row = 0; row < maxRows; row++) { + let placeholder = this.layout.get_child_at(column, row); + if (placeholder.child != null) + continue; + + let [proposedX, proposedY] = placeholder.get_transformed_position(); + let distance = DesktopIconsUtil.distanceBetweenPoints(proposedX, proposedY, x, y); + if (distance < minDistance) { + found = true; + minDistance = distance; + resColumn = column; + resRow = row; + } + } + } + + if (!found) + throw new Error(`Not enough place at monitor ${this._bgManager._monitorIndex}`); + + return [resColumn, resRow]; + } + + removeFileItem(fileItem) { + let index = this._fileItems.indexOf(fileItem); + if (index > -1) + this._fileItems.splice(index, 1); + else + throw new Error('Error removing children from container'); + + let [column, row] = this._getPosOfFileItem(fileItem); + let placeholder = this.layout.get_child_at(column, row); + placeholder.child = null; + let [selectedId, renameId] = this._fileItemHandlers.get(fileItem); + fileItem.disconnect(selectedId); + fileItem.disconnect(renameId); + this._fileItemHandlers.delete(fileItem); + } + + _fillPlaceholders() { + for (let column = 0; column < this._getMaxColumns(); column++) { + for (let row = 0; row < this._getMaxRows(); row++) { + this.layout.attach(new Placeholder(), column, row, 1, 1); + } + } + } + + reset() { + let tmpFileItemsCopy = this._fileItems.slice(); + for (let fileItem of tmpFileItemsCopy) + this.removeFileItem(fileItem); + this._grid.remove_all_children(); + + this._fillPlaceholders(); + } + + _onStageMotion(actor, event) { + if (this._drawingRubberBand) { + let [x, y] = event.get_coords(); + this._updateRubberBand(x, y); + this._selectFromRubberband(x, y); + } + return Clutter.EVENT_PROPAGATE; + } + + _onPressButton(actor, event) { + let button = event.get_button(); + let [x, y] = event.get_coords(); + + this._grid.grab_key_focus(); + + if (button == 1) { + let shiftPressed = !!(event.get_state() & Clutter.ModifierType.SHIFT_MASK); + let controlPressed = !!(event.get_state() & Clutter.ModifierType.CONTROL_MASK); + if (!shiftPressed && !controlPressed) + Extension.desktopManager.clearSelection(); + let [gridX, gridY] = this._grid.get_transformed_position(); + Extension.desktopManager.startRubberBand(x, y, gridX, gridY); + return Clutter.EVENT_STOP; + } + + if (button == 3) { + this._openMenu(x, y); + + return Clutter.EVENT_STOP; + } + + return Clutter.EVENT_PROPAGATE; + } + + _addDesktopBackgroundMenu() { + this.actor._desktopBackgroundMenu = this._createDesktopBackgroundMenu(); + this.actor._desktopBackgroundManager = new PopupMenu.PopupMenuManager({ actor: this.actor }); + this.actor._desktopBackgroundManager.addMenu(this.actor._desktopBackgroundMenu); + + this.actor.connect('destroy', () => { + this.actor._desktopBackgroundMenu.destroy(); + this.actor._desktopBackgroundMenu = null; + this.actor._desktopBackgroundManager = null; + }); + } + + _getMaxColumns() { + let gridWidth = this._grid.allocation.x2 - this._grid.allocation.x1; + return Math.floor(gridWidth / Prefs.get_desired_width(St.ThemeContext.get_for_stage(global.stage).scale_factor)); + } + + _getMaxRows() { + let gridHeight = this._grid.allocation.y2 - this._grid.allocation.y1; + return Math.floor(gridHeight / Prefs.get_desired_height(St.ThemeContext.get_for_stage(global.stage).scale_factor)); + } + + acceptDrop(source, actor, x, y, time) { + /* Coordinates are relative to the grid, we want to transform them to + * absolute coordinates to work across monitors */ + let [gridX, gridY] = this._grid.get_transformed_position(); + let [absoluteX, absoluteY] = [x + gridX, y + gridY]; + return Extension.desktopManager.acceptDrop(absoluteX, absoluteY); + } + + _getPosOfFileItem(itemToFind) { + if (itemToFind == null) + throw new Error('Error at _getPosOfFileItem: child cannot be null'); + + let found = false; + let maxColumns = this._getMaxColumns(); + let maxRows = this._getMaxRows(); + let column = 0; + let row = 0; + for (column = 0; column < maxColumns; column++) { + for (row = 0; row < maxRows; row++) { + let item = this.layout.get_child_at(column, row); + if (item.child && item.child._delegate.file.equal(itemToFind.file)) { + found = true; + break; + } + } + + if (found) + break; + } + + if (!found) + throw new Error('Position of file item was not found'); + + return [column, row]; + } + + _onFileItemSelected(fileItem, keepCurrentSelection, addToSelection) { + this._grid.grab_key_focus(); + } + + doRename(fileItem) { + this._renamePopup.onFileItemRenameClicked(fileItem); + } +}; + +var RenamePopup = class { + + constructor(grid) { + this._source = null; + this._isOpen = false; + + this._renameEntry = new St.Entry({ hint_text: _("Enter file name…"), + can_focus: true, + x_expand: true }); + this._renameEntry.clutter_text.connect('activate', this._onRenameAccepted.bind(this)); + this._renameOkButton= new St.Button({ label: _("OK"), + style_class: 'app-view-control button', + button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, + reactive: true, + can_focus: true, + x_expand: true }); + this._renameCancelButton = new St.Button({ label: _("Cancel"), + style_class: 'app-view-control button', + button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, + reactive: true, + can_focus: true, + x_expand: true }); + this._renameCancelButton.connect('clicked', () => { this._onRenameCanceled(); }); + this._renameOkButton.connect('clicked', () => { this._onRenameAccepted(); }); + let renameButtonsBoxLayout = new Clutter.BoxLayout({ homogeneous: true }); + let renameButtonsBox = new St.Widget({ layout_manager: renameButtonsBoxLayout, + x_expand: true }); + renameButtonsBox.add_child(this._renameCancelButton); + renameButtonsBox.add_child(this._renameOkButton); + + let renameContentLayout = new Clutter.BoxLayout({ spacing: 6, + orientation: Clutter.Orientation.VERTICAL }); + let renameContent = new St.Widget({ style_class: 'rename-popup', + layout_manager: renameContentLayout, + x_expand: true }); + renameContent.add_child(this._renameEntry); + renameContent.add_child(renameButtonsBox); + + this._boxPointer = new BoxPointer.BoxPointer(St.Side.TOP, { can_focus: false, x_expand: true }); + this.actor = this._boxPointer.actor; + this.actor.style_class = 'popup-menu-boxpointer'; + this.actor.add_style_class_name('popup-menu'); + this.actor.visible = false; + this._boxPointer.bin.set_child(renameContent); + + this._grabHelper = new GrabHelper.GrabHelper(grid.actor, { actionMode: Shell.ActionMode.POPUP }); + this._grabHelper.addActor(this.actor); + } + + _popup() { + if (this._isOpen) + return; + + this._isOpen = this._grabHelper.grab({ actor: this.actor, + onUngrab: this._popdown.bind(this) }); + + if (!this._isOpen) { + this._grabHelper.ungrab({ actor: this.actor }); + return; + } + + this._boxPointer.setPosition(this._source.actor, 0.5); + this._boxPointer.show(BoxPointer.PopupAnimation.FADE | + BoxPointer.PopupAnimation.SLIDE, + null); + + this.emit('open-state-changed', true); + } + + _popdown() { + if (!this._isOpen) + return; + + this._grabHelper.ungrab({ actor: this.actor }); + + this._boxPointer.hide(BoxPointer.PopupAnimation.FADE | + BoxPointer.PopupAnimation.SLIDE); + this._isOpen = false; + this.emit('open-state-changed', false); + } + + onFileItemRenameClicked(fileItem) { + this._source = fileItem; + + this._renameEntry.text = fileItem.displayName; + + this._popup(); + this._renameEntry.grab_key_focus(); + this._renameEntry.navigate_focus(null, Gtk.DirectionType.TAB_FORWARD, false) + let allChars = fileItem.displayName.length; + this._renameEntry.clutter_text.set_selection(0, allChars); + } + + _onRenameAccepted() { + this._popdown(); + DBusUtils.NautilusFileOperationsProxy.RenameFileRemote(this._source.file.get_uri(), + this._renameEntry.get_text(), + (result, error) => { + if (error) + throw new Error('Error renaming file: ' + error.message); + } + ); + } + + _onRenameCanceled() { + this._popdown(); + } +}; +Signals.addSignalMethods(RenamePopup.prototype); diff --git a/extensions/desktop-icons/desktopIconsUtil.js b/extensions/desktop-icons/desktopIconsUtil.js new file mode 100644 index 0000000..95f8e8b --- /dev/null +++ b/extensions/desktop-icons/desktopIconsUtil.js @@ -0,0 +1,60 @@ +/* Desktop Icons GNOME Shell extension + * + * Copyright (C) 2017 Carlos Soriano + * + * 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 3 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 Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Prefs = Me.imports.prefs; + +const TERMINAL_SCHEMA = 'org.gnome.desktop.default-applications.terminal'; +const EXEC_KEY = 'exec'; + +var DEFAULT_ATTRIBUTES = 'metadata::*,standard::*,access::*,time::modified,unix::mode'; + +function getDesktopDir() { + let desktopPath = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DESKTOP); + return Gio.File.new_for_commandline_arg(desktopPath); +} + +function clamp(value, min, max) { + return Math.max(Math.min(value, max), min); +}; + +function getTerminalCommand(workdir) { + let terminalSettings = new Gio.Settings({ schema_id: TERMINAL_SCHEMA }); + let exec = terminalSettings.get_string(EXEC_KEY); + let command = `${exec} --working-directory=${workdir}`; + + return command; +} + +function distanceBetweenPoints(x, y, x2, y2) { + return (Math.pow(x - x2, 2) + Math.pow(y - y2, 2)); +} + +function getExtraFolders() { + let extraFolders = new Array(); + if (Prefs.settings.get_boolean('show-home')) { + extraFolders.push([Gio.File.new_for_commandline_arg(GLib.get_home_dir()), Prefs.FileType.USER_DIRECTORY_HOME]); + } + if (Prefs.settings.get_boolean('show-trash')) { + extraFolders.push([Gio.File.new_for_uri('trash:///'), Prefs.FileType.USER_DIRECTORY_TRASH]); + } + return extraFolders; +} diff --git a/extensions/desktop-icons/desktopManager.js b/extensions/desktop-icons/desktopManager.js new file mode 100644 index 0000000..37e0014 --- /dev/null +++ b/extensions/desktop-icons/desktopManager.js @@ -0,0 +1,701 @@ +/* Desktop Icons GNOME Shell extension + * + * Copyright (C) 2017 Carlos Soriano + * + * 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 3 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 GObject = imports.gi.GObject; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Lang = imports.lang; +const St = imports.gi.St; +const Mainloop = imports.mainloop; +const Meta = imports.gi.Meta; + +const Animation = imports.ui.animation; +const Background = imports.ui.background; +const DND = imports.ui.dnd; +const Main = imports.ui.main; +const GrabHelper = imports.ui.grabHelper; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Extension = Me.imports.extension; +const DesktopGrid = Me.imports.desktopGrid; +const FileItem = Me.imports.fileItem; +const Prefs = Me.imports.prefs; +const DBusUtils = Me.imports.dbusUtils; +const DesktopIconsUtil = Me.imports.desktopIconsUtil; + +const Clipboard = St.Clipboard.get_default(); +const CLIPBOARD_TYPE = St.ClipboardType.CLIPBOARD; + +var S_IWOTH = 0x00002; + +function getDpy() { + return global.screen || global.display; +} + +function findMonitorIndexForPos(x, y) { + return getDpy().get_monitor_index_for_rect(new Meta.Rectangle({x, y})); +} + + +var DesktopManager = GObject.registerClass({ + Properties: { + 'writable-by-others': GObject.ParamSpec.boolean( + 'writable-by-others', + 'WritableByOthers', + 'Whether the desktop\'s directory can be written by others (o+w unix permission)', + GObject.ParamFlags.READABLE, + false + ) + } +}, class DesktopManager extends GObject.Object { + _init(params) { + super._init(params); + + this._layoutChildrenId = 0; + this._deleteChildrenId = 0; + this._monitorDesktopDir = null; + this._desktopMonitorCancellable = null; + this._desktopGrids = {}; + this._fileItemHandlers = new Map(); + this._fileItems = new Map(); + this._dragCancelled = false; + this._queryFileInfoCancellable = null; + this._unixMode = null; + this._writableByOthers = null; + + this._monitorsChangedId = Main.layoutManager.connect('monitors-changed', () => this._recreateDesktopIcons()); + this._rubberBand = new St.Widget({ style_class: 'rubber-band' }); + this._rubberBand.hide(); + Main.layoutManager.uiGroup.add_child(this._rubberBand); + this._grabHelper = new GrabHelper.GrabHelper(global.stage); + + this._addDesktopIcons(); + this._monitorDesktopFolder(); + + this.settingsId = Prefs.settings.connect('changed', () => this._recreateDesktopIcons()); + this.gtkSettingsId = Prefs.gtkSettings.connect('changed', (obj, key) => { + if (key == 'show-hidden') + this._recreateDesktopIcons(); + }); + + this._selection = new Set(); + this._inDrag = false; + this._dragXStart = Number.POSITIVE_INFINITY; + this._dragYStart = Number.POSITIVE_INFINITY; + } + + startRubberBand(x, y) { + this._rubberBandInitialX = x; + this._rubberBandInitialY = y; + this._updateRubberBand(x, y); + this._rubberBand.show(); + this._grabHelper.grab({ actor: global.stage }); + Extension.lockActivitiesButton = true; + this._stageReleaseEventId = global.stage.connect('button-release-event', (actor, event) => { + this.endRubberBand(); + }); + this._rubberBandId = global.stage.connect('motion-event', (actor, event) => { + [x, y] = event.get_coords(); + this._updateRubberBand(x, y); + let x0, y0, x1, y1; + if (x >= this._rubberBandInitialX) { + x0 = this._rubberBandInitialX; + x1 = x; + } else { + x1 = this._rubberBandInitialX; + x0 = x; + } + if (y >= this._rubberBandInitialY) { + y0 = this._rubberBandInitialY; + y1 = y; + } else { + y1 = this._rubberBandInitialY; + y0 = y; + } + for(let [fileUri, fileItem] of this._fileItems) { + fileItem.emit('selected', true, + fileItem.intersectsWith(x0, y0, x1 - x0, y1 - y0)); + } + }); + } + + endRubberBand() { + this._rubberBand.hide(); + Extension.lockActivitiesButton = false; + this._grabHelper.ungrab(); + global.stage.disconnect(this._rubberBandId); + global.stage.disconnect(this._stageReleaseEventId); + this._rubberBandId = 0; + this._stageReleaseEventId = 0; + } + + _updateRubberBand(currentX, currentY) { + let x = this._rubberBandInitialX < currentX ? this._rubberBandInitialX + : currentX; + let y = this._rubberBandInitialY < currentY ? this._rubberBandInitialY + : currentY; + let width = Math.abs(this._rubberBandInitialX - currentX); + let height = Math.abs(this._rubberBandInitialY - currentY); + /* TODO: Convert to gobject.set for 3.30 */ + this._rubberBand.set_position(x, y); + this._rubberBand.set_size(width, height); + } + + _recreateDesktopIcons() { + this._destroyDesktopIcons(); + this._addDesktopIcons(); + } + + _addDesktopIcons() { + forEachBackgroundManager(bgManager => { + let newGrid = new DesktopGrid.DesktopGrid(bgManager); + newGrid.actor.connect('destroy', (actor) => { + // if a grid loses its actor, remove it from the grid list + for(let grid in this._desktopGrids) + if (this._desktopGrids[grid].actor == actor) { + delete this._desktopGrids[grid]; + break; + } + }); + this._desktopGrids[bgManager._monitorIndex] = newGrid; + }); + + this._scanFiles(); + } + + _destroyDesktopIcons() { + Object.values(this._desktopGrids).forEach(grid => grid.actor.destroy()); + this._desktopGrids = {}; + } + + async _scanFiles() { + for (let [fileItem, id] of this._fileItemHandlers) + fileItem.disconnect(id); + this._fileItemHandlers = new Map(); + this._fileItems = new Map(); + + if (!this._unixMode) { + let desktopDir = DesktopIconsUtil.getDesktopDir(); + let fileInfo = desktopDir.query_info(Gio.FILE_ATTRIBUTE_UNIX_MODE, + Gio.FileQueryInfoFlags.NONE, + null); + this._unixMode = fileInfo.get_attribute_uint32(Gio.FILE_ATTRIBUTE_UNIX_MODE); + this._setWritableByOthers((this._unixMode & S_IWOTH) != 0); + } + + try { + for (let [file, info, extra] of await this._enumerateDesktop()) { + let fileItem = new FileItem.FileItem(file, info, extra); + this._fileItems.set(fileItem.file.get_uri(), fileItem); + let id = fileItem.connect('selected', + this._onFileItemSelected.bind(this)); + + this._fileItemHandlers.set(fileItem, id); + } + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + log(`Error loading desktop files ${e.message}`); + return; + } + + this.scheduleReLayoutChildren(); + } + + _enumerateDesktop() { + return new Promise((resolve, reject) => { + if (this._desktopEnumerateCancellable) + this._desktopEnumerateCancellable.cancel(); + + this._desktopEnumerateCancellable = new Gio.Cancellable(); + + let desktopDir = DesktopIconsUtil.getDesktopDir(); + desktopDir.enumerate_children_async(DesktopIconsUtil.DEFAULT_ATTRIBUTES, + Gio.FileQueryInfoFlags.NONE, + GLib.PRIORITY_DEFAULT, + this._desktopEnumerateCancellable, + (o, res) => { + try { + let fileEnum = desktopDir.enumerate_children_finish(res); + let resultGenerator = function *() { + let info; + while ((info = fileEnum.next_file(null))) + yield [fileEnum.get_child(info), info, Prefs.FileType.NONE]; + for (let [newFolder, extras] of DesktopIconsUtil.getExtraFolders()) { + yield [newFolder, newFolder.query_info(DesktopIconsUtil.DEFAULT_ATTRIBUTES, Gio.FileQueryInfoFlags.NONE, this._desktopEnumerateCancellable), extras]; + } + }.bind(this); + resolve(resultGenerator()); + } catch (e) { + reject(e); + } + }); + }); + } + + _monitorDesktopFolder() { + if (this._monitorDesktopDir) { + this._monitorDesktopDir.cancel(); + this._monitorDesktopDir = null; + } + + let desktopDir = DesktopIconsUtil.getDesktopDir(); + this._monitorDesktopDir = desktopDir.monitor_directory(Gio.FileMonitorFlags.WATCH_MOVES, null); + this._monitorDesktopDir.set_rate_limit(1000); + this._monitorDesktopDir.connect('changed', (obj, file, otherFile, eventType) => this._updateDesktopIfChanged(file, otherFile, eventType)); + } + + checkIfSpecialFilesAreSelected() { + for(let fileItem of this._selection) { + if (fileItem.isSpecial) + return true; + } + return false; + } + + get writableByOthers() { + return this._writableByOthers; + } + + _setWritableByOthers(value) { + if (value == this._writableByOthers) + return; + + this._writableByOthers = value + this.notify('writable-by-others'); + } + + _updateDesktopIfChanged (file, otherFile, eventType) { + let { + DELETED, MOVED_IN, MOVED_OUT, CREATED, RENAMED, CHANGES_DONE_HINT, ATTRIBUTE_CHANGED + } = Gio.FileMonitorEvent; + + let fileUri = file.get_uri(); + let fileItem = null; + if (this._fileItems.has(fileUri)) + fileItem = this._fileItems.get(fileUri); + switch(eventType) { + case RENAMED: + this._fileItems.delete(fileUri); + this._fileItems.set(otherFile.get_uri(), fileItem); + fileItem.onFileRenamed(otherFile); + return; + case CHANGES_DONE_HINT: + case ATTRIBUTE_CHANGED: + /* a file changed, rather than the desktop itself */ + let desktopDir = DesktopIconsUtil.getDesktopDir(); + if (file.get_uri() != desktopDir.get_uri()) + return; + + if (this._queryFileInfoCancellable) + this._queryFileInfoCancellable.cancel(); + + file.query_info_async(Gio.FILE_ATTRIBUTE_UNIX_MODE, + Gio.FileQueryInfoFlags.NONE, + GLib.PRIORITY_DEFAULT, + this._queryFileInfoCancellable, + (source, res) => { + try { + let info = source.query_info_finish(res); + this._queryFileInfoCancellable = null; + + this._unixMode = info.get_attribute_uint32(Gio.FILE_ATTRIBUTE_UNIX_MODE); + this._setWritableByOthers((this._unixMode & S_IWOTH) != 0); + + if (this._writableByOthers) + log(`desktop-icons: Desktop is writable by others - will not allow launching any desktop files`); + } catch(error) { + if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + global.log('Error getting desktop unix mode: ' + error); + } + }); + + return; + } + + // Only get a subset of events we are interested in. + // Note that CREATED will emit a CHANGES_DONE_HINT + if (![DELETED, MOVED_IN, MOVED_OUT, CREATED].includes(eventType)) + return; + + this._recreateDesktopIcons(); + } + + _setupDnD() { + this._draggableContainer = new St.Widget({ + visible: true, + width: 1, + height: 1, + x: 0, + y: 0, + style_class: 'draggable' + }); + this._draggableContainer._delegate = this; + this._draggable = DND.makeDraggable(this._draggableContainer, + { + manualMode: true, + dragActorOpacity: 100 + }); + + this._draggable.connect('drag-cancelled', () => this._onDragCancelled()); + this._draggable.connect('drag-end', () => this._onDragEnd()); + + this._draggable._dragActorDropped = event => this._dragActorDropped(event); + } + + dragStart() { + if (this._inDrag) { + return; + } + + this._setupDnD(); + let event = Clutter.get_current_event(); + let [x, y] = event.get_coords(); + [this._dragXStart, this._dragYStart] = event.get_coords(); + this._inDrag = true; + + for (let fileItem of this._selection) { + let clone = new Clutter.Clone({ + source: fileItem.actor, + reactive: false + }); + clone.x = fileItem.actor.get_transformed_position()[0]; + clone.y = fileItem.actor.get_transformed_position()[1]; + this._draggableContainer.add_child(clone); + } + + Main.layoutManager.uiGroup.add_child(this._draggableContainer); + this._draggable.startDrag(x, y, global.get_current_time(), event.get_event_sequence()); + } + + _onDragCancelled() { + let event = Clutter.get_current_event(); + let [x, y] = event.get_coords(); + this._dragCancelled = true; + } + + _onDragEnd() { + this._inDrag = false; + Main.layoutManager.uiGroup.remove_child(this._draggableContainer); + } + + _dragActorDropped(event) { + let [dropX, dropY] = event.get_coords(); + let target = this._draggable._dragActor.get_stage().get_actor_at_pos(Clutter.PickMode.ALL, + dropX, dropY); + + // We call observers only once per motion with the innermost + // target actor. If necessary, the observer can walk the + // parent itself. + let dropEvent = { + dropActor: this._draggable._dragActor, + targetActor: target, + clutterEvent: event + }; + for (let dragMonitor of DND.dragMonitors) { + let dropFunc = dragMonitor.dragDrop; + if (dropFunc) + switch (dropFunc(dropEvent)) { + case DragDropResult.FAILURE: + case DragDropResult.SUCCESS: + return true; + case DragDropResult.CONTINUE: + continue; + } + } + + // At this point it is too late to cancel a drag by destroying + // the actor, the fate of which is decided by acceptDrop and its + // side-effects + this._draggable._dragCancellable = false; + + let destroyActor = false; + while (target) { + if (target._delegate && target._delegate.acceptDrop) { + let [r, targX, targY] = target.transform_stage_point(dropX, dropY); + if (target._delegate.acceptDrop(this._draggable.actor._delegate, + this._draggable._dragActor, + targX, + targY, + event.get_time())) { + // If it accepted the drop without taking the actor, + // handle it ourselves. + if (this._draggable._dragActor.get_parent() == Main.uiGroup) { + if (this._draggable._restoreOnSuccess) { + this._draggable._restoreDragActor(event.get_time()); + return true; + } + else { + // We need this in order to make sure drag-end is fired + destroyActor = true; + } + } + + this._draggable._dragInProgress = false; + getDpy().set_cursor(Meta.Cursor.DEFAULT); + this._draggable.emit('drag-end', event.get_time(), true); + if (destroyActor) { + this._draggable._dragActor.destroy(); + } + this._draggable._dragComplete(); + + return true; + } + } + target = target.get_parent(); + } + + this._draggable._cancelDrag(event.get_time()); + + return true; + } + + acceptDrop(xEnd, yEnd) { + let savedCoordinates = new Map(); + let [xDiff, yDiff] = [xEnd - this._dragXStart, yEnd - this._dragYStart]; + /* Remove all items before dropping new ones, so we can freely reposition + * them. + */ + for (let item of this._selection) { + let [itemX, itemY] = item.actor.get_transformed_position(); + let monitorIndex = findMonitorIndexForPos(itemX, itemY); + savedCoordinates.set(item, [itemX, itemY]); + this._desktopGrids[monitorIndex].removeFileItem(item); + } + + for (let item of this._selection) { + let [itemX, itemY] = savedCoordinates.get(item); + /* Set the new ideal position where the item drop should happen */ + let newFileX = Math.round(xDiff + itemX); + let newFileY = Math.round(yDiff + itemY); + let monitorIndex = findMonitorIndexForPos(newFileX, newFileY); + this._desktopGrids[monitorIndex].addFileItemCloseTo(item, newFileX, newFileY, DesktopGrid.StoredCoordinates.OVERWRITE); + } + + return true; + } + + selectionDropOnFileItem (fileItemDestination) { + if (!fileItemDestination.isDirectory) + return false; + + let droppedUris = []; + for (let fileItem of this._selection) { + if (fileItem.isSpecial) + return false; + if (fileItemDestination.file.get_uri() == fileItem.file.get_uri()) + return false; + droppedUris.push(fileItem.file.get_uri()); + } + + if (droppedUris.length == 0) + return true; + + DBusUtils.NautilusFileOperationsProxy.MoveURIsRemote(droppedUris, + fileItemDestination.file.get_uri(), + (result, error) => { + if (error) + throw new Error('Error moving files: ' + error.message); + } + ); + for (let fileItem of this._selection) { + fileItem.state = FileItem.State.GONE; + } + + this._recreateDesktopIcons(); + + return true; + } + + _resetGridsAndScheduleLayout() { + this._deleteChildrenId = 0; + + Object.values(this._desktopGrids).forEach((grid) => grid.reset()); + + this._layoutChildrenId = GLib.idle_add(GLib.PRIORITY_LOW, () => this._layoutChildren()); + + return GLib.SOURCE_REMOVE; + } + + scheduleReLayoutChildren() { + if (this._deleteChildrenId != 0) + return; + + if (this._layoutChildrenId != 0) { + GLib.source_remove(this._layoutChildrenId); + this._layoutChildrenId = 0; + } + + + this._deleteChildrenId = GLib.idle_add(GLib.PRIORITY_LOW, () => this._resetGridsAndScheduleLayout()); + } + + _addFileItemCloseTo(item) { + let [x, y] = (item.savedCoordinates == null) ? [0, 0] : item.savedCoordinates; + let monitorIndex = findMonitorIndexForPos(x, y); + let desktopGrid = this._desktopGrids[monitorIndex]; + try { + desktopGrid.addFileItemCloseTo(item, x, y, DesktopGrid.StoredCoordinates.PRESERVE); + } catch (e) { + log(`Error adding children to desktop: ${e.message}`); + } + } + + _layoutChildren() { + let showHidden = Prefs.gtkSettings.get_boolean('show-hidden'); + /* + * Paint the icons in two passes: + * * first pass paints those that have their coordinates defined in the metadata + * * second pass paints those new files that still don't have their definitive coordinates + */ + for (let [fileUri, fileItem] of this._fileItems) { + if (fileItem.savedCoordinates == null) + continue; + if (fileItem.state != FileItem.State.NORMAL) + continue; + if (!showHidden && fileItem.isHidden) + continue; + this._addFileItemCloseTo(fileItem); + } + + for (let [fileUri, fileItem] of this._fileItems) { + if (fileItem.savedCoordinates !== null) + continue; + if (fileItem.state != FileItem.State.NORMAL) + continue; + if (!showHidden && fileItem.isHidden) + continue; + this._addFileItemCloseTo(fileItem); + } + + this._layoutChildrenId = 0; + return GLib.SOURCE_REMOVE; + } + + doRename() { + if (this._selection.size != 1) + return; + + let item = [...this._selection][0]; + if (item.canRename()) + item.doRename(); + } + + doOpen() { + for (let fileItem of this._selection) + fileItem.doOpen(); + } + + doTrash() { + DBusUtils.NautilusFileOperationsProxy.TrashFilesRemote([...this._selection].map((x) => { return x.file.get_uri(); }), + (source, error) => { + if (error) + throw new Error('Error trashing files on the desktop: ' + error.message); + } + ); + } + + doEmptyTrash() { + DBusUtils.NautilusFileOperationsProxy.EmptyTrashRemote( (source, error) => { + if (error) + throw new Error('Error trashing files on the desktop: ' + error.message); + }); + } + + _onFileItemSelected(fileItem, keepCurrentSelection, addToSelection) { + + if (!keepCurrentSelection && !this._inDrag) + this.clearSelection(); + + if (addToSelection) + this._selection.add(fileItem); + else + this._selection.delete(fileItem); + + for(let [fileUri, fileItem] of this._fileItems) + fileItem.isSelected = this._selection.has(fileItem); + } + + clearSelection() { + for (let [fileUri, fileItem] of this._fileItems) + fileItem.isSelected = false; + this._selection = new Set(); + } + + _getClipboardText(isCopy) { + let action = isCopy ? 'copy' : 'cut'; + let text = `x-special/nautilus-clipboard\n${action}\n${ + [...this._selection].map(s => s.file.get_uri()).join('\n') + }\n`; + + return text; + } + + doCopy() { + Clipboard.set_text(CLIPBOARD_TYPE, this._getClipboardText(true)); + } + + doCut() { + Clipboard.set_text(CLIPBOARD_TYPE, this._getClipboardText(false)); + } + + destroy() { + if (this._monitorDesktopDir) + this._monitorDesktopDir.cancel(); + this._monitorDesktopDir = null; + + if (this.settingsId) + Prefs.settings.disconnect(this.settingsId); + this.settingsId = 0; + if (this.gtkSettingsId) + Prefs.gtkSettings.disconnect(this.gtkSettingsId); + this.gtkSettingsId = 0; + + if (this._layoutChildrenId) + GLib.source_remove(this._layoutChildrenId); + this._layoutChildrenId = 0; + + if (this._deleteChildrenId) + GLib.source_remove(this._deleteChildrenId); + this._deleteChildrenId = 0; + + if (this._monitorsChangedId) + Main.layoutManager.disconnect(this._monitorsChangedId); + this._monitorsChangedId = 0; + if (this._stageReleaseEventId) + global.stage.disconnect(this._stageReleaseEventId); + this._stageReleaseEventId = 0; + + if (this._rubberBandId) + global.stage.disconnect(this._rubberBandId); + this._rubberBandId = 0; + + this._rubberBand.destroy(); + + if (this._queryFileInfoCancellable) + this._queryFileInfoCancellable.cancel(); + + Object.values(this._desktopGrids).forEach(grid => grid.actor.destroy()); + this._desktopGrids = {} + } +}); + +function forEachBackgroundManager(func) { + Main.layoutManager._bgManagers.forEach(func); +} diff --git a/extensions/desktop-icons/extension.js b/extensions/desktop-icons/extension.js new file mode 100644 index 0000000..e5ca440 --- /dev/null +++ b/extensions/desktop-icons/extension.js @@ -0,0 +1,71 @@ +/* Desktop Icons GNOME Shell extension + * + * Copyright (C) 2017 Carlos Soriano + * + * 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 3 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 Main = imports.ui.main; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Prefs = Me.imports.prefs; +const { DesktopManager } = Me.imports.desktopManager; +const DBusUtils = Me.imports.dbusUtils; + +var desktopManager = null; +var addBackgroundMenuOrig = null; +var _startupPreparedId; +var lockActivitiesButton = false; + +var oldShouldToggleByCornerOrButtonFunction = null; + +function init() { + addBackgroundMenuOrig = Main.layoutManager._addBackgroundMenu; + + Prefs.initTranslations(); +} + +function newShouldToggleByCornerOrButton() { + if (lockActivitiesButton) + return false; + else + return oldShouldToggleByCornerOrButtonFunction.bind(Main.overview); +} + +function enable() { + // register a new function to allow to lock the Activities button when doing a rubberband selection + oldShouldToggleByCornerOrButtonFunction = Main.overview.shouldToggleByCornerOrButton; + Main.overview.shouldToggleByCornerOrButton = newShouldToggleByCornerOrButton; + // wait until the startup process has ended + if (Main.layoutManager._startingUp) + _startupPreparedId = Main.layoutManager.connect('startup-complete', () => innerEnable(true)); + else + innerEnable(false); +} + +function innerEnable(disconnectSignal) { + if (disconnectSignal) + Main.layoutManager.disconnect(_startupPreparedId); + DBusUtils.init(); + Prefs.init(); + Main.layoutManager._addBackgroundMenu = function() {}; + desktopManager = new DesktopManager(); +} + +function disable() { + desktopManager.destroy(); + Main.layoutManager._addBackgroundMenu = addBackgroundMenuOrig; + Main.overview.shouldToggleByCornerOrButton = oldShouldToggleByCornerOrButtonFunction; +} \ No newline at end of file diff --git a/extensions/desktop-icons/fileItem.js b/extensions/desktop-icons/fileItem.js new file mode 100644 index 0000000..75c1d23 --- /dev/null +++ b/extensions/desktop-icons/fileItem.js @@ -0,0 +1,771 @@ +/* Desktop Icons GNOME Shell extension + * + * Copyright (C) 2017 Carlos Soriano + * + * 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 3 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 St = imports.gi.St; +const Pango = imports.gi.Pango; +const Meta = imports.gi.Meta; +const GdkPixbuf = imports.gi.GdkPixbuf; +const Cogl = imports.gi.Cogl; +const GnomeDesktop = imports.gi.GnomeDesktop; + +const Mainloop = imports.mainloop; +const Signals = imports.signals; + +const Background = imports.ui.background; +const Main = imports.ui.main; +const PopupMenu = imports.ui.popupMenu; +const Util = imports.misc.util; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Extension = Me.imports.extension; +const Prefs = Me.imports.prefs; +const DBusUtils = Me.imports.dbusUtils; +const DesktopIconsUtil = Me.imports.desktopIconsUtil; + +const Gettext = imports.gettext.domain('desktop-icons'); + +const _ = Gettext.gettext; + +const DRAG_TRESHOLD = 8; + +var S_IXUSR = 0o00100; +var S_IWOTH = 0o00002; + +var State = { + NORMAL: 0, + GONE: 1, +}; + +var FileItem = class { + + constructor(file, fileInfo, fileExtra) { + this._fileExtra = fileExtra; + this._loadThumbnailDataCancellable = null; + this._thumbnailScriptWatch = 0; + this._setMetadataCancellable = null; + this._queryFileInfoCancellable = null; + this._isSpecial = this._fileExtra != Prefs.FileType.NONE; + + this._file = file; + + this._savedCoordinates = null; + let savedCoordinates = fileInfo.get_attribute_as_string('metadata::nautilus-icon-position'); + if (savedCoordinates != null) + this._savedCoordinates = savedCoordinates.split(',').map(x => Number(x)); + + this._state = State.NORMAL; + + this.actor = new St.Bin({ visible: true }); + this.actor.set_fill(true, true); + let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; + this.actor.set_height(Prefs.get_desired_height(scaleFactor)); + this.actor.set_width(Prefs.get_desired_width(scaleFactor)); + this.actor._delegate = this; + this.actor.connect('destroy', () => this._onDestroy()); + + this._container = new St.BoxLayout({ reactive: true, + track_hover: true, + can_focus: true, + style_class: 'file-item', + x_expand: true, + y_expand: true, + x_align: Clutter.ActorAlign.FILL, + vertical: true }); + this.actor.set_child(this._container); + this._icon = new St.Bin(); + this._icon.set_height(Prefs.get_icon_size() * scaleFactor); + + this._iconContainer = new St.Bin({ visible: true }); + this._iconContainer.child = this._icon; + this._container.add_child(this._iconContainer); + + this._label = new St.Label({ + style_class: 'name-label' + }); + + this._container.add_child(this._label); + let clutterText = this._label.get_clutter_text(); + /* TODO: Convert to gobject.set for 3.30 */ + clutterText.set_line_wrap(true); + clutterText.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR); + clutterText.set_ellipsize(Pango.EllipsizeMode.END); + + this._container.connect('button-press-event', (actor, event) => this._onPressButton(actor, event)); + this._container.connect('motion-event', (actor, event) => this._onMotion(actor, event)); + this._container.connect('leave-event', (actor, event) => this._onLeave(actor, event)); + this._container.connect('button-release-event', (actor, event) => this._onReleaseButton(actor, event)); + + /* Set the metadata and update relevant UI */ + this._updateMetadataFromFileInfo(fileInfo); + + if (this._isDesktopFile) { + /* watch for the executable bit being removed or added */ + this._monitorDesktopFile = this._file.monitor_file(Gio.FileMonitorFlags.NONE, null); + this._monitorDesktopFileId = this._monitorDesktopFile.connect('changed', + (obj, file, otherFile, eventType) => { + switch(eventType) { + case Gio.FileMonitorEvent.ATTRIBUTE_CHANGED: + this._refreshMetadataAsync(true); + break; + } + }); + } + + this._createMenu(); + this._updateIcon(); + + this._isSelected = false; + this._primaryButtonPressed = false; + if (this._attributeCanExecute && !this._isDesktopFile) + this._execLine = this.file.get_path(); + if (fileExtra == Prefs.FileType.USER_DIRECTORY_TRASH) { + // if this icon is the trash, monitor the state of the directory to update the icon + this._trashChanged = false; + this._trashInitializeCancellable = null; + this._scheduleTrashRefreshId = 0; + this._monitorTrashDir = this._file.monitor_directory(Gio.FileMonitorFlags.WATCH_MOVES, null); + this._monitorTrashId = this._monitorTrashDir.connect('changed', (obj, file, otherFile, eventType) => { + switch(eventType) { + case Gio.FileMonitorEvent.DELETED: + case Gio.FileMonitorEvent.MOVED_OUT: + case Gio.FileMonitorEvent.CREATED: + case Gio.FileMonitorEvent.MOVED_IN: + if (this._queryTrashInfoCancellable || this._scheduleTrashRefreshId) { + if (this._scheduleTrashRefreshId) + GLib.source_remove(this._scheduleTrashRefreshId); + this._scheduleTrashRefreshId = Mainloop.timeout_add(200, () => this._refreshTrashIcon()); + } else { + this._refreshTrashIcon(); + } + break; + } + }); + } + + Extension.desktopManager.connect('notify::writable-by-others', () => { + if (!this._isDesktopFile) + return; + this._refreshMetadataAsync(true); + }); + } + + _onDestroy() { + /* Regular file data */ + if (this._setMetadataCancellable) + this._setMetadataCancellable.cancel(); + if (this._queryFileInfoCancellable) + this._queryFileInfoCancellable.cancel(); + + /* Thumbnailing */ + if (this._thumbnailScriptWatch) + GLib.source_remove(this._thumbnailScriptWatch); + if (this._loadThumbnailDataCancellable) + this._loadThumbnailDataCancellable.cancel(); + + /* Desktop file */ + if (this._monitorDesktopFileId) + this._monitorDesktopFile.disconnect(this._monitorDesktopFileId); + + /* Trash */ + if (this._monitorTrashDir) + this._monitorTrashDir.disconnect(this._monitorTrashId); + if (this._queryTrashInfoCancellable) + this._queryTrashInfoCancellable.cancel(); + if (this._scheduleTrashRefreshId) + GLib.source_remove(this._scheduleTrashRefreshId); + } + + _refreshMetadataAsync(rebuild) { + if (this._queryFileInfoCancellable) + this._queryFileInfoCancellable.cancel(); + this._queryFileInfoCancellable = new Gio.Cancellable(); + this._file.query_info_async(DesktopIconsUtil.DEFAULT_ATTRIBUTES, + Gio.FileQueryInfoFlags.NONE, + GLib.PRIORITY_DEFAULT, + this._queryFileInfoCancellable, + (source, res) => { + try { + let newFileInfo = source.query_info_finish(res); + this._queryFileInfoCancellable = null; + this._updateMetadataFromFileInfo(newFileInfo); + if (rebuild) { + this._createMenu(); + this._updateIcon(); + } + } catch(error) { + if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + global.log("Error getting the file info: " + error); + } + }); + } + + _updateMetadataFromFileInfo(fileInfo) { + this._fileInfo = fileInfo; + + let oldLabelText = this._label.text; + + this._displayName = fileInfo.get_attribute_as_string('standard::display-name'); + this._attributeCanExecute = fileInfo.get_attribute_boolean('access::can-execute'); + this._unixmode = fileInfo.get_attribute_uint32('unix::mode') + this._writableByOthers = (this._unixmode & S_IWOTH) != 0; + this._trusted = fileInfo.get_attribute_as_string('metadata::trusted') == 'true'; + this._attributeContentType = fileInfo.get_content_type(); + this._isDesktopFile = this._attributeContentType == 'application/x-desktop'; + + if (this._isDesktopFile && this._writableByOthers) + log(`desktop-icons: File ${this._displayName} is writable by others - will not allow launching`); + + if (this._isDesktopFile) { + this._desktopFile = Gio.DesktopAppInfo.new_from_filename(this._file.get_path()); + if (!this._desktopFile) { + log(`Couldn’t parse ${this._displayName} as a desktop file, will treat it as a regular file.`); + this._isDesktopFile = false; + } + } + + if (this.displayName != oldLabelText) { + this._label.text = this.displayName; + } + + this._fileType = fileInfo.get_file_type(); + this._isDirectory = this._fileType == Gio.FileType.DIRECTORY; + this._isSpecial = this._fileExtra != Prefs.FileType.NONE; + this._attributeHidden = fileInfo.get_is_hidden(); + this._isSymlink = fileInfo.get_is_symlink(); + this._modifiedTime = this._fileInfo.get_attribute_uint64("time::modified"); + /* + * This is a glib trick to detect broken symlinks. If a file is a symlink, the filetype + * points to the final file, unless it is broken; thus if the file type is SYMBOLIC_LINK, + * it must be a broken link. + * https://developer.gnome.org/gio/stable/GFile.html#g-file-query-info + */ + this._isBrokenSymlink = this._isSymlink && this._fileType == Gio.FileType.SYMBOLIC_LINK + } + + onFileRenamed(file) { + this._file = file; + this._refreshMetadataAsync(false); + } + + _updateIcon() { + if (this._fileExtra == Prefs.FileType.USER_DIRECTORY_TRASH) { + this._icon.child = this._createEmblemedStIcon(this._fileInfo.get_icon(), null); + return; + } + + let thumbnailFactory = GnomeDesktop.DesktopThumbnailFactory.new(GnomeDesktop.DesktopThumbnailSize.LARGE); + if (thumbnailFactory.can_thumbnail(this._file.get_uri(), + this._attributeContentType, + this._modifiedTime)) { + let thumbnail = thumbnailFactory.lookup(this._file.get_uri(), this._modifiedTime); + if (thumbnail == null) { + if (!thumbnailFactory.has_valid_failed_thumbnail(this._file.get_uri(), + this._modifiedTime)) { + let argv = []; + argv.push(GLib.build_filenamev([ExtensionUtils.getCurrentExtension().path, + 'createThumbnail.js'])); + argv.push(this._file.get_path()); + let [success, pid] = GLib.spawn_async(null, argv, null, + GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.DO_NOT_REAP_CHILD, null); + if (this._thumbnailScriptWatch) + GLib.source_remove(this._thumbnailScriptWatch); + this._thumbnailScriptWatch = GLib.child_watch_add(GLib.PRIORITY_DEFAULT, + pid, + (pid, exitCode) => { + if (exitCode == 0) + this._updateIcon(); + else + global.log('Failed to generate thumbnail for ' + this._filePath); + GLib.spawn_close_pid(pid); + return false; + } + ); + } + } else { + if (this._loadThumbnailDataCancellable) + this._loadThumbnailDataCancellable.cancel(); + this._loadThumbnailDataCancellable = new Gio.Cancellable(); + let thumbnailFile = Gio.File.new_for_path(thumbnail); + thumbnailFile.load_bytes_async(this._loadThumbnailDataCancellable, + (obj, res) => { + try { + let [thumbnailData, etag_out] = obj.load_bytes_finish(res); + let thumbnailStream = Gio.MemoryInputStream.new_from_bytes(thumbnailData); + let thumbnailPixbuf = GdkPixbuf.Pixbuf.new_from_stream(thumbnailStream, null); + + if (thumbnailPixbuf != null) { + let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; + let thumbnailImage = new Clutter.Image(); + thumbnailImage.set_data(thumbnailPixbuf.get_pixels(), + thumbnailPixbuf.has_alpha ? Cogl.PixelFormat.RGBA_8888 : Cogl.PixelFormat.RGB_888, + thumbnailPixbuf.width, + thumbnailPixbuf.height, + thumbnailPixbuf.rowstride + ); + let icon = new Clutter.Actor(); + icon.set_content(thumbnailImage); + let width = Prefs.get_desired_width(scaleFactor); + let height = Prefs.get_icon_size() * scaleFactor; + let aspectRatio = thumbnailPixbuf.width / thumbnailPixbuf.height; + if ((width / height) > aspectRatio) + icon.set_size(height * aspectRatio, height); + else + icon.set_size(width, width / aspectRatio); + this._icon.child = icon; + } + } catch (error) { + if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { + global.log('Error while loading thumbnail: ' + error); + this._icon.child = this._createEmblemedStIcon(this._fileInfo.get_icon(), null); + } + } + } + ); + } + } + + if (this._isBrokenSymlink) { + this._icon.child = this._createEmblemedStIcon(null, 'text-x-generic'); + } else { + if (this.trustedDesktopFile && this._desktopFile.has_key('Icon')) + this._icon.child = this._createEmblemedStIcon(null, this._desktopFile.get_string('Icon')); + else + this._icon.child = this._createEmblemedStIcon(this._fileInfo.get_icon(), null); + } + } + + _refreshTrashIcon() { + if (this._queryTrashInfoCancellable) + this._queryTrashInfoCancellable.cancel(); + this._queryTrashInfoCancellable = new Gio.Cancellable(); + + this._file.query_info_async(DesktopIconsUtil.DEFAULT_ATTRIBUTES, + Gio.FileQueryInfoFlags.NONE, + GLib.PRIORITY_DEFAULT, + this._queryTrashInfoCancellable, + (source, res) => { + try { + this._fileInfo = source.query_info_finish(res); + this._queryTrashInfoCancellable = null; + this._updateIcon(); + } catch(error) { + if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + global.log('Error getting the number of files in the trash: ' + error); + } + }); + + this._scheduleTrashRefreshId = 0; + return false; + } + + get file() { + return this._file; + } + + get isHidden() { + return this._fileInfo.get_is_hidden(); + } + + _createEmblemedStIcon(icon, iconName) { + if (icon == null) { + if (GLib.path_is_absolute(iconName)) { + let iconFile = Gio.File.new_for_commandline_arg(iconName); + icon = new Gio.FileIcon({ file: iconFile }); + } else { + icon = Gio.ThemedIcon.new_with_default_fallbacks(iconName); + } + } + let itemIcon = Gio.EmblemedIcon.new(icon, null); + + if (this._isSymlink) { + if (this._isBrokenSymlink) + itemIcon.add_emblem(Gio.Emblem.new(Gio.ThemedIcon.new('emblem-unreadable'))); + else + itemIcon.add_emblem(Gio.Emblem.new(Gio.ThemedIcon.new('emblem-symbolic-link'))); + } else if (this.trustedDesktopFile) { + itemIcon.add_emblem(Gio.Emblem.new(Gio.ThemedIcon.new('emblem-symbolic-link'))); + } + + return new St.Icon({ gicon: itemIcon, + icon_size: Prefs.get_icon_size() + }); + } + + doRename() { + if (!this.canRename()) { + log (`Error: ${this.file.get_uri()} cannot be renamed`); + return; + } + + this.emit('rename-clicked'); + } + + doOpen() { + if (this._isBrokenSymlink) { + log(`Error: Can’t open ${this.file.get_uri()} because it is a broken symlink.`); + return; + } + + if (this.trustedDesktopFile) { + this._desktopFile.launch_uris_as_manager([], null, GLib.SpawnFlags.SEARCH_PATH, null, null); + return; + } + + if (this._attributeCanExecute && !this._isDirectory && !this._isDesktopFile) { + if (this._execLine) + Util.spawnCommandLine(this._execLine); + return; + } + + Gio.AppInfo.launch_default_for_uri_async(this.file.get_uri(), + null, null, + (source, res) => { + try { + Gio.AppInfo.launch_default_for_uri_finish(res); + } catch (e) { + log('Error opening file ' + this.file.get_uri() + ': ' + e.message); + } + } + ); + } + + _onCopyClicked() { + Extension.desktopManager.doCopy(); + } + + _onCutClicked() { + Extension.desktopManager.doCut(); + } + + _onShowInFilesClicked() { + + DBusUtils.FreeDesktopFileManagerProxy.ShowItemsRemote([this.file.get_uri()], '', + (result, error) => { + if (error) + log('Error showing file on desktop: ' + error.message); + } + ); + } + + _onPropertiesClicked() { + + DBusUtils.FreeDesktopFileManagerProxy.ShowItemPropertiesRemote([this.file.get_uri()], '', + (result, error) => { + if (error) + log('Error showing properties: ' + error.message); + } + ); + } + + _onMoveToTrashClicked() { + Extension.desktopManager.doTrash(); + } + + _onEmptyTrashClicked() { + Extension.desktopManager.doEmptyTrash(); + } + + get _allowLaunchingText() { + if (this.trustedDesktopFile) + return _("Don’t Allow Launching"); + + return _("Allow Launching"); + } + + get metadataTrusted() { + return this._trusted; + } + + set metadataTrusted(value) { + this._trusted = value; + + let info = new Gio.FileInfo(); + info.set_attribute_string('metadata::trusted', + value ? 'true' : 'false'); + this._file.set_attributes_async(info, + Gio.FileQueryInfoFlags.NONE, + GLib.PRIORITY_LOW, + null, + (source, res) => { + try { + source.set_attributes_finish(res); + this._refreshMetadataAsync(true); + } catch(e) { + log(`Failed to set metadata::trusted: ${e.message}`); + } + }); + } + + _onAllowDisallowLaunchingClicked() { + this.metadataTrusted = !this.trustedDesktopFile; + + /* + * we're marking as trusted, make the file executable too. note that we + * do not ever remove the executable bit, since we don't know who set + * it. + */ + if (this.metadataTrusted && !this._attributeCanExecute) { + let info = new Gio.FileInfo(); + let newUnixMode = this._unixmode | S_IXUSR; + info.set_attribute_uint32(Gio.FILE_ATTRIBUTE_UNIX_MODE, newUnixMode); + this._file.set_attributes_async(info, + Gio.FileQueryInfoFlags.NONE, + GLib.PRIORITY_LOW, + null, + (source, res) => { + try { + source.set_attributes_finish (res); + } catch(e) { + log(`Failed to set unix mode: ${e.message}`); + } + }); + this._file.set_attributes_from_info(info, + Gio.FileQueryInfoFlags.NONE, + GLib.PRIORITY_LOW, + null); + } + } + + canRename() { + return !this.trustedDesktopFile && this._fileExtra == Prefs.FileType.NONE; + } + + _createMenu() { + this._menuManager = new PopupMenu.PopupMenuManager({ actor: this.actor }); + let side = St.Side.LEFT; + if (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL) + side = St.Side.RIGHT; + this._menu = new PopupMenu.PopupMenu(this.actor, 0.5, side); + this._menu.addAction(_('Open'), () => this.doOpen()); + switch (this._fileExtra) { + case Prefs.FileType.NONE: + this._actionCut = this._menu.addAction(_('Cut'), () => this._onCutClicked()); + this._actionCopy = this._menu.addAction(_('Copy'), () => this._onCopyClicked()); + if (this.canRename()) + this._menu.addAction(_('Rename…'), () => this.doRename()); + this._actionTrash = this._menu.addAction(_('Move to Trash'), () => this._onMoveToTrashClicked()); + if (this._isDesktopFile && !Extension.desktopManager.writableByOthers && !this._writableByOthers) { + this._menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + this._allowLaunchingMenuItem = this._menu.addAction(this._allowLaunchingText, + () => this._onAllowDisallowLaunchingClicked()); + + } + break; + case Prefs.FileType.USER_DIRECTORY_TRASH: + this._menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + this._menu.addAction(_('Empty Trash'), () => this._onEmptyTrashClicked()); + break; + default: + break; + } + this._menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + this._menu.addAction(_('Properties'), () => this._onPropertiesClicked()); + this._menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + this._menu.addAction(_('Show in Files'), () => this._onShowInFilesClicked()); + if (this._isDirectory && this.file.get_path() != null) + this._actionOpenInTerminal = this._menu.addAction(_('Open in Terminal'), () => this._onOpenTerminalClicked()); + + this._menuManager.addMenu(this._menu); + + Main.layoutManager.uiGroup.add_child(this._menu.actor); + this._menu.actor.hide(); + } + + _onOpenTerminalClicked () { + let command = DesktopIconsUtil.getTerminalCommand(this.file.get_path()); + Util.spawnCommandLine(command); + } + + _onPressButton(actor, event) { + let button = event.get_button(); + if (button == 3) { + if (!this.isSelected) + this.emit('selected', false, true); + this._menu.toggle(); + let specialFilesSelected = Extension.desktopManager.checkIfSpecialFilesAreSelected(); + if (this._actionCut) + this._actionCut.setSensitive(!specialFilesSelected); + if (this._actionCopy) + this._actionCopy.setSensitive(!specialFilesSelected); + if (this._actionTrash) + this._actionTrash.setSensitive(!specialFilesSelected); + return Clutter.EVENT_STOP; + } else if (button == 1) { + if (event.get_click_count() == 1) { + let [x, y] = event.get_coords(); + this._primaryButtonPressed = true; + this._buttonPressInitialX = x; + this._buttonPressInitialY = y; + let shiftPressed = !!(event.get_state() & Clutter.ModifierType.SHIFT_MASK); + let controlPressed = !!(event.get_state() & Clutter.ModifierType.CONTROL_MASK); + if (!this.isSelected) { + this.emit('selected', shiftPressed || controlPressed, true); + } + } + return Clutter.EVENT_STOP; + } + + return Clutter.EVENT_PROPAGATE; + } + + _onLeave(actor, event) { + this._primaryButtonPressed = false; + } + + _onMotion(actor, event) { + let [x, y] = event.get_coords(); + if (this._primaryButtonPressed) { + let xDiff = x - this._buttonPressInitialX; + let yDiff = y - this._buttonPressInitialY; + let distance = Math.sqrt(Math.pow(xDiff, 2) + Math.pow(yDiff, 2)); + if (distance > DRAG_TRESHOLD) { + // Don't need to track anymore this if we start drag, and also + // avoids reentrance here + this._primaryButtonPressed = false; + let event = Clutter.get_current_event(); + let [x, y] = event.get_coords(); + Extension.desktopManager.dragStart(); + } + } + + return Clutter.EVENT_PROPAGATE; + } + + _onReleaseButton(actor, event) { + let button = event.get_button(); + if (button == 1) { + // primaryButtonPressed is TRUE only if the user has pressed the button + // over an icon, and if (s)he has not started a drag&drop operation + if (this._primaryButtonPressed) { + this._primaryButtonPressed = false; + let shiftPressed = !!(event.get_state() & Clutter.ModifierType.SHIFT_MASK); + let controlPressed = !!(event.get_state() & Clutter.ModifierType.CONTROL_MASK); + if ((event.get_click_count() == 1) && Prefs.CLICK_POLICY_SINGLE && !shiftPressed && !controlPressed) + this.doOpen(); + this.emit('selected', shiftPressed || controlPressed, true); + return Clutter.EVENT_STOP; + } + if ((event.get_click_count() == 2) && (!Prefs.CLICK_POLICY_SINGLE)) + this.doOpen(); + } + return Clutter.EVENT_PROPAGATE; + } + + get savedCoordinates() { + return this._savedCoordinates; + } + + _onSetMetadataFileFinished(source, result) { + try { + let [success, info] = source.set_attributes_finish(result); + } catch (error) { + if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + log('Error setting metadata to desktop files ', error); + } + } + + set savedCoordinates(pos) { + if (this._setMetadataCancellable) + this._setMetadataCancellable.cancel(); + + this._setMetadataCancellable = new Gio.Cancellable(); + this._savedCoordinates = [pos[0], pos[1]]; + let info = new Gio.FileInfo(); + info.set_attribute_string('metadata::nautilus-icon-position', + `${pos[0]},${pos[1]}`); + this.file.set_attributes_async(info, + Gio.FileQueryInfoFlags.NONE, + GLib.PRIORITY_DEFAULT, + this._setMetadataCancellable, + (source, result) => this._onSetMetadataFileFinished(source, result) + ); + } + + intersectsWith(argX, argY, argWidth, argHeight) { + let rect = new Meta.Rectangle({ x: argX, y: argY, width: argWidth, height: argHeight }); + let [containerX, containerY] = this._container.get_transformed_position(); + let boundingBox = new Meta.Rectangle({ x: containerX, + y: containerY, + width: this._container.allocation.x2 - this._container.allocation.x1, + height: this._container.allocation.y2 - this._container.allocation.y1 }); + let [intersects, _] = rect.intersect(boundingBox); + + return intersects; + } + + set isSelected(isSelected) { + isSelected = !!isSelected; + if (isSelected == this._isSelected) + return; + + if (isSelected) + this._container.add_style_pseudo_class('selected'); + else + this._container.remove_style_pseudo_class('selected'); + + this._isSelected = isSelected; + } + + get isSelected() { + return this._isSelected; + } + + get isSpecial() { + return this._isSpecial; + } + + get state() { + return this._state; + } + + set state(state) { + if (state == this._state) + return; + + this._state = state; + } + + get isDirectory() { + return this._isDirectory; + } + + get trustedDesktopFile() { + return this._isDesktopFile && + this._attributeCanExecute && + this.metadataTrusted && + !Extension.desktopManager.writableByOthers && + !this._writableByOthers; + } + + get displayName() { + if (this.trustedDesktopFile) + return this._desktopFile.get_name(); + + return this._displayName || null; + } + + acceptDrop() { + return Extension.desktopManager.selectionDropOnFileItem(this); + } +}; +Signals.addSignalMethods(FileItem.prototype); diff --git a/extensions/desktop-icons/meson.build b/extensions/desktop-icons/meson.build new file mode 100644 index 0000000..4e03db1 --- /dev/null +++ b/extensions/desktop-icons/meson.build @@ -0,0 +1,18 @@ +extension_data += configure_file( + input: metadata_name + '.in', + output: metadata_name, + configuration: metadata_conf +) + +extension_schemas += files(join_paths('schemas', metadata_conf.get('gschemaname') + '.gschema.xml')) + +extension_sources += files( + 'createThumbnail.js', + 'dbusUtils.js', + 'desktopGrid.js', + 'desktopIconsUtil.js', + 'desktopManager.js', + 'extension.js', + 'fileItem.js', + 'prefs.js' +) diff --git a/extensions/desktop-icons/metadata.json.in b/extensions/desktop-icons/metadata.json.in new file mode 100644 index 0000000..78cabf0 --- /dev/null +++ b/extensions/desktop-icons/metadata.json.in @@ -0,0 +1,11 @@ +{ +"extension-id": "@extension_id@", +"uuid": "@uuid@", +"settings-schema": "@gschemaname@", +"gettext-domain": "@gettext_domain@", +"name": "Desktop Icons", +"description": "Provide desktop icons support for classic mode", +"original-authors": [ "csoriano@redhat.com" ], +"shell-version": [ "@shell_current@" ], +"url": "@url@" +} diff --git a/extensions/desktop-icons/po/LINGUAS b/extensions/desktop-icons/po/LINGUAS new file mode 100644 index 0000000..bc8bc5d --- /dev/null +++ b/extensions/desktop-icons/po/LINGUAS @@ -0,0 +1,12 @@ +cs +da +de +es +fi +fr +id +it +pl +pt_BR +ru +zh_TW diff --git a/extensions/desktop-icons/po/POTFILES.in b/extensions/desktop-icons/po/POTFILES.in new file mode 100644 index 0000000..7c2ebd3 --- /dev/null +++ b/extensions/desktop-icons/po/POTFILES.in @@ -0,0 +1,4 @@ +prefs.js +desktopGrid.js +fileItem.js +schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml \ No newline at end of file diff --git a/extensions/desktop-icons/po/cs.po b/extensions/desktop-icons/po/cs.po new file mode 100644 index 0000000..1da4122 --- /dev/null +++ b/extensions/desktop-icons/po/cs.po @@ -0,0 +1,136 @@ +# Czech translation for desktop-icons. +# Copyright (C) 2018 desktop-icons's COPYRIGHT HOLDER +# This file is distributed under the same license as the desktop-icons package. +# Marek Černocký , 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-10-01 20:15+0000\n" +"PO-Revision-Date: 2018-10-02 11:10+0200\n" +"Last-Translator: Marek Černocký \n" +"Language-Team: čeština \n" +"Language: cs\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" +"X-Generator: Gtranslator 2.91.7\n" + +#: prefs.js:89 +msgid "Size for the desktop icons" +msgstr "Velikost ikon na pracovní ploše" + +#: prefs.js:89 +msgid "Small" +msgstr "malé" + +#: prefs.js:89 +msgid "Standard" +msgstr "standardní" + +#: prefs.js:89 +msgid "Large" +msgstr "velké" + +#: prefs.js:89 +msgid "Huge" +msgstr "obrovské" + +#: prefs.js:90 +msgid "Show the personal folder in the desktop" +msgstr "Zobrazovat osobní složku na pracovní ploše" + +#: prefs.js:91 +msgid "Show the trash icon in the desktop" +msgstr "Zobrazovat ikonu koše na pracovní ploše" + +#: desktopGrid.js:178 desktopGrid.js:297 +msgid "New Folder" +msgstr "Nová složka" + +#: desktopGrid.js:299 +msgid "Paste" +msgstr "Vložit" + +#: desktopGrid.js:300 +msgid "Undo" +msgstr "Zpět" + +#: desktopGrid.js:301 +msgid "Redo" +msgstr "Znovu" + +#: desktopGrid.js:303 +msgid "Open Desktop in Files" +msgstr "Otevřít plochu v Souborech" + +#: desktopGrid.js:304 +msgid "Open Terminal" +msgstr "Otevřít terminál" + +#: desktopGrid.js:306 +msgid "Change Background…" +msgstr "Změnit pozadí…" + +#: desktopGrid.js:307 +msgid "Display Settings" +msgstr "Zobrazit nastavení" + +#: desktopGrid.js:308 +msgid "Settings" +msgstr "Nastavení" + +#: fileItem.js:226 +msgid "Open" +msgstr "Otevřít" + +#: fileItem.js:229 +msgid "Cut" +msgstr "Vyjmout" + +#: fileItem.js:230 +msgid "Copy" +msgstr "Kopírovat" + +#: fileItem.js:231 +msgid "Move to Trash" +msgstr "Přesunout do koše" + +#: fileItem.js:235 +msgid "Empty trash" +msgstr "Vyprázdnit koš" + +#: fileItem.js:241 +msgid "Properties" +msgstr "Vlastnosti" + +#: fileItem.js:243 +msgid "Show in Files" +msgstr "Zobrazit v Souborech" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:12 +msgid "Icon size" +msgstr "Velikost ikon" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:13 +msgid "Set the size for the desktop icons." +msgstr "Nastavit velikost pro ikony na pracovní ploše." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:17 +msgid "Show personal folder" +msgstr "Zobrazovat osobní složku" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:18 +msgid "Show the personal folder in the desktop." +msgstr "Zobrazovat osobní složku na pracovní ploše." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:22 +msgid "Show trash icon" +msgstr "Zobrazovat koš" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:23 +msgid "Show the trash icon in the desktop." +msgstr "Zobrazovat ikonu koše na pracovní ploše." diff --git a/extensions/desktop-icons/po/da.po b/extensions/desktop-icons/po/da.po new file mode 100644 index 0000000..98f2617 --- /dev/null +++ b/extensions/desktop-icons/po/da.po @@ -0,0 +1,136 @@ +# Danish translation for desktop-icons. +# Copyright (C) 2018 desktop-icons's COPYRIGHT HOLDER +# This file is distributed under the same license as the desktop-icons package. +# Alan Mortensen , 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-10-26 12:04+0000\n" +"PO-Revision-Date: 2018-10-27 16:42+0200\n" +"Language-Team: Danish \n" +"Language: da\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Last-Translator: Alan Mortensen \n" +"X-Generator: Poedit 2.0.6\n" + +#: prefs.js:89 +msgid "Size for the desktop icons" +msgstr "Størrelsen på skrivebordsikoner" + +#: prefs.js:89 +msgid "Small" +msgstr "Små" + +#: prefs.js:89 +msgid "Standard" +msgstr "Standard" + +#: prefs.js:89 +msgid "Large" +msgstr "Store" + +#: prefs.js:89 +msgid "Huge" +msgstr "Enorme" + +#: prefs.js:90 +msgid "Show the personal folder in the desktop" +msgstr "Vis den personlige mappe på skrivebordet" + +#: prefs.js:91 +msgid "Show the trash icon in the desktop" +msgstr "Vis papirkurvsikonet på skrivebordet" + +#: desktopGrid.js:178 desktopGrid.js:297 +msgid "New Folder" +msgstr "Ny mappe" + +#: desktopGrid.js:299 +msgid "Paste" +msgstr "Indsæt" + +#: desktopGrid.js:300 +msgid "Undo" +msgstr "Fortryd" + +#: desktopGrid.js:301 +msgid "Redo" +msgstr "Omgør" + +#: desktopGrid.js:303 +msgid "Open Desktop in Files" +msgstr "Åbn skrivebordet i Filer" + +#: desktopGrid.js:304 +msgid "Open Terminal" +msgstr "Åbn Terminal" + +#: desktopGrid.js:306 +msgid "Change Background…" +msgstr "Skift baggrund …" + +#: desktopGrid.js:307 +msgid "Display Settings" +msgstr "Skærmindstillinger" + +#: desktopGrid.js:308 +msgid "Settings" +msgstr "Indstillinger" + +#: fileItem.js:385 +msgid "Open" +msgstr "Åbn" + +#: fileItem.js:388 +msgid "Cut" +msgstr "Klip" + +#: fileItem.js:389 +msgid "Copy" +msgstr "Kopiér" + +#: fileItem.js:390 +msgid "Move to Trash" +msgstr "Flyt til papirkurven" + +#: fileItem.js:394 +msgid "Empty trash" +msgstr "Tøm papirkurven" + +#: fileItem.js:400 +msgid "Properties" +msgstr "Egenskaber" + +#: fileItem.js:402 +msgid "Show in Files" +msgstr "Vis i Filer" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:12 +msgid "Icon size" +msgstr "Ikonstørrelse" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:13 +msgid "Set the size for the desktop icons." +msgstr "Angiv størrelsen på skrivebordsikoner." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:17 +msgid "Show personal folder" +msgstr "Vis personlig mappe" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:18 +msgid "Show the personal folder in the desktop." +msgstr "Vis den personlige mappe på skrivebordet." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:22 +msgid "Show trash icon" +msgstr "Vis papirkurvsikon" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:23 +msgid "Show the trash icon in the desktop." +msgstr "Vis papirkurvsikonet på skrivebordet." diff --git a/extensions/desktop-icons/po/de.po b/extensions/desktop-icons/po/de.po new file mode 100644 index 0000000..e2b0fb3 --- /dev/null +++ b/extensions/desktop-icons/po/de.po @@ -0,0 +1,136 @@ +# German translation for desktop-icons. +# Copyright (C) 2018 desktop-icons's COPYRIGHT HOLDER +# This file is distributed under the same license as the desktop-icons package. +# Mario Blättermann , 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-10-02 21:39+0000\n" +"PO-Revision-Date: 2018-10-03 21:28+0200\n" +"Last-Translator: Mario Blättermann \n" +"Language-Team: German \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 2.1.1\n" + +#: prefs.js:89 +msgid "Size for the desktop icons" +msgstr "Größe der Arbeitsflächensymbole" + +#: prefs.js:89 +msgid "Small" +msgstr "Klein" + +#: prefs.js:89 +msgid "Standard" +msgstr "Standard" + +#: prefs.js:89 +msgid "Large" +msgstr "Groß" + +#: prefs.js:89 +msgid "Huge" +msgstr "Riesig" + +#: prefs.js:90 +msgid "Show the personal folder in the desktop" +msgstr "Den persönlichen Ordner auf der Arbeitsfläche anzeigen" + +#: prefs.js:91 +msgid "Show the trash icon in the desktop" +msgstr "Papierkorb-Symbol auf der Arbeitsfläche anzeigen" + +#: desktopGrid.js:178 desktopGrid.js:297 +msgid "New Folder" +msgstr "Neuer Ordner" + +#: desktopGrid.js:299 +msgid "Paste" +msgstr "Einfügen" + +#: desktopGrid.js:300 +msgid "Undo" +msgstr "Rückgängig" + +#: desktopGrid.js:301 +msgid "Redo" +msgstr "Wiederholen" + +#: desktopGrid.js:303 +msgid "Open Desktop in Files" +msgstr "Arbeitsfläche in Dateiverwaltung öffnen" + +#: desktopGrid.js:304 +msgid "Open Terminal" +msgstr "Terminal öffnen" + +#: desktopGrid.js:306 +msgid "Change Background…" +msgstr "Hintergrund ändern …" + +#: desktopGrid.js:307 +msgid "Display Settings" +msgstr "Anzeigeeinstellungen" + +#: desktopGrid.js:308 +msgid "Settings" +msgstr "Einstellungen" + +#: fileItem.js:226 +msgid "Open" +msgstr "Öffnen" + +#: fileItem.js:229 +msgid "Cut" +msgstr "Ausschneiden" + +#: fileItem.js:230 +msgid "Copy" +msgstr "Kopieren" + +#: fileItem.js:231 +msgid "Move to Trash" +msgstr "In den Papierkorb verschieben" + +#: fileItem.js:235 +msgid "Empty trash" +msgstr "Papierkorb leeren" + +#: fileItem.js:241 +msgid "Properties" +msgstr "Eigenschaften" + +#: fileItem.js:243 +msgid "Show in Files" +msgstr "In Dateiverwaltung anzeigen" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:12 +msgid "Icon size" +msgstr "Symbolgröße" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:13 +msgid "Set the size for the desktop icons." +msgstr "Die Größe der Arbeitsflächensymbole festlegen." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:17 +msgid "Show personal folder" +msgstr "Persönlichen Ordner anzeigen" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:18 +msgid "Show the personal folder in the desktop." +msgstr "Den persönlichen Ordner auf der Arbeitsfläche anzeigen." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:22 +msgid "Show trash icon" +msgstr "Papierkorb-Symbol anzeigen" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:23 +msgid "Show the trash icon in the desktop." +msgstr "Das Papierkorb-Symbol auf der Arbeitsfläche anzeigen." diff --git a/extensions/desktop-icons/po/es.po b/extensions/desktop-icons/po/es.po new file mode 100644 index 0000000..05b47c2 --- /dev/null +++ b/extensions/desktop-icons/po/es.po @@ -0,0 +1,186 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# Sergio Costas , 2018. +# Daniel Mustieles , 2018, 2019. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-12-14 09:12+0000\n" +"PO-Revision-Date: 2019-01-08 16:12+0100\n" +"Last-Translator: Daniel Mustieles \n" +"Language-Team: es \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Gtranslator 2.91.7\n" + +#: prefs.js:102 +msgid "Size for the desktop icons" +msgstr "Tamaño de los iconos del escritorio" + +#: prefs.js:102 +msgid "Small" +msgstr "Pequeño" + +#: prefs.js:102 +msgid "Standard" +msgstr "Estándar" + +#: prefs.js:102 +msgid "Large" +msgstr "Grande" + +#: prefs.js:102 +msgid "Huge" +msgstr "Inmenso" + +#: prefs.js:103 +msgid "Show the personal folder in the desktop" +msgstr "Mostrar la carpeta personal en el escritorio" + +#: prefs.js:104 +msgid "Show the trash icon in the desktop" +msgstr "Mostrar la papelera en el escritorio" + +#: desktopGrid.js:182 desktopGrid.js:301 +msgid "New Folder" +msgstr "Nueva carpeta" + +#: desktopGrid.js:303 +msgid "Paste" +msgstr "Pegar" + +#: desktopGrid.js:304 +msgid "Undo" +msgstr "Deshacer" + +#: desktopGrid.js:305 +msgid "Redo" +msgstr "Rehacer" + +#: desktopGrid.js:307 +msgid "Open Desktop in Files" +msgstr "Abrir el escritorio en Files" + +#: desktopGrid.js:308 +msgid "Open Terminal" +msgstr "Abrir un terminal" + +#: desktopGrid.js:310 +msgid "Change Background…" +msgstr "Cambiar el fondo..." + +#: desktopGrid.js:311 +msgid "Display Settings" +msgstr "Configuración de pantalla" + +#: desktopGrid.js:312 +msgid "Settings" +msgstr "Configuración" + +#: desktopGrid.js:568 +msgid "Enter file name…" +msgstr "Introduzca el nombre del archivo…" + +#: desktopGrid.js:572 +msgid "OK" +msgstr "Aceptar" + +#: desktopGrid.js:578 +msgid "Cancel" +msgstr "Cancelar" + +#: fileItem.js:485 +msgid "Don’t Allow Launching" +msgstr "No permitir lanzar" + +#: fileItem.js:487 +msgid "Allow Launching" +msgstr "Permitir lanzar" + +#: fileItem.js:550 +msgid "Open" +msgstr "Abrir" + +#: fileItem.js:553 +msgid "Cut" +msgstr "Cortar" + +#: fileItem.js:554 +msgid "Copy" +msgstr "Copiar" + +#: fileItem.js:556 +msgid "Rename" +msgstr "Renombrar" + +#: fileItem.js:557 +msgid "Move to Trash" +msgstr "Mover a la papelera" + +#: fileItem.js:567 +msgid "Empty Trash" +msgstr "Vaciar la papelera" + +#: fileItem.js:573 +msgid "Properties" +msgstr "Propiedades" + +#: fileItem.js:575 +msgid "Show in Files" +msgstr "Mostrar en Files" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:12 +msgid "Icon size" +msgstr "Tamaño de los iconos" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:13 +msgid "Set the size for the desktop icons." +msgstr "Establece el tamaño de los iconos del escritorio." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:17 +msgid "Show personal folder" +msgstr "Mostrar la carpeta personal" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:18 +msgid "Show the personal folder in the desktop." +msgstr "Mostrar la carpeta personal en el escritorio." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:22 +msgid "Show trash icon" +msgstr "Mostrar la papelera" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:23 +msgid "Show the trash icon in the desktop." +msgstr "Mostrar la papelera en el escritorio." + +#~ msgid "Ok" +#~ msgstr "Aceptar" + +#~ msgid "huge" +#~ msgstr "inmenso" + +#~ msgid "Maximum width for the icons and filename." +#~ msgstr "Ancho máximo de los iconos y el nombre de fichero." + +#~ msgid "Shows the Documents folder in the desktop." +#~ msgstr "Muestra la carpeta Documentos en el escritorio." + +#~ msgid "Shows the Downloads folder in the desktop." +#~ msgstr "Muestra la carpeta Descargas en el escritorio." + +#~ msgid "Shows the Music folder in the desktop." +#~ msgstr "Muestra la carpeta Música en el escritorio." + +#~ msgid "Shows the Pictures folder in the desktop." +#~ msgstr "Muestra la carpeta Imágenes en el escritorio." + +#~ msgid "Shows the Videos folder in the desktop." +#~ msgstr "Muestra la carpeta Vídeos en el escritorio." diff --git a/extensions/desktop-icons/po/fi.po b/extensions/desktop-icons/po/fi.po new file mode 100644 index 0000000..216e5c1 --- /dev/null +++ b/extensions/desktop-icons/po/fi.po @@ -0,0 +1,136 @@ +# Finnish translation for desktop-icons. +# Copyright (C) 2018 desktop-icons's COPYRIGHT HOLDER +# This file is distributed under the same license as the desktop-icons package. +# Jiri Grönroos , 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-10-14 08:09+0000\n" +"PO-Revision-Date: 2018-10-14 12:44+0300\n" +"Language-Team: Finnish \n" +"Language: fi\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Last-Translator: Jiri Grönroos \n" +"X-Generator: Poedit 2.0.6\n" + +#: prefs.js:89 +msgid "Size for the desktop icons" +msgstr "Työpöytäkuvakkeiden koko" + +#: prefs.js:89 +msgid "Small" +msgstr "Pieni" + +#: prefs.js:89 +msgid "Standard" +msgstr "Normaali" + +#: prefs.js:89 +msgid "Large" +msgstr "Suuri" + +#: prefs.js:89 +msgid "Huge" +msgstr "Valtava" + +#: prefs.js:90 +msgid "Show the personal folder in the desktop" +msgstr "Näytä kotikansio työpöydällä" + +#: prefs.js:91 +msgid "Show the trash icon in the desktop" +msgstr "Näytä roskakorin kuvake työpöydällä" + +#: desktopGrid.js:178 desktopGrid.js:297 +msgid "New Folder" +msgstr "Uusi kansio" + +#: desktopGrid.js:299 +msgid "Paste" +msgstr "Liitä" + +#: desktopGrid.js:300 +msgid "Undo" +msgstr "Kumoa" + +#: desktopGrid.js:301 +msgid "Redo" +msgstr "Tee uudeleen" + +#: desktopGrid.js:303 +msgid "Open Desktop in Files" +msgstr "Avaa työpöytä tiedostonhallinnassa" + +#: desktopGrid.js:304 +msgid "Open Terminal" +msgstr "Avaa pääte" + +#: desktopGrid.js:306 +msgid "Change Background…" +msgstr "Vaihda taustakuvaa…" + +#: desktopGrid.js:307 +msgid "Display Settings" +msgstr "Näytön asetukset" + +#: desktopGrid.js:308 +msgid "Settings" +msgstr "Asetukset" + +#: fileItem.js:211 +msgid "Open" +msgstr "Avaa" + +#: fileItem.js:214 +msgid "Cut" +msgstr "Leikkaa" + +#: fileItem.js:215 +msgid "Copy" +msgstr "Kopioi" + +#: fileItem.js:216 +msgid "Move to Trash" +msgstr "Siirrä roskakoriin" + +#: fileItem.js:220 +msgid "Empty trash" +msgstr "Tyhjennä roskakori" + +#: fileItem.js:226 +msgid "Properties" +msgstr "Ominaisuudet" + +#: fileItem.js:228 +msgid "Show in Files" +msgstr "Näytä tiedostonhallinnassa" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:12 +msgid "Icon size" +msgstr "Kuvakekoko" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:13 +msgid "Set the size for the desktop icons." +msgstr "Aseta työpöytäkuvakkeiden koko." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:17 +msgid "Show personal folder" +msgstr "Näytä kotikansio" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:18 +msgid "Show the personal folder in the desktop." +msgstr "Näytä kotikansio työpöydällä." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:22 +msgid "Show trash icon" +msgstr "Näytä roskakorin kuvake" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:23 +msgid "Show the trash icon in the desktop." +msgstr "Näytä roskakorin kuvake työpöydällä." diff --git a/extensions/desktop-icons/po/fr.po b/extensions/desktop-icons/po/fr.po new file mode 100644 index 0000000..13e8f3a --- /dev/null +++ b/extensions/desktop-icons/po/fr.po @@ -0,0 +1,164 @@ +# French translation for desktop-icons. +# Copyright (C) 2018 desktop-icons's COPYRIGHT HOLDER +# This file is distributed under the same license as the desktop-icons package. +# ghentdebian , 2018. +# Charles Monzat , 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-12-14 09:12+0000\n" +"PO-Revision-Date: 2018-12-16 17:47+0100\n" +"Last-Translator: Charles Monzat \n" +"Language-Team: GNOME French Team \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1)\n" +"X-Generator: Gtranslator 3.30.0\n" + +#: prefs.js:102 +msgid "Size for the desktop icons" +msgstr "Taille des icônes du bureau" + +#: prefs.js:102 +msgid "Small" +msgstr "Petite" + +#: prefs.js:102 +msgid "Standard" +msgstr "Normale" + +#: prefs.js:102 +msgid "Large" +msgstr "Grande" + +#: prefs.js:102 +msgid "Huge" +msgstr "Immense" + +#: prefs.js:103 +msgid "Show the personal folder in the desktop" +msgstr "Montrer le dossier personnel sur le bureau" + +#: prefs.js:104 +msgid "Show the trash icon in the desktop" +msgstr "Montrer la corbeille sur le bureau" + +#: desktopGrid.js:182 desktopGrid.js:301 +msgid "New Folder" +msgstr "Nouveau dossier" + +#: desktopGrid.js:303 +msgid "Paste" +msgstr "Coller" + +#: desktopGrid.js:304 +msgid "Undo" +msgstr "Annuler" + +#: desktopGrid.js:305 +msgid "Redo" +msgstr "Refaire" + +#: desktopGrid.js:307 +msgid "Open Desktop in Files" +msgstr "Ouvrir le bureau dans Fichiers" + +#: desktopGrid.js:308 +msgid "Open Terminal" +msgstr "Ouvrir un terminal" + +#: desktopGrid.js:310 +msgid "Change Background…" +msgstr "Changer l’arrière-plan…" + +#: desktopGrid.js:311 +msgid "Display Settings" +msgstr "Configuration d’affichage" + +#: desktopGrid.js:312 +msgid "Settings" +msgstr "Paramètres" + +#: desktopGrid.js:568 +msgid "Enter file name…" +msgstr "Saisir un nom de fichier…" + +#: desktopGrid.js:572 +msgid "OK" +msgstr "Valider" + +#: desktopGrid.js:578 +msgid "Cancel" +msgstr "Annuler" + +#: fileItem.js:485 +msgid "Don’t Allow Launching" +msgstr "Ne pas autoriser le lancement" + +#: fileItem.js:487 +msgid "Allow Launching" +msgstr "Autoriser le lancement" + +#: fileItem.js:550 +msgid "Open" +msgstr "Ouvrir" + +#: fileItem.js:553 +msgid "Cut" +msgstr "Couper" + +#: fileItem.js:554 +msgid "Copy" +msgstr "Copier" + +#: fileItem.js:556 +msgid "Rename" +msgstr "Renommer" + +#: fileItem.js:557 +msgid "Move to Trash" +msgstr "Mettre à la corbeille" + +#: fileItem.js:567 +msgid "Empty Trash" +msgstr "Vider la corbeille" + +#: fileItem.js:573 +msgid "Properties" +msgstr "Propriétés" + +#: fileItem.js:575 +msgid "Show in Files" +msgstr "Montrer dans Fichiers" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:12 +msgid "Icon size" +msgstr "Taille d’icônes" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:13 +msgid "Set the size for the desktop icons." +msgstr "Définir la taille des icônes du bureau." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:17 +msgid "Show personal folder" +msgstr "Montrer le dossier personnel" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:18 +msgid "Show the personal folder in the desktop." +msgstr "Montrer le dossier personnel sur le bureau." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:22 +msgid "Show trash icon" +msgstr "Montrer l’icône de la corbeille" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:23 +msgid "Show the trash icon in the desktop." +msgstr "Montrer la corbeille sur le bureau." + +#~ msgid "Ok" +#~ msgstr "Valider" diff --git a/extensions/desktop-icons/po/id.po b/extensions/desktop-icons/po/id.po new file mode 100644 index 0000000..c145d1a --- /dev/null +++ b/extensions/desktop-icons/po/id.po @@ -0,0 +1,135 @@ +# Indonesian translation for desktop-icons. +# Copyright (C) 2018 desktop-icons's COPYRIGHT HOLDER +# This file is distributed under the same license as the desktop-icons package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-10-02 09:18+0000\n" +"PO-Revision-Date: 2018-10-02 20:30+0700\n" +"Language-Team: Indonesian \n" +"Language: id\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Last-Translator: Kukuh Syafaat \n" +"X-Generator: Poedit 2.0.6\n" + +#: prefs.js:89 +msgid "Size for the desktop icons" +msgstr "Ukuran untuk ikon destop" + +#: prefs.js:89 +msgid "Small" +msgstr "Kecil" + +#: prefs.js:89 +msgid "Standard" +msgstr "Standar" + +#: prefs.js:89 +msgid "Large" +msgstr "Besar" + +#: prefs.js:89 +msgid "Huge" +msgstr "Sangat besar" + +#: prefs.js:90 +msgid "Show the personal folder in the desktop" +msgstr "Tampilkan folder pribadi di destop" + +#: prefs.js:91 +msgid "Show the trash icon in the desktop" +msgstr "Tampilkan ikon tong sampah di destop" + +#: desktopGrid.js:178 desktopGrid.js:297 +msgid "New Folder" +msgstr "Folder Baru" + +#: desktopGrid.js:299 +msgid "Paste" +msgstr "Tempel" + +#: desktopGrid.js:300 +msgid "Undo" +msgstr "Tak Jadi" + +#: desktopGrid.js:301 +msgid "Redo" +msgstr "Jadi Lagi" + +#: desktopGrid.js:303 +msgid "Open Desktop in Files" +msgstr "Buka Destop pada Berkas" + +#: desktopGrid.js:304 +msgid "Open Terminal" +msgstr "Buka Terminal" + +#: desktopGrid.js:306 +msgid "Change Background…" +msgstr "Ubah Latar Belakang…" + +#: desktopGrid.js:307 +msgid "Display Settings" +msgstr "Pengaturan Tampilan" + +#: desktopGrid.js:308 +msgid "Settings" +msgstr "Pengaturan" + +#: fileItem.js:226 +msgid "Open" +msgstr "Buka" + +#: fileItem.js:229 +msgid "Cut" +msgstr "Potong" + +#: fileItem.js:230 +msgid "Copy" +msgstr "Salin" + +#: fileItem.js:231 +msgid "Move to Trash" +msgstr "Pindahkan ke Tong Sampah" + +#: fileItem.js:235 +msgid "Empty trash" +msgstr "Kosongkan Tong Sampah" + +#: fileItem.js:241 +msgid "Properties" +msgstr "Properti" + +#: fileItem.js:243 +msgid "Show in Files" +msgstr "Tampilkan pada Berkas" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:12 +msgid "Icon size" +msgstr "Ukuran ikon" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:13 +msgid "Set the size for the desktop icons." +msgstr "Set ukuran untuk ikon destop." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:17 +msgid "Show personal folder" +msgstr "Tampilkan folder pribadi" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:18 +msgid "Show the personal folder in the desktop." +msgstr "Tampilkan folder pribadi di destop." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:22 +msgid "Show trash icon" +msgstr "Tampilkan ikon tong sampah" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:23 +msgid "Show the trash icon in the desktop." +msgstr "Tampilkan ikon tong sampah di destop." diff --git a/extensions/desktop-icons/po/it.po b/extensions/desktop-icons/po/it.po new file mode 100644 index 0000000..38c0572 --- /dev/null +++ b/extensions/desktop-icons/po/it.po @@ -0,0 +1,152 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Massimo Branchini , 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-11-29 09:07+0000\n" +"PO-Revision-Date: 2018-12-05 11:14+0100\n" +"Last-Translator: Massimo Branchini \n" +"Language-Team: Italian \n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 2.2\n" + +#: prefs.js:93 +msgid "Size for the desktop icons" +msgstr "Dimensione delle icone della scrivania" + +#: prefs.js:93 +msgid "Small" +msgstr "Piccola" + +#: prefs.js:93 +msgid "Standard" +msgstr "Normale" + +#: prefs.js:93 +msgid "Large" +msgstr "Grande" + +#: prefs.js:93 +msgid "Huge" +msgstr "Enorme" + +#: prefs.js:94 +msgid "Show the personal folder in the desktop" +msgstr "Mostra la cartella personale sulla scrivania" + +#: prefs.js:95 +msgid "Show the trash icon in the desktop" +msgstr "Mostra il cestino sulla scrivania" + +#: desktopGrid.js:185 desktopGrid.js:304 +msgid "New Folder" +msgstr "Nuova cartella" + +#: desktopGrid.js:306 +msgid "Paste" +msgstr "Incolla" + +#: desktopGrid.js:307 +msgid "Undo" +msgstr "Annulla" + +#: desktopGrid.js:308 +msgid "Redo" +msgstr "Ripeti" + +#: desktopGrid.js:310 +msgid "Open Desktop in Files" +msgstr "Apri la scrivania in File" + +#: desktopGrid.js:311 +msgid "Open Terminal" +msgstr "Apri un terminale" + +#: desktopGrid.js:313 +msgid "Change Background…" +msgstr "Cambia lo sfondo…" + +#: desktopGrid.js:314 +msgid "Display Settings" +msgstr "Impostazioni dello schermo" + +#: desktopGrid.js:315 +msgid "Settings" +msgstr "Impostazioni" + +#: desktopGrid.js:569 +msgid "Enter file name…" +msgstr "Indicare un nome per il file…" + +#: desktopGrid.js:573 +msgid "Ok" +msgstr "Ok" + +#: desktopGrid.js:579 +msgid "Cancel" +msgstr "Annulla" + +#: fileItem.js:393 +msgid "Open" +msgstr "Apri" + +#: fileItem.js:396 +msgid "Cut" +msgstr "Taglia" + +#: fileItem.js:397 +msgid "Copy" +msgstr "Copia" + +#: fileItem.js:398 +msgid "Rename" +msgstr "Rinomina" + +#: fileItem.js:399 +msgid "Move to Trash" +msgstr "Sposta nel cestino" + +#: fileItem.js:403 +msgid "Empty Trash" +msgstr "Svuota il cestino" + +#: fileItem.js:409 +msgid "Properties" +msgstr "Proprietà" + +#: fileItem.js:411 +msgid "Show in Files" +msgstr "Mostra in File" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:12 +msgid "Icon size" +msgstr "Dimensione dell'icona" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:13 +msgid "Set the size for the desktop icons." +msgstr "Imposta la grandezza delle icone della scrivania." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:17 +msgid "Show personal folder" +msgstr "Mostra la cartella personale" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:18 +msgid "Show the personal folder in the desktop." +msgstr "Mostra la cartella personale sulla scrivania." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:22 +msgid "Show trash icon" +msgstr "Mostra il cestino" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:23 +msgid "Show the trash icon in the desktop." +msgstr "Mostra il cestino sulla scrivania." diff --git a/extensions/desktop-icons/po/meson.build b/extensions/desktop-icons/po/meson.build new file mode 100644 index 0000000..b2e9e42 --- /dev/null +++ b/extensions/desktop-icons/po/meson.build @@ -0,0 +1 @@ +i18n.gettext (meson.project_name (), preset: 'glib') diff --git a/extensions/desktop-icons/po/pl.po b/extensions/desktop-icons/po/pl.po new file mode 100644 index 0000000..32ae8ef --- /dev/null +++ b/extensions/desktop-icons/po/pl.po @@ -0,0 +1,157 @@ +# Polish translation for desktop-icons. +# Copyright © 2018-2019 the desktop-icons authors. +# This file is distributed under the same license as the desktop-icons package. +# Piotr Drąg , 2018-2019. +# Aviary.pl , 2018-2019. +# +msgid "" +msgstr "" +"Project-Id-Version: desktop-icons\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2019-01-15 10:59+0000\n" +"PO-Revision-Date: 2019-01-15 20:57+0100\n" +"Last-Translator: Piotr Drąg \n" +"Language-Team: Polish \n" +"Language: pl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " +"|| n%100>=20) ? 1 : 2);\n" + +#: prefs.js:102 +msgid "Size for the desktop icons" +msgstr "Rozmiar ikon na pulpicie" + +#: prefs.js:102 +msgid "Small" +msgstr "Mały" + +#: prefs.js:102 +msgid "Standard" +msgstr "Standardowy" + +#: prefs.js:102 +msgid "Large" +msgstr "Duży" + +#: prefs.js:103 +msgid "Show the personal folder in the desktop" +msgstr "Katalog domowy na pulpicie" + +#: prefs.js:104 +msgid "Show the trash icon in the desktop" +msgstr "Kosz na pulpicie" + +#: desktopGrid.js:187 desktopGrid.js:306 +msgid "New Folder" +msgstr "Nowy katalog" + +#: desktopGrid.js:308 +msgid "Paste" +msgstr "Wklej" + +#: desktopGrid.js:309 +msgid "Undo" +msgstr "Cofnij" + +#: desktopGrid.js:310 +msgid "Redo" +msgstr "Ponów" + +#: desktopGrid.js:312 +msgid "Show Desktop in Files" +msgstr "Wyświetl pulpit w menedżerze plików" + +#: desktopGrid.js:313 fileItem.js:586 +msgid "Open in Terminal" +msgstr "Otwórz w terminalu" + +#: desktopGrid.js:315 +msgid "Change Background…" +msgstr "Zmień tło…" + +#: desktopGrid.js:317 +msgid "Display Settings" +msgstr "Ustawienia ekranu" + +#: desktopGrid.js:318 +msgid "Settings" +msgstr "Ustawienia" + +#: desktopGrid.js:559 +msgid "Enter file name…" +msgstr "Nazwa pliku…" + +#: desktopGrid.js:563 +msgid "OK" +msgstr "OK" + +#: desktopGrid.js:569 +msgid "Cancel" +msgstr "Anuluj" + +#: fileItem.js:490 +msgid "Don’t Allow Launching" +msgstr "Nie zezwalaj na uruchamianie" + +#: fileItem.js:492 +msgid "Allow Launching" +msgstr "Zezwól na uruchamianie" + +#: fileItem.js:559 +msgid "Open" +msgstr "Otwórz" + +#: fileItem.js:562 +msgid "Cut" +msgstr "Wytnij" + +#: fileItem.js:563 +msgid "Copy" +msgstr "Skopiuj" + +#: fileItem.js:565 +msgid "Rename…" +msgstr "Zmień nazwę…" + +#: fileItem.js:566 +msgid "Move to Trash" +msgstr "Przenieś do kosza" + +#: fileItem.js:576 +msgid "Empty Trash" +msgstr "Opróżnij kosz" + +#: fileItem.js:582 +msgid "Properties" +msgstr "Właściwości" + +#: fileItem.js:584 +msgid "Show in Files" +msgstr "Wyświetl w menedżerze plików" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:11 +msgid "Icon size" +msgstr "Rozmiar ikon" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:12 +msgid "Set the size for the desktop icons." +msgstr "Ustawia rozmiar ikon na pulpicie." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:16 +msgid "Show personal folder" +msgstr "Katalog domowy" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:17 +msgid "Show the personal folder in the desktop." +msgstr "Wyświetla katalog domowy na pulpicie." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:21 +msgid "Show trash icon" +msgstr "Kosz" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:22 +msgid "Show the trash icon in the desktop." +msgstr "Wyświetla kosz na pulpicie." diff --git a/extensions/desktop-icons/po/pt_BR.po b/extensions/desktop-icons/po/pt_BR.po new file mode 100644 index 0000000..42162f2 --- /dev/null +++ b/extensions/desktop-icons/po/pt_BR.po @@ -0,0 +1,163 @@ +# Brazilian Portuguese translation for desktop-icons. +# Copyright (C) 2018 desktop-icons's COPYRIGHT HOLDER +# This file is distributed under the same license as the desktop-icons package. +# Enrico Nicoletto , 2018. +# Rafael Fontenelle , 2018. +msgid "" +msgstr "" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/" +"desktop-icons/issues\n" +"POT-Creation-Date: 2018-12-14 09:12+0000\n" +"PO-Revision-Date: 2018-12-17 01:01-0200\n" +"Last-Translator: Rafael Fontenelle \n" +"Language-Team: Brazilian Portuguese \n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Virtaal 1.0.0-beta1\n" + +#: prefs.js:102 +msgid "Size for the desktop icons" +msgstr "Tamanho para os ícones da área de trabalho" + +#: prefs.js:102 +msgid "Small" +msgstr "Pequeno" + +#: prefs.js:102 +msgid "Standard" +msgstr "Padrão" + +#: prefs.js:102 +msgid "Large" +msgstr "Grande" + +#: prefs.js:102 +msgid "Huge" +msgstr "Enorme" + +#: prefs.js:103 +msgid "Show the personal folder in the desktop" +msgstr "Mostrar a pasta pessoal na área de trabalho" + +#: prefs.js:104 +msgid "Show the trash icon in the desktop" +msgstr "Mostrar o ícone da lixeira na área de trabalho" + +#: desktopGrid.js:182 desktopGrid.js:301 +msgid "New Folder" +msgstr "Nova pasta" + +#: desktopGrid.js:303 +msgid "Paste" +msgstr "Colar" + +#: desktopGrid.js:304 +msgid "Undo" +msgstr "Desfazer" + +#: desktopGrid.js:305 +msgid "Redo" +msgstr "Refazer" + +#: desktopGrid.js:307 +msgid "Open Desktop in Files" +msgstr "Abrir área de trabalho no Arquivos" + +#: desktopGrid.js:308 +msgid "Open Terminal" +msgstr "Abrir terminal" + +#: desktopGrid.js:310 +msgid "Change Background…" +msgstr "Alterar plano de fundo…" + +#: desktopGrid.js:311 +msgid "Display Settings" +msgstr "Configurações de exibição" + +#: desktopGrid.js:312 +msgid "Settings" +msgstr "Configurações" + +#: desktopGrid.js:568 +msgid "Enter file name…" +msgstr "Insira um nome de arquivo…" + +#: desktopGrid.js:572 +msgid "OK" +msgstr "OK" + +#: desktopGrid.js:578 +msgid "Cancel" +msgstr "Cancelar" + +#: fileItem.js:485 +msgid "Don’t Allow Launching" +msgstr "Não permitir iniciar" + +#: fileItem.js:487 +msgid "Allow Launching" +msgstr "Permitir iniciar" + +#: fileItem.js:550 +msgid "Open" +msgstr "Abrir" + +#: fileItem.js:553 +msgid "Cut" +msgstr "Recortar" + +#: fileItem.js:554 +msgid "Copy" +msgstr "Copiar" + +#: fileItem.js:556 +msgid "Rename" +msgstr "Renomear" + +#: fileItem.js:557 +msgid "Move to Trash" +msgstr "Mover para a lixeira" + +#: fileItem.js:567 +msgid "Empty Trash" +msgstr "Esvaziar lixeira" + +#: fileItem.js:573 +msgid "Properties" +msgstr "Propriedades" + +#: fileItem.js:575 +msgid "Show in Files" +msgstr "Mostrar no Arquivos" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:12 +msgid "Icon size" +msgstr "Tamanho do ícone" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:13 +msgid "Set the size for the desktop icons." +msgstr "Define o tamanho para os ícones da área de trabalho." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:17 +msgid "Show personal folder" +msgstr "Mostrar pasta pessoal" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:18 +msgid "Show the personal folder in the desktop." +msgstr "Mostra a pasta pessoal na área de trabalho." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:22 +msgid "Show trash icon" +msgstr "Mostrar ícone da lixeira" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:23 +msgid "Show the trash icon in the desktop." +msgstr "Mostra o ícone da lixeira na área de trabalho." + +#~ msgid "Ok" +#~ msgstr "Ok" diff --git a/extensions/desktop-icons/po/ru.po b/extensions/desktop-icons/po/ru.po new file mode 100644 index 0000000..4094f16 --- /dev/null +++ b/extensions/desktop-icons/po/ru.po @@ -0,0 +1,153 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Eaglers , 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-11-22 08:42+0000\n" +"PO-Revision-Date: 2018-11-22 22:02+0300\n" +"Last-Translator: Stas Solovey \n" +"Language-Team: Russian \n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Generator: Poedit 2.2\n" + +#: prefs.js:89 +msgid "Size for the desktop icons" +msgstr "Размер значков" + +#: prefs.js:89 +msgid "Small" +msgstr "Маленький" + +#: prefs.js:89 +msgid "Standard" +msgstr "Стандартный" + +#: prefs.js:89 +msgid "Large" +msgstr "Большой" + +#: prefs.js:89 +msgid "Huge" +msgstr "Огромный" + +#: prefs.js:90 +msgid "Show the personal folder in the desktop" +msgstr "Показывать домашнюю папку на рабочем столе" + +#: prefs.js:91 +msgid "Show the trash icon in the desktop" +msgstr "Показывать «Корзину» на рабочем столе" + +#: desktopGrid.js:185 desktopGrid.js:304 +msgid "New Folder" +msgstr "Создать папку" + +#: desktopGrid.js:306 +msgid "Paste" +msgstr "Вставить" + +#: desktopGrid.js:307 +msgid "Undo" +msgstr "Отменить" + +#: desktopGrid.js:308 +msgid "Redo" +msgstr "Повторить" + +#: desktopGrid.js:310 +msgid "Open Desktop in Files" +msgstr "Открыть «Рабочий стол» в «Файлах»" + +#: desktopGrid.js:311 +msgid "Open Terminal" +msgstr "Открыть терминал" + +#: desktopGrid.js:313 +msgid "Change Background…" +msgstr "Изменить фон…" + +#: desktopGrid.js:314 +msgid "Display Settings" +msgstr "Настройки дисплея" + +#: desktopGrid.js:315 +msgid "Settings" +msgstr "Параметры" + +#: desktopGrid.js:569 +msgid "Enter file name…" +msgstr "Ввести имя файла…" + +#: desktopGrid.js:573 +msgid "Ok" +msgstr "ОК" + +#: desktopGrid.js:579 +msgid "Cancel" +msgstr "Отмена" + +#: fileItem.js:390 +msgid "Open" +msgstr "Открыть" + +#: fileItem.js:393 +msgid "Cut" +msgstr "Вырезать" + +#: fileItem.js:394 +msgid "Copy" +msgstr "Вставить" + +#: fileItem.js:395 +msgid "Rename" +msgstr "Переименовать" + +#: fileItem.js:396 +msgid "Move to Trash" +msgstr "Переместить в корзину" + +#: fileItem.js:400 +msgid "Empty trash" +msgstr "Очистить корзину" + +#: fileItem.js:406 +msgid "Properties" +msgstr "Свойства" + +#: fileItem.js:408 +msgid "Show in Files" +msgstr "Показать в «Файлах»" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:12 +msgid "Icon size" +msgstr "Размер значков" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:13 +msgid "Set the size for the desktop icons." +msgstr "Установить размер значков на рабочем столе." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:17 +msgid "Show personal folder" +msgstr "Показывать домашнюю папку" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:18 +msgid "Show the personal folder in the desktop." +msgstr "Показывать значок домашней папки на рабочем столе." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:22 +msgid "Show trash icon" +msgstr "Показывать значок корзины" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:23 +msgid "Show the trash icon in the desktop." +msgstr "Показывать значок корзины на рабочем столе." diff --git a/extensions/desktop-icons/po/zh_TW.po b/extensions/desktop-icons/po/zh_TW.po new file mode 100644 index 0000000..8ce4ab9 --- /dev/null +++ b/extensions/desktop-icons/po/zh_TW.po @@ -0,0 +1,135 @@ +# Chinese (Taiwan) translation for desktop-icons. +# Copyright (C) 2018 desktop-icons's COPYRIGHT HOLDER +# This file is distributed under the same license as the desktop-icons package. +# Yi-Jyun Pan , 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-10-22 14:12+0000\n" +"PO-Revision-Date: 2018-10-24 21:31+0800\n" +"Language-Team: Chinese (Taiwan) \n" +"Language: zh_TW\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Last-Translator: pan93412 \n" +"X-Generator: Poedit 2.2\n" + +#: prefs.js:89 +msgid "Size for the desktop icons" +msgstr "桌面圖示的大小" + +#: prefs.js:89 +msgid "Small" +msgstr "小圖示" + +#: prefs.js:89 +msgid "Standard" +msgstr "標準大小圖示" + +#: prefs.js:89 +msgid "Large" +msgstr "大圖示" + +#: prefs.js:89 +msgid "Huge" +msgstr "巨大圖示" + +#: prefs.js:90 +msgid "Show the personal folder in the desktop" +msgstr "在桌面顯示個人資料夾" + +#: prefs.js:91 +msgid "Show the trash icon in the desktop" +msgstr "在桌面顯示垃圾桶圖示" + +#: desktopGrid.js:178 desktopGrid.js:297 +msgid "New Folder" +msgstr "新增資料夾" + +#: desktopGrid.js:299 +msgid "Paste" +msgstr "貼上" + +#: desktopGrid.js:300 +msgid "Undo" +msgstr "復原" + +#: desktopGrid.js:301 +msgid "Redo" +msgstr "重做" + +#: desktopGrid.js:303 +msgid "Open Desktop in Files" +msgstr "在《檔案》中開啟桌面" + +#: desktopGrid.js:304 +msgid "Open Terminal" +msgstr "開啟終端器" + +#: desktopGrid.js:306 +msgid "Change Background…" +msgstr "變更背景圖片…" + +#: desktopGrid.js:307 +msgid "Display Settings" +msgstr "顯示設定" + +#: desktopGrid.js:308 +msgid "Settings" +msgstr "設定" + +#: fileItem.js:223 +msgid "Open" +msgstr "開啟" + +#: fileItem.js:226 +msgid "Cut" +msgstr "剪下" + +#: fileItem.js:227 +msgid "Copy" +msgstr "複製" + +#: fileItem.js:228 +msgid "Move to Trash" +msgstr "移動到垃圾桶" + +#: fileItem.js:232 +msgid "Empty trash" +msgstr "清空回收桶" + +#: fileItem.js:238 +msgid "Properties" +msgstr "屬性" + +#: fileItem.js:240 +msgid "Show in Files" +msgstr "在《檔案》中顯示" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:12 +msgid "Icon size" +msgstr "圖示大小" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:13 +msgid "Set the size for the desktop icons." +msgstr "設定桌面圖示的大小。" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:17 +msgid "Show personal folder" +msgstr "顯示個人資料夾" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:18 +msgid "Show the personal folder in the desktop." +msgstr "在桌面顯示個人資料夾。" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:22 +msgid "Show trash icon" +msgstr "顯示垃圾桶圖示" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:23 +msgid "Show the trash icon in the desktop." +msgstr "在桌面顯示垃圾桶圖示。" diff --git a/extensions/desktop-icons/prefs.js b/extensions/desktop-icons/prefs.js new file mode 100644 index 0000000..0a5b1ce --- /dev/null +++ b/extensions/desktop-icons/prefs.js @@ -0,0 +1,159 @@ + +/* Desktop Icons GNOME Shell extension + * + * Copyright (C) 2017 Carlos Soriano + * + * 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 3 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 Gtk = imports.gi.Gtk; +const GObject = imports.gi.GObject; +const Gio = imports.gi.Gio; +const GioSSS = Gio.SettingsSchemaSource; +const ExtensionUtils = imports.misc.extensionUtils; +const Gettext = imports.gettext; + +const Config = imports.misc.config; + +var _ = Gettext.domain('desktop-icons').gettext; + +const SCHEMA_NAUTILUS = 'org.gnome.nautilus.preferences'; +const SCHEMA_GTK = 'org.gtk.Settings.FileChooser'; +const SCHEMA = 'org.gnome.shell.extensions.desktop-icons'; + +const ICON_SIZE = { 'small': 48, 'standard': 64, 'large': 96 }; +const ICON_WIDTH = { 'small': 120, 'standard': 128, 'large': 128 }; +const ICON_HEIGHT = { 'small': 98, 'standard': 114, 'large': 146 }; + +var FileType = { + NONE: null, + USER_DIRECTORY_HOME: 'show-home', + USER_DIRECTORY_TRASH: 'show-trash', +} + +var nautilusSettings; +var gtkSettings; +var settings; +// This is already in Nautilus settings, so it should not be made tweakable here +var CLICK_POLICY_SINGLE = false; + +function initTranslations() { + let extension = ExtensionUtils.getCurrentExtension(); + + let localedir = extension.dir.get_child('locale'); + if (localedir.query_exists(null)) + Gettext.bindtextdomain('desktop-icons', localedir.get_path()); + else + Gettext.bindtextdomain('desktop-icons', Config.LOCALEDIR); +} + +function init() { + let schemaSource = GioSSS.get_default(); + let schemaGtk = schemaSource.lookup(SCHEMA_GTK, true); + gtkSettings = new Gio.Settings({ settings_schema: schemaGtk }); + let schemaObj = schemaSource.lookup(SCHEMA_NAUTILUS, true); + if (!schemaObj) { + nautilusSettings = null; + } else { + nautilusSettings = new Gio.Settings({ settings_schema: schemaObj });; + nautilusSettings.connect('changed', _onNautilusSettingsChanged); + _onNautilusSettingsChanged(); + } + settings = get_schema(SCHEMA); +} + +function get_schema(schema) { + let extension = ExtensionUtils.getCurrentExtension(); + + // 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 }); +} + +function buildPrefsWidget() { + + let frame = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, border_width: 10, spacing: 10 }); + + frame.add(buildSelector('icon-size', _("Size for the desktop icons"), { 'small': _("Small"), 'standard': _("Standard"), 'large': _("Large") })); + frame.add(buildSwitcher('show-home', _("Show the personal folder in the desktop"))); + frame.add(buildSwitcher('show-trash', _("Show the trash icon in the desktop"))); + frame.show_all(); + return frame; +} + +function buildSwitcher(key, labelText) { + let hbox = new Gtk.Box({ orientation: Gtk.Orientation.HORIZONTAL, spacing: 10 }); + let label = new Gtk.Label({ label: labelText, xalign: 0 }); + let switcher = new Gtk.Switch({ active: settings.get_boolean(key) }); + settings.bind(key, switcher, 'active', 3); + hbox.pack_start(label, true, true, 0); + hbox.add(switcher); + return hbox; +} + +function buildSelector(key, labelText, elements) { + let listStore = new Gtk.ListStore(); + listStore.set_column_types ([GObject.TYPE_STRING, GObject.TYPE_STRING]); + let schemaKey = settings.settings_schema.get_key(key); + let values = schemaKey.get_range().get_child_value(1).get_child_value(0).get_strv(); + for (let val of values) { + let iter = listStore.append(); + let visibleText = val; + if (visibleText in elements) + visibleText = elements[visibleText]; + listStore.set (iter, [0, 1], [visibleText, val]); + } + let hbox = new Gtk.Box({ orientation: Gtk.Orientation.HORIZONTAL, spacing: 10 }); + let label = new Gtk.Label({ label: labelText, xalign: 0 }); + let combo = new Gtk.ComboBox({model: listStore}); + let rendererText = new Gtk.CellRendererText(); + combo.pack_start (rendererText, false); + combo.add_attribute (rendererText, 'text', 0); + combo.set_id_column(1); + settings.bind(key, combo, 'active-id', 3); + hbox.pack_start(label, true, true, 0); + hbox.add(combo); + return hbox; +} + +function _onNautilusSettingsChanged() { + CLICK_POLICY_SINGLE = nautilusSettings.get_string('click-policy') == 'single'; +} + +function get_icon_size() { + // this one doesn't need scaling because Gnome Shell automagically scales the icons + return ICON_SIZE[settings.get_string('icon-size')]; +} + +function get_desired_width(scale_factor) { + return ICON_WIDTH[settings.get_string('icon-size')] * scale_factor; +} + +function get_desired_height(scale_factor) { + return ICON_HEIGHT[settings.get_string('icon-size')] * scale_factor; +} diff --git a/extensions/desktop-icons/schemas/meson.build b/extensions/desktop-icons/schemas/meson.build new file mode 100644 index 0000000..2b17916 --- /dev/null +++ b/extensions/desktop-icons/schemas/meson.build @@ -0,0 +1,6 @@ +gnome.compile_schemas() + +install_data( + 'org.gnome.shell.extensions.desktop-icons.gschema.xml', + install_dir : schema_dir +) diff --git a/extensions/desktop-icons/schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml b/extensions/desktop-icons/schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml new file mode 100644 index 0000000..bb4e50f --- /dev/null +++ b/extensions/desktop-icons/schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml @@ -0,0 +1,25 @@ + + + + + + + + + + 'standard' + Icon size + Set the size for the desktop icons. + + + true + Show personal folder + Show the personal folder in the desktop. + + + true + Show trash icon + Show the trash icon in the desktop. + + + diff --git a/extensions/desktop-icons/stylesheet.css b/extensions/desktop-icons/stylesheet.css new file mode 100644 index 0000000..82209d7 --- /dev/null +++ b/extensions/desktop-icons/stylesheet.css @@ -0,0 +1,33 @@ +.file-item { + padding: 4px; + border: 1px; + margin: 1px; +} + +.file-item:hover { + background-color: rgba(238, 238, 238, 0.2); +} + +.file-item:selected { + background-color: rgba(74, 144, 217, 0.6); + border-color: rgba(74, 144, 217, 0.8); +} + +.rubber-band { + background-color: rgba(74, 144, 238, 0.4); +} + +.name-label { + text-shadow: 1px 1px black; + color: white; + text-align: center; +} + +.draggable { + background-color: red; +} + +.rename-popup { + min-width: 300px; + margin: 6px; +} diff --git a/meson.build b/meson.build index 201c484..99a4738 100644 --- a/meson.build +++ b/meson.build @@ -36,6 +36,7 @@ uuid_suffix = '@gnome-shell-extensions.gcampax.github.com' classic_extensions = [ 'alternate-tab', 'apps-menu', + 'desktop-icons', 'places-menu', 'launch-new-instance', 'window-list' diff --git a/po/cs.po b/po/cs.po index 04bb195..e8f2fa3 100644 --- a/po/cs.po +++ b/po/cs.po @@ -1,11 +1,28 @@ +# #-#-#-#-# cs.po (gnome-shell-extensions) #-#-#-#-# +# #-#-#-#-# cs.po (gnome-shell-extensions) #-#-#-#-# # Czech translation for gnome-shell-extensions. # Copyright (C) 2011 gnome-shell-extensions's COPYRIGHT HOLDER # This file is distributed under the same license as the gnome-shell-extensions package. # Petr Kovar , 2013. # Marek Černocký , 2011, 2012, 2013, 2014, 2015, 2017. # +# #-#-#-#-# cs.po (desktop-icons master) #-#-#-#-# +# Czech translation for desktop-icons. +# Copyright (C) 2018 desktop-icons's COPYRIGHT HOLDER +# This file is distributed under the same license as the desktop-icons package. +# Marek Černocký , 2018. +# +# #-#-#-#-# cs.po (desktop-icons master) #-#-#-#-# +# Czech translation for desktop-icons. +# Copyright (C) 2018 desktop-icons's COPYRIGHT HOLDER +# This file is distributed under the same license as the desktop-icons package. +# Marek Černocký , 2018. +# +#, fuzzy msgid "" msgstr "" +"#-#-#-#-# cs.po (gnome-shell-extensions) #-#-#-#-#\n" +"#-#-#-#-# cs.po (gnome-shell-extensions) #-#-#-#-#\n" "Project-Id-Version: gnome-shell-extensions\n" "Report-Msgid-Bugs-To: https://bugzilla.gnome.org/enter_bug.cgi?product=gnome-" "shell&keywords=I18N+L10N&component=extensions\n" @@ -19,6 +36,34 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" "X-Generator: Gtranslator 2.91.6\n" +"#-#-#-#-# cs.po (desktop-icons master) #-#-#-#-#\n" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-10-01 20:15+0000\n" +"PO-Revision-Date: 2018-10-02 11:10+0200\n" +"Last-Translator: Marek Černocký \n" +"Language-Team: čeština \n" +"Language: cs\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" +"X-Generator: Gtranslator 2.91.7\n" +"#-#-#-#-# cs.po (desktop-icons master) #-#-#-#-#\n" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-10-01 20:15+0000\n" +"PO-Revision-Date: 2018-10-02 11:10+0200\n" +"Last-Translator: Marek Černocký \n" +"Language-Team: čeština \n" +"Language: cs\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" +"X-Generator: Gtranslator 2.91.7\n" #: data/gnome-classic.desktop.in:3 data/gnome-classic.session.desktop.in:3 msgid "GNOME Classic" @@ -350,3 +395,119 @@ msgstr "Název" #, javascript-format msgid "Workspace %d" msgstr "Pracovní plocha %d" + +#: prefs.js:89 +msgid "Size for the desktop icons" +msgstr "Velikost ikon na pracovní ploše" + +#: prefs.js:89 +msgid "Small" +msgstr "malé" + +#: prefs.js:89 +msgid "Standard" +msgstr "standardní" + +#: prefs.js:89 +msgid "Large" +msgstr "velké" + +#: prefs.js:89 +msgid "Huge" +msgstr "obrovské" + +#: prefs.js:90 +msgid "Show the personal folder in the desktop" +msgstr "Zobrazovat osobní složku na pracovní ploše" + +#: prefs.js:91 +msgid "Show the trash icon in the desktop" +msgstr "Zobrazovat ikonu koše na pracovní ploše" + +#: desktopGrid.js:178 desktopGrid.js:297 +msgid "New Folder" +msgstr "Nová složka" + +#: desktopGrid.js:299 +msgid "Paste" +msgstr "Vložit" + +#: desktopGrid.js:300 +msgid "Undo" +msgstr "Zpět" + +#: desktopGrid.js:301 +msgid "Redo" +msgstr "Znovu" + +#: desktopGrid.js:303 +msgid "Open Desktop in Files" +msgstr "Otevřít plochu v Souborech" + +#: desktopGrid.js:304 +msgid "Open Terminal" +msgstr "Otevřít terminál" + +#: desktopGrid.js:306 +msgid "Change Background…" +msgstr "Změnit pozadí…" + +#: desktopGrid.js:307 +msgid "Display Settings" +msgstr "Zobrazit nastavení" + +#: desktopGrid.js:308 +msgid "Settings" +msgstr "Nastavení" + +#: fileItem.js:226 +msgid "Open" +msgstr "Otevřít" + +#: fileItem.js:229 +msgid "Cut" +msgstr "Vyjmout" + +#: fileItem.js:230 +msgid "Copy" +msgstr "Kopírovat" + +#: fileItem.js:231 +msgid "Move to Trash" +msgstr "Přesunout do koše" + +#: fileItem.js:235 +msgid "Empty trash" +msgstr "Vyprázdnit koš" + +#: fileItem.js:241 +msgid "Properties" +msgstr "Vlastnosti" + +#: fileItem.js:243 +msgid "Show in Files" +msgstr "Zobrazit v Souborech" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:12 +msgid "Icon size" +msgstr "Velikost ikon" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:13 +msgid "Set the size for the desktop icons." +msgstr "Nastavit velikost pro ikony na pracovní ploše." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:17 +msgid "Show personal folder" +msgstr "Zobrazovat osobní složku" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:18 +msgid "Show the personal folder in the desktop." +msgstr "Zobrazovat osobní složku na pracovní ploše." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:22 +msgid "Show trash icon" +msgstr "Zobrazovat koš" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:23 +msgid "Show the trash icon in the desktop." +msgstr "Zobrazovat ikonu koše na pracovní ploše." diff --git a/po/da.po b/po/da.po index 5a4c257..eeeef12 100644 --- a/po/da.po +++ b/po/da.po @@ -1,3 +1,5 @@ +# #-#-#-#-# da.po (gnome-shell-extensions master) #-#-#-#-# +# #-#-#-#-# da.po (gnome-shell-extensions master) #-#-#-#-# # Danish translation for gnome-shell-extensions. # Copyright (C) 2011-2017 gnome-shell-extensions's COPYRIGHT HOLDER # This file is distributed under the same license as the gnome-shell-extensions package. @@ -6,8 +8,23 @@ # Ask Hjorth Larsen , 2015, 2017. # Joe Hansen , 2017. # +# #-#-#-#-# da.po (desktop-icons master) #-#-#-#-# +# Danish translation for desktop-icons. +# Copyright (C) 2018 desktop-icons's COPYRIGHT HOLDER +# This file is distributed under the same license as the desktop-icons package. +# Alan Mortensen , 2018. +# +# #-#-#-#-# da.po (desktop-icons master) #-#-#-#-# +# Danish translation for desktop-icons. +# Copyright (C) 2018 desktop-icons's COPYRIGHT HOLDER +# This file is distributed under the same license as the desktop-icons package. +# Alan Mortensen , 2018. +# +#, fuzzy msgid "" msgstr "" +"#-#-#-#-# da.po (gnome-shell-extensions master) #-#-#-#-#\n" +"#-#-#-#-# da.po (gnome-shell-extensions master) #-#-#-#-#\n" "Project-Id-Version: gnome-shell-extensions master\n" "Report-Msgid-Bugs-To: https://bugzilla.gnome.org/enter_bug.cgi?product=gnome-" "shell&keywords=I18N+L10N&component=extensions\n" @@ -20,6 +37,34 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" +"#-#-#-#-# da.po (desktop-icons master) #-#-#-#-#\n" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-10-26 12:04+0000\n" +"PO-Revision-Date: 2018-10-27 16:42+0200\n" +"Language-Team: Danish \n" +"Language: da\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Last-Translator: Alan Mortensen \n" +"X-Generator: Poedit 2.0.6\n" +"#-#-#-#-# da.po (desktop-icons master) #-#-#-#-#\n" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-10-26 12:04+0000\n" +"PO-Revision-Date: 2018-10-27 16:42+0200\n" +"Language-Team: Danish \n" +"Language: da\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Last-Translator: Alan Mortensen \n" +"X-Generator: Poedit 2.0.6\n" #: data/gnome-classic.desktop.in:3 data/gnome-classic.session.desktop.in:3 msgid "GNOME Classic" @@ -359,3 +404,119 @@ msgstr "Navn" #, javascript-format msgid "Workspace %d" msgstr "Arbejdsområde %d" + +#: prefs.js:89 +msgid "Size for the desktop icons" +msgstr "Størrelsen på skrivebordsikoner" + +#: prefs.js:89 +msgid "Small" +msgstr "Små" + +#: prefs.js:89 +msgid "Standard" +msgstr "Standard" + +#: prefs.js:89 +msgid "Large" +msgstr "Store" + +#: prefs.js:89 +msgid "Huge" +msgstr "Enorme" + +#: prefs.js:90 +msgid "Show the personal folder in the desktop" +msgstr "Vis den personlige mappe på skrivebordet" + +#: prefs.js:91 +msgid "Show the trash icon in the desktop" +msgstr "Vis papirkurvsikonet på skrivebordet" + +#: desktopGrid.js:178 desktopGrid.js:297 +msgid "New Folder" +msgstr "Ny mappe" + +#: desktopGrid.js:299 +msgid "Paste" +msgstr "Indsæt" + +#: desktopGrid.js:300 +msgid "Undo" +msgstr "Fortryd" + +#: desktopGrid.js:301 +msgid "Redo" +msgstr "Omgør" + +#: desktopGrid.js:303 +msgid "Open Desktop in Files" +msgstr "Åbn skrivebordet i Filer" + +#: desktopGrid.js:304 +msgid "Open Terminal" +msgstr "Åbn Terminal" + +#: desktopGrid.js:306 +msgid "Change Background…" +msgstr "Skift baggrund …" + +#: desktopGrid.js:307 +msgid "Display Settings" +msgstr "Skærmindstillinger" + +#: desktopGrid.js:308 +msgid "Settings" +msgstr "Indstillinger" + +#: fileItem.js:385 +msgid "Open" +msgstr "Åbn" + +#: fileItem.js:388 +msgid "Cut" +msgstr "Klip" + +#: fileItem.js:389 +msgid "Copy" +msgstr "Kopiér" + +#: fileItem.js:390 +msgid "Move to Trash" +msgstr "Flyt til papirkurven" + +#: fileItem.js:394 +msgid "Empty trash" +msgstr "Tøm papirkurven" + +#: fileItem.js:400 +msgid "Properties" +msgstr "Egenskaber" + +#: fileItem.js:402 +msgid "Show in Files" +msgstr "Vis i Filer" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:12 +msgid "Icon size" +msgstr "Ikonstørrelse" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:13 +msgid "Set the size for the desktop icons." +msgstr "Angiv størrelsen på skrivebordsikoner." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:17 +msgid "Show personal folder" +msgstr "Vis personlig mappe" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:18 +msgid "Show the personal folder in the desktop." +msgstr "Vis den personlige mappe på skrivebordet." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:22 +msgid "Show trash icon" +msgstr "Vis papirkurvsikon" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:23 +msgid "Show the trash icon in the desktop." +msgstr "Vis papirkurvsikonet på skrivebordet." diff --git a/po/de.po b/po/de.po index 01924b8..6ca034e 100644 --- a/po/de.po +++ b/po/de.po @@ -1,3 +1,5 @@ +# #-#-#-#-# de.po (gnome-shell-extensions master) #-#-#-#-# +# #-#-#-#-# de.po (gnome-shell-extensions master) #-#-#-#-# # German translation for gnome-shell-extensions. # Copyright (C) 2011 gnome-shell-extensions's COPYRIGHT HOLDER # This file is distributed under the same license as the gnome-shell-extensions package. @@ -7,8 +9,23 @@ # Wolfgang Stöggl , 2014. # Paul Seyfert , 2017. # +# #-#-#-#-# de.po (desktop-icons master) #-#-#-#-# +# German translation for desktop-icons. +# Copyright (C) 2018 desktop-icons's COPYRIGHT HOLDER +# This file is distributed under the same license as the desktop-icons package. +# Mario Blättermann , 2018. +# +# #-#-#-#-# de.po (desktop-icons master) #-#-#-#-# +# German translation for desktop-icons. +# Copyright (C) 2018 desktop-icons's COPYRIGHT HOLDER +# This file is distributed under the same license as the desktop-icons package. +# Mario Blättermann , 2018. +# +#, fuzzy msgid "" msgstr "" +"#-#-#-#-# de.po (gnome-shell-extensions master) #-#-#-#-#\n" +"#-#-#-#-# de.po (gnome-shell-extensions master) #-#-#-#-#\n" "Project-Id-Version: gnome-shell-extensions master\n" "Report-Msgid-Bugs-To: https://bugzilla.gnome.org/enter_bug.cgi?product=gnome-" "shell&keywords=I18N+L10N&component=extensions\n" @@ -22,6 +39,34 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 2.0.2\n" +"#-#-#-#-# de.po (desktop-icons master) #-#-#-#-#\n" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-10-02 21:39+0000\n" +"PO-Revision-Date: 2018-10-03 21:28+0200\n" +"Last-Translator: Mario Blättermann \n" +"Language-Team: German \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 2.1.1\n" +"#-#-#-#-# de.po (desktop-icons master) #-#-#-#-#\n" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-10-02 21:39+0000\n" +"PO-Revision-Date: 2018-10-03 21:28+0200\n" +"Last-Translator: Mario Blättermann \n" +"Language-Team: German \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 2.1.1\n" #: data/gnome-classic.desktop.in:3 data/gnome-classic.session.desktop.in:3 msgid "GNOME Classic" @@ -366,3 +411,119 @@ msgstr "Name" #, javascript-format msgid "Workspace %d" msgstr "Arbeitsfläche %d" + +#: prefs.js:89 +msgid "Size for the desktop icons" +msgstr "Größe der Arbeitsflächensymbole" + +#: prefs.js:89 +msgid "Small" +msgstr "Klein" + +#: prefs.js:89 +msgid "Standard" +msgstr "Standard" + +#: prefs.js:89 +msgid "Large" +msgstr "Groß" + +#: prefs.js:89 +msgid "Huge" +msgstr "Riesig" + +#: prefs.js:90 +msgid "Show the personal folder in the desktop" +msgstr "Den persönlichen Ordner auf der Arbeitsfläche anzeigen" + +#: prefs.js:91 +msgid "Show the trash icon in the desktop" +msgstr "Papierkorb-Symbol auf der Arbeitsfläche anzeigen" + +#: desktopGrid.js:178 desktopGrid.js:297 +msgid "New Folder" +msgstr "Neuer Ordner" + +#: desktopGrid.js:299 +msgid "Paste" +msgstr "Einfügen" + +#: desktopGrid.js:300 +msgid "Undo" +msgstr "Rückgängig" + +#: desktopGrid.js:301 +msgid "Redo" +msgstr "Wiederholen" + +#: desktopGrid.js:303 +msgid "Open Desktop in Files" +msgstr "Arbeitsfläche in Dateiverwaltung öffnen" + +#: desktopGrid.js:304 +msgid "Open Terminal" +msgstr "Terminal öffnen" + +#: desktopGrid.js:306 +msgid "Change Background…" +msgstr "Hintergrund ändern …" + +#: desktopGrid.js:307 +msgid "Display Settings" +msgstr "Anzeigeeinstellungen" + +#: desktopGrid.js:308 +msgid "Settings" +msgstr "Einstellungen" + +#: fileItem.js:226 +msgid "Open" +msgstr "Öffnen" + +#: fileItem.js:229 +msgid "Cut" +msgstr "Ausschneiden" + +#: fileItem.js:230 +msgid "Copy" +msgstr "Kopieren" + +#: fileItem.js:231 +msgid "Move to Trash" +msgstr "In den Papierkorb verschieben" + +#: fileItem.js:235 +msgid "Empty trash" +msgstr "Papierkorb leeren" + +#: fileItem.js:241 +msgid "Properties" +msgstr "Eigenschaften" + +#: fileItem.js:243 +msgid "Show in Files" +msgstr "In Dateiverwaltung anzeigen" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:12 +msgid "Icon size" +msgstr "Symbolgröße" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:13 +msgid "Set the size for the desktop icons." +msgstr "Die Größe der Arbeitsflächensymbole festlegen." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:17 +msgid "Show personal folder" +msgstr "Persönlichen Ordner anzeigen" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:18 +msgid "Show the personal folder in the desktop." +msgstr "Den persönlichen Ordner auf der Arbeitsfläche anzeigen." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:22 +msgid "Show trash icon" +msgstr "Papierkorb-Symbol anzeigen" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:23 +msgid "Show the trash icon in the desktop." +msgstr "Das Papierkorb-Symbol auf der Arbeitsfläche anzeigen." diff --git a/po/es.po b/po/es.po index a3c0703..18cf8d4 100644 --- a/po/es.po +++ b/po/es.po @@ -1,3 +1,5 @@ +# #-#-#-#-# es.po (gnome-shell-extensions master) #-#-#-#-# +# #-#-#-#-# es.po (gnome-shell-extensions master) #-#-#-#-# # Spanish translation for gnome-shell-extensions. # Copyright (C) 2011 gnome-shell-extensions's COPYRIGHT HOLDER # This file is distributed under the same license as the gnome-shell-extensions package. @@ -6,8 +8,27 @@ # # Daniel Mustieles , 2011-2015, 2017. # +# #-#-#-#-# es.po (PACKAGE VERSION) #-#-#-#-# +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# Sergio Costas , 2018. +# Daniel Mustieles , 2018, 2019. +# +# #-#-#-#-# es.po (PACKAGE VERSION) #-#-#-#-# +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# Sergio Costas , 2018. +# Daniel Mustieles , 2018, 2019. +# +#, fuzzy msgid "" msgstr "" +"#-#-#-#-# es.po (gnome-shell-extensions master) #-#-#-#-#\n" +"#-#-#-#-# es.po (gnome-shell-extensions master) #-#-#-#-#\n" "Project-Id-Version: gnome-shell-extensions master\n" "Report-Msgid-Bugs-To: https://bugzilla.gnome.org/enter_bug.cgi?product=gnome-" "shell&keywords=I18N+L10N&component=extensions\n" @@ -21,6 +42,34 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Gtranslator 2.91.6\n" +"#-#-#-#-# es.po (PACKAGE VERSION) #-#-#-#-#\n" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-12-14 09:12+0000\n" +"PO-Revision-Date: 2019-01-08 16:12+0100\n" +"Last-Translator: Daniel Mustieles \n" +"Language-Team: es \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Gtranslator 2.91.7\n" +"#-#-#-#-# es.po (PACKAGE VERSION) #-#-#-#-#\n" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-12-14 09:12+0000\n" +"PO-Revision-Date: 2019-01-08 16:12+0100\n" +"Last-Translator: Daniel Mustieles \n" +"Language-Team: es \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Gtranslator 2.91.7\n" #: data/gnome-classic.desktop.in:3 data/gnome-classic.session.desktop.in:3 msgid "GNOME Classic" @@ -361,6 +410,146 @@ msgstr "Nombre" msgid "Workspace %d" msgstr "Área de trabajo %d" +#: desktopGrid.js:311 +msgid "Display Settings" +msgstr "Configuración de pantalla" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:12 +msgid "Icon size" +msgstr "Tamaño de los iconos" + +#: desktopGrid.js:578 +msgid "Cancel" +msgstr "Cancelar" + +#: prefs.js:102 +msgid "Size for the desktop icons" +msgstr "Tamaño de los iconos del escritorio" + +#: prefs.js:102 +msgid "Small" +msgstr "Pequeño" + +#: prefs.js:102 +msgid "Standard" +msgstr "Estándar" + +#: prefs.js:102 +msgid "Large" +msgstr "Grande" + +#: prefs.js:102 +msgid "Huge" +msgstr "Inmenso" + +#: prefs.js:103 +msgid "Show the personal folder in the desktop" +msgstr "Mostrar la carpeta personal en el escritorio" + +#: prefs.js:104 +msgid "Show the trash icon in the desktop" +msgstr "Mostrar la papelera en el escritorio" + +#: desktopGrid.js:182 desktopGrid.js:301 +msgid "New Folder" +msgstr "Nueva carpeta" + +#: desktopGrid.js:303 +msgid "Paste" +msgstr "Pegar" + +#: desktopGrid.js:304 +msgid "Undo" +msgstr "Deshacer" + +#: desktopGrid.js:305 +msgid "Redo" +msgstr "Rehacer" + +#: desktopGrid.js:307 +msgid "Open Desktop in Files" +msgstr "Abrir el escritorio en Files" + +#: desktopGrid.js:308 +msgid "Open Terminal" +msgstr "Abrir un terminal" + +#: desktopGrid.js:310 +msgid "Change Background…" +msgstr "Cambiar el fondo..." + +#: desktopGrid.js:312 +msgid "Settings" +msgstr "Configuración" + +#: desktopGrid.js:568 +msgid "Enter file name…" +msgstr "Introduzca el nombre del archivo…" + +#: desktopGrid.js:572 +msgid "OK" +msgstr "Aceptar" + +#: fileItem.js:485 +msgid "Don’t Allow Launching" +msgstr "No permitir lanzar" + +#: fileItem.js:487 +msgid "Allow Launching" +msgstr "Permitir lanzar" + +#: fileItem.js:550 +msgid "Open" +msgstr "Abrir" + +#: fileItem.js:553 +msgid "Cut" +msgstr "Cortar" + +#: fileItem.js:554 +msgid "Copy" +msgstr "Copiar" + +#: fileItem.js:556 +msgid "Rename" +msgstr "Renombrar" + +#: fileItem.js:557 +msgid "Move to Trash" +msgstr "Mover a la papelera" + +#: fileItem.js:567 +msgid "Empty Trash" +msgstr "Vaciar la papelera" + +#: fileItem.js:573 +msgid "Properties" +msgstr "Propiedades" + +#: fileItem.js:575 +msgid "Show in Files" +msgstr "Mostrar en Files" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:13 +msgid "Set the size for the desktop icons." +msgstr "Establece el tamaño de los iconos del escritorio." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:17 +msgid "Show personal folder" +msgstr "Mostrar la carpeta personal" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:18 +msgid "Show the personal folder in the desktop." +msgstr "Mostrar la carpeta personal en el escritorio." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:22 +msgid "Show trash icon" +msgstr "Mostrar la papelera" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:23 +msgid "Show the trash icon in the desktop." +msgstr "Mostrar la papelera en el escritorio." + #~ msgid "CPU" #~ msgstr "CPU" @@ -409,9 +598,6 @@ msgstr "Área de trabajo %d" #~ msgid "Display" #~ msgstr "Pantalla" -#~ msgid "Display Settings" -#~ msgstr "Configuración de pantalla" - #~ msgid "File System" #~ msgstr "Sistema de archivos" @@ -459,9 +645,6 @@ msgstr "Área de trabajo %d" #~ "Configura la posición del tablero en la pantalla. Los valores permitidos " #~ "son «right» (derecha) o «left» (izquierda)" -#~ msgid "Icon size" -#~ msgstr "Tamaño del icono" - #~ msgid "Sets icon size of the dock." #~ msgstr "Configura el tamaño de los íconos del tablero." @@ -632,9 +815,6 @@ msgstr "Área de trabajo %d" #~ msgid "Alt Tab Behaviour" #~ msgstr "Comportamiento de Alt+Tab" -#~ msgid "Cancel" -#~ msgstr "Cancelar" - #~ msgid "Notifications" #~ msgstr "Notificaciones" @@ -668,3 +848,27 @@ msgstr "Área de trabajo %d" #~ msgid "Busy" #~ msgstr "Ocupado" + +#~ msgid "Ok" +#~ msgstr "Aceptar" + +#~ msgid "huge" +#~ msgstr "inmenso" + +#~ msgid "Maximum width for the icons and filename." +#~ msgstr "Ancho máximo de los iconos y el nombre de fichero." + +#~ msgid "Shows the Documents folder in the desktop." +#~ msgstr "Muestra la carpeta Documentos en el escritorio." + +#~ msgid "Shows the Downloads folder in the desktop." +#~ msgstr "Muestra la carpeta Descargas en el escritorio." + +#~ msgid "Shows the Music folder in the desktop." +#~ msgstr "Muestra la carpeta Música en el escritorio." + +#~ msgid "Shows the Pictures folder in the desktop." +#~ msgstr "Muestra la carpeta Imágenes en el escritorio." + +#~ msgid "Shows the Videos folder in the desktop." +#~ msgstr "Muestra la carpeta Vídeos en el escritorio." diff --git a/po/fi.po b/po/fi.po index e036448..73b33cc 100644 --- a/po/fi.po +++ b/po/fi.po @@ -1,3 +1,5 @@ +# #-#-#-#-# fi.po (gnome-shell-extensions) #-#-#-#-# +# #-#-#-#-# fi.po (gnome-shell-extensions) #-#-#-#-# # Finnish translation of gnome-shell-extensions. # Copyright (C) 2011 Ville-Pekka Vainio # This file is distributed under the same license as the gnome-shell-extensions package. @@ -7,8 +9,23 @@ # Ville-Pekka Vainio , 2011. # Jiri Grönroos , 2012, 2013, 2014, 2015, 2017. # +# #-#-#-#-# fi.po (desktop-icons master) #-#-#-#-# +# Finnish translation for desktop-icons. +# Copyright (C) 2018 desktop-icons's COPYRIGHT HOLDER +# This file is distributed under the same license as the desktop-icons package. +# Jiri Grönroos , 2018. +# +# #-#-#-#-# fi.po (desktop-icons master) #-#-#-#-# +# Finnish translation for desktop-icons. +# Copyright (C) 2018 desktop-icons's COPYRIGHT HOLDER +# This file is distributed under the same license as the desktop-icons package. +# Jiri Grönroos , 2018. +# +#, fuzzy msgid "" msgstr "" +"#-#-#-#-# fi.po (gnome-shell-extensions) #-#-#-#-#\n" +"#-#-#-#-# fi.po (gnome-shell-extensions) #-#-#-#-#\n" "Project-Id-Version: gnome-shell-extensions\n" "Report-Msgid-Bugs-To: https://bugzilla.gnome.org/enter_bug.cgi?product=gnome-" "shell&keywords=I18N+L10N&component=extensions\n" @@ -24,6 +41,34 @@ msgstr "" "X-Generator: Gtranslator 2.91.7\n" "X-Project-Style: gnome\n" "X-POT-Import-Date: 2012-03-05 15:06:12+0000\n" +"#-#-#-#-# fi.po (desktop-icons master) #-#-#-#-#\n" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-10-14 08:09+0000\n" +"PO-Revision-Date: 2018-10-14 12:44+0300\n" +"Language-Team: Finnish \n" +"Language: fi\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Last-Translator: Jiri Grönroos \n" +"X-Generator: Poedit 2.0.6\n" +"#-#-#-#-# fi.po (desktop-icons master) #-#-#-#-#\n" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-10-14 08:09+0000\n" +"PO-Revision-Date: 2018-10-14 12:44+0300\n" +"Language-Team: Finnish \n" +"Language: fi\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Last-Translator: Jiri Grönroos \n" +"X-Generator: Poedit 2.0.6\n" #: data/gnome-classic.desktop.in:3 data/gnome-classic.session.desktop.in:3 msgid "GNOME Classic" @@ -352,6 +397,122 @@ msgstr "Nimi" msgid "Workspace %d" msgstr "Työtila %d" +#: desktopGrid.js:307 +msgid "Display Settings" +msgstr "Näytön asetukset" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:12 +msgid "Icon size" +msgstr "Kuvakekoko" + +#: prefs.js:89 +msgid "Size for the desktop icons" +msgstr "Työpöytäkuvakkeiden koko" + +#: prefs.js:89 +msgid "Small" +msgstr "Pieni" + +#: prefs.js:89 +msgid "Standard" +msgstr "Normaali" + +#: prefs.js:89 +msgid "Large" +msgstr "Suuri" + +#: prefs.js:89 +msgid "Huge" +msgstr "Valtava" + +#: prefs.js:90 +msgid "Show the personal folder in the desktop" +msgstr "Näytä kotikansio työpöydällä" + +#: prefs.js:91 +msgid "Show the trash icon in the desktop" +msgstr "Näytä roskakorin kuvake työpöydällä" + +#: desktopGrid.js:178 desktopGrid.js:297 +msgid "New Folder" +msgstr "Uusi kansio" + +#: desktopGrid.js:299 +msgid "Paste" +msgstr "Liitä" + +#: desktopGrid.js:300 +msgid "Undo" +msgstr "Kumoa" + +#: desktopGrid.js:301 +msgid "Redo" +msgstr "Tee uudeleen" + +#: desktopGrid.js:303 +msgid "Open Desktop in Files" +msgstr "Avaa työpöytä tiedostonhallinnassa" + +#: desktopGrid.js:304 +msgid "Open Terminal" +msgstr "Avaa pääte" + +#: desktopGrid.js:306 +msgid "Change Background…" +msgstr "Vaihda taustakuvaa…" + +#: desktopGrid.js:308 +msgid "Settings" +msgstr "Asetukset" + +#: fileItem.js:211 +msgid "Open" +msgstr "Avaa" + +#: fileItem.js:214 +msgid "Cut" +msgstr "Leikkaa" + +#: fileItem.js:215 +msgid "Copy" +msgstr "Kopioi" + +#: fileItem.js:216 +msgid "Move to Trash" +msgstr "Siirrä roskakoriin" + +#: fileItem.js:220 +msgid "Empty trash" +msgstr "Tyhjennä roskakori" + +#: fileItem.js:226 +msgid "Properties" +msgstr "Ominaisuudet" + +#: fileItem.js:228 +msgid "Show in Files" +msgstr "Näytä tiedostonhallinnassa" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:13 +msgid "Set the size for the desktop icons." +msgstr "Aseta työpöytäkuvakkeiden koko." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:17 +msgid "Show personal folder" +msgstr "Näytä kotikansio" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:18 +msgid "Show the personal folder in the desktop." +msgstr "Näytä kotikansio työpöydällä." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:22 +msgid "Show trash icon" +msgstr "Näytä roskakorin kuvake" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:23 +msgid "Show the trash icon in the desktop." +msgstr "Näytä roskakorin kuvake työpöydällä." + #~ msgid "CPU" #~ msgstr "Suoritin" @@ -388,9 +549,6 @@ msgstr "Työtila %d" #~ msgid "Display" #~ msgstr "Näyttö" -#~ msgid "Display Settings" -#~ msgstr "Näytön asetukset" - #~ msgid "Drag here to add favorites" #~ msgstr "Raahaa tähän lisätäksesi suosikkeihin" @@ -412,9 +570,6 @@ msgstr "Työtila %d" #~ msgstr "" #~ "Asettaa telakan sijainnin näytöllä. Sallitut arvot ovat 'right' tai 'left'" -#~ msgid "Icon size" -#~ msgstr "Kuvakkeiden koko" - #~ msgid "Sets icon size of the dock." #~ msgstr "Asettaa telakan kuvakkeiden koon." diff --git a/po/fr.po b/po/fr.po index 2b01d33..3f9826f 100644 --- a/po/fr.po +++ b/po/fr.po @@ -1,11 +1,30 @@ +# #-#-#-#-# fr.po (gnome-shell-extensions master) #-#-#-#-# +# #-#-#-#-# fr.po (gnome-shell-extensions master) #-#-#-#-# # French translation for gnome-shell-extensions. # Copyright (C) 2011-12 Listed translators # This file is distributed under the same license as the gnome-shell-extensions package. # Claude Paroz , 2011. # Alain Lojewski , 2012-2013. # +# #-#-#-#-# fr.po (desktop-icons master) #-#-#-#-# +# French translation for desktop-icons. +# Copyright (C) 2018 desktop-icons's COPYRIGHT HOLDER +# This file is distributed under the same license as the desktop-icons package. +# ghentdebian , 2018. +# Charles Monzat , 2018. +# +# #-#-#-#-# fr.po (desktop-icons master) #-#-#-#-# +# French translation for desktop-icons. +# Copyright (C) 2018 desktop-icons's COPYRIGHT HOLDER +# This file is distributed under the same license as the desktop-icons package. +# ghentdebian , 2018. +# Charles Monzat , 2018. +# +#, fuzzy msgid "" msgstr "" +"#-#-#-#-# fr.po (gnome-shell-extensions master) #-#-#-#-#\n" +"#-#-#-#-# fr.po (gnome-shell-extensions master) #-#-#-#-#\n" "Project-Id-Version: gnome-shell-extensions master\n" "Report-Msgid-Bugs-To: https://bugzilla.gnome.org/enter_bug.cgi?product=gnome-" "shell&keywords=I18N+L10N&component=extensions\n" @@ -18,6 +37,34 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" +"#-#-#-#-# fr.po (desktop-icons master) #-#-#-#-#\n" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-12-14 09:12+0000\n" +"PO-Revision-Date: 2018-12-16 17:47+0100\n" +"Last-Translator: Charles Monzat \n" +"Language-Team: GNOME French Team \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1)\n" +"X-Generator: Gtranslator 3.30.0\n" +"#-#-#-#-# fr.po (desktop-icons master) #-#-#-#-#\n" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-12-14 09:12+0000\n" +"PO-Revision-Date: 2018-12-16 17:47+0100\n" +"Last-Translator: Charles Monzat \n" +"Language-Team: GNOME French Team \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1)\n" +"X-Generator: Gtranslator 3.30.0\n" #: data/gnome-classic.desktop.in:3 data/gnome-classic.session.desktop.in:3 msgid "GNOME Classic" @@ -362,8 +409,151 @@ msgstr "Nom" msgid "Workspace %d" msgstr "Espace de travail %d" +#: prefs.js:102 +msgid "Size for the desktop icons" +msgstr "Taille des icônes du bureau" + +#: prefs.js:102 +msgid "Small" +msgstr "Petite" + +#: prefs.js:102 +msgid "Standard" +msgstr "Normale" + +#: prefs.js:102 +msgid "Large" +msgstr "Grande" + +#: prefs.js:102 +msgid "Huge" +msgstr "Immense" + +#: prefs.js:103 +msgid "Show the personal folder in the desktop" +msgstr "Montrer le dossier personnel sur le bureau" + +#: prefs.js:104 +msgid "Show the trash icon in the desktop" +msgstr "Montrer la corbeille sur le bureau" + +#: desktopGrid.js:182 desktopGrid.js:301 +msgid "New Folder" +msgstr "Nouveau dossier" + +#: desktopGrid.js:303 +msgid "Paste" +msgstr "Coller" + +#: desktopGrid.js:304 +msgid "Undo" +msgstr "Annuler" + +#: desktopGrid.js:305 +msgid "Redo" +msgstr "Refaire" + +#: desktopGrid.js:307 +msgid "Open Desktop in Files" +msgstr "Ouvrir le bureau dans Fichiers" + +#: desktopGrid.js:308 +msgid "Open Terminal" +msgstr "Ouvrir un terminal" + +#: desktopGrid.js:310 +msgid "Change Background…" +msgstr "Changer l’arrière-plan…" + +#: desktopGrid.js:311 +msgid "Display Settings" +msgstr "Configuration d’affichage" + +#: desktopGrid.js:312 +msgid "Settings" +msgstr "Paramètres" + +#: desktopGrid.js:568 +msgid "Enter file name…" +msgstr "Saisir un nom de fichier…" + +#: desktopGrid.js:572 +msgid "OK" +msgstr "Valider" + +#: desktopGrid.js:578 +msgid "Cancel" +msgstr "Annuler" + +#: fileItem.js:485 +msgid "Don’t Allow Launching" +msgstr "Ne pas autoriser le lancement" + +#: fileItem.js:487 +msgid "Allow Launching" +msgstr "Autoriser le lancement" + +#: fileItem.js:550 +msgid "Open" +msgstr "Ouvrir" + +#: fileItem.js:553 +msgid "Cut" +msgstr "Couper" + +#: fileItem.js:554 +msgid "Copy" +msgstr "Copier" + +#: fileItem.js:556 +msgid "Rename" +msgstr "Renommer" + +#: fileItem.js:557 +msgid "Move to Trash" +msgstr "Mettre à la corbeille" + +#: fileItem.js:567 +msgid "Empty Trash" +msgstr "Vider la corbeille" + +#: fileItem.js:573 +msgid "Properties" +msgstr "Propriétés" + +#: fileItem.js:575 +msgid "Show in Files" +msgstr "Montrer dans Fichiers" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:12 +msgid "Icon size" +msgstr "Taille d’icônes" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:13 +msgid "Set the size for the desktop icons." +msgstr "Définir la taille des icônes du bureau." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:17 +msgid "Show personal folder" +msgstr "Montrer le dossier personnel" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:18 +msgid "Show the personal folder in the desktop." +msgstr "Montrer le dossier personnel sur le bureau." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:22 +msgid "Show trash icon" +msgstr "Montrer l’icône de la corbeille" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:23 +msgid "Show the trash icon in the desktop." +msgstr "Montrer la corbeille sur le bureau." + #~ msgid "CPU" #~ msgstr "CPU" #~ msgid "Memory" #~ msgstr "Mémoire" + +#~ msgid "Ok" +#~ msgstr "Valider" diff --git a/po/id.po b/po/id.po index 8986a5e..033ae90 100644 --- a/po/id.po +++ b/po/id.po @@ -1,11 +1,28 @@ +# #-#-#-#-# id.po (gnome-shell-extensions master) #-#-#-#-# +# #-#-#-#-# id.po (gnome-shell-extensions master) #-#-#-#-# # Indonesian translation for gnome-shell-extensions. # Copyright (C) 2012 gnome-shell-extensions's COPYRIGHT HOLDER # This file is distributed under the same license as the gnome-shell-extensions package. # # Andika Triwidada , 2012, 2013. # Dirgita , 2012. +# #-#-#-#-# id.po (desktop-icons master) #-#-#-#-# +# Indonesian translation for desktop-icons. +# Copyright (C) 2018 desktop-icons's COPYRIGHT HOLDER +# This file is distributed under the same license as the desktop-icons package. +# FIRST AUTHOR , YEAR. +# +# #-#-#-#-# id.po (desktop-icons master) #-#-#-#-# +# Indonesian translation for desktop-icons. +# Copyright (C) 2018 desktop-icons's COPYRIGHT HOLDER +# This file is distributed under the same license as the desktop-icons package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy msgid "" msgstr "" +"#-#-#-#-# id.po (gnome-shell-extensions master) #-#-#-#-#\n" +"#-#-#-#-# id.po (gnome-shell-extensions master) #-#-#-#-#\n" "Project-Id-Version: gnome-shell-extensions master\n" "Report-Msgid-Bugs-To: https://bugzilla.gnome.org/enter_bug.cgi?product=gnome-" "shell&keywords=I18N+L10N&component=extensions\n" @@ -20,6 +37,32 @@ msgstr "" "Plural-Forms: nplurals=1; plural=0;\n" "X-Poedit-SourceCharset: UTF-8\n" "X-Generator: Poedit 2.0.2\n" +"#-#-#-#-# id.po (desktop-icons master) #-#-#-#-#\n" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-10-02 09:18+0000\n" +"PO-Revision-Date: 2018-10-02 20:30+0700\n" +"Language-Team: Indonesian \n" +"Language: id\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Last-Translator: Kukuh Syafaat \n" +"X-Generator: Poedit 2.0.6\n" +"#-#-#-#-# id.po (desktop-icons master) #-#-#-#-#\n" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-10-02 09:18+0000\n" +"PO-Revision-Date: 2018-10-02 20:30+0700\n" +"Language-Team: Indonesian \n" +"Language: id\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Last-Translator: Kukuh Syafaat \n" +"X-Generator: Poedit 2.0.6\n" #: data/gnome-classic.desktop.in:3 data/gnome-classic.session.desktop.in:3 msgid "GNOME Classic" @@ -358,6 +401,122 @@ msgstr "Nama" msgid "Workspace %d" msgstr "Ruang Kerja %d" +#: prefs.js:89 +msgid "Size for the desktop icons" +msgstr "Ukuran untuk ikon destop" + +#: prefs.js:89 +msgid "Small" +msgstr "Kecil" + +#: prefs.js:89 +msgid "Standard" +msgstr "Standar" + +#: prefs.js:89 +msgid "Large" +msgstr "Besar" + +#: prefs.js:89 +msgid "Huge" +msgstr "Sangat besar" + +#: prefs.js:90 +msgid "Show the personal folder in the desktop" +msgstr "Tampilkan folder pribadi di destop" + +#: prefs.js:91 +msgid "Show the trash icon in the desktop" +msgstr "Tampilkan ikon tong sampah di destop" + +#: desktopGrid.js:178 desktopGrid.js:297 +msgid "New Folder" +msgstr "Folder Baru" + +#: desktopGrid.js:299 +msgid "Paste" +msgstr "Tempel" + +#: desktopGrid.js:300 +msgid "Undo" +msgstr "Tak Jadi" + +#: desktopGrid.js:301 +msgid "Redo" +msgstr "Jadi Lagi" + +#: desktopGrid.js:303 +msgid "Open Desktop in Files" +msgstr "Buka Destop pada Berkas" + +#: desktopGrid.js:304 +msgid "Open Terminal" +msgstr "Buka Terminal" + +#: desktopGrid.js:306 +msgid "Change Background…" +msgstr "Ubah Latar Belakang…" + +#: desktopGrid.js:307 +msgid "Display Settings" +msgstr "Pengaturan Tampilan" + +#: desktopGrid.js:308 +msgid "Settings" +msgstr "Pengaturan" + +#: fileItem.js:226 +msgid "Open" +msgstr "Buka" + +#: fileItem.js:229 +msgid "Cut" +msgstr "Potong" + +#: fileItem.js:230 +msgid "Copy" +msgstr "Salin" + +#: fileItem.js:231 +msgid "Move to Trash" +msgstr "Pindahkan ke Tong Sampah" + +#: fileItem.js:235 +msgid "Empty trash" +msgstr "Kosongkan Tong Sampah" + +#: fileItem.js:241 +msgid "Properties" +msgstr "Properti" + +#: fileItem.js:243 +msgid "Show in Files" +msgstr "Tampilkan pada Berkas" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:12 +msgid "Icon size" +msgstr "Ukuran ikon" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:13 +msgid "Set the size for the desktop icons." +msgstr "Set ukuran untuk ikon destop." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:17 +msgid "Show personal folder" +msgstr "Tampilkan folder pribadi" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:18 +msgid "Show the personal folder in the desktop." +msgstr "Tampilkan folder pribadi di destop." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:22 +msgid "Show trash icon" +msgstr "Tampilkan ikon tong sampah" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:23 +msgid "Show the trash icon in the desktop." +msgstr "Tampilkan ikon tong sampah di destop." + #~ msgid "CPU" #~ msgstr "CPU" diff --git a/po/it.po b/po/it.po index 4e3a59c..1d6fd26 100644 --- a/po/it.po +++ b/po/it.po @@ -1,3 +1,5 @@ +# #-#-#-#-# it.po (gnome-shell-extensions) #-#-#-#-# +# #-#-#-#-# it.po (gnome-shell-extensions) #-#-#-#-# # Italian translations for GNOME Shell extensions # Copyright (C) 2011 Giovanni Campagna et al. # Copyright (C) 2012, 2013, 2014, 2015, 2017 The Free Software Foundation, Inc. @@ -6,8 +8,23 @@ # Milo Casagrande , 2013, 2014, 2015, 2017. # Gianvito Cavasoli , 2017. # +# #-#-#-#-# it.po (desktop-icons master) #-#-#-#-# +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Massimo Branchini , 2018. +# +# #-#-#-#-# it.po (desktop-icons master) #-#-#-#-# +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Massimo Branchini , 2018. +# +#, fuzzy msgid "" msgstr "" +"#-#-#-#-# it.po (gnome-shell-extensions) #-#-#-#-#\n" +"#-#-#-#-# it.po (gnome-shell-extensions) #-#-#-#-#\n" "Project-Id-Version: gnome-shell-extensions\n" "Report-Msgid-Bugs-To: https://bugzilla.gnome.org/enter_bug.cgi?product=gnome-" "shell&keywords=I18N+L10N&component=extensions\n" @@ -21,6 +38,34 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 1.8.12\n" +"#-#-#-#-# it.po (desktop-icons master) #-#-#-#-#\n" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-11-29 09:07+0000\n" +"PO-Revision-Date: 2018-12-05 11:14+0100\n" +"Last-Translator: Massimo Branchini \n" +"Language-Team: Italian \n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 2.2\n" +"#-#-#-#-# it.po (desktop-icons master) #-#-#-#-#\n" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-11-29 09:07+0000\n" +"PO-Revision-Date: 2018-12-05 11:14+0100\n" +"Last-Translator: Massimo Branchini \n" +"Language-Team: Italian \n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 2.2\n" #: data/gnome-classic.desktop.in:3 data/gnome-classic.session.desktop.in:3 msgid "GNOME Classic" @@ -362,3 +407,135 @@ msgstr "Nome" #, javascript-format msgid "Workspace %d" msgstr "Spazio di lavoro %d" + +#: prefs.js:93 +msgid "Size for the desktop icons" +msgstr "Dimensione delle icone della scrivania" + +#: prefs.js:93 +msgid "Small" +msgstr "Piccola" + +#: prefs.js:93 +msgid "Standard" +msgstr "Normale" + +#: prefs.js:93 +msgid "Large" +msgstr "Grande" + +#: prefs.js:93 +msgid "Huge" +msgstr "Enorme" + +#: prefs.js:94 +msgid "Show the personal folder in the desktop" +msgstr "Mostra la cartella personale sulla scrivania" + +#: prefs.js:95 +msgid "Show the trash icon in the desktop" +msgstr "Mostra il cestino sulla scrivania" + +#: desktopGrid.js:185 desktopGrid.js:304 +msgid "New Folder" +msgstr "Nuova cartella" + +#: desktopGrid.js:306 +msgid "Paste" +msgstr "Incolla" + +#: desktopGrid.js:307 +msgid "Undo" +msgstr "Annulla" + +#: desktopGrid.js:308 +msgid "Redo" +msgstr "Ripeti" + +#: desktopGrid.js:310 +msgid "Open Desktop in Files" +msgstr "Apri la scrivania in File" + +#: desktopGrid.js:311 +msgid "Open Terminal" +msgstr "Apri un terminale" + +#: desktopGrid.js:313 +msgid "Change Background…" +msgstr "Cambia lo sfondo…" + +#: desktopGrid.js:314 +msgid "Display Settings" +msgstr "Impostazioni dello schermo" + +#: desktopGrid.js:315 +msgid "Settings" +msgstr "Impostazioni" + +#: desktopGrid.js:569 +msgid "Enter file name…" +msgstr "Indicare un nome per il file…" + +#: desktopGrid.js:573 +msgid "Ok" +msgstr "Ok" + +#: desktopGrid.js:579 +msgid "Cancel" +msgstr "Annulla" + +#: fileItem.js:393 +msgid "Open" +msgstr "Apri" + +#: fileItem.js:396 +msgid "Cut" +msgstr "Taglia" + +#: fileItem.js:397 +msgid "Copy" +msgstr "Copia" + +#: fileItem.js:398 +msgid "Rename" +msgstr "Rinomina" + +#: fileItem.js:399 +msgid "Move to Trash" +msgstr "Sposta nel cestino" + +#: fileItem.js:403 +msgid "Empty Trash" +msgstr "Svuota il cestino" + +#: fileItem.js:409 +msgid "Properties" +msgstr "Proprietà" + +#: fileItem.js:411 +msgid "Show in Files" +msgstr "Mostra in File" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:12 +msgid "Icon size" +msgstr "Dimensione dell'icona" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:13 +msgid "Set the size for the desktop icons." +msgstr "Imposta la grandezza delle icone della scrivania." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:17 +msgid "Show personal folder" +msgstr "Mostra la cartella personale" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:18 +msgid "Show the personal folder in the desktop." +msgstr "Mostra la cartella personale sulla scrivania." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:22 +msgid "Show trash icon" +msgstr "Mostra il cestino" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:23 +msgid "Show the trash icon in the desktop." +msgstr "Mostra il cestino sulla scrivania." diff --git a/po/pl.po b/po/pl.po index 35799ee..94dd40b 100644 --- a/po/pl.po +++ b/po/pl.po @@ -1,11 +1,30 @@ +# #-#-#-#-# pl.po (gnome-shell-extensions) #-#-#-#-# +# #-#-#-#-# pl.po (gnome-shell-extensions) #-#-#-#-# # Polish translation for gnome-shell-extensions. # Copyright © 2011-2017 the gnome-shell-extensions authors. # This file is distributed under the same license as the gnome-shell-extensions package. # Piotr Drąg , 2011-2017. # Aviary.pl , 2011-2017. # +# #-#-#-#-# pl.po (desktop-icons) #-#-#-#-# +# Polish translation for desktop-icons. +# Copyright © 2018-2019 the desktop-icons authors. +# This file is distributed under the same license as the desktop-icons package. +# Piotr Drąg , 2018-2019. +# Aviary.pl , 2018-2019. +# +# #-#-#-#-# pl.po (desktop-icons) #-#-#-#-# +# Polish translation for desktop-icons. +# Copyright © 2018-2019 the desktop-icons authors. +# This file is distributed under the same license as the desktop-icons package. +# Piotr Drąg , 2018-2019. +# Aviary.pl , 2018-2019. +# +#, fuzzy msgid "" msgstr "" +"#-#-#-#-# pl.po (gnome-shell-extensions) #-#-#-#-#\n" +"#-#-#-#-# pl.po (gnome-shell-extensions) #-#-#-#-#\n" "Project-Id-Version: gnome-shell-extensions\n" "Report-Msgid-Bugs-To: https://bugzilla.gnome.org/enter_bug.cgi?product=gnome-" "shell&keywords=I18N+L10N&component=extensions\n" @@ -19,6 +38,34 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " "|| n%100>=20) ? 1 : 2);\n" +"#-#-#-#-# pl.po (desktop-icons) #-#-#-#-#\n" +"Project-Id-Version: desktop-icons\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2019-01-15 10:59+0000\n" +"PO-Revision-Date: 2019-01-15 20:57+0100\n" +"Last-Translator: Piotr Drąg \n" +"Language-Team: Polish \n" +"Language: pl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " +"|| n%100>=20) ? 1 : 2);\n" +"#-#-#-#-# pl.po (desktop-icons) #-#-#-#-#\n" +"Project-Id-Version: desktop-icons\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2019-01-15 10:59+0000\n" +"PO-Revision-Date: 2019-01-15 20:57+0100\n" +"Last-Translator: Piotr Drąg \n" +"Language-Team: Polish \n" +"Language: pl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " +"|| n%100>=20) ? 1 : 2);\n" #: data/gnome-classic.desktop.in:3 data/gnome-classic.session.desktop.in:3 msgid "GNOME Classic" @@ -358,3 +405,139 @@ msgstr "Nazwa" #, javascript-format msgid "Workspace %d" msgstr "%d. obszar roboczy" + +#: prefs.js:102 +msgid "Size for the desktop icons" +msgstr "Rozmiar ikon na pulpicie" + +#: prefs.js:102 +msgid "Small" +msgstr "Mały" + +#: prefs.js:102 +msgid "Standard" +msgstr "Standardowy" + +#: prefs.js:102 +msgid "Large" +msgstr "Duży" + +#: prefs.js:103 +msgid "Show the personal folder in the desktop" +msgstr "Katalog domowy na pulpicie" + +#: prefs.js:104 +msgid "Show the trash icon in the desktop" +msgstr "Kosz na pulpicie" + +#: desktopGrid.js:187 desktopGrid.js:306 +msgid "New Folder" +msgstr "Nowy katalog" + +#: desktopGrid.js:308 +msgid "Paste" +msgstr "Wklej" + +#: desktopGrid.js:309 +msgid "Undo" +msgstr "Cofnij" + +#: desktopGrid.js:310 +msgid "Redo" +msgstr "Ponów" + +#: desktopGrid.js:312 +msgid "Show Desktop in Files" +msgstr "Wyświetl pulpit w menedżerze plików" + +#: desktopGrid.js:313 fileItem.js:586 +msgid "Open in Terminal" +msgstr "Otwórz w terminalu" + +#: desktopGrid.js:315 +msgid "Change Background…" +msgstr "Zmień tło…" + +#: desktopGrid.js:317 +msgid "Display Settings" +msgstr "Ustawienia ekranu" + +#: desktopGrid.js:318 +msgid "Settings" +msgstr "Ustawienia" + +#: desktopGrid.js:559 +msgid "Enter file name…" +msgstr "Nazwa pliku…" + +#: desktopGrid.js:563 +msgid "OK" +msgstr "OK" + +#: desktopGrid.js:569 +msgid "Cancel" +msgstr "Anuluj" + +#: fileItem.js:490 +msgid "Don’t Allow Launching" +msgstr "Nie zezwalaj na uruchamianie" + +#: fileItem.js:492 +msgid "Allow Launching" +msgstr "Zezwól na uruchamianie" + +#: fileItem.js:559 +msgid "Open" +msgstr "Otwórz" + +#: fileItem.js:562 +msgid "Cut" +msgstr "Wytnij" + +#: fileItem.js:563 +msgid "Copy" +msgstr "Skopiuj" + +#: fileItem.js:565 +msgid "Rename…" +msgstr "Zmień nazwę…" + +#: fileItem.js:566 +msgid "Move to Trash" +msgstr "Przenieś do kosza" + +#: fileItem.js:576 +msgid "Empty Trash" +msgstr "Opróżnij kosz" + +#: fileItem.js:582 +msgid "Properties" +msgstr "Właściwości" + +#: fileItem.js:584 +msgid "Show in Files" +msgstr "Wyświetl w menedżerze plików" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:11 +msgid "Icon size" +msgstr "Rozmiar ikon" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:12 +msgid "Set the size for the desktop icons." +msgstr "Ustawia rozmiar ikon na pulpicie." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:16 +msgid "Show personal folder" +msgstr "Katalog domowy" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:17 +msgid "Show the personal folder in the desktop." +msgstr "Wyświetla katalog domowy na pulpicie." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:21 +msgid "Show trash icon" +msgstr "Kosz" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:22 +msgid "Show the trash icon in the desktop." +msgstr "Wyświetla kosz na pulpicie." diff --git a/po/pt_BR.po b/po/pt_BR.po index d029648..77f7a0d 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -1,3 +1,5 @@ +# #-#-#-#-# pt_BR.po (gnome-shell-extensions master) #-#-#-#-# +# #-#-#-#-# pt_BR.po (gnome-shell-extensions master) #-#-#-#-# # Brazilian Portuguese translation for gnome-shell-extensions. # Copyright (C) 2017 gnome-shell-extensions's COPYRIGHT HOLDER # This file is distributed under the same license as the gnome-shell-extensions package. @@ -9,8 +11,23 @@ # Og Maciel , 2012. # Enrico Nicoletto , 2013, 2014. # Rafael Fontenelle , 2013, 2017. +# #-#-#-#-# pt_BR.po (desktop-icons master) #-#-#-#-# +# Brazilian Portuguese translation for desktop-icons. +# Copyright (C) 2018 desktop-icons's COPYRIGHT HOLDER +# This file is distributed under the same license as the desktop-icons package. +# Enrico Nicoletto , 2018. +# Rafael Fontenelle , 2018. +# #-#-#-#-# pt_BR.po (desktop-icons master) #-#-#-#-# +# Brazilian Portuguese translation for desktop-icons. +# Copyright (C) 2018 desktop-icons's COPYRIGHT HOLDER +# This file is distributed under the same license as the desktop-icons package. +# Enrico Nicoletto , 2018. +# Rafael Fontenelle , 2018. +#, fuzzy msgid "" msgstr "" +"#-#-#-#-# pt_BR.po (gnome-shell-extensions master) #-#-#-#-#\n" +"#-#-#-#-# pt_BR.po (gnome-shell-extensions master) #-#-#-#-#\n" "Project-Id-Version: gnome-shell-extensions master\n" "Report-Msgid-Bugs-To: https://bugzilla.gnome.org/enter_bug.cgi?product=gnome-" "shell&keywords=I18N+L10N&component=extensions\n" @@ -25,6 +42,34 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "X-Generator: Virtaal 1.0.0-beta1\n" "X-Project-Style: gnome\n" +"#-#-#-#-# pt_BR.po (desktop-icons master) #-#-#-#-#\n" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-12-14 09:12+0000\n" +"PO-Revision-Date: 2018-12-17 01:01-0200\n" +"Last-Translator: Rafael Fontenelle \n" +"Language-Team: Brazilian Portuguese \n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Virtaal 1.0.0-beta1\n" +"#-#-#-#-# pt_BR.po (desktop-icons master) #-#-#-#-#\n" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-12-14 09:12+0000\n" +"PO-Revision-Date: 2018-12-17 01:01-0200\n" +"Last-Translator: Rafael Fontenelle \n" +"Language-Team: Brazilian Portuguese \n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Virtaal 1.0.0-beta1\n" #: data/gnome-classic.desktop.in:3 data/gnome-classic.session.desktop.in:3 msgid "GNOME Classic" @@ -366,6 +411,146 @@ msgstr "Nome" msgid "Workspace %d" msgstr "Espaço de trabalho %d" +#: desktopGrid.js:311 +msgid "Display Settings" +msgstr "Configurações de exibição" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:12 +msgid "Icon size" +msgstr "Tamanho do ícone" + +#: desktopGrid.js:578 +msgid "Cancel" +msgstr "Cancelar" + +#: prefs.js:102 +msgid "Size for the desktop icons" +msgstr "Tamanho para os ícones da área de trabalho" + +#: prefs.js:102 +msgid "Small" +msgstr "Pequeno" + +#: prefs.js:102 +msgid "Standard" +msgstr "Padrão" + +#: prefs.js:102 +msgid "Large" +msgstr "Grande" + +#: prefs.js:102 +msgid "Huge" +msgstr "Enorme" + +#: prefs.js:103 +msgid "Show the personal folder in the desktop" +msgstr "Mostrar a pasta pessoal na área de trabalho" + +#: prefs.js:104 +msgid "Show the trash icon in the desktop" +msgstr "Mostrar o ícone da lixeira na área de trabalho" + +#: desktopGrid.js:182 desktopGrid.js:301 +msgid "New Folder" +msgstr "Nova pasta" + +#: desktopGrid.js:303 +msgid "Paste" +msgstr "Colar" + +#: desktopGrid.js:304 +msgid "Undo" +msgstr "Desfazer" + +#: desktopGrid.js:305 +msgid "Redo" +msgstr "Refazer" + +#: desktopGrid.js:307 +msgid "Open Desktop in Files" +msgstr "Abrir área de trabalho no Arquivos" + +#: desktopGrid.js:308 +msgid "Open Terminal" +msgstr "Abrir terminal" + +#: desktopGrid.js:310 +msgid "Change Background…" +msgstr "Alterar plano de fundo…" + +#: desktopGrid.js:312 +msgid "Settings" +msgstr "Configurações" + +#: desktopGrid.js:568 +msgid "Enter file name…" +msgstr "Insira um nome de arquivo…" + +#: desktopGrid.js:572 +msgid "OK" +msgstr "OK" + +#: fileItem.js:485 +msgid "Don’t Allow Launching" +msgstr "Não permitir iniciar" + +#: fileItem.js:487 +msgid "Allow Launching" +msgstr "Permitir iniciar" + +#: fileItem.js:550 +msgid "Open" +msgstr "Abrir" + +#: fileItem.js:553 +msgid "Cut" +msgstr "Recortar" + +#: fileItem.js:554 +msgid "Copy" +msgstr "Copiar" + +#: fileItem.js:556 +msgid "Rename" +msgstr "Renomear" + +#: fileItem.js:557 +msgid "Move to Trash" +msgstr "Mover para a lixeira" + +#: fileItem.js:567 +msgid "Empty Trash" +msgstr "Esvaziar lixeira" + +#: fileItem.js:573 +msgid "Properties" +msgstr "Propriedades" + +#: fileItem.js:575 +msgid "Show in Files" +msgstr "Mostrar no Arquivos" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:13 +msgid "Set the size for the desktop icons." +msgstr "Define o tamanho para os ícones da área de trabalho." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:17 +msgid "Show personal folder" +msgstr "Mostrar pasta pessoal" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:18 +msgid "Show the personal folder in the desktop." +msgstr "Mostra a pasta pessoal na área de trabalho." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:22 +msgid "Show trash icon" +msgstr "Mostrar ícone da lixeira" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:23 +msgid "Show the trash icon in the desktop." +msgstr "Mostra o ícone da lixeira na área de trabalho." + #~ msgid "CPU" #~ msgstr "CPU" @@ -414,9 +599,6 @@ msgstr "Espaço de trabalho %d" #~ msgid "Display" #~ msgstr "Tela" -#~ msgid "Display Settings" -#~ msgstr "Configurações de tela" - #~ msgid "The application icon mode." #~ msgstr "O modo de ícone do aplicativo." @@ -451,9 +633,6 @@ msgstr "Espaço de trabalho %d" #~ "Define a posição do dock na tela. Os valores permitidos são \"right\" ou " #~ "\"left\"" -#~ msgid "Icon size" -#~ msgstr "Tamanho do ícone" - #~ msgid "Sets icon size of the dock." #~ msgstr "Define o tamanho do ícone do dock." @@ -613,9 +792,6 @@ msgstr "Espaço de trabalho %d" #~ msgid "Alt Tab Behaviour" #~ msgstr "Comportamento do Alt Tab" -#~ msgid "Cancel" -#~ msgstr "Cancelar" - #~ msgid "Ask the user for a default behaviour if true." #~ msgstr "Pergunte ao usuário por um comportamento padrão se marcado." @@ -639,3 +815,6 @@ msgstr "Espaço de trabalho %d" #~ msgid "Log Out..." #~ msgstr "Encerrar sessão..." + +#~ msgid "Ok" +#~ msgstr "Ok" diff --git a/po/ru.po b/po/ru.po index c18c0ba..5e48a26 100644 --- a/po/ru.po +++ b/po/ru.po @@ -1,11 +1,28 @@ +# #-#-#-#-# ru.po (gnome-shell-extensions gnome-3-0) #-#-#-#-# +# #-#-#-#-# ru.po (gnome-shell-extensions gnome-3-0) #-#-#-#-# # Russian translation for gnome-shell-extensions. # Copyright (C) 2011 gnome-shell-extensions's COPYRIGHT HOLDER # This file is distributed under the same license as the gnome-shell-extensions package. # Yuri Myasoedov , 2011, 2012, 2013. # Stas Solovey , 2011, 2012, 2013, 2015, 2017. # +# #-#-#-#-# ru.po #-#-#-#-# +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Eaglers , 2018. +# +# #-#-#-#-# ru.po #-#-#-#-# +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Eaglers , 2018. +# +#, fuzzy msgid "" msgstr "" +"#-#-#-#-# ru.po (gnome-shell-extensions gnome-3-0) #-#-#-#-#\n" +"#-#-#-#-# ru.po (gnome-shell-extensions gnome-3-0) #-#-#-#-#\n" "Project-Id-Version: gnome-shell-extensions gnome-3-0\n" "Report-Msgid-Bugs-To: https://bugzilla.gnome.org/enter_bug.cgi?product=gnome-" "shell&keywords=I18N+L10N&component=extensions\n" @@ -20,6 +37,36 @@ msgstr "" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" "X-Generator: Poedit 2.0.3\n" +"#-#-#-#-# ru.po #-#-#-#-#\n" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-11-22 08:42+0000\n" +"PO-Revision-Date: 2018-11-22 22:02+0300\n" +"Last-Translator: Stas Solovey \n" +"Language-Team: Russian \n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Generator: Poedit 2.2\n" +"#-#-#-#-# ru.po #-#-#-#-#\n" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-11-22 08:42+0000\n" +"PO-Revision-Date: 2018-11-22 22:02+0300\n" +"Last-Translator: Stas Solovey \n" +"Language-Team: Russian \n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Generator: Poedit 2.2\n" #: data/gnome-classic.desktop.in:3 data/gnome-classic.session.desktop.in:3 msgid "GNOME Classic" @@ -360,6 +407,138 @@ msgstr "Название" msgid "Workspace %d" msgstr "Рабочая область %d" +#: prefs.js:89 +msgid "Size for the desktop icons" +msgstr "Размер значков" + +#: prefs.js:89 +msgid "Small" +msgstr "Маленький" + +#: prefs.js:89 +msgid "Standard" +msgstr "Стандартный" + +#: prefs.js:89 +msgid "Large" +msgstr "Большой" + +#: prefs.js:89 +msgid "Huge" +msgstr "Огромный" + +#: prefs.js:90 +msgid "Show the personal folder in the desktop" +msgstr "Показывать домашнюю папку на рабочем столе" + +#: prefs.js:91 +msgid "Show the trash icon in the desktop" +msgstr "Показывать «Корзину» на рабочем столе" + +#: desktopGrid.js:185 desktopGrid.js:304 +msgid "New Folder" +msgstr "Создать папку" + +#: desktopGrid.js:306 +msgid "Paste" +msgstr "Вставить" + +#: desktopGrid.js:307 +msgid "Undo" +msgstr "Отменить" + +#: desktopGrid.js:308 +msgid "Redo" +msgstr "Повторить" + +#: desktopGrid.js:310 +msgid "Open Desktop in Files" +msgstr "Открыть «Рабочий стол» в «Файлах»" + +#: desktopGrid.js:311 +msgid "Open Terminal" +msgstr "Открыть терминал" + +#: desktopGrid.js:313 +msgid "Change Background…" +msgstr "Изменить фон…" + +#: desktopGrid.js:314 +msgid "Display Settings" +msgstr "Настройки дисплея" + +#: desktopGrid.js:315 +msgid "Settings" +msgstr "Параметры" + +#: desktopGrid.js:569 +msgid "Enter file name…" +msgstr "Ввести имя файла…" + +#: desktopGrid.js:573 +msgid "Ok" +msgstr "ОК" + +#: desktopGrid.js:579 +msgid "Cancel" +msgstr "Отмена" + +#: fileItem.js:390 +msgid "Open" +msgstr "Открыть" + +#: fileItem.js:393 +msgid "Cut" +msgstr "Вырезать" + +#: fileItem.js:394 +msgid "Copy" +msgstr "Вставить" + +#: fileItem.js:395 +msgid "Rename" +msgstr "Переименовать" + +#: fileItem.js:396 +msgid "Move to Trash" +msgstr "Переместить в корзину" + +#: fileItem.js:400 +msgid "Empty trash" +msgstr "Очистить корзину" + +#: fileItem.js:406 +msgid "Properties" +msgstr "Свойства" + +#: fileItem.js:408 +msgid "Show in Files" +msgstr "Показать в «Файлах»" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:12 +msgid "Icon size" +msgstr "Размер значков" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:13 +msgid "Set the size for the desktop icons." +msgstr "Установить размер значков на рабочем столе." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:17 +msgid "Show personal folder" +msgstr "Показывать домашнюю папку" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:18 +msgid "Show the personal folder in the desktop." +msgstr "Показывать значок домашней папки на рабочем столе." + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:22 +msgid "Show trash icon" +msgstr "Показывать значок корзины" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:23 +msgid "Show the trash icon in the desktop." +msgstr "Показывать значок корзины на рабочем столе." + #~ msgid "CPU" #~ msgstr "ЦП" diff --git a/po/zh_TW.po b/po/zh_TW.po index 74a95f8..8a675aa 100644 --- a/po/zh_TW.po +++ b/po/zh_TW.po @@ -1,10 +1,27 @@ +# #-#-#-#-# zh_TW.po (gnome-shell-extensions gnome-3-0) #-#-#-#-# +# #-#-#-#-# zh_TW.po (gnome-shell-extensions gnome-3-0) #-#-#-#-# # Chinese (Taiwan) translation for gnome-shell-extensions. # Copyright (C) 2011 gnome-shell-extensions's COPYRIGHT HOLDER # This file is distributed under the same license as the gnome-shell-extensions package. # Cheng-Chia Tseng , 2011. # +# #-#-#-#-# zh_TW.po (desktop-icons master) #-#-#-#-# +# Chinese (Taiwan) translation for desktop-icons. +# Copyright (C) 2018 desktop-icons's COPYRIGHT HOLDER +# This file is distributed under the same license as the desktop-icons package. +# Yi-Jyun Pan , 2018. +# +# #-#-#-#-# zh_TW.po (desktop-icons master) #-#-#-#-# +# Chinese (Taiwan) translation for desktop-icons. +# Copyright (C) 2018 desktop-icons's COPYRIGHT HOLDER +# This file is distributed under the same license as the desktop-icons package. +# Yi-Jyun Pan , 2018. +# +#, fuzzy msgid "" msgstr "" +"#-#-#-#-# zh_TW.po (gnome-shell-extensions gnome-3-0) #-#-#-#-#\n" +"#-#-#-#-# zh_TW.po (gnome-shell-extensions gnome-3-0) #-#-#-#-#\n" "Project-Id-Version: gnome-shell-extensions gnome-3-0\n" "Report-Msgid-Bugs-To: https://bugzilla.gnome.org/enter_bug.cgi?product=gnome-" "shell&keywords=I18N+L10N&component=extensions\n" @@ -17,6 +34,32 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 2.0.3\n" +"#-#-#-#-# zh_TW.po (desktop-icons master) #-#-#-#-#\n" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-10-22 14:12+0000\n" +"PO-Revision-Date: 2018-10-24 21:31+0800\n" +"Language-Team: Chinese (Taiwan) \n" +"Language: zh_TW\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Last-Translator: pan93412 \n" +"X-Generator: Poedit 2.2\n" +"#-#-#-#-# zh_TW.po (desktop-icons master) #-#-#-#-#\n" +"Project-Id-Version: desktop-icons master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/ShellExtensions/desktop-" +"icons/issues\n" +"POT-Creation-Date: 2018-10-22 14:12+0000\n" +"PO-Revision-Date: 2018-10-24 21:31+0800\n" +"Language-Team: Chinese (Taiwan) \n" +"Language: zh_TW\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Last-Translator: pan93412 \n" +"X-Generator: Poedit 2.2\n" #: data/gnome-classic.desktop.in:3 data/gnome-classic.session.desktop.in:3 msgid "GNOME Classic" @@ -344,6 +387,122 @@ msgstr "名稱" msgid "Workspace %d" msgstr "工作區 %d" +#: desktopGrid.js:307 +msgid "Display Settings" +msgstr "顯示設定" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:12 +msgid "Icon size" +msgstr "圖示大小" + +#: prefs.js:89 +msgid "Size for the desktop icons" +msgstr "桌面圖示的大小" + +#: prefs.js:89 +msgid "Small" +msgstr "小圖示" + +#: prefs.js:89 +msgid "Standard" +msgstr "標準大小圖示" + +#: prefs.js:89 +msgid "Large" +msgstr "大圖示" + +#: prefs.js:89 +msgid "Huge" +msgstr "巨大圖示" + +#: prefs.js:90 +msgid "Show the personal folder in the desktop" +msgstr "在桌面顯示個人資料夾" + +#: prefs.js:91 +msgid "Show the trash icon in the desktop" +msgstr "在桌面顯示垃圾桶圖示" + +#: desktopGrid.js:178 desktopGrid.js:297 +msgid "New Folder" +msgstr "新增資料夾" + +#: desktopGrid.js:299 +msgid "Paste" +msgstr "貼上" + +#: desktopGrid.js:300 +msgid "Undo" +msgstr "復原" + +#: desktopGrid.js:301 +msgid "Redo" +msgstr "重做" + +#: desktopGrid.js:303 +msgid "Open Desktop in Files" +msgstr "在《檔案》中開啟桌面" + +#: desktopGrid.js:304 +msgid "Open Terminal" +msgstr "開啟終端器" + +#: desktopGrid.js:306 +msgid "Change Background…" +msgstr "變更背景圖片…" + +#: desktopGrid.js:308 +msgid "Settings" +msgstr "設定" + +#: fileItem.js:223 +msgid "Open" +msgstr "開啟" + +#: fileItem.js:226 +msgid "Cut" +msgstr "剪下" + +#: fileItem.js:227 +msgid "Copy" +msgstr "複製" + +#: fileItem.js:228 +msgid "Move to Trash" +msgstr "移動到垃圾桶" + +#: fileItem.js:232 +msgid "Empty trash" +msgstr "清空回收桶" + +#: fileItem.js:238 +msgid "Properties" +msgstr "屬性" + +#: fileItem.js:240 +msgid "Show in Files" +msgstr "在《檔案》中顯示" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:13 +msgid "Set the size for the desktop icons." +msgstr "設定桌面圖示的大小。" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:17 +msgid "Show personal folder" +msgstr "顯示個人資料夾" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:18 +msgid "Show the personal folder in the desktop." +msgstr "在桌面顯示個人資料夾。" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:22 +msgid "Show trash icon" +msgstr "顯示垃圾桶圖示" + +#: schemas/org.gnome.shell.extensions.desktop-icons.gschema.xml:23 +msgid "Show the trash icon in the desktop." +msgstr "在桌面顯示垃圾桶圖示。" + #~ msgid "CPU" #~ msgstr "CPU" @@ -371,9 +530,6 @@ msgstr "工作區 %d" #~ msgid "Display" #~ msgstr "顯示" -#~ msgid "Display Settings" -#~ msgstr "顯示設定值" - #~ msgid "Suspend" #~ msgstr "暫停" @@ -481,9 +637,6 @@ msgstr "工作區 %d" #~ msgid "Enable/disable autohide" #~ msgstr "啟用/停用自動隱藏" -#~ msgid "Icon size" -#~ msgstr "圖示大小" - #~ msgid "Position of the dock" #~ msgstr "Dock 的位置" -- 2.20.1