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
2 changes: 1 addition & 1 deletion ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
39 changes: 36 additions & 3 deletions faststack/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@staticmethod
def _move_to_recycle(
src: Path,
Expand Down Expand Up @@ -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."""
Expand Down
31 changes: 31 additions & 0 deletions faststack/qml/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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: ({})

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1706,6 +1717,7 @@ ApplicationWindow {
"&nbsp;&nbsp;l: Quick auto levels (live)<br>" +
"&nbsp;&nbsp;L: Quick auto white balance + auto levels (live)<br>" +
"&nbsp;&nbsp;-: Darken current auto-adjust highlights/whites (live)<br>" +
"&nbsp;&nbsp;_: Raise current auto-adjust whites (live)<br>" +
"&nbsp;&nbsp;=: Deepen current auto-adjust shadows/background (live)<br>" +
"&nbsp;&nbsp;K: Background Darkening Tool<br>" +
"&nbsp;&nbsp;O (or right-click): Toggle crop mode<br>" +
Expand Down Expand Up @@ -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
Expand Down
109 changes: 109 additions & 0 deletions faststack/qml/QuitBatchesDialog.qml
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
6 changes: 6 additions & 0 deletions faststack/ui/keystrokes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading