Example #1
0
class DupeGuru(QObject):
    LOGO_NAME = 'logo_se'
    NAME = 'dupeGuru'

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.prefs = Preferences()
        self.prefs.load()
        self.model = DupeGuruModel(view=self)
        self._setup()

    #--- Private
    def _setup(self):
        core.pe.photo.PLAT_SPECIFIC_PHOTO_CLASS = PlatSpecificPhoto
        self._setupActions()
        self._update_options()
        self.recentResults = Recent(self, 'recentResults')
        self.recentResults.mustOpenItem.connect(self.model.load_from)
        self.resultWindow = None
        self.details_dialog = None
        self.directories_dialog = DirectoriesDialog(self)
        self.progress_window = ProgressWindow(self.directories_dialog,
                                              self.model.progress_window)
        self.problemDialog = ProblemDialog(parent=self.directories_dialog,
                                           model=self.model.problem_dialog)
        self.ignoreListDialog = IgnoreListDialog(
            parent=self.directories_dialog,
            model=self.model.ignore_list_dialog)
        self.deletionOptions = DeletionOptions(
            parent=self.directories_dialog, model=self.model.deletion_options)
        self.about_box = AboutBox(self.directories_dialog, self)

        self.directories_dialog.show()
        self.model.load()

        # 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)
        QCoreApplication.instance().aboutToQuit.connect(
            self.application_will_terminate)

    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),
            ('actionClearPictureCache', 'Ctrl+Shift+P', '',
             tr("Clear Picture Cache"), self.clearPictureCacheTriggered),
            ('actionShowHelp', 'F1', '', tr("dupeGuru Help"),
             self.showHelpTriggered),
            ('actionAbout', '', '', tr("About dupeGuru"),
             self.showAboutBoxTriggered),
            ('actionOpenDebugLog', '', '', tr("Open Debug Log"),
             self.openDebugLogTriggered),
        ]
        createActions(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
        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

    #--- 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:
            self.details_dialog.show()

    def showResultsWindow(self):
        if self.resultWindow is not None:
            self.resultWindow.show()

    #--- Signals
    willSavePrefs = 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.directories_dialog, "Wrong Locale", msg)

    def application_will_terminate(self):
        self.willSavePrefs.emit()
        self.prefs.save()
        self.model.save()

    def clearPictureCacheTriggered(self):
        title = tr("Clear Picture Cache")
        msg = tr(
            "Do you really want to remove all your cached picture analysis?")
        if self.confirm(title, msg, QMessageBox.No):
            self.model.clear_picture_cache()
            active = QApplication.activeWindow()
            QMessageBox.information(active, title,
                                    tr("Picture cache cleared."))

    def ignoreListTriggered(self):
        self.model.ignore_list_dialog.show()

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

    def preferencesTriggered(self):
        preferences_dialog = self._get_preferences_dialog_class()(
            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):
        self.directories_dialog.close()

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

    def showHelpTriggered(self):
        base_path = platform.HELP_PATH
        url = QUrl.fromLocalFile(op.abspath(op.join(base_path, 'index.html')))
        QDesktopServices.openUrl(url)

    #--- 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:
            self.details_dialog.close()
            self.details_dialog.setParent(None)
        if self.resultWindow is not None:
            self.resultWindow.close()
            self.resultWindow.setParent(None)
        self.resultWindow = ResultWindow(self.directories_dialog, self)
        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
Example #2
0
class MoneyGuru(ApplicationBase):
    VERSION = MoneyGuruModel.VERSION
    LOGO_NAME = 'logo'

    def __init__(self, filepath=None):
        ApplicationBase.__init__(self)
        self.prefs = Preferences()
        self.prefs.load()
        global APP_PREFS
        APP_PREFS = self.prefs
        locale = QLocale.system()
        dateFormat = self.prefs.dateFormat
        decimalSep = locale.decimalPoint()
        groupingSep = locale.groupSeparator()
        cachePath = QStandardPaths.standardLocations(QStandardPaths.CacheLocation)[0]
        appdata = getAppData()
        DateEdit.DATE_FORMAT = dateFormat
        self.model = MoneyGuruModel(
            view=self, date_format=dateFormat, decimal_sep=decimalSep,
            grouping_sep=groupingSep, cache_path=cachePath, appdata_path=appdata,
        )
        # on the Qt side, we're single document based, so it's one doc per app.
        self.doc = Document(app=self)
        self.doc.model.connect()
        self.mainWindow = MainWindow(doc=self.doc)
        self.preferencesPanel = PreferencesPanel(self.mainWindow, app=self)
        self.aboutBox = AboutBox(self.mainWindow, self)
        self.initialFilePath = None
        if filepath and op.exists(filepath):
            self.initialFilePath = filepath
        elif self.prefs.recentDocuments:
            self.initialFilePath = self.prefs.recentDocuments[0]

        self.finishedLaunching.connect(self.applicationFinishedLaunching)
        QCoreApplication.instance().aboutToQuit.connect(self.applicationWillTerminate)

    # --- Public
    def showAboutBox(self):
        self.aboutBox.show()

    def showHelp(self):
        help_path = op.abspath(op.join(HELP_PATH, 'index.html'))
        if op.exists(help_path):
            url = QUrl.fromLocalFile(help_path)
        else:
            url = QUrl("https://www.hardcoded.net/moneyguru/help/en")
        QDesktopServices.openUrl(url)

    def showPreferences(self):
        self.preferencesPanel.load()
        if self.preferencesPanel.exec_() == QDialog.Accepted:
            self.preferencesPanel.save()
            self.prefs.prefsChanged.emit()

    # --- Event Handling
    def applicationFinishedLaunching(self):
        self.prefs.restoreGeometry('mainWindowGeometry', self.mainWindow)
        self.prefs.restoreGeometry('importWindowGeometry', self.mainWindow.importWindow)
        self.mainWindow.show()
        if self.initialFilePath:
            self.doc.open(self.initialFilePath, initial=True)

    def applicationWillTerminate(self):
        self.doc.close()
        self.willSavePrefs.emit()
        self.prefs.saveGeometry('mainWindowGeometry', self.mainWindow)
        self.prefs.saveGeometry('importWindowGeometry', self.mainWindow.importWindow)
        self.prefs.save()
        self.model.shutdown()

    # --- Signals
    willSavePrefs = pyqtSignal()

    # --- 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 open_url(self, url):
        url = QUrl(url)
        QDesktopServices.openUrl(url)

    def reveal_path(self, path):
        url = QUrl.fromLocalFile(str(path))
        QDesktopServices.openUrl(url)
