feat: Preview window now uses seperate thread.

This commit is contained in:
Mr Finchum 2025-02-04 12:20:40 +01:00
parent 89c40a8ca1
commit 8bb655eb40
4 changed files with 84 additions and 57 deletions

View file

@ -1,5 +1,14 @@
# Changelog # 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.x
### 0.9.1: Patch for Unsuccessful Successful Update ### 0.9.1: Patch for Unsuccessful Successful Update
- Addressed a rare issue where the package did not update correctly using the updater. - Addressed a rare issue where the package did not update correctly using the updater.

View file

@ -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.ui.simple_dialog import SimpleDialog # Import the SimpleDialog class
from OptimaLab35 import __version__ 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 import QtWidgets
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
@ -52,7 +52,7 @@ class OptimaLab35(QMainWindow, Ui_MainWindow):
self.sd = SimpleDialog() self.sd = SimpleDialog()
# Change UI elements # 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.set_title()
self.default_ui_layout() self.default_ui_layout()
self.define_gui_interaction() self.define_gui_interaction()
@ -501,10 +501,12 @@ class UpdaterWindow(QMainWindow, Ui_Updater_Window):
def start_long_press(self): def start_long_press(self):
"""Start the timer when button is pressed.""" """Start the timer when button is pressed."""
# brave AI
self.timer.start(1000) # 1-second long press self.timer.start(1000) # 1-second long press
def cancel_long_press(self): def cancel_long_press(self):
"""Cancel long press if released early.""" """Cancel long press if released early."""
# brave AI
self.timer.stop() self.timer.stop()
def toggle_dev_ui(self): 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_optima35_localversion.setText(self.optima35_localversion)
self.ui.label_latest_version.setText("Latest version") self.ui.label_latest_version.setText("Latest version")
self.ui.label_optimalab35_latestversion.setText(self.ol35_last_state[1]) self.ui.label_optimalab35_latestversion.setText("...")
self.ui.label_optima35_latestversion.setText(self.o35_last_state[1]) self.ui.label_optima35_latestversion.setText("...")
self.ui.update_and_restart_Button.setEnabled(False) self.ui.update_and_restart_Button.setEnabled(False)
@ -706,14 +708,18 @@ class UpdaterWindow(QMainWindow, Ui_Updater_Window):
class PreviewWindow(QMainWindow, Ui_Preview_Window): class PreviewWindow(QMainWindow, Ui_Preview_Window):
values_selected = Signal(int, int, bool) values_selected = Signal(int, int, bool)
# Large ChatGPT with rewrite and bug fixes from me.
def __init__(self): def __init__(self):
super(PreviewWindow, self).__init__() super(PreviewWindow, self).__init__()
self.ui = Ui_Preview_Window() self.ui = Ui_Preview_Window()
self.ui.setupUi(self) self.ui.setupUi(self)
self.o = OptimaManager() self.o = OptimaManager()
self.threadpool = QThreadPool() # Thread pool for managing worker threads
self.ui.QLabel.setAlignment(Qt.AlignCenter) self.ui.QLabel.setAlignment(Qt.AlignCenter)
## Ui interaction
# UI interactions
self.ui.load_Button.clicked.connect(self.browse_file) self.ui.load_Button.clicked.connect(self.browse_file)
self.ui.update_Button.clicked.connect(self.update_preview) self.ui.update_Button.clicked.connect(self.update_preview)
self.ui.close_Button.clicked.connect(self.close_window) 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` # Connect UI elements to `on_ui_change`
self.ui.brightness_spinBox.valueChanged.connect(self.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.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_spinBox.valueChanged.connect(self.on_ui_change)
self.ui.contrast_Slider.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) self.ui.grayscale_checkBox.stateChanged.connect(self.on_ui_change)
def on_ui_change(self): def on_ui_change(self):
@ -738,66 +740,45 @@ class PreviewWindow(QMainWindow, Ui_Preview_Window):
self.update_preview() self.update_preview()
def browse_file(self): 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]: if file[0]:
self.ui.image_path_lineEdit.setText(file[0]) self.ui.image_path_lineEdit.setText(file[0])
self.update_preview() self.update_preview()
def process_image(self, path): def update_preview(self):
"""Loads and processes the image with modifications.""" """Handles loading and displaying the image in a separate thread."""
# Refactored by ChatGPT path = self.ui.image_path_lineEdit.text()
if not os.path.isfile(path):
return None
try: worker = ImageProcessorWorker(
img = self.o.process_image_object( path=path,
image_input_file=path, # Example: resize percentage optima_manager=self.o,
watermark="PREVIEW",
resize = 100,
grayscale=self.ui.grayscale_checkBox.isChecked(),
brightness=int(self.ui.brightness_spinBox.text()), brightness=int(self.ui.brightness_spinBox.text()),
contrast=int(self.ui.contrast_spinBox.text()) contrast=int(self.ui.contrast_spinBox.text()),
grayscale=self.ui.grayscale_checkBox.isChecked(),
callback=self.display_image # Callback to update UI
) )
return QPixmap.fromImage(img) self.threadpool.start(worker) # Run worker in a thread
except Exception as e:
QMessageBox.warning(self, "Warning", "Error loading image...")
print(f"Error loading image...\n{e}")
return None
def display_image(self, pixmap): def display_image(self, pixmap):
"""Adjusts the image to fit within the QLabel.""" """Adjusts the image to fit within the QLabel."""
# ChatGPT
if pixmap is None: if pixmap is None:
QMessageBox.warning(self, "Warning", "Error processing image...")
return return
# Get max available size (QLabel size)
max_size = self.ui.QLabel.size() max_size = self.ui.QLabel.size()
max_width = max_size.width() scaled_pixmap = pixmap.scaled(max_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
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
self.ui.QLabel.setPixmap(scaled_pixmap) self.ui.QLabel.setPixmap(scaled_pixmap)
# Adjust QLabel size to match image
self.ui.QLabel.resize(scaled_pixmap.size()) self.ui.QLabel.resize(scaled_pixmap.size())
def update_preview(self): def resizeEvent(self, event):
"""Handles loading and displaying the image.""" """Triggered when the preview window is resized."""
# ChatGPT file_path = self.ui.image_path_lineEdit.text()
path = self.ui.image_path_lineEdit.text() if os.path.exists(file_path):
pixmap = self.process_image(path) self.update_preview() # Re-process and display the image
self.display_image(pixmap) super().resizeEvent(event) # Keep the default behavior
def close_window(self): def close_window(self):
# Emit the signal with the values from the spinboxes and checkbox """Emits signal and closes the window."""
# chatgpt
if self.ui.checkBox.isChecked(): 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.values_selected.emit(self.ui.brightness_spinBox.value(), self.ui.contrast_spinBox.value(), self.ui.grayscale_checkBox.isChecked())
self.close() self.close()
@ -851,6 +832,39 @@ class ImageProcessorRunnable(QRunnable):
self.signals.finished.emit() 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(): def main():
app = QtWidgets.QApplication(sys.argv) app = QtWidgets.QApplication(sys.argv)

View file

@ -131,6 +131,7 @@ class Ui_Preview_Window(object):
self.verticalLayout_3.setObjectName(u"verticalLayout_3") self.verticalLayout_3.setObjectName(u"verticalLayout_3")
self.live_update = QCheckBox(self.widget_4) self.live_update = QCheckBox(self.widget_4)
self.live_update.setObjectName(u"live_update") self.live_update.setObjectName(u"live_update")
self.live_update.setChecked(True)
self.verticalLayout_3.addWidget(self.live_update) self.verticalLayout_3.addWidget(self.live_update)
@ -196,7 +197,7 @@ class Ui_Preview_Window(object):
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.grayscale_checkBox.setText(QCoreApplication.translate("Preview_Window", u"Black n White", None)) self.grayscale_checkBox.setText(QCoreApplication.translate("Preview_Window", u"Black n White", None))
#if QT_CONFIG(tooltip) #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) #endif // QT_CONFIG(tooltip)
self.live_update.setText(QCoreApplication.translate("Preview_Window", u"Live update", None)) self.live_update.setText(QCoreApplication.translate("Preview_Window", u"Live update", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)

View file

@ -191,11 +191,14 @@
<item> <item>
<widget class="QCheckBox" name="live_update"> <widget class="QCheckBox" name="live_update">
<property name="toolTip"> <property name="toolTip">
<string>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.</string> <string>Live update applies changes instantly. If the app becomes unresponsive or lags, disable this option.</string>
</property> </property>
<property name="text"> <property name="text">
<string>Live update</string> <string>Live update</string>
</property> </property>
<property name="checked">
<bool>true</bool>
</property>
</widget> </widget>
</item> </item>
<item> <item>