From 17f08bc74fcfd6d51ebcffbe917c8b13c2293a21 Mon Sep 17 00:00:00 2001 From: Mr Finchum Date: Sun, 23 Mar 2025 21:48:46 +0000 Subject: [PATCH] refactor: Splitting Classes into Separate Files --- CHANGELOG.md | 7 +- TODO.md | 6 - src/OptimaLab35/__main__.py | 28 +- src/OptimaLab35/{gui.py => mainWindow.py} | 520 +--------------------- src/OptimaLab35/previewWindow.py | 167 +++++++ src/OptimaLab35/settingsWindow.py | 357 +++++++++++++++ 6 files changed, 568 insertions(+), 517 deletions(-) delete mode 100644 TODO.md rename src/OptimaLab35/{gui.py => mainWindow.py} (51%) create mode 100644 src/OptimaLab35/previewWindow.py create mode 100644 src/OptimaLab35/settingsWindow.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c60c01e..3ec2081 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,16 @@ # Changelog +### 1.2.x: Refactor +#### 1.2.0: Splitting Classes into Separate Files +- Refactored `gui.py`, which previously contained almost all the code, into multiple files. +- Each window's logic is now in its own file, improving code organization. +- Window layouts remain in `.ui` folder, while their logic is now properly separated. + ## 1.1.x ### 1.1.0: New Function in Preview Window - Added a new feature to the preview window: **Hold a button to temporarily view the original (unedited) image.** This makes it easier to compare changes. - Minor UI adjustments. - ## 1.0.x ### 1.0.1: Fixed spelling - Fixes spelling some places diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 468cd9b..0000000 --- a/TODO.md +++ /dev/null @@ -1,6 +0,0 @@ -# TODO - -## Improvments -### Preview window -Improve performence by changing when the images is updated. -Right now, when moving the slider, the image gets updated for every single step increase resulting in lagging diff --git a/src/OptimaLab35/__main__.py b/src/OptimaLab35/__main__.py index 14932be..512c7b3 100644 --- a/src/OptimaLab35/__main__.py +++ b/src/OptimaLab35/__main__.py @@ -1,4 +1,30 @@ -from .gui import main +import sys +from PySide6 import QtWidgets +from .utils.utility import Utilities +from .mainWindow import OptimaLab35 +from .const import ( + CONFIG_BASE_PATH +) + +def main(): + u = Utilities(CONFIG_BASE_PATH) + app_settings = u.load_settings() + app = QtWidgets.QApplication(sys.argv) + + try: + import qdarktheme + app_settings["theme"]["theme_pkg"] = True + except ImportError: + app_settings["theme"]["theme_pkg"] = False + + if app_settings["theme"]["use_custom_theme"] and app_settings["theme"]["theme_pkg"]: + qdarktheme.setup_theme(app_settings["theme"]["mode"].lower()) + + u.save_settings(app_settings) + + window = OptimaLab35() + window.show() + app.exec() if __name__ == "__main__": main() diff --git a/src/OptimaLab35/gui.py b/src/OptimaLab35/mainWindow.py similarity index 51% rename from src/OptimaLab35/gui.py rename to src/OptimaLab35/mainWindow.py index cf40df3..3006465 100644 --- a/src/OptimaLab35/gui.py +++ b/src/OptimaLab35/mainWindow.py @@ -1,23 +1,22 @@ -import sys import os from datetime import datetime - -from .ui import resources_rc -from PyPiUpdater import PyPiUpdater from optima35.core import OptimaManager -from .utils.utility import Utilities -from .ui.main_window import Ui_MainWindow -from .ui.preview_window import Ui_Preview_Window -from .ui.settings_window import Ui_Settings_Window -from .ui.exif_handler_window import ExifEditor -from .ui.simple_dialog import SimpleDialog # Import the SimpleDialog class from OptimaLab35 import __version__ from .const import ( APPLICATION_NAME, CONFIG_BASE_PATH ) +from .ui import resources_rc +from .previewWindow import PreviewWindow +from .settingsWindow import SettingsWindow + +from .utils.utility import Utilities +from .ui.main_window import Ui_MainWindow +from .ui.exif_handler_window import ExifEditor +from .ui.simple_dialog import SimpleDialog # Import the SimpleDialog class + from PySide6 import QtWidgets, QtCore from PySide6.QtCore import ( @@ -26,9 +25,7 @@ from PySide6.QtCore import ( Signal, QObject, QRegularExpression, - Qt, - QTimer, - Slot + Qt ) from PySide6.QtWidgets import ( @@ -38,7 +35,7 @@ from PySide6.QtWidgets import ( QFileDialog ) -from PySide6.QtGui import QPixmap, QRegularExpressionValidator, QIcon +from PySide6.QtGui import QRegularExpressionValidator, QIcon class OptimaLab35(QMainWindow, Ui_MainWindow): def __init__(self): @@ -99,8 +96,6 @@ class OptimaLab35(QMainWindow, Ui_MainWindow): validator = QRegularExpressionValidator(regex) self.ui.lat_lineEdit.setValidator(validator) self.ui.long_lineEdit.setValidator(validator) - #layout.addWidget(self.ui.lat_lineEdit) - #layout.addWidget(self.ui.long_lineEdit) # UI related function, changing parts, open, etc. def open_preview_window(self): @@ -501,441 +496,6 @@ class OptimaLab35(QMainWindow, Ui_MainWindow): QApplication.closeAllWindows() event.accept() -class SettingsWindow(QMainWindow, Ui_Settings_Window): - # Mixture of code by me, code/functions refactored by ChatGPT and code directly from ChatGPT - def __init__(self, optimalab35_localversion, optima35_localversion): - super(SettingsWindow, self).__init__() - self.ui = Ui_Settings_Window() - self.ui.setupUi(self) - self.u = Utilities(os.path.expanduser("~/.config/OptimaLab35")) - self.app_settings = self.u.load_settings() - self.dev_mode = True if optimalab35_localversion == "0.0.1" else False - from PyPiUpdater import PyPiUpdater - # Update log file location - self.update_log_file = os.path.expanduser("~/.config/OptimaLab35/update_log.json") - # Store local versions - self.optimalab35_localversion = optimalab35_localversion - self.optima35_localversion = optima35_localversion - - # Create PyPiUpdater instances - self.ppu_ol35 = PyPiUpdater("OptimaLab35", self.optimalab35_localversion, self.update_log_file) - self.ppu_o35 = PyPiUpdater("optima35", self.optima35_localversion, self.update_log_file) - self.ol35_last_state = self.ppu_ol35.get_last_state() - self.o35_last_state = self.ppu_o35.get_last_state() - - # Track which packages need an update - self.updates_available = {"OptimaLab35": False, "optima35": False} - - self.define_gui_interaction() - - def define_gui_interaction(self): - """Setup UI interactions.""" - # Updater related - self.ui.label_optimalab35_localversion.setText(self.optimalab35_localversion) - self.ui.label_optima35_localversion.setText(self.optima35_localversion) - - self.ui.label_latest_version.setText("Latest version") - self.ui.label_optimalab35_latestversion.setText("...") - self.ui.label_optima35_latestversion.setText("...") - - self.ui.update_and_restart_Button.setEnabled(False) - - # Connect buttons to functions - self.ui.check_for_update_Button.clicked.connect(self.check_for_updates) - self.ui.update_and_restart_Button.clicked.connect(self.update_and_restart) - self.ui.label_last_check.setText(f"Last check: {self.time_to_string(self.ol35_last_state[0])}") - self.ui.dev_widget.setVisible(False) - - # Timer for long press detection - self.timer = QTimer() - self.timer.setSingleShot(True) - self.timer.timeout.connect(self.toggle_dev_ui) - - # Connect button press/release - self.ui.check_for_update_Button.pressed.connect(self.start_long_press) - self.ui.check_for_update_Button.released.connect(self.cancel_long_press) - self.ui.label_5.setText('
  • Changelog
  • ') - self.ui.label_5.setOpenExternalLinks(True) - #settings related - self.load_settings_into_ui() - self.ui.reset_exif_Button.clicked.connect(self.ask_reset_exif) - self.ui.save_and_close_Button.clicked.connect(self.save_and_close) - self.ui.save_and_restart_Button.clicked.connect(self.save_and_restart) - - if os.name == "nt": # Disable restart app when windows. - self.ui.save_and_restart_Button.setVisible(False) - self.ui.restart_checkBox.setChecked(False) - self.ui.restart_checkBox.setVisible(False) - -# setting related - - def load_settings_into_ui(self): - """Loads the settings into the UI elements.""" - settings = self.app_settings - theme_mode = settings["theme"]["mode"] - use_custom_theme = settings["theme"]["use_custom_theme"] - pkg_available = settings["theme"]["theme_pkg"] - - if pkg_available: - index = self.ui.theme_selection_comboBox.findText(theme_mode, QtCore.Qt.MatchFlag.MatchExactly) - if index != -1: - self.ui.theme_selection_comboBox.setCurrentIndex(index) - self.ui.enable_theme_checkBox.setChecked(use_custom_theme) - self.ui.install_pkg_Button.setVisible(False) - self.ui.enable_theme_checkBox.setEnabled(True) - else: - self.ui.enable_theme_checkBox.setEnabled(False) - self.ui.install_pkg_Button.clicked.connect(self.install_theme_pkg) - - - def install_theme_pkg(self): - a = self.ppu_ol35.install_package("PyQtDarkTheme-fork") - self.ui.install_pkg_Button.setEnabled(False) - self.ui.install_pkg_Button.setText("Please wait...") - - msg_box = QMessageBox() - msg_box.setIcon(QMessageBox.Information) - msg_box.setWindowTitle("Message") - msg_box.setText(a[1]) - msg_box.setStandardButtons(QMessageBox.Ok) - msg_box.exec() - if a[0]: - self.app_settings["theme"]["theme_pkg"] = True - self.load_settings_into_ui() - else: - self.ui.install_pkg_Button.setEnabled(True) - self.ui.install_pkg_Button.setText("Try again?") - - def save_settings(self): - self.app_settings["theme"]["mode"] = self.ui.theme_selection_comboBox.currentText() - self.app_settings["theme"]["use_custom_theme"] = self.ui.enable_theme_checkBox.isChecked() - self.u.save_settings(self.app_settings) - - def save_and_close(self): - self.save_settings() - self.close() - - def save_and_restart(self): - msg = QMessageBox() - msg.setIcon(QMessageBox.Icon.Question) - msg.setWindowTitle("Confirm Reset") - msg.setText("Are you sure you want to restart the app?") - msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) - - # Show the message box and wait for the user's response - response = msg.exec() - - # Check response and perform action - if response == QMessageBox.StandardButton.Yes: - self.save_settings() - self.restart_program() - else: - pass # Do nothing if "No" is selected - - def ask_reset_exif(self): - """Shows a dialog to ask the user if they are sure about resetting EXIF options to default.""" - # Create a QMessageBox with a Yes/No question - msg = QMessageBox() - msg.setIcon(QMessageBox.Icon.Question) - msg.setWindowTitle("Confirm Reset") - msg.setText("Are you sure you want to reset the EXIF options to default?") - msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) - - # Show the message box and wait for the user's response - response = msg.exec() - - # Check response and perform action - if response == QMessageBox.StandardButton.Yes: - self.u.default_exif() # Reset EXIF options to default - else: - pass # Do nothing if "No" is selected - -# update related parts - 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): - """Show or hide the hidden UI when long press is detected.""" - self.ui.dev_widget.setVisible(True) - - self.ui.check_local_Button.clicked.connect(self.local_check_for_updates) - self.ui.update_local_Button.clicked.connect(self.local_update) - - def local_check_for_updates(self): - dist_folder = os.path.expanduser("~/.config/OptimaLab35/dist/") - self.ui.label_optimalab35_latestversion.setText("Checking...") - self.ui.label_optima35_latestversion.setText("Checking...") - - # Check OptimaLab35 update - ol35_pkg_info = self.ppu_ol35.check_update_local(dist_folder) - if ol35_pkg_info[0] is None: - self.ui.label_optimalab35_latestversion.setText(ol35_pkg_info[1][0:13]) - else: - self.ui.label_optimalab35_latestversion.setText(ol35_pkg_info[1]) - self.updates_available["OptimaLab35"] = ol35_pkg_info[0] - - # Check optima35 update - o35_pkg_info = self.ppu_o35.check_update_local(dist_folder) - if o35_pkg_info[0] is None: - self.ui.label_optima35_latestversion.setText(o35_pkg_info[1][0:13]) - else: - self.ui.label_optima35_latestversion.setText(o35_pkg_info[1]) - self.updates_available["optima35"] = o35_pkg_info[0] - - def local_update(self): - dist_folder = os.path.expanduser("~/.config/OptimaLab35/dist/") - packages_to_update = [pkg for pkg, update in self.updates_available.items() if update] - - if not packages_to_update: - QMessageBox.information(self, "Update", "No updates available.") - return - - # Confirm update - msg = QMessageBox() - msg.setWindowTitle("Update Available") - message = f"Updating: {', '.join(packages_to_update)}\nUpdate " - - if self.ui.restart_checkBox.isChecked(): - message = message + "and restart app?" - else: - message = message + "app?" - - msg.setText(message) - msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) - result = msg.exec() - - if result == QMessageBox.Yes: - update_results = [] # Store results - - for package in packages_to_update: - if package == "OptimaLab35": - pkg_info = self.ppu_ol35.update_from_local(dist_folder) - elif package == "optima35": - pkg_info = self.ppu_o35.update_from_local(dist_folder) - - update_results.append(f"{package}: {'Success' if pkg_info[0] else 'Failed'}\n{pkg_info[1]}") - - # Show summary of updates - # Show update completion message - msg = QMessageBox() - msg.setWindowTitle("Update Complete") - msg.setText("\n\n".join(update_results)) - msg.setStandardButtons(QMessageBox.Ok) - msg.exec() - - # Restart the application after user clicks "OK" - if self.ui.restart_checkBox.isChecked(): - self.restart_program() - - def time_to_string(self, time_time): - try: - dt_obj = datetime.fromtimestamp(time_time) - date_string = dt_obj.strftime("%d %h %H:%M") - return date_string - except TypeError: - return "Missing information" - - def check_for_updates(self): - """Check for updates and update the UI.""" - self.ui.check_for_update_Button.setEnabled(False) - self.ui.label_optimalab35_latestversion.setText("Checking...") - self.ui.label_optima35_latestversion.setText("Checking...") - - # Check OptimaLab35 update - ol35_pkg_info = self.ppu_ol35.check_for_update() - if ol35_pkg_info[0] is None: - self.ui.label_optimalab35_latestversion.setText(ol35_pkg_info[1][0:13]) - else: - self.ui.label_optimalab35_latestversion.setText(ol35_pkg_info[1]) - self.updates_available["OptimaLab35"] = ol35_pkg_info[0] - - # Check optima35 update - o35_pkg_info = self.ppu_o35.check_for_update() - if o35_pkg_info[0] is None: - self.ui.label_optima35_latestversion.setText(o35_pkg_info[1][0:13]) - else: - self.ui.label_optima35_latestversion.setText(o35_pkg_info[1]) - self.updates_available["optima35"] = o35_pkg_info[0] - - # Enable update button if any update is available - if any(self.updates_available.values()): - if self.dev_mode: - self.ui.update_and_restart_Button.setEnabled(False) - self.ui.update_and_restart_Button.setText("Update disabled") - else: - self.ui.update_and_restart_Button.setEnabled(True) - - last_date = self.time_to_string(self.ppu_ol35.get_last_state()[0]) - self.ui.label_last_check.setText(f"Last check: {last_date}") - self.ui.label_latest_version.setText("Online version") - self.ui.check_for_update_Button.setEnabled(True) - - def update_and_restart(self): - """Update selected packages and restart the application.""" - packages_to_update = [pkg for pkg, update in self.updates_available.items() if update] - - if not packages_to_update: - QMessageBox.information(self, "Update", "No updates available.") - return - - # Confirm update - msg = QMessageBox() - msg.setWindowTitle("Update Available") - message = f"Updating: {', '.join(packages_to_update)}\nUpdate " - - if self.ui.restart_checkBox.isChecked(): - message = message + "and restart app?" - else: - message = message + "app?" - - msg.setText(message) - msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) - result = msg.exec() - - if result == QMessageBox.Yes: - update_results = [] # Store results - - for package in packages_to_update: - if package == "OptimaLab35": - pkg_info = self.ppu_ol35.update_package() - elif package == "optima35": - pkg_info = self.ppu_o35.update_package() - - update_results.append(f"{package}: {'Success' if pkg_info[0] else 'Failed'}\n{pkg_info[1]}") - - # Show summary of updates - # Show update completion message - msg = QMessageBox() - msg.setWindowTitle("Update Complete") - msg.setText("\n\n".join(update_results)) - msg.setStandardButtons(QMessageBox.Ok) - msg.exec() - - # Restart the application after user clicks "OK" - if self.ui.restart_checkBox.isChecked(): - self.restart_program() - - def restart_program(self): - """Restart the Python program after an update.""" - print("Restarting the application...") - # Close all running Qt windows before restarting - app = QApplication.instance() - if app: - app.quit() - - python = sys.executable - os.execl(python, python, *sys.argv) - -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 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) - - self.ui.reset_brightness_Button.clicked.connect(lambda: self.ui.brightness_spinBox.setValue(0)) - self.ui.reset_contrast_Button.clicked.connect(lambda: self.ui.contrast_spinBox.setValue(0)) - - # 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.contrast_spinBox.valueChanged.connect(self.on_ui_change) - self.ui.contrast_Slider.valueChanged.connect(self.on_ui_change) - self.ui.grayscale_checkBox.stateChanged.connect(self.on_ui_change) - self.ui_elements(False) - self.ui.show_OG_Button.pressed.connect(self.show_OG_image) - self.ui.show_OG_Button.released.connect(self.update_preview) - - def on_ui_change(self): - """Triggers update only if live update is enabled.""" - if self.ui.live_update.isChecked(): - self.update_preview() - - def browse_file(self): - 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() - self.ui_elements(True) - - def show_OG_image(self): - """Handles loading and displaying the image in a separate thread.""" - path = self.ui.image_path_lineEdit.text() - - worker = ImageProcessorWorker( - path = path, - optima_manager = self.o, - brightness = 0, - contrast = 0, - grayscale = False, - resize = self.ui.scale_Slider.value(), - callback = self.display_image # Callback to update UI - ) - self.threadpool.start(worker) - - def ui_elements(self, state): - self.ui.groupBox_2.setEnabled(state) - self.ui.groupBox.setEnabled(state) - self.ui.groupBox_5.setEnabled(state) - self.ui.show_OG_Button.setEnabled(state) - - def update_preview(self): - """Handles loading and displaying the image in a separate thread.""" - path = self.ui.image_path_lineEdit.text() - - 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(), - resize = self.ui.scale_Slider.value(), - 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.""" - if pixmap is None: - QMessageBox.warning(self, "Warning", "Error processing image...") - return - - max_size = self.ui.QLabel.size() - scaled_pixmap = pixmap.scaled(max_size, Qt.KeepAspectRatio, Qt.SmoothTransformation) - self.ui.QLabel.setPixmap(scaled_pixmap) - self.ui.QLabel.resize(scaled_pixmap.size()) - - 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): - """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 progress = Signal(int) @@ -984,61 +544,3 @@ class ImageProcessorRunnable(QRunnable): self.signals.progress.emit(int((i / len(self.image_files)) * 100)) 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, resize, callback): - super().__init__() - self.path = path - self.optima_manager = optima_manager - self.brightness = brightness - self.contrast = contrast - self.grayscale = grayscale - self.resize = resize - 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 = self.resize, - 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(): - u = Utilities(os.path.expanduser(CONFIG_BASE_PATH)) - app_settings = u.load_settings() - app = QtWidgets.QApplication(sys.argv) - - try: - import qdarktheme - app_settings["theme"]["theme_pkg"] = True - except Exception: - app_settings["theme"]["theme_pkg"] = False - - if app_settings["theme"]["use_custom_theme"] and app_settings["theme"]["theme_pkg"]: - qdarktheme.setup_theme(app_settings["theme"]["mode"].lower()) - - u.save_settings(app_settings) - del u - window = OptimaLab35() - window.show() - app.exec() - -if __name__ == "__main__": - main() diff --git a/src/OptimaLab35/previewWindow.py b/src/OptimaLab35/previewWindow.py new file mode 100644 index 0000000..72469be --- /dev/null +++ b/src/OptimaLab35/previewWindow.py @@ -0,0 +1,167 @@ +import os +from optima35.core import OptimaManager + +from OptimaLab35 import __version__ + +from .ui import resources_rc +from .ui.preview_window import Ui_Preview_Window + +from PySide6 import QtWidgets, QtCore + +from PySide6.QtCore import ( + QRunnable, + QThreadPool, + Signal, + QObject, + QRegularExpression, + Qt, + QTimer, + Slot +) + +from PySide6.QtWidgets import ( + QMessageBox, + QApplication, + QMainWindow, + QFileDialog +) + +from PySide6.QtGui import QPixmap, QRegularExpressionValidator, QIcon + +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.setWindowIcon(QIcon(":app-icon.png")) + self.ui.QLabel.setAlignment(Qt.AlignCenter) + + # 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) + + self.ui.reset_brightness_Button.clicked.connect(lambda: self.ui.brightness_spinBox.setValue(0)) + self.ui.reset_contrast_Button.clicked.connect(lambda: self.ui.contrast_spinBox.setValue(0)) + + # 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.contrast_spinBox.valueChanged.connect(self.on_ui_change) + self.ui.contrast_Slider.valueChanged.connect(self.on_ui_change) + self.ui.grayscale_checkBox.stateChanged.connect(self.on_ui_change) + self.ui_elements(False) + self.ui.show_OG_Button.pressed.connect(self.show_OG_image) + self.ui.show_OG_Button.released.connect(self.update_preview) + + def on_ui_change(self): + """Triggers update only if live update is enabled.""" + if self.ui.live_update.isChecked(): + self.update_preview() + + def browse_file(self): + 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() + self.ui_elements(True) + + def show_OG_image(self): + """Handles loading and displaying the image in a separate thread.""" + path = self.ui.image_path_lineEdit.text() + + worker = ImageProcessorWorker( + path = path, + optima_manager = self.o, + brightness = 0, + contrast = 0, + grayscale = False, + resize = self.ui.scale_Slider.value(), + callback = self.display_image # Callback to update UI + ) + self.threadpool.start(worker) + + def ui_elements(self, state): + self.ui.groupBox_2.setEnabled(state) + self.ui.groupBox.setEnabled(state) + self.ui.groupBox_5.setEnabled(state) + self.ui.show_OG_Button.setEnabled(state) + + def update_preview(self): + """Handles loading and displaying the image in a separate thread.""" + path = self.ui.image_path_lineEdit.text() + + 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(), + resize = self.ui.scale_Slider.value(), + 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.""" + if pixmap is None: + QMessageBox.warning(self, "Warning", "Error processing image...") + return + + max_size = self.ui.QLabel.size() + scaled_pixmap = pixmap.scaled(max_size, Qt.KeepAspectRatio, Qt.SmoothTransformation) + self.ui.QLabel.setPixmap(scaled_pixmap) + self.ui.QLabel.resize(scaled_pixmap.size()) + + 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): + """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 ImageProcessorWorker(QRunnable): + """Worker class to load and process the image in a separate thread.""" + # ChatGPT + def __init__(self, path, optima_manager, brightness, contrast, grayscale, resize, callback): + super().__init__() + self.path = path + self.optima_manager = optima_manager + self.brightness = brightness + self.contrast = contrast + self.grayscale = grayscale + self.resize = resize + 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 = self.resize, + 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) diff --git a/src/OptimaLab35/settingsWindow.py b/src/OptimaLab35/settingsWindow.py new file mode 100644 index 0000000..df5d494 --- /dev/null +++ b/src/OptimaLab35/settingsWindow.py @@ -0,0 +1,357 @@ +import sys +import os +from datetime import datetime +from PyPiUpdater import PyPiUpdater + +from OptimaLab35 import __version__ +from .const import ( + CONFIG_BASE_PATH +) + +from .ui import resources_rc +from .utils.utility import Utilities +from .ui.settings_window import Ui_Settings_Window + +from PySide6 import QtWidgets, QtCore + +from PySide6.QtCore import ( + QRegularExpression, + Qt, + QTimer +) + +from PySide6.QtWidgets import ( + QMessageBox, + QApplication, + QMainWindow +) + +from PySide6.QtGui import QIcon + +class SettingsWindow(QMainWindow, Ui_Settings_Window): + # Mixture of code by me, code/functions refactored by ChatGPT and code directly from ChatGPT + def __init__(self, optimalab35_localversion, optima35_localversion): + super(SettingsWindow, self).__init__() + self.ui = Ui_Settings_Window() + self.ui.setupUi(self) + self.u = Utilities(os.path.expanduser(CONFIG_BASE_PATH)) + self.app_settings = self.u.load_settings() + self.dev_mode = True if optimalab35_localversion == "0.0.1" else False + self.setWindowIcon(QIcon(":app-icon.png")) + + # Update log file location + self.update_log_file = os.path.expanduser(f"{CONFIG_BASE_PATH}/update_log.json") + # Store local versions + self.optimalab35_localversion = optimalab35_localversion + self.optima35_localversion = optima35_localversion + # Create PyPiUpdater instances + self.ppu_ol35 = PyPiUpdater("OptimaLab35", self.optimalab35_localversion, self.update_log_file) + self.ppu_o35 = PyPiUpdater("optima35", self.optima35_localversion, self.update_log_file) + self.ol35_last_state = self.ppu_ol35.get_last_state() + self.o35_last_state = self.ppu_o35.get_last_state() + # Track which packages need an update + self.updates_available = {"OptimaLab35": False, "optima35": False} + self.define_gui_interaction() + + def define_gui_interaction(self): + """Setup UI interactions.""" + # Updater related + self.ui.label_optimalab35_localversion.setText(self.optimalab35_localversion) + self.ui.label_optima35_localversion.setText(self.optima35_localversion) + + self.ui.label_latest_version.setText("Latest version") + self.ui.label_optimalab35_latestversion.setText("...") + self.ui.label_optima35_latestversion.setText("...") + + self.ui.update_and_restart_Button.setEnabled(False) + + # Connect buttons to functions + self.ui.check_for_update_Button.clicked.connect(self.check_for_updates) + self.ui.update_and_restart_Button.clicked.connect(self.update_and_restart) + self.ui.label_last_check.setText(f"Last check: {self.time_to_string(self.ol35_last_state[0])}") + self.ui.dev_widget.setVisible(False) + + # Timer for long press detection + self.timer = QTimer() + self.timer.setSingleShot(True) + self.timer.timeout.connect(self.toggle_dev_ui) + + # Connect button press/release + self.ui.check_for_update_Button.pressed.connect(self.start_long_press) + self.ui.check_for_update_Button.released.connect(self.cancel_long_press) + self.ui.label_5.setText('
  • Changelog
  • ') + self.ui.label_5.setOpenExternalLinks(True) + #settings related + self.load_settings_into_ui() + self.ui.reset_exif_Button.clicked.connect(self.ask_reset_exif) + self.ui.save_and_close_Button.clicked.connect(self.save_and_close) + self.ui.save_and_restart_Button.clicked.connect(self.save_and_restart) + + if os.name == "nt": # Disable restart app when windows. + self.ui.save_and_restart_Button.setVisible(False) + self.ui.restart_checkBox.setChecked(False) + self.ui.restart_checkBox.setVisible(False) + +# setting related + def load_settings_into_ui(self): + """Loads the settings into the UI elements.""" + settings = self.app_settings + theme_mode = settings["theme"]["mode"] + use_custom_theme = settings["theme"]["use_custom_theme"] + pkg_available = settings["theme"]["theme_pkg"] + + if pkg_available: + index = self.ui.theme_selection_comboBox.findText(theme_mode, QtCore.Qt.MatchFlag.MatchExactly) + if index != -1: + self.ui.theme_selection_comboBox.setCurrentIndex(index) + self.ui.enable_theme_checkBox.setChecked(use_custom_theme) + self.ui.install_pkg_Button.setVisible(False) + self.ui.enable_theme_checkBox.setEnabled(True) + else: + self.ui.enable_theme_checkBox.setEnabled(False) + self.ui.install_pkg_Button.clicked.connect(self.install_theme_pkg) + + def install_theme_pkg(self): + a = self.ppu_ol35.install_package("PyQtDarkTheme-fork") + self.ui.install_pkg_Button.setEnabled(False) + self.ui.install_pkg_Button.setText("Please wait...") + + msg_box = QMessageBox() + msg_box.setIcon(QMessageBox.Information) + msg_box.setWindowTitle("Message") + msg_box.setText(a[1]) + msg_box.setStandardButtons(QMessageBox.Ok) + msg_box.exec() + if a[0]: + self.app_settings["theme"]["theme_pkg"] = True + self.load_settings_into_ui() + else: + self.ui.install_pkg_Button.setEnabled(True) + self.ui.install_pkg_Button.setText("Try again?") + + def save_settings(self): + self.app_settings["theme"]["mode"] = self.ui.theme_selection_comboBox.currentText() + self.app_settings["theme"]["use_custom_theme"] = self.ui.enable_theme_checkBox.isChecked() + self.u.save_settings(self.app_settings) + + def save_and_close(self): + self.save_settings() + self.close() + + def save_and_restart(self): + msg = QMessageBox() + msg.setIcon(QMessageBox.Icon.Question) + msg.setWindowTitle("Confirm Reset") + msg.setText("Are you sure you want to restart the app?") + msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) + + # Show the message box and wait for the user's response + response = msg.exec() + + # Check response and perform action + if response == QMessageBox.StandardButton.Yes: + self.save_settings() + self.restart_program() + else: + pass # Do nothing if "No" is selected + + def ask_reset_exif(self): + """Shows a dialog to ask the user if they are sure about resetting EXIF options to default.""" + # Create a QMessageBox with a Yes/No question + msg = QMessageBox() + msg.setIcon(QMessageBox.Icon.Question) + msg.setWindowTitle("Confirm Reset") + msg.setText("Are you sure you want to reset the EXIF options to default?") + msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) + + # Show the message box and wait for the user's response + response = msg.exec() + + # Check response and perform action + if response == QMessageBox.StandardButton.Yes: + self.u.default_exif() # Reset EXIF options to default + else: + pass # Do nothing if "No" is selected + +# update related parts + 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): + """Show or hide the hidden UI when long press is detected.""" + self.ui.dev_widget.setVisible(True) + + self.ui.check_local_Button.clicked.connect(self.local_check_for_updates) + self.ui.update_local_Button.clicked.connect(self.local_update) + + def local_check_for_updates(self): + dist_folder = os.path.expanduser("~/.config/OptimaLab35/dist/") + self.ui.label_optimalab35_latestversion.setText("Checking...") + self.ui.label_optima35_latestversion.setText("Checking...") + + # Check OptimaLab35 update + ol35_pkg_info = self.ppu_ol35.check_update_local(dist_folder) + if ol35_pkg_info[0] is None: + self.ui.label_optimalab35_latestversion.setText(ol35_pkg_info[1][0:13]) + else: + self.ui.label_optimalab35_latestversion.setText(ol35_pkg_info[1]) + self.updates_available["OptimaLab35"] = ol35_pkg_info[0] + + # Check optima35 update + o35_pkg_info = self.ppu_o35.check_update_local(dist_folder) + if o35_pkg_info[0] is None: + self.ui.label_optima35_latestversion.setText(o35_pkg_info[1][0:13]) + else: + self.ui.label_optima35_latestversion.setText(o35_pkg_info[1]) + self.updates_available["optima35"] = o35_pkg_info[0] + + def local_update(self): + dist_folder = os.path.expanduser("~/.config/OptimaLab35/dist/") + packages_to_update = [pkg for pkg, update in self.updates_available.items() if update] + + if not packages_to_update: + QMessageBox.information(self, "Update", "No updates available.") + return + + # Confirm update + msg = QMessageBox() + msg.setWindowTitle("Update Available") + message = f"Updating: {', '.join(packages_to_update)}\nUpdate " + + if self.ui.restart_checkBox.isChecked(): + message = message + "and restart app?" + else: + message = message + "app?" + + msg.setText(message) + msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) + result = msg.exec() + + if result == QMessageBox.Yes: + update_results = [] # Store results + + for package in packages_to_update: + if package == "OptimaLab35": + pkg_info = self.ppu_ol35.update_from_local(dist_folder) + elif package == "optima35": + pkg_info = self.ppu_o35.update_from_local(dist_folder) + + update_results.append(f"{package}: {'Success' if pkg_info[0] else 'Failed'}\n{pkg_info[1]}") + + # Show summary of updates + # Show update completion message + msg = QMessageBox() + msg.setWindowTitle("Update Complete") + msg.setText("\n\n".join(update_results)) + msg.setStandardButtons(QMessageBox.Ok) + msg.exec() + + # Restart the application after user clicks "OK" + if self.ui.restart_checkBox.isChecked(): + self.restart_program() + + def time_to_string(self, time_time): + try: + dt_obj = datetime.fromtimestamp(time_time) + date_string = dt_obj.strftime("%d %h %H:%M") + return date_string + except TypeError: + return "Missing information" + + def check_for_updates(self): + """Check for updates and update the UI.""" + self.ui.check_for_update_Button.setEnabled(False) + self.ui.label_optimalab35_latestversion.setText("Checking...") + self.ui.label_optima35_latestversion.setText("Checking...") + + # Check OptimaLab35 update + ol35_pkg_info = self.ppu_ol35.check_for_update() + if ol35_pkg_info[0] is None: + self.ui.label_optimalab35_latestversion.setText(ol35_pkg_info[1][0:13]) + else: + self.ui.label_optimalab35_latestversion.setText(ol35_pkg_info[1]) + self.updates_available["OptimaLab35"] = ol35_pkg_info[0] + + # Check optima35 update + o35_pkg_info = self.ppu_o35.check_for_update() + if o35_pkg_info[0] is None: + self.ui.label_optima35_latestversion.setText(o35_pkg_info[1][0:13]) + else: + self.ui.label_optima35_latestversion.setText(o35_pkg_info[1]) + self.updates_available["optima35"] = o35_pkg_info[0] + + # Enable update button if any update is available + if any(self.updates_available.values()): + if self.dev_mode: + self.ui.update_and_restart_Button.setEnabled(False) + self.ui.update_and_restart_Button.setText("Update disabled") + else: + self.ui.update_and_restart_Button.setEnabled(True) + + last_date = self.time_to_string(self.ppu_ol35.get_last_state()[0]) + self.ui.label_last_check.setText(f"Last check: {last_date}") + self.ui.label_latest_version.setText("Online version") + self.ui.check_for_update_Button.setEnabled(True) + + def update_and_restart(self): + """Update selected packages and restart the application.""" + packages_to_update = [pkg for pkg, update in self.updates_available.items() if update] + + if not packages_to_update: + QMessageBox.information(self, "Update", "No updates available.") + return + + # Confirm update + msg = QMessageBox() + msg.setWindowTitle("Update Available") + message = f"Updating: {', '.join(packages_to_update)}\nUpdate " + + if self.ui.restart_checkBox.isChecked(): + message = message + "and restart app?" + else: + message = message + "app?" + + msg.setText(message) + msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) + result = msg.exec() + + if result == QMessageBox.Yes: + update_results = [] # Store results + + for package in packages_to_update: + if package == "OptimaLab35": + pkg_info = self.ppu_ol35.update_package() + elif package == "optima35": + pkg_info = self.ppu_o35.update_package() + + update_results.append(f"{package}: {'Success' if pkg_info[0] else 'Failed'}\n{pkg_info[1]}") + + # Show summary of updates + # Show update completion message + msg = QMessageBox() + msg.setWindowTitle("Update Complete") + msg.setText("\n\n".join(update_results)) + msg.setStandardButtons(QMessageBox.Ok) + msg.exec() + + # Restart the application after user clicks "OK" + if self.ui.restart_checkBox.isChecked(): + self.restart_program() + + def restart_program(self): + """Restart the Python program after an update.""" + print("Restarting the application...") + # Close all running Qt windows before restarting + app = QApplication.instance() + if app: + app.quit() + + python = sys.executable + os.execl(python, python, *sys.argv)