class MainWindow(QMainWindow): def __init__(self, *args, **kwargs): super(MainWindow, self).__init__() layout = QVBoxLayout() self.progress = QProgressBar() button = QPushButton("START IT UP") button.pressed.connect(self.execute) layout.addWidget(self.progress) layout.addWidget(button) w = QWidget() w.setLayout(layout) self.setCentralWidget(w) self.show() self.threadpool = QThreadPool() print("Multithreading with maximum %d threads" % self.threadpool.maxThreadCount()) def execute(self): worker = Worker() worker.signals.progress.connect(self.update_progress) # Execute self.threadpool.start(worker) def update_progress(self, progress): self.progress.setValue(progress)
def test_worker_no_progress_callback(qtbot): """ Testing for the progress_callback parameter not being present in the called function when no progress_callback """ func_to_test = Mock() w = Worker(func_to_test, "test", 3) threadpool = QThreadPool() with qtbot.waitSignal(w.signals.finished) as blocker: threadpool.start(w) assert w.fn == func_to_test assert 'progress_callback' not in w.kwargs func_to_test.assert_called_with("test", 3)
def test_worker_result_signal(qtbot, monkeypatch): """ Testing return value of worker's called function through result signal. """ thing = FakeClass() thing.func_to_test = Mock(return_value=5, unsafe=True) w = Worker(thing.func_to_test, "test", 3) with mock.patch.object(FakeClass, 'func_result', wraps=thing.func_result) as mock_func_result: w.signals.result.connect(thing.func_result) threadpool = QThreadPool() with qtbot.waitSignal(w.signals.finished) as blocker: threadpool.start(w) thing.func_to_test.assert_called_with("test", 3) mock_func_result.assert_called_with(5)
def test_worker_progress_callback(qtbot): """ Testing for the progress_callback parameter being present in the called function when progress_callback=True """ func_to_test = Mock() w = Worker(func_to_test, "test", 3, progress_callback=True) # This starts the Worker in the threadpool and then blocks the test from progressing until the finished signal is # emitted. qtbot is a pytest fixture used to test PyQt5. threadpool = QThreadPool() with qtbot.waitSignal(w.signals.finished) as blocker: threadpool.start(w) assert w.fn == func_to_test assert w.kwargs['progress_callback'] is not None func_to_test.assert_called_with( "test", 3, progress_callback=w.kwargs['progress_callback'])
def test_worker_error_signal(qtbot): """ Testing return value of worker's called function through result signal. """ thing = FakeClass() thing.func_to_test = Mock(side_effect=ValueError()) w = Worker(thing.func_to_test, "test", 3) with mock.patch.object(FakeClass, 'func_error', wraps=thing.func_error): w.signals.error.connect(thing.func_error) threadpool = QThreadPool() with qtbot.waitSignal(w.signals.finished) as blocker: threadpool.start(w) kall = thing.func_error.call_args args, kwargs = kall thing.func_to_test.assert_called_with("test", 3) assert isinstance(args[0][1], ValueError)
class MainWindow(QMainWindow): def __init__(self): super().__init__() layout = QVBoxLayout() self.progress = QProgressBar() button = QPushButton("START IT UP") button.pressed.connect(self.execute) self.status = QLabel("0 workers") layout.addWidget(self.progress) layout.addWidget(button) layout.addWidget(self.status) w = QWidget() w.setLayout(layout) # Dictionary holds the progress of current workers. self.worker_progress = {} self.setCentralWidget(w) self.show() self.threadpool = QThreadPool() print("Multithreading with maximum %d threads" % self.threadpool.maxThreadCount()) self.timer = QTimer() self.timer.setInterval(100) self.timer.timeout.connect(self.refresh_progress) self.timer.start() def execute(self): worker = Worker() worker.signals.progress.connect(self.update_progress) worker.signals.finished.connect(self.cleanup) # 任务完成后清除对应进度 # Execute self.threadpool.start(worker) def cleanup(self, job_id): if job_id in self.worker_progress: del self.worker_progress[job_id] # 对完成的任务,删除 # Update the progress bar if we've removed a value. self.refresh_progress() def update_progress(self, job_id, progress): self.worker_progress[job_id] = progress def calculate_progress(self): if not self.worker_progress: return 0 return sum(v for v in self.worker_progress.values()) / len( self.worker_progress) def refresh_progress(self): # Calculate total progress. progress = self.calculate_progress() print(self.worker_progress) self.progress.setValue(progress) self.status.setText("%d workers" % len(self.worker_progress))
class BatchProcessingController: """ This class is the controller for batch processing. It starts and ends processes, and controls the progress window. """ def __init__(self): """ Class initialiser function. """ self.batch_path = "" self.dvh_output_path = "" self.pyrad_output_path = "" self.clinical_data_input_path = "" self.clinical_data_output_path = "" self.processes = [] self.dicom_structure = None self.suv2roi_weights = None self.name_cleaning_options = None self.patient_files_loaded = False self.progress_window = ProgressWindow(None) self.timestamp = "" self.batch_summary = [{}, ""] # Threadpool for file loading self.threadpool = QThreadPool() self.interrupt_flag = threading.Event() def set_file_paths(self, file_paths): """ Sets all the required paths :param file_paths: dict of directories """ self.batch_path = file_paths.get('batch_path') self.dvh_output_path = file_paths.get('dvh_output_path') self.pyrad_output_path = file_paths.get('pyrad_output_path') self.clinical_data_input_path = \ file_paths.get('clinical_data_input_path') self.clinical_data_output_path = \ file_paths.get('clinical_data_output_path') def set_processes(self, processes): """ Sets the selected processes :param processes: list of selected processes """ self.processes = processes def set_suv2roi_weights(self, suv2roi_weights): """ Function used to set suv2roi_weights. :param suv2roi_weights: Dictionary of patient IDs and patient weight in grams. """ self.suv2roi_weights = suv2roi_weights def start_processing(self): """ Starts the batch process. """ # Create new instance of ProgressWindow self.progress_window = ProgressWindow(None) # Connect callbacks self.progress_window.signal_error.connect(self.error_processing) self.progress_window.signal_loaded.connect(self.completed_processing) # Start performing processes on patient files self.progress_window.start(self.perform_processes) def load_patient_files(self, path, progress_callback, search_complete_callback): """ Load the patient files from directory. """ # Set the interrup flag self.interrupt_flag.set() # Release the current thread, and create new threadpool self.threadpool.releaseThread() self.threadpool = QThreadPool() # Clear the interrupt flag self.interrupt_flag.clear() # Create new worker worker = Worker(DICOMDirectorySearch.get_dicom_structure, path, self.interrupt_flag, progress_callback=True) # Connect callbacks worker.signals.result.connect(search_complete_callback) worker.signals.progress.connect(progress_callback) # Start the worker self.threadpool.start(worker) def set_dicom_structure(self, dicom_structure): """ Function used to set dicom_structure :param dicom_structure: DICOMStructure """ self.dicom_structure = dicom_structure def set_name_cleaning_options(self, options): """ Set name cleaning options for batch ROI name cleaning. :param options: Dictionary of datasets, ROIs, and options for cleaning the ROIs. """ self.name_cleaning_options = options @staticmethod def get_patient_files(patient): """ Get patient files. :param patient: patient data. :return: cur_patient_files, dictionary of classes and series'. """ # Get files in patient cur_patient_files = {} for study in patient.studies.values(): for series_type in study.series.values(): for series in series_type.values(): image = list(series.images.values())[0] class_id = image.class_id if class_id not in cur_patient_files: cur_patient_files[class_id] = [] cur_patient_files[class_id].append(series) return cur_patient_files def perform_processes(self, interrupt_flag, progress_callback=None): """ Performs each selected process to each selected patient. :param interrupt_flag: A threading.Event() object that tells the function to stop loading. :param progress_callback: A signal that receives the current progress of the loading. """ # Clear batch summary self.batch_summary = [{}, ""] # Dictionary of process names and functions self.process_functions = { "iso2roi": self.batch_iso2roi_handler, "suv2roi": self.batch_suv2roi_handler, "dvh2csv": self.batch_dvh2csv_handler, "pyrad2csv": self.batch_pyrad2csv_handler, "pyrad2pyrad-sr": self.batch_pyrad2pyradsr_handler, "csv2clinicaldata-sr": self.batch_csv2clinicaldatasr_handler, "clinicaldata-sr2csv": self.batch_clinicaldatasr2csv_handler, "roiname2fmaid": self.batch_roiname2fmaid_handler, } patient_count = len(self.dicom_structure.patients) cur_patient_num = 0 self.timestamp = self.create_timestamp() # Loop through each patient for patient in self.dicom_structure.patients.values(): # Stop loading if interrupt_flag.is_set(): # TODO: convert print to logging print("Stopped Batch Processing") PatientDictContainer().clear() return False cur_patient_num += 1 progress_callback.emit( ("Loading patient ({}/{}) .. ".format(cur_patient_num, patient_count), 20)) # Perform processes on patient for process in self.processes: if process == 'roinamecleaning': continue self.process_functions[process](interrupt_flag, progress_callback, patient) # Perform batch ROI Name Cleaning on all patients if 'roinamecleaning' in self.processes: if self.name_cleaning_options: # Start the process process = \ BatchProcessROINameCleaning(progress_callback, interrupt_flag, self.name_cleaning_options) process.start() # Append process summary self.batch_summary[1] = process.summary progress_callback.emit(("Completed ROI Name Cleaning", 100)) PatientDictContainer().clear() def update_rtss(self, patient): """ Updates the patient dict container with the newly created RTSS (if a process generates one), so it can be used by future processes. :param patient: The patient with the newly-created RTSS. """ # Get new RTSS rtss = PatientDictContainer().dataset['rtss'] # Create a series and image from the RTSS rtss_series = Series(rtss.SeriesInstanceUID) rtss_series.series_description = rtss.get("SeriesDescription") rtss_image = Image(PatientDictContainer().filepaths['rtss'], rtss.SOPInstanceUID, rtss.SOPClassUID, rtss.Modality) rtss_series.add_image(rtss_image) # Add the new study to the patient patient.studies[rtss.StudyInstanceUID].add_series(rtss_series) # Update the patient dict container PatientDictContainer().set("rtss_modified", False) def batch_iso2roi_handler(self, interrupt_flag, progress_callback, patient): """ Handles creating, starting, and processing the results of batch ISO2ROI. :param interrupt_flag: A threading.Event() object that tells the function to stop loading. :param progress_callback: A signal that receives the current progress of the loading. :param patient: The patient to perform this process on. """ # Get current patient files cur_patient_files = \ BatchProcessingController.get_patient_files(patient) # Create and start process process = BatchProcessISO2ROI(progress_callback, interrupt_flag, cur_patient_files) success = process.start() # Add rtss to patient in case it is needed in future # processes if success: if PatientDictContainer().get("rtss_modified"): self.update_rtss(patient) reason = "SUCCESS" else: reason = process.summary # Append process summary if patient not in self.batch_summary[0].keys(): self.batch_summary[0][patient] = {} self.batch_summary[0][patient]["iso2roi"] = reason progress_callback.emit(("Completed ISO2ROI", 100)) def batch_suv2roi_handler(self, interrupt_flag, progress_callback, patient): """ Handles creating, starting, and processing the results of batch SUV2ROI. :param interrupt_flag: A threading.Event() object that tells the function to stop loading. :param progress_callback: A signal that receives the current progress of the loading. :param patient: The patient to perform this process on. """ # Get patient files cur_patient_files = \ BatchProcessingController.get_patient_files(patient) # Get patient weight if patient.patient_id in self.suv2roi_weights.keys(): if self.suv2roi_weights[patient.patient_id] is None: patient_weight = None else: patient_weight = \ self.suv2roi_weights[patient.patient_id] * 1000 else: patient_weight = None process = BatchProcessSUV2ROI(progress_callback, interrupt_flag, cur_patient_files, patient_weight) success = process.start() # Add rtss to patient in case it is needed in future # processes if success: if PatientDictContainer().get("rtss_modified"): self.update_rtss(patient) reason = "SUCCESS" else: reason = process.summary # Append process summary if patient not in self.batch_summary[0].keys(): self.batch_summary[0][patient] = {} self.batch_summary[0][patient]["suv2roi"] = reason progress_callback.emit(("Completed SUV2ROI", 100)) def batch_dvh2csv_handler(self, interrupt_flag, progress_callback, patient): """ Handles creating, starting, and processing the results of batch DVH2CSV. :param interrupt_flag: A threading.Event() object that tells the function to stop loading. :param progress_callback: A signal that receives the current progress of the loading. :param patient: The patient to perform this process on. """ # Get current patient files cur_patient_files = \ BatchProcessingController.get_patient_files(patient) # Create and start process process = BatchProcessDVH2CSV(progress_callback, interrupt_flag, cur_patient_files, self.dvh_output_path) process.set_filename('DVHs_' + self.timestamp + '.csv') success = process.start() # Set process summary if success: reason = "SUCCESS" else: reason = process.summary # Append process summary if patient not in self.batch_summary[0].keys(): self.batch_summary[0][patient] = {} self.batch_summary[0][patient]['dvh2csv'] = reason progress_callback.emit(("Completed DVH2CSV", 100)) def batch_pyrad2csv_handler(self, interrupt_flag, progress_callback, patient): """ Handles creating, starting, and processing the results of batch Pyrad2CSV. :param interrupt_flag: A threading.Event() object that tells the function to stop loading. :param progress_callback: A signal that receives the current progress of the loading. :param patient: The patient to perform this process on. """ # Get current files cur_patient_files = \ BatchProcessingController.get_patient_files(patient) process = BatchProcessPyRad2CSV(progress_callback, interrupt_flag, cur_patient_files, self.pyrad_output_path) process.set_filename('PyRadiomics_' + self.timestamp + '.csv') success = process.start() # Set summary message if success: reason = "SUCCESS" else: reason = process.summary # Append process summary if patient not in self.batch_summary[0].keys(): self.batch_summary[0][patient] = {} self.batch_summary[0][patient]['pyrad2csv'] = reason progress_callback.emit(("Completed PyRad2CSV", 100)) def batch_pyrad2pyradsr_handler(self, interrupt_flag, progress_callback, patient): """ Handles creating, starting, and processing the results of batch PyRad2PyRad-SR. :param interrupt_flag: A threading.Event() object that tells the function to stop loading. :param progress_callback: A signal that receives the current progress of the loading. :param patient: The patient to perform this process on. """ # Get current files cur_patient_files = \ BatchProcessingController.get_patient_files(patient) process = BatchProcessPyRad2PyRadSR(progress_callback, interrupt_flag, cur_patient_files) success = process.start() # Set summary message if success: reason = "SUCCESS" else: reason = process.summary # Append process summary if patient not in self.batch_summary[0].keys(): self.batch_summary[0][patient] = {} self.batch_summary[0][patient]['pyrad2pyradSR'] = reason progress_callback.emit(("Completed PyRad2PyRad-SR", 100)) def batch_csv2clinicaldatasr_handler(self, interrupt_flag, progress_callback, patient): """ Handles creating, starting, and processing the results of batch CSV2ClinicalData-SR. :param interrupt_flag: A threading.Event() object that tells the function to stop loading. :param progress_callback: A signal that receives the current progress of the loading. :param patient: The patient to perform this process on. """ # Get current files cur_patient_files = \ BatchProcessingController.get_patient_files(patient) process = \ BatchProcessCSV2ClinicalDataSR(progress_callback, interrupt_flag, cur_patient_files, self.clinical_data_input_path) success = process.start() # Update summary if success: reason = "SUCCESS" else: reason = process.summary if patient not in self.batch_summary[0].keys(): self.batch_summary[0][patient] = {} self.batch_summary[0][patient]["csv2clinicaldatasr"] = reason progress_callback.emit(("Completed CSV2ClinicalData-SR", 100)) def batch_clinicaldatasr2csv_handler(self, interrupt_flag, progress_callback, patient): """ Handles creating, starting, and processing the results of batch ClinicalData-SR2CSV. :param interrupt_flag: A threading.Event() object that tells the function to stop loading. :param progress_callback: A signal that receives the current progress of the loading. :param patient: The patient to perform this process on. """ cur_patient_files = \ BatchProcessingController.get_patient_files(patient) process = \ BatchProcessClinicalDataSR2CSV(progress_callback, interrupt_flag, cur_patient_files, self.clinical_data_output_path) success = process.start() # Update summary if success: reason = "SUCCESS" else: reason = process.summary if patient not in self.batch_summary[0].keys(): self.batch_summary[0][patient] = {} self.batch_summary[0][patient]["clinicaldatasr2csv"] = reason progress_callback.emit(("Completed ClinicalData-SR2CSV", 100)) def batch_roiname2fmaid_handler(self, interrupt_flag, progress_callback, patient): """ Handles creating, starting, and processing the results of batch ROIName2FMA-ID. :param interrupt_flag: A threading.Event() object that tells the function to stop loading. :param progress_callback: A signal that receives the current progress of the loading. :param patient: The patient to perform this process on. """ # Get patient files and start process cur_patient_files = \ BatchProcessingController.get_patient_files(patient) process = \ BatchProcessROIName2FMAID(progress_callback, interrupt_flag, cur_patient_files) process.start() # Append process summary reason = process.summary if patient not in self.batch_summary[0].keys(): self.batch_summary[0][patient] = {} self.batch_summary[0][patient]["roiname2fmaid"] = reason progress_callback.emit(("Completed ROI Name to FMA ID", 100)) def completed_processing(self): """ Runs when batch processing has been completed. """ self.progress_window.update_progress(("Processing complete!", 100)) self.progress_window.close() # Create window to store summary info batch_summary_window = BatchSummaryWindow() batch_summary_window.set_summary_text(self.batch_summary) batch_summary_window.exec_() def error_processing(self): """ Runs when there is an error during batch processing. """ print("Error performing batch processing.") self.progress_window.close() return @classmethod def create_timestamp(cls): """ Create a unique timestamp as a string. returns string """ cur_time = datetime.datetime.now() year = cur_time.year month = cur_time.month day = cur_time.day hour = cur_time.hour min = cur_time.minute sec = cur_time.second time_stamp = str(year) + str(month) + str(day) + str(hour) + \ str(min) + str(sec) return time_stamp
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 ProgressWindow(QDialog): signal_loaded = QtCore.Signal(tuple) signal_error = QtCore.Signal(int) signal_advise_calc_dvh = QtCore.Signal(bool) def __init__(self, *args, **kwargs): super(ProgressWindow, self).__init__(*args, **kwargs) # Setting up progress bar self.progress_bar = QtWidgets.QProgressBar() self.progress_bar.setGeometry(10, 50, 230, 20) self.progress_bar.setMaximum(100) self.setWindowTitle("Loading") self.setFixedSize(248, 80) self.text_field = QLabel("Loading") self.layout = QVBoxLayout() self.layout.addWidget(self.text_field) self.layout.addWidget(self.progress_bar) self.setLayout(self.layout) self.threadpool = QThreadPool() self.interrupt_flag = threading.Event() def start_loading(self, selected_files): image_loader = ImageLoader(selected_files, self) image_loader.signal_request_calc_dvh.connect(self.prompt_calc_dvh) worker = Worker(image_loader.load, self.interrupt_flag, progress_callback=True) worker.signals.result.connect(self.on_finish) worker.signals.error.connect(self.on_error) worker.signals.progress.connect(self.update_progress) self.threadpool.start(worker) def on_finish(self, result): self.update_progress(("Initializing patient window...", 90)) self.signal_loaded.emit((result, self)) def update_progress(self, progress_update): """ Function responsible for updating the bar percentage and the label. :param progress_update: A tuple containing update text and update percentage """ self.text_field.setText(progress_update[0]) self.progress_bar.setValue(progress_update[1]) def prompt_calc_dvh(self): choice = QMessageBox.question( self, "Calculate DVHs?", "RTSTRUCT and RTDOSE datasets identified. Would you " "like to calculate DVHs? (This may take up to several " "minutes on some systems.)", QMessageBox.Yes | QMessageBox.No) if choice == QMessageBox.Yes: self.signal_advise_calc_dvh.emit(True) else: self.signal_advise_calc_dvh.emit(False) def on_error(self, err): if type(err[1]) is ImageLoading.NotRTSetError: self.signal_error.emit(0) elif type(err[1]) is ImageLoading.NotAllowedClassError: self.signal_error.emit(1) def closeEvent(self, event: QtGui.QCloseEvent) -> None: self.interrupt_flag.set()
class tfm(QMainWindow, Ui_tfm): """ The main window class, setting up the application and implementing the needed events. """ def __init__(self, args: List[str]): """ At the moment the very, very long initialization of the main window, setting up everything. :param default_path: Use a user defined path as entrypoint. If it's empty, the home directory of the current user will be used. :type default_path: str """ super(tfm, self).__init__() self.setupUi(self) self.setWindowIcon(QIcon.fromTheme('system-file-manager')) self.clipboard = QApplication.clipboard() self.marked_to_cut = [] self.back_stack = stack() self.forward_stack = stack() self.config_dir = os.path.join( QStandardPaths.writableLocation( QStandardPaths().ConfigLocation), type(self).__name__) self.current_path = utility.handle_args(args) self.default_path = self.current_path self.threadpool = QThreadPool() # MAIN VIEW # # set up QFileSystemModel self.filesystem = QFileSystemModel() self.filesystem.setRootPath(self.current_path) self.filesystem.setReadOnly(False) # connect QFileSystemModel to View self.table_view.setModel(self.filesystem) self.table_view.setRootIndex( self.filesystem.index(self.current_path)) # set up header self.horizontal_header = self.table_view.horizontalHeader() self.horizontal_header.setSectionsMovable(True) # name self.horizontal_header.resizeSection(0, 200) # size self.horizontal_header.resizeSection(1, 100) # type self.horizontal_header.resizeSection(2, 100) # FS TREE # # create seperate FileSystemModel for the fs tree self.fs_tree_model = QFileSystemModel() self.fs_tree_model.setFilter(QDir.AllDirs | QDir.NoDotAndDotDot) self.fs_tree_model.setRootPath(QDir.rootPath()) # connect model to view self.fs_tree.setModel(self.fs_tree_model) # hide unneeded columns for column in range(1, self.fs_tree.header().count()): self.fs_tree.hideColumn(column) # expand root item self.fs_tree.expand(self.fs_tree_model.index(0, 0)) # BOOKMARKS # self.bookmarks = bm(path_to_bookmark_file=os.path.join(self.config_dir, 'bookmarks')) self.bookmark_view.setModel(self.bookmarks) # MOUNTS # self.udev_context = Context() self.mounts = mounts_model(context=self.udev_context) self.mounts_view.setModel(self.mounts) udev_monitor = Monitor.from_netlink(self.udev_context) udev_monitor.filter_by(subsystem='block', device_type='partition') udev_observer = MonitorObserver(udev_monitor, self.devices_changed) udev_observer.start() # STATUSBAR # # TODO: dir info self.item_info = QLabel() # self.dir_info = QLabel() self.part_info = QLabel() self.statusbar.addPermanentWidget(self.item_info) # self.statusbar.addPermanentWidget(self.dir_info) self.statusbar.addPermanentWidget(self.part_info) self.part_info.setText(utility.part_info(self.current_path)) # TOOLBAR # # initially disable back/forward navigation self.action_back.setEnabled(False) self.action_forward.setEnabled(False) # main menu self.main_menu = QMenu() self.main_menu.addAction(self.action_show_hidden) self.menu_button = QToolButton() self.menu_button.setMenu(self.main_menu) self.menu_button.setPopupMode(QToolButton().InstantPopup) self.menu_button.setDefaultAction(self.action_menu) self.toolbar.insertWidget(self.action_back, self.menu_button) # adress bar self.adressbar = QLineEdit() self.adressbar.setText(self.current_path) self.toolbar.insertWidget(self.action_go, self.adressbar) # menu for new file or directory self.new_menu = QMenu() self.new_menu.addAction(self.action_new_dir) self.new_menu.addAction(self.action_new_file) self.new_button = QToolButton() self.new_button.setMenu(self.new_menu) self.new_button.setPopupMode(QToolButton().MenuButtonPopup) self.new_button.setDefaultAction(self.action_new_dir) self.toolbar.insertWidget(self.action_back, self.new_button) self.connect_actions_to_events() self.set_shortcuts() self.set_icons() self.set_context_menus() # ---------------- setup ----------------------------------------------- # def set_icons(self): """ Adds icons to all applicable actions. """ self.action_back.setIcon(QIcon.fromTheme('go-previous')) self.action_forward.setIcon(QIcon.fromTheme('go-next')) self.action_home.setIcon(QIcon.fromTheme('go-home')) self.action_up.setIcon(QIcon.fromTheme('go-up')) self.action_go.setIcon(QIcon.fromTheme('go-jump')) self.action_new_dir.setIcon(QIcon.fromTheme('folder-new')) self.action_new_file.setIcon(QIcon.fromTheme('document-new')) self.action_menu.setIcon(QIcon.fromTheme('start-here')) self.action_copy.setIcon(QIcon.fromTheme('edit-copy')) self.action_cut.setIcon(QIcon.fromTheme('edit-cut')) self.action_paste.setIcon(QIcon.fromTheme('edit-paste')) self.action_delete.setIcon(QIcon.fromTheme('edit-delete')) self.action_add_to_bookmarks.setIcon(QIcon.fromTheme('list-add')) self.action_remove_bookmark.setIcon(QIcon.fromTheme('list-remove')) def connect_actions_to_events(self): """ Connects actions to their event functions. """ self.adressbar.returnPressed.connect(self.action_go_event) self.action_go.triggered.connect(self.action_go_event) self.action_home.triggered.connect(self.action_home_event) self.action_up.triggered.connect(self.action_up_event) self.action_back.triggered.connect(self.action_back_event) self.action_forward.triggered.connect(self.action_forward_event) self.action_copy.triggered.connect(self.action_copy_event) self.action_copy_path.triggered.connect(self.action_copy_path_event) self.action_paste.triggered.connect(self.action_paste_event) self.action_cut.triggered.connect(self.action_cut_event) self.action_delete.triggered.connect(self.action_delete_event) self.action_rename.triggered.connect(self.action_rename_event) self.action_show_hidden.toggled.connect(self.action_show_hidden_event) self.action_new_dir.triggered.connect(self.action_new_dir_event) self.action_new_file.triggered.connect(self.action_new_file_event) self.action_add_to_bookmarks.triggered.connect( self.action_add_to_bookmarks_event) self.action_remove_bookmark.triggered.connect( self.action_remove_bookmark_event) self.action_extract_here.triggered.connect( self.action_extract_here_event) self.action_mount_iso.triggered.connect(self.action_mount_iso_event) self.bookmark_view.pressed.connect(self.bookmark_selected_event) self.fs_tree.pressed.connect(self.fs_tree_event) self.mounts_view.pressed.connect(self.mount_selected_event) self.mounts_view.activated.connect(self.mount_toggle_event) self.table_view.pressed.connect(self.item_selected_event) self.table_view.activated.connect(self.item_open_event) def set_shortcuts(self): """ Set shortcuts for actions. """ self.action_copy.setShortcuts( QKeySequence.keyBindings(QKeySequence.Copy)) self.action_paste.setShortcuts( QKeySequence.keyBindings(QKeySequence.Paste)) self.action_cut.setShortcuts( QKeySequence.keyBindings(QKeySequence.Cut)) self.action_delete.setShortcuts( QKeySequence.keyBindings(QKeySequence.Delete)) def set_context_menus(self): """ Setup context menus. """ self.table_view.addAction(self.action_copy) self.table_view.addAction(self.action_copy_path) self.table_view.addAction(self.action_paste) self.table_view.addAction(self.action_cut) self.table_view.addAction(self.action_rename) self.table_view.addAction(self.action_delete) self.table_view.addAction(self.action_show_hidden) self.bookmark_view.addAction(self.action_remove_bookmark) # ---------------- events ---------------------------------------------- # def action_new_dir_event(self): """ Prompts the user for a dir name and creates one with that name in the current directory. """ dir_name, ok = QInputDialog().getText(self, 'Create new directory', 'Directory name:') if (ok): if (dir_name): if not QDir().mkpath(os.path.join(self.current_path, dir_name)): dialog = utility.message_dialog('Directory could not be' + ' created.', QMessageBox.Critical) dialog.exec() else: dialog = utility.message_dialog('Please enter a name for the ' + 'new directory.', QMessageBox.Warning) dialog.exec() def action_new_file_event(self): """ Prompts the user for a file name and creates one with that name in the current directory. """ file_name, ok = QInputDialog().getText(self, 'Create new file', 'File name:') if (ok): if (file_name): try: with open(os.path.join(self.current_path, file_name), 'a'): pass except OSError: dialog = utility.message_dialog('File cannot be created.', QMessageBox.Warning) dialog.exec() else: dialog = utility.message_dialog('Please enter a name for the' + ' new file.', QMessageBox.Warning) dialog.exec() def action_go_event(self): """ Changes the current dir to the one, that is given in the adress bar. """ next_dir = QDir(self.adressbar.text()) if (next_dir.isAbsolute() and next_dir.exists()): next_path = next_dir.path() self.update_current_path(next_path) else: dialog = utility.message_dialog('The path entered does not exist.', QMessageBox.Warning) dialog.exec() def action_home_event(self): """ Changes the current dir to the users $HOME. """ next_path = QDir().homePath() self.update_current_path(next_path) def action_up_event(self): """ Changes the current dir to one dir up in the hierarchy """ dir_up = QDir(self.current_path) if (dir_up.cdUp()): next_path = dir_up.path() self.update_current_path(next_path) else: logging.warning('There is no directory above the one you are in.' + ' This message should not occur, because the' + ' button triggering this should be deactivated' + ' Please report this issue.') def action_back_event(self): """ Goes back one step in the navigation history. If after this, there are no more steps, it disables it's button. """ next_path = self.back_stack.pop() self.forward_stack.push(self.current_path) self.action_forward.setEnabled(True) if (self.back_stack.empty()): self.action_back.setEnabled(False) self.update_current_path(next_path, skip_stack=True, reset_forward_stack=False) def action_forward_event(self): """ Goes forward one step in the navigation history. If after this, there are no more steps, it disables it's button. """ if (not self.forward_stack.empty()): next_path = self.forward_stack.pop() self.update_current_path(next_path, reset_forward_stack=False) # disable forward action if there are no more forward actions if (self.forward_stack.empty()): self.action_forward.setEnabled(False) else: logging.warning('Forward stack unexpectedly empty. This should not' + ' happen, because the button triggering this' + ' message should be deactivated. Please report' + ' this issue.') def item_open_event(self): """ Opens the selected files using xdg-open, runs it, if it is executable or changes the current dir if it's dir. """ selected_items = [] for index in self.table_view.selectedIndexes(): if index.column() == 0: selected_items.append(QFileInfo(os.path.join(self.current_path, index.siblingAtColumn(0).data()))) # warn before accidentally open a bunch of files open = True if len(selected_items) > 3: dialog = utility.question_dialog('Do you really want to open ' + str(len(selected_items)) + ' files?') button = dialog.exec() if button == QMessageBox.Cancel: open = False if open: for item in selected_items: if (item.isDir()): next_path = item.absoluteFilePath() self.update_current_path(next_path) elif (item.isFile()): if (item.isExecutable()): QProcess().startDetached(item.absoluteFilePath(), [], self.current_path) else: QProcess().startDetached('xdg-open', [item.absoluteFilePath()], self.current_path) else: dialog = utility.message_dialog('The type of the selected' + ' file can not be' + ' detected.', QMessageBox.Warning) dialog.exec() def item_selected_event(self): """ Updates statusbar info, depending on the selected file. """ selected_items = self.table_view.selectedIndexes() files = [] for item in selected_items: files.append(os.path.join(self.current_path, item.siblingAtColumn(0).data())) files = list(set(files)) if len(files) == 1: # update context menu according to file type if (os.path.isdir(files[0])): self.table_view.addAction(self.action_add_to_bookmarks) self.table_view.removeAction(self.action_extract_here) self.table_view.removeAction(self.action_mount_iso) else: self.table_view.removeAction(self.action_add_to_bookmarks) if (is_tarfile(files[0]) or is_zipfile(files[0])): self.table_view.addAction(self.action_extract_here) if (os.path.splitext(files[0])[1] == '.iso'): self.table_view.addAction(self.action_mount_iso) else: self.table_view.removeAction(self.action_extract_here) self.table_view.removeAction(self.action_mount_iso) files = list(set(files)) self.item_info.setText(utility.file_info(files)) def fs_tree_event(self): """ Changes the current dir to the dir selected in the fs_tree view. """ next_path = self.fs_tree_model.filePath(self.fs_tree.currentIndex()) self.update_current_path(next_path) def action_extract_here_event(self): """ Extracts the given file if possible. """ selected_item = os.path.join(self.current_path, self.table_view.currentIndex(). siblingAtColumn(0).data()) # setup QProgressDialog progress_dialog = QProgressDialog(parent=self) progress_dialog.reset() progress_dialog.setMinimumDuration(1000) progress_dialog.setLabelText('Extracting archive ' + selected_item + '...') progress_dialog.setValue(0) # setup extract_worker extract_worker = ew(archive_path=selected_item) # canceled progress_dialog.canceled.connect(extract_worker.cancel, type=Qt.DirectConnection) # ready extract_worker.signals.ready.connect(progress_dialog.setMaximum) # progress extract_worker.signals.progress.connect(progress_dialog.setValue) # progress message extract_worker.signals.progress_message.connect( progress_dialog.setLabelText) # finished extract_worker.signals.finished.connect(progress_dialog.reset) self.threadpool.start(extract_worker) def action_copy_event(self): """ Copies the currently selected files to the clipboard. """ # get current selection _, mime_data = utility.get_MIME( self.table_view.selectionModel().selectedIndexes()) self.clipboard.setMimeData(mime_data) def action_paste_event(self): """ Pastes the files currently in the clipboard to the current dir. """ # setup QProgressDialog progress_dialog = QProgressDialog(parent=self) progress_dialog.reset() progress_dialog.setMinimumDuration(1000) progress_dialog.setLabelText('Pasting...') progress_dialog.setValue(0) # setup paste_worker paste_worker = pw(clipboard=self.clipboard, target_path=self.current_path, marked_to_cut=self.marked_to_cut) # canceled progress_dialog.canceled.connect(paste_worker.cancel, type=Qt.DirectConnection) # ready paste_worker.signals.ready.connect(progress_dialog.setMaximum) # progress paste_worker.signals.progress.connect(progress_dialog.setValue) # progress message paste_worker.signals.progress_message.connect( progress_dialog.setLabelText) # finished paste_worker.signals.finished.connect(progress_dialog.reset) self.threadpool.start(paste_worker) # TODO: visual feedback for cut files def action_cut_event(self): """ Copies the current selection to the clipboard and marks them as cut. """ self.marked_to_cut, mime_data = utility.get_MIME( self.table_view.selectionModel().selectedIndexes()) self.clipboard.setMimeData(mime_data) def action_delete_event(self): """ Throws the current selection in the trash after asking for confirmation. """ path_list = utility.indexes_to_paths( self.table_view.selectionModel().selectedIndexes()) if len(path_list) < 0: return if (len(path_list) == 1): confirmation_msg = ('Do you want to throw "' + os.path.basename(path_list[0]) + '" in the trash?') else: confirmation_msg = ('Do you want to throw the ' + str(len(path_list)) + ' selected items and all the items contained' + ' in them in the trash?') dialog = utility.question_dialog(confirmation_msg) button_clicked = dialog.exec() if (button_clicked == QMessageBox.Yes): for item in path_list: send2trash(item) def action_rename_event(self): """ Prompts the user to enter a new name and changes the selected file's name. """ file_name = self.table_view.currentIndex().data() new_file_name, ok = QInputDialog().getText(self, 'Rename ' + file_name, 'New name:') if (ok): if (new_file_name): QFile().rename(os.path.join(self.current_path, file_name), os.path.join(self.current_path, new_file_name)) else: dialog = utility.message_dialog('Please enter the new name ' + 'for the file.', QMessageBox.Warning) dialog.exec() def action_show_hidden_event(self): """ Shows hidden files and dirs in the main view. """ if self.action_show_hidden.isChecked(): self.filesystem.setFilter(QDir.AllEntries | QDir.NoDotAndDotDot | QDir.AllDirs | QDir.Hidden) else: self.filesystem.setFilter(QDir.AllEntries | QDir.NoDotAndDotDot | QDir.AllDirs) def action_copy_path_event(self): """ Copy path of currently selected item to clipboard. """ path = os.path.join(self.current_path, self.table_view.currentIndex().siblingAtColumn(0) .data()) self.clipboard.setText(path) def action_add_to_bookmarks_event(self): """ Adds the selected dir to the bookmark by the name the user gave it via a dialog. """ path = os.path.join(self.current_path, self.table_view.currentIndex().data()) if (os.path.isdir(path)): name, ok = QInputDialog().getText(self, 'Create new bookmark', 'Bookmark name:', text=self.table_view. currentIndex().data()) if (ok): if (name): self.bookmarks.add_bookmark(name, path) self.bookmark_view.reset() else: dialog = utility.message_dialog('Please enter name for' + ' the bookmark.', QMessageBox.Warning) dialog.exec() else: dialog = utility.message_dialog('Please select a directory, not' + ' a file, to create a bookmark.', QMessageBox.Warning) dialog.exec() def action_remove_bookmark_event(self): """ Removes the selected bookmark after asking the user for confirmation. """ name = self.bookmark_view.currentIndex().data() dialog = utility.question_dialog('Do you really want to delete this ' + 'bookmark (' + name + ')?') button_clicked = dialog.exec() if (button_clicked == QMessageBox.Yes): self.bookmarks.remove_bookmark(name) self.bookmark_view.reset() def bookmark_selected_event(self): """ Changes the current dir to the one associated with the selected bookmark. """ next_path = self.bookmarks.get_path_from_name( self.bookmark_view.currentIndex().data()) self.update_current_path(next_path) def mount_selected_event(self): """ Checks the mount point of the selected drive and makes it the current path. """ try: device = self.mounts.get_device_at( self.mounts_view.currentIndex().row()) except IndexError: logging.critical('Device index out of range. This should never ' + 'happen, please report this error.') exit(1) mount_point = self.mounts.get_mount_point(device) if mount_point != '': self.update_current_path(mount_point) def action_mount_iso_event(self): """ """ iso_path = os.path.join(self.current_path, self.table_view.currentIndex() .siblingAtColumn(0).data()) self.mounts.mount_and_add_iso(iso_path) # TODO: handle performance better def mount_toggle_event(self): """ Tries to mount or unmount the selected drive. """ try: device = self.mounts.get_device_at( self.mounts_view.currentIndex().row()) except IndexError: logging.critical('Device index out of range. This should never ' + 'happen, please report this error.') exit(1) former_mount_point = self.mounts.get_mount_point(device) try: self.mounts.toggle_mount(device) except CalledProcessError: dialog = utility.message_dialog('udisks2 was unable to mount or' + ' unmount this devices. Please' + ' check, if it is installed' + ' correctly and report this' + ' error, if the problem' + ' presists.', QMessageBox.Critical) dialog.exec() return mount_point = self.mounts.get_mount_point(device) if mount_point != '': self.mount_selected_event() elif former_mount_point == self.current_path: self.update_current_path(self.default_path) # ---------------- functions ------------------------------------------- # def update_current_path(self, next_path: str, skip_stack=False, reset_forward_stack=True): """ Updates the current path to the value given in next_path. It also updates the UI accordingly and changes the forward an backward stacks, if needed. :param next_path: The path to the directory that should become the next dir. :type next_path: str :param skip_stack: This determines wether the stack handling should be skipped. Default value is False. :type skip_stack: bool :param reset_forward_stack: This determines wether the forward stack should be reset or not. Default is True :type reset_forward_stack: bool """ self.filesystem.setRootPath(next_path) self.table_view.setRootIndex( self.filesystem.index(next_path)) self.table_view.setCurrentIndex( self.filesystem.index(self.current_path)) self.adressbar.setText(next_path) self.part_info.setText(utility.part_info(next_path)) # disable up navigation if in fs root if (next_path == QDir().rootPath()): self.action_up.setEnabled(False) else: self.action_up.setEnabled(True) # handle back stack if (not skip_stack): if (self.back_stack.empty() or self.back_stack.top() != self.current_path): self.back_stack.push(self.current_path) # reenable back navigation self.action_back.setEnabled(True) if (reset_forward_stack): self.forward_stack.drop() self.action_forward.setEnabled(False) self.current_path = next_path def devices_changed(self, action: str, device: Device): """ Updates the mountable devices. Is run, when udev detects a change in the block device subsystem. :param action: Action that happened to the block device. :type action: str :param device: Device that changed it's status. :type device: Device """ if action == 'add': self.mounts.add(device) elif action == 'remove': self.mounts.remove(device) self.mounts.layoutChanged.emit()
class ProgressWindow(QDialog): # Signal that emits when loading has completed signal_loaded = QtCore.Signal(tuple) # Signal that emits when exceptions are raised signal_error = QtCore.Signal(Exception) # Signal that emits when calc dvh is advised signal_advise_calc_dvh = QtCore.Signal(bool) def __init__(self, *args, **kwargs): super(ProgressWindow, self).__init__(*args, **kwargs) # Setting up progress bar self.progress_bar = QtWidgets.QProgressBar() self.progress_bar.setGeometry(10, 50, 230, 20) self.progress_bar.setMaximum(100) self.setWindowTitle("Loading") self.setFixedSize(248, 80) self.text_field = QLabel("Loading") if platform.system() == 'Darwin': self.stylesheet_path = "res/stylesheet.qss" else: self.stylesheet_path = "res/stylesheet-win-linux.qss" self.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.setWindowIcon(window_icon) self.progress_bar.setStyleSheet(self.stylesheet) self.layout = QVBoxLayout() self.layout.addWidget(self.text_field) self.layout.addWidget(self.progress_bar) self.setLayout(self.layout) self.threadpool = QThreadPool() self.interrupt_flag = threading.Event() def start(self, funct): """ Function that executes 'funct' on new thread :param funct: function to execute. :param progress_callback: signal that receives the current progress of the loading. """ self.interrupt_flag.clear() worker = Worker(funct, self.interrupt_flag, progress_callback=True) worker.signals.result.connect(self.on_finish) worker.signals.error.connect(self.on_error) worker.signals.progress.connect(self.update_progress) self.threadpool.start(worker) self.exec_() def on_finish(self, result): """ Executes when the progress bar has finished """ self.signal_loaded.emit((result, self)) def update_progress(self, progress_update): """ Function responsible for updating the bar percentage and the label. :param progress_update: A tuple containing update text and update percentage """ self.text_field.setText(progress_update[0]) self.progress_bar.setValue(progress_update[1]) def on_error(self, err): """ Executes if an error occurred """ self.signal_error.emit(err) def closeEvent(self, event: QtGui.QCloseEvent) -> None: """ Override base closeEvent method """ self.interrupt_flag.set()
class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Hooty Light Client") self.program_started = False self.log_text_box = QPlainTextEdit(self) self.log_text_box.setReadOnly(True) self.url = self.get_url_config() self.edit_url = QLineEdit(self.url) self.run_button = QPushButton("Run Hooty!") widgy = QWidget() layout = QVBoxLayout() widgy.setLayout(layout) layout.addWidget(self.log_text_box) layout.addWidget(self.edit_url) layout.addWidget(self.run_button) self.setLayout(layout) self.setCentralWidget(widgy) self.thread_pool = QThreadPool() # Create a runner self.runner = runner.JobRunner(self.edit_url.text()) self.thread_pool.start(self.runner) self.runner.logHandler.log.signal.connect(self.write_log) # Run some actions when we press the hooty button! self.run_button.pressed.connect( lambda: self.runner.set_url(self.edit_url.text())) self.run_button.pressed.connect( lambda: self.write_url_config(self.edit_url.text())) self.run_button.pressed.connect(self.runner.clicked) self.run_button.pressed.connect(self.hooty_button_text) self.show() # Get the url from the config file if it is there def get_url_config(self): config = configparser.ConfigParser() if path.exists("hooty.ini"): try: config.read("hooty.ini") return config['DEFAULT']['url'] except: return "url.example" else: return "url.example" # Write your url to the file! # TODO: Maybe I should only write if the url is a valid working one.. def write_url_config(self, url): config = configparser.ConfigParser() config['DEFAULT'] = {'url': url} with open('hooty.ini', 'w') as configfile: config.write(configfile) # Change some text! # TODO: Something something variables def hooty_button_text(self): if self.run_button.text() == "Run Hooty!": self.run_button.setText("Hooty is running!") else: self.run_button.setText("Run Hooty!") # First time we run our actual worker thread # No reason to run it immediately def run_thread(self): # Thread runner if self.program_started is False: self.thread_pool.start(self.runner) # Make sure we actually kill the worker def closeEvent(self, event): self.runner.exit() # writing to our log & scroll to the bottom @Slot(str) def write_log(self, log_text): self.log_text_box.appendPlainText(log_text) self.log_text_box.centerCursor()
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 MainWidget(QWidget): def __init__(self, base): QWidget.__init__(self) self.base = base self.setWindowIcon(QIcon('icon.ico')) self.setWindowTitle(TITLE) self.set_background_color(QColor(255, 255, 255)) self.process_header_widget = QWidget() self.process_header_layout = QHBoxLayout(self.process_header_widget) self.process_header_layout.setContentsMargins(0, 0, 0, 0) self.process_label = QLabel('Available processes:') self.github_label = QLabel( '<a href="https://github.com/darktohka/p3dephaser">GitHub</a>') self.github_label.setOpenExternalLinks(True) self.refresh_button = QPushButton('Refresh') self.refresh_button.clicked.connect(self.refresh_processes) self.refresh_button.setFixedSize(100, 23) self.multifile_widget = QWidget() self.multifile_layout = QHBoxLayout(self.multifile_widget) self.multifile_layout.setContentsMargins(0, 0, 0, 0) self.multifileLabel = QLabel('Requested multifile names:') self.multifileBox = QLineEdit(self) self.multifileBox.returnPressed.connect(self.begin_scan) self.multifile_layout.addWidget(self.multifileLabel) self.multifile_layout.addWidget(self.multifileBox) self.scan_button = QPushButton('Scan') self.scan_button.clicked.connect(self.begin_scan) self.process_list_box = QListWidget() self.process_header_layout.addWidget(self.process_label) self.process_header_layout.addStretch(1) self.process_header_layout.addWidget(self.github_label) self.process_header_layout.addWidget(self.refresh_button) self.result_table = QTableWidget() self.result_table.setColumnCount(3) self.result_table.horizontalHeader().setStretchLastSection(True) self.result_table.horizontalHeader().setSectionResizeMode( QHeaderView.ResizeMode.Stretch) for i, header in enumerate(('Process', 'Multifile', 'Password')): self.result_table.setHorizontalHeaderItem(i, QTableWidgetItem(header)) self.base_layout = QVBoxLayout(self) self.base_layout.setContentsMargins(15, 15, 15, 15) self.base_layout.addWidget(self.process_header_widget) self.base_layout.addWidget(self.process_list_box) self.base_layout.addWidget(self.multifile_widget) self.base_layout.addWidget(self.scan_button) self.base_layout.addWidget(self.result_table) self.refresh_processes() self.thread_pool = QThreadPool() self.worker = None self.process_name = None self.stop_event = threading.Event() def set_background_color(self, color): self.setAutoFillBackground(True) palette = self.palette() palette.setColor(self.backgroundRole(), color) self.setPalette(palette) def get_processes(self): processes = [] for proc in psutil.process_iter(): processes.append(proc.as_dict(attrs=['pid', 'name'])) processes.sort( key=lambda process: (process['name'].lower(), process['pid'])) return processes def refresh_processes(self): self.process_list_box.clear() processes = self.get_processes() for process in processes: name = process['name'] pid = process['pid'] self.process_list_box.addItem(f'{name} (PID {pid})') def begin_scan(self): if self.worker: self.stop_event.set() self.scan_button.setEnabled(False) return items = self.process_list_box.selectedItems() if not items: QMessageBox.warning(self, TITLE, 'Please choose a process from the list!') return process = items[0].text()[:-1].split(' ') self.process_name = ' '.join(process[:-2]) pid = int(process[-1]) multifiles = self.multifileBox.text().split() if not multifiles: QMessageBox.warning(self, TITLE, 'Please choose some multifiles to target!') return multifile_names = '\n'.join( [f'- {multifile}' for multifile in multifiles]) question = f'Do you really want to scan {self.process_name} for the following multifiles?\n\n{multifile_names}' if QMessageBox.question( self, TITLE, question, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) != QMessageBox.StandardButton.Yes: return self.count = 0 self.setWindowTitle(f'{TITLE} - Scanning...') self.scan_button.setText('Stop') self.worker = ScanWorker(self, pid, multifiles) self.worker.signals.finished.connect(self.scan_over) self.worker.signals.error.connect(self.error_occurred) self.worker.signals.progress.connect(self.report_progress) self.thread_pool.start(self.worker) def scan_over(self): self.worker = None self.stop_event.clear() self.scan_button.setText('Scan') self.scan_button.setEnabled(True) self.setWindowTitle(TITLE) QMessageBox.information( self, TITLE, f'Scan complete!\n\n{self.count} password{"s have" if self.count != 1 else " has"} been found.' ) def error_occurred(self, error): exc, value, message = error QMessageBox.critical( self, TITLE, f'An error has occurred while trying to scan this process!\n\n{exc} {value}\n\n{message}' ) def report_progress(self, multifile, password): self.count += 1 index = self.result_table.rowCount() self.result_table.insertRow(index) for i, value in enumerate((self.process_name, multifile, password)): self.result_table.setItem(index, i, QTableWidgetItem(value))
def run(self): pool = QThreadPool() for new_thread in self.new_thread_list: pool.start(new_thread) pool.waitForDone()
class MainWindow(PreviewWindow): """ Extends PreviewWindow. The first window that is launched when the program starts. """ def __init__(self): super(MainWindow, self).__init__(None) self.ui = Ui_MainWindow() self.ui.setupUi(self) self.thread_pool = QThreadPool() self.input_directory = "" self.output_directory = "" self.merger_directory: Optional[str] = None self.editor_window: Optional[ClusterEditor] = None self.__generated_images_entries: List[ClusterImageEntry] = [] self.ui.buttonInputDir.clicked.connect(self.load_input_directory) self.ui.buttonOutputDir.clicked.connect(self.load_output_directory) self.ui.buttonGenerate.clicked.connect(self.generate_handler) self.ui.buttonCheckUncheck.clicked.connect(self.select_deselect) self.ui.buttonClearGenerated.clicked.connect(self.clear_generated) def source_layout(self) -> QLayout: return self.ui.scrollAreaWidgetContentsSrc.layout() def image_preview(self) -> QLabel: return self.ui.imagePreview def clear_image_entries(self) -> None: super(MainWindow, self).clear_image_entries() for ime in self.__generated_images_entries: ime.close() self.__generated_images_entries.clear() @Slot(ClusterImageEntry) def open_cluster_editor(self, calling_image_entry: ClusterImageEntry) -> None: """ Open a cluster editor with the given ClusteringImageEntry. :param ClusterImageEntry calling_image_entry: The cluster image entry which contains all layers information. """ if self.editor_window is not None and self.editor_window.isVisible(): self.editor_window.activateWindow() return self.editor_window = ClusterEditor(self, calling_image_entry) self.editor_window.applied_to_all.connect(self.merge_layers) self.editor_window.show() @Slot(list) def merge_layers(self, layers_indices: List[int]) -> None: """ Merge all the specified layers :param layers_indices: A range of the layers to merge :type layers_indices: list of int """ if len(layers_indices) == 0: return merged_cluster_ime: List[ClusterImageEntry] = [] first = True while len(self.__generated_images_entries) > 0: ime: ClusterImageEntry = self.__generated_images_entries.pop(0) merged: Optional[np.ndarray] = None parent_layers: List[int] = [] for i in layers_indices: layer_data: LayerData = ime.get_layer_data(i) if layer_data.is_merger: assert layer_data.parent_layers is not None parent_layers.extend(layer_data.parent_layers) else: assert layer_data.layer_index is not None parent_layers.append(layer_data.layer_index) layer = np.load(layer_data.array_path) merged = layer if merged is None else merged | layer for i in sorted(layers_indices, reverse=True): ime.remove_layer_data(i) if merged is None: break colored = apply_colormap(merged) merged_path_no_ext = os.path.join( self.merger_directory, f"{ime.basename}_layers_{LayerData.indices2str(parent_layers)}" ) merged_image_path = f"{merged_path_no_ext}.png" merged_array_path = f"{merged_path_no_ext}.npy" cv.imwrite(merged_image_path, colored) np.save(merged_array_path, merged) ime.add_layer_data( LayerData(merged_image_path, merged_array_path, is_merger=True, parent_layers=parent_layers)) new_cluster_array = create_cluster([ np.load(ime.get_layer_data(i).array_path) for i in range(ime.layer_count()) ]) new_cluster_colored = apply_colormap(new_cluster_array, cv.COLORMAP_JET) new_cluster_colored_image = array3d_to_pixmap( new_cluster_colored).toImage() new_cluster_path_no_ext = os.path.join(self.merger_directory, f"{ime.basename}_cluster") new_cluster_image_path = f"{new_cluster_path_no_ext}.png" new_cluster_array_path = f"{new_cluster_path_no_ext}.npy" np.save(new_cluster_array_path, new_cluster_array) cv.imwrite(new_cluster_image_path, new_cluster_colored) new_ime = ClusterImageEntry(ime.parent(), new_cluster_colored_image, ime.basename, new_cluster_image_path, new_cluster_array_path, ime.layers_data) new_ime.mouse_pressed.connect(self.image_entry_click_handler) new_ime.double_clicked.connect(self.open_cluster_editor) merged_cluster_ime.append(new_ime) ime.close() self.ui.scrollAreaWidgetContentsDst.layout().addWidget(new_ime) if first: self.set_preview_image(new_cluster_colored_image, new_ime) first = False self.__generated_images_entries = merged_cluster_ime # Need to be implemented if the unmerge feature is needed__mouse_pressed_handlers.clear() def unmerge_layer(self) -> None: raise NotImplementedError @Slot() def clear_generated(self) -> None: """ Clear the generated cluster area. """ for ime in self.__generated_images_entries: ime.close() self.__generated_images_entries.clear() self.image_preview().setText("Preview") self._selected_image_entry = None @Slot() def load_input_directory(self) -> None: """ Open a popup to select the directory where is located source images. """ in_dir = QFileDialog.getExistingDirectory(self) if len(in_dir) == 0: return self.input_directory = in_dir self.clear_image_entries() self.clear_preview_image() # self.ui.labelInDir.setText(f"Loaded: {self.input_directory}") src_layout = self.source_layout() first = True for entry in sorted(os.scandir(self.input_directory), key=lambda x: x.name): if not Tat.is_image(entry.path): continue qim = load_image(entry.path) ime = CheckableImageEntry(src_layout.parent(), qim, entry.name, entry.path) ime.mouse_pressed.connect(self.image_entry_click_handler) self.add_source_image_entry(ime) if first: self.set_preview_image(qim, ime) first = False if len(self.output_directory) != 0: self.ui.buttonGenerate.setEnabled(True) @Slot() def load_output_directory(self) -> None: """ Open a popup to select the directory where to generate the clusters and layers. """ out_dir = QFileDialog.getExistingDirectory(self) if len(out_dir) == 0: return self.output_directory = out_dir self.merger_directory = os.path.join(self.output_directory, "merged") Path(self.merger_directory).mkdir(exist_ok=True) # self.ui.labelOutDir.setText(f"Loaded: {self.output_directory}") if len(self.input_directory) != 0: self.ui.buttonGenerate.setEnabled(True) @Slot() def generate_handler(self) -> None: """ When the generate button is clicked, starts the clustering in background and shows the progress bar. """ progress_window = ProgressWindow() self.ui.buttonGenerate.setEnabled(False) self.setDisabled(True) @Slot(int, tuple) def add_cluster_image( progress: int, data: Tuple[str, str, str, List[LayerData]]) -> None: image_path, array_path, name, layers_data = data container: QLayout = self.ui.scrollAreaWidgetContentsDst.layout() ime = ClusterImageEntry(container.parent(), load_image(image_path), name, image_path, array_path, layers_data) ime.mouse_pressed.connect(self.image_entry_click_handler) ime.double_clicked.connect(self.open_cluster_editor) container.addWidget(ime) if len(self.__generated_images_entries) == 0: self.set_preview_image(load_image(ime.image_path), ime) progress_window.progress_bar().setValue(progress) self.__generated_images_entries.append(ime) selected_entries = self.get_selected_entries() worker = ClusteringWorker(selected_entries, self.ui.clusterCount.value(), self.ui.runCount.value(), self.ui.maxIterCount.value(), self.output_directory) @Slot() def finished_generating() -> None: progress_window.close() worker.signals.progress.disconnect(add_cluster_image) worker.signals.finished.disconnect(finished_generating) self.setEnabled(True) self.ui.buttonGenerate.setEnabled(True) worker.signals.progress.connect(add_cluster_image) worker.signals.finished.connect(finished_generating) progress_window.cancelled.connect(worker.interrupt) progress_window.progress_bar().setMaximum(len(selected_entries)) progress_window.show() self.thread_pool.start(worker)