diff --git a/SOURCES/0001-classification-banner-Handle-fullscreen-monitors.patch b/SOURCES/0001-classification-banner-Handle-fullscreen-monitors.patch new file mode 100644 index 0000000..e6f8315 --- /dev/null +++ b/SOURCES/0001-classification-banner-Handle-fullscreen-monitors.patch @@ -0,0 +1,68 @@ +From 3d32ab1848011a3a7af97255307b3541a7553b09 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 14 Dec 2022 16:55:51 +0100 +Subject: [PATCH] classification-banner: Handle fullscreen monitors + +When a monitor is in fullscreen, we don't want its classification +banner to be offset by an imaginary panel, but at the top of the +screen. +--- + extensions/classification-banner/extension.js | 22 +++++++++++++++---- + 1 file changed, 18 insertions(+), 4 deletions(-) + +diff --git a/extensions/classification-banner/extension.js b/extensions/classification-banner/extension.js +index 6c2fe007..1cf03b3f 100644 +--- a/extensions/classification-banner/extension.js ++++ b/extensions/classification-banner/extension.js +@@ -27,16 +27,19 @@ const Main = imports.ui.main; + const ClassificationBanner = GObject.registerClass( + class ClassificationBanner extends Clutter.Actor { + _init(index) { ++ const constraint = new Layout.MonitorConstraint({index}); + super._init({ + layout_manager: new Clutter.BinLayout(), +- constraints: new Layout.MonitorConstraint({ +- work_area: true, +- index, +- }), ++ constraints: constraint, + }); ++ this._monitorConstraint = constraint; + + this._settings = ExtensionUtils.getSettings(); + this.connect('destroy', () => { ++ if (this._fullscreenChangedId) ++ global.display.disconnect(this._fullscreenChangedId); ++ delete this._fullscreenChangedId; ++ + if (this._settings) + this._settings.run_dispose(); + this._settings = null; +@@ -95,6 +98,11 @@ class ClassificationBanner extends Clutter.Actor { + userLabel, 'visible', + Gio.SettingsBindFlags.GET); + ++ this._fullscreenChangedId = ++ global.display.connect('in-fullscreen-changed', ++ () => this._updateMonitorConstraint()); ++ this._updateMonitorConstraint(); ++ + this._settings.connect('changed::color', + () => this._updateStyles()); + this._settings.connect('changed::background-color', +@@ -111,6 +119,12 @@ class ClassificationBanner extends Clutter.Actor { + return `${key}: rgba(${red},${green},${blue},${alpha / 255});`; + } + ++ _updateMonitorConstraint() { ++ const {index} = this._monitorConstraint; ++ this._monitorConstraint.work_area = ++ !global.display.get_monitor_in_fullscreen(index); ++ } ++ + _updateStyles() { + const bgStyle = this._getColorSetting('background-color'); + const fgStyle = this._getColorSetting('color'); +-- +2.38.1 + diff --git a/SOURCES/0001-desktop-icons-Don-t-use-blocking-IO.patch b/SOURCES/0001-desktop-icons-Don-t-use-blocking-IO.patch new file mode 100644 index 0000000..0224a30 --- /dev/null +++ b/SOURCES/0001-desktop-icons-Don-t-use-blocking-IO.patch @@ -0,0 +1,76 @@ +From 93e3e938b322433aff862bbc46f80c60ab7dc2ab Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Tue, 17 Jan 2023 20:31:21 +0100 +Subject: [PATCH] desktop-icons: Don't use blocking IO + +--- + extensions/desktop-icons/desktopManager.js | 35 +++++++++++++++++----- + 1 file changed, 28 insertions(+), 7 deletions(-) + +diff --git a/extensions/desktop-icons/desktopManager.js b/extensions/desktop-icons/desktopManager.js +index 399aee03..2ce6eefb 100644 +--- a/extensions/desktop-icons/desktopManager.js ++++ b/extensions/desktop-icons/desktopManager.js +@@ -53,6 +53,21 @@ function findMonitorIndexForPos(x, y) { + return getDpy().get_monitor_index_for_rect(new Meta.Rectangle({x, y})); + } + ++async function queryInfo(file, attributes = DesktopIconsUtil.DEFAULT_ATTRIBUTES, cancellable = null) { ++ const flags = Gio.FileQueryInfoFlags.NONE; ++ const priority = GLib.PRIORITY_DEFAULT; ++ return new Promise((resolve, reject) => { ++ file.query_info_async(attributes, flags, priority, cancellable, (o, res) => { ++ try { ++ const info = file.query_info_finish(res); ++ resolve(info); ++ } catch (e) { ++ reject(e); ++ } ++ }); ++ }); ++} ++ + + var DesktopManager = GObject.registerClass({ + Properties: { +@@ -221,9 +236,7 @@ var DesktopManager = GObject.registerClass({ + + if (!this._unixMode) { + let desktopDir = DesktopIconsUtil.getDesktopDir(); +- let fileInfo = desktopDir.query_info(Gio.FILE_ATTRIBUTE_UNIX_MODE, +- Gio.FileQueryInfoFlags.NONE, +- null); ++ let fileInfo = await queryInfo(desktopDir, Gio.FILE_ATTRIBUTE_UNIX_MODE); + this._unixMode = fileInfo.get_attribute_uint32(Gio.FILE_ATTRIBUTE_UNIX_MODE); + this._setWritableByOthers((this._unixMode & S_IWOTH) != 0); + } +@@ -268,14 +281,22 @@ var DesktopManager = GObject.registerClass({ + Gio.FileQueryInfoFlags.NONE, + GLib.PRIORITY_DEFAULT, + this._desktopEnumerateCancellable, +- (source, result) => { ++ async (source, result) => { + try { + let fileEnum = source.enumerate_children_finish(result); ++ let extraFolders = await Promise.all(DesktopIconsUtil.getExtraFolders() ++ .map(async ([folder, extras]) => { ++ const info = await queryInfo(folder, ++ DesktopIconsUtil.DEFAULT_ATTRIBUTES, ++ this._desktopEnumerateCancellable); ++ return [folder, info, extras]; ++ })); ++ + let resultGenerator = function *() { ++ for (let [newFolder, info, extras] of extraFolders) ++ yield [newFolder, info, extras]; ++ + let info; +- for (let [newFolder, extras] of DesktopIconsUtil.getExtraFolders()) { +- yield [newFolder, newFolder.query_info(DesktopIconsUtil.DEFAULT_ATTRIBUTES, Gio.FileQueryInfoFlags.NONE, this._desktopEnumerateCancellable), extras]; +- } + while ((info = fileEnum.next_file(null))) + yield [fileEnum.get_child(info), info, Prefs.FileType.NONE]; + }.bind(this); +-- +2.38.1 + diff --git a/SOURCES/0001-fileItem-Just-destroy-menus.patch b/SOURCES/0001-fileItem-Just-destroy-menus.patch new file mode 100644 index 0000000..012428b --- /dev/null +++ b/SOURCES/0001-fileItem-Just-destroy-menus.patch @@ -0,0 +1,31 @@ +From 506c6d69eaa5e056d9580a28e9c200586b0e1fb0 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Fri, 2 Dec 2022 15:20:40 +0100 +Subject: [PATCH] fileItem: Just destroy menus + +The menu manager is smart enough to remove the menu automatically, +and the actor will be destroyed alongside the menu. Not doing those +actions explicitly allows the automatic handling to proceed without +confusing the grab state. +--- + extensions/desktop-icons/fileItem.js | 4 ---- + 1 file changed, 4 deletions(-) + +diff --git a/extensions/desktop-icons/fileItem.js b/extensions/desktop-icons/fileItem.js +index 44a93352..f2f03440 100644 +--- a/extensions/desktop-icons/fileItem.js ++++ b/extensions/desktop-icons/fileItem.js +@@ -575,10 +575,6 @@ var FileItem = class { + + _removeMenu() { + if (this._menu != null) { +- if (this._menuManager != null) +- this._menuManager.removeMenu(this._menu); +- +- Main.layoutManager.uiGroup.remove_child(this._menu.actor); + this._menu.destroy(); + this._menu = null; + } +-- +2.38.1 + diff --git a/SOURCES/0001-fileItem-Support-.desktop-files-of-type-Link.patch b/SOURCES/0001-fileItem-Support-.desktop-files-of-type-Link.patch new file mode 100644 index 0000000..f4bf2b4 --- /dev/null +++ b/SOURCES/0001-fileItem-Support-.desktop-files-of-type-Link.patch @@ -0,0 +1,147 @@ +From be4ab59a3f2bb9829dde390db3dd8868a08840eb Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Fri, 2 Dec 2022 19:28:54 +0100 +Subject: [PATCH] fileItem: Support .desktop files of type Link + +Gio only has direct support for .desktop files of type Application. + +However in the context of desktop icons (and file managers), shortcuts +of URLs are useful as well, so add explicit support for .desktop files +of type Link. +--- + extensions/desktop-icons/fileItem.js | 71 +++++++++++++++++++++++----- + 1 file changed, 60 insertions(+), 11 deletions(-) + +diff --git a/extensions/desktop-icons/fileItem.js b/extensions/desktop-icons/fileItem.js +index f2f03440..1c9a1e55 100644 +--- a/extensions/desktop-icons/fileItem.js ++++ b/extensions/desktop-icons/fileItem.js +@@ -239,12 +239,32 @@ var FileItem = class { + log(`desktop-icons: File ${this._displayName} is writable by others - will not allow launching`); + + if (this._isDesktopFile) { +- this._desktopFile = Gio.DesktopAppInfo.new_from_filename(this._file.get_path()); +- if (!this._desktopFile) { +- log(`Couldn’t parse ${this._displayName} as a desktop file, will treat it as a regular file.`); ++ try { ++ const keyFile = new GLib.KeyFile(); ++ keyFile.load_from_file(this._file.get_path(), GLib.KeyFileFlags.NONE); ++ ++ const type = keyFile.get_string( ++ GLib.KEY_FILE_DESKTOP_GROUP, GLib.KEY_FILE_DESKTOP_KEY_TYPE); ++ switch (type) { ++ case GLib.KEY_FILE_DESKTOP_TYPE_APPLICATION: ++ this._desktopFile = Gio.DesktopAppInfo.new_from_keyfile(keyFile); ++ if (!this._desktopFile) { ++ log(`Couldn’t parse ${this._displayName} as a desktop file, will treat it as a regular file.`); ++ this._isValidDesktopFile = false; ++ } else { ++ this._isValidDesktopFile = true; ++ } ++ break; ++ case GLib.KEY_FILE_DESKTOP_TYPE_LINK: ++ const url = keyFile.get_string( ++ GLib.KEY_FILE_DESKTOP_GROUP, GLib.KEY_FILE_DESKTOP_KEY_URL); ++ if (url) ++ this._linkFile = keyFile; ++ default: // fall-through ++ this._isValidDesktopFile = false; ++ } ++ } catch (e) { + this._isValidDesktopFile = false; +- } else { +- this._isValidDesktopFile = true; + } + } else { + this._isValidDesktopFile = false; +@@ -356,8 +376,17 @@ var FileItem = class { + if (this._isBrokenSymlink) { + this._icon.child = this._createEmblemedStIcon(null, 'text-x-generic'); + } else { +- if (this.trustedDesktopFile && this._desktopFile.has_key('Icon')) +- this._icon.child = this._createEmblemedStIcon(null, this._desktopFile.get_string('Icon')); ++ let iconName = null; ++ ++ try { ++ if (this.trustedDesktopFile) ++ iconName = this._desktopFile.get_string('Icon'); ++ else if (this._linkFile) ++ iconName = this._linkFile.get_string(GLib.KEY_FILE_DESKTOP_GROUP, GLib.KEY_FILE_DESKTOP_KEY_ICON); ++ } catch (e) {} ++ ++ if (iconName) ++ this._icon.child = this._createEmblemedStIcon(null, iconName); + else + this._icon.child = this._createEmblemedStIcon(this._fileInfo.get_icon(), null); + } +@@ -411,7 +440,7 @@ var FileItem = class { + itemIcon.add_emblem(Gio.Emblem.new(Gio.ThemedIcon.new('emblem-unreadable'))); + else + itemIcon.add_emblem(Gio.Emblem.new(Gio.ThemedIcon.new('emblem-symbolic-link'))); +- } else if (this.trustedDesktopFile) { ++ } else if (this.trustedDesktopFile || this._linkFile) { + itemIcon.add_emblem(Gio.Emblem.new(Gio.ThemedIcon.new('emblem-symbolic-link'))); + } + +@@ -440,6 +469,12 @@ var FileItem = class { + return; + } + ++ if (this._linkFile) { ++ this._openUri(this._linkFile.get_string( ++ GLib.KEY_FILE_DESKTOP_GROUP, GLib.KEY_FILE_DESKTOP_KEY_URL)); ++ return; ++ } ++ + if (this._attributeCanExecute && + !this._isDirectory && + !this._isValidDesktopFile && +@@ -449,13 +484,17 @@ var FileItem = class { + return; + } + +- Gio.AppInfo.launch_default_for_uri_async(this.file.get_uri(), ++ this._openUri(this.file.get_uri()); ++ } ++ ++ _openUri(uri) { ++ Gio.AppInfo.launch_default_for_uri_async(uri, + null, null, + (source, result) => { + try { + Gio.AppInfo.launch_default_for_uri_finish(result); + } catch (e) { +- log('Error opening file ' + this.file.get_uri() + ': ' + e.message); ++ log('Error opening file ' + uri + ': ' + e.message); + } + } + ); +@@ -555,7 +594,9 @@ var FileItem = class { + } + + canRename() { +- return !this.trustedDesktopFile && this._fileExtra == Prefs.FileType.NONE; ++ return !this.trustedDesktopFile && ++ !this._linkFile && ++ this._fileExtra == Prefs.FileType.NONE; + } + + _doOpenWith() { +@@ -819,6 +860,14 @@ var FileItem = class { + if (this.trustedDesktopFile) + return this._desktopFile.get_name(); + ++ if (this._linkFile) { ++ try { ++ const name = this._linkFile.get_string( ++ GLib.KEY_FILE_DESKTOP_GROUP, GLib.KEY_FILE_DESKTOP_KEY_NAME); ++ return name; ++ } catch (e) {} ++ } ++ + return this._displayName || null; + } + +-- +2.38.1 + diff --git a/SOURCES/0001-gesture-inhibitor-Allow-inhibiting-workspace-switch-.patch b/SOURCES/0001-gesture-inhibitor-Allow-inhibiting-workspace-switch-.patch new file mode 100644 index 0000000..8100011 --- /dev/null +++ b/SOURCES/0001-gesture-inhibitor-Allow-inhibiting-workspace-switch-.patch @@ -0,0 +1,51 @@ +From ce75829479b1e7bf99e74bf835174e91c8da2276 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Fri, 9 Dec 2022 15:31:08 +0100 +Subject: [PATCH] gesture-inhibitor: Allow inhibiting workspace switch gesture + +--- + extensions/gesture-inhibitor/extension.js | 5 ++++- + .../org.gnome.shell.extensions.gesture-inhibitor.gschema.xml | 4 ++++ + 2 files changed, 8 insertions(+), 1 deletion(-) + +diff --git a/extensions/gesture-inhibitor/extension.js b/extensions/gesture-inhibitor/extension.js +index e74ede2f..bf02d075 100644 +--- a/extensions/gesture-inhibitor/extension.js ++++ b/extensions/gesture-inhibitor/extension.js +@@ -37,6 +37,8 @@ class Extension { + this._showOverview = a; + else if (a instanceof WindowManager.AppSwitchAction) + this._appSwitch = a; ++ else if (a instanceof WindowManager.WorkspaceSwitchAction) ++ this._workspaceSwitch = a; + else if (a instanceof EdgeDragAction.EdgeDragAction && + a._side == St.Side.BOTTOM) + this._showOsk = a; +@@ -52,7 +54,8 @@ class Extension { + { setting: 'app-switch', action: this._appSwitch }, + { setting: 'show-osk', action: this._showOsk }, + { setting: 'unfullscreen', action: this._unfullscreen }, +- { setting: 'show-app-grid', action: this._showAppGrid } ++ { setting: 'show-app-grid', action: this._showAppGrid }, ++ { setting: 'workspace-switch', action: this._workspaceSwitch }, + ]; + } + +diff --git a/extensions/gesture-inhibitor/org.gnome.shell.extensions.gesture-inhibitor.gschema.xml b/extensions/gesture-inhibitor/org.gnome.shell.extensions.gesture-inhibitor.gschema.xml +index 1d67dcc0..a5e97a3d 100644 +--- a/extensions/gesture-inhibitor/org.gnome.shell.extensions.gesture-inhibitor.gschema.xml ++++ b/extensions/gesture-inhibitor/org.gnome.shell.extensions.gesture-inhibitor.gschema.xml +@@ -16,6 +16,10 @@ + true + Application switch gesture + ++ ++ true ++ Workspace switch gesture ++ + + true + Unfullscreen gesture +-- +2.38.1 + diff --git a/SOURCES/add-extra-extensions.patch b/SOURCES/add-extra-extensions.patch index 5e5a7bf..ea1f191 100644 --- a/SOURCES/add-extra-extensions.patch +++ b/SOURCES/add-extra-extensions.patch @@ -1,7 +1,7 @@ From ed28c7abd7c324dc6071ff96309854b1f5d48761 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Wed, 20 May 2015 17:44:50 +0200 -Subject: [PATCH 01/10] Add top-icons extension +Subject: [PATCH 01/11] Add top-icons extension --- extensions/top-icons/extension.js | 96 +++++++++++++++++++++++++++ @@ -164,13 +164,13 @@ index b987f2d4..6050c32f 100644 ] -- -2.33.1 +2.38.1 From b99f1a2ead84c4fe494a387a032715f2973fbfa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Wed, 20 May 2015 18:05:41 +0200 -Subject: [PATCH 02/10] Add dash-to-dock extension +Subject: [PATCH 02/11] Add dash-to-dock extension --- extensions/dash-to-dock/Settings.ui | 3335 +++++++++++++++++ @@ -13413,13 +13413,13 @@ index 6050c32f..2909135a 100644 'top-icons', 'user-theme' -- -2.33.1 +2.38.1 From 9ffe67c4d25f34fa6c3af5ee4ddbd0be3018ef14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Wed, 20 May 2015 18:55:47 +0200 -Subject: [PATCH 03/10] Add panel-favorites extension +Subject: [PATCH 03/11] Add panel-favorites extension --- extensions/panel-favorites/extension.js | 267 ++++++++++++++++++++ @@ -13766,13 +13766,13 @@ index 2909135a..e8e00dce 100644 'user-theme' ] -- -2.33.1 +2.38.1 From 4bd1716e559af83795eec5b02025798b02c09fa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Fri, 4 Mar 2016 17:07:21 +0100 -Subject: [PATCH 04/10] Add updates-dialog extension +Subject: [PATCH 04/11] Add updates-dialog extension --- extensions/updates-dialog/extension.js | 503 ++++++++++++++++++ @@ -14396,13 +14396,13 @@ index 9c1438ac..55f0e9aa 100644 extensions/user-theme/org.gnome.shell.extensions.user-theme.gschema.xml extensions/window-list/extension.js -- -2.33.1 +2.38.1 From 0ba4b86fa5f73bccd3ab1984d9deef0d39f656c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Thu, 1 Jun 2017 23:57:14 +0200 -Subject: [PATCH 05/10] Add no-hot-corner extension +Subject: [PATCH 05/11] Add no-hot-corner extension --- extensions/no-hot-corner/extension.js | 31 +++++++++++++++++++++++ @@ -14499,13 +14499,13 @@ index d129e6cd..6f27f460 100644 'top-icons', 'updates-dialog', -- -2.33.1 +2.38.1 From f56b4374904cdfd8e1790dc3cf5080b60f30ebea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Tue, 26 Mar 2019 19:44:43 +0100 -Subject: [PATCH 06/10] Add window-grouper extension +Subject: [PATCH 06/11] Add window-grouper extension --- extensions/window-grouper/extension.js | 109 ++++++++++ @@ -14903,13 +14903,13 @@ index 6f27f460..4b9d138c 100644 enabled_extensions = get_option('enable_extensions') -- -2.33.1 +2.38.1 From 25c4999ff6adf19a32bab2a4d6cccae42520563b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Tue, 26 Mar 2019 21:32:09 +0100 -Subject: [PATCH 07/10] Add disable-screenshield extension +Subject: [PATCH 07/11] Add disable-screenshield extension --- extensions/disable-screenshield/extension.js | 27 +++++++++++++++++++ @@ -15002,13 +15002,13 @@ index 4b9d138c..cf855a01 100644 'no-hot-corner', 'panel-favorites', -- -2.33.1 +2.38.1 From 59604979f6ba48b7ff8d1616ab9df739dcf46a20 Mon Sep 17 00:00:00 2001 From: Carlos Soriano Date: Mon, 13 Aug 2018 17:28:41 +0200 -Subject: [PATCH 08/10] Add desktop icons extension +Subject: [PATCH 08/11] Add desktop icons extension --- .../desktop-icons/createFolderDialog.js | 164 ++++ @@ -26738,13 +26738,13 @@ index 74a95f8a..fa5ba9b8 100644 #~ msgstr "Dock 的位置" -- -2.33.1 +2.38.1 From dc47faaf827011e5dd7a53f9007ea618c6e88203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Fri, 8 Oct 2021 19:36:18 +0200 -Subject: [PATCH 09/10] Add dash-to-panel +Subject: [PATCH 09/11] Add dash-to-panel --- extensions/dash-to-panel/COPYING | 341 + @@ -70770,13 +70770,13 @@ index fa5ba9b8..015d85a4 100644 #~ msgstr "退出應用程式" -- -2.33.1 +2.38.1 From e8facafa1d16fc62a01c02bcee9e34a388f81572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Thu, 2 Dec 2021 19:39:50 +0100 -Subject: [PATCH 10/10] Add classification-banner +Subject: [PATCH 10/11] Add classification-banner --- extensions/classification-banner/extension.js | 170 ++++++++ @@ -71427,5 +71427,789 @@ index d4791263..75a2beaa 100644 'dash-to-panel', 'disable-screenshield', -- -2.33.1 +2.38.1 + + +From 214b5c2959ab3c0ab64d32b79fbff011302d1af8 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Thu, 12 Jan 2023 19:43:52 +0100 +Subject: [PATCH 11/11] Add custom-menu extension + +--- + extensions/custom-menu/config.js | 484 ++++++++++++++++++++++++ + extensions/custom-menu/extension.js | 217 +++++++++++ + extensions/custom-menu/meson.build | 7 + + extensions/custom-menu/metadata.json.in | 10 + + extensions/custom-menu/stylesheet.css | 1 + + meson.build | 1 + + 6 files changed, 720 insertions(+) + create mode 100644 extensions/custom-menu/config.js + create mode 100644 extensions/custom-menu/extension.js + create mode 100644 extensions/custom-menu/meson.build + create mode 100644 extensions/custom-menu/metadata.json.in + create mode 100644 extensions/custom-menu/stylesheet.css + +diff --git a/extensions/custom-menu/config.js b/extensions/custom-menu/config.js +new file mode 100644 +index 00000000..652c0223 +--- /dev/null ++++ b/extensions/custom-menu/config.js +@@ -0,0 +1,484 @@ ++const Gio = imports.gi.Gio; ++const GLib = imports.gi.GLib; ++const Json = imports.gi.Json; ++const Lang = imports.lang; ++const Main = imports.ui.main; ++const PopupMenu = imports.ui.popupMenu; ++const ByteArray = imports.byteArray; ++ ++const Me = imports.misc.extensionUtils.getCurrentExtension(); ++const getLogger = Me.imports.extension.getLogger; ++ ++const Entry = new Lang.Class({ ++ Name: 'Entry', ++ Abstract: true, ++ ++ _init: function(prop) { ++ this.type = prop.type; ++ this.title = prop.title || ""; ++ ++ this.__vars = prop.__vars || []; ++ this.updateEnv(prop); ++ }, ++ ++ setTitle: function(text) { ++ this.item.label.get_clutter_text().set_text(text); ++ }, ++ ++ updateEnv: function(prop) { ++ this.__env = {} ++ if(!this.__vars) return; ++ ++ for(let i in this.__vars) { ++ let v = this.__vars[i]; ++ this.__env[v] = prop[v] ? String(prop[v]) : ""; ++ } ++ }, ++ ++ // the pulse function should be read as "a pulse arrives" ++ pulse: function() { }, ++ ++ _try_destroy: function() { ++ try { ++ if(this.item && this.item.destroy) ++ this.item.destroy(); ++ } catch(e) { /* Ignore all errors during destory*/ } ++ }, ++}); ++ ++const DerivedEntry = new Lang.Class({ ++ Name: 'DerivedEntry', ++ ++ _init: function(prop) { ++ if(!prop.base) ++ throw new Error("Base entry not specified in type definition."); ++ ++ this.base = prop.base; ++ this.vars = prop.vars || []; ++ ++ delete prop.base; ++ delete prop.vars; ++ ++ this.prop = prop; ++ }, ++ ++ createInstance: function(addit_prop) { ++ let cls = type_map[this.base]; ++ if(!cls) throw new Error("Bad base class."); ++ if(cls.createInstance) throw new Error("Not allowed to derive from dervied types"); ++ ++ for(let rp in this.prop) ++ addit_prop[rp] = this.prop[rp]; ++ addit_prop.__vars = this.vars; ++ ++ let instance = new cls(addit_prop); ++ ++ return instance; ++ }, ++}); ++ ++/* ++ * callback: function (stdout, stderr, exit_status) { } ++ */ ++let __pipeOpenQueue = []; ++let __pipeExecTimer = null; ++ ++function pipeOpen(cmdline, env, callback) { ++ let param = [cmdline, env, callback] ++ __pipeOpenQueue.push(param); ++ if(__pipeExecTimer === null) { ++ __pipeExecTimer = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, ++ function() { ++ let param = __pipeOpenQueue.shift(); ++ if(param === undefined) { ++ __pipeExecTimer = null; ++ return false; ++ } ++ if(realPipeOpen) realPipeOpen(param[0], param[1], param[2]); ++ return true; ++ }); ++ ++ } ++} ++ ++function realPipeOpen(cmdline, env, callback) { ++ let user_cb = callback; ++ let proc; ++ ++ function wait_cb(_, _res) { ++ let stdout_pipe = proc.get_stdout_pipe(); ++ let stderr_pipe = proc.get_stderr_pipe(); ++ ++ let stdout_content; ++ let stderr_content; ++ ++ // Only the first GLib.MAXINT16 characters are fetched for optimization. ++ stdout_pipe.read_bytes_async(GLib.MAXINT16, 0, null, function(osrc, ores) { ++ stdout_content = ByteArray.toString(stdout_pipe.read_bytes_finish(ores).get_data()); ++ stdout_pipe.close(null); ++ ++ stderr_pipe.read_bytes_async(GLib.MAXINT16, 0, null, function(esrc, eres) { ++ stderr_content = ByteArray.toString(stderr_pipe.read_bytes_finish(eres).get_data()); ++ stderr_pipe.close(null); ++ ++ user_cb(stdout_content, stderr_content, proc.get_exit_status()); ++ }); ++ }); ++ } ++ ++ if(user_cb) { ++ let _pipedLauncher = new Gio.SubprocessLauncher({ ++ flags: ++ Gio.SubprocessFlags.STDERR_PIPE | ++ Gio.SubprocessFlags.STDOUT_PIPE ++ }); ++ for(let key in env) { ++ _pipedLauncher.setenv(key, env[key], true); ++ } ++ proc = _pipedLauncher.spawnv(['bash', '-c', cmdline]); ++ proc.wait_async(null, wait_cb); ++ } else { ++ // Detached launcher is used to spawn commands that we are not concerned ++ // about its result. ++ let _detacLauncher = new Gio.SubprocessLauncher(); ++ for(let key in env) { ++ _detacLauncher.setenv(key, env[key], true); ++ } ++ proc = _detacLauncher.spawnv(['bash', '-c', cmdline]); ++ } ++ ++ getLogger().info("Spawned " + cmdline); ++ ++ // log(`Spawning ${cmdline}`); TODO: BEN ++ return proc.get_identifier(); ++} ++ ++function _generalSpawn(command, env, title) { ++ title = title || "Process"; ++ pipeOpen(command, env, function(stdout, stderr, exit_status) { ++ if(exit_status != 0) { ++ log ++ getLogger().warning(stderr); ++ getLogger().notify("proc", title + ++ " exited with status " + exit_status, stderr); ++ } ++ }); ++} ++ ++function quoteShellArg(arg) { ++ arg = arg.replace(/'/g, "'\"'\"'"); ++ return "'" + arg + "'"; ++} ++ ++// This cache is used to reduce detector cost. Each time creating an item, it ++// check if the result of this detector is cached, which prevent the togglers ++// from running detector on each creation. This is useful especially in search ++// mode. ++let _toggler_state_cache = { }; ++ ++const TogglerEntry = new Lang.Class({ ++ Name: 'TogglerEntry', ++ Extends: Entry, ++ ++ _init: function(prop) { ++ this.parent(prop); ++ ++ this.command_on = prop.command_on || ""; ++ this.command_off = prop.command_off || ""; ++ this.detector = prop.detector || ""; ++ this.auto_on = prop.auto_on || false; ++ this.notify_when = prop.notify_when || []; ++ // if the switch is manually turned off, auto_on is disabled. ++ this._manually_switched_off = false; ++ this.pulse(); // load initial state ++ }, ++ ++ createItem: function() { ++ this._try_destroy(); ++ this.item = new PopupMenu.PopupSwitchMenuItem(this.title, false); ++ this.item.label.get_clutter_text().set_use_markup(true); ++ this.item.connect('toggled', Lang.bind(this, this._onManuallyToggled)); ++ this._loadState(); ++ return this.item; ++ }, ++ ++ _onManuallyToggled: function(_, state) { ++ // when switched on again, this flag will get cleared. ++ this._manually_switched_off = !state; ++ this._storeState(state); ++ this._onToggled(state); ++ }, ++ ++ _onToggled: function(state) { ++ if(state) ++ _generalSpawn(this.command_on, this.__env, this.title); ++ else ++ _generalSpawn(this.command_off, this.__env, this.title); ++ }, ++ ++ _detect: function(callback) { ++ // abort detecting if detector is an empty string ++ if(!this.detector) ++ return; ++ ++ pipeOpen(this.detector, this.__env, function(out) { ++ out = String(out); ++ callback(!Boolean(out.match(/^\s*$/))); ++ }); ++ }, ++ ++ compareState: function(new_state) { ++ // compare the new state with cached state ++ // notify when state is different ++ let old_state = _toggler_state_cache[this.detector]; ++ if(old_state === undefined) return; ++ if(old_state == new_state) return; ++ ++ if(this.notify_when.indexOf(new_state ? "on" : "off") >= 0) { ++ let not_str = this.title + (new_state ? " started." : " stopped."); ++ if(!new_state && this.auto_on) ++ not_str += " Attempt to restart it now."; ++ getLogger().notify("state", not_str); ++ } ++ }, ++ ++ _storeState: function(state) { ++ let hash = JSON.stringify({ env: this.__env, detector: this.detector }); ++ _toggler_state_cache[hash] = state; ++ }, ++ ++ _loadState: function() { ++ let hash = JSON.stringify({ env: this.__env, detector: this.detector }); ++ let state = _toggler_state_cache[hash]; ++ if(state !== undefined) ++ this.item.setToggleState(state); // doesn't emit 'toggled' ++ }, ++ ++ pulse: function() { ++ this._detect(Lang.bind(this, function(state) { ++ this.compareState(state); ++ ++ this._storeState(state); ++ this._loadState(); ++ //global.log(this.title + ': ' + this._manually_switched_off); ++ ++ if(!state && !this._manually_switched_off && this.auto_on) ++ // do not call setToggleState here, because command_on may fail ++ this._onToggled(this.item, true); ++ })); ++ }, ++ ++ perform: function() { ++ this.item.toggle(); ++ }, ++}); ++ ++const LauncherEntry = new Lang.Class({ ++ Name: 'LauncherEntry', ++ Extends: Entry, ++ ++ _init: function(prop) { ++ this.parent(prop); ++ ++ this.command = prop.command || ""; ++ }, ++ ++ createItem: function() { ++ this._try_destroy(); ++ ++ this.item = new PopupMenu.PopupMenuItem(this.title); ++ this.item.label.get_clutter_text().set_use_markup(true); ++ this.item.connect('activate', Lang.bind(this, this._onClicked)); ++ ++ return this.item; ++ }, ++ ++ _onClicked: function(_) { ++ _generalSpawn(this.command, this.__env, this.title); ++ }, ++ ++ perform: function() { ++ this.item.emit('activate'); ++ }, ++}); ++ ++const SubMenuEntry = new Lang.Class({ ++ Name: 'SubMenuEntry', ++ Extends: Entry, ++ ++ _init: function(prop) { ++ this.parent(prop) ++ ++ if(prop.entries == undefined) ++ throw new Error("Expected entries provided in submenu entry."); ++ ++ this.entries = []; ++ ++ for(let i in prop.entries) { ++ let entry_prop = prop.entries[i]; ++ let entry = createEntry(entry_prop); ++ this.entries.push(entry); ++ } ++ }, ++ ++ createItem: function() { ++ this._try_destroy(); ++ ++ this.item = new PopupMenu.PopupSubMenuMenuItem(this.title); ++ this.item.label.get_clutter_text().set_use_markup(true); ++ for(let i in this.entries) { ++ let entry = this.entries[i]; ++ this.item.menu.addMenuItem(entry.createItem()); ++ } ++ ++ return this.item; ++ }, ++ ++ pulse: function() { ++ for(let i in this.entries) { ++ let entry = this.entries[i]; ++ entry.pulse(); ++ } ++ } ++}); ++ ++const SeparatorEntry = new Lang.Class({ ++ Name: 'SeparatorEntry', ++ Extends: Entry, ++ ++ _init: function(prop) { }, ++ ++ createItem: function() { ++ this._try_destroy(); ++ ++ this.item = new PopupMenu.PopupSeparatorMenuItem(this.title); ++ this.item.label.get_clutter_text().set_use_markup(true); ++ ++ return this.item; ++ }, ++}); ++ ++let type_map = {}; ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Config Loader loads config from JSON file. ++ ++// convert Json Nodes (GLib based) to native javascript value. ++function convertJson(node) { ++ if(node.get_node_type() == Json.NodeType.VALUE) ++ return node.get_value(); ++ if(node.get_node_type() == Json.NodeType.OBJECT) { ++ let obj = {} ++ node.get_object().foreach_member(function(_, k, v_n) { ++ obj[k] = convertJson(v_n); ++ }); ++ return obj; ++ } ++ if(node.get_node_type() == Json.NodeType.ARRAY) { ++ let arr = [] ++ node.get_array().foreach_element(function(_, i, elem) { ++ arr.push(convertJson(elem)); ++ }); ++ return arr; ++ } ++ return null; ++} ++ ++// ++function createEntry(entry_prop) { ++ if(!entry_prop.type) ++ throw new Error("No type specified in entry."); ++ ++ let cls = type_map[entry_prop.type]; ++ if(!cls) ++ throw new Error("Incorrect type '" + entry_prop.type + "'"); ++ else if(cls.createInstance) ++ return cls.createInstance(entry_prop); ++ ++ return new cls(entry_prop); ++} ++ ++var Loader = new Lang.Class({ ++ Name: 'ConfigLoader', ++ ++ _init: function(filename) { ++ if(filename) ++ this.loadConfig(filename); ++ }, ++ ++ loadConfig: function(filename) { ++ // reset type_map everytime load the config ++ type_map = { ++ launcher: LauncherEntry, ++ toggler: TogglerEntry, ++ submenu: SubMenuEntry, ++ separator: SeparatorEntry ++ }; ++ ++ type_map.systemd = new DerivedEntry({ ++ base: 'toggler', ++ vars: ['unit'], ++ command_on: "pkexec systemctl start ${unit}", ++ command_off: "pkexec systemctl stop ${unit}", ++ detector: "systemctl status ${unit} | grep Active:\\\\s\\*activ[ei]", ++ }); ++ ++ type_map.tmux = new DerivedEntry({ ++ base: 'toggler', ++ vars: ['command', 'session'], ++ command_on: 'tmux new -d -s ${session} bash -c "${command}"', ++ command_off: 'tmux kill-session -t ${session}', ++ detector: 'tmux has -t "${session}" 2>/dev/null && echo yes', ++ }); ++ ++ /* ++ * Refer to README file for detailed config file format. ++ */ ++ this.entries = []; // CAUTION: remove all entries. ++ ++ let config_parser = new Json.Parser(); ++ config_parser.load_from_file(filename); ++ ++ let conf = convertJson(config_parser.get_root()); ++ if (conf.entries == undefined) ++ throw new Error("Key 'entries' not found."); ++ if (conf.deftype) { ++ for (let tname in conf.deftype) { ++ if (type_map[tname]) ++ throw new Error("Type \""+tname+"\" duplicated."); ++ type_map[tname] = new DerivedEntry(conf.deftype[tname]); ++ } ++ } ++ ++ for (let conf_i in conf.entries) { ++ let entry_prop = conf.entries[conf_i]; ++ this.entries.push(createEntry(entry_prop)); ++ } ++ }, ++ ++ ++ saveDefaultConfig: function(filename) { ++ // Write default config ++ const PERMISSIONS_MODE = 0o640; ++ const jsonString = JSON.stringify({ ++ "_homepage_": "https://github.com/andreabenini/gnome-plugin.custom-menu-panel", ++ "_examples_": "https://github.com/andreabenini/gnome-plugin.custom-menu-panel/tree/main/examples", ++ "entries": [ { ++ "type": "launcher", ++ "title": "Edit menu", ++ "command": "gedit $HOME/.entries.json" ++ } ] ++ }, null, 4); ++ let fileConfig = Gio.File.new_for_path(filename); ++ if (GLib.mkdir_with_parents(fileConfig.get_parent().get_path(), PERMISSIONS_MODE) === 0) { ++ fileConfig.replace_contents(jsonString, null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, null); ++ } ++ // Try to load newly saved file ++ try { ++ this.loadConfig(filename); ++ } catch(e) { ++ Main.notify(_('Cannot create and load file: '+filename)); ++ } ++ }, /**/ ++ ++}); +diff --git a/extensions/custom-menu/extension.js b/extensions/custom-menu/extension.js +new file mode 100644 +index 00000000..9f3e3ce8 +--- /dev/null ++++ b/extensions/custom-menu/extension.js +@@ -0,0 +1,217 @@ ++/* extension.js ++ * ++ * This program is free software: you can redistribute it and/or modify ++ * it under the terms of the GNU General Public License as published by ++ * the Free Software Foundation, either version 3 of the License, or ++ * (at your option) any later version. ++ * ++ * This program is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with this program. If not, see . ++ * ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++ ++/** ++ * @author Ben ++ * @see https://github.com/andreabenini/gnome-plugin.custom-menu-panel ++ */ ++ ++/* exported init */ ++ ++const GETTEXT_DOMAIN = 'custom-menu-panel'; ++const CONFIGURATION_FILE = '/.entries.json'; ++ ++const { GObject, St } = imports.gi; ++ ++const Gettext = imports.gettext.domain(GETTEXT_DOMAIN); ++const _ = Gettext.gettext; ++ ++const ExtensionUtils = imports.misc.extensionUtils; ++const Lang = imports.lang; ++const GLib = imports.gi.GLib; ++const Gio = imports.gi.Gio; ++const BackgroundMenu = imports.ui.backgroundMenu; ++const Main = imports.ui.main; ++const PanelMenu = imports.ui.panelMenu; ++const PopupMenu = imports.ui.popupMenu; ++ ++const Me = imports.misc.extensionUtils.getCurrentExtension(); ++const Config = Me.imports.config ++ ++const LOGGER_INFO = 0; ++const LOGGER_WARNING = 1; ++const LOGGER_ERROR = 2; ++ ++const { BackgroundMenu: OriginalBackgroundMenu } = BackgroundMenu; ++ ++ ++class CustomMenu extends PopupMenu.PopupMenu { ++ constructor(sourceActor) { ++ super(sourceActor, 0.0, St.Side.TOP, 0); ++ ++ this._loadSetup(); ++ } ++ ++ /** ++ * LOAD Program settings from .entries.json file ++ */ ++ _loadSetup() { ++ this.removeAll(); ++ // Loading configuration from file ++ this.configLoader = new Config.Loader(); ++ try { ++ this.configLoader.loadConfig(GLib.get_home_dir() + CONFIGURATION_FILE); // $HOME/.entries.json ++ } catch(e) { ++ this.configLoader.saveDefaultConfig(GLib.get_home_dir() + CONFIGURATION_FILE); // create default entries ++ } ++ // Build the menu ++ let i = 0; ++ for (let i in this.configLoader.entries) { ++ let item = this.configLoader.entries[i].createItem(); ++ this.addMenuItem(item); ++ } ++ } /**/ ++} ++ ++class CustomBackgroundMenu extends CustomMenu { ++ constructor(layoutManager) { ++ super(layoutManager.dummyCursor); ++ ++ this.actor.add_style_class_name('background-menu'); ++ ++ layoutManager.uiGroup.add_actor(this.actor); ++ this.actor.hide(); ++ ++ this.connect('open-state-changed', (menu, open) => { ++ if (open) ++ this._updateMaxHeight(); ++ }); ++ } ++ ++ _updateMaxHeight() { ++ const monitor = Main.layoutManager.findMonitorForActor(this.actor); ++ const workArea = Main.layoutManager.getWorkAreaForMonitor(monitor.index); ++ const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage); ++ const vMargins = this.actor.margin_top + this.actor.margin_bottom; ++ const {y: offsetY} = this.sourceActor; ++ ++ const maxHeight = Math.round((monitor.height - offsetY - vMargins) / scaleFactor); ++ this.actor.style = `max-height: ${maxHeight}px;`; ++ } ++} ++ ++const Indicator = GObject.registerClass( ++ class Indicator extends PanelMenu.Button { ++ _init() { ++ super._init(0.0, _('Custom Menu Panel Indicator'), true); ++ this.add_child(new St.Icon({ ++ icon_name: 'view-list-bullet-symbolic', ++ style_class: 'system-status-icon', ++ })); ++ ++ this.setMenu(new CustomMenu(this)); ++ } ++ } ++); ++ ++ ++const Logger = new Lang.Class({ ++ Name: 'Logger', ++ ++ _init: function(log_file) { ++ this._log_file = log_file; ++ // initailize log_backend ++ if(!log_file) ++ this._initEmptyLog(); ++ else if(log_file == "gnome-shell") ++ this._initGnomeLog(); ++ else ++ this._initFileLog(); ++ ++ this.level = LOGGER_WARNING; ++ ++ this.info = function(t) { ++ if(this.level <= LOGGER_INFO) this.log(t) ++ }; ++ this.warning = function(t) { ++ if(this.level <= LOGGER_WARNING) this.log(t) ++ }; ++ this.error = function(t) { ++ if(this.level <= LOGGER_ERROR) this.log(t); ++ }; ++ }, ++ ++ _initEmptyLog: function() { ++ this.log = function(_) { }; ++ }, ++ ++ _initGnomeLog: function() { ++ this.log = function(s) { ++ global.log("custom-menu-panel> " + s); ++ }; ++ }, ++ ++ _initFileLog: function() { ++ this.log = function(s) { ++ // all operations are synchronous: any needs to optimize? ++ if(!this._output_file || !this._output_file.query_exists(null) || ++ !this._fstream || this._fstream.is_closed()) { ++ ++ this._output_file = Gio.File.new_for_path(this._log_file); ++ this._fstream = this._output_file.append_to( ++ Gio.FileCreateFlags.NONE, null); ++ ++ if(!this._fstream instanceof Gio.FileIOStream) { ++ this._initGnomeLog(); ++ this.log("IOError: Failed to append to " + this._log_file + ++ " [Gio.IOErrorEnum:" + this._fstream + "]"); ++ return; ++ } ++ } ++ ++ this._fstream.write(String(new Date())+" "+s+"\n", null); ++ this._fstream.flush(null); ++ } ++ }, ++ ++ notify: function(t, str, details) { ++ this.ncond = this.ncond || ['proc', 'ext', 'state']; ++ if(this.ncond.indexOf(t) < 0) return; ++ Main.notify(str, details || ""); ++ }, ++}); ++ ++// lazy-evaluation ++let logger = null; ++function getLogger() { ++ if(logger === null) ++ logger = new Logger("gnome-shell"); ++ return logger; ++} ++ ++class Extension { ++ constructor(uuid) { ++ this._uuid = uuid; ++ ++ ExtensionUtils.initTranslations(GETTEXT_DOMAIN); ++ } ++ ++ enable() { ++ BackgroundMenu.BackgroundMenu = CustomBackgroundMenu; ++ Main.layoutManager._updateBackgrounds(); ++ } ++ ++ disable() { ++ BackgroundMenu.BackgroundMenu = OriginalBackgroundMenu; ++ Main.layoutManager._updateBackgrounds(); ++ } ++} ++ ++function init(meta) { ++ return new Extension(meta.uuid); ++} +diff --git a/extensions/custom-menu/meson.build b/extensions/custom-menu/meson.build +new file mode 100644 +index 00000000..92450963 +--- /dev/null ++++ b/extensions/custom-menu/meson.build +@@ -0,0 +1,7 @@ ++extension_data += configure_file( ++ input: metadata_name + '.in', ++ output: metadata_name, ++ configuration: metadata_conf ++) ++ ++extension_sources += files('config.js') +diff --git a/extensions/custom-menu/metadata.json.in b/extensions/custom-menu/metadata.json.in +new file mode 100644 +index 00000000..054f639b +--- /dev/null ++++ b/extensions/custom-menu/metadata.json.in +@@ -0,0 +1,10 @@ ++{ ++"extension-id": "@extension_id@", ++"uuid": "@uuid@", ++"settings-schema": "@gschemaname@", ++"gettext-domain": "@gettext_domain@", ++"name": "Custom menu", ++"description": "Quick custom menu for launching your favorite applications", ++"shell-version": [ "@shell_current@" ], ++"url": "@url@" ++} +diff --git a/extensions/custom-menu/stylesheet.css b/extensions/custom-menu/stylesheet.css +new file mode 100644 +index 00000000..25134b65 +--- /dev/null ++++ b/extensions/custom-menu/stylesheet.css +@@ -0,0 +1 @@ ++/* This extensions requires no special styling */ +diff --git a/meson.build b/meson.build +index 75a2beaa..b0c94fe4 100644 +--- a/meson.build ++++ b/meson.build +@@ -51,6 +51,7 @@ all_extensions = default_extensions + all_extensions += [ + 'auto-move-windows', + 'classification-banner', ++ 'custom-menu', + 'dash-to-dock', + 'dash-to-panel', + 'disable-screenshield', +-- +2.38.1 diff --git a/SPECS/gnome-shell-extensions.spec b/SPECS/gnome-shell-extensions.spec index 7e70373..3768c78 100644 --- a/SPECS/gnome-shell-extensions.spec +++ b/SPECS/gnome-shell-extensions.spec @@ -6,7 +6,7 @@ Name: gnome-shell-extensions Version: 3.32.1 -Release: 29%{?dist} +Release: 33%{?dist} Summary: Modify and extend GNOME Shell functionality and behavior Group: User Interface/Desktops @@ -51,6 +51,11 @@ Patch0022: 0001-gesture-inhibitor-Put-a-foot-down-with-self-enabling.pat Patch0023: 0001-desktop-icons-Use-a-single-unique-name-to-access-nau.patch Patch0024: window-list-touch.patch Patch0025: 0001-auto-move-windows-Don-t-move-windows-already-on-all-.patch +Patch0026: 0001-fileItem-Just-destroy-menus.patch +Patch0027: 0001-fileItem-Support-.desktop-files-of-type-Link.patch +Patch0028: 0001-classification-banner-Handle-fullscreen-monitors.patch +Patch0029: 0001-gesture-inhibitor-Allow-inhibiting-workspace-switch-.patch +Patch0030: 0001-desktop-icons-Don-t-use-blocking-IO.patch %description GNOME Shell Extensions is a collection of extensions providing additional and @@ -60,6 +65,7 @@ Enabled extensions: * apps-menu * auto-move-windows * classification-banner + * custom-menu * dash-to-dock * dash-to-panel * disable-screenshield @@ -165,6 +171,16 @@ Requires: %{pkg_prefix}-common = %{version}-%{release} This GNOME Shell extension adds a banner that displays the classification level. +%package -n %{pkg_prefix}-custom-menu +Summary: Add a custom menu to the desktop +Group: User Interface/Desktops +License: GPLv2+ +Requires: %{pkg_prefix}-common = %{version}-%{release} + +%description -n %{pkg_prefix}-custom-menu +This GNOME Shell extension adds a custom menu to the desktop background. + + %package -n %{pkg_prefix}-dash-to-dock Summary: Show the dash outside the activities overview Group: User Interface/Desktops @@ -449,6 +465,10 @@ cp $RPM_SOURCE_DIR/gnome-classic.desktop $RPM_BUILD_ROOT%{_datadir}/xsessions %{_datadir}/gnome-shell/extensions/classification-banner*/ +%files -n %{pkg_prefix}-custom-menu +%{_datadir}/gnome-shell/extensions/custom-menu*/ + + %files -n %{pkg_prefix}-dash-to-dock %{_datadir}/glib-2.0/schemas/org.gnome.shell.extensions.dash-to-dock.gschema.xml %{_datadir}/gnome-shell/extensions/dash-to-dock*/ @@ -549,6 +569,26 @@ cp $RPM_SOURCE_DIR/gnome-classic.desktop $RPM_BUILD_ROOT%{_datadir}/xsessions %changelog +* Tue Jan 17 2023 Florian Müllner - 3.32.1-33 +- Avoid blocking IO in desktop-icons + Resolves: #2162017 + +* Thu Jan 12 2023 Florian Müllner - 3.32.1-32 +- Add custom-menu extension + Resolves: #2033572 + +* Wed Dec 14 2022 Florian Müllner - 3.32.1-31 +- Adjust classification banner position in fullscreen + Resolves: #2150107 +- Allow inhibiting workspace switch gesture + Resolves: #2138109 + +* Fri Dec 09 2022 Florian Müllner - 3.32.1-30 +- Fix stuck grab if disabled with open context menu + Resolves: #2149670 +- Support .desktop files of type Link + Resolves: #2143825 + * Mon Aug 29 2022 Jonas Ådahl - 3.32.1-29 - Avoid invalid window management in auto-move-windows Resolves: #2089311