Пример #1
0
class DupeGuru(QObject):
    LOGO_NAME = "logo_se"
    NAME = "dupeGuru"

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.prefs = Preferences()
        self.prefs.load()
        # Enable tabs instead of separate floating windows for each dialog
        # Could be passed as an argument to this class if we wanted
        self.use_tabs = True
        self.model = DupeGuruModel(view=self, portable=self.prefs.portable)
        self._setup()

    # --- Private
    def _setup(self):
        core.pe.photo.PLAT_SPECIFIC_PHOTO_CLASS = PlatSpecificPhoto
        self._setupActions()
        self.details_dialog = None
        self._update_options()
        self.recentResults = Recent(self, "recentResults")
        self.recentResults.mustOpenItem.connect(self.model.load_from)
        self.resultWindow = None
        if self.use_tabs:
            self.main_window = TabBarWindow(
                self) if not self.prefs.tabs_default_pos else TabWindow(self)
            parent_window = self.main_window
            self.directories_dialog = self.main_window.createPage(
                "DirectoriesDialog", app=self)
            self.main_window.addTab(self.directories_dialog,
                                    tr("Directories"),
                                    switch=False)
            self.actionDirectoriesWindow.setEnabled(False)
        else:  # floating windows only
            self.main_window = None
            self.directories_dialog = DirectoriesDialog(self)
            parent_window = self.directories_dialog

        self.progress_window = ProgressWindow(parent_window,
                                              self.model.progress_window)
        self.problemDialog = ProblemDialog(parent=parent_window,
                                           model=self.model.problem_dialog)
        if self.use_tabs:
            self.ignoreListDialog = self.main_window.createPage(
                "IgnoreListDialog",
                parent=self.main_window,
                model=self.model.ignore_list_dialog,
            )

            self.excludeListDialog = self.main_window.createPage(
                "ExcludeListDialog",
                app=self,
                parent=self.main_window,
                model=self.model.exclude_list_dialog,
            )
        else:
            self.ignoreListDialog = IgnoreListDialog(
                parent=parent_window, model=self.model.ignore_list_dialog)
            self.excludeDialog = ExcludeListDialog(
                app=self,
                parent=parent_window,
                model=self.model.exclude_list_dialog)

        self.deletionOptions = DeletionOptions(
            parent=parent_window, model=self.model.deletion_options)
        self.about_box = AboutBox(parent_window, self)

        parent_window.show()
        self.model.load()

        self.SIGTERM.connect(self.handleSIGTERM)

        # The timer scheme is because if the nag is not shown before the application is
        # completely initialized, the nag will be shown before the app shows up in the task bar
        # In some circumstances, the nag is hidden by other window, which may make the user think
        # that the application haven't launched.
        QTimer.singleShot(0, self.finishedLaunching)

    def _setupActions(self):
        # Setup actions that are common to both the directory dialog and the results window.
        # (name, shortcut, icon, desc, func)
        ACTIONS = [
            ("actionQuit", "Ctrl+Q", "", tr("Quit"), self.quitTriggered),
            (
                "actionPreferences",
                "Ctrl+P",
                "",
                tr("Options"),
                self.preferencesTriggered,
            ),
            ("actionIgnoreList", "", "", tr("Ignore List"),
             self.ignoreListTriggered),
            (
                "actionDirectoriesWindow",
                "",
                "",
                tr("Directories"),
                self.showDirectoriesWindow,
            ),
            (
                "actionClearCache",
                "Ctrl+Shift+P",
                "",
                tr("Clear Cache"),
                self.clearCacheTriggered,
            ),
            (
                "actionExcludeList",
                "",
                "",
                tr("Exclusion Filters"),
                self.excludeListTriggered,
            ),
            ("actionShowHelp", "F1", "", tr("dupeGuru Help"),
             self.showHelpTriggered),
            ("actionAbout", "", "", tr("About dupeGuru"),
             self.showAboutBoxTriggered),
            (
                "actionOpenDebugLog",
                "",
                "",
                tr("Open Debug Log"),
                self.openDebugLogTriggered,
            ),
        ]
        create_actions(ACTIONS, self)

    def _update_options(self):
        self.model.options["mix_file_kind"] = self.prefs.mix_file_kind
        self.model.options["escape_filter_regexp"] = not self.prefs.use_regexp
        self.model.options[
            "clean_empty_dirs"] = self.prefs.remove_empty_folders
        self.model.options[
            "ignore_hardlink_matches"] = self.prefs.ignore_hardlink_matches
        self.model.options["copymove_dest_type"] = self.prefs.destination_type
        self.model.options["scan_type"] = self.prefs.get_scan_type(
            self.model.app_mode)
        self.model.options["min_match_percentage"] = self.prefs.filter_hardness
        self.model.options["word_weighting"] = self.prefs.word_weighting
        self.model.options["match_similar_words"] = self.prefs.match_similar
        threshold = self.prefs.small_file_threshold if self.prefs.ignore_small_files else 0
        self.model.options[
            "size_threshold"] = threshold * 1024  # threshold is in KB. The scanner wants bytes
        large_threshold = self.prefs.large_file_threshold if self.prefs.ignore_large_files else 0
        self.model.options["large_size_threshold"] = (
            large_threshold * 1024 * 1024
        )  # threshold is in MB. The Scanner wants bytes
        big_file_size_threshold = self.prefs.big_file_size_threshold if self.prefs.big_file_partial_hashes else 0
        self.model.options["big_file_size_threshold"] = (
            big_file_size_threshold * 1024 * 1024
            # threshold is in MiB. The scanner wants bytes
        )
        scanned_tags = set()
        if self.prefs.scan_tag_track:
            scanned_tags.add("track")
        if self.prefs.scan_tag_artist:
            scanned_tags.add("artist")
        if self.prefs.scan_tag_album:
            scanned_tags.add("album")
        if self.prefs.scan_tag_title:
            scanned_tags.add("title")
        if self.prefs.scan_tag_genre:
            scanned_tags.add("genre")
        if self.prefs.scan_tag_year:
            scanned_tags.add("year")
        self.model.options["scanned_tags"] = scanned_tags
        self.model.options["match_scaled"] = self.prefs.match_scaled
        self.model.options[
            "picture_cache_type"] = self.prefs.picture_cache_type

        if self.details_dialog:
            self.details_dialog.update_options()

    # --- Private
    def _get_details_dialog_class(self):
        if self.model.app_mode == AppMode.PICTURE:
            return DetailsDialogPicture
        elif self.model.app_mode == AppMode.MUSIC:
            return DetailsDialogMusic
        else:
            return DetailsDialogStandard

    def _get_preferences_dialog_class(self):
        if self.model.app_mode == AppMode.PICTURE:
            return PreferencesDialogPicture
        elif self.model.app_mode == AppMode.MUSIC:
            return PreferencesDialogMusic
        else:
            return PreferencesDialogStandard

    # --- Public
    def add_selected_to_ignore_list(self):
        self.model.add_selected_to_ignore_list()

    def remove_selected(self):
        self.model.remove_selected(self)

    def confirm(self, title, msg, default_button=QMessageBox.Yes):
        active = QApplication.activeWindow()
        buttons = QMessageBox.Yes | QMessageBox.No
        answer = QMessageBox.question(active, title, msg, buttons,
                                      default_button)
        return answer == QMessageBox.Yes

    def invokeCustomCommand(self):
        self.model.invoke_custom_command()

    def show_details(self):
        if self.details_dialog is not None:
            if not self.details_dialog.isVisible():
                self.details_dialog.show()
            else:
                self.details_dialog.hide()

    def showResultsWindow(self):
        if self.resultWindow is not None:
            if self.use_tabs:
                if self.main_window.indexOfWidget(self.resultWindow) < 0:
                    self.main_window.addTab(self.resultWindow,
                                            tr("Results"),
                                            switch=True)
                    return
                self.main_window.showTab(self.resultWindow)
            else:
                self.resultWindow.show()

    def showDirectoriesWindow(self):
        if self.directories_dialog is not None:
            if self.use_tabs:
                self.main_window.showTab(self.directories_dialog)
            else:
                self.directories_dialog.show()

    def shutdown(self):
        self.willSavePrefs.emit()
        self.prefs.save()
        self.model.save()
        self.model.close()
        # Workaround for #857, hide() or close().
        if self.details_dialog is not None:
            self.details_dialog.close()
        QApplication.quit()

    # --- Signals
    willSavePrefs = pyqtSignal()
    SIGTERM = pyqtSignal()

    # --- Events
    def finishedLaunching(self):
        if sys.getfilesystemencoding() == "ascii":
            # No need to localize this, it's a debugging message.
            msg = (
                "Something is wrong with the way your system locale is set. If the files you're "
                "scanning have accented letters, you'll probably get a crash. It is advised that "
                "you set your system locale properly.")
            QMessageBox.warning(
                self.main_window
                if self.main_window else self.directories_dialog,
                "Wrong Locale",
                msg,
            )
        # Load results on open if passed a .dupeguru file
        if len(sys.argv) > 1:
            results = sys.argv[1]
            if results.endswith(".dupeguru"):
                self.model.load_from(results)
                self.recentResults.insertItem(results)

    def clearCacheTriggered(self):
        title = tr("Clear Cache")
        msg = tr(
            "Do you really want to clear the cache? This will remove all cached file hashes and picture analysis."
        )
        if self.confirm(title, msg, QMessageBox.No):
            self.model.clear_picture_cache()
            self.model.clear_hash_cache()
            active = QApplication.activeWindow()
            QMessageBox.information(active, title, tr("Cache cleared."))

    def ignoreListTriggered(self):
        if self.use_tabs:
            self.showTriggeredTabbedDialog(self.ignoreListDialog,
                                           tr("Ignore List"))
        else:  # floating windows
            self.model.ignore_list_dialog.show()

    def excludeListTriggered(self):
        if self.use_tabs:
            self.showTriggeredTabbedDialog(self.excludeListDialog,
                                           tr("Exclusion Filters"))
        else:  # floating windows
            self.model.exclude_list_dialog.show()

    def showTriggeredTabbedDialog(self, dialog, desc_string):
        """Add tab for dialog, name the tab with desc_string, then show it."""
        index = self.main_window.indexOfWidget(dialog)
        # Create the tab if it doesn't exist already
        if index < 0:  # or (not dialog.isVisible() and not self.main_window.isTabVisible(index)):
            index = self.main_window.addTab(dialog, desc_string, switch=True)
        # Show the tab for that widget
        self.main_window.setCurrentIndex(index)

    def openDebugLogTriggered(self):
        debug_log_path = op.join(self.model.appdata, "debug.log")
        desktop.open_path(debug_log_path)

    def preferencesTriggered(self):
        preferences_dialog = self._get_preferences_dialog_class()(
            self.main_window if self.main_window else self.directories_dialog,
            self)
        preferences_dialog.load()
        result = preferences_dialog.exec()
        if result == QDialog.Accepted:
            preferences_dialog.save()
            self.prefs.save()
            self._update_options()
        preferences_dialog.setParent(None)

    def quitTriggered(self):
        if self.details_dialog is not None:
            self.details_dialog.close()

        if self.main_window:
            self.main_window.close()
        else:
            self.directories_dialog.close()

    def showAboutBoxTriggered(self):
        self.about_box.show()

    def showHelpTriggered(self):
        base_path = platform.HELP_PATH
        help_path = op.abspath(op.join(base_path, "index.html"))
        if op.exists(help_path):
            url = QUrl.fromLocalFile(help_path)
        else:
            url = QUrl("https://dupeguru.voltaicideas.net/help/en/")
        QDesktopServices.openUrl(url)

    def handleSIGTERM(self):
        self.shutdown()

    # --- model --> view
    def get_default(self, key):
        return self.prefs.get_value(key)

    def set_default(self, key, value):
        self.prefs.set_value(key, value)

    def show_message(self, msg):
        window = QApplication.activeWindow()
        QMessageBox.information(window, "", msg)

    def ask_yes_no(self, prompt):
        return self.confirm("", prompt)

    def create_results_window(self):
        """Creates resultWindow and details_dialog depending on the selected ``app_mode``."""
        if self.details_dialog is not None:
            # The object is not deleted entirely, avoid saving its geometry in the future
            # self.willSavePrefs.disconnect(self.details_dialog.appWillSavePrefs)
            # or simply delete it on close which is probably cleaner:
            self.details_dialog.setAttribute(Qt.WA_DeleteOnClose)
            self.details_dialog.close()
            # if we don't do the following, Qt will crash when we recreate the Results dialog
            self.details_dialog.setParent(None)
        if self.resultWindow is not None:
            self.resultWindow.close()
            # This is better for tabs, as it takes care of duplicate items in menu bar
            self.resultWindow.deleteLater(
            ) if self.use_tabs else self.resultWindow.setParent(None)
        if self.use_tabs:
            self.resultWindow = self.main_window.createPage(
                "ResultWindow", parent=self.main_window, app=self)
        else:  # We don't use a tab widget, regular floating QMainWindow
            self.resultWindow = ResultWindow(self.directories_dialog, self)
            self.directories_dialog._updateActionsState()
        self.details_dialog = self._get_details_dialog_class()(
            self.resultWindow, self)

    def show_results_window(self):
        self.showResultsWindow()

    def show_problem_dialog(self):
        self.problemDialog.show()

    def select_dest_folder(self, prompt):
        flags = QFileDialog.ShowDirsOnly
        return QFileDialog.getExistingDirectory(self.resultWindow, prompt, "",
                                                flags)

    def select_dest_file(self, prompt, extension):
        files = tr("{} file (*.{})").format(extension.upper(), extension)
        destination, chosen_filter = QFileDialog.getSaveFileName(
            self.resultWindow, prompt, "", files)
        if not destination.endswith(".{}".format(extension)):
            destination = "{}.{}".format(destination, extension)
        return destination