示例#1
0
class MainWindow(QMainWindow):
    ################################
    # signals (aset ui --> aset api)
    ################################
    create_document_base = pyqtSignal(str, list)
    add_attribute = pyqtSignal(str, ASETDocumentBase)
    remove_attribute = pyqtSignal(str, ASETDocumentBase)
    forget_matches_for_attribute = pyqtSignal(str, ASETDocumentBase)
    load_document_base_from_bson = pyqtSignal(str)
    save_document_base_to_bson = pyqtSignal(str, ASETDocumentBase)
    save_table_to_csv = pyqtSignal(str, ASETDocumentBase)
    forget_matches = pyqtSignal(ASETDocumentBase)
    load_preprocessing_phase_from_config = pyqtSignal(str)
    save_preprocessing_phase_to_config = pyqtSignal(str, PreprocessingPhase)
    load_matching_phase_from_config = pyqtSignal(str)
    save_matching_phase_to_config = pyqtSignal(str, BaseMatchingPhase)
    run_preprocessing_phase = pyqtSignal(ASETDocumentBase, PreprocessingPhase, Statistics)
    run_matching_phase = pyqtSignal(ASETDocumentBase, BaseMatchingPhase, Statistics)
    save_statistics_to_json = pyqtSignal(str, Statistics)
    load_and_run_default_preprocessing_phase = pyqtSignal(ASETDocumentBase, Statistics)
    load_and_run_default_matching_phase = pyqtSignal(ASETDocumentBase, Statistics)

    ##############################
    # slots (aset api --> aset ui)
    ##############################
    @pyqtSlot(str, float)
    def status(self, message, progress):
        logger.debug("Called slot 'status'.")

        self.status_widget_message.setText(message)
        if progress == -1:
            self.status_widget_progress.setRange(0, 0)
        else:
            self.status_widget_progress.setRange(0, 100)
            self.status_widget_progress.setValue(int(progress * 100))

    @pyqtSlot(str)
    def finished(self, message):
        logger.debug("Called slot 'finished'.")

        self.status_widget_message.setText(message)
        self.status_widget_progress.setRange(0, 100)
        self.status_widget_progress.setValue(100)

        self.show_document_base_viewer_widget()
        self.enable_global_input()

    @pyqtSlot(str)
    def error(self, message):
        logger.debug("Called slot 'error'.")

        self.status_widget_message.setText(message)
        self.status_widget_progress.setRange(0, 100)
        self.status_widget_progress.setValue(0)

        self.show_document_base_viewer_widget()
        self.enable_global_input()

    @pyqtSlot(ASETDocumentBase)
    def document_base_to_ui(self, document_base):
        logger.debug("Called slot 'document_base_to_ui'.")

        self.document_base = document_base
        self.document_base_viewer_widget.update_document_base(self.document_base)

        self._was_enabled.append(self.save_document_base_to_bson_action)
        self._was_enabled.append(self.save_table_to_csv_action)
        self._was_enabled.append(self.add_attribute_action)
        self._was_enabled.append(self.remove_attribute_action)
        self._was_enabled.append(self.forget_matches_for_attribute_action)
        self._was_enabled.append(self.forget_matches_action)
        self._was_enabled.append(self.load_and_run_default_preprocessing_phase_action)
        self._was_enabled.append(self.load_and_run_default_matching_phase_action)
        if self.preprocessing_phase is not None:
            self._was_enabled.append(self.run_preprocessing_phase_action)
        if self.matching_phase is not None:
            self._was_enabled.append(self.run_matching_phase_action)

    @pyqtSlot(PreprocessingPhase)
    def preprocessing_phase_to_ui(self, preprocessing_phase):
        logger.debug("Called slot 'preprocessing_phase_to_ui'.")

        self.preprocessing_phase = preprocessing_phase

        self._was_enabled.append(self.save_preprocessing_phase_to_config_action)
        if self.document_base is not None:
            self._was_enabled.append(self.run_preprocessing_phase_action)

    @pyqtSlot(BaseMatchingPhase)
    def matching_phase_to_ui(self, matching_phase):
        logger.debug("Called slot 'matching_phase_to_ui'.")

        self.matching_phase = matching_phase

        self._was_enabled.append(self.save_matching_phase_to_config_action)
        if self.document_base is not None:
            self._was_enabled.append(self.run_preprocessing_phase_action)

    @pyqtSlot(Statistics)
    def statistics_to_ui(self, statistics):
        logger.debug("Called slot 'statistics_to_ui'.")

        self.statistics = statistics

    @pyqtSlot(dict)
    def feedback_request_to_ui(self, feedback_request):
        logger.debug("Called slot 'feedback_request_to_ui'.")

        self.interactive_matching_widget.handle_feedback_request(feedback_request)

    # noinspection PyUnresolvedReferences
    def _connect_slots_and_signals(self):
        self.create_document_base.connect(self.api.create_document_base)
        self.add_attribute.connect(self.api.add_attribute)
        self.remove_attribute.connect(self.api.remove_attribute)
        self.forget_matches_for_attribute.connect(self.api.forget_matches_for_attribute)
        self.load_document_base_from_bson.connect(self.api.load_document_base_from_bson)
        self.save_document_base_to_bson.connect(self.api.save_document_base_to_bson)
        self.save_table_to_csv.connect(self.api.save_table_to_csv)
        self.forget_matches.connect(self.api.forget_matches)
        self.load_preprocessing_phase_from_config.connect(self.api.load_preprocessing_phase_from_config)
        self.save_preprocessing_phase_to_config.connect(self.api.save_preprocessing_phase_to_config)
        self.load_matching_phase_from_config.connect(self.api.load_matching_phase_from_config)
        self.save_matching_phase_to_config.connect(self.api.save_matching_phase_to_config)
        self.run_preprocessing_phase.connect(self.api.run_preprocessing_phase)
        self.run_matching_phase.connect(self.api.run_matching_phase)
        self.save_statistics_to_json.connect(self.api.save_statistics_to_json)
        self.load_and_run_default_preprocessing_phase.connect(self.api.load_and_run_default_preprocessing_phase)
        self.load_and_run_default_matching_phase.connect(self.api.load_and_run_default_matching_phase)

        self.api.status.connect(self.status)
        self.api.finished.connect(self.finished)
        self.api.error.connect(self.error)
        self.api.document_base_to_ui.connect(self.document_base_to_ui)
        self.api.preprocessing_phase_to_ui.connect(self.preprocessing_phase_to_ui)
        self.api.matching_phase_to_ui.connect(self.matching_phase_to_ui)
        self.api.statistics_to_ui.connect(self.statistics_to_ui)
        self.api.feedback_request_to_ui.connect(self.feedback_request_to_ui)

    #######
    # tasks
    #######
    def load_document_base_from_bson_task(self):
        logger.info("Execute task 'load_document_base_from_bson_task'.")

        path, ok = QFileDialog.getOpenFileName(self, "Choose a document collection .bson file!")
        if ok:
            self.disable_global_input()
            # noinspection PyUnresolvedReferences
            self.load_document_base_from_bson.emit(str(path))

    def save_document_base_to_bson_task(self):
        logger.info("Execute task 'save_document_base_to_bson_task'.")

        if self.document_base is not None:
            path, ok = QFileDialog.getSaveFileName(self, "Choose where to save the document collection .bson file!")
            if ok:
                self.disable_global_input()
                # noinspection PyUnresolvedReferences
                self.save_document_base_to_bson.emit(str(path), self.document_base)

    def add_attribute_task(self):
        logger.info("Execute task 'add_attribute_task'.")

        if self.document_base is not None:
            name, ok = QInputDialog.getText(self, "Create Attribute", "Attribute name:")
            if ok:
                self.disable_global_input()
                # noinspection PyUnresolvedReferences
                self.add_attribute.emit(str(name), self.document_base)

    def remove_attribute_task(self):
        logger.info("Execute task 'remove_attribute_task'.")

        if self.document_base is not None:
            name, ok = QInputDialog.getText(self, "Remove Attribute", "Attribute name:")
            if ok:
                self.disable_global_input()
                # noinspection PyUnresolvedReferences
                self.remove_attribute.emit(str(name), self.document_base)

    def remove_attribute_with_given_name_task(self, attribute_name):
        logger.info("Execute task 'remove_attribute_with_given_name_task'.")

        if self.document_base is not None:
            self.disable_global_input()
            # noinspection PyUnresolvedReferences
            self.remove_attribute.emit(str(attribute_name), self.document_base)

    def forget_matches_for_attribute_task(self):
        logger.info("Execute task 'forget_matches_for_attribute_task'.")

        if self.document_base is not None:
            name, ok = QInputDialog.getText(self, "Forget Matches for Attribute", "Attribute name:")
            if ok:
                self.disable_global_input()
                # noinspection PyUnresolvedReferences
                self.forget_matches_for_attribute.emit(str(name), self.document_base)

    def forget_matches_for_attribute_with_given_name_task(self, attribute_name):
        logger.info("Execute task 'forget_matches_for_attribute_with_given_name_task'.")

        if self.document_base is not None:
            self.disable_global_input()
            # noinspection PyUnresolvedReferences
            self.forget_matches_for_attribute.emit(attribute_name, self.document_base)

    def forget_matches_task(self):
        logger.info("Execute task 'forget_matches_task'.")

        if self.document_base is not None:
            self.disable_global_input()
            # noinspection PyUnresolvedReferences
            self.forget_matches.emit(self.document_base)

    def enable_collect_statistics_task(self):
        logger.info("Execute task 'task_enable_collect_statistics'.")

        self.collect_statistics = True
        self.enable_collect_statistics_action.setEnabled(False)
        self.disable_collect_statistics_action.setEnabled(True)

    def disable_collect_statistics_task(self):
        logger.info("Execute task 'disable_collect_statistics_task'.")

        self.collect_statistics = False
        self.disable_collect_statistics_action.setEnabled(False)
        self.enable_collect_statistics_action.setEnabled(True)

    def save_statistics_to_json_task(self):
        logger.info("Execute task 'save_statistics_to_json_task'.")

        if self.statistics is not None:
            path = str(QFileDialog.getSaveFileName(self, "Choose where to save the statistics .json file!")[0])
            if path != "":
                self.disable_global_input()
                # noinspection PyUnresolvedReferences
                self.save_statistics_to_json.emit(path, self.statistics)

    def show_document_base_creator_widget_task(self):
        logger.info("Execute task 'show_document_base_creator_widget_task'.")

        self.disable_global_input()
        self.document_base_creator_widget.enable_input()
        self.document_base_creator_widget.initialize_for_new_document_base()
        self.show_document_base_creator_widget()
        self.document_base_creator_widget.path.setFocus()

    def create_document_base_task(self, path, attribute_names):
        logger.info("Execute task 'create_document_base_task'.")

        self.disable_global_input()
        # noinspection PyUnresolvedReferences
        self.create_document_base.emit(path, attribute_names)

    def save_table_to_csv_task(self):
        logger.info("Execute task 'save_table_to_csv_task'.")

        if self.document_base is not None:
            path = str(QFileDialog.getSaveFileName(self, "Choose where to save the table .csv file!")[0])
            if path != "":
                self.disable_global_input()
                # noinspection PyUnresolvedReferences
                self.save_table_to_csv.emit(path, self.document_base)

    def load_preprocessing_phase_from_config_task(self):
        logger.info("Execute task 'load_preprocessing_phase_from_config_task'.")

        path = str(QFileDialog.getOpenFileName(self, "Choose a configuration .json file!")[0])
        if path != "":
            self.disable_global_input()
            # noinspection PyUnresolvedReferences
            self.load_preprocessing_phase_from_config.emit(path)

    def save_preprocessing_phase_to_config_task(self):
        logger.info("Execute task 'save_preprocessing_phase_to_config_task'.")

        if self.preprocessing_phase is not None:
            path = str(QFileDialog.getSaveFileName(self, "Choose where to save the configuration .json file!")[0])
            if path != "":
                self.disable_global_input()
                # noinspection PyUnresolvedReferences
                self.save_preprocessing_phase_to_config.emit(path, self.preprocessing_phase)

    def load_matching_phase_from_config_task(self):
        logger.info("Execute task 'load_matching_phase_from_config_task'.")

        path = str(QFileDialog.getOpenFileName(self, "Choose a configuration .json file!")[0])
        if path != "":
            self.disable_global_input()
            # noinspection PyUnresolvedReferences
            self.load_matching_phase_from_config.emit(path)

    def save_matching_phase_to_config_task(self):
        logger.info("Execute task 'save_matching_phase_to_config_task'.")

        if self.matching_phase is not None:
            path = str(QFileDialog.getSaveFileName(self, "Choose where to save the configuration .json file!")[0])
            if path != "":
                self.disable_global_input()
                # noinspection PyUnresolvedReferences
                self.save_matching_phase_to_config.emit(path, self.matching_phase)

    def run_preprocessing_phase_task(self):
        logger.info("Execute task 'run_preprocessing_phase_task'.")

        if self.document_base is not None and self.preprocessing_phase is not None:
            self.statistics = Statistics(self.collect_statistics)
            self.save_statistics_to_json_action.setEnabled(self.collect_statistics)

            self.disable_global_input()

            # noinspection PyUnresolvedReferences
            self.run_preprocessing_phase.emit(self.document_base, self.preprocessing_phase, self.statistics)

    def run_matching_phase_task(self):
        logger.info("Execute task 'run_matching_phase_task'.")

        if self.document_base is not None and self.matching_phase is not None:
            self.statistics = Statistics(self.collect_statistics)
            self.save_statistics_to_json_action.setEnabled(self.collect_statistics)

            self.disable_global_input()
            self.interactive_matching_widget.enable_input()
            self.show_interactive_matching_widget()

            # noinspection PyUnresolvedReferences
            self.run_matching_phase.emit(self.document_base, self.matching_phase, self.statistics)

    def give_feedback_task(self, feedback):
        logger.info("Execute task 'give_feedback_task'.")

        self.api.feedback = feedback
        self.feedback_cond.wakeAll()

    def load_and_run_default_preprocessing_phase_task(self):
        logger.info("Execute task 'load_and_run_default_preprocessing_phase_task'.")

        if self.document_base is not None:
            self.statistics = Statistics(self.collect_statistics)
            self.save_statistics_to_json_action.setEnabled(self.collect_statistics)

            self.disable_global_input()

            # noinspection PyUnresolvedReferences
            self.load_and_run_default_preprocessing_phase.emit(self.document_base, self.statistics)

    def load_and_run_default_matching_phase_task(self):
        logger.info("Execute task 'load_and_run_default_matching_phase_task'.")

        if self.document_base is not None:
            self.statistics = Statistics(self.collect_statistics)
            self.save_statistics_to_json_action.setEnabled(self.collect_statistics)

            self.disable_global_input()
            self.interactive_matching_widget.enable_input()
            self.show_interactive_matching_widget()

            # noinspection PyUnresolvedReferences
            self.load_and_run_default_matching_phase.emit(self.document_base, self.statistics)

    ##################
    # controller logic
    ##################
    def enable_global_input(self):
        for action in self._was_enabled:
            action.setEnabled(True)

        self.document_base_creator_widget.enable_input()
        self.document_base_viewer_widget.enable_input()
        self.interactive_matching_widget.enable_input()
        self._was_enabled = []

    def disable_global_input(self):
        for action in self._all_actions:
            if action.isEnabled():
                self._was_enabled.append(action)
            action.setEnabled(False)

        self.document_base_creator_widget.disable_input()
        self.document_base_viewer_widget.disable_input()
        self.interactive_matching_widget.disable_input()

    def show_document_base_viewer_widget(self):
        if self.document_base_viewer_widget.isHidden():
            self.central_widget_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
            self.start_menu_widget.hide()
            self.interactive_matching_widget.hide()
            self.document_base_creator_widget.hide()

            self.central_widget_layout.removeWidget(self.start_menu_widget)
            self.central_widget_layout.removeWidget(self.interactive_matching_widget)
            self.central_widget_layout.removeWidget(self.document_base_creator_widget)
            self.central_widget_layout.addWidget(self.document_base_viewer_widget)
            self.document_base_viewer_widget.show()
            self.central_widget_layout.update()

    def show_interactive_matching_widget(self):
        if self.interactive_matching_widget.isHidden():
            self.central_widget_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
            self.start_menu_widget.hide()
            self.document_base_viewer_widget.hide()
            self.document_base_creator_widget.hide()

            self.central_widget_layout.removeWidget(self.start_menu_widget)
            self.central_widget_layout.removeWidget(self.document_base_viewer_widget)
            self.central_widget_layout.removeWidget(self.document_base_creator_widget)
            self.central_widget_layout.addWidget(self.interactive_matching_widget)
            self.interactive_matching_widget.show()
            self.central_widget_layout.update()

    def show_document_base_creator_widget(self):
        if self.document_base_creator_widget.isHidden():
            self.central_widget_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
            self.start_menu_widget.hide()
            self.document_base_viewer_widget.hide()
            self.interactive_matching_widget.hide()

            self.central_widget_layout.removeWidget(self.start_menu_widget)
            self.central_widget_layout.removeWidget(self.document_base_viewer_widget)
            self.central_widget_layout.removeWidget(self.interactive_matching_widget)
            self.central_widget_layout.addWidget(self.document_base_creator_widget)
            self.document_base_creator_widget.show()
            self.central_widget_layout.update()

    def show_start_menu_widget(self):
        if self.start_menu_widget.isHidden():
            self.central_widget_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
            self.document_base_viewer_widget.hide()
            self.document_base_creator_widget.hide()
            self.interactive_matching_widget.hide()

            self.central_widget_layout.removeWidget(self.document_base_viewer_widget)
            self.central_widget_layout.removeWidget(self.document_base_creator_widget)
            self.central_widget_layout.removeWidget(self.interactive_matching_widget)
            self.central_widget_layout.addWidget(self.start_menu_widget)
            self.start_menu_widget.show()
            self.central_widget_layout.update()

    def __init__(self) -> None:
        super(MainWindow, self).__init__()
        self.setWindowTitle("ASET")

        self.document_base = None
        self.preprocessing_phase = None
        self.matching_phase = None
        self.statistics = Statistics(True)
        self.collect_statistics = True

        # set up the api_thread and api and connect slots and signals
        self.feedback_mutex = QMutex()
        self.feedback_cond = QWaitCondition()
        self.api = ASETAPI(self.feedback_mutex, self.feedback_cond)
        self.api_thread = QThread()
        self.api.moveToThread(self.api_thread)
        self._connect_slots_and_signals()
        self.api_thread.start()

        # set up the status bar
        self.status_bar = self.statusBar()
        self.status_bar.setFont(STATUS_BAR_FONT)

        self.status_widget = QWidget(self.status_bar)
        self.status_widget_layout = QHBoxLayout(self.status_widget)
        self.status_widget_layout.setContentsMargins(0, 0, 0, 0)
        self.status_widget_message = QLabel()
        self.status_widget_message.setFont(STATUS_BAR_FONT)

        self.status_widget_message.setMinimumWidth(10)
        self.status_widget_layout.addWidget(self.status_widget_message)
        self.status_widget_progress = QProgressBar()
        self.status_widget_progress.setMinimumWidth(10)
        self.status_widget_progress.setMaximumWidth(200)
        self.status_widget_progress.setTextVisible(False)
        self.status_widget_progress.setMaximumHeight(20)
        self.status_widget_layout.addWidget(self.status_widget_progress)
        self.status_bar.addPermanentWidget(self.status_widget)

        # set up the actions
        self._all_actions = []
        self._was_enabled = []

        self.exit_action = QAction("&Exit", self)
        self.exit_action.setIcon(QIcon("aset_ui/resources/leave.svg"))
        self.exit_action.setStatusTip("Exit the application.")
        self.exit_action.triggered.connect(QApplication.instance().quit)
        self._all_actions.append(self.exit_action)

        self.show_document_base_creator_widget_action = QAction("&Create new document base", self)
        self.show_document_base_creator_widget_action.setIcon(QIcon("aset_ui/resources/two_documents.svg"))
        self.show_document_base_creator_widget_action.setStatusTip(
            "Create a new document base from a collection of .txt files and a list of attribute names."
        )
        self.show_document_base_creator_widget_action.triggered.connect(self.show_document_base_creator_widget_task)
        self._all_actions.append(self.show_document_base_creator_widget_action)

        self.add_attribute_action = QAction("&Add attribute", self)
        self.add_attribute_action.setIcon(QIcon("aset_ui/resources/plus.svg"))
        self.add_attribute_action.setStatusTip("Add a new attribute to the document base.")
        self.add_attribute_action.triggered.connect(self.add_attribute_task)
        self.add_attribute_action.setEnabled(False)
        self._all_actions.append(self.add_attribute_action)

        self.remove_attribute_action = QAction("&Remove attribute", self)
        self.remove_attribute_action.setIcon(QIcon("aset_ui/resources/trash.svg"))
        self.remove_attribute_action.setStatusTip("Remove an attribute from the document base.")
        self.remove_attribute_action.triggered.connect(self.remove_attribute_task)
        self.remove_attribute_action.setEnabled(False)
        self._all_actions.append(self.remove_attribute_action)

        self.forget_matches_for_attribute_action = QAction("&Forget matches for attribute", self)
        self.forget_matches_for_attribute_action.setIcon(QIcon("aset_ui/resources/redo.svg"))
        self.forget_matches_for_attribute_action.setStatusTip("Forget the matches for a single attribute.")
        self.forget_matches_for_attribute_action.triggered.connect(self.forget_matches_for_attribute_task)
        self.forget_matches_for_attribute_action.setEnabled(False)
        self._all_actions.append(self.forget_matches_for_attribute_action)

        self.load_document_base_from_bson_action = QAction("&Load document base", self)
        self.load_document_base_from_bson_action.setIcon(QIcon("aset_ui/resources/folder.svg"))
        self.load_document_base_from_bson_action.setStatusTip("Load an existing document base from a .bson file.")
        self.load_document_base_from_bson_action.triggered.connect(self.load_document_base_from_bson_task)
        self._all_actions.append(self.load_document_base_from_bson_action)

        self.save_document_base_to_bson_action = QAction("&Save document base", self)
        self.save_document_base_to_bson_action.setIcon(QIcon("aset_ui/resources/save.svg"))
        self.save_document_base_to_bson_action.setStatusTip("Save the document base in a .bson file.")
        self.save_document_base_to_bson_action.triggered.connect(self.save_document_base_to_bson_task)
        self.save_document_base_to_bson_action.setEnabled(False)
        self._all_actions.append(self.save_document_base_to_bson_action)

        self.save_table_to_csv_action = QAction("&Export table to CSV", self)
        self.save_table_to_csv_action.setIcon(QIcon("aset_ui/resources/table.svg"))
        self.save_table_to_csv_action.setStatusTip("Save the table to a .csv file.")
        self.save_table_to_csv_action.triggered.connect(self.save_table_to_csv_task)
        self.save_table_to_csv_action.setEnabled(False)
        self._all_actions.append(self.save_table_to_csv_action)

        self.forget_matches_action = QAction("&Forget all matches", self)
        self.forget_matches_action.setIcon(QIcon("aset_ui/resources/redo.svg"))
        self.forget_matches_action.setStatusTip("Forget the matches for all attributes.")
        self.forget_matches_action.triggered.connect(self.forget_matches_task)
        self.forget_matches_action.setEnabled(False)
        self._all_actions.append(self.forget_matches_action)

        self.load_and_run_default_preprocessing_phase_action = QAction(
            "&Load and run default preprocessing phase", self
        )
        self.load_and_run_default_preprocessing_phase_action.setStatusTip(
            "Load the default preprocessing phase and run it on the document collection."
        )
        self.load_and_run_default_preprocessing_phase_action.setIcon(QIcon("aset_ui/resources/run_run.svg"))
        self.load_and_run_default_preprocessing_phase_action.setDisabled(True)
        self.load_and_run_default_preprocessing_phase_action.triggered.connect(
            self.load_and_run_default_preprocessing_phase_task
        )
        self._all_actions.append(self.load_and_run_default_preprocessing_phase_action)

        self.load_preprocessing_phase_from_config_action = QAction("&Load preprocessing phase", self)
        self.load_preprocessing_phase_from_config_action.setStatusTip(
            "Load a preprocessing phase from a .json configuration file."
        )
        self.load_preprocessing_phase_from_config_action.triggered.connect(
            self.load_preprocessing_phase_from_config_task
        )
        self._all_actions.append(self.load_preprocessing_phase_from_config_action)

        self.save_preprocessing_phase_to_config_action = QAction("&Save preprocessing phase", self)
        self.save_preprocessing_phase_to_config_action.setStatusTip(
            "Save the preprocessing phase in a .json configuration file."
        )
        self.save_preprocessing_phase_to_config_action.triggered.connect(self.save_preprocessing_phase_to_config_task)
        self.save_preprocessing_phase_to_config_action.setEnabled(False)
        self._all_actions.append(self.save_preprocessing_phase_to_config_action)

        self.run_preprocessing_phase_action = QAction("Run preprocessing phase", self)
        self.run_preprocessing_phase_action.setIcon(QIcon("aset_ui/resources/run.svg"))
        self.run_preprocessing_phase_action.setStatusTip("Run the preprocessing phase on the document collection.")
        self.run_preprocessing_phase_action.triggered.connect(self.run_preprocessing_phase_task)
        self.run_preprocessing_phase_action.setEnabled(False)
        self._all_actions.append(self.run_preprocessing_phase_action)

        self.load_and_run_default_matching_phase_action = QAction(
            "&Load and run default matching phase", self
        )
        self.load_and_run_default_matching_phase_action.setStatusTip(
            "Load the default matching phase and run it on the document collection."
        )
        self.load_and_run_default_matching_phase_action.setIcon(QIcon("aset_ui/resources/run_run.svg"))
        self.load_and_run_default_matching_phase_action.setDisabled(True)
        self.load_and_run_default_matching_phase_action.triggered.connect(
            self.load_and_run_default_preprocessing_phase_task
        )
        self._all_actions.append(self.load_and_run_default_matching_phase_action)

        self.load_matching_phase_from_config_action = QAction("&Load matching phase", self)
        self.load_matching_phase_from_config_action.setStatusTip(
            "Load a matching phase from a .json configuration file."
        )
        self.load_matching_phase_from_config_action.triggered.connect(self.load_matching_phase_from_config_task)
        self._all_actions.append(self.load_matching_phase_from_config_action)

        self.save_matching_phase_to_config_action = QAction("&Save matching phase", self)
        self.save_matching_phase_to_config_action.setStatusTip("Save the matching phase in a .json configuration file.")
        self.save_matching_phase_to_config_action.triggered.connect(self.save_matching_phase_to_config_task)
        self.save_matching_phase_to_config_action.setEnabled(False)
        self._all_actions.append(self.save_matching_phase_to_config_action)

        self.run_matching_phase_action = QAction("Run matching phase", self)
        self.run_matching_phase_action.setIcon(QIcon("aset_ui/resources/run.svg"))
        self.run_matching_phase_action.setStatusTip("Run the matching phase on the document collection.")
        self.run_matching_phase_action.triggered.connect(self.run_matching_phase_task)
        self.run_matching_phase_action.setEnabled(False)
        self._all_actions.append(self.run_matching_phase_action)

        self.enable_collect_statistics_action = QAction("&Enable statistics", self)
        self.enable_collect_statistics_action.setIcon(QIcon("aset_ui/resources/statistics.svg"))
        self.enable_collect_statistics_action.setStatusTip("Enable collecting statistics.")
        self.enable_collect_statistics_action.triggered.connect(self.enable_collect_statistics_task)
        self.enable_collect_statistics_action.setEnabled(False)
        self._all_actions.append(self.enable_collect_statistics_action)

        self.disable_collect_statistics_action = QAction("&Disable statistics", self)
        self.disable_collect_statistics_action.setIcon(QIcon("aset_ui/resources/statistics_incorrect.svg"))
        self.disable_collect_statistics_action.setStatusTip("Disable collecting statistics.")
        self.disable_collect_statistics_action.triggered.connect(self.disable_collect_statistics_task)
        self._all_actions.append(self.disable_collect_statistics_action)

        self.save_statistics_to_json_action = QAction("&Save statistics", self)
        self.save_statistics_to_json_action.setIcon(QIcon("aset_ui/resources/statistics_save.svg"))
        self.save_statistics_to_json_action.setStatusTip("Save the statistics to a .json file.")
        self.save_statistics_to_json_action.triggered.connect(self.save_statistics_to_json_task)
        self.save_statistics_to_json_action.setEnabled(False)
        self._all_actions.append(self.save_statistics_to_json_action)

        # set up the menu bar
        self.menubar = self.menuBar()
        self.menubar.setFont(MENU_FONT)

        self.file_menu = self.menubar.addMenu("&File")
        self.file_menu.setFont(MENU_FONT)
        self.file_menu.addAction(self.exit_action)

        self.document_base_menu = self.menubar.addMenu("&Document Base")
        self.document_base_menu.setFont(MENU_FONT)
        self.document_base_menu.addAction(self.show_document_base_creator_widget_action)
        self.document_base_menu.addSeparator()
        self.document_base_menu.addAction(self.load_document_base_from_bson_action)
        self.document_base_menu.addAction(self.save_document_base_to_bson_action)
        self.document_base_menu.addSeparator()
        self.document_base_menu.addAction(self.save_table_to_csv_action)
        self.document_base_menu.addSeparator()
        self.document_base_menu.addAction(self.add_attribute_action)
        self.document_base_menu.addAction(self.remove_attribute_action)
        self.document_base_menu.addSeparator()
        self.document_base_menu.addAction(self.forget_matches_for_attribute_action)
        self.document_base_menu.addAction(self.forget_matches_action)

        self.preprocessing_menu = self.menubar.addMenu("&Preprocessing")
        self.preprocessing_menu.setFont(MENU_FONT)
        self.preprocessing_menu.addAction(self.load_and_run_default_preprocessing_phase_action)
        self.preprocessing_menu.addSeparator()
        self.preprocessing_menu.addAction(self.load_preprocessing_phase_from_config_action)
        self.preprocessing_menu.addAction(self.save_preprocessing_phase_to_config_action)
        self.preprocessing_menu.addSeparator()
        self.preprocessing_menu.addAction(self.run_preprocessing_phase_action)

        self.matching_menu = self.menubar.addMenu("&Matching")
        self.matching_menu.setFont(MENU_FONT)
        self.matching_menu.addAction(self.load_and_run_default_matching_phase_action)
        self.matching_menu.addSeparator()
        self.matching_menu.addAction(self.load_matching_phase_from_config_action)
        self.matching_menu.addAction(self.save_matching_phase_to_config_action)
        self.matching_menu.addSeparator()
        self.matching_menu.addAction(self.run_matching_phase_action)

        self.statistics_menu = self.menubar.addMenu("&Statistics")
        self.statistics_menu.setFont(MENU_FONT)
        self.statistics_menu.addAction(self.enable_collect_statistics_action)
        self.statistics_menu.addAction(self.disable_collect_statistics_action)
        self.statistics_menu.addSeparator()
        self.statistics_menu.addAction(self.save_statistics_to_json_action)

        # start menu
        self.start_menu_widget = QWidget()
        self.start_menu_layout = QVBoxLayout(self.start_menu_widget)
        self.start_menu_layout.setContentsMargins(0, 0, 0, 0)
        self.start_menu_layout.setSpacing(30)
        self.start_menu_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
        self.start_menu_widget.setMaximumWidth(400)

        self.start_menu_header = QLabel("Welcome to ASET!")
        self.start_menu_header.setFont(HEADER_FONT)
        self.start_menu_layout.addWidget(self.start_menu_header)

        self.start_menu_create_new_document_base_widget = QWidget()
        self.start_menu_create_new_document_base_layout = QVBoxLayout(self.start_menu_create_new_document_base_widget)
        self.start_menu_create_new_document_base_layout.setContentsMargins(0, 0, 0, 0)
        self.start_menu_create_new_document_base_layout.setSpacing(10)
        self.start_menu_layout.addWidget(self.start_menu_create_new_document_base_widget)

        self.start_menu_create_new_document_base_subheader = QLabel("Create a new document base.")
        self.start_menu_create_new_document_base_subheader.setFont(SUBHEADER_FONT)
        self.start_menu_create_new_document_base_layout.addWidget(self.start_menu_create_new_document_base_subheader)

        self.start_menu_create_new_document_base_wrapper_widget = QWidget()
        self.start_menu_create_new_document_base_wrapper_layout = QHBoxLayout(
            self.start_menu_create_new_document_base_wrapper_widget)
        self.start_menu_create_new_document_base_wrapper_layout.setContentsMargins(0, 0, 0, 0)
        self.start_menu_create_new_document_base_wrapper_layout.setSpacing(20)
        self.start_menu_create_new_document_base_wrapper_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
        self.start_menu_create_new_document_base_layout.addWidget(
            self.start_menu_create_new_document_base_wrapper_widget)

        self.start_menu_create_document_base_button = QPushButton()
        self.start_menu_create_document_base_button.setFixedHeight(45)
        self.start_menu_create_document_base_button.setFixedWidth(45)
        self.start_menu_create_document_base_button.setIcon(QIcon("aset_ui/resources/two_documents.svg"))
        self.start_menu_create_document_base_button.clicked.connect(self.show_document_base_creator_widget_task)
        self.start_menu_create_new_document_base_wrapper_layout.addWidget(self.start_menu_create_document_base_button)

        self.start_menu_create_document_base_label = QLabel(
            "Create a new document base from a directory\nof .txt files and a list of attribute names.")
        self.start_menu_create_document_base_label.setFont(LABEL_FONT)
        self.start_menu_create_new_document_base_wrapper_layout.addWidget(self.start_menu_create_document_base_label)

        self.start_menu_load_document_base_widget = QWidget()
        self.start_menu_load_document_base_layout = QVBoxLayout(self.start_menu_load_document_base_widget)
        self.start_menu_load_document_base_layout.setContentsMargins(0, 0, 0, 0)
        self.start_menu_load_document_base_layout.setSpacing(10)
        self.start_menu_layout.addWidget(self.start_menu_load_document_base_widget)

        self.start_menu_load_document_base_subheader = QLabel("Load an existing document base.")
        self.start_menu_load_document_base_subheader.setFont(SUBHEADER_FONT)
        self.start_menu_load_document_base_layout.addWidget(self.start_menu_load_document_base_subheader)

        self.start_menu_load_document_base_wrapper_widget = QWidget()
        self.start_menu_load_document_base_wrapper_layout = QHBoxLayout(
            self.start_menu_load_document_base_wrapper_widget)
        self.start_menu_load_document_base_wrapper_layout.setContentsMargins(0, 0, 0, 0)
        self.start_menu_load_document_base_wrapper_layout.setSpacing(20)
        self.start_menu_load_document_base_wrapper_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
        self.start_menu_load_document_base_layout.addWidget(self.start_menu_load_document_base_wrapper_widget)

        self.start_menu_load_document_base_button = QPushButton()
        self.start_menu_load_document_base_button.setFixedHeight(45)
        self.start_menu_load_document_base_button.setFixedWidth(45)
        self.start_menu_load_document_base_button.setIcon(QIcon("aset_ui/resources/folder.svg"))
        self.start_menu_load_document_base_button.clicked.connect(self.load_document_base_from_bson_task)
        self.start_menu_load_document_base_wrapper_layout.addWidget(self.start_menu_load_document_base_button)

        self.start_menu_load_document_base_label = QLabel("Load an existing document base\nfrom a .bson file.")
        self.start_menu_load_document_base_label.setFont(LABEL_FONT)
        self.start_menu_load_document_base_wrapper_layout.addWidget(self.start_menu_load_document_base_label)

        # main UI
        self.central_widget = QWidget(self)
        self.central_widget_layout = QHBoxLayout(self.central_widget)
        self.central_widget_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.setCentralWidget(self.central_widget)

        self.document_base_creator_widget = DocumentBaseCreatorWidget(self)
        self.document_base_viewer_widget = DocumentBaseViewerWidget(self)
        self.interactive_matching_widget = InteractiveMatchingWidget(self)

        self.document_base_creator_widget.hide()
        self.document_base_viewer_widget.hide()
        self.interactive_matching_widget.hide()
        self.central_widget_layout.addWidget(self.start_menu_widget)
        self.central_widget_layout.update()

        self.resize(1400, 800)
        self.show()

        logger.info("Initialized MainWindow.")
