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
## 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.

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 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)

View file

@ -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)

View file

@ -191,11 +191,14 @@
<item>
<widget class="QCheckBox" name="live_update">
<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 name="text">
<string>Live update</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>