diff --git a/js/ui/main.js b/js/ui/main.js
index 7a5d17b00f..6fffcee476 100644
--- a/js/ui/main.js
+++ b/js/ui/main.js
@@ -1318,6 +1318,40 @@ function _findModal(actor) {
return -1;
}
+function _completeModalSetup(actor, mode) {
+ if (modalCount == 0)
+ Meta.disable_unredirect_for_display(global.display);
+
+ global.set_stage_input_mode(Cinnamon.StageInputMode.FULLSCREEN);
+
+ actionMode = mode;
+
+ modalCount += 1;
+ let actorDestroyId = actor.connect('destroy', function() {
+ let index = _findModal(actor);
+ if (index >= 0)
+ popModal(actor);
+ });
+
+ let record = {
+ actor: actor,
+ focus: global.stage.get_key_focus(),
+ destroyId: actorDestroyId,
+ actionMode: mode
+ };
+ if (record.focus != null) {
+ record.focusDestroyId = record.focus.connect('destroy', function() {
+ record.focus = null;
+ record.focusDestroyId = null;
+ });
+ }
+ modalActorFocusStack.push(record);
+
+ global.stage.set_key_focus(actor);
+
+ layoutManager.updateChrome(true);
+}
+
/**
* pushModal:
* @actor (Clutter.Actor): actor which will be given keyboard focus
@@ -1356,38 +1390,44 @@ function pushModal(actor, timestamp, options, mode) {
log('pushModal: invocation of begin_modal failed');
return false;
}
- Meta.disable_unredirect_for_display(global.display);
}
- global.set_stage_input_mode(Cinnamon.StageInputMode.FULLSCREEN);
+ _completeModalSetup(actor, mode);
+ return true;
+}
- actionMode = mode;
+/**
+ * pushScreensaverModal:
+ * @actor (Clutter.Actor): actor which will be given keyboard focus.
+ * @timestamp (number): optional X server timestamp.
+ * @mode (Cinnamon.ActionMode): the action mode for the modal grab.
+ * @callback (function): called with (success) when the grab completes.
+ *
+ * Like pushModal(), but uses begin_modal_with_retry() to asynchronously
+ * retry the grab on X11, using libxdo to break stuck grabs from popup
+ * menus. The callback is called with true on success, false on failure.
+ */
+function pushScreensaverModal(actor, timestamp, mode, callback) {
+ if (timestamp == undefined)
+ timestamp = global.get_current_time();
- modalCount += 1;
- let actorDestroyId = actor.connect('destroy', function() {
- let index = _findModal(actor);
- if (index >= 0)
- popModal(actor);
- });
+ global.begin_modal_with_retry(timestamp, 0,
+ (obj, success) => {
+ if (!success) {
+ log('pushScreensaverModal: failed to acquire modal grab after retries (or cancelled)');
+ callback(false);
+ return;
+ }
- let record = {
- actor: actor,
- focus: global.stage.get_key_focus(),
- destroyId: actorDestroyId,
- actionMode: mode
- };
- if (record.focus != null) {
- record.focusDestroyId = record.focus.connect('destroy', function() {
- record.focus = null;
- record.focusDestroyId = null;
+ try {
+ _completeModalSetup(actor, mode);
+ callback(true);
+ } catch (e) {
+ global.logError(`pushScreensaverModal: error during modal setup: ${e.message}`);
+ global.end_modal(global.get_current_time());
+ callback(false);
+ }
});
- }
- modalActorFocusStack.push(record);
-
- global.stage.set_key_focus(actor);
-
- layoutManager.updateChrome(true);
- return true;
}
/**
diff --git a/js/ui/screensaver/screenShield.js b/js/ui/screensaver/screenShield.js
index 2b7e92fb4d..4105be18c0 100644
--- a/js/ui/screensaver/screenShield.js
+++ b/js/ui/screensaver/screenShield.js
@@ -129,6 +129,7 @@ var ScreenShield = GObject.registerClass({
this._widgetLoadIdleId = 0;
this._infoPanel = null;
this._inhibitor = null;
+ this._activationPending = false;
this._nameBlocker = new NameBlocker.NameBlocker();
@@ -356,19 +357,26 @@ var ScreenShield = GObject.registerClass({
}
lock(immediate = false, awayMessage = null) {
- if (this.isLocked())
+ if (this.isLocked() || this._activationPending) {
+ _log('ScreenShield: Already locked or activation pending, ignoring lock request');
return;
+ }
this._awayMessage = awayMessage;
_log(`ScreenShield: Locking screen (immediate=${immediate})`);
if (this._state === State.HIDDEN) {
- this.activate(immediate);
+ this.activate(immediate, (success) => {
+ if (success) {
+ this._stopLockDelay();
+ this._setLocked(true);
+ }
+ });
+ } else {
+ this._stopLockDelay();
+ this._setLocked(true);
}
-
- this._stopLockDelay();
- this._setLocked(true);
}
_setLocked(locked) {
@@ -397,9 +405,9 @@ var ScreenShield = GObject.registerClass({
this._hideShield(true);
}
- activate(immediate = false) {
- if (this._state !== State.HIDDEN) {
- _log('ScreenShield: Already active');
+ activate(immediate = false, lock_callback = null) {
+ if (this._state !== State.HIDDEN || this._activationPending) {
+ _log('ScreenShield: Already active or activation pending');
return;
}
@@ -409,44 +417,87 @@ var ScreenShield = GObject.registerClass({
this._lastMotionY = -1;
this._activationTime = GLib.get_monotonic_time();
- if (!Main.pushModal(this, global.get_current_time(), 0, Cinnamon.ActionMode.LOCK_SCREEN)) {
- global.logError('ScreenShield: Failed to acquire modal grab');
- return;
- }
+ this._activationPending = true;
+ _log('ScreenShield: requesting screensaver modal grab');
+ Main.pushScreensaverModal(this, global.get_current_time(), Cinnamon.ActionMode.LOCK_SCREEN,
+ (success) => {
+ this._activationPending = false;
- this._setState(State.SHOWN);
+ if (success) {
+ _log('ScreenShield: modal grab acquired, finishing activation');
- this._createBackgrounds();
+ if (this._finishActivation(immediate)) {
+ if (lock_callback)
+ lock_callback(true);
- this._capturedEventId = global.stage.connect('captured-event',
- this._onCapturedEvent.bind(this));
+ return;
+ } else {
+ Main.popModal(this);
+ }
+ }
- global.stage.hide_cursor();
+ global.logWarning('ScreenShield: Failed to acquire modal grab (or cancelled)');
+ this._activationTime = 0;
+ this._syncInhibitor();
+ if (lock_callback)
+ lock_callback(false);
+ });
+ }
- if (Main.deskletContainer)
- Main.deskletContainer.actor.hide();
+ _finishActivation(immediate) {
+ try {
+ this._createBackgrounds();
- Main.layoutManager.screenShieldGroup.show();
- this.show();
+ this._capturedEventId = global.stage.connect('captured-event',
+ this._onCapturedEvent.bind(this));
- if (immediate) {
- this.opacity = 255;
- this._activateBackupLocker();
- this._scheduleWidgetLoading();
- } else {
- this.opacity = 0;
- this.ease({
- opacity: 255,
- duration: FADE_TIME,
- mode: Clutter.AnimationMode.EASE_OUT_QUAD,
- onComplete: () => {
- this._activateBackupLocker();
- this._scheduleWidgetLoading();
- }
- });
- }
+ global.stage.hide_cursor();
+
+ if (Main.deskletContainer)
+ Main.deskletContainer.actor.hide();
+
+ Main.layoutManager.screenShieldGroup.show();
+ this.show();
+
+ if (immediate) {
+ this.opacity = 255;
+ this._activateBackupLocker();
+ this._scheduleWidgetLoading();
+ } else {
+ this.opacity = 0;
+ this.ease({
+ opacity: 255,
+ duration: FADE_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => {
+ this._activateBackupLocker();
+ this._scheduleWidgetLoading();
+ }
+ });
+ }
+
+ this._setState(State.SHOWN);
+ this._startLockDelay();
+ return true;
+ } catch (e) {
+ global.logError(`ScreenShield: error during activation: ${e.message}`);
+
+ if (this._capturedEventId) {
+ global.stage.disconnect(this._capturedEventId);
+ this._capturedEventId = 0;
+ }
+
+ global.stage.show_cursor();
+
+ if (Main.deskletContainer)
+ Main.deskletContainer.actor.show();
- this._startLockDelay();
+ Main.layoutManager.screenShieldGroup.hide();
+ this.hide();
+ this._destroyBackgrounds();
+
+ return false;
+ }
}
_startLockDelay() {
@@ -495,6 +546,12 @@ var ScreenShield = GObject.registerClass({
return;
}
+ if (this._activationPending) {
+ _log('ScreenShield: Cancelling pending activation');
+ global.end_modal(global.get_current_time());
+ return;
+ }
+
this._stopLockDelay();
this._hideShield(false);
}
@@ -552,7 +609,8 @@ var ScreenShield = GObject.registerClass({
_syncInhibitor() {
let lockEnabled = this._settings.get_boolean('lock-enabled');
let lockDisabled = Main.lockdownSettings.get_boolean('disable-lock-screen');
- let shouldInhibit = this._state === State.HIDDEN && lockEnabled && !lockDisabled;
+ let shouldInhibit = (this._state === State.HIDDEN || this._activationPending) &&
+ lockEnabled && !lockDisabled;
if (shouldInhibit && !this._inhibitor) {
_log('ScreenShield: Acquiring sleep inhibitor');
@@ -563,7 +621,7 @@ var ScreenShield = GObject.registerClass({
}
// Re-check after async - conditions may have changed
- let stillNeeded = this._state === State.HIDDEN &&
+ let stillNeeded = (this._state === State.HIDDEN || this._activationPending) &&
this._settings.get_boolean('lock-enabled') &&
!Main.lockdownSettings.get_boolean('disable-lock-screen');
if (stillNeeded) {
diff --git a/src/cinnamon-global-private.h b/src/cinnamon-global-private.h
index a1015b969a..14112e0647 100644
--- a/src/cinnamon-global-private.h
+++ b/src/cinnamon-global-private.h
@@ -20,6 +20,7 @@
#endif
#include
+#include
#include "cinnamon-enum-types.h"
@@ -31,6 +32,16 @@
#include
+typedef struct {
+ CinnamonGlobal *global;
+ guint32 timestamp;
+ MetaModalOptions options;
+ gint attempt;
+ gboolean tried_xdo;
+ CinnamonModalCallback callback;
+ gpointer user_data;
+} ModalRetryData;
+
struct _CinnamonGlobal {
GObject parent;
@@ -64,6 +75,10 @@ struct _CinnamonGlobal {
gboolean has_modal;
guint notif_service_id;
+
+ xdo_t *xdo;
+ guint modal_retry_source_id;
+ ModalRetryData *modal_retry_data;
};
void _cinnamon_global_init (const char *first_property_name,
diff --git a/src/cinnamon-global.c b/src/cinnamon-global.c
index e379e74a2d..14d5daa64e 100644
--- a/src/cinnamon-global.c
+++ b/src/cinnamon-global.c
@@ -28,6 +28,14 @@
static CinnamonGlobal *the_object = NULL;
+static gboolean _grab_debug = FALSE;
+
+#define debug_grab(fmt, ...) G_STMT_START { \
+ if (_grab_debug) \
+ g_message ("modal-retry: " fmt, ##__VA_ARGS__); \
+} G_STMT_END
+
+
enum {
PROP_0,
@@ -244,6 +252,7 @@ cinnamon_global_init (CinnamonGlobal *global)
g_mkdir_with_parents (global->userdatadir, 0700);
global->settings = g_settings_new ("org.cinnamon");
+ _grab_debug = g_settings_get_boolean (global->settings, "debug-screensaver");
setup_log_handler (global);
@@ -268,6 +277,25 @@ static void
cinnamon_global_finalize (GObject *object)
{
CinnamonGlobal *global = CINNAMON_GLOBAL (object);
+
+ if (global->modal_retry_source_id != 0)
+ {
+ g_source_remove (global->modal_retry_source_id);
+ global->modal_retry_source_id = 0;
+
+ if (global->modal_retry_data != NULL)
+ {
+ g_free (global->modal_retry_data);
+ global->modal_retry_data = NULL;
+ }
+ }
+
+ if (global->xdo != NULL)
+ {
+ xdo_free (global->xdo);
+ global->xdo = NULL;
+ }
+
g_object_unref (global->js_context);
g_object_unref (global->settings);
@@ -991,7 +1019,12 @@ static void
sync_input_region (CinnamonGlobal *global)
{
MetaDisplay *display = global->meta_display;
- MetaX11Display *x11_display = meta_display_get_x11_display (display);
+ MetaX11Display *x11_display;
+
+ if (meta_is_wayland_compositor ())
+ return;
+
+ x11_display = meta_display_get_x11_display (display);
if (global->has_modal)
meta_x11_display_set_stage_input_region (x11_display, None);
@@ -1029,16 +1062,180 @@ cinnamon_global_begin_modal (CinnamonGlobal *global,
return FALSE;
global->has_modal = meta_plugin_begin_modal (global->plugin, options, timestamp);
- if (!meta_is_wayland_compositor ())
- sync_input_region (global);
+ sync_input_region (global);
return global->has_modal;
}
+#define MODAL_RETRY_INTERVAL_MS 1000
+#define MODAL_MAX_RETRIES 4
+
+static gboolean
+modal_try_grab (CinnamonGlobal *global,
+ MetaModalOptions options,
+ guint32 timestamp)
+{
+ if (!meta_display_get_compositor (global->meta_display) || global->has_modal)
+ return FALSE;
+
+ return meta_plugin_begin_modal (global->plugin, options, timestamp);
+}
+
+static void
+modal_maybe_cancel_ui_grab (CinnamonGlobal *global)
+{
+ if (global->xdo == NULL)
+ {
+ debug_grab ("xdo context not available, skipping");
+ return;
+ }
+
+ debug_grab ("sending Escape key sequences");
+ xdo_send_keysequence_window (global->xdo, CURRENTWINDOW, "Escape", 12000);
+ xdo_send_keysequence_window (global->xdo, CURRENTWINDOW, "Escape", 12000);
+}
+
+static void
+modal_retry_complete (ModalRetryData *data,
+ gboolean success)
+{
+ debug_grab ("complete, success=%s", success ? "true" : "false");
+ data->global->modal_retry_source_id = 0;
+ data->global->modal_retry_data = NULL;
+ data->callback (data->global, success, data->user_data);
+ g_free (data);
+}
+
+static gboolean
+modal_retry_timeout (gpointer user_data)
+{
+ ModalRetryData *data = user_data;
+ CinnamonGlobal *global = data->global;
+
+ data->attempt++;
+
+ debug_grab ("attempt %d/%d (xdo_tried=%s)",
+ data->attempt, MODAL_MAX_RETRIES,
+ data->tried_xdo ? "true" : "false");
+
+ global->has_modal = modal_try_grab (global, data->options, data->timestamp);
+
+ if (global->has_modal)
+ {
+ debug_grab ("grab succeeded on attempt %d", data->attempt);
+ sync_input_region (global);
+ modal_retry_complete (data, TRUE);
+ return G_SOURCE_REMOVE;
+ }
+
+ if (data->attempt >= MODAL_MAX_RETRIES)
+ {
+ if (!data->tried_xdo)
+ {
+ debug_grab ("exhausted %d attempts, trying xdo escape + nuke focus",
+ MODAL_MAX_RETRIES);
+ data->tried_xdo = TRUE;
+ data->attempt = 0;
+
+ if (global->xdo == NULL)
+ {
+ global->xdo = xdo_new (NULL);
+ if (global->xdo == NULL)
+ g_warning ("could not create xdo context");
+ else
+ debug_grab ("xdo context created");
+ }
+
+ modal_maybe_cancel_ui_grab (global);
+ debug_grab ("unsetting input focus");
+ meta_display_unset_input_focus (global->meta_display, CurrentTime);
+
+ return G_SOURCE_CONTINUE;
+ }
+
+ debug_grab ("exhausted all retries, giving up");
+ modal_retry_complete (data, FALSE);
+ return G_SOURCE_REMOVE;
+ }
+
+ return G_SOURCE_CONTINUE;
+}
+
+/**
+ * cinnamon_global_begin_modal_with_retry:
+ * @global: a #CinnamonGlobal
+ * @timestamp: the X server timestamp of the event triggering the grab
+ * @options: #MetaModalOptions flags
+ * @callback: (scope async): function to call when the grab succeeds or fails
+ * @user_data: data to pass to @callback
+ *
+ * Like cinnamon_global_begin_modal(), but retries the grab asynchronously
+ * if the initial attempt fails. On X11, if retries fail, sends Escape key
+ * events via libxdo and clears X11 input focus to break any stuck grab
+ * (e.g. from a GTK popup menu), then retries again.
+ *
+ * Only one retry sequence can be active at a time.
+ */
+void
+cinnamon_global_begin_modal_with_retry (CinnamonGlobal *global,
+ guint32 timestamp,
+ MetaModalOptions options,
+ CinnamonModalCallback callback,
+ gpointer user_data)
+{
+ ModalRetryData *data;
+
+ g_return_if_fail (CINNAMON_IS_GLOBAL (global));
+ g_return_if_fail (callback != NULL);
+
+ debug_grab ("begin_modal_with_retry called");
+
+ if (global->modal_retry_source_id != 0)
+ {
+ debug_grab ("retry already in progress");
+ callback (global, FALSE, user_data);
+ return;
+ }
+
+ debug_grab ("trying initial grab");
+ global->has_modal = modal_try_grab (global, options, timestamp);
+
+ if (global->has_modal)
+ {
+ debug_grab ("initial grab succeeded");
+ sync_input_region (global);
+ callback (global, TRUE, user_data);
+ return;
+ }
+
+ if (meta_is_wayland_compositor ())
+ {
+ debug_grab ("initial grab failed on Wayland, no retry available");
+ callback (global, FALSE, user_data);
+ return;
+ }
+
+ debug_grab ("initial grab failed on X11, starting retry sequence");
+
+ data = g_new0 (ModalRetryData, 1);
+ data->global = global;
+ data->timestamp = timestamp;
+ data->options = options;
+ data->callback = callback;
+ data->user_data = user_data;
+ global->modal_retry_data = data;
+ global->modal_retry_source_id = g_timeout_add (MODAL_RETRY_INTERVAL_MS,
+ modal_retry_timeout,
+ data);
+}
+
/**
* cinnamon_global_end_modal:
* @global: a #CinnamonGlobal
*
- * Undoes the effect of cinnamon_global_begin_modal().
+ * Undoes the effect of cinnamon_global_begin_modal(). If an async
+ * retry sequence from cinnamon_global_begin_modal_with_retry() is
+ * in progress, it is cancelled and the callback is invoked with
+ * %FALSE.
*/
void
cinnamon_global_end_modal (CinnamonGlobal *global,
@@ -1047,6 +1244,15 @@ cinnamon_global_end_modal (CinnamonGlobal *global,
if (!meta_display_get_compositor (global->meta_display))
return;
+ if (global->modal_retry_data != NULL)
+ {
+ ModalRetryData *data = global->modal_retry_data;
+ debug_grab ("end_modal: cancelling in-progress retry");
+ g_source_remove (global->modal_retry_source_id);
+ modal_retry_complete (data, FALSE);
+ return;
+ }
+
if (!global->has_modal)
return;
@@ -1063,8 +1269,7 @@ cinnamon_global_end_modal (CinnamonGlobal *global,
meta_display_focus_default_window (global->meta_display,
get_current_time_maybe_roundtrip (global));
- if (!meta_is_wayland_compositor ())
- sync_input_region (global);
+ sync_input_region (global);
}
static int
diff --git a/src/cinnamon-global.h b/src/cinnamon-global.h
index ad0e429cab..02d62d30ac 100644
--- a/src/cinnamon-global.h
+++ b/src/cinnamon-global.h
@@ -43,6 +43,17 @@ void cinnamon_global_dump_gjs_stack (CinnamonGlobal *global
gboolean cinnamon_global_begin_modal (CinnamonGlobal *global,
guint32 timestamp,
MetaModalOptions options);
+
+typedef void (*CinnamonModalCallback) (CinnamonGlobal *global,
+ gboolean success,
+ gpointer user_data);
+
+void cinnamon_global_begin_modal_with_retry (CinnamonGlobal *global,
+ guint32 timestamp,
+ MetaModalOptions options,
+ CinnamonModalCallback callback,
+ gpointer user_data);
+
void cinnamon_global_end_modal (CinnamonGlobal *global,
guint32 timestamp);
diff --git a/src/meson.build b/src/meson.build
index 925b67226e..548b94f8dc 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -110,6 +110,7 @@ libcinnamon_deps = [
polkit,
st_dep,
xapp,
+ xdo,
xml,
]