diff --git a/README.md b/README.md index 90854840..d6782007 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ It is built with **Python**, running natively on Linux, macOS, and Windows. --- -![alt text](docs/media/0350.png) +![alt text](docs/media/0360.png) --- @@ -104,7 +104,7 @@ brew install libgphoto2 1. Run the installer (ignore the warnings) 2. Start the app and click through the warnings. -Scanner and camera scanning are **not available on Windows**. Both ride on Unix-first free-software libraries — SANE for scanners, libgphoto2 for cameras — that just don't build there. It's not really their fault: the open world spent decades writing generic, vendor-neutral drivers for hundreds of devices, while Windows stuck with closed per-vendor blobs and never grew an equivalent. So the free stack NegPy leans on has nowhere to stand on Windows. Everything else works fine. +Scanner and camera scanning are **not available on Windows**. Both rely on Unix-first free-software libraries - SANE for scanners, libgphoto2 for cameras, that just don't build there. It's not really their fault: the open source world spent decades writing generic, vendor-neutral drivers for hundreds of devices, while Windows stuck with closed per-vendor blobs and never grew an equivalent. So the free, open stack NegPy leans on has nowhere to stand on Windows. Good news: you can install Linux on pretty much any Windows machine. 🐧 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index de61c2b3..2b070f72 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -11,17 +11,16 @@ - New: **Crop composition guides** — the crop tool's fixed thirds grid becomes a **Guide** dropdown: Thirds, Phi Grid, Diagonals, Golden Triangles, Golden Spiral, Armature, Diagonal Method or Grid. `O` cycles guides, `Shift+O` rotates the spiral/triangles. - New: **Crop rotation handles** — four handles at the edges of the crop box spin the frame freehand, composing with the Fine Rotation slider for fine-tuning; both now range ±45°. @linkmodo - New: **Reverse scroll-to-zoom** — an optional toggle in Customize Shortcuts for users who expect scroll-up to zoom out. @linkmodo +- Change: **Auto Cast is built in** — the toggle is gone: Cast Removal always adapts its strength to how confidently the frame's neutral greys read, and the slider (default 0.5) trims on top; 0 = off. Frames that had Auto Cast off will shift slightly. - Change: **One visual language across the app** — all panels share the same section cards, button styles and sizes; every active tool and toggle uses the same red armed state; edited sliders, selectors and tabs are marked with a small red dot instead of coloured text; tooltips word-wrap; the Metadata page uses collapsible sections like the rest; the Analysis stats read-out shows plain values without the qualitative labels. - Change: **UI polish pass** — tooltip shortcut chips render as bordered keycaps; hovered items in menus, dropdowns and combo boxes highlight clearly; active/checked tools (Linear RAW, Lock Bounds) now read as engaged; Undo/Redo move to the toolbar (Save Edits into the More Actions menu); the crop tool shows contextual hover cursors (rotate / resize / move / draw); and confirmation dialogs default to the affirmative button on Enter. @linkmodo - Change: **Confirm before unloading** — Unload / Unload Selected / Clear All now prompt first, and the Delete key unloads the selected frame(s). @linkmodo -- Change: **Consistent sidebar spacing** — section headers and the fields below them share one left edge on every tab; export/metadata field text no longer sits flush against the card border. -- Change: **Colour page redesigned to match the Tone page** — full-width **Global / Shadows / Highlights** buttons on top; Temperature, the WB picker, the roll lock and a new reset button all follow the selected region. -- Change: **Auto Cast is built in** — the toggle is gone: Cast Removal always adapts its strength to how confidently the frame's neutral greys read, and the slider (default 0.5) trims on top; 0 = off. Frames that had Auto Cast off will shift slightly. - Removed: **Flare** and **Contrast Lift** — Snap and the zone density sliders cover the same ground with real control. Old edits load cleanly and render without them. - Fix: **flip under Fine Rotation now mirrors correctly** — flipping a straightened frame produced a doubled tilt instead of a true mirror of the current render. @linkmodo - Fix: **manual crop and freehand analysis region rotate with 90/180 turns** — they stayed put before, so a quarter-turn left the crop framing the wrong area. @linkmodo - Fix: **viewer clears when the session empties** — Clear All or Unloading the last frame left the previous image on screen with no way to dismiss it. @linkmodo -- Fix: **startup no longer crashes when the Documents folder is missing** — a OneDrive-backed Documents path that doesn't exist on disk (OneDrive unlinked or signed out) now falls back to `~/Documents` and then the home directory instead of failing to launch. @linkmodo +- Fix: **startup no longer crashes when the Documents folder is missing (windows)** — a OneDrive-backed Documents path that doesn't exist on disk (OneDrive unlinked or signed out) now falls back to `~/Documents` and then the home directory instead of failing to launch. @linkmodo +- Fix: **Apply Settings counts only the frames you can see** — with a filename filter active, "Whole roll" counted (and would apply to) every loaded file instead of just the visible ones. It now matches the filtered list. ## 0.35.0 diff --git a/docs/media/0350.png b/docs/media/0350.png deleted file mode 100644 index a3325375..00000000 Binary files a/docs/media/0350.png and /dev/null differ diff --git a/docs/media/0360.png b/docs/media/0360.png new file mode 100644 index 00000000..e1b6b7d5 Binary files /dev/null and b/docs/media/0360.png differ diff --git a/negpy/desktop/session.py b/negpy/desktop/session.py index 52aa6194..f2ae0ace 100644 --- a/negpy/desktop/session.py +++ b/negpy/desktop/session.py @@ -760,7 +760,7 @@ def sync_selected_settings(self, aspects: frozenset, scope: str = "selection") - self.settings_synced.emit("Render the source frame before syncing bounds") return 0 - target_indices = range(len(self.state.uploaded_files)) if scope == "roll" else self.state.selected_indices + target_indices = self.asset_model.visible_actual_indices_ordered() if scope == "roll" else self.state.selected_indices count = 0 for idx in target_indices: diff --git a/negpy/desktop/view/sidebar/files.py b/negpy/desktop/view/sidebar/files.py index 12616c0a..0b75194a 100644 --- a/negpy/desktop/view/sidebar/files.py +++ b/negpy/desktop/view/sidebar/files.py @@ -529,12 +529,14 @@ def _source_name(self) -> str: def _open_apply_dialog(self) -> None: state = self.session.state - n_files = len(state.uploaded_files) src = state.selected_file_idx if src == -1: return - sel_targets = len([i for i in set(state.selected_indices) if i != src and 0 <= i < n_files]) - roll_targets = max(0, n_files - 1) + # "Whole roll" means the visible (filtered) frames, not every loaded file — + # a filename filter is a non-destructive view, so hidden files aren't counted. + visible = self.session.asset_model.visible_actual_indices() + sel_targets = len([i for i in set(state.selected_indices) if i != src and i in visible]) + roll_targets = len([i for i in visible if i != src]) dlg = SyncSettingsDialog(self, self._source_name(), sel_targets, roll_targets) if dlg.exec() == QDialog.DialogCode.Accepted: diff --git a/tests/test_desktop_session.py b/tests/test_desktop_session.py index 7a182f58..6e615f18 100644 --- a/tests/test_desktop_session.py +++ b/tests/test_desktop_session.py @@ -325,6 +325,38 @@ def test_sync_selected_settings_invalid_aspect_is_noop(self): self.session.sync_selected_settings(frozenset({"bogus"})) self.mock_repo.save_file_settings.assert_not_called() + def _seed_roll(self): + self.session.state.uploaded_files = [ + {"name": "a.arw", "path": "pa", "hash": "hash1"}, + {"name": "b.arw", "path": "pb", "hash": "hash2"}, + {"name": "c.jpg", "path": "pc", "hash": "hash3"}, + ] + self.session.state.selected_file_idx = 0 + self.session.state.current_file_hash = "hash1" + self.session.state.config = WorkspaceConfig() + self.mock_repo.load_file_settings.return_value = WorkspaceConfig() + + def test_sync_roll_scope_respects_active_filter(self): + # A filename filter is a view; "whole roll" applies only to visible frames. + self._seed_roll() + self.session.asset_model.set_filter(".arw", regex=False) # hides c.jpg + + count = self.session.sync_selected_settings(frozenset({"exposure"}), scope="roll") + + saved = {c.args[0] for c in self.mock_repo.save_file_settings.call_args_list} + self.assertEqual(count, 1) + self.assertEqual(saved, {"hash2"}) # source + filtered-out c.jpg excluded + + def test_sync_roll_scope_no_filter_covers_all(self): + self._seed_roll() + self.session.asset_model.refresh() # no filter → every frame visible + + count = self.session.sync_selected_settings(frozenset({"exposure"}), scope="roll") + + saved = {c.args[0] for c in self.mock_repo.save_file_settings.call_args_list} + self.assertEqual(count, 2) + self.assertEqual(saved, {"hash2", "hash3"}) + def test_undo_redo_persistence(self): self.session.select_file(0) initial_config = self.session.state.config