Example #3
0
class DupeGuru(QObject):
    MODELCLASS = None
    LOGO_NAME = '<replace this>'
    NAME = '<replace this>'
    
    DETAILS_DIALOG_CLASS = None
    RESULT_WINDOW_CLASS = ResultWindow
    RESULT_MODEL_CLASS = None
    PREFERENCES_CLASS = None
    PREFERENCES_DIALOG_CLASS = None
    
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.prefs = self.PREFERENCES_CLASS()
        self.prefs.load()
        self.model = self.MODELCLASS(view=self)
        self._setup()
        self.prefsChanged.emit(self.prefs)
    
    #--- Private
    def _setup(self):
        self._setupActions()
        self._update_options()
        self.recentResults = Recent(self, 'recentResults')
        self.recentResults.mustOpenItem.connect(self.model.load_from)
        self.directories_dialog = DirectoriesDialog(self)
        self.resultWindow = self.RESULT_WINDOW_CLASS(self.directories_dialog, self)
        self.progress_window = ProgressWindow(self.resultWindow, self.model.progress_window) 
        self.details_dialog = self.DETAILS_DIALOG_CLASS(self.resultWindow, self)
        self.problemDialog = ProblemDialog(parent=self.resultWindow, model=self.model.problem_dialog)
        self.ignoreListDialog = IgnoreListDialog(parent=self.resultWindow, model=self.model.ignore_list_dialog)
        self.deletionOptions = DeletionOptions(parent=self.resultWindow, model=self.model.deletion_options)
        self.preferences_dialog = self.PREFERENCES_DIALOG_CLASS(self.resultWindow, self)
        self.about_box = AboutBox(self.resultWindow, self)
                
        self.directories_dialog.show()
        self.model.load()
        
        # 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)
        QCoreApplication.instance().aboutToQuit.connect(self.application_will_terminate)
    
    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("Preferences"), self.preferencesTriggered),
            ('actionIgnoreList', '', '', tr("Ignore List"), self.ignoreListTriggered),
            ('actionShowHelp', 'F1', '', tr("dupeGuru Help"), self.showHelpTriggered),
            ('actionAbout', '', '', tr("About dupeGuru"), self.showAboutBoxTriggered),
            ('actionOpenDebugLog', '', '', tr("Open Debug Log"), self.openDebugLogTriggered),
        ]
        createActions(ACTIONS, self)
    
    def _update_options(self):
        self.model.scanner.mix_file_kind = self.prefs.mix_file_kind
        self.model.options['escape_filter_regexp'] = 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
    
    #--- 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):
        self.details_dialog.show()
    
    def showResultsWindow(self):
        self.resultWindow.show()
    
    #--- Signals
    willSavePrefs = pyqtSignal()
    prefsChanged = pyqtSignal(object)
    
    #--- 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.directories_dialog, "Wrong Locale", msg)
    
    def application_will_terminate(self):
        self.willSavePrefs.emit()
        self.prefs.save()
        self.model.save()
    
    def ignoreListTriggered(self):
        self.model.ignore_list_dialog.show()
    
    def openDebugLogTriggered(self):
        debugLogPath = op.join(self.model.appdata, 'debug.log')
        desktop.open_path(debugLogPath)
    
    def preferencesTriggered(self):
        self.preferences_dialog.load()
        result = self.preferences_dialog.exec()
        if result == QDialog.Accepted:
            self.preferences_dialog.save()
            self.prefs.save()
            self._update_options()
            self.prefsChanged.emit(self.prefs)
    
    def quitTriggered(self):
        self.directories_dialog.close()
    
    def showAboutBoxTriggered(self):
        self.about_box.show()
    
    def showHelpTriggered(self):
        base_path = platform.HELP_PATH
        url = QUrl.fromLocalFile(op.abspath(op.join(base_path, 'index.html')))
        QDesktopServices.openUrl(url)
    
    #--- 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 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
