From baa910ac8146043e9af614c31b6e407d22b06f6b Mon Sep 17 00:00:00 2001 From: pax Date: Tue, 7 Apr 2026 20:48:09 -0500 Subject: [PATCH] Popout: fix first-fit aspect lock race, fill images to window, tighten combo/button padding across all themes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes that all surfaced from the bookmark/library decoupling shake-out: - Popout first-image aspect-lock race: _fit_to_content used to call _is_hypr_floating which returned None for both "not Hyprland" and "Hyprland but the window isn't visible to hyprctl yet". The latter happens on the very first popout open because the wm:openWindow event hasn't been processed when set_media fires. The method then fell through to a plain Qt resize and skipped the keep_aspect_ratio setprop, so the first image always opened unlocked and only subsequent navigations got the right shape. Now we inline the env-var check, distinguish the two None cases, and retry on Hyprland with a 40ms backoff (capped at 5 attempts / 200ms total) when the window isn't registered yet. - Image fill in popout (and embedded preview): ImageViewer._fit_to_view used min(scale_w, scale_h, 1.0) which clamped the zoom at native pixel size, so a smaller image in a larger window centered with letterbox space around it. Dropped the 1.0 cap so images scale up to fill the available view, matching how the video player fills its widget. Combined with the popout's keep_aspect_ratio, the window matches the image's aspect AND the image fills it cleanly. Tiled popouts with mismatched aspect still letterbox (intentional — the layout owns the window shape). - Combo + button padding tightening across all 12 bundled themes and Library sort combo: QPushButton padding 2px 8px → 2px 6px, QComboBox padding 2px 6px → 2px 4px, QComboBox::drop-down width 18px → 14px. Saves 8px non-text width per combo and 4px per button, so the new "Post ID" sort entry fits in 75px instead of needing 90. Library sort combo bumped from "Name" (lexicographic) to "Post ID" with a numeric stem sort that handles non-digit stems gracefully. --- booru_viewer/gui/library.py | 18 ++++++++++--- booru_viewer/gui/preview.py | 41 ++++++++++++++++++++++++++--- themes/catppuccin-mocha-rounded.qss | 6 ++--- themes/catppuccin-mocha-square.qss | 6 ++--- themes/everforest-rounded.qss | 6 ++--- themes/everforest-square.qss | 6 ++--- themes/gruvbox-rounded.qss | 6 ++--- themes/gruvbox-square.qss | 6 ++--- themes/nord-rounded.qss | 6 ++--- themes/nord-square.qss | 6 ++--- themes/solarized-dark-rounded.qss | 6 ++--- themes/solarized-dark-square.qss | 6 ++--- themes/tokyo-night-rounded.qss | 6 ++--- themes/tokyo-night-square.qss | 6 ++--- 14 files changed, 87 insertions(+), 44 deletions(-) diff --git a/booru_viewer/gui/library.py b/booru_viewer/gui/library.py index fef3f0b..6aa1fce 100644 --- a/booru_viewer/gui/library.py +++ b/booru_viewer/gui/library.py @@ -76,8 +76,10 @@ class LibraryView(QWidget): top.addWidget(self._folder_combo) self._sort_combo = QComboBox() - self._sort_combo.addItems(["Date", "Name", "Size"]) - self._sort_combo.setFixedWidth(80) + self._sort_combo.addItems(["Date", "Post ID", "Size"]) + # 75 is the tight floor: 68 clipped the trailing D under the + # bundled themes (font metrics ate more than the math suggested). + self._sort_combo.setFixedWidth(75) self._sort_combo.currentTextChanged.connect(lambda _: self.refresh()) top.addWidget(self._sort_combo) @@ -252,8 +254,16 @@ class LibraryView(QWidget): def _sort_files(self) -> None: mode = self._sort_combo.currentText() - if mode == "Name": - self._files.sort(key=lambda p: p.name.lower()) + if mode == "Post ID": + # Numeric sort by post id (filename stem). Library files are + # named {post_id}.{ext} in normal usage; anything with a + # non-digit stem (someone manually dropped a file in) sorts + # to the end alphabetically so the numeric ordering of real + # posts isn't disrupted by stray names. + def _key(p: Path) -> tuple: + stem = p.stem + return (0, int(stem)) if stem.isdigit() else (1, stem.lower()) + self._files.sort(key=_key) elif mode == "Size": self._files.sort(key=lambda p: p.stat().st_size, reverse=True) else: diff --git a/booru_viewer/gui/preview.py b/booru_viewer/gui/preview.py index 8911e64..0189c1a 100644 --- a/booru_viewer/gui/preview.py +++ b/booru_viewer/gui/preview.py @@ -380,11 +380,37 @@ class FullscreenPreview(QMainWindow): return None # not Hyprland return bool(win.get("floating")) - def _fit_to_content(self, content_w: int, content_h: int) -> None: - """Size window to fit content. Width preserved, height from aspect ratio, clamped to screen.""" + def _fit_to_content(self, content_w: int, content_h: int, _retry: int = 0) -> None: + """Size window to fit content. Width preserved, height from aspect ratio, clamped to screen. + + Distinguishes "not on Hyprland" (Qt drives geometry, no aspect + lock available) from "on Hyprland but the window isn't visible + to hyprctl yet" (the very first call after a popout open races + the wm:openWindow event — `hyprctl clients -j` returns no entry + for our title for ~tens of ms). The latter case used to fall + through to a plain Qt resize and skip the keep_aspect_ratio + setprop entirely, so the *first* image popout always opened + without aspect locking and only subsequent navigations got the + right shape. Now we retry with a short backoff when on Hyprland + and the window isn't found, capped so a real "not Hyprland" + signal can't loop. + """ if self.isFullScreen() or content_w <= 0 or content_h <= 0: return - floating = self._is_hypr_floating() + import os + on_hypr = bool(os.environ.get("HYPRLAND_INSTANCE_SIGNATURE")) + if on_hypr: + win = self._hyprctl_get_window() + if win is None: + if _retry < 5: + QTimer.singleShot( + 40, + lambda: self._fit_to_content(content_w, content_h, _retry + 1), + ) + return + floating = bool(win.get("floating")) + else: + floating = None if floating is False: self._hyprctl_resize(0, 0) # tiled: just set keep_aspect_ratio return @@ -789,7 +815,14 @@ class ImageViewer(QWidget): return scale_w = vw / pw scale_h = vh / ph - self._zoom = min(scale_w, scale_h, 1.0) + # No 1.0 cap — scale up to fill the available view, matching how + # the video player fills its widget. In the popout the window is + # already aspect-locked to the image's aspect, so scaling up + # produces a clean fill with no letterbox. In the embedded + # preview the user can drag the splitter past the image's native + # size; letting it scale up there fills the pane the same way + # the popout does. + self._zoom = min(scale_w, scale_h) self._offset = QPointF( (vw - pw * self._zoom) / 2, (vh - ph * self._zoom) / 2, diff --git a/themes/catppuccin-mocha-rounded.qss b/themes/catppuccin-mocha-rounded.qss index 182e739..07e3ecc 100644 --- a/themes/catppuccin-mocha-rounded.qss +++ b/themes/catppuccin-mocha-rounded.qss @@ -60,7 +60,7 @@ QPushButton { color: ${text}; border: 1px solid ${border_strong}; border-radius: 4px; - padding: 2px 8px; + padding: 2px 6px; min-height: 17px; } QPushButton:hover { @@ -145,7 +145,7 @@ QComboBox { color: ${text}; border: 1px solid ${border_strong}; border-radius: 4px; - padding: 2px 6px; + padding: 2px 4px; min-height: 16px; } QComboBox:hover { @@ -156,7 +156,7 @@ QComboBox:focus { } QComboBox::drop-down { border: none; - width: 18px; + width: 14px; } QComboBox QAbstractItemView { background-color: ${bg_subtle}; diff --git a/themes/catppuccin-mocha-square.qss b/themes/catppuccin-mocha-square.qss index 782ac11..963063c 100644 --- a/themes/catppuccin-mocha-square.qss +++ b/themes/catppuccin-mocha-square.qss @@ -59,7 +59,7 @@ QPushButton { background-color: ${bg_subtle}; color: ${text}; border: 1px solid ${border_strong}; - padding: 2px 8px; + padding: 2px 6px; min-height: 17px; } QPushButton:hover { @@ -141,7 +141,7 @@ QComboBox { background-color: ${bg_subtle}; color: ${text}; border: 1px solid ${border_strong}; - padding: 2px 6px; + padding: 2px 4px; min-height: 16px; } QComboBox:hover { @@ -152,7 +152,7 @@ QComboBox:focus { } QComboBox::drop-down { border: none; - width: 18px; + width: 14px; } QComboBox QAbstractItemView { background-color: ${bg_subtle}; diff --git a/themes/everforest-rounded.qss b/themes/everforest-rounded.qss index ea70778..ed678ce 100644 --- a/themes/everforest-rounded.qss +++ b/themes/everforest-rounded.qss @@ -60,7 +60,7 @@ QPushButton { color: ${text}; border: 1px solid ${border_strong}; border-radius: 4px; - padding: 2px 8px; + padding: 2px 6px; min-height: 17px; } QPushButton:hover { @@ -145,7 +145,7 @@ QComboBox { color: ${text}; border: 1px solid ${border_strong}; border-radius: 4px; - padding: 2px 6px; + padding: 2px 4px; min-height: 16px; } QComboBox:hover { @@ -156,7 +156,7 @@ QComboBox:focus { } QComboBox::drop-down { border: none; - width: 18px; + width: 14px; } QComboBox QAbstractItemView { background-color: ${bg_subtle}; diff --git a/themes/everforest-square.qss b/themes/everforest-square.qss index c412e7b..1bc2769 100644 --- a/themes/everforest-square.qss +++ b/themes/everforest-square.qss @@ -59,7 +59,7 @@ QPushButton { background-color: ${bg_subtle}; color: ${text}; border: 1px solid ${border_strong}; - padding: 2px 8px; + padding: 2px 6px; min-height: 17px; } QPushButton:hover { @@ -141,7 +141,7 @@ QComboBox { background-color: ${bg_subtle}; color: ${text}; border: 1px solid ${border_strong}; - padding: 2px 6px; + padding: 2px 4px; min-height: 16px; } QComboBox:hover { @@ -152,7 +152,7 @@ QComboBox:focus { } QComboBox::drop-down { border: none; - width: 18px; + width: 14px; } QComboBox QAbstractItemView { background-color: ${bg_subtle}; diff --git a/themes/gruvbox-rounded.qss b/themes/gruvbox-rounded.qss index 703b7ab..add390a 100644 --- a/themes/gruvbox-rounded.qss +++ b/themes/gruvbox-rounded.qss @@ -60,7 +60,7 @@ QPushButton { color: ${text}; border: 1px solid ${border_strong}; border-radius: 4px; - padding: 2px 8px; + padding: 2px 6px; min-height: 17px; } QPushButton:hover { @@ -145,7 +145,7 @@ QComboBox { color: ${text}; border: 1px solid ${border_strong}; border-radius: 4px; - padding: 2px 6px; + padding: 2px 4px; min-height: 16px; } QComboBox:hover { @@ -156,7 +156,7 @@ QComboBox:focus { } QComboBox::drop-down { border: none; - width: 18px; + width: 14px; } QComboBox QAbstractItemView { background-color: ${bg_subtle}; diff --git a/themes/gruvbox-square.qss b/themes/gruvbox-square.qss index 87a09a8..3653d4a 100644 --- a/themes/gruvbox-square.qss +++ b/themes/gruvbox-square.qss @@ -59,7 +59,7 @@ QPushButton { background-color: ${bg_subtle}; color: ${text}; border: 1px solid ${border_strong}; - padding: 2px 8px; + padding: 2px 6px; min-height: 17px; } QPushButton:hover { @@ -141,7 +141,7 @@ QComboBox { background-color: ${bg_subtle}; color: ${text}; border: 1px solid ${border_strong}; - padding: 2px 6px; + padding: 2px 4px; min-height: 16px; } QComboBox:hover { @@ -152,7 +152,7 @@ QComboBox:focus { } QComboBox::drop-down { border: none; - width: 18px; + width: 14px; } QComboBox QAbstractItemView { background-color: ${bg_subtle}; diff --git a/themes/nord-rounded.qss b/themes/nord-rounded.qss index 9dfc95c..96f6d58 100644 --- a/themes/nord-rounded.qss +++ b/themes/nord-rounded.qss @@ -60,7 +60,7 @@ QPushButton { color: ${text}; border: 1px solid ${border_strong}; border-radius: 4px; - padding: 2px 8px; + padding: 2px 6px; min-height: 17px; } QPushButton:hover { @@ -145,7 +145,7 @@ QComboBox { color: ${text}; border: 1px solid ${border_strong}; border-radius: 4px; - padding: 2px 6px; + padding: 2px 4px; min-height: 16px; } QComboBox:hover { @@ -156,7 +156,7 @@ QComboBox:focus { } QComboBox::drop-down { border: none; - width: 18px; + width: 14px; } QComboBox QAbstractItemView { background-color: ${bg_subtle}; diff --git a/themes/nord-square.qss b/themes/nord-square.qss index 5a93e41..21791bc 100644 --- a/themes/nord-square.qss +++ b/themes/nord-square.qss @@ -59,7 +59,7 @@ QPushButton { background-color: ${bg_subtle}; color: ${text}; border: 1px solid ${border_strong}; - padding: 2px 8px; + padding: 2px 6px; min-height: 17px; } QPushButton:hover { @@ -141,7 +141,7 @@ QComboBox { background-color: ${bg_subtle}; color: ${text}; border: 1px solid ${border_strong}; - padding: 2px 6px; + padding: 2px 4px; min-height: 16px; } QComboBox:hover { @@ -152,7 +152,7 @@ QComboBox:focus { } QComboBox::drop-down { border: none; - width: 18px; + width: 14px; } QComboBox QAbstractItemView { background-color: ${bg_subtle}; diff --git a/themes/solarized-dark-rounded.qss b/themes/solarized-dark-rounded.qss index 5c131d7..07e107a 100644 --- a/themes/solarized-dark-rounded.qss +++ b/themes/solarized-dark-rounded.qss @@ -60,7 +60,7 @@ QPushButton { color: ${text}; border: 1px solid ${border_strong}; border-radius: 4px; - padding: 2px 8px; + padding: 2px 6px; min-height: 17px; } QPushButton:hover { @@ -145,7 +145,7 @@ QComboBox { color: ${text}; border: 1px solid ${border_strong}; border-radius: 4px; - padding: 2px 6px; + padding: 2px 4px; min-height: 16px; } QComboBox:hover { @@ -156,7 +156,7 @@ QComboBox:focus { } QComboBox::drop-down { border: none; - width: 18px; + width: 14px; } QComboBox QAbstractItemView { background-color: ${bg_subtle}; diff --git a/themes/solarized-dark-square.qss b/themes/solarized-dark-square.qss index 8c3225f..8d6a17b 100644 --- a/themes/solarized-dark-square.qss +++ b/themes/solarized-dark-square.qss @@ -59,7 +59,7 @@ QPushButton { background-color: ${bg_subtle}; color: ${text}; border: 1px solid ${border_strong}; - padding: 2px 8px; + padding: 2px 6px; min-height: 17px; } QPushButton:hover { @@ -141,7 +141,7 @@ QComboBox { background-color: ${bg_subtle}; color: ${text}; border: 1px solid ${border_strong}; - padding: 2px 6px; + padding: 2px 4px; min-height: 16px; } QComboBox:hover { @@ -152,7 +152,7 @@ QComboBox:focus { } QComboBox::drop-down { border: none; - width: 18px; + width: 14px; } QComboBox QAbstractItemView { background-color: ${bg_subtle}; diff --git a/themes/tokyo-night-rounded.qss b/themes/tokyo-night-rounded.qss index 20bca9b..60e1637 100644 --- a/themes/tokyo-night-rounded.qss +++ b/themes/tokyo-night-rounded.qss @@ -60,7 +60,7 @@ QPushButton { color: ${text}; border: 1px solid ${border_strong}; border-radius: 4px; - padding: 2px 8px; + padding: 2px 6px; min-height: 17px; } QPushButton:hover { @@ -145,7 +145,7 @@ QComboBox { color: ${text}; border: 1px solid ${border_strong}; border-radius: 4px; - padding: 2px 6px; + padding: 2px 4px; min-height: 16px; } QComboBox:hover { @@ -156,7 +156,7 @@ QComboBox:focus { } QComboBox::drop-down { border: none; - width: 18px; + width: 14px; } QComboBox QAbstractItemView { background-color: ${bg_subtle}; diff --git a/themes/tokyo-night-square.qss b/themes/tokyo-night-square.qss index c500f35..9728361 100644 --- a/themes/tokyo-night-square.qss +++ b/themes/tokyo-night-square.qss @@ -59,7 +59,7 @@ QPushButton { background-color: ${bg_subtle}; color: ${text}; border: 1px solid ${border_strong}; - padding: 2px 8px; + padding: 2px 6px; min-height: 17px; } QPushButton:hover { @@ -141,7 +141,7 @@ QComboBox { background-color: ${bg_subtle}; color: ${text}; border: 1px solid ${border_strong}; - padding: 2px 6px; + padding: 2px 4px; min-height: 16px; } QComboBox:hover { @@ -152,7 +152,7 @@ QComboBox:focus { } QComboBox::drop-down { border: none; - width: 18px; + width: 14px; } QComboBox QAbstractItemView { background-color: ${bg_subtle};