diff --git a/ChangeLog.md b/ChangeLog.md index 6dd4fe4..35d3e33 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,7 +5,7 @@ Todo: More testing Linux / Mac. Create Windows .exe. Write better documentation ## 1.6.3 (2026-04-16) - Reworked quick auto-adjust and crop into one shared live edit session for the current image instead of saving on every keypress. -- `l` now runs quick auto-levels, `L` runs auto white balance plus auto-levels, `A` runs quick auto white balance, and `-` / `=` keep pushing highlights or shadows in 7-point steps inside that live session. +- `l` now runs quick auto-levels, `L` runs auto white balance plus auto-levels, `A` runs quick auto white balance, `-` lowers highlights/whites in 14-point steps, `_` raises whites in 14-point steps, and `=` keeps pushing shadows in 7-point steps inside that live session. - Crop and editor edits now keep accumulating in memory on the current image instead of forcing an immediate save or backup churn. - The live session is persisted once when you navigate away, start a drag, explicitly save, or quit, so preview stays responsive while drag-out and navigation still get the latest pixels. - Updated README/help text and kept the version at 1.6.3. diff --git a/README.md b/README.md index 4ee1ba1..c777b2f 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ This tool is optimized for speed, using `libjpeg-turbo` for decoding, aggressive - **Instant Navigation:** Sub-10ms next/previous image switching, high performance decoding via `PyTurboJPEG`. - **Image Editor:** Built-in editor with exposure, contrast, white balance, sharpness, and more (E key) - **Background Darkening:** Mask-based background darkening tool (K key) with smart edge detection, subject protection, and multiple modes. Paint rough background hints and the tool refines them into natural-looking dark backgrounds. -- **Quick Auto Adjust:** Press `l` for quick auto-levels, `L` for auto white balance + auto-levels together, `A` for auto white balance, `-` to keep darkening the highlight/white side in 7-point steps, and `=` to deepen the shadow side in 7-point steps. These update the live in-memory edit session immediately and save once when you navigate away, start a drag, or explicitly save. +- **Quick Auto Adjust:** Press `l` for quick auto-levels, `L` for auto white balance + auto-levels together, `A` for auto white balance, `-`/`_` to keep adjusting the highlight/white side in 14-point steps, and `=` to deepen the shadow side in 7-point steps. These update the live in-memory edit session immediately and save once when you navigate away, start a drag, or explicitly save. - **Photoshop / Gimp Integration:** Edit current image in Photoshop or Gimp (P key) - always uses RAW files when available. - **Clipboard Support:** Copy image path to clipboard (Ctrl+C) - **Image Filtering:** Filter images by filename @@ -142,7 +142,8 @@ If you do nothing, FastStack will still run, but JPEG decoding and thumbnail gen - `Ctrl+Shift+B`: Quick auto white balance (alternate) - `l`: Quick auto levels (live session; saved on navigation, drag, or Ctrl+S) - `L`: Quick auto white balance + auto levels (live session; saved on navigation, drag, or Ctrl+S) -- `-`: Darken the current auto-adjust highlights/whites by 7 points in the live session +- `-`: Darken the current auto-adjust highlights/whites by 14 points in the live session +- `_`: Raise the current auto-adjust whites by 14 points in the live session - `=`: Deepen the current auto-adjust shadows/background by 7 points in the live session - `E`: Toggle Image Editor - `Esc`: Close active dialog, editor, cancel crop, or exit fullscreen diff --git a/faststack/app.py b/faststack/app.py index f51a6e8..029d5c9 100644 --- a/faststack/app.py +++ b/faststack/app.py @@ -110,7 +110,7 @@ # LABEL: below this the direction word becomes "neutral" in the status message. _AWB_NOOP_EPS = 0.005 _AWB_LABEL_EPS = 0.002 -_AUTO_ADJUST_HIGHLIGHT_STEP = 0.07 +_AUTO_ADJUST_HIGHLIGHT_STEP = 0.14 _AUTO_ADJUST_BLACK_STEP = 0.07 @@ -2337,10 +2337,18 @@ def _format_auto_levels_detail( ) suffixes = [] + highlight_step_points = round(_AUTO_ADJUST_HIGHLIGHT_STEP * 100) if extra_highlight_steps > 0: - suffixes.append(f"highlights -{extra_highlight_steps * 7}pt") + suffixes.append( + f"highlights -{extra_highlight_steps * highlight_step_points}pt" + ) + elif extra_highlight_steps < 0: + suffixes.append( + f"whites +{-extra_highlight_steps * highlight_step_points}pt" + ) + black_step_points = round(_AUTO_ADJUST_BLACK_STEP * 100) if extra_black_steps > 0: - suffixes.append(f"blacks -{extra_black_steps * 7}pt") + suffixes.append(f"blacks -{extra_black_steps * black_step_points}pt") if suffixes: msg = f"{msg}; {', '.join(suffixes)}" return msg @@ -5136,6 +5144,21 @@ def get_batch_count_for_current_image(self) -> int: return 0 + @Slot(result=int) + def get_defined_batch_count(self) -> int: + """Return the total number of valid image indices in defined batches.""" + if not self.batches: + return 0 + + max_index = len(self.image_files) - 1 + total_count = 0 + for start, end in self.batches: + clamped_start = max(start, 0) + clamped_end = min(end, max_index) + if clamped_start <= clamped_end: + total_count += clamped_end - clamped_start + 1 + return total_count + @staticmethod def _move_to_recycle( src: Path, @@ -8812,6 +8835,16 @@ def reduce_auto_adjust_highlights(self): state.extra_highlight_steps += 1 self._apply_auto_adjust_preview(state) + @Slot() + def raise_auto_adjust_whites(self): + """Raise the white side by one fixed step in the live session.""" + state = self._ensure_or_seed_active_auto_adjust_state() + if state is None: + return + + state.extra_highlight_steps -= 1 + self._apply_auto_adjust_preview(state) + @Slot() def deepen_auto_adjust_blacks(self): """Deepen the shadow side by one fixed step in the live session.""" diff --git a/faststack/qml/Main.qml b/faststack/qml/Main.qml index ee95120..1e5d393 100644 --- a/faststack/qml/Main.qml +++ b/faststack/qml/Main.qml @@ -19,6 +19,7 @@ ApplicationWindow { property var uiStateRef: null property var controllerRef: null property bool allowCloseWithRecycleBins: false + property bool allowCloseWithBatches: false property bool fullScreenLoupe: false property var savedWindowGeometry: ({}) @@ -74,6 +75,16 @@ ApplicationWindow { return } + if (!root.allowCloseWithBatches && root.controllerRef) { + var definedBatchCount = root.controllerRef.get_defined_batch_count() + if (definedBatchCount > 0) { + close.accepted = false + quitBatchesDialog.batchCount = definedBatchCount + quitBatchesDialog.open() + return + } + } + if (root.controllerRef && !root.controllerRef.prepare_for_app_close()) { close.accepted = false return @@ -1706,6 +1717,7 @@ ApplicationWindow { "  l: Quick auto levels (live)
" + "  L: Quick auto white balance + auto levels (live)
" + "  -: Darken current auto-adjust highlights/whites (live)
" + + "  _: Raise current auto-adjust whites (live)
" + "  =: Deepen current auto-adjust shadows/background (live)
" + "  K: Background Darkening Tool
" + "  O (or right-click): Toggle crop mode
" + @@ -1773,6 +1785,25 @@ ApplicationWindow { backgroundColor: root.currentBackgroundColor textColor: root.currentTextColor } + + QuitBatchesDialog { + id: quitBatchesDialog + backgroundColor: root.currentBackgroundColor + textColor: root.currentTextColor + darkTheme: root.isDarkTheme + frameBorderColor: root.isDarkTheme ? "#404040" : "#d0d0d0" + cancelBgColor: root.isDarkTheme ? "#444444" : "#f0f0f0" + cancelHoverColor: root.isDarkTheme ? "#666666" : "#e0e0e0" + cancelPressedColor: root.isDarkTheme ? "#555555" : "#d0d0d0" + quitBgColor: root.isDarkTheme ? "#aa0000" : "#c62828" + quitHoverColor: root.isDarkTheme ? "#ff0000" : "#d32f2f" + quitPressedColor: root.isDarkTheme ? "#cc0000" : "#b71c1c" + controllerRef: root.controllerRef + onQuitConfirmed: { + root.allowCloseWithBatches = true + Qt.quit() + } + } HistogramWindow { id: histogramWindow diff --git a/faststack/qml/QuitBatchesDialog.qml b/faststack/qml/QuitBatchesDialog.qml new file mode 100644 index 0000000..027f3af --- /dev/null +++ b/faststack/qml/QuitBatchesDialog.qml @@ -0,0 +1,109 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +Dialog { + id: quitBatchesDialog + title: "Quit with Batches?" + modal: true + standardButtons: Dialog.NoButton + closePolicy: Popup.CloseOnEscape + width: Math.min(maxDialogWidth, parent ? parent.width * 0.9 : maxDialogWidth) + implicitHeight: quitDialogContent.implicitHeight + + property int batchCount: 0 + property int maxDialogWidth: 450 + property bool darkTheme: true + property color backgroundColor: "#1e1e1e" + property color textColor: "white" + property color frameBorderColor: darkTheme ? "#404040" : "#d0d0d0" + property color cancelBgColor: darkTheme ? "#444444" : "#f0f0f0" + property color cancelHoverColor: darkTheme ? "#666666" : "#e0e0e0" + property color cancelPressedColor: darkTheme ? "#555555" : "#d0d0d0" + property color quitBgColor: "#aa0000" + property color quitHoverColor: "#ff0000" + property color quitPressedColor: "#cc0000" + property var controllerRef: null + signal quitConfirmed() + + background: Rectangle { + color: quitBatchesDialog.backgroundColor + border.color: quitBatchesDialog.frameBorderColor + border.width: 1 + radius: 4 + } + + contentItem: Column { + id: quitDialogContent + spacing: 20 + padding: 20 + + Label { + text: `You have ${quitBatchesDialog.batchCount} image${quitBatchesDialog.batchCount === 1 ? '' : 's'} selected in batches.` + wrapMode: Text.WordWrap + width: parent.width - parent.padding * 2 + color: quitBatchesDialog.textColor + font.pixelSize: 14 + } + + Label { + text: "Batches are not saved after FastStack quits. Quit anyway?" + wrapMode: Text.WordWrap + width: parent.width - parent.padding * 2 + color: quitBatchesDialog.textColor + font.pixelSize: 14 + } + + Row { + spacing: 10 + anchors.horizontalCenter: parent.horizontalCenter + + Button { + id: cancelQuitButton + text: "Cancel" + onClicked: quitBatchesDialog.close() + background: Rectangle { + color: cancelQuitButton.down ? quitBatchesDialog.cancelPressedColor : (cancelQuitButton.hovered ? quitBatchesDialog.cancelHoverColor : quitBatchesDialog.cancelBgColor) + radius: 4 + } + contentItem: Text { + text: cancelQuitButton.text + color: quitBatchesDialog.textColor + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + + Button { + id: quitAnywayButton + text: "Quit Anyway" + onClicked: { + quitBatchesDialog.close() + quitBatchesDialog.quitConfirmed() + } + background: Rectangle { + color: quitAnywayButton.down ? quitBatchesDialog.quitPressedColor : (quitAnywayButton.hovered ? quitBatchesDialog.quitHoverColor : quitBatchesDialog.quitBgColor) + radius: 4 + } + contentItem: Text { + text: quitAnywayButton.text + color: quitBatchesDialog.textColor + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.bold: true + } + } + } + } + + onOpened: { + if (quitBatchesDialog.controllerRef) { + quitBatchesDialog.controllerRef.dialog_opened() + } + } + + onClosed: { + if (quitBatchesDialog.controllerRef) { + quitBatchesDialog.controllerRef.dialog_closed() + } + } +} diff --git a/faststack/ui/keystrokes.py b/faststack/ui/keystrokes.py index 5822ebb..61c57e6 100644 --- a/faststack/ui/keystrokes.py +++ b/faststack/ui/keystrokes.py @@ -121,12 +121,18 @@ def handle_key_press(self, event): if text == "-": self._call("reduce_auto_adjust_highlights") return True + if text == "_": + self._call("raise_auto_adjust_whites") + return True if text == "=": self._call("deepen_auto_adjust_blacks") return True if key == Qt.Key_Minus and modifiers == Qt.NoModifier: self._call("reduce_auto_adjust_highlights") return True + if key == Qt.Key_Underscore: + self._call("raise_auto_adjust_whites") + return True if key == Qt.Key_Equal and modifiers == Qt.NoModifier: self._call("deepen_auto_adjust_blacks") return True