示例#2
0
class MainWindow(QMainWindow):

    def __init__(self):
        """MainWindow Constructor for Image Manager"""
        super().__init__() # Constructor for QMainWindow
        self.initializeUI()
        
    def initializeUI(self):
        """Set up the GUI's main window."""
        self.setWindowTitle("Image Manager")
        self.setObjectName("ImageManager")
  
        # Set up the main window, menu, and dock widgets
        self.setUpMainWindow()
        self.displayImagePreviewDock()
        self.createActions()
        self.createMenus()
        self.show() # Display the main window

    def setUpMainWindow(self):
        """Set up the application's main window containing the QListWidget."""
        self.image_view_lw = ImageViewerListWidget(self)
        self.setCentralWidget(self.image_view_lw)

    def createActions(self):
        """Create the application's menu actions."""
        # Create actions for File menu
        self.import_act = QAction("Import Images...", self, triggered=self.importImages)
        self.import_act.setShortcut("Ctrl+I") 

        self.preferences_act = QAction("Preferences...", self, triggered=self.showPreferencesDialog)
           
        self.quit_act = QAction("Quit Task Manager", self, triggered=self.close)
        self.quit_act.setShortcut(QKeySequence.StandardKey.Quit) # Ctrl+Q

        # Create actions for Edit menu
        self.select_all_act = QAction("Select All", self, triggered=self.image_view_lw.selectAll)
        self.select_all_act.setShortcut(QKeySequence.StandardKey.SelectAll) # Ctrl+A

        self.delete_act = QAction("Delete Images", self, triggered=self.deleteImages)
        self.delete_act.setShortcut(QKeySequence.StandardKey.Delete) # Del
        self.delete_act.setEnabled(False)

        # Create actions for View menu
        # Handle the visibility of the dock widget
        self.show_dock_act = self.image_preview_dock.toggleViewAction()
        self.show_dock_act.setText("Show Image View") 

        self.sort_ascend_act = QAction("Sort Ascending", self, 
            triggered=lambda: self.sortListItems(Qt.SortOrder.AscendingOrder))
        self.sort_ascend_act.setEnabled(False)

        self.sort_descend_act = QAction("Sort Descending", self, 
            triggered=lambda: self.sortListItems(Qt.SortOrder.DescendingOrder))
        self.sort_descend_act.setEnabled(False)

        self.fullscreen_act = QAction("Show Fullscreen", self, 
            triggered=self.displayFullScreen, checkable=True)

        # Create actions for Help menu
        self.about_act = QAction("About Image Manager", 
            self, triggered=self.showAboutDialog)

    def createMenus(self):
        """Create the application's menu."""
        self.file_menu = self.menuBar().addMenu("&File")
        self.file_menu.addAction(self.import_act)
        self.file_menu.addSeparator()
        self.file_menu.addAction(self.preferences_act)
        self.file_menu.addSeparator()
        self.file_menu.addAction(self.quit_act)

        self.edit_menu = self.menuBar().addMenu("&Edit")
        self.edit_menu.addAction(self.select_all_act)
        self.edit_menu.addSeparator()
        self.edit_menu.addAction(self.delete_act)  

        self.view_menu = self.menuBar().addMenu("&View")
        self.view_menu.addAction(self.show_dock_act)  
        self.view_menu.addSeparator()
        self.view_menu.addAction(self.sort_ascend_act)
        self.view_menu.addAction(self.sort_descend_act)
        self.view_menu.addSeparator()
        self.view_menu.addAction(self.fullscreen_act) 

        self.help_menu = self.menuBar().addMenu("&Help")
        self.help_menu.addAction(self.about_act)  

    def displayImagePreviewDock(self):
        """Dock widget that displays a selected image in a scrollable 
        area and uses its file name as the dock's title."""        
        self.image_preview_dock = QDockWidget()
        self.image_preview_dock.setObjectName("PreviewDock")
        self.image_preview_dock.setWindowTitle("Show Image View")
        self.image_preview_dock.setAllowedAreas(Qt.DockWidgetArea.RightDockWidgetArea)

        self.display_image_label = QLabel()
        self.display_image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        self.view_scroll_area = QScrollArea()
        self.view_scroll_area.setMinimumWidth(300)
        self.view_scroll_area.setWidgetResizable(True)

        self.image_preview_dock.setWidget(self.view_scroll_area)
        # Set initial location of dock widget
        self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.image_preview_dock)  

    def displayFullScreen(self, state):
        """Check the state of checkable fullscreen_act. If True, show the 
        main window as fullscreen."""
        if state: self.showFullScreen()
        else: self.showNormal()

    def importImages(self):
        """Placeholder method."""
        pass

    def deleteImages(self):
        """Placeholder method."""
        pass

    def showPreferencesDialog(self):
        """Placeholder method."""
        pass

    def showAboutDialog(self):
        """Placeholder method."""
        pass