Example #4
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)
        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,
            ),
            (
                "actionClearPictureCache",
                "Ctrl+Shift+P",
                "",
                tr("Clear Picture Cache"),
                self.clearPictureCacheTriggered,
            ),
            (
                "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,
            ),
        ]
        createActions(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
        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()
        # 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,
            )

    def clearPictureCacheTriggered(self):
        title = tr("Clear Picture Cache")
        msg = tr("Do you really want to remove all your cached picture analysis?")
        if self.confirm(title, msg, QMessageBox.No):
            self.model.clear_picture_cache()
            active = QApplication.activeWindow()
            QMessageBox.information(active, title, tr("Picture 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):
        debugLogPath = op.join(self.model.appdata, "debug.log")
        desktop.open_path(debugLogPath)

    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
Example #5
0
class MusicGuru(MusicGuruBase, ApplicationBase):
    LOGO_NAME = 'mg_logo'

    def __init__(self):
        appdata = str(
            QDesktopServices.storageLocation(QDesktopServices.DataLocation))
        MusicGuruBase.__init__(self, appdata)
        ApplicationBase.__init__(self)
        if not op.exists(appdata):
            os.makedirs(appdata)
        logging.basicConfig(filename=op.join(appdata, 'debug.log'),
                            level=logging.WARNING)
        self.prefs = Preferences()
        self.prefs.load()
        self.selectedBoardItems = []
        self.selectedLocation = None
        self.mainWindow = MainWindow(app=self)
        self.locationsPanel = LocationsPanel(app=self)
        self.detailsPanel = DetailsPanel(app=self)
        self.ignoreBox = IgnoreBox(app=self)
        self.progress = Progress(self.mainWindow)
        self.aboutBox = AboutBox(self.mainWindow, self)

        self.connect(self.progress, SIGNAL('finished(QString)'),
                     self.jobFinished)
        self.connect(self, SIGNAL('applicationFinishedLaunching()'),
                     self.applicationFinishedLaunching)

    #--- Private
    def _placeDetailsPanel(self):
        # locations panel must be placed first
        if self.detailsPanel.isVisible():
            return
        desktop = QApplication.desktop()
        w = self.locationsPanel.width()
        h = self.detailsPanel.height()
        x = self.locationsPanel.x()
        windowBottom = self.locationsPanel.frameGeometry().y(
        ) + self.locationsPanel.frameGeometry().height()
        y = windowBottom
        self.detailsPanel.move(x, y)
        self.detailsPanel.resize(w, h)

    def _placeIgnoreBox(self):
        if self.ignoreBox.isVisible():
            return
        desktop = QApplication.desktop()
        windowWidth = self.mainWindow.frameGeometry().width()
        frameWidth = self.ignoreBox.frameGeometry().width(
        ) - self.ignoreBox.width()
        w = windowWidth - frameWidth
        h = self.ignoreBox.height()
        x = self.mainWindow.x()
        windowBottom = self.mainWindow.frameGeometry().y(
        ) + self.mainWindow.frameGeometry().height()
        y = min(windowBottom, desktop.height() - h)
        self.ignoreBox.move(x, y)
        self.ignoreBox.resize(w, h)

    def _placeLocationsPanel(self):
        if self.locationsPanel.isVisible():
            return
        desktop = QApplication.desktop()
        w = self.locationsPanel.width()
        windowHeight = self.mainWindow.frameGeometry().height()
        frameHeight = self.locationsPanel.frameGeometry().height(
        ) - self.locationsPanel.height()
        h = windowHeight - frameHeight - self.detailsPanel.frameGeometry(
        ).height()
        windowRight = self.mainWindow.frameGeometry().x(
        ) + self.mainWindow.frameGeometry().width()
        x = min(windowRight, desktop.width() - w)
        y = self.mainWindow.y()
        self.locationsPanel.move(x, y)
        self.locationsPanel.resize(w, h)

    def _setup_as_registered(self):
        self.prefs.registration_code = self.registration_code
        self.prefs.registration_email = self.registration_email
        self.prefs.save()
        self.mainWindow.actionRegister.setVisible(False)
        self.aboutBox.registerButton.hide()
        self.aboutBox.registeredEmailLabel.setText(
            self.prefs.registration_email)

    def _startJob(self, jobid, func):
        title = JOBID2TITLE[jobid]
        try:
            j = self.progress.create_job()
            self.progress.run(jobid, title, func, args=(j, ))
        except job.JobInProgressError:
            msg = "A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again."
            QMessageBox.information(self.mainWindow, "Action in progress", msg)

    #--- Public
    def addLocation(self, path, name, removeable):
        def do(j):
            MusicGuruBase.AddLocation(self, path, name, removeable, j)

        error_msg = self.CanAddLocation(path, name)
        if error_msg:
            QMessageBox.warning(self.mainWindow, "Add Location", error_msg)
            return
        self._startJob(JOB_ADD, do)

    def addLocationPrompt(self):
        dialog = AddLocationDialog(self)
        result = dialog.exec_()
        if result == QDialog.Accepted:
            self.addLocation(dialog.locationPath, dialog.locationName,
                             dialog.isLocationRemovable)

    def askForRegCode(self):
        if self.reg.ask_for_code():
            self._setup_as_registered()

    def copyOrMove(self, copy):
        def onNeedCd(location):
            # We can't do anything GUI related in a separate thread with Qt. Since copy/move
            # operations are performed asynchronously, the calls made to needCdDialog (created in
            # the main thread) must also be made asynchronously.
            return needCdDialog.askForDiskAsync(location.name)

        def do(j):
            MusicGuruBase.CopyOrMove(self, copy, dirpath, j, onNeedCd)

        needCdDialog = DiskNeededDialog()
        title = "Choose a destination"
        flags = QFileDialog.ShowDirsOnly
        dirpath = str(
            QFileDialog.getExistingDirectory(self.mainWindow, title, '',
                                             flags))
        if dirpath:
            jobid = JOB_MATERIALIZE_COPY if copy else JOB_MATERIALIZE_MOVE
            self._startJob(jobid, do)

    def massRename(self, model, whitespace):
        def do(j):
            self.board.MassRename(model, whitespace, j)

        self._startJob(JOB_MASS_RENAME, do)

    def moveConflicts(self, with_original=False):
        if self.board.MoveConflicts(with_original=with_original) > 0:
            self.emit(SIGNAL('boardChanged()'))
            self.emit(SIGNAL('ignoreBoxChanged()'))

    def moveSelectedToIgnoreBox(self):
        smart_move(self.selectedBoardItems,
                   self.board.ignore_box,
                   allow_merge=True)
        self.emit(SIGNAL('boardChanged()'))
        self.emit(SIGNAL('ignoreBoxChanged()'))

    def removeEmptyFolders(self):
        MusicGuruBase.RemoveEmptyDirs(self)
        self.emit(SIGNAL('boardChanged()'))

    def removeLocation(self, location):
        self.board.RemoveLocation(location)
        location.delete()
        self.emit(SIGNAL('locationsChanged()'))
        self.emit(SIGNAL('boardChanged()'))

    def removeLocationPrompt(self):
        location = self.selectedLocation
        if location is None:
            return
        title = "Remove location"
        msg = "Do you really want to remove location {0}?".format(
            location.name)
        buttons = QMessageBox.Yes | QMessageBox.No
        answer = QMessageBox.question(self.mainWindow, title, msg, buttons,
                                      QMessageBox.Yes)
        if answer != QMessageBox.Yes:
            return
        self.removeLocation(location)

    def renameInRespectiveLocations(self):
        def do(j):
            MusicGuruBase.RenameInRespectiveLocations(self, j)

        self._startJob(JOB_MATERIALIZE_RENAME, do)

    def selectBoardItems(self, items):
        self.selectedBoardItems = items
        self.emit(SIGNAL('boardSelectionChanged()'))

    def selectLocation(self, location):
        self.selectedLocation = location

    def showAboutBox(self):
        self.aboutBox.show()

    def showDetailsPanel(self):
        self._placeLocationsPanel()
        self._placeDetailsPanel()
        self.detailsPanel.show()
        self.detailsPanel.activateWindow()

    def showHelp(self):
        url = QUrl.fromLocalFile(op.join(op.abspath(HELP_PATH), 'intro.htm'))
        QDesktopServices.openUrl(url)

    def showIgnoreBox(self):
        self._placeIgnoreBox()
        self.ignoreBox.show()
        self.ignoreBox.activateWindow()

    def showLocationPanel(self):
        self._placeLocationsPanel()
        self.locationsPanel.show()
        self.locationsPanel.activateWindow()

    def split(self, model, capacity, grouping_level):
        def do(j):
            self.board.Split(model, capacity, grouping_level, j)

        self._startJob(JOB_SPLIT, do)

    def toggleLocation(self, location):
        self.board.ToggleLocation(location)
        self.emit(SIGNAL('locationsChanged()'))
        self.emit(SIGNAL('boardChanged()'))

    def undoSplit(self):
        self.board.Unsplit()
        self.emit(SIGNAL('boardChanged()'))

    def updateCollection(self):
        def do(j):
            self.collection.update_volumes(j)

        self._startJob(JOB_UPDATE, do)

    def updateLocation(self, location):
        def do(j):
            location.update(None, j)

        self._startJob(JOB_UPDATE, do)

    #--- Events
    def applicationFinishedLaunching(self):
        self.reg = Registration(self)
        self.set_registration(self.prefs.registration_code,
                              self.prefs.registration_email)
        if not self.registered and self.unpaid_hours >= 1:
            self.reg.show_nag()
        self.mainWindow.show()
        self.showLocationPanel()
        self.showDetailsPanel()
        self.updateCollection()

    def jobFinished(self, jobid):
        if jobid in (JOB_UPDATE, JOB_ADD):
            self.emit(SIGNAL('locationsChanged()'))
        if jobid in (JOB_MASS_RENAME, JOB_SPLIT):
            self.emit(SIGNAL('boardChanged()'))
        if jobid in (JOB_MATERIALIZE_RENAME, JOB_MATERIALIZE_MOVE):
            self.board.Empty()
            self.emit(SIGNAL('locationsChanged()'))
            self.emit(SIGNAL('boardChanged()'))
            self.emit(SIGNAL('ignoreBoxChanged()'))
Example #6
0
class DupeGuru(QObject):
    LOGO_NAME = 'logo_se'
    NAME = 'dupeGuru'

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.prefs = Preferences()
        self.prefs.load()
        self.model = DupeGuruModel(view=self)
        self._setup()

    #--- Private
    def _setup(self):
        core.pe.photo.PLAT_SPECIFIC_PHOTO_CLASS = PlatSpecificPhoto
        self._setupActions()
        self._update_options()
        self.recentResults = Recent(self, 'recentResults')
        self.recentResults.mustOpenItem.connect(self.model.load_from)
        self.resultWindow = None
        self.details_dialog = None
        self.directories_dialog = DirectoriesDialog(self)
        self.progress_window = ProgressWindow(self.directories_dialog, self.model.progress_window)
        self.problemDialog = ProblemDialog(parent=self.directories_dialog, model=self.model.problem_dialog)
        self.ignoreListDialog = IgnoreListDialog(parent=self.directories_dialog, model=self.model.ignore_list_dialog)
        self.deletionOptions = DeletionOptions(parent=self.directories_dialog, model=self.model.deletion_options)
        self.about_box = AboutBox(self.directories_dialog, self)

        self.directories_dialog.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),
            ('actionClearPictureCache', 'Ctrl+Shift+P', '', tr("Clear Picture Cache"), self.clearPictureCacheTriggered),
            ('actionShowHelp', 'F1', '', tr("dupeGuru Help"), self.showHelpTriggered),
            ('actionAbout', '', '', tr("About dupeGuru"), self.showAboutBoxTriggered),
            ('actionOpenDebugLog', '', '', tr("Open Debug Log"), self.openDebugLogTriggered),
        ]
        createActions(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
        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

    #--- 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:
            self.details_dialog.show()

    def showResultsWindow(self):
        if self.resultWindow is not None:
            self.resultWindow.show()

    def shutdown(self):
        self.willSavePrefs.emit()
        self.prefs.save()
        self.model.save()
        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.directories_dialog, "Wrong Locale", msg)

    def clearPictureCacheTriggered(self):
        title = tr("Clear Picture Cache")
        msg = tr("Do you really want to remove all your cached picture analysis?")
        if self.confirm(title, msg, QMessageBox.No):
            self.model.clear_picture_cache()
            active = QApplication.activeWindow()
            QMessageBox.information(active, title, tr("Picture cache cleared."))

    def ignoreListTriggered(self):
        self.model.ignore_list_dialog.show()

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

    def preferencesTriggered(self):
        preferences_dialog = self._get_preferences_dialog_class()(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):
        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://www.hardcoded.net/dupeguru/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:
            self.details_dialog.close()
            self.details_dialog.setParent(None)
        if self.resultWindow is not None:
            self.resultWindow.close()
            self.resultWindow.setParent(None)
        self.resultWindow = ResultWindow(self.directories_dialog, self)
        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
