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"))
log = <Logger GliAAns-UI (WARNING)>
class MainWindow(PyQt6.QtWidgets.QMainWindow):
 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.

MainWindow(context)
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.

context
settings
workspace_path
language_actions
threads
def set_language(self, lang_code):
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.

def clear_folder(self, folder_path, folder_name, return_to_import):
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.

def set_widgets(self, left_widget, right_widget):
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.

def copydelete_thread_error(self, msg):
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.

def copydelete_thread_success(self, msg, show=True):
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.

def closeEvent(self, event):
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.

def export_file_info(self):
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.

def new_thread(self, thread):
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.

def toggle_debug_log(self, checked):
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.