From 7c111b9a3857e1b92096d6f17129bd8571aade30 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Fri, 28 Jan 2022 00:27:35 +0800 Subject: [PATCH 01/49] Close windows according to rules --- closeSession.js | 40 +++++++++++++++++- schemas/gschemas.compiled | Bin 344 -> 460 bytes ...another-window-session-manager.gschema.xml | 7 +++ ui/popupMenuButtonItems.js | 8 ++++ utils/prefsUtils.js | 4 ++ 5 files changed, 57 insertions(+), 2 deletions(-) diff --git a/closeSession.js b/closeSession.js index 24129b2..d750784 100644 --- a/closeSession.js +++ b/closeSession.js @@ -2,13 +2,21 @@ const { Shell } = imports.gi; +const Main = imports.ui.main; + +const Util = imports.misc.util; + const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); const Log = Me.imports.utils.log; +const PrefsUtils = Me.imports.utils.prefsUtils; + +var enable_close_by_rules = true; var CloseSession = class { constructor() { this._log = new Log.Log(); + this._prefsUtils = new PrefsUtils.PrefsUtils(); this._skip_app_with_multiple_windows = true; this._defaultAppSystem = Shell.AppSystem.get_default(); @@ -28,14 +36,42 @@ var CloseSession = class { let running_apps = this._defaultAppSystem.get_running(); for (const app of running_apps) { - const app_name = app.get_name(); + if (this._tryCloseByRules(app)) { + continue; + } + if (this._skip_multiple_windows(app)) { this._log.debug(`Skipping ${app.get_name()} because it has more than one windows`); continue; } - this._log.debug(`Closing ${app_name}`); + this._log.debug(`Closing ${app.get_name()}`); app.request_quit(); } + + } + + _tryCloseByRules(app) { + if (!enable_close_by_rules) { + return false; + } + + const _rulesShortCutsString = this._prefsUtils.getSettingString('close-windows-rules-shortcut'); + const _rulesShortCuts = new Map(JSON.parse(_rulesShortCutsString)); + const shortCut = _rulesShortCuts.get(`${app.get_id()}:${app.get_name()}`); + if (shortCut) { + const windows = app.get_windows(); + if (windows.length) { + Main.activateWindow(windows[0]); + } else { + app.activate(global.get_current_time()); + } + const cmd = `ydotool key ${shortCut}` + this._log.info(`Closing the app ${app.get_name()} by sending a shortcut ${shortCut}: ${cmd}`); + Util.trySpawnCommandLine(`${cmd}`); + return true; + } + + return false; } _skip_multiple_windows(shellApp) { diff --git a/schemas/gschemas.compiled b/schemas/gschemas.compiled index bf729913bc3fad6156365a8702be1aeebe8f430f..b80bddd4c96b591692fee32eb2c38e4304ddc372 100644 GIT binary patch delta 286 zcmcb?bcT6?jU*2v0|Tol1H*qHI0|GlF!(T>0MchBhC0`?fcQYb1jNixe4w_D8>ASl zk)e!10;o6)h(%=BDnRN$Y#E?<1(040#4y7_Y>?qgfb<$5ZqDYO4^j_e^8wW#0MZ}_ z=oKWEWPp@1FihrWv@c7}$uCaTEzitL$uBR~Eh^1PE!Hj0$S*2ME-is5i;h-G%PdMw z%dgN&NiEJU$uF>SgYcDfl$=Y7aSDDfEIv#l9HNKnx3ASm#&+epOOkO L2dE-R6UqbtuCYP? delta 178 zcmX@Ze1mC%jpPRg1_oA928RDY5W)!LF!(Tl=!l7-&h^Y7J`jM!nSi)Cn|nTp25I7B zC<9Bd0woXB)^URj0I^wt;sQY055$HV3}S-}4gk_1!&6d|O4HLb^U`&5^HWkmavDjR ddIgCk86Y+TgMNNdx_)}zWO+t&E{F(-1OTnvB^v+$ diff --git a/schemas/org.gnome.shell.extensions.another-window-session-manager.gschema.xml b/schemas/org.gnome.shell.extensions.another-window-session-manager.gschema.xml index e316abc..f1f4d41 100644 --- a/schemas/org.gnome.shell.extensions.another-window-session-manager.gschema.xml +++ b/schemas/org.gnome.shell.extensions.another-window-session-manager.gschema.xml @@ -10,5 +10,12 @@ Enable the debugging mode to see what happened, for debugging or issue reporting or development purpose + + '[]' + Rules that are used to close applications + + Rules that are used to close applications + + \ No newline at end of file diff --git a/ui/popupMenuButtonItems.js b/ui/popupMenuButtonItems.js index 90c3c4b..cf55433 100644 --- a/ui/popupMenuButtonItems.js +++ b/ui/popupMenuButtonItems.js @@ -2,6 +2,8 @@ const { GObject, St, Clutter } = imports.gi; +const Main = imports.ui.main; + const PopupMenu = imports.ui.popupMenu; const ExtensionUtils = imports.misc.extensionUtils; @@ -135,6 +137,12 @@ class PopupMenuButtonItemClose extends PopupMenuButtonItem { super.createYesAndNoButtons(); this.yesButton.connect('clicked', () => { + // TODO Do this when enable_close_by_rules is true? + this._parent.close(); + if (Main.overview.visible) { + Main.overview.toggle(); + } + this.closeSession.closeWindows(); this._hideConfirm(); diff --git a/utils/prefsUtils.js b/utils/prefsUtils.js index 9656c6d..a7d794c 100644 --- a/utils/prefsUtils.js +++ b/utils/prefsUtils.js @@ -8,6 +8,10 @@ var PrefsUtils = class { this.settings = this._getSettings(); } + getSettingString(settingName) { + return this.settings.get_string(settingName); + } + _getSettings() { const _settings = ExtensionUtils.getSettings( 'org.gnome.shell.extensions.another-window-session-manager'); From 4e25c9e95d96d14fe7da5d26483fdf96d99f566c Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Mon, 2 May 2022 15:05:02 +0800 Subject: [PATCH 02/49] Use GtkListBox to implement close-windows --- prefs.js | 6 ++ prefsCloseWindow.js | 132 ++++++++++++++++++++++++++++++++++++++ schemas/gschemas.compiled | Bin 684 -> 752 bytes ui/prefs-gtk4.ui | 85 ++++++++++++++++++++++++ 4 files changed, 223 insertions(+) create mode 100644 prefsCloseWindow.js diff --git a/prefs.js b/prefs.js index 3641bdd..ff4614e 100644 --- a/prefs.js +++ b/prefs.js @@ -9,6 +9,8 @@ const Log = Me.imports.utils.log; Me.imports.utils.string; +const PrefsCloseWindow = Me.imports.prefsCloseWindow; + const Prefs = GObject.registerClass( { GTypeName: 'AnotherWindowSessionManagerPrefs', @@ -22,10 +24,13 @@ const Prefs = GObject.registerClass( this._log = new Log.Log(); + new PrefsCloseWindow.UICloseWindows().build(); + this.render_ui(); this._bindSettings(); // Set sensitive AFTER this._bindSettings() to make it work + const restore_at_startup_switch_state = this._settings.get_boolean('enable-autorestore-sessions'); this.timer_on_the_autostart_dialog_spinbutton.set_sensitive(restore_at_startup_switch_state); this.autostart_delay_spinbutton.set_sensitive(restore_at_startup_switch_state); @@ -33,6 +38,7 @@ const Prefs = GObject.registerClass( this.timer_on_the_autostart_dialog_spinbutton.set_sensitive( !this._settings.get_boolean('restore-at-startup-without-asking') ); + } _bindSettings() { diff --git a/prefsCloseWindow.js b/prefsCloseWindow.js new file mode 100644 index 0000000..92560d0 --- /dev/null +++ b/prefsCloseWindow.js @@ -0,0 +1,132 @@ + +const { Gio, GLib, GObject, Gtk, Pango } = imports.gi; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); + +// const _ = ExtensionUtils.gettext; + +var UICloseWindows = GObject.registerClass( +class UICloseWindows extends GObject.Object { + _init() { + super._init({ + }); + + this._builder = new Gtk.Builder(); + this._builder.add_from_file(Me.path + '/ui/prefs-gtk4.ui'); + } + + build() { + this.close_by_rules_switch = this._builder.get_object('close_by_rules_switch'); + this.close_by_rules_switch.connect('notify::active', (widget) => { + const active = widget.active; + + }); + + this.close_by_rules_list_box = this._builder.get_object('close_by_rules_list_box'); + this.close_by_rules_list_box.append(new AwsmNewRuleRow()); + + this._actionGroup = new Gio.SimpleActionGroup(); + this.close_by_rules_list_box.insert_action_group('rules', this._actionGroup); + + let action; + action = new Gio.SimpleAction({ name: 'add' }); + action.connect('activate', this._onAddActivated.bind(this)); + this._actionGroup.add_action(action); + + action = new Gio.SimpleAction({ + name: 'remove', + parameter_type: new GLib.VariantType('s'), + }); + action.connect('activate', this._onRemoveActivated.bind(this)); + this._actionGroup.add_action(action); + + action = new Gio.SimpleAction({ name: 'update' }); + action.connect('activate', () => { + this._settings.set_strv(SETTINGS_KEY, + this._getRuleRows().map(row => `${row.id}:${row.value}`)); + }); + this._actionGroup.add_action(action); + this._updateAction = action; + + + } + + _onAddActivated() { + const dialog = new AwsmNewRuleDialog(this.get_root()); + dialog.connect('response', (dlg, id) => { + const appInfo = id === Gtk.ResponseType.OK + ? dialog.get_widget().get_app_info() : null; + if (appInfo) { + this._settings.set_strv(SETTINGS_KEY, [ + ...this._settings.get_strv(SETTINGS_KEY), + `${appInfo.get_id()}:1`, + ]); + } + dialog.destroy(); + }); + dialog.show(); + } + + _onRemoveActivated(action, param) { + const removed = param.deepUnpack(); + this._settings.set_strv(SETTINGS_KEY, + this._settings.get_strv(SETTINGS_KEY).filter(entry => { + const [id] = entry.split(':'); + return id !== removed; + })); + } + + _getRuleRows() { + return [...this.close_by_rules_list_box].filter(row => !!row.id); + } +}); + + +const AwsmNewRuleRow = GObject.registerClass( +class AwsmNewRuleRow extends Gtk.ListBoxRow { + _init() { + super._init({ + action_name: 'rules.add', + child: new Gtk.Image({ + icon_name: 'list-add-symbolic', + pixel_size: 16, + margin_top: 12, + margin_bottom: 12, + margin_start: 12, + margin_end: 12, + }), + }); + this.update_property( + [Gtk.AccessibleProperty.LABEL], ['Add Rule']); + } +}); + +const AwsmNewRuleDialog = GObject.registerClass( +class AwsmNewRuleDialog extends Gtk.AppChooserDialog { + _init(parent) { + super._init({ + transient_for: parent, + modal: true, + }); + + this._settings = ExtensionUtils.getSettings(); + + this.get_widget().set({ + show_all: true, + show_other: true, // hide more button + }); + + this.get_widget().connect('application-selected', + this._updateSensitivity.bind(this)); + this._updateSensitivity(); + } + + _updateSensitivity() { + const rules = this._settings.get_strv(SETTINGS_KEY); + const appInfo = this.get_widget().get_app_info(); + this.set_response_sensitive(Gtk.ResponseType.OK, + appInfo && !rules.some(i => i.startsWith(appInfo.get_id()))); + } +}); + diff --git a/schemas/gschemas.compiled b/schemas/gschemas.compiled index 5082b644de6abab7ead66da84048d4eb2adf4899..fd228c38f9a3f9b8262a3746086c5eb34963cbf5 100644 GIT binary patch literal 752 zcmaJtkeb)bze)IyPiyCS=mT3A^qVl9FQ=De`G++HF{)<>a@jirT!o&E$t z>?|xqEUhg41r`=6Hlp*zTeJ`Zvokwik}pg$zbs55lw-{?3|{%x<<_Rx0yb|mC1R@0 zumyGuK7mmfVC)m)$xmKl#Ls{>Q4lHJkb#Y5ng(*mNo5nQY#@|&u{3-;Q8m5It+eFx zhEO7s2K!=^yT93gsV{Bc{tF$@oxt_pev$h7v#>38hRKh>?cDxg7wV+@;lG1FfWfQQ z8}dm{z#qrPCE%c5JSCrWFZ^lnEYLsJu|q!TG5G7?2zUt}Udbmt0{;+vM0)XIh|9mbr9~7MtcwVOB+CM#m5W&0v3_5ivR!s delta 372 zcmeysx`uUvjbsfY0|Tol1H*qHSjxlz1U?Kafb^P)q0UwuAU+U)#6g%Dh*^M`6^Pk@ z7$mP@uvredqGJ+L|o3pv+gTz5>KA^Y+ zkd_7FFZ$QMg2X{=DWJFokahrKnDanvkn=!v1Q3huVLAd*4`M3<)i(g?Rv2A!-IC1Q)FK8hkS3sdb|3``3J?GV3($CQkWBu|s0s--jU>&<_ZVfk M88nJDL8^cN0Naj1g#Z8m diff --git a/ui/prefs-gtk4.ui b/ui/prefs-gtk4.ui index 5dfa5e6..4ef24d4 100644 --- a/ui/prefs-gtk4.ui +++ b/ui/prefs-gtk4.ui @@ -15,6 +15,91 @@ 1 + + + + + vertical + + + 1 + + + 12 + 12 + 6 + 32 + + + 1 + Close by rules + 1 + 0 + + 0 + 0 + + + + + + 1 + end + center + + 1 + 0 + + + + + + + + 1 + start + + + + + + + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Close windows + + + + 1 From 29aa0dd8cc62b9ce727b64ee9d1a59e1e3883312 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Fri, 13 May 2022 02:22:29 +0800 Subject: [PATCH 03/49] Preferences UI: Add the close rules configuration --- model/closeWindowsRules.js | 10 + prefs.js | 21 +- prefsCloseWindow.js | 219 ++++++++++++++++-- restoreSession.js | 4 +- schemas/gschemas.compiled | Bin 752 -> 788 bytes ...another-window-session-manager.gschema.xml | 15 +- ui/prefs-gtk4.ui | 79 +++++-- 7 files changed, 303 insertions(+), 45 deletions(-) create mode 100644 model/closeWindowsRules.js diff --git a/model/closeWindowsRules.js b/model/closeWindowsRules.js new file mode 100644 index 0000000..a2099cd --- /dev/null +++ b/model/closeWindowsRules.js @@ -0,0 +1,10 @@ + +var CloseWindowsRules = class { + + type; // string, rule type, such as 'shortcut' + value; // string, the rule, such as 'Ctrl+Q' (TODO, string may not suitable) + appId; // string, such as 'firefox.desktop' + appName; // string, such as 'Firefox' + appDesktopFilePath; // string, such as '/usr/share/applications/firefox.desktop' + enabled; // boolean +} diff --git a/prefs.js b/prefs.js index ff4614e..03b937c 100644 --- a/prefs.js +++ b/prefs.js @@ -24,9 +24,8 @@ const Prefs = GObject.registerClass( this._log = new Log.Log(); - new PrefsCloseWindow.UICloseWindows().build(); - this.render_ui(); + new PrefsCloseWindow.UICloseWindows(this._builder).init(); this._bindSettings(); // Set sensitive AFTER this._bindSettings() to make it work @@ -77,6 +76,13 @@ const Prefs = GObject.registerClass( Gio.SettingsBindFlags.DEFAULT ); + this._settings.bind( + 'close-by-rules', + this.close_by_rules_switch, + 'active', + Gio.SettingsBindFlags.DEFAULT + ); + this._settings.connect('changed::enable-autorestore-sessions', (settings) => { if (this._settings.get_boolean('enable-autorestore-sessions')) { this._installAutostartDesktopFile(); @@ -93,6 +99,9 @@ const Prefs = GObject.registerClass( this._installAutostartDesktopFile(); }); + this._settings.connect('changed::close-by-rules', (settings) => { + log('cccc'); + }); } render_ui() { @@ -127,6 +136,14 @@ const Prefs = GObject.registerClass( this.timer_on_the_autostart_dialog_spinbutton.set_sensitive(!active); }); + this.close_rule_tree_view = this._builder.get_object('close_rule_tree_view'); + this.close_by_rules_switch = this._builder.get_object('close_by_rules_switch'); + this.close_by_rules_switch.connect('notify::active', (widget) => { + const active = widget.active; + log('xxxxx'); + }); + + } _installAutostartDesktopFile() { diff --git a/prefsCloseWindow.js b/prefsCloseWindow.js index 92560d0..75a6183 100644 --- a/prefsCloseWindow.js +++ b/prefsCloseWindow.js @@ -4,19 +4,30 @@ const { Gio, GLib, GObject, Gtk, Pango } = imports.gi; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); +const CloseWindowsRules = Me.imports.model.closeWindowsRules; + +const PrefsUtils = Me.imports.utils.prefsUtils; +const Log = Me.imports.utils.log; + // const _ = ExtensionUtils.gettext; +/** + * Based on https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/blob/main/extensions/auto-move-windows/prefs.js + */ var UICloseWindows = GObject.registerClass( class UICloseWindows extends GObject.Object { - _init() { + _init(builder) { super._init({ }); - this._builder = new Gtk.Builder(); - this._builder.add_from_file(Me.path + '/ui/prefs-gtk4.ui'); + this._log = new Log.Log(); + + this._builder = builder; + this._prefsUtils = new PrefsUtils.PrefsUtils(); + this._settings = this._prefsUtils.getSettings(); } - build() { + init() { this.close_by_rules_switch = this._builder.get_object('close_by_rules_switch'); this.close_by_rules_switch.connect('notify::active', (widget) => { const active = widget.active; @@ -43,25 +54,39 @@ class UICloseWindows extends GObject.Object { action = new Gio.SimpleAction({ name: 'update' }); action.connect('activate', () => { - this._settings.set_strv(SETTINGS_KEY, + this._settings.set_string("close-windows-rules", this._getRuleRows().map(row => `${row.id}:${row.value}`)); }); this._actionGroup.add_action(action); this._updateAction = action; + this._rulesChangedId = this._settings.connect( + 'changed::close-windows-rules', + this._sync.bind(this)); + this._sync(); } _onAddActivated() { - const dialog = new AwsmNewRuleDialog(this.get_root()); + const dialog = new AwsmNewRuleDialog(this._builder.get_object('close_rule_listbox_scrolledwindow').get_root()); dialog.connect('response', (dlg, id) => { const appInfo = id === Gtk.ResponseType.OK ? dialog.get_widget().get_app_info() : null; if (appInfo) { - this._settings.set_strv(SETTINGS_KEY, [ - ...this._settings.get_strv(SETTINGS_KEY), - `${appInfo.get_id()}:1`, - ]); + const closeWindowsRules = new CloseWindowsRules.CloseWindowsRules(); + closeWindowsRules.type = 'shortcut'; + closeWindowsRules.value = ''; + closeWindowsRules.appId = appInfo.get_id(); + closeWindowsRules.appName = appInfo.get_name(); + closeWindowsRules.appDesktopFilePath = appInfo.get_filename(); + closeWindowsRules.enabled = false; + + const oldCloseWindowsRules = this._settings.get_string('close-windows-rules'); + let oldCloseWindowsRulesObj = JSON.parse(oldCloseWindowsRules); + oldCloseWindowsRulesObj[closeWindowsRules.appDesktopFilePath] = closeWindowsRules; + const newCloseWindowsRules = JSON.stringify(oldCloseWindowsRulesObj); + this._settings.set_string('close-windows-rules', newCloseWindowsRules); + } dialog.destroy(); }); @@ -69,19 +94,171 @@ class UICloseWindows extends GObject.Object { } _onRemoveActivated(action, param) { - const removed = param.deepUnpack(); - this._settings.set_strv(SETTINGS_KEY, - this._settings.get_strv(SETTINGS_KEY).filter(entry => { - const [id] = entry.split(':'); - return id !== removed; - })); + // Get the real value inside the GLib.Variant + const removedAppDesktopFilePath = param.deepUnpack(); + const oldCloseWindowsRules = this._settings.get_string('close-windows-rules'); + let oldCloseWindowsRulesObj = JSON.parse(oldCloseWindowsRules); + delete oldCloseWindowsRulesObj[removedAppDesktopFilePath]; + const newCloseWindowsRules = JSON.stringify(oldCloseWindowsRulesObj); + this._settings.set_string('close-windows-rules', newCloseWindowsRules); } _getRuleRows() { - return [...this.close_by_rules_list_box].filter(row => !!row.id); + return [...this.close_by_rules_list_box].filter(row => !!row.appDesktopFilePath); + } + + _sync() { + const oldRules = this._getRuleRows(); + const newRules = JSON.parse(this._settings.get_string('close-windows-rules')); + + this._settings.block_signal_handler(this._rulesChangedId); + this._updateAction.enabled = false; + + // Update old rules or insert new rules + let index = -1 + for(const p in newRules) { + index++; + const ruleDetail = newRules[p]; + this._log.debug(`Checking rule changes for: ${JSON.stringify(ruleDetail)}`); + const row = oldRules.find(r => r.appDesktopFilePath === ruleDetail.appDesktopFilePath); + const appInfo = row + ? null : Gio.DesktopAppInfo.new_from_filename(ruleDetail.appDesktopFilePath); + + if (row) + row.set({ value : GLib.Variant.new_strv([ruleDetail.value]) }); + else if (appInfo) + this.close_by_rules_list_box.insert(new RuleRow(appInfo, ruleDetail), index); + } + + const removed = oldRules.filter((oldRuleDetail) => { + let matched = false; + for(const p in newRules) { + const newRuleDetail = newRules[p]; + if (newRuleDetail.appDesktopFilePath === oldRuleDetail.appDesktopFilePath) { + matched = true; + } + } + return !matched; + }); + + removed.forEach(r => this.close_by_rules_list_box.remove(r)); + + this._settings.unblock_signal_handler(this._rulesChangedId); + this._updateAction.enabled = true; } + }); +const RuleRow = GObject.registerClass({ + Properties: { + 'enabled': GObject.ParamSpec.boolean( + 'enabled', 'enabled', 'enabled', + GObject.ParamFlags.READWRITE, + false + ), + 'app-name': GObject.ParamSpec.string( + 'app-name', 'The application name', 'The application name', + GObject.ParamFlags.READABLE, + '' + ), + 'app-id': GObject.ParamSpec.string( + 'app-id', + 'The application id', + 'The .desktop file name of an app', + GObject.ParamFlags.READABLE, + '' + ), + 'app-desktop-file-path': GObject.ParamSpec.string( + 'app-desktop-file-path', + 'The app desktop file path', + 'The .desktop file name of an app', + GObject.ParamFlags.READABLE, + '' + ), + 'type': GObject.ParamSpec.string( + 'type', 'type', 'type', + GObject.ParamFlags.READWRITE, + 'Shortcut'), + 'value': GObject.param_spec_variant( + 'value', 'value', 'The rules', + // An array of strings + new GLib.VariantType('as'), + // Default value + null, + GObject.ParamFlags.READWRITE), + }, +}, class RuleRow extends Gtk.ListBoxRow { + _init(appInfo, ruleDetail) { + const box = new Gtk.Box({ + spacing: 6, + margin_top: 6, + margin_bottom: 6, + margin_start: 6, + margin_end: 6, + }); + + super._init({ + activatable: false, + value: GLib.Variant.new_strv([ruleDetail.value]), + child: box, + }); + this._appInfo = appInfo; + + const icon = new Gtk.Image({ + gicon: appInfo.get_icon(), + pixel_size: 32, + }); + icon.get_style_context().add_class('icon-dropshadow'); + box.append(icon); + + const label = new Gtk.Label({ + label: appInfo.get_display_name(), + halign: Gtk.Align.START, + hexpand: true, + max_width_chars: 20, + ellipsize: Pango.EllipsizeMode.END, + }); + box.append(label); + + const _model = new Gtk.ListStore(); + _model.set_column_types([GObject.TYPE_STRING, GObject.TYPE_STRING]); + let iter = _model.append(); + // https://docs.gtk.org/gtk4/method.ListStore.set.html + _model.set(iter, [0], ['Shortcut', 'Shortcut']); + const combo = new Gtk.ComboBox({ + model: _model, + halign: Gtk.Align.START + }); + combo.set_active_iter(iter); + this.bind_property('value', + combo, 'value', + GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL); + box.append(combo); + + const button = new Gtk.Button({ + action_name: 'rules.remove', + action_target: new GLib.Variant('s', this.appDesktopFilePath), + icon_name: 'edit-delete-symbolic', + }); + box.append(button); + + this.connect('notify::value', + () => this.activate_action('rules.update', null)); + } + + get appName() { + return this._appInfo.get_name(); + } + + get appId() { + return this._appInfo.get_id(); + } + + get appDesktopFilePath() { + return this._appInfo.get_filename(); + } + +}); const AwsmNewRuleRow = GObject.registerClass( class AwsmNewRuleRow extends Gtk.ListBoxRow { @@ -110,9 +287,11 @@ class AwsmNewRuleDialog extends Gtk.AppChooserDialog { modal: true, }); - this._settings = ExtensionUtils.getSettings(); + this._prefsUtils = new PrefsUtils.PrefsUtils(); + this._settings = this._prefsUtils.getSettings(); this.get_widget().set({ + show_recommended: true, show_all: true, show_other: true, // hide more button }); @@ -123,10 +302,10 @@ class AwsmNewRuleDialog extends Gtk.AppChooserDialog { } _updateSensitivity() { - const rules = this._settings.get_strv(SETTINGS_KEY); + const rules = this._settings.get_string('close-windows-rules'); const appInfo = this.get_widget().get_app_info(); this.set_response_sensitive(Gtk.ResponseType.OK, - appInfo && !rules.some(i => i.startsWith(appInfo.get_id()))); + appInfo && !JSON.parse(rules)[appInfo.get_filename()]); } }); diff --git a/restoreSession.js b/restoreSession.js index 383c36c..1a8508b 100644 --- a/restoreSession.js +++ b/restoreSession.js @@ -46,13 +46,13 @@ var RestoreSession = class { this._log.info(`Restoring saved session from ${session_file_path}`); try { - this.restoreSessionFromPath(session_file_path); + this.restoreSessionFromFile(session_file_path); } catch (e) { logError(e, `Failed to restore ${session_file_path}`); } } - restoreSessionFromPath(session_file_path) { + restoreSessionFromFile(session_file_path) { const session_file = Gio.File.new_for_path(session_file_path); let [success, contents] = session_file.load_contents(null); if (success) { diff --git a/schemas/gschemas.compiled b/schemas/gschemas.compiled index fd228c38f9a3f9b8262a3746086c5eb34963cbf5..b662fa25a0ae6aaef23f105e35d423582243af54 100644 GIT binary patch literal 788 zcmaJvDD3AVO2 zRyee1P8wRzWe6AH*X(r@uMy>ZG$|Vm*BNpM}rUE0J!+G z@P^F01zD1>;1d{)7b15!V^3hnij0FN0av7PYF*F7zH9P4HeZ6VK6BQ`+Pcs+h1$$) z$89QaJbl&ER;Q+58IfqDX5pLcV+d#fPyfL%^fTaha&f~r_A?`&F||!U-q4|2|4py}Nwok;wE)TA1xOxWfJDT9%AGf=y`_pWH=Y#-Sik?# pI;g7masx{gx~>dL`*rlgNW?wTJ??$0E?@7Tuix0-scu+6?g18vw7dWS literal 752 zcmaJtkeb)bze)IyPiyCS=mT3A^qVl9FQ=De`G++HF{)<>a@jirT!o&E$t z>?|xqEUhg41r`=6Hlp*zTeJ`Zvokwik}pg$zbs55lw-{?3|{%x<<_Rx0yb|mC1R@0 zumyGuK7mmfVC)m)$xmKl#Ls{>Q4lHJkb#Y5ng(*mNo5nQY#@|&u{3-;Q8m5It+eFx zhEO7s2K!=^yT93gsV{Bc{tF$@oxt_pev$h7v#>38hRKh>?cDxg7wV+@;lG1FfWfQQ z8}dm{z#qrPCE%c5JSCrWFZ^lnEYLsJu|q!TG5G7?2zUt}Udbmt0{;+vM0)XIh|9mbr9~7MtcwVOB+CM#m5W&0v3_5ivR!s diff --git a/schemas/org.gnome.shell.extensions.another-window-session-manager.gschema.xml b/schemas/org.gnome.shell.extensions.another-window-session-manager.gschema.xml index babc685..a58e682 100644 --- a/schemas/org.gnome.shell.extensions.another-window-session-manager.gschema.xml +++ b/schemas/org.gnome.shell.extensions.another-window-session-manager.gschema.xml @@ -10,8 +10,15 @@ Enable the debugging mode to see what happened, for debugging or issue reporting or development purpose - - '[]' + + true + Enable or disable the ability of closing windows by customed rules, such as a shortcut + + + + + + '{}' Rules that are used to close applications Rules that are used to close applications @@ -52,6 +59,6 @@ A specified amount of time to delay to execute the target command. - + - + \ No newline at end of file diff --git a/ui/prefs-gtk4.ui b/ui/prefs-gtk4.ui index 4ef24d4..2e7d7b2 100644 --- a/ui/prefs-gtk4.ui +++ b/ui/prefs-gtk4.ui @@ -52,23 +52,6 @@ - - - - - 1 - start - - - - - - - 0 - 1 - - - @@ -91,6 +74,68 @@ + + + 12 + 12 + 6 + 32 + + + 1 + + + + + vertical + + + True + True + + + + + + + + + + + + + + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 3fc93b43cd88d661614ee315d9f023bf20fc0df9 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sat, 14 May 2022 10:58:05 +0800 Subject: [PATCH 04/49] close windows rules UI: checkbox and combobox --- prefsCloseWindow.js | 69 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 14 deletions(-) diff --git a/prefsCloseWindow.js b/prefsCloseWindow.js index 75a6183..e102f30 100644 --- a/prefsCloseWindow.js +++ b/prefsCloseWindow.js @@ -52,10 +52,18 @@ class UICloseWindows extends GObject.Object { action.connect('activate', this._onRemoveActivated.bind(this)); this._actionGroup.add_action(action); - action = new Gio.SimpleAction({ name: 'update' }); - action.connect('activate', () => { - this._settings.set_string("close-windows-rules", - this._getRuleRows().map(row => `${row.id}:${row.value}`)); + action = new Gio.SimpleAction({ + name: 'update', + parameter_type: new GLib.VariantType('a{sv}'), + }); + action.connect('activate', (action, param) => { + const newRuleRow = param.recursiveUnpack(); + const oldCloseWindowsRules = this._settings.get_string('close-windows-rules'); + let oldCloseWindowsRulesObj = JSON.parse(oldCloseWindowsRules); + oldCloseWindowsRulesObj[newRuleRow.appDesktopFilePath].enabled = newRuleRow.enabled; + // oldCloseWindowsRulesObj[newRuleRow.appDesktopFilePath].value = newRuleRow.value; + const newCloseWindowsRules = JSON.stringify(oldCloseWindowsRulesObj); + this._settings.set_string('close-windows-rules', newCloseWindowsRules); }); this._actionGroup.add_action(action); this._updateAction = action; @@ -126,8 +134,10 @@ class UICloseWindows extends GObject.Object { if (row) row.set({ value : GLib.Variant.new_strv([ruleDetail.value]) }); - else if (appInfo) - this.close_by_rules_list_box.insert(new RuleRow(appInfo, ruleDetail), index); + else if (appInfo) { + const newRuleRow = new RuleRow(appInfo, ruleDetail); + this.close_by_rules_list_box.insert(newRuleRow, index); + } } const removed = oldRules.filter((oldRuleDetail) => { @@ -203,6 +213,16 @@ const RuleRow = GObject.registerClass({ child: box, }); this._appInfo = appInfo; + this._ruleDetail = ruleDetail; + + this._enabledCheckButton = new Gtk.CheckButton({ + active: ruleDetail.enabled, + }) + // `flags` contains GObject.BindingFlags.BIDIRECTIONAL so we don't need to set `enable` manually + this.bind_property('enabled', + this._enabledCheckButton, 'active', + GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL); + box.append(this._enabledCheckButton); const icon = new Gtk.Image({ gicon: appInfo.get_icon(), @@ -222,17 +242,22 @@ const RuleRow = GObject.registerClass({ const _model = new Gtk.ListStore(); _model.set_column_types([GObject.TYPE_STRING, GObject.TYPE_STRING]); - let iter = _model.append(); - // https://docs.gtk.org/gtk4/method.ListStore.set.html - _model.set(iter, [0], ['Shortcut', 'Shortcut']); const combo = new Gtk.ComboBox({ model: _model, halign: Gtk.Align.START }); - combo.set_active_iter(iter); - this.bind_property('value', - combo, 'value', - GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL); + // https://stackoverflow.com/questions/21568268/how-to-use-the-gtk-combobox-in-gjs + // https://tecnocode.co.uk/misc/platform-demos/combobox.js.xhtml + let renderer = new Gtk.CellRendererText(); + // Pack the renderers into the combobox in the order we want to see + combo.pack_start(renderer, true); + // Set the renderers to use the information from our liststore + combo.add_attribute(renderer, 'text', 1); + let iter = _model.append(); + // https://docs.gtk.org/gtk4/method.ListStore.set.html + _model.set(iter, [0, 1], ['Shortcut', 'Shortcut']); + // Set the first row in the combobox to be active on startup + combo.set_active(0); box.append(combo); const button = new Gtk.Button({ @@ -243,7 +268,23 @@ const RuleRow = GObject.registerClass({ box.append(button); this.connect('notify::value', - () => this.activate_action('rules.update', null)); + () => this.activate_action('rules.update', new GLib.Variant('a{sv}', { + appDesktopFilePath: GLib.Variant.new_string(this.appDesktopFilePath), + enabled: GLib.Variant.new_boolean(this._enabledCheckButton.get_active()), + value: this.value, + }))); + this.connect('notify::enabled', + () => { + this.activate_action('rules.update', new GLib.Variant('a{sv}',{ + appDesktopFilePath: GLib.Variant.new_string(this.appDesktopFilePath), + enabled: GLib.Variant.new_boolean(this._enabledCheckButton.get_active()), + value: this.value, + })) + }); + } + + get enabled() { + return this._ruleDetail.enabled; } get appName() { From b7d7ca46ad4726d88130ca0aba11f6f096b97288 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sat, 21 May 2022 13:25:00 +0800 Subject: [PATCH 05/49] Add the ability to set the shortcuts through `Gtk.EventControllerKey` --- closeSession.js | 6 +- prefsCloseWindow.js | 438 +++++++++++++++++++++++++++----------------- 2 files changed, 276 insertions(+), 168 deletions(-) diff --git a/closeSession.js b/closeSession.js index d750784..3fd3ac0 100644 --- a/closeSession.js +++ b/closeSession.js @@ -55,9 +55,9 @@ var CloseSession = class { return false; } - const _rulesShortCutsString = this._prefsUtils.getSettingString('close-windows-rules-shortcut'); - const _rulesShortCuts = new Map(JSON.parse(_rulesShortCutsString)); - const shortCut = _rulesShortCuts.get(`${app.get_id()}:${app.get_name()}`); + const _rules = this._prefsUtils.getSettingString('close-windows-rules'); + const _rulesMap = new Map(JSON.parse(_rules)); + const shortCut = _rulesMap.get(`${app.get_id()}:${app.get_name()}`); if (shortCut) { const windows = app.get_windows(); if (windows.length) { diff --git a/prefsCloseWindow.js b/prefsCloseWindow.js index e102f30..954891b 100644 --- a/prefsCloseWindow.js +++ b/prefsCloseWindow.js @@ -1,5 +1,5 @@ -const { Gio, GLib, GObject, Gtk, Pango } = imports.gi; +const { Gio, GLib, GObject, Gtk, Pango, Gdk } = imports.gi; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); @@ -15,134 +15,134 @@ const Log = Me.imports.utils.log; * Based on https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/blob/main/extensions/auto-move-windows/prefs.js */ var UICloseWindows = GObject.registerClass( -class UICloseWindows extends GObject.Object { - _init(builder) { - super._init({ - }); - - this._log = new Log.Log(); + class UICloseWindows extends GObject.Object { + _init(builder) { + super._init({ + }); - this._builder = builder; - this._prefsUtils = new PrefsUtils.PrefsUtils(); - this._settings = this._prefsUtils.getSettings(); - } + this._log = new Log.Log(); - init() { - this.close_by_rules_switch = this._builder.get_object('close_by_rules_switch'); - this.close_by_rules_switch.connect('notify::active', (widget) => { - const active = widget.active; - - }); - - this.close_by_rules_list_box = this._builder.get_object('close_by_rules_list_box'); - this.close_by_rules_list_box.append(new AwsmNewRuleRow()); - - this._actionGroup = new Gio.SimpleActionGroup(); - this.close_by_rules_list_box.insert_action_group('rules', this._actionGroup); + this._builder = builder; + this._prefsUtils = new PrefsUtils.PrefsUtils(); + this._settings = this._prefsUtils.getSettings(); + } - let action; - action = new Gio.SimpleAction({ name: 'add' }); - action.connect('activate', this._onAddActivated.bind(this)); - this._actionGroup.add_action(action); + init() { + this.close_by_rules_switch = this._builder.get_object('close_by_rules_switch'); + this.close_by_rules_switch.connect('notify::active', (widget) => { + const active = widget.active; - action = new Gio.SimpleAction({ - name: 'remove', - parameter_type: new GLib.VariantType('s'), - }); - action.connect('activate', this._onRemoveActivated.bind(this)); - this._actionGroup.add_action(action); + }); - action = new Gio.SimpleAction({ - name: 'update', - parameter_type: new GLib.VariantType('a{sv}'), - }); - action.connect('activate', (action, param) => { - const newRuleRow = param.recursiveUnpack(); - const oldCloseWindowsRules = this._settings.get_string('close-windows-rules'); - let oldCloseWindowsRulesObj = JSON.parse(oldCloseWindowsRules); - oldCloseWindowsRulesObj[newRuleRow.appDesktopFilePath].enabled = newRuleRow.enabled; - // oldCloseWindowsRulesObj[newRuleRow.appDesktopFilePath].value = newRuleRow.value; - const newCloseWindowsRules = JSON.stringify(oldCloseWindowsRulesObj); - this._settings.set_string('close-windows-rules', newCloseWindowsRules); - }); - this._actionGroup.add_action(action); - this._updateAction = action; + this.close_by_rules_list_box = this._builder.get_object('close_by_rules_list_box'); + this.close_by_rules_list_box.append(new AwsmNewRuleRow()); - this._rulesChangedId = this._settings.connect( - 'changed::close-windows-rules', - this._sync.bind(this)); - this._sync(); + this._actionGroup = new Gio.SimpleActionGroup(); + this.close_by_rules_list_box.insert_action_group('rules', this._actionGroup); - } + let action; + action = new Gio.SimpleAction({ name: 'add' }); + action.connect('activate', this._onAddActivated.bind(this)); + this._actionGroup.add_action(action); - _onAddActivated() { - const dialog = new AwsmNewRuleDialog(this._builder.get_object('close_rule_listbox_scrolledwindow').get_root()); - dialog.connect('response', (dlg, id) => { - const appInfo = id === Gtk.ResponseType.OK - ? dialog.get_widget().get_app_info() : null; - if (appInfo) { - const closeWindowsRules = new CloseWindowsRules.CloseWindowsRules(); - closeWindowsRules.type = 'shortcut'; - closeWindowsRules.value = ''; - closeWindowsRules.appId = appInfo.get_id(); - closeWindowsRules.appName = appInfo.get_name(); - closeWindowsRules.appDesktopFilePath = appInfo.get_filename(); - closeWindowsRules.enabled = false; + action = new Gio.SimpleAction({ + name: 'remove', + parameter_type: new GLib.VariantType('s'), + }); + action.connect('activate', this._onRemoveActivated.bind(this)); + this._actionGroup.add_action(action); + action = new Gio.SimpleAction({ + name: 'update', + parameter_type: new GLib.VariantType('a{sv}'), + }); + action.connect('activate', (action, param) => { + const newRuleRow = param.recursiveUnpack(); const oldCloseWindowsRules = this._settings.get_string('close-windows-rules'); - let oldCloseWindowsRulesObj = JSON.parse(oldCloseWindowsRules); - oldCloseWindowsRulesObj[closeWindowsRules.appDesktopFilePath] = closeWindowsRules; + let oldCloseWindowsRulesObj = JSON.parse(oldCloseWindowsRules); + oldCloseWindowsRulesObj[newRuleRow.appDesktopFilePath].enabled = newRuleRow.enabled; + // oldCloseWindowsRulesObj[newRuleRow.appDesktopFilePath].value = newRuleRow.value; const newCloseWindowsRules = JSON.stringify(oldCloseWindowsRulesObj); this._settings.set_string('close-windows-rules', newCloseWindowsRules); - - } - dialog.destroy(); - }); - dialog.show(); - } + }); + this._actionGroup.add_action(action); + this._updateAction = action; - _onRemoveActivated(action, param) { - // Get the real value inside the GLib.Variant - const removedAppDesktopFilePath = param.deepUnpack(); - const oldCloseWindowsRules = this._settings.get_string('close-windows-rules'); - let oldCloseWindowsRulesObj = JSON.parse(oldCloseWindowsRules); - delete oldCloseWindowsRulesObj[removedAppDesktopFilePath]; - const newCloseWindowsRules = JSON.stringify(oldCloseWindowsRulesObj); - this._settings.set_string('close-windows-rules', newCloseWindowsRules); - } + this._rulesChangedId = this._settings.connect( + 'changed::close-windows-rules', + this._sync.bind(this)); + this._sync(); - _getRuleRows() { - return [...this.close_by_rules_list_box].filter(row => !!row.appDesktopFilePath); - } + } - _sync() { - const oldRules = this._getRuleRows(); - const newRules = JSON.parse(this._settings.get_string('close-windows-rules')); - - this._settings.block_signal_handler(this._rulesChangedId); - this._updateAction.enabled = false; - - // Update old rules or insert new rules - let index = -1 - for(const p in newRules) { - index++; - const ruleDetail = newRules[p]; - this._log.debug(`Checking rule changes for: ${JSON.stringify(ruleDetail)}`); - const row = oldRules.find(r => r.appDesktopFilePath === ruleDetail.appDesktopFilePath); - const appInfo = row - ? null : Gio.DesktopAppInfo.new_from_filename(ruleDetail.appDesktopFilePath); - - if (row) - row.set({ value : GLib.Variant.new_strv([ruleDetail.value]) }); - else if (appInfo) { - const newRuleRow = new RuleRow(appInfo, ruleDetail); - this.close_by_rules_list_box.insert(newRuleRow, index); - } + _onAddActivated() { + const dialog = new AwsmNewRuleDialog(this._builder.get_object('close_rule_listbox_scrolledwindow').get_root()); + dialog.connect('response', (dlg, id) => { + const appInfo = id === Gtk.ResponseType.OK + ? dialog.get_widget().get_app_info() : null; + if (appInfo) { + const closeWindowsRules = new CloseWindowsRules.CloseWindowsRules(); + closeWindowsRules.type = 'shortcut'; + closeWindowsRules.value = ''; + closeWindowsRules.appId = appInfo.get_id(); + closeWindowsRules.appName = appInfo.get_name(); + closeWindowsRules.appDesktopFilePath = appInfo.get_filename(); + closeWindowsRules.enabled = false; + + const oldCloseWindowsRules = this._settings.get_string('close-windows-rules'); + let oldCloseWindowsRulesObj = JSON.parse(oldCloseWindowsRules); + oldCloseWindowsRulesObj[closeWindowsRules.appDesktopFilePath] = closeWindowsRules; + const newCloseWindowsRules = JSON.stringify(oldCloseWindowsRulesObj); + this._settings.set_string('close-windows-rules', newCloseWindowsRules); + + } + dialog.destroy(); + }); + dialog.show(); } - const removed = oldRules.filter((oldRuleDetail) => { + _onRemoveActivated(action, param) { + // Get the real value inside the GLib.Variant + const removedAppDesktopFilePath = param.deepUnpack(); + const oldCloseWindowsRules = this._settings.get_string('close-windows-rules'); + let oldCloseWindowsRulesObj = JSON.parse(oldCloseWindowsRules); + delete oldCloseWindowsRulesObj[removedAppDesktopFilePath]; + const newCloseWindowsRules = JSON.stringify(oldCloseWindowsRulesObj); + this._settings.set_string('close-windows-rules', newCloseWindowsRules); + } + + _getRuleRows() { + return [...this.close_by_rules_list_box].filter(row => !!row.appDesktopFilePath); + } + + _sync() { + const oldRules = this._getRuleRows(); + const newRules = JSON.parse(this._settings.get_string('close-windows-rules')); + + this._settings.block_signal_handler(this._rulesChangedId); + this._updateAction.enabled = false; + + // Update old rules or insert new rules + let index = -1 + for (const p in newRules) { + index++; + const ruleDetail = newRules[p]; + this._log.debug(`Checking rule changes for: ${JSON.stringify(ruleDetail)}`); + const row = oldRules.find(r => r.appDesktopFilePath === ruleDetail.appDesktopFilePath); + const appInfo = row + ? null : Gio.DesktopAppInfo.new_from_filename(ruleDetail.appDesktopFilePath); + + if (row) + row.set({ value: GLib.Variant.new_strv([ruleDetail.value]) }); + else if (appInfo) { + const newRuleRow = new RuleRow(appInfo, ruleDetail); + this.close_by_rules_list_box.insert(newRuleRow, index); + } + } + + const removed = oldRules.filter((oldRuleDetail) => { let matched = false; - for(const p in newRules) { + for (const p in newRules) { const newRuleDetail = newRules[p]; if (newRuleDetail.appDesktopFilePath === oldRuleDetail.appDesktopFilePath) { matched = true; @@ -151,13 +151,13 @@ class UICloseWindows extends GObject.Object { return !matched; }); - removed.forEach(r => this.close_by_rules_list_box.remove(r)); + removed.forEach(r => this.close_by_rules_list_box.remove(r)); - this._settings.unblock_signal_handler(this._rulesChangedId); - this._updateAction.enabled = true; - } + this._settings.unblock_signal_handler(this._rulesChangedId); + this._updateAction.enabled = true; + } -}); + }); const RuleRow = GObject.registerClass({ Properties: { @@ -172,15 +172,15 @@ const RuleRow = GObject.registerClass({ '' ), 'app-id': GObject.ParamSpec.string( - 'app-id', - 'The application id', + 'app-id', + 'The application id', 'The .desktop file name of an app', GObject.ParamFlags.READABLE, '' ), 'app-desktop-file-path': GObject.ParamSpec.string( - 'app-desktop-file-path', - 'The app desktop file path', + 'app-desktop-file-path', + 'The app desktop file path', 'The .desktop file name of an app', GObject.ParamFlags.READABLE, '' @@ -199,6 +199,8 @@ const RuleRow = GObject.registerClass({ }, }, class RuleRow extends Gtk.ListBoxRow { _init(appInfo, ruleDetail) { + this._log = new Log.Log(); + const box = new Gtk.Box({ spacing: 6, margin_top: 6, @@ -206,6 +208,7 @@ const RuleRow = GObject.registerClass({ margin_start: 6, margin_end: 6, }); + this._box = box; super._init({ activatable: false, @@ -248,7 +251,7 @@ const RuleRow = GObject.registerClass({ }); // https://stackoverflow.com/questions/21568268/how-to-use-the-gtk-combobox-in-gjs // https://tecnocode.co.uk/misc/platform-demos/combobox.js.xhtml - let renderer = new Gtk.CellRendererText(); + const renderer = new Gtk.CellRendererText(); // Pack the renderers into the combobox in the order we want to see combo.pack_start(renderer, true); // Set the renderers to use the information from our liststore @@ -260,12 +263,14 @@ const RuleRow = GObject.registerClass({ combo.set_active(0); box.append(combo); - const button = new Gtk.Button({ + this._append_accel(); + + const buttonRemove = new Gtk.Button({ action_name: 'rules.remove', action_target: new GLib.Variant('s', this.appDesktopFilePath), icon_name: 'edit-delete-symbolic', }); - box.append(button); + box.append(buttonRemove); this.connect('notify::value', () => this.activate_action('rules.update', new GLib.Variant('a{sv}', { @@ -275,7 +280,7 @@ const RuleRow = GObject.registerClass({ }))); this.connect('notify::enabled', () => { - this.activate_action('rules.update', new GLib.Variant('a{sv}',{ + this.activate_action('rules.update', new GLib.Variant('a{sv}', { appDesktopFilePath: GLib.Variant.new_string(this.appDesktopFilePath), enabled: GLib.Variant.new_boolean(this._enabledCheckButton.get_active()), value: this.value, @@ -283,6 +288,109 @@ const RuleRow = GObject.registerClass({ }); } + _append_accel() { + const rendererAccelBox = new Gtk.Box({ + spacing: 6, + margin_top: 6, + margin_bottom: 6, + margin_start: 6, + margin_end: 6, + }); + + const rules = this._ruleDetail.value; + + let rendererAccel = new Gtk.ShortcutLabel(); + rendererAccel.accelerator = "Escape"; + rendererAccelBox.append(rendererAccel); + + let next = new Gtk.Label({ + label: '→', + halign: Gtk.Align.CENTER + }); + rendererAccelBox.append(next); + + rendererAccel = new Gtk.ShortcutLabel(); + rendererAccel.accelerator = "Q"; + rendererAccelBox.append(rendererAccel); + + next = new Gtk.Label({ + label: '→', + halign: Gtk.Align.CENTER, + }); + rendererAccelBox.append(next); + + rendererAccel = new Gtk.ShortcutLabel(); + rendererAccel.accelerator = "w"; + rendererAccelBox.append(rendererAccel); + + const addAccelButton = new Gtk.Button({ + label: 'Add accelerator', + }); + const deleteAccelButton = new Gtk.Button({ + label: 'Delete', + }); + const rendererAccelOptBox = new Gtk.Box({ + spacing: 6, + margin_top: 6, + margin_bottom: 6, + margin_start: 6, + margin_end: 6, + }); + rendererAccelOptBox.append(addAccelButton); + rendererAccelOptBox.append(deleteAccelButton); + + addAccelButton.connect('clicked', () => { + const next = new Gtk.Label({ + label: '→', + halign: Gtk.Align.CENTER, + }); + + const newAccelButton = new Gtk.Button({ + label: 'New accelerator...', + }); + const eventControllerKey = new Gtk.EventControllerKey(); + newAccelButton.add_controller(eventControllerKey); + eventControllerKey.connect('key-pressed', this._onKeyPressed.bind(this)); + + rendererAccelBox.append(next); + rendererAccelBox.append(newAccelButton); + + const focused = newAccelButton.grab_focus(); + this._log.debug(`Grab the focus for setting the accelerator: ${focused}`); + }); + + + const box = new Gtk.Box(); + box.append(rendererAccelBox); + box.append(rendererAccelOptBox); + + const frame = new Gtk.Frame(); + frame.set_child(box); + + this._box.append(frame); + } + + _onKeyPressed(_eventControllerKey, keyval, keycode, state) { + let mask = state & Gtk.accelerator_get_default_mod_mask(); + mask &= ~Gdk.ModifierType.LOCK_MASK; + + // Backspace resets the new shortcut + if (mask === 0 && keyval === Gdk.KEY_BackSpace) { + _eventControllerKey.get_widget().set_label('New accelerator...'); + return Gdk.EVENT_STOP; + } + + if (!Gtk.accelerator_valid(keyval, mask)) return Gdk.EVENT_STOP; + const accel = Gtk.accelerator_name_with_keycode( + null, + keyval, + keycode, + mask + ); + _eventControllerKey.get_widget().set_label(Gtk.accelerator_get_label(keyval, mask)); + return Gdk.EVENT_STOP; + } + get enabled() { return this._ruleDetail.enabled; } @@ -302,51 +410,51 @@ const RuleRow = GObject.registerClass({ }); const AwsmNewRuleRow = GObject.registerClass( -class AwsmNewRuleRow extends Gtk.ListBoxRow { - _init() { - super._init({ - action_name: 'rules.add', - child: new Gtk.Image({ - icon_name: 'list-add-symbolic', - pixel_size: 16, - margin_top: 12, - margin_bottom: 12, - margin_start: 12, - margin_end: 12, - }), - }); - this.update_property( - [Gtk.AccessibleProperty.LABEL], ['Add Rule']); - } -}); + class AwsmNewRuleRow extends Gtk.ListBoxRow { + _init() { + super._init({ + action_name: 'rules.add', + child: new Gtk.Image({ + icon_name: 'list-add-symbolic', + pixel_size: 16, + margin_top: 12, + margin_bottom: 12, + margin_start: 12, + margin_end: 12, + }), + }); + this.update_property( + [Gtk.AccessibleProperty.LABEL], ['Add Rule']); + } + }); const AwsmNewRuleDialog = GObject.registerClass( -class AwsmNewRuleDialog extends Gtk.AppChooserDialog { - _init(parent) { - super._init({ - transient_for: parent, - modal: true, - }); + class AwsmNewRuleDialog extends Gtk.AppChooserDialog { + _init(parent) { + super._init({ + transient_for: parent, + modal: true, + }); - this._prefsUtils = new PrefsUtils.PrefsUtils(); - this._settings = this._prefsUtils.getSettings(); + this._prefsUtils = new PrefsUtils.PrefsUtils(); + this._settings = this._prefsUtils.getSettings(); - this.get_widget().set({ - show_recommended: true, - show_all: true, - show_other: true, // hide more button - }); + this.get_widget().set({ + show_recommended: true, + show_all: true, + show_other: true, // hide more button + }); - this.get_widget().connect('application-selected', - this._updateSensitivity.bind(this)); - this._updateSensitivity(); - } + this.get_widget().connect('application-selected', + this._updateSensitivity.bind(this)); + this._updateSensitivity(); + } - _updateSensitivity() { - const rules = this._settings.get_string('close-windows-rules'); - const appInfo = this.get_widget().get_app_info(); - this.set_response_sensitive(Gtk.ResponseType.OK, - appInfo && !JSON.parse(rules)[appInfo.get_filename()]); - } -}); + _updateSensitivity() { + const rules = this._settings.get_string('close-windows-rules'); + const appInfo = this.get_widget().get_app_info(); + this.set_response_sensitive(Gtk.ResponseType.OK, + appInfo && !JSON.parse(rules)[appInfo.get_filename()]); + } + }); From e7b459a08f5d7f3f3ede25417d301c4f9f1809a0 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sat, 21 May 2022 21:44:40 +0800 Subject: [PATCH 06/49] Remove `New Accelerator` button when pressing Backspace --- prefsCloseWindow.js | 32 ++++++++++++++++++++------------ ui/prefs-gtk4.ui | 2 ++ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/prefsCloseWindow.js b/prefsCloseWindow.js index 954891b..d760464 100644 --- a/prefsCloseWindow.js +++ b/prefsCloseWindow.js @@ -218,6 +218,9 @@ const RuleRow = GObject.registerClass({ this._appInfo = appInfo; this._ruleDetail = ruleDetail; + this._rendererAccelBox = null; + this._lastNextArrow = null; + this._enabledCheckButton = new Gtk.CheckButton({ active: ruleDetail.enabled, }) @@ -289,7 +292,7 @@ const RuleRow = GObject.registerClass({ } _append_accel() { - const rendererAccelBox = new Gtk.Box({ + this._rendererAccelBox = new Gtk.Box({ spacing: 6, margin_top: 6, margin_bottom: 6, @@ -301,27 +304,27 @@ const RuleRow = GObject.registerClass({ let rendererAccel = new Gtk.ShortcutLabel(); rendererAccel.accelerator = "Escape"; - rendererAccelBox.append(rendererAccel); + this._rendererAccelBox.append(rendererAccel); let next = new Gtk.Label({ label: '→', halign: Gtk.Align.CENTER }); - rendererAccelBox.append(next); + this._rendererAccelBox.append(next); rendererAccel = new Gtk.ShortcutLabel(); rendererAccel.accelerator = "Q"; - rendererAccelBox.append(rendererAccel); + this._rendererAccelBox.append(rendererAccel); next = new Gtk.Label({ label: '→', halign: Gtk.Align.CENTER, }); - rendererAccelBox.append(next); + this._rendererAccelBox.append(next); rendererAccel = new Gtk.ShortcutLabel(); rendererAccel.accelerator = "w"; - rendererAccelBox.append(rendererAccel); + this._rendererAccelBox.append(rendererAccel); const addAccelButton = new Gtk.Button({ label: 'Add accelerator', @@ -340,7 +343,7 @@ const RuleRow = GObject.registerClass({ rendererAccelOptBox.append(deleteAccelButton); addAccelButton.connect('clicked', () => { - const next = new Gtk.Label({ + this._lastNextArrow = new Gtk.Label({ label: '→', halign: Gtk.Align.CENTER, }); @@ -352,16 +355,17 @@ const RuleRow = GObject.registerClass({ newAccelButton.add_controller(eventControllerKey); eventControllerKey.connect('key-pressed', this._onKeyPressed.bind(this)); - rendererAccelBox.append(next); - rendererAccelBox.append(newAccelButton); + this._rendererAccelBox.append(this._lastNextArrow); + this._rendererAccelBox.append(newAccelButton); + this._rendererAccelBox.get_root().get_surface().inhibit_system_shortcuts(null); const focused = newAccelButton.grab_focus(); this._log.debug(`Grab the focus for setting the accelerator: ${focused}`); }); const box = new Gtk.Box(); - box.append(rendererAccelBox); + box.append(this._rendererAccelBox); box.append(rendererAccelOptBox); const frame = new Gtk.Frame(); @@ -376,7 +380,8 @@ const RuleRow = GObject.registerClass({ // Backspace resets the new shortcut if (mask === 0 && keyval === Gdk.KEY_BackSpace) { - _eventControllerKey.get_widget().set_label('New accelerator...'); + this._rendererAccelBox.remove(this._lastNextArrow); + this._rendererAccelBox.remove(_eventControllerKey.get_widget()); return Gdk.EVENT_STOP; } @@ -387,7 +392,10 @@ const RuleRow = GObject.registerClass({ keycode, mask ); - _eventControllerKey.get_widget().set_label(Gtk.accelerator_get_label(keyval, mask)); + const accelLabel = Gtk.accelerator_get_label(keyval, mask); + _eventControllerKey.get_widget().set_label(accelLabel); + log(`${keyval} ${keycode} ${state} ${accelLabel}`); + return Gdk.EVENT_STOP; } diff --git a/ui/prefs-gtk4.ui b/ui/prefs-gtk4.ui index 2e7d7b2..01fa08e 100644 --- a/ui/prefs-gtk4.ui +++ b/ui/prefs-gtk4.ui @@ -92,6 +92,8 @@ True True + + From 838139865a106073c68b32ea5ecc69249306946d Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sat, 28 May 2022 19:42:43 +0800 Subject: [PATCH 07/49] Save shortcuts --- model/closeWindowsRules.js | 2 +- prefsCloseWindow.js | 69 +++++++++++++++++++++++++++++--------- ui/uiHelper.js | 2 ++ 3 files changed, 56 insertions(+), 17 deletions(-) diff --git a/model/closeWindowsRules.js b/model/closeWindowsRules.js index a2099cd..3108917 100644 --- a/model/closeWindowsRules.js +++ b/model/closeWindowsRules.js @@ -2,7 +2,7 @@ var CloseWindowsRules = class { type; // string, rule type, such as 'shortcut' - value; // string, the rule, such as 'Ctrl+Q' (TODO, string may not suitable) + value; // object, order and the rule pairs, such as "{1: 'Ctrl+Q}'" appId; // string, such as 'firefox.desktop' appName; // string, such as 'Firefox' appDesktopFilePath; // string, such as '/usr/share/applications/firefox.desktop' diff --git a/prefsCloseWindow.js b/prefsCloseWindow.js index d760464..337b211 100644 --- a/prefsCloseWindow.js +++ b/prefsCloseWindow.js @@ -83,7 +83,7 @@ var UICloseWindows = GObject.registerClass( if (appInfo) { const closeWindowsRules = new CloseWindowsRules.CloseWindowsRules(); closeWindowsRules.type = 'shortcut'; - closeWindowsRules.value = ''; + closeWindowsRules.value = {}; closeWindowsRules.appId = appInfo.get_id(); closeWindowsRules.appName = appInfo.get_name(); closeWindowsRules.appDesktopFilePath = appInfo.get_filename(); @@ -132,9 +132,10 @@ var UICloseWindows = GObject.registerClass( const appInfo = row ? null : Gio.DesktopAppInfo.new_from_filename(ruleDetail.appDesktopFilePath); - if (row) - row.set({ value: GLib.Variant.new_strv([ruleDetail.value]) }); - else if (appInfo) { + if (row) { + // TODO + // row.set({ value: GLib.Variant.new_strv(ruleDetail.value) }); + } else if (appInfo) { const newRuleRow = new RuleRow(appInfo, ruleDetail); this.close_by_rules_list_box.insert(newRuleRow, index); } @@ -200,6 +201,8 @@ const RuleRow = GObject.registerClass({ }, class RuleRow extends Gtk.ListBoxRow { _init(appInfo, ruleDetail) { this._log = new Log.Log(); + this._prefsUtils = new PrefsUtils.PrefsUtils(); + this._settings = this._prefsUtils.getSettings(); const box = new Gtk.Box({ spacing: 6, @@ -212,7 +215,8 @@ const RuleRow = GObject.registerClass({ super._init({ activatable: false, - value: GLib.Variant.new_strv([ruleDetail.value]), + // TODO + // value: GLib.Variant.new_strv(ruleDetail.value), child: box, }); this._appInfo = appInfo; @@ -325,7 +329,7 @@ const RuleRow = GObject.registerClass({ rendererAccel = new Gtk.ShortcutLabel(); rendererAccel.accelerator = "w"; this._rendererAccelBox.append(rendererAccel); - + const addAccelButton = new Gtk.Button({ label: 'Add accelerator', }); @@ -362,7 +366,7 @@ const RuleRow = GObject.registerClass({ const focused = newAccelButton.grab_focus(); this._log.debug(`Grab the focus for setting the accelerator: ${focused}`); }); - + const box = new Gtk.Box(); box.append(this._rendererAccelBox); @@ -375,8 +379,11 @@ const RuleRow = GObject.registerClass({ } _onKeyPressed(_eventControllerKey, keyval, keycode, state) { + log(`1 ${keyval} ${keycode} ${state} ${Gtk.accelerator_get_default_mod_mask()}`); let mask = state & Gtk.accelerator_get_default_mod_mask(); + log(`2 ${keyval} ${keycode} ${state} ${mask}`); mask &= ~Gdk.ModifierType.LOCK_MASK; + log(`3 ${keyval} ${keycode} ${state} ${mask}`); // Backspace resets the new shortcut if (mask === 0 && keyval === Gdk.KEY_BackSpace) { @@ -386,19 +393,49 @@ const RuleRow = GObject.registerClass({ } if (!Gtk.accelerator_valid(keyval, mask)) return Gdk.EVENT_STOP; - const accel = Gtk.accelerator_name_with_keycode( - null, - keyval, - keycode, - mask - ); - const accelLabel = Gtk.accelerator_get_label(keyval, mask); - _eventControllerKey.get_widget().set_label(accelLabel); - log(`${keyval} ${keycode} ${state} ${accelLabel}`); + const shortCut = Gtk.accelerator_get_label(keyval, mask); + _eventControllerKey.get_widget().set_label(shortCut); + log(`5 ${keyval} ${keycode} ${state} ${mask} ${shortCut}`); + + const oldCloseWindowsRules = this._settings.get_string('close-windows-rules'); + let oldCloseWindowsRulesObj = JSON.parse(oldCloseWindowsRules); + const ruleValues = oldCloseWindowsRulesObj[this.appDesktopFilePath].value; + ruleValues[this.get_n_accelerators(this._rendererAccelBox)] = shortCut; + const newCloseWindowsRules = JSON.stringify(oldCloseWindowsRulesObj); + this._settings.set_string('close-windows-rules', newCloseWindowsRules); return Gdk.EVENT_STOP; } + /** + * + * @param {Gtk.Widget} widget + * @returns The amount of the underlying children in a widget + */ + get_n_accelerators(widget) { + if (!widget) { + return 0; + } + + const firstChild = widget.get_first_child(); + if (!firstChild) { + return 0; + } + + // 1 for the first child + let count = 1; + let next = firstChild.get_next_sibling(); + while (next != null) { + if (next.label !== '→') { + count++; + } + next = next.get_next_sibling(); + } + + return count; + } + + get enabled() { return this._ruleDetail.enabled; } diff --git a/ui/uiHelper.js b/ui/uiHelper.js index c68ec9d..3902fd0 100644 --- a/ui/uiHelper.js +++ b/ui/uiHelper.js @@ -1,3 +1,5 @@ +'use strict'; + const { Meta } = imports.gi; function isDialog(metaWindow) { From eecdd51c67285448fbaee0f47a8426a413b51d03 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sat, 28 May 2022 20:03:04 +0800 Subject: [PATCH 08/49] Improve the structure that stores shortcuts --- prefsCloseWindow.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prefsCloseWindow.js b/prefsCloseWindow.js index 337b211..57e6076 100644 --- a/prefsCloseWindow.js +++ b/prefsCloseWindow.js @@ -400,7 +400,7 @@ const RuleRow = GObject.registerClass({ const oldCloseWindowsRules = this._settings.get_string('close-windows-rules'); let oldCloseWindowsRulesObj = JSON.parse(oldCloseWindowsRules); const ruleValues = oldCloseWindowsRulesObj[this.appDesktopFilePath].value; - ruleValues[this.get_n_accelerators(this._rendererAccelBox)] = shortCut; + ruleValues[this.get_n_accelerators(this._rendererAccelBox)] = {shortCut: shortCut}; const newCloseWindowsRules = JSON.stringify(oldCloseWindowsRulesObj); this._settings.set_string('close-windows-rules', newCloseWindowsRules); From 0172985f92f727993c0dff0ff8388b686c409fee Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sat, 28 May 2022 20:34:16 +0800 Subject: [PATCH 09/49] Typo --- prefsCloseWindow.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/prefsCloseWindow.js b/prefsCloseWindow.js index 57e6076..fb0a489 100644 --- a/prefsCloseWindow.js +++ b/prefsCloseWindow.js @@ -393,14 +393,14 @@ const RuleRow = GObject.registerClass({ } if (!Gtk.accelerator_valid(keyval, mask)) return Gdk.EVENT_STOP; - const shortCut = Gtk.accelerator_get_label(keyval, mask); - _eventControllerKey.get_widget().set_label(shortCut); - log(`5 ${keyval} ${keycode} ${state} ${mask} ${shortCut}`); + const shortcut = Gtk.accelerator_get_label(keyval, mask); + _eventControllerKey.get_widget().set_label(shortcut); + log(`5 ${keyval} ${keycode} ${state} ${mask} ${shortcut}`); const oldCloseWindowsRules = this._settings.get_string('close-windows-rules'); let oldCloseWindowsRulesObj = JSON.parse(oldCloseWindowsRules); const ruleValues = oldCloseWindowsRulesObj[this.appDesktopFilePath].value; - ruleValues[this.get_n_accelerators(this._rendererAccelBox)] = {shortCut: shortCut}; + ruleValues[this.get_n_accelerators(this._rendererAccelBox)] = {shortcut: shortcut}; const newCloseWindowsRules = JSON.stringify(oldCloseWindowsRulesObj); this._settings.set_string('close-windows-rules', newCloseWindowsRules); From 83be54705e4527c55a72ed8a3ed0ec99a497b78a Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sat, 28 May 2022 23:03:56 +0800 Subject: [PATCH 10/49] Display shortcuts in UI --- prefsCloseWindow.js | 60 +++++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/prefsCloseWindow.js b/prefsCloseWindow.js index fb0a489..bb19928 100644 --- a/prefsCloseWindow.js +++ b/prefsCloseWindow.js @@ -305,30 +305,27 @@ const RuleRow = GObject.registerClass({ }); const rules = this._ruleDetail.value; - - let rendererAccel = new Gtk.ShortcutLabel(); - rendererAccel.accelerator = "Escape"; - this._rendererAccelBox.append(rendererAccel); - - let next = new Gtk.Label({ - label: '→', - halign: Gtk.Align.CENTER - }); - this._rendererAccelBox.append(next); - - rendererAccel = new Gtk.ShortcutLabel(); - rendererAccel.accelerator = "Q"; - this._rendererAccelBox.append(rendererAccel); - - next = new Gtk.Label({ - label: '→', - halign: Gtk.Align.CENTER, - }); - this._rendererAccelBox.append(next); - - rendererAccel = new Gtk.ShortcutLabel(); - rendererAccel.accelerator = "w"; - this._rendererAccelBox.append(rendererAccel); + const ruleOrders = Object.keys(rules); + const maxRuleOrder = Math.max(...ruleOrders); + for (const ruleOrder of ruleOrders) { + const rule = rules[ruleOrder]; + const shortcut = rule.shortcut; + const accelButton = new Gtk.Button({ + label: shortcut, + }); + accelButton._rule = rule; + const eventControllerKey = new Gtk.EventControllerKey(); + accelButton.add_controller(eventControllerKey); + eventControllerKey.connect('key-pressed', this._onKeyPressed.bind(this)); + this._rendererAccelBox.append(accelButton); + if (ruleOrder < maxRuleOrder) { + let next = new Gtk.Label({ + label: '→', + halign: Gtk.Align.CENTER + }); + this._rendererAccelBox.append(next); + } + } const addAccelButton = new Gtk.Button({ label: 'Add accelerator', @@ -400,7 +397,17 @@ const RuleRow = GObject.registerClass({ const oldCloseWindowsRules = this._settings.get_string('close-windows-rules'); let oldCloseWindowsRulesObj = JSON.parse(oldCloseWindowsRules); const ruleValues = oldCloseWindowsRulesObj[this.appDesktopFilePath].value; - ruleValues[this.get_n_accelerators(this._rendererAccelBox)] = {shortcut: shortcut}; + const _rule = _eventControllerKey.get_widget()._rule; + let order; + if (_rule) { + order = _rule.order; + } else { + order = this.get_n_accelerators(this._rendererAccelBox); + } + ruleValues[order] = { + shortcut: shortcut, + order: order + }; const newCloseWindowsRules = JSON.stringify(oldCloseWindowsRulesObj); this._settings.set_string('close-windows-rules', newCloseWindowsRules); @@ -422,8 +429,7 @@ const RuleRow = GObject.registerClass({ return 0; } - // 1 for the first child - let count = 1; + let count = 0; let next = firstChild.get_next_sibling(); while (next != null) { if (next.label !== '→') { From f0c9a2bf09126ea03d52a8b47c7429a43d19dc43 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sun, 29 May 2022 00:32:54 +0800 Subject: [PATCH 11/49] Remove accelators --- prefsCloseWindow.js | 53 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/prefsCloseWindow.js b/prefsCloseWindow.js index bb19928..b72962f 100644 --- a/prefsCloseWindow.js +++ b/prefsCloseWindow.js @@ -223,7 +223,6 @@ const RuleRow = GObject.registerClass({ this._ruleDetail = ruleDetail; this._rendererAccelBox = null; - this._lastNextArrow = null; this._enabledCheckButton = new Gtk.CheckButton({ active: ruleDetail.enabled, @@ -344,19 +343,19 @@ const RuleRow = GObject.registerClass({ rendererAccelOptBox.append(deleteAccelButton); addAccelButton.connect('clicked', () => { - this._lastNextArrow = new Gtk.Label({ - label: '→', - halign: Gtk.Align.CENTER, - }); - + if (this._get_n_accelerators(this._rendererAccelBox) > 0) { + const lastNextArrow = new Gtk.Label({ + label: '→', + halign: Gtk.Align.CENTER, + }); + this._rendererAccelBox.append(lastNextArrow); + } const newAccelButton = new Gtk.Button({ label: 'New accelerator...', }); const eventControllerKey = new Gtk.EventControllerKey(); newAccelButton.add_controller(eventControllerKey); eventControllerKey.connect('key-pressed', this._onKeyPressed.bind(this)); - - this._rendererAccelBox.append(this._lastNextArrow); this._rendererAccelBox.append(newAccelButton); this._rendererAccelBox.get_root().get_surface().inhibit_system_shortcuts(null); @@ -384,8 +383,7 @@ const RuleRow = GObject.registerClass({ // Backspace resets the new shortcut if (mask === 0 && keyval === Gdk.KEY_BackSpace) { - this._rendererAccelBox.remove(this._lastNextArrow); - this._rendererAccelBox.remove(_eventControllerKey.get_widget()); + this._removeAcceleratorButtons(_eventControllerKey.get_widget()); return Gdk.EVENT_STOP; } @@ -402,7 +400,7 @@ const RuleRow = GObject.registerClass({ if (_rule) { order = _rule.order; } else { - order = this.get_n_accelerators(this._rendererAccelBox); + order = this._get_n_accelerators(this._rendererAccelBox); } ruleValues[order] = { shortcut: shortcut, @@ -414,12 +412,40 @@ const RuleRow = GObject.registerClass({ return Gdk.EVENT_STOP; } + _removeAcceleratorButtons(currentWidgetRemoved) { + const previousWidgetRemoved = currentWidgetRemoved.get_prev_sibling(); + const nextWidgetRemoved = currentWidgetRemoved.get_next_sibling(); + + // The current widget is in the middle + if (previousWidgetRemoved && nextWidgetRemoved) { + this._rendererAccelBox.remove(previousWidgetRemoved); + this._rendererAccelBox.remove(currentWidgetRemoved); + return; + } + + // Only one accelerator + if (!previousWidgetRemoved && !nextWidgetRemoved) { + this._rendererAccelBox.remove(currentWidgetRemoved); + return; + } + + // The current widget is in the beginning + if (!previousWidgetRemoved) { + this._rendererAccelBox.remove(nextWidgetRemoved); + } + // The current widget is in the last + if (!nextWidgetRemoved) { + this._rendererAccelBox.remove(previousWidgetRemoved); + } + this._rendererAccelBox.remove(currentWidgetRemoved); + } + /** * * @param {Gtk.Widget} widget * @returns The amount of the underlying children in a widget */ - get_n_accelerators(widget) { + _get_n_accelerators(widget) { if (!widget) { return 0; } @@ -429,7 +455,8 @@ const RuleRow = GObject.registerClass({ return 0; } - let count = 0; + // 1 for the first child + let count = 1; let next = firstChild.get_next_sibling(); while (next != null) { if (next.label !== '→') { From 3538fe1b0320aac615497cc685e80b7ecd9a8fba Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sun, 29 May 2022 03:10:49 +0800 Subject: [PATCH 12/49] Improvement: Remove and add accelators --- prefsCloseWindow.js | 45 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/prefsCloseWindow.js b/prefsCloseWindow.js index b72962f..1f9e463 100644 --- a/prefsCloseWindow.js +++ b/prefsCloseWindow.js @@ -353,6 +353,20 @@ const RuleRow = GObject.registerClass({ const newAccelButton = new Gtk.Button({ label: 'New accelerator...', }); + + let order; + const previousAcceleratorButton = this._rendererAccelBox.get_last_child()?.get_prev_sibling(); + if (previousAcceleratorButton) { + order = previousAcceleratorButton._rule.order + 1 + } else { + // The vert first accelerator... + order = 1; + } + + newAccelButton._rule = { + order: order, + }; + const eventControllerKey = new Gtk.EventControllerKey(); newAccelButton.add_controller(eventControllerKey); eventControllerKey.connect('key-pressed', this._onKeyPressed.bind(this)); @@ -383,7 +397,7 @@ const RuleRow = GObject.registerClass({ // Backspace resets the new shortcut if (mask === 0 && keyval === Gdk.KEY_BackSpace) { - this._removeAcceleratorButtons(_eventControllerKey.get_widget()); + this._removeAccelerator(_eventControllerKey.get_widget()); return Gdk.EVENT_STOP; } @@ -395,13 +409,8 @@ const RuleRow = GObject.registerClass({ const oldCloseWindowsRules = this._settings.get_string('close-windows-rules'); let oldCloseWindowsRulesObj = JSON.parse(oldCloseWindowsRules); const ruleValues = oldCloseWindowsRulesObj[this.appDesktopFilePath].value; - const _rule = _eventControllerKey.get_widget()._rule; - let order; - if (_rule) { - order = _rule.order; - } else { - order = this._get_n_accelerators(this._rendererAccelBox); - } + const _currentAcceleratorRule = _eventControllerKey.get_widget()._rule; + let order = _currentAcceleratorRule.order; ruleValues[order] = { shortcut: shortcut, order: order @@ -412,6 +421,26 @@ const RuleRow = GObject.registerClass({ return Gdk.EVENT_STOP; } + _removeAccelerator(currentWidgetRemoved) { + this._removeAcceleratorSettings(currentWidgetRemoved); + this._removeAcceleratorButtons(currentWidgetRemoved); + } + + _removeAcceleratorSettings(currentWidgetRemoved) { + const _rule = currentWidgetRemoved._rule; + if (!_rule) { + return; + } + + const order =_rule.order; + const oldCloseWindowsRules = this._settings.get_string('close-windows-rules'); + let oldCloseWindowsRulesObj = JSON.parse(oldCloseWindowsRules); + const ruleValues = oldCloseWindowsRulesObj[this.appDesktopFilePath].value; + delete ruleValues[order]; + const newCloseWindowsRules = JSON.stringify(oldCloseWindowsRulesObj); + this._settings.set_string('close-windows-rules', newCloseWindowsRules); + } + _removeAcceleratorButtons(currentWidgetRemoved) { const previousWidgetRemoved = currentWidgetRemoved.get_prev_sibling(); const nextWidgetRemoved = currentWidgetRemoved.get_next_sibling(); From 05ad194492ef35c1335110c15496e3848f45bfdf Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sun, 29 May 2022 03:12:34 +0800 Subject: [PATCH 13/49] Remove logs --- prefsCloseWindow.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/prefsCloseWindow.js b/prefsCloseWindow.js index 1f9e463..e49d844 100644 --- a/prefsCloseWindow.js +++ b/prefsCloseWindow.js @@ -389,11 +389,8 @@ const RuleRow = GObject.registerClass({ } _onKeyPressed(_eventControllerKey, keyval, keycode, state) { - log(`1 ${keyval} ${keycode} ${state} ${Gtk.accelerator_get_default_mod_mask()}`); let mask = state & Gtk.accelerator_get_default_mod_mask(); - log(`2 ${keyval} ${keycode} ${state} ${mask}`); mask &= ~Gdk.ModifierType.LOCK_MASK; - log(`3 ${keyval} ${keycode} ${state} ${mask}`); // Backspace resets the new shortcut if (mask === 0 && keyval === Gdk.KEY_BackSpace) { @@ -404,7 +401,6 @@ const RuleRow = GObject.registerClass({ if (!Gtk.accelerator_valid(keyval, mask)) return Gdk.EVENT_STOP; const shortcut = Gtk.accelerator_get_label(keyval, mask); _eventControllerKey.get_widget().set_label(shortcut); - log(`5 ${keyval} ${keycode} ${state} ${mask} ${shortcut}`); const oldCloseWindowsRules = this._settings.get_string('close-windows-rules'); let oldCloseWindowsRulesObj = JSON.parse(oldCloseWindowsRules); From d9bee1edf4e0b294c5f8a5b243fd027768253e84 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sun, 29 May 2022 03:28:15 +0800 Subject: [PATCH 14/49] Grab the focus on the previous widget after removing an accelerator button --- prefsCloseWindow.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/prefsCloseWindow.js b/prefsCloseWindow.js index e49d844..526cea1 100644 --- a/prefsCloseWindow.js +++ b/prefsCloseWindow.js @@ -443,6 +443,7 @@ const RuleRow = GObject.registerClass({ // The current widget is in the middle if (previousWidgetRemoved && nextWidgetRemoved) { + previousWidgetRemoved.get_prev_sibling().grab_focus(); this._rendererAccelBox.remove(previousWidgetRemoved); this._rendererAccelBox.remove(currentWidgetRemoved); return; @@ -460,6 +461,7 @@ const RuleRow = GObject.registerClass({ } // The current widget is in the last if (!nextWidgetRemoved) { + previousWidgetRemoved.get_prev_sibling().grab_focus(); this._rendererAccelBox.remove(previousWidgetRemoved); } this._rendererAccelBox.remove(currentWidgetRemoved); From 7a746e9aa9909cd5833765fd50b385b32b4e5f3e Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sun, 29 May 2022 03:41:57 +0800 Subject: [PATCH 15/49] Delete accelerator --- prefsCloseWindow.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/prefsCloseWindow.js b/prefsCloseWindow.js index 526cea1..0307514 100644 --- a/prefsCloseWindow.js +++ b/prefsCloseWindow.js @@ -330,7 +330,8 @@ const RuleRow = GObject.registerClass({ label: 'Add accelerator', }); const deleteAccelButton = new Gtk.Button({ - label: 'Delete', + label: 'Delete accelerator', + icon_name: 'edit-clear-symbolic', }); const rendererAccelOptBox = new Gtk.Box({ spacing: 6, @@ -376,7 +377,10 @@ const RuleRow = GObject.registerClass({ const focused = newAccelButton.grab_focus(); this._log.debug(`Grab the focus for setting the accelerator: ${focused}`); }); - + // Delete from the last accelerator button + deleteAccelButton.connect('clicked', () => { + this._removeAccelerator(this._rendererAccelBox.get_last_child()); + }); const box = new Gtk.Box(); box.append(this._rendererAccelBox); @@ -418,6 +422,9 @@ const RuleRow = GObject.registerClass({ } _removeAccelerator(currentWidgetRemoved) { + if (!currentWidgetRemoved) { + return; + } this._removeAcceleratorSettings(currentWidgetRemoved); this._removeAcceleratorButtons(currentWidgetRemoved); } From 6ed1f3d8005f12133c70d5e5fba56fb8b4e12352 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sun, 29 May 2022 12:22:22 +0800 Subject: [PATCH 16/49] Close windows by sending shortcus via xdotool, not working for Ctrl+Q on firefox though, looks xdotool is case-sensitive --- closeSession.js | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/closeSession.js b/closeSession.js index 3fd3ac0..544407e 100644 --- a/closeSession.js +++ b/closeSession.js @@ -55,23 +55,32 @@ var CloseSession = class { return false; } - const _rules = this._prefsUtils.getSettingString('close-windows-rules'); - const _rulesMap = new Map(JSON.parse(_rules)); - const shortCut = _rulesMap.get(`${app.get_id()}:${app.get_name()}`); - if (shortCut) { - const windows = app.get_windows(); - if (windows.length) { - Main.activateWindow(windows[0]); - } else { - app.activate(global.get_current_time()); - } - const cmd = `ydotool key ${shortCut}` - this._log.info(`Closing the app ${app.get_name()} by sending a shortcut ${shortCut}: ${cmd}`); - Util.trySpawnCommandLine(`${cmd}`); - return true; + const closeWindowsRules = this._prefsUtils.getSettingString('close-windows-rules'); + const closeWindowsRulesObj = JSON.parse(closeWindowsRules); + const rules = closeWindowsRulesObj[app.get_app_info()?.get_filename()]; + if (!rules) { + return false; } - return false; + let success = false; + if (rules.type === 'shortcut') { + for (const order in rules.value) { + const rule = rules.value[order]; + const shortcut = rule.shortcut; + const windows = app.get_windows(); + if (windows.length) { + Main.activateWindow(windows[0]); + } else { + app.activate(global.get_current_time()); + } + const cmd = `xdotool key ${shortcut}` + this._log.info(`Closing the app ${app.get_name()} by sending a shortcut ${shortcut}: ${cmd}`); + Util.trySpawnCommandLine(`${cmd}`); + success = true; + } + + } + return success; } _skip_multiple_windows(shellApp) { From 0233be36bca9b5ec0fc89e2510dd6cf1d2ab795f Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sun, 29 May 2022 18:30:52 +0800 Subject: [PATCH 17/49] Lower letters if necessary --- closeSession.js | 12 +++++++++++- constants.js | 7 +++++++ model/closeWindowsRules.js | 23 ++++++++++++++++++++++- prefsCloseWindow.js | 3 +++ 4 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 constants.js diff --git a/closeSession.js b/closeSession.js index 544407e..a6a4da6 100644 --- a/closeSession.js +++ b/closeSession.js @@ -11,12 +11,16 @@ const Me = ExtensionUtils.getCurrentExtension(); const Log = Me.imports.utils.log; const PrefsUtils = Me.imports.utils.prefsUtils; +const Constants = Me.imports.constants; + var enable_close_by_rules = true; var CloseSession = class { constructor() { this._log = new Log.Log(); this._prefsUtils = new PrefsUtils.PrefsUtils(); + this._settings = this._prefsUtils.getSettings(); + this._enable_close_by_rules = this._settings.get_boolean('close-by-rules'); this._skip_app_with_multiple_windows = true; this._defaultAppSystem = Shell.AppSystem.get_default(); @@ -66,7 +70,13 @@ var CloseSession = class { if (rules.type === 'shortcut') { for (const order in rules.value) { const rule = rules.value[order]; - const shortcut = rule.shortcut; + let shortcut = rule.shortcut; + // The shift key is not pressed + if (!(rule.state & Constants.GDK_SHIFT_MASK)) { + const keys = shortcut.split('+'); + keys[keys.length - 1] = keys[keys.length - 1].toLowerCase(); + shortcut = keys.join('+'); + } const windows = app.get_windows(); if (windows.length) { Main.activateWindow(windows[0]); diff --git a/constants.js b/constants.js new file mode 100644 index 0000000..bbeae87 --- /dev/null +++ b/constants.js @@ -0,0 +1,7 @@ +/** + * GdkModifierType: the Shift key + * + * See: https://gitlab.gnome.org/GNOME/gtk/blob/d726ecdb5d1ece870585c7be89eb6355b2482544/gdk/gdkenums.h:L74 + */ +var GDK_SHIFT_MASK = 1 << 0; + diff --git a/model/closeWindowsRules.js b/model/closeWindowsRules.js index 3108917..5097382 100644 --- a/model/closeWindowsRules.js +++ b/model/closeWindowsRules.js @@ -2,9 +2,30 @@ var CloseWindowsRules = class { type; // string, rule type, such as 'shortcut' - value; // object, order and the rule pairs, such as "{1: 'Ctrl+Q}'" + value; // GdkShortcuts, order and the rule pairs, such as "{1: 'Ctrl+Q}'". appId; // string, such as 'firefox.desktop' appName; // string, such as 'Firefox' appDesktopFilePath; // string, such as '/usr/share/applications/firefox.desktop' enabled; // boolean } + +/** +* See: https://gitlab.gnome.org/GNOME/gtk/blob/d726ecdb5d1ece870585c7be89eb6355b2482544/gdk/gdkenums.h:L73 +* See: https://gitlab.gnome.org/GNOME/gtk/blob/1ce79b29e363e585872901424d3b72041b55e3e4/gtk/gtkeventcontrollerkey.c:L203 +*/ +var GdkShortcuts = class { + shortcut; + order; + /** + * the pressed key. + */ + keyval; + /** + * the raw code of the pressed key. + */ + keycode; + /** + * the bitmask, representing the state of modifier keys and pointer buttons. See `GdkModifierType` in Gtk source. + */ + state; +} \ No newline at end of file diff --git a/prefsCloseWindow.js b/prefsCloseWindow.js index 0307514..f5ed5ba 100644 --- a/prefsCloseWindow.js +++ b/prefsCloseWindow.js @@ -413,6 +413,9 @@ const RuleRow = GObject.registerClass({ let order = _currentAcceleratorRule.order; ruleValues[order] = { shortcut: shortcut, + keyval: keyval, + keycode: keycode, + state: state, order: order }; const newCloseWindowsRules = JSON.stringify(oldCloseWindowsRulesObj); From 259dbf1feeeb9dfade4bc330d0ad5061f787f59a Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sun, 29 May 2022 18:47:07 +0800 Subject: [PATCH 18/49] Enable closing the app with mutiple windows by rules / shortcuts --- closeSession.js | 4 +--- prefs.js | 5 +---- schemas/gschemas.compiled | Bin 788 -> 796 bytes ...s.another-window-session-manager.gschema.xml | 2 +- 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/closeSession.js b/closeSession.js index a6a4da6..85dc43c 100644 --- a/closeSession.js +++ b/closeSession.js @@ -13,14 +13,12 @@ const PrefsUtils = Me.imports.utils.prefsUtils; const Constants = Me.imports.constants; -var enable_close_by_rules = true; var CloseSession = class { constructor() { this._log = new Log.Log(); this._prefsUtils = new PrefsUtils.PrefsUtils(); this._settings = this._prefsUtils.getSettings(); - this._enable_close_by_rules = this._settings.get_boolean('close-by-rules'); this._skip_app_with_multiple_windows = true; this._defaultAppSystem = Shell.AppSystem.get_default(); @@ -55,7 +53,7 @@ var CloseSession = class { } _tryCloseByRules(app) { - if (!enable_close_by_rules) { + if (!this._settings.get_boolean('enable-close-by-rules')) { return false; } diff --git a/prefs.js b/prefs.js index 03b937c..2fa18b6 100644 --- a/prefs.js +++ b/prefs.js @@ -77,7 +77,7 @@ const Prefs = GObject.registerClass( ); this._settings.bind( - 'close-by-rules', + 'enable-close-by-rules', this.close_by_rules_switch, 'active', Gio.SettingsBindFlags.DEFAULT @@ -99,9 +99,6 @@ const Prefs = GObject.registerClass( this._installAutostartDesktopFile(); }); - this._settings.connect('changed::close-by-rules', (settings) => { - log('cccc'); - }); } render_ui() { diff --git a/schemas/gschemas.compiled b/schemas/gschemas.compiled index b662fa25a0ae6aaef23f105e35d423582243af54..6f196afc87eee7982ca8773d8934ee44cf970425 100644 GIT binary patch delta 166 zcmbQjHivD32d_9Y0}%KyNC0V>iJ|{v`!iW4G6IFBF)=WRGL$iF0Mc85SZoi|5s)~D ztq2sq0i^E&@iFH*F_1WjEdmt(0i^!|vG(;;8<04NEesSF09qhEc_X7SFDFox1&Bd5 lvrYcUxKu1PFEJ@6RW~^&zc^JlsZzJ7G$*x~Ve%5D0sx$7BcA{O delta 158 zcmbQkHid112QNP}0}%Ky2moo3iJ|{vtta`uW&{dNV`5<7V<=--0i@RgvDhA_BOq}Q zTM;OJ0Z3m3;$zNrVjyu4TLdWn0Z4xZV(sgxHXv~jTNo(L0knXB@ - + true Enable or disable the ability of closing windows by customed rules, such as a shortcut From c57a01e257c83e9a7fdaeaaa6030c848179de8d0 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sun, 29 May 2022 22:16:23 +0800 Subject: [PATCH 19/49] Enable or diable a rule --- closeSession.js | 2 +- prefsCloseWindow.js | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/closeSession.js b/closeSession.js index 85dc43c..8553a7b 100644 --- a/closeSession.js +++ b/closeSession.js @@ -60,7 +60,7 @@ var CloseSession = class { const closeWindowsRules = this._prefsUtils.getSettingString('close-windows-rules'); const closeWindowsRulesObj = JSON.parse(closeWindowsRules); const rules = closeWindowsRulesObj[app.get_app_info()?.get_filename()]; - if (!rules) { + if (!rules || !rules.enabled) { return false; } diff --git a/prefsCloseWindow.js b/prefsCloseWindow.js index f5ed5ba..b5e9261 100644 --- a/prefsCloseWindow.js +++ b/prefsCloseWindow.js @@ -277,19 +277,21 @@ const RuleRow = GObject.registerClass({ icon_name: 'edit-delete-symbolic', }); box.append(buttonRemove); - + + // TODO Not used, can be deleted? this.connect('notify::value', () => this.activate_action('rules.update', new GLib.Variant('a{sv}', { appDesktopFilePath: GLib.Variant.new_string(this.appDesktopFilePath), enabled: GLib.Variant.new_boolean(this._enabledCheckButton.get_active()), - value: this.value, + // value: this.value, }))); + this.connect('notify::enabled', () => { this.activate_action('rules.update', new GLib.Variant('a{sv}', { appDesktopFilePath: GLib.Variant.new_string(this.appDesktopFilePath), enabled: GLib.Variant.new_boolean(this._enabledCheckButton.get_active()), - value: this.value, + // value: this.value, })) }); } From 0c0a99ecc380cda034108857c3befd50cecc769f Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sun, 29 May 2022 22:18:52 +0800 Subject: [PATCH 20/49] Clean code --- prefs.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/prefs.js b/prefs.js index 2fa18b6..6351c26 100644 --- a/prefs.js +++ b/prefs.js @@ -132,14 +132,6 @@ const Prefs = GObject.registerClass( const active = widget.active; this.timer_on_the_autostart_dialog_spinbutton.set_sensitive(!active); }); - - this.close_rule_tree_view = this._builder.get_object('close_rule_tree_view'); - this.close_by_rules_switch = this._builder.get_object('close_by_rules_switch'); - this.close_by_rules_switch.connect('notify::active', (widget) => { - const active = widget.active; - log('xxxxx'); - }); - } From 26e60a56396419ebcafd2bc35ac54008df257709 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sun, 29 May 2022 22:35:36 +0800 Subject: [PATCH 21/49] log --- indicator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indicator.js b/indicator.js index 6a98208..f52f3f1 100644 --- a/indicator.js +++ b/indicator.js @@ -264,7 +264,7 @@ class AwsIndicator extends PanelMenu.Button { _addSessionItems() { if (!GLib.file_test(this._sessions_path, GLib.FileTest.EXISTS)) { // TODO Empty session - log(`${this._sessions_path} not found! It's harmless, please save some windows in the panel menu to create it automatically.`); + this._log.info(`${this._sessions_path} not found! It's harmless, please save some windows in the panel menu to create it automatically.`); return; } From ab63be44bbdf788f13fc1c558ce5207c6726e3b8 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Mon, 30 May 2022 00:30:55 +0800 Subject: [PATCH 22/49] TODO Add new accelerator automatically after adding a new rule --- prefs.js | 2 ++ prefsCloseWindow.js | 83 +++++++++++++++++++++++++-------------------- 2 files changed, 49 insertions(+), 36 deletions(-) diff --git a/prefs.js b/prefs.js index 6351c26..0433ef0 100644 --- a/prefs.js +++ b/prefs.js @@ -133,6 +133,8 @@ const Prefs = GObject.registerClass( this.timer_on_the_autostart_dialog_spinbutton.set_sensitive(!active); }); + this.close_by_rules_switch = this._builder.get_object('close_by_rules_switch'); + } _installAutostartDesktopFile() { diff --git a/prefsCloseWindow.js b/prefsCloseWindow.js index b5e9261..5cfdb8e 100644 --- a/prefsCloseWindow.js +++ b/prefsCloseWindow.js @@ -70,7 +70,11 @@ var UICloseWindows = GObject.registerClass( this._rulesChangedId = this._settings.connect( 'changed::close-windows-rules', - this._sync.bind(this)); + (settings) => { + // TODO Add new accelerator automatically after adding a new rule + // this._sync(true); + this._sync(); + }); this._sync(); } @@ -115,7 +119,7 @@ var UICloseWindows = GObject.registerClass( return [...this.close_by_rules_list_box].filter(row => !!row.appDesktopFilePath); } - _sync() { + _sync(autoNewAccelerator = false) { const oldRules = this._getRuleRows(); const newRules = JSON.parse(this._settings.get_string('close-windows-rules')); @@ -138,6 +142,11 @@ var UICloseWindows = GObject.registerClass( } else if (appInfo) { const newRuleRow = new RuleRow(appInfo, ruleDetail); this.close_by_rules_list_box.insert(newRuleRow, index); + if (autoNewAccelerator) { + // TODO Fix the below error when autoNewAccelerator is true in the case of adding the new accelerator button after adding a new rule: + // this._rendererAccelBox.get_root().get_surface() is null + newRuleRow._addNewAccel(); + } } } @@ -345,40 +354,7 @@ const RuleRow = GObject.registerClass({ rendererAccelOptBox.append(addAccelButton); rendererAccelOptBox.append(deleteAccelButton); - addAccelButton.connect('clicked', () => { - if (this._get_n_accelerators(this._rendererAccelBox) > 0) { - const lastNextArrow = new Gtk.Label({ - label: '→', - halign: Gtk.Align.CENTER, - }); - this._rendererAccelBox.append(lastNextArrow); - } - const newAccelButton = new Gtk.Button({ - label: 'New accelerator...', - }); - - let order; - const previousAcceleratorButton = this._rendererAccelBox.get_last_child()?.get_prev_sibling(); - if (previousAcceleratorButton) { - order = previousAcceleratorButton._rule.order + 1 - } else { - // The vert first accelerator... - order = 1; - } - - newAccelButton._rule = { - order: order, - }; - - const eventControllerKey = new Gtk.EventControllerKey(); - newAccelButton.add_controller(eventControllerKey); - eventControllerKey.connect('key-pressed', this._onKeyPressed.bind(this)); - this._rendererAccelBox.append(newAccelButton); - - this._rendererAccelBox.get_root().get_surface().inhibit_system_shortcuts(null); - const focused = newAccelButton.grab_focus(); - this._log.debug(`Grab the focus for setting the accelerator: ${focused}`); - }); + addAccelButton.connect('clicked', this._addNewAccel.bind(this)); // Delete from the last accelerator button deleteAccelButton.connect('clicked', () => { this._removeAccelerator(this._rendererAccelBox.get_last_child()); @@ -394,6 +370,41 @@ const RuleRow = GObject.registerClass({ this._box.append(frame); } + _addNewAccel() { + if (this._get_n_accelerators(this._rendererAccelBox) > 0) { + const lastNextArrow = new Gtk.Label({ + label: '→', + halign: Gtk.Align.CENTER, + }); + this._rendererAccelBox.append(lastNextArrow); + } + const newAccelButton = new Gtk.Button({ + label: 'New accelerator...', + }); + + let order; + const previousAcceleratorButton = this._rendererAccelBox.get_last_child()?.get_prev_sibling(); + if (previousAcceleratorButton) { + order = previousAcceleratorButton._rule.order + 1 + } else { + // The vert first accelerator... + order = 1; + } + + newAccelButton._rule = { + order: order, + }; + + const eventControllerKey = new Gtk.EventControllerKey(); + newAccelButton.add_controller(eventControllerKey); + eventControllerKey.connect('key-pressed', this._onKeyPressed.bind(this)); + this._rendererAccelBox.append(newAccelButton); + + this._rendererAccelBox.get_root().get_surface().inhibit_system_shortcuts(null); + const focused = newAccelButton.grab_focus(); + this._log.debug(`Grab the focus for setting the accelerator: ${focused}`); + } + _onKeyPressed(_eventControllerKey, keyval, keycode, state) { let mask = state & Gtk.accelerator_get_default_mod_mask(); mask &= ~Gdk.ModifierType.LOCK_MASK; From 056a5b2b09a444405381839323c2a4c6720872f8 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Mon, 30 May 2022 00:52:34 +0800 Subject: [PATCH 23/49] Add TODO --- prefsCloseWindow.js | 1 + 1 file changed, 1 insertion(+) diff --git a/prefsCloseWindow.js b/prefsCloseWindow.js index 5cfdb8e..0784fd2 100644 --- a/prefsCloseWindow.js +++ b/prefsCloseWindow.js @@ -400,6 +400,7 @@ const RuleRow = GObject.registerClass({ eventControllerKey.connect('key-pressed', this._onKeyPressed.bind(this)); this._rendererAccelBox.append(newAccelButton); + // TODO Calling this._rendererAccelBox.get_root().get_surface().restore_system_shortcuts(null); after this? this._rendererAccelBox.get_root().get_surface().inhibit_system_shortcuts(null); const focused = newAccelButton.grab_focus(); this._log.debug(`Grab the focus for setting the accelerator: ${focused}`); From 986c5b16bf730313e32856ce38ad7bc7b07e7d59 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Wed, 1 Jun 2022 01:13:22 +0800 Subject: [PATCH 24/49] don't lower when there is only one word or letter --- closeSession.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/closeSession.js b/closeSession.js index 8553a7b..a6635ec 100644 --- a/closeSession.js +++ b/closeSession.js @@ -72,8 +72,12 @@ var CloseSession = class { // The shift key is not pressed if (!(rule.state & Constants.GDK_SHIFT_MASK)) { const keys = shortcut.split('+'); - keys[keys.length - 1] = keys[keys.length - 1].toLowerCase(); - shortcut = keys.join('+'); + const lastKey = keys[keys.length - 1]; + // Only handle letters which the length is 1, ignoring keys like Return, Escape etc. + if (lastKey.length === 1) { + keys[keys.length - 1] = lastKey.toLowerCase(); + shortcut = keys.join('+'); + } } const windows = app.get_windows(); if (windows.length) { From 69b07e4448fd3ba853f4999f99a9f18d68ea5cb8 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Thu, 2 Jun 2022 01:11:50 +0800 Subject: [PATCH 25/49] Leave the overview first, so the keys can be sent to the activated windows to close it --- closeSession.js | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/closeSession.js b/closeSession.js index a6635ec..27a6347 100644 --- a/closeSession.js +++ b/closeSession.js @@ -79,15 +79,17 @@ var CloseSession = class { shortcut = keys.join('+'); } } - const windows = app.get_windows(); - if (windows.length) { - Main.activateWindow(windows[0]); + // Leave the overview first, so the keys can be sent to the activated windows + if (Main.overview.visible) { + Main.overview.hide(); + const hiddenId = Main.overview.connect('hidden', + () => { + Main.overview.disconnect(hiddenId); + this._activateAndCloseWindows(app, shortcut); + }); } else { - app.activate(global.get_current_time()); + this._activateAndCloseWindows(app, shortcut); } - const cmd = `xdotool key ${shortcut}` - this._log.info(`Closing the app ${app.get_name()} by sending a shortcut ${shortcut}: ${cmd}`); - Util.trySpawnCommandLine(`${cmd}`); success = true; } @@ -95,6 +97,18 @@ var CloseSession = class { return success; } + _activateAndCloseWindows(app, shortcut) { + const windows = app.get_windows(); + if (windows.length) { + Main.activateWindow(windows[0]); + } else { + app.activate(global.get_current_time()); + } + const cmd = `xdotool key ${shortcut}`; + this._log.info(`Closing the app ${app.get_name()} by sending a shortcut ${shortcut}: ${cmd}`); + Util.trySpawnCommandLine(`${cmd}`); + } + _skip_multiple_windows(shellApp) { if (shellApp.get_n_windows() > 1 && this._skip_app_with_multiple_windows) { const app_id = shellApp.get_id(); From 82f7907e4d87b5a5ac26cac1c68021db10d3b342 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Fri, 3 Jun 2022 23:18:22 +0800 Subject: [PATCH 26/49] Close windows by definded shortcuts by users --- closeSession.js | 171 +++++++++++++++++++++++-- prefsCloseWindow.js | 2 +- saveSession.js | 2 +- utils/fileUtils.js | 18 +-- utils/subprocessUtils.js | 268 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 437 insertions(+), 24 deletions(-) create mode 100644 utils/subprocessUtils.js diff --git a/closeSession.js b/closeSession.js index 27a6347..baf6b01 100644 --- a/closeSession.js +++ b/closeSession.js @@ -1,8 +1,9 @@ 'use strict'; -const { Shell } = imports.gi; +const { Shell, Gio, GLib } = imports.gi; const Main = imports.ui.main; +const Scripting = imports.ui.scripting; const Util = imports.misc.util; @@ -10,6 +11,7 @@ const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); const Log = Me.imports.utils.log; const PrefsUtils = Me.imports.utils.prefsUtils; +const SubprocessUtils = Me.imports.utils.subprocessUtils; const Constants = Me.imports.constants; @@ -22,6 +24,11 @@ var CloseSession = class { this._skip_app_with_multiple_windows = true; this._defaultAppSystem = Shell.AppSystem.get_default(); + + this._subprocessLauncher = new Gio.SubprocessLauncher({ + flags: (Gio.SubprocessFlags.STDOUT_PIPE | + Gio.SubprocessFlags.STDERR_PIPE)}); + // TODO Put into Settings // All apps in the whitelist should be closed safely, no worrying about lost data this.whitelist = ['org.gnome.Terminal.desktop', 'org.gnome.Nautilus.desktop', 'smplayer.desktop']; @@ -37,10 +44,14 @@ var CloseSession = class { } let running_apps = this._defaultAppSystem.get_running(); - for (const app of running_apps) { - if (this._tryCloseByRules(app)) { - continue; - } + let [running_apps_closing_by_rules, new_running_apps] = this._getRunningAppsClosingByRules(); + this._consume(running_apps_closing_by_rules, new_running_apps); + + + for (const app of new_running_apps) { + // if (this._tryCloseByRules(app)) { + // continue; + // } if (this._skip_multiple_windows(app)) { this._log.debug(`Skipping ${app.get_name()} because it has more than one windows`); @@ -52,6 +63,104 @@ var CloseSession = class { } + _consume(running_apps_closing_by_rules, new_running_apps) { + if (!running_apps_closing_by_rules || running_apps_closing_by_rules.length === 0) { + return; + } + + const app = running_apps_closing_by_rules.shift(); + + const closeWindowsRules = this._prefsUtils.getSettingString('close-windows-rules'); + const closeWindowsRulesObj = JSON.parse(closeWindowsRules); + const rules = closeWindowsRulesObj[app.get_app_info()?.get_filename()]; + + if (rules.type === 'shortcut') { + let shortcutsMixedWithKeycode = []; + let shortcutsOriginal = []; + for (const order in rules.value) { + const rule = rules.value[order]; + let shortcut = rule.shortcut; + if (rule.state === 0) { + shortcutsMixedWithKeycode.push(rule.keycode + ''); + } else { + // The shift key is not pressed, so convert the last key to the lowercase + // xdotool won't recognize it if the last key is uppercase + if (!(rule.state & Constants.GDK_SHIFT_MASK)) { + const keys = shortcut.split('+'); + const lastKey = keys[keys.length - 1]; + // Only handle letters which the length is 1, ignoring keys like Return, Escape etc. + if (lastKey.length === 1) { + keys[keys.length - 1] = lastKey.toLowerCase(); + shortcut = keys.join('+'); + } + } + + shortcutsMixedWithKeycode.push(shortcut); + } + shortcutsOriginal.push(shortcut); + } + + const allShortcutsString = shortcutsMixedWithKeycode.join(' '); + // Leave the overview first, so the keys can be sent to the activated windows + if (Main.overview.visible) { + Main.overview.hide(); + const hiddenId = Main.overview.connect('hidden', + () => { + Main.overview.disconnect(hiddenId); + this._activateWindow(app); + const cmd = ['xdotool', 'key' ].concat(shortcutsMixedWithKeycode); + const cmdStr = cmd.join(' '); + this._log.info(`Closing the app ${app.get_name()} by sending a shortcut ${shortcutsMixedWithKeycode.join(' ')}: ${cmdStr} (${shortcutsOriginal.join(' ')})`); + + // const [state, stdout, stderr, exit_status] = SubprocessUtils.trySpawnCommandLineSync(cmdStr); + SubprocessUtils.trySpawnAsync(cmd, () => { + this._consume(running_apps_closing_by_rules, new_running_apps); + }, () => { + new_running_apps.push(app); + }); + }); + } else { + this._activateWindow(app); + const cmd = ['xdotool', 'key' ].concat(shortcutsMixedWithKeycode); + const cmdStr = cmd.join(' '); + this._log.info(`Closing the app ${app.get_name()} by sending a shortcut ${shortcutsMixedWithKeycode.join(' ')}: ${cmdStr} (${shortcutsOriginal.join(' ')})`); + + // const [state, stdout, stderr, exit_status] = SubprocessUtils.trySpawnCommandLineSync(cmdStr); + SubprocessUtils.trySpawnAsync(cmd, () => { + this._consume(running_apps_closing_by_rules, new_running_apps); + }, () => { + new_running_apps.push(app); + }); + + } + + } + + } + + _getRunningAppsClosingByRules() { + if (!this._settings.get_boolean('enable-close-by-rules')) { + return []; + } + + let running_apps_closing_by_rules = []; + let new_running_apps = []; + let running_apps = this._defaultAppSystem.get_running(); + for (const app of running_apps) { + const closeWindowsRules = this._prefsUtils.getSettingString('close-windows-rules'); + const closeWindowsRulesObj = JSON.parse(closeWindowsRules); + const rules = closeWindowsRulesObj[app.get_app_info()?.get_filename()]; + if (!rules || !rules.enabled) { + new_running_apps.push(app); + continue; + } + + running_apps_closing_by_rules.push(app); + } + + return [running_apps_closing_by_rules, new_running_apps]; + } + _tryCloseByRules(app) { if (!this._settings.get_boolean('enable-close-by-rules')) { return false; @@ -87,26 +196,62 @@ var CloseSession = class { Main.overview.disconnect(hiddenId); this._activateAndCloseWindows(app, shortcut); }); + success = true; } else { - this._activateAndCloseWindows(app, shortcut); + success = this._activateAndCloseWindows(app, shortcut); } - success = true; } } return success; } - _activateAndCloseWindows(app, shortcut) { + _activateWindow(app) { + this._log.info(`Activate the app ${app.get_name()}`); const windows = app.get_windows(); - if (windows.length) { + if (windows.length > 1) { Main.activateWindow(windows[0]); } else { - app.activate(global.get_current_time()); + app.activate(); } - const cmd = `xdotool key ${shortcut}`; - this._log.info(`Closing the app ${app.get_name()} by sending a shortcut ${shortcut}: ${cmd}`); - Util.trySpawnCommandLine(`${cmd}`); + } + + _activateAndCloseWindows(app, shortcut) { + this._activateWindow(app); + // const rId = windows[0].connect('raised',() => { + // windows[0].disconnect(rId); + // log('raised'); + + const cmd = ['xdotool', 'key', `${shortcut}`]; + const cmdStr = cmd.join(' '); + this._log.info(`Closing the app ${app.get_name()} by sending a shortcut ${shortcut}: ${cmdStr}`); + + // const [state, stdout, stderr, exit_status] = SubprocessUtils.trySpawnCommandLineSync(cmdStr); + SubprocessUtils.trySpawnAsync(cmd); + // const result = SubprocessUtils.execCommunicate(cmd); + // this._awaitingId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 2000, () => { + // // Waiting for a moment + // }); + // GLib.Source.set_name_by_id(this._awaitingId, '[gnome-shell-extension-another-window-session-manager] this._activateAndCloseWindows'); + // return true; + // const proc = this._subprocessLauncher.spawnv(cmd); + // // Get the result to make sure the command has been sent to the window + // const result = proc.communicate_utf8(null, null); + // log(result); + // let [successful, stdout, stderr] = result; + // let status = proc.get_successful(); + // if (stderr) { + // this._log.error(new Error(), `Failed to send the command ${cmdStr} to the app. stderr: ${stderr}`); + // } + // this._log.info(state); + // return state; + + // }); + // const fId = windows[0].connect('focus',() => { + // windows[0].disconnect(fId); + // log('focus'); + // }); + return true; } _skip_multiple_windows(shellApp) { diff --git a/prefsCloseWindow.js b/prefsCloseWindow.js index 0784fd2..ba90260 100644 --- a/prefsCloseWindow.js +++ b/prefsCloseWindow.js @@ -416,7 +416,7 @@ const RuleRow = GObject.registerClass({ return Gdk.EVENT_STOP; } - if (!Gtk.accelerator_valid(keyval, mask)) return Gdk.EVENT_STOP; + // if (!Gtk.accelerator_valid(keyval, mask)) return Gdk.EVENT_STOP; const shortcut = Gtk.accelerator_get_label(keyval, mask); _eventControllerKey.get_widget().set_label(shortcut); diff --git a/saveSession.js b/saveSession.js index a04a46d..b21c441 100644 --- a/saveSession.js +++ b/saveSession.js @@ -127,7 +127,7 @@ var SaveSession = class { if (status === 0 && stdout) { cmdStr = `${stdout.split(':')[1].trim()}/${cmdStr}` } else { - logError(`Failed to query the working directory according to ${pid}, and the current command line is ${cmdStr}`); + logError(`Failed to query the working directory according to ${pid}, and the current command line is ${cmdStr}. stderr: ${stderr}`); } } diff --git a/utils/fileUtils.js b/utils/fileUtils.js index bdfba32..48d46ba 100644 --- a/utils/fileUtils.js +++ b/utils/fileUtils.js @@ -20,7 +20,7 @@ var desktop_file_store_path = `${desktop_file_store_path_base}/__another-window- var recently_closed_session_name = 'Recently Closed Session'; var recently_closed_session_path = GLib.build_filenamev([sessions_path, recently_closed_session_name]); -var autostart_restore_desktop_file_path = GLib.build_filenamev([home_dir,'/.config/autostart/_gnome-shell-extension-another-window-session-manager.desktop']); +var autostart_restore_desktop_file_path = GLib.build_filenamev([home_dir, '/.config/autostart/_gnome-shell-extension-another-window-session-manager.desktop']); function get_sessions_path() { @@ -60,13 +60,13 @@ function listAllSessions(sessionPath, recursion, debug, callback) { let fileEnumerator; try { fileEnumerator = sessionPathFile.enumerate_children( - [Gio.FILE_ATTRIBUTE_STANDARD_NAME, - Gio.FILE_ATTRIBUTE_STANDARD_TYPE, - Gio.FILE_ATTRIBUTE_TIME_MODIFIED, - Gio.FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE].join(','), + [Gio.FILE_ATTRIBUTE_STANDARD_NAME, + Gio.FILE_ATTRIBUTE_STANDARD_TYPE, + Gio.FILE_ATTRIBUTE_TIME_MODIFIED, + Gio.FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE].join(','), Gio.FileQueryInfoFlags.NONE, null); - } catch(e) { + } catch (e) { logError(e, `Failed to list directory ${sessionPath}`); fileEnumerator = null; } @@ -87,7 +87,7 @@ function listAllSessions(sessionPath, recursion, debug, callback) { } } } - + } function sessionExists(sessionName) { @@ -102,7 +102,7 @@ function trashSession(sessionName) { if (!sessionExists(sessionName)) { return true; } - + let trashed = false; const sessionFilePath = GLib.build_filenamev([sessions_path, sessionName]); try { @@ -112,7 +112,7 @@ function trashSession(sessionName) { logError(new Error(`Failed to trash file ${sessionFilePath}. Reason: Unknown.`)); } return trashed; - } catch(e) { + } catch (e) { logError(e, `Failed to trash file ${sessionFilePath}`); return false; } diff --git a/utils/subprocessUtils.js b/utils/subprocessUtils.js new file mode 100644 index 0000000..e8b4c71 --- /dev/null +++ b/utils/subprocessUtils.js @@ -0,0 +1,268 @@ +'use strict'; + +const GLib = imports.gi.GLib; +const Gio = imports.gi.Gio; + +// A simple asynchronous read loop +function readOutput(stream, lineBuffer) { + stream.read_line_async(0, null, (stream, res) => { + try { + let line = stream.read_line_finish_utf8(res)[0]; + + if (line !== null) { + lineBuffer.push(line); + readOutput(stream, lineBuffer); + } + } catch (e) { + logError(e); + } + }); +} + +var trySpawnCommandLineSync = function(commandLine) { + try { + let [state, stdout, stderr, exit_status] = GLib.spawn_command_line_sync(commandLine); + + if (stdout instanceof Uint8Array) { + stdout = imports.byteArray.toString(stdout); + } + if (stderr instanceof Uint8Array) { + stderr = imports.byteArray.toString(stderr); + } + return [state, stdout, stderr, exit_status]; + + } catch (e) { + logError(e); + } + // Failed + return [1]; +} + +// Based on: +// 1. https://gjs.guide/guides/gio/subprocesses.html#asynchronous-communication +// 2. https://gitlab.gnome.org/GNOME/gnome-shell/blob/8fda3116f03d95fabf3fac6d082b5fa268158d00/js/misc/util.js:L111 +var trySpawnSync = function(commandLineArray) { + try { + let [state, stdout, stderr, exit_status] = GLib.spawn_sync( + // Working directory, passing %null to use the parent's + null, + // An array of arguments + commandLineArray, + // Process ENV, passing %null to use the parent's + null, + // Flags; we need to use PATH so `ls` can be found and also need to know + // when the process has finished to check the output and status. + GLib.SpawnFlags.SEARCH_PATH, + // Child setup function + () => { + try { + global.context.restore_rlimit_nofile(); + } catch (err) { + } + } + ); + + if (stdout instanceof Uint8Array) { + stdout = imports.byteArray.toString(stdout); + } + if (stderr instanceof Uint8Array) { + stderr = imports.byteArray.toString(stderr); + } + return [state, stdout, stderr, exit_status]; + + } catch (e) { + logError(e); + } + // Failed + return [1]; +} + +// Based on: +// 1. https://gjs.guide/guides/gio/subprocesses.html#asynchronous-communication +// 2. https://gitlab.gnome.org/GNOME/gnome-shell/blob/8fda3116f03d95fabf3fac6d082b5fa268158d00/js/misc/util.js:L111 +var trySpawnAsync = function(commandLineArray, callBackOnSuccess, callBackOnFailure) { + try { + let [, pid, stdin, stdout, stderr] = GLib.spawn_async_with_pipes( + // Working directory, passing %null to use the parent's + null, + // An array of arguments + commandLineArray, + // Process ENV, passing %null to use the parent's + null, + // Flags; we need to use PATH so `ls` can be found and also need to know + // when the process has finished to check the output and status. + GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.DO_NOT_REAP_CHILD, + // Child setup function + () => { + try { + global.context.restore_rlimit_nofile(); + } catch (err) { + } + } + ); + + // Any unsused streams still have to be closed explicitly, otherwise the + // file descriptors may be left open + GLib.close(stdin); + + // Okay, now let's get output stream for `stdout` + let stdoutStream = new Gio.DataInputStream({ + base_stream: new Gio.UnixInputStream({ + fd: stdout, + close_fd: true + }), + close_base_stream: true + }); + + // We'll read the output asynchronously to avoid blocking the main thread + let stdoutLines = []; + readOutput(stdoutStream, stdoutLines); + + // We want the real error from `stderr`, so we'll have to do the same here + let stderrStream = new Gio.DataInputStream({ + base_stream: new Gio.UnixInputStream({ + fd: stderr, + close_fd: true + }), + close_base_stream: true + }); + + let stderrLines = []; + readOutput(stderrStream, stderrLines); + + // Watch for the process to finish, being sure to set a lower priority than + // we set for the read loop, so we get all the output + GLib.child_watch_add(GLib.PRIORITY_DEFAULT_IDLE, pid, (pid, status) => { + if (status === 0) { + log('success ' + stdoutLines.join('\n')); + if (callBackOnSuccess) { + callBackOnSuccess(); + } + } else { + logError(new Error(stderrLines.join('\n'))); + if (callBackOnFailure) { + callBackOnFailure(); + } + } + + // Ensure we close the remaining streams and process + stdoutStream.close(null); + stderrStream.close(null); + GLib.spawn_close_pid(pid); + + }); + } catch (e) { + logError(e); + } +} + + +/** + * Execute a command asynchronously and check the exit status. + * + * If given, @cancellable can be used to stop the process before it finishes. + * + * @param {string[]} argv - a list of string arguments + * @param {Gio.Cancellable} [cancellable] - optional cancellable object + * @returns {Promise} - The process success + */ + async function execCheck(argv, cancellable = null) { + let cancelId = 0; + let proc = new Gio.Subprocess({ + argv: argv, + flags: Gio.SubprocessFlags.NONE + }); + proc.init(cancellable); + + if (cancellable instanceof Gio.Cancellable) { + cancelId = cancellable.connect(() => proc.force_exit()); + } + + return new Promise((resolve, reject) => { + proc.wait_check_async(null, (proc, res) => { + try { + if (!proc.wait_check_finish(res)) { + let status = proc.get_exit_status(); + + throw new Gio.IOErrorEnum({ + code: Gio.io_error_from_errno(status), + message: GLib.strerror(status) + }); + } + + resolve(); + } catch (e) { + reject(e); + } finally { + if (cancelId > 0) { + cancellable.disconnect(cancelId); + } + } + }); + }); +} + + +/** + * Execute a command asynchronously and return the output from `stdout` on + * success or throw an error with output from `stderr` on failure. + * + * If given, @input will be passed to `stdin` and @cancellable can be used to + * stop the process before it finishes. + * + * @param {string[]} argv - a list of string arguments + * @param {string} [input] - Input to write to `stdin` or %null to ignore + * @param {Gio.Cancellable} [cancellable] - optional cancellable object + * @returns {Promise} - The process output + */ +function execCommunicate(argv, input = null, cancellable = null) { + let cancelId = 0; + let flags = (Gio.SubprocessFlags.STDOUT_PIPE | + Gio.SubprocessFlags.STDERR_PIPE); + + if (input !== null) + flags |= Gio.SubprocessFlags.STDIN_PIPE; + + let proc = new Gio.Subprocess({ + argv: argv, + flags: flags + }); + // proc.init(cancellable); + + // if (cancellable instanceof Gio.Cancellable) { + // cancelId = cancellable.connect(() => proc.force_exit()); + // } + + const result = proc.communicate_utf8(null, null); + let [, stdout, stderr] = result; + // let status = proc.get_successful(); + // if (stderr) { + // this._log.error(new Error(), `Failed to send the command ${cmdStr} to the app. stderr: ${stderr}`); + // } + // this._log.info(stdout); + return [proc.get_successful(), stdout, stderr]; + + // return new Promise((resolve, reject) => { + // proc.communicate_utf8_async(input, null, (proc, res) => { + // try { + // let [, stdout, stderr] = proc.communicate_utf8_finish(res); + // let status = proc.get_exit_status(); + + // if (status !== 0) { + // throw new Gio.IOErrorEnum({ + // code: Gio.io_error_from_errno(status), + // message: stderr ? stderr.trim() : GLib.strerror(status) + // }); + // } + + // resolve(stdout.trim()); + // } catch (e) { + // reject(e); + // } finally { + // if (cancelId > 0) { + // cancellable.disconnect(cancelId); + // } + // } + // }); + // }); +} \ No newline at end of file From f9d38259a4567309d6b6aabacc6ab6a55912b4f0 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sat, 4 Jun 2022 01:01:29 +0800 Subject: [PATCH 27/49] Clean code --- closeSession.js | 142 +++++++-------------------------------- utils/subprocessUtils.js | 128 +---------------------------------- 2 files changed, 25 insertions(+), 245 deletions(-) diff --git a/closeSession.js b/closeSession.js index baf6b01..6760e81 100644 --- a/closeSession.js +++ b/closeSession.js @@ -43,16 +43,10 @@ var CloseSession = class { workspaceManager.get_workspace_by_index(i)._keepAliveId = false; } - let running_apps = this._defaultAppSystem.get_running(); let [running_apps_closing_by_rules, new_running_apps] = this._getRunningAppsClosingByRules(); - this._consume(running_apps_closing_by_rules, new_running_apps); + this._tryCloseAppsByRules(running_apps_closing_by_rules, new_running_apps); - for (const app of new_running_apps) { - // if (this._tryCloseByRules(app)) { - // continue; - // } - if (this._skip_multiple_windows(app)) { this._log.debug(`Skipping ${app.get_name()} because it has more than one windows`); continue; @@ -63,7 +57,7 @@ var CloseSession = class { } - _consume(running_apps_closing_by_rules, new_running_apps) { + _tryCloseAppsByRules(running_apps_closing_by_rules, new_running_apps) { if (!running_apps_closing_by_rules || running_apps_closing_by_rules.length === 0) { return; } @@ -100,47 +94,41 @@ var CloseSession = class { shortcutsOriginal.push(shortcut); } - const allShortcutsString = shortcutsMixedWithKeycode.join(' '); // Leave the overview first, so the keys can be sent to the activated windows if (Main.overview.visible) { Main.overview.hide(); const hiddenId = Main.overview.connect('hidden', () => { Main.overview.disconnect(hiddenId); - this._activateWindow(app); - const cmd = ['xdotool', 'key' ].concat(shortcutsMixedWithKeycode); - const cmdStr = cmd.join(' '); - this._log.info(`Closing the app ${app.get_name()} by sending a shortcut ${shortcutsMixedWithKeycode.join(' ')}: ${cmdStr} (${shortcutsOriginal.join(' ')})`); - - // const [state, stdout, stderr, exit_status] = SubprocessUtils.trySpawnCommandLineSync(cmdStr); - SubprocessUtils.trySpawnAsync(cmd, () => { - this._consume(running_apps_closing_by_rules, new_running_apps); - }, () => { - new_running_apps.push(app); - }); + this._activateAndCloseWindows(app, shortcutsMixedWithKeycode, shortcutsOriginal, running_apps_closing_by_rules, new_running_apps); }); } else { - this._activateWindow(app); - const cmd = ['xdotool', 'key' ].concat(shortcutsMixedWithKeycode); - const cmdStr = cmd.join(' '); - this._log.info(`Closing the app ${app.get_name()} by sending a shortcut ${shortcutsMixedWithKeycode.join(' ')}: ${cmdStr} (${shortcutsOriginal.join(' ')})`); - - // const [state, stdout, stderr, exit_status] = SubprocessUtils.trySpawnCommandLineSync(cmdStr); - SubprocessUtils.trySpawnAsync(cmd, () => { - this._consume(running_apps_closing_by_rules, new_running_apps); - }, () => { - new_running_apps.push(app); - }); - + this._activateAndCloseWindows(app, shortcutsMixedWithKeycode, shortcutsOriginal, running_apps_closing_by_rules, new_running_apps); } } } + _activateAndCloseWindows(app, shortcutsMixedWithKeycode, shortcutsOriginal, running_apps_closing_by_rules, new_running_apps) { + this._activateWindow(app); + const cmd = ['xdotool', 'key'].concat(shortcutsMixedWithKeycode); + const cmdStr = cmd.join(' '); + this._log.info(`Closing the app ${app.get_name()} by sending a shortcut ${shortcutsMixedWithKeycode.join(' ')}: ${cmdStr} (${shortcutsOriginal.join(' ')})`); + + SubprocessUtils.trySpawnAsync(cmd, (output) => { + this._log.info(`Succeed to send keys to close the windows of the previous app ${app.get_name()}. output: ${output}`); + this._tryCloseAppsByRules(running_apps_closing_by_rules, new_running_apps); + }, (output) => { + this._log.info(`Failed to send keys to close the windows of the previous app ${app.get_name()}. output: ${output}`); + new_running_apps.push(app); + this._tryCloseAppsByRules(running_apps_closing_by_rules, new_running_apps); + }); + } + _getRunningAppsClosingByRules() { if (!this._settings.get_boolean('enable-close-by-rules')) { - return []; + return [[], this._defaultAppSystem.get_running()]; } let running_apps_closing_by_rules = []; @@ -152,60 +140,14 @@ var CloseSession = class { const rules = closeWindowsRulesObj[app.get_app_info()?.get_filename()]; if (!rules || !rules.enabled) { new_running_apps.push(app); - continue; + } else { + running_apps_closing_by_rules.push(app); } - - running_apps_closing_by_rules.push(app); } return [running_apps_closing_by_rules, new_running_apps]; } - _tryCloseByRules(app) { - if (!this._settings.get_boolean('enable-close-by-rules')) { - return false; - } - - const closeWindowsRules = this._prefsUtils.getSettingString('close-windows-rules'); - const closeWindowsRulesObj = JSON.parse(closeWindowsRules); - const rules = closeWindowsRulesObj[app.get_app_info()?.get_filename()]; - if (!rules || !rules.enabled) { - return false; - } - - let success = false; - if (rules.type === 'shortcut') { - for (const order in rules.value) { - const rule = rules.value[order]; - let shortcut = rule.shortcut; - // The shift key is not pressed - if (!(rule.state & Constants.GDK_SHIFT_MASK)) { - const keys = shortcut.split('+'); - const lastKey = keys[keys.length - 1]; - // Only handle letters which the length is 1, ignoring keys like Return, Escape etc. - if (lastKey.length === 1) { - keys[keys.length - 1] = lastKey.toLowerCase(); - shortcut = keys.join('+'); - } - } - // Leave the overview first, so the keys can be sent to the activated windows - if (Main.overview.visible) { - Main.overview.hide(); - const hiddenId = Main.overview.connect('hidden', - () => { - Main.overview.disconnect(hiddenId); - this._activateAndCloseWindows(app, shortcut); - }); - success = true; - } else { - success = this._activateAndCloseWindows(app, shortcut); - } - } - - } - return success; - } - _activateWindow(app) { this._log.info(`Activate the app ${app.get_name()}`); const windows = app.get_windows(); @@ -216,44 +158,6 @@ var CloseSession = class { } } - _activateAndCloseWindows(app, shortcut) { - this._activateWindow(app); - // const rId = windows[0].connect('raised',() => { - // windows[0].disconnect(rId); - // log('raised'); - - const cmd = ['xdotool', 'key', `${shortcut}`]; - const cmdStr = cmd.join(' '); - this._log.info(`Closing the app ${app.get_name()} by sending a shortcut ${shortcut}: ${cmdStr}`); - - // const [state, stdout, stderr, exit_status] = SubprocessUtils.trySpawnCommandLineSync(cmdStr); - SubprocessUtils.trySpawnAsync(cmd); - // const result = SubprocessUtils.execCommunicate(cmd); - // this._awaitingId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 2000, () => { - // // Waiting for a moment - // }); - // GLib.Source.set_name_by_id(this._awaitingId, '[gnome-shell-extension-another-window-session-manager] this._activateAndCloseWindows'); - // return true; - // const proc = this._subprocessLauncher.spawnv(cmd); - // // Get the result to make sure the command has been sent to the window - // const result = proc.communicate_utf8(null, null); - // log(result); - // let [successful, stdout, stderr] = result; - // let status = proc.get_successful(); - // if (stderr) { - // this._log.error(new Error(), `Failed to send the command ${cmdStr} to the app. stderr: ${stderr}`); - // } - // this._log.info(state); - // return state; - - // }); - // const fId = windows[0].connect('focus',() => { - // windows[0].disconnect(fId); - // log('focus'); - // }); - return true; - } - _skip_multiple_windows(shellApp) { if (shellApp.get_n_windows() > 1 && this._skip_app_with_multiple_windows) { const app_id = shellApp.get_id(); diff --git a/utils/subprocessUtils.js b/utils/subprocessUtils.js index e8b4c71..c97dc6f 100644 --- a/utils/subprocessUtils.js +++ b/utils/subprocessUtils.js @@ -19,64 +19,6 @@ function readOutput(stream, lineBuffer) { }); } -var trySpawnCommandLineSync = function(commandLine) { - try { - let [state, stdout, stderr, exit_status] = GLib.spawn_command_line_sync(commandLine); - - if (stdout instanceof Uint8Array) { - stdout = imports.byteArray.toString(stdout); - } - if (stderr instanceof Uint8Array) { - stderr = imports.byteArray.toString(stderr); - } - return [state, stdout, stderr, exit_status]; - - } catch (e) { - logError(e); - } - // Failed - return [1]; -} - -// Based on: -// 1. https://gjs.guide/guides/gio/subprocesses.html#asynchronous-communication -// 2. https://gitlab.gnome.org/GNOME/gnome-shell/blob/8fda3116f03d95fabf3fac6d082b5fa268158d00/js/misc/util.js:L111 -var trySpawnSync = function(commandLineArray) { - try { - let [state, stdout, stderr, exit_status] = GLib.spawn_sync( - // Working directory, passing %null to use the parent's - null, - // An array of arguments - commandLineArray, - // Process ENV, passing %null to use the parent's - null, - // Flags; we need to use PATH so `ls` can be found and also need to know - // when the process has finished to check the output and status. - GLib.SpawnFlags.SEARCH_PATH, - // Child setup function - () => { - try { - global.context.restore_rlimit_nofile(); - } catch (err) { - } - } - ); - - if (stdout instanceof Uint8Array) { - stdout = imports.byteArray.toString(stdout); - } - if (stderr instanceof Uint8Array) { - stderr = imports.byteArray.toString(stderr); - } - return [state, stdout, stderr, exit_status]; - - } catch (e) { - logError(e); - } - // Failed - return [1]; -} - // Based on: // 1. https://gjs.guide/guides/gio/subprocesses.html#asynchronous-communication // 2. https://gitlab.gnome.org/GNOME/gnome-shell/blob/8fda3116f03d95fabf3fac6d082b5fa268158d00/js/misc/util.js:L111 @@ -134,14 +76,12 @@ var trySpawnAsync = function(commandLineArray, callBackOnSuccess, callBackOnFail // we set for the read loop, so we get all the output GLib.child_watch_add(GLib.PRIORITY_DEFAULT_IDLE, pid, (pid, status) => { if (status === 0) { - log('success ' + stdoutLines.join('\n')); if (callBackOnSuccess) { - callBackOnSuccess(); + callBackOnSuccess(stdoutLines.join('\n')); } } else { - logError(new Error(stderrLines.join('\n'))); if (callBackOnFailure) { - callBackOnFailure(); + callBackOnFailure(stderrLines.join('\n')); } } @@ -202,67 +142,3 @@ var trySpawnAsync = function(commandLineArray, callBackOnSuccess, callBackOnFail }); } - -/** - * Execute a command asynchronously and return the output from `stdout` on - * success or throw an error with output from `stderr` on failure. - * - * If given, @input will be passed to `stdin` and @cancellable can be used to - * stop the process before it finishes. - * - * @param {string[]} argv - a list of string arguments - * @param {string} [input] - Input to write to `stdin` or %null to ignore - * @param {Gio.Cancellable} [cancellable] - optional cancellable object - * @returns {Promise} - The process output - */ -function execCommunicate(argv, input = null, cancellable = null) { - let cancelId = 0; - let flags = (Gio.SubprocessFlags.STDOUT_PIPE | - Gio.SubprocessFlags.STDERR_PIPE); - - if (input !== null) - flags |= Gio.SubprocessFlags.STDIN_PIPE; - - let proc = new Gio.Subprocess({ - argv: argv, - flags: flags - }); - // proc.init(cancellable); - - // if (cancellable instanceof Gio.Cancellable) { - // cancelId = cancellable.connect(() => proc.force_exit()); - // } - - const result = proc.communicate_utf8(null, null); - let [, stdout, stderr] = result; - // let status = proc.get_successful(); - // if (stderr) { - // this._log.error(new Error(), `Failed to send the command ${cmdStr} to the app. stderr: ${stderr}`); - // } - // this._log.info(stdout); - return [proc.get_successful(), stdout, stderr]; - - // return new Promise((resolve, reject) => { - // proc.communicate_utf8_async(input, null, (proc, res) => { - // try { - // let [, stdout, stderr] = proc.communicate_utf8_finish(res); - // let status = proc.get_exit_status(); - - // if (status !== 0) { - // throw new Gio.IOErrorEnum({ - // code: Gio.io_error_from_errno(status), - // message: stderr ? stderr.trim() : GLib.strerror(status) - // }); - // } - - // resolve(stdout.trim()); - // } catch (e) { - // reject(e); - // } finally { - // if (cancelId > 0) { - // cancellable.disconnect(cancelId); - // } - // } - // }); - // }); -} \ No newline at end of file From f4563236394867c314be90435ead8a93f0af78f6 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sat, 4 Jun 2022 01:42:37 +0800 Subject: [PATCH 28/49] Clean code --- closeSession.js | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/closeSession.js b/closeSession.js index 6760e81..b3cf069 100644 --- a/closeSession.js +++ b/closeSession.js @@ -44,20 +44,24 @@ var CloseSession = class { } let [running_apps_closing_by_rules, new_running_apps] = this._getRunningAppsClosingByRules(); - this._tryCloseAppsByRules(running_apps_closing_by_rules, new_running_apps); + this._tryCloseAppsByRules(running_apps_closing_by_rules); for (const app of new_running_apps) { - if (this._skip_multiple_windows(app)) { - this._log.debug(`Skipping ${app.get_name()} because it has more than one windows`); - continue; - } - this._log.debug(`Closing ${app.get_name()}`); - app.request_quit(); + this._closeOneApp(app); } } - _tryCloseAppsByRules(running_apps_closing_by_rules, new_running_apps) { + _closeOneApp(app) { + if (this._skip_multiple_windows(app)) { + this._log.debug(`Skipping ${app.get_name()} because it has more than one windows`); + } else { + this._log.debug(`Closing ${app.get_name()}`); + app.request_quit(); + } + } + + _tryCloseAppsByRules(running_apps_closing_by_rules) { if (!running_apps_closing_by_rules || running_apps_closing_by_rules.length === 0) { return; } @@ -100,17 +104,17 @@ var CloseSession = class { const hiddenId = Main.overview.connect('hidden', () => { Main.overview.disconnect(hiddenId); - this._activateAndCloseWindows(app, shortcutsMixedWithKeycode, shortcutsOriginal, running_apps_closing_by_rules, new_running_apps); + this._activateAndCloseWindows(app, shortcutsMixedWithKeycode, shortcutsOriginal, running_apps_closing_by_rules); }); } else { - this._activateAndCloseWindows(app, shortcutsMixedWithKeycode, shortcutsOriginal, running_apps_closing_by_rules, new_running_apps); + this._activateAndCloseWindows(app, shortcutsMixedWithKeycode, shortcutsOriginal, running_apps_closing_by_rules); } } } - _activateAndCloseWindows(app, shortcutsMixedWithKeycode, shortcutsOriginal, running_apps_closing_by_rules, new_running_apps) { + _activateAndCloseWindows(app, shortcutsMixedWithKeycode, shortcutsOriginal, running_apps_closing_by_rules) { this._activateWindow(app); const cmd = ['xdotool', 'key'].concat(shortcutsMixedWithKeycode); const cmdStr = cmd.join(' '); @@ -118,11 +122,12 @@ var CloseSession = class { SubprocessUtils.trySpawnAsync(cmd, (output) => { this._log.info(`Succeed to send keys to close the windows of the previous app ${app.get_name()}. output: ${output}`); - this._tryCloseAppsByRules(running_apps_closing_by_rules, new_running_apps); + this._tryCloseAppsByRules(running_apps_closing_by_rules); }, (output) => { this._log.info(`Failed to send keys to close the windows of the previous app ${app.get_name()}. output: ${output}`); - new_running_apps.push(app); - this._tryCloseAppsByRules(running_apps_closing_by_rules, new_running_apps); + // Fallback to close it again in the normal way + this._closeOneApp(app); + this._tryCloseAppsByRules(running_apps_closing_by_rules); }); } @@ -162,7 +167,7 @@ var CloseSession = class { if (shellApp.get_n_windows() > 1 && this._skip_app_with_multiple_windows) { const app_id = shellApp.get_id(); if (this.whitelist.includes(app_id)) { - this._log.debug(`${shellApp.get_name()} / ${app_id} in the whitelist.`); + this._log.debug(`${shellApp.get_name()} (${app_id}) in the whitelist. Closing it anyway.`); return false; } return true; From ffe20452386f3d03e870952f853ebe058a00d57a Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sat, 4 Jun 2022 02:38:35 +0800 Subject: [PATCH 29/49] Activate and focus a window --- closeSession.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/closeSession.js b/closeSession.js index b3cf069..c41fd3d 100644 --- a/closeSession.js +++ b/closeSession.js @@ -115,7 +115,7 @@ var CloseSession = class { } _activateAndCloseWindows(app, shortcutsMixedWithKeycode, shortcutsOriginal, running_apps_closing_by_rules) { - this._activateWindow(app); + this._activateAndFocusWindow(app); const cmd = ['xdotool', 'key'].concat(shortcutsMixedWithKeycode); const cmdStr = cmd.join(' '); this._log.info(`Closing the app ${app.get_name()} by sending a shortcut ${shortcutsMixedWithKeycode.join(' ')}: ${cmdStr} (${shortcutsOriginal.join(' ')})`); @@ -153,14 +153,10 @@ var CloseSession = class { return [running_apps_closing_by_rules, new_running_apps]; } - _activateWindow(app) { + _activateAndFocusWindow(app) { this._log.info(`Activate the app ${app.get_name()}`); const windows = app.get_windows(); - if (windows.length > 1) { - Main.activateWindow(windows[0]); - } else { - app.activate(); - } + Main.activateWindow(windows[0]); } _skip_multiple_windows(shellApp) { From cd9b1434049fcecd4f283541472dd1a6ec18bea8 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sun, 5 Jun 2022 23:37:24 +0800 Subject: [PATCH 30/49] UI render: remove ScrolledWindow on Gnome 42 --- prefsCloseWindow.js | 19 +++++++++++++++++-- ui/prefs-gtk4.ui | 23 ++++++----------------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/prefsCloseWindow.js b/prefsCloseWindow.js index ba90260..acd466a 100644 --- a/prefsCloseWindow.js +++ b/prefsCloseWindow.js @@ -8,6 +8,7 @@ const CloseWindowsRules = Me.imports.model.closeWindowsRules; const PrefsUtils = Me.imports.utils.prefsUtils; const Log = Me.imports.utils.log; +const GnomeVersion = Me.imports.utils.gnomeVersion; // const _ = ExtensionUtils.gettext; @@ -35,6 +36,14 @@ var UICloseWindows = GObject.registerClass( }); this.close_by_rules_list_box = this._builder.get_object('close_by_rules_list_box'); + // Remove GtkScrolledWindow on Gnome 42 + // See: https://gjs.guide/extensions/upgrading/gnome-shell-42.html#gtk-scrolledwindow + if (!GnomeVersion.isOlderThan42()) { + this.close_by_rules_list_box.unparent(); + const close_by_rules_multi_grid2 = this._builder.get_object('close_by_rules_multi_grid2'); + close_by_rules_multi_grid2.attach(this.close_by_rules_list_box, 0, 0, 1, 1); + } + this.close_by_rules_list_box.append(new AwsmNewRuleRow()); this._actionGroup = new Gio.SimpleActionGroup(); @@ -80,7 +89,7 @@ var UICloseWindows = GObject.registerClass( } _onAddActivated() { - const dialog = new AwsmNewRuleDialog(this._builder.get_object('close_rule_listbox_scrolledwindow').get_root()); + const dialog = new AwsmNewRuleDialog(this._builder.get_object('prefs_notebook').get_root()); dialog.connect('response', (dlg, id) => { const appInfo = id === Gtk.ResponseType.OK ? dialog.get_widget().get_app_info() : null; @@ -247,22 +256,28 @@ const RuleRow = GObject.registerClass({ pixel_size: 32, }); icon.get_style_context().add_class('icon-dropshadow'); + icon.set_tooltip_text(appInfo.get_display_name()); box.append(icon); const label = new Gtk.Label({ label: appInfo.get_display_name(), halign: Gtk.Align.START, hexpand: true, + // Make sure that text align left + xalign: 0, + width_chars: 20, max_width_chars: 20, ellipsize: Pango.EllipsizeMode.END, }); + label.set_tooltip_text(appInfo.get_display_name()); box.append(label); const _model = new Gtk.ListStore(); _model.set_column_types([GObject.TYPE_STRING, GObject.TYPE_STRING]); const combo = new Gtk.ComboBox({ model: _model, - halign: Gtk.Align.START + halign: Gtk.Align.START, + // hexpand: true, }); // https://stackoverflow.com/questions/21568268/how-to-use-the-gtk-combobox-in-gjs // https://tecnocode.co.uk/misc/platform-demos/combobox.js.xhtml diff --git a/ui/prefs-gtk4.ui b/ui/prefs-gtk4.ui index 01fa08e..d1ef293 100644 --- a/ui/prefs-gtk4.ui +++ b/ui/prefs-gtk4.ui @@ -84,24 +84,13 @@ 1 - + - - vertical - - - True - True - - - - - - - - - - + + True + True + + From 702f227788f53549318343061cfb01c2ce93fcfd Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sun, 12 Jun 2022 11:45:22 +0800 Subject: [PATCH 31/49] Lose focus if key pressed unless the key is Backspace --- prefsCloseWindow.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/prefsCloseWindow.js b/prefsCloseWindow.js index acd466a..d7735d2 100644 --- a/prefsCloseWindow.js +++ b/prefsCloseWindow.js @@ -342,6 +342,7 @@ const RuleRow = GObject.registerClass({ const eventControllerKey = new Gtk.EventControllerKey(); accelButton.add_controller(eventControllerKey); eventControllerKey.connect('key-pressed', this._onKeyPressed.bind(this)); + eventControllerKey.connect('key-released', this._onKeyReleased.bind(this)); this._rendererAccelBox.append(accelButton); if (ruleOrder < maxRuleOrder) { let next = new Gtk.Label({ @@ -413,6 +414,7 @@ const RuleRow = GObject.registerClass({ const eventControllerKey = new Gtk.EventControllerKey(); newAccelButton.add_controller(eventControllerKey); eventControllerKey.connect('key-pressed', this._onKeyPressed.bind(this)); + eventControllerKey.connect('key-released', this._onKeyReleased.bind(this)); this._rendererAccelBox.append(newAccelButton); // TODO Calling this._rendererAccelBox.get_root().get_surface().restore_system_shortcuts(null); after this? @@ -421,11 +423,25 @@ const RuleRow = GObject.registerClass({ this._log.debug(`Grab the focus for setting the accelerator: ${focused}`); } + _onKeyReleased(_eventControllerKey, keyval, keycode, state) { + let mask = state & Gtk.accelerator_get_default_mod_mask(); + mask &= ~Gdk.ModifierType.LOCK_MASK; + + // Backspace remove the new shortcut + if (mask === 0 && keyval === Gdk.KEY_BackSpace) { + this._removeAccelerator(_eventControllerKey.get_widget()); + return Gdk.EVENT_STOP; + } + + this._rendererAccelBox.get_root().get_surface().restore_system_shortcuts(null); + this.grab_focus(); + } + _onKeyPressed(_eventControllerKey, keyval, keycode, state) { let mask = state & Gtk.accelerator_get_default_mod_mask(); mask &= ~Gdk.ModifierType.LOCK_MASK; - // Backspace resets the new shortcut + // Backspace remove the new shortcut if (mask === 0 && keyval === Gdk.KEY_BackSpace) { this._removeAccelerator(_eventControllerKey.get_widget()); return Gdk.EVENT_STOP; From c50846619a3d89e4f1cab3a5ac0a9c27ba3afd01 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Mon, 13 Jun 2022 00:16:02 +0800 Subject: [PATCH 32/49] alignment --- prefsCloseWindow.js | 68 +++++++++++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/prefsCloseWindow.js b/prefsCloseWindow.js index d7735d2..de086ad 100644 --- a/prefsCloseWindow.js +++ b/prefsCloseWindow.js @@ -222,20 +222,25 @@ const RuleRow = GObject.registerClass({ this._prefsUtils = new PrefsUtils.PrefsUtils(); this._settings = this._prefsUtils.getSettings(); - const box = new Gtk.Box({ - spacing: 6, - margin_top: 6, - margin_bottom: 6, - margin_start: 6, - margin_end: 6, + const ruleRowBox = this._newBox({ + hexpand: true, + halign: Gtk.Align.FILL + }); + + const boxLeft = this._newBox({ + hexpand: true, + halign: Gtk.Align.START + }); + + const boxRight = this._newBox({ + halign: Gtk.Align.END }); - this._box = box; super._init({ activatable: false, // TODO // value: GLib.Variant.new_strv(ruleDetail.value), - child: box, + child: ruleRowBox, }); this._appInfo = appInfo; this._ruleDetail = ruleDetail; @@ -249,7 +254,7 @@ const RuleRow = GObject.registerClass({ this.bind_property('enabled', this._enabledCheckButton, 'active', GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL); - box.append(this._enabledCheckButton); + boxLeft.append(this._enabledCheckButton); const icon = new Gtk.Image({ gicon: appInfo.get_icon(), @@ -257,7 +262,7 @@ const RuleRow = GObject.registerClass({ }); icon.get_style_context().add_class('icon-dropshadow'); icon.set_tooltip_text(appInfo.get_display_name()); - box.append(icon); + boxLeft.append(icon); const label = new Gtk.Label({ label: appInfo.get_display_name(), @@ -270,14 +275,14 @@ const RuleRow = GObject.registerClass({ ellipsize: Pango.EllipsizeMode.END, }); label.set_tooltip_text(appInfo.get_display_name()); - box.append(label); + boxLeft.append(label); const _model = new Gtk.ListStore(); _model.set_column_types([GObject.TYPE_STRING, GObject.TYPE_STRING]); const combo = new Gtk.ComboBox({ model: _model, halign: Gtk.Align.START, - // hexpand: true, + hexpand: true, }); // https://stackoverflow.com/questions/21568268/how-to-use-the-gtk-combobox-in-gjs // https://tecnocode.co.uk/misc/platform-demos/combobox.js.xhtml @@ -291,16 +296,24 @@ const RuleRow = GObject.registerClass({ _model.set(iter, [0, 1], ['Shortcut', 'Shortcut']); // Set the first row in the combobox to be active on startup combo.set_active(0); - box.append(combo); + boxLeft.append(combo); - this._append_accel(); + this._append_accel(boxRight); const buttonRemove = new Gtk.Button({ action_name: 'rules.remove', action_target: new GLib.Variant('s', this.appDesktopFilePath), icon_name: 'edit-delete-symbolic', }); - box.append(buttonRemove); + const boxRemoveButton = this._newBox({ + hexpand: true, + halign: Gtk.Align.START + }); + boxRemoveButton.append(buttonRemove); + boxRight.append(boxRemoveButton); + + ruleRowBox.append(boxLeft); + ruleRowBox.append(boxRight); // TODO Not used, can be deleted? this.connect('notify::value', @@ -320,14 +333,20 @@ const RuleRow = GObject.registerClass({ }); } - _append_accel() { - this._rendererAccelBox = new Gtk.Box({ + _newBox(properties) { + const box = new Gtk.Box({ spacing: 6, margin_top: 6, margin_bottom: 6, margin_start: 6, margin_end: 6, - }); + }) + Object.assign(box, properties); + return box; + } + + _append_accel(parentWidget) { + this._rendererAccelBox = this._newBox(); const rules = this._ruleDetail.value; const ruleOrders = Object.keys(rules); @@ -360,13 +379,7 @@ const RuleRow = GObject.registerClass({ label: 'Delete accelerator', icon_name: 'edit-clear-symbolic', }); - const rendererAccelOptBox = new Gtk.Box({ - spacing: 6, - margin_top: 6, - margin_bottom: 6, - margin_start: 6, - margin_end: 6, - }); + const rendererAccelOptBox = this._newBox(); rendererAccelOptBox.append(addAccelButton); rendererAccelOptBox.append(deleteAccelButton); @@ -383,7 +396,7 @@ const RuleRow = GObject.registerClass({ const frame = new Gtk.Frame(); frame.set_child(box); - this._box.append(frame); + parentWidget.append(frame); } _addNewAccel() { @@ -430,6 +443,7 @@ const RuleRow = GObject.registerClass({ // Backspace remove the new shortcut if (mask === 0 && keyval === Gdk.KEY_BackSpace) { this._removeAccelerator(_eventControllerKey.get_widget()); + this._rendererAccelBox.get_root().get_surface().restore_system_shortcuts(null); return Gdk.EVENT_STOP; } @@ -516,7 +530,7 @@ const RuleRow = GObject.registerClass({ } // The current widget is in the last if (!nextWidgetRemoved) { - previousWidgetRemoved.get_prev_sibling().grab_focus(); + this.grab_focus(); this._rendererAccelBox.remove(previousWidgetRemoved); } this._rendererAccelBox.remove(currentWidgetRemoved); From 1094c1e8198d69c308a022a413df91a15598293c Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Fri, 17 Jun 2022 01:02:30 +0800 Subject: [PATCH 33/49] Close windows via `ydotool` --- closeSession.js | 68 ++++++++++++++++++++++++++++----------------- constants.js | 7 ++++- prefsCloseWindow.js | 4 +-- 3 files changed, 51 insertions(+), 28 deletions(-) diff --git a/closeSession.js b/closeSession.js index c41fd3d..b213e88 100644 --- a/closeSession.js +++ b/closeSession.js @@ -73,28 +73,21 @@ var CloseSession = class { const rules = closeWindowsRulesObj[app.get_app_info()?.get_filename()]; if (rules.type === 'shortcut') { - let shortcutsMixedWithKeycode = []; let shortcutsOriginal = []; + let keycodes = []; for (const order in rules.value) { const rule = rules.value[order]; let shortcut = rule.shortcut; - if (rule.state === 0) { - shortcutsMixedWithKeycode.push(rule.keycode + ''); - } else { - // The shift key is not pressed, so convert the last key to the lowercase - // xdotool won't recognize it if the last key is uppercase - if (!(rule.state & Constants.GDK_SHIFT_MASK)) { - const keys = shortcut.split('+'); - const lastKey = keys[keys.length - 1]; - // Only handle letters which the length is 1, ignoring keys like Return, Escape etc. - if (lastKey.length === 1) { - keys[keys.length - 1] = lastKey.toLowerCase(); - shortcut = keys.join('+'); - } - } - - shortcutsMixedWithKeycode.push(shortcut); - } + let state = rule.state; + let keycode = rule.keycode; + const linuxKeycodes = this._convertToLinuxKeycodes(state, keycode); + const translatedLinuxKeycodes = linuxKeycodes.slice() + // Press keys + .map(k => k + ':1') + .concat(linuxKeycodes.slice() + // Release keys + .reverse().map(k => k + ':0')) + keycodes = keycodes.concat(translatedLinuxKeycodes); shortcutsOriginal.push(shortcut); } @@ -104,22 +97,47 @@ var CloseSession = class { const hiddenId = Main.overview.connect('hidden', () => { Main.overview.disconnect(hiddenId); - this._activateAndCloseWindows(app, shortcutsMixedWithKeycode, shortcutsOriginal, running_apps_closing_by_rules); + this._activateAndCloseWindows(app, keycodes, shortcutsOriginal, running_apps_closing_by_rules); }); } else { - this._activateAndCloseWindows(app, shortcutsMixedWithKeycode, shortcutsOriginal, running_apps_closing_by_rules); + this._activateAndCloseWindows(app, keycodes, shortcutsOriginal, running_apps_closing_by_rules); } } } - _activateAndCloseWindows(app, shortcutsMixedWithKeycode, shortcutsOriginal, running_apps_closing_by_rules) { + _convertToLinuxKeycodes(state, keycode) { + let keycodes = []; + // Convert to key codes defined in /usr/include/linux/input-event-codes.h + if (state & Constants.GDK_SHIFT_MASK) { + // KEY_LEFTSHIFT + keycodes.push(42); + } + if (state & Constants.GDK_CONTROL_MASK) { + // KEY_LEFTCTRL + keycodes.push(29); + } + if (state & Constants.GDK_ALT_MASK) { + // KEY_LEFTALT + keycodes.push(56); + } + if (state & Constants.GDK_META_MASK) { + // KEY_LEFTMETA + keycodes.push(125); + } + // The Xorg keycodes are 8 larger than the Linux keycodes. + // See https://wiki.archlinux.org/title/Keyboard_input#Identifying_keycodes_in_Xorg + keycodes.push(keycode - 8); + return keycodes; + } + + _activateAndCloseWindows(app, linuxKeyCodes, shortcutsOriginal, running_apps_closing_by_rules) { this._activateAndFocusWindow(app); - const cmd = ['xdotool', 'key'].concat(shortcutsMixedWithKeycode); + const cmd = ['ydotool', 'key', '--key-delay', '0'].concat(linuxKeyCodes); const cmdStr = cmd.join(' '); - this._log.info(`Closing the app ${app.get_name()} by sending a shortcut ${shortcutsMixedWithKeycode.join(' ')}: ${cmdStr} (${shortcutsOriginal.join(' ')})`); - + this._log.info(`Closing the app ${app.get_name()} by sending: ${cmdStr} (${shortcutsOriginal.join(' ')})`); + SubprocessUtils.trySpawnAsync(cmd, (output) => { this._log.info(`Succeed to send keys to close the windows of the previous app ${app.get_name()}. output: ${output}`); this._tryCloseAppsByRules(running_apps_closing_by_rules); @@ -182,4 +200,4 @@ var CloseSession = class { } } -} \ No newline at end of file +} diff --git a/constants.js b/constants.js index bbeae87..f70c890 100644 --- a/constants.js +++ b/constants.js @@ -1,7 +1,12 @@ /** - * GdkModifierType: the Shift key + * GdkModifierType * * See: https://gitlab.gnome.org/GNOME/gtk/blob/d726ecdb5d1ece870585c7be89eb6355b2482544/gdk/gdkenums.h:L74 */ var GDK_SHIFT_MASK = 1 << 0; +var GDK_CONTROL_MASK = 1 << 2; +var GDK_ALT_MASK = 1 << 3; + +var GDK_META_MASK = 1 << 28; + diff --git a/prefsCloseWindow.js b/prefsCloseWindow.js index de086ad..f43a7ab 100644 --- a/prefsCloseWindow.js +++ b/prefsCloseWindow.js @@ -443,11 +443,11 @@ const RuleRow = GObject.registerClass({ // Backspace remove the new shortcut if (mask === 0 && keyval === Gdk.KEY_BackSpace) { this._removeAccelerator(_eventControllerKey.get_widget()); - this._rendererAccelBox.get_root().get_surface().restore_system_shortcuts(null); + this._rendererAccelBox.get_root().get_surface().restore_system_shortcuts(); return Gdk.EVENT_STOP; } - this._rendererAccelBox.get_root().get_surface().restore_system_shortcuts(null); + this._rendererAccelBox.get_root().get_surface().restore_system_shortcuts(); this.grab_focus(); } From a2a666e33ef1445fb4b5e342488ae98f87978668 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Fri, 17 Jun 2022 19:49:42 +0800 Subject: [PATCH 34/49] extract method --- prefsCloseWindow.js | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/prefsCloseWindow.js b/prefsCloseWindow.js index f43a7ab..15f0c5f 100644 --- a/prefsCloseWindow.js +++ b/prefsCloseWindow.js @@ -277,26 +277,7 @@ const RuleRow = GObject.registerClass({ label.set_tooltip_text(appInfo.get_display_name()); boxLeft.append(label); - const _model = new Gtk.ListStore(); - _model.set_column_types([GObject.TYPE_STRING, GObject.TYPE_STRING]); - const combo = new Gtk.ComboBox({ - model: _model, - halign: Gtk.Align.START, - hexpand: true, - }); - // https://stackoverflow.com/questions/21568268/how-to-use-the-gtk-combobox-in-gjs - // https://tecnocode.co.uk/misc/platform-demos/combobox.js.xhtml - const renderer = new Gtk.CellRendererText(); - // Pack the renderers into the combobox in the order we want to see - combo.pack_start(renderer, true); - // Set the renderers to use the information from our liststore - combo.add_attribute(renderer, 'text', 1); - let iter = _model.append(); - // https://docs.gtk.org/gtk4/method.ListStore.set.html - _model.set(iter, [0, 1], ['Shortcut', 'Shortcut']); - // Set the first row in the combobox to be active on startup - combo.set_active(0); - boxLeft.append(combo); + boxLeft.append(this._newShortcutComboBox()); this._append_accel(boxRight); @@ -333,6 +314,29 @@ const RuleRow = GObject.registerClass({ }); } + _newShortcutComboBox() { + const _model = new Gtk.ListStore(); + _model.set_column_types([GObject.TYPE_STRING, GObject.TYPE_STRING]); + const combo = new Gtk.ComboBox({ + model: _model, + halign: Gtk.Align.START, + hexpand: true, + }); + // https://stackoverflow.com/questions/21568268/how-to-use-the-gtk-combobox-in-gjs + // https://tecnocode.co.uk/misc/platform-demos/combobox.js.xhtml + const renderer = new Gtk.CellRendererText(); + // Pack the renderers into the combobox in the order we want to see + combo.pack_start(renderer, true); + // Set the renderers to use the information from our liststore + combo.add_attribute(renderer, 'text', 1); + let iter = _model.append(); + // https://docs.gtk.org/gtk4/method.ListStore.set.html + _model.set(iter, [0, 1], ['Shortcut', 'Shortcut']); + // Set the first row in the combobox to be active on startup + combo.set_active(0); + return combo; + } + _newBox(properties) { const box = new Gtk.Box({ spacing: 6, From 5331c33c122ab6499631e23a47db32039bb2481e Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Fri, 17 Jun 2022 23:29:28 +0800 Subject: [PATCH 35/49] UI: add key delay --- closeSession.js | 15 ++++++++++----- model/closeWindowsRules.js | 1 + prefsCloseWindow.js | 32 ++++++++++++++++++++++++++++++++ ui/prefs-gtk4.ui | 2 +- 4 files changed, 44 insertions(+), 6 deletions(-) diff --git a/closeSession.js b/closeSession.js index b213e88..dc807d5 100644 --- a/closeSession.js +++ b/closeSession.js @@ -72,7 +72,7 @@ var CloseSession = class { const closeWindowsRulesObj = JSON.parse(closeWindowsRules); const rules = closeWindowsRulesObj[app.get_app_info()?.get_filename()]; - if (rules.type === 'shortcut') { + if (rules?.type === 'shortcut') { let shortcutsOriginal = []; let keycodes = []; for (const order in rules.value) { @@ -132,12 +132,17 @@ var CloseSession = class { return keycodes; } - _activateAndCloseWindows(app, linuxKeyCodes, shortcutsOriginal, running_apps_closing_by_rules) { - this._activateAndFocusWindow(app); - const cmd = ['ydotool', 'key', '--key-delay', '0'].concat(linuxKeyCodes); + _activateAndCloseWindows(app, linuxKeyCodes, shortcutsOriginal, running_apps_closing_by_rules) { + const closeWindowsRules = this._prefsUtils.getSettingString('close-windows-rules'); + const closeWindowsRulesObj = JSON.parse(closeWindowsRules); + const rules = closeWindowsRulesObj[app.get_app_info()?.get_filename()]; + const keyDelay = rules?.keyDelay; + const cmd = ['ydotool', 'key', '--key-delay', !keyDelay ? '0' : keyDelay + ''].concat(linuxKeyCodes); const cmdStr = cmd.join(' '); + this._log.info(`Closing the app ${app.get_name()} by sending: ${cmdStr} (${shortcutsOriginal.join(' ')})`); + this._activateAndFocusWindow(app); SubprocessUtils.trySpawnAsync(cmd, (output) => { this._log.info(`Succeed to send keys to close the windows of the previous app ${app.get_name()}. output: ${output}`); this._tryCloseAppsByRules(running_apps_closing_by_rules); @@ -161,7 +166,7 @@ var CloseSession = class { const closeWindowsRules = this._prefsUtils.getSettingString('close-windows-rules'); const closeWindowsRulesObj = JSON.parse(closeWindowsRules); const rules = closeWindowsRulesObj[app.get_app_info()?.get_filename()]; - if (!rules || !rules.enabled) { + if (!rules || !rules.enabled || !rules.value) { new_running_apps.push(app); } else { running_apps_closing_by_rules.push(app); diff --git a/model/closeWindowsRules.js b/model/closeWindowsRules.js index 5097382..ef2abb3 100644 --- a/model/closeWindowsRules.js +++ b/model/closeWindowsRules.js @@ -7,6 +7,7 @@ var CloseWindowsRules = class { appName; // string, such as 'Firefox' appDesktopFilePath; // string, such as '/usr/share/applications/firefox.desktop' enabled; // boolean + keyDelay; // int, for example: `enabydotool key --key-delay 500 29:1 16:1 16:0 29:0` } /** diff --git a/prefsCloseWindow.js b/prefsCloseWindow.js index 15f0c5f..4a0d9e9 100644 --- a/prefsCloseWindow.js +++ b/prefsCloseWindow.js @@ -215,6 +215,7 @@ const RuleRow = GObject.registerClass({ // Default value null, GObject.ParamFlags.READWRITE), + }, }, class RuleRow extends Gtk.ListBoxRow { _init(appInfo, ruleDetail) { @@ -279,6 +280,8 @@ const RuleRow = GObject.registerClass({ boxLeft.append(this._newShortcutComboBox()); + boxLeft.append(this._newDelaySpinButton()); + this._append_accel(boxRight); const buttonRemove = new Gtk.Button({ @@ -314,6 +317,30 @@ const RuleRow = GObject.registerClass({ }); } + _newDelaySpinButton() { + const savedKeyDelay = this._ruleDetail.keyDelay; + const spinButton = new Gtk.SpinButton({ + adjustment: new Gtk.Adjustment({ + lower: 0, + // Up to 5 minutes + upper: 300000, + step_increment: 1, + value: savedKeyDelay ? savedKeyDelay : 0 + }), + snap_to_ticks: true, + margin_end: 6, + }); + spinButton.connect('value-changed', (widget) => { + const keyDelayValue = widget.get_value(); + const oldCloseWindowsRules = this._settings.get_string('close-windows-rules'); + let oldCloseWindowsRulesObj = JSON.parse(oldCloseWindowsRules); + oldCloseWindowsRulesObj[this.appDesktopFilePath].keyDelay = keyDelayValue; + const newCloseWindowsRules = JSON.stringify(oldCloseWindowsRulesObj); + this._settings.set_string('close-windows-rules', newCloseWindowsRules); + }); + return spinButton; + } + _newShortcutComboBox() { const _model = new Gtk.ListStore(); _model.set_column_types([GObject.TYPE_STRING, GObject.TYPE_STRING]); @@ -399,6 +426,11 @@ const RuleRow = GObject.registerClass({ const frame = new Gtk.Frame(); frame.set_child(box); + const cssProvider = new Gtk.CssProvider(); + cssProvider.load_from_data( + "frame { border-style: dashed; }"); + frame.get_style_context().add_provider(cssProvider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); parentWidget.append(frame); } diff --git a/ui/prefs-gtk4.ui b/ui/prefs-gtk4.ui index d1ef293..ab64416 100644 --- a/ui/prefs-gtk4.ui +++ b/ui/prefs-gtk4.ui @@ -89,7 +89,7 @@ True True - + True From 9ece9f4ca1c7779f23c2f2e24487f84c3981fb78 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sat, 18 Jun 2022 23:36:07 +0800 Subject: [PATCH 36/49] stash --- closeSession.js | 62 +++++++++--- extension.js | 4 + indicator.js | 1 + ...oseWindowsRules.js => closeWindowsRule.js} | 2 +- openWindowsInfoTracker.js | 95 +++++++++++++++++++ prefsCloseWindow.js | 30 ++---- 6 files changed, 161 insertions(+), 33 deletions(-) rename model/{closeWindowsRules.js => closeWindowsRule.js} (96%) create mode 100644 openWindowsInfoTracker.js diff --git a/closeSession.js b/closeSession.js index dc807d5..94be451 100644 --- a/closeSession.js +++ b/closeSession.js @@ -13,6 +13,8 @@ const Log = Me.imports.utils.log; const PrefsUtils = Me.imports.utils.prefsUtils; const SubprocessUtils = Me.imports.utils.subprocessUtils; +const OpenWindowsInfoTracker = Me.imports.openWindowsInfoTracker; + const Constants = Me.imports.constants; @@ -29,6 +31,8 @@ var CloseSession = class { flags: (Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE)}); + this._openWindows = OpenWindowsInfoTracker.openWindows; + // TODO Put into Settings // All apps in the whitelist should be closed safely, no worrying about lost data this.whitelist = ['org.gnome.Terminal.desktop', 'org.gnome.Nautilus.desktop', 'smplayer.desktop']; @@ -73,6 +77,7 @@ var CloseSession = class { const rules = closeWindowsRulesObj[app.get_app_info()?.get_filename()]; if (rules?.type === 'shortcut') { + let keycodesSegments = []; let shortcutsOriginal = []; let keycodes = []; for (const order in rules.value) { @@ -87,7 +92,8 @@ var CloseSession = class { .concat(linuxKeycodes.slice() // Release keys .reverse().map(k => k + ':0')) - keycodes = keycodes.concat(translatedLinuxKeycodes); + // keycodes = keycodes.concat(translatedLinuxKeycodes); + keycodesSegments.push(translatedLinuxKeycodes); shortcutsOriginal.push(shortcut); } @@ -97,10 +103,22 @@ var CloseSession = class { const hiddenId = Main.overview.connect('hidden', () => { Main.overview.disconnect(hiddenId); - this._activateAndCloseWindows(app, keycodes, shortcutsOriginal, running_apps_closing_by_rules); + const result = this._activateAndCloseWindows(app, keycodesSegments, shortcutsOriginal, running_apps_closing_by_rules); + if (!result) { + // Fallback to close it again in the normal way + this._closeOneApp(app); + } else { + this._tryCloseAppsByRules(running_apps_closing_by_rules); + } }); } else { - this._activateAndCloseWindows(app, keycodes, shortcutsOriginal, running_apps_closing_by_rules); + const result = this._activateAndCloseWindows(app, keycodesSegments, shortcutsOriginal, running_apps_closing_by_rules); + if (!result) { + // Fallback to close it again in the normal way + this._closeOneApp(app); + } else { + this._tryCloseAppsByRules(running_apps_closing_by_rules); + } } } @@ -132,7 +150,11 @@ var CloseSession = class { return keycodes; } - _activateAndCloseWindows(app, linuxKeyCodes, shortcutsOriginal, running_apps_closing_by_rules) { + _activateAndCloseWindows(app, linuxKeyCodesSegments, shortcutsOriginal, running_apps_closing_by_rules) { + if (!linuxKeyCodesSegments || linuxKeyCodesSegments.length === 0) { + return; + } + const linuxKeyCodes = linuxKeyCodesSegments.shift(); const closeWindowsRules = this._prefsUtils.getSettingString('close-windows-rules'); const closeWindowsRulesObj = JSON.parse(closeWindowsRules); const rules = closeWindowsRulesObj[app.get_app_info()?.get_filename()]; @@ -145,12 +167,11 @@ var CloseSession = class { this._activateAndFocusWindow(app); SubprocessUtils.trySpawnAsync(cmd, (output) => { this._log.info(`Succeed to send keys to close the windows of the previous app ${app.get_name()}. output: ${output}`); - this._tryCloseAppsByRules(running_apps_closing_by_rules); + this._activateAndCloseWindows(app, linuxKeyCodesSegments, shortcutsOriginal, running_apps_closing_by_rules); + return true; }, (output) => { this._log.info(`Failed to send keys to close the windows of the previous app ${app.get_name()}. output: ${output}`); - // Fallback to close it again in the normal way - this._closeOneApp(app); - this._tryCloseAppsByRules(running_apps_closing_by_rules); + return false; }); } @@ -177,9 +198,28 @@ var CloseSession = class { } _activateAndFocusWindow(app) { - this._log.info(`Activate the app ${app.get_name()}`); - const windows = app.get_windows(); - Main.activateWindow(windows[0]); + let activated = false; + const openWindows = this._openWindows.get(app); + if (openWindows) { + openWindows.windows = openWindows.windows.filter(savedWindow => { + return app.get_windows().find(w => w === savedWindow); + }); + + if (openWindows.windows.length > 0) { + const window = openWindows.windows[openWindows.windows.length - 1]; + this._log.info(`Activating the saved and running window ${window.get_title()} of ${app.get_name()}`); + Main.activateWindow(window); + activated = true; + } + } + + // Fall back to the normal way + if (!activated) { + const windows = app.get_windows(); + const window = windows[0]; + this._log.info(`Activating the running window ${window.get_title()} of ${app.get_name()}`); + Main.activateWindow(window); + } } _skip_multiple_windows(shellApp) { diff --git a/extension.js b/extension.js index 8b354cb..2374206 100644 --- a/extension.js +++ b/extension.js @@ -5,6 +5,8 @@ const Me = ExtensionUtils.getCurrentExtension(); const Main = imports.ui.main; +const OpenWindowsInfoTracker = Me.imports.openWindowsInfoTracker; + const Indicator = Me.imports.indicator; const Autostart = Me.imports.ui.autostart; @@ -18,6 +20,8 @@ function enable() { _autostartServiceProvider = new Autostart.AutostartServiceProvider(); + new OpenWindowsInfoTracker.OpenWindowsInfoTracker(); + } function disable() { diff --git a/indicator.js b/indicator.js index f52f3f1..43a75c9 100644 --- a/indicator.js +++ b/indicator.js @@ -77,6 +77,7 @@ class AwsIndicator extends PanelMenu.Button { } + // TODO Move this method and related code to a single .js file _windowCreated(display, metaWindow, userData) { if (!Meta.is_wayland_compositor()) { // We call createEnoughWorkspaceAndMoveWindows() if and only if all conditions checked. diff --git a/model/closeWindowsRules.js b/model/closeWindowsRule.js similarity index 96% rename from model/closeWindowsRules.js rename to model/closeWindowsRule.js index ef2abb3..2f545d3 100644 --- a/model/closeWindowsRules.js +++ b/model/closeWindowsRule.js @@ -1,5 +1,5 @@ -var CloseWindowsRules = class { +var CloseWindowsRule = class { type; // string, rule type, such as 'shortcut' value; // GdkShortcuts, order and the rule pairs, such as "{1: 'Ctrl+Q}'". diff --git a/openWindowsInfoTracker.js b/openWindowsInfoTracker.js new file mode 100644 index 0000000..321b940 --- /dev/null +++ b/openWindowsInfoTracker.js @@ -0,0 +1,95 @@ +'use strict'; + +const { Shell, Meta } = imports.gi; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); + +const Log = Me.imports.utils.log; + + +var openWindows = new Map(); + +var OpenWindowsInfoTracker = class { + + constructor() { + this._windowTracker = Shell.WindowTracker.get_default(); + + this._log = new Log.Log(); + + this._display = global.display; + this._displayId = this._display.connect('window-created', this._windowCreated.bind(this)); + } + + _windowCreated(display, metaWindow, userData) { + if (!metaWindow.createdTimeAwsm) { + metaWindow.createdTimeAwsm = new Date().getTime(); + } + + // let metaWindowActor = metaWindow.get_compositor_private(); + // https://github.com/paperwm/PaperWM/blob/10215f57e8b34a044e10b7407cac8fac4b93bbbc/tiling.js#L2120 + // https://gjs-docs.gnome.org/meta8~8_api/meta.windowactor#signal-first-frame + // let firstFrameId = metaWindowActor.connect('first-frame', () => { + // if (firstFrameId) { + // metaWindowActor.disconnect(firstFrameId); + // firstFrameId = 0 + // } + + const shellApp = this._windowTracker.get_window_app(metaWindow); + if (!shellApp) { + return; + } + + const currentTime = new Date().getTime(); + + const shellAppWindows = openWindows.get(shellApp); + if (shellAppWindows) { + const savedMetaWindow = shellAppWindows.windows.find(w => w.metaWindow === metaWindow); + if (!savedMetaWindow) { + shellAppWindows.windows.push({ + metaWindow: metaWindow, + title: metaWindow.get_title(), + createdTime: currentTime + }); + } + } else { + const windows = []; + windows.push({ + metaWindow: metaWindow, + title: metaWindow.get_title(), + createdTime: currentTime + }); + // desktopAppInfo could be null if the shellApp is window backed + const desktopAppInfo = shellApp.get_app_info(); + openWindows.set(shellApp, { + shellApp: shellApp, + desktopId: desktopAppInfo?.get_id(), + appName: shellApp.get_name(), + desktopFullPath: desktopAppInfo?.get_filename(), + windows: windows + }); + } + + const windows = shellApp.get_windows(); + for (const window of windows) { + this._log.debug(window.get_title() + ' ' + window.createdTime); + } + + // if (this._log.isDebug()) { + // for (const [key, value] of openWindows) { + // this._log.debug(`Tracking ${key}: ${JSON.stringify(value)}`); + // } + // this._log.debug(`Tracking window ${metaWindow}(${metaWindow.get_title()}) of ${shellApp.get_name()}. openWindows: ${JSON.stringify(Array.from(openWindows.entries()))}`); + // } + // }); + + } + + destroy() { + if (this._displayId) { + this._display.disconnect(this._displayId); + this._displayId = 0; + } + } + +} \ No newline at end of file diff --git a/prefsCloseWindow.js b/prefsCloseWindow.js index 4a0d9e9..a375b5e 100644 --- a/prefsCloseWindow.js +++ b/prefsCloseWindow.js @@ -4,7 +4,7 @@ const { Gio, GLib, GObject, Gtk, Pango, Gdk } = imports.gi; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); -const CloseWindowsRules = Me.imports.model.closeWindowsRules; +const CloseWindowsRule = Me.imports.model.closeWindowsRule; const PrefsUtils = Me.imports.utils.prefsUtils; const Log = Me.imports.utils.log; @@ -30,10 +30,6 @@ var UICloseWindows = GObject.registerClass( init() { this.close_by_rules_switch = this._builder.get_object('close_by_rules_switch'); - this.close_by_rules_switch.connect('notify::active', (widget) => { - const active = widget.active; - - }); this.close_by_rules_list_box = this._builder.get_object('close_by_rules_list_box'); // Remove GtkScrolledWindow on Gnome 42 @@ -94,17 +90,17 @@ var UICloseWindows = GObject.registerClass( const appInfo = id === Gtk.ResponseType.OK ? dialog.get_widget().get_app_info() : null; if (appInfo) { - const closeWindowsRules = new CloseWindowsRules.CloseWindowsRules(); - closeWindowsRules.type = 'shortcut'; - closeWindowsRules.value = {}; - closeWindowsRules.appId = appInfo.get_id(); - closeWindowsRules.appName = appInfo.get_name(); - closeWindowsRules.appDesktopFilePath = appInfo.get_filename(); - closeWindowsRules.enabled = false; + const closeWindowsRule = new CloseWindowsRule.CloseWindowsRule(); + closeWindowsRule.type = 'shortcut'; + closeWindowsRule.value = {}; + closeWindowsRule.appId = appInfo.get_id(); + closeWindowsRule.appName = appInfo.get_name(); + closeWindowsRule.appDesktopFilePath = appInfo.get_filename(); + closeWindowsRule.enabled = false; const oldCloseWindowsRules = this._settings.get_string('close-windows-rules'); let oldCloseWindowsRulesObj = JSON.parse(oldCloseWindowsRules); - oldCloseWindowsRulesObj[closeWindowsRules.appDesktopFilePath] = closeWindowsRules; + oldCloseWindowsRulesObj[closeWindowsRule.appDesktopFilePath] = closeWindowsRule; const newCloseWindowsRules = JSON.stringify(oldCloseWindowsRulesObj); this._settings.set_string('close-windows-rules', newCloseWindowsRules); @@ -298,14 +294,6 @@ const RuleRow = GObject.registerClass({ ruleRowBox.append(boxLeft); ruleRowBox.append(boxRight); - - // TODO Not used, can be deleted? - this.connect('notify::value', - () => this.activate_action('rules.update', new GLib.Variant('a{sv}', { - appDesktopFilePath: GLib.Variant.new_string(this.appDesktopFilePath), - enabled: GLib.Variant.new_boolean(this._enabledCheckButton.get_active()), - // value: this.value, - }))); this.connect('notify::enabled', () => { From 7a1112e6084730fefe33699dad4e6def8d4e7dd7 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Mon, 20 Jun 2022 02:01:53 +0800 Subject: [PATCH 37/49] On X11, use stable sequence of a window to sort windows and close windows from the top level --- closeSession.js | 55 +++++---- openWindowsInfoTracker.js | 111 ++++++++---------- schemas/gschemas.compiled | Bin 796 -> 860 bytes ...another-window-session-manager.gschema.xml | 6 + 4 files changed, 87 insertions(+), 85 deletions(-) diff --git a/closeSession.js b/closeSession.js index 94be451..e40f768 100644 --- a/closeSession.js +++ b/closeSession.js @@ -31,8 +31,6 @@ var CloseSession = class { flags: (Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE)}); - this._openWindows = OpenWindowsInfoTracker.openWindows; - // TODO Put into Settings // All apps in the whitelist should be closed safely, no worrying about lost data this.whitelist = ['org.gnome.Terminal.desktop', 'org.gnome.Nautilus.desktop', 'smplayer.desktop']; @@ -198,28 +196,41 @@ var CloseSession = class { } _activateAndFocusWindow(app) { - let activated = false; - const openWindows = this._openWindows.get(app); - if (openWindows) { - openWindows.windows = openWindows.windows.filter(savedWindow => { - return app.get_windows().find(w => w === savedWindow); - }); - - if (openWindows.windows.length > 0) { - const window = openWindows.windows[openWindows.windows.length - 1]; - this._log.info(`Activating the saved and running window ${window.get_title()} of ${app.get_name()}`); - Main.activateWindow(window); - activated = true; + const savedWindowsMappingJsonStr = this._settings.get_string('windows-mapping'); + const savedWindowsMapping = new Map(JSON.parse(savedWindowsMappingJsonStr)); + + const app_info = shellApp.get_app_info(); + const desktopFullPath = app_info.get_filename(); + const xidObj = savedWindowsMapping.get(desktopFullPath); + const windows = app.get_windows(); + windows.sort((w1, w2) => { + const xid1 = w1.get_description(); + const value1 = xidObj[xid1]; + const windowStableSequence1 = value1.windowStableSequence; + + const xid2 = w2.get_description(); + const value2 = xidObj[xid2]; + const windowStableSequence2 = value2.windowStableSequence; + + const diff = windowStableSequence1 - windowStableSequence2; + if (diff === 0) { + return 0; } - } - // Fall back to the normal way - if (!activated) { - const windows = app.get_windows(); - const window = windows[0]; - this._log.info(`Activating the running window ${window.get_title()} of ${app.get_name()}`); - Main.activateWindow(window); - } + if (diff > 0) { + return 1; + } + + if (diff < 0) { + return -1; + } + + }); + + const topLevelWindow = windows[windows.length - 1]; + this._log.info(`Activating the running window ${topLevelWindow.get_title()} of ${app.get_name()}`); + Main.activateWindow(topLevelWindow); + } _skip_multiple_windows(shellApp) { diff --git a/openWindowsInfoTracker.js b/openWindowsInfoTracker.js index 321b940..6dbfd65 100644 --- a/openWindowsInfoTracker.js +++ b/openWindowsInfoTracker.js @@ -1,88 +1,67 @@ 'use strict'; -const { Shell, Meta } = imports.gi; +const { Shell, Meta, Gio } = imports.gi; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); const Log = Me.imports.utils.log; - - -var openWindows = new Map(); +const PrefsUtils = Me.imports.utils.prefsUtils; var OpenWindowsInfoTracker = class { constructor() { this._windowTracker = Shell.WindowTracker.get_default(); + this._defaultAppSystem = Shell.AppSystem.get_default(); this._log = new Log.Log(); + this._prefsUtils = new PrefsUtils.PrefsUtils(); + this._settings = this._prefsUtils.getSettings(); this._display = global.display; this._displayId = this._display.connect('window-created', this._windowCreated.bind(this)); - } + } + _windowCreated(display, metaWindow, userData) { - if (!metaWindow.createdTimeAwsm) { - metaWindow.createdTimeAwsm = new Date().getTime(); + const shellApp = this._windowTracker.get_window_app(metaWindow); + if (!shellApp) { + return; } - - // let metaWindowActor = metaWindow.get_compositor_private(); - // https://github.com/paperwm/PaperWM/blob/10215f57e8b34a044e10b7407cac8fac4b93bbbc/tiling.js#L2120 - // https://gjs-docs.gnome.org/meta8~8_api/meta.windowactor#signal-first-frame - // let firstFrameId = metaWindowActor.connect('first-frame', () => { - // if (firstFrameId) { - // metaWindowActor.disconnect(firstFrameId); - // firstFrameId = 0 - // } - - const shellApp = this._windowTracker.get_window_app(metaWindow); - if (!shellApp) { - return; - } - - const currentTime = new Date().getTime(); - - const shellAppWindows = openWindows.get(shellApp); - if (shellAppWindows) { - const savedMetaWindow = shellAppWindows.windows.find(w => w.metaWindow === metaWindow); - if (!savedMetaWindow) { - shellAppWindows.windows.push({ - metaWindow: metaWindow, - title: metaWindow.get_title(), - createdTime: currentTime - }); - } - } else { - const windows = []; - windows.push({ - metaWindow: metaWindow, - title: metaWindow.get_title(), - createdTime: currentTime - }); - // desktopAppInfo could be null if the shellApp is window backed - const desktopAppInfo = shellApp.get_app_info(); - openWindows.set(shellApp, { - shellApp: shellApp, - desktopId: desktopAppInfo?.get_id(), - appName: shellApp.get_name(), - desktopFullPath: desktopAppInfo?.get_filename(), - windows: windows - }); - } - const windows = shellApp.get_windows(); - for (const window of windows) { - this._log.debug(window.get_title() + ' ' + window.createdTime); + const app_info = shellApp.get_app_info(); + if (!app_info) { + return; + } + + const xid = metaWindow.get_description(); + const windowStableSequence = metaWindow.get_stable_sequence(); + const savedWindowsMappingJsonStr = this._settings.get_string('windows-mapping'); + const savedWindowsMapping = new Map(JSON.parse(savedWindowsMappingJsonStr)); + + const desktopFullPath = app_info.get_filename(); + let xidObj = savedWindowsMapping.get(desktopFullPath); + if (xidObj && !xidObj[xid]) { + xidObj[xid] = { + windowTitle: metaWindow.get_title(), + xid: xid, + windowStableSequence: windowStableSequence + }; + } else { + if (!xidObj) { + xidObj = {}; } + xidObj[xid] = { + windowTitle: metaWindow.get_title(), + xid: xid, + windowStableSequence: windowStableSequence + }; + savedWindowsMapping.set(desktopFullPath, xidObj); + } - // if (this._log.isDebug()) { - // for (const [key, value] of openWindows) { - // this._log.debug(`Tracking ${key}: ${JSON.stringify(value)}`); - // } - // this._log.debug(`Tracking window ${metaWindow}(${metaWindow.get_title()}) of ${shellApp.get_name()}. openWindows: ${JSON.stringify(Array.from(openWindows.entries()))}`); - // } - // }); - + const newSavedWindowsMappingJsonStr = JSON.stringify(Array.from(savedWindowsMapping.entries())); + this._settings.set_string('windows-mapping', newSavedWindowsMappingJsonStr); + Gio.Settings.sync(); } destroy() { @@ -90,6 +69,12 @@ var OpenWindowsInfoTracker = class { this._display.disconnect(this._displayId); this._displayId = 0; } + + if (this._metaRestartId) { + this._display.disconnect(this._metaRestartId); + this._metaRestartId = 0; + } + } -} \ No newline at end of file +} diff --git a/schemas/gschemas.compiled b/schemas/gschemas.compiled index 6f196afc87eee7982ca8773d8934ee44cf970425..16244e835ea2292d3480b27a2392097f4fb59012 100644 GIT binary patch literal 860 zcmaJ4e-6n=?0qaLWBi6K@Z${sSiR$@Rai>O$nQ3Gl*=M8&H_GXTq*~Fkp;}1|l zYg-#DTZR}M)dn;_pk{Le0(3@zJ2rNy|;WvSB17g+Oq&&ZFDl-dQ;%) z*U~vMzb(mic?tduqcbP+2j}(y7;;Ni!S?}IvbqD?)_!1iO zteAPhc@j!x(f9eeGWrc2c`X5VRQV`oTT5=b#O6C4NR#w>9fz0 z6g^{gzeE~DOj5Rk@{GX2|IGwFMmHX}sqa0&7vGC{4j~F{7tZV;fYRf*)9#H|Rb3kYcq`twUn&)f(PW>Rd-1Mxtiur5GAw8o7c`3t dn*CeQ?EedziTJiVO+Ee1rj%Ia6zhyiu0aF63@z zT6R}D?dZ#nGAh*tuMr}8S+QVF)-eRsfZ<0NfgS^{lJjfEv7TA6BhJ8=K)*I>)<(Vs zqUDnXTGX!*KZ*ns!0gBKY5LT2@Rz~wfp1&m-|17og1-jd0RHTE$N6^Z8vIT0F>sI# z9WtJJ68;bHFJS8T;9vUG^YAah|A5og?i79MxA1SlcfitxUZGF@2L1&4%08>o+d?~j z`rUKVXB{>#^exKjqd3QfqoGIaAC89pv2VP?c~yAX(z)6Ty%jj8jcVpvK9vHexT0pg zrJH4%W+s&#+tQUyKi*cpUHv8)fFuk+5(Xf77=YyY0Z4>!y(&KE)@j+?lSP?p7cL%P sp8n&Nm*EZNHipT!ZRw?QTd0enh + + '[]' + The mapping of xid and stable sequence of a window on X11 + + + '{}' Rules that are used to close applications From 2657e3cd7f977694fc98bf7e37a5ebde89354dd5 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Tue, 21 Jun 2022 01:35:08 +0800 Subject: [PATCH 38/49] stash --- closeSession.js | 2 +- .../org.freedesktop.login1.Manager.xml | 15 ++++++ openWindowsInfoTracker.js | 50 +++++++++++++++++-- 3 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 dbus-interfaces/org.freedesktop.login1.Manager.xml diff --git a/closeSession.js b/closeSession.js index e40f768..2690be3 100644 --- a/closeSession.js +++ b/closeSession.js @@ -199,7 +199,7 @@ var CloseSession = class { const savedWindowsMappingJsonStr = this._settings.get_string('windows-mapping'); const savedWindowsMapping = new Map(JSON.parse(savedWindowsMappingJsonStr)); - const app_info = shellApp.get_app_info(); + const app_info = app.get_app_info(); const desktopFullPath = app_info.get_filename(); const xidObj = savedWindowsMapping.get(desktopFullPath); const windows = app.get_windows(); diff --git a/dbus-interfaces/org.freedesktop.login1.Manager.xml b/dbus-interfaces/org.freedesktop.login1.Manager.xml new file mode 100644 index 0000000..c5b051c --- /dev/null +++ b/dbus-interfaces/org.freedesktop.login1.Manager.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/openWindowsInfoTracker.js b/openWindowsInfoTracker.js index 6dbfd65..5d24b0a 100644 --- a/openWindowsInfoTracker.js +++ b/openWindowsInfoTracker.js @@ -1,6 +1,8 @@ 'use strict'; -const { Shell, Meta, Gio } = imports.gi; +const { Shell, Meta, Gio, GLib } = imports.gi; + +const ByteArray = imports.byteArray; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); @@ -18,9 +20,47 @@ var OpenWindowsInfoTracker = class { this._prefsUtils = new PrefsUtils.PrefsUtils(); this._settings = this._prefsUtils.getSettings(); + const SystemdLoginManagerIface = ByteArray.toString( + Me.dir.get_child('dbus-interfaces').get_child('org.freedesktop.login1.Manager.xml').load_contents(null)[1]); + const SystemdLoginManager = Gio.DBusProxy.makeProxyWrapper(SystemdLoginManagerIface); + this._proxy = new SystemdLoginManager(Gio.DBus.system, + 'org.freedesktop.login1', + '/org/freedesktop/login1'); + this._proxy.connectSignal('UserNew', this._userNew.bind(this)); + this._proxy.connectSignal('UserRemoved', this._userRemove.bind(this)); + this._proxy.connectSignal('PrepareForShutdown', this._prepareForShutdown.bind(this)); + this._display = global.display; this._displayId = this._display.connect('window-created', this._windowCreated.bind(this)); + + } + + _userNew(proxy, sender, [uid, object_path]) { + log('_userNew'); + log(uid); + log(object_path); + } + + _userRemove(proxy, sender, [uid, object_path]) { + log('_userRemove'); + log(uid); + log(object_path); + } + + _prepareForShutdown(proxy, sender, [aboutToShutdown]) { + log(`Cleaning windows-mapping before shutdown or reboot. ${aboutToShutdown}`); + this._settings.set_string('windows-mapping', '{}'); + } + async inhibit(reason, cancellable) { + log('inhibit ddd') + const inVariant = new GLib.Variant('(ssss)', + ['sleep', 'GNOME Shell ex', reason, 'delay']); + const [outVariant_, fdList] = + await this._proxy.call_with_unix_fd_list('Inhibit', + inVariant, 0, -1, null, cancellable); + const [fd] = fdList.steal_fds(); + return new Gio.UnixInputStream({ fd }); } _windowCreated(display, metaWindow, userData) { @@ -37,8 +77,12 @@ var OpenWindowsInfoTracker = class { const xid = metaWindow.get_description(); const windowStableSequence = metaWindow.get_stable_sequence(); const savedWindowsMappingJsonStr = this._settings.get_string('windows-mapping'); - const savedWindowsMapping = new Map(JSON.parse(savedWindowsMappingJsonStr)); - + let savedWindowsMapping; + if (savedWindowsMappingJsonStr === '{}') { + savedWindowsMapping = new Map(); + } else { + savedWindowsMapping = new Map(JSON.parse(savedWindowsMappingJsonStr)); + } const desktopFullPath = app_info.get_filename(); let xidObj = savedWindowsMapping.get(desktopFullPath); if (xidObj && !xidObj[xid]) { From 93f9cb58e9f99d25fbb32a02ff8975a48c85bce5 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Tue, 21 Jun 2022 02:50:03 +0800 Subject: [PATCH 39/49] Cleanup when the app status changed --- .../org.freedesktop.login1.Manager.xml | 15 ---- openWindowsInfoTracker.js | 88 +++++++++---------- 2 files changed, 42 insertions(+), 61 deletions(-) delete mode 100644 dbus-interfaces/org.freedesktop.login1.Manager.xml diff --git a/dbus-interfaces/org.freedesktop.login1.Manager.xml b/dbus-interfaces/org.freedesktop.login1.Manager.xml deleted file mode 100644 index c5b051c..0000000 --- a/dbus-interfaces/org.freedesktop.login1.Manager.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/openWindowsInfoTracker.js b/openWindowsInfoTracker.js index 5d24b0a..f67ecff 100644 --- a/openWindowsInfoTracker.js +++ b/openWindowsInfoTracker.js @@ -2,8 +2,6 @@ const { Shell, Meta, Gio, GLib } = imports.gi; -const ByteArray = imports.byteArray; - const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); @@ -20,47 +18,11 @@ var OpenWindowsInfoTracker = class { this._prefsUtils = new PrefsUtils.PrefsUtils(); this._settings = this._prefsUtils.getSettings(); - const SystemdLoginManagerIface = ByteArray.toString( - Me.dir.get_child('dbus-interfaces').get_child('org.freedesktop.login1.Manager.xml').load_contents(null)[1]); - const SystemdLoginManager = Gio.DBusProxy.makeProxyWrapper(SystemdLoginManagerIface); - this._proxy = new SystemdLoginManager(Gio.DBus.system, - 'org.freedesktop.login1', - '/org/freedesktop/login1'); - this._proxy.connectSignal('UserNew', this._userNew.bind(this)); - this._proxy.connectSignal('UserRemoved', this._userRemove.bind(this)); - this._proxy.connectSignal('PrepareForShutdown', this._prepareForShutdown.bind(this)); + this._appSystem = Shell.AppSystem.get_default(); + this._appSystem.connect('app-state-changed', this._cleanUp.bind(this)); this._display = global.display; this._displayId = this._display.connect('window-created', this._windowCreated.bind(this)); - - } - - _userNew(proxy, sender, [uid, object_path]) { - log('_userNew'); - log(uid); - log(object_path); - } - - _userRemove(proxy, sender, [uid, object_path]) { - log('_userRemove'); - log(uid); - log(object_path); - } - - _prepareForShutdown(proxy, sender, [aboutToShutdown]) { - log(`Cleaning windows-mapping before shutdown or reboot. ${aboutToShutdown}`); - this._settings.set_string('windows-mapping', '{}'); - } - - async inhibit(reason, cancellable) { - log('inhibit ddd') - const inVariant = new GLib.Variant('(ssss)', - ['sleep', 'GNOME Shell ex', reason, 'delay']); - const [outVariant_, fdList] = - await this._proxy.call_with_unix_fd_list('Inhibit', - inVariant, 0, -1, null, cancellable); - const [fd] = fdList.steal_fds(); - return new Gio.UnixInputStream({ fd }); } _windowCreated(display, metaWindow, userData) { @@ -85,12 +47,21 @@ var OpenWindowsInfoTracker = class { } const desktopFullPath = app_info.get_filename(); let xidObj = savedWindowsMapping.get(desktopFullPath); - if (xidObj && !xidObj[xid]) { - xidObj[xid] = { - windowTitle: metaWindow.get_title(), - xid: xid, - windowStableSequence: windowStableSequence - }; + if (xidObj) { + const windows = shellApp.get_windows(); + const removedXids = Object.keys(xidObj).filter(xid => + !windows.find(w => w.get_description() === xid)); + removedXids.forEach(xid => { + delete xidObj[xid]; + }); + + if (!xidObj[xid]){ + xidObj[xid] = { + windowTitle: metaWindow.get_title(), + xid: xid, + windowStableSequence: windowStableSequence + }; + } } else { if (!xidObj) { xidObj = {}; @@ -106,6 +77,31 @@ var OpenWindowsInfoTracker = class { const newSavedWindowsMappingJsonStr = JSON.stringify(Array.from(savedWindowsMapping.entries())); this._settings.set_string('windows-mapping', newSavedWindowsMappingJsonStr); Gio.Settings.sync(); + + } + + _cleanUp(appSys, app) { + let state = app.state; + if (state != Shell.AppState.STOPPED) { + return; + } + + const app_info = app.get_app_info(); + if (!app_info) { + return; + } + + const savedWindowsMappingJsonStr = this._settings.get_string('windows-mapping'); + if (savedWindowsMappingJsonStr === '{}') { + return; + } + + let savedWindowsMapping = new Map(JSON.parse(savedWindowsMappingJsonStr)); + const desktopFullPath = app_info.get_filename(); + savedWindowsMapping.delete(desktopFullPath); + const newSavedWindowsMappingJsonStr = JSON.stringify(Array.from(savedWindowsMapping.entries())); + this._settings.set_string('windows-mapping', newSavedWindowsMappingJsonStr); + Gio.Settings.sync(); } destroy() { From 19a0e21f9d39f4a1d9ff16d042502cbf8f897eb2 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Tue, 21 Jun 2022 18:11:28 +0800 Subject: [PATCH 40/49] Try to fix the below issue: ` libmutter:ERROR:../src/core/window.c:5278:meta_window_get_workspaces: code should not be reached Bail out! libmutter:ERROR:../src/core/window.c:5278:meta_window_get_workspaces: code should not be reached == Stack trace for context 0x55a0482f7170 == 55a051daf820 i ~/.local/share/gnome-shell/extensions/another-window-session-manager@gmail.com/moveSession.js:337 (2a18805975b0 @ 323) 55a051daf760 i ~/.local/share/gnome-shell/extensions/another-window-session-manager@gmail.com/moveSession.js:316 (2a1880597510 @ 315) 55a051daf6a8 i ~/.local/share/gnome-shell/extensions/another-window-session-manager@gmail.com/moveSession.js:205 (2a1880597380 @ 67) ` --- closeSession.js | 3 ++- indicator.js | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/closeSession.js b/closeSession.js index 2690be3..7837934 100644 --- a/closeSession.js +++ b/closeSession.js @@ -59,7 +59,8 @@ var CloseSession = class { this._log.debug(`Skipping ${app.get_name()} because it has more than one windows`); } else { this._log.debug(`Closing ${app.get_name()}`); - app.request_quit(); + app.get_windows().forEach(w => w._aboutToClose = true); + app.request_quit(); } } diff --git a/indicator.js b/indicator.js index 43a75c9..d0eb949 100644 --- a/indicator.js +++ b/indicator.js @@ -133,6 +133,10 @@ class AwsIndicator extends PanelMenu.Button { return; } + if (metaWindow._aboutToClose) { + return; + } + const shellApp = this._windowTracker.get_window_app(metaWindow); if (!shellApp) { return; @@ -166,6 +170,10 @@ class AwsIndicator extends PanelMenu.Button { return; } + if (metaWindow._aboutToClose) { + return; + } + const shellApp = this._windowTracker.get_window_app(metaWindow); if (!shellApp) { return; From a9f5394fc765739c6668ab5a42c80b32ff08b57d Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Fri, 24 Jun 2022 18:52:43 +0800 Subject: [PATCH 41/49] 1. Reset when the system is logout, reboot or shutdown 2. Proceed the next rule --- closeSession.js | 20 ++----- .../org.freedesktop.login1.Manager.xml | 26 +++++++++ ....gnome.SessionManager.EndSessionDialog.xml | 16 ++++++ openWindowsInfoTracker.js | 55 ++++++++----------- utils/loginManager.js | 53 ++++++++++++++++++ 5 files changed, 123 insertions(+), 47 deletions(-) create mode 100644 dbus-interfaces/org.freedesktop.login1.Manager.xml create mode 100644 dbus-interfaces/org.gnome.SessionManager.EndSessionDialog.xml create mode 100644 utils/loginManager.js diff --git a/closeSession.js b/closeSession.js index 7837934..b7ee6b0 100644 --- a/closeSession.js +++ b/closeSession.js @@ -102,22 +102,10 @@ var CloseSession = class { const hiddenId = Main.overview.connect('hidden', () => { Main.overview.disconnect(hiddenId); - const result = this._activateAndCloseWindows(app, keycodesSegments, shortcutsOriginal, running_apps_closing_by_rules); - if (!result) { - // Fallback to close it again in the normal way - this._closeOneApp(app); - } else { - this._tryCloseAppsByRules(running_apps_closing_by_rules); - } + this._activateAndCloseWindows(app, keycodesSegments, shortcutsOriginal, running_apps_closing_by_rules); }); } else { - const result = this._activateAndCloseWindows(app, keycodesSegments, shortcutsOriginal, running_apps_closing_by_rules); - if (!result) { - // Fallback to close it again in the normal way - this._closeOneApp(app); - } else { - this._tryCloseAppsByRules(running_apps_closing_by_rules); - } + this._activateAndCloseWindows(app, keycodesSegments, shortcutsOriginal, running_apps_closing_by_rules); } } @@ -151,6 +139,8 @@ var CloseSession = class { _activateAndCloseWindows(app, linuxKeyCodesSegments, shortcutsOriginal, running_apps_closing_by_rules) { if (!linuxKeyCodesSegments || linuxKeyCodesSegments.length === 0) { + // Proceed the next rule + this._tryCloseAppsByRules(running_apps_closing_by_rules); return; } const linuxKeyCodes = linuxKeyCodesSegments.shift(); @@ -167,10 +157,8 @@ var CloseSession = class { SubprocessUtils.trySpawnAsync(cmd, (output) => { this._log.info(`Succeed to send keys to close the windows of the previous app ${app.get_name()}. output: ${output}`); this._activateAndCloseWindows(app, linuxKeyCodesSegments, shortcutsOriginal, running_apps_closing_by_rules); - return true; }, (output) => { this._log.info(`Failed to send keys to close the windows of the previous app ${app.get_name()}. output: ${output}`); - return false; }); } diff --git a/dbus-interfaces/org.freedesktop.login1.Manager.xml b/dbus-interfaces/org.freedesktop.login1.Manager.xml new file mode 100644 index 0000000..4bd3b8c --- /dev/null +++ b/dbus-interfaces/org.freedesktop.login1.Manager.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dbus-interfaces/org.gnome.SessionManager.EndSessionDialog.xml b/dbus-interfaces/org.gnome.SessionManager.EndSessionDialog.xml new file mode 100644 index 0000000..3a57aa4 --- /dev/null +++ b/dbus-interfaces/org.gnome.SessionManager.EndSessionDialog.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/openWindowsInfoTracker.js b/openWindowsInfoTracker.js index f67ecff..c279cda 100644 --- a/openWindowsInfoTracker.js +++ b/openWindowsInfoTracker.js @@ -2,12 +2,18 @@ const { Shell, Meta, Gio, GLib } = imports.gi; +const ByteArray = imports.byteArray; + const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); const Log = Me.imports.utils.log; const PrefsUtils = Me.imports.utils.prefsUtils; +const EndSessionDialogIface = ByteArray.toString( + Me.dir.get_child('dbus-interfaces').get_child('org.gnome.SessionManager.EndSessionDialog.xml').load_contents(null)[1]); +const EndSessionDialogProxy = Gio.DBusProxy.makeProxyWrapper(EndSessionDialogIface); + var OpenWindowsInfoTracker = class { constructor() { @@ -18,12 +24,28 @@ var OpenWindowsInfoTracker = class { this._prefsUtils = new PrefsUtils.PrefsUtils(); this._settings = this._prefsUtils.getSettings(); - this._appSystem = Shell.AppSystem.get_default(); - this._appSystem.connect('app-state-changed', this._cleanUp.bind(this)); + this._endSessionProxy = new EndSessionDialogProxy(Gio.DBus.session, + 'org.gnome.SessionManager.EndSessionDialog', + '/org/gnome/SessionManager/EndSessionDialog'); + + this._endSessionProxy.connectSignal('ConfirmedLogout', this._resetWindowsMapping.bind(this)); + this._endSessionProxy.connectSignal('ConfirmedReboot', this._resetWindowsMapping.bind(this)); + this._endSessionProxy.connectSignal('ConfirmedShutdown', this._resetWindowsMapping.bind(this)); + this._endSessionProxy.connectSignal('Closed', this._close.bind(this)); + this._endSessionProxy.connectSignal('Canceled', this._close.bind(this)); this._display = global.display; this._displayId = this._display.connect('window-created', this._windowCreated.bind(this)); } + + _close() { + log(`_close`); + } + + _resetWindowsMapping(proxy, sender, [aboutToShutdown]) { + log(`Resetting windows-mapping before logout / shutdown / reboot. ${aboutToShutdown}`); + this._settings.set_string('windows-mapping', '{}'); + } _windowCreated(display, metaWindow, userData) { const shellApp = this._windowTracker.get_window_app(metaWindow); @@ -80,40 +102,11 @@ var OpenWindowsInfoTracker = class { } - _cleanUp(appSys, app) { - let state = app.state; - if (state != Shell.AppState.STOPPED) { - return; - } - - const app_info = app.get_app_info(); - if (!app_info) { - return; - } - - const savedWindowsMappingJsonStr = this._settings.get_string('windows-mapping'); - if (savedWindowsMappingJsonStr === '{}') { - return; - } - - let savedWindowsMapping = new Map(JSON.parse(savedWindowsMappingJsonStr)); - const desktopFullPath = app_info.get_filename(); - savedWindowsMapping.delete(desktopFullPath); - const newSavedWindowsMappingJsonStr = JSON.stringify(Array.from(savedWindowsMapping.entries())); - this._settings.set_string('windows-mapping', newSavedWindowsMappingJsonStr); - Gio.Settings.sync(); - } - destroy() { if (this._displayId) { this._display.disconnect(this._displayId); this._displayId = 0; } - - if (this._metaRestartId) { - this._display.disconnect(this._metaRestartId); - this._metaRestartId = 0; - } } diff --git a/utils/loginManager.js b/utils/loginManager.js new file mode 100644 index 0000000..e651d85 --- /dev/null +++ b/utils/loginManager.js @@ -0,0 +1,53 @@ + + +var LoginManager = class { + + constructor() { + const SystemdLoginManagerIface = ByteArray.toString( + Me.dir.get_child('dbus-interfaces').get_child('org.freedesktop.login1.Manager.xml').load_contents(null)[1]); + const SystemdLoginManager = Gio.DBusProxy.makeProxyWrapper(SystemdLoginManagerIface); + this._proxy = new SystemdLoginManager(Gio.DBus.system, + 'org.freedesktop.login1', + '/org/freedesktop/login1'); + this._proxy.connectSignal('SeatNew', this._userNew.bind(this)); + this._proxy.connectSignal('UserRemoved', this._userRemove.bind(this)); + this._proxy.connectSignal('SessionRemoved', this._sessionRemoved.bind(this)); + this._proxy.connectSignal('PrepareForShutdown', this._prepareForShutdown.bind(this)); + } + + _userNew(proxy, sender, [uid, object_path]) { + log('_userNew'); + log(uid); + log(object_path); + } + + _sessionRemoved(proxy, sender, [session_id, object_path]) { + log('_sessionRemoved'); + log(session_id); + log(object_path); + } + + _userRemove(proxy, sender, [uid, object_path]) { + log('_userRemove'); + log(uid); + log(object_path); + } + + _prepareForShutdown(proxy, sender, [aboutToShutdown]) { + log(`Cleaning windows-mapping before shutdown or reboot. ${aboutToShutdown}`); + this._settings.set_string('windows-mapping', '{}'); + } + + inhibit(reason) { + log('inhibit ddd') + const inVariant = new GLib.Variant('(ssss)', + ['shutdown', 'gnome-shell-extension-another-window-session-manager', reason, 'delay']); + // See: https://gjs-docs.gnome.org/gio20~2.66p/gio.dbusproxy#method-call_with_unix_fd_list_sync + const [outVariant_, fdList] = + this._proxy.call_with_unix_fd_list_sync('Inhibit', + inVariant, Gio.DBusCallFlags.NONE, -1, null, null); + const [fd] = fdList.steal_fds(); + return new Gio.UnixInputStream({ fd }); + } + +} \ No newline at end of file From 9dde84b3f678bdcdfced6ce719855abde7d4bdd6 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sat, 25 Jun 2022 02:02:01 +0800 Subject: [PATCH 42/49] Reset when the system is logout, reboot or shutdown --- openWindowsInfoTracker.js | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/openWindowsInfoTracker.js b/openWindowsInfoTracker.js index c279cda..73f62b0 100644 --- a/openWindowsInfoTracker.js +++ b/openWindowsInfoTracker.js @@ -25,25 +25,39 @@ var OpenWindowsInfoTracker = class { this._settings = this._prefsUtils.getSettings(); this._endSessionProxy = new EndSessionDialogProxy(Gio.DBus.session, - 'org.gnome.SessionManager.EndSessionDialog', - '/org/gnome/SessionManager/EndSessionDialog'); + 'org.gnome.Shell', + '/org/gnome/SessionManager/EndSessionDialog'); - this._endSessionProxy.connectSignal('ConfirmedLogout', this._resetWindowsMapping.bind(this)); - this._endSessionProxy.connectSignal('ConfirmedReboot', this._resetWindowsMapping.bind(this)); - this._endSessionProxy.connectSignal('ConfirmedShutdown', this._resetWindowsMapping.bind(this)); - this._endSessionProxy.connectSignal('Closed', this._close.bind(this)); - this._endSessionProxy.connectSignal('Canceled', this._close.bind(this)); + this._endSessionProxy.connectSignal('ConfirmedLogout', this._onConfirmedLogout.bind(this)); + this._endSessionProxy.connectSignal('ConfirmedReboot', this._onConfirmedReboot.bind(this)); + this._endSessionProxy.connectSignal('ConfirmedShutdown', this._onConfirmedShutdown.bind(this)); + this._endSessionProxy.connectSignal('Closed', this._onClose.bind(this)); + this._endSessionProxy.connectSignal('Canceled', this._onCancel.bind(this)); this._display = global.display; this._displayId = this._display.connect('window-created', this._windowCreated.bind(this)); } - _close() { - log(`_close`); + _onClose() { + this._log.debug(`User closed endSessionDialog`); } - _resetWindowsMapping(proxy, sender, [aboutToShutdown]) { - log(`Resetting windows-mapping before logout / shutdown / reboot. ${aboutToShutdown}`); + _onCancel() { + this._log.debug(`User cancel endSessionDialog`); + } + + _onConfirmedLogout(proxy, sender) { + this._log.debug(`Resetting windows-mapping before logout.`); + this._settings.set_string('windows-mapping', '{}'); + } + + _onConfirmedReboot(proxy, sender) { + this._log.debug(`Resetting windows-mapping before reboot.`); + this._settings.set_string('windows-mapping', '{}'); + } + + _onConfirmedShutdown(proxy, sender) { + this._log.debug(`Resetting windows-mapping before shutdown.`); this._settings.set_string('windows-mapping', '{}'); } From a0a6ca6d9206b3a04fa1d65c3a8fa53fac8ac37d Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sun, 26 Jun 2022 00:59:30 +0800 Subject: [PATCH 43/49] tweak ui --- ui/prefs-gtk4.ui | 51 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/ui/prefs-gtk4.ui b/ui/prefs-gtk4.ui index ab64416..017c514 100644 --- a/ui/prefs-gtk4.ui +++ b/ui/prefs-gtk4.ui @@ -183,8 +183,7 @@ 1 - Restore at startup -(Install a .desktop to ~/.config/autostart if enabled) + Restore at startup 1 0 @@ -201,6 +200,23 @@ 1 0 + 2 + + + + + + 1 + Install a .desktop to ~/.config/autostart if enabled + True + 1 + 0 + + + 0 + 1 @@ -212,7 +228,7 @@ 0 0 - 1 + 2 @@ -223,7 +239,7 @@ center 1 - 1 + 2 @@ -236,7 +252,7 @@ 0 0 - 2 + 3 @@ -249,20 +265,19 @@ 10 1 - 2 + 3 1 - Autostart delay([0, 3600]s) -(Update the above .desktop if changed) + Autostart delay([0, 3600]s) 1 0 0 - 3 + 4 @@ -275,12 +290,26 @@ 20 1 - 3 + 4 + 2 - + + 1 + Update the above .desktop if changed + True + 1 + 0 + + + 0 + 5 + + From 5b54281bbb92833fd89c1c796914287552731782 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Sun, 26 Jun 2022 02:12:27 +0800 Subject: [PATCH 44/49] Install the udev rules to make `ydotool` work under the naumal user --- bin/install-udev-rules-for-ydotool.sh | 6 ++++ prefs.js | 43 +++++++++++++++++++++++++-- template/60-awsm-ydotool-uinput.rules | 6 ++++ ui/prefs-gtk4.ui | 16 +++++++++- utils/fileUtils.js | 3 ++ 5 files changed, 71 insertions(+), 3 deletions(-) create mode 100755 bin/install-udev-rules-for-ydotool.sh create mode 100644 template/60-awsm-ydotool-uinput.rules diff --git a/bin/install-udev-rules-for-ydotool.sh b/bin/install-udev-rules-for-ydotool.sh new file mode 100755 index 0000000..3251c32 --- /dev/null +++ b/bin/install-udev-rules-for-ydotool.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# $1: FileUtils.desktop_template_path_ydotool_uinput_rules +# $2: FileUtils.system_udev_rules_path_ydotool_uinput_rules + +cp "$1" "$2" && chmod 644 "$2" + diff --git a/prefs.js b/prefs.js index 0433ef0..d1155d3 100644 --- a/prefs.js +++ b/prefs.js @@ -4,6 +4,7 @@ const { Gtk, GObject, Gio, GLib } = imports.gi; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); +const SubprocessUtils = Me.imports.utils.subprocessUtils; const FileUtils = Me.imports.utils.fileUtils; const Log = Me.imports.utils.log; @@ -99,6 +100,12 @@ const Prefs = GObject.registerClass( this._installAutostartDesktopFile(); }); + this._settings.connect('changed::enable-close-by-rules', (settings) => { + if (this._settings.get_boolean('enable-close-by-rules')) { + this._install_udev_rules_for_ydotool(); + } + }); + } render_ui() { @@ -137,6 +144,38 @@ const Prefs = GObject.registerClass( } + _install_udev_rules_for_ydotool() { + // Check the `/dev/uinput` permission of `read` and `write` + const uinputFile = Gio.File.new_for_path('/dev/uinput'); + let info = uinputFile.query_info( + [Gio.FILE_ATTRIBUTE_ACCESS_CAN_READ, + Gio.FILE_ATTRIBUTE_ACCESS_CAN_WRITE].join(','), + Gio.FileQueryInfoFlags.NONE, + null); + + const readable = info.get_attribute_boolean(Gio.FILE_ATTRIBUTE_ACCESS_CAN_READ); + const writable = info.get_attribute_boolean(Gio.FILE_ATTRIBUTE_ACCESS_CAN_WRITE); + if (readable && writable) { + return; + } + + // Copy `60-awsm-ydotool-input.rules` to `/etc/udev/rules.d/` + const pkexecPath = GLib.find_program_in_path('pkexec'); + const cmd = [pkexecPath, + GLib.build_filenamev([Me.path, '/bin/install-udev-rules-for-ydotool.sh']), + FileUtils.desktop_template_path_ydotool_uinput_rules, + FileUtils.system_udev_rules_path_ydotool_uinput_rules, + ]; + SubprocessUtils.trySpawnAsync(cmd, (output) => { + this._log.info(`Installed the udev uinput rules ${FileUtils.desktop_template_path_ydotool_uinput_rules} to ${FileUtils.system_udev_rules_path_ydotool_uinput_rules}! This rule should take effect after relogin or reboot.`); + // TODO Send notification + }, (output) => { + this._settings.set_boolean('enable-close-by-rules', false); + this._log.error(new Error(output), `Failed to install the udev uinput rules '${FileUtils.desktop_template_path_ydotool_uinput_rules}'`) + // TODO Send notification + }); + } + _installAutostartDesktopFile() { const argument = { autostartDelay: this._settings.get_int('autostart-delay'), @@ -156,10 +195,10 @@ const Prefs = GObject.registerClass( if (success) { this._log.info(`Installed the autostart desktop file: ${FileUtils.autostart_restore_desktop_file_path}!`); } else { - this._log.error(`Failed to install the autostart desktop file: ${FileUtils.autostart_restore_desktop_file_path}`) + this._log.error(new Error(`Failed to install the autostart desktop file: ${FileUtils.autostart_restore_desktop_file_path}`)) } } else { - this._log.error(`Failed to create folder: ${autostart_restore_desktop_file_path_parent}`); + this._log.error(new Error(`Failed to create folder: ${autostart_restore_desktop_file_path_parent}`)); } } diff --git a/template/60-awsm-ydotool-uinput.rules b/template/60-awsm-ydotool-uinput.rules new file mode 100644 index 0000000..fd9aec6 --- /dev/null +++ b/template/60-awsm-ydotool-uinput.rules @@ -0,0 +1,6 @@ +# See: +# https://github.com/ValveSoftware/steam-devices/blob/master/60-steam-input.rules +# https://github.com/ReimuNotMoe/ydotool/issues/25 + +# ydotool udev write access +KERNEL=="uinput", SUBSYSTEM=="misc", TAG+="uaccess", OPTIONS+="static_node=uinput" diff --git a/ui/prefs-gtk4.ui b/ui/prefs-gtk4.ui index 017c514..43cfe4b 100644 --- a/ui/prefs-gtk4.ui +++ b/ui/prefs-gtk4.ui @@ -49,11 +49,25 @@ 1 0 + 2 - + + 1 + Authentication may be required to install the udev rules to make `ydotool` work under the naumal user + True + 1 + 0 + + + 0 + 1 + + diff --git a/utils/fileUtils.js b/utils/fileUtils.js index 48d46ba..6c35193 100644 --- a/utils/fileUtils.js +++ b/utils/fileUtils.js @@ -22,6 +22,9 @@ var recently_closed_session_path = GLib.build_filenamev([sessions_path, recently var autostart_restore_desktop_file_path = GLib.build_filenamev([home_dir, '/.config/autostart/_gnome-shell-extension-another-window-session-manager.desktop']); +var desktop_template_path_ydotool_uinput_rules = GLib.build_filenamev([Me.path, '/template/60-awsm-ydotool-uinput.rules']); +var system_udev_rules_path_ydotool_uinput_rules = '/etc/udev/rules.d/60-awsm-ydotool-uinput.rules'; + function get_sessions_path() { return sessions_path; From d2daedbfa6733fe367f30b756bc42824773cf1fb Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Mon, 27 Jun 2022 00:38:03 +0800 Subject: [PATCH 45/49] Update README.md --- README.md | 43 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c3aaa67..34d3c47 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,36 @@ To modify the delay, timer, and how to restore a session: 1. Search saved session by the session name fuzzily 1. ... -## How to `Restore a session at startup`? +## Close windows + +### How to make `Close by rules` work + +```bash +# 1. Install `ydotool` using the package manager and make sure the version is greater than v1.0.0 +sudo dnf install ydotool +#Or install it from the source code: https://github.com/ReimuNotMoe/ydotool + +#Check the permission of `/dev/uinput`, if it's `crw-rw----+`, you can skip step 2 +# 2. Get permission to access to `/dev/uinput` as the normal user +sudo echo '# See: + # https://github.com/ValveSoftware/steam-devices/blob/master/60-steam-input.rules + # https://github.com/ReimuNotMoe/ydotool/issues/25 + + # ydotool udev write access + KERNEL=="uinput", SUBSYSTEM=="misc", TAG+="uaccess", OPTIONS+="static_node=uinput"' > /etc/udev/rules.d/60-awsm-ydotool-uinput.rules +#Remove executable permission (a.k.a. x) +sudo chmod 644 /etc/udev/rules.d/60-awsm-ydotool-uinput.rules + +# 3. Autostart the ydotoold service under the normal user +sudo cp /usr/lib/systemd/system/ydotool.service /usr/lib/systemd/user +sudo systemctl --user enable ydotool.service +``` + +And then reboot the system to take effect. Relogin maybe work too. + +## Restore sessions + +### How to `Restore a session at startup`? To make it work, you must enable it through `Restore sessions -> Restore at startup` in the Preferences AND active a session by clicking in the popup menu. @@ -91,9 +120,17 @@ Please do not modify `_gnome-shell-extension-another-window-session-manager.desk # Dependencies -This project uses `ps` and `pwdx` to get some information from a process, install it via `dnf install procps-ng` if you don't have. +* procps-ng + +Use `ps` and `pwdx` to get some information from a process, install it via `dnf install procps-ng` if you don't have. + +* glib2 + +Use `gdbus` to call the remote method, which is provided by this exension, to implement the `restore at start` feature. `gdbus` is part of `glib2`. + +* ydotool -And it uses `gdbus` to call the remote method, which is provided by this exension, to implement the `restore at start` feature. `gdbus` is part of `glib2`. +Send keys to close the application with multiple windows. # Known issues From db98b83751ea52d9a21df133bfb0b3160dc5130f Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Mon, 27 Jun 2022 00:50:03 +0800 Subject: [PATCH 46/49] Add a link How to make `Close by rules` work --- closeSession.js | 18 ++++++++++-------- prefs.js | 10 +++++----- ui/prefs-gtk4.ui | 2 +- utils/subprocessUtils.js | 13 +++++++++++++ 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/closeSession.js b/closeSession.js index b7ee6b0..99345e1 100644 --- a/closeSession.js +++ b/closeSession.js @@ -67,7 +67,7 @@ var CloseSession = class { _tryCloseAppsByRules(running_apps_closing_by_rules) { if (!running_apps_closing_by_rules || running_apps_closing_by_rules.length === 0) { return; - } + } const app = running_apps_closing_by_rules.shift(); @@ -154,12 +154,13 @@ var CloseSession = class { this._log.info(`Closing the app ${app.get_name()} by sending: ${cmdStr} (${shortcutsOriginal.join(' ')})`); this._activateAndFocusWindow(app); - SubprocessUtils.trySpawnAsync(cmd, (output) => { + SubprocessUtils.trySpawnAsync(cmd, + (output) => { this._log.info(`Succeed to send keys to close the windows of the previous app ${app.get_name()}. output: ${output}`); this._activateAndCloseWindows(app, linuxKeyCodesSegments, shortcutsOriginal, running_apps_closing_by_rules); - }, (output) => { - this._log.info(`Failed to send keys to close the windows of the previous app ${app.get_name()}. output: ${output}`); - }); + }, (output) => { + this._log.error(new Error(`Failed to send keys to close the windows of the previous app ${app.get_name()}. output: ${output}`)); + }); } _getRunningAppsClosingByRules() { @@ -217,9 +218,10 @@ var CloseSession = class { }); const topLevelWindow = windows[windows.length - 1]; - this._log.info(`Activating the running window ${topLevelWindow.get_title()} of ${app.get_name()}`); - Main.activateWindow(topLevelWindow); - + if (topLevelWindow) { + this._log.info(`Activating the running window ${topLevelWindow.get_title()} of ${app.get_name()}`); + Main.activateWindow(topLevelWindow); + } } _skip_multiple_windows(shellApp) { diff --git a/prefs.js b/prefs.js index d1155d3..07cb3aa 100644 --- a/prefs.js +++ b/prefs.js @@ -100,11 +100,11 @@ const Prefs = GObject.registerClass( this._installAutostartDesktopFile(); }); - this._settings.connect('changed::enable-close-by-rules', (settings) => { - if (this._settings.get_boolean('enable-close-by-rules')) { - this._install_udev_rules_for_ydotool(); - } - }); + // this._settings.connect('changed::enable-close-by-rules', (settings) => { + // if (this._settings.get_boolean('enable-close-by-rules')) { + // this._install_udev_rules_for_ydotool(); + // } + // }); } diff --git a/ui/prefs-gtk4.ui b/ui/prefs-gtk4.ui index 43cfe4b..62f3aef 100644 --- a/ui/prefs-gtk4.ui +++ b/ui/prefs-gtk4.ui @@ -56,7 +56,7 @@ 1 - Authentication may be required to install the udev rules to make `ydotool` work under the naumal user + To make this feature work, see: <a href='https://github.com/nlpsuge/gnome-shell-extension-another-window-session-manager#how-to-make-close-by-rules-work'>How to make `Close by rules` work</a> True 1 0 diff --git a/utils/subprocessUtils.js b/utils/subprocessUtils.js index c97dc6f..a9188e6 100644 --- a/utils/subprocessUtils.js +++ b/utils/subprocessUtils.js @@ -19,6 +19,19 @@ function readOutput(stream, lineBuffer) { }); } +var trySpawn = function(commandLineArray, callBackOnSuccess, callBackOnFailure) { + return new Promise((resolve, reject) => { + trySpawnAsync(commandLineArray, + (output) => { + callBackOnSuccess(); + resolve(); + }, + (output) => { + callBackOnFailure(); + reject(); + }) + }); +} // Based on: // 1. https://gjs.guide/guides/gio/subprocesses.html#asynchronous-communication // 2. https://gitlab.gnome.org/GNOME/gnome-shell/blob/8fda3116f03d95fabf3fac6d082b5fa268158d00/js/misc/util.js:L111 From 6dfcc24eb768ccb61a9945ace5f5da0d11574dd3 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Mon, 27 Jun 2022 19:49:01 +0800 Subject: [PATCH 47/49] Add the ability to restore applications at intervals of some milliseconds --- indicator.js | 2 +- prefs.js | 8 + restoreSession.js | 142 ++++++++++-------- schemas/gschemas.compiled | Bin 860 -> 924 bytes ...another-window-session-manager.gschema.xml | 7 + ui/prefs-gtk4.ui | 45 +++++- 6 files changed, 135 insertions(+), 69 deletions(-) diff --git a/indicator.js b/indicator.js index d0eb949..ee51cd1 100644 --- a/indicator.js +++ b/indicator.js @@ -32,8 +32,8 @@ class AwsIndicator extends PanelMenu.Button { this._windowTracker = Shell.WindowTracker.get_default(); this._prefsUtils = new PrefsUtils.PrefsUtils(); - this._log = new Log.Log(); this._settings = this._prefsUtils.getSettings(); + this._log = new Log.Log(); this._signal = new Signal.Signal(); diff --git a/prefs.js b/prefs.js index 07cb3aa..728e166 100644 --- a/prefs.js +++ b/prefs.js @@ -70,6 +70,13 @@ const Prefs = GObject.registerClass( Gio.SettingsBindFlags.DEFAULT ); + this._settings.bind( + 'restore-session-interval', + this.restore_session_interval_spinbutton, + 'value', + Gio.SettingsBindFlags.DEFAULT + ); + this._settings.bind( 'autostart-delay', this.autostart_delay_spinbutton, @@ -116,6 +123,7 @@ const Prefs = GObject.registerClass( this.debugging_mode_switch = this._builder.get_object('debugging_mode_switch'); + this.restore_session_interval_spinbutton = this._builder.get_object('restore_session_interval_spinbutton'); this.timer_on_the_autostart_dialog_spinbutton = this._builder.get_object('timer_on_the_autostart_dialog_spinbutton'); this.autostart_delay_spinbutton = this._builder.get_object('autostart_delay_spinbutton'); diff --git a/restoreSession.js b/restoreSession.js index 1a8508b..6001ca1 100644 --- a/restoreSession.js +++ b/restoreSession.js @@ -10,6 +10,7 @@ const Me = ExtensionUtils.getCurrentExtension(); const FileUtils = Me.imports.utils.fileUtils; const Log = Me.imports.utils.log; +const PrefsUtils = Me.imports.utils.prefsUtils; // All launching apps by Shell.App#launch() var restoringApps = new Map(); @@ -18,11 +19,14 @@ var RestoreSession = class { constructor() { this._log = new Log.Log(); + this._prefsUtils = new PrefsUtils.PrefsUtils(); + this._settings = this._prefsUtils.getSettings(); this.sessionName = FileUtils.default_sessionName; this._defaultAppSystem = Shell.AppSystem.get_default(); - this._windowTracker = Shell.WindowTracker.get_default(); + this._windowTracker = Shell.WindowTracker.get_default(); + this._restore_session_interval = this._settings.get_int('restore-session-interval'); // All launched apps info by Shell.App#launch() this._restoredApps = new Map(); @@ -64,73 +68,85 @@ var RestoreSession = class { return; } - for (const session_config_object of session_config_objects) { - const app_name = session_config_object.app_name; - let launched = false; - let running = false; - try { - const desktop_file_id = session_config_object.desktop_file_id; - if (desktop_file_id) { - const shell_app = this._defaultAppSystem.lookup_app(desktop_file_id) - if (shell_app) { - const restoringShellAppData = restoringApps.get(shell_app); - if (restoringShellAppData) { - restoringShellAppData.saved_window_sessions.push(session_config_object); - } else { - restoringApps.set(shell_app, { - saved_window_sessions: [session_config_object] - }); - } - - [launched, running] = this.launch(shell_app); - if (launched) { - if (!running) { - this._log.info(`${app_name} launched!`); - } - const existingShellAppData = this._restoredApps.get(shell_app); - if (existingShellAppData) { - existingShellAppData.saved_window_sessions.push(session_config_object); - } else { - this._restoredApps.set(shell_app, { - saved_window_sessions: [session_config_object] - }); - } - } else { - logError(new Error(`Failed to restore ${app_name}`, `Cannot find ${desktop_file_id}.`)); - global.notify_error(`Failed to restore ${app_name}`, `Reason: Cannot find ${desktop_file_id}.`); - } - } else { - logError(new Error(`Failed to restore ${app_name}. Reason: don't find Shell.App by ${desktop_file_id}, App is not installed or something is wrong in ${desktop_file_id}?`)); - global.notify_error(`Failed to restore ${app_name}`, `Reason: don't find Shell.App by ${desktop_file_id}. App is not installed or something is wrong in ${desktop_file_id}?`); - } + this._restoreSessionTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, + // In milliseconds. + // Note that this timing might not precise, see https://gjs-docs.gnome.org/glib20~2.66.1/glib.timeout_add + this._restore_session_interval, + () => { + if (session_config_objects.length === 0) { + return GLib.SOURCE_REMOVE; + } + this._restoreSessionOne(session_config_objects.shift()); + return GLib.SOURCE_CONTINUE; + } + ); + } + + } + + _restoreSessionOne(session_config_object) { + const app_name = session_config_object.app_name; + let launched = false; + let running = false; + try { + const desktop_file_id = session_config_object.desktop_file_id; + if (desktop_file_id) { + const shell_app = this._defaultAppSystem.lookup_app(desktop_file_id) + if (shell_app) { + const restoringShellAppData = restoringApps.get(shell_app); + if (restoringShellAppData) { + restoringShellAppData.saved_window_sessions.push(session_config_object); } else { - // TODO check running state to skip running apps - - const cmd = session_config_object.cmd; - if (cmd && cmd.length !== 0) { - const cmdString = cmd.join(' '); - Util.trySpawnCommandLine(cmdString); - launched = true; - // Important log. Indicate that this app may has no .desktop file, need to be handled specially. - this._log.info(`${app_name} launched via command line ${cmdString}!`); + restoringApps.set(shell_app, { + saved_window_sessions: [session_config_object] + }); + } + + [launched, running] = this.launch(shell_app); + if (launched) { + if (!running) { + this._log.info(`${app_name} launched!`); + } + const existingShellAppData = this._restoredApps.get(shell_app); + if (existingShellAppData) { + existingShellAppData.saved_window_sessions.push(session_config_object); } else { - // TODO try to launch via app_info by searching the app name? - let errorMsg = `Empty command line for ${app_name}`; - logError(new Error(errorMsg), `Invalid command line: ${cmd}`); - global.notify_error(errorMsg, `Invalid command line: ${cmd}`); + this._restoredApps.set(shell_app, { + saved_window_sessions: [session_config_object] + }); } + } else { + logError(new Error(`Failed to restore ${app_name}`, `Cannot find ${desktop_file_id}.`)); + global.notify_error(`Failed to restore ${app_name}`, `Reason: Cannot find ${desktop_file_id}.`); } - - } catch (e) { - logError(e, `Failed to restore ${app_name}`); - if (!launched) { - global.notify_error(`Failed to restore ${app_name}`, e.message); - } + } else { + logError(new Error(`Failed to restore ${app_name}. Reason: don't find Shell.App by ${desktop_file_id}, App is not installed or something is wrong in ${desktop_file_id}?`)); + global.notify_error(`Failed to restore ${app_name}`, `Reason: don't find Shell.App by ${desktop_file_id}. App is not installed or something is wrong in ${desktop_file_id}?`); + } + } else { + // TODO check running state to skip running apps + + const cmd = session_config_object.cmd; + if (cmd && cmd.length !== 0) { + const cmdString = cmd.join(' '); + Util.trySpawnCommandLine(cmdString); + launched = true; + // Important log. Indicate that this app may has no .desktop file, need to be handled specially. + this._log.info(`${app_name} launched via command line ${cmdString}!`); + } else { + // TODO try to launch via app_info by searching the app name? + let errorMsg = `Empty command line for ${app_name}`; + logError(new Error(errorMsg), `Invalid command line: ${cmd}`); + global.notify_error(errorMsg, `Invalid command line: ${cmd}`); } } - } - + } catch (e) { + logError(e, `Failed to restore ${app_name}`); + if (!launched) { + global.notify_error(`Failed to restore ${app_name}`, e.message); + } + } } launch(shellApp) { @@ -211,6 +227,10 @@ var RestoreSession = class { } this._connectIds = null; } + + if (this._restoreSessionTimeoutId) { + this._restoreSessionTimeoutId = null; + } } diff --git a/schemas/gschemas.compiled b/schemas/gschemas.compiled index 16244e835ea2292d3480b27a2392097f4fb59012..00ac58f5d13cb906a1f344b1df3fa107bf2f70f1 100644 GIT binary patch literal 924 zcmaJBlCcy6J z@>MeTmc?!H0{jg|Yfgw?STCnwh&AyF{2H(>i!);fI`(~C6tUh8+W6cWA1h-+Upu*# zo1Wd0UVHj-pp442<26D=r)n0ANiY|Nmbszi=n14Tc$a|sqgHQfH_(TX_&zd)Jwwx(iSLMcebM7+$ zXWrDIg7iVTP!92)Lf@7_D!+jgai(6#PaBmMT0SnU*D~Fat}HZSaZNCP7G2#Ud%93N z^cjo&XyP=lG&e!JO;vE4`}HICc)i0Rqdt68b!r^(-`&Kiv|A_CTv27UJF32~uKx|6 Cip&oH literal 860 zcmaJ4e-6n=?0qaLWBi6K@Z${sSiR$@Rai>O$nQ3Gl*=M8&H_GXTq*~Fkp;}1|l zYg-#DTZR}M)dn;_pk{Le0(3@zJ2rNy|;WvSB17g+Oq&&ZFDl-dQ;%) z*U~vMzb(mic?tduqcbP+2j}(y7;;Ni!S?}IvbqD?)_!1iO zteAPhc@j!x(f9eeGWrc2c`X5VRQV`oTT5=b#O6C4NR#w>9fz0 z6g^{gzeE~DOj5Rk@{GX2|IGwFMmHX}sqa0&7vGC{4j~F{7tZV;fYRf*)9#H|Rb3kYcq`twUn&)f(PW>Rd-1Mxtiur5GAw8o7c`3t dn*CeQ?EedziTJiVO+Ee1rj%Ia6zhyi + + 0 + The interval restoring applications + + Restore applications at intervals of some milliseconds, up to 5 minutes + + false Restore at startup without asking diff --git a/ui/prefs-gtk4.ui b/ui/prefs-gtk4.ui index 62f3aef..0419363 100644 --- a/ui/prefs-gtk4.ui +++ b/ui/prefs-gtk4.ui @@ -7,6 +7,12 @@ 10 + + 300000 + 1 + 100 + + 3600 1 @@ -234,6 +240,31 @@ + + + 1 + Restore applications interval([0, 300000]ms) + 1 + 0 + + 0 + 2 + + + + + + 1 + 10 + restore_session_interval_spinbutton_adjustment + 1 + 0 + + 1 + 2 + + + 1 @@ -242,7 +273,7 @@ 0 0 - 2 + 3 @@ -253,7 +284,7 @@ center 1 - 2 + 3 @@ -266,7 +297,7 @@ 0 0 - 3 + 4 @@ -279,7 +310,7 @@ 10 1 - 3 + 4 @@ -291,7 +322,7 @@ 0 0 - 4 + 5 @@ -304,7 +335,7 @@ 20 1 - 4 + 5 2 @@ -321,7 +352,7 @@ 0 - 5 + 6 From 2fac1b409805fa82f2dda1bfd050ba52a4cf65a6 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Mon, 27 Jun 2022 20:11:04 +0800 Subject: [PATCH 48/49] Bump version to 17 --- metadata.json | 2 +- restoreSession.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/metadata.json b/metadata.json index 74eddd0..c98ca65 100644 --- a/metadata.json +++ b/metadata.json @@ -9,5 +9,5 @@ ], "url": "https://github.com/nlpsuge/gnome-shell-extension-another-window-session-manager", "uuid": "another-window-session-manager@gmail.com", - "version": 16 + "version": 17 } diff --git a/restoreSession.js b/restoreSession.js index 6001ca1..4517265 100644 --- a/restoreSession.js +++ b/restoreSession.js @@ -70,7 +70,7 @@ var RestoreSession = class { this._restoreSessionTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, // In milliseconds. - // Note that this timing might not precise, see https://gjs-docs.gnome.org/glib20~2.66.1/glib.timeout_add + // Note that this timing might not be precise, see https://gjs-docs.gnome.org/glib20~2.66.1/glib.timeout_add this._restore_session_interval, () => { if (session_config_objects.length === 0) { From 23ae35c9cccfc51171ce7b852ceb364e533c4e66 Mon Sep 17 00:00:00 2001 From: m^n <2271720+nlpsuge@users.noreply.github.com> Date: Tue, 28 Jun 2022 00:49:56 +0800 Subject: [PATCH 49/49] Close by rules: compatible with Wayland --- README.md | 1 + closeSession.js | 71 +++++++++++++++++++++++++++++---------- openWindowsInfoTracker.js | 5 +++ utils/log.js | 4 +++ 4 files changed, 63 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 34d3c47..324f12b 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ sudo dnf install ydotool #Check the permission of `/dev/uinput`, if it's `crw-rw----+`, you can skip step 2 # 2. Get permission to access to `/dev/uinput` as the normal user +sudo touch /etc/udev/rules.d/60-awsm-ydotool-uinput.rules sudo echo '# See: # https://github.com/ValveSoftware/steam-devices/blob/master/60-steam-input.rules # https://github.com/ReimuNotMoe/ydotool/issues/25 diff --git a/closeSession.js b/closeSession.js index 99345e1..8241a9f 100644 --- a/closeSession.js +++ b/closeSession.js @@ -1,6 +1,6 @@ 'use strict'; -const { Shell, Gio, GLib } = imports.gi; +const { Meta, Shell, Gio, GLib } = imports.gi; const Main = imports.ui.main; const Scripting = imports.ui.scripting; @@ -186,6 +186,31 @@ var CloseSession = class { } _activateAndFocusWindow(app) { + let windows; + if (Meta.is_wayland_compositor()) { + windows = this._sortWindowsOnWayland(app); + } else { + windows = this._sortWindowsOnX11(app); + } + + const topLevelWindow = windows[windows.length - 1]; + if (topLevelWindow) { + this._log.info(`Activating the running window ${topLevelWindow.get_title()} of ${app.get_name()}`); + Main.activateWindow(topLevelWindow); + } + } + + _sortWindowsOnWayland(app) { + const windows = app.get_windows(); + windows.sort((w1, w2) => { + const windowStableSequence1 = w1.get_stable_sequence(); + const windowStableSequence2 = w2.get_stable_sequence(); + return this._compareWindowStableSequence(windowStableSequence1, windowStableSequence2); + }); + return windows; + } + + _sortWindowsOnX11(app) { const savedWindowsMappingJsonStr = this._settings.get_string('windows-mapping'); const savedWindowsMapping = new Map(JSON.parse(savedWindowsMappingJsonStr)); @@ -196,31 +221,41 @@ var CloseSession = class { windows.sort((w1, w2) => { const xid1 = w1.get_description(); const value1 = xidObj[xid1]; - const windowStableSequence1 = value1.windowStableSequence; + let windowStableSequence1; + if (value1) { + windowStableSequence1 = value1.windowStableSequence; + } else { + windowStableSequence1 = w1.get_stable_sequence(); + this._log.warn(`Mapping for this xid ${xid1} and stable sequence does not exist, use sequence ${windowStableSequence1} instead. app name: ${app.get_name()}, window title: ${w1.get_title()}`); + } const xid2 = w2.get_description(); const value2 = xidObj[xid2]; - const windowStableSequence2 = value2.windowStableSequence; - - const diff = windowStableSequence1 - windowStableSequence2; - if (diff === 0) { - return 0; + let windowStableSequence2; + if (value2) { + windowStableSequence2 = value2.windowStableSequence; + } else { + windowStableSequence2 = w2.get_stable_sequence(); + this._log.warn(`Mapping for this xid ${xid2} and stable sequence does not exist, use sequence ${windowStableSequence2} instead. app name: ${app.get_name()}, window title: ${w2.get_title()}`); } - if (diff > 0) { - return 1; - } + return this._compareWindowStableSequence(windowStableSequence1, windowStableSequence2); + }); + return windows; + } - if (diff < 0) { - return -1; - } + _compareWindowStableSequence(windowStableSequence1, windowStableSequence2) { + const diff = windowStableSequence1 - windowStableSequence2; + if (diff === 0) { + return 0; + } - }); + if (diff > 0) { + return 1; + } - const topLevelWindow = windows[windows.length - 1]; - if (topLevelWindow) { - this._log.info(`Activating the running window ${topLevelWindow.get_title()} of ${app.get_name()}`); - Main.activateWindow(topLevelWindow); + if (diff < 0) { + return -1; } } diff --git a/openWindowsInfoTracker.js b/openWindowsInfoTracker.js index 73f62b0..8497318 100644 --- a/openWindowsInfoTracker.js +++ b/openWindowsInfoTracker.js @@ -17,6 +17,11 @@ const EndSessionDialogProxy = Gio.DBusProxy.makeProxyWrapper(EndSessionDialogIfa var OpenWindowsInfoTracker = class { constructor() { + // Only track windows on X11 + if (Meta.is_wayland_compositor()) { + return; + } + this._windowTracker = Shell.WindowTracker.get_default(); this._defaultAppSystem = Shell.AppSystem.get_default(); diff --git a/utils/log.js b/utils/log.js index f993154..24631b0 100644 --- a/utils/log.js +++ b/utils/log.js @@ -31,6 +31,10 @@ var Log = class { log(`[INFO ][Another window session manager] ${logContent}`); } + warn(logContent) { + log(`[WARNING ][Another window session manager] ${logContent}`); + } + destroy() { if (this._prefsUtils) { this._prefsUtils.destroy();