Example #7
0
class MoneyGuru(ApplicationBase):
    VERSION = MoneyGuruModel.VERSION
    LOGO_NAME = 'logo'

    def __init__(self, filepath=None):
        ApplicationBase.__init__(self)
        self.prefs = Preferences()
        self.prefs.load()
        global APP_PREFS
        APP_PREFS = self.prefs
        locale = QLocale.system()
        dateFormat = self.prefs.dateFormat
        decimalSep = locale.decimalPoint()
        groupingSep = locale.groupSeparator()
        cachePath = QStandardPaths.standardLocations(
            QStandardPaths.CacheLocation)[0]
        appdata = getAppData()
        DateEdit.DATE_FORMAT = dateFormat
        self.model = MoneyGuruModel(
            view=self,
            date_format=dateFormat,
            decimal_sep=decimalSep,
            grouping_sep=groupingSep,
            cache_path=cachePath,
            appdata_path=appdata,
        )
        # on the Qt side, we're single document based, so it's one doc per app.
        self.doc = Document(app=self)
        self.doc.model.connect()
        self.mainWindow = MainWindow(doc=self.doc)
        self.preferencesPanel = PreferencesPanel(self.mainWindow, app=self)
        self.aboutBox = AboutBox(self.mainWindow, self)
        self.initialFilePath = None
        if filepath and op.exists(filepath):
            self.initialFilePath = filepath
        elif self.prefs.recentDocuments:
            self.initialFilePath = self.prefs.recentDocuments[0]

        self.finishedLaunching.connect(self.applicationFinishedLaunching)
        QCoreApplication.instance().aboutToQuit.connect(
            self.applicationWillTerminate)

    # --- Public
    def showAboutBox(self):
        self.aboutBox.show()

    def showHelp(self):
        url = QUrl.fromLocalFile(op.abspath(op.join(HELP_PATH, 'index.html')))
        QDesktopServices.openUrl(url)

    def showPreferences(self):
        self.preferencesPanel.load()
        if self.preferencesPanel.exec_() == QDialog.Accepted:
            self.preferencesPanel.save()
            self.prefs.prefsChanged.emit()

    # --- Event Handling
    def applicationFinishedLaunching(self):
        self.prefs.restoreGeometry('mainWindowGeometry', self.mainWindow)
        self.prefs.restoreGeometry('importWindowGeometry',
                                   self.mainWindow.importWindow)
        self.mainWindow.show()
        if self.initialFilePath:
            self.doc.open(self.initialFilePath, initial=True)

    def applicationWillTerminate(self):
        self.doc.close()
        self.willSavePrefs.emit()
        self.prefs.saveGeometry('mainWindowGeometry', self.mainWindow)
        self.prefs.saveGeometry('importWindowGeometry',
                                self.mainWindow.importWindow)
        self.prefs.save()
        self.model.shutdown()

    # --- Signals
    willSavePrefs = pyqtSignal()

    # --- 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 open_url(self, url):
        url = QUrl(url)
        QDesktopServices.openUrl(url)

    def reveal_path(self, path):
        url = QUrl.fromLocalFile(str(path))
        QDesktopServices.openUrl(url)
