From afe9de13c5b3b3350c48a18aa8b1c6b518473d90 Mon Sep 17 00:00:00 2001 From: František Zatloukal Date: May 18 2022 15:11:38 +0000 Subject: dash-to-dock 72 --- diff --git a/1656.patch b/1656.patch deleted file mode 100644 index 7c63ce5..0000000 --- a/1656.patch +++ /dev/null @@ -1,5057 +0,0 @@ -From dae57192831c8808cb62b09223bf22c987db8565 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Wed, 10 Nov 2021 16:49:39 +0100 -Subject: [PATCH 01/51] locations: Use native GIcon as location Apps-icons - -We create a AppIcon via a fake desktop file, and this leads to having -to go through the theme to ensure if we've an icon. - -Apparently this can create slowdowns and issues, while we can leave the -shell do this by just exposing the real GIcon. - -So, let's wrap the shell's get_icon function and the AppIcon one to get -the application icon, and create it using the real GIcon. - -This will ensure that we'll always have the correct icon and that we -don't have to do all the lookup ourself. - -Fixes: #1239 -LP: #1874578 ---- - locations.js | 1 + - 1 file changed, 1 insertion(+) - -diff --git a/locations.js b/locations.js -index 1ac25d09b..687e2b280 100644 ---- a/locations.js -+++ b/locations.js -@@ -88,6 +88,7 @@ function wrapWindowsBackedApp(shellApp, params = {}) { - } - shellApp._mi = m; // Method injector - shellApp._pi = p; // Property injector -+ shellApp._aMi = aM; // appInfo method Injector - - m('get_state', () => - shellApp.get_windows().length ? Shell.AppState.RUNNING : Shell.AppState.STOPPED); - -From 7545a7981363b85484f9e908dd57f23f152adc9a Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Wed, 10 Nov 2021 16:53:51 +0100 -Subject: [PATCH 02/51] locations: Use native Gio.FileMonitor rate limit - instead of a custom idle - -Also reduce update delay to 1 second, we don't really need to check it -so often. ---- - locations.js | 25 +++++++++++++------------ - 1 file changed, 13 insertions(+), 12 deletions(-) - -diff --git a/locations.js b/locations.js -index 687e2b280..be29e4a5f 100644 ---- a/locations.js -+++ b/locations.js -@@ -21,7 +21,7 @@ const Utils = Me.imports.utils; - const FALLBACK_REMOVABLE_MEDIA_ICON = 'drive-removable-media'; - const FILE_MANAGER_DESKTOP_APP_ID = 'org.gnome.Nautilus.desktop'; - const TRASH_URI = 'trash://'; --const UPDATE_TRASH_DELAY = 500; -+const UPDATE_TRASH_DELAY = 1000; - - const NautilusFileOperations2Interface = '\ - \ -@@ -373,10 +373,8 @@ var Trash = class DashToDock_Trash { - this._file = Gio.file_new_for_uri(TRASH_URI); - try { - this._monitor = this._file.monitor_directory(0, this._cancellable); -- this._signalId = this._monitor.connect( -- 'changed', -- this._onTrashChange.bind(this) -- ); -+ this._monitor.set_rate_limit(UPDATE_TRASH_DELAY); -+ this._signalId = this._monitor.connect('changed', () => this._onTrashChange()); - } catch (e) { - if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) - return; -@@ -397,15 +395,18 @@ var Trash = class DashToDock_Trash { - } - - _onTrashChange() { -- if (this._schedUpdateId) { -- GLib.source_remove(this._schedUpdateId); -- } -+ if (this._schedUpdateId) -+ return; -+ -+ if (this._monitor.is_cancelled()) -+ return; -+ - this._schedUpdateId = GLib.timeout_add( - GLib.PRIORITY_LOW, UPDATE_TRASH_DELAY, () => { -- this._schedUpdateId = 0; -- this._updateTrash(); -- return GLib.SOURCE_REMOVE; -- }); -+ this._schedUpdateId = 0; -+ this._updateTrash(); -+ return GLib.SOURCE_REMOVE; -+ }); - } - - async _updateTrash() { - -From cd0a1eb5521197f1783e779378fd921d6bfd46c4 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Thu, 9 Dec 2021 00:05:17 +0100 -Subject: [PATCH 03/51] locations: Fix typo on FileManager app (un)wrapping - functions - ---- - docking.js | 6 +++--- - locations.js | 4 ++-- - 2 files changed, 5 insertions(+), 5 deletions(-) - -diff --git a/docking.js b/docking.js -index 20d5e76e7..6ae4f4a94 100644 ---- a/docking.js -+++ b/docking.js -@@ -1685,7 +1685,7 @@ var DockManager = class DashToDock_DockManager { - this._trash = null; - } - -- Locations.unWrapWindowsManagerApp(); -+ Locations.unWrapFileManagerApp(); - [this._methodInjections, this._vfuncInjections, this._propertyInjections].forEach( - injections => injections.removeWithLabel('locations')); - -@@ -1694,7 +1694,7 @@ var DockManager = class DashToDock_DockManager { - 'get_id', function () { return this.customId ?? this.vfunc_get_id() }); - - if (this.settings.isolateLocations) { -- const fileManagerApp = Locations.wrapWindowsManagerApp(); -+ const fileManagerApp = Locations.wrapFileManagerApp(); - - this._methodInjections.addWithLabel('locations', [ - Shell.AppSystem.prototype, 'get_running', -@@ -2366,7 +2366,7 @@ var DockManager = class DashToDock_DockManager { - } - this._trash?.destroy(); - this._trash = null; -- Locations.unWrapWindowsManagerApp(); -+ Locations.unWrapFileManagerApp(); - this._removables?.destroy(); - this._removables = null; - this._iconTheme.destroy(); -diff --git a/locations.js b/locations.js -index be29e4a5f..f826a8f63 100644 ---- a/locations.js -+++ b/locations.js -@@ -256,7 +256,7 @@ function getFileManagerApp() { - return Shell.AppSystem.get_default().lookup_app(FILE_MANAGER_DESKTOP_APP_ID); - } - --function wrapWindowsManagerApp() { -+function wrapFileManagerApp() { - const fileManagerApp = getFileManagerApp(); - if (!fileManagerApp) - return null; -@@ -308,7 +308,7 @@ function wrapWindowsManagerApp() { - return fileManagerApp; - } - --function unWrapWindowsManagerApp() { -+function unWrapFileManagerApp() { - const fileManagerApp = getFileManagerApp(); - if (!fileManagerApp || !fileManagerApp._dtdData) - return; - -From 52888a702c25d1338fba3a6f729ac8480524c29d Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Thu, 9 Dec 2021 01:52:10 +0100 -Subject: [PATCH 04/51] locations: Fix handling of location apps on isolated - workspaces mode - -Emit windows changed on workspace switches, as all shell apps do in such -case. ---- - locations.js | 3 +++ - 1 file changed, 3 insertions(+) - -diff --git a/locations.js b/locations.js -index f826a8f63..4d034dbfd 100644 ---- a/locations.js -+++ b/locations.js -@@ -242,10 +242,13 @@ function makeLocationApp(params) { - - const windowsChangedId = fm1Client.connect('windows-changed', () => - shellApp._updateWindows()); -+ const workspaceChangedId = global.workspaceManager.connect('workspace-switched', -+ () => shellApp.emit('windows-changed')); - - const parentDestroy = shellApp.destroy; - shellApp.destroy = function () { - fm1Client.disconnect(windowsChangedId); -+ global.workspaceManager.disconnect(workspaceChangedId); - parentDestroy.call(this); - } - - -From 296169066b8963411fd7c265920cfac62b27e73b Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Mon, 13 Dec 2021 16:11:23 +0100 -Subject: [PATCH 05/51] locations: Also destroy all removable devices on - Removables manager destruction - -We may not destroy the removable apps wrappers when deactivating the -removables manager ---- - locations.js | 3 +++ - 1 file changed, 3 insertions(+) - -diff --git a/locations.js b/locations.js -index 4d034dbfd..be7f76e20 100644 ---- a/locations.js -+++ b/locations.js -@@ -529,6 +529,9 @@ var Removables = class DashToDock_Removables { - } - - destroy() { -+ [...this._volumeApps, ...this._mountApps].forEach(a => a.destroy()); -+ this._volumeApps = []; -+ this._mountApps = []; - this._signalsHandler.destroy(); - this._monitor = null; - } - -From a716c1affb1ac1160f9274462cae232af4ce5123 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Mon, 13 Dec 2021 16:15:42 +0100 -Subject: [PATCH 06/51] locations: Only emit changed signal if a monitored - device has been removed - ---- - locations.js | 4 ++-- - 1 file changed, 2 insertions(+), 2 deletions(-) - -diff --git a/locations.js b/locations.js -index be7f76e20..7c860ba3b 100644 ---- a/locations.js -+++ b/locations.js -@@ -581,9 +581,9 @@ var Removables = class DashToDock_Removables { - if (app.get_name() == volume.get_name()) { - const [volumeApp] = this._volumeApps.splice(i, 1); - volumeApp.destroy(); -+ this.emit('changed'); - } - } -- this.emit('changed'); - } - - _onMountAdded(monitor, mount) { -@@ -633,9 +633,9 @@ var Removables = class DashToDock_Removables { - if (app.get_name() == mount.get_name()) { - const [mountApp] = this._mountApps.splice(i, 1); - mountApp.destroy(); -+ this.emit('changed'); - } - } -- this.emit('changed'); - } - - getApps() { - -From 9a45528de18331a8e9c5971cf1e6b5433a436554 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Wed, 10 Nov 2021 18:39:51 +0100 -Subject: [PATCH 07/51] locations: Only update Trash icon on changes - -There's no need to create a trash icon all the times that it changes, -we can just update its content dynamically by monkey-patching the -required methods. - -As per this also make possible to define actions easier, without having -to deal with static ones. ---- - appIcons.js | 2 ++ - dash.js | 3 -- - locations.js | 94 +++++++++++++++++++++++++++++----------------------- - 3 files changed, 55 insertions(+), 44 deletions(-) - -diff --git a/appIcons.js b/appIcons.js -index 29b5d563c..d536c38a6 100644 ---- a/appIcons.js -+++ b/appIcons.js -@@ -864,6 +864,8 @@ var DockLocationAppIcon = GObject.registerClass({ - this._signalsHandler.add(global.display, 'notify::focus-window', - () => this._updateFocusState()); - } -+ -+ this._signalsHandler.add(this.app, 'notify::icon', () => this.icon.update()); - } - - get location() { -diff --git a/dash.js b/dash.js -index 1024b258f..3ad75881f 100644 ---- a/dash.js -+++ b/dash.js -@@ -781,10 +781,7 @@ var DockDash = GObject.registerClass({ - oldApps = oldApps.filter(app => !app.location || app.isTrash) - } - -- this._signalsHandler.removeWithLabel('show-trash'); - if (dockManager.trash) { -- this._signalsHandler.addWithLabel('show-trash', -- dockManager.trash, 'changed', this._queueRedisplay.bind(this)); - const trashApp = dockManager.trash.getApp(); - if (!newApps.includes(trashApp)) - newApps.push(trashApp); -diff --git a/locations.js b/locations.js -index 7c860ba3b..772c6bccb 100644 ---- a/locations.js -+++ b/locations.js -@@ -90,6 +90,18 @@ function wrapWindowsBackedApp(shellApp, params = {}) { - shellApp._pi = p; // Property injector - shellApp._aMi = aM; // appInfo method Injector - -+ shellApp._setActions = function(actionsGetter) { -+ aM('list_actions', () => Object.keys(actionsGetter())); -+ aM('get_action_name', (_om, name) => actionsGetter()[name]?.name); -+ m('launch_action', (launchAction, actionName, ...args) => { -+ const actions = actionsGetter(); -+ if (actionName in actions) -+ actions[actionName].exec(...args); -+ else -+ return launchAction.call(shellApp, actionName, ...args); -+ }); -+ }; -+ - m('get_state', () => - shellApp.get_windows().length ? Shell.AppState.RUNNING : Shell.AppState.STOPPED); - p('state', { get: () => shellApp.get_state() }); -@@ -422,7 +434,7 @@ var Trash = class DashToDock_Trash { - const children = await childrenEnumerator.next_files_async(1, - priority, cancellable); - this._empty = !children.length; -- this._ensureApp(); -+ this._updateApp(); - - await childrenEnumerator.close_async(priority, null); - } catch (e) { -@@ -431,49 +443,50 @@ var Trash = class DashToDock_Trash { - } - } - -+ _updateApp() { -+ if (this._lastEmpty === this._empty) -+ return -+ -+ this._lastEmpty = this._empty; -+ this._trashApp?.notify('icon'); -+ } -+ - _ensureApp() { -- if (this._trashApp == null || -- this._lastEmpty !== this._empty) { -- let trashKeys = new GLib.KeyFile(); -- trashKeys.set_string('Desktop Entry', 'Name', __('Trash')); -- trashKeys.set_string('Desktop Entry', 'Type', 'Application'); -- trashKeys.set_string('Desktop Entry', 'Exec', 'gio open %s'.format(TRASH_URI)); -- trashKeys.set_string('Desktop Entry', 'StartupNotify', 'false'); -- if (!this._empty) { -- trashKeys.set_string('Desktop Entry', 'Actions', 'empty-trash;'); -- trashKeys.set_string('Desktop Action empty-trash', 'Name', __('Empty Trash')); -- trashKeys.set_string('Desktop Action empty-trash', 'Exec', 'true'); -- } -+ if (this._trashApp) -+ return; - -- let trashAppInfo = Gio.DesktopAppInfo.new_from_keyfile(trashKeys); -- this._trashApp?.destroy(); -- this._trashApp = makeLocationApp({ -- location: TRASH_URI + '/', -- appInfo: trashAppInfo, -- gicon: Gio.ThemedIcon.new(this._empty ? 'user-trash' : 'user-trash-full'), -- }); -+ const trashKeys = new GLib.KeyFile(); -+ trashKeys.set_string('Desktop Entry', 'Name', __('Trash')); -+ trashKeys.set_string('Desktop Entry', 'Type', 'Application'); -+ trashKeys.set_string('Desktop Entry', 'Exec', 'gio open %s'.format(TRASH_URI)); -+ trashKeys.set_string('Desktop Entry', 'StartupNotify', 'false'); - -- if (!this._empty) { -- this._trashApp._mi('launch_action', -- (launchAction, actionName, timestamp, ...args) => { -- if (actionName === 'empty-trash') { -- const nautilus = makeNautilusFileOperationsProxy(); -- const askConfirmation = true; -- nautilus.EmptyTrashRemote(askConfirmation, -- nautilus.platformData({ timestamp }), (_p, error) => { -- if (error) -- logError(error, 'Empty trash failed'); -- }); -- return; -- } -- -- return launchAction.call(this, actionName, timestamp, ...args); -- }); -- } -- this._lastEmpty = this._empty; -+ const trashAppInfo = Gio.DesktopAppInfo.new_from_keyfile(trashKeys); -+ const trashIcon = () => Gio.ThemedIcon.new(this._empty ? -+ 'user-trash' : 'user-trash-full'); - -- this.emit('changed'); -- } -+ this._trashApp = makeLocationApp({ -+ location: TRASH_URI + '/', -+ appInfo: trashAppInfo, -+ gicon: trashIcon(), -+ }); -+ -+ this._trashApp._mi('get_icon', () => trashIcon()); -+ -+ this._trashApp._setActions(() => (this._empty ? {} : { -+ 'empty-trash': { -+ name: __('Empty Trash'), -+ exec: timestamp => { -+ const nautilus = makeNautilusFileOperationsProxy(); -+ const askConfirmation = true; -+ nautilus.EmptyTrashRemote(askConfirmation, -+ nautilus.platformData({ timestamp }), (_p, error) => { -+ if (error) -+ logError(error, 'Empty trash failed'); -+ }); -+ } -+ } -+ })); - } - - getApp() { -@@ -481,7 +494,6 @@ var Trash = class DashToDock_Trash { - return this._trashApp; - } - } --Signals.addSignalMethods(Trash.prototype); - - /** - * This class maintains Shell.App representations for removable devices - -From 4b45ffe2b98b400a09a80bb5162d10d0bd3ea131 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Wed, 10 Nov 2021 20:03:16 +0100 -Subject: [PATCH 08/51] locations: Remove unmounted locations by value - -We used to remove locations by name but this may lead to troubles, so -let's just attach them to the wrapped object so that we can compare with -actual objects. - -The data is also automatically unreferenced on destruction. ---- - locations.js | 28 ++++++++++++++-------------- - 1 file changed, 14 insertions(+), 14 deletions(-) - -diff --git a/locations.js b/locations.js -index 772c6bccb..1e8ed28b3 100644 ---- a/locations.js -+++ b/locations.js -@@ -583,18 +583,18 @@ var Removables = class DashToDock_Removables { - appInfo: volumeAppInfo, - gicon: volume.get_icon() || Gio.ThemedIcon.new(FALLBACK_REMOVABLE_MEDIA_ICON), - }); -+ volumeApp.appInfo.volume = volume; - this._volumeApps.push(volumeApp); - this.emit('changed'); - } - - _onVolumeRemoved(monitor, volume) { -- for (let i = 0; i < this._volumeApps.length; i++) { -- let app = this._volumeApps[i]; -- if (app.get_name() == volume.get_name()) { -- const [volumeApp] = this._volumeApps.splice(i, 1); -- volumeApp.destroy(); -- this.emit('changed'); -- } -+ const volumeIndex = this._volumeApps.findIndex(({ appInfo }) => -+ appInfo.volume === volume); -+ if (volumeIndex !== -1) { -+ const [volumeApp] = this._volumeApps.splice(volumeIndex, 1); -+ volumeApp.destroy(); -+ this.emit('changed'); - } - } - -@@ -635,18 +635,18 @@ var Removables = class DashToDock_Removables { - location: escapedUri, - gicon: mount.get_icon() || new Gio.ThemedIcon(FALLBACK_REMOVABLE_MEDIA_ICON), - }); -+ mountApp.appInfo.mount = mount; - this._mountApps.push(mountApp); - this.emit('changed'); - } - - _onMountRemoved(monitor, mount) { -- for (let i = 0; i < this._mountApps.length; i++) { -- let app = this._mountApps[i]; -- if (app.get_name() == mount.get_name()) { -- const [mountApp] = this._mountApps.splice(i, 1); -- mountApp.destroy(); -- this.emit('changed'); -- } -+ const mountIndex = this._mountApps.findIndex(({ appInfo }) => -+ appInfo.mount === mount); -+ if (mountIndex !== -1) { -+ const [mountApp] = this._mountApps.splice(mountIndex, 1); -+ mountApp.destroy(); -+ this.emit('changed'); - } - } - - -From 4dd314650a7f59142f27a23355edf53d8d940d39 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Tue, 7 Dec 2021 16:00:50 +0100 -Subject: [PATCH 09/51] locations: Use native AppInfo's and Shell mount - operations - -As per gjs 1.72 it won't be possible to inject an interface vfunc as it -used to work in previous versions, this would make gnome-shell to crash -as we may try to get the location "apps" ID which is now null (and not -customId anymore). - -So, let's re-implement the things in a cleaner way by actually using -real custom AppInfo's that are still based on Gio.DesktopAppInfo -(beause the shell will still need to be such type), but that are -implementing the needed virtual functions of the parent interface. - -As per this, we can finally use native shell mount operations to handle -mount options such as mounting encrypted disks. ---- - appIcons.js | 8 +- - docking.js | 5 +- - locations.js | 582 ++++++++++++++++++++++++++++++++++++++++----------- - 3 files changed, 464 insertions(+), 131 deletions(-) - -diff --git a/appIcons.js b/appIcons.js -index d536c38a6..00eac89b7 100644 ---- a/appIcons.js -+++ b/appIcons.js -@@ -31,6 +31,7 @@ const Workspace = imports.ui.workspace; - const ExtensionUtils = imports.misc.extensionUtils; - const Me = ExtensionUtils.getCurrentExtension(); - const Docking = Me.imports.docking; -+const Locations = Me.imports.locations; - const Utils = Me.imports.utils; - const WindowPreview = Me.imports.windowPreview; - const AppIconIndicators = Me.imports.appIconIndicators; -@@ -853,8 +854,8 @@ var DockAppIcon = GObject.registerClass({ - var DockLocationAppIcon = GObject.registerClass({ - }, class DockLocationAppIcon extends DockAbstractAppIcon { - _init(app, monitorIndex, iconAnimator) { -- if (!app.location) -- throw new Error('Provided application %s has no location'.format(app)); -+ if (!(app.appInfo instanceof Locations.LocationAppInfo)) -+ throw new Error('Provided application %s is not a Location'.format(app)); - - super._init(app, monitorIndex, iconAnimator); - -@@ -881,10 +882,9 @@ var DockLocationAppIcon = GObject.registerClass({ - }); - - function makeAppIcon(app, monitorIndex, iconAnimator) { -- if (app.location) -+ if (app.appInfo instanceof Locations.LocationAppInfo) - return new DockLocationAppIcon(app, monitorIndex, iconAnimator); - -- - return new DockAppIcon(app, monitorIndex, iconAnimator); - } - -diff --git a/docking.js b/docking.js -index 6ae4f4a94..cb80eb9ad 100644 ---- a/docking.js -+++ b/docking.js -@@ -1686,13 +1686,10 @@ var DockManager = class DashToDock_DockManager { - } - - Locations.unWrapFileManagerApp(); -- [this._methodInjections, this._vfuncInjections, this._propertyInjections].forEach( -+ [this._methodInjections, this._propertyInjections].forEach( - injections => injections.removeWithLabel('locations')); - - if (showMounts || showTrash) { -- this._vfuncInjections.addWithLabel('locations', Gio.DesktopAppInfo.prototype, -- 'get_id', function () { return this.customId ?? this.vfunc_get_id() }); -- - if (this.settings.isolateLocations) { - const fileManagerApp = Locations.wrapFileManagerApp(); - -diff --git a/locations.js b/locations.js -index 1e8ed28b3..c31a1f340 100644 ---- a/locations.js -+++ b/locations.js -@@ -5,6 +5,7 @@ const GLib = imports.gi.GLib; - const GObject = imports.gi.GObject; - const Gtk = imports.gi.Gtk; - const Shell = imports.gi.Shell; -+const ShellMountOperation = imports.ui.shellMountOperation; - const Signals = imports.signals; - const St = imports.gi.St; - -@@ -19,7 +20,9 @@ const Docking = Me.imports.docking; - const Utils = Me.imports.utils; - - const FALLBACK_REMOVABLE_MEDIA_ICON = 'drive-removable-media'; -+const FALLBACK_TRASH_ICON = 'user-trash'; - const FILE_MANAGER_DESKTOP_APP_ID = 'org.gnome.Nautilus.desktop'; -+const ATTRIBUTE_METADATA_CUSTOM_ICON = 'metadata::custom-icon'; - const TRASH_URI = 'trash://'; - const UPDATE_TRASH_DELAY = 1000; - -@@ -65,7 +68,390 @@ function makeNautilusFileOperationsProxy() { - return proxy; - } - --function wrapWindowsBackedApp(shellApp, params = {}) { -+var LocationAppInfo = GObject.registerClass({ -+ Implements: [Gio.AppInfo], -+ Properties: { -+ 'location': GObject.ParamSpec.object( -+ 'location', 'location', 'location', -+ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, -+ Gio.File.$gtype), -+ 'name': GObject.ParamSpec.string( -+ 'name', 'name', 'name', -+ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, -+ null), -+ 'icon': GObject.ParamSpec.object( -+ 'icon', 'icon', 'icon', -+ GObject.ParamFlags.READWRITE, -+ Gio.Icon.$gtype), -+ 'cancellable': GObject.ParamSpec.object( -+ 'cancellable', 'cancellable', 'cancellable', -+ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, -+ Gio.Cancellable.$gtype), -+ }, -+}, class LocationAppInfo extends Gio.DesktopAppInfo { -+ list_actions() { -+ return []; -+ } -+ -+ get_action_name() { -+ return null -+ } -+ -+ get_boolean() { -+ return false; -+ } -+ -+ vfunc_dup() { -+ return new LocationAppInfo({ -+ location: this.location, -+ name: this.name, -+ icon: this.icon, -+ cancellable: this.cancellable, -+ }); -+ } -+ -+ vfunc_equal(other) { -+ if (this.location) -+ return this.location.equal(other?.location); -+ -+ return this.name === other.name && -+ (this.icon ? this.icon.equal(other?.icon) : !other?.icon); -+ } -+ -+ vfunc_get_id() { -+ return 'location:%s'.format(this.location?.get_uri()); -+ } -+ -+ vfunc_get_name() { -+ return this.name; -+ } -+ -+ vfunc_get_description() { -+ return null; -+ } -+ -+ vfunc_get_executable() { -+ return null; -+ } -+ -+ vfunc_get_icon() { -+ return this.icon; -+ } -+ -+ vfunc_launch(files, context) { -+ if (files?.length) { -+ throw new GLib.Error(Gio.IOErrorEnum, -+ Gio.IOErrorEnum.NOT_SUPPORTED, 'Launching with files not supported'); -+ } -+ -+ const [ret] = GLib.spawn_async(null, this.get_commandline().split(' '), -+ context?.get_environment() || null, GLib.SpawnFlags.SEARCH_PATH, null); -+ return ret; -+ } -+ -+ vfunc_supports_uris() { -+ return false; -+ } -+ -+ vfunc_supports_files() { -+ return false; -+ } -+ -+ vfunc_launch_uris(uris, context) { -+ return this.launch(uris, context); -+ } -+ -+ vfunc_should_show() { -+ return true; -+ } -+ -+ vfunc_set_as_default_for_type() { -+ throw new GLib.Error(Gio.IOErrorEnum, -+ Gio.IOErrorEnum.NOT_SUPPORTED, 'Not supported'); -+ } -+ -+ vfunc_set_as_default_for_extension() { -+ throw new GLib.Error(Gio.IOErrorEnum, -+ Gio.IOErrorEnum.NOT_SUPPORTED, 'Not supported'); -+ } -+ -+ vfunc_add_supports_type() { -+ throw new GLib.Error(Gio.IOErrorEnum, -+ Gio.IOErrorEnum.NOT_SUPPORTED, 'Not supported'); -+ } -+ -+ vfunc_can_remove_supports_type() { -+ return false; -+ } -+ -+ vfunc_remove_supports_type() { -+ return false; -+ } -+ -+ vfunc_can_delete() { -+ return false; -+ } -+ -+ vfunc_do_delete() { -+ return false; -+ } -+ -+ vfunc_get_commandline() { -+ return 'gio open %s'.format(this.location?.get_uri()); -+ } -+ -+ vfunc_get_display_name() { -+ return this.name; -+ } -+ -+ vfunc_set_as_last_used_for_type() { -+ throw new GLib.Error(Gio.IOErrorEnum, -+ Gio.IOErrorEnum.NOT_SUPPORTED, 'Not supported'); -+ } -+ -+ vfunc_get_supported_types() { -+ return []; -+ } -+}); -+ -+const VolumeAppInfo = GObject.registerClass({ -+ Implements: [Gio.AppInfo], -+ Properties: { -+ 'volume': GObject.ParamSpec.object( -+ 'volume', 'volume', 'volume', -+ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, -+ Gio.Volume.$gtype), -+ }, -+}, -+class VolumeAppInfo extends LocationAppInfo { -+ _init(volume, cancellable = null) { -+ super._init({ -+ volume, -+ location: volume.get_activation_root(), -+ name: volume.get_name(), -+ icon: volume.get_icon(), -+ cancellable, -+ }); -+ } -+ -+ vfunc_dup() { -+ return new VolumeAppInfo({ -+ volume: this.volume, -+ cancellable: this.cancellable, -+ }); -+ } -+ -+ vfunc_get_id() { -+ const uuid = this.volume.get_uuid(); -+ return uuid ? 'volume:%s'.format(uuid) : super.vfunc_get_id(); -+ } -+ -+ vfunc_equal(other) { -+ if (this.volume === other?.volume) -+ return true; -+ -+ return this.get_id() === other?.get_id(); -+ } -+ -+ list_actions() { -+ const actions = []; -+ -+ if (this.volume.can_mount()) -+ actions.push('mount'); -+ if (this.volume.can_eject()) -+ actions.push('eject'); -+ -+ return actions; -+ } -+ -+ get_action_name(action) { -+ switch (action) { -+ case 'mount': -+ return __('Mount'); -+ case 'eject': -+ return __('Eject'); -+ default: -+ return null; -+ } -+ } -+ -+ async launchAction(action) { -+ if (!this.list_actions().includes(action)) -+ throw new Error('Action %s is not supported by %s', action, this); -+ -+ const operation = new ShellMountOperation.ShellMountOperation(this.volume); -+ try { -+ if (action === 'mount') { -+ await this.volume.mount(Gio.MountMountFlags.NONE, operation.mountOp, -+ this.cancellable); -+ } else if (action === 'eject') { -+ await this.volume.eject_with_operation(Gio.MountUnmountFlags.FORCE, -+ operation.mountOp, this.cancellable); -+ } -+ } catch (e) { -+ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED)) { -+ if (action === 'mount') { -+ global.notify_error(__("Failed to mount “%s”".format( -+ this.get_name())), e.message); -+ } else if (action === 'eject') { -+ global.notify_error(__("Failed to eject “%s”".format( -+ this.get_name())), e.message); -+ } -+ } -+ -+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { -+ logError(e, 'Impossible to %s volume %s'.format(action, -+ this.volume.get_name())); -+ } -+ } finally { -+ operation.close(); -+ } -+ } -+}); -+ -+const MountAppInfo = GObject.registerClass({ -+ Implements: [Gio.AppInfo], -+ Properties: { -+ 'mount': GObject.ParamSpec.object( -+ 'mount', 'mount', 'mount', -+ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, -+ Gio.Mount.$gtype), -+ }, -+}, -+class MountAppInfo extends LocationAppInfo { -+ _init(mount, cancellable = null) { -+ super._init({ -+ mount, -+ location: mount.get_default_location(), -+ name: mount.get_name(), -+ icon: mount.get_icon(), -+ cancellable, -+ }); -+ } -+ -+ vfunc_dup() { -+ return new MountAppInfo({ -+ mount: this.mount, -+ cancellable: this.cancellable, -+ }); -+ } -+ -+ vfunc_get_id() { -+ const uuid = this.mount.get_uuid() ?? this.mount.get_volume()?.get_uuid(); -+ return uuid ? 'mount:%s'.format(uuid) : super.vfunc_get_id(); -+ } -+ -+ vfunc_equal(other) { -+ if (this.mount === other?.mount) -+ return true; -+ -+ return this.get_id() === other?.get_id(); -+ } -+ -+ list_actions() { -+ const actions = []; -+ -+ if (this.mount.can_unmount()) -+ actions.push('unmount'); -+ if (this.mount.can_eject()) -+ actions.push('eject'); -+ -+ return actions; -+ } -+ -+ get_action_name(action) { -+ switch (action) { -+ case 'unmount': -+ return __('Unmount'); -+ case 'eject': -+ return __('Eject'); -+ default: -+ return null; -+ } -+ } -+ -+ async launchAction(action) { -+ if (!this.list_actions().includes(action)) -+ throw new Error('Action %s is not supported by %s', action, this); -+ -+ const operation = new ShellMountOperation.ShellMountOperation(this.mount); -+ try { -+ if (action === 'unmount') { -+ await this.mount.unmount_with_operation(Gio.MountUnmountFlags.FORCE, -+ operation.mountOp, this.cancellable); -+ } else if (action === 'eject') { -+ await this.mount.eject_with_operation(Gio.MountUnmountFlags.FORCE, -+ operation.mountOp, this.cancellable); -+ } -+ } catch (e) { -+ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED)) { -+ if (action === 'unmount') { -+ global.notify_error(__("Failed to umount “%s”".format( -+ this.get_name())), e.message); -+ } else if (action === 'eject') { -+ global.notify_error(__("Failed to eject “%s”".format( -+ this.get_name())), e.message); -+ } -+ } -+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { -+ logError(e, 'Impossible to %s mount %s'.format(action, -+ this.mount.get_name())); -+ } -+ } finally { -+ operation.close(); -+ } -+ } -+}); -+ -+const TrashAppInfo = GObject.registerClass({ -+ Implements: [Gio.AppInfo], -+ Properties: { -+ 'empty': GObject.ParamSpec.boolean( -+ 'empty', 'empty', 'empty', -+ GObject.ParamFlags.READWRITE, -+ true), -+ }, -+}, -+class TrashAppInfo extends LocationAppInfo { -+ _init(cancellable = null) { -+ super._init({ -+ location: Gio.file_new_for_uri(TRASH_URI), -+ name: __('Trash'), -+ cancellable, -+ }); -+ this.connect('notify::empty', () => -+ (this.icon = Gio.ThemedIcon.new(this.empty ? 'user-trash' : 'user-trash-full'))); -+ this.notify('empty'); -+ } -+ -+ list_actions() { -+ return this.empty ? [] : ['empty-trash']; -+ } -+ -+ get_action_name(action) { -+ switch (action) { -+ case 'empty-trash': -+ return __('Empty Trash'); -+ default: -+ return null; -+ } -+ } -+ -+ launchAction(action, timestamp) { -+ if (!this.list_actions().includes(action)) -+ throw new Error('Action %s is not supported by %s', action, this); -+ -+ const nautilus = makeNautilusFileOperationsProxy(); -+ const askConfirmation = true; -+ nautilus.EmptyTrashRemote(askConfirmation, -+ nautilus.platformData({ timestamp }), (_p, error) => { -+ if (error) -+ logError(error, 'Empty trash failed'); -+ }, this.cancellable); -+ } -+}); -+ -+function wrapWindowsBackedApp(shellApp) { - if (shellApp._dtdData) - throw new Error('%s has been already wrapped'.format(shellApp)); - -@@ -82,43 +468,13 @@ function wrapWindowsBackedApp(shellApp, params = {}) { - - const m = (...args) => shellApp._dtdData.methodInjections.add(shellApp, ...args); - const p = (...args) => shellApp._dtdData.propertyInjections.add(shellApp, ...args); -- const aM = (...args) => { -- if (shellApp.appInfo) -- shellApp._dtdData.methodInjections.add(shellApp.appInfo, ...args); -- } - shellApp._mi = m; // Method injector - shellApp._pi = p; // Property injector -- shellApp._aMi = aM; // appInfo method Injector -- -- shellApp._setActions = function(actionsGetter) { -- aM('list_actions', () => Object.keys(actionsGetter())); -- aM('get_action_name', (_om, name) => actionsGetter()[name]?.name); -- m('launch_action', (launchAction, actionName, ...args) => { -- const actions = actionsGetter(); -- if (actionName in actions) -- actions[actionName].exec(...args); -- else -- return launchAction.call(shellApp, actionName, ...args); -- }); -- }; - - m('get_state', () => - shellApp.get_windows().length ? Shell.AppState.RUNNING : Shell.AppState.STOPPED); - p('state', { get: () => shellApp.get_state() }); - -- if (params.gicon) { -- const { gicon } = params; -- m('get_icon', () => gicon); -- p('icon', { get: () => shellApp.get_icon() }); -- aM('get_icon', () => shellApp.get_icon()); -- -- m('create_icon_texture', (_om, icon_size) => new St.Icon({ -- icon_size, -- gicon: shellApp.icon, -- fallback_icon_name: FALLBACK_REMOVABLE_MEDIA_ICON, -- })); -- } -- - m('get_windows', () => shellApp._dtdData.windows); - m('get_n_windows', () => shellApp.get_windows().length); - m('get_pids', () => shellApp.get_windows().reduce((pids, w) => { -@@ -212,26 +568,36 @@ function wrapWindowsBackedApp(shellApp, params = {}) { - - // We can't inherit from Shell.App as it's a final type, so let's patch it - function makeLocationApp(params) { -- if (!params.location) -+ if (!(params?.appInfo instanceof LocationAppInfo)) - throw new TypeError('Invalid location'); - -- const location = params.location; -- const gicon = params.gicon; -- delete params.location; -- delete params.gicon; -+ const { fallbackIconName } = params; -+ delete params.fallbackIconName; - - const shellApp = new Shell.App(params); -- wrapWindowsBackedApp(shellApp, { gicon }); -- shellApp.appInfo.customId = 'location:%s'.format(location); -+ wrapWindowsBackedApp(shellApp); - - Object.defineProperties(shellApp, { -- location: { value: location }, -- isTrash: { value: location.startsWith(TRASH_URI) }, -+ location: { get: () => shellApp.appInfo.location }, -+ isTrash: { get: () => shellApp.appInfo instanceof TrashAppInfo }, - }); - - shellApp._mi('toString', defaultToString => - '[LocationApp - %s]'.format(defaultToString.call(shellApp))); - -+ shellApp._mi('launch', (_om, timestamp, workspace, _gpuPref) => -+ shellApp.appInfo.launch([], -+ global.create_app_launch_context(timestamp, workspace))); -+ -+ shellApp._mi('launch_action', (_om, actionName, ...args) => -+ shellApp.appInfo.launchAction(actionName, ...args)); -+ -+ shellApp._mi('create_icon_texture', (_om, iconSize) => new St.Icon({ -+ iconSize, -+ gicon: shellApp.icon, -+ fallbackIconName, -+ })); -+ - // FIXME: We need to add a new API to Nautilus to open new windows - shellApp._mi('can_open_new_window', () => false); - -@@ -239,7 +605,7 @@ function makeLocationApp(params) { - shellApp._updateWindows = function () { - const oldState = this.state; - const oldWindows = this.get_windows(); -- this._dtdData.windows = fm1Client.getWindows(this.location); -+ this._dtdData.windows = fm1Client.getWindows(this.location?.get_uri()); - - if (this.get_windows().length !== oldWindows.length || - this.get_windows().some((win, index) => win !== oldWindows[index])) -@@ -256,11 +622,14 @@ function makeLocationApp(params) { - shellApp._updateWindows()); - const workspaceChangedId = global.workspaceManager.connect('workspace-switched', - () => shellApp.emit('windows-changed')); -+ const iconChangedId = shellApp.appInfo.connect('notify::icon', () => -+ shellApp.notify('icon')); - - const parentDestroy = shellApp.destroy; - shellApp.destroy = function () { - fm1Client.disconnect(windowsChangedId); - global.workspaceManager.disconnect(workspaceChangedId); -+ shellApp.appInfo.disconnect(iconChangedId); - parentDestroy.call(this); - } - -@@ -369,8 +738,6 @@ function shellAppCompare(app, other) { - * up-to-date as the trash fills and is emptied over time. - */ - var Trash = class DashToDock_Trash { -- _promisified = false; -- - static initPromises() { - if (Trash._promisified) - return; -@@ -395,7 +762,6 @@ var Trash = class DashToDock_Trash { - return; - logError(e, 'Impossible to monitor trash'); - } -- this._empty = true; - this._schedUpdateId = 0; - this._updateTrash(); - } -@@ -433,8 +799,7 @@ var Trash = class DashToDock_Trash { - priority, cancellable); - const children = await childrenEnumerator.next_files_async(1, - priority, cancellable); -- this._empty = !children.length; -- this._updateApp(); -+ this._updateApp(!children.length); - - await childrenEnumerator.close_async(priority, null); - } catch (e) { -@@ -443,50 +808,21 @@ var Trash = class DashToDock_Trash { - } - } - -- _updateApp() { -- if (this._lastEmpty === this._empty) -+ _updateApp(isEmpty) { -+ if (!this._trashApp) - return - -- this._lastEmpty = this._empty; -- this._trashApp?.notify('icon'); -+ this._trashApp.appInfo.empty = isEmpty; - } - - _ensureApp() { - if (this._trashApp) - return; - -- const trashKeys = new GLib.KeyFile(); -- trashKeys.set_string('Desktop Entry', 'Name', __('Trash')); -- trashKeys.set_string('Desktop Entry', 'Type', 'Application'); -- trashKeys.set_string('Desktop Entry', 'Exec', 'gio open %s'.format(TRASH_URI)); -- trashKeys.set_string('Desktop Entry', 'StartupNotify', 'false'); -- -- const trashAppInfo = Gio.DesktopAppInfo.new_from_keyfile(trashKeys); -- const trashIcon = () => Gio.ThemedIcon.new(this._empty ? -- 'user-trash' : 'user-trash-full'); -- - this._trashApp = makeLocationApp({ -- location: TRASH_URI + '/', -- appInfo: trashAppInfo, -- gicon: trashIcon(), -+ appInfo: new TrashAppInfo(this._cancellable), -+ fallbackIconName: FALLBACK_TRASH_ICON, - }); -- -- this._trashApp._mi('get_icon', () => trashIcon()); -- -- this._trashApp._setActions(() => (this._empty ? {} : { -- 'empty-trash': { -- name: __('Empty Trash'), -- exec: timestamp => { -- const nautilus = makeNautilusFileOperationsProxy(); -- const askConfirmation = true; -- nautilus.EmptyTrashRemote(askConfirmation, -- nautilus.platformData({ timestamp }), (_p, error) => { -- if (error) -- logError(error, 'Empty trash failed'); -- }); -- } -- } -- })); - } - - getApp() { -@@ -502,10 +838,36 @@ var Trash = class DashToDock_Trash { - */ - var Removables = class DashToDock_Removables { - -+ static initVolumePromises(object) { -+ // TODO: This can be simplified using actual interface type when we -+ // can depend on gjs 1.72 -+ if (!(object instanceof Gio.Volume) || object.constructor.prototype._d2dPromisified) -+ return; -+ -+ Gio._promisify(object.constructor.prototype, 'mount', 'mount_finish'); -+ Gio._promisify(object.constructor.prototype, 'eject_with_operation', -+ 'eject_with_operation_finish'); -+ object.constructor.prototype._d2dPromisified = true; -+ } -+ -+ static initMountPromises(object) { -+ // TODO: This can be simplified using actual interface type when we -+ // can depend on gjs 1.72 -+ if (!(object instanceof Gio.Mount) || object.constructor.prototype._d2dPromisified) -+ return; -+ -+ Gio._promisify(object.constructor.prototype, 'eject_with_operation', -+ 'eject_with_operation_finish'); -+ Gio._promisify(object.constructor.prototype, 'unmount_with_operation', -+ 'unmount_with_operation_finish'); -+ object.constructor.prototype._d2dPromisified = true; -+ } -+ - constructor() { - this._signalsHandler = new Utils.GlobalSignalsHandler(); - - this._monitor = Gio.VolumeMonitor.get(); -+ this._cancellable = new Gio.Cancellable(); - this._volumeApps = [] - this._mountApps = [] - -@@ -544,12 +906,19 @@ var Removables = class DashToDock_Removables { - [...this._volumeApps, ...this._mountApps].forEach(a => a.destroy()); - this._volumeApps = []; - this._mountApps = []; -+ this._cancellable.cancel(); -+ this._cancellable = null; - this._signalsHandler.destroy(); - this._monitor = null; - } - - _onVolumeAdded(monitor, volume) { -- if (!volume.can_mount()) { -+ Removables.initVolumePromises(volume); -+ -+ if (volume.get_mount()) -+ return; -+ -+ if (!volume.can_mount() && !volume.can_eject()) { - return; - } - -@@ -557,8 +926,7 @@ var Removables = class DashToDock_Removables { - return; - } - -- let activationRoot = volume.get_activation_root(); -- if (!activationRoot) { -+ if (!volume.get_activation_root()) { - // Can't offer to mount a device if we don't know - // where to mount it. - // These devices are usually ejectable so you -@@ -566,24 +934,11 @@ var Removables = class DashToDock_Removables { - return; - } - -- let escapedUri = activationRoot.get_uri() -- let uri = GLib.uri_unescape_string(escapedUri, null); -- -- let volumeKeys = new GLib.KeyFile(); -- volumeKeys.set_string('Desktop Entry', 'Name', volume.get_name()); -- volumeKeys.set_string('Desktop Entry', 'Type', 'Application'); -- volumeKeys.set_string('Desktop Entry', 'Exec', 'gio open "' + uri + '"'); -- volumeKeys.set_string('Desktop Entry', 'StartupNotify', 'false'); -- volumeKeys.set_string('Desktop Entry', 'Actions', 'mount;'); -- volumeKeys.set_string('Desktop Action mount', 'Name', __('Mount')); -- volumeKeys.set_string('Desktop Action mount', 'Exec', 'gio mount "' + uri + '"'); -- let volumeAppInfo = Gio.DesktopAppInfo.new_from_keyfile(volumeKeys); -+ const appInfo = new VolumeAppInfo(volume, this._cancellable); - const volumeApp = makeLocationApp({ -- location: escapedUri, -- appInfo: volumeAppInfo, -- gicon: volume.get_icon() || Gio.ThemedIcon.new(FALLBACK_REMOVABLE_MEDIA_ICON), -+ appInfo, -+ fallbackIconName: FALLBACK_REMOVABLE_MEDIA_ICON, - }); -- volumeApp.appInfo.volume = volume; - this._volumeApps.push(volumeApp); - this.emit('changed'); - } -@@ -599,6 +954,8 @@ var Removables = class DashToDock_Removables { - } - - _onMountAdded(monitor, mount) { -+ Removables.initMountPromises(mount); -+ - // Filter out uninteresting mounts - if (!mount.can_eject() && !mount.can_unmount()) - return; -@@ -610,32 +967,11 @@ var Removables = class DashToDock_Removables { - return; - } - -- const escapedUri = mount.get_default_location().get_uri() -- let uri = GLib.uri_unescape_string(escapedUri, null); -- -- let mountKeys = new GLib.KeyFile(); -- mountKeys.set_string('Desktop Entry', 'Name', mount.get_name()); -- mountKeys.set_string('Desktop Entry', 'Type', 'Application'); -- mountKeys.set_string('Desktop Entry', 'Exec', 'gio open "' + uri + '"'); -- mountKeys.set_string('Desktop Entry', 'StartupNotify', 'false'); -- mountKeys.set_string('Desktop Entry', 'Actions', 'unmount;'); -- if (mount.can_eject()) { -- mountKeys.set_string('Desktop Action unmount', 'Name', __('Eject')); -- mountKeys.set_string('Desktop Action unmount', 'Exec', -- 'gio mount -e "' + uri + '"'); -- } else { -- mountKeys.set_string('Desktop Entry', 'Actions', 'unmount;'); -- mountKeys.set_string('Desktop Action unmount', 'Name', __('Unmount')); -- mountKeys.set_string('Desktop Action unmount', 'Exec', -- 'gio mount -u "' + uri + '"'); -- } -- let mountAppInfo = Gio.DesktopAppInfo.new_from_keyfile(mountKeys); -+ const appInfo = new MountAppInfo(mount, this._cancellable); - const mountApp = makeLocationApp({ -- appInfo: mountAppInfo, -- location: escapedUri, -- gicon: mount.get_icon() || new Gio.ThemedIcon(FALLBACK_REMOVABLE_MEDIA_ICON), -+ appInfo, -+ fallbackIconName: FALLBACK_REMOVABLE_MEDIA_ICON, - }); -- mountApp.appInfo.mount = mount; - this._mountApps.push(mountApp); - this.emit('changed'); - } - -From 252ef24ec9fb9befffd37305231d3afe12e39c89 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Tue, 22 Feb 2022 23:28:28 +0100 -Subject: [PATCH 10/51] docking: Ignore left/right corner changes if not - available - -They are gone in GNOME 42 ---- - docking.js | 9 ++++++--- - 1 file changed, 6 insertions(+), 3 deletions(-) - -diff --git a/docking.js b/docking.js -index cb80eb9ad..a7620ff69 100644 ---- a/docking.js -+++ b/docking.js -@@ -2379,9 +2379,12 @@ var DockManager = class DashToDock_DockManager { - } - - /** -- * Adjust Panel corners -+ * Adjust Panel corners, remove this when 41 won't be supported anymore - */ - _adjustPanelCorners() { -+ if (!Main.panel._rightCorner || !Main.panel._leftCorner) -+ return; -+ - let position = Utils.getPosition(); - let isHorizontal = ((position == St.Side.TOP) || (position == St.Side.BOTTOM)); - let dockOnPrimary = this._settings.multiMonitor || -@@ -2396,8 +2399,8 @@ var DockManager = class DashToDock_DockManager { - } - - _revertPanelCorners() { -- Main.panel._leftCorner.show(); -- Main.panel._rightCorner.show(); -+ Main.panel._leftCorner?.show(); -+ Main.panel._rightCorner?.show(); - } - }; - Signals.addSignalMethods(DockManager.prototype); - -From f7744302adf93d75adba12ab92b78d02b0ee1a28 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Wed, 23 Feb 2022 15:32:12 +0100 -Subject: [PATCH 11/51] prefs: Add support to show in GNOME 42 libadwaita-based - dialog - ---- - prefs.js | 13 ++++++++++--- - 1 file changed, 10 insertions(+), 3 deletions(-) - -diff --git a/prefs.js b/prefs.js -index e903700db..4de4e69a6 100644 ---- a/prefs.js -+++ b/prefs.js -@@ -26,11 +26,13 @@ try { - imports.searchPath.push('resource:///org/gnome/Extensions/js'); - } - -+const Config = imports.misc.config; - const ExtensionUtils = imports.misc.extensionUtils; - const Me = ExtensionUtils.getCurrentExtension(); - - const SCALE_UPDATE_TIMEOUT = 500; - const DEFAULT_ICONS_SIZES = [128, 96, 64, 48, 32, 24, 16]; -+const [SHELL_VERSION] = Config?.PACKAGE_VERSION?.split('.') ?? [undefined]; - - const TransparencyMode = { - DEFAULT: 0, -@@ -202,14 +204,19 @@ var Settings = GObject.registerClass({ - this._builder.add_from_file('./Settings.ui'); - } - -- this.widget = new Gtk.ScrolledWindow({ hscrollbar_policy: Gtk.PolicyType.NEVER }); -+ this.widget = new Gtk.ScrolledWindow({ -+ hscrollbar_policy: Gtk.PolicyType.NEVER, -+ vscrollbar_policy: (SHELL_VERSION >= 42) ? -+ Gtk.PolicyType.NEVER : Gtk.PolicyType.AUTOMATIC, -+ }); - this._notebook = this._builder.get_object('settings_notebook'); - this.widget.set_child(this._notebook); - - // Set a reasonable initial window height - this.widget.connect('realize', () => { -- const window = this.widget.get_root(); -- window.set_size_request(-1, 750); -+ this.widget.get_root().set_size_request(-1, 850); -+ if (SHELL_VERSION >= 42) -+ this.widget.set_size_request(-1, 850); - }); - - // Timeout to delay the update of the settings - -From fbd85bcab02a58b3db779d72a96317a6ff1a247c Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Wed, 23 Feb 2022 15:33:40 +0100 -Subject: [PATCH 12/51] utils: Support vfunc injections on newer GObject - classes as per gjs 1.71 - -We need to hookup the prototype too in such classes, so let's do this. ---- - utils.js | 13 ++++++++++--- - 1 file changed, 10 insertions(+), 3 deletions(-) - -diff --git a/utils.js b/utils.js -index 7c4988052..11f7015f4 100644 ---- a/utils.js -+++ b/utils.js -@@ -260,7 +260,7 @@ var VFuncInjectionsHandler = class DashToDock_VFuncInjectionsHandler extends Bas - const original = prototype[`vfunc_${name}`]; - if (!(original instanceof Function)) - throw new Error(`Virtual function ${name} is not available for ${prototype}`); -- prototype[Gi.hook_up_vfunc_symbol](name, injectedFunction); -+ this._replaceVFunc(prototype, name, injectedFunction); - return [prototype, name]; - } - -@@ -271,10 +271,10 @@ var VFuncInjectionsHandler = class DashToDock_VFuncInjectionsHandler extends Bas - // This may fail if trying to reset to a never-overridden vfunc - // as gjs doesn't consider it a function, even if it's true that - // originalVFunc instanceof Function. -- prototype[Gi.hook_up_vfunc_symbol](name, originalVFunc); -+ this._replaceVFunc(prototype, name, originalVFunc); - } catch { - try { -- prototype[Gi.hook_up_vfunc_symbol](name, function (...args) { -+ this._replaceVFunc(prototype, name, function (...args) { - return originalVFunc.call(this, ...args); - }); - } catch (e) { -@@ -282,6 +282,13 @@ var VFuncInjectionsHandler = class DashToDock_VFuncInjectionsHandler extends Bas - } - } - } -+ -+ _replaceVFunc(prototype, name, func) { -+ if (Gi.gobject_prototype_symbol && Gi.gobject_prototype_symbol in prototype) -+ prototype = prototype[Gi.gobject_prototype_symbol]; -+ -+ return prototype[Gi.hook_up_vfunc_symbol](name, func); -+ } - }; - - /** - -From 5ad140ca227aad58686e4ec016357ddf383864f4 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Mon, 24 Jan 2022 06:28:18 +0100 -Subject: [PATCH 13/51] metadata: Add support for gnome-shell 42 - -It's now possible to safely support g-s 42 ---- - metadata.json | 3 ++- - 1 file changed, 2 insertions(+), 1 deletion(-) - -diff --git a/metadata.json b/metadata.json -index 88faed7ba..d4d9e0355 100644 ---- a/metadata.json -+++ b/metadata.json -@@ -1,7 +1,8 @@ - { - "shell-version": [ - "40", -- "41" -+ "41", -+ "42" - ], - "uuid": "dash-to-dock@micxgx.gmail.com", - "name": "Dash to Dock", - -From 609143de01ea532a353e9d193dda1e10969de003 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Wed, 8 Dec 2021 21:07:00 +0100 -Subject: [PATCH 14/51] fileManager1API: Cleanup the code to get windows from - location path - -Also move the function to get the Meta windows by their GTK object path -to utilities as that's a generic function that in theory could be used -for other features. ---- - fileManager1API.js | 81 +++++++++++++++++----------------------------- - utils.js | 20 ++++++++++++ - 2 files changed, 50 insertions(+), 51 deletions(-) - -diff --git a/fileManager1API.js b/fileManager1API.js -index c89a8aa7b..382e94e6f 100644 ---- a/fileManager1API.js -+++ b/fileManager1API.js -@@ -26,7 +26,7 @@ var FileManager1Client = class DashToDock_FileManager1Client { - this._signalsHandler = new Utils.GlobalSignalsHandler(); - this._cancellable = new Gio.Cancellable(); - -- this._locationMap = new Map(); -+ this._windowsByLocation = new Map(); - this._proxy = new FileManager1Proxy(Gio.DBus.session, - "org.freedesktop.FileManager1", - "/org/freedesktop/FileManager1", -@@ -72,19 +72,16 @@ var FileManager1Client = class DashToDock_FileManager1Client { - * sub-directories of that location. - */ - getWindows(location) { -- const ret = new Set(); -- let locationEsc = location; -- if (!location.endsWith('/')) -- locationEsc += '/'; -- -- for (let [k,v] of this._locationMap) { -- if ((k + '/').startsWith(locationEsc)) { -- for (let l of v) { -- ret.add(l); -- } -- } -- } -- return Array.from(ret); -+ if (!location) -+ return []; -+ -+ location += location.endsWith('/') ? '' : '/'; -+ const windows = []; -+ this._windowsByLocation.forEach((wins, l) => { -+ if (l.startsWith(location)) -+ windows.push(...wins); -+ }); -+ return [...new Set(windows)]; - } - - _onPropertyChanged(proxy, changed, invalidated) { -@@ -108,45 +105,27 @@ var FileManager1Client = class DashToDock_FileManager1Client { - } - - _updateFromPaths() { -- let pathToLocations = this._proxy.OpenWindowsWithLocations; -- let pathToWindow = getPathToWindow(); -- -- let locationToWindow = new Map(); -- for (let path in pathToLocations) { -- let locations = pathToLocations[path]; -- for (let i = 0; i < locations.length; i++) { -- let l = locations[i]; -- // Use a set to deduplicate when a window has a -- // location open in multiple tabs. -- if (!locationToWindow.has(l)) { -- locationToWindow.set(l, new Set()); -+ const locationsByWindowsPath = this._proxy.OpenWindowsWithLocations; -+ const windowsByPath = Utils.getWindowsByObjectPath(); -+ -+ this._windowsByLocation = new Map(); -+ Object.entries(locationsByWindowsPath).forEach(([windowPath, locations]) => { -+ locations.forEach(location => { -+ const window = windowsByPath.get(windowPath); -+ -+ if (window) { -+ location += location.endsWith('/') ? '' : '/'; -+ // Use a set to deduplicate when a window has a -+ // location open in multiple tabs. -+ const windows = this._windowsByLocation.get(location) || new Set(); -+ windows.add(window); -+ -+ if (windows.size === 1) -+ this._windowsByLocation.set(location, windows); - } -- let window = pathToWindow.get(path); -- if (window != null) { -- locationToWindow.get(l).add(window); -- } -- } -- } -- this._locationMap = locationToWindow; -+ }); -+ }); - this.emit('windows-changed'); - } - } - Signals.addSignalMethods(FileManager1Client.prototype); -- --/** -- * Construct a map of gtk application window object paths to MetaWindows. -- */ --function getPathToWindow() { -- let pathToWindow = new Map(); -- -- for (let i = 0; i < global.workspace_manager.n_workspaces; i++) { -- let ws = global.workspace_manager.get_workspace_by_index(i); -- ws.list_windows().map(function(w) { -- let path = w.get_gtk_window_object_path(); -- if (path != null) { -- pathToWindow.set(path, w); -- } -- }); -- } -- return pathToWindow; --} -diff --git a/utils.js b/utils.js -index 11f7015f4..adea7c496 100644 ---- a/utils.js -+++ b/utils.js -@@ -417,3 +417,23 @@ var IconTheme = class DashToDockIconTheme { - this._iconTheme = null; - } - } -+ -+/** -+ * Construct a map of gtk application window object paths to MetaWindows. -+ */ -+function getWindowsByObjectPath() { -+ const windowsByObjectPath = new Map(); -+ const { workspaceManager } = global; -+ const workspaces = [...new Array(workspaceManager.nWorkspaces)].map( -+ (_c, i) => workspaceManager.get_workspace_by_index(i)); -+ -+ workspaces.forEach(ws => { -+ ws.list_windows().forEach(w => { -+ const path = w.get_gtk_window_object_path(); -+ if (path != null) -+ windowsByObjectPath.set(path, w); -+ }); -+ }); -+ -+ return windowsByObjectPath; -+} - -From c991dcddecb2ac0750114395fdccab63659da7e5 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Wed, 8 Dec 2021 23:52:16 +0100 -Subject: [PATCH 15/51] fileManager1API: Keep windows paths cached so we can - monitor changes - -We can avoid notifying about various changes the location apps when no -changes are happening on windows, so let's keep track of them. ---- - fileManager1API.js | 51 +++++++++++++++++++++++++++++++++++++++------- - 1 file changed, 44 insertions(+), 7 deletions(-) - -diff --git a/fileManager1API.js b/fileManager1API.js -index 382e94e6f..8f79b981d 100644 ---- a/fileManager1API.js -+++ b/fileManager1API.js -@@ -1,5 +1,6 @@ - // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- - -+const GLib = imports.gi.GLib; - const Gio = imports.gi.Gio; - const Signals = imports.signals; - -@@ -26,6 +27,7 @@ var FileManager1Client = class DashToDock_FileManager1Client { - this._signalsHandler = new Utils.GlobalSignalsHandler(); - this._cancellable = new Gio.Cancellable(); - -+ this._windowsByPath = new Map(); - this._windowsByLocation = new Map(); - this._proxy = new FileManager1Proxy(Gio.DBus.session, - "org.freedesktop.FileManager1", -@@ -35,7 +37,9 @@ var FileManager1Client = class DashToDock_FileManager1Client { - if (error) { - if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) - global.log(error); -+ return; - } else { -+ this._updateWindows(); - this._updateLocationMap(); - } - }, this._cancellable); -@@ -47,23 +51,33 @@ var FileManager1Client = class DashToDock_FileManager1Client { - ], [ - // We must additionally listen for Screen events to know when to - // rebuild our location map when the set of available windows changes. -- global.workspace_manager, -- 'workspace-switched', -- this._updateLocationMap.bind(this) -+ global.workspaceManager, -+ 'workspace-added', -+ () => this._onWindowsChanged(), -+ ], [ -+ global.workspaceManager, -+ 'workspace-removed', -+ () => this._onWindowsChanged(), - ], [ - global.display, - 'window-entered-monitor', -- this._updateLocationMap.bind(this) -+ () => this._onWindowsChanged(), - ], [ - global.display, - 'window-left-monitor', -- this._updateLocationMap.bind(this) -+ () => this._onWindowsChanged(), - ]); - } - - destroy() { -+ if (this._windowsUpdateIdle) { -+ GLib.source_remove(this._windowsUpdateIdle); -+ delete this._windowsUpdateIdle; -+ } - this._cancellable.cancel(); - this._signalsHandler.destroy(); -+ this._windowsByLocation.clear(); -+ this._windowsByPath.clear() - this._proxy = null; - } - -@@ -92,6 +106,30 @@ var FileManager1Client = class DashToDock_FileManager1Client { - } - } - -+ _updateWindows() { -+ const oldSize = this._windowsByPath.size; -+ const oldPaths = this._windowsByPath.keys(); -+ this._windowsByPath = Utils.getWindowsByObjectPath(); -+ -+ if (oldSize != this._windowsByPath.size) -+ return true; -+ -+ return [...oldPaths].some(path => !this._windowsByPath.has(path)); -+ } -+ -+ _onWindowsChanged() { -+ if (this._windowsUpdateIdle) -+ return; -+ -+ this._windowsUpdateIdle = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { -+ if (this._updateWindows()) -+ this._updateLocationMap(); -+ -+ delete this._windowsUpdateIdle; -+ return GLib.SOURCE_REMOVE; -+ }); -+ } -+ - _updateLocationMap() { - let properties = this._proxy.get_cached_property_names(); - if (properties == null) { -@@ -106,12 +144,11 @@ var FileManager1Client = class DashToDock_FileManager1Client { - - _updateFromPaths() { - const locationsByWindowsPath = this._proxy.OpenWindowsWithLocations; -- const windowsByPath = Utils.getWindowsByObjectPath(); - - this._windowsByLocation = new Map(); - Object.entries(locationsByWindowsPath).forEach(([windowPath, locations]) => { - locations.forEach(location => { -- const window = windowsByPath.get(windowPath); -+ const window = this._windowsByPath.get(windowPath); - - if (window) { - location += location.endsWith('/') ? '' : '/'; - -From e3febfca9eb34387ff27a16e928cbc7390fba5db Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Thu, 9 Dec 2021 00:52:22 +0100 -Subject: [PATCH 16/51] utils: Add support for (un)blocking signals handlers - ---- - utils.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ - 1 file changed, 44 insertions(+) - -diff --git a/utils.js b/utils.js -index adea7c496..293640e05 100644 ---- a/utils.js -+++ b/utils.js -@@ -50,6 +50,14 @@ const BasicHandler = class DashToDock_BasicHandler { - this.removeWithLabel(label); - } - -+ block() { -+ Object.keys(this._storage).forEach(label => this.blockWithLabel(label)); -+ } -+ -+ unblock() { -+ Object.keys(this._storage).forEach(label => this.unblockWithLabel(label)); -+ } -+ - addWithLabel(label, ...args) { - let argsArray = [...args]; - if (argsArray.every(arg => !Array.isArray(arg))) -@@ -76,6 +84,14 @@ const BasicHandler = class DashToDock_BasicHandler { - delete this._storage[label]; - } - -+ blockWithLabel(label) { -+ (this._storage[label] || []).forEach(item => this._block(item)); -+ } -+ -+ unblockWithLabel(label) { -+ (this._storage[label] || []).forEach(item => this._unblock(item)); -+ } -+ - // Virtual methods to be implemented by subclass - - /** -@@ -91,6 +107,20 @@ const BasicHandler = class DashToDock_BasicHandler { - _remove(_item) { - throw new GObject.NotImplementedError(`_remove in ${this.constructor.name}`); - } -+ -+ /** -+ * Block single element -+ */ -+ _block(_item) { -+ throw new GObject.NotImplementedError(`_block in ${this.constructor.name}`); -+ } -+ -+ /** -+ * Unblock single element -+ */ -+ _unblock(_item) { -+ throw new GObject.NotImplementedError(`_unblock in ${this.constructor.name}`); -+ } - }; - - /** -@@ -120,6 +150,20 @@ var GlobalSignalsHandler = class DashToDock_GlobalSignalHandler extends BasicHan - const [object, id] = item; - object.disconnect(id); - } -+ -+ _block(item) { -+ const [object, id] = item; -+ -+ if (object instanceof GObject.Object) -+ GObject.Object.prototype.block_signal_handler.call(object, id); -+ } -+ -+ _unblock(item) { -+ const [object, id] = item; -+ -+ if (object instanceof GObject.Object) -+ GObject.Object.prototype.unblock_signal_handler.call(object, id); -+ } - }; - - /** - -From d6f9bb937ca3bf6710e787053ad5508a44b8bddf Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Thu, 9 Dec 2021 03:20:44 +0100 -Subject: [PATCH 17/51] locations: Smart managing wrapped windows backed - applications signal connections - -Just use the signal connections handler for managing the signal handlers -for location apps. - -We are already managing various connections manually, but we can avoid -doing this. ---- - locations.js | 39 +++++++++++++-------------------------- - 1 file changed, 13 insertions(+), 26 deletions(-) - -diff --git a/locations.js b/locations.js -index c31a1f340..2c89045ff 100644 ---- a/locations.js -+++ b/locations.js -@@ -457,10 +457,12 @@ function wrapWindowsBackedApp(shellApp) { - - shellApp._dtdData = { - windows: [], -+ signalConnections: new Utils.GlobalSignalsHandler(), - methodInjections: new Utils.InjectionsHandler(), - propertyInjections: new Utils.PropertyInjectionsHandler(), - destroy: function () { - this.windows = []; -+ this.signalConnections.destroy(); - this.methodInjections.destroy(); - this.propertyInjections.destroy(); - } -@@ -470,6 +472,7 @@ function wrapWindowsBackedApp(shellApp) { - const p = (...args) => shellApp._dtdData.propertyInjections.add(shellApp, ...args); - shellApp._mi = m; // Method injector - shellApp._pi = p; // Property injector -+ shellApp._signalConnections = shellApp._dtdData.signalConnections; - - m('get_state', () => - shellApp.get_windows().length ? Shell.AppState.RUNNING : Shell.AppState.STOPPED); -@@ -509,7 +512,7 @@ function wrapWindowsBackedApp(shellApp) { - } - - shellApp._checkFocused(); -- const focusWindowNotifyId = global.display.connect('notify::focus-window', () => -+ shellApp._signalConnections.add(global.display, 'notify::focus-window', () => - shellApp._checkFocused()); - - // Re-implements shell_app_activate_window for generic activation and alt-tab support -@@ -556,7 +559,6 @@ function wrapWindowsBackedApp(shellApp) { - m('compare', (_om, other) => shellAppCompare(shellApp, other)); - - shellApp.destroy = function() { -- global.display.disconnect(focusWindowNotifyId); - updateWindowsIdle && GLib.source_remove(updateWindowsIdle); - this._dtdData.destroy(); - this._dtdData = undefined; -@@ -618,21 +620,13 @@ function makeLocationApp(params) { - } - }; - -- const windowsChangedId = fm1Client.connect('windows-changed', () => -+ shellApp._signalConnections.add(fm1Client, 'windows-changed', () => - shellApp._updateWindows()); -- const workspaceChangedId = global.workspaceManager.connect('workspace-switched', -- () => shellApp.emit('windows-changed')); -- const iconChangedId = shellApp.appInfo.connect('notify::icon', () => -+ shellApp._signalConnections.add(global.workspaceManager, -+ 'workspace-switched', () => shellApp.emit('windows-changed')); -+ shellApp._signalConnections.add(shellApp.appInfo, 'notify::icon', () => - shellApp.notify('icon')); - -- const parentDestroy = shellApp.destroy; -- shellApp.destroy = function () { -- fm1Client.disconnect(windowsChangedId); -- global.workspaceManager.disconnect(workspaceChangedId); -- shellApp.appInfo.disconnect(iconChangedId); -- parentDestroy.call(this); -- } -- - return shellApp; - } - -@@ -652,9 +646,9 @@ function wrapFileManagerApp() { - wrapWindowsBackedApp(fileManagerApp); - - const { fm1Client } = Docking.DockManager.getDefault(); -- const windowsChangedId = fileManagerApp.connect('windows-changed', () => -- fileManagerApp._updateWindows()); -- const fm1WindowsChangedId = fm1Client.connect('windows-changed', () => -+ fileManagerApp._signalConnections.addWithLabel('windowsChanged', -+ fileManagerApp, 'windows-changed', () => fileManagerApp._updateWindows()); -+ fileManagerApp._signalConnections.add(fm1Client, 'windows-changed', () => - fileManagerApp._updateWindows()); - - fileManagerApp._updateWindows = function () { -@@ -667,9 +661,9 @@ function wrapFileManagerApp() { - - if (this.get_windows().length !== oldWindows.length || - this.get_windows().some((win, index) => win !== oldWindows[index])) { -- this.block_signal_handler(windowsChangedId); -+ this._signalConnections.blockWithLabel('windowsChanged'); - this.emit('windows-changed'); -- this.unblock_signal_handler(windowsChangedId); -+ this._signalConnections.unblockWithLabel('windowsChanged'); - } - - if (oldState !== this.state) { -@@ -682,13 +676,6 @@ function wrapFileManagerApp() { - fileManagerApp._mi('toString', defaultToString => - '[FileManagerApp - %s]'.format(defaultToString.call(fileManagerApp))); - -- const parentDestroy = fileManagerApp.destroy; -- fileManagerApp.destroy = function () { -- fileManagerApp.disconnect(windowsChangedId); -- fm1Client.disconnect(fm1WindowsChangedId); -- parentDestroy.call(this); -- } -- - return fileManagerApp; - } - - -From 88afaab99ecbce9e665a3e8d02b8a7cfd2dcd28e Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Thu, 9 Dec 2021 03:33:25 +0100 -Subject: [PATCH 18/51] locations: Use proxy properties to expose private items - to public object - -It's just an hacky way to have all managed inside the dtdData object but -to expose it externally managing its lifespan automatically ---- - locations.js | 59 +++++++++++++++++++++++++++++++++++++++++++--------- - 1 file changed, 49 insertions(+), 10 deletions(-) - -diff --git a/locations.js b/locations.js -index 2c89045ff..715b2625f 100644 ---- a/locations.js -+++ b/locations.js -@@ -457,28 +457,66 @@ function wrapWindowsBackedApp(shellApp) { - - shellApp._dtdData = { - windows: [], -+ isFocused: false, -+ proxyProperties: [], - signalConnections: new Utils.GlobalSignalsHandler(), - methodInjections: new Utils.InjectionsHandler(), - propertyInjections: new Utils.PropertyInjectionsHandler(), -+ addProxyProperties: function (parent, proxyProperties) { -+ Object.entries(proxyProperties).forEach(([p, o]) => { -+ const publicProp = o.public ? p : '_' + p; -+ const get = (o.getter && o.value instanceof Function) ? -+ () => this[p]() : () => this[p]; -+ Object.defineProperty(parent, publicProp, Object.assign({ -+ get, -+ set: v => (this[p] = v), -+ configurable: true, -+ enumerable: !!o.enumerable, -+ }, o.readOnly ? { set: undefined } : {})); -+ o.value && (this[p] = o.value); -+ this.proxyProperties.push(publicProp); -+ }); -+ }, - destroy: function () { - this.windows = []; -+ this.proxyProperties = []; - this.signalConnections.destroy(); - this.methodInjections.destroy(); - this.propertyInjections.destroy(); - } - }; - -+ shellApp._dtdData.addProxyProperties(shellApp, { -+ windows: {}, -+ isFocused: { public: true }, -+ signalConnections: { readOnly: true }, -+ updateWindows: {}, -+ checkFocused: {}, -+ setDtdData: {}, -+ }); -+ -+ shellApp._setDtdData = function (data, params = {}) { -+ for (const [name, value] of Object.entries(data)) { -+ if (params.readOnly && name in this._dtdData) -+ throw new Error('Property %s is already defined'.format(name)); -+ const defaultParams = { public: true, readOnly: true }; -+ this._dtdData.addProxyProperties(this, { -+ [name]: { ...defaultParams, ...params, value } -+ }); -+ } -+ }; -+ - const m = (...args) => shellApp._dtdData.methodInjections.add(shellApp, ...args); - const p = (...args) => shellApp._dtdData.propertyInjections.add(shellApp, ...args); -- shellApp._mi = m; // Method injector -- shellApp._pi = p; // Property injector -- shellApp._signalConnections = shellApp._dtdData.signalConnections; -+ -+ // mi is Method injector, pi is Property injector -+ shellApp._setDtdData({ mi: m, pi: p }, { public: false }); - - m('get_state', () => - shellApp.get_windows().length ? Shell.AppState.RUNNING : Shell.AppState.STOPPED); - p('state', { get: () => shellApp.get_state() }); - -- m('get_windows', () => shellApp._dtdData.windows); -+ m('get_windows', () => shellApp._windows); - m('get_n_windows', () => shellApp.get_windows().length); - m('get_pids', () => shellApp.get_windows().reduce((pids, w) => { - if (w.get_pid() > 0 && !pids.includes(w.get_pid())) -@@ -560,6 +598,7 @@ function wrapWindowsBackedApp(shellApp) { - - shellApp.destroy = function() { - updateWindowsIdle && GLib.source_remove(updateWindowsIdle); -+ this._dtdData.proxyProperties.forEach(p => (delete this[p])); - this._dtdData.destroy(); - this._dtdData = undefined; - this.destroy = undefined; -@@ -579,10 +618,10 @@ function makeLocationApp(params) { - const shellApp = new Shell.App(params); - wrapWindowsBackedApp(shellApp); - -- Object.defineProperties(shellApp, { -- location: { get: () => shellApp.appInfo.location }, -- isTrash: { get: () => shellApp.appInfo instanceof TrashAppInfo }, -- }); -+ shellApp._setDtdData({ -+ location: () => shellApp.appInfo.location, -+ isTrash: shellApp.appInfo instanceof TrashAppInfo, -+ }, { getter: true, enumerable: true }); - - shellApp._mi('toString', defaultToString => - '[LocationApp - %s]'.format(defaultToString.call(shellApp))); -@@ -607,7 +646,7 @@ function makeLocationApp(params) { - shellApp._updateWindows = function () { - const oldState = this.state; - const oldWindows = this.get_windows(); -- this._dtdData.windows = fm1Client.getWindows(this.location?.get_uri()); -+ this._windows = fm1Client.getWindows(this.location?.get_uri()); - - if (this.get_windows().length !== oldWindows.length || - this.get_windows().some((win, index) => win !== oldWindows[index])) -@@ -656,7 +695,7 @@ function wrapFileManagerApp() { - const oldWindows = this.get_windows(); - const locationWindows = []; - getRunningApps().forEach(a => locationWindows.push(...a.get_windows())); -- this._dtdData.windows = originalGetWindows.call(this).filter(w => -+ this._windows = originalGetWindows.call(this).filter(w => - !locationWindows.includes(w)); - - if (this.get_windows().length !== oldWindows.length || - -From 021666e00b86377c9e33c613580ab17026355893 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Thu, 9 Dec 2021 14:59:20 +0100 -Subject: [PATCH 19/51] locations: Chain up to parent destroy() function if any - -It's not defined right now, but in case it will be, we'll be ready. ---- - locations.js | 4 +++- - 1 file changed, 3 insertions(+), 1 deletion(-) - -diff --git a/locations.js b/locations.js -index 715b2625f..1b64f68e5 100644 ---- a/locations.js -+++ b/locations.js -@@ -596,12 +596,14 @@ function wrapWindowsBackedApp(shellApp) { - - m('compare', (_om, other) => shellAppCompare(shellApp, other)); - -+ const { destroy: defaultDestroy } = shellApp; - shellApp.destroy = function() { - updateWindowsIdle && GLib.source_remove(updateWindowsIdle); - this._dtdData.proxyProperties.forEach(p => (delete this[p])); - this._dtdData.destroy(); - this._dtdData = undefined; -- this.destroy = undefined; -+ this.destroy = defaultDestroy; -+ defaultDestroy && defaultDestroy.call(this); - } - - return shellApp; - -From c1d4a1cb66edca34052330533398ad5c67eeb5f1 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Sun, 23 Jan 2022 02:54:10 +0100 -Subject: [PATCH 20/51] locations: Manage sources as part of DtdData - -It allows to remove sources automatically without having us to care -about the cleanup. ---- - locations.js | 12 +++++++----- - 1 file changed, 7 insertions(+), 5 deletions(-) - -diff --git a/locations.js b/locations.js -index 1b64f68e5..05f0d3fef 100644 ---- a/locations.js -+++ b/locations.js -@@ -459,6 +459,7 @@ function wrapWindowsBackedApp(shellApp) { - windows: [], - isFocused: false, - proxyProperties: [], -+ sources: new Set(), - signalConnections: new Utils.GlobalSignalsHandler(), - methodInjections: new Utils.InjectionsHandler(), - propertyInjections: new Utils.PropertyInjectionsHandler(), -@@ -480,6 +481,8 @@ function wrapWindowsBackedApp(shellApp) { - destroy: function () { - this.windows = []; - this.proxyProperties = []; -+ this.sources.forEach(s => GLib.source_remove(s)); -+ this.sources.clear(); - this.signalConnections.destroy(); - this.methodInjections.destroy(); - this.propertyInjections.destroy(); -@@ -490,7 +493,7 @@ function wrapWindowsBackedApp(shellApp) { - windows: {}, - isFocused: { public: true }, - signalConnections: { readOnly: true }, -- updateWindows: {}, -+ sources: { readOnly: true }, - checkFocused: {}, - setDtdData: {}, - }); -@@ -532,11 +535,11 @@ function wrapWindowsBackedApp(shellApp) { - throw new GObject.NotImplementedError(`_updateWindows in ${this.constructor.name}`); - }; - -- let updateWindowsIdle = GLib.idle_add(GLib.DEFAULT_PRIORITY, () => { -+ shellApp._sources.add(GLib.idle_add(GLib.DEFAULT_PRIORITY, () => { - shellApp._updateWindows(); -- updateWindowsIdle = undefined; -+ shellApp._sources.delete(GLib.main_current_source().source_id); - return GLib.SOURCE_REMOVE; -- }); -+ })); - - const windowTracker = Shell.WindowTracker.get_default(); - shellApp._checkFocused = function () { -@@ -598,7 +601,6 @@ function wrapWindowsBackedApp(shellApp) { - - const { destroy: defaultDestroy } = shellApp; - shellApp.destroy = function() { -- updateWindowsIdle && GLib.source_remove(updateWindowsIdle); - this._dtdData.proxyProperties.forEach(p => (delete this[p])); - this._dtdData.destroy(); - this._dtdData = undefined; - -From e496666f77aff56280a3e7a514d630e8231c2204 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Thu, 9 Dec 2021 15:05:46 +0100 -Subject: [PATCH 21/51] locations: Manage windows changes in base class - -So we can share it between locationa apps and filemanager. ---- - locations.js | 64 +++++++++++++++++++++++++++------------------------- - 1 file changed, 33 insertions(+), 31 deletions(-) - -diff --git a/locations.js b/locations.js -index 05f0d3fef..6e3e74756 100644 ---- a/locations.js -+++ b/locations.js -@@ -531,9 +531,33 @@ function wrapWindowsBackedApp(shellApp) { - m('request_quit', () => shellApp.get_windows().filter(w => - w.can_close()).forEach(w => w.delete(global.get_current_time()))); - -- shellApp._updateWindows = function () { -- throw new GObject.NotImplementedError(`_updateWindows in ${this.constructor.name}`); -- }; -+ shellApp._setDtdData({ -+ _updateWindows: function () { -+ throw new GObject.NotImplementedError(`_updateWindows in ${this.constructor.name}`); -+ }, -+ -+ _setWindows: function (windows) { -+ const oldState = this.state; -+ const oldWindows = this.get_windows().slice(); -+ const result = { windowsChanged: false, stateChanged: false }; -+ -+ if (windows.length !== oldWindows.length || -+ windows.some((win, index) => win !== oldWindows[index])) { -+ this._windows = windows; -+ this.emit('windows-changed'); -+ result.windowsChanged = true; -+ } -+ -+ if (this.state !== oldState) { -+ Shell.AppSystem.get_default().emit('app-state-changed', this); -+ this.notify('state'); -+ this._checkFocused(); -+ result.stateChanged = true; -+ } -+ -+ return result; -+ }, -+ }, { readOnly: false }); - - shellApp._sources.add(GLib.idle_add(GLib.DEFAULT_PRIORITY, () => { - shellApp._updateWindows(); -@@ -648,19 +672,8 @@ function makeLocationApp(params) { - - const { fm1Client } = Docking.DockManager.getDefault(); - shellApp._updateWindows = function () { -- const oldState = this.state; -- const oldWindows = this.get_windows(); -- this._windows = fm1Client.getWindows(this.location?.get_uri()); -- -- if (this.get_windows().length !== oldWindows.length || -- this.get_windows().some((win, index) => win !== oldWindows[index])) -- this.emit('windows-changed'); -- -- if (oldState !== this.state) { -- Shell.AppSystem.get_default().emit('app-state-changed', this); -- this.notify('state'); -- this._checkFocused(); -- } -+ const windows = fm1Client.getWindows(this.location?.get_uri()); -+ this._setWindows(windows); - }; - - shellApp._signalConnections.add(fm1Client, 'windows-changed', () => -@@ -695,25 +708,14 @@ function wrapFileManagerApp() { - fileManagerApp._updateWindows()); - - fileManagerApp._updateWindows = function () { -- const oldState = this.state; -- const oldWindows = this.get_windows(); - const locationWindows = []; - getRunningApps().forEach(a => locationWindows.push(...a.get_windows())); -- this._windows = originalGetWindows.call(this).filter(w => -+ const windows = originalGetWindows.call(this).filter(w => - !locationWindows.includes(w)); - -- if (this.get_windows().length !== oldWindows.length || -- this.get_windows().some((win, index) => win !== oldWindows[index])) { -- this._signalConnections.blockWithLabel('windowsChanged'); -- this.emit('windows-changed'); -- this._signalConnections.unblockWithLabel('windowsChanged'); -- } -- -- if (oldState !== this.state) { -- Shell.AppSystem.get_default().emit('app-state-changed', this); -- this.notify('state'); -- this._checkFocused(); -- } -+ this._signalConnections.blockWithLabel('windowsChanged'); -+ this._setWindows(windows); -+ this._signalConnections.unblockWithLabel('windowsChanged'); - }; - - fileManagerApp._mi('toString', defaultToString => - -From ec7f47cc5db0770e7e3ae5fc576e43b4335e6a27 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Thu, 9 Dec 2021 15:10:08 +0100 -Subject: [PATCH 22/51] cleanup: Move shellAppCompare under Utils as it's a - generic tool - ---- - docking.js | 2 +- - locations.js | 35 +---------------------------------- - utils.js | 34 ++++++++++++++++++++++++++++++++++ - 3 files changed, 36 insertions(+), 35 deletions(-) - -diff --git a/docking.js b/docking.js -index a7620ff69..de8660a1c 100644 ---- a/docking.js -+++ b/docking.js -@@ -1705,7 +1705,7 @@ var DockManager = class DashToDock_DockManager { - if (fileManagerIdx > -1 && fileManagerApp?.state !== Shell.AppState.RUNNING) - runningApps.splice(fileManagerIdx, 1); - -- return [...runningApps, ...locationApps].sort(Locations.shellAppCompare); -+ return [...runningApps, ...locationApps].sort(Utils.shellAppCompare); - } - ], - [ -diff --git a/locations.js b/locations.js -index 6e3e74756..2d7985cd1 100644 ---- a/locations.js -+++ b/locations.js -@@ -621,7 +621,7 @@ function wrapWindowsBackedApp(shellApp) { - - m('activate', () => shellApp.activate_full(-1, 0)); - -- m('compare', (_om, other) => shellAppCompare(shellApp, other)); -+ m('compare', (_om, other) => Utils.shellAppCompare(shellApp, other)); - - const { destroy: defaultDestroy } = shellApp; - shellApp.destroy = function() { -@@ -732,39 +732,6 @@ function unWrapFileManagerApp() { - fileManagerApp.destroy(); - } - --// Re-implements shell_app_compare so that can be used to resort running apps --function shellAppCompare(app, other) { -- if (app.state !== other.state) { -- if (app.state === Shell.AppState.RUNNING) -- return -1; -- return 1; -- } -- -- const windows = app.get_windows(); -- const otherWindows = other.get_windows(); -- -- const isMinimized = windows => !windows.some(w => w.showing_on_its_workspace()); -- const otherMinimized = isMinimized(otherWindows); -- if (isMinimized(windows) != otherMinimized) { -- if (otherMinimized) -- return -1; -- return 1; -- } -- -- if (app.state === Shell.AppState.RUNNING) { -- if (windows.length && !otherWindows.length) -- return -1; -- else if (!windows.length && otherWindows.length) -- return 1; -- -- const lastUserTime = windows => -- Math.max(...windows.map(w => w.get_user_time())); -- return lastUserTime(otherWindows) - lastUserTime(windows); -- } -- -- return 0; --} -- - /** - * This class maintains a Shell.App representing the Trash and keeps it - * up-to-date as the trash fills and is emptied over time. -diff --git a/utils.js b/utils.js -index 293640e05..0b62f70c2 100644 ---- a/utils.js -+++ b/utils.js -@@ -5,6 +5,7 @@ const Clutter = imports.gi.Clutter; - const GObject = imports.gi.GObject; - const Gtk = imports.gi.Gtk; - const Meta = imports.gi.Meta; -+const Shell = imports.gi.Shell; - const St = imports.gi.St; - - const Me = imports.misc.extensionUtils.getCurrentExtension(); -@@ -481,3 +482,36 @@ function getWindowsByObjectPath() { - - return windowsByObjectPath; - } -+ -+// Re-implements shell_app_compare so that can be used to resort running apps -+function shellAppCompare(appA, appB) { -+ if (appA.state !== appB.state) { -+ if (appA.state === Shell.AppState.RUNNING) -+ return -1; -+ return 1; -+ } -+ -+ const windowsA = appA.get_windows(); -+ const windowsB = appB.get_windows(); -+ -+ const isMinimized = windows => !windows.some(w => w.showing_on_its_workspace()); -+ const minimizedB = isMinimized(windowsB); -+ if (isMinimized(windowsA) != minimizedB) { -+ if (minimizedB) -+ return -1; -+ return 1; -+ } -+ -+ if (appA.state === Shell.AppState.RUNNING) { -+ if (windowsA.length && !windowsB.length) -+ return -1; -+ else if (!windowsA.length && windowsB.length) -+ return 1; -+ -+ const lastUserTime = windows => -+ Math.max(...windows.map(w => w.get_user_time())); -+ return lastUserTime(windowsB) - lastUserTime(windowsA); -+ } -+ -+ return 0; -+} - -From 23dd498e6e134da8c9ed94394fba029b839eec5f Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Thu, 9 Dec 2021 15:27:41 +0100 -Subject: [PATCH 23/51] locations: Keep locations windows ordered following the - shell algorithm - -Re-implement the same algorithm used by the shell to order the apps -windows and re-order them both when they change or on workspace switches ---- - locations.js | 22 ++++++++++++++++------ - utils.js | 22 ++++++++++++++++++++++ - 2 files changed, 38 insertions(+), 6 deletions(-) - -diff --git a/locations.js b/locations.js -index 2d7985cd1..3dbce9396 100644 ---- a/locations.js -+++ b/locations.js -@@ -671,17 +671,27 @@ function makeLocationApp(params) { - shellApp._mi('can_open_new_window', () => false); - - const { fm1Client } = Docking.DockManager.getDefault(); -- shellApp._updateWindows = function () { -- const windows = fm1Client.getWindows(this.location?.get_uri()); -- this._setWindows(windows); -- }; -+ shellApp._setDtdData({ -+ _sortWindows: function () { -+ this._windows.sort(Utils.shellWindowsCompare); -+ }, -+ -+ _updateWindows: function () { -+ const windows = fm1Client.getWindows(this.location?.get_uri()).sort( -+ Utils.shellWindowsCompare); -+ this._setWindows(windows); -+ }, -+ }); - - shellApp._signalConnections.add(fm1Client, 'windows-changed', () => - shellApp._updateWindows()); -- shellApp._signalConnections.add(global.workspaceManager, -- 'workspace-switched', () => shellApp.emit('windows-changed')); - shellApp._signalConnections.add(shellApp.appInfo, 'notify::icon', () => - shellApp.notify('icon')); -+ shellApp._signalConnections.add(global.workspaceManager, -+ 'workspace-switched', () => { -+ shellApp._sortWindows(); -+ shellApp.emit('windows-changed'); -+ }); - - return shellApp; - } -diff --git a/utils.js b/utils.js -index 0b62f70c2..b3aed2679 100644 ---- a/utils.js -+++ b/utils.js -@@ -515,3 +515,25 @@ function shellAppCompare(appA, appB) { - - return 0; - } -+ -+// Re-implements shell_app_compare_windows -+function shellWindowsCompare(winA, winB) { -+ const activeWorkspace = global.workspaceManager.get_active_workspace(); -+ const wsA = winA.get_workspace() === activeWorkspace; -+ const wsB = winB.get_workspace() === activeWorkspace; -+ -+ if (wsA && !wsB) -+ return -1; -+ else if (!wsA && wsB) -+ return 1; -+ -+ const visA = winA.showing_on_its_workspace(); -+ const visB = winB.showing_on_its_workspace(); -+ -+ if (visA && !visB) -+ return -1; -+ else if (!visA && visB) -+ return 1; -+ -+ return winB.get_user_time() - winA.get_user_time(); -+} - -From 631259b5fb72ad59c75d5fb6658ffae4bade7720 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Thu, 9 Dec 2021 16:33:53 +0100 -Subject: [PATCH 24/51] locations: Use windows array directly where there's no - need for method call - -In some cases the get_windows() method may be overridden to perform more -operations than just getting the windows, so it's better not to use it -in case we're just parsing the windows content. ---- - locations.js | 14 +++++++------- - 1 file changed, 7 insertions(+), 7 deletions(-) - -diff --git a/locations.js b/locations.js -index 3dbce9396..83fa2dff4 100644 ---- a/locations.js -+++ b/locations.js -@@ -516,19 +516,19 @@ function wrapWindowsBackedApp(shellApp) { - shellApp._setDtdData({ mi: m, pi: p }, { public: false }); - - m('get_state', () => -- shellApp.get_windows().length ? Shell.AppState.RUNNING : Shell.AppState.STOPPED); -+ shellApp.get_n_windows() ? Shell.AppState.RUNNING : Shell.AppState.STOPPED); - p('state', { get: () => shellApp.get_state() }); - - m('get_windows', () => shellApp._windows); -- m('get_n_windows', () => shellApp.get_windows().length); -- m('get_pids', () => shellApp.get_windows().reduce((pids, w) => { -+ m('get_n_windows', () => shellApp._windows.length); -+ m('get_pids', () => shellApp._windows.reduce((pids, w) => { - if (w.get_pid() > 0 && !pids.includes(w.get_pid())) - pids.push(w.get_pid()); - return pids; - }, [])); -- m('is_on_workspace', (_om, workspace) => shellApp.get_windows().some(w => -+ m('is_on_workspace', (_om, workspace) => shellApp._windows.some(w => - w.get_workspace() === workspace)); -- m('request_quit', () => shellApp.get_windows().filter(w => -+ m('request_quit', () => shellApp._windows.filter(w => - w.can_close()).forEach(w => w.delete(global.get_current_time()))); - - shellApp._setDtdData({ -@@ -567,7 +567,7 @@ function wrapWindowsBackedApp(shellApp) { - - const windowTracker = Shell.WindowTracker.get_default(); - shellApp._checkFocused = function () { -- if (this.get_windows().some(w => w.has_focus())) { -+ if (this._windows.some(w => w.has_focus())) { - this.isFocused = true; - windowTracker.notify('focus-app'); - } else if (this.isFocused) { -@@ -584,7 +584,7 @@ function wrapWindowsBackedApp(shellApp) { - m('activate_window', function (_om, window, timestamp) { - if (!window) - [window] = this.get_windows(); -- else if (!this.get_windows().includes(window)) -+ else if (!this._windows.includes(window)) - return; - - const currentWorkspace = global.workspace_manager.get_active_workspace(); - -From bb7501ccc014aa8b2ba9cc963b6f8678b84be91f Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Thu, 9 Dec 2021 16:35:49 +0100 -Subject: [PATCH 25/51] locations: Resort and notify windows changes on - user-time changes - -In case the user time of a window changed, we may need to inform about -windows changes. This same behavior is used by the upstream shell but we -didn't implement it fully. ---- - locations.js | 15 ++++++++++++++- - 1 file changed, 14 insertions(+), 1 deletion(-) - -diff --git a/locations.js b/locations.js -index 83fa2dff4..839aad75b 100644 ---- a/locations.js -+++ b/locations.js -@@ -679,7 +679,20 @@ function makeLocationApp(params) { - _updateWindows: function () { - const windows = fm1Client.getWindows(this.location?.get_uri()).sort( - Utils.shellWindowsCompare); -- this._setWindows(windows); -+ const { windowsChanged } = this._setWindows(windows); -+ -+ if (!windowsChanged) -+ return; -+ -+ this._signalConnections.removeWithLabel('location-windows'); -+ windows.forEach(w => -+ this._signalConnections.addWithLabel('location-windows', w, -+ 'notify::user-time', () => { -+ if (w != this._windows[0]) { -+ this._sortWindows(); -+ this.emit('windows-changed'); -+ } -+ })); - }, - }); - - -From aea490262b91fcee4dc2e2658c9dd5c1a32dfb4e Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Thu, 9 Dec 2021 16:50:39 +0100 -Subject: [PATCH 26/51] locations: Sort location windows apps only demand - -Follow the same strategy as upstream shell does, so avoid sorting -windows all the times we recorded a change, but only when the windows -are actually fetched. ---- - locations.js | 27 ++++++++++++++++++--------- - 1 file changed, 18 insertions(+), 9 deletions(-) - -diff --git a/locations.js b/locations.js -index 839aad75b..0df7acd19 100644 ---- a/locations.js -+++ b/locations.js -@@ -670,10 +670,24 @@ function makeLocationApp(params) { - // FIXME: We need to add a new API to Nautilus to open new windows - shellApp._mi('can_open_new_window', () => false); - -+ shellApp._mi('get_windows', function () { -+ if (this._needsResort) -+ this._sortWindows(); -+ return this._windows; -+ }); -+ - const { fm1Client } = Docking.DockManager.getDefault(); - shellApp._setDtdData({ -+ _needsResort: true, -+ -+ _windowsOrderChanged: function() { -+ this._needsResort = true; -+ this.emit('windows-changed'); -+ }, -+ - _sortWindows: function () { - this._windows.sort(Utils.shellWindowsCompare); -+ this._needsResort = false; - }, - - _updateWindows: function () { -@@ -688,23 +702,18 @@ function makeLocationApp(params) { - windows.forEach(w => - this._signalConnections.addWithLabel('location-windows', w, - 'notify::user-time', () => { -- if (w != this._windows[0]) { -- this._sortWindows(); -- this.emit('windows-changed'); -- } -+ if (w != this._windows[0]) -+ this._windowsOrderChanged(); - })); - }, -- }); -+ }, { readOnly: false }); - - shellApp._signalConnections.add(fm1Client, 'windows-changed', () => - shellApp._updateWindows()); - shellApp._signalConnections.add(shellApp.appInfo, 'notify::icon', () => - shellApp.notify('icon')); - shellApp._signalConnections.add(global.workspaceManager, -- 'workspace-switched', () => { -- shellApp._sortWindows(); -- shellApp.emit('windows-changed'); -- }); -+ 'workspace-switched', () => shellApp._windowsOrderChanged()); - - return shellApp; - } - -From ee2e5fcaf6ad556d3cfeedee76923ca878f5c092 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Thu, 9 Dec 2021 18:04:49 +0100 -Subject: [PATCH 27/51] fileManager1API: Only emit windows-changed signal if - windows / locations changed - -Compare the computed map with the old one before emitting the -windows-changed signal, so that we can avoid rebuilding the dock more -than needed ---- - fileManager1API.js | 27 +++++++++++++++++++++++---- - 1 file changed, 23 insertions(+), 4 deletions(-) - -diff --git a/fileManager1API.js b/fileManager1API.js -index 8f79b981d..3d675d0e6 100644 ---- a/fileManager1API.js -+++ b/fileManager1API.js -@@ -142,10 +142,25 @@ var FileManager1Client = class DashToDock_FileManager1Client { - } - } - -+ _locationMapsEquals(mapA, mapB) { -+ if (mapA.size !== mapB.size) -+ return false; -+ -+ const setsEquals = (a, b) => a.size === b.size && -+ [...a].every(value => b.has(value)); -+ -+ for (const [key, val] of mapA) { -+ const windowsSet = mapB.get(key); -+ if (!windowsSet || !setsEquals(windowsSet, val)) -+ return false; -+ } -+ return true; -+ } -+ - _updateFromPaths() { - const locationsByWindowsPath = this._proxy.OpenWindowsWithLocations; - -- this._windowsByLocation = new Map(); -+ const windowsByLocation = new Map(); - Object.entries(locationsByWindowsPath).forEach(([windowPath, locations]) => { - locations.forEach(location => { - const window = this._windowsByPath.get(windowPath); -@@ -154,15 +169,19 @@ var FileManager1Client = class DashToDock_FileManager1Client { - location += location.endsWith('/') ? '' : '/'; - // Use a set to deduplicate when a window has a - // location open in multiple tabs. -- const windows = this._windowsByLocation.get(location) || new Set(); -+ const windows = windowsByLocation.get(location) || new Set(); - windows.add(window); - - if (windows.size === 1) -- this._windowsByLocation.set(location, windows); -+ windowsByLocation.set(location, windows); - } - }); - }); -- this.emit('windows-changed'); -+ -+ if (!this._locationMapsEquals(this._windowsByLocation, windowsByLocation)) { -+ this._windowsByLocation = windowsByLocation; -+ this.emit('windows-changed'); -+ } - } - } - Signals.addSignalMethods(FileManager1Client.prototype); - -From baca53d3958ab5c0849f786e45db85427d2acbbd Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Thu, 9 Dec 2021 18:06:08 +0100 -Subject: [PATCH 28/51] fileManager1API: Manage unmanaged windows early - -Remove unmanaged windows early, so that dbus won't be confused by them ---- - fileManager1API.js | 11 +++++++++++ - 1 file changed, 11 insertions(+) - -diff --git a/fileManager1API.js b/fileManager1API.js -index 3d675d0e6..c3abeb76a 100644 ---- a/fileManager1API.js -+++ b/fileManager1API.js -@@ -161,6 +161,8 @@ var FileManager1Client = class DashToDock_FileManager1Client { - const locationsByWindowsPath = this._proxy.OpenWindowsWithLocations; - - const windowsByLocation = new Map(); -+ this._signalsHandler.removeWithLabel('windows'); -+ - Object.entries(locationsByWindowsPath).forEach(([windowPath, locations]) => { - locations.forEach(location => { - const window = this._windowsByPath.get(windowPath); -@@ -174,6 +176,15 @@ var FileManager1Client = class DashToDock_FileManager1Client { - - if (windows.size === 1) - windowsByLocation.set(location, windows); -+ -+ this._signalsHandler.addWithLabel('windows', window, -+ 'unmanaged', () => { -+ const wins = this._windowsByLocation.get(location); -+ wins.delete(window); -+ if (!wins.size) -+ this._windowsByLocation.delete(location); -+ this.emit('windows-changed'); -+ }); - } - }); - }); - -From ea8a330767636381d7729d134eba3e3038646a04 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Thu, 9 Dec 2021 19:38:53 +0100 -Subject: [PATCH 29/51] locations: Include actual app ID in the string - representation - ---- - locations.js | 3 ++- - 1 file changed, 2 insertions(+), 1 deletion(-) - -diff --git a/locations.js b/locations.js -index 0df7acd19..915fed2f8 100644 ---- a/locations.js -+++ b/locations.js -@@ -652,7 +652,8 @@ function makeLocationApp(params) { - }, { getter: true, enumerable: true }); - - shellApp._mi('toString', defaultToString => -- '[LocationApp - %s]'.format(defaultToString.call(shellApp))); -+ '[LocationApp "%s" - %s]'.format(shellApp.get_id(), -+ defaultToString.call(shellApp))); - - shellApp._mi('launch', (_om, timestamp, workspace, _gpuPref) => - shellApp.appInfo.launch([], - -From 80f16608ac8b580e5ea81b8ce2e12ede5f753063 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Wed, 15 Dec 2021 16:28:35 +0100 -Subject: [PATCH 30/51] utils: Add CancellableChild, a GCancellable that - monitors a parent instance - -At times it may be needed to trigger actions that may be canceled when -both a parent (global) GCancellable and the instance itself is canceled. - -For example, we ant to trigger an async operation on an object that must -be both cancelled on request and when the object main cancellable is -cancelled. - -To perform this, we create a new GCancellable subtype that includes a -parent instance and that connects to the 'cancellable' signal of that, -performing a cancellation of itself when this happens. -At the same time, we need to handle the disconnection carefully, as it -must be done on an idle when cancellation was triggered by the parent -not to end-up in a dead-lock. ---- - utils.js | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - 1 file changed, 57 insertions(+) - -diff --git a/utils.js b/utils.js -index b3aed2679..5a48cc425 100644 ---- a/utils.js -+++ b/utils.js -@@ -2,6 +2,8 @@ - const Gi = imports._gi; - - const Clutter = imports.gi.Clutter; -+const GLib = imports.gi.GLib; -+const Gio = imports.gi.Gio; - const GObject = imports.gi.GObject; - const Gtk = imports.gi.Gtk; - const Meta = imports.gi.Meta; -@@ -537,3 +539,58 @@ function shellWindowsCompare(winA, winB) { - - return winB.get_user_time() - winA.get_user_time(); - } -+ -+var CancellableChild = GObject.registerClass({ -+ Properties: { -+ 'parent': GObject.ParamSpec.object( -+ 'parent', 'parent', 'parent', -+ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, -+ Gio.Cancellable.$gtype), -+ }, -+}, -+class CancellableChild extends Gio.Cancellable { -+ _init(parent) { -+ if (parent && !(parent instanceof Gio.Cancellable)) -+ throw TypeError('Not a valid cancellable'); -+ -+ super._init({ parent }); -+ -+ if (parent?.is_cancelled()) { -+ this.cancel(); -+ return; -+ } -+ -+ this._connectToParent(); -+ } -+ -+ _connectToParent() { -+ this._connectId = this?.parent.connect(() => { -+ this._realCancel(); -+ -+ if (this._disconnectIdle) -+ return; -+ -+ this._disconnectIdle = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { -+ delete this._disconnectIdle; -+ this._disconnectFromParent(); -+ return GLib.SOURCE_REMOVE; -+ }); -+ }); -+ } -+ -+ _disconnectFromParent() { -+ if (this._connectId && !this._disconnectIdle) { -+ this.parent.disconnect(this._connectId); -+ delete this._connectId; -+ } -+ } -+ -+ _realCancel() { -+ Gio.Cancellable.prototype.cancel.call(this); -+ } -+ -+ cancel() { -+ this._disconnectFromParent(); -+ this._realCancel(); -+ } -+}); - -From 58d68a3cd5cd1b717ea0b498883b147b24265733 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Thu, 9 Dec 2021 22:42:18 +0100 -Subject: [PATCH 31/51] locations: Get Trash icon from the folder attributes - -Some folders or mounted locations may use some icons that are different -from the standard ones, so instead of just using a standard fallback -icon we can try to fetch it from the file info parameters. - -The trash is a simple case in which we can just use this information to -show the proper icon without having us to hardcode anything. - -Helps with #1508 ---- - locations.js | 51 +++++++++++++++++++++++++++++++++++++++++++++++---- - 1 file changed, 47 insertions(+), 4 deletions(-) - -diff --git a/locations.js b/locations.js -index 915fed2f8..cb5c9cf90 100644 ---- a/locations.js -+++ b/locations.js -@@ -212,6 +212,48 @@ var LocationAppInfo = GObject.registerClass({ - vfunc_get_supported_types() { - return []; - } -+ -+ async _queryLocationIcon(params) { -+ if (!this.location) -+ return null; -+ -+ const cancellable = params.cancellable ?? this.cancellable; -+ -+ try { -+ const info = await this.location.query_info_async( -+ Gio.FILE_ATTRIBUTE_STANDARD_ICON, -+ Gio.FileQueryInfoFlags.NONE, -+ GLib.PRIORITY_LOW, cancellable); -+ -+ return info?.get_icon(); -+ } catch (e) { -+ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND) || -+ e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_MOUNTED)) -+ return null; -+ throw e; -+ } -+ } -+ -+ async _updateLocationIcon(params={}) { -+ const cancellable = new Utils.CancellableChild(this.cancellable); -+ -+ try { -+ this._updateIconCancellable?.cancel(); -+ this._updateIconCancellable = cancellable; -+ -+ const icon = await this._queryLocationIcon({ cancellable, ...params }); -+ -+ if (icon && !icon.equal(this.icon)) -+ this.icon = icon; -+ } catch (e) { -+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) -+ logError(e, 'Impossible to update icon for %s'.format(this.get_id())); -+ } finally { -+ cancellable.cancel(); -+ if (this._updateIconCancellable === cancellable) -+ delete this._updateIconCancellable; -+ } -+ } - }); - - const VolumeAppInfo = GObject.registerClass({ -@@ -417,10 +459,10 @@ class TrashAppInfo extends LocationAppInfo { - super._init({ - location: Gio.file_new_for_uri(TRASH_URI), - name: __('Trash'), -+ icon: Gio.ThemedIcon.new(FALLBACK_TRASH_ICON), - cancellable, - }); -- this.connect('notify::empty', () => -- (this.icon = Gio.ThemedIcon.new(this.empty ? 'user-trash' : 'user-trash-full'))); -+ this.connect('notify::empty', () => this._updateLocationIcon()); - this.notify('empty'); - } - -@@ -774,10 +816,11 @@ var Trash = class DashToDock_Trash { - if (Trash._promisified) - return; - -+ const trashProto = Gio.file_new_for_uri(TRASH_URI).constructor.prototype; - Gio._promisify(Gio.FileEnumerator.prototype, 'close_async', 'close_finish'); - Gio._promisify(Gio.FileEnumerator.prototype, 'next_files_async', 'next_files_finish'); -- Gio._promisify(Gio.file_new_for_uri(TRASH_URI).constructor.prototype, -- 'enumerate_children_async', 'enumerate_children_finish'); -+ Gio._promisify(trashProto, 'enumerate_children_async', 'enumerate_children_finish'); -+ Gio._promisify(trashProto, 'query_info_async', 'query_info_finish'); - Trash._promisified = true; - } - - -From f0f3efc4004809593c1fe0789bcc4c43cf7205ee Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Thu, 9 Dec 2021 22:43:36 +0100 -Subject: [PATCH 32/51] locations: Add support for showing custom icon files on - mounts - -Use queryLocationIcons to fetch multiple icon types (to just do one -async query) supporting the metadata::custom-icon icons, and optionally -use it on mounts and vollumes. - -Fixes: #1508 ---- - locations.js | 55 +++++++++++++++++++++++++++++++++++++++++++--------- - 1 file changed, 46 insertions(+), 9 deletions(-) - -diff --git a/locations.js b/locations.js -index cb5c9cf90..c8d2e3955 100644 ---- a/locations.js -+++ b/locations.js -@@ -37,6 +37,10 @@ const NautilusFileOperations2Interface = '\ - - const NautilusFileOperations2ProxyInterface = Gio.DBusProxy.makeProxyWrapper(NautilusFileOperations2Interface); - -+if (imports.system.version >= 17101) { -+ Gio._promisify(Gio.File.prototype, 'query_info_async', 'query_info_finish'); -+} -+ - function makeNautilusFileOperationsProxy() { - const proxy = new NautilusFileOperations2ProxyInterface( - Gio.DBus.session, -@@ -213,35 +217,66 @@ var LocationAppInfo = GObject.registerClass({ - return []; - } - -- async _queryLocationIcon(params) { -+ async _queryLocationIcons(params) { -+ const icons = { standard: null, custom: null }; - if (!this.location) -- return null; -+ return icons; - - const cancellable = params.cancellable ?? this.cancellable; -+ const iconsQuery = []; -+ if (params?.standard) -+ iconsQuery.push(Gio.FILE_ATTRIBUTE_STANDARD_ICON); -+ -+ if (params?.custom) -+ iconsQuery.push(ATTRIBUTE_METADATA_CUSTOM_ICON); -+ -+ if (!iconsQuery.length) -+ throw new Error('Invalid Query Location Icons parameters'); - -+ let info; - try { -- const info = await this.location.query_info_async( -- Gio.FILE_ATTRIBUTE_STANDARD_ICON, -+ // This is should not be needed in newer Gjs (> GNOME 41) -+ if (imports.system.version < 17101) { -+ Gio._promisify(this.location.constructor.prototype, 'query_info_async', -+ 'query_info_finish'); -+ } -+ info = await this.location.query_info_async( -+ iconsQuery.join(','), - Gio.FileQueryInfoFlags.NONE, - GLib.PRIORITY_LOW, cancellable); -- -- return info?.get_icon(); -+ icons.standard = info.get_icon(); - } catch (e) { - if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND) || - e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_MOUNTED)) -- return null; -+ return icons; - throw e; - } -+ -+ const customIcon = info.get_attribute_string(ATTRIBUTE_METADATA_CUSTOM_ICON); -+ if (customIcon) { -+ const customIconFile = GLib.uri_parse_scheme(customIcon) ? -+ Gio.File.new_for_uri(customIcon) : Gio.File.new_for_path(customIcon); -+ const iconFileInfo = await customIconFile.query_info_async( -+ Gio.FILE_ATTRIBUTE_STANDARD_TYPE, -+ Gio.FileQueryInfoFlags.NONE, -+ GLib.PRIORITY_LOW, cancellable); -+ -+ if (iconFileInfo.get_file_type() === Gio.FileType.REGULAR) -+ icons.custom = Gio.FileIcon.new(customIconFile); -+ } -+ -+ return icons; - } - -- async _updateLocationIcon(params={}) { -+ async _updateLocationIcon(params = { standard: true, custom: true }) { - const cancellable = new Utils.CancellableChild(this.cancellable); - - try { - this._updateIconCancellable?.cancel(); - this._updateIconCancellable = cancellable; - -- const icon = await this._queryLocationIcon({ cancellable, ...params }); -+ const icons = await this._queryLocationIcons({ cancellable, ...params }); -+ const icon = icons.custom ?? icons.standard; - - if (icon && !icon.equal(this.icon)) - this.icon = icon; -@@ -274,6 +309,7 @@ class VolumeAppInfo extends LocationAppInfo { - icon: volume.get_icon(), - cancellable, - }); -+ this._updateLocationIcon({ custom: true }); - } - - vfunc_dup() { -@@ -369,6 +405,7 @@ class MountAppInfo extends LocationAppInfo { - icon: mount.get_icon(), - cancellable, - }); -+ this._updateLocationIcon({ custom: true }); - } - - vfunc_dup() { - -From 6765ac695a9f778c81cd648753a63eea71e06c01 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Mon, 13 Dec 2021 15:47:18 +0100 -Subject: [PATCH 33/51] fileManager1API: Include transient windows as part of - the location windows - -If a location window has a transient window opened, we need to ensure -that it's part of the parent window group, otherwise it may be -associated with another location or file-manager icon. ---- - fileManager1API.js | 9 ++++++--- - 1 file changed, 6 insertions(+), 3 deletions(-) - -diff --git a/fileManager1API.js b/fileManager1API.js -index c3abeb76a..022e9485d 100644 ---- a/fileManager1API.js -+++ b/fileManager1API.js -@@ -165,9 +165,12 @@ var FileManager1Client = class DashToDock_FileManager1Client { - - Object.entries(locationsByWindowsPath).forEach(([windowPath, locations]) => { - locations.forEach(location => { -- const window = this._windowsByPath.get(windowPath); -+ const win = this._windowsByPath.get(windowPath); -+ const windowGroup = win ? [win] : []; - -- if (window) { -+ win?.foreach_transient(w => (windowGroup.push(w) || true)); -+ -+ windowGroup.forEach(window => { - location += location.endsWith('/') ? '' : '/'; - // Use a set to deduplicate when a window has a - // location open in multiple tabs. -@@ -185,7 +188,7 @@ var FileManager1Client = class DashToDock_FileManager1Client { - this._windowsByLocation.delete(location); - this.emit('windows-changed'); - }); -- } -+ }); - }); - }); - - -From 4173bb934217611aa73cfbbbc05c56a053fdcd7a Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Mon, 13 Dec 2021 16:43:21 +0100 -Subject: [PATCH 34/51] locations: Monitor windows-changed from apps and not - from fm1Client - -Windows changes may not be processed at the same time we got a fm1Client -change now, so we should monitor apps signals instead of rely on what -happens on the backend ---- - locations.js | 19 ++++++++++++++++--- - 1 file changed, 16 insertions(+), 3 deletions(-) - -diff --git a/locations.js b/locations.js -index c8d2e3955..03181f793 100644 ---- a/locations.js -+++ b/locations.js -@@ -813,11 +813,21 @@ function wrapFileManagerApp() { - const originalGetWindows = fileManagerApp.get_windows; - wrapWindowsBackedApp(fileManagerApp); - -- const { fm1Client } = Docking.DockManager.getDefault(); -+ const { removables, trash } = Docking.DockManager.getDefault(); - fileManagerApp._signalConnections.addWithLabel('windowsChanged', - fileManagerApp, 'windows-changed', () => fileManagerApp._updateWindows()); -- fileManagerApp._signalConnections.add(fm1Client, 'windows-changed', () => -- fileManagerApp._updateWindows()); -+ -+ if (removables) { -+ fileManagerApp._signalConnections.add(removables, 'changed', () => -+ fileManagerApp._updateWindows()); -+ fileManagerApp._signalConnections.add(removables, 'windows-changed', () => -+ fileManagerApp._updateWindows()); -+ } -+ -+ if (trash?.getApp()) { -+ fileManagerApp._signalConnections.add(trash.getApp(), 'windows-changed', () => -+ fileManagerApp._updateWindows()); -+ } - - fileManagerApp._updateWindows = function () { - const locationWindows = []; -@@ -1051,6 +1061,9 @@ var Removables = class DashToDock_Removables { - appInfo, - fallbackIconName: FALLBACK_REMOVABLE_MEDIA_ICON, - }); -+ -+ volumeApp._signalConnections.add(volumeApp, 'windows-changed', -+ () => this.emit('windows-changed', volumeApp)); - this._volumeApps.push(volumeApp); - this.emit('changed'); - } - -From d56bef751c3c69a4ac6a79f6746b645cf27ee302 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Mon, 13 Dec 2021 17:12:44 +0100 -Subject: [PATCH 35/51] locations: Ignore native file manager windows-changed - signal emissions - -We don't care about exposing windows-changed signal if the file manager -app is wrapped to isolate devices. As we're handling signaling manually -anyways ---- - locations.js | 5 ++++- - 1 file changed, 4 insertions(+), 1 deletion(-) - -diff --git a/locations.js b/locations.js -index 03181f793..4a30455b7 100644 ---- a/locations.js -+++ b/locations.js -@@ -815,7 +815,10 @@ function wrapFileManagerApp() { - - const { removables, trash } = Docking.DockManager.getDefault(); - fileManagerApp._signalConnections.addWithLabel('windowsChanged', -- fileManagerApp, 'windows-changed', () => fileManagerApp._updateWindows()); -+ fileManagerApp, 'windows-changed', () => { -+ fileManagerApp.stop_emission_by_name('windows-changed'); -+ fileManagerApp._updateWindows(); -+ }); - - if (removables) { - fileManagerApp._signalConnections.add(removables, 'changed', () => - -From 7074fc7835ef3fb1da0b5b67dc737a94498dadc3 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Mon, 13 Dec 2021 17:14:13 +0100 -Subject: [PATCH 36/51] locations: Try to get trash status from metadata info - before iterating - -Try to just read the trash item count file info before enumerating the -trash. It may be just cheaper and will work anyways ---- - locations.js | 18 ++++++++++++++++-- - 1 file changed, 16 insertions(+), 2 deletions(-) - -diff --git a/locations.js b/locations.js -index 4a30455b7..07039ad3b 100644 ---- a/locations.js -+++ b/locations.js -@@ -916,9 +916,23 @@ var Trash = class DashToDock_Trash { - } - - async _updateTrash() { -+ const priority = GLib.PRIORITY_LOW; -+ const cancellable = this._cancellable; -+ -+ try { -+ const trashInfo = await this._file.query_info_async( -+ Gio.FILE_ATTRIBUTE_TRASH_ITEM_COUNT, -+ Gio.FileQueryInfoFlags.NONE, -+ priority, cancellable); -+ this._updateApp(!trashInfo.get_attribute_uint32( -+ Gio.FILE_ATTRIBUTE_TRASH_ITEM_COUNT)); -+ return; -+ } catch (e) { -+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) -+ logError(e, 'Impossible to get trash children from infos'); -+ } -+ - try { -- const priority = GLib.PRIORITY_LOW; -- const cancellable = this._cancellable; - const childrenEnumerator = await this._file.enumerate_children_async( - Gio.FILE_ATTRIBUTE_STANDARD_TYPE, Gio.FileQueryInfoFlags.NONE, - priority, cancellable); - -From 1b5b9a67b12f8ec4840c6ce639d283b1223755ba Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Mon, 13 Dec 2021 17:52:23 +0100 -Subject: [PATCH 37/51] locations: Move handling of trash logic into - TrashAppInfo - -Since we've a native trash app info now we can handle the logic inside -that. - -We need to handle data destruction from parent app wrapper though ---- - locations.js | 165 ++++++++++++++++++++++++++------------------------- - 1 file changed, 83 insertions(+), 82 deletions(-) - -diff --git a/locations.js b/locations.js -index 07039ad3b..6f8025af8 100644 ---- a/locations.js -+++ b/locations.js -@@ -492,6 +492,18 @@ const TrashAppInfo = GObject.registerClass({ - }, - }, - class TrashAppInfo extends LocationAppInfo { -+ static initPromises(file) { -+ if (TrashAppInfo._promisified) -+ return; -+ -+ const trashProto = file.constructor.prototype; -+ Gio._promisify(Gio.FileEnumerator.prototype, 'close_async', 'close_finish'); -+ Gio._promisify(Gio.FileEnumerator.prototype, 'next_files_async', 'next_files_finish'); -+ Gio._promisify(trashProto, 'enumerate_children_async', 'enumerate_children_finish'); -+ Gio._promisify(trashProto, 'query_info_async', 'query_info_finish'); -+ TrashAppInfo._promisified = true; -+ } -+ - _init(cancellable = null) { - super._init({ - location: Gio.file_new_for_uri(TRASH_URI), -@@ -499,10 +511,33 @@ class TrashAppInfo extends LocationAppInfo { - icon: Gio.ThemedIcon.new(FALLBACK_TRASH_ICON), - cancellable, - }); -+ TrashAppInfo.initPromises(this.location); -+ -+ try { -+ this._monitor = this.location.monitor_directory(0, this.cancellable); -+ this._monitor.set_rate_limit(UPDATE_TRASH_DELAY); -+ this._monitorChangedId = this._monitor.connect('changed', () => -+ this._onTrashChange()); -+ } catch (e) { -+ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) -+ return; -+ logError(e, 'Impossible to monitor trash'); -+ } -+ this._updateTrash(); -+ - this.connect('notify::empty', () => this._updateLocationIcon()); - this.notify('empty'); - } - -+ destroy() { -+ if (this._trashChangedIdle) -+ GLib.source_remove(this._trashChangedIdle); -+ -+ this._monitor?.disconnect(this._monitorChangedId); -+ this._monitor = null; -+ this.location = null; -+ } -+ - list_actions() { - return this.empty ? [] : ['empty-trash']; - } -@@ -516,6 +551,53 @@ class TrashAppInfo extends LocationAppInfo { - } - } - -+ _onTrashChange() { -+ if (this._trashChangedIdle) -+ return; -+ -+ if (this._monitor.is_cancelled()) -+ return; -+ -+ this._trashChangedIdle = GLib.timeout_add( -+ GLib.PRIORITY_LOW, UPDATE_TRASH_DELAY, () => { -+ this._trashChangedIdle = 0; -+ this._updateTrash(); -+ return GLib.SOURCE_REMOVE; -+ }); -+ } -+ -+ async _updateTrash() { -+ const priority = GLib.PRIORITY_LOW; -+ const { cancellable } = this; -+ -+ try { -+ const trashInfo = await this.location.query_info_async( -+ Gio.FILE_ATTRIBUTE_TRASH_ITEM_COUNT, -+ Gio.FileQueryInfoFlags.NONE, -+ priority, cancellable); -+ this.empty = !trashInfo.get_attribute_uint32( -+ Gio.FILE_ATTRIBUTE_TRASH_ITEM_COUNT); -+ return; -+ } catch (e) { -+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) -+ logError(e, 'Impossible to get trash children from infos'); -+ } -+ -+ try { -+ const childrenEnumerator = await this.location.enumerate_children_async( -+ Gio.FILE_ATTRIBUTE_STANDARD_TYPE, Gio.FileQueryInfoFlags.NONE, -+ priority, cancellable); -+ const children = await childrenEnumerator.next_files_async(1, -+ priority, cancellable); -+ this.empty = !children.length; -+ -+ await childrenEnumerator.close_async(priority, null); -+ } catch (e) { -+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) -+ logError(e, 'Impossible to enumerate trash children'); -+ } -+ } -+ - launchAction(action, timestamp) { - if (!this.list_actions().includes(action)) - throw new Error('Action %s is not supported by %s', action, this); -@@ -707,6 +789,7 @@ function wrapWindowsBackedApp(shellApp) { - this._dtdData.proxyProperties.forEach(p => (delete this[p])); - this._dtdData.destroy(); - this._dtdData = undefined; -+ this.appInfo.destroy && this.appInfo.destroy(); - this.destroy = defaultDestroy; - defaultDestroy && defaultDestroy.call(this); - } -@@ -862,98 +945,16 @@ function unWrapFileManagerApp() { - * up-to-date as the trash fills and is emptied over time. - */ - var Trash = class DashToDock_Trash { -- static initPromises() { -- if (Trash._promisified) -- return; -- -- const trashProto = Gio.file_new_for_uri(TRASH_URI).constructor.prototype; -- Gio._promisify(Gio.FileEnumerator.prototype, 'close_async', 'close_finish'); -- Gio._promisify(Gio.FileEnumerator.prototype, 'next_files_async', 'next_files_finish'); -- Gio._promisify(trashProto, 'enumerate_children_async', 'enumerate_children_finish'); -- Gio._promisify(trashProto, 'query_info_async', 'query_info_finish'); -- Trash._promisified = true; -- } -- - constructor() { -- Trash.initPromises(); - this._cancellable = new Gio.Cancellable(); -- this._file = Gio.file_new_for_uri(TRASH_URI); -- try { -- this._monitor = this._file.monitor_directory(0, this._cancellable); -- this._monitor.set_rate_limit(UPDATE_TRASH_DELAY); -- this._signalId = this._monitor.connect('changed', () => this._onTrashChange()); -- } catch (e) { -- if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) -- return; -- logError(e, 'Impossible to monitor trash'); -- } -- this._schedUpdateId = 0; -- this._updateTrash(); - } - - destroy() { - this._cancellable.cancel(); - this._cancellable = null; -- this._monitor?.disconnect(this._signalId); -- this._monitor = null; -- this._file = null; - this._trashApp?.destroy(); - } - -- _onTrashChange() { -- if (this._schedUpdateId) -- return; -- -- if (this._monitor.is_cancelled()) -- return; -- -- this._schedUpdateId = GLib.timeout_add( -- GLib.PRIORITY_LOW, UPDATE_TRASH_DELAY, () => { -- this._schedUpdateId = 0; -- this._updateTrash(); -- return GLib.SOURCE_REMOVE; -- }); -- } -- -- async _updateTrash() { -- const priority = GLib.PRIORITY_LOW; -- const cancellable = this._cancellable; -- -- try { -- const trashInfo = await this._file.query_info_async( -- Gio.FILE_ATTRIBUTE_TRASH_ITEM_COUNT, -- Gio.FileQueryInfoFlags.NONE, -- priority, cancellable); -- this._updateApp(!trashInfo.get_attribute_uint32( -- Gio.FILE_ATTRIBUTE_TRASH_ITEM_COUNT)); -- return; -- } catch (e) { -- if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) -- logError(e, 'Impossible to get trash children from infos'); -- } -- -- try { -- const childrenEnumerator = await this._file.enumerate_children_async( -- Gio.FILE_ATTRIBUTE_STANDARD_TYPE, Gio.FileQueryInfoFlags.NONE, -- priority, cancellable); -- const children = await childrenEnumerator.next_files_async(1, -- priority, cancellable); -- this._updateApp(!children.length); -- -- await childrenEnumerator.close_async(priority, null); -- } catch (e) { -- if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) -- logError(e, 'Impossible to enumerate trash children'); -- } -- } -- -- _updateApp(isEmpty) { -- if (!this._trashApp) -- return -- -- this._trashApp.appInfo.empty = isEmpty; -- } -- - _ensureApp() { - if (this._trashApp) - return; - -From 97b69dfa64cd409c1ca6ec6012e5c03f6ab5042a Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Mon, 13 Dec 2021 20:37:53 +0100 -Subject: [PATCH 38/51] locations: Manage all mountable volumes, whether - they're mounted or not - -We used to control volumes and volumes differently, while a mount can -always refer to its parent volume, so we can just handle both cases -using a more abstract class to handle all the mountable volumes so that -in case an unmounted volume gets mounted we can avoid creating a new -app but just update the state of the old one. - -This will allow also to control more devices as the ones that are in a -not-mounted state ---- - locations.js | 308 ++++++++++++++++++++++----------------------------- - 1 file changed, 130 insertions(+), 178 deletions(-) - -diff --git a/locations.js b/locations.js -index 6f8025af8..80ed318fa 100644 ---- a/locations.js -+++ b/locations.js -@@ -77,11 +77,11 @@ var LocationAppInfo = GObject.registerClass({ - Properties: { - 'location': GObject.ParamSpec.object( - 'location', 'location', 'location', -- GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, -+ GObject.ParamFlags.READWRITE, - Gio.File.$gtype), - 'name': GObject.ParamSpec.string( - 'name', 'name', 'name', -- GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, -+ GObject.ParamFlags.READWRITE, - null), - 'icon': GObject.ParamSpec.object( - 'icon', 'icon', 'icon', -@@ -291,41 +291,56 @@ var LocationAppInfo = GObject.registerClass({ - } - }); - --const VolumeAppInfo = GObject.registerClass({ -+const MountableVolumeAppInfo = GObject.registerClass({ - Implements: [Gio.AppInfo], - Properties: { - 'volume': GObject.ParamSpec.object( - 'volume', 'volume', 'volume', - GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, - Gio.Volume.$gtype), -+ 'mount': GObject.ParamSpec.object( -+ 'mount', 'mount', 'mount', -+ GObject.ParamFlags.READWRITE, -+ Gio.Mount.$gtype), - }, - }, --class VolumeAppInfo extends LocationAppInfo { -+class MountableVolumeAppInfo extends LocationAppInfo { - _init(volume, cancellable = null) { - super._init({ - volume, -- location: volume.get_activation_root(), -- name: volume.get_name(), -- icon: volume.get_icon(), - cancellable, - }); -- this._updateLocationIcon({ custom: true }); -+ -+ this._signalsHandler = new Utils.GlobalSignalsHandler(); -+ -+ const updateAndMonitor = () => { -+ this._update(); -+ this._monitorChanges(); -+ }; -+ updateAndMonitor(); -+ this._mountChanged = this.connect('notify::mount', updateAndMonitor); -+ } -+ -+ destroy() { -+ this.disconnect(this._mountChanged); -+ this.mount = null; -+ this._signalsHandler.destroy(); - } - - vfunc_dup() { -- return new VolumeAppInfo({ -+ return new MountableVolumeAppInfo({ - volume: this.volume, - cancellable: this.cancellable, - }); - } - - vfunc_get_id() { -- const uuid = this.volume.get_uuid(); -- return uuid ? 'volume:%s'.format(uuid) : super.vfunc_get_id(); -+ const uuid = this.mount?.get_uuid() ?? this.volume.get_uuid(); -+ return uuid ? 'mountable-volume:%s'.format(uuid) : super.vfunc_get_id(); - } - - vfunc_equal(other) { -- if (this.volume === other?.volume) -+ if (this.volume === other?.volume && this.mount === other?.mount) - return true; - - return this.get_id() === other?.get_id(); -@@ -333,6 +348,16 @@ class VolumeAppInfo extends LocationAppInfo { - - list_actions() { - const actions = []; -+ const { mount } = this; -+ -+ if (mount) { -+ if (this.mount.can_unmount()) -+ actions.push('unmount'); -+ if (this.mount.can_eject()) -+ actions.push('eject'); -+ -+ return actions; -+ } - - if (this.volume.can_mount()) - actions.push('mount'); -@@ -346,6 +371,8 @@ class VolumeAppInfo extends LocationAppInfo { - switch (action) { - case 'mount': - return __('Mount'); -+ case 'unmount': -+ return __('Unmount'); - case 'eject': - return __('Eject'); - default: -@@ -353,99 +380,53 @@ class VolumeAppInfo extends LocationAppInfo { - } - } - -- async launchAction(action) { -- if (!this.list_actions().includes(action)) -- throw new Error('Action %s is not supported by %s', action, this); -- -- const operation = new ShellMountOperation.ShellMountOperation(this.volume); -- try { -- if (action === 'mount') { -- await this.volume.mount(Gio.MountMountFlags.NONE, operation.mountOp, -- this.cancellable); -- } else if (action === 'eject') { -- await this.volume.eject_with_operation(Gio.MountUnmountFlags.FORCE, -- operation.mountOp, this.cancellable); -- } -- } catch (e) { -- if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED)) { -- if (action === 'mount') { -- global.notify_error(__("Failed to mount “%s”".format( -- this.get_name())), e.message); -- } else if (action === 'eject') { -- global.notify_error(__("Failed to eject “%s”".format( -- this.get_name())), e.message); -- } -- } -- -- if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { -- logError(e, 'Impossible to %s volume %s'.format(action, -- this.volume.get_name())); -- } -- } finally { -- operation.close(); -- } -- } --}); -+ vfunc_launch(files, context) { -+ if (this.mount || files?.length) -+ return super.vfunc_launch(files, context); - --const MountAppInfo = GObject.registerClass({ -- Implements: [Gio.AppInfo], -- Properties: { -- 'mount': GObject.ParamSpec.object( -- 'mount', 'mount', 'mount', -- GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, -- Gio.Mount.$gtype), -- }, --}, --class MountAppInfo extends LocationAppInfo { -- _init(mount, cancellable = null) { -- super._init({ -- mount, -- location: mount.get_default_location(), -- name: mount.get_name(), -- icon: mount.get_icon(), -- cancellable, -- }); -- this._updateLocationIcon({ custom: true }); -+ this.mountAndLaunch(files, context); -+ return true; - } - -- vfunc_dup() { -- return new MountAppInfo({ -- mount: this.mount, -- cancellable: this.cancellable, -- }); -- } -+ _update() { -+ this.mount = this.volume.get_mount(); - -- vfunc_get_id() { -- const uuid = this.mount.get_uuid() ?? this.mount.get_volume()?.get_uuid(); -- return uuid ? 'mount:%s'.format(uuid) : super.vfunc_get_id(); -- } -+ const removable = this.mount ?? this.volume; -+ this.name = removable.get_name(); -+ this.icon = removable.get_icon(); - -- vfunc_equal(other) { -- if (this.mount === other?.mount) -- return true; -+ this.location = this.mount?.get_default_location() ?? -+ this.volume.get_activation_root(); - -- return this.get_id() === other?.get_id(); -+ this._updateLocationIcon({ custom: true }); - } - -- list_actions() { -- const actions = []; -+ _monitorChanges() { -+ this._signalsHandler.destroy(); - -- if (this.mount.can_unmount()) -- actions.push('unmount'); -- if (this.mount.can_eject()) -- actions.push('eject'); -+ const removable = this.mount ?? this.volume; -+ this._signalsHandler.add(removable, 'changed', () => this._update()); - -- return actions; -+ if (this.mount) { -+ this._signalsHandler.add(this.mount, 'pre-unmount', () => this._update()); -+ this._signalsHandler.add(this.mount, 'unmounted', () => this._update()); -+ } - } - -- get_action_name(action) { -- switch (action) { -- case 'unmount': -- return __('Unmount'); -- case 'eject': -- return __('Eject'); -- default: -- return null; -+ async mountAndLaunch(files, context) { -+ if (this.mount) -+ return super.vfunc_launch(files, context); -+ -+ try { -+ await this.launchAction('mount'); -+ if (!this.mount) { -+ throw new Error('No mounted location to open for %s'.format( -+ this.get_id())); -+ } -+ -+ return super.vfunc_launch(files, context); -+ } catch (e) { -+ logError(e, 'Mount and launch %s'.format(this.get_id())); - } - } - -@@ -453,18 +434,31 @@ class MountAppInfo extends LocationAppInfo { - if (!this.list_actions().includes(action)) - throw new Error('Action %s is not supported by %s', action, this); - -- const operation = new ShellMountOperation.ShellMountOperation(this.mount); -+ const removable = this.mount ?? this.volume; -+ const operation = new ShellMountOperation.ShellMountOperation(removable); - try { -- if (action === 'unmount') { -+ if (action === 'mount') { -+ await this.volume.mount(Gio.MountMountFlags.NONE, operation.mountOp, -+ this.cancellable); -+ } else if (action === 'unmount') { - await this.mount.unmount_with_operation(Gio.MountUnmountFlags.FORCE, - operation.mountOp, this.cancellable); - } else if (action === 'eject') { -- await this.mount.eject_with_operation(Gio.MountUnmountFlags.FORCE, -+ await removable.eject_with_operation(Gio.MountUnmountFlags.FORCE, - operation.mountOp, this.cancellable); -+ } else { -+ logError(new Error(), 'No action %s on removable %s'.format(action, -+ removable.get_name())); -+ return false; - } -+ -+ return true; - } catch (e) { - if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED)) { -- if (action === 'unmount') { -+ if (action === 'mount') { -+ global.notify_error(__("Failed to mount “%s”".format( -+ this.get_name())), e.message); -+ } else if (action === 'unmount') { - global.notify_error(__("Failed to umount “%s”".format( - this.get_name())), e.message); - } else if (action === 'eject') { -@@ -472,11 +466,15 @@ class MountAppInfo extends LocationAppInfo { - this.get_name())), e.message); - } - } -+ - if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { -- logError(e, 'Impossible to %s mount %s'.format(action, -- this.mount.get_name())); -+ logError(e, 'Impossible to %s removable %s'.format(action, -+ removable.get_name())); - } -+ -+ return false; - } finally { -+ this._update(); - operation.close(); - } - } -@@ -770,6 +768,7 @@ function wrapWindowsBackedApp(shellApp) { - try { - this.launch(timestamp, workspace, Shell.AppLaunchGpu.APP_PREF); - } catch (e) { -+ logError(e); - global.notify_error(_("Failed to launch “%s”".format( - this.get_name())), e.message); - } -@@ -1008,73 +1007,60 @@ var Removables = class DashToDock_Removables { - - this._monitor = Gio.VolumeMonitor.get(); - this._cancellable = new Gio.Cancellable(); -- this._volumeApps = [] -- this._mountApps = [] -- -- this._monitor.get_volumes().forEach( -- (volume) => { -- this._onVolumeAdded(this._monitor, volume); -- } -- ); - -- this._monitor.get_mounts().forEach( -- (mount) => { -- this._onMountAdded(this._monitor, mount); -- } -- ); -+ this._monitor.get_mounts().forEach(m => Removables.initMountPromises(m)); -+ this._updateVolumes(); - - this._signalsHandler.add([ -- this._monitor, -- 'mount-added', -- this._onMountAdded.bind(this) -- ], [ -- this._monitor, -- 'mount-removed', -- this._onMountRemoved.bind(this) -- ], [ - this._monitor, - 'volume-added', -- this._onVolumeAdded.bind(this) -+ (_, volume) => this._onVolumeAdded(volume), - ], [ - this._monitor, - 'volume-removed', -- this._onVolumeRemoved.bind(this) -+ (_, volume) => this._onVolumeRemoved(volume), -+ ], [ -+ this._monitor, -+ 'mount-added', -+ (_, mount) => this._onMountAdded(mount), - ]); - } - - destroy() { -- [...this._volumeApps, ...this._mountApps].forEach(a => a.destroy()); -+ this._volumeApps.forEach(a => a.destroy()); - this._volumeApps = []; -- this._mountApps = []; - this._cancellable.cancel(); - this._cancellable = null; - this._signalsHandler.destroy(); - this._monitor = null; - } - -- _onVolumeAdded(monitor, volume) { -- Removables.initVolumePromises(volume); -+ _updateVolumes() { -+ this._volumeApps?.forEach(a => a.destroy()); -+ this._volumeApps = []; -+ this.emit('changed'); - -- if (volume.get_mount()) -- return; -+ this._monitor.get_volumes().forEach(v => this._onVolumeAdded(v)); -+ } - -- if (!volume.can_mount() && !volume.can_eject()) { -- return; -- } -+ _onVolumeAdded(volume) { -+ Removables.initVolumePromises(volume); - - if (volume.get_identifier('class') == 'network') { - return; - } - -- if (!volume.get_activation_root()) { -- // Can't offer to mount a device if we don't know -- // where to mount it. -- // These devices are usually ejectable so you -- // don't normally unmount them anyway. -+ const mount = volume.get_mount(); -+ if (mount) { -+ if (mount.is_shadowed()) -+ return; -+ if (!mount.can_eject() && !mount.can_unmount()) -+ return; -+ } else { - return; - } - -- const appInfo = new VolumeAppInfo(volume, this._cancellable); -+ const appInfo = new MountableVolumeAppInfo(volume, this._cancellable); - const volumeApp = makeLocationApp({ - appInfo, - fallbackIconName: FALLBACK_REMOVABLE_MEDIA_ICON, -@@ -1082,11 +1068,13 @@ var Removables = class DashToDock_Removables { - - volumeApp._signalConnections.add(volumeApp, 'windows-changed', - () => this.emit('windows-changed', volumeApp)); -+ volumeApp._signalConnections.add(appInfo, 'notify::mount', -+ () => (!appInfo.mount && this._onVolumeRemoved(appInfo.volume))); - this._volumeApps.push(volumeApp); - this.emit('changed'); - } - -- _onVolumeRemoved(monitor, volume) { -+ _onVolumeRemoved(volume) { - const volumeIndex = this._volumeApps.findIndex(({ appInfo }) => - appInfo.volume === volume); - if (volumeIndex !== -1) { -@@ -1096,51 +1084,15 @@ var Removables = class DashToDock_Removables { - } - } - -- _onMountAdded(monitor, mount) { -+ _onMountAdded(mount) { - Removables.initMountPromises(mount); - -- // Filter out uninteresting mounts -- if (!mount.can_eject() && !mount.can_unmount()) -- return; -- if (mount.is_shadowed()) -- return; -- -- let volume = mount.get_volume(); -- if (!volume || volume.get_identifier('class') == 'network') { -- return; -- } -- -- const appInfo = new MountAppInfo(mount, this._cancellable); -- const mountApp = makeLocationApp({ -- appInfo, -- fallbackIconName: FALLBACK_REMOVABLE_MEDIA_ICON, -- }); -- this._mountApps.push(mountApp); -- this.emit('changed'); -- } -- -- _onMountRemoved(monitor, mount) { -- const mountIndex = this._mountApps.findIndex(({ appInfo }) => -- appInfo.mount === mount); -- if (mountIndex !== -1) { -- const [mountApp] = this._mountApps.splice(mountIndex, 1); -- mountApp.destroy(); -- this.emit('changed'); -- } -+ if (!this._volumeApps.find(({ appInfo }) => appInfo.mount === mount)) -+ this._onVolumeAdded(mount.get_volume()); - } - - getApps() { -- // When we have both a volume app and a mount app, we prefer -- // the mount app. -- let apps = new Map(); -- this._volumeApps.map(function(app) { -- apps.set(app.get_name(), app); -- }); -- this._mountApps.map(function(app) { -- apps.set(app.get_name(), app); -- }); -- -- return [...apps.values()]; -+ return this._volumeApps; - } - } - Signals.addSignalMethods(Removables.prototype); - -From f1e45bcda5d37a3a271778b2e5ec31a55c866b95 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Tue, 22 Feb 2022 23:29:01 +0100 -Subject: [PATCH 39/51] locations: Do not try to add null volumes from valid - mounts - -For some implementations we're getting the volume set after the mount so -let's ignore the volume in such case. - -LP: #1960898 ---- - locations.js | 10 ++++++++-- - 1 file changed, 8 insertions(+), 2 deletions(-) - -diff --git a/locations.js b/locations.js -index 80ed318fa..83d7f7ba5 100644 ---- a/locations.js -+++ b/locations.js -@@ -1087,8 +1087,14 @@ var Removables = class DashToDock_Removables { - _onMountAdded(mount) { - Removables.initMountPromises(mount); - -- if (!this._volumeApps.find(({ appInfo }) => appInfo.mount === mount)) -- this._onVolumeAdded(mount.get_volume()); -+ if (!this._volumeApps.find(({ appInfo }) => appInfo.mount === mount)) { -+ // In some Gio.Mount implementations the volume may be set after -+ // mount is emitted, so we could just ignore it as we'll get it -+ // later via volume-added -+ const volume = mount.get_volume(); -+ if (volume) -+ this._onVolumeAdded(volume); -+ } - } - - getApps() { - -From 07cd49ee4e402222469dcf205d3fe0a39c1a7d2f Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Mon, 13 Dec 2021 21:47:25 +0100 -Subject: [PATCH 40/51] locations: Add support for optionally showing all - mountable devices - -Some devices may not be visible in the launcher even if we can -technically list them, but it was not possible to mount them before -because we were using gio open only and we needed a mount location. - -This is not the case anymore now, so we can just support showing all the -devices, whether they're mounted or not. ---- - Settings.ui | 15 +++++++++++- - locations.js | 24 ++++++++++++++++--- - prefs.js | 4 ++++ - ....shell.extensions.dash-to-dock.gschema.xml | 5 ++++ - 4 files changed, 44 insertions(+), 4 deletions(-) - -diff --git a/Settings.ui b/Settings.ui -index 6dc692456..e275588e5 100644 ---- a/Settings.ui -+++ b/Settings.ui -@@ -1208,7 +1208,7 @@ - 0 - 1 - start -- Show mounted volumes and devices -+ Show volumes and devices - - - 0 -@@ -1216,6 +1216,19 @@ - - - -+ -+ -+ Only if mounted -+ start -+ 12 -+ -+ -+ 0 -+ 1 -+ 1 -+ -+ -+ - - - -diff --git a/locations.js b/locations.js -index 83d7f7ba5..5b6b89223 100644 ---- a/locations.js -+++ b/locations.js -@@ -1023,6 +1023,14 @@ var Removables = class DashToDock_Removables { - this._monitor, - 'mount-added', - (_, mount) => this._onMountAdded(mount), -+ ], [ -+ Docking.DockManager.settings, -+ 'changed::show-mounts-only-mounted', -+ () => this._updateVolumes(), -+ ], [ -+ Docking.DockManager.settings, -+ 'changed::show-mounts-network', -+ () => this._updateVolumes(), - ]); - } - -@@ -1057,7 +1065,10 @@ var Removables = class DashToDock_Removables { - if (!mount.can_eject() && !mount.can_unmount()) - return; - } else { -- return; -+ if (Docking.DockManager.settings.showMountsOnlyMounted) -+ return; -+ if (!volume.can_mount() && !volume.can_eject()) -+ return; - } - - const appInfo = new MountableVolumeAppInfo(volume, this._cancellable); -@@ -1068,8 +1079,12 @@ var Removables = class DashToDock_Removables { - - volumeApp._signalConnections.add(volumeApp, 'windows-changed', - () => this.emit('windows-changed', volumeApp)); -- volumeApp._signalConnections.add(appInfo, 'notify::mount', -- () => (!appInfo.mount && this._onVolumeRemoved(appInfo.volume))); -+ -+ if (Docking.DockManager.settings.showMountsOnlyMounted) { -+ volumeApp._signalConnections.add(appInfo, 'notify::mount', -+ () => (!appInfo.mount && this._onVolumeRemoved(appInfo.volume))); -+ } -+ - this._volumeApps.push(volumeApp); - this.emit('changed'); - } -@@ -1087,6 +1102,9 @@ var Removables = class DashToDock_Removables { - _onMountAdded(mount) { - Removables.initMountPromises(mount); - -+ if (!Docking.DockManager.settings.showMountsOnlyMounted) -+ return; -+ - if (!this._volumeApps.find(({ appInfo }) => appInfo.mount === mount)) { - // In some Gio.Mount implementations the volume may be set after - // mount is emitted, so we could just ignore it as we'll get it -diff --git a/prefs.js b/prefs.js -index 4de4e69a6..37aa3c282 100644 ---- a/prefs.js -+++ b/prefs.js -@@ -648,6 +648,10 @@ var Settings = GObject.registerClass({ - this._builder.get_object('show_mounts_switch'), - 'active', - Gio.SettingsBindFlags.DEFAULT); -+ this._settings.bind('show-mounts-only-mounted', -+ this._builder.get_object('show_only_mounted_devices_check'), -+ 'active', -+ Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('isolate-locations', - this._builder.get_object('isolate_locations_switch'), - 'active', -diff --git a/schemas/org.gnome.shell.extensions.dash-to-dock.gschema.xml b/schemas/org.gnome.shell.extensions.dash-to-dock.gschema.xml -index 7131b31ec..39ac661f2 100644 ---- a/schemas/org.gnome.shell.extensions.dash-to-dock.gschema.xml -+++ b/schemas/org.gnome.shell.extensions.dash-to-dock.gschema.xml -@@ -247,6 +247,11 @@ - Show mounted volumes and devices - Show or hide mounted volume and device icons in the dash - -+ -+ true -+ Only show mounted volumes and devices -+ Show or hide unmounted volume and device icons in the dash -+ - - true - Isolate volumes, devices and trash windows - -From 9321e246f4d7ae5423f894262a0b8b7a39e3a9b2 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Mon, 13 Dec 2021 22:55:54 +0100 -Subject: [PATCH 41/51] locations: Add support for showing network volumes - -Fixes #1338 ---- - Settings.ui | 13 +++++++++++++ - locations.js | 17 ++++++++++++++++- - prefs.js | 4 ++++ - ...me.shell.extensions.dash-to-dock.gschema.xml | 5 +++++ - 4 files changed, 38 insertions(+), 1 deletion(-) - -diff --git a/Settings.ui b/Settings.ui -index e275588e5..b46adcdad 100644 ---- a/Settings.ui -+++ b/Settings.ui -@@ -1229,6 +1229,19 @@ - - - -+ -+ -+ Include network volumes -+ start -+ 12 -+ -+ -+ 0 -+ 2 -+ 2 -+ -+ -+ - - - -diff --git a/locations.js b/locations.js -index 5b6b89223..32bfda7d0 100644 ---- a/locations.js -+++ b/locations.js -@@ -319,9 +319,23 @@ class MountableVolumeAppInfo extends LocationAppInfo { - }; - updateAndMonitor(); - this._mountChanged = this.connect('notify::mount', updateAndMonitor); -+ -+ if (!this.mount && this.volume.get_identifier('class') == 'network') { -+ // For some devices the mount point isn't advertised promptly -+ // even if it's already existing, and there's no signaling about -+ this._lazyUpdater = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 2, () => { -+ this._update(); -+ delete this._lazyUpdater; -+ return GLib.SOURCE_REMOVE; -+ }); -+ } - } - - destroy() { -+ if (this._lazyUpdater) { -+ GLib.source_remove(this._lazyUpdater); -+ delete this._lazyUpdater; -+ } - this.disconnect(this._mountChanged); - this.mount = null; - this._signalsHandler.destroy(); -@@ -1054,7 +1068,8 @@ var Removables = class DashToDock_Removables { - _onVolumeAdded(volume) { - Removables.initVolumePromises(volume); - -- if (volume.get_identifier('class') == 'network') { -+ if (!Docking.DockManager.settings.showMountsNetwork && -+ volume.get_identifier('class') == 'network') { - return; - } - -diff --git a/prefs.js b/prefs.js -index 37aa3c282..226493417 100644 ---- a/prefs.js -+++ b/prefs.js -@@ -652,6 +652,10 @@ var Settings = GObject.registerClass({ - this._builder.get_object('show_only_mounted_devices_check'), - 'active', - Gio.SettingsBindFlags.DEFAULT); -+ this._settings.bind('show-mounts-network', -+ this._builder.get_object('show_network_volumes_check'), -+ 'active', -+ Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('isolate-locations', - this._builder.get_object('isolate_locations_switch'), - 'active', -diff --git a/schemas/org.gnome.shell.extensions.dash-to-dock.gschema.xml b/schemas/org.gnome.shell.extensions.dash-to-dock.gschema.xml -index 39ac661f2..ef6afbf83 100644 ---- a/schemas/org.gnome.shell.extensions.dash-to-dock.gschema.xml -+++ b/schemas/org.gnome.shell.extensions.dash-to-dock.gschema.xml -@@ -252,6 +252,11 @@ - Only show mounted volumes and devices - Show or hide unmounted volume and device icons in the dash - -+ -+ false -+ Show network mounted volumes -+ Show or hide network volumes in the dash -+ - - true - Isolate volumes, devices and trash windows - -From 1b7039fc07d526b48b0ee3e599b01c1790832372 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Wed, 15 Dec 2021 17:31:08 +0100 -Subject: [PATCH 42/51] locations: Use ChildCancellable to avoid queuing - multiple trash updates - -It's just faster to wait the last event not being cancelled in an idle -instead of doing them after the idle is ready. ---- - locations.js | 26 +++++++++++++------------- - 1 file changed, 13 insertions(+), 13 deletions(-) - -diff --git a/locations.js b/locations.js -index 32bfda7d0..9206de8d0 100644 ---- a/locations.js -+++ b/locations.js -@@ -542,9 +542,7 @@ class TrashAppInfo extends LocationAppInfo { - } - - destroy() { -- if (this._trashChangedIdle) -- GLib.source_remove(this._trashChangedIdle); -- -+ this._updateTrashCancellable?.cancel(); - this._monitor?.disconnect(this._monitorChangedId); - this._monitor = null; - this.location = null; -@@ -564,23 +562,17 @@ class TrashAppInfo extends LocationAppInfo { - } - - _onTrashChange() { -- if (this._trashChangedIdle) -- return; -- - if (this._monitor.is_cancelled()) - return; - -- this._trashChangedIdle = GLib.timeout_add( -- GLib.PRIORITY_LOW, UPDATE_TRASH_DELAY, () => { -- this._trashChangedIdle = 0; -- this._updateTrash(); -- return GLib.SOURCE_REMOVE; -- }); -+ this._updateTrash(); - } - - async _updateTrash() { - const priority = GLib.PRIORITY_LOW; -- const { cancellable } = this; -+ this._updateTrashCancellable?.cancel(); -+ const cancellable = new Utils.CancellableChild(this.cancellable); -+ this._updateTrashCancellable = cancellable; - - try { - const trashInfo = await this.location.query_info_async( -@@ -593,6 +585,10 @@ class TrashAppInfo extends LocationAppInfo { - } catch (e) { - if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) - logError(e, 'Impossible to get trash children from infos'); -+ } finally { -+ cancellable.cancel(); -+ if (this._updateIconCancellable === cancellable) -+ delete this._updateTrashCancellable; - } - - try { -@@ -607,6 +603,10 @@ class TrashAppInfo extends LocationAppInfo { - } catch (e) { - if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) - logError(e, 'Impossible to enumerate trash children'); -+ } finally { -+ cancellable.cancel(); -+ if (this._updateIconCancellable === cancellable) -+ delete this._updateTrashCancellable; - } - } - - -From 666a55d95af19020a0e7506938f8a7fb0c71447c Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Wed, 15 Dec 2021 19:16:54 +0100 -Subject: [PATCH 43/51] locations: Do not try to perform (un)mount actions - concurrently - -If the user already requested to mount or umount a device, there's no -point to permit calling this again till the operation is not completed. - -Notify about ---- - locations.js | 45 +++++++++++++++++++++++++++++++++------------ - 1 file changed, 33 insertions(+), 12 deletions(-) - -diff --git a/locations.js b/locations.js -index 9206de8d0..bc3474af8 100644 ---- a/locations.js -+++ b/locations.js -@@ -444,10 +444,40 @@ class MountableVolumeAppInfo extends LocationAppInfo { - } - } - -+ _notifyActionError(action, message) { -+ if (action === 'mount') { -+ global.notify_error(__("Failed to mount “%s”".format( -+ this.get_name())), message); -+ } else if (action === 'unmount') { -+ global.notify_error(__("Failed to umount “%s”".format( -+ this.get_name())), message); -+ } else if (action === 'eject') { -+ global.notify_error(__("Failed to eject “%s”".format( -+ this.get_name())), message); -+ } -+ } -+ - async launchAction(action) { - if (!this.list_actions().includes(action)) - throw new Error('Action %s is not supported by %s', action, this); - -+ if (this._currentAction) { -+ if (this._currentAction === 'mount') { -+ this._notifyActionError(action, -+ __("Mount operation already in progress")); -+ } else if (this._currentAction === 'unmount') { -+ this._notifyActionError(action, -+ __("Umount operation already in progress")); -+ } else if (this._currentAction === 'eject') { -+ this._notifyActionError(action, -+ __("Eject operation already in progress")); -+ } -+ -+ throw new Error('Another action %s is being performed in %s'.format( -+ this._currentAction, this)); -+ } -+ -+ this._currentAction = action; - const removable = this.mount ?? this.volume; - const operation = new ShellMountOperation.ShellMountOperation(removable); - try { -@@ -468,18 +498,8 @@ class MountableVolumeAppInfo extends LocationAppInfo { - - return true; - } catch (e) { -- if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED)) { -- if (action === 'mount') { -- global.notify_error(__("Failed to mount “%s”".format( -- this.get_name())), e.message); -- } else if (action === 'unmount') { -- global.notify_error(__("Failed to umount “%s”".format( -- this.get_name())), e.message); -- } else if (action === 'eject') { -- global.notify_error(__("Failed to eject “%s”".format( -- this.get_name())), e.message); -- } -- } -+ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED)) -+ this._notifyActionError(action, e); - - if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { - logError(e, 'Impossible to %s removable %s'.format(action, -@@ -488,6 +508,7 @@ class MountableVolumeAppInfo extends LocationAppInfo { - - return false; - } finally { -+ delete this._currentAction; - this._update(); - operation.close(); - } - -From adb8f28372cd76883c4f57de19a10b5c8f7ca0b1 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Wed, 15 Dec 2021 19:24:39 +0100 -Subject: [PATCH 44/51] appIcons: Show menu item as insensitive if the app is - busy - -So, for example still present the mount/unmount operations on devices -that are being mounted/unmounted but do not leave the user activate them ---- - appIcons.js | 1 + - locations.js | 25 +++++++++++++++++++++++++ - 2 files changed, 26 insertions(+) - -diff --git a/appIcons.js b/appIcons.js -index 00eac89b7..e17db2387 100644 ---- a/appIcons.js -+++ b/appIcons.js -@@ -1026,6 +1026,7 @@ const DockAppIconMenu = class DockAppIconMenu extends PopupMenu.PopupMenu { - for (let i = 0; i < actions.length; i++) { - let action = actions[i]; - let item = this._appendMenuItem(appInfo.get_action_name(action)); -+ item.sensitive = !appInfo.busy; - item.connect('activate', (emitter, event) => { - this._source.app.launch_action(action, event.get_time(), -1); - this.emit('activate-window', null); -diff --git a/locations.js b/locations.js -index bc3474af8..fdcbcdba3 100644 ---- a/locations.js -+++ b/locations.js -@@ -302,6 +302,10 @@ const MountableVolumeAppInfo = GObject.registerClass({ - 'mount', 'mount', 'mount', - GObject.ParamFlags.READWRITE, - Gio.Mount.$gtype), -+ 'busy': GObject.ParamSpec.boolean( -+ 'busy', 'busy', 'busy', -+ GObject.ParamFlags.READWRITE, -+ false), - }, - }, - class MountableVolumeAppInfo extends LocationAppInfo { -@@ -331,6 +335,14 @@ class MountableVolumeAppInfo extends LocationAppInfo { - } - } - -+ get busy() { -+ return !!this._currentAction; -+ } -+ -+ get currentAction() { -+ return this._currentAction; -+ } -+ - destroy() { - if (this._lazyUpdater) { - GLib.source_remove(this._lazyUpdater); -@@ -478,6 +490,7 @@ class MountableVolumeAppInfo extends LocationAppInfo { - } - - this._currentAction = action; -+ this.notify('busy'); - const removable = this.mount ?? this.volume; - const operation = new ShellMountOperation.ShellMountOperation(removable); - try { -@@ -509,6 +522,7 @@ class MountableVolumeAppInfo extends LocationAppInfo { - return false; - } finally { - delete this._currentAction; -+ this.notify('busy'); - this._update(); - operation.close(); - } -@@ -867,6 +881,17 @@ function makeLocationApp(params) { - // FIXME: We need to add a new API to Nautilus to open new windows - shellApp._mi('can_open_new_window', () => false); - -+ if (shellApp.appInfo instanceof MountableVolumeAppInfo) { -+ shellApp._mi('get_busy', function (parentGetBusy) { -+ if (this.appInfo.busy) -+ return true; -+ return parentGetBusy.call(this); -+ }); -+ shellApp._pi('busy', { get: () => shellApp.get_busy() }); -+ shellApp._signalConnections.add(shellApp.appInfo, 'notify::busy', _ => -+ shellApp.notify('busy')); -+ } -+ - shellApp._mi('get_windows', function () { - if (this._needsResort) - this._sortWindows(); - -From 66f6be8afa6148e7151421e5a81ab806382fb443 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Mon, 24 Jan 2022 03:06:19 +0100 -Subject: [PATCH 45/51] locations: Wait for location apps to own windows before - updating file-manager - -When a new filemanager window is opened, we may assign it to nautilus -while we're still receiving its location infos, causing it to be -temporarily associated to the file manager app, instead to the actual -location app. - -Causing some blinking, so let's use some quick timeout to wait the dbus -events to come before proceeding with the assignments. ---- - locations.js | 8 +++++++- - 1 file changed, 7 insertions(+), 1 deletion(-) - -diff --git a/locations.js b/locations.js -index fdcbcdba3..18cc6116e 100644 ---- a/locations.js -+++ b/locations.js -@@ -959,7 +959,13 @@ function wrapFileManagerApp() { - fileManagerApp._signalConnections.addWithLabel('windowsChanged', - fileManagerApp, 'windows-changed', () => { - fileManagerApp.stop_emission_by_name('windows-changed'); -- fileManagerApp._updateWindows(); -+ // Let's wait for the location app to take control before of us -+ const id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, () => { -+ fileManagerApp._sources.delete(id); -+ fileManagerApp._updateWindows(); -+ return GLib.SOURCE_REMOVE; -+ }); -+ fileManagerApp._sources.add(id); - }); - - if (removables) { - -From 2bd01126c75739202818a4604498b0a44972c5dd Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Mon, 24 Jan 2022 03:14:54 +0100 -Subject: [PATCH 46/51] locations: Add support for starting app state - -An application can take some time to start (especially when performing -busy operations such as mounting a remote device), so in this case it's -better to mark the application as starting and only when we've windows -set as running. - -This can be now easily implemented for the case ---- - locations.js | 46 +++++++++++++++++++++++++++++++++++++++------- - 1 file changed, 39 insertions(+), 7 deletions(-) - -diff --git a/locations.js b/locations.js -index 18cc6116e..3c17e0895 100644 ---- a/locations.js -+++ b/locations.js -@@ -665,6 +665,8 @@ function wrapWindowsBackedApp(shellApp) { - - shellApp._dtdData = { - windows: [], -+ state: undefined, -+ startingWorkspace: 0, - isFocused: false, - proxyProperties: [], - sources: new Set(), -@@ -699,6 +701,8 @@ function wrapWindowsBackedApp(shellApp) { - - shellApp._dtdData.addProxyProperties(shellApp, { - windows: {}, -+ state: {}, -+ startingWorkspace: {}, - isFocused: { public: true }, - signalConnections: { readOnly: true }, - sources: { readOnly: true }, -@@ -723,8 +727,7 @@ function wrapWindowsBackedApp(shellApp) { - // mi is Method injector, pi is Property injector - shellApp._setDtdData({ mi: m, pi: p }, { public: false }); - -- m('get_state', () => -- shellApp.get_n_windows() ? Shell.AppState.RUNNING : Shell.AppState.STOPPED); -+ m('get_state', () => shellApp._state ?? shellApp._getStateByWindows()); - p('state', { get: () => shellApp.get_state() }); - - m('get_windows', () => shellApp._windows); -@@ -735,19 +738,39 @@ function wrapWindowsBackedApp(shellApp) { - return pids; - }, [])); - m('is_on_workspace', (_om, workspace) => shellApp._windows.some(w => -- w.get_workspace() === workspace)); -+ w.get_workspace() === workspace) || -+ (shellApp.state === Shell.AppState.STARTING && -+ [-1, workspace.index()].includes(shellApp._startingWorkspace))); - m('request_quit', () => shellApp._windows.filter(w => - w.can_close()).forEach(w => w.delete(global.get_current_time()))); - - shellApp._setDtdData({ -+ _getStateByWindows: function() { -+ return this.get_n_windows() ? Shell.AppState.RUNNING : Shell.AppState.STOPPED; -+ }, -+ - _updateWindows: function () { - throw new GObject.NotImplementedError(`_updateWindows in ${this.constructor.name}`); - }, - -+ _notifyStateChanged() { -+ Shell.AppSystem.get_default().emit('app-state-changed', this); -+ this.notify('state'); -+ }, -+ -+ _setState: function (state) { -+ const oldState = this.state; -+ this._state = state; -+ -+ if (this.state !== oldState) -+ this._notifyStateChanged(); -+ }, -+ - _setWindows: function (windows) { - const oldState = this.state; - const oldWindows = this.get_windows().slice(); - const result = { windowsChanged: false, stateChanged: false }; -+ this._state = undefined; - - if (windows.length !== oldWindows.length || - windows.some((win, index) => win !== oldWindows[index])) { -@@ -757,8 +780,7 @@ function wrapWindowsBackedApp(shellApp) { - } - - if (this.state !== oldState) { -- Shell.AppSystem.get_default().emit('app-state-changed', this); -- this.notify('state'); -+ this._notifyStateChanged(); - this._checkFocused(); - result.stateChanged = true; - } -@@ -815,6 +837,8 @@ function wrapWindowsBackedApp(shellApp) { - switch (this.state) { - case Shell.AppState.STOPPED: - try { -+ this._startingWorkspace = workspace; -+ this._setState(Shell.AppState.STARTING); - this.launch(timestamp, workspace, Shell.AppLaunchGpu.APP_PREF); - } catch (e) { - logError(e); -@@ -1188,7 +1212,7 @@ var Removables = class DashToDock_Removables { - } - Signals.addSignalMethods(Removables.prototype); - --function getRunningApps() { -+function getApps() { - const dockManager = Docking.DockManager.getDefault(); - const locationApps = []; - -@@ -1198,5 +1222,13 @@ function getRunningApps() { - if (dockManager.trash) - locationApps.push(dockManager.trash.getApp()); - -- return locationApps.filter(a => a.state === Shell.AppState.RUNNING); -+ return locationApps; -+} -+ -+function getRunningApps() { -+ return getApps().filter(a => a.state === Shell.AppState.RUNNING); -+} -+ -+function getStartingApps() { -+ return getApps().filter(a => a.state === Shell.AppState.STARTING); - } - -From 85ea33d0844d8e94f89a88fa03f1cf3d785d4f7f Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Mon, 24 Jan 2022 04:48:45 +0100 -Subject: [PATCH 47/51] locations: Support opening new location windows when - using nautilus - -Sadly nautilus doesn't provide an API to open new windows when a -location is already opened, so using `gio open` can lead to focusing an -already opened location. - -As per this, manually handle opening new windows if the command-line -supports --new-window parameter. ---- - locations.js | 37 +++++++++++++++++++++++++++++++++++-- - 1 file changed, 35 insertions(+), 2 deletions(-) - -diff --git a/locations.js b/locations.js -index 3c17e0895..874e2d1cb 100644 ---- a/locations.js -+++ b/locations.js -@@ -148,6 +148,10 @@ var LocationAppInfo = GObject.registerClass({ - Gio.IOErrorEnum.NOT_SUPPORTED, 'Launching with files not supported'); - } - -+ const handler = this._getHandlerApp(); -+ if (handler) -+ return handler.launch_uris([this.location.get_uri()], context); -+ - const [ret] = GLib.spawn_async(null, this.get_commandline().split(' '), - context?.get_environment() || null, GLib.SpawnFlags.SEARCH_PATH, null); - return ret; -@@ -201,7 +205,8 @@ var LocationAppInfo = GObject.registerClass({ - } - - vfunc_get_commandline() { -- return 'gio open %s'.format(this.location?.get_uri()); -+ return this._getHandlerApp()?.get_commandline() ?? -+ 'gio open %s'.format(this.location?.get_uri()); - } - - vfunc_get_display_name() { -@@ -289,6 +294,24 @@ var LocationAppInfo = GObject.registerClass({ - delete this._updateIconCancellable; - } - } -+ -+ _getHandlerApp() { -+ if (!this.location) -+ return null; -+ -+ try { -+ return this.location.query_default_handler(this.cancellable); -+ } catch (e) { -+ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_MOUNTED)) -+ return getFileManagerApp()?.appInfo; -+ -+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { -+ logError(e, 'Impossible to find an URI handler for %s'.format( -+ this.get_id())); -+ } -+ return null; -+ } -+ } - }); - - const MountableVolumeAppInfo = GObject.registerClass({ -@@ -903,7 +926,17 @@ function makeLocationApp(params) { - })); - - // FIXME: We need to add a new API to Nautilus to open new windows -- shellApp._mi('can_open_new_window', () => false); -+ shellApp._mi('can_open_new_window', () => -+ shellApp.appInfo.get_commandline()?.split(' ').includes('--new-window')); -+ -+ shellApp._mi('open_new_window', function (_om, workspace) { -+ const context = global.create_app_launch_context(0, workspace); -+ const [ret] = GLib.spawn_async(null, -+ [...this.appInfo.get_commandline().split(' ').filter( -+ t => !t.startsWith('%')), this.appInfo.location.get_uri() ], -+ context.get_environment(), GLib.SpawnFlags.SEARCH_PATH, null); -+ return ret; -+ }); - - if (shellApp.appInfo instanceof MountableVolumeAppInfo) { - shellApp._mi('get_busy', function (parentGetBusy) { - -From 0f97f600757ab3817be7a10a8f478a9f8fcc2320 Mon Sep 17 00:00:00 2001 -From: Daniel van Vugt -Date: Wed, 2 Mar 2022 15:30:36 +0800 -Subject: [PATCH 48/51] locations: Reintroduce manual rate limiting for trash - monitoring - -Because `g_file_monitor_set_rate_limit` doesn't work. It's a stub in -glib! - -Fixes: https://launchpad.net/bugs/1962699 ---- - locations.js | 18 ++++++++++++++++-- - 1 file changed, 16 insertions(+), 2 deletions(-) - -diff --git a/locations.js b/locations.js -index 874e2d1cb..0aa7f8407 100644 ---- a/locations.js -+++ b/locations.js -@@ -585,7 +585,7 @@ class TrashAppInfo extends LocationAppInfo { - - try { - this._monitor = this.location.monitor_directory(0, this.cancellable); -- this._monitor.set_rate_limit(UPDATE_TRASH_DELAY); -+ this._schedUpdateId = 0; - this._monitorChangedId = this._monitor.connect('changed', () => - this._onTrashChange()); - } catch (e) { -@@ -600,6 +600,10 @@ class TrashAppInfo extends LocationAppInfo { - } - - destroy() { -+ if (this._schedUpdateId) { -+ GLib.source_remove(this._schedUpdateId); -+ this._schedUpdateId = 0; -+ } - this._updateTrashCancellable?.cancel(); - this._monitor?.disconnect(this._monitorChangedId); - this._monitor = null; -@@ -620,10 +624,20 @@ class TrashAppInfo extends LocationAppInfo { - } - - _onTrashChange() { -+ if (this._schedUpdateId) { -+ GLib.source_remove(this._schedUpdateId); -+ this._schedUpdateId = 0; -+ } -+ - if (this._monitor.is_cancelled()) - return; - -- this._updateTrash(); -+ this._schedUpdateId = GLib.timeout_add(GLib.PRIORITY_LOW, -+ UPDATE_TRASH_DELAY, () => { -+ this._schedUpdateId = 0; -+ this._updateTrash(); -+ return GLib.SOURCE_REMOVE; -+ }); - } - - async _updateTrash() { - -From ae11cb72c091d413887312441cdbe78fa2e0befc Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Mon, 11 Apr 2022 17:26:49 +0200 -Subject: [PATCH 49/51] locations: Always ignore OR windows in windows-backed - app implementation - -We were already filtering them out, but this follows upstream change: - https://gitlab.gnome.org/GNOME/gnome-shell/-/commit/c02ca54943 ---- - locations.js | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/locations.js b/locations.js -index 0aa7f8407..9bd7ba669 100644 ---- a/locations.js -+++ b/locations.js -@@ -811,7 +811,7 @@ function wrapWindowsBackedApp(shellApp) { - - if (windows.length !== oldWindows.length || - windows.some((win, index) => win !== oldWindows[index])) { -- this._windows = windows; -+ this._windows = windows.filter(w => !w.is_override_redirect()); - this.emit('windows-changed'); - result.windowsChanged = true; - } - -From d3783824840c969f4990e1bd2a1b56517bddb73c Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Thu, 28 Apr 2022 17:57:55 +0200 -Subject: [PATCH 50/51] appIcons/prefs: Follow upstream terminology for - Favorite or Pinned apps - -We use the same upstream wording here, to avoid having to change it in -multiple places, so it has to be dynamic. Depending on the version we're -running on. - -Fixes #1697 ---- - Settings.ui | 2 +- - appIcons.js | 11 +++++++++-- - prefs.js | 6 ++++++ - 3 files changed, 16 insertions(+), 3 deletions(-) - -diff --git a/Settings.ui b/Settings.ui -index b46adcdad..83bc10499 100644 ---- a/Settings.ui -+++ b/Settings.ui -@@ -921,7 +921,7 @@ - 0 - 1 - start -- Show favorite applications -+ Show pinned applications - - - 0 -diff --git a/appIcons.js b/appIcons.js -index e17db2387..cfb415f2e 100644 ---- a/appIcons.js -+++ b/appIcons.js -@@ -16,6 +16,8 @@ const Gettext = imports.gettext.domain('dashtodock'); - const __ = Gettext.gettext; - const N__ = function(e) { return e }; - -+const Config = imports.misc.config; -+ - const AppDisplay = imports.ui.appDisplay; - const AppFavorites = imports.ui.appFavorites; - const BoxPointer = imports.ui.boxpointer; -@@ -1041,15 +1043,20 @@ const DockAppIconMenu = class DockAppIconMenu extends PopupMenu.PopupMenu { - this._appendSeparator(); - - let isFavorite = AppFavorites.getAppFavorites().isFavorite(this._source.app.get_id()); -+ const [majorVersion] = Config.PACKAGE_VERSION.split('.'); - - if (isFavorite) { -- let item = this._appendMenuItem(_('Remove from Favorites')); -+ const label = majorVersion >= 42 ? _('Unpin') : -+ _('Remove from Favorites'); -+ let item = this._appendMenuItem(label); - item.connect('activate', () => { - let favs = AppFavorites.getAppFavorites(); - favs.removeFavorite(this._source.app.get_id()); - }); - } else { -- let item = this._appendMenuItem(_('Add to Favorites')); -+ const label = majorVersion >= 42 ? _('Pin to Dash') : -+ _('Add to Favorites'); -+ let item = this._appendMenuItem(label); - item.connect('activate', () => { - let favs = AppFavorites.getAppFavorites(); - favs.addFavorite(this._source.app.get_id()); -diff --git a/prefs.js b/prefs.js -index 226493417..e97900b6e 100644 ---- a/prefs.js -+++ b/prefs.js -@@ -224,6 +224,12 @@ var Settings = GObject.registerClass({ - this._icon_size_timeout = 0; - this._opacity_timeout = 0; - -+ if (Config.PACKAGE_VERSION.split('.')[0] < 42) { -+ // Remove this when we won't support earlier versions -+ this._builder.get_object('shrink_dash_label1').label = -+ __('Show favorite applications'); -+ } -+ - this._monitorsConfig = new MonitorsConfig(); - this._bindSettings(); - } - -From a6447f246ad10075dd8f054b4625bff6009acd02 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= -Date: Thu, 28 Apr 2022 21:37:46 +0200 -Subject: [PATCH 51/51] docking: Use snake-case name for implicit defined - properties -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit - -Apparently gjs was wrong in generating the camelCase name for variables -in Turkish (at least) as there 'i'.toLocaleUpperCase() is `İ`. - -So let's workaround this, meanwhile gjs is properly fixed everywhere: - https://gitlab.gnome.org/GNOME/gjs/-/merge_requests/742 - -Fixes #1687 ---- - docking.js | 7 +++++++ - 1 file changed, 7 insertions(+) - -diff --git a/docking.js b/docking.js -index de8660a1c..ced22e3f2 100644 ---- a/docking.js -+++ b/docking.js -@@ -205,6 +205,13 @@ var DockedDash = GObject.registerClass({ - reactive: false, - style_class: positionStyleClass[this._position], - }); -+ -+ if (this.monitorIndex === undefined) { -+ // Hello turkish locale, gjs has instead defined this.monitorİndex -+ // See: https://gitlab.gnome.org/GNOME/gjs/-/merge_requests/742 -+ this.monitorIndex = this.monitor_index; -+ } -+ - this._rtl = (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL); - - // Load settings diff --git a/gnome-shell-extension-dash-to-dock.spec b/gnome-shell-extension-dash-to-dock.spec index 92aa1e1..9cf26b0 100644 --- a/gnome-shell-extension-dash-to-dock.spec +++ b/gnome-shell-extension-dash-to-dock.spec @@ -2,14 +2,14 @@ %global gschemadir %{_datadir}/glib-2.0/schemas %global giturl https://github.com/micheleg/dash-to-dock -%global commit 004f257c3dea5f1851dadd753be87afc555b3dfb -%global commit_short %%(c=%%{commit}; echo ${c:0:7}) -%global commit_date 20220428 +#%%global commit 004f257c3dea5f1851dadd753be87afc555b3dfb +#%%global commit_short %%(c=%%{commit}; echo ${c:0:7}) +#%%global commit_date 20220428 Name: gnome-shell-extension-dash-to-dock -Version: 71 -#Release: 4%%{?dist} -Release: 5.%{commit_date}git%{commit_short}%{?dist} +Version: 72 +Release: 1%{?dist} +#Release: 5.%%{commit_date}git%%{commit_short}%%{?dist} Summary: Dock for the Gnome Shell by micxgx@gmail.com License: GPLv2+ @@ -20,10 +20,6 @@ Source0: %{giturl}/archive/%{commit}.tar.gz Source0: %{giturl}/archive/extensions.gnome.org-v%{version}.tar.gz#/%{name}-%{version}.tar.gz %endif -# GNOME 42 Support -# https://github.com/micheleg/dash-to-dock/pull/1656 -Patch01: 1656.patch - BuildArch: noarch BuildRequires: gettext @@ -87,6 +83,9 @@ fi %changelog +* Wed May 18 2022 Frantisek Zatloukal - 72-1 +- dash-to-dock 72 + * Mon May 02 2022 Frantisek Zatloukal - 71-5.20220428git004f257 - Updated support for GNOME 42 - Updated git snapshot with various fixes diff --git a/sources b/sources index ccdcb41..e9a5f55 100644 --- a/sources +++ b/sources @@ -1 +1 @@ -SHA512 (004f257c3dea5f1851dadd753be87afc555b3dfb.tar.gz) = 698ae8a9c0ad03c3a432b9ebdfb29a8942d45647611d4ad9fdad74128e4849cb92464e7d1dacaeedaff5b70dc3a02a0a3d9ee2b6f823678dfc771c4a188ffe98 +SHA512 (gnome-shell-extension-dash-to-dock-72.tar.gz) = b9fc02af08dd1bf73837d013676a23d262b87ff875b4ec8aea1016f3445972f503a0ffb859fcda7aaa2f5f8e5a2388cf3eb0cd6c8e5b1aca458da22542aaa395