GTK4 quirks ----------- Siril's GUI (in :file:`src/gui-gtk4/`) was ported from GTK3 to GTK4 over a long series of numbered phases, and the port surfaced a set of behaviour changes that are easy to trip over. Several of the items below cost real debugging time. This chapter records them in a ``symptom → cause → required pattern`` form so you can recognise the failure quickly and reach for the already-established fix instead of rediscovering it. Read this page alongside :ref:`osdifferences:os differences`, which holds the platform-specific detail (macOS ``NSEvent`` monitors, dark-mode, and so on), and :ref:`threading:threading` / :ref:`locking:locking`, which the dialog and quit paths interact with. .. versionadded:: 1.5 The whole GTK4 GUI layer is new in the 1.5 development line. Modal windows need a transient parent ====================================== **Symptom.** A window created with ``gtk_window_set_modal(win, TRUE)`` behaves as if it were not modal — clicks reach widgets behind it — and on some window managers (notably KDE/kwin) a code-created modal can wedge the whole desktop session so that no clicks land anywhere. **Cause.** GTK4 grants modality relative to a *transient parent*. Without ``gtk_window_set_transient_for()`` the window has no parent to be modal over, so GTK either ignores the modality or the compositor takes a full-session grab that nothing owns. **Required pattern.** Every modal you create in code gets a transient parent, set *before* it is presented. The parent is normally the main window, looked up from the builder as ``control_window``, or the root of the widget that triggered the dialog: - ``gtk_window_set_transient_for(win, GTK_WINDOW(gtk_builder_get_object(gui.builder, "control_window")))`` — the pattern in ``siril_open_dialog()`` and ``run_custom_message_window()`` in :file:`src/gui-gtk4/dialogs.c` and :file:`src/gui-gtk4/message_dialog.c`. - ``gtk_window_set_transient_for(dialog, GTK_WINDOW(gtk_widget_get_root(widget)))`` when you only have the originating widget — as in ``compositing.c`` and ``keywords_tree.c``. Setting the transient parent also gives you free centring: GTK4 removed ``GTK_WIN_POS_CENTER`` and the dialog window-type hint, and the compositor now centres transient windows for you (see the comment in ``siril_open_dialog()``). Focus is not returned to the parent on dismissal ================================================ **Symptom.** After a transient dialog or a native file dialog closes, the parent window silently stops responding to its accelerators (``Ctrl+O``, ``Ctrl+P``, …). Clicking back into the window fixes it. On the GTK console you may also see *"Broken accounting of active state"* if you re-present too eagerly. **Cause.** On Wayland and macOS the compositor does not always hand keyboard focus/activation back to the parent toplevel when the child is dismissed, so the parent is no longer the active window and its shortcut controller never sees the keys. X11 and most Wayland cases *do* return focus, so a blind re-present is both pointless and harmful there. **Required pattern.** Re-present the parent on an idle, guarded so it only acts when the parent is genuinely inactive. Use ``reactivate_parent()`` (which reads the dialog's transient parent and falls back to ``control_window``) after hiding but before destroying a dialog; ``siril_fc_run()`` and ``run_custom_message_window()`` already do this. The machinery is ``present_toplevel()`` / ``present_toplevel_idle()`` in :file:`src/gui-gtk4/dialogs.c`: the re-present is deferred to the next main-loop iteration (the triggering click is still in flight) and only fires when ``gtk_window_is_active()`` is false. Event controllers replace signals ================================== **Symptom.** Legacy ``"button-press-event"`` / ``"scroll-event"`` / ``"key-press-event"`` handlers no longer compile or never fire; event structs like ``GdkEventButton`` and their field accesses (``event->x``, ``event->button``) are gone. **Cause.** GTK4 folds the per-type events into one opaque ``GdkEvent`` and routes input through ``GtkEventController`` objects rather than widget signals. The transition shim :file:`src/gui-gtk4/gtk3_event_compat.h` keeps legacy *type names* (``GdkEventButton``, ``GdkEventScroll``, ``GdkEventMotion``, ``GdkEventKey``, ``GdkEventCrossing``) compiling by aliasing them all onto ``GdkEvent`` — but it deliberately does **not** provide field access. Every field read must be rewritten through a controller API. **Required pattern.** Attach a controller with ``gtk_widget_add_controller()`` and connect its signals: - ``GtkGestureClick`` for button presses. Its ``"pressed"`` handler receives ``int n_press`` — GTK4's replacement for the removed ``GDK_DOUBLE_BUTTON_PRESS`` / ``GDK_TRIPLE_BUTTON_PRESS`` event types — so ``n_press >= 2`` is a double-click. Claim the sequence with ``gtk_gesture_set_state(..., GTK_EVENT_SEQUENCE_CLAIMED)`` to consume it. See ``on_mask_button_clicked()`` and ``on_headerbar_pressed()`` in :file:`src/gui-gtk4/callbacks.c`. - ``GtkEventControllerMotion`` for hover/enter/leave, ``GtkGestureDrag`` for drags, ``GtkEventControllerScroll`` for the wheel, ``GtkEventControllerKey`` for keys, ``GtkShortcutController`` for accelerators. All of these are wired in ``attach_drawingarea_event_controllers()`` in :file:`src/gui-gtk4/image_interactions.c`. Mind the **propagation phase**. Several stock widgets consume events in the bubble phase before they reach your controller. When you need to see the event first, set ``gtk_event_controller_set_propagation_phase(ctrl, GTK_PHASE_CAPTURE)``. This is why the Escape-to-close controller (``ensure_escape_controller()`` in :file:`src/gui-gtk4/dialogs.c`) and the capture-phase accelerator controller (``rebuild_capture_accel_controller()`` in :file:`src/gui-gtk4/callbacks.c`) both run in the capture phase — GTK4 plain ``GtkWindow`` also lost the built-in Escape-closes-dialog binding that ``GtkDialog`` gave for free, so Escape is now wired explicitly and routed through ``gtk_window_close()``. The compat sentinels are not real events **************************************** ``gtk3_event_compat.h`` also re-defines ``GDK_DOUBLE_BUTTON_PRESS`` and ``GDK_TRIPLE_BUTTON_PRESS`` as out-of-range values (``0x1000`` / ``0x1001``) that cannot collide with real ``GdkEventType`` members. These exist only as **in-memory sentinels** for storing and comparing mouse-button configuration (single vs double-click), *not* as event types you can receive. Siril's own mouse-config enum (for example ``SIRIL_EVENT_2BUTTON_PRESS`` used in ``image_interactions.c``) is the value you actually act on; the live double-click test at input time is always ``n_press`` on the gesture. Scroll events carry no pointer coordinates ========================================== **Symptom.** A wheel-zoom handler needs the cursor position to zoom about the point under the pointer, but the ``GtkEventControllerScroll`` ``"scroll"`` signal only delivers ``dx``/``dy``, no ``x``/``y``. **Cause.** GTK4's scroll signal does not carry pointer coordinates, and ``gdk_event_get_position()`` on the underlying event returns surface-relative coordinates that are not what the zoom code expects. **Required pattern.** Cache the last cursor position from the motion controller and read it back in the scroll handler as the zoom anchor. In :file:`src/gui-gtk4/image_interactions.c` the motion callbacks write ``last_motion_widget_x`` / ``last_motion_widget_y`` and ``on_drawingarea_scroll_cb()`` reads them. This is always current: for a scroll to fire over the drawing area the pointer must have crossed it (firing motion) first. The discrete direction, when you need it, comes from ``gdk_scroll_event_get_direction()`` on ``gtk_event_controller_get_current_event()``. File dialogs are async-only =========================== **Symptom.** There is no synchronous "run the dialog and return the chosen path" call anymore; blocking on a file dialog freezes the app, and on macOS it can deadlock inside the ``CFRunLoop`` when the native panel is torn down (the same freeze class as the historic open-dialog freeze). **Cause.** GTK 4.10 deprecated ``GtkFileChooserDialog`` in favour of ``GtkFileDialog``, whose ``open`` / ``save`` / ``select_folder`` / ``open_multiple`` methods are asynchronous — they return immediately and deliver the result to a ``GAsyncResult`` finish callback. They also speak ``GFile``, not path strings. **Required pattern.** Use the ``SirilFileChooser`` wrapper in :file:`src/gui-gtk4/dialogs.c`, which preserves the familiar create → configure → run → read → destroy shape by spinning a local ``GMainLoop`` in ``siril_fc_run()`` until the finish callback fires: - create with ``siril_fc_open()`` / ``siril_fc_add()`` / ``siril_fc_save()``; - configure with ``siril_fc_set_current_folder_path()``, ``siril_fc_set_filename()``, ``siril_fc_set_current_name()``, ``siril_fc_add_filter_pattern()``, ``siril_fc_set_select_multiple()``; - run with ``siril_fc_run()``; - read with ``siril_fc_get_filename()`` / ``siril_fc_get_filenames()`` (these convert the returned ``GFile`` to a path for you); - free with ``siril_fc_destroy()``. The wrapper builds ``GFile`` objects internally via ``g_file_new_for_path()``, and ``siril_fc_run()`` re-presents the parent on return for the focus reason above. The one exception left on the legacy path is ``open_dialog.c``, because ``GtkFileDialog`` has no slot for the custom image-preview widget. The string-based folder helper ``siril_file_chooser_set_current_folder_path()`` lives in :file:`src/gui-gtk4/utils.c` and wraps the GTK4 signature change (``GFile*`` instead of ``const char *path``). Never block the main loop waiting on a file dialog by any *other* means — the ``GMainLoop`` spin above is deliberate and returns cleanly; a naive busy-wait is what re-introduces the macOS modal-teardown freeze. Clipboard is asynchronous too ============================= **Symptom.** The old synchronous ``gtk_clipboard_wait_for_text()`` is gone; reading the clipboard inline (e.g. inside an owner-change handler) is no longer possible. **Cause.** GTK4 uses ``GdkClipboard`` obtained from ``gtk_widget_get_clipboard()`` / ``gdk_display_get_clipboard()``, and text reads are asynchronous. **Required pattern.** Split the read: the signal handler kicks off ``gdk_clipboard_read_text_async()`` and a continuation callback does the work once the text is available. See ``handle_owner_change()`` and ``clipboard_text_ready_cb()`` in :file:`src/gui-gtk4/callbacks.c`. Guard the async read behind ``gdk_clipboard_get_formats()`` first: firing a text read on non-text clipboard contents (for example an ``image/png`` from a screenshot tool) has crashed reliably inside GLib/GTK's async dispatch. Popover autohide is unreliable ============================== **Symptom.** An autohide ``GtkPopover`` stays open when you click elsewhere in the window; only Escape closes it. Clicking the popover's own border is enough to break its dismissal. **Cause.** Autohide relies on an outside-click grab. On Wayland/mutter and on macOS that grab can be lost, and once it is, GTK never sees the outside click that should pop the popover down. On macOS an autohide popover maps as a separate focused ``NSWindow``, so AppKit does not even deliver the outside ``mouseDown`` to the main window's GTK view. **Required pattern.** A capture-phase click controller on the main window pops down any mapped autohide popover on press — ``install_global_popover_autodismiss()`` / ``on_window_press_dismiss_popovers()`` in :file:`src/gui-gtk4/callbacks.c`. The shared, unconditional dismissal is ``close_open_autohide_popovers()`` (it skips the clicked menu button's own subtree so the button can still toggle itself). On macOS the GTK controller never fires, so an ``NSEvent`` monitor (``siril_macos_fix_popover_autohide()`` in :file:`src/core/OS_utils.c`) calls back into GTK through the ``gui_iface.dismiss_autohide_popovers`` function pointer, which is wired to ``close_open_autohide_popovers()`` in :file:`src/gui-gtk4/gui_iface_impl.c` — keeping the Cocoa code free of GTK headers. The nested-popover get_root trap ******************************** When you create a popover programmatically, do **not** blindly parent it on ``gtk_widget_get_root()`` of an arbitrary widget: for a ``GtkMenuButton`` that crashes in realize (the button's surface is not a valid popup parent), and for a popover-inside-a-popover ``gtk_widget_get_root()`` can return a non-window ``GtkRoot``. Look up ``control_window`` from the builder as the parent instead — it is always realized and always provides a valid native surface. See ``popover_new_with_image()`` in :file:`src/gui-gtk4/utils.c`. APIs that simply went away ========================== **Window positioning.** ``gtk_window_get_position()`` / ``gtk_window_move()`` and ``GTK_WIN_POS_CENTER`` are gone; placement is compositor-driven. Siril no longer records or restores dialog positions across reopens (see the note in ``siril_open_dialog()`` in :file:`src/gui-gtk4/dialogs.c`). Rely on the transient-parent centring described above. **Quitting.** There is no ``gtk_main_quit()`` in GTK4. Quit the application through the ``GApplication`` — the ``"quit"`` action registered in :file:`src/main.c` and ``g_application_quit()`` in :file:`src/gui-gtk4/gui_iface_impl.c`. (There *is* a Siril function literally named ``gtk_main_quit()`` in :file:`src/gui-gtk4/callbacks.c`, but it is a Siril shim that pumps the loop to drain in-flight work before cleanup, not the old GTK symbol — don't confuse the two.) Widget lifecycle: hide, don't destroy ===================================== **Symptom.** Destroying a reusable dialog and rebuilding it on next use loses its state and, worse, invalidates any cached widget pointers. **Cause.** Siril's dialogs are built once from the builder and reused. Builder objects live for the lifetime of the application, so a widget looked up with ``lookup_widget()`` / ``gtk_builder_get_object()`` stays valid. **Required pattern.** Toggle visibility rather than destroy: ``siril_open_dialog()`` presents and ``siril_close_dialog()`` calls ``gtk_widget_set_visible(w, FALSE)`` (both in :file:`src/gui-gtk4/dialogs.c`). Per-window setup that must run only once (the Escape controller, for instance) is guarded by an object-data flag so reopening is idempotent — see ``ensure_escape_controller()``. Reserve ``gtk_window_destroy()`` for windows genuinely built ad hoc in code, such as the custom message window in :file:`src/gui-gtk4/message_dialog.c`. macOS specials ============== These are summarised here and covered in full in :ref:`osdifferences:os differences`. - **Cmd-key accelerators.** GTK4's ``GdkMacosView`` does not override ``performKeyEquivalent:`` (GTK3's ``GdkQuartzView`` did), so ``Command+key`` shortcuts never reach the shortcut controller. The workaround is a local ``NSEvent`` key-down monitor, ``siril_macos_fix_keyboard_shortcuts()`` in :file:`src/core/OS_utils.c`, which forwards Cmd+key events into GDK. Accelerators are registered with ```` on macOS — ``set_accel_map()`` in :file:`src/gui-gtk4/callbacks.c` runs each entry through ``siril_remap_accel()`` (in :file:`src/gui-gtk4/utils.c`) to swap ```` for ```` so Command matches. - **Popover mouseDown monitor.** The autohide-popover ``NSEvent`` monitor ``siril_macos_fix_popover_autohide()`` described above. Both monitors ignore native panels by checking that the window's content view carries a ``"Gdk"`` class-name prefix. Grab and click-freeze hazards on X11 ==================================== **Symptom.** On some X11 desktops (KDE has been the reproducer) Siril can appear to freeze the desktop — clicks land nowhere — typically when a dialog or popover comes up while the app is backgrounded during startup. **Cause.** This is a leaked/held grab: a modal anchored to a background window can hold input focus behind an unrelated window, which reads as a frozen desktop. The KDE setting ``AllowDeactivateGrabs`` is the discriminator between desktops that recover and those that lock. **Required pattern (hardening, kept deliberately light).** - Don't take popdown grabs during startup — keep autohide popovers out of the startup path. - When a modal must appear over a possibly-backgrounded parent, raise that parent first: ``raise_parent_if_backgrounded()`` in :file:`src/gui-gtk4/message_dialog.c` raises the transient parent when it is mapped but not active, so the modal surfaces in front of the user instead of trapping focus behind another window. It is a no-op for the normal foreground case, so it never steals focus.