Unused: Path, Post, QPainterPath, QMenu, QApplication.
FlowLayout.clear() now stops any in-flight fade animation before
calling deleteLater() on thumbnails. Without this, a mid-flight
QPropertyAnimation can fire property updates on a widget that's
queued for deletion.
Two fixes:
1. Stale state cleanup. If a rubber band drag is interrupted without a
matching release event (Wayland focus steal, drag outside window,
tab switch, alt-tab), _rb_origin and the rubber band widget stay
stuck. The next click then reuses the stale origin and rubber band
stops working until the app is restarted. New _clear_stale_rubber_band
helper is called at the top of every mouse press entry point
(Grid.mousePressEvent, on_padding_click, ThumbnailWidget pixmap
press) so the next interaction starts from a clean slate.
2. Scroll offset sign error in _rb_drag. The intersection test
translated thumb geometry by +vp_offset, but thumb.geometry() is in
widget coords and rb_rect is in viewport coords — the translation
needs to convert between them. Switched to translating rb_rect into
widget coords (rb_widget = rb_rect.translated(vp_offset)) before the
intersection test, which is the mathematically correct direction.
Rubber band selection now tracks the visible band when scrolled.
behavior change: rubber band stays responsive after interrupted drags
- ThumbnailWidget detects clicks outside the pixmap and calls
grid.on_padding_click() via parent walk (signals + event filters
both failed on Wayland/QScrollArea)
- Grid tracks a pending rubber band origin; only activates past 30px
manhattan distance so small clicks deselect cleanly
- Move/release events forwarded from ThumbnailWidget to grid for both
the pending-drag check and the active rubber band drag
- Fixed mapFrom/mapTo direction (mapFrom's first arg must be a parent)
The previous FlowLayout._do_layout walked each thumb summing
`widget.width() + THUMB_SPACING` and wrapped on `x + item_w >
self.width()`. This was vulnerable to two issues that conspired to
produce the "grid collapses by a column when switching to a post"
bug:
1. **Per-widget width drift**: ThumbnailWidget calls
`setFixedSize(THUMB_SIZE, THUMB_SIZE)` in __init__, capturing the
constant at construction time. If `THUMB_SIZE` is later mutated
via `_apply_settings` (main_window.py:2953 writes
`grid_mod.THUMB_SIZE = new_size`), existing thumbs keep their old
fixed size while new ones (e.g. from infinite-scroll backfill via
`append_posts`) get the new value. Mixed widths break the
width-summing wrap loop.
2. **Off-by-one in the columns property**: `w // (THUMB_SIZE +
THUMB_SPACING)` overcounted by 1 at column boundaries because it
omitted the leading THUMB_SPACING margin. A row that fits N
thumbs needs `THUMB_SPACING + N * step` pixels, not `N * step`.
For width=1135 with step=188, the formula returned 6 columns
while `_do_layout` only fit 5 — the two diverged whenever the
grid sat in the boundary range.
Both are fixed by using a deterministic position formula:
cols = max(1, (width - THUMB_SPACING) // step)
for each thumb i:
col = i % cols
row = i // cols
x = THUMB_SPACING + col * step
y = THUMB_SPACING + row * step
The layout is now a function of `self.width()` and the constants
only — no per-widget reads, no width-summing accumulator. The
columns property uses the EXACT same formula so callers (e.g.
main_window's keyboard Up/Down nav step) always get the value the
visual layout actually used.
Standardizing on the constant means existing thumbs that were
created with an old `THUMB_SIZE` value still position correctly
(they sit in the cells positioned by the new step), and any future
mutation of THUMB_SIZE only affects newly-created thumbs without
breaking the layout of the surviving ones.
Affects all three tabs (Browse / Bookmarks / Library) since they
all use ThumbnailGrid from grid.py.
Verification:
- Phase A test suite (16 tests) still passes
- Popout state machine tests (65 tests) still pass
- Total: 81 / 81 automated tests green
- Imports clean
- Manual: open the popout to a column boundary (resize window
width such that the grid is exactly N columns wide), switch
between posts — column count should NOT flip to N-1 anymore.
Also verify keyboard Up/Down nav steps by exactly the column
count visible on screen (was off-by-one before at boundaries).
The ThumbnailGrid was setting horizontal scrollbar to AlwaysOff
explicitly but leaving the vertical scrollbar at the default
AsNeeded. When content first overflowed enough to summon the
vertical scrollbar, the viewport width dropped by ~14-16px
(scrollbar width), and FlowLayout's column count flipped down by 1
because the integer-division formula sat right at a boundary.
columns = max(1, w // (THUMB_SIZE + THUMB_SPACING))
For THUMB_SIZE=180 + THUMB_SPACING=6 (per-column step = 186):
- viewport 1122 → 6 columns
- viewport 1108 (1122 - 14 scrollbar) → 5 columns
If the popout/main window happened to sit anywhere in the range
where `viewport_width % 186 < scrollbar_width`, the column count
flipped when the scrollbar appeared. The user saw "the grid
collapses by a column when switching to a post" — the actual
trigger isn't post selection, it's the grid scrolling enough to
bring the selected thumbnail into view, which makes content
visibly overflow and summons the scrollbar. From the user's
perspective the two events looked correlated.
Fix: setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn). The
scrollbar is now always visible, its width is always reserved in
the viewport, and FlowLayout's column count is stable across the
scrollbar visibility transition.
Trade-off: a slim grey scrollbar strip is always visible on the
right edge of the grid, even when content fits on one screen and
would otherwise have no scrollbar. For an image grid that almost
always overflows in practice, this is the standard behavior (most
file browsers / image viewers do the same) and the cost is
invisible after the first few thumbnails load.
Affects all three tabs (Browse / Bookmarks / Library) since they
all use ThumbnailGrid from grid.py.
Verification:
- Phase A test suite (16 tests) still passes
- Popout state machine tests (65 tests) still pass
- Total: 81 / 81 automated tests green
- Imports clean
- Manual: open the popout to a column boundary (resize window
width such that the grid is exactly N columns wide before any
scrolling), then scroll down — column count should NOT flip to
N-1 anymore.
Old staggered drain (50ms per post) was added for visual polish but
made infinite scroll painfully slow — a 40-post page took 2 seconds
just to add to the grid. Thumbnails already load async via _fetch_thumbnail,
so the stagger was just delaying grid population for no real benefit.
Now all posts are added instantly in one pass with thumbnails filling in
as they arrive. Scroll trigger widened from 1 row to 3 rows from bottom
so the next page starts loading before you reach the end.
FlowLayout.columns now reads the scroll area viewport width
instead of its own width, which can be stale after appending
posts or scrollbar visibility changes.
When enabled, hides prev/next buttons and loads more posts
automatically when scrolling to the bottom. Posts appended
to the grid, deduped against already-shown posts. Restart
required to toggle.
Right-click selects the thumbnail (border highlight) but doesn't
trigger post_selected/activated, so preview stays on current post.
Added hover border highlight. Removed _last_activated_index guard.
- Missing file indicator (red dot) for NAS/lost files, QSS-controllable
via qproperty-missingColor
- Library items now show green saved dot
- Missing files detected on refresh and marked red
- Skip video files in library thumbnail generation (PIL can't open them)
- _on_bookmark_done only sets bookmarked for bookmark ops, saved for save ops
- Smaller green dot with yellow star to its right
- Default bookmark star color: yellow (#ffcc00)
Major restructure of the favorites/library system:
- Rename "Favorites" to "Bookmarks" throughout (DB API, GUI, signals)
- Add Library tab for browsing saved files on disk with sorting
- Decouple bookmark from save — independent operations now
- Two indicators on thumbnails: star (bookmarked), green dot (saved)
- Both indicators QSS-controllable (qproperty-bookmarkedColor/savedColor)
- Unbookmarking no longer deletes saved files
- Saving no longer auto-bookmarks
- Library tab: folder sidebar, sort by date/name/size, async thumbnails
- DB table kept as "favorites" internally for migration safety
- Prefetch adjacent posts is now a toggle in Settings > General (off by default)
- Prefetch progress bar on thumbnails shows download state
- Blacklist Post: right-click to hide a specific post by URL
- "Create from Template" opens themes reference on git.pax.moe
and spawns the default text editor with custom.qss
- Up/Down in grid's last incomplete row moves to last post instead
of triggering page turn
- Preview panel set to NoFocus so clicking it doesn't steal
keyboard focus from the grid
Supports Danbooru, Gelbooru, Moebooru, and e621. Features include tag search
with autocomplete, favorites with folders, save-to-library, video playback,
drag-and-drop, multi-select, custom CSS theming, and cross-platform support.