i10c-beta
changed/i10c-beta/gnome-shell-extensions-47%7ealpha-3.el10
commit
4045acccc4
@ -0,0 +1 @@
|
|||||||
|
SOURCES/gnome-shell-extensions-47.alpha.tar.xz
|
@ -0,0 +1 @@
|
|||||||
|
c3c6c96b0db36cada61b74e35f86e9eede07a647 SOURCES/gnome-shell-extensions-47.alpha.tar.xz
|
@ -0,0 +1,32 @@
|
|||||||
|
From 8a2191519e2431a946aa1be36474bfe323a454a8 Mon Sep 17 00:00:00 2001
|
||||||
|
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
|
||||||
|
Date: Fri, 23 Feb 2018 16:56:46 +0100
|
||||||
|
Subject: [PATCH] Include top-icons in classic session
|
||||||
|
|
||||||
|
---
|
||||||
|
meson.build | 2 +-
|
||||||
|
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||||
|
|
||||||
|
diff --git a/meson.build b/meson.build
|
||||||
|
index dce1731c..b915b68c 100644
|
||||||
|
--- a/meson.build
|
||||||
|
+++ b/meson.build
|
||||||
|
@@ -34,6 +34,7 @@ classic_extensions = [
|
||||||
|
'apps-menu',
|
||||||
|
'places-menu',
|
||||||
|
'launch-new-instance',
|
||||||
|
+ 'top-icons',
|
||||||
|
'window-list'
|
||||||
|
]
|
||||||
|
|
||||||
|
@@ -44,7 +45,6 @@ default_extensions += [
|
||||||
|
'light-style',
|
||||||
|
'screenshot-window-sizer',
|
||||||
|
'system-monitor',
|
||||||
|
- 'top-icons',
|
||||||
|
'windowsNavigator',
|
||||||
|
'workspace-indicator'
|
||||||
|
]
|
||||||
|
--
|
||||||
|
2.45.2
|
||||||
|
|
@ -0,0 +1,51 @@
|
|||||||
|
From 97d71d4a7ef4b1d4c9c2eab55db62173311f5366 Mon Sep 17 00:00:00 2001
|
||||||
|
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
|
||||||
|
Date: Tue, 2 Jul 2024 19:04:10 +0200
|
||||||
|
Subject: [PATCH] workspace-indicator: Re-fittsify workspace previews
|
||||||
|
|
||||||
|
For the window-list extension, it is important that the workspace
|
||||||
|
previews extend to the bottom edge for easier click targets.
|
||||||
|
|
||||||
|
That broke while merging the code with the workspace-indicator,
|
||||||
|
fix it again by moving the padding from the parent box into the
|
||||||
|
thumbnail children.
|
||||||
|
---
|
||||||
|
.../workspace-indicator/stylesheet-dark.css | 15 ++++++++++++++-
|
||||||
|
1 file changed, 14 insertions(+), 1 deletion(-)
|
||||||
|
|
||||||
|
diff --git a/extensions/workspace-indicator/stylesheet-dark.css b/extensions/workspace-indicator/stylesheet-dark.css
|
||||||
|
index b4a716b8..3c57c3e6 100644
|
||||||
|
--- a/extensions/workspace-indicator/stylesheet-dark.css
|
||||||
|
+++ b/extensions/workspace-indicator/stylesheet-dark.css
|
||||||
|
@@ -18,7 +18,6 @@
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-indicator .workspaces-box {
|
||||||
|
- padding: 5px;
|
||||||
|
spacing: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -27,6 +26,20 @@
|
||||||
|
spacing: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
+.workspace-indicator .workspace-box {
|
||||||
|
+ padding-top: 5px;
|
||||||
|
+ padding-bottom: 5px;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+.workspace-indicator StButton:first-child:ltr > .workspace-box,
|
||||||
|
+.workspace-indicator StButton:last-child:rtl > .workspace-box {
|
||||||
|
+ padding-left: 5px;
|
||||||
|
+}
|
||||||
|
+.workspace-indicator StButton:last-child:ltr > .workspace-box,
|
||||||
|
+.workspace-indicator StButton:first-child:rtl > .workspace-box {
|
||||||
|
+ padding-right: 5px;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
.workspace-indicator-menu .workspace-box {
|
||||||
|
spacing: 6px;
|
||||||
|
}
|
||||||
|
--
|
||||||
|
2.45.2
|
||||||
|
|
@ -0,0 +1,158 @@
|
|||||||
|
From 778e3f5ec9b8897af89af1919381a14e2e3494f6 Mon Sep 17 00:00:00 2001
|
||||||
|
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
|
||||||
|
Date: Wed, 20 May 2015 17:44:50 +0200
|
||||||
|
Subject: [PATCH 1/5] Add top-icons extension
|
||||||
|
|
||||||
|
---
|
||||||
|
extensions/top-icons/extension.js | 91 +++++++++++++++++++++++++++
|
||||||
|
extensions/top-icons/meson.build | 9 +++
|
||||||
|
extensions/top-icons/metadata.json.in | 10 +++
|
||||||
|
meson.build | 1 +
|
||||||
|
4 files changed, 111 insertions(+)
|
||||||
|
create mode 100644 extensions/top-icons/extension.js
|
||||||
|
create mode 100644 extensions/top-icons/meson.build
|
||||||
|
create mode 100644 extensions/top-icons/metadata.json.in
|
||||||
|
|
||||||
|
diff --git a/extensions/top-icons/extension.js b/extensions/top-icons/extension.js
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..c28f1386
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/extensions/top-icons/extension.js
|
||||||
|
@@ -0,0 +1,91 @@
|
||||||
|
+// SPDX-FileCopyrightText: 2018 Adel Gadllah <adel.gadllah@gmail.com>
|
||||||
|
+// SPDX-FileCopyrightText: 2018 Florian Müllner <fmuellner@gnome.org>
|
||||||
|
+//
|
||||||
|
+// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
+
|
||||||
|
+import Clutter from 'gi://Clutter';
|
||||||
|
+import Shell from 'gi://Shell';
|
||||||
|
+import St from 'gi://St';
|
||||||
|
+
|
||||||
|
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||||
|
+import {Button as PanelButton} from 'resource:///org/gnome/shell/ui/panelMenu.js';
|
||||||
|
+
|
||||||
|
+const PANEL_ICON_SIZE = 16;
|
||||||
|
+
|
||||||
|
+const STANDARD_TRAY_ICON_IMPLEMENTATIONS = [
|
||||||
|
+ 'bluetooth-applet',
|
||||||
|
+ 'gnome-sound-applet',
|
||||||
|
+ 'nm-applet',
|
||||||
|
+ 'gnome-power-manager',
|
||||||
|
+ 'keyboard',
|
||||||
|
+ 'a11y-keyboard',
|
||||||
|
+ 'kbd-scrolllock',
|
||||||
|
+ 'kbd-numlock',
|
||||||
|
+ 'kbd-capslock',
|
||||||
|
+ 'ibus-ui-gtk',
|
||||||
|
+];
|
||||||
|
+
|
||||||
|
+export default class SysTray {
|
||||||
|
+ constructor() {
|
||||||
|
+ this._icons = new Map();
|
||||||
|
+ this._tray = null;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _onTrayIconAdded(o, icon) {
|
||||||
|
+ let wmClass = icon.wm_class ? icon.wm_class.toLowerCase() : '';
|
||||||
|
+ if (STANDARD_TRAY_ICON_IMPLEMENTATIONS.includes(wmClass))
|
||||||
|
+ return;
|
||||||
|
+
|
||||||
|
+ let button = new PanelButton(0.5, null, true);
|
||||||
|
+
|
||||||
|
+ let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
|
||||||
|
+ let iconSize = PANEL_ICON_SIZE * scaleFactor;
|
||||||
|
+
|
||||||
|
+ icon.set({
|
||||||
|
+ width: iconSize,
|
||||||
|
+ height: iconSize,
|
||||||
|
+ x_align: Clutter.ActorAlign.CENTER,
|
||||||
|
+ y_align: Clutter.ActorAlign.CENTER,
|
||||||
|
+ });
|
||||||
|
+
|
||||||
|
+ let iconBin = new St.Widget({
|
||||||
|
+ layout_manager: new Clutter.BinLayout(),
|
||||||
|
+ style_class: 'system-status-icon',
|
||||||
|
+ });
|
||||||
|
+ iconBin.add_child(icon);
|
||||||
|
+ button.add_child(iconBin);
|
||||||
|
+
|
||||||
|
+ this._icons.set(icon, button);
|
||||||
|
+
|
||||||
|
+ button.connect('button-release-event',
|
||||||
|
+ (actor, event) => icon.click(event));
|
||||||
|
+ button.connect('key-press-event',
|
||||||
|
+ (actor, event) => icon.click(event));
|
||||||
|
+
|
||||||
|
+ const role = `${icon}`;
|
||||||
|
+ Main.panel.addToStatusArea(role, button);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _onTrayIconRemoved(o, icon) {
|
||||||
|
+ const button = this._icons.get(icon);
|
||||||
|
+ button?.destroy();
|
||||||
|
+ this._icons.delete(icon);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ enable() {
|
||||||
|
+ this._tray = new Shell.TrayManager();
|
||||||
|
+ this._tray.connect('tray-icon-added',
|
||||||
|
+ this._onTrayIconAdded.bind(this));
|
||||||
|
+ this._tray.connect('tray-icon-removed',
|
||||||
|
+ this._onTrayIconRemoved.bind(this));
|
||||||
|
+ this._tray.manage_screen(Main.panel);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ disable() {
|
||||||
|
+ this._icons.forEach(button => button.destroy());
|
||||||
|
+ this._icons.clear();
|
||||||
|
+
|
||||||
|
+ this._tray.unmanage_screen();
|
||||||
|
+ this._tray = null;
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
diff --git a/extensions/top-icons/meson.build b/extensions/top-icons/meson.build
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..b30272ad
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/extensions/top-icons/meson.build
|
||||||
|
@@ -0,0 +1,9 @@
|
||||||
|
+# SPDX-FileCopyrightText: 2018 Florian Müllner <fmuellner@gnome.org>
|
||||||
|
+#
|
||||||
|
+# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
+
|
||||||
|
+extension_data += configure_file(
|
||||||
|
+ input: metadata_name + '.in',
|
||||||
|
+ output: metadata_name,
|
||||||
|
+ configuration: metadata_conf
|
||||||
|
+)
|
||||||
|
diff --git a/extensions/top-icons/metadata.json.in b/extensions/top-icons/metadata.json.in
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..1d2e0bc2
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/extensions/top-icons/metadata.json.in
|
||||||
|
@@ -0,0 +1,10 @@
|
||||||
|
+{
|
||||||
|
+"extension-id": "@extension_id@",
|
||||||
|
+"uuid": "@uuid@",
|
||||||
|
+"settings-schema": "@gschemaname@",
|
||||||
|
+"gettext-domain": "@gettext_domain@",
|
||||||
|
+"name": "Top Icons",
|
||||||
|
+"description": "Show legacy tray icons on top",
|
||||||
|
+"shell-version": [ "@shell_current@" ],
|
||||||
|
+"url": "@url@"
|
||||||
|
+}
|
||||||
|
diff --git a/meson.build b/meson.build
|
||||||
|
index 536efe49..a7294c18 100644
|
||||||
|
--- a/meson.build
|
||||||
|
+++ b/meson.build
|
||||||
|
@@ -43,6 +43,7 @@ default_extensions += [
|
||||||
|
'light-style',
|
||||||
|
'screenshot-window-sizer',
|
||||||
|
'system-monitor',
|
||||||
|
+ 'top-icons',
|
||||||
|
'windowsNavigator',
|
||||||
|
'workspace-indicator'
|
||||||
|
]
|
||||||
|
--
|
||||||
|
2.45.2
|
||||||
|
|
@ -0,0 +1,181 @@
|
|||||||
|
From ff5063cb006a3723f422017d44787cfd908bb147 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Carlos Garnacho <carlosg@gnome.org>
|
||||||
|
Date: Thu, 28 Jan 2021 00:06:12 +0100
|
||||||
|
Subject: [PATCH 2/5] Add gesture-inhibitor extension
|
||||||
|
|
||||||
|
This extension may disable default GNOME Shell gestures.
|
||||||
|
---
|
||||||
|
extensions/gesture-inhibitor/extension.js | 79 +++++++++++++++++++
|
||||||
|
extensions/gesture-inhibitor/meson.build | 8 ++
|
||||||
|
extensions/gesture-inhibitor/metadata.json.in | 12 +++
|
||||||
|
...l.extensions.gesture-inhibitor.gschema.xml | 25 ++++++
|
||||||
|
meson.build | 1 +
|
||||||
|
5 files changed, 125 insertions(+)
|
||||||
|
create mode 100644 extensions/gesture-inhibitor/extension.js
|
||||||
|
create mode 100644 extensions/gesture-inhibitor/meson.build
|
||||||
|
create mode 100644 extensions/gesture-inhibitor/metadata.json.in
|
||||||
|
create mode 100644 extensions/gesture-inhibitor/org.gnome.shell.extensions.gesture-inhibitor.gschema.xml
|
||||||
|
|
||||||
|
diff --git a/extensions/gesture-inhibitor/extension.js b/extensions/gesture-inhibitor/extension.js
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..872020ba
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/extensions/gesture-inhibitor/extension.js
|
||||||
|
@@ -0,0 +1,79 @@
|
||||||
|
+// SPDX-FileCopyrightText: 2021 Carlos Garnacho <carlosg@gnome.org>
|
||||||
|
+//
|
||||||
|
+// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
+//
|
||||||
|
+
|
||||||
|
+import Clutter from 'gi://Clutter';
|
||||||
|
+import Gio from 'gi://Gio';
|
||||||
|
+import St from 'gi://St';
|
||||||
|
+
|
||||||
|
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||||
|
+
|
||||||
|
+import {AppSwitchAction} from 'resource:///org/gnome/shell/ui/windowManager.js';
|
||||||
|
+import {EdgeDragAction} from 'resource:///org/gnome/shell/ui/edgeDragAction.js';
|
||||||
|
+
|
||||||
|
+import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
|
||||||
|
+
|
||||||
|
+export default class GestureInhibitorExtension extends Extension {
|
||||||
|
+ constructor(metadata) {
|
||||||
|
+ super(metadata);
|
||||||
|
+
|
||||||
|
+ let actions = global.stage.get_actions();
|
||||||
|
+
|
||||||
|
+ actions.forEach(a => {
|
||||||
|
+ if (a instanceof AppSwitchAction)
|
||||||
|
+ this._appSwitch = a;
|
||||||
|
+ else if (a instanceof EdgeDragAction &&
|
||||||
|
+ a._side === St.Side.BOTTOM)
|
||||||
|
+ this._showOsk = a;
|
||||||
|
+ else if (a instanceof EdgeDragAction &&
|
||||||
|
+ a._side === St.Side.TOP)
|
||||||
|
+ this._unfullscreen = a;
|
||||||
|
+ });
|
||||||
|
+
|
||||||
|
+ this._map = [
|
||||||
|
+ {setting: 'overview', action: Main.overview._swipeTracker},
|
||||||
|
+ {setting: 'app-switch', action: this._appSwitch},
|
||||||
|
+ {setting: 'show-osk', action: this._showOsk},
|
||||||
|
+ {setting: 'unfullscreen', action: this._unfullscreen},
|
||||||
|
+ {setting: 'workspace-switch', action: Main.wm._workspaceAnimation._swipeTracker},
|
||||||
|
+ ];
|
||||||
|
+
|
||||||
|
+ this._enabledDesc = Object.getOwnPropertyDescriptor(
|
||||||
|
+ Clutter.ActorMeta.prototype, 'enabled');
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _overrideEnabledSetter(obj, set) {
|
||||||
|
+ if (!(obj instanceof Clutter.ActorMeta))
|
||||||
|
+ return;
|
||||||
|
+
|
||||||
|
+ const desc = set
|
||||||
|
+ ? {...this._enabledDesc, set}
|
||||||
|
+ : {...this._enabledDesc};
|
||||||
|
+ Object.defineProperty(obj, 'enabled', desc);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ enable() {
|
||||||
|
+ const settings = this.getSettings();
|
||||||
|
+
|
||||||
|
+ this._map.forEach(m => {
|
||||||
|
+ settings.bind(m.setting, m.action, 'enabled',
|
||||||
|
+ Gio.SettingsBindFlags.DEFAULT);
|
||||||
|
+
|
||||||
|
+ this._overrideEnabledSetter(m.action, function (value) {
|
||||||
|
+ if (settings.get_boolean(m.setting)) {
|
||||||
|
+ // eslint-disable-next-line no-invalid-this
|
||||||
|
+ this.set_enabled(value);
|
||||||
|
+ }
|
||||||
|
+ });
|
||||||
|
+ });
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ disable() {
|
||||||
|
+ this._map.forEach(m => {
|
||||||
|
+ Gio.Settings.unbind(m.action, 'enabled');
|
||||||
|
+ this._overrideEnabledSetter(m.action);
|
||||||
|
+ m.action.enabled = true;
|
||||||
|
+ });
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
diff --git a/extensions/gesture-inhibitor/meson.build b/extensions/gesture-inhibitor/meson.build
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..fdad5cc8
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/extensions/gesture-inhibitor/meson.build
|
||||||
|
@@ -0,0 +1,8 @@
|
||||||
|
+extension_data += configure_file(
|
||||||
|
+ input: metadata_name + '.in',
|
||||||
|
+ output: metadata_name,
|
||||||
|
+ configuration: metadata_conf
|
||||||
|
+)
|
||||||
|
+
|
||||||
|
+# extension_sources += files('prefs.js')
|
||||||
|
+extension_schemas += files(metadata_conf.get('gschemaname') + '.gschema.xml')
|
||||||
|
diff --git a/extensions/gesture-inhibitor/metadata.json.in b/extensions/gesture-inhibitor/metadata.json.in
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..37d6a117
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/extensions/gesture-inhibitor/metadata.json.in
|
||||||
|
@@ -0,0 +1,12 @@
|
||||||
|
+{
|
||||||
|
+ "uuid": "@uuid@",
|
||||||
|
+ "extension-id": "@extension_id@",
|
||||||
|
+ "settings-schema": "@gschemaname@",
|
||||||
|
+ "gettext-domain": "@gettext_domain@",
|
||||||
|
+ "name": "Gesture Inhibitor",
|
||||||
|
+ "description": "Makes touchscreen gestures optional.",
|
||||||
|
+ "shell-version": [ "@shell_current@" ],
|
||||||
|
+ "original-authors": [ "cgarnach@redhat.com" ],
|
||||||
|
+ "url": "@url@"
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
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
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..b06d027a
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/extensions/gesture-inhibitor/org.gnome.shell.extensions.gesture-inhibitor.gschema.xml
|
||||||
|
@@ -0,0 +1,25 @@
|
||||||
|
+<schemalist>
|
||||||
|
+ <schema id="org.gnome.shell.extensions.gesture-inhibitor" path="/org/gnome/shell/extensions/gesture-inhibitor/">
|
||||||
|
+ <key name="show-osk" type="b">
|
||||||
|
+ <default>true</default>
|
||||||
|
+ <summary>Show OSK gesture</summary>
|
||||||
|
+ </key>
|
||||||
|
+ <key name="overview" type="b">
|
||||||
|
+ <default>true</default>
|
||||||
|
+ <summary>Show Overview gesture</summary>
|
||||||
|
+ </key>
|
||||||
|
+ <key name="app-switch" type="b">
|
||||||
|
+ <default>true</default>
|
||||||
|
+ <summary>Application switch gesture</summary>
|
||||||
|
+ </key>
|
||||||
|
+ <key name="workspace-switch" type="b">
|
||||||
|
+ <default>true</default>
|
||||||
|
+ <summary>Workspace switch gesture</summary>
|
||||||
|
+ </key>
|
||||||
|
+ <key name="unfullscreen" type="b">
|
||||||
|
+ <default>true</default>
|
||||||
|
+ <summary>Unfullscreen gesture</summary>
|
||||||
|
+ </key>
|
||||||
|
+ </schema>
|
||||||
|
+</schemalist>
|
||||||
|
+
|
||||||
|
diff --git a/meson.build b/meson.build
|
||||||
|
index a7294c18..e36d948d 100644
|
||||||
|
--- a/meson.build
|
||||||
|
+++ b/meson.build
|
||||||
|
@@ -51,6 +51,7 @@ default_extensions += [
|
||||||
|
all_extensions = default_extensions
|
||||||
|
all_extensions += [
|
||||||
|
'auto-move-windows',
|
||||||
|
+ 'gesture-inhibitor',
|
||||||
|
'native-window-placement',
|
||||||
|
'user-theme'
|
||||||
|
]
|
||||||
|
--
|
||||||
|
2.45.2
|
||||||
|
|
@ -0,0 +1,479 @@
|
|||||||
|
From 950f0fded26d8664bce5410db4c280674147cdd8 Mon Sep 17 00:00:00 2001
|
||||||
|
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
|
||||||
|
Date: Thu, 2 Dec 2021 19:39:50 +0100
|
||||||
|
Subject: [PATCH] Add classification-banner
|
||||||
|
|
||||||
|
---
|
||||||
|
extensions/classification-banner/extension.js | 163 +++++++++++++++
|
||||||
|
extensions/classification-banner/meson.build | 9 +
|
||||||
|
.../classification-banner/metadata.json.in | 11 +
|
||||||
|
...tensions.classification-banner.gschema.xml | 29 +++
|
||||||
|
extensions/classification-banner/prefs.js | 192 ++++++++++++++++++
|
||||||
|
.../classification-banner/stylesheet.css | 3 +
|
||||||
|
meson.build | 1 +
|
||||||
|
7 files changed, 408 insertions(+)
|
||||||
|
create mode 100644 extensions/classification-banner/extension.js
|
||||||
|
create mode 100644 extensions/classification-banner/meson.build
|
||||||
|
create mode 100644 extensions/classification-banner/metadata.json.in
|
||||||
|
create mode 100644 extensions/classification-banner/org.gnome.shell.extensions.classification-banner.gschema.xml
|
||||||
|
create mode 100644 extensions/classification-banner/prefs.js
|
||||||
|
create mode 100644 extensions/classification-banner/stylesheet.css
|
||||||
|
|
||||||
|
diff --git a/extensions/classification-banner/extension.js b/extensions/classification-banner/extension.js
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..32c7d794
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/extensions/classification-banner/extension.js
|
||||||
|
@@ -0,0 +1,163 @@
|
||||||
|
+// SPDX-FileCopyrightText: 2021 Florian Müllner <fmuellner@gnome.org>
|
||||||
|
+// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
+
|
||||||
|
+import Clutter from 'gi://Clutter';
|
||||||
|
+import Cogl from 'gi://Cogl';
|
||||||
|
+import Gio from 'gi://Gio';
|
||||||
|
+import GLib from 'gi://GLib';
|
||||||
|
+import GObject from 'gi://GObject';
|
||||||
|
+import Shell from 'gi://Shell';
|
||||||
|
+import St from 'gi://St';
|
||||||
|
+
|
||||||
|
+import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
|
||||||
|
+
|
||||||
|
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||||
|
+import {MonitorConstraint} from 'resource:///org/gnome/shell/ui/layout.js';
|
||||||
|
+
|
||||||
|
+class ClassificationBanner extends Clutter.Actor {
|
||||||
|
+ static {
|
||||||
|
+ GObject.registerClass(this);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ #topBanner;
|
||||||
|
+ #bottomBanner;
|
||||||
|
+ #monitorConstraint;
|
||||||
|
+ #settings;
|
||||||
|
+
|
||||||
|
+ constructor(index, settings) {
|
||||||
|
+ const constraint = new MonitorConstraint({index});
|
||||||
|
+ super({
|
||||||
|
+ layout_manager: new Clutter.BinLayout(),
|
||||||
|
+ constraints: constraint,
|
||||||
|
+ });
|
||||||
|
+ this.#monitorConstraint = constraint;
|
||||||
|
+
|
||||||
|
+ Shell.util_set_hidden_from_pick(this, true);
|
||||||
|
+
|
||||||
|
+ this.#settings = settings;
|
||||||
|
+
|
||||||
|
+ this.#topBanner = new St.BoxLayout({
|
||||||
|
+ style_class: 'classification-banner',
|
||||||
|
+ x_expand: true,
|
||||||
|
+ y_expand: true,
|
||||||
|
+ y_align: Clutter.ActorAlign.START,
|
||||||
|
+ });
|
||||||
|
+ this.add_child(this.#topBanner);
|
||||||
|
+ this.#settings.bind('top-banner',
|
||||||
|
+ this.#topBanner, 'visible',
|
||||||
|
+ Gio.SettingsBindFlags.GET);
|
||||||
|
+
|
||||||
|
+ this.#bottomBanner = new St.BoxLayout({
|
||||||
|
+ style_class: 'classification-banner',
|
||||||
|
+ x_expand: true,
|
||||||
|
+ y_expand: true,
|
||||||
|
+ y_align: Clutter.ActorAlign.END,
|
||||||
|
+ });
|
||||||
|
+ this.add_child(this.#bottomBanner);
|
||||||
|
+ this.#settings.bind('bottom-banner',
|
||||||
|
+ this.#bottomBanner, 'visible',
|
||||||
|
+ Gio.SettingsBindFlags.GET);
|
||||||
|
+
|
||||||
|
+ for (const banner of [this.#topBanner, this.#bottomBanner]) {
|
||||||
|
+ const label = new St.Label({
|
||||||
|
+ style_class: 'classification-message',
|
||||||
|
+ x_align: Clutter.ActorAlign.CENTER,
|
||||||
|
+ x_expand: true,
|
||||||
|
+ });
|
||||||
|
+ banner.add_child(label);
|
||||||
|
+
|
||||||
|
+ this.#settings.bind('message',
|
||||||
|
+ label, 'text',
|
||||||
|
+ Gio.SettingsBindFlags.GET);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ const hostLabel = new St.Label({
|
||||||
|
+ style_class: 'classification-system-info',
|
||||||
|
+ text: GLib.get_host_name(),
|
||||||
|
+ });
|
||||||
|
+ this.#topBanner.insert_child_at_index(hostLabel, 0);
|
||||||
|
+ this.#settings.bind('system-info',
|
||||||
|
+ hostLabel, 'visible',
|
||||||
|
+ Gio.SettingsBindFlags.GET);
|
||||||
|
+
|
||||||
|
+ const userLabel = new St.Label({
|
||||||
|
+ style_class: 'classification-system-info',
|
||||||
|
+ text: GLib.get_user_name(),
|
||||||
|
+ });
|
||||||
|
+ this.#topBanner.add_child(userLabel);
|
||||||
|
+ this.#settings.bind('system-info',
|
||||||
|
+ userLabel, 'visible',
|
||||||
|
+ Gio.SettingsBindFlags.GET);
|
||||||
|
+
|
||||||
|
+ global.display.connectObject('in-fullscreen-changed',
|
||||||
|
+ () => this.#updateMonitorConstraint(), this);
|
||||||
|
+ this.#updateMonitorConstraint();
|
||||||
|
+
|
||||||
|
+ this.#settings.connectObject(
|
||||||
|
+ 'changed::color', () => this.#updateStyles(),
|
||||||
|
+ 'changed::background-color', () => this.#updateStyles(),
|
||||||
|
+ this);
|
||||||
|
+ this.#updateStyles();
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ #getColorSetting(key) {
|
||||||
|
+ const str = this.#settings.get_string(key);
|
||||||
|
+ const [valid, color] = Cogl.Color.from_string(str);
|
||||||
|
+ if (!valid)
|
||||||
|
+ return '';
|
||||||
|
+ const {red, green, blue, alpha} = color;
|
||||||
|
+ 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');
|
||||||
|
+ const style = `${bgStyle}${fgStyle}`;
|
||||||
|
+ this.#topBanner.set({style});
|
||||||
|
+ this.#bottomBanner.set({style});
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+export default class ClassificationBannerExtension extends Extension {
|
||||||
|
+ #banners = [];
|
||||||
|
+
|
||||||
|
+ #updateMonitors() {
|
||||||
|
+ const {monitors, panelBox, primaryIndex} = Main.layoutManager;
|
||||||
|
+ if (monitors.length !== this.#banners.length) {
|
||||||
|
+ this.#clearBanners();
|
||||||
|
+
|
||||||
|
+ const settings = this.getSettings();
|
||||||
|
+ for (let i = 0; i < monitors.length; i++) {
|
||||||
|
+ const banner = new ClassificationBanner(i, settings);
|
||||||
|
+ Main.uiGroup.add_child(banner);
|
||||||
|
+ this.#banners.push(banner);
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ const primaryBanner = this.#banners[primaryIndex];
|
||||||
|
+ if (primaryBanner)
|
||||||
|
+ Main.uiGroup.set_child_below_sibling(primaryBanner, panelBox);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ #clearBanners() {
|
||||||
|
+ this.#banners.forEach(b => b.destroy());
|
||||||
|
+ this.#banners = [];
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ enable() {
|
||||||
|
+ Main.layoutManager.connectObject('monitors-changed',
|
||||||
|
+ () => this.#updateMonitors(), this);
|
||||||
|
+ this.#updateMonitors();
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ disable() {
|
||||||
|
+ Main.layoutManager.disconnectObject(this);
|
||||||
|
+ this.#clearBanners();
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
diff --git a/extensions/classification-banner/meson.build b/extensions/classification-banner/meson.build
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..aa943741
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/extensions/classification-banner/meson.build
|
||||||
|
@@ -0,0 +1,9 @@
|
||||||
|
+extension_data += configure_file(
|
||||||
|
+ input: metadata_name + '.in',
|
||||||
|
+ output: metadata_name,
|
||||||
|
+ configuration: metadata_conf
|
||||||
|
+)
|
||||||
|
+extension_data += files('stylesheet.css')
|
||||||
|
+
|
||||||
|
+extension_sources += files('prefs.js')
|
||||||
|
+extension_schemas += files(metadata_conf.get('gschemaname') + '.gschema.xml')
|
||||||
|
diff --git a/extensions/classification-banner/metadata.json.in b/extensions/classification-banner/metadata.json.in
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..f93b1a2d
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/extensions/classification-banner/metadata.json.in
|
||||||
|
@@ -0,0 +1,11 @@
|
||||||
|
+{
|
||||||
|
+"extension-id": "@extension_id@",
|
||||||
|
+"uuid": "@uuid@",
|
||||||
|
+"settings-schema": "@gschemaname@",
|
||||||
|
+"gettext-domain": "@gettext_domain@",
|
||||||
|
+"name": "Classification Banner",
|
||||||
|
+"description": "Display classification level banner",
|
||||||
|
+"shell-version": [ "@shell_current@" ],
|
||||||
|
+"session-modes": [ "gdm", "unlock-dialog", "user" ],
|
||||||
|
+"url": "@url@"
|
||||||
|
+}
|
||||||
|
diff --git a/extensions/classification-banner/org.gnome.shell.extensions.classification-banner.gschema.xml b/extensions/classification-banner/org.gnome.shell.extensions.classification-banner.gschema.xml
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..0314ef60
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/extensions/classification-banner/org.gnome.shell.extensions.classification-banner.gschema.xml
|
||||||
|
@@ -0,0 +1,29 @@
|
||||||
|
+<schemalist gettext-domain="gnome-shell-extensions">
|
||||||
|
+ <schema id="org.gnome.shell.extensions.classification-banner"
|
||||||
|
+ path="/org/gnome/shell/extensions/classification-banner/">
|
||||||
|
+ <key name="top-banner" type="b">
|
||||||
|
+ <default>true</default>
|
||||||
|
+ <summary>Show a banner at the top</summary>
|
||||||
|
+ </key>
|
||||||
|
+ <key name="bottom-banner" type="b">
|
||||||
|
+ <default>true</default>
|
||||||
|
+ <summary>Show a banner at the bottom</summary>
|
||||||
|
+ </key>
|
||||||
|
+ <key name="message" type="s">
|
||||||
|
+ <default>"UNCLASSIFIED"</default>
|
||||||
|
+ <summary>classification message</summary>
|
||||||
|
+ </key>
|
||||||
|
+ <key name="color" type="s">
|
||||||
|
+ <default>"#fff"</default>
|
||||||
|
+ <summary>text color</summary>
|
||||||
|
+ </key>
|
||||||
|
+ <key name="background-color" type="s">
|
||||||
|
+ <default>"rgba(0,122,51,0.75)"</default>
|
||||||
|
+ <summary>background color</summary>
|
||||||
|
+ </key>
|
||||||
|
+ <key name="system-info" type="b">
|
||||||
|
+ <default>false</default>
|
||||||
|
+ <summary>Include system info in top banner</summary>
|
||||||
|
+ </key>
|
||||||
|
+ </schema>
|
||||||
|
+</schemalist>
|
||||||
|
diff --git a/extensions/classification-banner/prefs.js b/extensions/classification-banner/prefs.js
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..dc73ddae
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/extensions/classification-banner/prefs.js
|
||||||
|
@@ -0,0 +1,192 @@
|
||||||
|
+import Adw from 'gi://Adw';
|
||||||
|
+import Gdk from 'gi://Gdk';
|
||||||
|
+import Gio from 'gi://Gio';
|
||||||
|
+import GObject from 'gi://GObject';
|
||||||
|
+import Gtk from 'gi://Gtk';
|
||||||
|
+
|
||||||
|
+import {ExtensionPreferences, gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
|
||||||
|
+
|
||||||
|
+class GenericPrefs extends Adw.PreferencesGroup {
|
||||||
|
+ static {
|
||||||
|
+ GObject.registerClass(this);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ #actionGroup = new Gio.SimpleActionGroup();
|
||||||
|
+ #settings;
|
||||||
|
+
|
||||||
|
+ constructor(settings) {
|
||||||
|
+ super();
|
||||||
|
+
|
||||||
|
+ this.#settings = settings;
|
||||||
|
+ this.insert_action_group('options', this.#actionGroup);
|
||||||
|
+
|
||||||
|
+ this.#actionGroup.add_action(settings.create_action('top-banner'));
|
||||||
|
+ this.#actionGroup.add_action(settings.create_action('bottom-banner'));
|
||||||
|
+ this.#actionGroup.add_action(settings.create_action('system-info'));
|
||||||
|
+
|
||||||
|
+ this.add(new Adw.SwitchRow({
|
||||||
|
+ title: _('Top Banner'),
|
||||||
|
+ action_name: 'options.top-banner',
|
||||||
|
+ }));
|
||||||
|
+
|
||||||
|
+ this.add(new Adw.SwitchRow({
|
||||||
|
+ title: _('Bottom Banner'),
|
||||||
|
+ action_name: 'options.bottom-banner',
|
||||||
|
+ }));
|
||||||
|
+
|
||||||
|
+ this.add(new Adw.SwitchRow({
|
||||||
|
+ title: _('System Info'),
|
||||||
|
+ action_name: 'options.system-info',
|
||||||
|
+ }));
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+class BannerPreset extends GObject.Object {
|
||||||
|
+ static [GObject.properties] = {
|
||||||
|
+ 'message': GObject.ParamSpec.string(
|
||||||
|
+ 'message', 'message', 'message',
|
||||||
|
+ GObject.ParamFlags.READWRITE,
|
||||||
|
+ null),
|
||||||
|
+ 'color': GObject.ParamSpec.string(
|
||||||
|
+ 'color', 'color', 'color',
|
||||||
|
+ GObject.ParamFlags.READWRITE,
|
||||||
|
+ null),
|
||||||
|
+ 'background-color': GObject.ParamSpec.string(
|
||||||
|
+ 'background-color', 'background-color', 'background-color',
|
||||||
|
+ GObject.ParamFlags.READWRITE,
|
||||||
|
+ null),
|
||||||
|
+ };
|
||||||
|
+
|
||||||
|
+ static {
|
||||||
|
+ GObject.registerClass(this);
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+class AppearancePrefs extends Adw.PreferencesGroup {
|
||||||
|
+ static {
|
||||||
|
+ GObject.registerClass(this);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ #settings;
|
||||||
|
+
|
||||||
|
+ constructor(settings) {
|
||||||
|
+ super();
|
||||||
|
+
|
||||||
|
+ this.#settings = settings;
|
||||||
|
+
|
||||||
|
+ const model = new Gio.ListStore({item_type: BannerPreset.$gtype});
|
||||||
|
+ model.append(new BannerPreset({
|
||||||
|
+ message: 'UNCLASSIFIED',
|
||||||
|
+ color: '#fff',
|
||||||
|
+ background_color: 'rgba(0, 122, 51, 0.75)',
|
||||||
|
+ }));
|
||||||
|
+ model.append(new BannerPreset({
|
||||||
|
+ message: 'CONFIDENTIAL',
|
||||||
|
+ color: '#fff',
|
||||||
|
+ background_color: 'rgba(0, 51, 160, 0.75)',
|
||||||
|
+ }));
|
||||||
|
+ model.append(new BannerPreset({
|
||||||
|
+ message: 'SECRET',
|
||||||
|
+ color: '#fff',
|
||||||
|
+ background_color: 'rgba(200, 16, 46, 0.75)',
|
||||||
|
+ }));
|
||||||
|
+ model.append(new BannerPreset({
|
||||||
|
+ message: 'TOP SECRET',
|
||||||
|
+ color: '#fff',
|
||||||
|
+ background_color: 'rgba(255, 103, 31, 0.75)',
|
||||||
|
+ }));
|
||||||
|
+ model.append(new BannerPreset({
|
||||||
|
+ message: 'TOP SECRET//SCI',
|
||||||
|
+ color: '#000',
|
||||||
|
+ background_color: 'rgba(247, 234, 72, 0.75)',
|
||||||
|
+ }));
|
||||||
|
+
|
||||||
|
+ let row, activatableWidget;
|
||||||
|
+ row = this.#createPresetsRow(model);
|
||||||
|
+ row.connect('notify::selected-item', comboRow => {
|
||||||
|
+ const {message, color, backgroundColor} = comboRow.selected_item;
|
||||||
|
+ this.#settings.set_string('message', message);
|
||||||
|
+ this.#settings.set_string('color', color);
|
||||||
|
+ this.#settings.set_string('background-color', backgroundColor);
|
||||||
|
+ });
|
||||||
|
+ this.add(row);
|
||||||
|
+
|
||||||
|
+ activatableWidget = new Gtk.Entry({
|
||||||
|
+ valign: Gtk.Align.CENTER,
|
||||||
|
+ });
|
||||||
|
+ this.#settings.bind('message',
|
||||||
|
+ activatableWidget, 'text',
|
||||||
|
+ Gio.SettingsBindFlags.DEFAULT);
|
||||||
|
+ row = new Adw.ActionRow({title: _('Message'), activatableWidget});
|
||||||
|
+ row.add_suffix(activatableWidget);
|
||||||
|
+ this.add(row);
|
||||||
|
+
|
||||||
|
+ activatableWidget = this.#createColorButton('background-color', {
|
||||||
|
+ use_alpha: true,
|
||||||
|
+ });
|
||||||
|
+ row = new Adw.ActionRow({title: _('Background color'), activatableWidget});
|
||||||
|
+ row.add_suffix(activatableWidget);
|
||||||
|
+ this.add(row);
|
||||||
|
+
|
||||||
|
+ activatableWidget = this.#createColorButton('color');
|
||||||
|
+ row = new Adw.ActionRow({title: _('Text color'), activatableWidget});
|
||||||
|
+ row.add_suffix(activatableWidget);
|
||||||
|
+ this.add(row);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ #createPresetsRow(model) {
|
||||||
|
+ const listFactory = new Gtk.SignalListItemFactory();
|
||||||
|
+ listFactory.connect('setup',
|
||||||
|
+ (f, item) => item.set_child(new Gtk.Label()));
|
||||||
|
+ listFactory.connect('bind', (f, listItem) => {
|
||||||
|
+ const {child, item} = listItem;
|
||||||
|
+
|
||||||
|
+ const provider = new Gtk.CssProvider();
|
||||||
|
+ provider.load_from_data(`* {
|
||||||
|
+ border-radius: 99px;
|
||||||
|
+ padding: 6px;
|
||||||
|
+ color: ${item.color};
|
||||||
|
+ background-color: ${item.background_color};
|
||||||
|
+ }`, -1);
|
||||||
|
+ child.get_style_context().add_provider(provider,
|
||||||
|
+ Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
|
||||||
|
+ child.label = item.message;
|
||||||
|
+ });
|
||||||
|
+
|
||||||
|
+ return new Adw.ComboRow({
|
||||||
|
+ title: _('Presets'),
|
||||||
|
+ model,
|
||||||
|
+ listFactory,
|
||||||
|
+ expression: Gtk.ConstantExpression.new_for_value(''),
|
||||||
|
+ });
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ #createColorButton(key, params = {}) {
|
||||||
|
+ const rgba = new Gdk.RGBA();
|
||||||
|
+ rgba.parse(this.#settings.get_string(key));
|
||||||
|
+
|
||||||
|
+ const button = new Gtk.ColorButton({
|
||||||
|
+ ...params,
|
||||||
|
+ rgba,
|
||||||
|
+ valign: Gtk.Align.CENTER,
|
||||||
|
+ });
|
||||||
|
+ this.#settings.connect(`changed::${key}`, () => {
|
||||||
|
+ const newRgba = new Gdk.RGBA();
|
||||||
|
+ newRgba.parse(this.#settings.get_string(key));
|
||||||
|
+ if (!newRgba.equal(button.rgba))
|
||||||
|
+ button.set({rgba: newRgba});
|
||||||
|
+ });
|
||||||
|
+ button.connect('notify::rgba',
|
||||||
|
+ () => this.#settings.set_string(key, button.rgba.to_string()));
|
||||||
|
+ return button;
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+export default class ClassificationPrefs extends ExtensionPreferences {
|
||||||
|
+ getPreferencesWidget() {
|
||||||
|
+ const page = new Adw.PreferencesPage();
|
||||||
|
+ page.add(new AppearancePrefs(this.getSettings()));
|
||||||
|
+ page.add(new GenericPrefs(this.getSettings()));
|
||||||
|
+ return page;
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
diff --git a/extensions/classification-banner/stylesheet.css b/extensions/classification-banner/stylesheet.css
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..fb6a697e
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/extensions/classification-banner/stylesheet.css
|
||||||
|
@@ -0,0 +1,3 @@
|
||||||
|
+.classification-system-info { padding: 0 24px; }
|
||||||
|
+.classification-message { font-weight: bold; }
|
||||||
|
+.classification-banner { font-size: 0.9em; }
|
||||||
|
diff --git a/meson.build b/meson.build
|
||||||
|
index e36d948d..63bd9ee0 100644
|
||||||
|
--- a/meson.build
|
||||||
|
+++ b/meson.build
|
||||||
|
@@ -51,6 +51,7 @@ default_extensions += [
|
||||||
|
all_extensions = default_extensions
|
||||||
|
all_extensions += [
|
||||||
|
'auto-move-windows',
|
||||||
|
+ 'classification-banner',
|
||||||
|
'gesture-inhibitor',
|
||||||
|
'native-window-placement',
|
||||||
|
'user-theme'
|
||||||
|
--
|
||||||
|
2.45.2
|
||||||
|
|
@ -0,0 +1,878 @@
|
|||||||
|
From 271cee0eab89dcbf7b6c307c55cfbbbf843e7a0e Mon Sep 17 00:00:00 2001
|
||||||
|
From: Ray Strode <rstrode@redhat.com>
|
||||||
|
Date: Tue, 24 Aug 2021 15:03:57 -0400
|
||||||
|
Subject: [PATCH 4/5] Add heads-up-display
|
||||||
|
|
||||||
|
---
|
||||||
|
extensions/heads-up-display/extension.js | 404 ++++++++++++++++++
|
||||||
|
extensions/heads-up-display/headsUpMessage.js | 166 +++++++
|
||||||
|
extensions/heads-up-display/meson.build | 13 +
|
||||||
|
extensions/heads-up-display/metadata.json.in | 12 +
|
||||||
|
...ll.extensions.heads-up-display.gschema.xml | 60 +++
|
||||||
|
extensions/heads-up-display/prefs.js | 92 ++++
|
||||||
|
extensions/heads-up-display/stylesheet.css | 38 ++
|
||||||
|
meson.build | 1 +
|
||||||
|
po/POTFILES.in | 1 +
|
||||||
|
9 files changed, 787 insertions(+)
|
||||||
|
create mode 100644 extensions/heads-up-display/extension.js
|
||||||
|
create mode 100644 extensions/heads-up-display/headsUpMessage.js
|
||||||
|
create mode 100644 extensions/heads-up-display/meson.build
|
||||||
|
create mode 100644 extensions/heads-up-display/metadata.json.in
|
||||||
|
create mode 100644 extensions/heads-up-display/org.gnome.shell.extensions.heads-up-display.gschema.xml
|
||||||
|
create mode 100644 extensions/heads-up-display/prefs.js
|
||||||
|
create mode 100644 extensions/heads-up-display/stylesheet.css
|
||||||
|
|
||||||
|
diff --git a/extensions/heads-up-display/extension.js b/extensions/heads-up-display/extension.js
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..a71b5925
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/extensions/heads-up-display/extension.js
|
||||||
|
@@ -0,0 +1,404 @@
|
||||||
|
+// SPDX-FileCopyrightText: 2021 Ray Strode <rstrode@redhat.com>
|
||||||
|
+//
|
||||||
|
+// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
+
|
||||||
|
+import GObject from 'gi://GObject';
|
||||||
|
+import Meta from 'gi://Meta';
|
||||||
|
+import Mtk from 'gi://Mtk';
|
||||||
|
+
|
||||||
|
+import {Extension, gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js';
|
||||||
|
+
|
||||||
|
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||||
|
+import {MonitorConstraint} from 'resource:///org/gnome/shell/ui/layout.js';
|
||||||
|
+
|
||||||
|
+import {HeadsUpMessage} from './headsUpMessage.js';
|
||||||
|
+
|
||||||
|
+var HeadsUpConstraint = GObject.registerClass({
|
||||||
|
+ Properties: {
|
||||||
|
+ 'offset': GObject.ParamSpec.int(
|
||||||
|
+ 'offset', 'Offset', 'offset',
|
||||||
|
+ GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE,
|
||||||
|
+ -1, 0, -1),
|
||||||
|
+ 'active': GObject.ParamSpec.boolean(
|
||||||
|
+ 'active', 'Active', 'active',
|
||||||
|
+ GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE,
|
||||||
|
+ true),
|
||||||
|
+ },
|
||||||
|
+}, class HeadsUpConstraint extends MonitorConstraint {
|
||||||
|
+ constructor(props) {
|
||||||
|
+ super(props);
|
||||||
|
+ this._offset = 0;
|
||||||
|
+ this._active = true;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ get offset() {
|
||||||
|
+ return this._offset;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ set offset(o) {
|
||||||
|
+ this._offset = o;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ get active() {
|
||||||
|
+ return this._active;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ set active(a) {
|
||||||
|
+ this._active = a;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ vfunc_update_allocation(actor, actorBox) {
|
||||||
|
+ if (!Main.layoutManager.primaryMonitor)
|
||||||
|
+ return;
|
||||||
|
+
|
||||||
|
+ if (!this.active)
|
||||||
|
+ return;
|
||||||
|
+
|
||||||
|
+ if (actor.has_allocation())
|
||||||
|
+ return;
|
||||||
|
+
|
||||||
|
+ const workArea = Main.layoutManager.getWorkAreaForMonitor(Main.layoutManager.primaryIndex);
|
||||||
|
+ actorBox.init_rect(workArea.x, workArea.y + this.offset, workArea.width, workArea.height - this.offset);
|
||||||
|
+ }
|
||||||
|
+});
|
||||||
|
+
|
||||||
|
+export default class HeadsUpDisplayExtension extends Extension {
|
||||||
|
+ enable() {
|
||||||
|
+ this._settings = this.getSettings('org.gnome.shell.extensions.heads-up-display');
|
||||||
|
+ this._settings.connectObject('changed',
|
||||||
|
+ () => this._updateMessage(), this);
|
||||||
|
+
|
||||||
|
+ this._idleMonitor = global.backend.get_core_idle_monitor();
|
||||||
|
+ this._messageInhibitedUntilIdle = false;
|
||||||
|
+ global.window_manager.connectObject('map',
|
||||||
|
+ this._onWindowMap.bind(this), this);
|
||||||
|
+
|
||||||
|
+ if (Main.layoutManager._startingUp)
|
||||||
|
+ Main.layoutManager.connectObject('startup-complete', () => this._onStartupComplete(), this);
|
||||||
|
+ else
|
||||||
|
+ this._onStartupComplete();
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ disable() {
|
||||||
|
+ this._dismissMessage();
|
||||||
|
+
|
||||||
|
+ this._stopWatchingForIdle();
|
||||||
|
+
|
||||||
|
+ Main.sessionMode.disconnectObject(this);
|
||||||
|
+ Main.overview.disconnectObject(this);
|
||||||
|
+ Main.layoutManager.panelBox.disconnectObject(this);
|
||||||
|
+ Main.layoutManager.disconnectObject(this);
|
||||||
|
+ global.window_manager.disconnectObject(this);
|
||||||
|
+
|
||||||
|
+ if (this._screenShieldVisibleId) {
|
||||||
|
+ Main.screenShield._dialog._clock.disconnect(this._screenShieldVisibleId);
|
||||||
|
+ this._screenShieldVisibleId = 0;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ this._settings.disconnectObject(this);
|
||||||
|
+ delete this._settings;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _onWindowMap(shellwm, actor) {
|
||||||
|
+ const windowObject = actor.meta_window;
|
||||||
|
+ const windowType = windowObject.get_window_type();
|
||||||
|
+
|
||||||
|
+ if (windowType !== Meta.WindowType.NORMAL)
|
||||||
|
+ return;
|
||||||
|
+
|
||||||
|
+ if (!this._message || !this._message.visible)
|
||||||
|
+ return;
|
||||||
|
+
|
||||||
|
+ const messageRect = new Mtk.Rectangle({
|
||||||
|
+ x: this._message.x,
|
||||||
|
+ y: this._message.y,
|
||||||
|
+ width: this._message.width,
|
||||||
|
+ height: this._message.height,
|
||||||
|
+ });
|
||||||
|
+ const windowRect = windowObject.get_frame_rect();
|
||||||
|
+
|
||||||
|
+ if (windowRect.intersect(messageRect))
|
||||||
|
+ windowObject.move_frame(false, windowRect.x, this._message.y + this._message.height);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _onStartupComplete() {
|
||||||
|
+ Main.overview.connectObject(
|
||||||
|
+ 'showing', () => this._updateMessage(),
|
||||||
|
+ 'hidden', () => this._updateMessage(),
|
||||||
|
+ this);
|
||||||
|
+ Main.layoutManager.panelBox.connectObject('notify::visible',
|
||||||
|
+ () => this._updateMessage(), this);
|
||||||
|
+ Main.sessionMode.connectObject('updated',
|
||||||
|
+ () => this._onSessionModeUpdated(), this);
|
||||||
|
+
|
||||||
|
+ this._updateMessage();
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _onSessionModeUpdated() {
|
||||||
|
+ if (!Main.sessionMode.hasWindows)
|
||||||
|
+ this._messageInhibitedUntilIdle = false;
|
||||||
|
+
|
||||||
|
+ const dialog = Main.screenShield._dialog;
|
||||||
|
+ if (!Main.sessionMode.isGreeter && dialog && !this._screenShieldVisibleId) {
|
||||||
|
+ this._screenShieldVisibleId = dialog._clock.connect('notify::visible', this._updateMessage.bind(this));
|
||||||
|
+ this._screenShieldDestroyId = dialog._clock.connect('destroy', () => {
|
||||||
|
+ this._screenShieldVisibleId = 0;
|
||||||
|
+ this._screenShieldDestroyId = 0;
|
||||||
|
+ });
|
||||||
|
+ }
|
||||||
|
+ this._updateMessage();
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _stopWatchingForIdle() {
|
||||||
|
+ if (this._idleWatchId) {
|
||||||
|
+ this._idleMonitor.remove_watch(this._idleWatchId);
|
||||||
|
+ this._idleWatchId = 0;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (this._idleTimeoutChangedId) {
|
||||||
|
+ this._settings.disconnect(this._idleTimeoutChangedId);
|
||||||
|
+ this._idleTimeoutChangedId = 0;
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _onIdleTimeoutChanged() {
|
||||||
|
+ this._stopWatchingForIdle();
|
||||||
|
+ this._messageInhibitedUntilIdle = false;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _onUserIdle() {
|
||||||
|
+ this._messageInhibitedUntilIdle = false;
|
||||||
|
+ this._updateMessage();
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _watchForIdle() {
|
||||||
|
+ this._stopWatchingForIdle();
|
||||||
|
+
|
||||||
|
+ const idleTimeout = this._settings.get_uint('idle-timeout');
|
||||||
|
+
|
||||||
|
+ this._idleTimeoutChangedId =
|
||||||
|
+ this._settings.connect('changed::idle-timeout',
|
||||||
|
+ this._onIdleTimeoutChanged.bind(this));
|
||||||
|
+ this._idleWatchId = this._idleMonitor.add_idle_watch(idleTimeout * 1000,
|
||||||
|
+ this._onUserIdle.bind(this));
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _updateMessage() {
|
||||||
|
+ if (this._messageInhibitedUntilIdle) {
|
||||||
|
+ if (this._message)
|
||||||
|
+ this._dismissMessage();
|
||||||
|
+ return;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ this._stopWatchingForIdle();
|
||||||
|
+
|
||||||
|
+ if (Main.sessionMode.hasOverview && Main.overview.visible) {
|
||||||
|
+ this._dismissMessage();
|
||||||
|
+ return;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (!Main.layoutManager.panelBox.visible) {
|
||||||
|
+ this._dismissMessage();
|
||||||
|
+ return;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ let supportedModes = [];
|
||||||
|
+
|
||||||
|
+ if (this._settings.get_boolean('show-when-unlocked'))
|
||||||
|
+ supportedModes.push('user');
|
||||||
|
+
|
||||||
|
+ if (this._settings.get_boolean('show-when-unlocking') ||
|
||||||
|
+ this._settings.get_boolean('show-when-locked'))
|
||||||
|
+ supportedModes.push('unlock-dialog');
|
||||||
|
+
|
||||||
|
+ if (this._settings.get_boolean('show-on-login-screen'))
|
||||||
|
+ supportedModes.push('gdm');
|
||||||
|
+
|
||||||
|
+ if (!supportedModes.includes(Main.sessionMode.currentMode) &&
|
||||||
|
+ !supportedModes.includes(Main.sessionMode.parentMode)) {
|
||||||
|
+ this._dismissMessage();
|
||||||
|
+ return;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (Main.sessionMode.currentMode === 'unlock-dialog') {
|
||||||
|
+ const dialog = Main.screenShield._dialog;
|
||||||
|
+ if (!this._settings.get_boolean('show-when-locked')) {
|
||||||
|
+ if (dialog._clock.visible) {
|
||||||
|
+ this._dismissMessage();
|
||||||
|
+ return;
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (!this._settings.get_boolean('show-when-unlocking')) {
|
||||||
|
+ if (!dialog._clock.visible) {
|
||||||
|
+ this._dismissMessage();
|
||||||
|
+ return;
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ const heading = this._settings.get_string('message-heading');
|
||||||
|
+ const body = this._settings.get_string('message-body');
|
||||||
|
+
|
||||||
|
+ if (!heading && !body) {
|
||||||
|
+ this._dismissMessage();
|
||||||
|
+ return;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (!this._message) {
|
||||||
|
+ this._message = new HeadsUpMessage(heading, body);
|
||||||
|
+
|
||||||
|
+ this._message.connect('notify::allocation', this._adaptSessionForMessage.bind(this));
|
||||||
|
+ this._message.connect('clicked', this._onMessageClicked.bind(this));
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ this._message.reactive = true;
|
||||||
|
+ this._message.track_hover = true;
|
||||||
|
+
|
||||||
|
+ this._message.setHeading(heading);
|
||||||
|
+ this._message.setBody(body);
|
||||||
|
+
|
||||||
|
+ if (!Main.sessionMode.hasWindows) {
|
||||||
|
+ this._message.track_hover = false;
|
||||||
|
+ this._message.reactive = false;
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _onMessageClicked() {
|
||||||
|
+ if (!Main.sessionMode.hasWindows)
|
||||||
|
+ return;
|
||||||
|
+
|
||||||
|
+ this._watchForIdle();
|
||||||
|
+ this._messageInhibitedUntilIdle = true;
|
||||||
|
+ this._updateMessage();
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _dismissMessage() {
|
||||||
|
+ if (!this._message)
|
||||||
|
+ return;
|
||||||
|
+
|
||||||
|
+ this._message.visible = false;
|
||||||
|
+ this._message.destroy();
|
||||||
|
+ this._message = null;
|
||||||
|
+ this._resetMessageTray();
|
||||||
|
+ this._resetLoginDialog();
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _resetMessageTray() {
|
||||||
|
+ if (!Main.messageTray)
|
||||||
|
+ return;
|
||||||
|
+
|
||||||
|
+ if (this._updateMessageTrayId) {
|
||||||
|
+ global.stage.disconnect(this._updateMessageTrayId);
|
||||||
|
+ this._updateMessageTrayId = 0;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (this._messageTrayConstraint) {
|
||||||
|
+ Main.messageTray.remove_constraint(this._messageTrayConstraint);
|
||||||
|
+ this._messageTrayConstraint = null;
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _alignMessageTray() {
|
||||||
|
+ if (!Main.messageTray)
|
||||||
|
+ return;
|
||||||
|
+
|
||||||
|
+ if (!this._message || !this._message.visible) {
|
||||||
|
+ this._resetMessageTray();
|
||||||
|
+ return;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (this._updateMessageTrayId)
|
||||||
|
+ return;
|
||||||
|
+
|
||||||
|
+ this._updateMessageTrayId = global.stage.connect('before-update', () => {
|
||||||
|
+ if (!this._messageTrayConstraint) {
|
||||||
|
+ this._messageTrayConstraint = new HeadsUpConstraint({primary: true});
|
||||||
|
+
|
||||||
|
+ Main.layoutManager.panelBox.bind_property('visible',
|
||||||
|
+ this._messageTrayConstraint, 'active',
|
||||||
|
+ GObject.BindingFlags.SYNC_CREATE);
|
||||||
|
+
|
||||||
|
+ Main.messageTray.add_constraint(this._messageTrayConstraint);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ const panelBottom = Main.layoutManager.panelBox.y + Main.layoutManager.panelBox.height;
|
||||||
|
+ const messageBottom = this._message.y + this._message.height;
|
||||||
|
+
|
||||||
|
+ this._messageTrayConstraint.offset = messageBottom - panelBottom;
|
||||||
|
+ global.stage.disconnect(this._updateMessageTrayId);
|
||||||
|
+ this._updateMessageTrayId = 0;
|
||||||
|
+ });
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _resetLoginDialog() {
|
||||||
|
+ if (!Main.sessionMode.isGreeter)
|
||||||
|
+ return;
|
||||||
|
+
|
||||||
|
+ if (!Main.screenShield || !Main.screenShield._dialog)
|
||||||
|
+ return;
|
||||||
|
+
|
||||||
|
+ const dialog = Main.screenShield._dialog;
|
||||||
|
+
|
||||||
|
+ if (this._authPromptAllocatedId) {
|
||||||
|
+ dialog.disconnect(this._authPromptAllocatedId);
|
||||||
|
+ this._authPromptAllocatedId = 0;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (this._updateLoginDialogId) {
|
||||||
|
+ global.stage.disconnect(this._updateLoginDialogId);
|
||||||
|
+ this._updateLoginDialogId = 0;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (this._loginDialogConstraint) {
|
||||||
|
+ dialog.remove_constraint(this._loginDialogConstraint);
|
||||||
|
+ this._loginDialogConstraint = null;
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _adaptLoginDialogForMessage() {
|
||||||
|
+ if (!Main.sessionMode.isGreeter)
|
||||||
|
+ return;
|
||||||
|
+
|
||||||
|
+ if (!Main.screenShield || !Main.screenShield._dialog)
|
||||||
|
+ return;
|
||||||
|
+
|
||||||
|
+ if (!this._message || !this._message.visible) {
|
||||||
|
+ this._resetLoginDialog();
|
||||||
|
+ return;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ const dialog = Main.screenShield._dialog;
|
||||||
|
+
|
||||||
|
+ if (this._updateLoginDialogId)
|
||||||
|
+ return;
|
||||||
|
+
|
||||||
|
+ this._updateLoginDialogId = global.stage.connect('before-update', () => {
|
||||||
|
+ let messageHeight = this._message.y + this._message.height;
|
||||||
|
+ if (dialog._logoBin.visible)
|
||||||
|
+ messageHeight -= dialog._logoBin.height;
|
||||||
|
+
|
||||||
|
+ if (!this._logindDialogConstraint) {
|
||||||
|
+ this._loginDialogConstraint = new HeadsUpConstraint({primary: true});
|
||||||
|
+ dialog.add_constraint(this._loginDialogConstraint);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ this._loginDialogConstraint.offset = messageHeight;
|
||||||
|
+
|
||||||
|
+ global.stage.disconnect(this._updateLoginDialogId);
|
||||||
|
+ this._updateLoginDialogId = 0;
|
||||||
|
+ });
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _adaptSessionForMessage() {
|
||||||
|
+ this._alignMessageTray();
|
||||||
|
+
|
||||||
|
+ if (Main.sessionMode.isGreeter) {
|
||||||
|
+ this._adaptLoginDialogForMessage();
|
||||||
|
+ if (!this._authPromptAllocatedId) {
|
||||||
|
+ const dialog = Main.screenShield._dialog;
|
||||||
|
+ this._authPromptAllocatedId = dialog._authPrompt.connect('notify::allocation', this._adaptLoginDialogForMessage.bind(this));
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
diff --git a/extensions/heads-up-display/headsUpMessage.js b/extensions/heads-up-display/headsUpMessage.js
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..30298847
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/extensions/heads-up-display/headsUpMessage.js
|
||||||
|
@@ -0,0 +1,166 @@
|
||||||
|
+// SPDX-FileCopyrightText: 2021 Ray Strode <rstrode@redhat.com>
|
||||||
|
+//
|
||||||
|
+// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
+
|
||||||
|
+import Atk from 'gi://Atk';
|
||||||
|
+import Clutter from 'gi://Clutter';
|
||||||
|
+import GObject from 'gi://GObject';
|
||||||
|
+import St from 'gi://St';
|
||||||
|
+
|
||||||
|
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||||
|
+
|
||||||
|
+const HeadsUpMessageBodyLabel = GObject.registerClass({
|
||||||
|
+}, class HeadsUpMessageBodyLabel extends St.Label {
|
||||||
|
+ constructor(params) {
|
||||||
|
+ super(params);
|
||||||
|
+
|
||||||
|
+ this._widthCoverage = 0.75;
|
||||||
|
+ this._heightCoverage = 0.25;
|
||||||
|
+
|
||||||
|
+ global.display.connectObject('workareas-changed',
|
||||||
|
+ () => this._getWorkAreaAndMeasureLineHeight());
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _getWorkAreaAndMeasureLineHeight() {
|
||||||
|
+ if (!this.get_parent())
|
||||||
|
+ return;
|
||||||
|
+
|
||||||
|
+ this._workArea = Main.layoutManager.getWorkAreaForMonitor(Main.layoutManager.primaryIndex);
|
||||||
|
+
|
||||||
|
+ this.clutter_text.single_line_mode = true;
|
||||||
|
+ this.clutter_text.line_wrap = false;
|
||||||
|
+
|
||||||
|
+ this._lineHeight = super.vfunc_get_preferred_height(-1)[0];
|
||||||
|
+
|
||||||
|
+ this.clutter_text.single_line_mode = false;
|
||||||
|
+ this.clutter_text.line_wrap = true;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ vfunc_parent_set() {
|
||||||
|
+ this._getWorkAreaAndMeasureLineHeight();
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ vfunc_get_preferred_width(forHeight) {
|
||||||
|
+ const maxWidth = this._widthCoverage * this._workArea.width;
|
||||||
|
+
|
||||||
|
+ let [labelMinimumWidth, labelNaturalWidth] = super.vfunc_get_preferred_width(forHeight);
|
||||||
|
+
|
||||||
|
+ labelMinimumWidth = Math.min(labelMinimumWidth, maxWidth);
|
||||||
|
+ labelNaturalWidth = Math.min(labelNaturalWidth, maxWidth);
|
||||||
|
+
|
||||||
|
+ return [labelMinimumWidth, labelNaturalWidth];
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ vfunc_get_preferred_height(forWidth) {
|
||||||
|
+ const labelHeightUpperBound = this._heightCoverage * this._workArea.height;
|
||||||
|
+ const numberOfLines = Math.floor(labelHeightUpperBound / this._lineHeight);
|
||||||
|
+ this._numberOfLines = Math.max(numberOfLines, 1);
|
||||||
|
+
|
||||||
|
+ const maxHeight = this._lineHeight * this._numberOfLines;
|
||||||
|
+
|
||||||
|
+ let [labelMinimumHeight, labelNaturalHeight] = super.vfunc_get_preferred_height(forWidth);
|
||||||
|
+
|
||||||
|
+ labelMinimumHeight = Math.min(labelMinimumHeight, maxHeight);
|
||||||
|
+ labelNaturalHeight = Math.min(labelNaturalHeight, maxHeight);
|
||||||
|
+
|
||||||
|
+ return [labelMinimumHeight, labelNaturalHeight];
|
||||||
|
+ }
|
||||||
|
+});
|
||||||
|
+
|
||||||
|
+export const HeadsUpMessage = GObject.registerClass({
|
||||||
|
+}, class HeadsUpMessage extends St.Button {
|
||||||
|
+ constructor(heading, body) {
|
||||||
|
+ super({
|
||||||
|
+ style_class: 'message',
|
||||||
|
+ accessible_role: Atk.Role.NOTIFICATION,
|
||||||
|
+ can_focus: false,
|
||||||
|
+ opacity: 0,
|
||||||
|
+ });
|
||||||
|
+
|
||||||
|
+ Main.layoutManager.addChrome(this, {affectsInputRegion: true});
|
||||||
|
+
|
||||||
|
+ this.add_style_class_name('heads-up-display-message');
|
||||||
|
+
|
||||||
|
+ this.connect('destroy', () => this._onDestroy());
|
||||||
|
+
|
||||||
|
+ Main.layoutManager.panelBox.connectObject('notify::allocation',
|
||||||
|
+ () => this._alignWithPanel());
|
||||||
|
+ this.connect('notify::allocation',
|
||||||
|
+ () => this._alignWithPanel());
|
||||||
|
+
|
||||||
|
+ const contentsBox = new St.BoxLayout({
|
||||||
|
+ style_class: 'heads-up-message-content',
|
||||||
|
+ vertical: true,
|
||||||
|
+ x_align: Clutter.ActorAlign.CENTER,
|
||||||
|
+ });
|
||||||
|
+ this.add_child(contentsBox);
|
||||||
|
+
|
||||||
|
+ this._headingLabel = new St.Label({
|
||||||
|
+ style_class: 'heads-up-message-heading',
|
||||||
|
+ x_expand: true,
|
||||||
|
+ x_align: Clutter.ActorAlign.CENTER,
|
||||||
|
+ });
|
||||||
|
+
|
||||||
|
+ this.setHeading(heading);
|
||||||
|
+ contentsBox.add_child(this._headingLabel);
|
||||||
|
+
|
||||||
|
+ this._bodyLabel = new HeadsUpMessageBodyLabel({
|
||||||
|
+ style_class: 'heads-up-message-body',
|
||||||
|
+ x_expand: true,
|
||||||
|
+ y_expand: true,
|
||||||
|
+ });
|
||||||
|
+ contentsBox.add_child(this._bodyLabel);
|
||||||
|
+
|
||||||
|
+ this.setBody(body);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ vfunc_parent_set() {
|
||||||
|
+ this._alignWithPanel();
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _alignWithPanel() {
|
||||||
|
+ if (this._beforeUpdateId)
|
||||||
|
+ return;
|
||||||
|
+
|
||||||
|
+ this._beforeUpdateId = global.stage.connect('before-update', () => {
|
||||||
|
+ let x = Main.panel.x;
|
||||||
|
+ let y = Main.panel.y + Main.panel.height;
|
||||||
|
+
|
||||||
|
+ x += Main.panel.width / 2;
|
||||||
|
+ x -= this.width / 2;
|
||||||
|
+ x = Math.floor(x);
|
||||||
|
+ this.set_position(x, y);
|
||||||
|
+ this.opacity = 255;
|
||||||
|
+
|
||||||
|
+ global.stage.disconnect(this._beforeUpdateId);
|
||||||
|
+ this._beforeUpdateId = 0;
|
||||||
|
+ });
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ setHeading(text) {
|
||||||
|
+ if (text) {
|
||||||
|
+ const heading = text ? text.replace(/\n/g, ' ') : '';
|
||||||
|
+ this._headingLabel.text = heading;
|
||||||
|
+ this._headingLabel.visible = true;
|
||||||
|
+ } else {
|
||||||
|
+ this._headingLabel.text = text;
|
||||||
|
+ this._headingLabel.visible = false;
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ setBody(text) {
|
||||||
|
+ this._bodyLabel.text = text;
|
||||||
|
+
|
||||||
|
+ if (text)
|
||||||
|
+ this._bodyLabel.visible = true;
|
||||||
|
+ else
|
||||||
|
+ this._bodyLabel.visible = false;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _onDestroy() {
|
||||||
|
+ if (this._beforeUpdateId) {
|
||||||
|
+ global.stage.disconnect(this._beforeUpdateId);
|
||||||
|
+ this._beforeUpdateId = 0;
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+});
|
||||||
|
diff --git a/extensions/heads-up-display/meson.build b/extensions/heads-up-display/meson.build
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..42ce222c
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/extensions/heads-up-display/meson.build
|
||||||
|
@@ -0,0 +1,13 @@
|
||||||
|
+# SPDX-FileCopyrightText: 2021 Ray Strode <rstrode@redhat.com>
|
||||||
|
+#
|
||||||
|
+# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
+
|
||||||
|
+extension_data += configure_file(
|
||||||
|
+ input: metadata_name + '.in',
|
||||||
|
+ output: metadata_name,
|
||||||
|
+ configuration: metadata_conf
|
||||||
|
+)
|
||||||
|
+
|
||||||
|
+extension_data += files('stylesheet.css')
|
||||||
|
+extension_sources += files('headsUpMessage.js', 'prefs.js')
|
||||||
|
+extension_schemas += files(metadata_conf.get('gschemaname') + '.gschema.xml')
|
||||||
|
diff --git a/extensions/heads-up-display/metadata.json.in b/extensions/heads-up-display/metadata.json.in
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..01bcd4df
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/extensions/heads-up-display/metadata.json.in
|
||||||
|
@@ -0,0 +1,12 @@
|
||||||
|
+{
|
||||||
|
+"extension-id": "@extension_id@",
|
||||||
|
+"uuid": "@uuid@",
|
||||||
|
+"settings-schema": "@gschemaname@",
|
||||||
|
+"gettext-domain": "@gettext_domain@",
|
||||||
|
+"name": "Heads-up Display Message",
|
||||||
|
+"description": "Add a message to be displayed on screen always above all windows and chrome.",
|
||||||
|
+"original-authors": [ "rstrode@redhat.com" ],
|
||||||
|
+"shell-version": [ "@shell_current@" ],
|
||||||
|
+"url": "@url@",
|
||||||
|
+"session-modes": [ "gdm", "lock-screen", "unlock-dialog", "user" ]
|
||||||
|
+}
|
||||||
|
diff --git a/extensions/heads-up-display/org.gnome.shell.extensions.heads-up-display.gschema.xml b/extensions/heads-up-display/org.gnome.shell.extensions.heads-up-display.gschema.xml
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..1e2119c8
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/extensions/heads-up-display/org.gnome.shell.extensions.heads-up-display.gschema.xml
|
||||||
|
@@ -0,0 +1,60 @@
|
||||||
|
+<!--
|
||||||
|
+SPDX-FileCopyrightText: 2021 Ray Strode <rstrode@redhat.com>
|
||||||
|
+
|
||||||
|
+SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
+-->
|
||||||
|
+
|
||||||
|
+<schemalist gettext-domain="gnome-shell-extensions">
|
||||||
|
+ <schema id="org.gnome.shell.extensions.heads-up-display"
|
||||||
|
+ path="/org/gnome/shell/extensions/heads-up-display/">
|
||||||
|
+ <key name="idle-timeout" type="u">
|
||||||
|
+ <default>30</default>
|
||||||
|
+ <summary>Idle Timeout</summary>
|
||||||
|
+ <description>
|
||||||
|
+ Number of seconds until message is reshown after user goes idle.
|
||||||
|
+ </description>
|
||||||
|
+ </key>
|
||||||
|
+ <key name="message-heading" type="s">
|
||||||
|
+ <default>""</default>
|
||||||
|
+ <summary>Message to show at top of display</summary>
|
||||||
|
+ <description>
|
||||||
|
+ The top line of the heads up display message.
|
||||||
|
+ </description>
|
||||||
|
+ </key>
|
||||||
|
+ <key name="message-body" type="s">
|
||||||
|
+ <default>""</default>
|
||||||
|
+ <summary>Banner message</summary>
|
||||||
|
+ <description>
|
||||||
|
+ A message to always show at the top of the screen.
|
||||||
|
+ </description>
|
||||||
|
+ </key>
|
||||||
|
+ <key name="show-on-login-screen" type="b">
|
||||||
|
+ <default>true</default>
|
||||||
|
+ <summary>Show on login screen</summary>
|
||||||
|
+ <description>
|
||||||
|
+ Whether or not the message should display on the login screen
|
||||||
|
+ </description>
|
||||||
|
+ </key>
|
||||||
|
+ <key name="show-when-locked" type="b">
|
||||||
|
+ <default>false</default>
|
||||||
|
+ <summary>Show on screen shield</summary>
|
||||||
|
+ <description>
|
||||||
|
+ Whether or not the message should display when the screen is locked
|
||||||
|
+ </description>
|
||||||
|
+ </key>
|
||||||
|
+ <key name="show-when-unlocking" type="b">
|
||||||
|
+ <default>false</default>
|
||||||
|
+ <summary>Show on unlock screen</summary>
|
||||||
|
+ <description>
|
||||||
|
+ Whether or not the message should display on the unlock screen.
|
||||||
|
+ </description>
|
||||||
|
+ </key>
|
||||||
|
+ <key name="show-when-unlocked" type="b">
|
||||||
|
+ <default>false</default>
|
||||||
|
+ <summary>Show in user session</summary>
|
||||||
|
+ <description>
|
||||||
|
+ Whether or not the message should display when the screen is unlocked.
|
||||||
|
+ </description>
|
||||||
|
+ </key>
|
||||||
|
+ </schema>
|
||||||
|
+</schemalist>
|
||||||
|
diff --git a/extensions/heads-up-display/prefs.js b/extensions/heads-up-display/prefs.js
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..304c8813
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/extensions/heads-up-display/prefs.js
|
||||||
|
@@ -0,0 +1,92 @@
|
||||||
|
+// SPDX-FileCopyrightText: 2021 Ray Strode <rstrode@redhat.com>
|
||||||
|
+//
|
||||||
|
+// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
+
|
||||||
|
+import Adw from 'gi://Adw';
|
||||||
|
+import Gio from 'gi://Gio';
|
||||||
|
+import GObject from 'gi://GObject';
|
||||||
|
+import Gtk from 'gi://Gtk';
|
||||||
|
+
|
||||||
|
+import {ExtensionPreferences, gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
|
||||||
|
+
|
||||||
|
+class GeneralGroup extends Adw.PreferencesGroup {
|
||||||
|
+ static {
|
||||||
|
+ GObject.registerClass(this);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ constructor(settings) {
|
||||||
|
+ super();
|
||||||
|
+
|
||||||
|
+ const actionGroup = new Gio.SimpleActionGroup();
|
||||||
|
+ this.insert_action_group('options', actionGroup);
|
||||||
|
+
|
||||||
|
+ actionGroup.add_action(settings.create_action('show-when-locked'));
|
||||||
|
+ actionGroup.add_action(settings.create_action('show-when-unlocking'));
|
||||||
|
+ actionGroup.add_action(settings.create_action('show-when-unlocked'));
|
||||||
|
+
|
||||||
|
+ this.add(new Adw.SwitchRow({
|
||||||
|
+ title: _('Show message when screen is locked'),
|
||||||
|
+ action_name: 'options.show-when-locked',
|
||||||
|
+ }));
|
||||||
|
+ this.add(new Adw.SwitchRow({
|
||||||
|
+ title: _('Show message on unlock screen'),
|
||||||
|
+ action_name: 'options.show-when-unlocking',
|
||||||
|
+ }));
|
||||||
|
+ this.add(new Adw.SwitchRow({
|
||||||
|
+ title: _('Show message when screen is unlocked'),
|
||||||
|
+ action_name: 'options.show-when-unlocked',
|
||||||
|
+ }));
|
||||||
|
+
|
||||||
|
+ const spinRow = new Adw.SpinRow({
|
||||||
|
+ title: _('Seconds after user goes idle before reshowing message'),
|
||||||
|
+ adjustment: new Gtk.Adjustment({
|
||||||
|
+ lower: 0,
|
||||||
|
+ upper: 2147483647,
|
||||||
|
+ step_increment: 1,
|
||||||
|
+ page_increment: 60,
|
||||||
|
+ page_size: 60,
|
||||||
|
+ }),
|
||||||
|
+ });
|
||||||
|
+ settings.bind('idle-timeout',
|
||||||
|
+ spinRow, 'value',
|
||||||
|
+ Gio.SettingsBindFlags.DEFAULT);
|
||||||
|
+ this.add(spinRow);
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+class MessageGroup extends Adw.PreferencesGroup {
|
||||||
|
+ static {
|
||||||
|
+ GObject.registerClass(this);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ constructor(settings) {
|
||||||
|
+ super({
|
||||||
|
+ title: _('Message'),
|
||||||
|
+ });
|
||||||
|
+
|
||||||
|
+ const textView = new Gtk.TextView({
|
||||||
|
+ accepts_tab: false,
|
||||||
|
+ wrap_mode: Gtk.WrapMode.WORD,
|
||||||
|
+ top_margin: 6,
|
||||||
|
+ bottom_margin: 6,
|
||||||
|
+ left_margin: 6,
|
||||||
|
+ right_margin: 6,
|
||||||
|
+ vexpand: true,
|
||||||
|
+ });
|
||||||
|
+ textView.add_css_class('card');
|
||||||
|
+
|
||||||
|
+ settings.bind('message-body',
|
||||||
|
+ textView.get_buffer(), 'text',
|
||||||
|
+ Gio.SettingsBindFlags.DEFAULT);
|
||||||
|
+ this.add(textView);
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+export default class HeadsUpDisplayPrefs extends ExtensionPreferences {
|
||||||
|
+ getPreferencesWidget() {
|
||||||
|
+ const page = new Adw.PreferencesPage();
|
||||||
|
+ page.add(new GeneralGroup(this.getSettings()));
|
||||||
|
+ page.add(new MessageGroup(this.getSettings()));
|
||||||
|
+ return page;
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
diff --git a/extensions/heads-up-display/stylesheet.css b/extensions/heads-up-display/stylesheet.css
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..a1a34e3f
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/extensions/heads-up-display/stylesheet.css
|
||||||
|
@@ -0,0 +1,38 @@
|
||||||
|
+/*
|
||||||
|
+ * SPDX-FileCopyrightText: 2021 Ray Strode <rstrode@redhat.com>
|
||||||
|
+ *
|
||||||
|
+ * SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
+ */
|
||||||
|
+
|
||||||
|
+.heads-up-display-message {
|
||||||
|
+ background-color: rgba(0.24, 0.24, 0.24, 0.80);
|
||||||
|
+ border: 1px solid black;
|
||||||
|
+ border-radius: 6px;
|
||||||
|
+ color: #eeeeec;
|
||||||
|
+ font-size: 11pt;
|
||||||
|
+ margin-top: 0.5em;
|
||||||
|
+ margin-bottom: 0.5em;
|
||||||
|
+ padding: 0.9em;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+.heads-up-display-message:insensitive {
|
||||||
|
+ background-color: rgba(0.24, 0.24, 0.24, 0.33);
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+.heads-up-display-message:hover {
|
||||||
|
+ background-color: rgba(0.24, 0.24, 0.24, 0.2);
|
||||||
|
+ border: 1px solid rgba(0.0, 0.0, 0.0, 0.5);
|
||||||
|
+ color: #4d4d4d;
|
||||||
|
+ transition-duration: 250ms;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+.heads-up-message-heading {
|
||||||
|
+ height: 1.75em;
|
||||||
|
+ font-size: 1.25em;
|
||||||
|
+ font-weight: bold;
|
||||||
|
+ text-align: center;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+.heads-up-message-body {
|
||||||
|
+ text-align: center;
|
||||||
|
+}
|
||||||
|
diff --git a/meson.build b/meson.build
|
||||||
|
index 63bd9ee0..82269ff5 100644
|
||||||
|
--- a/meson.build
|
||||||
|
+++ b/meson.build
|
||||||
|
@@ -40,6 +40,7 @@ classic_extensions = [
|
||||||
|
default_extensions = classic_extensions
|
||||||
|
default_extensions += [
|
||||||
|
'drive-menu',
|
||||||
|
+ 'heads-up-display',
|
||||||
|
'light-style',
|
||||||
|
'screenshot-window-sizer',
|
||||||
|
'system-monitor',
|
||||||
|
diff --git a/po/POTFILES.in b/po/POTFILES.in
|
||||||
|
index 447465a1..b7cb8a7c 100644
|
||||||
|
--- a/po/POTFILES.in
|
||||||
|
+++ b/po/POTFILES.in
|
||||||
|
@@ -6,6 +6,7 @@ extensions/auto-move-windows/extension.js
|
||||||
|
extensions/auto-move-windows/org.gnome.shell.extensions.auto-move-windows.gschema.xml
|
||||||
|
extensions/auto-move-windows/prefs.js
|
||||||
|
extensions/drive-menu/extension.js
|
||||||
|
+extensions/heads-up-display/prefs.js
|
||||||
|
extensions/native-window-placement/extension.js
|
||||||
|
extensions/native-window-placement/org.gnome.shell.extensions.native-window-placement.gschema.xml
|
||||||
|
extensions/places-menu/extension.js
|
||||||
|
--
|
||||||
|
2.45.2
|
||||||
|
|
@ -0,0 +1,719 @@
|
|||||||
|
From b146a94c18e9e9ddbc7f29e8d885768d19fd7d49 Mon Sep 17 00:00:00 2001
|
||||||
|
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
|
||||||
|
Date: Thu, 12 Jan 2023 19:43:52 +0100
|
||||||
|
Subject: [PATCH 5/5] Add custom-menu extension
|
||||||
|
|
||||||
|
---
|
||||||
|
extensions/custom-menu/config.js | 445 ++++++++++++++++++++++++
|
||||||
|
extensions/custom-menu/extension.js | 201 +++++++++++
|
||||||
|
extensions/custom-menu/meson.build | 7 +
|
||||||
|
extensions/custom-menu/metadata.json.in | 10 +
|
||||||
|
meson.build | 1 +
|
||||||
|
5 files changed, 664 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
|
||||||
|
|
||||||
|
diff --git a/extensions/custom-menu/config.js b/extensions/custom-menu/config.js
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..d08e3201
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/extensions/custom-menu/config.js
|
||||||
|
@@ -0,0 +1,445 @@
|
||||||
|
+import Gio from 'gi://Gio';
|
||||||
|
+import GLib from 'gi://GLib';
|
||||||
|
+import Json from 'gi://Json';
|
||||||
|
+
|
||||||
|
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||||
|
+import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
|
||||||
|
+
|
||||||
|
+import {getLogger} from './extension.js';
|
||||||
|
+
|
||||||
|
+class Entry {
|
||||||
|
+ constructor(prop) {
|
||||||
|
+ this.type = prop.type;
|
||||||
|
+ this.title = prop.title || "";
|
||||||
|
+ this.__vars = prop.__vars || [];
|
||||||
|
+ this.updateEnv(prop);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ setTitle(text) {
|
||||||
|
+ this.item.label.get_clutter_text().set_text(text);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ updateEnv(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() {
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _try_destroy() {
|
||||||
|
+ try {
|
||||||
|
+ if (this.item && this.item.destroy) {
|
||||||
|
+ this.item.destroy();
|
||||||
|
+ }
|
||||||
|
+ } catch(e) { /* Ignore all errors during destory*/ }
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+class DerivedEntry {
|
||||||
|
+ constructor(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(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;
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+let __pipeOpenQueue = [];
|
||||||
|
+
|
||||||
|
+/* callback: function (stdout, stderr, exit_status) { } */
|
||||||
|
+function pipeOpen(cmdline, env, callback) {
|
||||||
|
+ if (cmdline === undefined || callback === undefined) {
|
||||||
|
+ return false;
|
||||||
|
+ }
|
||||||
|
+ realPipeOpen(cmdline, env, callback);
|
||||||
|
+ 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) {
|
||||||
|
+ const decoder = new TextDecoder();
|
||||||
|
+ stdout_content = decoder.decode(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 = decoder.decode(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);
|
||||||
|
+ 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);
|
||||||
|
+ }
|
||||||
|
+ });
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+// Detect menu toggle on startup
|
||||||
|
+function _toggleDetect(command, env, object) {
|
||||||
|
+ pipeOpen(command, env, function(stdout, stderr, exit_status) {
|
||||||
|
+ if (exit_status == 0) {
|
||||||
|
+ object.item.setToggleState(true);
|
||||||
|
+ }
|
||||||
|
+ });
|
||||||
|
+} /**/
|
||||||
|
+
|
||||||
|
+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 = { };
|
||||||
|
+
|
||||||
|
+class TogglerEntry extends Entry {
|
||||||
|
+ constructor(prop) {
|
||||||
|
+ super(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() {
|
||||||
|
+ 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', this._onManuallyToggled.bind(this));
|
||||||
|
+ this._loadState();
|
||||||
|
+ _toggleDetect(this.detector, this.__env, this);
|
||||||
|
+ return this.item;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _onManuallyToggled(_, state) {
|
||||||
|
+ // when switched on again, this flag will get cleared.
|
||||||
|
+ this._manually_switched_off = !state;
|
||||||
|
+ this._storeState(state);
|
||||||
|
+ this._onToggled(state);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _onToggled(state) {
|
||||||
|
+ if (state) {
|
||||||
|
+ _generalSpawn(this.command_on, this.__env, this.title);
|
||||||
|
+ } else {
|
||||||
|
+ _generalSpawn(this.command_off, this.__env, this.title);
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _detect(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*$/)));
|
||||||
|
+ });
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ // compare the new state with cached state notify when state is different
|
||||||
|
+ compareState(new_state) {
|
||||||
|
+ 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(state) {
|
||||||
|
+ let hash = JSON.stringify({ env: this.__env, detector: this.detector });
|
||||||
|
+ _toggler_state_cache[hash] = state;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _loadState() {
|
||||||
|
+ 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() {
|
||||||
|
+ this._detect(state => {
|
||||||
|
+ this.compareState(state);
|
||||||
|
+ this._storeState(state);
|
||||||
|
+ this._loadState();
|
||||||
|
+ 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() {
|
||||||
|
+ this.item.toggle();
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+class LauncherEntry extends Entry {
|
||||||
|
+ constructor(prop) {
|
||||||
|
+ super(prop);
|
||||||
|
+ this.command = prop.command || "";
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ createItem() {
|
||||||
|
+ this._try_destroy();
|
||||||
|
+ this.item = new PopupMenu.PopupMenuItem(this.title);
|
||||||
|
+ this.item.label.get_clutter_text().set_use_markup(true);
|
||||||
|
+ this.item.connect('activate', this._onClicked.bind(this));
|
||||||
|
+ return this.item;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _onClicked(_) {
|
||||||
|
+ _generalSpawn(this.command, this.__env, this.title);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ perform() {
|
||||||
|
+ this.item.emit('activate');
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+class SubMenuEntry extends Entry {
|
||||||
|
+ constructor(prop) {
|
||||||
|
+ super(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() {
|
||||||
|
+ 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() {
|
||||||
|
+ for (let i in this.entries) {
|
||||||
|
+ let entry = this.entries[i];
|
||||||
|
+ entry.pulse();
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+class SeparatorEntry extends Entry {
|
||||||
|
+ createItem() {
|
||||||
|
+ 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);
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+export class Loader {
|
||||||
|
+ constructor(filename) {
|
||||||
|
+ if (filename) {
|
||||||
|
+ this.loadConfig(filename);
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ loadConfig(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(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..9edbc548
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/extensions/custom-menu/extension.js
|
||||||
|
@@ -0,0 +1,201 @@
|
||||||
|
+/* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
+ *
|
||||||
|
+ * SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
+ */
|
||||||
|
+
|
||||||
|
+/**
|
||||||
|
+ * @author Ben
|
||||||
|
+ * @see https://github.com/andreabenini/gnome-plugin.custom-menu-panel
|
||||||
|
+ */
|
||||||
|
+
|
||||||
|
+const CONFIGURATION_FILE = '/.entries.json';
|
||||||
|
+
|
||||||
|
+import Gio from 'gi://Gio';
|
||||||
|
+import GLib from 'gi://GLib';
|
||||||
|
+import GObject from 'gi://GObject';
|
||||||
|
+import St from 'gi://St';
|
||||||
|
+
|
||||||
|
+import {Extension, gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js';
|
||||||
|
+
|
||||||
|
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||||
|
+import * as BackgroundMenu from 'resource:///org/gnome/shell/ui/backgroundMenu.js';
|
||||||
|
+import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
|
||||||
|
+import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
|
||||||
|
+
|
||||||
|
+import * as Config from './config.js';
|
||||||
|
+
|
||||||
|
+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));
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+); /**/
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+class Logger {
|
||||||
|
+ constructor(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() {
|
||||||
|
+ this.log = function(_) { };
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _initGnomeLog() {
|
||||||
|
+ this.log = function(s) {
|
||||||
|
+ global.log("custom-menu-panel> " + s);
|
||||||
|
+ };
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ _initFileLog() {
|
||||||
|
+ 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(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;
|
||||||
|
+export function getLogger() {
|
||||||
|
+ if (logger === null) {
|
||||||
|
+ logger = new Logger("gnome-shell");
|
||||||
|
+ }
|
||||||
|
+ return logger;
|
||||||
|
+} /**/
|
||||||
|
+
|
||||||
|
+export default class CustomMenuExtension extends Extension {
|
||||||
|
+ enable() {
|
||||||
|
+ BackgroundMenu.BackgroundMenu = CustomBackgroundMenu;
|
||||||
|
+ Main.layoutManager._updateBackgrounds();
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ disable() {
|
||||||
|
+ BackgroundMenu.BackgroundMenu = OriginalBackgroundMenu;
|
||||||
|
+ Main.layoutManager._updateBackgrounds();
|
||||||
|
+ }
|
||||||
|
+} /**/
|
||||||
|
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/meson.build b/meson.build
|
||||||
|
index 82269ff5..dce1731c 100644
|
||||||
|
--- a/meson.build
|
||||||
|
+++ b/meson.build
|
||||||
|
@@ -53,6 +53,7 @@ all_extensions = default_extensions
|
||||||
|
all_extensions += [
|
||||||
|
'auto-move-windows',
|
||||||
|
'classification-banner',
|
||||||
|
+ 'custom-menu',
|
||||||
|
'gesture-inhibitor',
|
||||||
|
'native-window-placement',
|
||||||
|
'user-theme'
|
||||||
|
--
|
||||||
|
2.45.2
|
||||||
|
|
@ -0,0 +1,72 @@
|
|||||||
|
From d0f2273765ab61e55c5cf10e7283a545fcafa947 Mon Sep 17 00:00:00 2001
|
||||||
|
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
|
||||||
|
Date: Thu, 22 Aug 2024 13:24:20 +0200
|
||||||
|
Subject: [PATCH] Add stub desktop-icons extension
|
||||||
|
|
||||||
|
---
|
||||||
|
extensions/desktop-icons/extension.js | 5 +++++
|
||||||
|
extensions/desktop-icons/meson.build | 9 +++++++++
|
||||||
|
extensions/desktop-icons/metadata.json.in | 10 ++++++++++
|
||||||
|
meson.build | 1 +
|
||||||
|
4 files changed, 25 insertions(+)
|
||||||
|
create mode 100644 extensions/desktop-icons/extension.js
|
||||||
|
create mode 100644 extensions/desktop-icons/meson.build
|
||||||
|
create mode 100644 extensions/desktop-icons/metadata.json.in
|
||||||
|
|
||||||
|
diff --git a/extensions/desktop-icons/extension.js b/extensions/desktop-icons/extension.js
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..bbc96ef2
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/extensions/desktop-icons/extension.js
|
||||||
|
@@ -0,0 +1,5 @@
|
||||||
|
+// SPDX-FileCopyrightText: 2024 Florian Müllner <fmuellner@gnome.org>
|
||||||
|
+//
|
||||||
|
+// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
+
|
||||||
|
+export {Extension as default} from 'resource:///org/gnome/shell/extensions/extension.js';
|
||||||
|
diff --git a/extensions/desktop-icons/meson.build b/extensions/desktop-icons/meson.build
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..7b28a2ef
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/extensions/desktop-icons/meson.build
|
||||||
|
@@ -0,0 +1,9 @@
|
||||||
|
+# SPDX-FileCopyrightText: 2017 Florian Müllner <fmuellner@gnome.org>
|
||||||
|
+#
|
||||||
|
+# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
+
|
||||||
|
+extension_data += configure_file(
|
||||||
|
+ input: metadata_name + '.in',
|
||||||
|
+ output: metadata_name,
|
||||||
|
+ configuration: metadata_conf
|
||||||
|
+)
|
||||||
|
diff --git a/extensions/desktop-icons/metadata.json.in b/extensions/desktop-icons/metadata.json.in
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..78a55abb
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/extensions/desktop-icons/metadata.json.in
|
||||||
|
@@ -0,0 +1,10 @@
|
||||||
|
+{
|
||||||
|
+"extension-id": "@extension_id@",
|
||||||
|
+"uuid": "@uuid@",
|
||||||
|
+"settings-schema": "@gschemaname@",
|
||||||
|
+"gettext-domain": "@gettext_domain@",
|
||||||
|
+"name": "Desktop Icons",
|
||||||
|
+"description": "Show icons on the desktop",
|
||||||
|
+"shell-version": [ "@shell_current@" ],
|
||||||
|
+"url": "@url@"
|
||||||
|
+}
|
||||||
|
diff --git a/meson.build b/meson.build
|
||||||
|
index b915b68c..63a7432e 100644
|
||||||
|
--- a/meson.build
|
||||||
|
+++ b/meson.build
|
||||||
|
@@ -40,6 +40,7 @@ classic_extensions = [
|
||||||
|
|
||||||
|
default_extensions = classic_extensions
|
||||||
|
default_extensions += [
|
||||||
|
+ 'desktop-icons',
|
||||||
|
'drive-menu',
|
||||||
|
'heads-up-display',
|
||||||
|
'light-style',
|
||||||
|
--
|
||||||
|
2.45.2
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue