From 1979c1c2e2641bef6949285234eabb3df5f48b4d Mon Sep 17 00:00:00 2001 From: Araz Gray Date: Fri, 9 Oct 2026 14:02:59 +0300 Subject: [PATCH] fix: keep window width on KWin/Wayland when adding tabs or toggling visibility RectCalculator.set_final_window_rect() was only applied on show(). Adding/removing a tab (notebook.show_all()) let KWin/XWayland shrink the window to the VTE natural width, and a fresh map was snapped back to it asynchronously. Re-assert the configured size after tab changes, floor the minimum width, and watch configure events for late snap-backs. --- guake/gsettings.py | 6 +++-- guake/guake_app.py | 59 ++++++++++++++++++++++++++++++++++++++++++++-- guake/notebook.py | 27 +++++++++++++++++++++ guake/utils.py | 4 ++++ 4 files changed, 92 insertions(+), 4 deletions(-) diff --git a/guake/gsettings.py b/guake/gsettings.py index 80b004b15..0339b2162 100644 --- a/guake/gsettings.py +++ b/guake/gsettings.py @@ -146,7 +146,8 @@ def alignment_changed(self, settings, key, user_data): """If the gconf var window_halignment be changed, this method will be called and will call the move function in guake. """ - RectCalculator.set_final_window_rect(self.settings, self.guake.window) + rect = RectCalculator.set_final_window_rect(self.settings, self.guake.window) + self.guake._enforced_width = int(rect.width) self.guake.set_tab_position() self.guake.force_move_if_shown() @@ -155,7 +156,8 @@ def size_changed(self, settings, key, user_data): this method will be called and will call the resize function in guake. """ - RectCalculator.set_final_window_rect(self.settings, self.guake.window) + rect = RectCalculator.set_final_window_rect(self.settings, self.guake.window) + self.guake._enforced_width = int(rect.width) def cursor_blink_mode_changed(self, settings, key, user_data): """Called when cursor blink mode settings has been changed""" diff --git a/guake/guake_app.py b/guake/guake_app.py index f81e7bcb1..17858eaea 100644 --- a/guake/guake_app.py +++ b/guake/guake_app.py @@ -237,6 +237,10 @@ def load_schema(): self.window.connect("focus-out-event", self.on_window_losefocus) self.window.connect("focus-in-event", self.on_window_takefocus) + # Watchdog state: last width we enforced; see _on_window_configure. + self._enforced_width = None + self._size_restore_pending = False + self.window.connect("configure-event", self._on_window_configure) # Handling the delete-event of the main window to avoid # problems when closing it. @@ -697,6 +701,10 @@ def show(self): # setting window in all desktops window_rect = RectCalculator.set_final_window_rect(self.settings, self.window) + self._enforced_width = int(window_rect.width) + # Used by the WM as the map-time size; without it KWin/XWayland maps + # at VTE natural width and we get a shrink-then-grow flicker. + self.window.set_default_size(window_rect.width, window_rect.height) self.window.stick() # add tab must be called before window.show to avoid a @@ -739,13 +747,24 @@ def show(self): # glib.timeout_add_seconds(1, lambda: self.timeout_restore(time)) # + # Flip type hint before mapping: doing it after show() makes KWin + # re-manage the window at VTE natural width (flicker). + self.window.set_type_hint(Gdk.WindowTypeHint.DOCK) + self.window.set_type_hint(Gdk.WindowTypeHint.NORMAL) log.debug("order to present and deiconify") self.window.present() self.window.deiconify() self.window.show() + # Re-assert size after mapping; the configure watchdog below + # catches any late snap-back by the compositor. + RectCalculator.set_final_window_rect(self.settings, self.window) + GLib.idle_add( + lambda: not getattr(self, "hidden", True) + and self.window.get_visible() + and RectCalculator.set_final_window_rect(self.settings, self.window) + and False + ) self.window.get_window().focus(time) - self.window.set_type_hint(Gdk.WindowTypeHint.DOCK) - self.window.set_type_hint(Gdk.WindowTypeHint.NORMAL) # This is here because vte color configuration works only after the # widget is shown. @@ -787,6 +806,42 @@ def hide(self): # Hide popover self.notebook_manager.get_current_notebook().popover.hide() + def _on_window_configure(self, widget, event): + """Watchdog: KWin/XWayland sometimes snaps the mapped window back + to VTE natural width. Re-assert the enforced size when a configure + shows a large unexpected shrink (debounced). + """ + try: + if getattr(self, "hidden", True): + return False + if self._enforced_width is None or self._size_restore_pending: + return False + if self.fullscreen_manager.is_fullscreen(): + return False + if event.width < self._enforced_width - 64: + self._size_restore_pending = True + GLib.timeout_add(120, self._restore_enforced_size) + except Exception: + log.exception("configure watchdog failed") + return False + + def _restore_enforced_size(self): + self._size_restore_pending = False + try: + if getattr(self, "hidden", True): + return False + if not self.window.get_visible(): + return False + if self.fullscreen_manager.is_fullscreen(): + return False + w, _h = self.window.get_size() + if self._enforced_width is not None and w < self._enforced_width - 64: + rect = RectCalculator.set_final_window_rect(self.settings, self.window) + self._enforced_width = int(rect.width) + except Exception: + log.exception("failed to restore enforced window size") + return False + def force_move_if_shown(self): if not self.hidden: # when displayed, GTK might refuse to move the window (X or Y position). Just hide and diff --git a/guake/notebook.py b/guake/notebook.py index 96e199b65..47b63b8a7 100644 --- a/guake/notebook.py +++ b/guake/notebook.py @@ -30,6 +30,7 @@ from guake.menus import mk_notebook_context_menu from guake.prefs import PrefsDialog from guake.utils import HidePrevention +from guake.utils import RectCalculator from guake.utils import gdk_is_x11_display from guake.utils import get_process_name from guake.utils import save_tabs_when_changed @@ -40,6 +41,7 @@ gi.require_version("Gtk", "3.0") gi.require_version("Wnck", "3.0") +from gi.repository import GLib from gi.repository import GObject from gi.repository import Gdk from gi.repository import Gtk @@ -359,6 +361,7 @@ def remove_page(self, page_num): self.hide_tabbar_if_one_tab() self.emit("page-deleted") + self.restore_window_size() def delete_page_by_label(self, label, kill=True, prompt=0): self.delete_page(self.find_tab_index_by_label(label), kill, prompt) @@ -396,12 +399,36 @@ def new_page(self, directory=None, position=None, empty=False, open_tab_cwd=Fals if not empty: self.terminal_attached(terminal) self.hide_tabbar_if_one_tab() + self.restore_window_size() if self.guake: # Attack background image draw callback to root terminal box root_terminal_box.connect_after("draw", self.guake.background_image_manager.draw) return root_terminal_box, page_num, terminal + def restore_window_size(self): + """Re-assert the configured window size after a tab is added/removed. + + GTK renegotiates the toplevel natural size on insert_page/show_all + and KWin (XWayland) shrinks Guake. show() already enforces the size + via RectCalculator, but new_page()/remove_page() did not. + """ + try: + guake = getattr(self, "guake", None) + if not guake or not getattr(guake, "window", None): + return + if getattr(guake, "hidden", True): + return + window = guake.window + if not window.get_visible(): + return + GLib.idle_add( + lambda: RectCalculator.set_final_window_rect(guake.settings, window) + and False + ) + except Exception: # never break tab creation on resize failure + log.exception("failed to restore window size after tab change") + def hide_tabbar_if_one_tab(self): """Hide the tab bar if hide-tabs-if-one-tab is true and there is only one notebook page""" diff --git a/guake/utils.py b/guake/utils.py index dc07c80e6..c7d056f1a 100644 --- a/guake/utils.py +++ b/guake/utils.py @@ -307,6 +307,7 @@ def set_final_window_rect(cls, settings, window): if width_percents == 100 and height_percents == 100: log.debug("MAXIMIZING MAIN WINDOW") + window.set_size_request(-1, -1) window.move(window_rect.x, window_rect.y) window.maximize() elif not FullscreenManager(settings, window).is_fullscreen(): @@ -319,6 +320,9 @@ def set_final_window_rect(cls, settings, window): # Note: move_resize is only on GTK3 window.resize(window_rect.width, window_rect.height) window.move(window_rect.x, window_rect.y) + # Floor the width: KWin/XWayland otherwise snaps a fresh map + # back to VTE natural width until something re-asserts it. + window.set_size_request(window_rect.width, -1) log.debug("Updated window position: %r", window.get_position()) return window_rect