Example #8
0
class PdfMasher(ApplicationBase):
    LOGO_NAME = 'logo'

    def __init__(self):
        ApplicationBase.__init__(self)
        self.prefs = Preferences()
        self.prefs.load()
        self.model = App(view=self)
        self._setupActions()
        self.mainWindow = MainWindow(app=self)
        self.aboutBox = AboutBox(self.mainWindow, self, withreg=False)
        self._progress = Progress(self.mainWindow)

        self.connect(self, SIGNAL('applicationFinishedLaunching()'),
                     self.applicationFinishedLaunching)
        self.connect(QCoreApplication.instance(), SIGNAL('aboutToQuit()'),
                     self.applicationWillTerminate)
        self._progress.finished.connect(self.jobFinished)

    #--- Public
    def askForRegCode(self):
        self.reg.ask_for_code()

    #--- Private
    def _setupActions(self):
        ACTIONS = [
            ('actionLoadProject', 'Ctrl+Shift+O', '', tr("Load Project"),
             self.model.load_project),
            ('actionSaveProject', 'Ctrl+S', '', tr("Save Project"),
             self.model.save_project),
            ('actionQuit', 'Ctrl+Q', '', tr("Quit"), self.quitTriggered),
            ('actionShowHelp', 'F1', '', tr("PdfMasher Help"),
             self.showHelpTriggered),
            ('actionAbout', '', '', tr("About PdfMasher"),
             self.showAboutBoxTriggered),
            ('actionCheckForUpdate', '', '', tr("Check for Update"),
             self.checkForUpdateTriggered),
            ('actionOpenDebugLog', '', '', tr("Open Debug Log"),
             self.openDebugLogTriggered),
        ]
        createActions(ACTIONS, self)

        if ISLINUX:
            self.actionCheckForUpdate.setVisible(
                False)  # This only works on Windows

    #--- Event Handling
    def applicationFinishedLaunching(self):
        self.mainWindow.show()

    def applicationWillTerminate(self):
        self.prefs.save()

    def jobFinished(self, jobid):
        self.model._job_completed(jobid)

    def checkForUpdateTriggered(self):
        QProcess.execute('updater.exe', ['/checknow'])

    def openDebugLogTriggered(self):
        appdata = getAppData()
        debugLogPath = op.join(appdata, 'debug.log')
        url = QUrl.fromLocalFile(debugLogPath)
        QDesktopServices.openUrl(url)

    def quitTriggered(self):
        self.mainWindow.close()

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

    def showHelpTriggered(self):
        url = QUrl.fromLocalFile(op.abspath(op.join(HELP_PATH, 'index.html')))
        QDesktopServices.openUrl(url)

    #--- model --> view
    @staticmethod
    def open_path(path):
        url = QUrl.fromLocalFile(path)
        QDesktopServices.openUrl(url)

    @staticmethod
    def reveal_path(path):
        PdfMasher.open_path(op.dirname(path))

    def open_url(self, url):
        url = QUrl(url)
        QDesktopServices.openUrl(url)

    def show_message(self, msg):
        QMessageBox.information(self.mainWindow, '', msg)

    def start_job(self, jobid, func, *args):
        title = JOBID2TITLE[jobid]
        try:
            j = self._progress.create_job()
            args = tuple([j] + list(args))
            self._progress.run(jobid, title, func, args=args)
        except job.JobInProgressError:
            msg = "A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again."
            QMessageBox.information(self.mainWindow, "Action in progress", msg)

    def get_default(self, key):
        return self.prefs.get_value(key)

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

    def query_load_path(self, prompt, allowed_exts):
        myfilters = [
            "{} file (*.{})".format(ext.upper(), ext) for ext in allowed_exts
        ]
        files = ';;'.join(myfilters + ["All Files (*.*)"])
        return QFileDialog.getOpenFileName(self.mainWindow, prompt, '', files)

    def query_save_path(self, prompt, allowed_exts):
        myfilters = [
            "{} file (*.{})".format(ext.upper(), ext) for ext in allowed_exts
        ]
        files = ';;'.join(myfilters + ["All Files (*.*)"])
        return QFileDialog.getSaveFileName(self.mainWindow, prompt, '', files)
