From 102bc584d2cf9528e89ce68c2b000ed2f5cf4dc2 Mon Sep 17 00:00:00 2001 From: AlanRockefeller Date: Sun, 16 Nov 2025 10:30:17 -0500 Subject: [PATCH 1/3] feat: Add Photoshop integration and clipboard functionality This commit introduces new features: - Pressing 'E' now opens the current image in Adobe Photoshop. - Pressing 'Ctrl+C' copies the path of the current image to the clipboard. - The application's status bar now displays messages for actions like copying to clipboard or launching Photoshop. Changes include: - Updated aststack/faststack/config.py to include a 'photoshop' section for executable path and arguments. - Modified aststack/faststack/ui/keystrokes.py to map 'E' to 'edit_in_photoshop' and 'Ctrl+C' to 'copy_path_to_clipboard'. - Implemented edit_in_photoshop, copy_path_to_clipboard, and update_status_message methods in aststack/faststack/app.py. - Enhanced aststack/faststack/ui/provider.py by adding statusMessageChanged signal and statusMessage property to UIState. - Updated aststack/faststack/qml/Main.qml to display the uiState.statusMessage in the footer. - Removed the unused aststack/faststack/ui/state.py file. --- faststack/faststack/app.py | 243 +++++++++++++++++++++++++-- faststack/faststack/config.py | 4 + faststack/faststack/qml/Main.qml | 30 +++- faststack/faststack/ui/keystrokes.py | 89 +++++++--- faststack/faststack/ui/provider.py | 52 ++++-- 5 files changed, 360 insertions(+), 58 deletions(-) diff --git a/faststack/faststack/app.py b/faststack/faststack/app.py index a4d7f84..826c747 100644 --- a/faststack/faststack/app.py +++ b/faststack/faststack/app.py @@ -2,17 +2,32 @@ import logging import sys +import struct from pathlib import Path from typing import Optional, List, Dict from datetime import date - import os import typer import concurrent.futures -from PySide6.QtCore import QUrl, QTimer, QObject, QEvent, Signal +import threading +import subprocess +from faststack.ui.provider import ImageProvider, UIState +from PySide6.QtGui import QDrag, QPixmap +from PySide6.QtCore import ( + QUrl, + QTimer, + QObject, + QEvent, + Signal, + Slot, + QMimeData, + Qt, + QPoint +) from PySide6.QtWidgets import QApplication, QFileDialog from PySide6.QtQml import QQmlApplicationEngine +# ⬇️ these are the ones that went missing from faststack.config import config from faststack.logging_setup import setup_logging from faststack.models import ImageFile, DecodedImage, EntryMetadata @@ -22,14 +37,31 @@ from faststack.io.helicon import launch_helicon_focus from faststack.imaging.cache import ByteLRUCache, get_decoded_image_size from faststack.imaging.prefetch import Prefetcher -from faststack.ui.provider import ImageProvider, UIState +from faststack.ui.provider import ImageProvider from faststack.ui.keystrokes import Keybinder -import threading + +def make_hdrop(paths): + """ + Build a real CF_HDROP (DROPFILES) payload for Windows drag-and-drop. + paths: list[str] + """ + files_part = ("\0".join(paths) + "\0\0").encode("utf-16le") + + # DROPFILES header (20 bytes): = len(self.image_files): + return + + file_path = self.image_files[self.current_index].path + if not file_path.exists(): + log.error(f"File does not exist, cannot start drag: {file_path}") + return + + if self.main_window is None: + return + + drag = QDrag(self.main_window) + mime_data = QMimeData() + + # --- Windows file drop payload --- + if sys.platform.startswith("win"): + hdrop = make_hdrop([str(file_path)]) + mime_data.setData('application/x-qt-windows-mime;value="FileDrop"', hdrop) + mime_data.setData('application/x-qt-windows-mime;value="FileNameW"', + (str(file_path) + "\0").encode("utf-16le")) + mime_data.setData('application/x-qt-windows-mime;value="FileName"', + (str(file_path) + "\0").encode("mbcs", errors="replace")) + else: + mime_data.setUrls([QUrl.fromLocalFile(str(file_path))]) + + drag.setMimeData(mime_data) + + # --- thumbnail / drag preview --- + pix = QPixmap(str(file_path)) + if not pix.isNull(): + # scale it down so it’s not huge + scaled = pix.scaled(128, 128, Qt.KeepAspectRatio, Qt.SmoothTransformation) + drag.setPixmap(scaled) + # hotspot = center of image + drag.setHotSpot(QPoint(scaled.width() // 2, scaled.height() // 2)) + + log.info(f"Starting drag for {file_path}") + drag.exec(Qt.CopyAction) + def _get_stack_info(self, index: int) -> str: info = "" for i, (start, end) in enumerate(self.stacks): @@ -483,6 +678,21 @@ def _get_stack_info(self, index: int) -> str: log.info(f"_get_stack_info for index {index}: {info}") return info + def get_stack_summary(self) -> str: + if not self.stacks: + return "No stacks defined." + summary = [] + for i, (start, end) in enumerate(self.stacks): + summary.append(f"Stack {i+1}: {start}-{end}") + return "; ".join(summary) + + def is_stacked(self) -> bool: + if not self.image_files: + return False + stem = self.image_files[self.current_index].path.stem + meta = self.sidecar.get_metadata(stem) + return meta.stacked + def main(image_dir: Optional[Path] = typer.Argument(None, help="Directory of images to view")): """FastStack Application Entry Point""" setup_logging() @@ -518,6 +728,7 @@ def main(image_dir: Optional[Path] = typer.Argument(None, help="Directory of ima # Expose controller and UI state to QML context = engine.rootContext() context.setContextProperty("uiState", controller.ui_state) + context.setContextProperty("controller", controller) qml_file = Path(__file__).parent / "qml" / "Main.qml" engine.load(QUrl.fromLocalFile(str(qml_file))) diff --git a/faststack/faststack/config.py b/faststack/faststack/config.py index 56bb2b2..4ff8fd9 100644 --- a/faststack/faststack/config.py +++ b/faststack/faststack/config.py @@ -19,6 +19,10 @@ "exe": "C:\\Program Files\\Helicon Software\\Helicon Focus 8\\HeliconFocus.exe", "args": "", }, + "photoshop": { + "exe": "C:\\Program Files\\Adobe\\Adobe Photoshop 2026\\Photoshop.exe", + "args": "", + }, } class AppConfig: diff --git a/faststack/faststack/qml/Main.qml b/faststack/faststack/qml/Main.qml index 6eb9015..d0722e3 100644 --- a/faststack/faststack/qml/Main.qml +++ b/faststack/faststack/qml/Main.qml @@ -15,22 +15,22 @@ ApplicationWindow { flags: Qt.FramelessWindowHint | Qt.Window title: "FastStack" - Material.theme: isDarkTheme ? Material.Dark : Material.Light + Material.theme: uiState.theme === 0 ? Material.Dark : Material.Light - property bool isDarkTheme: uiState.get_theme() === 0 + property bool isDarkTheme: uiState.theme === 0 property color currentBackgroundColor: isDarkTheme ? "#000000" : "white" property color currentTextColor: isDarkTheme ? "white" : "black" background: Rectangle { color: root.currentBackgroundColor } function toggleTheme() { - uiState.set_theme(isDarkTheme ? 1 : 0) // 0 for dark, 1 for light + uiState.theme = (uiState.theme === 0 ? 1 : 0) // 0 for dark, 1 for light } Connections { target: uiState function onThemeChanged() { - root.isDarkTheme = uiState.get_theme() === 0 + root.isDarkTheme = uiState.theme === 0 } } @@ -109,6 +109,13 @@ ApplicationWindow { font.pixelSize: 16 } } + Label { + id: statusMessageLabel + text: uiState.statusMessage + color: root.currentTextColor + visible: uiState.statusMessage !== "" + Layout.rightMargin: 10 + } } } @@ -158,7 +165,7 @@ ApplicationWindow { settingsDialog.heliconPath = uiState.get_helicon_path() settingsDialog.cacheSize = uiState.get_cache_size() settingsDialog.prefetchRadius = uiState.get_prefetch_radius() - settingsDialog.theme = uiState.get_theme() + settingsDialog.theme = uiState.theme settingsDialog.defaultDirectory = uiState.get_default_directory() settingsDialog.open() } @@ -175,6 +182,8 @@ ApplicationWindow { Action { text: "Clear Stacks"; onTriggered: uiState.clear_all_stacks() } Action { text: "Show Stacks"; onTriggered: showStacksDialog.open() } Action { text: "Preload All Images"; onTriggered: uiState.preloadAllImages() } + Action { text: "Filter Images..."; onTriggered: filterDialog.open() } + Action { text: "Clear Filename Filter"; onTriggered: controller.clear_filter() } } Menu { title: "&Help" @@ -274,7 +283,7 @@ ApplicationWindow { } contentItem: Text { - text: uiState.get_stack_summary // Access property directly + text: uiState.stackSummary || "No stacks defined." padding: 10 wrapMode: Text.WordWrap color: root.currentTextColor @@ -284,4 +293,13 @@ ApplicationWindow { SettingsDialog { id: settingsDialog } + + FilterDialog { + id: filterDialog + onAccepted: { + controller.apply_filter(filterString) + } +} + + } diff --git a/faststack/faststack/ui/keystrokes.py b/faststack/faststack/ui/keystrokes.py index 812a764..2db830c 100644 --- a/faststack/faststack/ui/keystrokes.py +++ b/faststack/faststack/ui/keystrokes.py @@ -1,45 +1,88 @@ -"""Maps Qt Key events to application actions.""" - +# faststack/ui/keystrokes.py import logging from PySide6.QtCore import Qt log = logging.getLogger(__name__) class Keybinder: - def __init__(self, main_window): - self.main_window = main_window + def __init__(self, controller): + """ + controller is your AppController. + We will call controller.() by default, + but if controller.main_window has a QML method of the same name, + we'll call that instead so the footer/UI stays in sync. + """ + self.controller = controller + + # map keys → method names (not callables) self.key_map = { # Navigation - Qt.Key.Key_J: self.main_window.next_image, - Qt.Key.Key_Right: self.main_window.next_image, - Qt.Key.Key_K: self.main_window.prev_image, - Qt.Key.Key_Left: self.main_window.prev_image, + Qt.Key_J: "next_image", + Qt.Key_Right: "next_image", + Qt.Key_K: "prev_image", + Qt.Key_Left: "prev_image", # View Mode - Qt.Key.Key_G: self.main_window.toggle_grid_view, + Qt.Key_G: "toggle_grid_view", # Metadata - Qt.Key.Key_Space: self.main_window.toggle_current_flag, - Qt.Key.Key_X: self.main_window.toggle_current_reject, + Qt.Key_Space: "toggle_current_flag", + Qt.Key_X: "toggle_current_reject", # Stacking - Qt.Key.Key_BracketLeft: self.main_window.begin_new_stack, - Qt.Key.Key_BracketRight: self.main_window.end_current_stack, + Qt.Key_BracketLeft: "begin_new_stack", + Qt.Key_BracketRight: "end_current_stack", # Actions - Qt.Key.Key_S: self.main_window.toggle_selection, - Qt.Key.Key_Enter: self.main_window.launch_helicon, - Qt.Key.Key_Return: self.main_window.launch_helicon, + Qt.Key_S: "toggle_selection", + Qt.Key_Enter: "launch_helicon", + Qt.Key_Return: "launch_helicon", + Qt.Key_E: "edit_in_photoshop", + Qt.Key_C: "clear_all_stacks", # Keep C for clear_all_stacks + } - # Stack Management - Qt.Key.Key_C: self.main_window.clear_all_stacks, + self.modifier_key_map = { + (Qt.Key_C, Qt.ControlModifier): "copy_path_to_clipboard", } + def _call(self, method_name: str): + """ + Try QML root first (to keep footer/UI happy), then controller. + """ + mw = getattr(self.controller, "main_window", None) + if mw is not None and hasattr(mw, method_name): + getattr(mw, method_name)() + return + + if hasattr(self.controller, method_name): + getattr(self.controller, method_name)() + return + + log.warning(f"Keybinder: neither main_window nor controller has '{method_name}'") + def handle_key_press(self, event): - """Handles a key press event from the main window.""" - log.info(f"Key pressed: {event.key()}") - action = self.key_map.get(event.key()) - if action: - action() + key = event.key() + text = event.text() + log.info(f"Key pressed: {key} ({text!r}) with modifiers {event.modifiers()}") + + # Check for modifier + key combinations + for (mapped_key, mapped_modifier), method_name in self.modifier_key_map.items(): + if key == mapped_key and event.modifiers() & mapped_modifier: + self._call(method_name) + return True + + # Check for single key presses + method_name = self.key_map.get(key) + if method_name: + self._call(method_name) + return True + + # extra safety for layouts where bracket keycodes are odd + if text == "[": + self._call("begin_new_stack") + return True + if text == "]": + self._call("end_current_stack") return True + return False diff --git a/faststack/faststack/ui/provider.py b/faststack/faststack/ui/provider.py index 8e7dbdb..5fd42fe 100644 --- a/faststack/faststack/ui/provider.py +++ b/faststack/faststack/ui/provider.py @@ -1,10 +1,7 @@ """QML Image Provider and application state bridge.""" import logging -from typing import Optional - -import numpy as np -from PySide6.QtCore import QObject, Signal, Property, QUrl, Slot, Qt +from PySide6.QtCore import QObject, Signal, Property, Slot, Qt from PySide6.QtGui import QImage from PySide6.QtQuick import QQuickImageProvider @@ -12,6 +9,7 @@ log = logging.getLogger(__name__) + class ImageProvider(QQuickImageProvider): def __init__(self, app_controller): super().__init__(QQuickImageProvider.ImageType.Image) @@ -24,14 +22,12 @@ def requestImage(self, id: str, size: object, requestedSize: object) -> QImage: if not id: return self.placeholder - # The ID is expected to be the image index try: image_index_str = id.split('/')[0] index = int(image_index_str) image_data = self.app_controller.get_decoded_image(index) if image_data: - # Zero-copy QImage from numpy buffer qimg = QImage( image_data.buffer, image_data.width, @@ -39,15 +35,16 @@ def requestImage(self, id: str, size: object, requestedSize: object) -> QImage: image_data.bytes_per_line, QImage.Format.Format_RGB888 ) - # Keep a reference to the original buffer to prevent garbage collection + # keep buffer alive qimg.original_buffer = image_data.buffer return qimg except (ValueError, IndexError) as e: log.error(f"Invalid image ID requested from QML: {id}. Error: {e}") - + return self.placeholder + class UIState(QObject): """Manages the state exposed to the QML user interface.""" @@ -60,13 +57,31 @@ class UIState(QObject): preloadingStateChanged = Signal() preloadProgressChanged = Signal() isZoomedChanged = Signal() + statusMessageChanged = Signal() # New signal for status messages def __init__(self, app_controller): super().__init__() self.app_controller = app_controller self._is_preloading = False self._preload_progress = 0 - + # 1 = light, 0 = dark (controller will overwrite this on startup) + self._theme = 1 + self._status_message = "" # New private variable for status message + + # ---- THEME PROPERTY ---- + @Property(int, notify=themeChanged) + def theme(self): + return self._theme + + @theme.setter + def theme(self, value: int): + value = int(value) + if value == self._theme: + return + self._theme = value + self.themeChanged.emit() + + # ---- ZOOM ---- @Property(bool, notify=isZoomedChanged) def isZoomed(self): return self.app_controller.is_zoomed @@ -75,6 +90,7 @@ def isZoomed(self): def setZoomed(self, zoomed: bool): self.app_controller.set_zoomed(zoomed) + # ---- PRELOADING ---- @Property(bool, notify=preloadingStateChanged) def isPreloading(self): return self._is_preloading @@ -95,6 +111,7 @@ def preloadProgress(self, value): self._preload_progress = value self.preloadProgressChanged.emit() + # ---- IMAGE / METADATA ---- @Property(int, notify=currentIndexChanged) def currentIndex(self): return self.app_controller.current_index @@ -105,11 +122,8 @@ def imageCount(self): @Property(str, notify=currentImageSourceChanged) def currentImageSource(self): - # The source is the provider ID, which we tie to the index and a generation counter - # to force QML to request a new image even if the index is the same. return f"image://provider/{self.app_controller.current_index}/{self.app_controller.ui_refresh_generation}" - # --- Metadata Properties --- @Property(str, notify=metadataChanged) def currentFilename(self): return self.app_controller.get_current_metadata().get("filename", "") @@ -138,13 +152,22 @@ def stackInfoText(self): def get_stack_summary(self): if not self.app_controller.stacks: return "No stacks defined." - summary = f"Found {len(self.app_controller.stacks)} stacks:\n\n" for i, (start, end) in enumerate(self.app_controller.stacks): count = end - start + 1 summary += f"Stack {i+1}: {count} photos (indices {start}-{end})\n" return summary + @Property(str, notify=statusMessageChanged) + def statusMessage(self): + return self._status_message + + @statusMessage.setter + def statusMessage(self, value: str): + if self._status_message != value: + self._status_message = value + self.statusMessageChanged.emit() + # --- Slots for QML to call --- @Slot() def nextImage(self): @@ -200,10 +223,12 @@ def set_prefetch_radius(self, radius): @Slot(result=int) def get_theme(self): + # this lets QML ask the controller, but the real binding is uiState.theme return self.app_controller.get_theme() @Slot(int) def set_theme(self, theme_index): + # delegate to controller so it can save to config self.app_controller.set_theme(theme_index) @Slot(result=str) @@ -225,3 +250,4 @@ def preloadAllImages(self): @Slot(int, int) def onDisplaySizeChanged(self, width: int, height: int): self.app_controller.on_display_size_changed(width, height) + From b5226525df8db70e4d66932c7c7d8c2fc05af32c Mon Sep 17 00:00:00 2001 From: AlanRockefeller Date: Sun, 16 Nov 2025 10:33:50 -0500 Subject: [PATCH 2/3] fix: Correct SyntaxError in lambda function Replaced the lambda function in update_status_message with a nested function to avoid a SyntaxError caused by an assignment within the lambda. --- faststack/faststack/app.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/faststack/faststack/app.py b/faststack/faststack/app.py index 826c747..cbb0bae 100644 --- a/faststack/faststack/app.py +++ b/faststack/faststack/app.py @@ -619,8 +619,12 @@ def update_status_message(self, message: str, timeout: int = 3000): """ Updates the UI status message and clears it after a timeout. """ + def clear_message(): + if self.ui_state.statusMessage == message: + self.ui_state.statusMessage = "" + self.ui_state.statusMessage = message - QTimer.singleShot(timeout, lambda: self.ui_state.statusMessage = "") + QTimer.singleShot(timeout, clear_message) From ea433ef80df0846f4d06c8a606d4a55500863bd3 Mon Sep 17 00:00:00 2001 From: AlanRockefeller Date: Sun, 16 Nov 2025 10:52:33 -0500 Subject: [PATCH 3/3] fix: Correct UI issues in Key Bindings and Settings dialogs This commit addresses two UI issues: 1. The Key Bindings dialog now uses a ScrollView to ensure all text is visible, and the key bindings list has been updated. 2. The Settings dialog now includes a field for selecting the Photoshop executable path. Changes include: - Updated aststack/faststack/qml/Main.qml to wrap the Key Bindings text in a ScrollView and to load the Photoshop path into the Settings dialog. - Modified aststack/faststack/qml/SettingsDialog.qml to include UI elements for Photoshop path selection. - Added get_photoshop_path and set_photoshop_path methods to aststack/faststack/app.py. - Exposed the new methods to QML through aststack/faststack/ui/provider.py. --- faststack/faststack/app.py | 7 ++++ faststack/faststack/qml/Main.qml | 48 ++++++++++++---------- faststack/faststack/qml/SettingsDialog.qml | 30 +++++++++++++- faststack/faststack/ui/provider.py | 8 ++++ 4 files changed, 70 insertions(+), 23 deletions(-) diff --git a/faststack/faststack/app.py b/faststack/faststack/app.py index cbb0bae..80137d0 100644 --- a/faststack/faststack/app.py +++ b/faststack/faststack/app.py @@ -461,6 +461,13 @@ def set_helicon_path(self, path): config.set('helicon', 'exe', path) config.save() + def get_photoshop_path(self): + return config.get('photoshop', 'exe') + + def set_photoshop_path(self, path): + config.set('photoshop', 'exe', path) + config.save() + def open_file_dialog(self): dialog = QFileDialog() dialog.setFileMode(QFileDialog.FileMode.ExistingFile) diff --git a/faststack/faststack/qml/Main.qml b/faststack/faststack/qml/Main.qml index d0722e3..95aac22 100644 --- a/faststack/faststack/qml/Main.qml +++ b/faststack/faststack/qml/Main.qml @@ -163,6 +163,7 @@ ApplicationWindow { text: "&Settings..." onTriggered: { settingsDialog.heliconPath = uiState.get_helicon_path() + settingsDialog.photoshopPath = uiState.get_photoshop_path() settingsDialog.cacheSize = uiState.get_cache_size() settingsDialog.prefetchRadius = uiState.get_prefetch_radius() settingsDialog.theme = uiState.theme @@ -246,27 +247,32 @@ ApplicationWindow { color: root.currentBackgroundColor } - contentItem: Text { - text: "FastStack Keyboard and Mouse Commands

" + - "Navigation:
" + - "  J / Right Arrow: Next Image
" + - "  K / Left Arrow: Previous Image

" + - "Viewing:
" + - "  Mouse Wheel: Zoom in/out
" + - "  Left-click + Drag: Pan image
" + - "  G: Toggle Grid View (not implemented)

" + - "Rating & Stacking:
" + - "  Space: Toggle Flag
" + - "  X: Toggle Reject
" + - "  S: Add to selection for Helicon
" + - "  [: Begin new stack
" + - "  ]: End current stack
" + - "  C: Clear all stacks

" + - "Actions:
" + - "  Enter: Launch Helicon Focus" - padding: 10 - wrapMode: Text.WordWrap - color: root.currentTextColor + contentItem: ScrollView { + clip: true + Text { + text: "FastStack Keyboard and Mouse Commands

" + + "Navigation:
" + + "  J / Right Arrow: Next Image
" + + "  K / Left Arrow: Previous Image

" + + "Viewing:
" + + "  Mouse Wheel: Zoom in/out
" + + "  Left-click + Drag: Pan image
" + + "  G: Toggle Grid View (not implemented)

" + + "Rating & Stacking:
" + + "  Space: Toggle Flag
" + + "  X: Toggle Reject
" + + "  S: Add to selection for Helicon
" + + "  [: Begin new stack
" + + "  ]: End current stack
" + + "  C: Clear all stacks

" + + "Actions:
" + + "  Enter: Launch Helicon Focus
" + + "  E: Edit in Photoshop
" + + "  Ctrl+C: Copy image path to clipboard" + padding: 10 + wrapMode: Text.WordWrap + color: root.currentTextColor + } } } diff --git a/faststack/faststack/qml/SettingsDialog.qml b/faststack/faststack/qml/SettingsDialog.qml index d2284fe..809a375 100644 --- a/faststack/faststack/qml/SettingsDialog.qml +++ b/faststack/faststack/qml/SettingsDialog.qml @@ -8,16 +8,18 @@ Dialog { standardButtons: Dialog.Ok | Dialog.Cancel modal: true width: 600 - height: 400 + height: 600 property string heliconPath: "" property double cacheSize: 1.5 property int prefetchRadius: 4 property int theme: 0 property string defaultDirectory: "" + property string photoshopPath: "" onAccepted: { uiState.set_helicon_path(heliconPath) + uiState.set_photoshop_path(photoshopPath) uiState.set_cache_size(cacheSize) uiState.set_prefetch_radius(prefetchRadius) uiState.set_theme(theme) @@ -51,6 +53,30 @@ Dialog { } } + // 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 { @@ -105,4 +131,4 @@ Dialog { } } } -} \ No newline at end of file +} diff --git a/faststack/faststack/ui/provider.py b/faststack/faststack/ui/provider.py index 5fd42fe..d5fee0e 100644 --- a/faststack/faststack/ui/provider.py +++ b/faststack/faststack/ui/provider.py @@ -197,6 +197,14 @@ def get_helicon_path(self): def set_helicon_path(self, path): self.app_controller.set_helicon_path(path) + @Slot(result=str) + def get_photoshop_path(self): + return self.app_controller.get_photoshop_path() + + @Slot(str) + def set_photoshop_path(self, path): + self.app_controller.set_photoshop_path(path) + @Slot(result=str) def open_file_dialog(self): return self.app_controller.open_file_dialog()