示例#3
0
class MainWindow(QMainWindow):
    def __init__(self):
        """ MainWindow Constructor """
        super().__init__()
        self.initializeUI()

    def initializeUI(self):
        """Initialize settings, call functions that define 
        UI elements, and display the main window."""
        self.setMinimumSize(700, 400)
        self.setWindowTitle("GIF and Image Viewer")

        # Set up the main window, menu, and dock widget
        self.setUpMainWindow()
        self.displayFilesDock()
        self.createActions()
        self.createMenus()
        self.createToolbar()
        self.show()  # Display the main window

    def setUpMainWindow(self):
        """Set up the application's main window and widgets."""
        self.movie = QMovie()  # Create movie object
        self.movie.stateChanged.connect(self.changeButtonStates)

        self.media_label = QLabel()  # Create label to place images/GIFs on
        self.media_label.setPixmap(QPixmap("icons/image_label.png"))
        self.media_label.setFrameShape(QFrame.Shape.StyledPanel)
        self.media_label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        self.setCentralWidget(self.media_label)

    def createActions(self):
        """Create the application's menu actions."""
        # Create actions for File menu
        self.open_act = QAction("Open...", self, triggered=self.openDirectory)
        self.open_act.setShortcut(QKeySequence.StandardKey.Open)

        self.quit_act = QAction("Quit Viewer", self, triggered=self.close)
        self.quit_act.setShortcut(QKeySequence.StandardKey.Quit)  # Ctrl+Q

        # Create actions for View menu
        # Handle the visibility of the dock widget
        self.show_dock_act = self.files_dock.toggleViewAction()
        self.show_dock_act.setText("Show Media Folder")

        # Create actions for the toolbar (These actions could also be
        # added to the GUI's menu bar or to a context menu)
        self.play_act = QAction(QIcon("icons/play.png"),
                                "Play",
                                self,
                                triggered=self.startMovie)
        self.pause_act = QAction(QIcon("icons/pause.png"),
                                 "Pause",
                                 self,
                                 triggered=self.pauseMovie)
        self.stop_act = QAction(QIcon("icons/stop.png"),
                                "Stop/Reset",
                                self,
                                triggered=self.stopMovie)
        self.disableMovieButtons()

    def createMenus(self):
        """Create the application's menu."""
        # Make the toolbar appear in the main window for macOS users.
        # More information about this in Chapter 2 - Building the Foundation for GUIs
        if QSysInfo.productType() == "macos" or "osx":
            self.menuBar().setNativeMenuBar(False)

        self.file_menu = self.menuBar().addMenu("&File")
        self.file_menu.addAction(self.open_act)
        self.file_menu.addSeparator()
        self.file_menu.addAction(self.quit_act)

        self.view_menu = self.menuBar().addMenu("&View")
        self.view_menu.addAction(self.show_dock_act)

    def createToolbar(self):
        """Create the application's toolbar for playing GIFs."""
        toolbar = self.addToolBar("GIF Controls Toolbar")
        toolbar.setIconSize(QSize(24, 24))

        # Add actions to the toolbar
        toolbar.addAction(self.play_act)
        toolbar.addAction(self.pause_act)
        toolbar.addAction(self.stop_act)

    def displayFilesDock(self):
        """Dock widget that displays the movie file location in a QLineEdit 
        widget, provides a button for opening directories with images and GIFs, 
        and shows the media from the selected folder in a QTreeWidget."""
        self.files_dock = QDockWidget()
        self.files_dock.setWindowTitle("Media Folder")
        self.files_dock.setAllowedAreas(Qt.DockWidgetArea.LeftDockWidgetArea)

        folder_label = QLabel("Media Location:")
        # The QLineEdit widget is set to read-only as a quick way to display
        # the folder path
        self.folder_line = QLineEdit()
        self.folder_line.setMinimumWidth(100)
        self.folder_line.setReadOnly(True)

        open_button = QPushButton("Open...")
        open_button.clicked.connect(self.openDirectory)

        folder_h_box = QHBoxLayout()
        folder_h_box.addWidget(folder_label)
        folder_h_box.addWidget(self.folder_line)
        folder_h_box.addWidget(open_button)

        self.files_tree = QTreeWidget()
        self.files_tree.setHeaderLabel("Media Files")
        self.files_tree.setColumnCount(1)

        # Set up the dock's layout
        dock_v_box = QVBoxLayout()
        dock_v_box.addLayout(folder_h_box)
        dock_v_box.addWidget(self.files_tree)

        dock_container = QWidget()
        dock_container.setLayout(dock_v_box)

        self.files_dock.setWidget(dock_container)
        self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea,
                           self.files_dock)

    def openDirectory(self):
        """Open a QFileDialog for selecting a local directory. Only display 
        image and GIF files."""
        directory = QFileDialog.getExistingDirectory(
            self, "Choose Directory", "", QFileDialog.Option.ShowDirsOnly
        )  # Specify the file mode to only select directories

        if directory:
            self.movie.setFileName(directory)
            # Check if image data is valid before playing
            if self.movie.isValid():
                # Use setMovie() to set the label's contents as the selected GIF
                self.media_label.setMovie(self.movie)
                self.startMovie()  # Call method to begin playing

    def startMovie(self):
        """Start playing the movie."""
        self.movie.start()

    def pauseMovie(self):
        """Pause the movie."""
        self.movie.setPaused(True)

    def stopMovie(self):
        """Stop playing the movie and reset the movie back to 
        the first frame."""
        self.movie.stop()
        self.movie.jumpToFrame(0)

    def changeButtonStates(self, state):
        """Slot that handles enabling/disabling buttons in the toolbar
        based on the state of QMovie."""
        if state == QMovie.MovieState.Running:
            # The animation begins playing once control returns to the event loop
            self.play_act.setEnabled(False)
            self.pause_act.setEnabled(True)
            self.stop_act.setEnabled(True)
        if state == QMovie.MovieState.Paused:
            self.play_act.setEnabled(True)
            self.pause_act.setEnabled(False)
            self.stop_act.setEnabled(False)
        if state == QMovie.MovieState.NotRunning:
            self.play_act.setEnabled(True)
            self.pause_act.setEnabled(False)
            self.stop_act.setEnabled(False)

    def disableMovieButtons(self):
        """Simple method to disable the movie buttons in the toolbar."""
        self.play_act.setEnabled(False)
        self.pause_act.setEnabled(False)
        self.stop_act.setEnabled(False)
