Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

---

Expand Down Expand Up @@ -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 camerasthat 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. 🐧

Expand Down
7 changes: 3 additions & 4 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Binary file removed docs/media/0350.png
Binary file not shown.
Binary file added docs/media/0360.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion negpy/desktop/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 5 additions & 3 deletions negpy/desktop/view/sidebar/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
32 changes: 32 additions & 0 deletions tests/test_desktop_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading