From b8b05dbd7e118f93b97f7e4c3617d8a7b1b265ce Mon Sep 17 00:00:00 2001 From: AlanRockefeller Date: Sat, 22 Nov 2025 22:13:27 -0500 Subject: [PATCH 1/2] Release v1.2 - fixed menus --- faststack/README.md | 2 +- faststack/faststack/app.py | 280 +++++++++++---- faststack/faststack/config.py | 9 + faststack/faststack/faststack.json | 6 + faststack/faststack/imaging/editor.py | 41 ++- faststack/faststack/imaging/jpeg.py | 15 +- faststack/faststack/qml/FilterDialog.qml | 13 +- faststack/faststack/qml/ImageEditorDialog.qml | 23 +- .../faststack/qml/ImageEditorDialog.qml.old | 196 ---------- faststack/faststack/qml/JumpToImageDialog.qml | 15 +- faststack/faststack/qml/Main.qml | 44 ++- faststack/faststack/qml/SettingsDialog.qml | 340 ++++++++++++------ faststack/faststack/ui/provider.py | 75 ++++ 13 files changed, 648 insertions(+), 411 deletions(-) create mode 100644 faststack/faststack/faststack.json delete mode 100644 faststack/faststack/qml/ImageEditorDialog.qml.old diff --git a/faststack/README.md b/faststack/README.md index c90741e..d2bceaf 100644 --- a/faststack/README.md +++ b/faststack/README.md @@ -9,7 +9,7 @@ This tool is optimized for speed, using `libjpeg-turbo` for decoding, aggressive ## Features -- **Instant Navigation:** Sub-10ms next/previous image switching, high-peformance decoding via `PyTurboJPEG`. +- **Instant Navigation:** Sub-10ms next/previous image switching, high performance decoding via `PyTurboJPEG`. - **Zoom & Pan:** Smooth zooming and panning. - **Stack Selection:** Group images into stacks (`[`, `]`) and select them for processing (`S`). - **Helicon Focus Integration:** Launch Helicon Focus with your selected RAW files with a single keypress (`Enter`). diff --git a/faststack/faststack/app.py b/faststack/faststack/app.py index cdf92fe..ec9cbb5 100644 --- a/faststack/faststack/app.py +++ b/faststack/faststack/app.py @@ -7,7 +7,7 @@ import time import argparse from pathlib import Path -from typing import Optional, List, Dict, Any +from typing import Optional, List, Dict, Any, Tuple from datetime import date import os import concurrent.futures @@ -29,6 +29,8 @@ ) from PySide6.QtWidgets import QApplication, QFileDialog, QMessageBox from PySide6.QtQml import QQmlApplicationEngine +from PIL import Image +Image.MAX_IMAGE_PIXELS = None # ⬇️ these are the ones that went missing from faststack.config import config @@ -615,13 +617,7 @@ def end_current_batch(self): def clear_all_batches(self): """Clear all defined batches.""" - log.info("Clearing all defined batches.") - self.batches = [] - self.batch_start_index = None - self._metadata_cache_index = (-1, -1) # Invalidate cache - self.dataChanged.emit() - self.sync_ui_state() - self.update_status_message("All batches cleared") + self.clear_all_stacks() def remove_from_batch_or_stack(self): """Remove current image from any batch or stack it's in.""" @@ -957,15 +953,20 @@ def _delete_temp_file(self, tmp_path: Path): log.error("Error deleting temporary file %s: %s", tmp_path, e) def clear_all_stacks(self): - log.info("Clearing all defined stacks and stack start marker.") + log.info("Clearing all defined stacks, batches, and markers.") self.stacks = [] - self.stack_start_index = None # Clear the stack start marker too + self.stack_start_index = None + self.batches = [] + self.batch_start_index = None + self.sidecar.data.stacks = self.stacks self.sidecar.save() - self._metadata_cache_index = (-1, -1) # Invalidate cache - self.dataChanged.emit() # Notify QML of data change - self.ui_state.stackSummaryChanged.emit() # Update stack summary in dialog + + self._metadata_cache_index = (-1, -1) + self.dataChanged.emit() + self.ui_state.stackSummaryChanged.emit() self.sync_ui_state() + self.update_status_message("All stacks and batches cleared") def get_helicon_path(self): return config.get('helicon', 'exe') @@ -1092,10 +1093,76 @@ def set_saturation_factor(self, factor: float): def get_default_directory(self): return config.get('core', 'default_directory') + @Slot(result=str) + def get_awb_mode(self): + return config.get("awb", "mode") + + @Slot(str) + def set_awb_mode(self, mode): + config.set("awb", "mode", mode) + config.save() + + @Slot(result=float) + def get_awb_strength(self): + return config.getfloat("awb", "strength") + + @Slot(float) + def set_awb_strength(self, value): + config.set("awb", "strength", value) + config.save() + + @Slot(result=int) + def get_awb_warm_bias(self): + return config.getint("awb", "warm_bias") + + @Slot(int) + def set_awb_warm_bias(self, value): + config.set("awb", "warm_bias", value) + config.save() + + @Slot(result=int) + def get_awb_luma_lower_bound(self): + return config.getint("awb", "luma_lower_bound") + + @Slot(int) + def set_awb_luma_lower_bound(self, value): + config.set("awb", "luma_lower_bound", value) + config.save() + + @Slot(result=int) + def get_awb_luma_upper_bound(self): + return config.getint("awb", "luma_upper_bound") + + @Slot(int) + def set_awb_luma_upper_bound(self, value): + config.set("awb", "luma_upper_bound", value) + config.save() + + @Slot(result=int) + def get_awb_rgb_lower_bound(self): + return config.getint("awb", "rgb_lower_bound") + + @Slot(int) + def set_awb_rgb_lower_bound(self, value): + config.set("awb", "rgb_lower_bound", value) + config.save() + + @Slot(result=int) + def get_awb_rgb_upper_bound(self): + return config.getint("awb", "rgb_upper_bound") + + @Slot(int) + def set_awb_rgb_upper_bound(self, value): + config.set("awb", "rgb_upper_bound", value) + config.save() + + def get_default_directory(self): + return config.get('core', 'default_directory') + def set_default_directory(self, path): config.set('core', 'default_directory', path) config.save() - + def open_directory_dialog(self): dialog = QFileDialog() dialog.setFileMode(QFileDialog.FileMode.Directory) @@ -1103,6 +1170,15 @@ def open_directory_dialog(self): return dialog.selectedFiles()[0] return "" + @Slot() + def open_folder(self): + """Opens a directory dialog and reloads the application with the selected folder.""" + path = self.open_directory_dialog() + if path: + self.image_dir = Path(path) + self.load() + + def preload_all_images(self): if self.ui_state.isPreloading: log.info("Preloading is already in progress.") @@ -1277,16 +1353,15 @@ def undo_delete(self): self.delete_history.append((jpg_path, raw_path)) elif action_type == "auto_white_balance": - filepath, saved_path = action_data - filepath_obj = Path(filepath) - backup_path = filepath_obj.parent / f"{filepath_obj.stem}_backup{filepath_obj.suffix}" - + saved_path, backup_path = action_data + filepath_obj = Path(saved_path) + try: if backup_path.exists(): # Restore the backup filepath_obj.unlink() # Remove the edited version backup_path.rename(filepath_obj) # Restore backup - log.info("Restored backup for %s", filepath) + log.info("Restored backup %s for %s", backup_path.name, saved_path) # Refresh the view self.display_generation += 1 @@ -1297,15 +1372,15 @@ def undo_delete(self): self.update_status_message("Undid auto white balance") else: + # This case should not be reached if glob finds files self.update_status_message("Backup not found") - log.warning("Backup not found at %s", backup_path) - # Put it back in history if backup not found - self.undo_history.append(("auto_white_balance", (filepath, saved_path), timestamp)) + log.warning("Backup %s disappeared before it could be restored.", backup_path) + self.undo_history.append(("auto_white_balance", (saved_path, backup_path), timestamp)) except OSError as e: self.update_status_message(f"Undo failed: {e}") log.exception("Failed to undo auto white balance") # Put it back in history if it failed - self.undo_history.append(("auto_white_balance", (filepath, saved_path), timestamp)) + self.undo_history.append(("auto_white_balance", (saved_path, backup_path), timestamp)) def shutdown(self): log.info("Application shutting down.") @@ -1749,11 +1824,12 @@ def quick_auto_white_balance(self): self.auto_white_balance() # Save the edited image (this creates a backup automatically) - saved_path = self.image_editor.save_image() - if saved_path: + save_result = self.image_editor.save_image() + if save_result: + saved_path, backup_path = save_result # Track this action for undo timestamp = time.time() - self.undo_history.append(("auto_white_balance", (filepath, saved_path), timestamp)) + self.undo_history.append(("auto_white_balance", (saved_path, backup_path), timestamp)) # Force the image editor to clear its current state so it reloads fresh self.image_editor.original_image = None @@ -1785,77 +1861,157 @@ def quick_auto_white_balance(self): @Slot() def auto_white_balance(self): - """Calculates and applies auto white balance using grey world assumption.""" + """ + Dispatcher for auto white balance. Calls the appropriate method based on + the mode set in the config ('lab' or 'rgb'). + """ + mode = config.get('awb', 'mode', fallback='lab') + if mode == 'lab': + self.auto_white_balance_lab() + elif mode == 'rgb': + self.auto_white_balance_legacy() + else: + log.error(f"Unknown AWB mode: {mode}") + self.update_status_message(f"Error: Unknown AWB mode '{mode}'") + + def auto_white_balance_legacy(self): + """ + Calculates and applies auto white balance using the legacy grey world + assumption on the entire RGB image. + """ if not self.image_editor.original_image: log.warning("No image loaded in editor for auto white balance") return - import numpy as np + try: + import numpy as np + except ImportError: + log.error("NumPy not found. Please install with: pip install numpy") + self.update_status_message("Error: NumPy not installed") + return + + log.info("Applying legacy (RGB Grey World) Auto White Balance") - # Work with the original image for accurate calculation img = self.image_editor.original_image - - # Convert to numpy array arr = np.array(img, dtype=np.float32) - # Calculate mean values for each channel r_mean = arr[:, :, 0].mean() g_mean = arr[:, :, 1].mean() b_mean = arr[:, :, 2].mean() - # Grey world assumption: average should be neutral grey grey_target = (r_mean + g_mean + b_mean) / 3.0 - log.info("Auto white balance - means: R=%.1f G=%.1f B=%.1f, target=%.1f", - r_mean, g_mean, b_mean, grey_target) - - # Calculate how much each channel differs from grey (positive = too high) r_diff = r_mean - grey_target g_diff = g_mean - grey_target - b_diff = b_mean - grey_target - - # From editor.py, the white balance equations are: - # R' = R + by_shift + mg_shift - # G' = G + by_shift - mg_shift - # B' = B - by_shift + mg_shift - # - # To neutralize: - # We want R' = G' = B' = grey_target - # So: R + by_shift + mg_shift = grey_target => by_shift + mg_shift = -r_diff - # G + by_shift - mg_shift = grey_target => by_shift - mg_shift = -g_diff - # B - by_shift + mg_shift = grey_target => -by_shift + mg_shift = -b_diff - # - # From first two equations: - # by_shift = -(r_diff + g_diff) / 2 - # mg_shift = -(r_diff - g_diff) / 2 by_shift = -(r_diff + g_diff) / 2.0 mg_shift = -(r_diff - g_diff) / 2.0 - # Convert to the -1 to 1 range expected by the editor (editor multiplies by 0.5 then 127.5) - # So our value goes through: value * 0.5 * 127.5 = value * 63.75 - # We want: by_shift, so value = by_shift / 63.75 by_value = by_shift / 63.75 mg_value = mg_shift / 63.75 - # Clamp to -1.0 to 1.0 range by_value = float(np.clip(by_value, -1.0, 1.0)) mg_value = float(np.clip(mg_value, -1.0, 1.0)) - log.info("Auto white balance: by_shift=%.1f mg_shift=%.1f", by_shift, mg_shift) - log.info("Auto white balance values: B/Y=%.3f, M/G=%.3f", by_value, mg_value) + self.image_editor.set_edit_param('white_balance_by', by_value) + self.image_editor.set_edit_param('white_balance_mg', mg_value) + + self.ui_state.white_balance_by = by_value + self.ui_state.white_balance_mg = mg_value + + self.ui_refresh_generation += 1 + self.ui_state.currentImageSourceChanged.emit() + self.update_status_message("Auto white balance applied (Legacy)") + + + def auto_white_balance_lab(self): + """ + Calculates and applies auto white balance using the Lab color space, + filtering out clipped and saturated pixels for a more robust result. + """ + if not self.image_editor.original_image: + log.warning("No image loaded in editor for auto white balance") + return + + try: + import cv2 + import numpy as np + except ImportError: + log.error("OpenCV or NumPy not found. Please install with: pip install opencv-python numpy") + self.update_status_message("Error: OpenCV or NumPy not installed") + return + + img = self.image_editor.original_image + # Ensure image is RGB before processing + if img.mode != 'RGB': + img = img.convert('RGB') + + arr = np.array(img, dtype=np.uint8) + + # --- Tunable Constants for Auto White Balance (from config) --- + _LOWER_BOUND_RGB = config.getint('awb', 'rgb_lower_bound', 5) + _UPPER_BOUND_RGB = config.getint('awb', 'rgb_upper_bound', 250) + _LUMA_LOWER_BOUND = config.getint('awb', 'luma_lower_bound', 30) + _LUMA_UPPER_BOUND = config.getint('awb', 'luma_upper_bound', 220) + warm_bias = config.getint('awb', 'warm_bias', 6) + _TARGET_A_LAB = 128.0 + _TARGET_B_LAB = 128.0 + warm_bias + _SCALING_FACTOR_LAB_TO_SLIDER = 128.0 + _CORRECTION_STRENGTH = config.getfloat('awb', 'strength', 0.7) + + # --- 1. Reject clipped channels and use a luma midtone mask --- + mask = ( + (arr[:, :, 0] > _LOWER_BOUND_RGB) & (arr[:, :, 0] < _UPPER_BOUND_RGB) & + (arr[:, :, 1] > _LOWER_BOUND_RGB) & (arr[:, :, 1] < _UPPER_BOUND_RGB) & + (arr[:, :, 2] > _LOWER_BOUND_RGB) & (arr[:, :, 2] < _UPPER_BOUND_RGB) + ) + + luma = (0.2126 * arr[:, :, 0] + 0.7152 * arr[:, :, 1] + 0.0722 * arr[:, :, 2]) + mask &= (luma > _LUMA_LOWER_BOUND) & (luma < _LUMA_UPPER_BOUND) + + if not np.any(mask): + log.warning("Auto white balance: No pixels found after clipping and luma filter. Aborting.") + self.update_status_message("AWB failed: no valid pixels found") + return + + # --- 2. Work in Lab color space --- + lab_image = cv2.cvtColor(arr, cv2.COLOR_RGB2LAB) + + a_channel = lab_image[:, :, 1] + b_channel = lab_image[:, :, 2] + + masked_a = a_channel[mask] + masked_b = b_channel[mask] + + a_mean = masked_a.mean() + b_mean = masked_b.mean() + + a_shift = _TARGET_A_LAB - a_mean + b_shift = _TARGET_B_LAB - b_mean + + log.info( + "Auto WB (Lab) - means: a*=%.1f, b*=%.1f; targets: a*=%.1f, b*=%.1f; shifts: a*=%.1f, b*=%.1f", + a_mean, b_mean, _TARGET_A_LAB, _TARGET_B_LAB, a_shift, b_shift + ) + + # --- 3. Convert Lab shift to our slider values with strength factor --- + by_value = (b_shift / _SCALING_FACTOR_LAB_TO_SLIDER) * _CORRECTION_STRENGTH + mg_value = (a_shift / _SCALING_FACTOR_LAB_TO_SLIDER) * _CORRECTION_STRENGTH + + by_value = float(np.clip(by_value, -1.0, 1.0)) + mg_value = float(np.clip(mg_value, -1.0, 1.0)) + + log.info(f"Auto white balance values: B/Y={by_value:.3f}, M/G={mg_value:.3f}") - # Apply the adjustments self.image_editor.set_edit_param('white_balance_by', by_value) self.image_editor.set_edit_param('white_balance_mg', mg_value) - # Update UIState properties directly to force slider refresh self.ui_state.white_balance_by = by_value self.ui_state.white_balance_mg = mg_value - # Trigger image refresh self.ui_refresh_generation += 1 self.ui_state.currentImageSourceChanged.emit() + self.update_status_message("Auto white balance applied") def _get_stack_info(self, index: int) -> str: info = "" diff --git a/faststack/faststack/config.py b/faststack/faststack/config.py index 095be0e..92019c4 100644 --- a/faststack/faststack/config.py +++ b/faststack/faststack/config.py @@ -28,6 +28,15 @@ "saturation_factor": "0.85", # Option A: 0.0-1.0, lower = less saturated "monitor_icc_path": "", # Option C: path to monitor ICC profile }, + "awb": { + "mode": "lab", # "lab" or "rgb" + "strength": "0.7", + "warm_bias": "6", + "luma_lower_bound": "30", + "luma_upper_bound": "220", + "rgb_lower_bound": "5", + "rgb_upper_bound": "250", + }, } class AppConfig: diff --git a/faststack/faststack/faststack.json b/faststack/faststack/faststack.json new file mode 100644 index 0000000..82bff3b --- /dev/null +++ b/faststack/faststack/faststack.json @@ -0,0 +1,6 @@ +{ + "version": 2, + "last_index": 0, + "entries": {}, + "stacks": [] +} \ No newline at end of file diff --git a/faststack/faststack/imaging/editor.py b/faststack/faststack/imaging/editor.py index 38a9a2b..7cc19ee 100644 --- a/faststack/faststack/imaging/editor.py +++ b/faststack/faststack/imaging/editor.py @@ -242,8 +242,12 @@ def set_crop_box(self, crop_box: Tuple[int, int, int, int]): """Set the normalized crop box (left, top, right, bottom) from 0-1000.""" self.current_edits['crop_box'] = crop_box - def save_image(self) -> Optional[Path]: - """Saves the edited image, backing up the original.""" + def save_image(self) -> Optional[Tuple[Path, Path]]: + """Saves the edited image, backing up the original. + + Returns: + A tuple of (saved_path, backup_path) on success, otherwise None. + """ if self.original_image is None or self.current_filepath is None: return None @@ -271,23 +275,28 @@ def save_image(self) -> Optional[Path]: # Perform the backup and overwrite shutil.copy2(original_path, backup_path) - # Preserve EXIF data from original image - try: - # Load the original image again to extract EXIF - original_img = Image.open(original_path) + # Re-open original to correctly detect format and get EXIF + with Image.open(original_path) as original_img: + original_format = original_img.format or original_path.suffix.lstrip('.').upper() exif_data = original_img.info.get('exif') - - # Save with EXIF data preserved + + save_kwargs = {} + if original_format == 'JPEG': + save_kwargs['format'] = 'JPEG' + save_kwargs['quality'] = 95 if exif_data: - final_img.save(original_path, format='JPEG', quality=95, exif=exif_data) - else: - final_img.save(original_path, format='JPEG', quality=95) + save_kwargs['exif'] = exif_data + else: + save_kwargs['format'] = original_format + + try: + final_img.save(original_path, **save_kwargs) except Exception as e: - print(f"Warning: Could not preserve EXIF data: {e}") - # Fall back to saving without EXIF if there's an issue - final_img.save(original_path, format='JPEG', quality=95) - - return original_path + print(f"Warning: Could not save with original format settings: {e}") + # Fallback to saving based on suffix + final_img.save(original_path) + + return original_path, backup_path except Exception as e: print(f"Failed to save edited image or backup: {e}") return None diff --git a/faststack/faststack/imaging/jpeg.py b/faststack/faststack/imaging/jpeg.py index cfaa316..97f54d9 100644 --- a/faststack/faststack/imaging/jpeg.py +++ b/faststack/faststack/imaging/jpeg.py @@ -118,10 +118,17 @@ def decode_jpeg_resized( try: # Get image header to determine dimensions img_width, img_height, _, _ = jpeg_decoder.decode_header(jpeg_bytes) - - # Calculate best scaling factor for TurboJPEG (supports 1/8, 1/4, 1/2, etc.) - scale_factor = _get_turbojpeg_scaling_factor(img_width, img_height, max(width, height)) - + + # Determine which dimension is the limiting factor + if img_width * height > img_height * width: + # Image is wider relative to target box; width is the constraint + max_dim = width + else: + # Image is taller relative to target box; height is the constraint + max_dim = height + + scale_factor = _get_turbojpeg_scaling_factor(img_width, img_height, max_dim) + if scale_factor: decoded = jpeg_decoder.decode( jpeg_bytes, diff --git a/faststack/faststack/qml/FilterDialog.qml b/faststack/faststack/qml/FilterDialog.qml index 95926db..16b6e72 100644 --- a/faststack/faststack/qml/FilterDialog.qml +++ b/faststack/faststack/qml/FilterDialog.qml @@ -12,12 +12,15 @@ Dialog { height: 250 property string filterString: "" + property color backgroundColor: "#1e1e1e" + property color textColor: "white" + // Match the app's theme dynamically - Material.theme: uiState && uiState.theme === 0 ? Material.Dark : Material.Light + // Material.theme: uiState && uiState.theme === 0 ? Material.Dark : Material.Light background: Rectangle { - color: Material.theme === Material.Dark ? "#1e1e1e" : "white" + color: filterDialog.backgroundColor border.color: Material.theme === Material.Dark ? "#404040" : "#c0c0c0" border.width: 1 radius: 4 @@ -31,6 +34,7 @@ Dialog { text: "Show only images whose filename contains:" wrapMode: Text.WordWrap width: parent.width - parent.padding * 2 + color: filterDialog.textColor } TextField { @@ -42,6 +46,10 @@ Dialog { focus: true font.pixelSize: 16 verticalAlignment: TextInput.AlignVCenter + color: filterDialog.textColor + background: Rectangle { + color: filterDialog.backgroundColor + } onTextChanged: { filterDialog.filterString = text @@ -57,6 +65,7 @@ Dialog { opacity: 0.7 wrapMode: Text.WordWrap width: parent.width - parent.padding * 2 + color: filterDialog.textColor } } diff --git a/faststack/faststack/qml/ImageEditorDialog.qml b/faststack/faststack/qml/ImageEditorDialog.qml index 07c3c43..618c237 100644 --- a/faststack/faststack/qml/ImageEditorDialog.qml +++ b/faststack/faststack/qml/ImageEditorDialog.qml @@ -12,6 +12,9 @@ Window { visible: uiState.isEditorOpen flags: Qt.Window | Qt.WindowTitleHint | Qt.WindowCloseButtonHint property int updatePulse: 0 + property color backgroundColor: "red" // Placeholder, will be set from Main.qml + property color textColor: "white" // Placeholder, will be set from Main.qml + Material.theme: uiState.theme === 0 ? Material.Dark : Material.Light Material.accent: "#4fb360" @@ -35,7 +38,7 @@ Window { } // Background - color: "#2b2b2b" + color: imageEditorDialog.backgroundColor ScrollView { anchors.fill: parent @@ -54,7 +57,7 @@ Window { spacing: 2 // --- Light Group --- - Label { text: "Light"; font.bold: true; color: uiState.theme === 0 ? "white" : "black" } + Label { text: "Light"; font.bold: true; color: imageEditorDialog.textColor } ListModel { id: lightModel ListElement { name: "Exposure"; key: "exposure" } @@ -68,7 +71,7 @@ Window { Repeater { model: lightModel; delegate: editSlider } // --- Detail Group --- - Label { text: "Detail"; font.bold: true; color: uiState.theme === 0 ? "white" : "black"; Layout.topMargin: 10 } + Label { text: "Detail"; font.bold: true; color: imageEditorDialog.textColor; Layout.topMargin: 10 } ListModel { id: detailModel ListElement { name: "Clarity"; key: "clarity" } @@ -84,7 +87,7 @@ Window { spacing: 2 // --- Color Group --- - Label { text: "Color"; font.bold: true; color: uiState.theme === 0 ? "white" : "black" } + Label { text: "Color"; font.bold: true; color: imageEditorDialog.textColor } ListModel { id: colorModel ListElement { name: "Saturation"; key: "saturation"; reverse: false } @@ -105,7 +108,7 @@ Window { } // --- Effects Group --- - Label { text: "Effects"; font.bold: true; color: uiState.theme === 0 ? "white" : "black"; Layout.topMargin: 10 } + Label { text: "Effects"; font.bold: true; color: imageEditorDialog.textColor; Layout.topMargin: 10 } ListModel { id: effectsModel ListElement { name: "Vignette"; key: "vignette"; min: 0; max: 100 } @@ -113,10 +116,10 @@ Window { Repeater { model: effectsModel; delegate: editSlider } // --- Transform Group --- - Label { text: "Transform"; font.bold: true; color: uiState.theme === 0 ? "white" : "black"; Layout.topMargin: 10 } + Label { text: "Transform"; font.bold: true; color: imageEditorDialog.textColor; Layout.topMargin: 10 } RowLayout { Layout.fillWidth: true - Label { text: "Rotation"; color: uiState.theme === 0 ? "white" : "black" } + Label { text: "Rotation"; color: imageEditorDialog.textColor } Button { text: "↶"; onClicked: controller.rotate_image_ccw() } Button { text: "↷"; onClicked: controller.rotate_image_cw() } } @@ -158,7 +161,7 @@ Window { Text { text: model.name + ": " + displayValue.toFixed(0) - color: uiState.theme === 0 ? "white" : "black" + color: imageEditorDialog.textColor font.pixelSize: 14 wrapMode: Text.WordWrap Layout.fillWidth: true @@ -212,7 +215,7 @@ Window { width: slider.availableWidth height: 4 radius: 2 - color: "#404040" + color: imageEditorDialog.backgroundColor === "#2b2b2b" ? Qt.lighter(imageEditorDialog.backgroundColor, 1.2) : Qt.darker(imageEditorDialog.backgroundColor, 1.2) } handle: Rectangle { @@ -222,7 +225,7 @@ Window { height: 16 radius: 8 color: slider.pressed ? "#4fb360" : "#6fcf7c" - border.color: "#3d8a4a" + border.color: uiState.theme === 0 ? Qt.darker(Material.accent, 1.2) : Qt.lighter(Material.accent, 1.2) } } } diff --git a/faststack/faststack/qml/ImageEditorDialog.qml.old b/faststack/faststack/qml/ImageEditorDialog.qml.old deleted file mode 100644 index a17ee0d..0000000 --- a/faststack/faststack/qml/ImageEditorDialog.qml.old +++ /dev/null @@ -1,196 +0,0 @@ -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Controls.Material 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 - -Window { - id: editDialog - width: 720 - height: 600 - title: "Image Editor" - visible: uiState.isEditorOpen - flags: Qt.Window | Qt.WindowTitleHint | Qt.WindowCloseButtonHint - - Material.theme: uiState.theme === 0 ? Material.Dark : Material.Light - Material.accent: "#4fb360" - - // When the dialog is closed by the user (e.g. clicking X), update the state - onVisibleChanged: { - if (!visible) { - uiState.isEditorOpen = false - } - } - - property int slidersPressedCount: 0 - onSlidersPressedCountChanged: { - uiState.setAnySliderPressed(slidersPressedCount > 0) - } - - function getBackendValue(key) { - if (key in uiState) return uiState[key]; - return 0.0; - } - - // Background - color: "#2b2b2b" - - ScrollView { - anchors.fill: parent - anchors.margins: 10 - clip: true - contentWidth: availableWidth - - RowLayout { - width: parent.width - spacing: 20 - - ColumnLayout { // Left Column - Layout.fillWidth: true - Layout.preferredWidth: (parent.width - 20) / 2 - Layout.alignment: Qt.AlignTop - spacing: 2 - - // --- Light Group --- - Label { text: "Light"; font.bold: true; color: uiState.theme === 0 ? "white" : "black" } - ListModel { - id: lightModel - ListElement { name: "Exposure"; key: "exposure" } - ListElement { name: "Highlights"; key: "highlights" } - ListElement { name: "Shadows"; key: "shadows" } - ListElement { name: "Whites"; key: "whites" } - ListElement { name: "Blacks"; key: "blacks" } - ListElement { name: "Brightness"; key: "brightness" } - ListElement { name: "Contrast"; key: "contrast" } - } - Repeater { model: lightModel; delegate: editSlider } - - // --- Detail Group --- - Label { text: "Detail"; font.bold: true; color: uiState.theme === 0 ? "white" : "black"; Layout.topMargin: 10 } - ListModel { - id: detailModel - ListElement { name: "Clarity"; key: "clarity" } - ListElement { name: "Sharpness"; key: "sharpness" } - } - Repeater { model: detailModel; delegate: editSlider } - } - - ColumnLayout { // Right Column - Layout.fillWidth: true - Layout.preferredWidth: (parent.width - 20) / 2 - Layout.alignment: Qt.AlignTop - spacing: 2 - - // --- Color Group --- - Label { text: "Color"; font.bold: true; color: uiState.theme === 0 ? "white" : "black" } - ListModel { - id: colorModel - ListElement { name: "Saturation"; key: "saturation"; reverse: false } - ListElement { name: "Vibrance"; key: "vibrance"; reverse: false } - ListElement { name: "White Balance (B/Y)"; key: "white_balance_by"; reverse: false } - ListElement { name: "White Balance (G/M)"; key: "white_balance_mg"; reverse: false } - } - Repeater { model: colorModel; delegate: editSlider } - - // --- Effects Group --- - Label { text: "Effects"; font.bold: true; color: uiState.theme === 0 ? "white" : "black"; Layout.topMargin: 10 } - ListModel { - id: effectsModel - ListElement { name: "Vignette"; key: "vignette"; min: 0; max: 100 } - } - Repeater { model: effectsModel; delegate: editSlider } - - // --- Transform Group --- - Label { text: "Transform"; font.bold: true; color: uiState.theme === 0 ? "white" : "black"; Layout.topMargin: 10 } - RowLayout { - Layout.fillWidth: true - Label { text: "Rotation"; color: uiState.theme === 0 ? "white" : "black" } - Button { text: "↶"; onClicked: controller.rotate_image_ccw() } - Button { text: "↷"; onClicked: controller.rotate_image_cw() } - } - - // --- Action Buttons --- - Item { Layout.fillHeight: true; Layout.minimumHeight: 20 } - Button { - text: "Reset All Edits" - Layout.fillWidth: true - onClicked: controller.reset_edit_parameters() - } - Button { - text: "Save Edited Image (Ctrl+S)" - Layout.fillWidth: true - onClicked: controller.save_edited_image() - } - Button { - text: "Close Editor (E)" - Layout.fillWidth: true - onClicked: { - uiState.isEditorOpen = false - } - } - } - } - } - - Component { - id: editSlider - ColumnLayout { - Layout.fillWidth: true - spacing: 0 - - property bool isReversed: model.reverse !== undefined ? model.reverse : false - property real displayValue: isReversed ? -slider.value : slider.value - - Text { - text: model.name + ": " + displayValue.toFixed(0) - color: uiState.theme === 0 ? "white" : "black" - font.pixelSize: 14 - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - Slider { - id: slider - Layout.fillWidth: true - Layout.minimumHeight: 30 - from: model.min === undefined ? -100 : model.min - to: model.max === undefined ? 100 : model.max - stepSize: 1 - - property real backendValue: { - var val = editDialog.getBackendValue(model.key) * (model.max === undefined ? 100 : model.max) - return isReversed ? -val : val - } - - value: backendValue - - onMoved: { - var sendValue = isReversed ? -value : value - controller.set_edit_parameter(model.key, sendValue / (model.max === undefined ? 100.0 : model.max)) - } - - onPressedChanged: { - if (pressed) editDialog.slidersPressedCount++; else editDialog.slidersPressedCount--; - } - - background: Rectangle { - x: slider.leftPadding - y: slider.topPadding + slider.availableHeight / 2 - height / 2 - width: slider.availableWidth - height: 4 - radius: 2 - color: "#404040" - } - - handle: Rectangle { - x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width) - y: slider.topPadding + slider.availableHeight / 2 - height / 2 - width: 16 - height: 16 - radius: 8 - color: slider.pressed ? "#4fb360" : "#6fcf7c" - border.color: "#3d8a4a" - } - } - } - } -} diff --git a/faststack/faststack/qml/JumpToImageDialog.qml b/faststack/faststack/qml/JumpToImageDialog.qml index 0b9fe4e..b5d5e3d 100644 --- a/faststack/faststack/qml/JumpToImageDialog.qml +++ b/faststack/faststack/qml/JumpToImageDialog.qml @@ -12,10 +12,16 @@ Dialog { width: 400 property int maxImageCount: 0 + property color backgroundColor: "red" // Placeholder, will be set from Main.qml + property color textColor: "white" // Placeholder, will be set from Main.qml + // Inherit Material theme from parent - Material.theme: uiState && uiState.theme === 0 ? Material.Dark : Material.Light - Material.accent: "#4fb360" + // Material.theme: uiState && uiState.theme === 0 ? Material.Dark : Material.Light + // Material.accent: "#4fb360" + background: Rectangle { + color: jumpDialog.backgroundColor + } onOpened: { imageNumberField.text = "" @@ -49,6 +55,7 @@ Dialog { text: "Enter image number (1-" + jumpDialog.maxImageCount + "):" Layout.fillWidth: true wrapMode: Text.WordWrap + color: jumpDialog.textColor } TextField { @@ -66,6 +73,10 @@ Dialog { bottom: 1 top: jumpDialog.maxImageCount } + color: jumpDialog.textColor + background: Rectangle { + color: jumpDialog.backgroundColor + } Keys.onReturnPressed: jumpDialog.accept() Keys.onEnterPressed: jumpDialog.accept() diff --git a/faststack/faststack/qml/Main.qml b/faststack/faststack/qml/Main.qml index d198544..76464e1 100644 --- a/faststack/faststack/qml/Main.qml +++ b/faststack/faststack/qml/Main.qml @@ -22,8 +22,10 @@ ApplicationWindow { Material.accent: "#4fb360" property bool isDarkTheme: uiState ? uiState.theme === 0 : true - property color currentBackgroundColor: isDarkTheme ? "#000000" : "white" + property color currentBackgroundColor: isDarkTheme ? "#2b2b2b" : "#ffffff" property color currentTextColor: isDarkTheme ? "white" : "black" + property color hoverColor: isDarkTheme ? Qt.lighter(currentBackgroundColor, 1.5) : Qt.darker(currentBackgroundColor, 1.1) + background: Rectangle { color: root.currentBackgroundColor } @@ -99,7 +101,7 @@ ApplicationWindow { id: fileBtn width: fileLabel.width + 20 height: 30 - color: fileMouseArea.containsMouse ? (root.isDarkTheme ? "#555555" : "#e0e0e0") : "transparent" + color: fileMouseArea.containsMouse ? hoverColor : "transparent" radius: 4 Text { @@ -126,7 +128,7 @@ ApplicationWindow { id: viewBtn width: viewLabel.width + 20 height: 30 - color: viewMouseArea.containsMouse ? (root.isDarkTheme ? "#555555" : "#e0e0e0") : "transparent" + color: viewMouseArea.containsMouse ? hoverColor : "transparent" radius: 4 Text { @@ -153,7 +155,7 @@ ApplicationWindow { id: actionsBtn width: actionsLabel.width + 20 height: 30 - color: actionsMouseArea.containsMouse ? (root.isDarkTheme ? "#555555" : "#e0e0e0") : "transparent" + color: actionsMouseArea.containsMouse ? hoverColor : "transparent" radius: 4 Text { @@ -180,7 +182,7 @@ ApplicationWindow { id: helpBtn width: helpLabel.width + 20 height: 30 - color: helpMouseArea.containsMouse ? (root.isDarkTheme ? "#555555" : "#e0e0e0") : "transparent" + color: helpMouseArea.containsMouse ? hoverColor : "transparent" radius: 4 Text { @@ -213,7 +215,7 @@ ApplicationWindow { background: Rectangle { implicitWidth: 200 implicitHeight: fileMenuColumn.implicitHeight - color: root.isDarkTheme ? "#424242" : "#ffffff" + color: root.currentBackgroundColor border.color: root.isDarkTheme ? "#666666" : "#cccccc" radius: 4 } @@ -226,11 +228,13 @@ ApplicationWindow { height: 36 text: "Open Folder..." onClicked: { - console.log("Open folder triggered") + if (uiState) { + uiState.open_folder() + } fileMenu.close() } background: Rectangle { - color: parent.hovered ? (root.isDarkTheme ? "#555555" : "#e0e0e0") : "transparent" + color: parent.hovered ? hoverColor : "transparent" } contentItem: Text { text: parent.text @@ -251,6 +255,13 @@ ApplicationWindow { settingsDialog.prefetchRadius = uiState.get_prefetch_radius() settingsDialog.theme = uiState.theme settingsDialog.defaultDirectory = uiState.get_default_directory() + settingsDialog.awbMode = uiState.awbMode + settingsDialog.awbStrength = uiState.awbStrength + settingsDialog.awbWarmBias = uiState.awbWarmBias + settingsDialog.awbLumaLowerBound = uiState.awbLumaLowerBound + settingsDialog.awbLumaUpperBound = uiState.awbLumaUpperBound + settingsDialog.awbRgbLowerBound = uiState.awbRgbLowerBound + settingsDialog.awbRgbUpperBound = uiState.awbRgbUpperBound } settingsDialog.open() fileMenu.close() @@ -296,7 +307,7 @@ ApplicationWindow { background: Rectangle { implicitWidth: 220 implicitHeight: viewMenuColumn.implicitHeight - color: root.isDarkTheme ? "#424242" : "#ffffff" + color: root.currentBackgroundColor border.color: root.isDarkTheme ? "#666666" : "#cccccc" radius: 4 } @@ -338,7 +349,6 @@ ApplicationWindow { text: "Color: None (Original)" onClicked: { if (controller) controller.set_color_mode("none") - if (uiState) uiState.colorMode = "none" viewMenu.close() } background: Rectangle { @@ -363,7 +373,6 @@ ApplicationWindow { text: "Color: Saturation Compensation" onClicked: { if (controller) controller.set_color_mode("saturation") - if (uiState) uiState.colorMode = "saturation" viewMenu.close() } background: Rectangle { @@ -388,7 +397,6 @@ ApplicationWindow { text: "Color: Full ICC Profile" onClicked: { if (controller) controller.set_color_mode("icc") - if (uiState) uiState.colorMode = "icc" viewMenu.close() } background: Rectangle { @@ -416,7 +424,7 @@ ApplicationWindow { background: Rectangle { implicitWidth: 220 implicitHeight: actionsMenuColumn.implicitHeight - color: root.isDarkTheme ? "#424242" : "#ffffff" + color: root.currentBackgroundColor border.color: root.isDarkTheme ? "#666666" : "#cccccc" radius: 4 } @@ -555,7 +563,7 @@ ApplicationWindow { background: Rectangle { implicitWidth: 200 implicitHeight: helpMenuColumn.implicitHeight - color: root.isDarkTheme ? "#424242" : "#ffffff" + color: root.currentBackgroundColor border.color: root.isDarkTheme ? "#666666" : "#cccccc" radius: 4 } @@ -623,7 +631,7 @@ ApplicationWindow { implicitHeight: footerRow.implicitHeight + 10 // Add some padding anchors.left: parent.left anchors.right: parent.right - color: "#80000000" // Semi-transparent black + color: Qt.rgba(root.currentBackgroundColor.r, root.currentBackgroundColor.g, root.currentBackgroundColor.b, 0.8) RowLayout { id: footerRow @@ -841,6 +849,8 @@ ApplicationWindow { FilterDialog { id: filterDialog + backgroundColor: root.currentBackgroundColor + textColor: root.currentTextColor onAccepted: { if (uiState) uiState.applyFilter(filterString) } @@ -848,11 +858,15 @@ ApplicationWindow { JumpToImageDialog { id: jumpToImageDialog + backgroundColor: root.currentBackgroundColor + textColor: root.currentTextColor maxImageCount: uiState ? uiState.imageCount : 0 } ImageEditorDialog { id: imageEditorDialog + backgroundColor: root.currentBackgroundColor + textColor: root.currentTextColor onVisibleChanged: { if (!visible) { mainViewLoader.forceActiveFocus() diff --git a/faststack/faststack/qml/SettingsDialog.qml b/faststack/faststack/qml/SettingsDialog.qml index 65e3792..4c86ae5 100644 --- a/faststack/faststack/qml/SettingsDialog.qml +++ b/faststack/faststack/qml/SettingsDialog.qml @@ -25,8 +25,10 @@ Dialog { } onOpened: { - // Refresh text field when dialog opens with current value + // Refresh text fields when dialog opens with current values cacheSizeField.text = settingsDialog.cacheSize.toFixed(1) + heliconPathField.text = settingsDialog.heliconPath + photoshopPathField.text = settingsDialog.photoshopPath } property string heliconPath: "" @@ -36,6 +38,15 @@ Dialog { property string defaultDirectory: "" property string photoshopPath: "" + property string awbMode: "lab" + property double awbStrength: 0.7 + property int awbWarmBias: 6 + + property int awbLumaLowerBound: 30 + property int awbLumaUpperBound: 220 + property int awbRgbLowerBound: 5 + property int awbRgbUpperBound: 250 + onAccepted: { uiState.set_helicon_path(heliconPath) uiState.set_photoshop_path(photoshopPath) @@ -43,120 +54,243 @@ Dialog { uiState.set_prefetch_radius(prefetchRadius) uiState.set_theme(theme) uiState.set_default_directory(defaultDirectory) + + uiState.awbMode = awbMode + uiState.awbStrength = awbStrength + uiState.awbWarmBias = awbWarmBias + + uiState.awbLumaLowerBound = awbLumaLowerBound + uiState.awbLumaUpperBound = awbLumaUpperBound + uiState.awbRgbLowerBound = awbRgbLowerBound + uiState.awbRgbUpperBound = awbRgbUpperBound } - contentItem: GridLayout { - columns: 3 - - // Helicon Path - Label { text: "Helicon Focus Path:" } - TextField { - id: heliconPathField - Layout.fillWidth: true - text: settingsDialog.heliconPath - onTextChanged: settingsDialog.heliconPath = text - } - RowLayout { + contentItem: ColumnLayout { + Row { + id: tabButtons + spacing: 5 + Button { - text: "Browse..." - onClicked: { - var path = uiState.open_file_dialog() - if (path) heliconPathField.text = path - } + text: "General" + highlighted: settingsStackLayout.currentIndex === 0 + onClicked: settingsStackLayout.currentIndex = 0 } - Label { - id: checkMarkLabel - text: "✔" - color: "lightgreen" - visible: uiState.check_path_exists(heliconPathField.text) + Button { + text: "Auto White Balance" + highlighted: settingsStackLayout.currentIndex === 1 + onClicked: settingsStackLayout.currentIndex = 1 } } - // Photoshop Path - Label { text: "Photoshop Path:" } - TextField { - id: photoshopPathField - Layout.fillWidth: true - text: settingsDialog.photoshopPath - onTextChanged: settingsDialog.photoshopPath = text - } - RowLayout { - Button { - text: "Browse..." - onClicked: { - var path = uiState.open_file_dialog() - if (path) photoshopPathField.text = path + StackLayout { + id: settingsStackLayout + currentIndex: 0 + + GridLayout { + columns: 3 + + // Helicon Path + Label { text: "Helicon Focus Path:" } + TextField { + id: heliconPathField + Layout.fillWidth: true + text: settingsDialog.heliconPath + onTextChanged: settingsDialog.heliconPath = text + } + RowLayout { + Button { + text: "Browse..." + onClicked: { + var path = uiState.open_file_dialog() + if (path) heliconPathField.text = path + } + } + Label { + id: checkMarkLabel + text: "✔" + color: "lightgreen" + visible: uiState.check_path_exists(heliconPathField.text) + } } - } - Label { - id: photoshopCheckMarkLabel - text: "✔" - color: "lightgreen" - visible: uiState.check_path_exists(photoshopPathField.text) - } - } - // Cache Size - Label { text: "Cache Size (GB):" } - TextField { - id: cacheSizeField - Layout.fillWidth: true - - Component.onCompleted: { - text = settingsDialog.cacheSize.toFixed(1) - } - - onEditingFinished: { - var value = parseFloat(text) - if (!isNaN(value) && value >= 0.5 && value <= 16) { - settingsDialog.cacheSize = value - text = value.toFixed(1) // Format it - } else { - // Invalid input, reset to current value - text = settingsDialog.cacheSize.toFixed(1) + // Photoshop Path + Label { text: "Photoshop Path:" } + TextField { + id: photoshopPathField + Layout.fillWidth: true + text: settingsDialog.photoshopPath + onTextChanged: settingsDialog.photoshopPath = text + } + RowLayout { + Button { + text: "Browse..." + onClicked: { + var path = uiState.open_file_dialog() + if (path) photoshopPathField.text = path + } + } + Label { + id: photoshopCheckMarkLabel + text: "✔" + color: "lightgreen" + visible: uiState.check_path_exists(photoshopPathField.text) + } + } + + // Cache Size + Label { text: "Cache Size (GB):" } + TextField { + id: cacheSizeField + Layout.fillWidth: true + + Component.onCompleted: { + text = settingsDialog.cacheSize.toFixed(1) + } + + onEditingFinished: { + var value = parseFloat(text) + if (!isNaN(value) && value >= 0.5 && value <= 16) { + settingsDialog.cacheSize = value + text = value.toFixed(1) // Format it + } else { + // Invalid input, reset to current value + text = settingsDialog.cacheSize.toFixed(1) + } + } + } + Label { + id: cacheUsageLabel + text: "In use: " + settingsDialog.cacheUsage.toFixed(2) + " GB" + color: "#1013e6" + } + + // Prefetch Radius + Label { text: "Prefetch Radius:" } + SpinBox { + id: prefetchRadiusSpinBox + from: 1 + to: 20 + value: settingsDialog.prefetchRadius + onValueChanged: settingsDialog.prefetchRadius = value + } + Label {} // Placeholder + + // Theme + Label { text: "Theme:" } + ComboBox { + id: themeComboBox + model: ["Dark", "Light"] + currentIndex: settingsDialog.theme + onCurrentIndexChanged: settingsDialog.theme = currentIndex + } + Label {} // Placeholder + + // Default Directory + Label { text: "Default Image Directory:" } + TextField { + id: defaultDirectoryField + Layout.fillWidth: true + text: settingsDialog.defaultDirectory + onTextChanged: settingsDialog.defaultDirectory = text + } + Button { + text: "Browse..." + onClicked: { + var path = uiState.open_directory_dialog() + if (path) defaultDirectoryField.text = path + } } } - } - Label { - id: cacheUsageLabel - text: "In use: " + settingsDialog.cacheUsage.toFixed(2) + " GB" - color: "#1013e6" - } - // Prefetch Radius - Label { text: "Prefetch Radius:" } - SpinBox { - id: prefetchRadiusSpinBox - from: 1 - to: 20 - value: settingsDialog.prefetchRadius - onValueChanged: settingsDialog.prefetchRadius = value - } - Label {} // Placeholder - - // Theme - Label { text: "Theme:" } - ComboBox { - id: themeComboBox - model: ["Dark", "Light"] - currentIndex: settingsDialog.theme - onCurrentIndexChanged: settingsDialog.theme = currentIndex - } - Label {} // Placeholder - - // Default Directory - Label { text: "Default Image Directory:" } - TextField { - id: defaultDirectoryField - Layout.fillWidth: true - text: settingsDialog.defaultDirectory - onTextChanged: settingsDialog.defaultDirectory = text - } - Button { - text: "Browse..." - onClicked: { - var path = uiState.open_directory_dialog() - if (path) defaultDirectoryField.text = path + GridLayout { + columns: 3 + + // --- Auto White Balance --- + Label { + text: "Auto WB Mode:" + Layout.topMargin: 10 + } + ComboBox { + id: awbModeComboBox + model: ["lab", "rgb"] + currentIndex: model.indexOf(settingsDialog.awbMode) + onCurrentIndexChanged: settingsDialog.awbMode = model[currentIndex] + Layout.topMargin: 10 + } + Label { + Layout.topMargin: 10 + } + + Label { text: "Auto WB Strength:" } + Slider { + id: awbStrengthSlider + from: 0.3 + to: 1.0 + value: settingsDialog.awbStrength + onValueChanged: settingsDialog.awbStrength = value + } + Label { text: (awbStrengthSlider.value * 100).toFixed(0) + "%" } + + Label { text: "Auto WB Warm Bias:" } + SpinBox { + id: awbWarmBiasSpinBox + from: -10 + to: 20 + value: settingsDialog.awbWarmBias + onValueChanged: settingsDialog.awbWarmBias = value + } + Label {} // Placeholder + + // --- Advanced AWB Settings --- + CheckBox { + id: advancedAwbCheckBox + text: "Advanced Settings" + checked: false + Layout.columnSpan: 3 + } + + GridLayout { + visible: advancedAwbCheckBox.checked + columns: 3 + Layout.columnSpan: 3 + Layout.fillWidth: true + + Label { text: "Luma Lower Bound:" } + SpinBox { + from: 0 + to: 255 + value: settingsDialog.awbLumaLowerBound + onValueChanged: settingsDialog.awbLumaLowerBound = value + } + Label {} + + Label { text: "Luma Upper Bound:" } + SpinBox { + from: 0 + to: 255 + value: settingsDialog.awbLumaUpperBound + onValueChanged: settingsDialog.awbLumaUpperBound = value + } + Label {} + + Label { text: "RGB Lower Bound:" } + SpinBox { + from: 0 + to: 255 + value: settingsDialog.awbRgbLowerBound + onValueChanged: settingsDialog.awbRgbLowerBound = value + } + Label {} + + Label { text: "RGB Upper Bound:" } + SpinBox { + from: 0 + to: 255 + value: settingsDialog.awbRgbUpperBound + onValueChanged: settingsDialog.awbRgbUpperBound = value + } + Label {} + } } } } diff --git a/faststack/faststack/ui/provider.py b/faststack/faststack/ui/provider.py index 26d369f..5a5e841 100644 --- a/faststack/faststack/ui/provider.py +++ b/faststack/faststack/ui/provider.py @@ -84,6 +84,13 @@ class UIState(QObject): filterStringChanged = Signal() # Signal for filter string updates colorModeChanged = Signal() # Signal for color mode updates saturationFactorChanged = Signal() # Signal for saturation factor updates + awbModeChanged = Signal() + awbStrengthChanged = Signal() + awbWarmBiasChanged = Signal() + awbLumaLowerBoundChanged = Signal() + awbLumaUpperBoundChanged = Signal() + awbRgbLowerBoundChanged = Signal() + awbRgbUpperBoundChanged = Signal() default_directory_changed = Signal(str) # Image Editor Signals is_editor_open_changed = Signal(bool) @@ -284,6 +291,69 @@ def saturationFactor(self): """Returns the current saturation factor.""" return self.app_controller.get_saturation_factor() + @Property(str, notify=awbModeChanged) + def awbMode(self): + return self.app_controller.get_awb_mode() + + @awbMode.setter + def awbMode(self, mode: str): + self.app_controller.set_awb_mode(mode) + self.awbModeChanged.emit() + + @Property(float, notify=awbStrengthChanged) + def awbStrength(self): + return self.app_controller.get_awb_strength() + + @awbStrength.setter + def awbStrength(self, value: float): + self.app_controller.set_awb_strength(value) + self.awbStrengthChanged.emit() + + @Property(int, notify=awbWarmBiasChanged) + def awbWarmBias(self): + return self.app_controller.get_awb_warm_bias() + + @awbWarmBias.setter + def awbWarmBias(self, value: int): + self.app_controller.set_awb_warm_bias(value) + self.awbWarmBiasChanged.emit() + + @Property(int, notify=awbLumaLowerBoundChanged) + def awbLumaLowerBound(self): + return self.app_controller.get_awb_luma_lower_bound() + + @awbLumaLowerBound.setter + def awbLumaLowerBound(self, value: int): + self.app_controller.set_awb_luma_lower_bound(value) + self.awbLumaLowerBoundChanged.emit() + + @Property(int, notify=awbLumaUpperBoundChanged) + def awbLumaUpperBound(self): + return self.app_controller.get_awb_luma_upper_bound() + + @awbLumaUpperBound.setter + def awbLumaUpperBound(self, value: int): + self.app_controller.set_awb_luma_upper_bound(value) + self.awbLumaUpperBoundChanged.emit() + + @Property(int, notify=awbRgbLowerBoundChanged) + def awbRgbLowerBound(self): + return self.app_controller.get_awb_rgb_lower_bound() + + @awbRgbLowerBound.setter + def awbRgbLowerBound(self, value: int): + self.app_controller.set_awb_rgb_lower_bound(value) + self.awbRgbLowerBoundChanged.emit() + + @Property(int, notify=awbRgbUpperBoundChanged) + def awbRgbUpperBound(self): + return self.app_controller.get_awb_rgb_upper_bound() + + @awbRgbUpperBound.setter + def awbRgbUpperBound(self, value: int): + self.app_controller.set_awb_rgb_upper_bound(value) + self.awbRgbUpperBoundChanged.emit() + @Property(str, constant=True) def currentDirectory(self): """Returns the path of the current working directory.""" @@ -373,6 +443,11 @@ def set_default_directory(self, path): def open_directory_dialog(self): return self.app_controller.open_directory_dialog() + @Slot() + def open_folder(self): + self.app_controller.open_folder() + + @Slot() def preloadAllImages(self): self.app_controller.preload_all_images() From 09a0158ce4e118181057efe8b3da6edca7ad6d24 Mon Sep 17 00:00:00 2001 From: AlanRockefeller Date: Sat, 22 Nov 2025 22:14:44 -0500 Subject: [PATCH 2/2] Release v1.2 - fixed menus --- faststack/ChangeLog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/faststack/ChangeLog.md b/faststack/ChangeLog.md index c1283a8..e2aa0e3 100644 --- a/faststack/ChangeLog.md +++ b/faststack/ChangeLog.md @@ -5,6 +5,7 @@ Todo: Make it work on Linux / Mac. Create Windows .exe. Write better docum # [1.2.0] - 2025-11-22 - Fixed menus, they now work well and look cool. +- Updated auto white balance to make it better, and put some controls for it in the settings ## [1.1.0] - 2025-11-22