示例#4
0
class PhotoEditorGUI(QMainWindow):
    def __init__(self):
        super().__init__()

        self.initializeUI()

        self.image = QImage()

    def initializeUI(self):
        self.setMinimumSize(300, 200)
        self.setWindowTitle("Photo Editor")
        self.showMaximized()

        self.zoom_factor = 1

        self.createMainLabel()
        self.createEditingBar()
        self.createMenu()
        self.createToolBar()

        self.show()

    def createMenu(self):
        """Set up the menubar."""
        # Actions for Photo Editor menu
        about_act = QAction('About', self)
        about_act.triggered.connect(self.aboutDialog)

        self.exit_act = QAction(QIcon(os.path.join(icon_path, "exit.png")),
                                'Quit Photo Editor', self)
        self.exit_act.setShortcut('Ctrl+Q')
        self.exit_act.triggered.connect(self.close)

        # Actions for File menu
        self.new_act = QAction(QIcon(os.path.join(icon_path, "new.png")),
                               'New...')

        self.open_act = QAction(QIcon(os.path.join(icon_path, "open.png")),
                                'Open...', self)
        self.open_act.setShortcut('Ctrl+O')
        self.open_act.triggered.connect(self.image_label.openImage)

        self.print_act = QAction(QIcon(os.path.join(icon_path, "print.png")),
                                 "Print...", self)
        self.print_act.setShortcut('Ctrl+P')
        #self.print_act.triggered.connect(self.printImage)
        self.print_act.setEnabled(False)

        self.save_act = QAction(QIcon(os.path.join(icon_path, "save.png")),
                                "Save...", self)
        self.save_act.setShortcut('Ctrl+S')
        self.save_act.triggered.connect(self.image_label.saveImage)
        self.save_act.setEnabled(False)

        # Actions for Edit menu
        self.revert_act = QAction("Revert to Original", self)
        self.revert_act.triggered.connect(self.image_label.revertToOriginal)
        self.revert_act.setEnabled(False)

        # Actions for Tools menu
        self.crop_act = QAction(QIcon(os.path.join(icon_path, "crop.png")),
                                "Crop", self)
        self.crop_act.setShortcut('Shift+X')
        self.crop_act.triggered.connect(self.image_label.cropImage)

        self.resize_act = QAction(QIcon(os.path.join(icon_path, "resize.png")),
                                  "Resize", self)
        self.resize_act.setShortcut('Shift+Z')
        self.resize_act.triggered.connect(self.image_label.resizeImage)

        self.rotate90_cw_act = QAction(
            QIcon(os.path.join(icon_path, "rotate90_cw.png")), 'Rotate 90º CW',
            self)
        self.rotate90_cw_act.triggered.connect(
            lambda: self.image_label.rotateImage90("cw"))

        self.rotate90_ccw_act = QAction(
            QIcon(os.path.join(icon_path, "rotate90_ccw.png")),
            'Rotate 90º CCW', self)
        self.rotate90_ccw_act.triggered.connect(
            lambda: self.image_label.rotateImage90("ccw"))

        self.flip_horizontal = QAction(
            QIcon(os.path.join(icon_path, "flip_horizontal.png")),
            'Flip Horizontal', self)
        self.flip_horizontal.triggered.connect(
            lambda: self.image_label.flipImage("horizontal"))

        self.flip_vertical = QAction(
            QIcon(os.path.join(icon_path, "flip_vertical.png")),
            'Flip Vertical', self)
        self.flip_vertical.triggered.connect(
            lambda: self.image_label.flipImage('vertical'))

        self.zoom_in_act = QAction(
            QIcon(os.path.join(icon_path, "zoom_in.png")), 'Zoom In', self)
        self.zoom_in_act.setShortcut('Ctrl++')
        self.zoom_in_act.triggered.connect(lambda: self.zoomOnImage(1.25))
        self.zoom_in_act.setEnabled(False)

        self.zoom_out_act = QAction(
            QIcon(os.path.join(icon_path, "zoom_out.png")), 'Zoom Out', self)
        self.zoom_out_act.setShortcut('Ctrl+-')
        self.zoom_out_act.triggered.connect(lambda: self.zoomOnImage(0.8))
        self.zoom_out_act.setEnabled(False)

        self.normal_size_Act = QAction("Normal Size", self)
        self.normal_size_Act.setShortcut('Ctrl+=')
        self.normal_size_Act.triggered.connect(self.normalSize)
        self.normal_size_Act.setEnabled(False)

        # Actions for Views menu
        #self.tools_menu_act = QAction(QIcon(os.path.join(icon_path, "edit.png")),'Tools View...', self, checkable=True)

        # Create menubar
        menu_bar = self.menuBar()
        menu_bar.setNativeMenuBar(False)

        # Create Photo Editor menu and add actions
        main_menu = menu_bar.addMenu('Photo Editor')
        main_menu.addAction(about_act)
        main_menu.addSeparator()
        main_menu.addAction(self.exit_act)

        # Create file menu and add actions
        file_menu = menu_bar.addMenu('File')
        file_menu.addAction(self.open_act)
        file_menu.addAction(self.save_act)
        file_menu.addSeparator()
        file_menu.addAction(self.print_act)

        edit_menu = menu_bar.addMenu('Edit')
        edit_menu.addAction(self.revert_act)

        tool_menu = menu_bar.addMenu('Tools')
        tool_menu.addAction(self.crop_act)
        tool_menu.addAction(self.resize_act)
        tool_menu.addSeparator()
        tool_menu.addAction(self.rotate90_cw_act)
        tool_menu.addAction(self.rotate90_ccw_act)
        tool_menu.addAction(self.flip_horizontal)
        tool_menu.addAction(self.flip_vertical)
        tool_menu.addSeparator()
        tool_menu.addAction(self.zoom_in_act)
        tool_menu.addAction(self.zoom_out_act)
        tool_menu.addAction(self.normal_size_Act)

        views_menu = menu_bar.addMenu('Views')
        views_menu.addAction(self.tools_menu_act)

    def createToolBar(self):
        """Set up the toolbar."""
        tool_bar = QToolBar("Main Toolbar")
        tool_bar.setIconSize(QSize(26, 26))
        self.addToolBar(tool_bar)

        # Add actions to the toolbar
        tool_bar.addAction(self.open_act)
        tool_bar.addAction(self.save_act)
        tool_bar.addAction(self.print_act)
        tool_bar.addAction(self.exit_act)
        tool_bar.addSeparator()
        tool_bar.addAction(self.crop_act)
        tool_bar.addAction(self.resize_act)
        tool_bar.addSeparator()
        tool_bar.addAction(self.rotate90_ccw_act)
        tool_bar.addAction(self.rotate90_cw_act)
        tool_bar.addAction(self.flip_horizontal)
        tool_bar.addAction(self.flip_vertical)
        tool_bar.addSeparator()
        tool_bar.addAction(self.zoom_in_act)
        tool_bar.addAction(self.zoom_out_act)

    def createEditingBar(self):
        """Create dock widget for editing tools."""
        #TODO: Add a tab widget for the different editing tools
        self.editing_bar = QDockWidget("Tools")
        self.editing_bar.setAllowedAreas(
            Qt.DockWidgetAreas.LeftDockWidgetArea
            | Qt.DockWidgetAreas.RightDockWidgetArea)
        self.editing_bar.setMinimumWidth(90)

        # Create editing tool buttons
        filters_label = QLabel("Filters")

        convert_to_grayscale = QToolButton()
        convert_to_grayscale.setIcon(
            QIcon(os.path.join(icon_path, "grayscale.png")))
        convert_to_grayscale.clicked.connect(self.image_label.convertToGray)

        convert_to_RGB = QToolButton()
        convert_to_RGB.setIcon(QIcon(os.path.join(icon_path, "rgb.png")))
        convert_to_RGB.clicked.connect(self.image_label.convertToRGB)

        convert_to_sepia = QToolButton()
        convert_to_sepia.setIcon(QIcon(os.path.join(icon_path, "sepia.png")))
        convert_to_sepia.clicked.connect(self.image_label.convertToSepia)

        change_hue = QToolButton()
        change_hue.setIcon(QIcon(os.path.join(icon_path, "")))
        change_hue.clicked.connect(self.image_label.changeHue)

        brightness_label = QLabel("Brightness")
        self.brightness_slider = QSlider(Qt.Orientations.Horizontal)
        self.brightness_slider.setRange(-255, 255)
        self.brightness_slider.setTickInterval(35)
        self.brightness_slider.setTickPosition(QSlider.TickPosition.TicksAbove)
        self.brightness_slider.valueChanged.connect(
            self.image_label.changeBrighteness)

        contrast_label = QLabel("Contrast")
        self.contrast_slider = QSlider(Qt.Orientations.Horizontal)
        self.contrast_slider.setRange(-255, 255)
        self.contrast_slider.setTickInterval(35)
        self.contrast_slider.setTickPosition(QSlider.TickPosition.TicksAbove)
        self.contrast_slider.valueChanged.connect(
            self.image_label.changeContrast)

        # Set layout for dock widget
        editing_grid = QGridLayout()
        #editing_grid.addWidget(filters_label, 0, 0, 0, 2, Qt.AlignTop)
        editing_grid.addWidget(convert_to_grayscale, 1, 0)
        editing_grid.addWidget(convert_to_RGB, 1, 1)
        editing_grid.addWidget(convert_to_sepia, 2, 0)
        editing_grid.addWidget(change_hue, 2, 1)
        editing_grid.addWidget(brightness_label, 3, 0)
        editing_grid.addWidget(self.brightness_slider, 4, 0, 1, 0)
        editing_grid.addWidget(contrast_label, 5, 0)
        editing_grid.addWidget(self.contrast_slider, 6, 0, 1, 0)
        editing_grid.setRowStretch(7, 10)

        container = QWidget()
        container.setLayout(editing_grid)

        self.editing_bar.setWidget(container)

        self.addDockWidget(Qt.DockWidgetAreas.LeftDockWidgetArea,
                           self.editing_bar)

        self.tools_menu_act = self.editing_bar.toggleViewAction()

    def createMainLabel(self):
        """Create an instance of the imageLabel class and set it 
           as the main window's central widget."""
        self.image_label = imageLabel(self)
        self.image_label.resize(self.image_label.pixmap().size())

        self.scroll_area = QScrollArea()
        self.scroll_area.setBackgroundRole(QPalette.ColorRole.Dark)
        self.scroll_area.setAlignment(Qt.Alignment.AlignCenter)
        #self.scroll_area.setWidgetResizable(False)
        #scroll_area.setMinimumSize(800, 800)

        self.scroll_area.setWidget(self.image_label)
        #self.scroll_area.setVisible(False)

        self.setCentralWidget(self.scroll_area)

        #self.resize(QApplication.primaryScreen().availableSize() * 3 / 5)

    def updateActions(self):
        """Update the values of menu and toolbar items when an image 
        is loaded."""
        self.save_act.setEnabled(True)
        self.revert_act.setEnabled(True)
        self.zoom_in_act.setEnabled(True)
        self.zoom_out_act.setEnabled(True)
        self.normal_size_Act.setEnabled(True)

    def zoomOnImage(self, zoom_value):
        """Zoom in and zoom out."""
        self.zoom_factor *= zoom_value
        self.image_label.resize(self.zoom_factor *
                                self.image_label.pixmap().size())

        self.adjustScrollBar(self.scroll_area.horizontalScrollBar(),
                             zoom_value)
        self.adjustScrollBar(self.scroll_area.verticalScrollBar(), zoom_value)

        self.zoom_in_act.setEnabled(self.zoom_factor < 4.0)
        self.zoom_out_act.setEnabled(self.zoom_factor > 0.333)

    def normalSize(self):
        """View image with its normal dimensions."""
        self.image_label.adjustSize()
        self.zoom_factor = 1.0

    def adjustScrollBar(self, scroll_bar, value):
        """Adjust the scrollbar when zooming in or out."""
        scroll_bar.setValue(
            int(value * scroll_bar.value()) +
            ((value - 1) * scroll_bar.pageStep() / 2))

    def aboutDialog(self):
        QMessageBox.about(
            self, "About Photo Editor",
            "Photo Editor\nVersion 0.2\n\nCreated by Joshua Willman")

    def keyPressEvent(self, event):
        """Handle key press events."""
        if event.key() == Qt.Key_Escape:
            self.close()
        if event.key() == Qt.Key_F1:  # fn + F1 on Mac
            if self.isMaximized():
                self.showNormal()
            else:
                self.showMaximized()

    def closeEvent(self, event):
        pass
