Releases: ppoilbarbe/PBPicat
Releases · ppoilbarbe/PBPicat
Release list
v1.15.0
Added
F2shortcut for the "Rename selection" button — clicking the button moved keyboard focus away from the file list, preventing further arrow-key navigation until it regained focus; a shortcut triggers the action without stealing focus. Also added to the shortcuts window (F1) and to the button's tooltip.- Hidden catalogs — catalogs whose name starts with
.or-are now excluded from the Catalog menu and the "Delete catalog…" list.list_catalogs()gained aninclude_hiddenkeyword argument (defaultFalse) to opt into seeing them. Switching to a hidden catalog no longer overwritescatalog.conf, so restarting the app always resumes the last non-hidden catalog rather than a hidden one. - Optional positional CLI argument to load a specific catalog at startup —
pbpicat <name>(may be a hidden catalog; a name starting with-must be passed after--, e.g.pbpicat -- -secret). If no catalog with that name exists, a warning dialog is shown and startup falls back to the normal behaviour (last opened catalog). - "Sort ↓" / "Sort ↑" buttons in the history edit windows (
Settings → Histories…) alongside the existing Move up/down — sort ascending/descending, locale-aware (QCollatorusing the interface's display language) and insensitive to case and diacritics (O=o=Ô=Ǫ,Œ=OE=œ). Disabled when fewer than 2 items are in the list. Empty-string entries always sort to the end, regardless of direction.
Changed
- "New catalog…" / "Duplicate catalog…" — name already taken — previously just showed a blocking "Already exists" error and cancelled. Now asks whether to open the existing catalog instead (checked against all catalogs, including hidden ones); choosing "Open" switches to it, otherwise the action is cancelled as before.
Fixed
- Arrow-key navigation jumped to the first row after renaming a file (not the first in the list) with the
F2shortcut — renaming triggers a disk change thatQFileSystemWatcherpicks up shortly afterrefresh_and_select()'s own refresh already ran, restarting the 400 ms debounce and firing a second, redundant_refresh_preserve_selection(). That method (andrefresh_and_select_paths()) restored the previous selection after the table rebuild but never restored the current index, which the rebuild invalidates — so the renamed-to row stayed visibly selected whilecurrentIndex()was-1, and the next arrow-key press navigated as if starting from row 0. Both methods now delegate to a new shared_select_paths_and_set_current()helper, which also callsselectionModel().setCurrentIndex()for the first restored row — centralizing the fix instead of duplicating it in both call sites.
v1.14.0
Added
- README link to the documentation —
README.mdnow links to the Read the Docs site.
Fixed
docs/conf.py: the## [Unreleased]section ofCHANGELOG.mdwas dropped by the RST changelog generator except for its orphaned### Added/### Changed/... subheadings, which were emitted without content and at an incorrect nesting level.[Unreleased]is now recognised like a dated release and included only when it has actual content.- Application froze (hard hang, required killing the process) after renaming a couple of files in a row with the image viewer open — each rename made
refresh_and_select()reload the full-size image synchronously on the GUI thread (ImageViewer.load_image→load_pixmap) while_start_worker()concurrently spawned_ThumbnailWorkerbackground threads decoding thumbnails (load_qimage) for the same refreshed rows. Qt'sQImageReader/image-format plugins are not safe to invoke concurrently from multiple threads, and decoding from the GUI thread at the same time as a worker thread deadlocked insideQImageReader.read(). Confirmed via a repro script that reliably hung the process, withfaulthandlerstack dumps (triggered bySIGABRT) pinpointing both the GUI thread and worker threads blocked inQImageReader.read()simultaneously.image_io.py'sload_qimage()andload_pixmap()now serialize all Qt decodes behind a sharedthreading.Lock. FileListWidget.refresh_and_select()did not stop the pending_refresh_debouncetimer before refreshing, unlike its sibling refresh methods, allowing a directory-watcher-triggered refresh to fire redundantly right after a rename's explicit refresh.pbpicat.svg— replaced thestyle="width: 100%; height: 100%;"attribute with explicitwidth="1024" height="1024", matching the convention used by icons in other applications.
v1.13.0
Added
- Sphinx/ReadTheDocs documentation —
docs/(user guide, API reference, changelog generated fromCHANGELOG.mdat build time, app icon reused as logo/favicon) and aCODING.mddeveloper guide covering versioning, changelog, and release conventions. Wired intomake docs/make docs-live,.readthedocs.yaml, and a CI job that builds the docs on every push.
v1.12.0
Fixed
- Large photos (50-100 MP phone cameras) rejected with
qt.gui.imageio: QImageIOHandler: Rejecting image as it exceeds the current allocation limit of 256 megabyteswhen opened in the viewer — Qt's default 256 MiB allocation limit is checked against the image's undecoded dimensions; a 50+ MP photo decodes to 200-400 MB as RGB32, exceeding it, which made Qt reject the read outright and fall back to a much slower full-resolution Pillow decode just to display the file.image_io.py's_make_reader()now raises the limit to 1024 MiB (_ALLOCATION_LIMIT_MIB). Thumbnail generation was never affected (it downscales viasetScaledSize(), checked against the target size, not the source). - Batch rotation stopped at the first failing file — when rotating multiple selected files and one failed (e.g. a truncated JPEG),
_rotate_images()stopped processing the rest of the selection. Now continues with the remaining files, and a single dialog lists all failures with their respective file name and message. - Rotation errors did not show which file caused them —
QMessageBoxfor both rotation failures and undo failures showed only the raw error text (e.g. "Premature end of JPEG file"), with no way to tell which file among the selection was at fault. Both dialogs now prefix the message with the file name. - Rotation on a genuinely truncated JPEG failed with the cryptic
jpegtranmessage "Premature end of JPEG file" — some files (e.g. panorama shots) are missing a small amount of data at the end, most likely from an incomplete write by the camera;jpegtrancorrectly refuses to rotate them since it cannot reconstruct the missing DCT blocks losslessly. Added a user-friendly explanation shown above the rawjpegtranmessage inrotate_lossless()'sRuntimeError(_jpegtran_error_hints()inimage_ops.py). - Some JPEGs triggered a permanent
qt.gui.imageio.jpeg: Corrupt JPEG data: N extraneous bytes before marker 0xd9warning — trailing garbage bytes before the EOI marker (seen on some camera/app-produced JPEGs). Unlike the SOS defect above, there is no safe byte-level fix (the exact boundary between real entropy data and garbage can't be determined without a full Huffman decode), and it also has no practical impact: Qt/Pillow decode the image fine regardless. Silenced viaQLoggingCategory.setFilterRules("qt.gui.imageio.jpeg=false")in__main__.py. - JPEGs from some phone cameras (e.g. certain Samsung front-camera modules) triggered a permanent
qt.gui.imageio.jpeg: Invalid SOS parameters for sequential JPEGwarning, displayed without EXIF auto-rotation, and failed lossless rotation — these files have a malformed SOS header (Se=0instead of the mandatory63for baseline JPEGs), whichjpegtranrejects outright and Qt's decoder merely warns about. Addedrepair_jpeg_sos()inimage_ops.py, a lossless header fix (no entropy-coded data touched, no-op on well-formed files) applied before both Qt decoding (image_io.py) andjpegtraninvocation. Also fixedload_pixmap()'sauto_rotate=Truefast path, which used a bareQPixmap(str(path))and never actually enabled Qt'ssetAutoTransform, silently skipping EXIF auto-rotation in the main viewer (thumbnails were unaffected, as they already went throughQImageReaderwithsetAutoTransform). - Double-click on image opens viewer with spurious "Cannot display multiple files simultaneously" error — Qt's
ExtendedSelectionmode emitsitemSelectionChangedtwice during a double-click (clear then re-select), causing_on_selection_changedto see 0 rows and wrongly trigger the multi-file message. Fixed by ignoring the transient empty-selection state (len(rows) == 0) in_on_selection_changed. - File list selection jumps to row 0 after opening image viewer —
focusInEventcalledselectRow(0)whenever focus returned via a non-mouse event and the selection appeared empty, which happened when the viewer window stole focus. Fixed by skipping the auto-select-first-row logic while the image viewer is visible. - Mouse-wheel scrolling through a long file list could stall repeatedly — restarting the visible-thumbnails worker (on each scroll-driven debounce firing) used to block the GUI thread waiting for the previous worker's current in-flight JPEG decode to finish; with large real photos and many debounce firings over a long scroll session, this added up to noticeable stalls, especially scrolling back up through thousands of rows. Restarting on scroll/resize now cancels the previous worker without blocking (
_cancel_worker_async()), since the table itself isn't being rebuilt and a stray late thumbnail from the cancelled thread simply lands on a still-valid row. - Wheel/keyboard scrolling a long distance still felt slow, worse the further travelled — unlike a scrollbar-handle drag (a single jump to a destination, loading only its final position), traversing the list with the mouse wheel passes through every intermediate row, and each pause between wheel notches longer than the debounce interval (previously 100 ms) triggered a batch load for that transient position — so the number of batches triggered, and the cumulative lag, scaled with the distance scrolled rather than the destination itself. Raised the debounce to 400 ms, comfortably longer than typical inter-notch gaps during continuous scrolling, so intermediate positions are skipped and only the position where scrolling actually settles gets loaded.
- Rotation-error dialog could grow unusably tall with a large selection — rotating thousands of files with many failures joined every file's error into one long plain message, pushing the OK button off-screen. The dialog now shows a short summary with the full per-file list in a collapsible/scrollable "Show Details" section (
QMessageBox.setDetailedText()) instead.
Added
- Keyboard shortcuts F9 / F10 for "Apply EXIF orientation" and "Force EXIF orientation to 0°", previously accessible only via menu/toolbar. Follows the existing F6/F7/F8 rotation shortcut sequence.
- Missing SVG icons (
auto-rotate,object-rotate-left,object-rotate-right,object-flip-vertical,reset-exif,folder-new,folder-open) — buttons and actions with no SVG fallback file displayed no icon on systems without an icon theme (e.g. Linux bundle).
Changed
- Thumbnail loading in the file list is now viewport-lazy — previously, opening or refreshing a directory queued thumbnail generation for every file up front, in a single background thread, one at a time. On directories with thousands of files this could take many minutes, and files near the end of the list would sit without a thumbnail the whole time.
_start_worker()now only loads thumbnails for rows currently visible (plus one screenful of margin), and re-triggers on scroll/resize (debounced) so thumbnails pop in on demand as the list is scrolled. Directory load/refresh time is no longer proportional to the total file count. Dragging the scrollbar handle across a long list skips loading until the handle is released, instead of loading a batch at every intermediate position passed through during the drag (which could add up to nearly as many decodes as loading everything). - Thumbnails for a freshly-scrolled-to batch of rows now load in parallel (up to 4 concurrent
_ThumbnailWorkerthreads, capped by CPU count) instead of one thread decoding the whole visible batch sequentially — cuts the delay before icons appear after landing on a new scroll position. - "Reset EXIF orientation" renamed to "Force EXIF orientation to 0°" — the previous wording didn't make clear what the action actually does (menu/toolbar/shortcuts window/status tip).
- File list column header — now shows the number of selected files alongside the total (
File name (sel/n)) whenever 2 or more files are selected, instead of only the total (File name (n)). A single selected file still shows just the total, since the count is redundant there. _linux.py—open_defaultnow usessubprocess.Popen(["xdg-open", …])instead ofQDesktopServices.openUrl(), which bypassed the user's MIME association and always openedxedinstead of the actual default application.- SVG icons — all files replaced with high-resolution (1024×1024) versions with a consistent blue rounded-square background;
pbpicat.svgsimplified (534 KB → 24 KB). - SVG file names normalised: hyphens only (no underscores), semantic names (
history,duplicate,rename-template,reset-exif,auto-rotate,open-with,zoom-fit,zoom-in,zoom-out,zoom-original,zoom-width,zoom-height). file_list_widget.py— directQDesktopServices.openUrl()calls for sidecars and videos now go throughopen_default().
Removed
- Obsolete SVG files:
document-open-recent.svg,edit-copy.svg,template.svg,open_with.svg,zoom_fit.svg,zoom_in.svg,zoom_out.svg,zoom_original.svg,zoom_width.svg,zoom_height.svg.
v1.11.0
Added
hooks/pyi_rth_fonts.py— PyInstaller runtime hook for portable
fontconfig on Linux. At frozen-app startup (before Qt initialises) it writes
a minimalfonts.confintosys._MEIPASSpointing to the bundledfonts/
directory and the system/etc/fonts/fonts.conf, setsFONTCONFIG_FILE, then
callsFcFini()/FcInit()via ctypes to force fontconfig to re-read the
variable beforeQFontDatabaseis populated. The hook is no-op on non-Linux
platforms and in non-frozen runs.- Conda fonts bundled in
pbpicat.spec— the$CONDA_PREFIX/fonts/
directory is included asfonts/in the frozen bundle when it exists; the
runtime hook above uses this directory.hooks/pyi_rth_fonts.pyis
registered as a runtime hook in the spec. use_trashsetting (defaulttrue) — new Behaviour tab in Settings →
Catalog configuration… with a Move deleted files to trash checkbox; when
enabled, deletions are routed throughQFile.moveToTrash()instead of
Path.unlink()._delete_rows_and_select(next_row)inFileListWidget— removes only the
rows corresponding to deleted files from the table without restarting the
thumbnail worker for remaining rows; replaces the previous full-refresh call
after deletion.fonts-conda-ecosystemadded toenvironment.yml(Ubuntu, DejaVu,
Inconsolata, SourceCodePro); these fonts are already bundled into the frozen
binary viapbpicat.specbut were missing from the environment declaration,
so a freshconda env createwould produce a bundle with no fonts._load_bundled_fonts(app)in__main__.py: registers all bundled.ttf
files intoQFontDatabaseand explicitly sets Ubuntu as the application
font. Thelibfontconfig.sobundled by PyInstaller has its default config
path hardcoded to the build machine's conda prefix; on any other machine that
path is absent and fontconfig fails silently, leaving Qt with no valid system
font and an unpredictable default. Forcing Ubuntu bypasses fontconfig for
font selection while the runtime hook (pyi_rth_fonts.py) still provides a
portablefonts.confso rendering settings (anti-aliasing, hinting…) are
inherited from the system/etc/fonts/fonts.conf.--dev-config-dir DIRCLI flag — overrides the XDG configuration directory at launch, enabling isolated test runs without touching the real config.set_base_dir()inconfig.py— public function to redirect_BASE_DIR,_CATALOG_CONF, and_GLOBAL_CONFIG_PATHbeforeinit_catalogs()is called.make run ARGS="..."— therunMakefile target now forwards$(ARGS)to the Python invocation so extra flags can be passed from the command line.argparse_qt.py— new module exposingadd_qt_arguments(parser): adds all Qt command-line flags as--double-dashargparse options grouped under Qt options; each option appends its single-dash equivalent toargs.qt_argsfor direct forwarding toQApplication. Qt options are hidden from theusage:line to reduce noise while remaining fully visible in the--helpoutput.- Ctrl+right-click zoom out in the image viewer — symmetric with Ctrl+left-click zoom in; both center the zoom on the clicked pixel.
- Mouse shortcuts section in the shortcuts window — viewer mouse shortcuts (pan, center on point, zoom in/out centered on point) listed under a Mouse subsection alongside the existing Keyboard subsection.
app_qsettings()inconfig.py— application-levelQSettingsstored in~/.config/pbpicat/app.conf, independent of the active catalog.- Persistent window geometry — position and size of all windows (main window, image viewer, shortcuts, settings, global settings, field histories) are saved and restored between sessions; stored at application level so they survive catalog changes. The About dialog is excluded.
- Qt built-in translations — standard dialog button labels (OK, Cancel, Close, Yes, No, …) are now displayed in the active UI language by loading the appropriate
qtbase_<lang>.qmfile at startup.
Changed
- Any catalog can now be deleted — the hard-coded protection of the
"default"catalog is removed; the UI prevents deleting the last remaining catalog instead (message: "The last catalog cannot be deleted."). When the active catalog is deleted the app switches to the next available catalog instead of always falling back to"default". load_current_catalog_name()— falls back to the first available catalog (instead of"default") whencatalog.confis absent or invalid.init_catalogs()— no longer creates thedefaultdirectory unconditionally; it only creates it when no catalog exists yet.fix_po_files.py— now also strips pybabel location comments (#: file.py:N) to eliminate spurious line-number churn in.podiffs.- Shortcuts window renamed — formerly Keyboard shortcuts, now Shortcuts; made non-modal so it can stay open while working.
- Key names localised — Del and Esc now display as Suppr and Échap on French keyboards via
QKeySequence.toString(NativeText); mouse button names (left-click, right-click) are translated into each UI language. - Deletion confirmation dialog — wording now adapts to the
use_trashsetting: "Move to trash: …" / "Move N files to trash?" when trash is enabled; existing "Permanently delete" wording otherwise. - Empty parent directory cleanup skipped in trash mode — when
use_trashis enabled the post-deletion directory sweep is omitted; the OS trash mechanism handles the directory state.
v1.10.1
Fixed
- About dialog shows wrong version in bundle —
copy_metadata("pbpicat")is now included inpbpicat.specso thatimportlib.metadata.version()resolves correctly at runtime inside a PyInstaller bundle.make distnow runspip install -e .beforehand to ensure package metadata matchespyproject.toml.
Changed
bump_version.py— removed the stale regex entry forsrc/pbpicat/__main__.py(no longer contains a hardcoded version string).
v1.10.0
Added
- Reset EXIF orientation — new Images menu entry and ImageViewer toolbar button (Reset EXIF orientation) that sets the EXIF Orientation tag to 1 (normal) without rotating pixels. Disabled when the selected image has no orientation tag. Undoable (
("reset_exif", [(path, orig_orient)])); status bar shows the count of affected files; undo restores the original tag value. exif_auto_rotatesetting (defaulttrue) — new boolean config key; when false, thumbnails and the ImageViewer display images without applying the EXIF Orientation tag. Exposed in Settings → Catalog configuration… → Images as the "Apply EXIF rotation" checkbox. Changing the setting live reloads the current image in an open viewer viaset_auto_rotate().- F6 / F7 / F8 keyboard shortcuts for rotation — Rotate 90° CCW (F6), Rotate 180° (F7), Rotate 90° CW (F8) in the main window; entries added to the keyboard shortcuts dialog.
- X and Z as aliases in ImageViewer — X = Fit window (alias for 0), Z = Actual size / 1:1 (alias for 1); toolbar tooltips updated to show both keys.
- Ctrl+click in ImageViewer — zooms to the clicked point in CUSTOM mode (previously this was done via double-click).
_sanitize_exif_for_dump()inimage_ops.py— converts Undefined-typed EXIF tags stored as plain integers (seen with some camera firmware) to bytes before callingpiexif.dump().
Changed
load_qimage/load_pixmap— addedauto_rotateparameter (defaultTrue); the Pillow path now callsImageOps.exif_transpose()whenauto_rotate=True; theQImageReaderpath usessetAutoTransform(auto_rotate).set_exif_orientation()— reads raw EXIF bytes via Pillow (img.info["exif"]) before callingpiexif.load()to avoid failures on certain JPEG files; is a no-op when the file has no EXIF block; calls_sanitize_exif_for_dump()beforepiexif.dump().- Double-click in ImageViewer — now centers the viewport on the clicked point instead of zooming to it.
- ImageViewer scroll centering — on mode switch (Fit / 1:1 / Width / Height) and on image load the viewport is now centered; proportional scroll preservation is restricted to CUSTOM zoom mode.
tools/fix_po_dates.py→tools/fix_po_files.py— file renamed;make translateupdated accordingly.ruff-pre-commit— updated fromv0.15.15tov0.15.17in.pre-commit-config.yaml; frozen commit hash replaced with version tag.
Fixed
- Zoom step additive instead of multiplicative — zoom in/out now adds/subtracts the configured step in percentage points (e.g. 23% → 48% → 73% with a 25% step) instead of multiplying by a factor (which gave 23% → 29% → 36%).
set_exif_orientation()crash on non-standard EXIF —piexif.dump()raised a type error when camera firmware had stored Undefined-typed values as integers instead of bytes; fixed by sanitizing with_sanitize_exif_for_dump()before dumping.
v1.9.0
Added
- Lossless rotation — new Images menu entries Rotate 90° CCW, Rotate 90° CW, Rotate 180°, and Apply EXIF orientation; same buttons in the ImageViewer toolbar between zoom controls and action buttons.
- JPEG: uses
jpegtran(bundled in the PyInstaller executable); raises aRuntimeErrorshown as a warning dialog if unavailable. - Other formats (PNG, TIFF, BMP, WebP): uses Pillow — always lossless.
- After JPEG rotation the EXIF Orientation tag is stripped via
piexif. - Apply EXIF orientation is disabled when the selected image has no EXIF orientation tag; enabled/disabled state is kept in sync with the selection.
- JPEG: uses
- Rotation undo — rotation operations are pushed to
_undo_stackas("rotation", [(path, undo_op, orig_orient)])and undone by the existing Undo button; the button label changes to Undo rotation (N). image_ops.py— new module:is_jpeg,get_exif_orientation,set_exif_orientation,_strip_jpeg_orientation,_find_jpegtran,_jpeg_apply,_pil_apply,_pil_save_lossless,rotate_lossless.piexifdependency — added topyproject.toml,environment.yml, and as a PyInstaller hidden import.jpegtranin PyInstaller bundle —pbpicat.specnow locatesjpegtranviashutil.whichat build time and includes it as a binary;_find_jpegtrancheckssys._MEIPASSfirst.tools/fix_po_dates.py— normalisesPOT-Creation-DateandPO-Revision-Dateheaders to eliminate spurious diffs between locales.tools/po_check.py— inspects.pofiles (statistics, untranslated entries, pattern search) without grepping or calling msgfmt (both break on multi-line entries).- i18n strings — translated rotation action labels and status messages in all eight locales (de, en, es, fr, it, ru, vi, zh_CN).
Changed
make translate— now passes--no-locationtopybabel extract(removes file/line references from.pot) and runstools/fix_po_dates.pyafterpybabel updateto keep date headers stable.- ImageViewer toolbar — rotation buttons (↺ ↻ ↕ EXIF) inserted between zoom controls and action buttons; Apply EXIF orientation button disabled when loaded image has no orientation tag.
- SPEC.md — updated to document lossless rotation actions, their enabled/disabled rules, and the revised ImageViewer toolbar layout.
- README.md — updated to document lossless rotation, Open/Open with actions, keyboard navigation, Renumber from 1, ImageViewer keyboard shortcuts, and the Settings menu restructuring (Catalog configuration vs. Program settings).
Fixed
- Row selection on click after scroll — clicking the Nth visible row (after scrolling) previously selected the Nth row from the top of the list instead of the actual clicked row. Root cause:
focusInEventcalledselectRow(0)before the click was processed, scrolling the viewport and shifting row coordinates. Fixed by skipping auto-select whenevent.reason() == MouseFocusReason.
v1.8.0
Added
- About dialog enriched — now shows Python version, PySide6 version, platform string, and author(s) as clickable
mailto:links read from package metadata. - Menu icons — all menu-bar actions (File, Catalog, Images, Settings, Help) now display icons; new SVG assets created for Quit, Duplicate, Refresh, Catalog configuration, History, Program settings, Keyboard shortcuts, and About.
- Context menu icons — the file-list context menu reuses the same
QActionobjects as the Images menu, ensuring identical icons, labels, shortcuts and enabled/disabled state at all times. ui/icons.py— sharedget_icon(svg_name, theme_name, text_fallback)helper that resolves icons via FreeDesktop theme → bundled SVG → text fallback; consolidates previously duplicated logic fromimage_viewer.py.- Keyboard shortcuts dialog — added missing entries: Ctrl+Z (Undo rename), Ctrl+O (Open), Ctrl+Shift+O (Open with), Ctrl+N (New catalog), Ctrl+Shift+D (Duplicate catalog); Del and Escape now rendered via
QKeySequence.toString(NativeText)for correct localisation. - Undo rename shortcut — Ctrl+Z activates the Undo rename button.
Changed
- Version read from package metadata —
importlib.metadatareplaces the hardcoded version string in__main__.py; the About dialog always shows the installed version. - Context menu selects clicked row — right-clicking an unselected row now selects it before the menu appears, so actions operate on the expected file.
Fixed
- PyInstaller bundle crash on startup —
emailwas listed inexcludesbut is required byimportlib.metadataat import time and byemail.utilsin the About dialog; removed from excludes. - Menu icons missing in PyInstaller bundle —
_RESOURCE_DIRinui/icons.pywas computed from__file__, which points inside the package directory in the bundle; now usessys._MEIPASSwhen available, matching the actual extraction path of bundled SVG assets.
v1.7.0
Added
- Images menu — new Images menu (between Catalog and Settings) with Open (Ctrl+O), Open with… (Ctrl+Shift+O), Template, Delete (Del), and Refresh (F5) actions.
- Open / Open with — Open launches the selected file(s) with the system default application; Open with… prompts for a specific application. Both actions are available in the file-list context menu and the image-viewer toolbar.
- Platform helpers — new
platform/package providing XDG/Linux (application picker dialog), macOS (open -a), and Windows (ShellExecuteW) implementations for Open with. - Image viewer toolbar extended — Open, Open with, Template, and Delete buttons added after the zoom controls.
- Confirm deletions setting — new catalog setting (default: on) that requires confirmation before deleting files; a configurable threshold controls whether individual file names are listed.
- Keyboard navigation — Left/Right arrows in the file list transfer focus to the directory tree; Right arrow on a tree leaf transfers focus back to the file list.
- Return/Enter opens file — pressing Return or Enter in the file list opens the image viewer (images) or the default external application (other media).
- Auto-select on focus — when the file list gains focus with no active selection, the first row is automatically selected.
- New keyboard shortcuts — Ctrl+, opens catalog settings; Ctrl+Alt+, opens program settings; both are also shown in the keyboard-shortcuts dialog.
- Status tips on all menu actions — every menu entry now has a descriptive status-bar tip.
- Duplicate catalog (Ctrl+Shift+D) — copies the current catalog's settings and field history to a new catalog name.
- Undo rename counter — the Undo rename button shows
N/total(renames pending / total done in the session), e.g. Undo rename 3/17. - Auto-select restored files after undo — after undoing a rename or renumber, the files moved back to their original location are automatically selected in the file list.
- Test suite — 551 tests across 14 modules at 98 % overall coverage; all non-UI modules and most UI modules at 100 %.
Changed
- "Refresh" moved to Images menu — the Refresh action (F5) is the last entry of the Images menu; the now-empty View menu is removed.
- Multi-selection in image viewer — selecting more than one file while the viewer is open shows an informational message instead of closing the viewer.
- Catalog configuration… renamed — the menu entry was previously labelled Configuration… under Settings.
config_dir()moved toplatform/— each platform module now exposesconfig_dir()returning the OS-native configuration directory ($XDG_CONFIG_HOME/pbpicaton Linux,~/Library/Application Support/pbpicaton macOS,%APPDATA%\pbpicaton Windows).config.pyno longer contains XDG-specific or OS-specific code.
Fixed
- Multi-selection lost after undo —
refresh_and_select_pathscancels the filesystem-watcher debounce before refreshing, and_refresh_preserve_selectionnow restores all previously selected rows instead of only the first. - Image viewer auto-updated on directory change — the viewer no longer changes image when the file list loads a new directory or gains focus (auto-select row 0); it updates only on explicit user selection.