Coverage for main/ui/main_window.py: 96%
183 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-03 15:51 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-03 15:51 +0000
1import logging
2import os
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
12from threads.utils_threads import CopyDeleteThread
13from logger import get_logger, set_log_level
15log = get_logger()
18class MainWindow(QMainWindow):
19 """
20 Main application window for the GliAAns UI.
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)
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 """
40 def __init__(self, context):
41 """
42 Initialize the main window and setup UI components.
44 Args:
45 context (dict): Application context containing shared data and signals.
46 """
47 super().__init__()
48 self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
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 = {}
56 self.threads = []
58 # Initialize UI and menus
59 self._setup_ui()
60 self._setup_menus()
61 self._translate_ui()
63 # Closing the app when this window is closed
64 self.destroyed.connect(QApplication.instance().quit)
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)
74 central_widget = QtWidgets.QWidget()
75 self.setCentralWidget(central_widget)
76 self.main_layout = QVBoxLayout(central_widget)
78 self._setup_splitter()
79 self._setup_footer()
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)
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)
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)
97 self.main_layout.addWidget(self.footer)
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)
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)
109 self.export_action = QAction("Export file/folder", self)
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)
116 self.import_file_action.triggered.connect(lambda: self.context["tree_view"].add_file_to_workspace(folder_path=None,is_dir=False))
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")
123 self.export_action.triggered.connect(self.export_file_info)
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)
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)
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 )
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)
157 self._add_language_option("English", "en")
158 self._add_language_option("Italiano", "it")
160 self.debug_log_action = QAction("Debug Log", self)
161 self.debug_log_action.setCheckable(True)
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)
169 # --- Help menu ---
170 self.help_menu = self.menu_bar.addMenu("Help")
172 def _add_language_option(self, name, code):
173 """
174 Add a selectable language option to the language menu.
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
186 # Restore saved language preference
187 if self.settings.value("language", "en", type=str) == code:
188 self.language_actions[code].setChecked(True)
190 def set_language(self, lang_code):
191 """
192 Emit a signal to update the UI language.
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)
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.
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)
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 )
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 )
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()
245 log.info(f"{folder_name} emptied")
247 if return_to_import and "return_to_import" in self.context:
248 self.context["return_to_import"]()
250 def set_widgets(self, left_widget, right_widget):
251 """
252 Set the main splitter widgets (left = navigation, right = content).
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)
271 def copydelete_thread_error(self, msg):
272 """
273 Handle errors emitted by background copy/delete threads.
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}")
285 def copydelete_thread_success(self, msg, show=True):
286 """
287 Handle successful completion of copy/delete threads.
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 )
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)
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...")
316 while self.threads:
317 thread = self.threads.pop(0)
319 log.debug(f"Waiting for thread {thread} to finish...")
321 try:
322 thread.finished.disconnect()
323 except TypeError:
324 pass
326 try:
327 thread.error.disconnect()
328 except TypeError:
329 pass
331 thread.wait(5000)
333 log.debug("All threads stopped. Accepting close event.")
335 event.accept()
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 )
345 def new_thread(self, thread):
346 """
347 Add and start a new background thread for copy/delete operations.
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()
357 def toggle_debug_log(self, checked):
358 """
359 Enable or disable debug logging dynamically.
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)
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"))
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"))
388 self.next_button.setText(QtCore.QCoreApplication.translate("MainWindow", "Next"))
389 self.back_button.setText(QtCore.QCoreApplication.translate("MainWindow", "Back"))