示例#5
0
class VideoWindow(QMainWindow):
    def __init__(self, parent=None):
        super(VideoWindow, self).__init__(parent)
        self.setWindowTitle("StudioProject")
        self.statusBar = QStatusBar()
        self.setStatusBar(self.statusBar)
        self.gScene = QGraphicsScene(self)
        self.gView = GraphicView(self.gScene, self)
        self.gView.viewport().setAttribute(
            Qt.WidgetAttribute.WA_AcceptTouchEvents, False)
        # self.gView.setBackgroundBrush(QBrush(Qt.black))

        self.videoStartDatetime = None
        self.videoCurrentDatetime = None

        self.projectFile = ''
        self.graphicsFile = ''
        self.videoFile = ''

        self.obsTb = ObsToolbox(self)

        # # ===================== Setting video item ==============================
        # self.videoItem = QGraphicsVideoItem()
        # self.videoItem.setAspectRatioMode(Qt.KeepAspectRatio)
        # self.gScene.addItem(self.videoItem)
        # self.videoItem.mouseMoveEvent = self.gView.mouseMoveEvent

        self.mediaPlayer = QMediaPlayer(self)
        # self.mediaPlayer.setVideoOutput(self.videoItem)
        self.mediaPlayer.playbackStateChanged.connect(self.mediaStateChanged)
        self.mediaPlayer.positionChanged.connect(self.positionChanged)
        self.mediaPlayer.durationChanged.connect(self.durationChanged)
        self.mediaPlayer.errorOccurred.connect(self.handleError)
        # self.mediaPlayer.setMuted(True)
        # self.mediaPlayer.setNotifyInterval(100)

        self.playButton = QPushButton()
        self.playButton.setEnabled(False)
        self.playButton.setIcon(self.style().standardIcon(
            QStyle.StandardPixmap.SP_MediaPlay))
        self.playButton.clicked.connect(self.play)

        self.changePlayRateBtn = QPushButton('1x')
        self.changePlayRateBtn.setFixedWidth(40)
        # self.incrPlayRateBtn.setEnabled(False)
        self.changePlayRateBtn.clicked.connect(self.changePlayRate)

        self.positionSlider = QSlider(Qt.Orientation.Horizontal)
        self.positionSlider.setRange(0, 0)
        self.positionSlider.sliderMoved.connect(self.setPosition)

        self.timerLabel = QLabel()
        self.timerLabel.setText('--:--:--')
        self.timerLabel.setFixedWidth(58)

        self.dateLabel = QLabel()
        self.dateLabel.setText('Video date: --')
        self.statusBar.addPermanentWidget(self.dateLabel)

        # Create open action
        self.openVideoAction = QAction(QIcon('icons/video-file.png'),
                                       'Open video', self)
        self.openVideoAction.setShortcut('Ctrl+O')
        self.openVideoAction.setStatusTip('Open video file')
        self.openVideoAction.triggered.connect(self.openVideoFile)

        # Create observation action
        obsTbAction = QAction(QIcon('icons/checklist.png'),
                              'Observation toolbox', self)
        obsTbAction.setStatusTip('Open observation toolbox')
        obsTbAction.triggered.connect(self.openObsToolbox)

        self.drawPointAction = QAction(QIcon('icons/drawPoint.png'),
                                       'Draw point', self)
        self.drawPointAction.setStatusTip('Draw point over the video')
        self.drawPointAction.setCheckable(True)
        self.drawPointAction.setEnabled(False)
        self.drawPointAction.triggered.connect(self.drawingClick)

        self.drawLineAction = QAction(QIcon('icons/drawLine.png'), 'Draw line',
                                      self)
        self.drawLineAction.setStatusTip('Draw line over the video')
        self.drawLineAction.setCheckable(True)
        self.drawLineAction.setEnabled(False)
        self.drawLineAction.triggered.connect(self.drawingClick)

        self.drawZoneAction = QAction(QIcon('icons/drawZone.png'), 'Draw zone',
                                      self)
        self.drawZoneAction.setStatusTip('Draw zone over the video')
        self.drawZoneAction.setCheckable(True)
        self.drawZoneAction.setEnabled(False)
        self.drawZoneAction.triggered.connect(self.drawingClick)

        self.maskGenAction = QAction(QIcon('icons/mask.png'),
                                     'Generate mask file', self)
        self.maskGenAction.setStatusTip(
            'Generate mask file for TrafficIntelligence')
        self.maskGenAction.setCheckable(True)
        self.maskGenAction.setEnabled(False)
        self.maskGenAction.triggered.connect(self.generateMask)

        actionGroup = QActionGroup(self)
        actionGroup.addAction(self.drawPointAction)
        actionGroup.addAction(self.drawLineAction)
        actionGroup.addAction(self.drawZoneAction)

        openProjectAction = QAction(QIcon('icons/open-project.png'),
                                    'Open project', self)
        openProjectAction.setStatusTip('Open project')
        openProjectAction.triggered.connect(self.openProject)

        self.saveProjectAction = QAction(QIcon('icons/save-project.png'),
                                         'Save project', self)
        self.saveProjectAction.setStatusTip('Save project')
        self.saveProjectAction.setEnabled(False)
        self.saveProjectAction.triggered.connect(self.saveProject)

        self.saveGraphAction = QAction(QIcon('icons/save-graphics.png'),
                                       'Save graphics', self)
        self.saveGraphAction.setStatusTip('Save graphics to database')
        self.saveGraphAction.setEnabled(False)
        self.saveGraphAction.triggered.connect(self.saveGraphics)

        self.loadGraphAction = QAction(QIcon('icons/folders.png'),
                                       'Load graphics', self)
        self.loadGraphAction.setStatusTip('Load graphics from database')
        self.loadGraphAction.setEnabled(False)
        self.loadGraphAction.triggered.connect(self.loadGraphics)

        # Create exit action
        exitAction = QAction(QIcon('icons/close.png'), 'Exit', self)
        exitAction.setShortcut('Ctrl+Q')
        exitAction.setStatusTip('Exit application')
        exitAction.triggered.connect(self.exitCall)  # self.exitCall

        # Create menu bar and add action
        # menuBar = self.menuBar()
        # menuBar.setNativeMenuBar(False)
        # fileMenu = menuBar.addMenu('&File')
        # fileMenu.addAction(openVideoAction)
        # fileMenu.addAction(obsTbAction)
        # fileMenu.addAction(exitAction)

        self.toolbar = self.addToolBar('Tools')
        self.toolbar.setIconSize(QSize(24, 24))
        self.toolbar.addAction(openProjectAction)
        self.toolbar.addAction(self.saveProjectAction)
        self.toolbar.addAction(self.openVideoAction)

        # self.toolbar.insertSeparator(self.loadGraphAction)
        # self.toolbar.addAction(self.loadGraphAction)
        # self.toolbar.addAction(self.saveGraphAction)
        # self.toolbar.addAction(self.drawPointAction)
        # self.toolbar.addAction(self.drawLineAction)
        # self.toolbar.addAction(self.drawZoneAction)
        self.toolbar.addAction(self.maskGenAction)
        # self.toolbar.insertSeparator(self.drawPointAction)

        self.toolbar.insertSeparator(obsTbAction)
        self.toolbar.addAction(obsTbAction)

        self.toolbar.insertSeparator(exitAction)
        self.toolbar.addAction(exitAction)

        # Create a widget for window contents
        wid = QWidget(self)
        self.setCentralWidget(wid)

        # Create layouts to place inside widget
        controlLayout = QHBoxLayout()
        controlLayout.setContentsMargins(0, 0, 0, 0)
        # controlLayout.addWidget(self.decrPlayRateBtn)
        controlLayout.addWidget(self.playButton)
        controlLayout.addWidget(self.changePlayRateBtn)
        controlLayout.addWidget(self.timerLabel)
        controlLayout.addWidget(self.positionSlider)
        # controlLayout.addWidget(self.durationLabel)

        layout = QVBoxLayout()
        layout.addWidget(self.gView)
        layout.addLayout(controlLayout)

        # Set widget to contain window contents
        wid.setLayout(layout)

    # def showEvent(self, event):
    # self.gView.fitInView(self.videoItem, Qt.KeepAspectRatio)

    def openVideoFile(self):
        # self.mediaPlayer.setMedia(QMediaContent())
        if self.sender() == self.openVideoAction:
            self.videoFile, _ = QFileDialog.getOpenFileName(
                self, "Open video", QDir.homePath())
            # if self.videoFile != '':
            #     self.setWindowTitle('{} - {}'.format(os.path.basename(self.videoFile),
            #                                          os.path.basename(self.projectFile)))

        if self.videoFile != '':
            self.setWindowTitle('{} - {}'.format(
                os.path.basename(self.videoFile),
                os.path.basename(self.projectFile)))
            self.saveProjectAction.setEnabled(True)
            self.maskGenAction.setEnabled(True)
            # self.loadGraphAction.setEnabled(True)
            # self.saveGraphAction.setEnabled(True)
            # self.drawPointAction.setEnabled(True)
            # self.drawLineAction.setEnabled(True)
            # self.drawZoneAction.setEnabled(True)

            creation_datetime, width, height = getVideoMetadata(self.videoFile)
            self.videoStartDatetime = self.videoCurrentDatetime = creation_datetime
            self.dateLabel.setText(creation_datetime.strftime('%a, %b %d, %Y'))

            self.gView.setSceneRect(0, 0, width, height)

            self.videoItem = QGraphicsVideoItem()
            self.videoItem.setAspectRatioMode(
                Qt.AspectRatioMode.KeepAspectRatio)
            self.gScene.addItem(self.videoItem)
            self.videoItem.mouseMoveEvent = self.gView.mouseMoveEvent
            self.videoItem.setSize(QSizeF(width, height))

            self.mediaPlayer.setVideoOutput(self.videoItem)
            self.mediaPlayer.setSource(QUrl.fromLocalFile(self.videoFile))

            self.gView.labelSize = width / 50

            self.playButton.setEnabled(True)
            # self.gView.setViewport(QOpenGLWidget())
            self.mediaPlayer.pause()

    def exitCall(self):
        # sys.exit(app.exec())
        # self.mediaPlayer.pause()
        self.close()

    def play(self):
        # self.gView.fitInView(self.videoItem, Qt.KeepAspectRatio)

        if self.mediaPlayer.playbackState(
        ) == QMediaPlayer.PlaybackState.PlayingState:
            self.mediaPlayer.pause()

        else:
            self.mediaPlayer.play()

    def changePlayRate(self):
        if self.mediaPlayer.playbackRate() < 2:
            r = self.mediaPlayer.playbackRate() + 0.5
            self.mediaPlayer.setPlaybackRate(r)
            self.changePlayRateBtn.setText('{:g}x'.format(r))
            self.statusBar.showMessage('Play back rate = {:g}x'.format(r),
                                       2000)
        elif self.mediaPlayer.playbackRate() == 2:
            self.mediaPlayer.setPlaybackRate(1)
            self.changePlayRateBtn.setText('{}x'.format(1))
            self.statusBar.showMessage('Play back rate = {}x'.format(1), 2000)

    def mediaStateChanged(self, state):
        if self.mediaPlayer.playbackState(
        ) == QMediaPlayer.PlaybackState.PlayingState:
            self.playButton.setIcon(self.style().standardIcon(
                QStyle.StandardPixmap.SP_MediaPause))
        else:
            self.playButton.setIcon(self.style().standardIcon(
                QStyle.StandardPixmap.SP_MediaPlay))

    def positionChanged(self, position):
        self.positionSlider.setValue(position)
        s, m, h = self.convertMillis(position)
        self.videoCurrentDatetime = self.videoStartDatetime + \
                                    timedelta(hours=h, minutes=m, seconds=s)
        self.timerLabel.setText('{:02d}:{:02d}:{:02d}'.format(
            self.videoCurrentDatetime.time().hour,
            self.videoCurrentDatetime.time().minute,
            self.videoCurrentDatetime.time().second))

    def durationChanged(self, duration):
        self.positionSlider.setRange(0, duration)

        # s, m, h = self.convertMillis(duration)
        # self.durationLabel.setText('{:02d}:{:02d}'.format(m, s))

    def setPosition(self, position):
        self.mediaPlayer.setPosition(position)

    def handleError(self):
        self.playButton.setEnabled(False)
        # self.errorLabel.setText("Error: " + self.mediaPlayer.errorString())

    def openObsToolbox(self):
        if not self.obsTb.isVisible():
            self.obsTb.show()

    def drawingClick(self):
        # if self.sender() == self.drawLineAction:
        #     self.labelingAction.setChecked(False)
        # else:
        #     self.drawLineAction.setChecked(False)
        cursor = QCursor(Qt.CursorShape.CrossCursor)
        self.gView.setCursor(cursor)

    def generateMask(self):
        if not self.sender().isChecked():
            self.gView.unsetCursor()
            return
        cursor = QCursor(Qt.CursorShape.CrossCursor)
        self.gView.setCursor(cursor)

        # dbfilename = self.obsTb.dbFilename
        # if dbfilename != None:
        #     self.session = connectDatabase(dbfilename)
        # else:
        #     msg = QMessageBox()
        #     msg.setIcon(QMessageBox.Information)
        #     msg.setText('The database file is not defined.')
        #     msg.setInformativeText('In order to set the database file, open the Observation Toolbox')
        #     msg.setIcon(QMessageBox.Critical)
        #     msg.exec_()
        #     return

        # if self.gView.unsavedLines == [] and self.gView.unsavedZones == [] and \
        #         self.gView.unsavedPoints == []:
        #     QMessageBox.information(self, 'Save', 'There is no polygon to generate mask!')
        #     return

    def saveMaskFile(self):
        creation_datetime, width, height = getVideoMetadata(self.videoFile)
        item = self.gView.gPolyItem  #self.gView.unsavedZones[0]
        mask_polygon = item.polygon()
        xy = []
        for p in mask_polygon:
            xy.append((p.x(), p.y()))

        img = Image.new('RGB', (width, height), color='black')
        img1 = ImageDraw.Draw(img)
        img1.polygon(xy, fill="white", outline="white")

        fileName, _ = QFileDialog.getSaveFileName(self, "Open database file",
                                                  QDir.homePath(),
                                                  "PNG files (*.png)")
        if fileName != '':
            img.save(fileName)

        self.gView.scene().removeItem(item)
        self.gView.unsavedZones = []

    def openProject(self):
        self.projectFile, _ = QFileDialog.getOpenFileName(
            self, "Open project file", QDir.homePath(), "Project (*.prj)")

        if self.projectFile == '':
            return

        self.saveProjectAction.setEnabled(True)
        self.maskGenAction.setEnabled(True)
        # self.loadGraphAction.setEnabled(True)
        # self.saveGraphAction.setEnabled(True)
        # self.drawPointAction.setEnabled(True)
        # self.drawLineAction.setEnabled(True)
        # self.drawZoneAction.setEnabled(True)

        tree = ET.parse(self.projectFile)
        root = tree.getroot()
        gItems = []
        for elem in root:
            subEelTexts = {}
            for subelem in elem:
                subEelTexts[subelem.tag] = subelem.text
            gItems.append([elem.tag, subEelTexts])

        for key in gItems:
            if key[0] == 'database':
                item = key[1]
                if item['fileName'] is not None:
                    self.obsTb.dbFilename = item['fileName']
                    self.obsTb.opendbFile()

            elif key[0] == 'video':
                item = key[1]
                if item['fileName'] is not None:
                    self.videoFile = item['fileName']
                    self.openVideoFile()
                    self.mediaPlayer.setPosition(int(item['sliderValue']))
                    if item['fileName'] is not None:
                        self.loadGraphics()

            elif key[0] == 'trajectory':
                item = key[1]
                if item['metadata'] != None:
                    self.obsTb.mdbFileLedit.setText(item['metadata'])
                    self.obsTb.openMdbFile()
                    self.obsTb.siteNameCombobx.setCurrentIndex(
                        int(item['site']))
                    self.obsTb.camViewCombobx.setCurrentIndex(
                        int(item['cam_view']))
                    self.obsTb.trjDbCombobx.setCurrentIndex(
                        int(item['traj_db']))

            elif key[0] == 'window':
                item = key[1]
                x, y = item['mainWin_pos'].split(',')
                w, h = item['mainWin_size'].split(',')
                self.setGeometry(int(x), int(y), int(w), int(h))
                if item['obsTbx_open'] == 'True':
                    self.obsTb.show()
                    x, y = item['obsTbx_pos'].split(',')
                    w, h = item['obsTbx_size'].split(',')
                    self.obsTb.setGeometry(int(x), int(y), int(w), int(h))

        # self.setWindowTitle('{} - {}'.format(os.path.basename(self.videoFile),
        #                                      os.path.basename(self.projectFile)))

    def saveProject(self):

        if self.projectFile == '':
            fileDir = QDir.homePath()
        else:
            fileDir = self.projectFile

        self.projectFile, _ = QFileDialog.getSaveFileName(
            self, "Save project file", fileDir, "Project (*.prj)")
        # fileName = "/Users/Abbas/project.xml"
        if self.projectFile == '':
            return

        file = QFile(self.projectFile)
        if (not file.open(QIODevice.OpenModeFlag.WriteOnly
                          | QIODevice.OpenModeFlag.Text)):
            return

        xmlWriter = QXmlStreamWriter(file)
        xmlWriter.setAutoFormatting(True)
        xmlWriter.writeStartDocument()

        xmlWriter.writeStartElement('project')

        xmlWriter.writeStartElement('database')
        xmlWriter.writeTextElement("fileName", self.obsTb.dbFilename)
        xmlWriter.writeEndElement()

        xmlWriter.writeStartElement('video')
        xmlWriter.writeTextElement(
            "fileName",
            self.videoFile)  #mediaPlayer.media().canonicalUrl().path())
        xmlWriter.writeTextElement("sliderValue",
                                   str(self.mediaPlayer.position()))
        xmlWriter.writeEndElement()

        xmlWriter.writeStartElement('trajectory')
        xmlWriter.writeTextElement("metadata", self.obsTb.mdbFileLedit.text())
        xmlWriter.writeTextElement(
            "site", str(self.obsTb.siteNameCombobx.currentIndex()))
        xmlWriter.writeTextElement(
            "cam_view", str(self.obsTb.camViewCombobx.currentIndex()))
        xmlWriter.writeTextElement("traj_db",
                                   str(self.obsTb.trjDbCombobx.currentIndex()))
        xmlWriter.writeEndElement()

        xmlWriter.writeStartElement('window')
        xmlWriter.writeTextElement(
            "mainWin_size", "{},{}".format(int(self.width()),
                                           int(self.height())))
        xmlWriter.writeTextElement(
            "mainWin_pos", "{},{}".format(int(self.x()), int(self.y())))
        xmlWriter.writeTextElement("obsTbx_open", str(self.obsTb.isVisible()))
        xmlWriter.writeTextElement(
            "obsTbx_size", "{},{}".format(int(self.obsTb.width()),
                                          int(self.obsTb.height())))
        xmlWriter.writeTextElement(
            "obsTbx_pos", "{},{}".format(int(self.obsTb.x()),
                                         int(self.obsTb.y())))
        xmlWriter.writeEndElement()

        xmlWriter.writeEndElement()

        self.setWindowTitle('{} - {}'.format(
            os.path.basename(self.videoFile),
            os.path.basename(self.projectFile)))
        if self.obsTb.dbFilename != None:
            self.obsTb.setWindowTitle('{} - {}'.format(
                os.path.basename(self.obsTb.dbFilename),
                os.path.basename(self.projectFile)))

    def saveGraphics(self):
        dbfilename = self.obsTb.dbFilename
        if dbfilename != None:
            self.session = connectDatabase(dbfilename)
        else:
            msg = QMessageBox()
            msg.setIcon(QMessageBox.Information)
            msg.setText('The database file is not defined.')
            msg.setInformativeText(
                'In order to set the database file, open the Observation Toolbox'
            )
            msg.setIcon(QMessageBox.Critical)
            msg.exec_()
            return

        if self.gView.unsavedLines == [] and self.gView.unsavedZones == [] and \
                self.gView.unsavedPoints == []:
            QMessageBox.information(self, 'Save',
                                    'No new graphical items to save!')
            return

        for item in self.gView.unsavedLines:
            x1 = round(item.line().x1(), 2)
            y1 = round(item.line().y1(), 2)
            x2 = round(item.line().x2(), 2)
            y2 = round(item.line().y2(), 2)
            line = Line(None, None, x1, y1, x2, y2)
            self.session.add(line)
            self.session.flush()
            label = self.generate_itemGroup([x1, x2], [y1, y2], line.idx)
            self.gView.scene().addItem(label)

        for item in self.gView.unsavedZones:
            xs = []
            ys = []
            for p in item.polygon():
                xs.append(round(p.x(), 2))
                ys.append(round(p.y(), 2))
            zone = Zone(None, None, xs, ys)
            self.session.add(zone)
            self.session.flush()

            label = self.generate_itemGroup(xs, ys, zone.idx)
            self.gView.scene().addItem(label)

        for item in self.gView.unsavedPoints:
            x = round(item.rect().center().x(), 2)
            y = round(item.rect().center().y(), 2)

            point = Point(x, y)
            self.session.add(point)
            self.session.flush()

            label = self.generate_itemGroup([x], [y], point.idx)
            self.gView.scene().removeItem(item)
            self.gView.scene().addItem(label)

        QMessageBox.information(
            self, 'Save',
            '{} point(s), {} line(s) and {} zone(s) saved to database successfully!'
            .format(len(self.gView.unsavedPoints),
                    len(self.gView.unsavedLines),
                    len(self.gView.unsavedZones)))
        self.gView.unsavedLines = []
        self.gView.unsavedZones = []
        self.gView.unsavedPoints = []

        self.session.commit()

    def generate_itemGroup(self, xs, ys, label, type):
        gItemGroup = QGraphicsItemGroup()

        pointBbx = QRectF()
        pointBbx.setSize(QSizeF(self.gView.labelSize, self.gView.labelSize))

        textLabel = QGraphicsTextItem(label)

        if len(xs) == 1:
            pointBbx.moveCenter(QPointF(xs[0], ys[0]))
            textLabel.setPos(xs[0] - (textLabel.boundingRect().width() / 2),
                             ys[0] - (textLabel.boundingRect().height() / 2))

            pointShape = QGraphicsEllipseItem(pointBbx)
            shapeColor = Qt.GlobalColor.white
            textColor = Qt.GlobalColor.black
            tooltip = 'P{}:{}'
        elif len(xs) == 2:
            pointBbx.moveCenter(QPointF(xs[1], ys[1]))
            textLabel.setPos(xs[1] - (textLabel.boundingRect().width() / 2),
                             ys[1] - (textLabel.boundingRect().height() / 2))

            r, g, b = np.random.choice(range(256), size=3)
            line_item = QGraphicsLineItem(xs[0], ys[0], xs[1], ys[1])
            line_item.setPen(
                QPen(QColor(r, g, b, 128), self.gView.labelSize / 6))
            gItemGroup.addToGroup(line_item)

            # line_end = QGraphicsEllipseItem(xs[1], ys[1],
            #                                 int(self.gView.labelSize/3), int(self.gView.labelSize/3))
            # line_end.setPen(QPen(QColor(r, g, b), 0.5))
            # line_end.setBrush(QBrush(QColor(r, g, b)))
            # gItemGroup.addToGroup(line_end)

            pointShape = QGraphicsEllipseItem(pointBbx)
            shapeColor = QColor(r, g, b, 128)
            textColor = Qt.GlobalColor.black
            tooltip = 'L{}:{}'
            # textLabel.setRotation(np.arctan((ys[1] - ys[0])/(xs[1] - xs[0]))*(180/3.14))
        else:
            pointBbx.moveCenter(QPointF(np.mean(xs), np.mean(ys)))
            textLabel.setPos(
                np.mean(xs) - (textLabel.boundingRect().width() / 2),
                np.mean(ys) - (textLabel.boundingRect().height() / 2))

            points = [QPointF(x, y) for x, y in zip(xs, ys)]
            polygon = QPolygonF(points)
            r, g, b = np.random.choice(range(256), size=3)
            zone_item = QGraphicsPolygonItem(polygon)
            zone_item.setPen(QPen(QColor(r, g, b), self.gView.labelSize / 10))
            zone_item.setBrush(QBrush(QColor(r, g, b, 40)))
            gItemGroup.addToGroup(zone_item)

            pointShape = QGraphicsRectItem(pointBbx)
            shapeColor = Qt.GlobalColor.darkBlue
            textColor = Qt.GlobalColor.white
            tooltip = 'Z{}:{}'

        pointShape.setPen(QPen(Qt.GlobalColor.white, 0.5))
        pointShape.setBrush(QBrush(shapeColor))
        # self.gView.scene().addEllipse(pointBbx, QPen(Qt.white, 0.5), QBrush(Qt.black))
        gItemGroup.setToolTip(tooltip.format(label, type))
        gItemGroup.addToGroup(pointShape)

        labelFont = QFont()
        labelFont.setPointSize(round(self.gView.labelSize / 2))
        labelFont.setBold(True)

        textLabel.setFont(labelFont)
        textLabel.setDefaultTextColor(textColor)

        gItemGroup.addToGroup(textLabel)
        return gItemGroup

    def loadGraphics(self):
        dbfilename = self.obsTb.dbFilename
        if dbfilename != None:
            self.session = connectDatabase(dbfilename)
        else:
            msg = QMessageBox()
            # msg.setIcon(QMessageBox.Icon.Information)
            msg.setText('The database file is not defined.')
            msg.setInformativeText(
                'In order to set the database file, open the Observation Toolbox'
            )
            msg.setIcon(QMessageBox.Icon.Critical)
            msg.exec()
            return

        for gitem in self.gView.scene().items():
            if isinstance(gitem, QGraphicsItemGroup):
                self.gView.scene().removeItem(gitem)

        q_line = self.session.query(Line)
        q_zone = self.session.query(Zone)
        if q_line.all() == [] and q_zone.all() == []:
            QMessageBox.information(self, 'Warning!',
                                    'There is no graphics to load!')
            return

        line_items = []
        for line in q_line:
            p1 = line.points[0]
            p2 = line.points[1]

            if line.type != None:
                lineType = line.type.name
            else:
                lineType = None
            gItmGroup = self.generate_itemGroup([p1.x, p2.x], [p1.y, p2.y],
                                                str(line.idx), lineType)
            self.gScene.addItem(gItmGroup)

            line_items.append(str(line.idx))

        self.obsTb.line_list_wdgt.clear()
        self.obsTb.line_list_wdgt.addItems(line_items)
        self.obsTb.line_list_wdgt.setCurrentRow(0)

        self.obsTb.line_newRecButton.setEnabled(False)
        self.obsTb.line_saveButton.setEnabled(True)
        self.obsTb.line_saveButton.setText('Edit line(s)')
        self.obsTb.line_saveButton.setIcon(QIcon('icons/edit.png'))

        zone_items = []
        for zone in q_zone:
            if zone.type != None:
                zoneType = zone.type.name
            else:
                zoneType = None
            gItmGroup = self.generate_itemGroup(
                [point.x for point in zone.points],
                [point.y for point in zone.points], str(zone.idx), zoneType)
            self.gScene.addItem(gItmGroup)

            zone_items.append(str(zone.idx))

        self.obsTb.zone_list_wdgt.clear()
        self.obsTb.zone_list_wdgt.addItems(zone_items)
        self.obsTb.zone_list_wdgt.setCurrentRow(0)

        self.obsTb.zone_newRecButton.setEnabled(False)
        self.obsTb.zone_saveButton.setEnabled(True)
        self.obsTb.zone_saveButton.setText('Edit zone(s)')
        self.obsTb.zone_saveButton.setIcon(QIcon('icons/edit.png'))

    @staticmethod
    def convertMillis(millis):
        seconds = int(millis / 1000) % 60
        minutes = int(millis / (1000 * 60)) % 60
        hours = int(millis / (1000 * 60 * 60)) % 24
        return seconds, minutes, hours
