From 8bb655eb403ad130d9f42bc904e7c84bd7784085 Mon Sep 17 00:00:00 2001 From: CodeByMrFinchum Date: Tue, 4 Feb 2025 12:20:40 +0100 Subject: [PATCH] feat: Preview window now uses seperate thread. --- CHANGELOG.md | 9 ++ src/OptimaLab35/gui.py | 124 +++++++++++++++------------ src/OptimaLab35/ui/preview_window.py | 3 +- src/OptimaLab35/ui/preview_window.ui | 5 +- 4 files changed, 84 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50ef93c..be219da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.10.x +### 0.10.0: Multithreading for Preview Window +- The preview window now processes images in a separate thread, and live update preview is enabled by default. + - This improves UI responsiveness. + - The image now resizes dynamically to fit the window when the window size changes. +- Minor UI improvements. + +--- + ## 0.9.x ### 0.9.1: Patch for Unsuccessful Successful Update - Addressed a rare issue where the package did not update correctly using the updater. diff --git a/src/OptimaLab35/gui.py b/src/OptimaLab35/gui.py index 22fdafb..e69088d 100644 --- a/src/OptimaLab35/gui.py +++ b/src/OptimaLab35/gui.py @@ -12,7 +12,7 @@ from OptimaLab35.ui.exif_handler_window import ExifEditor from OptimaLab35.ui.simple_dialog import SimpleDialog # Import the SimpleDialog class from OptimaLab35 import __version__ -from PySide6.QtCore import QRunnable, QThreadPool, Signal, QObject, QRegularExpression, Qt, QTimer +from PySide6.QtCore import QRunnable, QThreadPool, Signal, QObject, QRegularExpression, Qt, QTimer, Slot from PySide6 import QtWidgets from PySide6.QtWidgets import ( @@ -52,7 +52,7 @@ class OptimaLab35(QMainWindow, Ui_MainWindow): self.sd = SimpleDialog() # Change UI elements - self.change_statusbar(f"Using {self.o.name} v{self.o.version}", 5000) + self.change_statusbar(f"{self.name} v{self.version}", 10000) self.set_title() self.default_ui_layout() self.define_gui_interaction() @@ -501,10 +501,12 @@ class UpdaterWindow(QMainWindow, Ui_Updater_Window): def start_long_press(self): """Start the timer when button is pressed.""" + # brave AI self.timer.start(1000) # 1-second long press def cancel_long_press(self): """Cancel long press if released early.""" + # brave AI self.timer.stop() def toggle_dev_ui(self): @@ -520,8 +522,8 @@ class UpdaterWindow(QMainWindow, Ui_Updater_Window): self.ui.label_optima35_localversion.setText(self.optima35_localversion) self.ui.label_latest_version.setText("Latest version") - self.ui.label_optimalab35_latestversion.setText(self.ol35_last_state[1]) - self.ui.label_optima35_latestversion.setText(self.o35_last_state[1]) + self.ui.label_optimalab35_latestversion.setText("...") + self.ui.label_optima35_latestversion.setText("...") self.ui.update_and_restart_Button.setEnabled(False) @@ -706,14 +708,18 @@ class UpdaterWindow(QMainWindow, Ui_Updater_Window): class PreviewWindow(QMainWindow, Ui_Preview_Window): values_selected = Signal(int, int, bool) + # Large ChatGPT with rewrite and bug fixes from me. def __init__(self): super(PreviewWindow, self).__init__() self.ui = Ui_Preview_Window() self.ui.setupUi(self) self.o = OptimaManager() + self.threadpool = QThreadPool() # Thread pool for managing worker threads + self.ui.QLabel.setAlignment(Qt.AlignCenter) - ## Ui interaction + + # UI interactions self.ui.load_Button.clicked.connect(self.browse_file) self.ui.update_Button.clicked.connect(self.update_preview) self.ui.close_Button.clicked.connect(self.close_window) @@ -724,12 +730,8 @@ class PreviewWindow(QMainWindow, Ui_Preview_Window): # Connect UI elements to `on_ui_change` self.ui.brightness_spinBox.valueChanged.connect(self.on_ui_change) self.ui.brightness_Slider.valueChanged.connect(self.on_ui_change) - #self.ui.reset_brightness_Button.clicked.connect(self.on_ui_change) - self.ui.contrast_spinBox.valueChanged.connect(self.on_ui_change) self.ui.contrast_Slider.valueChanged.connect(self.on_ui_change) - #self.ui.reset_contrast_Button.clicked.connect(self.on_ui_change) - self.ui.grayscale_checkBox.stateChanged.connect(self.on_ui_change) def on_ui_change(self): @@ -738,69 +740,48 @@ class PreviewWindow(QMainWindow, Ui_Preview_Window): self.update_preview() def browse_file(self): - file = QFileDialog.getOpenFileName(self, caption = "Select File", filter = ("Images (*.png *.webp *.jpg *.jpeg)")) + file = QFileDialog.getOpenFileName(self, caption="Select File", filter="Images (*.png *.webp *.jpg *.jpeg)") if file[0]: self.ui.image_path_lineEdit.setText(file[0]) self.update_preview() - def process_image(self, path): - """Loads and processes the image with modifications.""" - # Refactored by ChatGPT - if not os.path.isfile(path): - return None + def update_preview(self): + """Handles loading and displaying the image in a separate thread.""" + path = self.ui.image_path_lineEdit.text() - try: - img = self.o.process_image_object( - image_input_file=path, # Example: resize percentage - watermark="PREVIEW", - resize = 100, - grayscale=self.ui.grayscale_checkBox.isChecked(), - brightness=int(self.ui.brightness_spinBox.text()), - contrast=int(self.ui.contrast_spinBox.text()) - ) - return QPixmap.fromImage(img) - except Exception as e: - QMessageBox.warning(self, "Warning", "Error loading image...") - print(f"Error loading image...\n{e}") - return None + worker = ImageProcessorWorker( + path=path, + optima_manager=self.o, + brightness=int(self.ui.brightness_spinBox.text()), + contrast=int(self.ui.contrast_spinBox.text()), + grayscale=self.ui.grayscale_checkBox.isChecked(), + callback=self.display_image # Callback to update UI + ) + self.threadpool.start(worker) # Run worker in a thread def display_image(self, pixmap): """Adjusts the image to fit within the QLabel.""" - # ChatGPT if pixmap is None: + QMessageBox.warning(self, "Warning", "Error processing image...") return - # Get max available size (QLabel size) max_size = self.ui.QLabel.size() - max_width = max_size.width() - max_height = max_size.height() - - # Scale image to fit within the available space while maintaining aspect ratio - scaled_pixmap = pixmap.scaled( - max_width, max_height, - Qt.KeepAspectRatio, - Qt.SmoothTransformation - ) - - # Set the scaled image + scaled_pixmap = pixmap.scaled(max_size, Qt.KeepAspectRatio, Qt.SmoothTransformation) self.ui.QLabel.setPixmap(scaled_pixmap) - - # Adjust QLabel size to match image self.ui.QLabel.resize(scaled_pixmap.size()) - def update_preview(self): - """Handles loading and displaying the image.""" - # ChatGPT - path = self.ui.image_path_lineEdit.text() - pixmap = self.process_image(path) - self.display_image(pixmap) + def resizeEvent(self, event): + """Triggered when the preview window is resized.""" + file_path = self.ui.image_path_lineEdit.text() + if os.path.exists(file_path): + self.update_preview() # Re-process and display the image + super().resizeEvent(event) # Keep the default behavior def close_window(self): - # Emit the signal with the values from the spinboxes and checkbox - # chatgpt - if self.ui.checkBox.isChecked(): - self.values_selected.emit(self.ui.brightness_spinBox.value(), self.ui.contrast_spinBox.value(), self.ui.grayscale_checkBox.isChecked()) - self.close() + """Emits signal and closes the window.""" + if self.ui.checkBox.isChecked(): + self.values_selected.emit(self.ui.brightness_spinBox.value(), self.ui.contrast_spinBox.value(), self.ui.grayscale_checkBox.isChecked()) + self.close() class WorkerSignals(QObject): # ChatGPT @@ -851,6 +832,39 @@ class ImageProcessorRunnable(QRunnable): self.signals.finished.emit() +class ImageProcessorWorker(QRunnable): + """Worker class to load and process the image in a separate thread.""" + # ChatGPT + def __init__(self, path, optima_manager, brightness, contrast, grayscale, callback): + super().__init__() + self.path = path + self.optima_manager = optima_manager + self.brightness = brightness + self.contrast = contrast + self.grayscale = grayscale + self.callback = callback # Function to call when processing is done + + @Slot() + def run(self): + """Runs the image processing in a separate thread.""" + if not os.path.isfile(self.path): + self.callback(None) + return + + try: + img = self.optima_manager.process_image_object( + image_input_file=self.path, + watermark="PREVIEW", + resize=100, + grayscale=self.grayscale, + brightness=self.brightness, + contrast=self.contrast + ) + pixmap = QPixmap.fromImage(img) + self.callback(pixmap) + except Exception as e: + print(f"Error processing image: {e}") + self.callback(None) def main(): app = QtWidgets.QApplication(sys.argv) diff --git a/src/OptimaLab35/ui/preview_window.py b/src/OptimaLab35/ui/preview_window.py index 5d2a021..712c5aa 100644 --- a/src/OptimaLab35/ui/preview_window.py +++ b/src/OptimaLab35/ui/preview_window.py @@ -131,6 +131,7 @@ class Ui_Preview_Window(object): self.verticalLayout_3.setObjectName(u"verticalLayout_3") self.live_update = QCheckBox(self.widget_4) self.live_update.setObjectName(u"live_update") + self.live_update.setChecked(True) self.verticalLayout_3.addWidget(self.live_update) @@ -196,7 +197,7 @@ class Ui_Preview_Window(object): #endif // QT_CONFIG(tooltip) self.grayscale_checkBox.setText(QCoreApplication.translate("Preview_Window", u"Black n White", None)) #if QT_CONFIG(tooltip) - self.live_update.setToolTip(QCoreApplication.translate("Preview_Window", u"Enable live updates to immediately preview changes as you adjust brightness, contrast, or grayscale. Be aware that this might impact performance, especially with large images.", None)) + self.live_update.setToolTip(QCoreApplication.translate("Preview_Window", u"Live update applies changes instantly. If the app becomes unresponsive or lags, disable this option.", None)) #endif // QT_CONFIG(tooltip) self.live_update.setText(QCoreApplication.translate("Preview_Window", u"Live update", None)) #if QT_CONFIG(tooltip) diff --git a/src/OptimaLab35/ui/preview_window.ui b/src/OptimaLab35/ui/preview_window.ui index 33ad094..99fc876 100644 --- a/src/OptimaLab35/ui/preview_window.ui +++ b/src/OptimaLab35/ui/preview_window.ui @@ -191,11 +191,14 @@ - Enable live updates to immediately preview changes as you adjust brightness, contrast, or grayscale. Be aware that this might impact performance, especially with large images. + Live update applies changes instantly. If the app becomes unresponsive or lags, disable this option. Live update + + true +