Exemplo n.º 1
0
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)
Exemplo n.º 2
0
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)
Exemplo n.º 3
0
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)
Exemplo n.º 4
0
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'])
Exemplo n.º 5
0
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)
Exemplo n.º 6
0
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
Exemplo n.º 8
0
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
Exemplo n.º 9
0
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()
Exemplo n.º 10
0
Arquivo: tfm.py Projeto: tmahlburg/tfm
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()
Exemplo n.º 11
0
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()
Exemplo n.º 12
0
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()
Exemplo n.º 13
0
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()
Exemplo n.º 14
0
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))
Exemplo n.º 15
0
 def run(self):
     pool = QThreadPool()
     for new_thread in self.new_thread_list:
         pool.start(new_thread)
     pool.waitForDone()
Exemplo n.º 16
0
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)