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