From d5dd07519ab1fe58717b538c49b3b94c840af67e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Thu, 2 Dec 2021 19:39:50 +0100 Subject: [PATCH 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 +// 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 @@ + + + + true + Show a banner at the top + + + true + Show a banner at the bottom + + + "UNCLASSIFIED" + classification message + + + "#fff" + text color + + + "rgba(0,122,51,0.75)" + background color + + + false + Include system info in top banner + + + 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