From 242d1b01c7f7a234e6590fab841cfaf9b984058f Mon Sep 17 00:00:00 2001 From: Marcin Zawalski Date: Sat, 11 Jul 2026 15:54:42 +0200 Subject: [PATCH 1/4] feat: bindable shortcuts for new tone sliders Snap, Shadows/Highlights Density, and Shadows/Highlights Grade sliders had no entries in the shortcut editor. Register them (empty defaults, users assign their own keys), wire the adjust handlers, and add tooltip keycap chips like the other bound sliders. --- negpy/desktop/view/keyboard_shortcuts.py | 10 ++++++ negpy/desktop/view/shortcut_registry.py | 10 ++++++ negpy/desktop/view/sidebar/controls_panel.py | 35 ++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/negpy/desktop/view/keyboard_shortcuts.py b/negpy/desktop/view/keyboard_shortcuts.py index d351094f..35785f39 100644 --- a/negpy/desktop/view/keyboard_shortcuts.py +++ b/negpy/desktop/view/keyboard_shortcuts.py @@ -96,6 +96,16 @@ def _build_actions(self) -> dict[str, Callable[[], None]]: "shoulder_dec": (lambda: controls.tone_sidebar.sh_slider, -1.0), "shoulder_width_inc": (lambda: controls.tone_sidebar.sh_w_slider, 1.0), "shoulder_width_dec": (lambda: controls.tone_sidebar.sh_w_slider, -1.0), + "snap_inc": (lambda: controls.tone_sidebar.midtone_gamma_slider, 1.0), + "snap_dec": (lambda: controls.tone_sidebar.midtone_gamma_slider, -1.0), + "shadow_density_inc": (lambda: controls.tone_sidebar.shadow_density_slider, 1.0), + "shadow_density_dec": (lambda: controls.tone_sidebar.shadow_density_slider, -1.0), + "highlight_density_inc": (lambda: controls.tone_sidebar.highlight_density_slider, 1.0), + "highlight_density_dec": (lambda: controls.tone_sidebar.highlight_density_slider, -1.0), + "shadow_grade_inc": (lambda: controls.tone_sidebar.shadow_grade_slider, 1.0), + "shadow_grade_dec": (lambda: controls.tone_sidebar.shadow_grade_slider, -1.0), + "highlight_grade_inc": (lambda: controls.tone_sidebar.highlight_grade_slider, 1.0), + "highlight_grade_dec": (lambda: controls.tone_sidebar.highlight_grade_slider, -1.0), "offset_inc": (lambda: controls.geometry_sidebar.offset_slider, 1.0), "offset_dec": (lambda: controls.geometry_sidebar.offset_slider, -1.0), "fine_rot_inc": (lambda: controls.geometry_sidebar.fine_rot_slider, 1.0), diff --git a/negpy/desktop/view/shortcut_registry.py b/negpy/desktop/view/shortcut_registry.py index 0e3cf50c..b3b5319a 100644 --- a/negpy/desktop/view/shortcut_registry.py +++ b/negpy/desktop/view/shortcut_registry.py @@ -48,6 +48,16 @@ class ShortcutEntry: "shoulder_inc": ShortcutEntry("Alt+U", "Shoulder up", "Exposure"), "shoulder_width_dec": ShortcutEntry("Alt+Shift+I", "Shoulder width down", "Exposure"), "shoulder_width_inc": ShortcutEntry("Alt+I", "Shoulder width up", "Exposure"), + "snap_dec": ShortcutEntry("", "Snap (midtone) down", "Exposure"), + "snap_inc": ShortcutEntry("", "Snap (midtone) up", "Exposure"), + "shadow_density_dec": ShortcutEntry("", "Shadows density down", "Exposure"), + "shadow_density_inc": ShortcutEntry("", "Shadows density up", "Exposure"), + "highlight_density_dec": ShortcutEntry("", "Highlights density down", "Exposure"), + "highlight_density_inc": ShortcutEntry("", "Highlights density up", "Exposure"), + "shadow_grade_dec": ShortcutEntry("", "Shadows grade down", "Exposure"), + "shadow_grade_inc": ShortcutEntry("", "Shadows grade up", "Exposure"), + "highlight_grade_dec": ShortcutEntry("", "Highlights grade down", "Exposure"), + "highlight_grade_inc": ShortcutEntry("", "Highlights grade up", "Exposure"), "lock_bounds_toggle": ShortcutEntry("Alt+Q", "Toggle bounds lock", "Process"), "analysis_buffer_dec": ShortcutEntry("Alt+Shift+B", "Analysis buffer down", "Process"), "analysis_buffer_inc": ShortcutEntry("Alt+B", "Analysis buffer up", "Process"), diff --git a/negpy/desktop/view/sidebar/controls_panel.py b/negpy/desktop/view/sidebar/controls_panel.py index 9986bc5f..a0342f7c 100644 --- a/negpy/desktop/view/sidebar/controls_panel.py +++ b/negpy/desktop/view/sidebar/controls_panel.py @@ -389,6 +389,41 @@ def apply_shortcut_tooltips(self) -> None: ["shoulder_width_inc", "shoulder_width_dec"], ) ) + exp.midtone_gamma_slider.setToolTip( + tooltip_with_shortcut( + "Snap — paper midtone gamma trim: steepens or flattens the S-curve around the reference " + "tone; paper white/black stay put. In R/G/B mode: this layer's Snap trim", + ["snap_inc", "snap_dec"], + ) + ) + exp.shadow_density_slider.setToolTip( + tooltip_with_shortcut( + "Shadow zone density (ΔD): weighted to the deep shadows, bounded by paper black. " + "Positive darkens shadows; negative lifts them", + ["shadow_density_inc", "shadow_density_dec"], + ) + ) + exp.highlight_density_slider.setToolTip( + tooltip_with_shortcut( + "Highlight zone density (ΔD): weighted to the highlights, bounded by paper white. " + "Positive burns highlights in; negative bleaches them", + ["highlight_density_inc", "highlight_density_dec"], + ) + ) + exp.shadow_grade_slider.setToolTip( + tooltip_with_shortcut( + "Split grade — shadow zone contrast trim (ISO-R): rotates the curve locally in the deep " + "shadows. In R/G/B mode: this layer's shadow-grade trim", + ["shadow_grade_inc", "shadow_grade_dec"], + ) + ) + exp.highlight_grade_slider.setToolTip( + tooltip_with_shortcut( + "Split grade — highlight zone contrast trim (ISO-R): rotates the curve locally in the " + "highlights. In R/G/B mode: this layer's highlight-grade trim", + ["highlight_grade_inc", "highlight_grade_dec"], + ) + ) geo.manual_crop_btn.setToolTip( tooltip_with_shortcut( From 0ec4a3353ffc14a7872d1f46afea4f80796b2cd5 Mon Sep 17 00:00:00 2001 From: Marcin Zawalski Date: Sat, 11 Jul 2026 15:54:42 +0200 Subject: [PATCH 2/4] docs: README camera scanning + metadata/gear library Add Camera Scanning and Metadata & Gear Library feature bullets linking their docs, plus optional libgphoto2 install notes for macOS/Linux. --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index a012148b..efa186da 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ It is built with **Python**, running natively on Linux, macOS, and Windows. * **Positive/Slide Support**: Dedicated **E-6 mode** with optional normalization to save expired or faded film. **Capture & Input** +* **Camera Scanning**: Capture negatives with a tethered camera straight into NegPy — a single RAW, or automated red/green/blue narrowband triplets driven by an RGB [Scanlight](https://github.com/jackw01/scanlight) that feed the RGB Scan merge. macOS/Linux, optional dependency. [Camera Scanning guide](docs/CAMERA_SCANNING.md) * **Scanner Support**: Direct control of SANE-compatible scanners. [supported devices](http://www.sane-project.org/sane-supported-devices.html) * **RGB Scan (Trichromatic Capture)**: Merge three narrowband red/green/blue exposures of one negative into a single low-noise colour scan, with automatic sub-pixel alignment to kill fringing. * **Flat-Field Correction**: Correct illumination falloff / vignetting from your light source or scanner via a reference scan of the bare light. Named profiles, toggle per image. @@ -47,6 +48,7 @@ It is built with **Python**, running natively on Linux, macOS, and Windows. * **Non-destructive**: Original files never touched; edits stored as recipes. * **Database**: Edits in a local SQLite db keyed by file hash — move or rename files without losing work. * **Persistent Undo/Redo & History**: Up to 100 edits per file. **History panel** lists every step — jump to any state, branch, or export an earlier version. Survives restarts. +* **Metadata & Gear Library**: Archival metadata for the original analog capture — manage a library of cameras, lenses, and film stocks, apply gear presets per frame, and write real camera/lens/ISO EXIF (plus XMP scan tags) into exports so Lightroom shows your film gear. [see the guide](docs/USER_GUIDE.md#11-metadata-panel) * **Keyboard Shortcuts**: [see here](docs/KEYBOARD.md) --- @@ -72,6 +74,12 @@ sudo pacman -S sane # Arch ``` Or your distro's equivalent. The app launches fine without so you can ignore that if you don't plan to use a scanner. +**Camera scanning support** (optional) uses `python-gphoto2` for tethered capture, and may need the system `libgphoto2` installed: +``` +sudo pacman -S libgphoto2 # Arch +``` +Or look up your distro's equivalent package. Building from source, add the Python dependency with `uv sync --group camera`. See the [Camera Scanning guide](docs/CAMERA_SCANNING.md) for setup and supported cameras. Not available on Windows (libgphoto2 has no Windows build). + You can also clone the repo and build it yourself, instruction here: [CONTRIBUTING.md](CONTRIBUTING.md) #### Unsigned Software Warning @@ -88,10 +96,20 @@ brew install sane-backends ``` The app launches fine without so you can ignore that if you don't plan to use a scanner. +**Camera scanning support** (optional) uses `python-gphoto2`, and may need `libgphoto2` from [Homebrew](https://brew.sh/): +``` +brew install libgphoto2 +``` +Building from source, add the Python dependency with `uv sync --group camera` — see the [Camera Scanning guide](docs/CAMERA_SCANNING.md). Quit **Preview / Photos / Image Capture** before scanning so macOS releases the camera. + **Windows**: 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 (they're happy on macOS too, not only Linux). 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. + +Good news: you can install Linux on pretty much any Windows machine. 🐧 + --- ## Data Location From 6d8a57fbcf209521974dd28efa1af99860c03860 Mon Sep 17 00:00:00 2001 From: Marcin Zawalski Date: Sat, 11 Jul 2026 16:08:48 +0200 Subject: [PATCH 3/4] update README.md --- README.md | 5 ++--- negpy/desktop/view/sidebar/export.py | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index efa186da..cd1941bd 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ Or your distro's equivalent. The app launches fine without so you can ignore tha ``` sudo pacman -S libgphoto2 # Arch ``` -Or look up your distro's equivalent package. Building from source, add the Python dependency with `uv sync --group camera`. See the [Camera Scanning guide](docs/CAMERA_SCANNING.md) for setup and supported cameras. Not available on Windows (libgphoto2 has no Windows build). +Or look up your distro's equivalent package. You can also clone the repo and build it yourself, instruction here: [CONTRIBUTING.md](CONTRIBUTING.md) @@ -100,13 +100,12 @@ The app launches fine without so you can ignore that if you don't plan to use a ``` brew install libgphoto2 ``` -Building from source, add the Python dependency with `uv sync --group camera` — see the [Camera Scanning guide](docs/CAMERA_SCANNING.md). Quit **Preview / Photos / Image Capture** before scanning so macOS releases the camera. **Windows**: 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 (they're happy on macOS too, not only Linux). 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 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. Good news: you can install Linux on pretty much any Windows machine. 🐧 diff --git a/negpy/desktop/view/sidebar/export.py b/negpy/desktop/view/sidebar/export.py index 7835f16a..0e8961e1 100644 --- a/negpy/desktop/view/sidebar/export.py +++ b/negpy/desktop/view/sidebar/export.py @@ -45,7 +45,6 @@ class ExportSidebar(BaseSidebar): SIDE_MARGIN = THEME.space_xl def _init_ui(self) -> None: - self.update_timer = QTimer() self.update_timer.setSingleShot(True) self.update_timer.setInterval(500) From 5e61807a28a2042d2d6d8ebba1d54de2e7ff8fbf Mon Sep 17 00:00:00 2001 From: Marcin Zawalski Date: Sat, 11 Jul 2026 16:11:28 +0200 Subject: [PATCH 4/4] update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cd1941bd..90854840 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,6 @@ sudo pacman -S libgphoto2 # Arch ``` Or look up your distro's equivalent package. -You can also clone the repo and build it yourself, instruction here: [CONTRIBUTING.md](CONTRIBUTING.md) #### Unsigned Software Warning Since this is a free hobby project, I don't pay Apple or Microsoft ransom for their developer certificates. You'll get a scary warning the first time you run it. @@ -111,6 +110,10 @@ Good news: you can install Linux on pretty much any Windows machine. 🐧 --- +You can also clone the repo and build it yourself, instruction here: [CONTRIBUTING.md](CONTRIBUTING.md) + +--- + ## Data Location Everything lives in your `Documents/NegPy` folder: * `edits.db`: Your edits.