From 6f8d1e0b635c2063974e7fa976a4da16f1154c2c Mon Sep 17 00:00:00 2001 From: JosephMcc Date: Mon, 23 Sep 2024 10:40:26 -0700 Subject: [PATCH 1/3] layout.js: Allow adding already parented actors for chrome tracking We need this for the force-close dialog. It will be included in the MetaWindowGroup, but needs to tracked as chrome to recieve pointer events --- js/ui/layout.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/js/ui/layout.js b/js/ui/layout.js index dd6fb7eabe..715f1525cd 100644 --- a/js/ui/layout.js +++ b/js/ui/layout.js @@ -492,8 +492,7 @@ LayoutManager.prototype = { * - addToWindowgroup (boolean): The actor should be added as a top-level window. * - doNotAdd (boolean): The actor should not be added to the uiGroup. This has no effect if %addToWindowgroup is %true. * - * Tells the chrome to track @actor, which must be a descendant - * of an actor added via addChrome(). This can be used to extend the + * Tells the chrome to track @actor. This can be used to extend the * struts or input region to cover specific children. * * @params can have any of the same values as in addChrome(), @@ -637,10 +636,9 @@ Chrome.prototype = { ancestor = ancestor.get_parent(); index = this._findActor(ancestor); } - if (!ancestor) - throw new Error('actor is not a descendent of a chrome actor'); - let ancestorData = this._trackedActors[index]; + let ancestorData = ancestor ? this._trackedActors[index] + : defaultParams; if (!params) params = {}; // We can't use Params.parse here because we want to drop From a18a6fdaca32699ec3a17ab47ebefbbcdf09ae07 Mon Sep 17 00:00:00 2001 From: JosephMcc Date: Mon, 23 Sep 2024 11:22:41 -0700 Subject: [PATCH 2/3] Make the force quit dialog a Cinnamon dialog With the new dialogs and theming in place we can start looking at converting some of these away from Gtk dialogs --- files/usr/bin/cinnamon-close-dialog | 104 ------------- js/ui/closeDialog.js | 222 ++++++++++++++++++++++++++++ js/ui/windowManager.js | 5 +- js/ui/wmGtkDialogs.js | 59 -------- 4 files changed, 225 insertions(+), 165 deletions(-) delete mode 100755 files/usr/bin/cinnamon-close-dialog create mode 100644 js/ui/closeDialog.js diff --git a/files/usr/bin/cinnamon-close-dialog b/files/usr/bin/cinnamon-close-dialog deleted file mode 100755 index bbb8daed24..0000000000 --- a/files/usr/bin/cinnamon-close-dialog +++ /dev/null @@ -1,104 +0,0 @@ -#! /usr/bin/python3 - -""" -Close dialog spawned by cinnamon (closeDialog.js) that prompts the user to kill a hung window. -""" -import signal -import gettext -import argparse - -import gi -gi.require_version('Gtk', '3.0') -gi.require_version('XApp', '1.0') - -from gi.repository import GLib, Gtk, GdkX11, Gdk, XApp - -signal.signal(signal.SIGINT, signal.SIG_DFL) - -gettext.install("cinnamon", "/usr/share/locale", names=["ngettext"]) - -UNSET = -1 -KILL = 0 -WAIT = 1 - -global response -response = UNSET - -class CloseDialog(XApp.GtkWindow): - def __init__(self, parent_xid, title): - XApp.GtkWindow.__init__(self, - resizable=False, - modal=True, - type_hint=Gdk.WindowTypeHint.DIALOG) - - self.set_title("") - self.set_default_size(300, -1) - self.set_skip_taskbar_hint(True) - - mainbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - self.add(mainbox) - - content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, - halign=Gtk.Align.CENTER, - margin_left=10, margin_right=10) - mainbox.pack_start(content_box, True, False, 0) - - image = Gtk.Image(icon_name="window-close-symbolic", pixel_size=48) - content_box.pack_start(image, False, True, 0) - - text = _("%s is not responding.") % title - prompt = Gtk.Label(label="%s" % text, use_markup=True, wrap=True) - content_box.pack_start(prompt, True, True, 5) - - bb = Gtk.ButtonBox(layout_style=Gtk.ButtonBoxStyle.SPREAD, - spacing=10) - mainbox.pack_end(bb, False, False, 10) - - button = Gtk.Button(label=_("Force quit")) - button.get_style_context().add_class(Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION) - bb.pack_start(button, False, False, 0) - button.connect("clicked", self.force_quit_clicked) - - button = Gtk.Button(label=_("Wait")) - bb.pack_start(button, False, False, 0) - button.connect("clicked", self.wait_clicked) - - self.show_all() - - self.connect("delete-event", lambda w, e: self.return_response(WAIT)) - GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGTERM, lambda: self.return_response(WAIT)) - - self.realize() - - try: - parent = GdkX11.X11Window.foreign_new_for_display(Gdk.Display.get_default(), parent_xid) - self.get_window().set_transient_for(parent) - except TypeError: - parent = None - - def force_quit_clicked(self, button): - self.return_response(KILL) - - def wait_clicked(self, button): - self.return_response(WAIT) - - def return_response(self, code): - global response - response = code - Gtk.main_quit() - -if __name__ == "__main__": - - parser = argparse.ArgumentParser(description="Cinnamon dialog for force-closing stuck windows.\n Returns 0 to confirm, 1 to wait, 2 this window was terminated.") - parser.add_argument("xid", type=int, help="The parent window's xid") - parser.add_argument("message", type=str, help="The program title") - - args = parser.parse_args() - - dialog = CloseDialog(args.xid, args.message) - Gtk.main() - dialog.destroy() - - print(response) - exit(response) - diff --git a/js/ui/closeDialog.js b/js/ui/closeDialog.js new file mode 100644 index 0000000000..60d6a47040 --- /dev/null +++ b/js/ui/closeDialog.js @@ -0,0 +1,222 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Clutter = imports.gi.Clutter; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Meta = imports.gi.Meta; +const St = imports.gi.St; +const Cinnamon = imports.gi.Cinnamon; + +const Dialog = imports.ui.dialog; +const Main = imports.ui.main; + +const FROZEN_WINDOW_BRIGHTNESS = -0.3; +const DIALOG_TRANSITION_TIME = 150; +const ALIVE_TIMEOUT = 5000; + +var CloseDialog = GObject.registerClass({ + Implements: [Meta.CloseDialog], + Properties: { + 'window': GObject.ParamSpec.override('window', Meta.CloseDialog), + }, +}, class CloseDialog extends GObject.Object { + _init(window) { + super._init(); + this._window = window; + this._dialog = null; + this._tracked = undefined; + this._timeoutId = 0; + this._windowFocusChangedId = 0; + this._keyFocusChangedId = 0; + } + + get window() { + return this._window; + } + + set window(window) { + this._window = window; + } + + _createDialogContent() { + let tracker = Cinnamon.WindowTracker.get_default(); + let windowApp = tracker.get_window_app(this._window); + + /* Translators: %s is an application name */ + let title = _('ā€œ%sā€ Is Not Responding').format(windowApp.get_name()); + let description = _('You may choose to wait a short while for it to ' + + 'continue or force the app to quit entirely'); + return new Dialog.MessageDialogContent({title, description}); + } + + _updateScale() { + // Since this is a child of MetaWindowActor (which, for Wayland clients, + // applies the geometry scale factor to its children itself, see + // meta_window_actor_set_geometry_scale()), make sure we don't apply + // the factor twice in the end. + if (this._window.get_client_type() !== Meta.WindowClientType.WAYLAND) + return; + + let { scaleFactor } = St.ThemeContext.get_for_stage(global.stage); + this._dialog.set_scale(1 / scaleFactor, 1 / scaleFactor); + } + + _initDialog() { + if (this._dialog) + return; + + let windowActor = this._window.get_compositor_private(); + this._dialog = new Dialog.Dialog(windowActor, 'close-dialog'); + this._dialog.width = windowActor.width; + this._dialog.height = windowActor.height; + + this._dialog.contentLayout.add_child(this._createDialogContent()); + this._dialog.addButton({ + label: _('Force Quit'), + action: this._onClose.bind(this), + destructive_action: true, + }); + this._dialog.addButton({ + label: _('Wait'), + action: this._onWait.bind(this), + key: Clutter.KEY_Escape, + }); + + global.focus_manager.add_group(this._dialog); + + let themeContext = St.ThemeContext.get_for_stage(global.stage); + themeContext.connect('notify::scale-factor', this._updateScale.bind(this)); + + this._updateScale(); + } + + _addWindowEffect() { + // We set the effect on the surface actor, so the dialog itself + // (which is a child of the MetaWindowActor) does not get the + // effect applied itself. + let windowActor = this._window.get_compositor_private(); + let surfaceActor = windowActor.get_first_child(); + let effect = new Clutter.BrightnessContrastEffect(); + effect.set_brightness(FROZEN_WINDOW_BRIGHTNESS); + surfaceActor.add_effect_with_name("cinnamon-frozen-window", effect); + } + + _removeWindowEffect() { + let windowActor = this._window.get_compositor_private(); + let surfaceActor = windowActor.get_first_child(); + surfaceActor.remove_effect_by_name("cinnamon-frozen-window"); + } + + _onWait() { + this.response(Meta.CloseDialogResponse.WAIT); + } + + _onClose() { + this.response(Meta.CloseDialogResponse.FORCE_CLOSE); + } + + _onFocusChanged() { + if (Meta.is_wayland_compositor()) + return; + + let focusWindow = global.display.focus_window; + let keyFocus = global.stage.key_focus; + + let shouldTrack; + if (focusWindow != null) + shouldTrack = focusWindow == this._window; + else + shouldTrack = keyFocus && this._dialog.contains(keyFocus); + + if (this._tracked === shouldTrack) + return; + + if (shouldTrack) + Main.layoutManager.trackChrome(this._dialog, + { affectsInputRegion: true }); + else + Main.layoutManager.untrackChrome(this._dialog); + + // The buttons are broken when they aren't added to the input region, + // so disable them properly in that case + this._dialog.buttonLayout.get_children().forEach(b => { + b.reactive = shouldTrack; + }); + + this._tracked = shouldTrack; + } + + vfunc_show() { + if (this._dialog != null) + return; + + Meta.disable_unredirect_for_display(global.display); + + this._timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, ALIVE_TIMEOUT, + () => { + this._window.check_alive(global.display.get_current_time_roundtrip()); + return GLib.SOURCE_CONTINUE; + }); + + this._windowFocusChangedId = + global.display.connect('notify::focus-window', + this._onFocusChanged.bind(this)); + + this._keyFocusChangedId = + global.stage.connect('notify::key-focus', + this._onFocusChanged.bind(this)); + + this._addWindowEffect(); + this._initDialog(); + + this._dialog._dialog.scale_y = 0; + this._dialog._dialog.set_pivot_point(0.5, 0.5); + + this._dialog._dialog.ease({ + scale_y: 1, + mode: Clutter.AnimationMode.LINEAR, + duration: DIALOG_TRANSITION_TIME, + onComplete: this._onFocusChanged.bind(this) + }); + } + + vfunc_hide() { + if (this._dialog == null) + return; + + Meta.enable_unredirect_for_display(global.display); + + GLib.source_remove(this._timeoutId); + this._timeoutId = 0; + + global.display.disconnect(this._windowFocusChangedId); + this._windowFocusChangedId = 0; + + global.stage.disconnect(this._keyFocusChangedId); + this._keyFocusChangedId = 0; + + this._dialog._dialog.remove_all_transitions(); + + let dialog = this._dialog; + this._dialog = null; + this._removeWindowEffect(); + + dialog.makeInactive(); + dialog._dialog.ease({ + scale_y: 0, + mode: Clutter.AnimationMode.LINEAR, + duration: DIALOG_TRANSITION_TIME, + onComplete: () => dialog.destroy(), + }); + } + + vfunc_focus() { + if (!this._dialog) + return; + + const keyFocus = global.stage.key_focus; + if (!keyFocus || !this._dialog.contains(keyFocus)) + this._dialog.initialKeyFocus.grab_key_focus(); + } +}); diff --git a/js/ui/windowManager.js b/js/ui/windowManager.js index 8fd516c035..cc7f1f6542 100644 --- a/js/ui/windowManager.js +++ b/js/ui/windowManager.js @@ -13,6 +13,7 @@ const GObject = imports.gi.GObject; const AppSwitcher = imports.ui.appSwitcher.appSwitcher; const ModalDialog = imports.ui.modalDialog; const WmGtkDialogs = imports.ui.wmGtkDialogs; +const CloseDialog = imports.ui.closeDialog; const WorkspaceOsd = imports.ui.workspaceOsd; const {CoverflowSwitcher} = imports.ui.appSwitcher.coverflowSwitcher; @@ -1392,8 +1393,8 @@ var WindowManager = class WindowManager { } } - _createCloseDialog(shellwm, window) { - return new WmGtkDialogs.CloseDialog(window); + _createCloseDialog(cinnamonwm, window) { + return new CloseDialog.CloseDialog(window); } _confirmDisplayChange() { diff --git a/js/ui/wmGtkDialogs.js b/js/ui/wmGtkDialogs.js index 3cc77f43ba..73970d19cb 100644 --- a/js/ui/wmGtkDialogs.js +++ b/js/ui/wmGtkDialogs.js @@ -54,65 +54,6 @@ var DisplayChangesDialog = class { } }; - -var CloseDialog = GObject.registerClass({ - Implements: [Meta.CloseDialog], - Properties: { - 'window': GObject.ParamSpec.override('window', Meta.CloseDialog), - }, -}, class CloseDialog extends GObject.Object { - _init(window) { - super._init(); - this._window = window; - - this.proc = null; - } - - vfunc_show() { - try { - this.proc = Gio.Subprocess.new( - [ - "cinnamon-close-dialog", - this._window.get_xwindow().toString(), - this._window.get_title() - ], - 0); - - this.proc.wait_async(null, this._wait_finish.bind(this)); - } catch (e) { - global.logWarning(`Could not spawn kill dialog: ${e}`); - - this.proc = null; - this.response(Meta.CloseDialogResponse.WAIT); - } - } - - _wait_finish(proc, result) { - try { - this.proc.wait_finish(result); - } catch (e) { - global.logWarning(`Something went wrong with kill dialog: ${e}`); - } - - if (this.proc.get_status() == 0) { - this.response(Meta.CloseDialogResponse.FORCE_CLOSE); - } else { - this.response(Meta.CloseDialogResponse.WAIT); - } - } - - vfunc_hide() { - if (this.proc === null) { - return; - } - - this.proc.send_signal(SIGTERM); - } - - vfunc_focus() { - } -}); - var HoverClickHelper = class { constructor(wm) { this.proc = null; From 104d01a3d1fe3d124734d24bb885a167ddafc7f5 Mon Sep 17 00:00:00 2001 From: JosephMcc Date: Mon, 23 Sep 2024 12:35:20 -0700 Subject: [PATCH 3/3] closeDialog: Switch the buttons around Gtk dialogs have the action on the right so move the Force Quit button to the right side to match the layout users are accustomed to --- js/ui/closeDialog.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/js/ui/closeDialog.js b/js/ui/closeDialog.js index 60d6a47040..252012053e 100644 --- a/js/ui/closeDialog.js +++ b/js/ui/closeDialog.js @@ -72,16 +72,16 @@ var CloseDialog = GObject.registerClass({ this._dialog.height = windowActor.height; this._dialog.contentLayout.add_child(this._createDialogContent()); - this._dialog.addButton({ - label: _('Force Quit'), - action: this._onClose.bind(this), - destructive_action: true, - }); this._dialog.addButton({ label: _('Wait'), action: this._onWait.bind(this), key: Clutter.KEY_Escape, }); + this._dialog.addButton({ + label: _('Force Quit'), + action: this._onClose.bind(this), + destructive_action: true, + }); global.focus_manager.add_group(this._dialog);