Example #9
0
class PdfMasher(ApplicationBase):
    LOGO_NAME = 'logo'
    
    def __init__(self):
        ApplicationBase.__init__(self)
        self.prefs = Preferences()
        self.prefs.load()
        self.model = App(view=self)
        self._setupActions()
        self.mainWindow = MainWindow(app=self)
        self.aboutBox = AboutBox(self.mainWindow, self, withreg=False)
        self._progress = Progress(self.mainWindow)
        
        self.connect(self, SIGNAL('applicationFinishedLaunching()'), self.applicationFinishedLaunching)
        self.connect(QCoreApplication.instance(), SIGNAL('aboutToQuit()'), self.applicationWillTerminate)
        self._progress.finished.connect(self.jobFinished)
    
    #--- Public
    def askForRegCode(self):
        self.reg.ask_for_code()
    
    #--- Private
    def _setupActions(self):
        ACTIONS = [
            ('actionLoadProject', 'Ctrl+Shift+O', '', tr("Load Project"), self.model.load_project),
            ('actionSaveProject', 'Ctrl+S', '', tr("Save Project"), self.model.save_project),
            ('actionQuit', 'Ctrl+Q', '', tr("Quit"), self.quitTriggered),
            ('actionShowHelp', 'F1', '', tr("PdfMasher Help"), self.showHelpTriggered),
            ('actionAbout', '', '', tr("About PdfMasher"), self.showAboutBoxTriggered),
            ('actionCheckForUpdate', '', '', tr("Check for Update"), self.checkForUpdateTriggered),
            ('actionOpenDebugLog', '', '', tr("Open Debug Log"), self.openDebugLogTriggered),
        ]
        createActions(ACTIONS, self)
        
        if ISLINUX:
            self.actionCheckForUpdate.setVisible(False) # This only works on Windows
    
    #--- Event Handling
    def applicationFinishedLaunching(self):
        self.mainWindow.show()
    
    def applicationWillTerminate(self):
        self.prefs.save()
    
    def jobFinished(self, jobid):
        self.model._job_completed(jobid)
    
    def checkForUpdateTriggered(self):
        QProcess.execute('updater.exe', ['/checknow'])
    
    def openDebugLogTriggered(self):
        appdata = getAppData()
        debugLogPath = op.join(appdata, 'debug.log')
        url = QUrl.fromLocalFile(debugLogPath)
        QDesktopServices.openUrl(url)
    
    def quitTriggered(self):
        self.mainWindow.close()
    
    def showAboutBoxTriggered(self):
        self.aboutBox.show()
    
    def showHelpTriggered(self):
        url = QUrl.fromLocalFile(op.abspath(op.join(HELP_PATH, 'index.html')))
        QDesktopServices.openUrl(url)
    
    #--- model --> view
    @staticmethod
    def open_path(path):
        url = QUrl.fromLocalFile(path)
        QDesktopServices.openUrl(url)
    
    @staticmethod
    def reveal_path(path):
        PdfMasher.open_path(op.dirname(path))
    
    def open_url(self, url):
        url = QUrl(url)
        QDesktopServices.openUrl(url)
    
    def show_message(self, msg):
        QMessageBox.information(self.mainWindow, '', msg)
    
    def start_job(self, jobid, func, *args):
        title = JOBID2TITLE[jobid]
        try:
            j = self._progress.create_job()
            args = tuple([j] + list(args))
            self._progress.run(jobid, title, func, args=args)
        except job.JobInProgressError:
            msg = "A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again."
            QMessageBox.information(self.mainWindow, "Action in progress", msg)
    
    def get_default(self, key):
        return self.prefs.get_value(key)
    
    def set_default(self, key, value):
        self.prefs.set_value(key, value)
    
    def query_load_path(self, prompt, allowed_exts):
        myfilters = ["{} file (*.{})".format(ext.upper(), ext) for ext in allowed_exts]
        files = ';;'.join(myfilters + ["All Files (*.*)"])
        return QFileDialog.getOpenFileName(self.mainWindow, prompt, '', files)
    
    def query_save_path(self, prompt, allowed_exts):
        myfilters = ["{} file (*.{})".format(ext.upper(), ext) for ext in allowed_exts]
        files = ';;'.join(myfilters + ["All Files (*.*)"])
        return QFileDialog.getSaveFileName(self.mainWindow, prompt, '', files)