class MainWindow(QMainWindow):

    # Create a QSettings object rather than storing values. Pass in a company 
    # name and an application name
    settings = QSettings("Custom GUIs", "Image Manager GUI")
    #print(settings.fileName()) # NOTE: Uncomment to print the path to settings

    images_path = "Images" # File path to the Images directory
    image_dir = QDir(images_path)
    info_dialog = None # Create variable for modeless dialog

    def __init__(self):
        """MainWindow Constructor for Image Manager"""
        super().__init__() # Constructor for QMainWindow
        self.initializeUI()

    def initializeUI(self):
        """Set up the GUI's main window and load initial settings and data."""
        self.setWindowTitle("Image Manager")
        self.setObjectName("ImageManager")

        # Set up the main window, menu, dock widgets, and initialize the GUI's settings
        self.setUpMainWindow()
        self.displayImagePreviewDock()
        self.createActions()
        self.createMenus()
        self.loadStoredImageData()
        self.getInitialSettings()
        self.show() # Display the main window

    def setUpMainWindow(self):
        """Set up the application's main window containing the QListWidget."""
        self.image_view_lw = ImageViewerListWidget(self)
        # Use signals/slots to interact with the list widget 
        self.image_view_lw.itemSelectionChanged.connect(self.updateDockInfo)
        self.image_view_lw.itemDoubleClicked.connect(self.displayImageInfoDialog)
        # Use the list widget's internal model to enable/disable menu items
        self.image_view_lw.model().rowsInserted.connect(self.manageMenuItems)
        self.image_view_lw.model().rowsRemoved.connect(self.manageMenuItems)

        self.setCentralWidget(self.image_view_lw)

    def createActions(self):
        """Create the application's menu actions."""
        # Create actions for File menu
        self.import_act = QAction("Import Images...", self, triggered=self.importImages)
        self.import_act.setShortcut("Ctrl+I") 

        self.preferences_act = QAction("Preferences...", self, triggered=self.showPreferencesDialog)

        self.quit_act = QAction("Quit Task Manager", self, triggered=self.close)
        self.quit_act.setShortcut(QKeySequence.StandardKey.Quit) # Ctrl+Q

        # Create actions for Edit menu
        self.select_all_act = QAction("Select All", self, triggered=self.image_view_lw.selectAll)
        self.select_all_act.setShortcut(QKeySequence.StandardKey.SelectAll) # Ctrl+A

        self.delete_act = QAction("Delete Images", self, triggered=self.deleteImages)
        self.delete_act.setShortcut(QKeySequence.StandardKey.Delete) # Del
        self.delete_act.setEnabled(False)

        # Create actions for View menu
        # Handle the visibility of the dock widget that displays images
        self.show_dock_act = self.image_preview_dock.toggleViewAction()
        self.show_dock_act.setText("Show Image View")  

        self.sort_ascend_act = QAction("Sort Ascending", self,
            triggered=lambda: self.sortListItems(Qt.SortOrder.AscendingOrder))
        self.sort_ascend_act.setEnabled(False)

        self.sort_descend_act = QAction("Sort Descending", self,
            triggered=lambda: self.sortListItems(Qt.SortOrder.DescendingOrder))
        self.sort_descend_act.setEnabled(False)

        self.fullscreen_act = QAction("Show Fullscreen", self, 
            triggered=self.displayFullScreen, checkable=True)

        # Create actions for Help menu
        self.about_act = QAction("About Image Manager", 
            self, triggered=self.showAboutDialog)  

    def createMenus(self):
        """Create the application's menu."""
        if QSysInfo.productType() == "macos" or "osx":
            self.menuBar().setNativeMenuBar(False)

        self.file_menu = self.menuBar().addMenu("&File")
        self.file_menu.addAction(self.import_act)
        self.file_menu.addSeparator()
        self.file_menu.addAction(self.preferences_act)
        self.file_menu.addSeparator()
        self.file_menu.addAction(self.quit_act)

        self.edit_menu = self.menuBar().addMenu("&Edit")
        self.edit_menu.addAction(self.select_all_act)
        self.edit_menu.addSeparator()
        self.edit_menu.addAction(self.delete_act)  

        self.view_menu = self.menuBar().addMenu("&View")
        self.view_menu.addAction(self.show_dock_act)  
        self.view_menu.addSeparator()
        self.view_menu.addAction(self.sort_ascend_act)
        self.view_menu.addAction(self.sort_descend_act)
        self.view_menu.addSeparator()
        self.view_menu.addAction(self.fullscreen_act)       

        self.help_menu = self.menuBar().addMenu("&Help")
        self.help_menu.addAction(self.about_act)        

    def manageMenuItems(self, parent, first, last):
        """Slot to enable/disable menu items if rows have been 
        added/deleted to QListWidget. The rowsInserted() and 
        rowsRemoved() that trigger this slot return the 'parent',
        'first', and 'last' values, but they are not used in 
        this method."""
        if self.image_view_lw.count() == 0:
            self.delete_act.setEnabled(False)
            self.sort_ascend_act.setEnabled(False)
            self.sort_descend_act.setEnabled(False)
        elif self.image_view_lw.count() > 0:
            self.delete_act.setEnabled(True)
            self.sort_ascend_act.setEnabled(True)
            self.sort_descend_act.setEnabled(True)   

    def displayImagePreviewDock(self):
        """Dock widget that displays a selected image in a scrollable 
        area and uses its file name as the dock's title."""
        self.image_preview_dock = QDockWidget()
        self.image_preview_dock.setObjectName("PreviewDock")
        self.image_preview_dock.setWindowTitle("Show Image View")
        self.image_preview_dock.setAllowedAreas(Qt.DockWidgetAreas.RightDockWidgetArea)

        self.display_image_label = QLabel()
        self.display_image_label.setAlignment(Qt.Alignment.AlignCenter)

        self.view_scroll_area = QScrollArea()
        self.view_scroll_area.setMinimumWidth(300)
        self.view_scroll_area.setWidgetResizable(True)

        self.image_preview_dock.setWidget(self.view_scroll_area)
        # Set initial location of dock widget in the main window
        self.addDockWidget(Qt.DockWidgetAreas.RightDockWidgetArea, self.image_preview_dock)   

    def updateDockInfo(self):
        """Slot to update the image that the dock widget displays."""
        # Only display an image if one item is selected
        if (len(self.image_view_lw.selectedItems()) == 0 or 
            len(self.image_view_lw.selectedItems()) > 1):
            self.image_preview_dock.setWindowTitle("Show Image View")
            self.display_image_label.clear()
        else:
            curr_item = self.image_view_lw.currentItem()
            self.image_preview_dock.setWindowTitle(curr_item.text())
            self.show_dock_act.setText("Show Image View") 

            # Get the current height of the dock widget
            dock_height = self.image_preview_dock.height()
            # Get the size of the original image/item
            icon_size = curr_item.icon().availableSizes()[0]
            icon_width = icon_size.width()

            # Return a pixmap from the item's icon and display in the scroll area
            pixmap = curr_item.icon().pixmap(QSize(icon_width, dock_height)) 
            self.display_image_label.setPixmap(pixmap)
            self.view_scroll_area.setWidget(self.display_image_label) 
        
    def importImages(self):
        """Import the images a user selects, remove duplicates, and add
        items to the QListWidget."""
        duplicate_images = [] # Store the names of duplicate images
        image_paths, _ = QFileDialog.getOpenFileNames(self, 
            "Select Image Files", "", "Images (*.png *.xpm *.jpg *.jpeg)")

        if image_paths:
            if self.image_dir.exists():
                for image_path in image_paths:
                    # Pass image path to QFileInfo object
                    image_info = QFileInfo(image_path) 
                    file_name = image_info.fileName()
                    item_name = image_info.baseName()

                    # Copy the files into the Images directory, check for files 
                    # with the same name
                    new_name = self.image_dir.absolutePath() + f"/{file_name}"
                    file_exists = QFile.copy(image_path, new_name)
                    if file_exists == False:
                        duplicate_images.append(image_path)
                    else:
                        self.createListItems(image_path, item_name, image_info, new_name)
                        if self.is_delete_checked == True: # Handle deleting images
                            QFile.moveToTrash(image_path) 
            else:
                QMessageBox.warning(self, "Images Location Not Found",
                    """<p>The Images Location cannot be found. Restart the application to
                    recreate the directory.</p>""")

        # Display a custom dialog to inform the user of duplicate images
        if len(duplicate_images) != 0:
            duplicates_dialog = QMessageBox(self)
            duplicates_dialog.setIcon(QMessageBox.Icon.Information)
            duplicates_dialog.setWindowTitle("Duplicate Images")
            duplicates_dialog.setText("""<p>Some images were not imported because 
                they already exist.</p>""")

            details = '\n'.join([item for item in duplicate_images])
            duplicates_dialog.setDetailedText(details)
            duplicates_dialog.exec()

            duplicate_images.clear() # Clear the list 
        # Check if window is still in focus. If not, give it focus
        if self.isActiveWindow() == False:
            self.activateWindow()

    def createListItems(self, image_path, item_name, image_info, new_name=None):
        """Simple method for creating QListWidgetItem objects. 
        'image_path': the path to the file.
        'item_name': the base name used for QListWidgetItem objects.
        'image_info': the QFileInfo object.
        'new_name': used when importing new photos, making sure the program
        points to the new image location."""
        list_item = QListWidgetItem(QIcon(image_path), item_name)
        self.image_view_lw.setIconSize(QSize(80, 80))
        self.image_view_lw.addItem(list_item)
        if new_name != None:
            image_info.setFile(new_name)
        self.image_view_lw.images_info_list.append(image_info) 

    def sortListItems(self, order): 
        """First, sort the items in the QListWidget using sortItems(). Then handle 
        sorting the QFileInfo objects in the images_info_list using Python's sort() to 
        match how the QListWidget sorts items."""
        self.image_view_lw.sortItems(order)
        if order == Qt.SortOrder.AscendingOrder:
            self.image_view_lw.images_info_list.sort(key=lambda item: (item.baseName().upper(), item.baseName()[0].islower()))
        elif order == Qt.SortOrder.DescendingOrder:
            self.image_view_lw.images_info_list.sort(reverse=True, key=lambda item: (item.baseName().upper(), item.baseName()[0].islower()))

    def deleteImages(self):
        """Delete images from the QListWidget and from where images
        are stored on disk."""
        number_of_photos = len(self.image_view_lw.selectedItems())
        answer = QMessageBox.warning(self, "Delete Image(s)", 
            f"Are you sure you want to delete {number_of_photos} image(s)?", 
            QMessageBox.StandardButtons.No | QMessageBox.StandardButtons.Yes, 
            QMessageBox.StandardButtons.No)

        if answer == QMessageBox.StandardButtons.Yes:
            for item in self.image_view_lw.selectedItems():
                index = self.image_view_lw.indexFromItem(item).row()
                # Get the image's information before deletion
                image_info = self.image_view_lw.images_info_list[index] 

                # Remove items from the Images directory, from the list widget, 
                # and the images_info_list that stores QFileInfo objects
                QFile.moveToTrash(image_info.absoluteFilePath()) 
                self.image_view_lw.takeItem(index)
                del self.image_view_lw.images_info_list[index] 
                del item        

    def loadStoredImageData(self):
        """Load images from the Images directory. The Images directory is 
        created the first time running the application."""
        if not(self.image_dir.exists()):
            QDir().mkdir(self.images_path)
        elif self.image_dir.exists():
            # Create a list of the files in the Images directory
            images = self.image_dir.entryInfoList(QDir.Filters.AllEntries | QDir.Filters.NoDotAndDotDot)
            for image in images: 
                # Imported files are QFileInfo objects
                item_name = image.baseName()
                path = image.absoluteFilePath()
                self.createListItems(path, item_name, image) 

    def displayImageInfoDialog(self, item): 
        """Display image metadata in a modeless dialog box. 'index' is the index of 
        the item that is clicked on."""
        index = self.image_view_lw.indexFromItem(item).row()
        if self.info_dialog == None: 
            self.info_dialog = ImageInfoDialog(self, self.image_view_lw.images_info_list[index])
        elif self.info_dialog != None:
            self.info_dialog.close()
            self.info_dialog = ImageInfoDialog(self, self.image_view_lw.images_info_list[index])
        self.info_dialog.show()         

    def showPreferencesDialog(self):
        """Display the application's preferences dialog. Save the value of the 
        delete_images_checkbox in the settings."""
        prefs_dialog = PreferencesDialog(self, self.image_dir, self.is_delete_checked)
        response = prefs_dialog.exec()

        if response == 1: # QDialog.DialogCode.Accepted == 1
            self.settings.setValue("delete_images", prefs_dialog.delete_images_checkbox.isChecked())
            self.is_delete_checked = self.settings.value("delete_images", type=bool)

    def displayFullScreen(self, state):
        """Check the state of checkable fullscreen_act. If True, show the 
        main window as fullscreen."""
        if state: self.showFullScreen()
        else: self.showNormal()

    def showAboutDialog(self):
        """Display the application's about dialog."""
        QMessageBox.about(self, "Image Manager",
            """<h3 style='text-align:center'>Image Manager</h3>
            <p style='font-weight: normal'>The <b><i>Image Manager GUI</i></b> 
            demonstrates how to build an application for managing photos. This 
            program also examines some of the common features found in many GUIs.</p>
            <p style='font-weight: normal'>This application is part of
            <b><i>Building Custom UIs with PyQt</i></b>.</p>
            <p style='font-weight: normal'>Designed by: <b>Joshua Willman</b></p>
            <p style='font-weight: normal'>Icons created by: <b>Joshua Willman</b></p>""")

    def getInitialSettings(self):
        """Get initial settings of the application using QSettings upon startup."""
        position = self.settings.value("position", QPoint(200, 0))
        size = self.settings.value("size", QSize(800, 500))
        self.is_delete_checked = self.settings.value("delete_images", type=bool) 
        # restoreState() is used here to restore the image_preview_dock widget
        self.restoreState(self.settings.value("window_state", bytes(QByteArray())))

        self.resize(size)
        self.move(position)
        return self.is_delete_checked
    
    def saveSettings(self):
        """Save the settings of the application."""
        self.settings.setValue("position", self.pos())
        self.settings.setValue("size", self.size())
        self.settings.setValue("window_state", self.saveState())

    def closeEvent(self, event):
        """Save the application's settings in the closeEvent()."""
        self.saveSettings()
        event.setAccepted(True)
