commit bf244fddf37aa2359740995b3e53050d49775949 Author: Marcin Jakubowski Date: Fri Dec 3 13:24:13 2021 +0100 initial version diff --git a/extension.js b/extension.js new file mode 100644 index 0000000..7672142 --- /dev/null +++ b/extension.js @@ -0,0 +1,208 @@ +/* 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 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +const { Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Prefs = Me.imports.prefs; +const Main = imports.ui.main; +const MessageTray = imports.ui.messageTray.MessageTray; +const Lang = imports.lang; + +const BannerBin = Main.messageTray._bannerBin; + +const { NOTIFICATION_TIMEOUT, HIDE_TIMEOUT, LONGER_HIDE_TIMEOUT, IDLE_TIME, State, Urgency } = imports.ui.messageTray; + +let ANIMATION_TIME = 200; +let ANIMATION_DIRECTION = 2; +let ANCHOR_VERTICAL = 0; +let ANCHOR_HORIZONTAL = 2; + +function patcher(obj, method, original, patch) { + const body = eval(`${obj}.prototype.${method}.toString()`); + const newBody = body.replace(original, patch).replace(method + "(", "function(") + eval(`${obj}.prototype.${method} = ${newBody}`); +} + + +const originalShow = MessageTray.prototype._showNotification; +const originalHide = MessageTray.prototype._hideNotification; +const originalUpdateShowing = MessageTray.prototype._updateShowingNotification; + +function calcTarget(self) { + let x = 0, y = 0; + switch (ANCHOR_HORIZONTAL) { + case 0: // left + x = 0; + break; + case 1: // right + x = global.screen_width - self._banner.width; + break; + case 2: // center + x = (global.screen_width - self._banner.width) / 2.0; + break; + } + switch (ANCHOR_VERTICAL) { + case 0: // top + y = 0; + break; + case 1: // bottom + y = global.screen_height - self._banner.height; + break; + case 2: // center + y = (global.screen_height - self._banner.height) / 2.0; + break; + } + + return { x, y } +} + +function calcHide(self) { + let { x, y } = calcTarget(self) + switch (ANIMATION_DIRECTION) { + case 0: // from left + x = -self._banner.width; + break; + case 1: // from right + x = global.screen_width; + break; + case 2: // from top + y = -self._banner.height + break; + case 3: // from bottom + y = global.screen_height + break; + } + + return { x, y } +} + +function calcStart(self) { + const { x, y } = calcHide(self); + self._bannerBin.x = x; + self._bannerBin.y = y; + + + // if banner is not expanded and anchored to the bottom + // it won't have enough vertical space to expand + // in such case, move it up enough to fit the expanded banner + if (!self._banner.expanded && ANCHOR_VERTICAL == 1) { + const unexpandedHeight = self._banner.height + // expand without animation to measure height + self._banner.expand(false); + const expandedDifference = self._banner.height - unexpandedHeight; + // go back to unexpanded + self._banner.unexpand(false); + + // move up when needed + self._banner.connect('expanded', () => { + self._bannerBin.ease({ + y: self._bannerBin.y - expandedDifference, + duration: ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + }); + } +} + +// each of the methods has hardcoded values for showing banners +// at the top. instead of rewriting whole functions, just patch the +// relevant lines +const patches = [ + { + "obj": "MessageTray", "method": "_updateShowingNotification", + "original": 'y: 0', + "patch": '...calcTarget(this)' + }, + { + "obj": "MessageTray", "method": "_showNotification", + "original": 'this._bannerBin.y = -this._banner.height', + "patch": 'calcStart(this)' + }, + { + "obj": "MessageTray", "method": "_hideNotification", + "original": 'y: -this._bannerBin.height', + "patch": '...calcHide(this)' + } +]; + +class Extension { + constructor() { + this._previous_y_align = BannerBin.get_y_align(); + this._previous_x_align = BannerBin.get_x_align(); + this._loadSettings(); + } + + _loadSettings() { + this._settings = Prefs.SettingsSchema; + this._settingsChangedId = this._settings.connect('changed', + Lang.bind(this, this._onSettingsChange)); + + this._fetchSettings(); + } + + _fetchSettings() { + ANCHOR_VERTICAL = this._settings.get_int(Prefs.Fields.ANCHOR_VERTICAL); + ANCHOR_HORIZONTAL = this._settings.get_int(Prefs.Fields.ANCHOR_HORIZONTAL); + ANIMATION_DIRECTION = this._settings.get_int(Prefs.Fields.ANIMATION_DIRECTION); + ANIMATION_TIME = this._settings.get_int(Prefs.Fields.ANIMATION_TIME); + } + + _onSettingsChange() { + this._fetchSettings(); + this.enable(); + } + + enable() { + // generally alignment can be controller with START/CENTER/END + // but CENTER and END are problematic to implement animations with + // (especially x -> END and animations from left/right) + // all positions will then be calculated in relation to the top-left + // corner (START/START). + let x_align = Clutter.ActorAlign.START; + let y_align = Clutter.ActorAlign.START; + BannerBin.set_x_align(x_align); + BannerBin.set_y_align(y_align); + this.restore() + for (const { obj, method, original, patch } of patches) { + patcher(obj, method, original, patch) + } + + } + + disable() { + BannerBin.set_x_align(this._previous_x_align); + BannerBin.set_y_align(this._previous_y_align); + BannerBin.x = 0 + BannerBin.y = 0 + this.restore() + + } + + restore() { + MessageTray.prototype._hideNotification = originalHide; + MessageTray.prototype._showNotification = originalShow; + MessageTray.prototype._updateShowingNotification = originalUpdateShowing; + } +} + +function init() { + return new Extension(); +} + diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..a8dd574 --- /dev/null +++ b/metadata.json @@ -0,0 +1,9 @@ +{ + "name": "Notification Banner Reloaded", + "description": "Configure notification banner position and animation to your liking", + "uuid": "notification-banner-reloaded@mjakubowski.github.com", + "shell-version": [ + "41", + "40" + ] +} diff --git a/prefs.js b/prefs.js new file mode 100644 index 0000000..b853066 --- /dev/null +++ b/prefs.js @@ -0,0 +1,170 @@ +/* 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 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +/* + Swaths of pref related code borrowed from Clipboard Indicator, an amazing extension + https://github.com/Tudmotu/gnome-shell-extension-clipboard-indicator + https://extensions.gnome.org/extension/779/clipboard-indicator/ +*/ + + + +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; +const Gio = imports.gi.Gio; +const Lang = imports.lang; +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); + +const Gettext = imports.gettext; +const _ = Gettext.domain('notification-banner-reloaded').gettext; + +var Fields = { + ANCHOR_VERTICAL : 'anchor-vertical', + ANCHOR_HORIZONTAL : 'anchor-horizontal', + ANIMATION_TIME : 'animation-time', + ANIMATION_DIRECTION : 'animation-direction', +}; + +const SCHEMA_NAME = 'org.gnome.shell.extensions.notification-banner-reloaded'; + +const getSchema = function () { + let schemaDir = Me.dir.get_child('schemas').get_path(); + let schemaSource = Gio.SettingsSchemaSource.new_from_directory(schemaDir, Gio.SettingsSchemaSource.get_default(), false); + let schema = schemaSource.lookup(SCHEMA_NAME, false); + + return new Gio.Settings({ settings_schema: schema }); +}; + +var SettingsSchema = getSchema(); + +function init() { + let localeDir = Me.dir.get_child('locale'); + if (localeDir.query_exists(null)) + Gettext.bindtextdomain('notification-banner-reloaded', localeDir.get_path()); +} + +const App = new Lang.Class({ + Name: 'NotificationBannerReloaded.App', + _init: function() { + this.main = new Gtk.Grid({ + margin_top: 10, + margin_bottom: 10, + margin_start: 10, + margin_end: 10, + row_spacing: 12, + column_spacing: 18, + column_homogeneous: false, + row_homogeneous: false + }); + + this.animationTime = new Gtk.SpinButton({ + adjustment: new Gtk.Adjustment({ + lower: 100, + upper: 5000, + step_increment: 100 + }) + }); + this.anchorHorizontal = new Gtk.ComboBox({ + model: this._create_options([ _('Left'), _('Right'), _('Center') ]) + }); + this.anchorVertical = new Gtk.ComboBox({ + model: this._create_options([ _('Top'), _('Bottom'), _('Center') ]) + }); + this.animationDirection = new Gtk.ComboBox({ + model: this._create_options([ _('Slide from Left'), _('Slide from Right'), _('Slide from Top'), _('Slide from Bottom')]) + }); + + let rendererText = new Gtk.CellRendererText(); + for (widget of [this.anchorHorizontal, this.anchorVertical, this.animationDirection]) { + widget.pack_start(rendererText, false); + widget.add_attribute(rendererText, "text", 0); + } + + let anchorHorizontalLabel = new Gtk.Label({ + label: _("Horizontal Position"), + hexpand: true, + halign: Gtk.Align.START + }); + let anchorVerticalLabel = new Gtk.Label({ + label: _("Vertical Position"), + hexpand: true, + halign: Gtk.Align.START + }); + let animationDirectionLabel = new Gtk.Label({ + label: _("Animation Direction"), + hexpand: true, + halign: Gtk.Align.START + }); + let animationTimeLabel = new Gtk.Label({ + label: _("Animation Time"), + hexpand: true, + halign: Gtk.Align.START + }); + + const addRow = ((main) => { + let row = 0; + return (label, input) => { + let inputWidget = input; + + if (input instanceof Gtk.Switch) { + inputWidget = new Gtk.Box({orientation: Gtk.Orientation.HORIZONTAL,}); + inputWidget.append(input); + } + + if (label) { + main.attach(label, 0, row, 1, 1); + main.attach(inputWidget, 1, row, 1, 1); + } + else { + main.attach(inputWidget, 0, row, 2, 1); + } + + row++; + }; + })(this.main); + + addRow(anchorHorizontalLabel, this.anchorHorizontal); + addRow(anchorVerticalLabel, this.anchorVertical); + addRow(animationDirectionLabel, this.animationDirection); + addRow(animationTimeLabel, this.animationTime); + + SettingsSchema.bind(Fields.ANCHOR_HORIZONTAL, this.anchorHorizontal, 'active', Gio.SettingsBindFlags.DEFAULT); + SettingsSchema.bind(Fields.ANCHOR_VERTICAL, this.anchorVertical, 'active', Gio.SettingsBindFlags.DEFAULT); + SettingsSchema.bind(Fields.ANIMATION_DIRECTION, this.animationDirection, 'active', Gio.SettingsBindFlags.DEFAULT); + SettingsSchema.bind(Fields.ANIMATION_TIME, this.animationTime, 'value', Gio.SettingsBindFlags.DEFAULT); + }, + + _create_options : function(opts){ + let options = opts.map(function (v) { return { name: v }}); + let liststore = new Gtk.ListStore(); + liststore.set_column_types([GObject.TYPE_STRING]) + for (let i = 0; i < options.length; i++ ) { + let option = options[i]; + let iter = liststore.append(); + liststore.set (iter, [0], [option.name]); + } + return liststore; + } +}); + +function buildPrefsWidget(){ + let widget = new App(); + return widget.main; +} + diff --git a/schemas/gschemas.compiled b/schemas/gschemas.compiled new file mode 100644 index 0000000..e023309 Binary files /dev/null and b/schemas/gschemas.compiled differ diff --git a/schemas/org.gnome.shell.extensions.notification-banner-reloaded.gschema.xml b/schemas/org.gnome.shell.extensions.notification-banner-reloaded.gschema.xml new file mode 100644 index 0000000..ced7e4d --- /dev/null +++ b/schemas/org.gnome.shell.extensions.notification-banner-reloaded.gschema.xml @@ -0,0 +1,33 @@ + + + + + 200 + Duration of the show/hide animation + Duration of the show/hide animation, in milliseconds + + + + + 0 + Vertical position of the banner + + + + + + 2 + Horizontal position of the banner + + + + + + 2 + Animation direction + + + + +