Example #10
0
class MusicGuru(MusicGuruBase, ApplicationBase):
    LOGO_NAME = 'mg_logo'
    
    def __init__(self):
        appdata = str(QDesktopServices.storageLocation(QDesktopServices.DataLocation))
        MusicGuruBase.__init__(self, appdata)
        ApplicationBase.__init__(self)
        if not op.exists(appdata):
            os.makedirs(appdata)
        logging.basicConfig(filename=op.join(appdata, 'debug.log'), level=logging.WARNING)
        self.prefs = Preferences()
        self.prefs.load()
        self.selectedBoardItems = []
        self.selectedLocation = None
        self.mainWindow = MainWindow(app=self)
        self.locationsPanel = LocationsPanel(app=self)
        self.detailsPanel = DetailsPanel(app=self)
        self.ignoreBox = IgnoreBox(app=self)
        self.progress = Progress(self.mainWindow)
        self.aboutBox = AboutBox(self.mainWindow, self)
        
        self.connect(self.progress, SIGNAL('finished(QString)'), self.jobFinished)
        self.connect(self, SIGNAL('applicationFinishedLaunching()'), self.applicationFinishedLaunching)
    
    #--- Private
    def _placeDetailsPanel(self):
        # locations panel must be placed first
        if self.detailsPanel.isVisible():
            return
        desktop = QApplication.desktop()
        w = self.locationsPanel.width()
        h = self.detailsPanel.height()
        x = self.locationsPanel.x()
        windowBottom = self.locationsPanel.frameGeometry().y() + self.locationsPanel.frameGeometry().height()
        y = windowBottom
        self.detailsPanel.move(x, y)
        self.detailsPanel.resize(w, h)
    
    def _placeIgnoreBox(self):
        if self.ignoreBox.isVisible():
            return
        desktop = QApplication.desktop()
        windowWidth = self.mainWindow.frameGeometry().width()
        frameWidth = self.ignoreBox.frameGeometry().width() - self.ignoreBox.width()
        w = windowWidth - frameWidth
        h = self.ignoreBox.height()
        x = self.mainWindow.x()
        windowBottom = self.mainWindow.frameGeometry().y() + self.mainWindow.frameGeometry().height()
        y = min(windowBottom, desktop.height() - h)
        self.ignoreBox.move(x, y)
        self.ignoreBox.resize(w, h)
    
    def _placeLocationsPanel(self):
        if self.locationsPanel.isVisible():
            return
        desktop = QApplication.desktop()
        w = self.locationsPanel.width()
        windowHeight = self.mainWindow.frameGeometry().height()
        frameHeight = self.locationsPanel.frameGeometry().height() - self.locationsPanel.height()
        h = windowHeight - frameHeight - self.detailsPanel.frameGeometry().height()
        windowRight = self.mainWindow.frameGeometry().x() + self.mainWindow.frameGeometry().width()
        x = min(windowRight, desktop.width() - w)
        y = self.mainWindow.y()
        self.locationsPanel.move(x, y)
        self.locationsPanel.resize(w, h)
    
    def _setup_as_registered(self):
        self.prefs.registration_code = self.registration_code
        self.prefs.registration_email = self.registration_email
        self.prefs.save()
        self.mainWindow.actionRegister.setVisible(False)
        self.aboutBox.registerButton.hide()
        self.aboutBox.registeredEmailLabel.setText(self.prefs.registration_email)
    
    def _startJob(self, jobid, func):
        title = JOBID2TITLE[jobid]
        try:
            j = self.progress.create_job()
            self.progress.run(jobid, title, func, args=(j, ))
        except job.JobInProgressError:
            msg = "A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again."
            QMessageBox.information(self.mainWindow, "Action in progress", msg)
    
    #--- Public
    def addLocation(self, path, name, removeable):
        def do(j):
            MusicGuruBase.AddLocation(self, path, name, removeable, j)
        
        error_msg = self.CanAddLocation(path, name)
        if error_msg:
            QMessageBox.warning(self.mainWindow, "Add Location", error_msg)
            return
        self._startJob(JOB_ADD, do)
    
    def addLocationPrompt(self):
        dialog = AddLocationDialog(self)
        result = dialog.exec_()
        if result == QDialog.Accepted:
            self.addLocation(dialog.locationPath, dialog.locationName, dialog.isLocationRemovable)
    
    def askForRegCode(self):
        if self.reg.ask_for_code():
            self._setup_as_registered()
    
    def copyOrMove(self, copy):
        def onNeedCd(location):
            # We can't do anything GUI related in a separate thread with Qt. Since copy/move
            # operations are performed asynchronously, the calls made to needCdDialog (created in
            # the main thread) must also be made asynchronously.
            return needCdDialog.askForDiskAsync(location.name)
        
        def do(j):
            MusicGuruBase.CopyOrMove(self, copy, dirpath, j, onNeedCd)
        
        needCdDialog = DiskNeededDialog()
        title = "Choose a destination"
        flags = QFileDialog.ShowDirsOnly
        dirpath = str(QFileDialog.getExistingDirectory(self.mainWindow, title, '', flags))
        if dirpath:
            jobid = JOB_MATERIALIZE_COPY if copy else JOB_MATERIALIZE_MOVE
            self._startJob(jobid, do)
    
    def massRename(self, model, whitespace):
        def do(j):
            self.board.MassRename(model, whitespace, j)
        
        self._startJob(JOB_MASS_RENAME, do)
    
    def moveConflicts(self, with_original=False):
        if self.board.MoveConflicts(with_original=with_original) > 0:
            self.emit(SIGNAL('boardChanged()'))
            self.emit(SIGNAL('ignoreBoxChanged()'))
    
    def moveSelectedToIgnoreBox(self):
        smart_move(self.selectedBoardItems, self.board.ignore_box, allow_merge=True)
        self.emit(SIGNAL('boardChanged()'))
        self.emit(SIGNAL('ignoreBoxChanged()'))
    
    def removeEmptyFolders(self):
        MusicGuruBase.RemoveEmptyDirs(self)
        self.emit(SIGNAL('boardChanged()'))
    
    def removeLocation(self, location):
        self.board.RemoveLocation(location)
        location.delete()
        self.emit(SIGNAL('locationsChanged()'))
        self.emit(SIGNAL('boardChanged()'))
    
    def removeLocationPrompt(self):
        location = self.selectedLocation
        if location is None:
            return
        title = "Remove location"
        msg = "Do you really want to remove location {0}?".format(location.name)
        buttons = QMessageBox.Yes | QMessageBox.No
        answer = QMessageBox.question(self.mainWindow, title, msg, buttons, QMessageBox.Yes)
        if answer != QMessageBox.Yes:
            return
        self.removeLocation(location)
    
    def renameInRespectiveLocations(self):
        def do(j):
            MusicGuruBase.RenameInRespectiveLocations(self, j)
        
        self._startJob(JOB_MATERIALIZE_RENAME, do)
    
    def selectBoardItems(self, items):
        self.selectedBoardItems = items
        self.emit(SIGNAL('boardSelectionChanged()'))
    
    def selectLocation(self, location):
        self.selectedLocation = location
    
    def showAboutBox(self):
        self.aboutBox.show()
    
    def showDetailsPanel(self):
        self._placeLocationsPanel()
        self._placeDetailsPanel()
        self.detailsPanel.show()
        self.detailsPanel.activateWindow()
    
    def showHelp(self):
        url = QUrl.fromLocalFile(op.join(op.abspath(HELP_PATH), 'intro.htm'))
        QDesktopServices.openUrl(url)
    
    def showIgnoreBox(self):
        self._placeIgnoreBox()
        self.ignoreBox.show()
        self.ignoreBox.activateWindow()
    
    def showLocationPanel(self):
        self._placeLocationsPanel()
        self.locationsPanel.show()
        self.locationsPanel.activateWindow()
    
    def split(self, model, capacity, grouping_level):
        def do(j):
            self.board.Split(model, capacity, grouping_level, j)
        
        self._startJob(JOB_SPLIT, do)
    
    def toggleLocation(self, location):
        self.board.ToggleLocation(location)
        self.emit(SIGNAL('locationsChanged()'))
        self.emit(SIGNAL('boardChanged()'))
    
    def undoSplit(self):
        self.board.Unsplit()
        self.emit(SIGNAL('boardChanged()'))
    
    def updateCollection(self):
        def do(j):
            self.collection.update_volumes(j)
        
        self._startJob(JOB_UPDATE, do)
    
    def updateLocation(self, location):
        def do(j):
            location.update(None, j)
        
        self._startJob(JOB_UPDATE, do)
    
    #--- Events
    def applicationFinishedLaunching(self):
        self.reg = Registration(self)
        self.set_registration(self.prefs.registration_code, self.prefs.registration_email)
        if not self.registered and self.unpaid_hours >= 1:
            self.reg.show_nag()
        self.mainWindow.show()
        self.showLocationPanel()
        self.showDetailsPanel()
        self.updateCollection()
    
    def jobFinished(self, jobid):
        if jobid in (JOB_UPDATE, JOB_ADD):
            self.emit(SIGNAL('locationsChanged()'))
        if jobid in (JOB_MASS_RENAME, JOB_SPLIT):
            self.emit(SIGNAL('boardChanged()'))
        if jobid in (JOB_MATERIALIZE_RENAME, JOB_MATERIALIZE_MOVE):
            self.board.Empty()
            self.emit(SIGNAL('locationsChanged()'))
            self.emit(SIGNAL('boardChanged()'))
            self.emit(SIGNAL('ignoreBoxChanged()'))