class MainWindow(QMainWindow):

    def __init__(self):
        """ MainWindow Constructor """
        super().__init__()
        self.initializeUI()
        
    def initializeUI(self):
        """Set up the GUI's main window."""        
        self.setMinimumSize(700, 400)
        self.setWindowTitle("GIF and Image Viewer")

        # Set up the main window, menu, and dock widget
        self.setUpMainWindow()
        self.displayFilesDock()
        self.createActions()
        self.createMenus()
        self.createToolbar()
        self.show() # Display the main window

    def setUpMainWindow(self):
        """Set up the application's main window and widgets."""
        self.movie = QMovie() # Create movie object
        self.movie.stateChanged.connect(self.changeButtonStates)
        
        self.media_label = QLabel() # Create label to place images/GIFs on
        self.media_label.setPixmap(QPixmap("icons/image_label.png"))
        self.media_label.setFrameShape(QFrame.Shape.StyledPanel)
        self.media_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        # Prevent the label from resizing and affecting the 
        # dock widget when viewing images
        self.media_label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Expanding)

        self.setCentralWidget(self.media_label)

    def createActions(self):
        """Create the application's menu actions."""
        # Create actions for File menu
        self.open_act = QAction("Open...", self, triggered=self.openDirectory)
        self.open_act.setShortcut(QKeySequence.StandardKey.Open) 

        self.quit_act = QAction("Quit Viewer", self, triggered=self.close)
        self.quit_act.setShortcut(QKeySequence.StandardKey.Quit) # Ctrl+Q

        # Create actions for View menu
        # Handle the visibility of the dock widget
        self.show_dock_act = self.files_dock.toggleViewAction()
        self.show_dock_act.setText("Show Media Folder") 

        # Create actions for the toolbar (These actions could also be 
        # added to the GUI's menu bar or to a context menu)
        self.play_act = QAction(QIcon("icons/play.png"), "Play", self, triggered=self.startMovie)
        self.pause_act = QAction(QIcon("icons/pause.png"), "Pause", self, triggered=self.pauseMovie)
        self.stop_act = QAction(QIcon("icons/stop.png"), "Stop/Reset", self, triggered=self.stopMovie)
        self.disableMovieButtons()

    def createMenus(self):
        """Create the application's menu."""
        # Make the toolbar appear in the main window for macOS users.
        # More information about this in Chapter 2 - Building the Foundation for GUIs
        if QSysInfo.productType() == "macos" or "osx":
            self.menuBar().setNativeMenuBar(False)

        self.file_menu = self.menuBar().addMenu("&File")
        self.file_menu.addAction(self.open_act)
        self.file_menu.addSeparator()
        self.file_menu.addAction(self.quit_act)

        self.view_menu = self.menuBar().addMenu("&View")
        self.view_menu.addAction(self.show_dock_act)      

    def createToolbar(self):
        """Create the application's toolbar for playing GIFs."""
        toolbar = self.addToolBar("GIF Controls Toolbar")
        toolbar.setIconSize(QSize(24, 24))

        # Add actions to the toolbar
        toolbar.addAction(self.play_act)
        toolbar.addAction(self.pause_act)
        toolbar.addAction(self.stop_act)

    def displayFilesDock(self):
        """Dock widget that displays the movie file location in a QLineEdit 
        widget, provides a button for opening directories with images and GIFs, 
        and shows the media from the selected folder in a QTreeWidget."""
        self.files_dock = QDockWidget()
        self.files_dock.setWindowTitle("Media Folder")
        self.files_dock.setAllowedAreas(Qt.DockWidgetArea.LeftDockWidgetArea)

        folder_label = QLabel("Media Location:")
        # The QLineEdit widget is set to read-only as a quick way to display 
        # the folder path
        self.folder_line = QLineEdit()
        self.folder_line.setMinimumWidth(100)
        self.folder_line.setReadOnly(True)

        open_button = QPushButton("Open...")
        open_button.clicked.connect(self.openDirectory)

        folder_h_box = QHBoxLayout()
        folder_h_box.addWidget(folder_label)
        folder_h_box.addWidget(self.folder_line)
        folder_h_box.addWidget(open_button)

        self.files_tree = QTreeWidget()
        self.files_tree.setHeaderLabel("Media Files")
        self.files_tree.setColumnCount(1)
        self.files_tree.itemSelectionChanged.connect(self.displayMediaFile)

        # Set up the dock's layout
        dock_v_box = QVBoxLayout()
        dock_v_box.addLayout(folder_h_box)
        dock_v_box.addWidget(self.files_tree)

        dock_container = QWidget()
        dock_container.setLayout(dock_v_box)

        self.files_dock.setWidget(dock_container)
        self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.files_dock)

    def openDirectory(self):
        """Open a QFileDialog for selecting a local directory. Only display 
        image and GIF files.""" 
        directory = QFileDialog.getExistingDirectory(self, "Choose Directory", "", 
            QFileDialog.Option.ShowDirsOnly) # Specify the file mode to only select directories

        if directory:
            # Display the selected folder text in the QLineEdit. There are other, and 
            # possibly better, ways to handle displaying the text, but for simplicity, 
            # this is the method used in this GUI
            self.folder_line.setText(directory)

            # Get the contents of the directory and only display files with
            # specified extensions
            file_dir = QDir(directory)
            filters = ["*.gif", "*.png", "*.jpg", "*.jpeg"]
            files_info = file_dir.entryInfoList(filters, QDir.Filter.Files)

            # Clear the contents of the QTreeWidget
            if self.files_tree.model().rowCount() > 0:         
                # NOTE: Since files_tree is connected to the itemSelectionChanged signal,
                # using the clear() method below will also cause the signal to be emitted. 
                # This causes an undesired issue because of how the items are deleted. To avoid
                # the signal being called, QObject.blockSignals() will halt files_tree from 
                # emitting signals while removing items
                self.files_tree.blockSignals(True)
                # Use the convenience method clear() provided by QTreeWidget to remove 
                # all items and selections
                self.files_tree.clear() 
                self.files_tree.blockSignals(False)

                # Reset the QLabel and its image, and disable the movie buttons (in case the 
                # last item selected was a GIF)
                self.media_label.clear()
                self.media_label.setPixmap(QPixmap("icons/image_label.png"))
                self.disableMovieButtons()

            # Create items for each file and add them to the tree
            for file in files_info:
                item = QTreeWidgetItem()
                item.setText(0, file.fileName())
                self.files_tree.addTopLevelItem(item)

    def displayMediaFile(self): 
        """Display the selected media file on the QLabel. Used instead of 
        the itemClicked signal to handle whether the user clicks on an item
        or if arrow keys are used to navigate the items in the tree."""  
        # Check the state of the movie, i.e. check if a movie is playing. If it is, 
        # stop the movie
        if self.movie.state() == QMovie.MovieState.Running:
            self.stopMovie()

        # Get the text from the QLineEdit, folder_line, and concatenate it with 
        # the selected item's text
        column = self.files_tree.currentColumn()
        media_location = self.folder_line.text() + "/" + self.files_tree.selectedItems()[0].text(column)
        
        if media_location.split(".")[1] == "gif":
            self.movie.setFileName(media_location)
            # Check if image data is valid before playing
            if self.movie.isValid(): 
                # Use setMovie() to set the label's contents as the selected GIF
                self.media_label.setMovie(self.movie)
                self.startMovie() # Call method to begin playing
        else:
            # Disable all buttons when an image is selected
            self.disableMovieButtons()

            # Set the label's pixmap (have the image fit the current size of the image label)
            self.media_label.setPixmap(QPixmap(media_location).scaled(self.media_label.size(), 
                Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation))  

    def startMovie(self):
        """Start playing the movie."""
        self.movie.start() 

    def pauseMovie(self):
        """Pause the movie."""
        self.movie.setPaused(True)

    def stopMovie(self):
        """Stop playing the movie and reset the movie back to 
        the first frame."""
        self.movie.stop()
        self.movie.jumpToFrame(0)

    def changeButtonStates(self, state):
        """Slot that handles enabling/disabling buttons in the toolbar
        based on the state of QMovie."""
        if state == QMovie.MovieState.Running:
            # The animation begins playing once control returns to the event loop
            self.play_act.setEnabled(False)
            self.pause_act.setEnabled(True)
            self.stop_act.setEnabled(True)
        if state == QMovie.MovieState.Paused:
            self.play_act.setEnabled(True)
            self.pause_act.setEnabled(False)
            self.stop_act.setEnabled(False)
        if state == QMovie.MovieState.NotRunning:
            self.play_act.setEnabled(True)
            self.pause_act.setEnabled(False)
            self.stop_act.setEnabled(False)

    def disableMovieButtons(self):
        """Simple method to disable the movie buttons in the toolbar."""
        self.play_act.setEnabled(False)
        self.pause_act.setEnabled(False)
        self.stop_act.setEnabled(False)