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
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)
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
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
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()'))
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
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)
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)
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)
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()'))
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)