Example #11
0
class MoneyGuru(ApplicationBase):
    VERSION = MoneyGuruModel.VERSION
    LOGO_NAME = 'logo'
    
    def __init__(self):
        ApplicationBase.__init__(self)
        global APP_INSTANCE
        APP_INSTANCE = self
        self.prefs = Preferences()
        self.prefs.load()
        locale = QLocale.system()
        dateFormat = self.prefs.dateFormat
        decimalSep = locale.decimalPoint()
        groupingSep = locale.groupSeparator()
        cachePath = QDesktopServices.storageLocation(QDesktopServices.CacheLocation)
        appdata = getAppData()
        plugin_model_path = op.join(BASE_PATH, 'plugin_examples')
        DateEdit.DATE_FORMAT = dateFormat
        self.model = MoneyGuruModel(view=self, date_format=dateFormat, decimal_sep=decimalSep,
            grouping_sep=groupingSep, cache_path=cachePath, appdata_path=appdata,
            plugin_model_path=plugin_model_path)
        # on the Qt side, we're single document based, so it's one doc per app.
        self.doc = Document(app=self)
        self.doc.model.connect()
        self.mainWindow = MainWindow(doc=self.doc)
        self.importWindow = ImportWindow(self.mainWindow, doc=self.doc)
        self.csvOptionsWindow = CSVOptionsWindow(self.mainWindow, doc=self.doc)
        self.preferencesPanel = PreferencesPanel(self.mainWindow, app=self)
        self.aboutBox = AboutBox(self.mainWindow, self, withreg=False)
        if sys.argv[1:] and op.exists(sys.argv[1]):
            self.doc.open(sys.argv[1])
        elif self.prefs.recentDocuments:
            self.doc.open(self.prefs.recentDocuments[0])
        
        self.connect(self, SIGNAL('applicationFinishedLaunching()'), self.applicationFinishedLaunching)
        QCoreApplication.instance().aboutToQuit.connect(self.applicationWillTerminate)

        self.prefsChanged.emit()
    
    #--- Public
    def showAboutBox(self):
        self.aboutBox.show()
    
    def showHelp(self):
        url = QUrl.fromLocalFile(op.abspath(op.join(HELP_PATH, 'index.html')))
        QDesktopServices.openUrl(url)
    
    def showPreferences(self):
        self.preferencesPanel.load()
        if self.preferencesPanel.exec_() == QDialog.Accepted:
            self.preferencesPanel.save()
            self.prefsChanged.emit()
    
    #--- Event Handling
    def applicationFinishedLaunching(self):
        self.prefs.restoreGeometry('mainWindowGeometry', self.mainWindow)
        self.prefs.restoreGeometry('importWindowGeometry', self.importWindow)
        self.mainWindow.show()
    
    def applicationWillTerminate(self):
        self.doc.close()
        self.willSavePrefs.emit()
        self.prefs.saveGeometry('mainWindowGeometry', self.mainWindow)
        self.prefs.saveGeometry('importWindowGeometry', self.importWindow)
        self.prefs.save()
        self.model.shutdown()
    
    #--- Signals
    prefsChanged = pyqtSignal()
    willSavePrefs = pyqtSignal()
    
    #--- 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_fairware_nag(self, prompt):
        reg = Registration(self.model)
        reg.show_fairware_nag(prompt)
    
    def show_demo_nag(self, prompt):
        reg = Registration(self.model)
        reg.show_demo_nag(prompt)
    
    def show_message(self, msg):
        window = QApplication.activeWindow()
        QMessageBox.information(window, '', msg)
    
    def open_url(self, url):
        url = QUrl(url)
        QDesktopServices.openUrl(url)
    
    def reveal_path(self, path):
        url = QUrl.fromLocalFile(str(path))
        QDesktopServices.openUrl(url)