gnome-shell-extensions/extra-extensions-0003-Add-classification-banner.patch

689 lines
22 KiB
Diff
Raw Normal View History

From d5dd07519ab1fe58717b538c49b3b94c840af67e 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 3/5] Add classification-banner
---
extensions/classification-banner/adwShim.js | 202 ++++++++++++++++++
extensions/classification-banner/extension.js | 162 ++++++++++++++
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 +
8 files changed, 609 insertions(+)
create mode 100644 extensions/classification-banner/adwShim.js
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/adwShim.js b/extensions/classification-banner/adwShim.js
new file mode 100644
index 00000000..46a8afca
--- /dev/null
+++ b/extensions/classification-banner/adwShim.js
@@ -0,0 +1,202 @@
+/* exported init PreferencesPage PreferencesGroup ActionRow ComboRow */
+const { Gio, GObject, Gtk } = imports.gi;
+
+function init() {
+}
+
+var PreferencesGroup = GObject.registerClass(
+class PreferencesGroup extends Gtk.Widget {
+ _init(params) {
+ super._init({
+ ...params,
+ layout_manager: new Gtk.BinLayout(),
+ });
+
+ this._listBox = new Gtk.ListBox({
+ css_classes: ['rich-list'],
+ show_separators: true,
+ selection_mode: Gtk.SelectionMode.NONE,
+ });
+
+ const frame = new Gtk.Frame({ child: this._listBox });
+ frame.set_parent(this);
+ }
+
+ add(child) {
+ this._listBox.append(child);
+ }
+});
+
+var PreferencesPage = GObject.registerClass(
+class PreferencesPage extends Gtk.Widget {
+ _init(params) {
+ super._init({
+ ...params,
+ layout_manager: new Gtk.BinLayout(),
+ });
+
+ const scrolledWindow = new Gtk.ScrolledWindow({
+ hscrollbar_policy: Gtk.PolicyType.NEVER,
+ });
+ scrolledWindow.set_parent(this);
+
+ this._box = new Gtk.Box({
+ orientation: Gtk.Orientation.VERTICAL,
+ halign: Gtk.Align.CENTER,
+ spacing: 24,
+ margin_top: 24,
+ margin_bottom: 24,
+ margin_start: 12,
+ margin_end: 12,
+ });
+ scrolledWindow.set_child(this._box);
+
+ const provider = new Gtk.CssProvider();
+ provider.load_from_data('* { min-width: 500px; }');
+ this._box.get_style_context().add_provider(provider,
+ Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
+ }
+
+ add(child) {
+ this._box.append(child);
+ }
+});
+
+var ActionRow = GObject.registerClass({
+ Properties: {
+ 'activatable-widget': GObject.ParamSpec.object(
+ 'activatable-widget', 'activatable-widget', 'activatable-widget',
+ GObject.ParamFlags.READWRITE,
+ Gtk.Widget),
+ 'title': GObject.ParamSpec.string(
+ 'title', 'title', 'title',
+ GObject.ParamFlags.READWRITE,
+ null),
+ },
+}, class ActionRow extends Gtk.ListBoxRow {
+ _init(params) {
+ super._init(params);
+
+ const box = new Gtk.Box({
+ spacing: 12,
+ });
+ this.set_child(box);
+
+ this._prefixes = new Gtk.Box({
+ spacing: 12,
+ visible: false,
+ });
+ box.append(this._prefixes);
+
+ this._title = new Gtk.Label({
+ css_classes: ['title'],
+ hexpand: true,
+ xalign: 0,
+ });
+ box.append(this._title);
+
+ this._suffixes = new Gtk.Box({
+ spacing: 12,
+ visible: false,
+ });
+ box.append(this._suffixes);
+
+ this.bind_property('title',
+ this._title, 'label',
+ GObject.BindingFlags.SYNC_CREATE);
+
+ this.connect('notify::parent', () => {
+ const parent = this.get_parent();
+ parent?.connect('row-activated', (list, row) => {
+ if (row === this)
+ this.activate();
+ });
+ });
+ }
+
+ vfunc_activate() {
+ this.activatable_widget?.mnemonic_activate(false);
+ }
+
+ activate() {
+ this.vfunc_activate();
+ }
+
+ add_prefix(child) {
+ this._prefixes.append(child);
+ this._prefixes.show();
+ }
+
+ add_suffix(child) {
+ this._suffixes.append(child);
+ this._suffixes.show();
+ }
+});
+
+var ComboRow = GObject.registerClass({
+ Properties: {
+ 'selected-item': GObject.ParamSpec.object(
+ 'selected-item', 'selected-item', 'selected-item',
+ GObject.ParamFlags.READABLE,
+ GObject.Object),
+ 'model': GObject.ParamSpec.object(
+ 'model', 'model', 'model',
+ GObject.ParamFlags.READWRITE,
+ Gio.ListModel),
+ 'list-factory': GObject.ParamSpec.object(
+ 'list-factory', 'list-factory', 'list-factory',
+ GObject.ParamFlags.READWRITE,
+ Gtk.ListItemFactory),
+ 'expression': Gtk.param_spec_expression(
+ 'expression', 'expression', 'expression',
+ GObject.ParamFlags.READWRITE),
+ },
+}, class ComboRow extends ActionRow {
+ _init(params) {
+ super._init({
+ ...params,
+ activatable: true,
+ });
+
+ const box = new Gtk.Box({
+ valign: Gtk.Align.CENTER,
+ });
+ box.append(new Gtk.Image({
+ icon_name: 'pan-down-symbolic',
+ }));
+ this.add_suffix(box);
+
+ this._popover = new Gtk.Popover();
+ this._popover.set_parent(box);
+
+ this._selection = new Gtk.SingleSelection();
+ this._selected = -1;
+
+ this._listView = new Gtk.ListView({
+ model: this._selection,
+ single_click_activate: true,
+ });
+ this._popover.set_child(this._listView);
+
+ this._listView.connect('activate', (view, pos) => {
+ this._selected = pos;
+ this.notify('selected-item');
+ this._popover.popdown();
+ });
+
+ this.bind_property('model',
+ this._selection, 'model',
+ GObject.BindingFlags.SYNC_CREATE);
+ this.bind_property('list-factory',
+ this._listView, 'factory',
+ GObject.BindingFlags.SYNC_CREATE);
+ }
+
+ get selected_item() {
+ return this._selection.selected_item;
+ }
+
+ vfunc_activate() {
+ this._popover.popup();
+ }
+});
diff --git a/extensions/classification-banner/extension.js b/extensions/classification-banner/extension.js
new file mode 100644
index 00000000..e872d57d
--- /dev/null
+++ b/extensions/classification-banner/extension.js
@@ -0,0 +1,162 @@
+// SPDX-FileCopyrightText: 2021 Florian Müllner <fmuellner@gnome.org>
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+import Clutter from 'gi://Clutter';
+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] = Clutter.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