ui.main_window
1import logging 2import os 3 4from PyQt6 import QtCore, QtWidgets 5from PyQt6.QtCore import Qt 6from PyQt6.QtWidgets import ( 7 QMainWindow, QVBoxLayout, 8 QSplitter, QMenuBar, QHBoxLayout, QSizePolicy, QMessageBox, QApplication, 9) 10from PyQt6.QtGui import QAction, QActionGroup 11 12from threads.utils_threads import CopyDeleteThread 13from logger import get_logger, set_log_level 14 15log = get_logger() 16 17 18class MainWindow(QMainWindow): 19 """ 20 Main application window for the GliAAns UI. 21 22 This class represents the primary interface window of the application. 23 It handles: 24 - UI setup and layout management 25 - Menu bar and actions 26 - Language switching 27 - Threaded file operations (copy/delete) 28 - Logging level control 29 - Workspace management (clear/export) 30 31 Attributes: 32 context (dict): Shared context containing references to settings, workspace path, 33 signals, and external UI components. 34 settings (QSettings): Application settings instance. 35 workspace_path (str): Path to the active workspace. 36 language_actions (dict): Mapping between language codes and QAction objects. 37 threads (list): List of active `CopyDeleteThread` instances. 38 """ 39 40 def __init__(self, context): 41 """ 42 Initialize the main window and setup UI components. 43 44 Args: 45 context (dict): Application context containing shared data and signals. 46 """ 47 super().__init__() 48 self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True) 49 50 self.context = context 51 self.context["language_changed"].connect(self._translate_ui) 52 self.settings = self.context["settings"] 53 self.workspace_path = self.context["workspace_path"] 54 self.language_actions = {} 55 56 self.threads = [] 57 58 # Initialize UI and menus 59 self._setup_ui() 60 self._setup_menus() 61 self._translate_ui() 62 63 # Closing the app when this window is closed 64 self.destroyed.connect(QApplication.instance().quit) 65 66 # -------------------------- 67 # UI SETUP 68 # -------------------------- 69 def _setup_ui(self): 70 """Configure the main window layout and structure.""" 71 self.resize(950, 700) 72 self.setMinimumSize(950, 700) 73 74 central_widget = QtWidgets.QWidget() 75 self.setCentralWidget(central_widget) 76 self.main_layout = QVBoxLayout(central_widget) 77 78 self._setup_splitter() 79 self._setup_footer() 80 81 def _setup_splitter(self): 82 """Setup the horizontal splitter dividing the main UI panels.""" 83 self.splitter = QSplitter(QtCore.Qt.Orientation.Horizontal) 84 self.main_layout.addWidget(self.splitter) 85 86 def _setup_footer(self): 87 """Create and configure the footer containing navigation buttons.""" 88 self.footer = QtWidgets.QWidget() 89 self.footer.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum) 90 self.footer_layout = QHBoxLayout(self.footer) 91 self.footer_layout.setContentsMargins(0, 0, 0, 0) 92 93 self.next_button, self.back_button = self.context["create_buttons"]() 94 self.footer_layout.addWidget(self.back_button, 0, Qt.AlignmentFlag.AlignLeft) 95 self.footer_layout.addWidget(self.next_button, 0, Qt.AlignmentFlag.AlignRight) 96 97 self.main_layout.addWidget(self.footer) 98 99 def _setup_menus(self): 100 """Initialize and configure the menu bar and all menu actions.""" 101 self.menu_bar = QMenuBar() 102 self.setMenuBar(self.menu_bar) 103 104 # --- File menu --- 105 self.file_menu = self.menu_bar.addMenu("File") 106 self.import_file_action = QAction("Import file",self) 107 self.import_folder_action = QAction("Import Folder", self) 108 109 self.export_action = QAction("Export file/folder", self) 110 111 self.file_menu.addAction(self.import_file_action) 112 self.file_menu.addSeparator() 113 self.file_menu.addAction(self.import_folder_action) 114 self.file_menu.addAction(self.export_action) 115 116 self.import_file_action.triggered.connect(lambda: self.context["tree_view"].add_file_to_workspace(folder_path=None,is_dir=False)) 117 118 if "import_page" in self.context and self.context["import_page"]: 119 self.import_folder_action.triggered.connect(self.context["import_page"].open_folder_dialog) 120 else: 121 log.critical("Import page not in context") 122 123 self.export_action.triggered.connect(self.export_file_info) 124 125 126 # --- Workspace menu --- 127 self.workspace_menu = self.menu_bar.addMenu("Workspace") 128 self.clear_all_action = QAction("Clear workspace", self) 129 self.export_workspace_action = QAction("Export workspace", self) 130 self.clear_pipeline_outputs_action = QAction("Clear pipeline outputs", self) 131 132 self.workspace_menu.addAction(self.clear_all_action) 133 self.workspace_menu.addAction(self.export_workspace_action) 134 self.workspace_menu.addSeparator() 135 self.workspace_menu.addAction(self.clear_pipeline_outputs_action) 136 137 self.clear_all_action.triggered.connect( 138 lambda: self.clear_folder(folder_path=self.workspace_path, 139 folder_name="workspace", 140 return_to_import=True) 141 ) 142 self.export_workspace_action.triggered.connect( 143 lambda: self.context["tree_view"].export_files(self.workspace_path, is_dir=True) 144 ) 145 self.clear_pipeline_outputs_action.triggered.connect( 146 lambda: self.clear_folder(folder_path=os.path.join(self.workspace_path, "pipeline"), 147 folder_name="outputs", 148 return_to_import=False) 149 ) 150 151 # --- Settings menu --- 152 self.settings_menu = self.menu_bar.addMenu("Settings") 153 self.language_menu = self.settings_menu.addMenu("Language") 154 self.language_action_group = QActionGroup(self) 155 self.language_action_group.setExclusive(True) 156 157 self._add_language_option("English", "en") 158 self._add_language_option("Italiano", "it") 159 160 self.debug_log_action = QAction("Debug Log", self) 161 self.debug_log_action.setCheckable(True) 162 163 # Restore saved debug setting (default: False) 164 debug_enabled = self.settings.value("debug_log", False, type=bool) 165 self.debug_log_action.setChecked(debug_enabled) 166 self.debug_log_action.toggled.connect(self.toggle_debug_log) 167 self.settings_menu.addAction(self.debug_log_action) 168 169 # --- Help menu --- 170 self.help_menu = self.menu_bar.addMenu("Help") 171 172 def _add_language_option(self, name, code): 173 """ 174 Add a selectable language option to the language menu. 175 176 Args: 177 name (str): Human-readable language name. 178 code (str): Language code (e.g., 'en', 'it'). 179 """ 180 action = QAction(name, self, checkable=True) 181 self.language_action_group.addAction(action) 182 self.language_menu.addAction(action) 183 action.triggered.connect(lambda: self.set_language(code)) 184 self.language_actions[code] = action 185 186 # Restore saved language preference 187 if self.settings.value("language", "en", type=str) == code: 188 self.language_actions[code].setChecked(True) 189 190 def set_language(self, lang_code): 191 """ 192 Emit a signal to update the UI language. 193 194 Args: 195 lang_code (str): ISO code of the selected language. 196 """ 197 self.context["language_changed"].emit(lang_code) 198 if lang_code in self.language_actions: 199 self.language_actions[lang_code].setChecked(True) 200 201 def clear_folder(self, folder_path, folder_name, return_to_import): 202 """ 203 Clear all files/folders within a given directory using threaded deletion. 204 205 Args: 206 folder_path (str): Target folder path to clear. 207 folder_name (str): User-friendly name for the confirmation dialog. 208 return_to_import (bool): If True, returns the user to the import page after clearing. 209 """ 210 message = QtCore.QCoreApplication.translate( 211 "MainWindow", 212 "Are you sure you want to delete completely {0}?\n" 213 ).format(folder_name) 214 215 if return_to_import: 216 message += QtCore.QCoreApplication.translate( 217 "MainWindow", 218 "WARNING: All data will be removed and you will be returned to the import page." 219 ) 220 221 reply = QMessageBox.question( 222 self, 223 QtCore.QCoreApplication.translate("MainWindow", "Confirm deletion"), 224 message, 225 QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, 226 QMessageBox.StandardButton.No 227 ) 228 229 if reply == QMessageBox.StandardButton.Yes: 230 for item in os.listdir(folder_path): 231 item_path = os.path.join(folder_path, item) 232 self.threads.append(CopyDeleteThread( 233 src=item_path, 234 is_folder=os.path.isdir(item_path), 235 delete=True 236 )) 237 self.threads[-1].error.connect( 238 lambda msg, it=item: self.copydelete_thread_error(f"Error while clearing {it}: {msg}") 239 ) 240 self.threads[-1].finished.connect( 241 lambda msg, show=False: self.copydelete_thread_success(msg, show) 242 ) 243 self.threads[-1].start() 244 245 log.info(f"{folder_name} emptied") 246 247 if return_to_import and "return_to_import" in self.context: 248 self.context["return_to_import"]() 249 250 def set_widgets(self, left_widget, right_widget): 251 """ 252 Set the main splitter widgets (left = navigation, right = content). 253 254 Args: 255 left_widget (QWidget): Left-side panel widget (e.g., tree view). 256 right_widget (QWidget): Right-side content widget. 257 """ 258 if self.splitter.count() > 1: 259 self.splitter.replaceWidget(1, right_widget) 260 self.splitter.setSizes([200, 600]) 261 self.left_panel.adjust_tree_columns() 262 else: 263 self.splitter.addWidget(left_widget) 264 self.left_panel = left_widget 265 self.splitter.addWidget(right_widget) 266 self.right_panel = right_widget 267 self.splitter.setSizes([200, 600]) 268 self.left_panel.adjust_tree_columns() 269 self.left_panel.new_thread.connect(self.new_thread) 270 271 def copydelete_thread_error(self, msg): 272 """ 273 Handle errors emitted by background copy/delete threads. 274 275 Args: 276 msg (str): Error message from the thread. 277 """ 278 QMessageBox.warning(self, "Error", msg) 279 log.error(msg) 280 thread_to_remove = self.sender() 281 if thread_to_remove in self.threads: 282 self.threads.remove(thread_to_remove) 283 log.error(f"Error while copying/deleting: {msg}") 284 285 def copydelete_thread_success(self, msg, show=True): 286 """ 287 Handle successful completion of copy/delete threads. 288 289 Args: 290 msg (str): Success message from the thread. 291 show (bool): If True, display a success message to the user. 292 """ 293 if show: 294 QMessageBox.information( 295 self, 296 QtCore.QCoreApplication.translate("MainWindow", "Success!"), 297 msg 298 ) 299 300 thread_to_remove = self.sender() 301 if self.context and "update_main_buttons" in self.context: 302 self.context["update_main_buttons"]() 303 log.debug("Success: " + msg) 304 if thread_to_remove in self.threads: 305 self.threads.remove(thread_to_remove) 306 307 def closeEvent(self, event): 308 """ 309 Override close event to safely terminate all running threads. 310 Args: 311 event (QCloseEvent): The close event. 312 """ 313 if hasattr(self, "threads"): 314 log.debug(f"Close event triggered. Waiting for {len(self.threads)} active threads...") 315 316 while self.threads: 317 thread = self.threads.pop(0) 318 319 log.debug(f"Waiting for thread {thread} to finish...") 320 321 try: 322 thread.finished.disconnect() 323 except TypeError: 324 pass 325 326 try: 327 thread.error.disconnect() 328 except TypeError: 329 pass 330 331 thread.wait(5000) 332 333 log.debug("All threads stopped. Accepting close event.") 334 335 event.accept() 336 337 def export_file_info(self): 338 """Display information about how to export files/folders.""" 339 QMessageBox.information( 340 self, 341 QtCore.QCoreApplication.translate("MainWindow", "Export file info"), 342 QtCore.QCoreApplication.translate("MainWindow", "To export a file/folder, right click on it in the left view") 343 ) 344 345 def new_thread(self, thread): 346 """ 347 Add and start a new background thread for copy/delete operations. 348 349 Args: 350 thread (CopyDeleteThread): Thread instance to start. 351 """ 352 self.threads.append(thread) 353 self.threads[-1].error.connect(lambda msg: self.copydelete_thread_error(f"Error: {msg}")) 354 self.threads[-1].finished.connect(self.copydelete_thread_success) 355 self.threads[-1].start() 356 357 def toggle_debug_log(self, checked): 358 """ 359 Enable or disable debug logging dynamically. 360 361 Args: 362 checked (bool): True to enable debug logging, False to disable. 363 """ 364 self.settings.setValue("debug_log", checked) 365 if checked: 366 set_log_level(logging.DEBUG) 367 else: 368 set_log_level(logging.ERROR) 369 370 def _translate_ui(self): 371 """Update all translatable UI text elements based on the current language.""" 372 self.setWindowTitle(QtCore.QCoreApplication.translate("MainWindow", "GliAAns UI")) 373 self.file_menu.setTitle(QtCore.QCoreApplication.translate("MainWindow", "File")) 374 self.workspace_menu.setTitle(QtCore.QCoreApplication.translate("MainWindow", "Workspace")) 375 self.settings_menu.setTitle(QtCore.QCoreApplication.translate("MainWindow", "Settings")) 376 self.help_menu.setTitle(QtCore.QCoreApplication.translate("MainWindow", "Help")) 377 self.language_menu.setTitle(QtCore.QCoreApplication.translate("MainWindow", "Language")) 378 379 self.import_folder_action.setText(QtCore.QCoreApplication.translate("MainWindow", "Import folder")) 380 #self.import_folder_action.setText(QtCore.QCoreApplication.translate("MainWindow", "Import file")) 381 self.export_action.setText(QtCore.QCoreApplication.translate("MainWindow", "Export file/folder")) 382 self.clear_all_action.setText(QtCore.QCoreApplication.translate("MainWindow", "Clear workspace")) 383 self.export_workspace_action.setText(QtCore.QCoreApplication.translate("MainWindow", "Export Workspace")) 384 self.clear_pipeline_outputs_action.setText(QtCore.QCoreApplication.translate("MainWindow", "Clear pipeline outputs")) 385 self.language_actions["en"].setText(QtCore.QCoreApplication.translate("MainWindow", "English")) 386 self.language_actions["it"].setText(QtCore.QCoreApplication.translate("MainWindow", "Italiano")) 387 388 self.next_button.setText(QtCore.QCoreApplication.translate("MainWindow", "Next")) 389 self.back_button.setText(QtCore.QCoreApplication.translate("MainWindow", "Back"))
19class MainWindow(QMainWindow): 20 """ 21 Main application window for the GliAAns UI. 22 23 This class represents the primary interface window of the application. 24 It handles: 25 - UI setup and layout management 26 - Menu bar and actions 27 - Language switching 28 - Threaded file operations (copy/delete) 29 - Logging level control 30 - Workspace management (clear/export) 31 32 Attributes: 33 context (dict): Shared context containing references to settings, workspace path, 34 signals, and external UI components. 35 settings (QSettings): Application settings instance. 36 workspace_path (str): Path to the active workspace. 37 language_actions (dict): Mapping between language codes and QAction objects. 38 threads (list): List of active `CopyDeleteThread` instances. 39 """ 40 41 def __init__(self, context): 42 """ 43 Initialize the main window and setup UI components. 44 45 Args: 46 context (dict): Application context containing shared data and signals. 47 """ 48 super().__init__() 49 self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True) 50 51 self.context = context 52 self.context["language_changed"].connect(self._translate_ui) 53 self.settings = self.context["settings"] 54 self.workspace_path = self.context["workspace_path"] 55 self.language_actions = {} 56 57 self.threads = [] 58 59 # Initialize UI and menus 60 self._setup_ui() 61 self._setup_menus() 62 self._translate_ui() 63 64 # Closing the app when this window is closed 65 self.destroyed.connect(QApplication.instance().quit) 66 67 # -------------------------- 68 # UI SETUP 69 # -------------------------- 70 def _setup_ui(self): 71 """Configure the main window layout and structure.""" 72 self.resize(950, 700) 73 self.setMinimumSize(950, 700) 74 75 central_widget = QtWidgets.QWidget() 76 self.setCentralWidget(central_widget) 77 self.main_layout = QVBoxLayout(central_widget) 78 79 self._setup_splitter() 80 self._setup_footer() 81 82 def _setup_splitter(self): 83 """Setup the horizontal splitter dividing the main UI panels.""" 84 self.splitter = QSplitter(QtCore.Qt.Orientation.Horizontal) 85 self.main_layout.addWidget(self.splitter) 86 87 def _setup_footer(self): 88 """Create and configure the footer containing navigation buttons.""" 89 self.footer = QtWidgets.QWidget() 90 self.footer.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum) 91 self.footer_layout = QHBoxLayout(self.footer) 92 self.footer_layout.setContentsMargins(0, 0, 0, 0) 93 94 self.next_button, self.back_button = self.context["create_buttons"]() 95 self.footer_layout.addWidget(self.back_button, 0, Qt.AlignmentFlag.AlignLeft) 96 self.footer_layout.addWidget(self.next_button, 0, Qt.AlignmentFlag.AlignRight) 97 98 self.main_layout.addWidget(self.footer) 99 100 def _setup_menus(self): 101 """Initialize and configure the menu bar and all menu actions.""" 102 self.menu_bar = QMenuBar() 103 self.setMenuBar(self.menu_bar) 104 105 # --- File menu --- 106 self.file_menu = self.menu_bar.addMenu("File") 107 self.import_file_action = QAction("Import file",self) 108 self.import_folder_action = QAction("Import Folder", self) 109 110 self.export_action = QAction("Export file/folder", self) 111 112 self.file_menu.addAction(self.import_file_action) 113 self.file_menu.addSeparator() 114 self.file_menu.addAction(self.import_folder_action) 115 self.file_menu.addAction(self.export_action) 116 117 self.import_file_action.triggered.connect(lambda: self.context["tree_view"].add_file_to_workspace(folder_path=None,is_dir=False)) 118 119 if "import_page" in self.context and self.context["import_page"]: 120 self.import_folder_action.triggered.connect(self.context["import_page"].open_folder_dialog) 121 else: 122 log.critical("Import page not in context") 123 124 self.export_action.triggered.connect(self.export_file_info) 125 126 127 # --- Workspace menu --- 128 self.workspace_menu = self.menu_bar.addMenu("Workspace") 129 self.clear_all_action = QAction("Clear workspace", self) 130 self.export_workspace_action = QAction("Export workspace", self) 131 self.clear_pipeline_outputs_action = QAction("Clear pipeline outputs", self) 132 133 self.workspace_menu.addAction(self.clear_all_action) 134 self.workspace_menu.addAction(self.export_workspace_action) 135 self.workspace_menu.addSeparator() 136 self.workspace_menu.addAction(self.clear_pipeline_outputs_action) 137 138 self.clear_all_action.triggered.connect( 139 lambda: self.clear_folder(folder_path=self.workspace_path, 140 folder_name="workspace", 141 return_to_import=True) 142 ) 143 self.export_workspace_action.triggered.connect( 144 lambda: self.context["tree_view"].export_files(self.workspace_path, is_dir=True) 145 ) 146 self.clear_pipeline_outputs_action.triggered.connect( 147 lambda: self.clear_folder(folder_path=os.path.join(self.workspace_path, "pipeline"), 148 folder_name="outputs", 149 return_to_import=False) 150 ) 151 152 # --- Settings menu --- 153 self.settings_menu = self.menu_bar.addMenu("Settings") 154 self.language_menu = self.settings_menu.addMenu("Language") 155 self.language_action_group = QActionGroup(self) 156 self.language_action_group.setExclusive(True) 157 158 self._add_language_option("English", "en") 159 self._add_language_option("Italiano", "it") 160 161 self.debug_log_action = QAction("Debug Log", self) 162 self.debug_log_action.setCheckable(True) 163 164 # Restore saved debug setting (default: False) 165 debug_enabled = self.settings.value("debug_log", False, type=bool) 166 self.debug_log_action.setChecked(debug_enabled) 167 self.debug_log_action.toggled.connect(self.toggle_debug_log) 168 self.settings_menu.addAction(self.debug_log_action) 169 170 # --- Help menu --- 171 self.help_menu = self.menu_bar.addMenu("Help") 172 173 def _add_language_option(self, name, code): 174 """ 175 Add a selectable language option to the language menu. 176 177 Args: 178 name (str): Human-readable language name. 179 code (str): Language code (e.g., 'en', 'it'). 180 """ 181 action = QAction(name, self, checkable=True) 182 self.language_action_group.addAction(action) 183 self.language_menu.addAction(action) 184 action.triggered.connect(lambda: self.set_language(code)) 185 self.language_actions[code] = action 186 187 # Restore saved language preference 188 if self.settings.value("language", "en", type=str) == code: 189 self.language_actions[code].setChecked(True) 190 191 def set_language(self, lang_code): 192 """ 193 Emit a signal to update the UI language. 194 195 Args: 196 lang_code (str): ISO code of the selected language. 197 """ 198 self.context["language_changed"].emit(lang_code) 199 if lang_code in self.language_actions: 200 self.language_actions[lang_code].setChecked(True) 201 202 def clear_folder(self, folder_path, folder_name, return_to_import): 203 """ 204 Clear all files/folders within a given directory using threaded deletion. 205 206 Args: 207 folder_path (str): Target folder path to clear. 208 folder_name (str): User-friendly name for the confirmation dialog. 209 return_to_import (bool): If True, returns the user to the import page after clearing. 210 """ 211 message = QtCore.QCoreApplication.translate( 212 "MainWindow", 213 "Are you sure you want to delete completely {0}?\n" 214 ).format(folder_name) 215 216 if return_to_import: 217 message += QtCore.QCoreApplication.translate( 218 "MainWindow", 219 "WARNING: All data will be removed and you will be returned to the import page." 220 ) 221 222 reply = QMessageBox.question( 223 self, 224 QtCore.QCoreApplication.translate("MainWindow", "Confirm deletion"), 225 message, 226 QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, 227 QMessageBox.StandardButton.No 228 ) 229 230 if reply == QMessageBox.StandardButton.Yes: 231 for item in os.listdir(folder_path): 232 item_path = os.path.join(folder_path, item) 233 self.threads.append(CopyDeleteThread( 234 src=item_path, 235 is_folder=os.path.isdir(item_path), 236 delete=True 237 )) 238 self.threads[-1].error.connect( 239 lambda msg, it=item: self.copydelete_thread_error(f"Error while clearing {it}: {msg}") 240 ) 241 self.threads[-1].finished.connect( 242 lambda msg, show=False: self.copydelete_thread_success(msg, show) 243 ) 244 self.threads[-1].start() 245 246 log.info(f"{folder_name} emptied") 247 248 if return_to_import and "return_to_import" in self.context: 249 self.context["return_to_import"]() 250 251 def set_widgets(self, left_widget, right_widget): 252 """ 253 Set the main splitter widgets (left = navigation, right = content). 254 255 Args: 256 left_widget (QWidget): Left-side panel widget (e.g., tree view). 257 right_widget (QWidget): Right-side content widget. 258 """ 259 if self.splitter.count() > 1: 260 self.splitter.replaceWidget(1, right_widget) 261 self.splitter.setSizes([200, 600]) 262 self.left_panel.adjust_tree_columns() 263 else: 264 self.splitter.addWidget(left_widget) 265 self.left_panel = left_widget 266 self.splitter.addWidget(right_widget) 267 self.right_panel = right_widget 268 self.splitter.setSizes([200, 600]) 269 self.left_panel.adjust_tree_columns() 270 self.left_panel.new_thread.connect(self.new_thread) 271 272 def copydelete_thread_error(self, msg): 273 """ 274 Handle errors emitted by background copy/delete threads. 275 276 Args: 277 msg (str): Error message from the thread. 278 """ 279 QMessageBox.warning(self, "Error", msg) 280 log.error(msg) 281 thread_to_remove = self.sender() 282 if thread_to_remove in self.threads: 283 self.threads.remove(thread_to_remove) 284 log.error(f"Error while copying/deleting: {msg}") 285 286 def copydelete_thread_success(self, msg, show=True): 287 """ 288 Handle successful completion of copy/delete threads. 289 290 Args: 291 msg (str): Success message from the thread. 292 show (bool): If True, display a success message to the user. 293 """ 294 if show: 295 QMessageBox.information( 296 self, 297 QtCore.QCoreApplication.translate("MainWindow", "Success!"), 298 msg 299 ) 300 301 thread_to_remove = self.sender() 302 if self.context and "update_main_buttons" in self.context: 303 self.context["update_main_buttons"]() 304 log.debug("Success: " + msg) 305 if thread_to_remove in self.threads: 306 self.threads.remove(thread_to_remove) 307 308 def closeEvent(self, event): 309 """ 310 Override close event to safely terminate all running threads. 311 Args: 312 event (QCloseEvent): The close event. 313 """ 314 if hasattr(self, "threads"): 315 log.debug(f"Close event triggered. Waiting for {len(self.threads)} active threads...") 316 317 while self.threads: 318 thread = self.threads.pop(0) 319 320 log.debug(f"Waiting for thread {thread} to finish...") 321 322 try: 323 thread.finished.disconnect() 324 except TypeError: 325 pass 326 327 try: 328 thread.error.disconnect() 329 except TypeError: 330 pass 331 332 thread.wait(5000) 333 334 log.debug("All threads stopped. Accepting close event.") 335 336 event.accept() 337 338 def export_file_info(self): 339 """Display information about how to export files/folders.""" 340 QMessageBox.information( 341 self, 342 QtCore.QCoreApplication.translate("MainWindow", "Export file info"), 343 QtCore.QCoreApplication.translate("MainWindow", "To export a file/folder, right click on it in the left view") 344 ) 345 346 def new_thread(self, thread): 347 """ 348 Add and start a new background thread for copy/delete operations. 349 350 Args: 351 thread (CopyDeleteThread): Thread instance to start. 352 """ 353 self.threads.append(thread) 354 self.threads[-1].error.connect(lambda msg: self.copydelete_thread_error(f"Error: {msg}")) 355 self.threads[-1].finished.connect(self.copydelete_thread_success) 356 self.threads[-1].start() 357 358 def toggle_debug_log(self, checked): 359 """ 360 Enable or disable debug logging dynamically. 361 362 Args: 363 checked (bool): True to enable debug logging, False to disable. 364 """ 365 self.settings.setValue("debug_log", checked) 366 if checked: 367 set_log_level(logging.DEBUG) 368 else: 369 set_log_level(logging.ERROR) 370 371 def _translate_ui(self): 372 """Update all translatable UI text elements based on the current language.""" 373 self.setWindowTitle(QtCore.QCoreApplication.translate("MainWindow", "GliAAns UI")) 374 self.file_menu.setTitle(QtCore.QCoreApplication.translate("MainWindow", "File")) 375 self.workspace_menu.setTitle(QtCore.QCoreApplication.translate("MainWindow", "Workspace")) 376 self.settings_menu.setTitle(QtCore.QCoreApplication.translate("MainWindow", "Settings")) 377 self.help_menu.setTitle(QtCore.QCoreApplication.translate("MainWindow", "Help")) 378 self.language_menu.setTitle(QtCore.QCoreApplication.translate("MainWindow", "Language")) 379 380 self.import_folder_action.setText(QtCore.QCoreApplication.translate("MainWindow", "Import folder")) 381 #self.import_folder_action.setText(QtCore.QCoreApplication.translate("MainWindow", "Import file")) 382 self.export_action.setText(QtCore.QCoreApplication.translate("MainWindow", "Export file/folder")) 383 self.clear_all_action.setText(QtCore.QCoreApplication.translate("MainWindow", "Clear workspace")) 384 self.export_workspace_action.setText(QtCore.QCoreApplication.translate("MainWindow", "Export Workspace")) 385 self.clear_pipeline_outputs_action.setText(QtCore.QCoreApplication.translate("MainWindow", "Clear pipeline outputs")) 386 self.language_actions["en"].setText(QtCore.QCoreApplication.translate("MainWindow", "English")) 387 self.language_actions["it"].setText(QtCore.QCoreApplication.translate("MainWindow", "Italiano")) 388 389 self.next_button.setText(QtCore.QCoreApplication.translate("MainWindow", "Next")) 390 self.back_button.setText(QtCore.QCoreApplication.translate("MainWindow", "Back"))
Main application window for the GliAAns UI.
This class represents the primary interface window of the application. It handles:
- UI setup and layout management
- Menu bar and actions
- Language switching
- Threaded file operations (copy/delete)
- Logging level control
- Workspace management (clear/export)
Attributes:
context (dict): Shared context containing references to settings, workspace path,
signals, and external UI components.
settings (QSettings): Application settings instance.
workspace_path (str): Path to the active workspace.
language_actions (dict): Mapping between language codes and QAction objects.
threads (list): List of active CopyDeleteThread instances.
41 def __init__(self, context): 42 """ 43 Initialize the main window and setup UI components. 44 45 Args: 46 context (dict): Application context containing shared data and signals. 47 """ 48 super().__init__() 49 self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True) 50 51 self.context = context 52 self.context["language_changed"].connect(self._translate_ui) 53 self.settings = self.context["settings"] 54 self.workspace_path = self.context["workspace_path"] 55 self.language_actions = {} 56 57 self.threads = [] 58 59 # Initialize UI and menus 60 self._setup_ui() 61 self._setup_menus() 62 self._translate_ui() 63 64 # Closing the app when this window is closed 65 self.destroyed.connect(QApplication.instance().quit)
Initialize the main window and setup UI components.
Args: context (dict): Application context containing shared data and signals.
191 def set_language(self, lang_code): 192 """ 193 Emit a signal to update the UI language. 194 195 Args: 196 lang_code (str): ISO code of the selected language. 197 """ 198 self.context["language_changed"].emit(lang_code) 199 if lang_code in self.language_actions: 200 self.language_actions[lang_code].setChecked(True)
Emit a signal to update the UI language.
Args: lang_code (str): ISO code of the selected language.
202 def clear_folder(self, folder_path, folder_name, return_to_import): 203 """ 204 Clear all files/folders within a given directory using threaded deletion. 205 206 Args: 207 folder_path (str): Target folder path to clear. 208 folder_name (str): User-friendly name for the confirmation dialog. 209 return_to_import (bool): If True, returns the user to the import page after clearing. 210 """ 211 message = QtCore.QCoreApplication.translate( 212 "MainWindow", 213 "Are you sure you want to delete completely {0}?\n" 214 ).format(folder_name) 215 216 if return_to_import: 217 message += QtCore.QCoreApplication.translate( 218 "MainWindow", 219 "WARNING: All data will be removed and you will be returned to the import page." 220 ) 221 222 reply = QMessageBox.question( 223 self, 224 QtCore.QCoreApplication.translate("MainWindow", "Confirm deletion"), 225 message, 226 QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, 227 QMessageBox.StandardButton.No 228 ) 229 230 if reply == QMessageBox.StandardButton.Yes: 231 for item in os.listdir(folder_path): 232 item_path = os.path.join(folder_path, item) 233 self.threads.append(CopyDeleteThread( 234 src=item_path, 235 is_folder=os.path.isdir(item_path), 236 delete=True 237 )) 238 self.threads[-1].error.connect( 239 lambda msg, it=item: self.copydelete_thread_error(f"Error while clearing {it}: {msg}") 240 ) 241 self.threads[-1].finished.connect( 242 lambda msg, show=False: self.copydelete_thread_success(msg, show) 243 ) 244 self.threads[-1].start() 245 246 log.info(f"{folder_name} emptied") 247 248 if return_to_import and "return_to_import" in self.context: 249 self.context["return_to_import"]()
Clear all files/folders within a given directory using threaded deletion.
Args: folder_path (str): Target folder path to clear. folder_name (str): User-friendly name for the confirmation dialog. return_to_import (bool): If True, returns the user to the import page after clearing.
251 def set_widgets(self, left_widget, right_widget): 252 """ 253 Set the main splitter widgets (left = navigation, right = content). 254 255 Args: 256 left_widget (QWidget): Left-side panel widget (e.g., tree view). 257 right_widget (QWidget): Right-side content widget. 258 """ 259 if self.splitter.count() > 1: 260 self.splitter.replaceWidget(1, right_widget) 261 self.splitter.setSizes([200, 600]) 262 self.left_panel.adjust_tree_columns() 263 else: 264 self.splitter.addWidget(left_widget) 265 self.left_panel = left_widget 266 self.splitter.addWidget(right_widget) 267 self.right_panel = right_widget 268 self.splitter.setSizes([200, 600]) 269 self.left_panel.adjust_tree_columns() 270 self.left_panel.new_thread.connect(self.new_thread)
Set the main splitter widgets (left = navigation, right = content).
Args: left_widget (QWidget): Left-side panel widget (e.g., tree view). right_widget (QWidget): Right-side content widget.
272 def copydelete_thread_error(self, msg): 273 """ 274 Handle errors emitted by background copy/delete threads. 275 276 Args: 277 msg (str): Error message from the thread. 278 """ 279 QMessageBox.warning(self, "Error", msg) 280 log.error(msg) 281 thread_to_remove = self.sender() 282 if thread_to_remove in self.threads: 283 self.threads.remove(thread_to_remove) 284 log.error(f"Error while copying/deleting: {msg}")
Handle errors emitted by background copy/delete threads.
Args: msg (str): Error message from the thread.
286 def copydelete_thread_success(self, msg, show=True): 287 """ 288 Handle successful completion of copy/delete threads. 289 290 Args: 291 msg (str): Success message from the thread. 292 show (bool): If True, display a success message to the user. 293 """ 294 if show: 295 QMessageBox.information( 296 self, 297 QtCore.QCoreApplication.translate("MainWindow", "Success!"), 298 msg 299 ) 300 301 thread_to_remove = self.sender() 302 if self.context and "update_main_buttons" in self.context: 303 self.context["update_main_buttons"]() 304 log.debug("Success: " + msg) 305 if thread_to_remove in self.threads: 306 self.threads.remove(thread_to_remove)
Handle successful completion of copy/delete threads.
Args: msg (str): Success message from the thread. show (bool): If True, display a success message to the user.
308 def closeEvent(self, event): 309 """ 310 Override close event to safely terminate all running threads. 311 Args: 312 event (QCloseEvent): The close event. 313 """ 314 if hasattr(self, "threads"): 315 log.debug(f"Close event triggered. Waiting for {len(self.threads)} active threads...") 316 317 while self.threads: 318 thread = self.threads.pop(0) 319 320 log.debug(f"Waiting for thread {thread} to finish...") 321 322 try: 323 thread.finished.disconnect() 324 except TypeError: 325 pass 326 327 try: 328 thread.error.disconnect() 329 except TypeError: 330 pass 331 332 thread.wait(5000) 333 334 log.debug("All threads stopped. Accepting close event.") 335 336 event.accept()
Override close event to safely terminate all running threads. Args: event (QCloseEvent): The close event.
338 def export_file_info(self): 339 """Display information about how to export files/folders.""" 340 QMessageBox.information( 341 self, 342 QtCore.QCoreApplication.translate("MainWindow", "Export file info"), 343 QtCore.QCoreApplication.translate("MainWindow", "To export a file/folder, right click on it in the left view") 344 )
Display information about how to export files/folders.
346 def new_thread(self, thread): 347 """ 348 Add and start a new background thread for copy/delete operations. 349 350 Args: 351 thread (CopyDeleteThread): Thread instance to start. 352 """ 353 self.threads.append(thread) 354 self.threads[-1].error.connect(lambda msg: self.copydelete_thread_error(f"Error: {msg}")) 355 self.threads[-1].finished.connect(self.copydelete_thread_success) 356 self.threads[-1].start()
Add and start a new background thread for copy/delete operations.
Args: thread (CopyDeleteThread): Thread instance to start.
358 def toggle_debug_log(self, checked): 359 """ 360 Enable or disable debug logging dynamically. 361 362 Args: 363 checked (bool): True to enable debug logging, False to disable. 364 """ 365 self.settings.setValue("debug_log", checked) 366 if checked: 367 set_log_level(logging.DEBUG) 368 else: 369 set_log_level(logging.ERROR)
Enable or disable debug logging dynamically.
Args: checked (bool): True to enable debug logging, False to disable.