class MainWindow(QMainWindow): def __init__(self): super().__init__() self.n_times_clicked = 0 self.setWindowTitle("My App") self.button = QPushButton("Press Me!") self.button.clicked.connect(self.the_button_was_clicked) self.windowTitleChanged.connect(self.the_window_title_changed) # Set the central widget of the Window. self.setCentralWidget(self.button) def the_button_was_clicked(self): print("Clicked.") new_window_title = choice(window_titles) print("Setting title: %s" % new_window_title) self.setWindowTitle(new_window_title) def the_window_title_changed(self, window_title): print("Window title changed: %s" % window_title) if window_title == 'Something went wrong': self.button.setDisabled(True)
class myWindow(QMainWindow): def __init__(self) -> None: super().__init__() self.resize(500, 300) # 非组件对象 self.values = {} self.ut = UpdateValues() # 界面组件对象 self.centralwidget = QWidget(self) self.table = QTableWidget(self.centralwidget) self.startButton = QPushButton(self.centralwidget, text='开始') self.stopButton = QPushButton(self.centralwidget, text='停止') self.stopButton.setDisabled(True) self.table.setColumnCount(3) self.table.setHorizontalHeaderItem(0, QTableWidgetItem('GID')) self.table.setHorizontalHeaderItem(2, QTableWidgetItem('name')) self.table.setHorizontalHeaderItem(1, QTableWidgetItem('speed')) self.table.horizontalHeader().setStretchLastSection(True) self.table.setRowCount(1) self.layout = QGridLayout(self.centralwidget) self.layout.addWidget(self.startButton) self.layout.addWidget(self.stopButton) self.layout.addWidget(self.table) self.setCentralWidget(self.centralwidget) self.ut.resultReady.connect(self.on_changeValue) self.startButton.clicked.connect(self.changeValue) self.stopButton.clicked.connect(self.stopUpdate) def changeValue(self): self.startButton.setDisabled(True) self.stopButton.setEnabled(True) self.ut.isRun = 1 self.ut.start() def stopUpdate(self): self.ut.isRun = 0 self.startButton.setEnabled(True) self.stopButton.setDisabled(True) @Slot() def on_changeValue(self, values: dict): print(values) self.table.setItem(0, 0, QTableWidgetItem(str(values['gid']))) self.table.setItem(0, 1, QTableWidgetItem(str(values['file']))) self.table.setItem(0, 2, QTableWidgetItem(str(values['speed'])))
class GroupBoxWorkout(QGroupBox, BasicWidget): def __init__(self, title, parent): super().__init__(title, parent) self.setLayout(QHBoxLayout(self)) self.button_start = QPushButton("Start Workout", self) self.button_start.clicked.connect(self.switch_button_ability) self.layout().addWidget(self.button_start) self.button_finish = QPushButton("Finish Workout", self) self.button_finish.setDisabled(True) self.button_finish.clicked.connect(self.switch_button_ability) self.layout().addWidget(self.button_finish) def switch_button_ability(self): if self.button_start.isEnabled( ) and not self.button_finish.isEnabled(): self.button_start.setDisabled(True) self.button_finish.setEnabled(True) elif not self.button_start.isEnabled( ) and self.button_finish.isEnabled(): self.button_start.setEnabled(True) self.button_finish.setDisabled(True)
class UITransferROIWindow: def setup_ui(self, transfer_roi_window_instance, signal_roi_transferred_to_fixed_container, signal_roi_transferred_to_moving_container): self.patient_dict_container = PatientDictContainer() self.moving_dict_container = MovingDictContainer() self.fixed_image_initial_rois = self.patient_dict_container.get("rois") self.moving_image_initial_rois = self.moving_dict_container.get("rois") self.transfer_roi_window_instance = transfer_roi_window_instance self.signal_roi_transferred_to_fixed_container = \ signal_roi_transferred_to_fixed_container self.signal_roi_transferred_to_moving_container = \ signal_roi_transferred_to_moving_container self.fixed_to_moving_rois = {} self.moving_to_fixed_rois = {} self.add_suffix = True self.progress_window = ProgressWindow( self, QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint) self.progress_window.setFixedSize(250, 100) self.progress_window.signal_loaded \ .connect(self.onTransferRoiFinished) self.progress_window.signal_error.connect(self.onTransferRoiError) self.init_layout() def retranslate_ui(self, transfer_roi_window_instance): _translate = QtCore.QCoreApplication.translate transfer_roi_window_instance.setWindowTitle( _translate("TransferRoiWindowInstance", "OnkoDICOM - Transfer Region of Interest")) self.add_suffix_checkbox.setText( _translate("AddSuffixCheckBox", "Add Suffix")) self.patient_A_label.setText( _translate("PatientAROILabel", "First Image Set ROIs")) self.patient_B_label.setText( _translate("PatientBROILabel", "Second Image Set ROIs")) self.transfer_all_rois_to_patient_B_button.setText( _translate("ROITransferToBButton", "All")) self.transfer_all_rois_to_patient_A_button.setText( _translate("ROITransferToAButton", "All")) self.save_button.setText(_translate("SaveButton", "Save")) self.reset_button.setText(_translate("ResetButton", "Reset")) def init_layout(self): """ Initialize the layout for the Transfer ROI Window. """ if platform.system() == 'Darwin': self.stylesheet_path = "res/stylesheet.qss" else: self.stylesheet_path = "res/stylesheet-win-linux.qss" stylesheet = open(resource_path(self.stylesheet_path)).read() window_icon = QIcon() window_icon.addPixmap(QPixmap(resource_path("res/images/icon.ico")), QIcon.Normal, QIcon.Off) self.transfer_roi_window_instance.setObjectName( "TransferRoiWindowInstance") self.transfer_roi_window_instance.setWindowIcon(window_icon) # Creating a grid layout to hold all elements self.transfer_roi_window_grid_layout = QGridLayout() self.transfer_roi_window_grid_layout.setColumnStretch(0, 1) self.transfer_roi_window_grid_layout.setColumnStretch(1, 1) self.transfer_roi_window_grid_layout.setColumnStretch(2, 1) self.init_patient_labels() self.init_transfer_arrow_buttons() self.init_patient_A_initial_roi_list() self.init_patient_B_rois_to_A_layout() self.init_patient_A_rois_to_B_layout() self.init_patient_B_initial_roi_list() self.init_add_suffix_checkbox() self.init_save_and_reset_button_layout() # Create a new central widget to hold the grid layout self.transfer_roi_window_instance_central_widget = QWidget() self.transfer_roi_window_instance_central_widget.setLayout( self.transfer_roi_window_grid_layout) self.retranslate_ui(self.transfer_roi_window_instance) self.transfer_roi_window_instance.setStyleSheet(stylesheet) self.transfer_roi_window_instance.setCentralWidget( self.transfer_roi_window_instance_central_widget) QtCore.QMetaObject.connectSlotsByName( self.transfer_roi_window_instance) def init_transfer_arrow_buttons(self): """ Initialize the layout for arrow buttons """ self.transfer_all_rois_to_patient_B_button = QPushButton() self.transfer_all_rois_to_patient_B_button.setObjectName( "ROITransferToBButton") transfer_all_rois_to_patient_B_icon = QtGui.QIcon() transfer_all_rois_to_patient_B_icon.addPixmap( QtGui.QPixmap( resource_path('res/images/btn-icons/forward_slide_icon.png')), QtGui.QIcon.Normal, QtGui.QIcon.On) self.transfer_all_rois_to_patient_B_button \ .setIcon(transfer_all_rois_to_patient_B_icon) self.transfer_all_rois_to_patient_B_button.clicked.connect( self.transfer_all_rois_to_patient_B_button_clicked) self.transfer_roi_window_grid_layout.addWidget( self.transfer_all_rois_to_patient_B_button, 1, 1) self.transfer_all_rois_to_patient_A_button = QPushButton() self.transfer_all_rois_to_patient_A_button.setObjectName( "ROITransferToAButton") self.transfer_all_rois_to_patient_A_button.setMaximumWidth(100) transfer_all_rois_to_patient_A_icon = QtGui.QIcon() transfer_all_rois_to_patient_A_icon.addPixmap( QtGui.QPixmap( resource_path('res/images/btn-icons/backward_slide_icon.png')), QtGui.QIcon.Normal, QtGui.QIcon.On) self.transfer_all_rois_to_patient_A_button \ .setIcon(transfer_all_rois_to_patient_A_icon) self.transfer_all_rois_to_patient_A_button.clicked.connect( self.transfer_all_rois_to_patient_A_button_clicked) self.transfer_roi_window_grid_layout.addWidget( self.transfer_all_rois_to_patient_A_button, 2, 1) def transfer_all_rois_to_patient_B_button_clicked(self): """ This function is triggered when the right arrow button is clicked. """ self.fixed_to_moving_rois.clear() self.patient_A_rois_to_B_list_widget.clear() for i in range(0, len(self.fixed_image_initial_rois)): self.patient_A_initial_roi_double_clicked( self.patient_A_initial_rois_list_widget.item(i)) def transfer_all_rois_to_patient_A_button_clicked(self): """ This function is triggered when the left arrow button is clicked. """ self.moving_to_fixed_rois.clear() self.patient_B_rois_to_A_list_widget.clear() for i in range(0, len(self.moving_image_initial_rois)): self.patient_B_initial_roi_double_clicked( self.patient_B_initial_rois_list_widget.item(i)) def init_add_suffix_checkbox(self): """ Initialize the layout for add suffix checkbox """ self.add_suffix_checkbox = QCheckBox() self.add_suffix_checkbox.setObjectName("AddSuffixCheckBox") self.add_suffix_checkbox.setChecked(self.add_suffix) self.add_suffix_checkbox.clicked.connect( self.add_suffix_checkbox_clicked) self.transfer_roi_window_grid_layout.addWidget( self.add_suffix_checkbox, 3, 0) def init_patient_labels(self): """ Initialize the layout for two patient labels """ self.patient_A_label = QLabel() self.patient_A_label.setObjectName("PatientAROILabel") self.patient_A_label.setMinimumHeight(50) self.patient_A_label.setAlignment(Qt.AlignCenter) self.patient_A_label.setStyleSheet( "QLabel { background-color : green; color : white; " "font-size: 15pt; font-weight: bold;}") self.patient_B_label = QLabel() self.patient_B_label.setObjectName("PatientBROILabel") self.patient_B_label.setMinimumHeight(50) self.patient_B_label.setAlignment(Qt.AlignCenter) self.patient_B_label.setStyleSheet( "QLabel { background-color : red; color : white; " "font-size: 15pt; font-weight: bold;}") self.transfer_roi_window_grid_layout.addWidget(self.patient_A_label, 0, 0) self.transfer_roi_window_grid_layout.addWidget(self.patient_B_label, 0, 2) def init_save_and_reset_button_layout(self): """ Initialize the layout for save and reset buttons """ self.reset_and_save_buttons_layout = QHBoxLayout() self.reset_button = QPushButton() self.reset_button.setObjectName("ResetButton") self.reset_button.clicked.connect(self.reset_clicked) self.save_button = QPushButton() self.save_button.setObjectName("SaveButton") self.save_button.setDisabled(True) self.save_button.clicked.connect(self.transfer_roi_clicked) self.reset_and_save_buttons_layout.setAlignment(Qt.AlignRight) self.reset_and_save_buttons_layout.addWidget(self.reset_button) self.reset_and_save_buttons_layout.addWidget(self.save_button) # Create a widget to hold Reset and Save buttons self.reset_and_save_button_central_widget = QWidget() self.reset_and_save_button_central_widget.setLayout( self.reset_and_save_buttons_layout) self.transfer_roi_window_grid_layout.addWidget( self.reset_and_save_button_central_widget, 3, 2) def add_suffix_checkbox_clicked(self): """ This function is triggered when the add suffix checkbox is clicked """ self.add_suffix = self.add_suffix_checkbox.isChecked() def init_patient_B_rois_to_A_layout(self): """ Initialize the layout for transfer rois from B to A container """ # Create scrolling area widget to contain the content. self.patient_B_rois_to_A_list_widget = QListWidget(self) self.transfer_roi_window_grid_layout \ .addWidget(self.patient_B_rois_to_A_list_widget, 2, 0) self.patient_B_rois_to_A_list_widget.itemDoubleClicked.connect( self.patient_B_to_A_rois_double_clicked) def init_patient_A_rois_to_B_layout(self): """ Initialize the layout for transfer rois from A to B container """ self.patient_A_rois_to_B_list_widget = QListWidget(self) self.transfer_roi_window_grid_layout \ .addWidget(self.patient_A_rois_to_B_list_widget, 1, 2) self.patient_A_rois_to_B_list_widget.itemDoubleClicked.connect( self.patient_A_to_B_rois_double_clicked) def init_patient_A_initial_roi_list(self): """ Initialize the layout for patient A's roi list """ self.patient_A_initial_rois_list_widget = QListWidget(self) self.patient_A_initial_rois_list_widget.itemDoubleClicked.connect( self.patient_A_initial_roi_double_clicked) for idx in self.fixed_image_initial_rois: roi_label = QListWidgetItem( self.fixed_image_initial_rois[idx]['name']) roi_label.setForeground(Qt.darkGreen) roi_label.setData(Qt.UserRole, self.fixed_image_initial_rois[idx]) self.patient_A_initial_rois_list_widget.addItem(roi_label) self.transfer_roi_window_grid_layout.addWidget( self.patient_A_initial_rois_list_widget, 1, 0) def init_patient_B_initial_roi_list(self): """ Initialize the layout for patient B's roi list """ self.patient_B_initial_rois_list_widget = QListWidget(self) self.patient_B_initial_rois_list_widget.itemDoubleClicked.connect( self.patient_B_initial_roi_double_clicked) for idx in self.moving_image_initial_rois: roi_label = QListWidgetItem( self.moving_image_initial_rois[idx]['name']) roi_label.setForeground(Qt.red) roi_label.setData(Qt.UserRole, self.moving_image_initial_rois[idx]) self.patient_B_initial_rois_list_widget.addItem(roi_label) self.transfer_roi_window_grid_layout.addWidget( self.patient_B_initial_rois_list_widget, 2, 2) def patient_A_to_B_rois_double_clicked(self, item): """ This function is triggered when a roi in "A to B" list is double-clicked. """ roi_to_remove = item.data(Qt.UserRole) to_delete_value = roi_to_remove['name'] self.fixed_to_moving_rois.pop(to_delete_value) self.patient_A_rois_to_B_list_widget.clear() for key, value in self.fixed_to_moving_rois.items(): roi_label = QListWidgetItem(value) roi_label.setForeground(Qt.red) roi_label.setData(Qt.UserRole, {'name': key}) self.patient_A_rois_to_B_list_widget.addItem(roi_label) if self.transfer_list_is_empty(): self.save_button.setDisabled(True) def patient_B_to_A_rois_double_clicked(self, item): """ This function is triggered when a roi in "B to A" list is double-clicked. """ roi_to_remove = item.data(Qt.UserRole) to_delete_value = roi_to_remove['name'] self.moving_to_fixed_rois.pop(to_delete_value) self.patient_B_rois_to_A_list_widget.clear() for key, value in self.moving_to_fixed_rois.items(): roi_label = QListWidgetItem(value) roi_label.setForeground(Qt.red) roi_label.setData(Qt.UserRole, {'name': key}) self.patient_B_rois_to_A_list_widget.addItem(roi_label) if self.transfer_list_is_empty(): self.save_button.setDisabled(True) def patient_A_initial_roi_double_clicked(self, item): """ This function is triggered when a roi in patient A's roi list is double-clicked. """ roi_to_add = item.data(Qt.UserRole) transferred_roi_name = roi_to_add['name'] # If the clicked roi is already transferred, return if transferred_roi_name in self.fixed_to_moving_rois.keys(): QMessageBox.about(self, "Transfer Failed", "Chosen ROI has already been transferred!") return # Create a set to store all current roi names in target patient # including both initial rois name and added roi names so far patient_B_initial_roi_name_list = set() for item in self.fixed_to_moving_rois.values(): patient_B_initial_roi_name_list.add(item) for idx in self.moving_image_initial_rois: patient_B_initial_roi_name_list.add( self.moving_image_initial_rois[idx]['name']) # Check if clicked roi name has duplicate # in patient B's initial roi names list if transferred_roi_name in patient_B_initial_roi_name_list: if self.add_suffix: transferred_roi_name = generate_non_duplicated_name( transferred_roi_name, patient_B_initial_roi_name_list) else: QMessageBox.about( self, "Transfer Failed", "Duplicated ROI name. " "Please consider adding suffix.") return # Add clicked roi to transferred list self.fixed_to_moving_rois[roi_to_add['name']] = transferred_roi_name roi_label = QListWidgetItem(transferred_roi_name) roi_label.setForeground(Qt.red) roi_label.setData(Qt.UserRole, roi_to_add) self.patient_A_rois_to_B_list_widget.addItem(roi_label) self.save_button.setDisabled(False) def patient_B_initial_roi_double_clicked(self, item): """ This function is triggered when a roi in patient B's roi list is double-clicked. """ roi_to_add = item.data(Qt.UserRole) transferred_roi_name = roi_to_add['name'] # If the clicked roi is already transferred, return if transferred_roi_name in self.moving_to_fixed_rois.keys(): QMessageBox.about(self, "Transfer Failed", "Chosen ROI has already been transferred!") return # Create a set to store all current roi names in target patient # including both initial rois name and added roi names so far patient_A_current_roi_name_list = set() for item in self.moving_to_fixed_rois.values(): patient_A_current_roi_name_list.add(item) for idx in self.fixed_image_initial_rois: patient_A_current_roi_name_list.add( self.fixed_image_initial_rois[idx]['name']) # Check if clicked roi name has duplicate in # target patient's roi names list if transferred_roi_name in patient_A_current_roi_name_list: # if add suffix is ticked, iteratively try adding suffix # from _A to _Z, stop when no duplicate found if self.add_suffix: transferred_roi_name = generate_non_duplicated_name( transferred_roi_name, patient_A_current_roi_name_list) else: QMessageBox.about( self, "Transfer Failed", "Duplicated ROI name. " "Please consider adding suffix.") return # Add clicked roi to transferred list self.moving_to_fixed_rois[roi_to_add['name']] = transferred_roi_name roi_label = QListWidgetItem(transferred_roi_name) roi_label.setForeground(Qt.red) roi_label.setData(Qt.UserRole, roi_to_add) self.patient_B_rois_to_A_list_widget.addItem(roi_label) self.save_button.setDisabled(False) def reset_clicked(self): """ This function is triggered when reset button is clicked. """ self.fixed_to_moving_rois.clear() self.moving_to_fixed_rois.clear() self.patient_A_rois_to_B_list_widget.clear() self.patient_B_rois_to_A_list_widget.clear() self.save_button.setDisabled(True) def transfer_list_is_empty(self): """ This function is to check if the transfer list is empty """ return len(self.fixed_to_moving_rois) == 0 \ and len(self.moving_to_fixed_rois) == 0 def save_clicked(self, interrupt_flag, progress_callback): """ This function is triggered when the save button is clicked. It contains all steps in the ROI transferring process. :param interrupt_flag: interrupt flag to stop process :param progress_callback: signal that receives the current progress of the loading. """ progress_callback.emit(("Converting images to sitk", 0)) # check if interrupt flag is set if not check_interrupt_flag(interrupt_flag): return False rtss = self.patient_dict_container.get("dataset_rtss") # get sitk for the fixed image dicom_image = read_dicom_image_to_sitk( self.patient_dict_container.filepaths) if not check_interrupt_flag(interrupt_flag): return False # get array of roi indexes from sitk images rois_images_fixed = transform_point_set_from_dicom_struct( dicom_image, rtss, self.fixed_to_moving_rois.keys(), spacing_override=None, interrupt_flag=interrupt_flag) moving_rtss = self.moving_dict_container.get("dataset_rtss") if not check_interrupt_flag(interrupt_flag): return False # get sitk for the moving image moving_dicom_image = read_dicom_image_to_sitk( self.moving_dict_container.filepaths) if not check_interrupt_flag(interrupt_flag): return False # get array of roi indexes from sitk images progress_callback \ .emit(("Retrieving ROIs from \nboth image sets", 20)) # check if interrupt flag is set if not check_interrupt_flag(interrupt_flag): return False if moving_rtss: rois_images_moving = transform_point_set_from_dicom_struct( moving_dicom_image, moving_rtss, self.moving_to_fixed_rois.keys(), spacing_override=None, interrupt_flag=interrupt_flag) else: rois_images_moving = ([], []) if not check_interrupt_flag(interrupt_flag): return False tfm = self.moving_dict_container.get("tfm") progress_callback.emit( ("Transfering ROIs from moving \nto fixed image set", 40)) # check if interrupt flag is set if not check_interrupt_flag(interrupt_flag): return False # transform roi from moving_dict to fixed_dict self.transfer_rois(self.moving_to_fixed_rois, tfm, dicom_image, rois_images_moving, self.patient_dict_container) progress_callback.emit( ("Transfering ROIs from fixed \nto moving image set", 60)) if not check_interrupt_flag(interrupt_flag): return False # transform roi from moving_dict to fixed_dict self.transfer_rois(self.fixed_to_moving_rois, tfm.GetInverse(), moving_dicom_image, rois_images_fixed, self.moving_dict_container) progress_callback.emit(("Saving ROIs to RTSS", 80)) # check if interrupt flag is set if not check_interrupt_flag(interrupt_flag): return False progress_callback.emit(("Reloading window", 90)) return True def transfer_roi_clicked(self): """ telling progress window to start ROI transfer """ self.progress_window.start(self.save_clicked) def onTransferRoiError(self, exception): """ This function is triggered when there is an error in the ROI transferring process. :param exception: exception thrown """ QMessageBox.about(self.progress_window, "Unable to transfer ROIs", "Please check your image set and ROI data.") self.progress_window.close() def onTransferRoiFinished(self, result): """ This function is triggered when ROI transferring process is finished. """ # emit changed dataset to structure_modified function and # auto_save_roi function if result[0] is True: if len(self.fixed_to_moving_rois) > 0: self.signal_roi_transferred_to_moving_container.emit( (self.moving_dict_container.get("dataset_rtss"), { "transfer": None })) if len(self.moving_to_fixed_rois) > 0: self.signal_roi_transferred_to_fixed_container.emit( (self.patient_dict_container.get("dataset_rtss"), { "transfer": None })) self.progress_window.close() QMessageBox.about(self.transfer_roi_window_instance, "Saved", "ROIs are successfully transferred!") else: QMessageBox.about(self.transfer_roi_window_instance, "Cancelled", "ROIs Transfer is cancelled.") self.closeWindow() def transfer_rois(self, transfer_dict, tfm, reference_image, original_roi_list, patient_dict_container): """ Converting (transferring) ROIs from one image set to another and save the transferred rois to rtss. :param transfer_dict: dictionary of rois to be transfer. key is original roi names, value is the name after transferred. :param original_roi_list: tuple of sitk rois from the base image. :param tfm: the tfm that contains information for transferring rois :param reference_image: the reference (base) image :param patient_dict_container: container of the transfer image set. """ for roi_name, new_roi_name in transfer_dict.items(): for index, name in enumerate(original_roi_list[1]): if name == roi_name: sitk_image = original_roi_list[0][index] new_contour = apply_linear_transform( input_image=sitk_image, transform=tfm, reference_image=reference_image, is_structure=True) contour = sitk.GetArrayViewFromImage(new_contour) contours = np.transpose(contour.nonzero()) self.save_roi_to_patient_dict_container( contours, new_roi_name, patient_dict_container) def save_roi_to_patient_dict_container(self, contours, roi_name, patient_dict_container): """ Save the transferred ROI to the corresponding rtss. :param contours: np array of coordinates of the ROI to be saved. :param roi_name: name of the ROI to be saved :param patient_dict_container: container of the transfer image set. """ pixels_coords_dict = {} slice_ids_dict = get_dict_slice_to_uid(patient_dict_container) total_slices = len(slice_ids_dict) for contour in contours: curr_slice_id = total_slices - contour[0] if curr_slice_id >= total_slices: curr_slice_id = 0 if curr_slice_id not in pixels_coords_dict: pixels_coords_dict[curr_slice_id] = [ tuple([contour[2], contour[1]]) ] else: pixels_coords_dict[curr_slice_id].append( tuple([contour[2], contour[1]])) rois_to_save = {} for key in pixels_coords_dict.keys(): coords = pixels_coords_dict[key] polygon_list = ROI.calculate_concave_hull_of_points(coords) if len(polygon_list) > 0: rois_to_save[key] = { 'ds': patient_dict_container.dataset[key], 'coords': polygon_list } roi_list = ROI.convert_hull_list_to_contours_data( rois_to_save, patient_dict_container) if len(roi_list) > 0: print("Saving ", roi_name) if isinstance(patient_dict_container, MovingDictContainer): new_rtss = ROI.create_roi( patient_dict_container.get("dataset_rtss"), roi_name, roi_list, rtss_owner="MOVING") self.moving_dict_container.set("dataset_rtss", new_rtss) self.moving_dict_container.set("rtss_modified", True) else: new_rtss = ROI.create_roi( patient_dict_container.get("dataset_rtss"), roi_name, roi_list) self.patient_dict_container.set("dataset_rtss", new_rtss) self.patient_dict_container.set("rtss_modified", True) def closeWindow(self): """ function to close transfer roi window """ self.close()
class Importador(QWidget): """GUI class for the BGG -> Ludopedia importer""" enable_editables = Signal(bool) alternative_chosen = Signal(object) def __init__(self, parent=None): super().__init__(parent) self.thread = QThread() self.worker = None grid_layout = QGridLayout(self) login_group_box = self.create_login_group() data_group_box = self.create_data_group() self.enable_editables.connect(login_group_box.setEnabled) self.enable_editables.connect(data_group_box.setEnabled) self.import_button = QPushButton('Importar', self) self.import_button.setEnabled(False) self.enable_editables.connect(self.import_button.setEnabled) self.import_button.clicked.connect(self.enable_editables) self.import_button.clicked.connect(self.load_data) self.bgg_user_line_edit.textChanged.connect(self.enable_import) self.ludo_mail_line_edit.textChanged.connect(self.enable_import) self.ludo_pass_line_edit.textChanged.connect(self.enable_import) grid_layout.addWidget(login_group_box, 1, 1, 1, 2) grid_layout.addWidget(data_group_box, 2, 1, 1, 2) grid_layout.addWidget(self.import_button, 8, 2) self.log_widget = QTextEdit(self) self.log_widget.setReadOnly(True) grid_layout.addWidget(self.log_widget, 9, 1, 30, 2) def create_qlineedit(self, text): """Creates a label with the given text and an accompanying line edit""" line_edit = QLineEdit(self) line_edit_label = QLabel(text, line_edit) line_edit_label.setBuddy(line_edit) return (line_edit, line_edit_label) def create_login_group(self): """Create labels and line edits for providing BGG and ludopedia login information""" (self.bgg_user_line_edit, bgg_user_label) = self.create_qlineedit('Usuario BoardGameGeek:') (self.ludo_mail_line_edit, ludo_mail_label) = self.create_qlineedit('E-mail Ludopedia:') (self.ludo_pass_line_edit, ludo_pass_label) = self.create_qlineedit('Senha Ludopedia:') self.ludo_pass_line_edit.setEchoMode(QLineEdit.PasswordEchoOnEdit) group_box = QGroupBox('Login') grid_layout = QGridLayout(group_box) grid_layout.addWidget(bgg_user_label, 1, 1) grid_layout.addWidget(self.bgg_user_line_edit, 1, 2) grid_layout.addWidget(ludo_mail_label, 2, 1) grid_layout.addWidget(self.ludo_mail_line_edit, 2, 2) grid_layout.addWidget(ludo_pass_label, 3, 1) grid_layout.addWidget(self.ludo_pass_line_edit, 3, 2) group_box.setLayout(grid_layout) return group_box def create_data_group(self): """Creates group for holding specific choice data selection""" button_group = QButtonGroup(self) button_group.setExclusive(True) colecao_radio_button = QRadioButton('Coleção') self.partidas_radio_button = QRadioButton('Partidas') colecao_radio_button.setChecked(True) button_group.addButton(colecao_radio_button) button_group.addButton(self.partidas_radio_button) (self.min_date_picker, min_date_label) = create_date_picker('À Partir de:', self) (self.max_date_picker, max_date_label) = create_date_picker('Até:', self) self.min_date_picker.dateChanged.connect( self.max_date_picker.setMinimumDate) colecao_radio_button.toggled.connect(self.min_date_picker.setDisabled) colecao_radio_button.toggled.connect(self.max_date_picker.setDisabled) self.map_users_button = QPushButton( 'Ver mapa de usuarios BGG -> Ludopedia', self) self.map_users_button.setEnabled(False) self.map_users_button.clicked.connect(self.user_map) colecao_radio_button.toggled.connect(self.map_users_button.setDisabled) group_box = QGroupBox('Dados') grid_layout = QGridLayout(group_box) grid_layout.addWidget(colecao_radio_button, 1, 1) grid_layout.addWidget(self.partidas_radio_button, 1, 2) grid_layout.addWidget(min_date_label, 2, 1) grid_layout.addWidget(self.min_date_picker, 2, 2) grid_layout.addWidget(max_date_label, 3, 1) grid_layout.addWidget(self.max_date_picker, 3, 2) grid_layout.addWidget(self.map_users_button, 4, 1, 1, 2) group_box.setLayout(grid_layout) return group_box def enable_import(self): """Slot to toggle state of the import button""" self.import_button.setDisabled(not self.bgg_user_line_edit.text() or not self.ludo_mail_line_edit.text() or not self.ludo_pass_line_edit.text()) def log_text(self, message_type, text): """Logs the given text to the QPlainTextWidget""" current_time = QTime.currentTime().toString() if message_type == MessageType.ERROR: self.log_widget.insertHtml( f'[{current_time}] {ERROR_HTML}{text}<br>') elif message_type == MessageType.GENERIC: self.log_widget.insertHtml(f'[{current_time}] {text}<br>') elif message_type == MessageType.DEBUG and ENABLE_DEBUG: self.log_widget.insertHtml( f'[{current_time}] {DEBUG_HTML}{text}<br>') self.log_widget.moveCursor(QTextCursor.End) if ENABLE_DEBUG: print(text) def disconnect_thread(self): """Disconnect the started signal from the thread""" self.thread.started.disconnect() def configure_thread(self, worker): """Does basic thread startup and cleanup configuration""" worker.finished.connect(self.thread.quit) worker.moveToThread(self.thread) self.thread.started.connect(worker.run) worker.message.connect(self.log_text) worker.finished.connect(self.disconnect_thread) worker.exit_on_error.connect(self.thread.quit) worker.exit_on_error.connect(lambda: self.enable_editables.emit(True)) def load_data(self): """Load data from bgg""" try: (session, ludo_user_id) = self.login_ludopedia() bgg_user = self.bgg_user_line_edit.text() if self.partidas_radio_button.isChecked(): current_date = format_qdate(QDate.currentDate()) min_date = parse_date( format_qdate(self.min_date_picker.date()), current_date) max_date = parse_date( format_qdate(self.max_date_picker.date()), min_date) self.worker = BGGPlayFetcher(bgg_user, min_date, max_date) self.configure_thread(self.worker) self.worker.finished.connect(lambda plays: self.post_plays( session, plays, bgg_user, ludo_user_id)) else: self.worker = BGGColectionFetcher(bgg_user) self.configure_thread(self.worker) self.worker.finished.connect( lambda bgg_collection: self.import_collection( session, bgg_collection)) self.thread.start() except InputError: self.enable_editables.emit(True) def show_play_table(self, plays): """Shows a table with all the plays to be imported, allowing user to select some to skip""" tree_model = PlayTableModel(plays) table_widget = QTableView() table_widget.setModel(tree_model) table_widget.verticalHeader().setVisible(False) table_view_header = table_widget.horizontalHeader() table_view_header.setStretchLastSection(True) for column in range(tree_model.columnCount()): column_size = tree_model.data(tree_model.index(0, column), PlayTableModel.SIZE_ROLE) table_view_header.resizeSection(column, column_size) table_widget_dialog = QDialog(self) table_widget_dialog.setModal(True) grid_layout = QGridLayout(table_widget_dialog) grid_layout.addWidget(table_widget, 1, 1) table_widget_dialog.resize(800, 600) table_widget_dialog.exec_() skipped_plays = tree_model.get_skipped_plays() return [play for play in plays if play.id not in skipped_plays] def post_plays(self, session, plays, bgg_user, ludo_user_id): """Receives plays from the Play Fetched thread and start the Ludopedia Logger""" user_map = self.get_bgg_to_ludo_users() if bgg_user not in user_map: user_map[bgg_user] = ludo_user_id selected_plays = self.show_play_table(plays) self.worker = LudopediaPlayLogger(session, selected_plays, bgg_user, user_map) self.worker.request_search.connect( self.request_search_and_show_alternatives, Qt.BlockingQueuedConnection) self.worker.request_alternative.connect(self.request_alternative, Qt.BlockingQueuedConnection) self.alternative_chosen.connect(self.worker.receive_alternative, Qt.DirectConnection) self.configure_thread(self.worker) self.worker.finished.connect(lambda: self.enable_editables.emit(True)) self.thread.start() def user_map(self): """Slot to show user map from bgg to ludopedia""" user_map_dialog = QDialog(self) user_map_dialog.setModal(True) bgg_to_ludo = self.get_bgg_to_ludo_users() user_list = [f'{key} -> {value}' for key, value in bgg_to_ludo.items()] list_widget = QListWidget(user_map_dialog) list_widget.addItems(user_list) list_widget.setResizeMode(QListView.Adjust) list_widget.sortItems() grid_layout = QGridLayout(user_map_dialog) grid_layout.addWidget(list_widget, 1, 1) user_map_dialog.resize(400, 400) user_map_dialog.show() def login_ludopedia(self): """Logins into Ludopedia manually and returns the session and user_id""" self.log_text(MessageType.GENERIC, 'Obtendo dados do Ludopedia') payload = { 'email': self.ludo_mail_line_edit.text(), 'pass': self.ludo_pass_line_edit.text() } session = requests.Session() session_request = session.post(LUDOPEDIA_LOGIN_URL, data=payload) if 'senha incorretos' in session_request.text: self.log_text( MessageType.ERROR, 'Não foi possível logar na Ludopedia com as informações fornecidas' ) raise InputError user_re = re.search(r'id_usuario=(\d+)', session_request.text) user_id = user_re.group(1) if user_re else None return (session, user_id) def import_collection(self, session, collection): """Imports a given collection into Ludopedia""" self.worker = LudopediaCollectionLogger(session, collection) self.configure_thread(self.worker) self.worker.finished.connect(lambda: self.enable_editables.emit(True)) self.thread.start() def show_alternatives_dialog(self, bgg_play, data): """Show alternative games to use as the game to log a play""" alternatives_dialog = QInputDialog(self) alternatives_list = [ f'{item["nm_jogo"]} ({item["ano_publicacao"]})' for item in data ] alternatives_dialog.setComboBoxItems(alternatives_list) alternatives_dialog.setOption(QInputDialog.UseListViewForComboBoxItems) game_str = f'{bgg_play.game_name} ({bgg_play.year_published})' alternatives_dialog.setLabelText( f'Escolha uma alternativa para o jogo "{game_str}"') if alternatives_dialog.exec_(): selected_index = alternatives_list.index( alternatives_dialog.textValue()) return data[selected_index] return None def request_search_and_show_alternatives(self, session, bgg_play): """Request a new string to use for game search and then show results to be picked""" new_search_dialog = QInputDialog(self) game_str = f'{bgg_play.game_name} ({bgg_play.year_published})' new_search_dialog.setLabelText( f'Jogo "{game_str}" não encontrado\nBuscar por:') new_search_dialog.setInputMode(QInputDialog.TextInput) if new_search_dialog.exec_(): data = search_ludopedia_games(session, new_search_dialog.textValue()) data = self.show_alternatives_dialog(bgg_play, data) self.alternative_chosen.emit(data) def request_alternative(self, bgg_play, data): """Request an alternative from user and emit choice""" alternative = self.show_alternatives_dialog(bgg_play, data) self.alternative_chosen.emit(alternative) def get_bgg_to_ludo_users(self): """Reads usuarios.txt file to map a bgg user to its corresponding ludopedia one""" try: parser = ConfigParser() with open("usuarios.txt") as lines: lines = chain(("[top]", ), lines) parser.read_file(lines) bgg_to_ludo_user = dict(parser['top']) bgg_to_ludo_user_id = dict() for bgg_user, ludo_user in bgg_to_ludo_user.items(): if is_invalid_bgg_user(bgg_user): self.log_text( MessageType.ERROR, f'Usuário do BGG "{bgg_user}" inválido' f' no mapa de usuários') continue if ludo_user.isdigit(): bgg_to_ludo_user_id[bgg_user] = ludo_user self.log_text( MessageType.DEBUG, f'Usuário do BGG "{bgg_user}" já mapeado' f' ao id ludopedia: {ludo_user}') else: ludo_user_id = get_ludo_user_id(ludo_user) if ludo_user_id: self.log_text(MessageType.DEBUG, f'{ludo_user_id} para {ludo_user}') bgg_to_ludo_user_id[bgg_user] = ludo_user_id else: self.log_text( MessageType.ERROR, f'Falha ao buscar id de usuario da' f' ludopedia para "{ludo_user}"') return bgg_to_ludo_user_id except FileNotFoundError: self.log_error( MessageType.ERROR, 'Não foi possível encontrar o arquivo "usuarios.txt') return {}
class UIOpenPatientWindow(object): patient_info_initialized = QtCore.Signal(object) def setup_ui(self, open_patient_window_instance): if platform.system() == 'Darwin': self.stylesheet_path = "res/stylesheet.qss" else: self.stylesheet_path = "res/stylesheet-win-linux.qss" window_icon = QIcon() window_icon.addPixmap(QPixmap(resource_path("res/images/icon.ico")), QIcon.Normal, QIcon.Off) open_patient_window_instance.setObjectName("OpenPatientWindowInstance") open_patient_window_instance.setWindowIcon(window_icon) open_patient_window_instance.resize(840, 530) # Create a vertical box for containing the other elements and layouts self.open_patient_window_instance_vertical_box = QVBoxLayout() self.open_patient_window_instance_vertical_box.setObjectName( "OpenPatientWindowInstanceVerticalBox") # Create a label to prompt the user to enter the path to the directory that contains the DICOM files self.open_patient_directory_prompt = QLabel() self.open_patient_directory_prompt.setObjectName( "OpenPatientDirectoryPrompt") self.open_patient_directory_prompt.setAlignment(Qt.AlignLeft) self.open_patient_window_instance_vertical_box.addWidget( self.open_patient_directory_prompt) # Create a horizontal box to hold the input box for the directory and the choose button self.open_patient_directory_input_horizontal_box = QHBoxLayout() self.open_patient_directory_input_horizontal_box.setObjectName( "OpenPatientDirectoryInputHorizontalBox") # Create a textbox to contain the path to the directory that contains the DICOM files self.open_patient_directory_input_box = UIOpenPatientWindowDragAndDropEvent( self) self.open_patient_directory_input_box.setObjectName( "OpenPatientDirectoryInputBox") self.open_patient_directory_input_box.setSizePolicy( QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)) self.open_patient_directory_input_box.returnPressed.connect( self.scan_directory_for_patient) self.open_patient_directory_input_horizontal_box.addWidget( self.open_patient_directory_input_box) # Create a choose button to open the file dialog self.open_patient_directory_choose_button = QPushButton() self.open_patient_directory_choose_button.setObjectName( "OpenPatientDirectoryChooseButton") self.open_patient_directory_choose_button.setSizePolicy( QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)) self.open_patient_directory_choose_button.resize( self.open_patient_directory_choose_button.sizeHint().width(), self.open_patient_directory_input_box.height()) self.open_patient_directory_choose_button.setCursor( QtGui.QCursor(QtCore.Qt.PointingHandCursor)) self.open_patient_directory_input_horizontal_box.addWidget( self.open_patient_directory_choose_button) self.open_patient_directory_choose_button.clicked.connect( self.choose_button_clicked) # Create a widget to hold the input fields self.open_patient_directory_input_widget = QWidget() self.open_patient_directory_input_horizontal_box.setStretch(0, 4) self.open_patient_directory_input_widget.setLayout( self.open_patient_directory_input_horizontal_box) self.open_patient_window_instance_vertical_box.addWidget( self.open_patient_directory_input_widget) # Create a horizontal box to hold the stop button and direction to the user on where to select the patient self.open_patient_appear_prompt_and_stop_horizontal_box = QHBoxLayout() self.open_patient_appear_prompt_and_stop_horizontal_box.setObjectName( "OpenPatientAppearPromptAndStopHorizontalBox") # Create a label to show direction on where the files will appear self.open_patient_directory_appear_prompt = QLabel() self.open_patient_directory_appear_prompt.setObjectName( "OpenPatientDirectoryAppearPrompt") self.open_patient_directory_appear_prompt.setAlignment(Qt.AlignLeft) self.open_patient_appear_prompt_and_stop_horizontal_box.addWidget( self.open_patient_directory_appear_prompt) self.open_patient_appear_prompt_and_stop_horizontal_box.addStretch(1) # Create a button to stop searching self.open_patient_window_stop_button = QPushButton() self.open_patient_window_stop_button.setObjectName( "OpenPatientWindowStopButton") self.open_patient_window_stop_button.setSizePolicy( QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)) self.open_patient_window_stop_button.resize( self.open_patient_window_stop_button.sizeHint().width(), self.open_patient_window_stop_button.sizeHint().height()) self.open_patient_window_stop_button.setCursor( QtGui.QCursor(QtCore.Qt.PointingHandCursor)) self.open_patient_window_stop_button.clicked.connect( self.stop_button_clicked) self.open_patient_window_stop_button.setProperty( "QPushButtonClass", "fail-button") self.open_patient_window_stop_button.setVisible( False) # Button doesn't show until a search commences self.open_patient_appear_prompt_and_stop_horizontal_box.addWidget( self.open_patient_window_stop_button) # Create a widget to hold the layout self.open_patient_appear_prompt_and_stop_widget = QWidget() self.open_patient_appear_prompt_and_stop_widget.setLayout( self.open_patient_appear_prompt_and_stop_horizontal_box) self.open_patient_window_instance_vertical_box.addWidget( self.open_patient_appear_prompt_and_stop_widget) # Create a tree view list to list out all patients in the directory selected above self.open_patient_window_patients_tree = QTreeWidget() self.open_patient_window_patients_tree.setObjectName( "OpenPatientWindowPatientsTree") self.open_patient_window_patients_tree.setSizePolicy( QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)) self.open_patient_window_patients_tree.resize( self.open_patient_window_patients_tree.sizeHint().width(), self.open_patient_window_patients_tree.sizeHint().height()) self.open_patient_window_patients_tree.setHeaderHidden(False) self.open_patient_window_patients_tree.setHeaderLabels([""]) self.open_patient_window_patients_tree.itemChanged.connect( self.tree_item_changed) self.open_patient_window_instance_vertical_box.addWidget( self.open_patient_window_patients_tree) self.last_patient = None # Create a label to show what would happen if they select the patient self.open_patient_directory_result_label = QtWidgets.QLabel() self.open_patient_directory_result_label.setObjectName( "OpenPatientDirectoryResultLabel") self.open_patient_directory_result_label.setAlignment(Qt.AlignLeft) self.open_patient_window_instance_vertical_box.addWidget( self.open_patient_directory_result_label) # Create a horizontal box to hold the Cancel and Open button self.open_patient_window_patient_open_actions_horizontal_box = QHBoxLayout( ) self.open_patient_window_patient_open_actions_horizontal_box.setObjectName( "OpenPatientWindowPatientOpenActionsHorizontalBox") self.open_patient_window_patient_open_actions_horizontal_box.addStretch( 1) # Add a button to go back/exit from the application self.open_patient_window_exit_button = QPushButton() self.open_patient_window_exit_button.setObjectName( "OpenPatientWindowExitButton") self.open_patient_window_exit_button.setSizePolicy( QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)) self.open_patient_window_exit_button.resize( self.open_patient_window_stop_button.sizeHint().width(), self.open_patient_window_stop_button.sizeHint().height()) self.open_patient_window_exit_button.setCursor( QtGui.QCursor(QtCore.Qt.PointingHandCursor)) self.open_patient_window_exit_button.clicked.connect( self.exit_button_clicked) self.open_patient_window_exit_button.setProperty( "QPushButtonClass", "fail-button") self.open_patient_window_patient_open_actions_horizontal_box.addWidget( self.open_patient_window_exit_button) # Add a button to confirm opening of the patient self.open_patient_window_confirm_button = QPushButton() self.open_patient_window_confirm_button.setObjectName( "OpenPatientWindowConfirmButton") self.open_patient_window_confirm_button.setSizePolicy( QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)) self.open_patient_window_confirm_button.resize( self.open_patient_window_confirm_button.sizeHint().width(), self.open_patient_window_confirm_button.sizeHint().height()) self.open_patient_window_confirm_button.setCursor( QtGui.QCursor(QtCore.Qt.PointingHandCursor)) self.open_patient_window_confirm_button.setDisabled(True) self.open_patient_window_confirm_button.clicked.connect( self.confirm_button_clicked) self.open_patient_window_confirm_button.setProperty( "QPushButtonClass", "success-button") self.open_patient_window_patient_open_actions_horizontal_box.addWidget( self.open_patient_window_confirm_button) # Create a widget to house all of the actions button for open patient window self.open_patient_window_patient_open_actions_widget = QWidget() self.open_patient_window_patient_open_actions_widget.setLayout( self.open_patient_window_patient_open_actions_horizontal_box) self.open_patient_window_instance_vertical_box.addWidget( self.open_patient_window_patient_open_actions_widget) # Set the vertical box fourth element, the tree view, to stretch out as far as possible self.open_patient_window_instance_vertical_box.setStretch( 3, 4) # Stretch the treeview out as far as possible self.open_patient_window_instance_central_widget = QWidget() self.open_patient_window_instance_central_widget.setObjectName( "OpenPatientWindowInstanceCentralWidget") self.open_patient_window_instance_central_widget.setLayout( self.open_patient_window_instance_vertical_box) # Create threadpool for multithreading self.threadpool = QThreadPool() print("Multithreading with maximum %d threads" % self.threadpool.maxThreadCount()) # Create interrupt event for stopping the directory search self.interrupt_flag = threading.Event() # Bind all texts into the buttons and labels self.retranslate_ui(open_patient_window_instance) # Set the central widget, ready for display open_patient_window_instance.setCentralWidget( self.open_patient_window_instance_central_widget) # Set the current stylesheet to the instance and connect it back to the caller through slot _stylesheet = open(resource_path(self.stylesheet_path)).read() open_patient_window_instance.setStyleSheet(_stylesheet) QtCore.QMetaObject.connectSlotsByName(open_patient_window_instance) def retranslate_ui(self, open_patient_window_instance): _translate = QtCore.QCoreApplication.translate open_patient_window_instance.setWindowTitle( _translate("OpenPatientWindowInstance", "OnkoDICOM - Select Patient")) self.open_patient_directory_prompt.setText( _translate( "OpenPatientWindowInstance", "Choose the path of the folder containing DICOM files to load Patient's details:" )) self.open_patient_directory_input_box.setPlaceholderText( _translate( "OpenPatientWindowInstance", "Enter DICOM Files Path (For example, C:\path\\to\your\DICOM\Files)" )) self.open_patient_directory_choose_button.setText( _translate("OpenPatientWindowInstance", "Choose")) self.open_patient_directory_appear_prompt.setText( _translate( "OpenPatientWindowInstance", "Patient File directory shown below once file path chosen. Please select the file(s) you want to open:" )) self.open_patient_directory_result_label.setText( "The selected directory(s) above will be opened in the OnkoDICOM program." ) self.open_patient_window_stop_button.setText( _translate("OpenPatientWindowInstance", "Stop Search")) self.open_patient_window_exit_button.setText( _translate("OpenPatientWindowInstance", "Exit")) self.open_patient_window_confirm_button.setText( _translate("OpenPatientWindowInstance", "Confirm")) def exit_button_clicked(self): QCoreApplication.exit(0) def scan_directory_for_patient(self): # Reset tree view header and last patient self.open_patient_window_patients_tree.setHeaderLabels([""]) self.last_patient = None self.filepath = self.open_patient_directory_input_box.text() # Proceed if a folder was selected if self.filepath != "": # Update the QTreeWidget to reflect data being loaded # First, clear the widget of any existing data self.open_patient_window_patients_tree.clear() # Next, update the tree widget self.open_patient_window_patients_tree.addTopLevelItem( QTreeWidgetItem(["Loading selected directory..."])) # The choose button is disabled until the thread finishes executing self.open_patient_directory_choose_button.setEnabled(False) # Reveals the Stop Search button for the duration of the search self.open_patient_window_stop_button.setVisible(True) # The interrupt flag is then un-set if a previous search has been stopped. self.interrupt_flag.clear() # Then, create a new thread that will load the selected folder worker = Worker(DICOMDirectorySearch.get_dicom_structure, self.filepath, self.interrupt_flag, progress_callback=True) worker.signals.result.connect(self.on_search_complete) worker.signals.progress.connect(self.search_progress) # Execute the thread self.threadpool.start(worker) def choose_button_clicked(self): """ Executes when the choose button is clicked. Gets filepath from the user and loads all files and subdirectories. """ # Get folder path from pop up dialog box self.filepath = QtWidgets.QFileDialog.getExistingDirectory( None, 'Select patient folder...', '') self.open_patient_directory_input_box.setText(self.filepath) self.scan_directory_for_patient() def stop_button_clicked(self): self.interrupt_flag.set() def search_progress(self, progress_update): """ Current progress of the file search. """ self.open_patient_window_patients_tree.clear() self.open_patient_window_patients_tree.addTopLevelItem( QTreeWidgetItem([ "Loading selected directory... (%s files searched)" % progress_update ])) def on_search_complete(self, dicom_structure): """ Executes once the directory search is complete. :param dicom_structure: DICOMStructure object constructed by the directory search. """ self.open_patient_directory_choose_button.setEnabled(True) self.open_patient_window_stop_button.setVisible(False) self.open_patient_window_patients_tree.clear() if dicom_structure is None: # dicom_structure will be None if function was interrupted. return for patient_item in dicom_structure.get_tree_items_list(): self.open_patient_window_patients_tree.addTopLevelItem( patient_item) if len(dicom_structure.patients) == 0: QMessageBox.about(self, "No files found", "Selected directory contains no DICOM files.") def tree_item_changed(self, item, _): """ Executes when a tree item is checked or unchecked. If a different patient is checked, uncheck the previous patient. Inform user about missing DICOM files. """ selected_patient = item # If the item is not top-level, bubble up to see which top-level item this item belongs to if self.open_patient_window_patients_tree.invisibleRootItem( ).indexOfChild(item) == -1: while self.open_patient_window_patients_tree.invisibleRootItem( ).indexOfChild(selected_patient) == -1: selected_patient = selected_patient.parent() # Uncheck previous patient if a different patient is selected if item.checkState( 0 ) == Qt.CheckState.Checked and self.last_patient != selected_patient: if self.last_patient is not None: self.last_patient.setCheckState(0, Qt.CheckState.Unchecked) self.last_patient.setSelected(False) self.last_patient = selected_patient # Get the types of all selected leaves self.selected_series_types = set() for checked_item in self.get_checked_leaves(): series_type = checked_item.dicom_object.get_series_type() if type(series_type) == str: self.selected_series_types.add(series_type) else: self.selected_series_types.update(series_type) # Check the existence of IMAGE, RTSTRUCT and RTDOSE files if len(list({'CT', 'MR', 'PT'} & self.selected_series_types)) == 0: header = "Cannot proceed without an image file." self.open_patient_window_confirm_button.setDisabled(True) elif 'RTSTRUCT' not in self.selected_series_types: header = "DVH and Radiomics calculations are not available without a RTSTRUCT file." elif 'RTDOSE' not in self.selected_series_types: header = "DVH calculations are not available without a RTDOSE file." else: header = "" self.open_patient_window_patients_tree.setHeaderLabel(header) if len(list({'CT', 'MR', 'PT'} & self.selected_series_types)) != 0: self.open_patient_window_confirm_button.setDisabled(False) def confirm_button_clicked(self): """ Begins loading of the selected files. """ selected_files = [] for item in self.get_checked_leaves(): selected_files += item.dicom_object.get_files() self.progress_window = ProgressWindow( self, QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint) self.progress_window.signal_loaded.connect(self.on_loaded) self.progress_window.signal_error.connect(self.on_loading_error) self.progress_window.start_loading(selected_files) self.progress_window.exec_() def on_loaded(self, results): """ Executes when the progress bar finishes loaded the selected files. """ if results[0] is True: # Will be NoneType if loading was interrupted. self.patient_info_initialized.emit( results[1]) # Emits the progress window. def on_loading_error(self, error_code): """ Error handling for progress window. """ if error_code == 0: QMessageBox.about( self.progress_window, "Unable to open selection", "Selected files cannot be opened as they are not a DICOM-RT set." ) self.progress_window.close() elif error_code == 1: QMessageBox.about( self.progress_window, "Unable to open selection", "Selected files cannot be opened as they contain unsupported DICOM classes." ) self.progress_window.close() def get_checked_leaves(self): """ :return: A list of all QTreeWidgetItems in the QTreeWidget that are both leaves and checked. """ checked_items = [] def recurse(parent_item: QTreeWidgetItem): for i in range(parent_item.childCount()): child = parent_item.child(i) grand_children = child.childCount() if grand_children > 0: recurse(child) else: if child.checkState(0) == Qt.Checked: checked_items.append(child) recurse(self.open_patient_window_patients_tree.invisibleRootItem()) return checked_items
class SelectRTSSPopUp(QDialog): signal_rtss_selected = QtCore.Signal(Series) def __init__(self, existing_rtss, parent=None): QDialog.__init__(self, parent=parent) if platform.system() == 'Darwin': self.stylesheet_path = "res/stylesheet.qss" else: self.stylesheet_path = "res/stylesheet-win-linux.qss" stylesheet = open(resource_path(self.stylesheet_path)).read() self.setStyleSheet(stylesheet) self.setWindowTitle("Multiple RTSTRUCTs detected!") self.setMinimumSize(350, 180) self.icon = QtGui.QIcon() self.icon.addPixmap( QtGui.QPixmap(resource_path("res/images/icon.ico")), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.setWindowIcon(self.icon) self.explanation_text = QLabel("Multiple RTSTRUCTs attached to the " "selected image set have been " "identified." "\nPlease select 1 " "RTSTRUCTs to continue!") # Create scrolling area widget to contain the content. self.scroll_area = QScrollArea() self.scroll_area.setWidgetResizable(True) self.scroll_area_content = QWidget(self.scroll_area) self.scroll_area.ensureWidgetVisible(self.scroll_area_content) # Create layout for checkboxes self.layout_content = QVBoxLayout(self.scroll_area_content) self.layout_content.setContentsMargins(5, 5, 5, 5) self.layout_content.setSpacing(0) self.layout_content.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignTop) # Add all the attached RTSSs as checkboxes self.checkbox_group = QButtonGroup() self.checkbox_group.setExclusive(True) for i in range(len(existing_rtss)): checkbox = QCheckBox() checkbox.rtss = existing_rtss[i] rtss = dcmread(checkbox.rtss.get_files()[0]) checkbox.setFocusPolicy(QtCore.Qt.NoFocus) checkbox.setText("Series: %s (%s, %s %s)" % ( checkbox.rtss.series_description, checkbox.rtss.get_series_type(), len(rtss.StructureSetROISequence), "ROIs" if len(rtss.StructureSetROISequence) > 1 else "ROI" )) self.checkbox_group.addButton(checkbox) self.layout_content.addWidget(checkbox) self.checkbox_group.buttonClicked.connect(self.on_checkbox_clicked) # Create a cancel button self.cancel_button = QPushButton("Cancel") self.cancel_button.clicked.connect(self.on_cancel_clicked) # Create a continue button self.continue_button = QPushButton("Continue Process") self.continue_button.setDisabled(True) self.continue_button.clicked.connect(self.on_continue_clicked) # Create a widget to contain cancel and continue buttons self.button_area = QWidget() self.button_layout = QHBoxLayout() self.button_layout.addWidget(self.cancel_button) self.button_layout.addWidget(self.continue_button) self.button_area.setLayout(self.button_layout) # Add all components to a vertical layout self.layout = QVBoxLayout() self.layout.addWidget(self.explanation_text) self.layout.addWidget(self.scroll_area) self.layout.addWidget(self.button_area) self.setLayout(self.layout) def on_checkbox_clicked(self, checkbox): """ function to set a rtss as the selected rtss :param checkbox: the selected QCheckbox that contains a rtss """ self.continue_button.setDisabled(False) self.selected_rtss = checkbox.rtss def on_continue_clicked(self): """ function to continue the process """ # Emit the selected RTSS self.signal_rtss_selected.emit(self.selected_rtss) self.close() def on_cancel_clicked(self): """ function to cancel the operation """ self.close()
class UIImageFusionWindow(object): image_fusion_info_initialized = QtCore.Signal(object) def setup_ui(self, open_image_fusion_select_instance): """Sets up a UI""" if platform.system() == 'Darwin': self.stylesheet_path = "res/stylesheet.qss" else: self.stylesheet_path = "res/stylesheet-win-linux.qss" window_icon = QIcon() window_icon.addPixmap(QPixmap(resource_path("res/images/icon.ico")), QIcon.Normal, QIcon.Off) open_image_fusion_select_instance.setObjectName( "OpenPatientWindowInstance") open_image_fusion_select_instance.setWindowIcon(window_icon) open_image_fusion_select_instance.resize(840, 530) # Create a vertical box for containing the other elements and layouts self.open_patient_window_instance_vertical_box = QVBoxLayout() self.open_patient_window_instance_vertical_box.setObjectName( "OpenPatientWindowInstanceVerticalBox") # Create a label to prompt the user to enter the path to the # directory that contains the DICOM files self.open_patient_directory_prompt = QLabel() self.open_patient_directory_prompt.setObjectName( "OpenPatientDirectoryPrompt") self.open_patient_directory_prompt.setAlignment(Qt.AlignLeft) self.open_patient_window_instance_vertical_box.addWidget( self.open_patient_directory_prompt) # Create a horizontal box to hold the input box for the directory # and the choose button self.open_patient_directory_input_horizontal_box = QHBoxLayout() self.open_patient_directory_input_horizontal_box.setObjectName( "OpenPatientDirectoryInputHorizontalBox") # Create a textbox to contain the path to the directory that contains # the DICOM files self.open_patient_directory_input_box = \ UIImageFusionWindowDragAndDropEvent(self) self.open_patient_directory_input_box.setObjectName( "OpenPatientDirectoryInputBox") self.open_patient_directory_input_box.setSizePolicy( QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)) self.open_patient_directory_input_box.returnPressed.connect( self.scan_directory_for_patient) self.open_patient_directory_input_horizontal_box.addWidget( self.open_patient_directory_input_box) # Create a choose button to open the file dialog self.open_patient_directory_choose_button = QPushButton() self.open_patient_directory_choose_button.setObjectName( "OpenPatientDirectoryChooseButton") self.open_patient_directory_choose_button.setSizePolicy( QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)) self.open_patient_directory_choose_button.resize( self.open_patient_directory_choose_button.sizeHint().width(), self.open_patient_directory_input_box.height()) self.open_patient_directory_choose_button.setCursor( QtGui.QCursor(QtCore.Qt.PointingHandCursor)) self.open_patient_directory_input_horizontal_box.addWidget( self.open_patient_directory_choose_button) self.open_patient_directory_choose_button.clicked.connect( self.choose_button_clicked) # Create a widget to hold the input fields self.open_patient_directory_input_widget = QWidget() self.open_patient_directory_input_horizontal_box.setStretch(0, 4) self.open_patient_directory_input_widget.setLayout( self.open_patient_directory_input_horizontal_box) self.open_patient_window_instance_vertical_box.addWidget( self.open_patient_directory_input_widget) # Create a horizontal box to hold the stop button and direction to # the user on where to select the patient self.open_patient_appear_prompt_and_stop_horizontal_box = QHBoxLayout() self.open_patient_appear_prompt_and_stop_horizontal_box.setObjectName( "OpenPatientAppearPromptAndStopHorizontalBox") # Create a label to show direction on where the files will appear self.open_patient_directory_appear_prompt = QLabel() self.open_patient_directory_appear_prompt.setObjectName( "OpenPatientDirectoryAppearPrompt") self.open_patient_directory_appear_prompt.setAlignment(Qt.AlignLeft) self.open_patient_appear_prompt_and_stop_horizontal_box.addWidget( self.open_patient_directory_appear_prompt) self.open_patient_appear_prompt_and_stop_horizontal_box.addStretch(1) # Create a button to stop searching self.open_patient_window_stop_button = QPushButton() self.open_patient_window_stop_button.setObjectName( "OpenPatientWindowStopButton") self.open_patient_window_stop_button.setSizePolicy( QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)) self.open_patient_window_stop_button.resize( self.open_patient_window_stop_button.sizeHint().width(), self.open_patient_window_stop_button.sizeHint().height()) self.open_patient_window_stop_button.setCursor( QtGui.QCursor(QtCore.Qt.PointingHandCursor)) self.open_patient_window_stop_button.clicked.connect( self.stop_button_clicked) self.open_patient_window_stop_button.setProperty( "QPushButtonClass", "fail-button") self.open_patient_window_stop_button.setVisible(False) self.open_patient_appear_prompt_and_stop_horizontal_box.addWidget( self.open_patient_window_stop_button) # Create a widget to hold the layout self.open_patient_appear_prompt_and_stop_widget = QWidget() self.open_patient_appear_prompt_and_stop_widget.setLayout( self.open_patient_appear_prompt_and_stop_horizontal_box) self.open_patient_window_instance_vertical_box.addWidget( self.open_patient_appear_prompt_and_stop_widget) # Create a tree view list to list out all patients in the directory # selected above self.open_patient_window_patients_tree = QTreeWidget() self.open_patient_window_patients_tree.setObjectName( "OpenPatientWindowPatientsTree") self.open_patient_window_patients_tree.setSizePolicy( QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)) self.open_patient_window_patients_tree.resize( self.open_patient_window_patients_tree.sizeHint().width(), self.open_patient_window_patients_tree.sizeHint().height()) self.open_patient_window_patients_tree.setHeaderHidden(False) self.open_patient_window_patients_tree.setHeaderLabels([""]) self.open_patient_window_patients_tree.itemChanged.connect( self.tree_item_clicked) self.open_patient_window_instance_vertical_box.addWidget( self.open_patient_window_patients_tree) self.last_patient = None # Create a label to show what would happen if they select the patient self.open_patient_directory_result_label = QtWidgets.QLabel() self.open_patient_directory_result_label.setObjectName( "OpenPatientDirectoryResultLabel") self.open_patient_directory_result_label.setAlignment(Qt.AlignLeft) self.open_patient_window_instance_vertical_box.addWidget( self.open_patient_directory_result_label) # Create a horizontal box to hold the Cancel and Open button self.open_patient_window_patient_open_actions_horizontal_box = \ QHBoxLayout() self.open_patient_window_patient_open_actions_horizontal_box. \ setObjectName("OpenPatientWindowPatientOpenActionsHorizontalBox") self.open_patient_window_patient_open_actions_horizontal_box. \ addStretch(1) # Add a button to go back/close from the application self.open_patient_window_close_button = QPushButton() self.open_patient_window_close_button.setObjectName( "OpenPatientWindowcloseButton") self.open_patient_window_close_button.setSizePolicy( QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)) self.open_patient_window_close_button.resize( self.open_patient_window_stop_button.sizeHint().width(), self.open_patient_window_stop_button.sizeHint().height()) self.open_patient_window_close_button.setCursor( QtGui.QCursor(QtCore.Qt.PointingHandCursor)) self.open_patient_window_close_button.clicked.connect( self.close_button_clicked) self.open_patient_window_close_button.setProperty( "QPushButtonClass", "fail-button") self.open_patient_window_patient_open_actions_horizontal_box. \ addWidget(self.open_patient_window_close_button) # Add a button to confirm opening of the patient self.open_patient_window_confirm_button = QPushButton() self.open_patient_window_confirm_button.setObjectName( "OpenPatientWindowConfirmButton") self.open_patient_window_confirm_button.setSizePolicy( QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)) self.open_patient_window_confirm_button.resize( self.open_patient_window_confirm_button.sizeHint().width(), self.open_patient_window_confirm_button.sizeHint().height()) self.open_patient_window_confirm_button.setCursor( QtGui.QCursor(QtCore.Qt.PointingHandCursor)) self.open_patient_window_confirm_button.setDisabled(True) self.open_patient_window_confirm_button.clicked.connect( self.confirm_button_clicked) self.open_patient_window_confirm_button.setProperty( "QPushButtonClass", "success-button") self.open_patient_window_patient_open_actions_horizontal_box. \ addWidget( self.open_patient_window_confirm_button) # Create a widget to house all of the actions button for open patient # window self.open_patient_window_patient_open_actions_widget = QWidget() self.open_patient_window_patient_open_actions_widget.setLayout( self.open_patient_window_patient_open_actions_horizontal_box) self.open_patient_window_instance_vertical_box.addWidget( self.open_patient_window_patient_open_actions_widget) # Set the vertical box fourth element, the tree view, to stretch # out as far as possible self.open_patient_window_instance_vertical_box.setStretch(3, 4) self.open_patient_window_instance_central_widget = QWidget() self.open_patient_window_instance_central_widget.setObjectName( "OpenPatientWindowInstanceCentralWidget") self.open_patient_window_instance_central_widget.setLayout( self.open_patient_window_instance_vertical_box) # Create threadpool for multithreading self.threadpool = QThreadPool() # print("Multithreading with maximum %d threads" % self.threadpool. # maxThreadCount()) # Create interrupt event for stopping the directory search self.interrupt_flag = threading.Event() # Bind all texts into the buttons and labels self.retranslate_ui(open_image_fusion_select_instance) # Set the central widget, ready for display open_image_fusion_select_instance.setCentralWidget( self.open_patient_window_instance_central_widget) # Set the current stylesheet to the instance and connect it back # to the caller through slot _stylesheet = open(resource_path(self.stylesheet_path)).read() open_image_fusion_select_instance.setStyleSheet(_stylesheet) QtCore.QMetaObject.connectSlotsByName( open_image_fusion_select_instance) def retranslate_ui(self, open_image_fusion_select_instance): """Translates UI""" _translate = QtCore.QCoreApplication.translate open_image_fusion_select_instance.setWindowTitle( _translate("OpenPatientWindowInstance", "OnkoDICOM - Select Patient")) self.open_patient_directory_prompt.setText(_translate( "OpenPatientWindowInstance", "Choose an image to merge with:")) self.open_patient_directory_input_box.setPlaceholderText( _translate("OpenPatientWindowInstance", "Enter DICOM Files Path (For example, " "C:\path\\to\your\DICOM\Files)")) self.open_patient_directory_choose_button.setText(_translate( "OpenPatientWindowInstance", "Choose")) self.open_patient_directory_appear_prompt.setText(_translate( "OpenPatientWindowInstance", "Please select below the image set you wish to overlay:")) self.open_patient_directory_result_label. \ setText("The selected imageset(s) above will be " "co-registered with the current imageset.") self.open_patient_window_stop_button.setText(_translate( "OpenPatientWindowInstance", "Stop Search")) self.open_patient_window_close_button.setText(_translate( "OpenPatientWindowInstance", "Close")) self.open_patient_window_confirm_button.setText(_translate( "OpenPatientWindowInstance", "Confirm")) def update_patient(self): self.clear_checked_leaves() self.patient_dict_container = PatientDictContainer() self.patient = self.patient_dict_container.get("basic_info") self.patient_id = self.patient['id'] dataset = self.patient_dict_container.dataset[0] self.patient_current_image_series_uid = \ dataset.get("SeriesInstanceUID") def clear_checked_leaves(self): """ Resets all leaves to their unchecked state """ def recurse(parent_item: QTreeWidgetItem): for i in range(parent_item.childCount()): child = parent_item.child(i) grand_children = child.childCount() if grand_children > 0: recurse(child) else: if child.checkState(0) == Qt.Checked: child.setCheckState(0, Qt.CheckState.Unchecked) child.setSelected(False) recurse(self.open_patient_window_patients_tree.invisibleRootItem()) self.open_patient_window_patients_tree.collapseAll() def close_button_clicked(self): """Closes the window.""" self.close() def scan_directory_for_patient(self): # Reset tree view header and last patient self.open_patient_window_confirm_button.setDisabled(True) self.open_patient_window_patients_tree.setHeaderLabels([""]) self.last_patient = None self.filepath = self.open_patient_directory_input_box.text() # Proceed if a folder was selected if self.filepath != "": # Update the QTreeWidget to reflect data being loaded # First, clear the widget of any existing data self.open_patient_window_patients_tree.clear() # Next, update the tree widget self.open_patient_window_patients_tree.addTopLevelItem( QTreeWidgetItem(["Loading selected directory..."])) # The choose button is disabled until the thread finishes executing self.open_patient_directory_choose_button.setEnabled(False) # Reveals the Stop Search button for the duration of the search self.open_patient_window_stop_button.setVisible(True) # The interrupt flag is then un-set if a previous search has been # stopped. self.interrupt_flag.clear() # Then, create a new thread that will load the selected folder worker = Worker(DICOMDirectorySearch.get_dicom_structure, self.filepath, self.interrupt_flag, progress_callback=True) worker.signals.result.connect(self.on_search_complete) worker.signals.progress.connect(self.search_progress) # Execute the thread self.threadpool.start(worker) def choose_button_clicked(self): """ Executes when the choose button is clicked. Gets filepath from the user and loads all files and subdirectories. """ # Get folder path from pop up dialog box self.filepath = QtWidgets.QFileDialog.getExistingDirectory( None, 'Select patient folder...', '') self.open_patient_directory_input_box.setText(self.filepath) self.scan_directory_for_patient() def stop_button_clicked(self): self.interrupt_flag.set() def search_progress(self, progress_update): """ Current progress of the file search. """ self.open_patient_window_patients_tree.clear() self.open_patient_window_patients_tree.addTopLevelItem( QTreeWidgetItem(["Loading selected directory... " "(%s files searched)" % progress_update])) def on_search_complete(self, dicom_structure): """ Executes once the directory search is complete. :param dicom_structure: DICOMStructure object constructed by the directory search. """ self.open_patient_directory_choose_button.setEnabled(True) self.open_patient_window_stop_button.setVisible(False) self.open_patient_window_patients_tree.clear() # dicom_structure will be None if function was interrupted. if dicom_structure is None: return for patient_item in dicom_structure.get_tree_items_list(): self.open_patient_window_patients_tree.addTopLevelItem( patient_item) patient_item.setExpanded(True) # Display all studies # Display all image sets for i in range(patient_item.childCount()): study = patient_item.child(i) study.setExpanded(True) if len(dicom_structure.patients) == 0: QMessageBox.about(self, "No files found", "Selected directory contains no DICOM files.") def tree_item_clicked(self, item, _): """ Executes when a tree item is checked or unchecked. If a different patient is checked, uncheck the previous patient. Inform user about missing DICOM files. """ # If patient is only selected, but not checked, set it to "focus" to # coincide with stylesheet. And if the selected item is an image set, # display its child branches. if item.checkState(0) == Qt.CheckState.Unchecked: self.open_patient_window_patients_tree.setCurrentItem(item) else: # Otherwise don't "focus", then set patient as selected self.open_patient_window_patients_tree.setCurrentItem(None) item.setSelected(True) # Expand or collapse the tree branch if item is an image series # Only collapse if the selected image series is expanded but unchecked # Otherwise, expand its tree branch to show RT files is_expanded = False \ if (item.isExpanded() is True and item.checkState(0) == Qt.CheckState.Unchecked) else True self.display_a_tree_branch(item, is_expanded) selected_patient = item # If the item is not top-level, bubble up to see which top-level item # this item belongs to if self.open_patient_window_patients_tree.invisibleRootItem(). \ indexOfChild(item) == -1: while self.open_patient_window_patients_tree.invisibleRootItem(). \ indexOfChild(selected_patient) == -1: selected_patient = selected_patient.parent() # Uncheck previous patient if a different patient is selected if item.checkState(0) == Qt.CheckState.Checked and self.last_patient \ != selected_patient: if self.last_patient is not None: last_patient_checked_items = self.get_checked_nodes( self.last_patient) for checked_item in last_patient_checked_items: checked_item.setCheckState(0, Qt.Unchecked) self.last_patient = selected_patient # Check selected items and display warning messages self.check_selected_items(selected_patient) def display_a_tree_branch(self, node, is_expanded): # TO DO: # Could Team 23 please update the defintion of this docstring as # well as same function presented in OpenPatientWindow. """ Displays a tree branch Parameters: node : root node the tree is_expanded (boolean): flag for checking if a particular node/leaf is expanded. """ node.setExpanded(is_expanded) if node.childCount() > 0: for i in range(node.childCount()): self.display_a_tree_branch(node.child(i), is_expanded) else: return def check_selected_items(self, selected_patient): """ Check and display warning messages based on the existence and quantity of image series, RTSTRUCT, RTPLAN, RTDOSE and SR files Parameters: selected_patient (DICOMStructure): DICOM Object of patient """ # Get the types of all selected leaves & Get the names of all selected # studies checked_nodes = self.get_checked_nodes( self.open_patient_window_patients_tree.invisibleRootItem()) selected_series_types = [checked_node.dicom_object.get_series_type() for checked_node in checked_nodes] selected_series_id = [checked_node.dicom_object.series_uid for checked_node in checked_nodes] # Total number of selected image series total_selected_image_series = selected_series_types.count('CT') + \ selected_series_types.count('MR') + \ selected_series_types.count('PT') # Check the existence of IMAGE, RTSTRUCT, RTPLAN and RTDOSE files proceed = True if total_selected_image_series < 1: header = "Cannot proceed without an image." proceed = False elif total_selected_image_series > 1: header = "Cannot proceed with more than 1 selected image." proceed = False elif selected_patient.dicom_object.patient_id.strip() != \ self.patient_id: header = "Cannot proceed with different patient." proceed = False elif self.patient_current_image_series_uid in selected_series_id: header = "Cannot fuse with the same series." proceed = False elif not self.check_selected_items_referencing(checked_nodes): # Check that selected items properly reference each other header = "Selected series do not reference each other." proceed = False elif 'RTSTRUCT' not in selected_series_types and \ self.check_existing_rtss(checked_nodes): header = "The associated RTSTRUCT must be selected." proceed = False elif 'RTDOSE' in selected_series_types: header = "Cannot fuse with a RTDOSE file." proceed = False else: header = "" self.open_patient_window_confirm_button.setDisabled(not proceed) # Set the tree header self.open_patient_window_patients_tree.setHeaderLabel(header) def check_selected_items_referencing(self, items): """ Check if selected tree items properly reference each other. :param items: List of selected DICOMWidgetItems. :return: True if the selected items belong to the same tree branch. """ # Dictionary of series of different file types series = { "IMAGE": None, "RTSTRUCT": None, "RTPLAN": None, "RTDOSE": None, "SR": None } for item in items: series_type = item.dicom_object.get_series_type() if series_type in series: series[series_type] = item else: series["IMAGE"] = item # Check if the RTSTRUCT, RTPLAN, and RTDOSE are a child item of the # image series if series["IMAGE"]: if series["RTSTRUCT"] and series["RTSTRUCT"].parent() != \ series["IMAGE"]: return False if series["RTPLAN"] and \ series["RTPLAN"].parent().parent() != series["IMAGE"]: return False if series["SR"] and series["SR"].parent() != series["IMAGE"]: return False return True def check_existing_rtss(self, items): """ Check for existing rtss :return: bool, whether there is a rtss associated with the selected image series """ image_series = ['CT', 'MR', 'PT'] for item in items: if item.dicom_object.get_series_type() in image_series: for i in range(item.childCount()): if item.child(i).dicom_object: return True return False def get_checked_nodes(self, root): """ :param root: QTreeWidgetItem as a root. :return: A list of all QTreeWidgetItems in the QTreeWidget that are checked under the root. """ checked_items = [] def recurse(parent_item: QTreeWidgetItem): for i in range(parent_item.childCount()): child = parent_item.child(i) if int(child.flags()) & int(Qt.ItemIsUserCheckable) and \ child.checkState(0) == Qt.Checked: checked_items.append(child) grand_children = child.childCount() if grand_children > 0: recurse(child) recurse(root) return checked_items def confirm_button_clicked(self): """ Begins loading of the selected files. """ selected_files = [] for item in self.get_checked_nodes( self.open_patient_window_patients_tree.invisibleRootItem()): selected_files += item.dicom_object.get_files() self.progress_window = ImageFusionProgressWindow(self) self.progress_window.signal_loaded.connect(self.on_loaded) self.progress_window.signal_error.connect(self.on_loading_error) self.progress_window.start_loading(selected_files) def on_loaded(self, results): """ Executes when the progress bar finishes loaded the selected files. """ if results[0] is True: # Will be NoneType if loading was interrupted. self.image_fusion_info_initialized.emit(results[1]) def on_loading_error(self, exception): """ Error handling for progress window. """ if type(exception[1]) == ImageLoading.NotRTSetError: QMessageBox.about(self.progress_window, "Unable to open selection", "Selected files cannot be opened as they are not" " a DICOM-RT set.") self.progress_window.close() elif type(exception[1]) == ImageLoading.NotAllowedClassError: QMessageBox.about(self.progress_window, "Unable to open selection", "Selected files cannot be opened as they contain" " unsupported DICOM classes.") self.progress_window.close()
class SettingsWindow(QDialog): def __init__(self, parent: Optional[QWidget] = None, firstStart: bool = False) -> None: super().__init__(parent, ) if parent: self.setWindowTitle('Settings') else: self.setWindowTitle(getTitleString('Settings')) self.setAttribute(Qt.WA_DeleteOnClose) settings = QSettings() mainLayout = QVBoxLayout(self) mainLayout.setContentsMargins(5, 5, 5, 5) # First Start info if firstStart: firstStartInfo = QLabel( ''' <p><strong>Hello! It looks like this is your first time using w3modmanager, or the game installation path recently changed.</strong></p> <p> Please review the settings below. </p> ''', self) firstStartInfo.setWordWrap(True) firstStartInfo.setContentsMargins(10, 10, 10, 10) firstStartInfo.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) mainLayout.addWidget(firstStartInfo) # Game gbGame = QGroupBox('Game Path', self) mainLayout.addWidget(gbGame) gbGameLayout = QVBoxLayout(gbGame) gamePathLayout = QHBoxLayout() self.gamePath = QLineEdit(self) self.gamePath.setPlaceholderText('Path to witcher3.exe...') if settings.value('gamePath'): self.gamePath.setText(str(settings.value('gamePath'))) self.gamePath.textChanged.connect( lambda: self.validateGamePath(self.gamePath.text())) gamePathLayout.addWidget(self.gamePath) self.locateGame = QPushButton('Detect', self) self.locateGame.clicked.connect(self.locateGameEvent) self.locateGame.setToolTip( 'Automatically detect the game path if possible') gamePathLayout.addWidget(self.locateGame) selectGame = QPushButton('Browse', self) selectGame.clicked.connect(self.selectGameEvent) gamePathLayout.addWidget(selectGame) gbGameLayout.addLayout(gamePathLayout) gamePathInfoLayout = QHBoxLayout() self.gamePathInfo = QLabel('', self) self.gamePathInfo.setContentsMargins(4, 4, 4, 4) self.gamePathInfo.setMinimumHeight(40) self.gamePathInfo.setWordWrap(True) gamePathInfoLayout.addWidget(self.gamePathInfo) gbGameLayout.addLayout(gamePathInfoLayout) # Config gbConfig = QGroupBox('Game Config', self) mainLayout.addWidget(gbConfig) gbConfigLayout = QVBoxLayout(gbConfig) configPathLayout = QHBoxLayout() self.configPath = QLineEdit(self) self.configPath.setPlaceholderText('Path to config folder...') if settings.value('configPath'): self.configPath.setText(str(settings.value('configPath'))) self.configPath.textChanged.connect( lambda: self.validateConfigPath(self.configPath.text())) configPathLayout.addWidget(self.configPath) self.locateConfig = QPushButton('Detect', self) self.locateConfig.clicked.connect(self.locateConfigEvent) self.locateConfig.setToolTip( 'Automatically detect the config folder if possible') configPathLayout.addWidget(self.locateConfig) selectConfig = QPushButton('Browse', self) selectConfig.clicked.connect(self.selectConfigEvent) configPathLayout.addWidget(selectConfig) gbConfigLayout.addLayout(configPathLayout) configPathInfoLayout = QHBoxLayout() self.configPathInfo = QLabel('', self) self.configPathInfo.setContentsMargins(4, 4, 4, 4) self.configPathInfo.setMinimumHeight(40) self.configPathInfo.setWordWrap(True) configPathInfoLayout.addWidget(self.configPathInfo) gbConfigLayout.addLayout(configPathInfoLayout) # Script Merger gbScriptMerger = QGroupBox('Script Merger', self) mainLayout.addWidget(gbScriptMerger) gbScriptMergerLayout = QVBoxLayout(gbScriptMerger) scriptMergerPathLayout = QHBoxLayout() self.scriptMergerPath = QLineEdit(self) self.scriptMergerPath.setPlaceholderText( 'Path to WitcherScriptMerger.exe...') if settings.value('scriptMergerPath'): self.scriptMergerPath.setText( str(settings.value('scriptMergerPath'))) self.scriptMergerPath.textChanged.connect( lambda: self.validateScriptMergerPath(self.scriptMergerPath.text() )) scriptMergerPathLayout.addWidget(self.scriptMergerPath) self.locateScriptMerger = QPushButton('Detect', self) self.locateScriptMerger.clicked.connect(self.locateScriptMergerEvent) self.locateScriptMerger.setToolTip( 'Automatically detect the script merger path if possible') scriptMergerPathLayout.addWidget(self.locateScriptMerger) selectScriptMerger = QPushButton('Browse', self) selectScriptMerger.clicked.connect(self.selectScriptMergerEvent) scriptMergerPathLayout.addWidget(selectScriptMerger) gbScriptMergerLayout.addLayout(scriptMergerPathLayout) scriptMergerPathInfoLayout = QHBoxLayout() self.scriptMergerPathInfo = QLabel('', self) self.scriptMergerPathInfo.setOpenExternalLinks(True) self.scriptMergerPathInfo.setContentsMargins(4, 4, 4, 4) self.scriptMergerPathInfo.setMinimumHeight(40) self.scriptMergerPathInfo.setWordWrap(True) scriptMergerPathInfoLayout.addWidget(self.scriptMergerPathInfo) gbScriptMergerLayout.addLayout(scriptMergerPathInfoLayout) # Nexus Mods API gbNexusModsAPI = QGroupBox('Nexus Mods API', self) mainLayout.addWidget(gbNexusModsAPI) gbNexusModsAPILayout = QVBoxLayout(gbNexusModsAPI) self.nexusAPIKey = QLineEdit(self) self.nexusAPIKey.setPlaceholderText('Personal API Key...') if settings.value('nexusAPIKey'): self.nexusAPIKey.setText(str(settings.value('nexusAPIKey'))) self.nexusAPIKey.textChanged.connect( lambda: self.validateApiKey(self.nexusAPIKey.text())) gbNexusModsAPILayout.addWidget(self.nexusAPIKey) self.nexusAPIKeyInfo = QLabel('🌐', self) self.nexusAPIKeyInfo.setOpenExternalLinks(True) self.nexusAPIKeyInfo.setWordWrap(True) self.nexusAPIKeyInfo.setContentsMargins(4, 4, 4, 4) self.nexusAPIKeyInfo.setMinimumHeight(48) gbNexusModsAPILayout.addWidget(self.nexusAPIKeyInfo) self.nexusGetInfo = QCheckBox('Get Mod details after adding a new mod', self) self.nexusGetInfo.setChecked( settings.value('nexusGetInfo', 'True') == 'True') self.nexusGetInfo.setDisabled(True) gbNexusModsAPILayout.addWidget(self.nexusGetInfo) self.nexusCheckUpdates = QCheckBox('Check for Mod updates on startup', self) self.nexusCheckUpdates.setChecked( settings.value('nexusCheckUpdates', 'False') == 'True') self.nexusCheckUpdates.setDisabled(True) gbNexusModsAPILayout.addWidget(self.nexusCheckUpdates) self.nexusCheckClipboard = QCheckBox( 'Monitor the Clipboard for Nexus Mods URLs', self) self.nexusCheckClipboard.setChecked( settings.value('nexusCheckClipboard', 'False') == 'True') self.nexusCheckClipboard.setDisabled(True) gbNexusModsAPILayout.addWidget(self.nexusCheckClipboard) # Output gbOutput = QGroupBox('Output Preferences', self) mainLayout.addWidget(gbOutput) gbOutputLayout = QVBoxLayout(gbOutput) self.unhideOutput = QCheckBox('Auto-show output panel', self) self.unhideOutput.setChecked( settings.value('unhideOutput', 'True') == 'True') gbOutputLayout.addWidget(self.unhideOutput) self.debugOutput = QCheckBox('Show debug output', self) self.debugOutput.setChecked( settings.value('debugOutput', 'False') == 'True') gbOutputLayout.addWidget(self.debugOutput) # Actions actionsLayout = QHBoxLayout() actionsLayout.setAlignment(Qt.AlignRight) self.save = QPushButton('Save', self) self.save.clicked.connect(self.saveEvent) self.save.setAutoDefault(True) self.save.setDefault(True) actionsLayout.addWidget(self.save) cancel = QPushButton('Cancel', self) cancel.clicked.connect(self.cancelEvent) actionsLayout.addWidget(cancel) mainLayout.addLayout(actionsLayout) # Setup if not settings.value('gamePath'): self.locateGameEvent() self.setMinimumSize(QSize(440, 440)) self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) self.validGamePath = False self.validConfigPath = False self.validNexusAPIKey = False self.validScriptMergerPath = False self.validateGamePath(self.gamePath.text()) self.validateConfigPath(self.configPath.text()) self.validateApiKey(self.nexusAPIKey.text()) self.validateScriptMergerPath(self.scriptMergerPath.text()) self.updateSaveButton() self.finished.connect( lambda: self.validateApiKey.cancel()) # type: ignore def saveEvent(self) -> None: settings = QSettings() settings.setValue('settingsWindowGeometry', self.saveGeometry()) settings.setValue('gamePath', self.gamePath.text()) settings.setValue('configPath', self.configPath.text()) settings.setValue('scriptMergerPath', self.scriptMergerPath.text()) settings.setValue('nexusAPIKey', self.nexusAPIKey.text()) settings.setValue('nexusGetInfo', str(self.nexusGetInfo.isChecked())) settings.setValue('nexusCheckUpdates', str(self.nexusCheckUpdates.isChecked())) settings.setValue('nexusCheckClipboard', str(self.nexusCheckClipboard.isChecked())) settings.setValue('debugOutput', str(self.debugOutput.isChecked())) settings.setValue('unhideOutput', str(self.unhideOutput.isChecked())) self.close() def cancelEvent(self) -> None: self.close() def selectGameEvent(self) -> None: dialog: QFileDialog = QFileDialog(self, 'Select witcher3.exe', '', 'The Witcher 3 (witcher3.exe)') dialog.setOptions(QFileDialog.ReadOnly) dialog.setFileMode(QFileDialog.ExistingFile) if (dialog.exec_()): if dialog.selectedFiles(): self.gamePath.setText(dialog.selectedFiles()[0]) def selectConfigEvent(self) -> None: dialog: QFileDialog = QFileDialog(self, 'Select config folder', '', 'The Witcher 3') dialog.setOptions(QFileDialog.ReadOnly) dialog.setFileMode(QFileDialog.Directory) if (dialog.exec_()): if dialog.selectedFiles(): self.configPath.setText(dialog.selectedFiles()[0]) def selectScriptMergerEvent(self) -> None: dialog: QFileDialog = QFileDialog( self, 'Select WitcherScriptMerger.exe', '', 'Script Merger (WitcherScriptMerger.exe)') dialog.setOptions(QFileDialog.ReadOnly) dialog.setFileMode(QFileDialog.ExistingFile) if (dialog.exec_()): if dialog.selectedFiles(): self.scriptMergerPath.setText(dialog.selectedFiles()[0]) def locateGameEvent(self) -> None: game = fetcher.findGamePath() if game: self.gamePath.setText(str(game)) else: self.gamePathInfo.setText(''' <font color="#888"> Could not detect The Witcher 3!<br> Please make sure the game is installed, or set the path manually. </font>''') def locateConfigEvent(self) -> None: config = fetcher.findConfigPath() if config: self.configPath.setText(str(config)) else: self.configPathInfo.setText(''' <font color="#888"> Could not detect a valid config path! Please make sure the The Witcher 3 was started at least once, or set the path manually. </font>''') def locateScriptMergerEvent(self) -> None: scriptmerger = findScriptMergerPath() if scriptmerger: self.scriptMergerPath.setText(str(scriptmerger)) else: self.scriptMergerPathInfo.setText(''' <font color="#888"> Could not detect Script Merger! Please make sure Script Merger is running,<br> or set the path manually. Download Script Merger <a href="https://www.nexusmods.com/witcher3/mods/484">here</a>. </font>''') def validateGamePath(self, text: str) -> bool: # validate game installation path if not verifyGamePath(Path(text)): self.gamePath.setStyleSheet(''' *{ border: 1px solid #B22222; padding: 1px 0px; } ''') self.gamePathInfo.setText( '<font color="#888">Please enter a valid game path.</font>') self.validGamePath = False self.locateGame.setDisabled(False) self.updateSaveButton() return False else: self.gamePath.setStyleSheet('') self.gamePathInfo.setText( '<font color="#888">Everything looks good!</font>') self.validGamePath = True self.locateGame.setDisabled(True) self.updateSaveButton() return True def validateConfigPath(self, text: str) -> bool: # validate game config path if not verifyConfigPath(Path(text)): self.configPath.setStyleSheet(''' *{ border: 1px solid #B22222; padding: 1px 0px; } ''') self.configPathInfo.setText('''<font color="#888"> Please enter a valid config path. You need to start the The Witcher 3 at least once to generate the necessary user.settings and input.settings files.</font> ''') self.validConfigPath = False self.locateConfig.setDisabled(False) self.updateSaveButton() return False else: self.configPath.setStyleSheet('') self.configPathInfo.setText( '<font color="#888">Everything looks good!</font>') self.validConfigPath = True self.locateConfig.setDisabled(True) self.updateSaveButton() return True def validateScriptMergerPath(self, text: str) -> bool: # validate script merger path if not text: self.scriptMergerPath.setStyleSheet('') self.scriptMergerPathInfo.setText(''' <font color="#888">Script Merger is used to resolve conflicts between mods \ by merging scripts and other text files. \ Download Script Merger <a href="https://www.nexusmods.com/witcher3/mods/484">here</a>.</font> ''') self.validScriptMergerPath = True self.updateSaveButton() return True if not verifyScriptMergerPath(Path(text)): self.scriptMergerPath.setStyleSheet(''' *{ border: 1px solid #B22222; padding: 1px 0px; } ''') self.scriptMergerPathInfo.setText( '''<font color="#888">Please enter a valid script merger path.</font> ''') self.validScriptMergerPath = False self.locateScriptMerger.setDisabled(False) self.updateSaveButton() return False else: self.scriptMergerPath.setStyleSheet('') self.scriptMergerPathInfo.setText( '<font color="#888">Everything looks good!</font>') self.validScriptMergerPath = True self.locateScriptMerger.setDisabled(True) self.updateSaveButton() return True @debounce(200, cancel_running=True) async def validateApiKey(self, text: str) -> bool: # validate neus mods api key self.nexusGetInfo.setDisabled(True) self.nexusCheckUpdates.setDisabled(True) self.nexusCheckClipboard.setDisabled(True) self.nexusAPIKey.setStyleSheet('') if not text: self.nexusAPIKeyInfo.setText(''' <font color="#888">The API Key is used to check for mod updates, \ to get mod details and to download mods. \ Get your Personal API Key <a href="https://www.nexusmods.com/users/myaccount?tab=api">here</a>.</font> ''') self.validNexusAPIKey = True self.updateSaveButton() return True self.nexusAPIKeyInfo.setText('🌐') try: apiUser = await getUserInformation(text) except UnauthorizedError: self.nexusAPIKey.setStyleSheet(''' *{ border: 1px solid #B22222; padding: 1px 0px; } ''') self.nexusAPIKeyInfo.setText(''' <font color="#888">Not a valid API Key. \ Get your Personal API Key <a href="https://www.nexusmods.com/users/myaccount?tab=api">here</a>.</font> ''') self.validNexusAPIKey = False self.updateSaveButton() return False except (RequestError, ResponseError, Exception) as e: self.nexusAPIKey.setStyleSheet(''' *{ border: 1px solid #B22222; padding: 1px 0px; } ''') self.nexusAPIKeyInfo.setText(f''' <font color="#888">Could not validate API Key: {str(e) if str(e) else 'Request error'}.</font> ''') self.validNexusAPIKey = False self.updateSaveButton() return False self.nexusAPIKeyInfo.setText( f'<font color="#888">Valid API Key for {apiUser["name"]}!</font>') self.validNexusAPIKey = True self.nexusGetInfo.setDisabled(False) self.nexusCheckUpdates.setDisabled(False) self.nexusCheckClipboard.setDisabled(False) self.updateSaveButton() return True def updateSaveButton(self) -> None: # TODO: release: disable saving invalid settings # self.save.setDisabled(not all(( # self.validConfigPath, # self.validGamePath, # self.validNexusAPIKey, # self.validScriptMergerPath, # ))) # noqa self.save.setDisabled(False)
class GUI: class Window: statistics = 1 options = 2 app = QApplication([]) statisticsWindow = QWidget(f=Qt.FramelessWindowHint) optionsWindow = QWidget(f=Qt.FramelessWindowHint) defaultSizeX = 800 defaultSizeY = 600 newSizeX = None newSizeY = None hypixelAPILoad = 0 minecraftAPILoad = 0 overlayLoad = 0 currentWindow = Window.statistics def __init__(self, winx: int, winy: int, version: str, statisticsTypes: list, statistics: dict): self.newSizeX = winx self.newSizeY = winy self.version = version self.stats = statistics self.statTypes = statisticsTypes self.buildStatistics() def buildWindow(self, window: QWidget): window.resize(self.newSizeX, self.newSizeY) font = QFont() font.setBold(False) font.setUnderline(False) font.setKerning(False) font.setWeight(QFont.Weight.Normal) window.setFont(font) window.setObjectName(u"background") window.setStyleSheet("QWidget#background {background-color: gray}") def buildOptions(self): self.buildWindow() QLabel("Options", self.optionsWindow) def buildStatistics(self): self.buildWindow(self.window()) self.statTableMain = QTableView(self.window()) self.statTableMain.setObjectName(u"statTableMain") statTableHeader = QHeaderView(Qt.Orientation.Vertical) # self.statTableMain.commitData() # https://doc.qt.io/qt-5/sql-model.html self.statTableMain.setVerticalHeader(statTableHeader) self.statTableMain.setGeometry(self.winw(5), self.winh(10), self.winw(90), self.winh(80)) hypixelAPILabel = QLabel("Hypixel API Load:", self.window()) hypixelAPILabel.setGeometry(self.winw(5), self.winh(90), self.winw(15), self.winh(5)) minecraftAPILabel = QLabel("Minecraft API Load:", self.window()) minecraftAPILabel.setGeometry(self.winw(20), self.winh(90), self.winw(15), self.winh(5)) overlayProgressLabel = QLabel("Overlay Load:", self.window()) overlayProgressLabel.setGeometry(self.winw(35), self.winh(90), self.winw(15), self.winh(5)) self.hypixelProgressBar = QProgressBar(self.window()) self.hypixelProgressBar.setValue(self.hypixelAPILoad) self.hypixelProgressBar.setGeometry(self.winw(5), self.winh(95), self.winw(15), self.winh(3)) self.minecraftProgressBar = QProgressBar(self.window()) self.minecraftProgressBar.setValue(self.minecraftAPILoad) self.minecraftProgressBar.setGeometry(self.winw(20), self.winh(95), self.winw(15), self.winh(3)) self.overlayProgressBar = QProgressBar(self.window()) self.overlayProgressBar.setValue(self.overlayLoad) self.overlayProgressBar.setGeometry(self.winw(35), self.winh(95), self.winw(15), self.winh(3)) self.statisticsButton = QPushButton("&Stats", self.window()) self.statisticsButton.setGeometry(self.winw(5), self.winh(4), self.winw(10), self.winh(4)) self.statisticsButton.setObjectName(u"menuButton") self.optionsButton = QPushButton("&Options", self.window()) self.optionsButton.setGeometry(self.winw(16), self.winh(4), self.winw(10), self.winh(4)) self.optionsButton.setObjectName(u"menuButton") self.exitButton = QPushButton("&Exit", self.window()) self.exitButton.setGeometry(self.winw(85), self.winh(4), self.winw(10), self.winh(4)) self.exitButton.setObjectName(u"menuButton") self.statisticsButton.clicked.connect( lambda: self.statisticsButtonClick()) self.optionsButton.clicked.connect(lambda: self.optionsButtonClick()) self.exitButton.clicked.connect(lambda: self.exitButtonClick()) def updateButtons(self): self.statisticsButton.setDisabled( self.currentWindow == self.Window.statistics) self.optionsButton.setDisabled( self.currentWindow == self.Window.options) def statisticsButtonClick(self): self.currentWindow = self.Window.statistics def optionsButtonClick(self): self.currentWindow = self.Window.options def exitButtonClick(self): self.app.exit() def winw(self, percent: int): return percent / 100 * self.defaultSizeX def winh(self, percent: int): return percent / 100 * self.defaultSizeY def scale(self, normal: int, dir="both"): size = self.window().size() xscale = size.width() / self.defaultSizeX yscale = size.height() / self.defaultSizeY normal *= xscale if dir == "both" or dir == "hor" else 1 normal *= yscale if dir == "both" or dir == "ver" else 1 return normal def window(self): if self.currentWindow == self.Window.statistics: return self.statisticsWindow else: return self.optionsWindow def run(self): self.window().show() self.app.exec_()
class DownloadWindow(QDialog): def __init__(self, parent: Optional[QWidget] = None, url: str = '') -> None: super().__init__(parent, ) if parent: self.setWindowTitle('Download Mod') else: self.setWindowTitle(getTitleString('Download Mod')) self.setAttribute(Qt.WA_DeleteOnClose) mainLayout = QVBoxLayout(self) mainLayout.setContentsMargins(5, 5, 5, 5) self.signals = DownloadWindowEvents(self) # URL input gbUrl = QGroupBox('Mod URL') gbUrlLayout = QVBoxLayout() gbUrl.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.url = QLineEdit() self.url.setPlaceholderText( 'https://www.nexusmods.com/witcher3/mods/...') self.url.setText(url) self.url.textChanged.connect(lambda: self.validateUrl(self.url.text())) gbUrlLayout.addWidget(self.url) self.urlInfo = QLabel('🌐') self.urlInfo.setContentsMargins(4, 4, 4, 4) self.urlInfo.setMinimumHeight(36) self.urlInfo.setWordWrap(True) gbUrlLayout.addWidget(self.urlInfo) gbUrl.setLayout(gbUrlLayout) mainLayout.addWidget(gbUrl) # File selection gbFiles = QGroupBox('Mod Files') gbFilesLayout = QVBoxLayout() gbFiles.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) self.files = QTableWidget(0, 4) self.files.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel) self.files.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel) self.files.setContextMenuPolicy(Qt.CustomContextMenu) self.files.setSelectionMode(QAbstractItemView.ExtendedSelection) self.files.setSelectionBehavior(QAbstractItemView.SelectRows) self.files.setWordWrap(False) self.files.setSortingEnabled(True) self.files.setFocusPolicy(Qt.StrongFocus) self.files.verticalHeader().hide() self.files.setSortingEnabled(True) self.files.sortByColumn(2, Qt.DescendingOrder) self.files.verticalHeader().setVisible(False) self.files.verticalHeader().setDefaultSectionSize(25) self.files.horizontalHeader().setHighlightSections(False) self.files.horizontalHeader().setStretchLastSection(True) self.files.setHorizontalHeaderLabels( ['File Name', 'Version', 'Upload Date', 'Description']) self.files.setEditTriggers(QAbstractItemView.NoEditTriggers) self.files.verticalScrollBar().valueChanged.connect( lambda: self.files.clearFocus()) self.files.itemSelectionChanged.connect(lambda: self.validateFiles()) self.files.setDisabled(True) self.files.setStyleSheet(''' QTableView { gridline-color: rgba(255,255,255,1); } QTableView::item { padding: 5px; margin: 1px 0; } QTableView::item:!selected:hover { background-color: rgb(217, 235, 249); padding: 0; } ''') gbFilesLayout.addWidget(self.files) _mouseMoveEvent = self.files.mouseMoveEvent self.files.hoverIndexRow = -1 def mouseMoveEvent(event: QMouseEvent) -> None: self.files.hoverIndexRow = self.files.indexAt(event.pos()).row() _mouseMoveEvent(event) self.files.mouseMoveEvent = mouseMoveEvent # type: ignore self.files.setItemDelegate(ModListItemDelegate(self.files)) self.files.setMouseTracking(True) gbFiles.setLayout(gbFilesLayout) mainLayout.addWidget(gbFiles) # Actions actionsLayout = QHBoxLayout() actionsLayout.setAlignment(Qt.AlignRight) self.download = QPushButton('Download', self) self.download.clicked.connect(lambda: self.downloadEvent()) self.download.setAutoDefault(True) self.download.setDefault(True) self.download.setDisabled(True) actionsLayout.addWidget(self.download) cancel = QPushButton('Cancel', self) cancel.clicked.connect(self.cancelEvent) actionsLayout.addWidget(cancel) mainLayout.addLayout(actionsLayout) # Setup self.setMinimumSize(QSize(420, 420)) self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) self.resize(QSize(720, 420)) self.finished.connect( lambda: self.validateUrl.cancel()) # type: ignore self.finished.connect( lambda: self.downloadEvent.cancel()) # type: ignore self.modId = 0 self.validateUrl(self.url.text()) def cancelEvent(self) -> None: self.close() @debounce(200, cancel_running=True) async def validateUrl(self, url: str) -> bool: self.download.setDisabled(True) self.files.setDisabled(True) self.files.clearSelection() self.files.clearFocus() self.files.clearContents() self.files.setRowCount(0) self.files.setSortingEnabled(False) self.url.setStyleSheet('') self.modId = 0 if not url: self.urlInfo.setText(''' <font color="#888">Please enter a valid mod url.</font> ''') return False modId = getModId(url) if not modId: self.files.setDisabled(True) self.url.setStyleSheet(''' *{ border: 1px solid #B22222; padding: 1px 0px; } ''') self.urlInfo.setText(''' <font color="#888">Please enter a valid mod url.</font> ''') return False self.urlInfo.setText('🌐') try: filesResponse = await getModFiles(modId) except (RequestError, ResponseError, Exception) as e: self.url.setStyleSheet(''' *{ border: 1px solid #B22222; padding: 1px 0px; } ''') self.urlInfo.setText(f''' <font color="#888">Could not get mod files: {e}.</font> ''') return False try: files = filesResponse['files'] if not len(files): self.urlInfo.setText(f''' <font color="#888">Mod "{modId}" has no files!</font> ''') return False self.files.setRowCount(len(files)) for i in range(len(files)): file = files[i] fileid = int(file['file_id']) name = str(file['name']) version = str(file['version']) _uploadtime = dateparser.parse(file['uploaded_time']) uploadtime = _uploadtime.astimezone(tz=None).strftime( '%Y-%m-%d %H:%M:%S') if _uploadtime else '?' description = html.unescape(str(file['description'])) nameItem = QTableWidgetItem(name) nameItem.setToolTip(name) nameItem.setData(Qt.UserRole, fileid) self.files.setItem(i, 0, nameItem) versionItem = QTableWidgetItem(version) versionItem.setToolTip(version) self.files.setItem(i, 1, versionItem) uploadtimeItem = QTableWidgetItem(uploadtime) uploadtimeItem.setToolTip(uploadtime) self.files.setItem(i, 2, uploadtimeItem) descriptionItem = QTableWidgetItem(description) descriptionItem.setToolTip(description) self.files.setItem(i, 3, descriptionItem) except KeyError as e: logger.exception( f'Could not find key "{str(e)}" in mod files response') self.urlInfo.setText(f''' <font color="#888">Could not find key "{str(e)}" in mod files response.</font> ''') return False self.urlInfo.setText(f''' <font color="#888">Found {len(files)} available files.</font> ''') self.files.resizeColumnsToContents() self.files.setDisabled(False) self.files.setSortingEnabled(True) self.modId = modId return True def validateFiles(self) -> bool: selection = self.files.selectionModel().selectedRows() if len(selection) > 0: self.download.setText(f'Download {len(selection)} mods') self.download.setDisabled(False) return True return False @debounce(25, cancel_running=True) async def downloadEvent(self) -> None: self.download.setDisabled(True) self.url.setDisabled(True) selection = self.files.selectionModel().selectedRows() files = [ self.files.item(index.row(), 0).data(Qt.UserRole) for index in selection ] self.files.setDisabled(True) try: urls = await asyncio.gather( *[getModFileUrls(self.modId, file) for file in files], loop=asyncio.get_running_loop()) except (RequestError, ResponseError, Exception) as e: self.url.setStyleSheet(''' *{ border: 1px solid #B22222; padding: 1px 0px; } ''') self.urlInfo.setText(f''' <font color="#888">Could not download mod files: {e}.</font> ''') return try: self.signals.download.emit([url[0]['URI'] for url in urls]) except KeyError as e: logger.exception( f'Could not find key "{str(e)}" in file download response') self.urlInfo.setText(f''' <font color="#888">Could not find key "{str(e)}" in file download response.</font> ''') return self.close()