fosKeyMan
Loading...
Searching...
No Matches
mainwindow.py
Go to the documentation of this file.
2r"""
3Defines the main Graphical User Interface.
4
5\author Xiali Song, Bertram Richter
6\date 2025
7"""
8
9import logging
10import shutil
11import sys
12import os
13import json
14import webbrowser
15import zipfile
16
17from PySide6.QtGui import QIcon, QColor, QBrush
18from PySide6.QtSvgWidgets import QSvgWidget
19from PySide6.QtWidgets import QApplication, QMainWindow, QMessageBox, QTableWidgetItem, QHeaderView, QDialog, \
20 QFileDialog, QPushButton, QWidget, QHBoxLayout, QLabel, QVBoxLayout, QLineEdit, QComboBox, QDateEdit
21from PySide6.QtCore import QTranslator, Qt, QCoreApplication, QSize, QDate
22
23from frontend.columnconfigurator import ColumnConfigurator
24from frontend.metadataeditor import MetadataEditor
25from frontend.renamesensor import RenameSensor
26from frontend.trashmanager import TrashManager
27from frontend.ui.uimain import Ui_MainWindow
28from backend.keyhandler import KeyHandler
29from backend.foldercontent import FolderContent
30from frontend.configmanager import ConfigManager
31from frontend.hoverinfo import HoverInfo
32from frontend.tableoperator import TableOperator
33from frontend.keystatus import ActivationStatus
34from frontend.ui.uiopen import Ui_Open
35from frontend.ui.uitrash import Ui_Trash
36from utils.utils import format_json_to_html
37
38logging.basicConfig(
39 filename='fosKeyManOperation.log',
40 level=logging.INFO,
41 format='%(asctime)s - %(levelname)s - %(message)s',
42 datefmt='%Y-%m-%d %H:%M:%S'
43)
44
45
46class MainWindow(QMainWindow):
47 r"""
48 Main User Interface for the application.
49
50 This class represents the main window of the application, which includes setting up the UI, loading configurations,
51 initializing handlers, setting up the table, and connecting various actions for user interaction.
52 """
53 def __init__(self):
54 super().__init__()
55
57 self.ui.setupUi(self)
58 self.directory1 = None
59 self.directory2 = None
60 self.directory3 = None
61 self.key_handler = None
62 self.folder_content = None
63 self.config_manager = ConfigManager(file_path('fosKeyManConfig.json'))
64 self.directory1, self.directory2, self.directory3, self.language, self.custom_columns = self.config_manager.check_and_load_previous_config()
65 self.translator = QTranslator(self)
66 self.table_operator = TableOperator(self.ui.tableWidget)
67 self.connect_actions()
68 self.hover_info = HoverInfo(self.ui.tableWidget, self)
69 self.ui.tableWidget.cellClicked.connect(self.table_cell_info)
70 self.ui.searchPushButton.clicked.connect(self.execute_search)
71 self.setWindowIcon(QIcon(resource_path('resources/foskeyman_logo_short.svg')))
74
76 r"""
77 Dynamically adapts window size to available screen space.
78 """
79 screen = self.screen()
80 screen_size = screen.availableGeometry()
81
82 width = int(screen_size.width() * 0.7)
83 height = int(screen_size.height() * 0.6)
84
85 self.resize(width, height)
86
88 r"""
89 Show the main window and check the configuration. Switch the language based on the saved configuration.
90
91 If all required paths (directory1, directory2) are valid, it will set up the table.
92 If any of the paths are missing, it will open the setting dialog for the user to initialize the configuration.
93 """
94 self.show()
95 self.switch_language(self.language)
96 if self.directory1 and self.directory2 and self.directory3:
98 self.setup_table()
100 else:
102
104 r"""
105 Dynamically set up the filter dock widget based on the current table columns.
106 """
107 layout = self.ui.filterFormLayout
108
109 while layout.rowCount():
110 layout.removeRow(0)
111
112 self.dynamic_filter_inputs = {}
113
114 status_label = QLabel(self.tr("Status"), self)
115 status_combo = QComboBox(self)
116 status_combo.addItems([
117 self.tr("All"),
118 self.tr("Activated"),
119 self.tr("Deactivated")
120 ])
121 layout.addRow(status_label, status_combo)
122 self.dynamic_filter_inputs["status"] = status_combo
123
124 serial_label = QLabel(self.tr("Serial Number"), self)
125 serial_input = QLineEdit(self)
126 layout.addRow(serial_label, serial_input)
127 self.dynamic_filter_inputs["serial_number"] = serial_input
128
129 name_label = QLabel(self.tr("Sensor Name"), self)
130 name_input = QLineEdit(self)
131 layout.addRow(name_label, name_input)
132 self.dynamic_filter_inputs["sensor_name"] = name_input
133
134 for col in self.custom_columns:
135 label = QLabel(col, self)
136 line_edit = QLineEdit(self)
137 line_edit.setObjectName(f"{col.lower().replace(' ', '')}LineEdit")
138 layout.addRow(label, line_edit)
139 self.dynamic_filter_inputs[col] = line_edit
140
141 date_container = QWidget(self)
142 date_layout = QHBoxLayout(date_container)
143 date_layout.setContentsMargins(0, 0, 0, 0)
144
145 start_label = QLabel(self.tr("Start"), self)
146 start_date_edit = QDateEdit(self)
147 start_date_edit.setDisplayFormat("yyyy-MM-dd")
148 start_date_edit.setCalendarPopup(True)
149 start_date_edit.setDate(QDate(2000, 1, 1))
150
151 end_label = QLabel(self.tr("End"), self)
152 end_date_edit = QDateEdit(self)
153 end_date_edit.setDisplayFormat("yyyy-MM-dd")
154 end_date_edit.setCalendarPopup(True)
155 end_date_edit.setDate(QDate.currentDate())
156
157 date_layout.addWidget(start_label)
158 date_layout.addWidget(start_date_edit)
159 date_layout.addWidget(end_label)
160 date_layout.addWidget(end_date_edit)
161
162 layout.addRow(date_container)
163 self.dynamic_filter_inputs["start"] = start_date_edit
164 self.dynamic_filter_inputs["end"] = end_date_edit
165
167 r"""
168 Connect UI actions to corresponding methods.
169 """
170 # actions for switch language
171 self.ui.actionEnglish.triggered.connect(lambda: self.switch_language('english'))
172 self.ui.actionGerman.triggered.connect(lambda: self.switch_language('german'))
173 # actions for table setup (connect directory)
174 self.ui.actionOpen.triggered.connect(self.open_setting_dialog)
175 # activate, deactivate, select all actions
176 self.ui.actionActive.triggered.connect(self.toggle_activation)
177 self.ui.actionDeactive.triggered.connect(self.toggle_deactivation)
178 self.ui.actionSelectAll.triggered.connect(self.table_operator.check_all_boxes)
179 # actions for right side tool widget
180 self.ui.actionFilter.triggered.connect(self.open_filter_widget)
181 self.ui.actionInformation.triggered.connect(self.open_info_widget)
182 self.ui.filterDockWidget.visibilityChanged.connect(self.ui.actionFilter.setChecked)
183 self.ui.infoDockWidget.visibilityChanged.connect(self.ui.actionInformation.setChecked)
184 self.ui.actionSearch.triggered.connect(self.open_search_widget)
185 self.ui.searchDockWidget.visibilityChanged.connect(self.ui.actionSearch.setChecked)
186 # actions in filter tool widget
187 self.ui.filterButton.clicked.connect(
188 lambda: self.table_operator.filter_table(self.dynamic_filter_inputs)
189 )
190 self.ui.cancelButton.clicked.connect(
191 lambda: self.table_operator.reset_filter(self.dynamic_filter_inputs)
192 )
193 self.ui.actionRefresh.triggered.connect(self.setup_table)
194 self.ui.actionDelete.triggered.connect(self.delete_keyfile)
195 self.ui.actionDeletedKeyfiles.triggered.connect(self.open_trash_manage_dialog)
196 self.ui.actionRenameSensor.triggered.connect(self.rename_sensor_name)
197 self.ui.actionExit.triggered.connect(self.exit_application)
198 self.ui.actionDocumentation.triggered.connect(self.open_documentation)
199 self.ui.actionImportKeyfiles.triggered.connect(self.keyfile_import)
200 self.ui.actionExportKeyfiles.triggered.connect(self.keyfile_export)
201 self.ui.actionAbout.triggered.connect(self.show_about_dialog)
202 self.ui.actionSaveChange.triggered.connect(self.save_as_json)
203 self.ui.actionEdit.triggered.connect(self.open_json_edit_dialog)
204 self.ui.actionTableColumn.triggered.connect(self.open_column_configurator)
205
207 r"""
208 Open a dialog to edit metadata of the selected keyfile.
209 Update the table row and the metadata.json file after editing is completed .
210 """
211 checked_serial_numbers = self.get_checked_serial_numbers()
212 if not checked_serial_numbers:
213 QMessageBox.warning(self, self.tr("Error"), self.tr("No keyfile selected for edit."))
214 return
215
216 serial_number = checked_serial_numbers[0]
217 metadata = self.folder_content.read_metadata(serial_number)
218
219 dialog = MetadataEditor(serial_number, metadata, parent=self)
220 if dialog.exec_() == QDialog.DialogCode.Accepted:
221 updated_metadata = dialog.result_metadata
222 self.folder_content.update_metadata(serial_number, updated_metadata)
223 self.update_table_row([serial_number])
225
227 r"""
228 Open a dialog to configure table columns.
229 Save the selected columns to config.json and refresh the table and filter in the UI.
230 """
231 dialog = ColumnConfigurator(self.custom_columns, parent=self)
232 if dialog.exec_() == QDialog.DialogCode.Accepted:
233 self.custom_columns = dialog.selected_columns
234
235 self.config_manager.custom_columns = self.custom_columns
236 self.config_manager.save_config()
237
238 self.setup_table()
240
241 def save_as_json(self):
242 r"""
243 Save the contents of the table to 'metadata.json' file inside each corresponding keyfile directory.
244 If the JSON file exists, update it; if it does not exist, create and save a new file.
245 """
246
247 table = self.ui.tableWidget
248 read_only_columns = [0, 1, 2, 3, 10, 11]
249
250 for row in range(table.rowCount()):
251 serial_number = table.item(row, 2).data(Qt.ItemDataRole.UserRole + 2)
252
253 if self.check_activation_status(serial_number) == ActivationStatus.ACTIVATED:
254 keyfile_path = os.path.join(self.directory1, serial_number)
255 elif self.check_activation_status(serial_number) == ActivationStatus.DEACTIVATED:
256 keyfile_path = os.path.join(self.directory2, serial_number)
257 else:
258 continue
259
260 if not os.path.isdir(keyfile_path):
261 continue
262
263 meta_json_path = os.path.join(keyfile_path, "metadata.json")
264 meta_data = {}
265
266 if os.path.exists(meta_json_path):
267 with open(meta_json_path, "r", encoding="utf-8") as f:
268 try:
269 existing_data = json.load(f)
270 except json.JSONDecodeError:
271 existing_data = {}
272 else:
273 existing_data = {}
274
275 for col in range(table.columnCount()):
276 if col in read_only_columns:
277 continue
278
279 item = table.item(row, col)
280 column_name = table.horizontalHeaderItem(col).text()
281
282 field_name = column_name
283
284 if item and item.text().strip():
285 meta_data[field_name] = item.text().strip()
286 elif field_name in existing_data:
287 del existing_data[field_name]
288
289 existing_data.update(meta_data)
290
291 with open(meta_json_path, "w", encoding="utf-8") as f:
292 json.dump(existing_data, f, indent=4, ensure_ascii=False)
293
294 # if existing_data:
295 # with open(meta_json_path, "w", encoding="utf-8") as f:
296 # json.dump(existing_data, f, indent=4, ensure_ascii=False)
297 # elif os.path.exists(meta_json_path):
298 # os.remove(meta_json_path)
300
302 r"""
303 Open a dialog displaying information about the software.
304 """
305 default_info = {
306 "version": "Unknown",
307 "release_date": "Unknown"
308 }
309 try:
310 with open(resource_path('resources/about.json'), 'r', encoding='utf-8') as file:
311 info = json.load(file)
312 except (FileNotFoundError, json.JSONDecodeError):
313 info = default_info
314 version = info.get("version", default_info["version"])
315 release_date = info.get("release_date", default_info["release_date"])
316 dialog = QDialog(self)
317 dialog.setWindowTitle("About")
318 dialog.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
319 layout = QVBoxLayout(dialog)
320 svg_logo = QSvgWidget(resource_path('resources/foskeyman_logo_long.svg'))
321 layout.addWidget(svg_logo, alignment=Qt.AlignmentFlag.AlignCenter)
322 info_label = QLabel(f"Version: {version}\nRelease Date: {release_date}")
323 info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
324 layout.addWidget(info_label)
325 author_label = QLabel(f"Authors:\nBertram Richter\nXiaoli Song")
326 author_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
327 layout.addWidget(author_label)
328 copyright_label = QLabel(f"Copyright:\nInstitut für Massivbau\nTechnische Universität Dresden")
329 copyright_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
330 layout.addWidget(copyright_label)
331 license_label = QLabel(
332 "License:\nThis software is licensed under the GNU General Public License (GPL) Version 3, 29  June  2007."
333 )
334 license_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
335 license_label.setWordWrap(True)
336 layout.addWidget(license_label)
337 dialog.setLayout(layout)
338 dialog.exec_()
339
340 def keyfile_import(self):
341 r"""
342 Import selected .od6pkg keyfiles as extracted folders into the deactivated directory.
343 Conflicts are handled based on user choice.
344 """
345 file_paths, _ = QFileDialog.getOpenFileNames(
346 self,
347 self.tr("Select .od6pkg Files to Import"),
348 "",
349 self.tr("OD6 Package Files (*.od6pkg)")
350 )
351
352 if not file_paths:
353 return
354
355 try:
356 for source_path in file_paths:
357 item_name = os.path.basename(source_path)
358
359 base_name = os.path.splitext(item_name)[0]
360 final_target_path = os.path.join(self.directory2, base_name)
361
362 check_path_dir2 = os.path.join(self.directory2, base_name)
363 check_path_dir1 = os.path.join(self.directory1, base_name)
364
365 conflict_path = None
366 if os.path.exists(check_path_dir2):
367 conflict_path = check_path_dir2
368 elif os.path.exists(check_path_dir1):
369 conflict_path = check_path_dir1
370
371 if conflict_path:
372 user_choice = QMessageBox.question(
373 self,
374 "Conflict Detected",
375 f"The folder '{base_name}' already exists. Overwrite?",
376 QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
377 )
378 if user_choice == QMessageBox.StandardButton.Yes:
379 shutil.rmtree(conflict_path)
380 else:
381 continue
382
383 try:
384 with zipfile.ZipFile(source_path, 'r') as zip_ref:
385 zip_ref.extractall(final_target_path)
386 except zipfile.BadZipFile:
387 QMessageBox.warning(
388 self,
389 self.tr("Invalid File"),
390 self.tr(f"File '{item_name}' is not a valid .od6pkg archive and will be skipped.")
391 )
392 continue
393
394 QMessageBox.information(
395 self,
396 self.tr("Success"),
397 self.tr("Selected keyfiles have been imported to the deactivated directory.")
398 )
399 except Exception as e:
400 QMessageBox.critical(self, self.tr("Error"), self.tr(f"An error occurred: {e}"))
401
402 self.setup_table()
403
404 def keyfile_export(self):
405 r"""
406 Export selected keyfiles from activated or deactivated directories to an external directory.
407 Compress each keyfile folder into .od6pkg format.
408 """
409 checked_serial_numbers = self.get_checked_serial_numbers()
410 if not checked_serial_numbers:
411 QMessageBox.warning(self, self.tr("Error"), self.tr("No keyfile selected for export."))
412 return
413
414 export_path = QFileDialog.getExistingDirectory(self, self.tr("Select Export Directory"))
415 if not export_path:
416 return
417
418 for serial_number in checked_serial_numbers:
419 source_folder = None
420 if self.check_activation_status(serial_number) == ActivationStatus.ACTIVATED:
421 source_folder = os.path.join(self.directory1, serial_number)
422 elif self.check_activation_status(serial_number) == ActivationStatus.DEACTIVATED:
423 source_folder = os.path.join(self.directory2, serial_number)
424
425 if not source_folder or not os.path.exists(source_folder):
426 QMessageBox.warning(self, self.tr("Error"),
427 self.tr(f"Source folder for keyfile {serial_number} does not exist."))
428 continue
429
430 od6pkg_path = os.path.join(export_path, f"{serial_number}.od6pkg")
431
432 if os.path.exists(od6pkg_path):
433 user_choice = QMessageBox.question(
434 self,
435 self.tr("Conflict Detected"),
436 self.tr(f"The file '{serial_number}.od6pkg' already exists. Overwrite?"),
437 QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
438 )
439 if user_choice != QMessageBox.StandardButton.Yes:
440 continue
441 os.remove(od6pkg_path)
442
443 try:
444 with zipfile.ZipFile(od6pkg_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
445 for root, dirs, files in os.walk(source_folder):
446 for file in files:
447 file_path = os.path.join(root, file)
448 arcname = os.path.relpath(file_path, start=source_folder)
449 zipf.write(file_path, arcname)
450
451 logging.info(f"Keyfile {serial_number} exported and packaged to {od6pkg_path}.")
452 except Exception as e:
453 QMessageBox.warning(self, self.tr("Error"),
454 self.tr(f"Failed to export keyfile {serial_number}: {e}"))
455
456 QMessageBox.information(self, self.tr("Success"), self.tr("Selected keyfiles exported successfully."))
458
459 def delete_keyfile(self):
460 r"""
461 Delete selected keyfiles and move them to trash directory.
462 """
463 checked_serial_numbers = self.get_checked_serial_numbers()
464 if not checked_serial_numbers:
465 QMessageBox.warning(self, self.tr("Warning"), self.tr("No keyfile selected for deletion."))
466 return
467
468 confirm = QMessageBox.question(
469 self,
470 self.tr("Confirm Deletion"),
471 self.tr("Are you sure you want to delete the selected keyfiles?"),
472 QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
473 )
474 if confirm != QMessageBox.StandardButton.Yes:
475 return
476
477 for serial in checked_serial_numbers:
478 self.key_handler.delete_key(serial)
479
480 self.setup_table()
481
483 """
484 Open a dialog that displays deleted keyfiles and allows user to restore or permanently delete them.
485 """
486 dialog = TrashManager(self.key_handler, parent=self)
487 dialog.exec()
488 self.setup_table()
489
491 r"""
492 Open a dialog for selecting two directories.
493 If valid, initialize KeyHandler and FolderContent, and save the paths to the config.json file.
494 """
495 dialog = QDialog(self)
496 open_ui = Ui_Open()
497 open_ui.setupUi(dialog)
498 self.config_manager.open_ui = open_ui
499 open_ui.acBrowseButton.clicked.connect(lambda: self.config_manager.select_directory1(dialog, open_ui))
500 open_ui.deacBrowseButton.clicked.connect(lambda: self.config_manager.select_directory2(dialog, open_ui))
501 open_ui.trashBrowseButton.clicked.connect(lambda: self.config_manager.select_directory3(dialog, open_ui))
502
503 dialog.setWindowFlags(dialog.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
504 dialog.setWindowTitle(self.tr("Directory Settings"))
505 # Pre-fill the input fields if the directories have already been selected previously
506 if self.directory1 and self.directory2 and self.directory3:
507 open_ui.acLineEdit.setText(self.directory1)
508 open_ui.deacLineEdit.setText(self.directory2)
509 open_ui.trashLineEdit.setText(self.directory3)
510
511 open_ui.confirmButton.clicked.connect(lambda: self.config_manager.confirm_directory_selection(dialog, open_ui))
512 open_ui.cancelButton.clicked.connect(dialog.reject)
513 if dialog.exec_():
514 if self.config_manager.directory1 and self.config_manager.directory2 and self.config_manager.directory3:
515 self.directory1 = self.config_manager.directory1
516 self.directory2 = self.config_manager.directory2
517 self.directory3 = self.config_manager.directory3
518 # self.custom_columns = self.config_manager.custom_columns
519 self.config_manager.save_config()
521
523 r"""
524 Initialize KeyHandler and FolderContent based on the selected directory paths.
525 If success, set up the table for further operations.
526 """
527 self.key_handler = KeyHandler(self.directory1, self.directory2, self.directory3)
528 if not self.key_handler.check_directories():
529 QMessageBox.warning(self, "Error", self.tr("Directory validation failure"))
530 return
532 self.setup_table()
534
536 r"""
537 Rename the sensor name. Only work for the first selected entry.
538 """
539 serial_numbers = self.get_checked_serial_numbers()
540 if not serial_numbers:
541 return
542 serial_number = serial_numbers[0]
543 row = None
544 for r in range(self.ui.tableWidget.rowCount()):
545 if self.ui.tableWidget.item(r, 2) and self.ui.tableWidget.item(r, 2).text() == serial_number:
546 row = r
547 break
548 sensor_name = self.ui.tableWidget.item(row, 3).text()
549 if sensor_name:
550 dialog = RenameSensor(serial_number, sensor_name, parent=self)
551 if dialog.exec_() == QDialog.DialogCode.Accepted:
552 new_name = dialog.get_new_sensor_name()
553 self.folder_content.edit_sensor_name_for_key(serial_number, new_name)
554 logging.info(f"Sensor name updated for Serial Number {serial_number}: {sensor_name} -> {new_name}")
555 self.update_table_row([serial_number])
557
558 def check_activation_status(self, serial_number):
559 r"""
560 Check the activation status for a given serial number.
561 \param serial_number (str): The serial number to check.
562 \return (ActivationStatus): The activation status of the given serial number, or None if not found.
563 """
564 for row in range(self.ui.tableWidget.rowCount()):
565 if self.ui.tableWidget.item(row, 2).data(Qt.ItemDataRole.UserRole + 2) == serial_number:
566 activation_item = self.ui.tableWidget.item(row, 1)
567 activation_status = activation_item.data(Qt.ItemDataRole.UserRole + 1)
568 return activation_status
569 return None
570
572 r"""Retrieve serial numbers for rows where the checkbox is checked. """
573 serial_numbers = []
574 for row in range(self.ui.tableWidget.rowCount()):
575 checkbox_item = self.ui.tableWidget.item(row, 0)
576 if checkbox_item and checkbox_item.checkState() == Qt.CheckState.Checked:
577 serial_number = self.ui.tableWidget.item(row, 2).data(Qt.ItemDataRole.UserRole + 2)
578 serial_numbers.append(serial_number)
579 return serial_numbers
580
582 r"""Reset all checkboxes in the table to an unchecked state."""
583 for row in range(self.ui.tableWidget.rowCount()):
584 checkbox_item = self.ui.tableWidget.item(row, 0)
585 if checkbox_item and checkbox_item.flags() & Qt.ItemFlag.ItemIsUserCheckable:
586 checkbox_item.setCheckState(Qt.CheckState.Unchecked)
587
589 r"""Control visibility of filter widget"""
590 self.ui.filterDockWidget.setVisible(not self.ui.filterDockWidget.isVisible())
591
593 r"""Control visibility of extra information widget"""
594 self.ui.infoDockWidget.setVisible(not self.ui.infoDockWidget.isVisible())
595
597 r"""Control visibility of full text search widget"""
598 self.ui.searchDockWidget.setVisible(not self.ui.searchDockWidget.isVisible())
599
600 def table_cell_info(self, row, column):
601 r"""
602 Display relevant information on the right side panel for the selected table cell.
603 \param row (int): The row index of the selected table cell.
604 \param column (int): The column index of the selected table cell.
605 """
606 if column == 0 or column == 1:
607 return
608 if self.ui.tableWidget.item(row, 2).data(Qt.ItemDataRole.UserRole + 2) is None:
609 return
610 serial_number = self.ui.tableWidget.item(row, 2).data(Qt.ItemDataRole.UserRole + 2)
611 user_properties = self.folder_content.read_user_properties(serial_number)
612 gage_segment = self.folder_content.read_gage_segment(serial_number)
613 od6ref_file = self.folder_content.read_od6ref_file(serial_number)
614 output = ""
615 if user_properties is not None:
616 output += "<h3>user_properties.json</h3>"
617 formatted_json = json.dumps(user_properties, indent=4, ensure_ascii=False)
618 output += format_json_to_html(formatted_json)
619 else:
620 output += " "
621 if gage_segment is not None:
622 output += "<h3>gage_segment.json</h3>"
623 formatted_json = json.dumps(gage_segment, indent=4, ensure_ascii=False)
624 output += format_json_to_html(formatted_json)
625 else:
626 output += " "
627 if od6ref_file is not None:
628 output += "<h3>.od6ref</h3>"
629 formatted_json = json.dumps(od6ref_file, indent=4, ensure_ascii=False)
630 output += format_json_to_html(formatted_json)
631 else:
632 output += " "
633 self.ui.infoTextBrowser.setHtml(output)
634
635 def setup_table(self):
636 r"""
637 Set up the table with the necessary headers, styles, and configurations.
638 Enables the ability to drag and move columns for custom arrangement.
639 Sets certain columns as read-only to prevent unintended modification.
640 Populate table with data in metadata.json, and connect a cell click event to display additional information.
641 """
642 fixed_columns = [
643 ' ',
644 self.tr('Status'),
645 self.tr('Serial Number'),
646 self.tr('Sensor Name')
647 ]
648
649 custom_columns = self.custom_columns
650
651 fixed_tail_columns = [
652 self.tr('Last Edit Date'),
653 self.tr('Sensor Length (m)')
654 ]
655
656 all_columns = fixed_columns + [self.tr(col) for col in custom_columns] + fixed_tail_columns
657
658 self.ui.tableWidget.setColumnCount(len(all_columns))
659 self.ui.tableWidget.setHorizontalHeaderLabels(all_columns)
660
661 fixed_keys = ['checkbox', 'status', 'serial_number', 'sensor_name']
662 fixed_tail_keys = ['last_edit_date', 'sensor_length']
663
664 for i, key in enumerate(fixed_keys):
665 header_item = self.ui.tableWidget.horizontalHeaderItem(i)
666 header_item.setData(Qt.ItemDataRole.UserRole, key)
667
668 for i, key in enumerate(fixed_tail_keys):
669 idx = len(all_columns) - len(fixed_tail_keys) + i
670 header_item = self.ui.tableWidget.horizontalHeaderItem(idx)
671 header_item.setData(Qt.ItemDataRole.UserRole, key)
672
673 self.ui.tableWidget.setStyleSheet("""
674 QHeaderView::section {
675 background-color: lightgray;
676 color: black;
677 font-weight: bold;
678 height: 30px;
679 border: 1px solid black;
680 }
681 QTableWidget {
682 gridline-color: grey;
683 }
684 """)
685 self.ui.tableWidget.setColumnWidth(0, 10)
686 self.ui.tableWidget.setColumnWidth(1, 180)
687
688 header = self.ui.tableWidget.horizontalHeader()
689 header.setSectionsMovable(True)
690 header.setDragEnabled(True)
691 header.setDragDropMode(QHeaderView.DragDropMode.DragDrop)
692
693 if self.key_handler is None:
694 return
695
696 self.ui.tableWidget.setRowCount(0)
697 self.populate_table()
698
699 read_only_indices = [1, 2, 3] + [len(all_columns) - 2, len(all_columns) - 1]
700 self.set_columns_read_only(read_only_indices)
701 self.set_columns_background_color([2, 3, len(all_columns) - 2, len(all_columns) - 1])
702
704
705 def set_columns_read_only(self, columns):
706 r"""
707 Set the specified columns to read-only.
708 These columns will still allow user interaction like selection and clicking, but their content will not be editable
709 \param columns (List[int]): A list of column indices that should be set to read-only.
710 """
711 row_count = self.ui.tableWidget.rowCount()
712 for row in range(row_count):
713 for column in columns:
714 item = self.ui.tableWidget.item(row, column)
715 if item:
716 item.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled)
717
719 r"""
720 Apply a light gray background color to the specified columns.
721 \param columns (List[int]): A list of column indices (integers) to apply the background color.
722 """
723 row_count = self.ui.tableWidget.rowCount()
724 for row in range(row_count):
725 for column in columns:
726 item = self.ui.tableWidget.item(row, column)
727 if item:
728 item.setBackground(QBrush(QColor(245, 245, 245)))
729
730 def populate_table(self):
731 r"""
732 Load activated and deactivated keys, set their status, and fill in all related metadata fields.
733 """
734 self.ui.tableWidget.setSortingEnabled(False)
735
736 activated_keys = set(self.key_handler.read_keys('activated'))
737 deactivated_keys = set(self.key_handler.read_keys('deactivated'))
738
739 keys_with_status = [(key, "Activated") for key in activated_keys] + \
740 [(key, "Deactivated") for key in deactivated_keys]
741
742 self.ui.tableWidget.setRowCount(len(keys_with_status))
743
744 for row_idx, (key, status) in enumerate(keys_with_status):
745 total_cols = self.ui.tableWidget.columnCount()
746
747 for col in range(total_cols):
748 if self.ui.tableWidget.item(row_idx, col) is None:
749 self.ui.tableWidget.setItem(row_idx, col, QTableWidgetItem())
750
751 if status == 'Activated':
752 activation_status = ActivationStatus.ACTIVATED
753 self.ui.tableWidget.setCellWidget(row_idx, 1, self.create_status_button('Activated'))
754 else:
755 activation_status = ActivationStatus.DEACTIVATED
756 self.ui.tableWidget.setCellWidget(row_idx, 1, self.create_status_button('Deactivated'))
757
758 activation_item = self.ui.tableWidget.item(row_idx, 1)
759 activation_item.setData(Qt.ItemDataRole.DisplayRole, activation_status.value)
760 activation_item.setForeground(Qt.GlobalColor.transparent)
761 activation_item.setData(Qt.ItemDataRole.UserRole + 1, activation_status)
762
763 check_item = self.ui.tableWidget.item(row_idx, 0)
764 check_item.setCheckState(Qt.CheckState.Unchecked)
765
766 metadata = self.folder_content.read_metadata(key)
767
768 self.ui.tableWidget.item(row_idx, 2).setText(key)
769 self.ui.tableWidget.item(row_idx, 2).setData(Qt.ItemDataRole.UserRole + 2, key)
770
771 self.ui.tableWidget.item(row_idx, 3).setText(self.folder_content.read_sensor_name_for_key(key))
772
773 edit_date = self.folder_content.get_last_edit_date(key)
774 edit_date_str = edit_date.strftime("%Y-%m-%d") if edit_date else ""
775
776 last_edit_idx = self.ui.tableWidget.columnCount() - 2
777 sensor_len_idx = self.ui.tableWidget.columnCount() - 1
778
779 self.ui.tableWidget.item(row_idx, last_edit_idx).setText(edit_date_str)
780
781 sensor_length = self.folder_content.read_sensor_length_for_key(key)
782 sensor_length_str = str(sensor_length) if sensor_length is not None else ""
783 self.ui.tableWidget.item(row_idx, sensor_len_idx).setText(sensor_length_str)
784
785 for i, col_name in enumerate(self.custom_columns):
786 col_idx = 4 + i
787 value = metadata.get(col_name, "")
788 self.ui.tableWidget.item(row_idx, col_idx).setText(value)
789
790 self.ui.tableWidget.setSortingEnabled(True)
791
792 def create_status_button(self, status):
793 r"""
794 Creates a colored QPushButton based on its activation status.
795 The button will be colored differently depending on whether the status is 'Activated' or
796 'Deactivated'. The button is not clickable and will display the status text.
797 \param status (str): The activation status ('Activated', 'Deactivated').
798 \return (QWidget): A QWidget containing the styled QPushButton for status display.
799 """
800 status_translation_map = {
801 'Activated': self.tr("Activated"),
802 'Deactivated': self.tr("Deactivated"),
803 }
804 translated_status = status_translation_map[status]
805 button = QPushButton(translated_status)
806 button.setFixedSize(QSize(120, 20))
807 button.setEnabled(False)
808 # green
809 if status == 'Activated':
810 button.setStyleSheet("""
811 background-color: #228B22;
812 color: white;
813 border-radius: 10px;
814 font-weight: bold;
815 """)
816 # grey
817 elif status == 'Deactivated':
818 button.setStyleSheet("""
819 background-color: #D3D3D3;
820 color: black;
821 border-radius: 10px;
822 font-weight: bold;
823 """)
824
825 container = QWidget()
826 layout = QHBoxLayout()
827 layout.addWidget(button)
828 layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
829 layout.setContentsMargins(0, 0, 0, 0)
830 container.setLayout(layout)
831 return container
832
834 r"""
835 Activate the selected items (checkbox is checked) in the table.
836 For valid items, the keyfile moved from deactivated directory to activate directory.
837 Upon successful activation, the item's status is updated to 'Activated' and the status button is turn green.
838 """
839 for i in range(self.ui.tableWidget.rowCount()):
840 check_item = self.ui.tableWidget.item(i, 0)
841 activation_item = self.ui.tableWidget.item(i, 1)
842 serial_number = self.ui.tableWidget.item(i, 2).data(Qt.ItemDataRole.UserRole + 2)
843 if check_item.checkState() == Qt.CheckState.Checked:
844 success = self.key_handler.activate_key(serial_number)
845 if success:
846 activation_item.setData(Qt.ItemDataRole.UserRole + 1, ActivationStatus.ACTIVATED)
847 self.ui.tableWidget.setCellWidget(i, 1, self.create_status_button('Activated'))
848 check_item.setCheckState(Qt.CheckState.Unchecked)
849
851 r"""
852 Deactivate the selected item (checkbox is checked) in the table.
853 For valid items, the keyfile moved from the activated directory to the deactivated directory.
854 Upon successful deactivation, the item's status is updated to 'Deactivated', and the status button turns grey.
855 """
856 for i in range(self.ui.tableWidget.rowCount()):
857 check_item = self.ui.tableWidget.item(i, 0)
858 activation_item = self.ui.tableWidget.item(i, 1)
859 serial_number = self.ui.tableWidget.item(i, 2).data(Qt.ItemDataRole.UserRole + 2)
860 if check_item.checkState() == Qt.CheckState.Checked:
861 success = self.key_handler.deactivate_key(serial_number)
862 if success:
863 activation_item.setData(Qt.ItemDataRole.UserRole + 1, ActivationStatus.DEACTIVATED)
864 self.ui.tableWidget.setCellWidget(i, 1, self.create_status_button('Deactivated'))
865 check_item.setCheckState(Qt.CheckState.Unchecked)
866
867 def switch_language(self, language):
868 r"""
869 Switch the UI language and save it to the configuration file.
870 \param language (str): The target language, either 'English' or 'German'.
871 """
872 if language == 'german':
873 # self.translator.load(os.path.join(os.path.dirname(__file__), '../resources/translations/Translate_DE.qm'))
874 translation_path = resource_path('resources/translations/Translate_DE.qm')
875 self.translator.load(translation_path)
876 QApplication.instance().installTranslator(self.translator)
877 elif language == 'english':
878 QApplication.instance().removeTranslator(self.translator)
880 self.ui.retranslateUi(self)
881 self.config_manager.language = language
882 self.config_manager.save_config()
883 self.setup_table()
885
887 r"""Method to handle application exit."""
889 QCoreApplication.instance().quit()
890
891 def closeEvent(self, event):
892 """Triggered when user clicks X to close the window."""
894 event.accept()
895
897 r"""
898 Save the current order of custom columns based on the table's visual layout (only applies to customized columns;
899 fixed columns are not affected). Update the configuration and write the new column order to config.json.
900 """
901 header = self.ui.tableWidget.horizontalHeader()
902 total_columns = self.ui.tableWidget.columnCount()
903
904 ordered_column_names = []
905 for visual_pos in range(total_columns):
906 logical_index = header.logicalIndex(visual_pos)
907 header_item = self.ui.tableWidget.horizontalHeaderItem(logical_index)
908 if header_item:
909 column_name = header_item.text()
910 ordered_column_names.append(column_name)
911
912 ordered_custom_columns = []
913 for name in ordered_column_names:
914 if name in self.custom_columns:
915 ordered_custom_columns.append(name)
916
917 self.custom_columns = ordered_custom_columns
918 self.config_manager.custom_columns = self.custom_columns
919 self.config_manager.save_config()
920
922 r"""Open the Doxygen generated documentation web page."""
923 documentation_url = "https://tud-imb.github.io/fosKeyMan/"
924 webbrowser.open(documentation_url)
925
927 r"""
928 Populate QComboBox with the keyfiles.
929 """
930 self.ui.searchComboBox.clear()
931 self.ui.searchComboBox.addItem(self.tr("Search Selected"))
932 keyfiles = set()
933 for row in range(self.ui.tableWidget.rowCount()):
934 serial_number = self.ui.tableWidget.item(row, 2).data(Qt.ItemDataRole.UserRole + 2)
935 if serial_number:
936 keyfiles.add(serial_number)
937 for key in sorted(keyfiles):
938 self.ui.searchComboBox.addItem(key)
939
940 def execute_search(self):
941 r"""
942 Perform search based on the selected key item in the comboBox and display the results in TextBrowser.
943 """
944 key = self.ui.searchComboBox.currentText()
945 if key == self.tr("Search Selected"):
946 key = None
947 search_term = self.ui.searchLineEdit.text()
948 if not search_term:
949 self.ui.searchTextBrowser.setPlainText(self.tr("Search term cannot be empty. Please enter a search term."))
950 return
951 search_results = self.folder_content.full_text_search(search_term, key)
952 self.ui.searchTextBrowser.clear()
953 if search_results:
954 # self.ui.searchTextBrowser.append("Search Results:\n")
955 for key, match in search_results:
956 result_text = f"Keyfile: {key}.zip\n"
957 for entry in match:
958 result_text += "\n".join([f"{k}: {v}" for k, v in entry.items()]) + "\n"
959 result_text += "\n\n"
960 self.ui.searchTextBrowser.append(result_text)
961 else:
962 self.ui.searchTextBrowser.setPlainText(self.tr("No results found."))
963
964 def update_table_row(self, serial_numbers):
965 r"""
966 Update specific rows in the table given a list of serial numbers.
967 \param serial_numbers (List[str]): A list of serial numbers for the rows to be updated.
968 """
969 for serial_number in serial_numbers:
970 row_index = None
971
972 for row in range(self.ui.tableWidget.rowCount()):
973 item = self.ui.tableWidget.item(row, 2)
974 if item and item.data(Qt.ItemDataRole.UserRole + 2) == serial_number:
975 row_index = row
976 break
977
978 if row_index is None:
979 continue
980
981 if serial_number in self.key_handler.read_keys('activated'):
982 activation_status = ActivationStatus.ACTIVATED
983 self.ui.tableWidget.setCellWidget(row_index, 1, self.create_status_button('Activated'))
984 elif serial_number in self.key_handler.read_keys('deactivated'):
985 activation_status = ActivationStatus.DEACTIVATED
986 self.ui.tableWidget.setCellWidget(row_index, 1, self.create_status_button('Deactivated'))
987 else:
988 continue
989
990 activation_item = self.ui.tableWidget.item(row_index, 1)
991 activation_item.setData(Qt.ItemDataRole.UserRole + 1, activation_status)
992
993 check_item = self.ui.tableWidget.item(row_index, 0)
994 check_item.setCheckState(Qt.CheckState.Unchecked)
995
996 metadata = self.folder_content.read_metadata(serial_number)
997
998 self.ui.tableWidget.item(row_index, 2).setText(serial_number)
999 self.ui.tableWidget.item(row_index, 2).setData(Qt.ItemDataRole.UserRole + 2, serial_number)
1000
1001 self.ui.tableWidget.item(row_index, 3).setText(self.folder_content.read_sensor_name_for_key(serial_number))
1002
1003 edit_date = self.folder_content.get_last_edit_date(serial_number)
1004 edit_date_str = edit_date.strftime("%Y-%m-%d") if edit_date else ""
1005 last_edit_idx = self.ui.tableWidget.columnCount() - 2
1006 self.ui.tableWidget.item(row_index, last_edit_idx).setText(edit_date_str)
1007
1008 sensor_length = self.folder_content.read_sensor_length_for_key(serial_number)
1009 sensor_length_str = str(sensor_length) if sensor_length is not None else ""
1010 sensor_len_idx = self.ui.tableWidget.columnCount() - 1
1011 self.ui.tableWidget.item(row_index, sensor_len_idx).setText(sensor_length_str)
1012
1013 if metadata:
1014 for i, col_name in enumerate(self.custom_columns):
1015 col_idx = 4 + i
1016 value = metadata.get(col_name, "")
1017 self.ui.tableWidget.item(row_index, col_idx).setText(value)
1018
1019
1020def file_path(relative_path):
1021 r"""
1022 Join the base directory with the given relative path to generate an absolute path.
1023 This function determines the base directory based on whether the application is running
1024 in a frozen state (e.g., when packaged with PyInstaller) or in a regular environment.
1025 \param relative_path (str): The relative path to the target file or directory.
1026 \return (str): The absolute path to the target file or directory.
1027 """
1028 if getattr(sys, 'frozen', False):
1029 base_dir = os.path.dirname(sys.executable)
1030 else:
1031 base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
1032 return os.path.join(base_dir, relative_path)
1033
1034
1035def resource_path(relative_path):
1036 r"""
1037 Similar to file_path, it generates the absolute path to a resource.
1038 If the application is frozen (e.g., packaged with PyInstaller), it uses the _MEIPASS directory.
1039 \param relative_path (str): The relative path to the resource.
1040 \return (str): The absolute path to the resource.
1041 """
1042 if getattr(sys, 'frozen', False):
1043 base_dir = sys._MEIPASS
1044 else:
1045 base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
1046 return os.path.join(base_dir, relative_path)
1047
1048
1049def main():
1050 r"""Initialize and run the application."""
1051 app = QApplication(sys.argv)
1052 window = MainWindow()
1053 window.show_and_check_config()
1054 sys.exit(app.exec_())
Handle reading JSON files (userProperties.json and gageSegment.json) from keyfile folders located in ...
Handle keyfile operations related to activation and deactivation, as well as reading keyfiles from di...
Definition keyhandler.py:15
Class to configure (add, remove) the customized table columns.
Manage the loading, saving, and validation of configuration settings for directories.
A tooltip dialog that provides detailed information about a table cell when the mouse hovers over it.
Definition hoverinfo.py:6
Main User Interface for the application.
Definition mainwindow.py:46
setup_filter_dockwidget(self)
Dynamically set up the filter dock widget based on the current table columns.
populate_table(self)
Load activated and deactivated keys, set their status, and fill in all related metadata fields.
switch_language(self, language)
Switch the UI language and save it to the configuration file.
populate_search_combobox(self)
Populate QComboBox with the keyfiles.
connect_actions(self)
Connect UI actions to corresponding methods.
set_columns_read_only(self, columns)
Set the specified columns to read-only.
show_and_check_config(self)
Show the main window and check the configuration.
Definition mainwindow.py:87
update_table_row(self, serial_numbers)
Update specific rows in the table given a list of serial numbers.
set_columns_background_color(self, columns)
Apply a light gray background color to the specified columns.
create_status_button(self, status)
Creates a colored QPushButton based on its activation status.
closeEvent(self, event)
Triggered when user clicks X to close the window.
check_activation_status(self, serial_number)
Check the activation status for a given serial number.
adjust_window_size(self)
Dynamically adapts window size to available screen space.
Definition mainwindow.py:75
save_current_column_order(self)
Save the current order of custom columns based on the table's visual layout (only applies to customiz...
initialize_handlers(self)
Initialize KeyHandler and FolderContent based on the selected directory paths.
get_checked_serial_numbers(self)
Retrieve serial numbers for rows where the checkbox is checked.
reset_all_checkboxes(self)
Reset all checkboxes in the table to an unchecked state.
Class to edit the metadata.json contents for a keyfile.
Dialog to rename the sensor name.
Class to handle operations on the table, such as adding or deleting rows, filtering rows based on dif...
Class to manage deleted keyfiles in the trash directory.
resource_path(relative_path)
Similar to file_path, it generates the absolute path to a resource.
file_path(relative_path)
Join the base directory with the given relative path to generate an absolute path.
main()
Initialize and run the application.