3Defines the main Graphical User Interface.
5\author Xiali Song, Bertram Richter
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
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
39 filename=
'fosKeyManOperation.log',
41 format=
'%(asctime)s - %(levelname)s - %(message)s',
42 datefmt=
'%Y-%m-%d %H:%M:%S'
48 Main User Interface for the application.
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.
71 self.setWindowIcon(QIcon(
resource_path(
'resources/foskeyman_logo_short.svg')))
77 Dynamically adapts window size to available screen space.
79 screen = self.screen()
80 screen_size = screen.availableGeometry()
82 width = int(screen_size.width() * 0.7)
83 height = int(screen_size.height() * 0.6)
85 self.resize(width, height)
89 Show the main window and check the configuration. Switch the language based on the saved configuration.
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.
105 Dynamically set up the filter dock widget based on the current table columns.
107 layout = self.
ui.filterFormLayout
109 while layout.rowCount():
114 status_label = QLabel(self.tr(
"Status"), self)
115 status_combo = QComboBox(self)
116 status_combo.addItems([
118 self.tr(
"Activated"),
119 self.tr(
"Deactivated")
121 layout.addRow(status_label, status_combo)
124 serial_label = QLabel(self.tr(
"Serial Number"), self)
125 serial_input = QLineEdit(self)
126 layout.addRow(serial_label, serial_input)
129 name_label = QLabel(self.tr(
"Sensor Name"), self)
130 name_input = QLineEdit(self)
131 layout.addRow(name_label, name_input)
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)
141 date_container = QWidget(self)
142 date_layout = QHBoxLayout(date_container)
143 date_layout.setContentsMargins(0, 0, 0, 0)
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))
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())
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)
162 layout.addRow(date_container)
168 Connect UI actions to corresponding methods.
178 self.
ui.actionSelectAll.triggered.connect(self.
table_operator.check_all_boxes)
182 self.
ui.filterDockWidget.visibilityChanged.connect(self.
ui.actionFilter.setChecked)
183 self.
ui.infoDockWidget.visibilityChanged.connect(self.
ui.actionInformation.setChecked)
185 self.
ui.searchDockWidget.visibilityChanged.connect(self.
ui.actionSearch.setChecked)
187 self.
ui.filterButton.clicked.connect(
190 self.
ui.cancelButton.clicked.connect(
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 .
212 if not checked_serial_numbers:
213 QMessageBox.warning(self, self.tr(
"Error"), self.tr(
"No keyfile selected for edit."))
216 serial_number = checked_serial_numbers[0]
220 if dialog.exec_() == QDialog.DialogCode.Accepted:
221 updated_metadata = dialog.result_metadata
222 self.
folder_content.update_metadata(serial_number, updated_metadata)
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.
232 if dialog.exec_() == QDialog.DialogCode.Accepted:
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.
247 table = self.
ui.tableWidget
248 read_only_columns = [0, 1, 2, 3, 10, 11]
250 for row
in range(table.rowCount()):
251 serial_number = table.item(row, 2).data(Qt.ItemDataRole.UserRole + 2)
254 keyfile_path = os.path.join(self.
directory1, serial_number)
256 keyfile_path = os.path.join(self.
directory2, serial_number)
260 if not os.path.isdir(keyfile_path):
263 meta_json_path = os.path.join(keyfile_path,
"metadata.json")
266 if os.path.exists(meta_json_path):
267 with open(meta_json_path,
"r", encoding=
"utf-8")
as f:
269 existing_data = json.load(f)
270 except json.JSONDecodeError:
275 for col
in range(table.columnCount()):
276 if col
in read_only_columns:
279 item = table.item(row, col)
280 column_name = table.horizontalHeaderItem(col).text()
282 field_name = column_name
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]
289 existing_data.update(meta_data)
291 with open(meta_json_path,
"w", encoding=
"utf-8")
as f:
292 json.dump(existing_data, f, indent=4, ensure_ascii=
False)
303 Open a dialog displaying information about the software.
306 "version":
"Unknown",
307 "release_date":
"Unknown"
310 with open(
resource_path(
'resources/about.json'),
'r', encoding=
'utf-8')
as file:
311 info = json.load(file)
312 except (FileNotFoundError, json.JSONDecodeError):
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."
334 license_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
335 license_label.setWordWrap(
True)
336 layout.addWidget(license_label)
337 dialog.setLayout(layout)
342 Import selected .od6pkg keyfiles as extracted folders into the deactivated directory.
343 Conflicts are handled based on user choice.
345 file_paths, _ = QFileDialog.getOpenFileNames(
347 self.tr(
"Select .od6pkg Files to Import"),
349 self.tr(
"OD6 Package Files (*.od6pkg)")
356 for source_path
in file_paths:
357 item_name = os.path.basename(source_path)
359 base_name = os.path.splitext(item_name)[0]
360 final_target_path = os.path.join(self.
directory2, base_name)
362 check_path_dir2 = os.path.join(self.
directory2, base_name)
363 check_path_dir1 = os.path.join(self.
directory1, base_name)
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
372 user_choice = QMessageBox.question(
375 f
"The folder '{base_name}' already exists. Overwrite?",
376 QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
378 if user_choice == QMessageBox.StandardButton.Yes:
379 shutil.rmtree(conflict_path)
384 with zipfile.ZipFile(source_path,
'r')
as zip_ref:
385 zip_ref.extractall(final_target_path)
386 except zipfile.BadZipFile:
389 self.tr(
"Invalid File"),
390 self.tr(f
"File '{item_name}' is not a valid .od6pkg archive and will be skipped.")
394 QMessageBox.information(
397 self.tr(
"Selected keyfiles have been imported to the deactivated directory.")
399 except Exception
as e:
400 QMessageBox.critical(self, self.tr(
"Error"), self.tr(f
"An error occurred: {e}"))
406 Export selected keyfiles from activated or deactivated directories to an external directory.
407 Compress each keyfile folder into .od6pkg format.
410 if not checked_serial_numbers:
411 QMessageBox.warning(self, self.tr(
"Error"), self.tr(
"No keyfile selected for export."))
414 export_path = QFileDialog.getExistingDirectory(self, self.tr(
"Select Export Directory"))
418 for serial_number
in checked_serial_numbers:
421 source_folder = os.path.join(self.
directory1, serial_number)
423 source_folder = os.path.join(self.
directory2, serial_number)
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."))
430 od6pkg_path = os.path.join(export_path, f
"{serial_number}.od6pkg")
432 if os.path.exists(od6pkg_path):
433 user_choice = QMessageBox.question(
435 self.tr(
"Conflict Detected"),
436 self.tr(f
"The file '{serial_number}.od6pkg' already exists. Overwrite?"),
437 QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
439 if user_choice != QMessageBox.StandardButton.Yes:
441 os.remove(od6pkg_path)
444 with zipfile.ZipFile(od6pkg_path,
'w', zipfile.ZIP_DEFLATED)
as zipf:
445 for root, dirs, files
in os.walk(source_folder):
447 file_path = os.path.join(root, file)
448 arcname = os.path.relpath(file_path, start=source_folder)
449 zipf.write(file_path, arcname)
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}"))
456 QMessageBox.information(self, self.tr(
"Success"), self.tr(
"Selected keyfiles exported successfully."))
461 Delete selected keyfiles and move them to trash directory.
464 if not checked_serial_numbers:
465 QMessageBox.warning(self, self.tr(
"Warning"), self.tr(
"No keyfile selected for deletion."))
468 confirm = QMessageBox.question(
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
474 if confirm != QMessageBox.StandardButton.Yes:
477 for serial
in checked_serial_numbers:
484 Open a dialog that displays deleted keyfiles and allows user to restore or permanently delete them.
492 Open a dialog for selecting two directories.
493 If valid, initialize KeyHandler and FolderContent, and save the paths to the config.json file.
495 dialog = QDialog(self)
497 open_ui.setupUi(dialog)
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))
503 dialog.setWindowFlags(dialog.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
504 dialog.setWindowTitle(self.tr(
"Directory Settings"))
509 open_ui.trashLineEdit.setText(self.
directory3)
511 open_ui.confirmButton.clicked.connect(
lambda: self.
config_manager.confirm_directory_selection(dialog, open_ui))
512 open_ui.cancelButton.clicked.connect(dialog.reject)
524 Initialize KeyHandler and FolderContent based on the selected directory paths.
525 If success, set up the table for further operations.
529 QMessageBox.warning(self,
"Error", self.tr(
"Directory validation failure"))
537 Rename the sensor name. Only work for the first selected entry.
540 if not serial_numbers:
542 serial_number = serial_numbers[0]
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:
548 sensor_name = self.
ui.tableWidget.item(row, 3).text()
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}")
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.
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
572 r"""Retrieve serial numbers for rows where the checkbox is checked. """
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
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)
589 r"""Control visibility of filter widget"""
590 self.
ui.filterDockWidget.setVisible(
not self.
ui.filterDockWidget.isVisible())
593 r"""Control visibility of extra information widget"""
594 self.
ui.infoDockWidget.setVisible(
not self.
ui.infoDockWidget.isVisible())
597 r"""Control visibility of full text search widget"""
598 self.
ui.searchDockWidget.setVisible(
not self.
ui.searchDockWidget.isVisible())
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.
606 if column == 0
or column == 1:
608 if self.
ui.tableWidget.item(row, 2).data(Qt.ItemDataRole.UserRole + 2)
is None:
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)
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)
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)
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)
633 self.
ui.infoTextBrowser.setHtml(output)
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.
645 self.tr(
'Serial Number'),
646 self.tr(
'Sensor Name')
651 fixed_tail_columns = [
652 self.tr(
'Last Edit Date'),
653 self.tr(
'Sensor Length (m)')
656 all_columns = fixed_columns + [self.tr(col)
for col
in custom_columns] + fixed_tail_columns
658 self.
ui.tableWidget.setColumnCount(len(all_columns))
659 self.
ui.tableWidget.setHorizontalHeaderLabels(all_columns)
661 fixed_keys = [
'checkbox',
'status',
'serial_number',
'sensor_name']
662 fixed_tail_keys = [
'last_edit_date',
'sensor_length']
664 for i, key
in enumerate(fixed_keys):
665 header_item = self.
ui.tableWidget.horizontalHeaderItem(i)
666 header_item.setData(Qt.ItemDataRole.UserRole, key)
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)
673 self.
ui.tableWidget.setStyleSheet(
"""
674 QHeaderView::section {
675 background-color: lightgray;
679 border: 1px solid black;
682 gridline-color: grey;
685 self.
ui.tableWidget.setColumnWidth(0, 10)
686 self.
ui.tableWidget.setColumnWidth(1, 180)
688 header = self.
ui.tableWidget.horizontalHeader()
689 header.setSectionsMovable(
True)
690 header.setDragEnabled(
True)
691 header.setDragDropMode(QHeaderView.DragDropMode.DragDrop)
696 self.
ui.tableWidget.setRowCount(0)
699 read_only_indices = [1, 2, 3] + [len(all_columns) - 2, len(all_columns) - 1]
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.
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)
716 item.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled)
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.
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)
728 item.setBackground(QBrush(QColor(245, 245, 245)))
732 Load activated and deactivated keys, set their status, and fill in all related metadata fields.
734 self.
ui.tableWidget.setSortingEnabled(
False)
736 activated_keys = set(self.
key_handler.read_keys(
'activated'))
737 deactivated_keys = set(self.
key_handler.read_keys(
'deactivated'))
739 keys_with_status = [(key,
"Activated")
for key
in activated_keys] + \
740 [(key,
"Deactivated")
for key
in deactivated_keys]
742 self.
ui.tableWidget.setRowCount(len(keys_with_status))
744 for row_idx, (key, status)
in enumerate(keys_with_status):
745 total_cols = self.
ui.tableWidget.columnCount()
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())
751 if status ==
'Activated':
752 activation_status = ActivationStatus.ACTIVATED
755 activation_status = ActivationStatus.DEACTIVATED
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)
763 check_item = self.
ui.tableWidget.item(row_idx, 0)
764 check_item.setCheckState(Qt.CheckState.Unchecked)
768 self.
ui.tableWidget.item(row_idx, 2).setText(key)
769 self.
ui.tableWidget.item(row_idx, 2).setData(Qt.ItemDataRole.UserRole + 2, key)
771 self.
ui.tableWidget.item(row_idx, 3).setText(self.
folder_content.read_sensor_name_for_key(key))
774 edit_date_str = edit_date.strftime(
"%Y-%m-%d")
if edit_date
else ""
776 last_edit_idx = self.
ui.tableWidget.columnCount() - 2
777 sensor_len_idx = self.
ui.tableWidget.columnCount() - 1
779 self.
ui.tableWidget.item(row_idx, last_edit_idx).setText(edit_date_str)
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)
787 value = metadata.get(col_name,
"")
788 self.
ui.tableWidget.item(row_idx, col_idx).setText(value)
790 self.
ui.tableWidget.setSortingEnabled(
True)
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.
800 status_translation_map = {
801 'Activated': self.tr(
"Activated"),
802 'Deactivated': self.tr(
"Deactivated"),
804 translated_status = status_translation_map[status]
805 button = QPushButton(translated_status)
806 button.setFixedSize(QSize(120, 20))
807 button.setEnabled(
False)
809 if status ==
'Activated':
810 button.setStyleSheet(
"""
811 background-color: #228B22;
817 elif status ==
'Deactivated':
818 button.setStyleSheet(
"""
819 background-color: #D3D3D3;
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)
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.
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)
846 activation_item.setData(Qt.ItemDataRole.UserRole + 1, ActivationStatus.ACTIVATED)
848 check_item.setCheckState(Qt.CheckState.Unchecked)
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.
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)
863 activation_item.setData(Qt.ItemDataRole.UserRole + 1, ActivationStatus.DEACTIVATED)
865 check_item.setCheckState(Qt.CheckState.Unchecked)
869 Switch the UI language and save it to the configuration file.
870 \param language (str): The target language, either 'English' or 'German'.
872 if language ==
'german':
874 translation_path =
resource_path(
'resources/translations/Translate_DE.qm')
876 QApplication.instance().installTranslator(self.
translator)
877 elif language ==
'english':
878 QApplication.instance().removeTranslator(self.
translator)
880 self.
ui.retranslateUi(self)
887 r"""Method to handle application exit."""
889 QCoreApplication.instance().quit()
892 """Triggered when user clicks X to close the window."""
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.
901 header = self.
ui.tableWidget.horizontalHeader()
902 total_columns = self.
ui.tableWidget.columnCount()
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)
909 column_name = header_item.text()
910 ordered_column_names.append(column_name)
912 ordered_custom_columns = []
913 for name
in ordered_column_names:
915 ordered_custom_columns.append(name)
922 r"""Open the Doxygen generated documentation web page."""
923 documentation_url =
"https://tud-imb.github.io/fosKeyMan/"
924 webbrowser.open(documentation_url)
928 Populate QComboBox with the keyfiles.
930 self.
ui.searchComboBox.clear()
931 self.
ui.searchComboBox.addItem(self.tr(
"Search Selected"))
933 for row
in range(self.
ui.tableWidget.rowCount()):
934 serial_number = self.
ui.tableWidget.item(row, 2).data(Qt.ItemDataRole.UserRole + 2)
936 keyfiles.add(serial_number)
937 for key
in sorted(keyfiles):
938 self.
ui.searchComboBox.addItem(key)
942 Perform search based on the selected key item in the comboBox and display the results in TextBrowser.
944 key = self.
ui.searchComboBox.currentText()
945 if key == self.tr(
"Search Selected"):
947 search_term = self.
ui.searchLineEdit.text()
949 self.
ui.searchTextBrowser.setPlainText(self.tr(
"Search term cannot be empty. Please enter a search term."))
951 search_results = self.
folder_content.full_text_search(search_term, key)
952 self.
ui.searchTextBrowser.clear()
955 for key, match
in search_results:
956 result_text = f
"Keyfile: {key}.zip\n"
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)
962 self.
ui.searchTextBrowser.setPlainText(self.tr(
"No results found."))
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.
969 for serial_number
in serial_numbers:
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:
978 if row_index
is None:
981 if serial_number
in self.
key_handler.read_keys(
'activated'):
982 activation_status = ActivationStatus.ACTIVATED
984 elif serial_number
in self.
key_handler.read_keys(
'deactivated'):
985 activation_status = ActivationStatus.DEACTIVATED
990 activation_item = self.
ui.tableWidget.item(row_index, 1)
991 activation_item.setData(Qt.ItemDataRole.UserRole + 1, activation_status)
993 check_item = self.
ui.tableWidget.item(row_index, 0)
994 check_item.setCheckState(Qt.CheckState.Unchecked)
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)
1001 self.
ui.tableWidget.item(row_index, 3).setText(self.
folder_content.read_sensor_name_for_key(serial_number))
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)
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)
1016 value = metadata.get(col_name,
"")
1017 self.
ui.tableWidget.item(row_index, col_idx).setText(value)
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.
1028 if getattr(sys,
'frozen',
False):
1029 base_dir = os.path.dirname(sys.executable)
1031 base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
1032 return os.path.join(base_dir, relative_path)
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.
1042 if getattr(sys,
'frozen',
False):
1043 base_dir = sys._MEIPASS
1045 base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
1046 return os.path.join(base_dir, relative_path)
1050 r"""Initialize and run the application."""
1051 app = QApplication(sys.argv)
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...
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.
Main User Interface for the application.
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.
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.
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.
dict dynamic_filter_inputs
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.