class ProviderOptionsCaa(ProviderOptions): """ Options for Cover Art Archive cover art provider """ options = [ config.BoolOption("setting", "caa_save_single_front_image", False), config.BoolOption("setting", "caa_approved_only", False), config.BoolOption("setting", "caa_image_type_as_filename", False), config.IntOption("setting", "caa_image_size", 1), config.ListOption("setting", "caa_image_types", [u"front"]), config.BoolOption("setting", "caa_restrict_image_types", True), ] _options_ui = Ui_CaaOptions def __init__(self, parent=None): super(ProviderOptionsCaa, self).__init__(parent) self.ui.restrict_images_types.clicked.connect(self.update_caa_types) self.ui.select_caa_types.clicked.connect(self.select_caa_types) def load(self): self.ui.cb_image_size.setCurrentIndex(config.setting["caa_image_size"]) self.ui.cb_save_single_front_image.setChecked( config.setting["caa_save_single_front_image"]) self.ui.cb_approved_only.setChecked( config.setting["caa_approved_only"]) self.ui.cb_type_as_filename.setChecked( config.setting["caa_image_type_as_filename"]) self.ui.restrict_images_types.setChecked( config.setting["caa_restrict_image_types"]) self.update_caa_types() def save(self): config.setting["caa_image_size"] = \ self.ui.cb_image_size.currentIndex() config.setting["caa_save_single_front_image"] = \ self.ui.cb_save_single_front_image.isChecked() config.setting["caa_approved_only"] = \ self.ui.cb_approved_only.isChecked() config.setting["caa_image_type_as_filename"] = \ self.ui.cb_type_as_filename.isChecked() config.setting["caa_restrict_image_types"] = \ self.ui.restrict_images_types.isChecked() def update_caa_types(self): enabled = self.ui.restrict_images_types.isChecked() self.ui.select_caa_types.setEnabled(enabled) def select_caa_types(self): (types, ok) = CAATypesSelectorDialog.run(self, config.setting["caa_image_types"]) if ok: config.setting["caa_image_types"] = types
class AdvancedOptionsPage(OptionsPage): NAME = "advanced" TITLE = N_("Advanced") PARENT = None SORT_ORDER = 90 ACTIVE = True HELP_URL = '/config/options_advanced.html' options = [ config.TextOption("setting", "ignore_regex", ""), config.BoolOption("setting", "ignore_hidden_files", False), config.BoolOption("setting", "recursively_add_files", True), config.IntOption("setting", "ignore_track_duration_difference_under", 2), config.BoolOption("setting", "completeness_ignore_videos", False), config.BoolOption("setting", "completeness_ignore_pregap", False), config.BoolOption("setting", "completeness_ignore_data", False), config.BoolOption("setting", "completeness_ignore_silence", False), config.ListOption("setting", "compare_ignore_tags", []), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_AdvancedOptionsPage() self.ui.setupUi(self) self.init_regex_checker(self.ui.ignore_regex, self.ui.regex_error) def load(self): self.ui.ignore_regex.setText(config.setting["ignore_regex"]) self.ui.ignore_hidden_files.setChecked(config.setting["ignore_hidden_files"]) self.ui.recursively_add_files.setChecked(config.setting["recursively_add_files"]) self.ui.ignore_track_duration_difference_under.setValue(config.setting["ignore_track_duration_difference_under"]) self.ui.completeness_ignore_videos.setChecked(config.setting["completeness_ignore_videos"]) self.ui.completeness_ignore_pregap.setChecked(config.setting["completeness_ignore_pregap"]) self.ui.completeness_ignore_data.setChecked(config.setting["completeness_ignore_data"]) self.ui.completeness_ignore_silence.setChecked(config.setting["completeness_ignore_silence"]) self.ui.compare_ignore_tags.update(config.setting["compare_ignore_tags"]) self.ui.compare_ignore_tags.set_user_sortable(False) def save(self): config.setting["ignore_regex"] = self.ui.ignore_regex.text() config.setting["ignore_hidden_files"] = self.ui.ignore_hidden_files.isChecked() config.setting["recursively_add_files"] = self.ui.recursively_add_files.isChecked() config.setting["ignore_track_duration_difference_under"] = self.ui.ignore_track_duration_difference_under.value() config.setting["completeness_ignore_videos"] = self.ui.completeness_ignore_videos.isChecked() config.setting["completeness_ignore_pregap"] = self.ui.completeness_ignore_pregap.isChecked() config.setting["completeness_ignore_data"] = self.ui.completeness_ignore_data.isChecked() config.setting["completeness_ignore_silence"] = self.ui.completeness_ignore_silence.isChecked() tags = list(self.ui.compare_ignore_tags.tags) if tags != config.setting["compare_ignore_tags"]: config.setting["compare_ignore_tags"] = tags def restore_defaults(self): self.ui.compare_ignore_tags.clear() super().restore_defaults()
class TagsOptionsPage(OptionsPage): NAME = "tags" TITLE = N_("Tags") PARENT = None SORT_ORDER = 30 ACTIVE = True HELP_URL = '/config/options_tags.html' options = [ config.BoolOption("setting", "dont_write_tags", False), config.BoolOption("setting", "preserve_timestamps", False), config.BoolOption("setting", "clear_existing_tags", False), config.BoolOption("setting", "remove_id3_from_flac", False), config.BoolOption("setting", "remove_ape_from_mp3", False), config.ListOption("setting", "preserved_tags", []), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_TagsOptionsPage() self.ui.setupUi(self) def load(self): self.ui.write_tags.setChecked(not config.setting["dont_write_tags"]) self.ui.preserve_timestamps.setChecked( config.setting["preserve_timestamps"]) self.ui.clear_existing_tags.setChecked( config.setting["clear_existing_tags"]) self.ui.remove_ape_from_mp3.setChecked( config.setting["remove_ape_from_mp3"]) self.ui.remove_id3_from_flac.setChecked( config.setting["remove_id3_from_flac"]) self.ui.preserved_tags.update(config.setting["preserved_tags"]) self.ui.preserved_tags.set_user_sortable(False) def save(self): config.setting["dont_write_tags"] = not self.ui.write_tags.isChecked() config.setting[ "preserve_timestamps"] = self.ui.preserve_timestamps.isChecked() clear_existing_tags = self.ui.clear_existing_tags.isChecked() if clear_existing_tags != config.setting["clear_existing_tags"]: config.setting["clear_existing_tags"] = clear_existing_tags self.tagger.window.metadata_box.update() config.setting[ "remove_ape_from_mp3"] = self.ui.remove_ape_from_mp3.isChecked() config.setting[ "remove_id3_from_flac"] = self.ui.remove_id3_from_flac.isChecked() config.setting["preserved_tags"] = list(self.ui.preserved_tags.tags) self.tagger.window.enable_tag_saving_action.setChecked( not config.setting["dont_write_tags"])
class InterfaceTopTagsOptionsPage(OptionsPage): NAME = "interface_top_tags" TITLE = N_("Top Tags") PARENT = "interface" SORT_ORDER = 30 ACTIVE = True HELP_URL = '/config/options_interface_top_tags.html' options = [ config.ListOption("setting", "metadatabox_top_tags", [ "title", "artist", "album", "tracknumber", "~length", "date", ]), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_InterfaceTopTagsOptionsPage() self.ui.setupUi(self) def load(self): tags = config.setting["metadatabox_top_tags"] self.ui.top_tags_list.update(tags) def save(self): tags = list(self.ui.top_tags_list.tags) if tags != config.setting["metadatabox_top_tags"]: config.setting["metadatabox_top_tags"] = tags self.tagger.window.metadata_box.update() def restore_defaults(self): self.ui.top_tags_list.clear() super().restore_defaults()
class CoverOptionsPage(OptionsPage): NAME = "cover" TITLE = N_("Cover Art") PARENT = None SORT_ORDER = 35 ACTIVE = True options = [ config.BoolOption("setting", "save_images_to_tags", True), config.BoolOption("setting", "embed_only_one_front_image", True), config.BoolOption("setting", "save_images_to_files", False), config.TextOption("setting", "cover_image_filename", "cover"), config.BoolOption("setting", "save_images_overwrite", False), config.ListOption("setting", "ca_providers", [ ('Cover Art Archive', True), ('Amazon', True), ('Whitelist', True), ('CaaReleaseGroup', False), ('Local', False), ]), ] def __init__(self, parent=None): super(CoverOptionsPage, self).__init__(parent) self.ui = Ui_CoverOptionsPage() self.ui.setupUi(self) self.ui.save_images_to_files.clicked.connect(self.update_filename) self.ui.save_images_to_tags.clicked.connect(self.update_save_images_to_tags) self.provider_list_widget = SortableCheckboxListWidget() self.ui.ca_providers_list.insertWidget(0, self.provider_list_widget) self.ca_providers = [] def load_cover_art_providers(self): """Load available providers, initialize provider-specific options, restore state of each """ providers = cover_art_providers() for provider in providers: try: title = _(provider.TITLE) except AttributeError: title = provider.NAME checked = is_provider_enabled(provider.NAME) self.provider_list_widget.addItem(SortableCheckboxListItem(title, checked=checked, data=provider.NAME)) def update_providers_options(items): self.ca_providers = [(item.data, item.checked) for item in items] self.provider_list_widget.changed.connect(update_providers_options) def restore_defaults(self): # Remove previous entries self.provider_list_widget.clear() super(CoverOptionsPage, self).restore_defaults() def load(self): self.ui.save_images_to_tags.setChecked(config.setting["save_images_to_tags"]) self.ui.cb_embed_front_only.setChecked(config.setting["embed_only_one_front_image"]) self.ui.save_images_to_files.setChecked(config.setting["save_images_to_files"]) self.ui.cover_image_filename.setText(config.setting["cover_image_filename"]) self.ui.save_images_overwrite.setChecked(config.setting["save_images_overwrite"]) self.ca_providers = config.setting["ca_providers"] self.load_cover_art_providers() self.update_all() def save(self): config.setting["save_images_to_tags"] = self.ui.save_images_to_tags.isChecked() config.setting["embed_only_one_front_image"] = self.ui.cb_embed_front_only.isChecked() config.setting["save_images_to_files"] = self.ui.save_images_to_files.isChecked() config.setting["cover_image_filename"] = self.ui.cover_image_filename.text() config.setting["save_images_overwrite"] = self.ui.save_images_overwrite.isChecked() config.setting["ca_providers"] = self.ca_providers def update_all(self): self.update_filename() self.update_save_images_to_tags() def update_filename(self): enabled = self.ui.save_images_to_files.isChecked() self.ui.cover_image_filename.setEnabled(enabled) self.ui.save_images_overwrite.setEnabled(enabled) def update_save_images_to_tags(self): enabled = self.ui.save_images_to_tags.isChecked() self.ui.cb_embed_front_only.setEnabled(enabled)
class ScriptingOptionsPage(OptionsPage): NAME = "scripting" TITLE = N_("Scripting") PARENT = None SORT_ORDER = 85 ACTIVE = True HELP_URL = '/config/options_scripting.html' options = [ config.BoolOption("setting", "enable_tagger_scripts", False), config.ListOption("setting", "list_of_scripts", []), config.IntOption("persist", "last_selected_script_pos", 0), config.Option("persist", "scripting_splitter", QtCore.QByteArray()), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_ScriptingOptionsPage() self.ui.setupUi(self) self.ui.tagger_script.setEnabled(False) self.ui.splitter.setStretchFactor(0, 1) self.ui.splitter.setStretchFactor(1, 2) self.move_view = MoveableListView(self.ui.script_list, self.ui.move_up_button, self.ui.move_down_button) self.ui.scripting_documentation_button.clicked.connect( self.show_scripting_documentation) self.scripting_documentation_shown = None def show_scripting_documentation(self): if not self.scripting_documentation_shown: self.scriptdoc_dialog = ScriptingDocumentationDialog(parent=self) self.scriptdoc_dialog.show() else: self.scriptdoc_dialog.raise_() self.scriptdoc_dialog.activateWindow() def enable_tagger_scripts_toggled(self, on): if on and self.ui.script_list.count() == 0: self.ui.script_list.add_script() def script_selected(self): items = self.ui.script_list.selectedItems() if items: item = items[0] self.ui.tagger_script.setEnabled(True) self.ui.tagger_script.setText(item.script) self.ui.tagger_script.setFocus(QtCore.Qt.OtherFocusReason) else: self.ui.tagger_script.setEnabled(False) self.ui.tagger_script.setText("") def live_update_and_check(self): items = self.ui.script_list.selectedItems() if items: script = items[0] script.script = self.ui.tagger_script.toPlainText() self.ui.script_error.setStyleSheet("") self.ui.script_error.setText("") try: self.check() except OptionsCheckError as e: self.ui.script_error.setStyleSheet(self.STYLESHEET_ERROR) self.ui.script_error.setText(e.info) return def check(self): parser = ScriptParser() try: parser.eval(self.ui.tagger_script.toPlainText()) except Exception as e: raise ScriptCheckError(_("Script Error"), str(e)) def restore_defaults(self): # Remove existing scripts self.ui.script_list.clear() self.ui.tagger_script.setText("") super().restore_defaults() def load(self): self.ui.enable_tagger_scripts.setChecked( config.setting["enable_tagger_scripts"]) for pos, name, enabled, text in config.setting["list_of_scripts"]: list_item = ScriptListWidgetItem(name, enabled, text) self.ui.script_list.addItem(list_item) # Select the last selected script item last_selected_script_pos = config.persist["last_selected_script_pos"] last_selected_script = self.ui.script_list.item( last_selected_script_pos) if last_selected_script: self.ui.script_list.setCurrentItem(last_selected_script) last_selected_script.setSelected(True) self.restore_state() def _all_scripts(self): for row in range(0, self.ui.script_list.count()): item = self.ui.script_list.item(row) yield item.get_all() @restore_method def restore_state(self): # Preserve previous splitter position self.ui.splitter.restoreState(config.persist["scripting_splitter"]) def save(self): config.setting[ "enable_tagger_scripts"] = self.ui.enable_tagger_scripts.isChecked( ) config.setting["list_of_scripts"] = list(self._all_scripts()) config.persist[ "last_selected_script_pos"] = self.ui.script_list.currentRow() config.persist["scripting_splitter"] = self.ui.splitter.saveState() def display_error(self, error): # Ignore scripting errors, those are handled inline if not isinstance(error, ScriptCheckError): super().display_error(error)
class CoverOptionsPage(OptionsPage): NAME = "cover" TITLE = N_("Cover Art") PARENT = None SORT_ORDER = 35 ACTIVE = True options = [ config.BoolOption("setting", "save_images_to_tags", True), config.BoolOption("setting", "save_only_front_images_to_tags", True), config.BoolOption("setting", "save_images_to_files", False), config.TextOption("setting", "cover_image_filename", "cover"), config.BoolOption("setting", "save_images_overwrite", False), config.BoolOption("setting", "ca_provider_use_amazon", True), config.BoolOption("setting", "ca_provider_use_caa", True), config.BoolOption("setting", "ca_provider_use_whitelist", True), config.BoolOption("setting", "caa_approved_only", True), config.BoolOption("setting", "caa_image_type_as_filename", False), config.IntOption("setting", "caa_image_size", 1), config.ListOption("setting", "caa_image_types", [u"front"]), ] def __init__(self, parent=None): super(CoverOptionsPage, self).__init__(parent) self.ui = Ui_CoverOptionsPage() self.ui.setupUi(self) self.ui.save_images_to_files.clicked.connect(self.update_filename) def load(self): self.ui.save_images_to_tags.setChecked( config.setting["save_images_to_tags"]) self.ui.cb_embed_front_only.setChecked( config.setting["save_only_front_images_to_tags"]) self.ui.save_images_to_files.setChecked( config.setting["save_images_to_files"]) self.ui.cover_image_filename.setText( config.setting["cover_image_filename"]) self.ui.save_images_overwrite.setChecked( config.setting["save_images_overwrite"]) self.update_filename() self.ui.caprovider_amazon.setChecked( config.setting["ca_provider_use_amazon"]) self.ui.caprovider_caa.setChecked( config.setting["ca_provider_use_caa"]) self.ui.caprovider_whitelist.setChecked( config.setting["ca_provider_use_whitelist"]) self.ui.gb_caa.setEnabled(config.setting["ca_provider_use_caa"]) self.ui.cb_image_size.setCurrentIndex(config.setting["caa_image_size"]) widget = self.ui.caa_types_selector_1 self._selector = CAATypesSelector(widget, config.setting["caa_image_types"]) config.setting["caa_image_types"] = self._selector.get_selected_types() self.ui.cb_approved_only.setChecked( config.setting["caa_approved_only"]) self.ui.cb_type_as_filename.setChecked( config.setting["caa_image_type_as_filename"]) self.connect(self.ui.caprovider_caa, QtCore.SIGNAL("toggled(bool)"), self.ui.gb_caa.setEnabled) def save(self): config.setting[ "save_images_to_tags"] = self.ui.save_images_to_tags.isChecked() config.setting[ "save_only_front_images_to_tags"] = self.ui.cb_embed_front_only.isChecked( ) config.setting[ "save_images_to_files"] = self.ui.save_images_to_files.isChecked() config.setting["cover_image_filename"] = unicode( self.ui.cover_image_filename.text()) config.setting["ca_provider_use_amazon"] =\ self.ui.caprovider_amazon.isChecked() config.setting["ca_provider_use_caa"] =\ self.ui.caprovider_caa.isChecked() config.setting["ca_provider_use_whitelist"] =\ self.ui.caprovider_whitelist.isChecked() config.setting["caa_image_size"] =\ self.ui.cb_image_size.currentIndex() config.setting["caa_image_types"] = self._selector.get_selected_types() config.setting["caa_approved_only"] =\ self.ui.cb_approved_only.isChecked() config.setting["caa_image_type_as_filename"] = \ self.ui.cb_type_as_filename.isChecked() config.setting[ "save_images_overwrite"] = self.ui.save_images_overwrite.isChecked( ) def update_filename(self): enabled = self.ui.save_images_to_files.isChecked() self.ui.cover_image_filename.setEnabled(enabled) self.ui.save_images_overwrite.setEnabled(enabled)
class InterfaceOptionsPage(OptionsPage): NAME = "interface" TITLE = N_("User Interface") PARENT = None SORT_ORDER = 80 ACTIVE = True SEPARATOR = '—' * 5 TOOLBAR_BUTTONS = { 'add_directory_action': { 'label': N_('Add Folder'), 'icon': 'folder' }, 'add_files_action': { 'label': N_('Add Files'), 'icon': 'document-open' }, 'cluster_action': { 'label': N_('Cluster'), 'icon': 'picard-cluster' }, 'autotag_action': { 'label': N_('Lookup'), 'icon': 'picard-auto-tag' }, 'analyze_action': { 'label': N_('Scan'), 'icon': 'picard-analyze' }, 'browser_lookup_action': { 'label': N_('Lookup in Browser'), 'icon': 'lookup-musicbrainz' }, 'save_action': { 'label': N_('Save'), 'icon': 'document-save' }, 'view_info_action': { 'label': N_('Info'), 'icon': 'picard-edit-tags' }, 'remove_action': { 'label': N_('Remove'), 'icon': 'list-remove' }, 'submit_acoustid_action': { 'label': N_('Submit AcoustIDs'), 'icon': 'acoustid-fingerprinter' }, 'play_file_action': { 'label': N_('Open in Player'), 'icon': 'play-music' }, 'cd_lookup_action': { 'label': N_('Lookup CD...'), 'icon': 'media-optical' }, } ACTION_NAMES = set(TOOLBAR_BUTTONS.keys()) options = [ config.BoolOption("setting", "toolbar_show_labels", True), config.BoolOption("setting", "toolbar_multiselect", False), config.BoolOption("setting", "builtin_search", False), config.BoolOption("setting", "use_adv_search_syntax", False), config.BoolOption("setting", "quit_confirmation", True), config.TextOption("setting", "ui_language", ""), config.BoolOption("setting", "starting_directory", False), config.TextOption("setting", "starting_directory_path", _default_starting_dir), config.TextOption("setting", "load_image_behavior", "append"), config.ListOption("setting", "toolbar_layout", [ 'add_directory_action', 'add_files_action', 'separator', 'cluster_action', 'separator', 'autotag_action', 'analyze_action', 'browser_lookup_action', 'separator', 'save_action', 'view_info_action', 'remove_action', 'separator', 'cd_lookup_action', 'separator', 'submit_acoustid_action', ]), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_InterfaceOptionsPage() self.ui.setupUi(self) self.ui.ui_language.addItem(_('System default'), '') language_list = [(l[0], l[1], _(l[2])) for l in UI_LANGUAGES] def fcmp(x): return locale.strxfrm(x[2]) for lang_code, native, translation in sorted(language_list, key=fcmp): if native and native != translation: name = '%s (%s)' % (translation, native) else: name = translation self.ui.ui_language.addItem(name, lang_code) self.ui.starting_directory.stateChanged.connect( partial(enabledSlot, self.ui.starting_directory_path.setEnabled)) self.ui.starting_directory.stateChanged.connect( partial(enabledSlot, self.ui.starting_directory_browse.setEnabled)) self.ui.starting_directory_browse.clicked.connect( self.starting_directory_browse) self.ui.add_button.clicked.connect(self.add_to_toolbar) self.ui.insert_separator_button.clicked.connect(self.insert_separator) self.ui.remove_button.clicked.connect(self.remove_action) self.move_view = MoveableListView(self.ui.toolbar_layout_list, self.ui.up_button, self.ui.down_button, self.update_action_buttons) self.update_buttons = self.move_view.update_buttons def load(self): self.ui.toolbar_show_labels.setChecked( config.setting["toolbar_show_labels"]) self.ui.toolbar_multiselect.setChecked( config.setting["toolbar_multiselect"]) self.ui.builtin_search.setChecked(config.setting["builtin_search"]) self.ui.use_adv_search_syntax.setChecked( config.setting["use_adv_search_syntax"]) self.ui.quit_confirmation.setChecked( config.setting["quit_confirmation"]) current_ui_language = config.setting["ui_language"] self.ui.ui_language.setCurrentIndex( self.ui.ui_language.findData(current_ui_language)) self.ui.starting_directory.setChecked( config.setting["starting_directory"]) self.ui.starting_directory_path.setText( config.setting["starting_directory_path"]) self.populate_action_list() self.ui.toolbar_layout_list.setCurrentRow(0) self.update_buttons() def save(self): config.setting[ "toolbar_show_labels"] = self.ui.toolbar_show_labels.isChecked() config.setting[ "toolbar_multiselect"] = self.ui.toolbar_multiselect.isChecked() config.setting["builtin_search"] = self.ui.builtin_search.isChecked() config.setting[ "use_adv_search_syntax"] = self.ui.use_adv_search_syntax.isChecked( ) config.setting[ "quit_confirmation"] = self.ui.quit_confirmation.isChecked() self.tagger.window.update_toolbar_style() new_language = self.ui.ui_language.itemData( self.ui.ui_language.currentIndex()) if new_language != config.setting["ui_language"]: config.setting["ui_language"] = self.ui.ui_language.itemData( self.ui.ui_language.currentIndex()) dialog = QtWidgets.QMessageBox( QtWidgets.QMessageBox.Information, _('Language changed'), _('You have changed the interface language. You have to restart Picard in order for the change to take effect.' ), QtWidgets.QMessageBox.Ok, self) dialog.exec_() config.setting[ "starting_directory"] = self.ui.starting_directory.isChecked() config.setting["starting_directory_path"] = os.path.normpath( self.ui.starting_directory_path.text()) self.update_layout_config() def restore_defaults(self): super().restore_defaults() self.update_buttons() def starting_directory_browse(self): item = self.ui.starting_directory_path path = QtWidgets.QFileDialog.getExistingDirectory( self, "", item.text()) if path: path = os.path.normpath(path) item.setText(path) def _get_icon_from_name(self, name): return self.TOOLBAR_BUTTONS[name]['icon'] def _insert_item(self, action, index=None): list_item = ToolbarListItem(action) list_item.setToolTip(_('Drag and Drop to re-order')) if action in self.TOOLBAR_BUTTONS: # TODO: Remove temporary workaround once https://github.com/python-babel/babel/issues/415 has been resolved. babel_415_workaround = self.TOOLBAR_BUTTONS[action]['label'] list_item.setText(_(babel_415_workaround)) list_item.setIcon( icontheme.lookup(self._get_icon_from_name(action), icontheme.ICON_SIZE_MENU)) else: list_item.setText(self.SEPARATOR) if index is not None: self.ui.toolbar_layout_list.insertItem(index, list_item) else: self.ui.toolbar_layout_list.addItem(list_item) return list_item def _all_list_items(self): return [ self.ui.toolbar_layout_list.item(i).action_name for i in range(self.ui.toolbar_layout_list.count()) ] def _added_actions(self): actions = self._all_list_items() return set(action for action in actions if action != 'separator') def populate_action_list(self): self.ui.toolbar_layout_list.clear() for name in config.setting['toolbar_layout']: if name in self.ACTION_NAMES or name == 'separator': self._insert_item(name) def update_action_buttons(self): self.ui.add_button.setEnabled( self._added_actions() != self.ACTION_NAMES) def add_to_toolbar(self): display_list = set.difference(self.ACTION_NAMES, self._added_actions()) selected_action, ok = AddActionDialog.get_selected_action( display_list, self) if ok: list_item = self._insert_item( selected_action, self.ui.toolbar_layout_list.currentRow() + 1) self.ui.toolbar_layout_list.setCurrentItem(list_item) self.update_buttons() def insert_separator(self): insert_index = self.ui.toolbar_layout_list.currentRow() + 1 self._insert_item('separator', insert_index) def remove_action(self): item = self.ui.toolbar_layout_list.takeItem( self.ui.toolbar_layout_list.currentRow()) del item self.update_buttons() def update_layout_config(self): config.setting['toolbar_layout'] = self._all_list_items() self._update_toolbar() def _update_toolbar(self): widget = self.parent() while not isinstance(widget, QtWidgets.QMainWindow): widget = widget.parent() # Call the main window's create toolbar method widget.create_action_toolbar() widget.set_tab_order()
class CoverOptionsPage(OptionsPage): NAME = "cover" TITLE = N_("Cover Art") PARENT = None SORT_ORDER = 35 ACTIVE = True HELP_URL = '/config/options_cover.html' options = [ config.BoolOption("setting", "save_images_to_tags", True), config.BoolOption("setting", "embed_only_one_front_image", True), config.BoolOption("setting", "save_images_to_files", False), config.TextOption("setting", "cover_image_filename", "cover"), config.BoolOption("setting", "save_images_overwrite", False), config.ListOption("setting", "ca_providers", [ ('Cover Art Archive', True), ('Amazon', True), ('Whitelist', True), ('CaaReleaseGroup', False), ('Local', False), ]), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_CoverOptionsPage() self.ui.setupUi(self) self.ui.save_images_to_files.clicked.connect(self.update_filename) self.ui.save_images_to_tags.clicked.connect(self.update_save_images_to_tags) self.move_view = MoveableListView(self.ui.ca_providers_list, self.ui.up_button, self.ui.down_button) def load_cover_art_providers(self): """Load available providers, initialize provider-specific options, restore state of each """ for p in cover_art_providers(): self.ui.ca_providers_list.addItem(CheckboxListItem(_(p.title), checked=p.enabled, data=p.name)) def restore_defaults(self): # Remove previous entries self.ui.ca_providers_list.clear() super().restore_defaults() def ca_providers(self): items = [] for i in range(self.ui.ca_providers_list.count()): item = self.ui.ca_providers_list.item(i) items.append((item.data, item.checked)) return items def load(self): self.ui.save_images_to_tags.setChecked(config.setting["save_images_to_tags"]) self.ui.cb_embed_front_only.setChecked(config.setting["embed_only_one_front_image"]) self.ui.save_images_to_files.setChecked(config.setting["save_images_to_files"]) self.ui.cover_image_filename.setText(config.setting["cover_image_filename"]) self.ui.save_images_overwrite.setChecked(config.setting["save_images_overwrite"]) self.load_cover_art_providers() self.ui.ca_providers_list.setCurrentRow(0) self.update_all() def save(self): config.setting["save_images_to_tags"] = self.ui.save_images_to_tags.isChecked() config.setting["embed_only_one_front_image"] = self.ui.cb_embed_front_only.isChecked() config.setting["save_images_to_files"] = self.ui.save_images_to_files.isChecked() config.setting["cover_image_filename"] = self.ui.cover_image_filename.text() config.setting["save_images_overwrite"] = self.ui.save_images_overwrite.isChecked() config.setting["ca_providers"] = self.ca_providers() def update_all(self): self.update_filename() self.update_save_images_to_tags() def update_ca_providers_groupbox_state(self): files_enabled = self.ui.save_images_to_files.isChecked() tags_enabled = self.ui.save_images_to_tags.isChecked() self.ui.ca_providers_groupbox.setEnabled(files_enabled or tags_enabled) def update_filename(self): enabled = self.ui.save_images_to_files.isChecked() self.ui.cover_image_filename.setEnabled(enabled) self.ui.save_images_overwrite.setEnabled(enabled) self.update_ca_providers_groupbox_state() def update_save_images_to_tags(self): enabled = self.ui.save_images_to_tags.isChecked() self.ui.cb_embed_front_only.setEnabled(enabled) self.update_ca_providers_groupbox_state()
class ScriptingOptionsPage(OptionsPage): NAME = "scripting" TITLE = N_("Scripting") PARENT = None SORT_ORDER = 85 ACTIVE = True options = [ config.BoolOption("setting", "enable_tagger_scripts", False), config.ListOption("setting", "list_of_scripts", []), config.IntOption("persist", "last_selected_script_pos", 0), config.Option("persist", "scripting_splitter", QtCore.QByteArray()), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_ScriptingOptionsPage() self.ui.setupUi(self) self.highlighter = TaggerScriptSyntaxHighlighter( self.ui.tagger_script.document()) self.ui.tagger_script.textChanged.connect(self.live_update_and_check) self.ui.script_name.textChanged.connect(self.script_name_changed) self.ui.add_script.clicked.connect(self.add_to_list_of_scripts) self.ui.script_list.itemSelectionChanged.connect(self.script_selected) self.ui.tagger_script.setEnabled(False) self.ui.script_name.setEnabled(False) self.listitem_to_scriptitem = {} self.list_of_scripts = [] self.last_selected_script_pos = 0 self.ui.splitter.setStretchFactor(0, 1) self.ui.splitter.setStretchFactor(1, 2) def script_name_changed(self): items = self.ui.script_list.selectedItems() if items: script = self.listitem_to_scriptitem[items[0]] script.name = self.ui.script_name.text() list_widget = self.ui.script_list.itemWidget(items[0]) list_widget.update_name(script.name) self.list_of_scripts[script.pos] = script.get_all() def script_selected(self): items = self.ui.script_list.selectedItems() if items: self.ui.tagger_script.setEnabled(True) self.ui.script_name.setEnabled(True) script = self.listitem_to_scriptitem[items[0]] self.ui.tagger_script.setText(script.text) self.ui.script_name.setText(script.name) self.last_selected_script_pos = script.pos def setSignals(self, list_widget, item): list_widget.set_up_connection( lambda: self.move_script(self.ui.script_list.row(item), 1)) list_widget.set_down_connection( lambda: self.move_script(self.ui.script_list.row(item), -1)) list_widget.set_remove_connection( lambda: self.remove_from_list_of_scripts( self.ui.script_list.row(item))) list_widget.set_checkbox_connection(lambda: self.update_check_state( item, list_widget.checkbox_state())) list_widget.set_rename_connection(lambda: self.rename_script(item)) def rename_script(self, item): item.setSelected(True) self.ui.script_name.setFocus() self.ui.script_name.selectAll() def update_check_state(self, item, checkbox_state): script = self.listitem_to_scriptitem[item] script.enabled = checkbox_state self.list_of_scripts[script.pos] = script.get_all() def add_to_list_of_scripts(self): count = self.ui.script_list.count() numbered_name = _(DEFAULT_NUMBERED_SCRIPT_NAME) % (count + 1) script = ScriptItem(pos=count, name=numbered_name) list_item = HashableListWidgetItem() list_widget = AdvancedScriptItem(numbered_name) self.setSignals(list_widget, list_item) self.ui.script_list.addItem(list_item) self.ui.script_list.setItemWidget(list_item, list_widget) self.listitem_to_scriptitem[list_item] = script self.list_of_scripts.append(script.get_all()) list_item.setSelected(True) def update_script_positions(self): for i, script in enumerate(self.list_of_scripts): self.list_of_scripts[i] = (i, script[1], script[2], script[3]) item = self.ui.script_list.item(i) self.listitem_to_scriptitem[item].pos = i def remove_from_list_of_scripts(self, row): item = self.ui.script_list.item(row) confirm_remove = QtWidgets.QMessageBox() msg = _("Are you sure you want to remove this script?") reply = confirm_remove.question(confirm_remove, _('Confirm Remove'), msg, QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) if item and reply == QtWidgets.QMessageBox.Yes: item = self.ui.script_list.takeItem(row) script = self.listitem_to_scriptitem[item] del self.listitem_to_scriptitem[item] del self.list_of_scripts[script.pos] del script item = None # update positions of other items self.update_script_positions() if not self.ui.script_list: self.ui.tagger_script.setText("") self.ui.tagger_script.setEnabled(False) self.ui.script_name.setText("") self.ui.script_name.setEnabled(False) # update position of last_selected_script if row == self.last_selected_script_pos: self.last_selected_script_pos = 0 # workaround to remove residue on UI if not self.ui.script_list.selectedItems(): current_item = self.ui.script_list.currentItem() if current_item: current_item.setSelected(True) else: item = self.ui.script_list.item(0) item.setSelected(True) elif row < self.last_selected_script_pos: self.last_selected_script_pos -= 1 def move_script(self, row, step): item1 = self.ui.script_list.item(row) item2 = self.ui.script_list.item(row - step) if item1 and item2: # make changes in the ui list_item = self.ui.script_list.takeItem(row) script = self.listitem_to_scriptitem[list_item] # list_widget has to be set again list_widget = AdvancedScriptItem(name=script.name, state=script.enabled) self.setSignals(list_widget, list_item) self.ui.script_list.insertItem(row - step, list_item) self.ui.script_list.setItemWidget(list_item, list_widget) # make changes in the picklable list script1 = self.listitem_to_scriptitem[item1] script2 = self.listitem_to_scriptitem[item2] # workaround since tuples are immutable indices = script1.pos, script2.pos self.list_of_scripts = [ i for j, i in enumerate(self.list_of_scripts) if j not in indices ] new_script1 = (script1.pos - step, script1.name, script1.enabled, script1.text) new_script2 = (script2.pos + step, script2.name, script2.enabled, script2.text) self.list_of_scripts.append(new_script1) self.list_of_scripts.append(new_script2) self.list_of_scripts = sorted(self.list_of_scripts, key=lambda x: x[0]) # corresponding mapping support also has to be updated self.listitem_to_scriptitem[item1] = ScriptItem( script1.pos - step, script1.name, script1.enabled, script1.text) self.listitem_to_scriptitem[item2] = ScriptItem( script2.pos + step, script2.name, script2.enabled, script2.text) def live_update_and_check(self): items = self.ui.script_list.selectedItems() if items: script = self.listitem_to_scriptitem[items[0]] script.text = self.ui.tagger_script.toPlainText() self.list_of_scripts[script.pos] = script.get_all() self.ui.script_error.setStyleSheet("") self.ui.script_error.setText("") try: self.check() except OptionsCheckError as e: self.ui.script_error.setStyleSheet(self.STYLESHEET_ERROR) self.ui.script_error.setText(e.info) return def check(self): parser = ScriptParser() try: parser.eval(self.ui.tagger_script.toPlainText()) except Exception as e: raise OptionsCheckError(_("Script Error"), string_(e)) def restore_defaults(self): # Remove existing scripts self.ui.script_list.clear() self.ui.script_name.setText("") self.ui.tagger_script.setText("") super().restore_defaults() def load(self): self.ui.enable_tagger_scripts.setChecked( config.setting["enable_tagger_scripts"]) self.list_of_scripts = config.setting["list_of_scripts"] for s_pos, s_name, s_enabled, s_text in self.list_of_scripts: script = ScriptItem(s_pos, s_name, s_enabled, s_text) list_item = HashableListWidgetItem() list_widget = AdvancedScriptItem(name=s_name, state=s_enabled) self.setSignals(list_widget, list_item) self.ui.script_list.addItem(list_item) self.ui.script_list.setItemWidget(list_item, list_widget) self.listitem_to_scriptitem[list_item] = script # Select the last selected script item self.last_selected_script_pos = config.persist[ "last_selected_script_pos"] last_selected_script = self.ui.script_list.item( self.last_selected_script_pos) if last_selected_script: last_selected_script.setSelected(True) # Preserve previous splitter position self.ui.splitter.restoreState(config.persist["scripting_splitter"]) args = { "picard-doc-scripting-url": PICARD_URLS['doc_scripting'], } text = _('<a href="%(picard-doc-scripting-url)s">Open Scripting' ' Documentation in your browser</a>') % args self.ui.scripting_doc_link.setText(text) def save(self): config.setting[ "enable_tagger_scripts"] = self.ui.enable_tagger_scripts.isChecked( ) config.setting["list_of_scripts"] = self.list_of_scripts config.persist[ "last_selected_script_pos"] = self.last_selected_script_pos config.persist["scripting_splitter"] = self.ui.splitter.saveState() def display_error(self, error): pass
class PluginsOptionsPage(OptionsPage): NAME = "plugins" TITLE = N_("Plugins") PARENT = None SORT_ORDER = 70 ACTIVE = True options = [ config.ListOption("setting", "enabled_plugins", []), config.Option("persist", "plugins_list_state", QtCore.QByteArray()), config.Option("persist", "plugins_list_sort_section", 0), config.Option("persist", "plugins_list_sort_order", QtCore.Qt.AscendingOrder), config.Option("persist", "plugins_list_selected", ""), ] def __init__(self, parent=None): super(PluginsOptionsPage, self).__init__(parent) self.ui = Ui_PluginsOptionsPage() self.ui.setupUi(self) self.items = {} self.ui.plugins.itemSelectionChanged.connect(self.change_details) self.ui.plugins.mimeTypes = self.mimeTypes self.ui.plugins.dropEvent = self.dropEvent self.ui.plugins.dragEnterEvent = self.dragEnterEvent if sys.platform == "win32": self.loader = "file:///%s" else: self.loader = "file://%s" self.ui.install_plugin.clicked.connect(self.open_plugins) self.ui.folder_open.clicked.connect(self.open_plugin_dir) self.ui.reload_list_of_plugins.clicked.connect( self.reload_list_of_plugins) self.tagger.pluginmanager.plugin_installed.connect( self.plugin_installed) self.tagger.pluginmanager.plugin_updated.connect(self.plugin_updated) self.ui.plugins.header().setStretchLastSection(False) self.ui.plugins.header().setSectionResizeMode( 0, QtWidgets.QHeaderView.Stretch) self.ui.plugins.header().setSectionResizeMode( 1, QtWidgets.QHeaderView.Stretch) self.ui.plugins.header().resizeSection(2, 100) self.ui.plugins.setSortingEnabled(True) def save_state(self): header = self.ui.plugins.header() config.persist["plugins_list_state"] = header.saveState() config.persist[ "plugins_list_sort_section"] = header.sortIndicatorSection() config.persist["plugins_list_sort_order"] = header.sortIndicatorOrder() try: selected = self.items[self.ui.plugins.selectedItems() [0]].module_name except IndexError: selected = "" config.persist["plugins_list_selected"] = selected def restore_state(self, restore_selection=False): header = self.ui.plugins.header() header.restoreState(config.persist["plugins_list_state"]) idx = config.persist["plugins_list_sort_section"] order = config.persist["plugins_list_sort_order"] header.setSortIndicator(idx, order) self.ui.plugins.sortByColumn(idx, order) selected = restore_selection and config.persist["plugins_list_selected"] if selected: for i, p in self.items.items(): if selected == p.module_name: self.ui.plugins.setCurrentItem(i) self.ui.plugins.scrollToItem(i) break else: self.ui.plugins.setCurrentItem(self.ui.plugins.topLevelItem(0)) def _populate(self): self.ui.details.setText("<b>" + _("No plugins installed.") + "</b>") self._user_interaction(False) plugins = sorted(self.tagger.pluginmanager.plugins, key=attrgetter('name')) enabled_plugins = config.setting["enabled_plugins"] available_plugins = dict([ (p.module_name, p.version) for p in self.tagger.pluginmanager.available_plugins ]) installed = [] for plugin in plugins: if plugin.module_name in enabled_plugins: plugin.enabled = True if plugin.module_name in available_plugins.keys(): latest = available_plugins[plugin.module_name] if latest.split('.') > plugin.version.split('.'): plugin.new_version = latest plugin.can_be_updated = True self.add_plugin_item(plugin) installed.append(plugin.module_name) for plugin in sorted(self.tagger.pluginmanager.available_plugins, key=attrgetter('name')): if plugin.module_name not in installed: plugin.can_be_downloaded = True self.add_plugin_item(plugin) self._user_interaction(True) def _remove_all(self): for i, p in self.items.items(): idx = self.ui.plugins.indexOfTopLevelItem(i) self.ui.plugins.takeTopLevelItem(idx) self.items = {} def restore_defaults(self): # Plugin manager has to be updated for plugin in self.tagger.pluginmanager.plugins: plugin.enabled = False # Remove previous entries self._user_interaction(False) self._remove_all() super(PluginsOptionsPage, self).restore_defaults() def load(self): self._populate() self.restore_state() def _reload(self): self._populate() self.restore_state(restore_selection=True) def _user_interaction(self, enabled): self.ui.plugins.blockSignals(not enabled) self.ui.plugins_container.setEnabled(enabled) def reload_list_of_plugins(self): self.ui.details.setText("") self._user_interaction(False) self.save_state() self._remove_all() self.tagger.pluginmanager.query_available_plugins( callback=self._reload) def plugin_installed(self, plugin): if not plugin.compatible: msgbox = QtWidgets.QMessageBox(self) msgbox.setText( _("The plugin '%s' is not compatible with this version of Picard." ) % plugin.name) msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok) msgbox.setDefaultButton(QtWidgets.QMessageBox.Ok) msgbox.exec_() return plugin.new_version = "" plugin.enabled = False plugin.can_be_updated = False plugin.can_be_downloaded = False for i, p in self.items.items(): if plugin.module_name == p.module_name: if i.checkState(0) == QtCore.Qt.Checked: plugin.enabled = True self.add_plugin_item(plugin, item=i) self.ui.plugins.setCurrentItem(i) self.change_details() break else: self.add_plugin_item(plugin) def plugin_updated(self, plugin_name): for i, p in self.items.items(): if plugin_name == p.module_name: p.can_be_updated = False p.can_be_downloaded = False p.marked_for_update = True msgbox = QtWidgets.QMessageBox(self) msgbox.setText( _("The plugin '%s' will be upgraded to version %s on next run of Picard." ) % (p.name, p.new_version)) msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok) msgbox.setDefaultButton(QtWidgets.QMessageBox.Ok) msgbox.exec_() self.add_plugin_item(p, item=i) self.ui.plugins.setCurrentItem(i) self.change_details() break def add_plugin_item(self, plugin, item=None): if item is None: item = PluginTreeWidgetItem(self.ui.plugins) item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable) item.setText(0, plugin.name) item.setSortData(0, plugin.name.lower()) if plugin.enabled: item.setCheckState(0, QtCore.Qt.Checked) else: item.setCheckState(0, QtCore.Qt.Unchecked) if plugin.marked_for_update: item.setText(1, plugin.new_version) else: item.setText(1, plugin.version) label = None if plugin.can_be_updated: label = _("Update") elif plugin.can_be_downloaded: label = _("Install") item.setFlags(item.flags() ^ QtCore.Qt.ItemIsUserCheckable) if label is not None: button = QtWidgets.QPushButton(label) button.setMaximumHeight( button.fontMetrics().boundingRect(label).height() + 7) self.ui.plugins.setItemWidget(item, 2, button) def download_button_process(): self.ui.plugins.setCurrentItem(item) self.download_plugin() button.released.connect(download_button_process) else: # Note: setText() don't work after it was set to a button if plugin.marked_for_update: label = _("Updated") else: label = _("Installed") self.ui.plugins.setItemWidget(item, 2, QtWidgets.QLabel(label)) item.setSortData(2, label) self.ui.plugins.header().resizeSections( QtWidgets.QHeaderView.ResizeToContents) self.items[item] = plugin return item def save(self): enabled_plugins = [] for item, plugin in self.items.items(): if item.checkState(0) == QtCore.Qt.Checked: enabled_plugins.append(plugin.module_name) config.setting["enabled_plugins"] = enabled_plugins self.save_state() def change_details(self): try: plugin = self.items[self.ui.plugins.selectedItems()[0]] except IndexError: return text = [] if plugin.new_version: if plugin.marked_for_update: text.append("<b>" + _("Restart Picard to upgrade to new version") + ": " + plugin.new_version + "</b>") else: text.append("<b>" + _("New version available") + ": " + plugin.new_version + "</b>") if plugin.description: text.append(plugin.description + "<hr width='90%'/>") if plugin.name: text.append("<b>" + _("Name") + "</b>: " + plugin.name) if plugin.author: text.append("<b>" + _("Authors") + "</b>: " + plugin.author) if plugin.license: text.append("<b>" + _("License") + "</b>: " + plugin.license) text.append("<b>" + _("Files") + "</b>: " + plugin.files_list) self.ui.details.setText("<p>%s</p>" % "<br/>\n".join(text)) def open_plugins(self): files, _filter = QtWidgets.QFileDialog.getOpenFileNames( self, "", QtCore.QDir.homePath(), "Picard plugin (*.py *.pyc *.zip)") if files: for path in files: self.tagger.pluginmanager.install_plugin(path) def download_plugin(self): selected = self.ui.plugins.selectedItems()[0] plugin = self.items[selected] self.tagger.webservice.get(PLUGINS_API['host'], PLUGINS_API['port'], PLUGINS_API['endpoint']['download'], partial(self.download_handler, plugin=plugin), parse_response_type=None, priority=True, important=True, queryargs={"id": plugin.module_name}) def download_handler(self, response, reply, error, plugin): if error: msgbox = QtWidgets.QMessageBox(self) msgbox.setText( _("The plugin '%s' could not be downloaded.") % plugin.module_name) msgbox.setInformativeText(_("Please try again later.")) msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok) msgbox.setDefaultButton(QtWidgets.QMessageBox.Ok) msgbox.exec_() log.error( "Error occurred while trying to download the plugin: '%s'" % plugin.module_name) return self.tagger.pluginmanager.install_plugin( None, plugin_name=plugin.module_name, plugin_data=response) def open_plugin_dir(self): QtGui.QDesktopServices.openUrl( QtCore.QUrl(self.loader % USER_PLUGIN_DIR, QtCore.QUrl.TolerantMode)) def mimeTypes(self): return ["text/uri-list"] def dragEnterEvent(self, event): event.setDropAction(QtCore.Qt.CopyAction) event.accept() def dropEvent(self, event): for path in [ os.path.normpath(u.toLocalFile()) for u in event.mimeData().urls() ]: self.tagger.pluginmanager.install_plugin(path)
class PluginsOptionsPage(OptionsPage): NAME = "plugins" TITLE = N_("Plugins") PARENT = None SORT_ORDER = 70 ACTIVE = True options = [ config.ListOption("setting", "enabled_plugins", []), ] def __init__(self, parent=None): super(PluginsOptionsPage, self).__init__(parent) self.ui = Ui_PluginsOptionsPage() self.ui.setupUi(self) self.items = {} self.ui.plugins.itemSelectionChanged.connect(self.change_details) self.ui.plugins.mimeTypes = self.mimeTypes self.ui.plugins.dropEvent = self.dropEvent self.ui.plugins.dragEnterEvent = self.dragEnterEvent if sys.platform == "win32": self.loader = "file:///%s" else: self.loader = "file://%s" self.ui.install_plugin.clicked.connect(self.open_plugins) self.ui.folder_open.clicked.connect(self.open_plugin_dir) self.ui.plugin_download.clicked.connect(self.open_plugin_site) self.tagger.pluginmanager.plugin_installed.connect( self.plugin_installed) def load(self): plugins = sorted(self.tagger.pluginmanager.plugins, cmp=cmp_plugins) enabled_plugins = config.setting["enabled_plugins"] firstitem = None for plugin in plugins: enabled = plugin.module_name in enabled_plugins item = self.add_plugin_item(plugin, enabled=enabled) if not firstitem: firstitem = item self.ui.plugins.setCurrentItem(firstitem) def plugin_installed(self, plugin): if not plugin.compatible: msgbox = QtGui.QMessageBox(self) msgbox.setText( u"The plugin ‘%s’ is not compatible with this version of Picard." % plugin.name) msgbox.setStandardButtons(QtGui.QMessageBox.Ok) msgbox.setDefaultButton(QtGui.QMessageBox.Ok) msgbox.exec_() return for i, p in self.items.items(): if plugin.module_name == p.module_name: enabled = i.checkState(0) == QtCore.Qt.Checked self.add_plugin_item(plugin, enabled=enabled, item=i) break else: self.add_plugin_item(plugin) def add_plugin_item(self, plugin, enabled=False, item=None): if item is None: item = QtGui.QTreeWidgetItem(self.ui.plugins) item.setText(0, plugin.name) if enabled: item.setCheckState(0, QtCore.Qt.Checked) else: item.setCheckState(0, QtCore.Qt.Unchecked) item.setText(1, plugin.version) item.setText(2, plugin.author) self.ui.plugins.header().resizeSections( QtGui.QHeaderView.ResizeToContents) self.items[item] = plugin return item def save(self): enabled_plugins = [] for item, plugin in self.items.iteritems(): if item.checkState(0) == QtCore.Qt.Checked: enabled_plugins.append(plugin.module_name) config.setting["enabled_plugins"] = enabled_plugins def change_details(self): plugin = self.items[self.ui.plugins.selectedItems()[0]] text = [] name = plugin.name descr = plugin.description if descr: text.append(descr + "<br/>") text.append('______________________________') if name: text.append("<b>" + _("Name") + "</b>: " + name) author = plugin.author if author: text.append("<b>" + _("Author") + "</b>: " + author) text.append("<b>" + _("File") + "</b>: " + plugin.file[len(plugin.dir) + 1:]) self.ui.details.setText("<p>%s</p>" % "<br/>\n".join(text)) def open_plugins(self): files = QtGui.QFileDialog.getOpenFileNames( self, "", QtCore.QDir.homePath(), "Picard plugin (*.py *.pyc)") if files: files = map(unicode, files) for path in files: self.install_plugin(path) def install_plugin(self, path): path = encode_filename(path) file = os.path.basename(path) dest = os.path.join(USER_PLUGIN_DIR, file) if os.path.exists(dest): msgbox = QtGui.QMessageBox(self) msgbox.setText("A plugin named %s is already installed." % file) msgbox.setInformativeText( "Do you want to overwrite the existing plugin?") msgbox.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) msgbox.setDefaultButton(QtGui.QMessageBox.No) if msgbox.exec_() == QtGui.QMessageBox.No: return self.tagger.pluginmanager.install_plugin(path, dest) def open_plugin_dir(self): QtGui.QDesktopServices.openUrl( QtCore.QUrl(self.loader % USER_PLUGIN_DIR, QtCore.QUrl.TolerantMode)) def open_plugin_site(self): webbrowser2.goto('plugins') def mimeTypes(self): return ["text/uri-list"] def dragEnterEvent(self, event): event.setDropAction(QtCore.Qt.CopyAction) event.accept() def dropEvent(self, event): for path in [ os.path.normpath(unicode(u.toLocalFile())) for u in event.mimeData().urls() ]: self.install_plugin(path)
class ScriptingOptionsPage(OptionsPage): NAME = "scripting" TITLE = N_("Scripting") PARENT = None SORT_ORDER = 85 ACTIVE = True options = [ config.BoolOption("setting", "enable_tagger_scripts", False), config.ListOption("setting", "list_of_scripts", []), config.IntOption("persist", "last_selected_script_pos", 0), config.Option("persist", "scripting_splitter", QtCore.QByteArray()), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_ScriptingOptionsPage() self.ui.setupUi(self) self.highlighter = TaggerScriptSyntaxHighlighter( self.ui.tagger_script.document()) self.ui.tagger_script.setEnabled(False) self.ui.splitter.setStretchFactor(0, 1) self.ui.splitter.setStretchFactor(1, 2) font = QtGui.QFont('Monospace') font.setStyleHint(QtGui.QFont.TypeWriter) self.ui.tagger_script.setFont(font) self.move_view = MoveableListView(self.ui.script_list, self.ui.move_up_button, self.ui.move_down_button) def script_selected(self): items = self.ui.script_list.selectedItems() if items: item = items[0] self.ui.tagger_script.setEnabled(True) self.ui.tagger_script.setText(item.script) else: self.ui.tagger_script.setEnabled(False) self.ui.tagger_script.setText("") def live_update_and_check(self): items = self.ui.script_list.selectedItems() if items: script = items[0] script.script = self.ui.tagger_script.toPlainText() self.ui.script_error.setStyleSheet("") self.ui.script_error.setText("") try: self.check() except OptionsCheckError as e: self.ui.script_error.setStyleSheet(self.STYLESHEET_ERROR) self.ui.script_error.setText(e.info) return def check(self): parser = ScriptParser() try: parser.eval(self.ui.tagger_script.toPlainText()) except Exception as e: raise ScriptCheckError(_("Script Error"), str(e)) def restore_defaults(self): # Remove existing scripts self.ui.script_list.clear() self.ui.tagger_script.setText("") super().restore_defaults() def load(self): self.ui.enable_tagger_scripts.setChecked( config.setting["enable_tagger_scripts"]) for pos, name, enabled, text in config.setting["list_of_scripts"]: list_item = ScriptListWidgetItem(name, enabled, text) self.ui.script_list.addItem(list_item) # Select the last selected script item last_selected_script_pos = config.persist["last_selected_script_pos"] last_selected_script = self.ui.script_list.item( last_selected_script_pos) if last_selected_script: self.ui.script_list.setCurrentItem(last_selected_script) last_selected_script.setSelected(True) self.restore_state() args = { "picard-doc-scripting-url": PICARD_URLS['doc_scripting'], } text = _('<a href="%(picard-doc-scripting-url)s">Open Scripting' ' Documentation in your browser</a>') % args self.ui.scripting_doc_link.setText(text) def _all_scripts(self): for row in range(0, self.ui.script_list.count()): item = self.ui.script_list.item(row) yield item.get_all() @restore_method def restore_state(self): # Preserve previous splitter position self.ui.splitter.restoreState(config.persist["scripting_splitter"]) def save(self): config.setting[ "enable_tagger_scripts"] = self.ui.enable_tagger_scripts.isChecked( ) config.setting["list_of_scripts"] = list(self._all_scripts()) config.persist[ "last_selected_script_pos"] = self.ui.script_list.currentRow() config.persist["scripting_splitter"] = self.ui.splitter.saveState() def display_error(self, error): # Ignore scripting errors, those are handled inline if not isinstance(error, ScriptCheckError): super().display_error(error)
class OptionsDialog(PicardDialog, SingletonDialog): autorestore = False options = [ config.TextOption("persist", "options_last_active_page", ""), config.ListOption("persist", "options_pages_tree_state", []), config.Option("persist", "options_splitter", QtCore.QByteArray()), ] def add_pages(self, parent, default_page, parent_item): pages = [(p.SORT_ORDER, p.NAME, p) for p in self.pages if p.PARENT == parent] items = [] for foo, bar, page in sorted(pages): item = HashableTreeWidgetItem(parent_item) item.setText(0, _(page.TITLE)) if page.ACTIVE: self.item_to_page[item] = page self.page_to_item[page.NAME] = item self.ui.pages_stack.addWidget(page) else: item.setFlags(QtCore.Qt.ItemIsEnabled) self.add_pages(page.NAME, default_page, item) if page.NAME == default_page: self.default_item = item items.append(item) if not self.default_item and not parent: self.default_item = items[0] def __init__(self, default_page=None, parent=None): super().__init__(parent) self.setWindowModality(QtCore.Qt.ApplicationModal) self.setAttribute(QtCore.Qt.WA_DeleteOnClose) from picard.ui.ui_options import Ui_Dialog self.ui = Ui_Dialog() self.ui.setupUi(self) self.ui.reset_all_button = QtWidgets.QPushButton( _("&Restore all Defaults")) self.ui.reset_all_button.setToolTip( _("Reset all of Picard's settings")) self.ui.reset_button = QtWidgets.QPushButton(_("Restore &Defaults")) self.ui.reset_button.setToolTip( _("Reset all settings for current option page")) ok = StandardButton(StandardButton.OK) ok.setText(_("Make It So!")) self.ui.buttonbox.addButton(ok, QtWidgets.QDialogButtonBox.AcceptRole) self.ui.buttonbox.addButton(StandardButton(StandardButton.CANCEL), QtWidgets.QDialogButtonBox.RejectRole) self.ui.buttonbox.addButton(StandardButton(StandardButton.HELP), QtWidgets.QDialogButtonBox.HelpRole) self.ui.buttonbox.addButton(self.ui.reset_all_button, QtWidgets.QDialogButtonBox.ActionRole) self.ui.buttonbox.addButton(self.ui.reset_button, QtWidgets.QDialogButtonBox.ActionRole) self.ui.buttonbox.accepted.connect(self.accept) self.ui.buttonbox.rejected.connect(self.reject) self.ui.reset_all_button.clicked.connect(self.confirm_reset_all) self.ui.reset_button.clicked.connect(self.confirm_reset) self.ui.buttonbox.helpRequested.connect(self.help) self.pages = [] for Page in page_classes: try: page = Page(self.ui.pages_stack) self.pages.append(page) except Exception: log.exception('Failed initializing options page %r', page) self.item_to_page = {} self.page_to_item = {} self.default_item = None if not default_page: default_page = config.persist["options_last_active_page"] self.add_pages(None, default_page, self.ui.pages_tree) # work-around to set optimal option pane width self.ui.pages_tree.expandAll() max_page_name = self.ui.pages_tree.sizeHintForColumn( 0) + 2 * self.ui.pages_tree.frameWidth() self.ui.splitter.setSizes( [max_page_name, self.geometry().width() - max_page_name]) self.ui.pages_tree.setHeaderLabels([""]) self.ui.pages_tree.header().hide() self.ui.pages_tree.itemSelectionChanged.connect(self.switch_page) self.restoreWindowState() self.finished.connect(self.saveWindowState) for page in self.pages: try: page.load() except Exception: log.exception('Failed loading options page %r', page) self.disable_page(page.NAME) self.ui.pages_tree.setCurrentItem(self.default_item) def switch_page(self): items = self.ui.pages_tree.selectedItems() if items: page = self.item_to_page[items[0]] config.persist["options_last_active_page"] = page.NAME self.ui.pages_stack.setCurrentWidget(page) def disable_page(self, name): item = self.page_to_item[name] item.setDisabled(True) def help(self): current_page = self.ui.pages_stack.currentWidget() url = "{}#{}".format(PICARD_URLS['doc_options'], current_page.NAME) webbrowser2.open(url) def accept(self): for page in self.pages: try: page.check() except OptionsCheckError as e: self._show_page_error(page, e) return except Exception as e: log.exception('Failed checking options page %r', page) self._show_page_error(page, e) return for page in self.pages: try: page.save() except Exception as e: log.exception('Failed saving options page %r', page) self._show_page_error(page, e) return super().accept() def _show_page_error(self, page, error): if not isinstance(error, OptionsCheckError): error = OptionsCheckError(_('Unexpected error'), str(error)) self.ui.pages_tree.setCurrentItem(self.page_to_item[page.NAME]) page.display_error(error) def saveWindowState(self): expanded_pages = [] for page, item in self.page_to_item.items(): index = self.ui.pages_tree.indexFromItem(item) is_expanded = self.ui.pages_tree.isExpanded(index) expanded_pages.append((page, is_expanded)) config.persist["options_pages_tree_state"] = expanded_pages config.persist["options_splitter"] = self.ui.splitter.saveState() @restore_method def restoreWindowState(self): pages_tree_state = config.persist["options_pages_tree_state"] if not pages_tree_state: self.ui.pages_tree.expandAll() else: for page, is_expanded in pages_tree_state: try: item = self.page_to_item[page] except KeyError: continue item.setExpanded(is_expanded) self.restore_geometry() self.ui.splitter.restoreState(config.persist["options_splitter"]) def restore_all_defaults(self): for page in self.pages: page.restore_defaults() def restore_page_defaults(self): self.ui.pages_stack.currentWidget().restore_defaults() def confirm_reset(self): msg = _("You are about to reset your options for this page.") self._show_dialog(msg, self.restore_page_defaults) def confirm_reset_all(self): msg = _("Warning! This will reset all of your settings.") self._show_dialog(msg, self.restore_all_defaults) def _show_dialog(self, msg, function): message_box = QtWidgets.QMessageBox(self) message_box.setIcon(QtWidgets.QMessageBox.Warning) message_box.setWindowModality(QtCore.Qt.WindowModal) message_box.setWindowTitle(_("Confirm Reset")) message_box.setText(_("Are you sure?") + "\n\n" + msg) message_box.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) if message_box.exec_() == QtWidgets.QMessageBox.Yes: function()
class ProviderOptionsCaa(ProviderOptions): """ Options for Cover Art Archive cover art provider """ options = [ config.BoolOption("setting", "caa_save_single_front_image", False), config.BoolOption("setting", "caa_approved_only", False), config.BoolOption("setting", "caa_image_type_as_filename", False), config.IntOption("setting", "caa_image_size", _CAA_IMAGE_SIZE_DEFAULT), config.ListOption("setting", "caa_image_types", _CAA_IMAGE_TYPE_DEFAULT_INCLUDE), config.BoolOption("setting", "caa_restrict_image_types", True), config.ListOption("setting", "caa_image_types_to_omit", _CAA_IMAGE_TYPE_DEFAULT_EXCLUDE), ] _options_ui = Ui_CaaOptions def __init__(self, parent=None): super().__init__(parent) self.ui.restrict_images_types.clicked.connect(self.update_caa_types) self.ui.select_caa_types.clicked.connect(self.select_caa_types) def restore_defaults(self): self.caa_image_types = _CAA_IMAGE_TYPE_DEFAULT_INCLUDE self.caa_image_types_to_omit = _CAA_IMAGE_TYPE_DEFAULT_EXCLUDE super().restore_defaults() def load(self): self.ui.cb_image_size.clear() for item_id, item in _CAA_THUMBNAIL_SIZE_MAP.items(): self.ui.cb_image_size.addItem(_(item.label), userData=item_id) size = config.setting["caa_image_size"] index = self.ui.cb_image_size.findData(size) if index < 0: index = self.ui.cb_image_size.findData(_CAA_IMAGE_SIZE_DEFAULT) self.ui.cb_image_size.setCurrentIndex(index) self.ui.cb_save_single_front_image.setChecked(config.setting["caa_save_single_front_image"]) self.ui.cb_approved_only.setChecked(config.setting["caa_approved_only"]) self.ui.cb_type_as_filename.setChecked(config.setting["caa_image_type_as_filename"]) self.ui.restrict_images_types.setChecked( config.setting["caa_restrict_image_types"]) self.caa_image_types = config.setting["caa_image_types"] self.caa_image_types_to_omit = config.setting["caa_image_types_to_omit"] self.update_caa_types() def save(self): size = self.ui.cb_image_size.currentData() config.setting["caa_image_size"] = size config.setting["caa_save_single_front_image"] = \ self.ui.cb_save_single_front_image.isChecked() config.setting["caa_approved_only"] = \ self.ui.cb_approved_only.isChecked() config.setting["caa_image_type_as_filename"] = \ self.ui.cb_type_as_filename.isChecked() config.setting["caa_restrict_image_types"] = \ self.ui.restrict_images_types.isChecked() config.setting["caa_image_types"] = self.caa_image_types config.setting["caa_image_types_to_omit"] = self.caa_image_types_to_omit def update_caa_types(self): enabled = self.ui.restrict_images_types.isChecked() self.ui.select_caa_types.setEnabled(enabled) def select_caa_types(self): (types, types_to_omit, ok) = CAATypesSelectorDialog.run( self, self.caa_image_types, self.caa_image_types_to_omit) if ok: self.caa_image_types = types self.caa_image_types_to_omit = types_to_omit
class ReleasesOptionsPage(OptionsPage): NAME = "releases" TITLE = N_("Preferred Releases") PARENT = "metadata" SORT_ORDER = 10 ACTIVE = True HELP_URL = '/config/options_releases.html' options = [ config.ListOption("setting", "release_type_scores", _release_type_scores), config.ListOption("setting", "preferred_release_countries", []), config.ListOption("setting", "preferred_release_formats", []), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_ReleasesOptionsPage() self.ui.setupUi(self) self._release_type_sliders = {} def add_slider(name, griditer, context): label = pgettext_attributes(context, name) self._release_type_sliders[name] = \ ReleaseTypeScore(self.ui.type_group, self.ui.gridLayout, label, next(griditer)) griditer = RowColIter( len(RELEASE_PRIMARY_GROUPS) + len(RELEASE_SECONDARY_GROUPS) + 1) # +1 for Reset button for name in RELEASE_PRIMARY_GROUPS: add_slider(name, griditer, context='release_group_primary_type') for name in sorted(RELEASE_SECONDARY_GROUPS, key=lambda v: pgettext_attributes( 'release_group_secondary_type', v)): add_slider(name, griditer, context='release_group_secondary_type') self.reset_preferred_types_btn = QtWidgets.QPushButton( self.ui.type_group) self.reset_preferred_types_btn.setText(_("Reset all")) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( self.reset_preferred_types_btn.sizePolicy().hasHeightForWidth()) self.reset_preferred_types_btn.setSizePolicy(sizePolicy) r, c = next(griditer) self.ui.gridLayout.addWidget(self.reset_preferred_types_btn, r, c, 1, 2) self.reset_preferred_types_btn.clicked.connect( self.reset_preferred_types) self.ui.add_countries.clicked.connect(self.add_preferred_countries) self.ui.remove_countries.clicked.connect( self.remove_preferred_countries) self.ui.add_formats.clicked.connect(self.add_preferred_formats) self.ui.remove_formats.clicked.connect(self.remove_preferred_formats) self.ui.country_list.setSelectionMode( QtWidgets.QAbstractItemView.ExtendedSelection) self.ui.preferred_country_list.setSelectionMode( QtWidgets.QAbstractItemView.ExtendedSelection) self.ui.format_list.setSelectionMode( QtWidgets.QAbstractItemView.ExtendedSelection) self.ui.preferred_format_list.setSelectionMode( QtWidgets.QAbstractItemView.ExtendedSelection) def restore_defaults(self): # Clear lists self.ui.preferred_country_list.clear() self.ui.preferred_format_list.clear() self.ui.country_list.clear() self.ui.format_list.clear() super().restore_defaults() def load(self): scores = dict(config.setting["release_type_scores"]) for (release_type, release_type_slider) in self._release_type_sliders.items(): release_type_slider.setValue( scores.get(release_type, _DEFAULT_SCORE)) self._load_list_items("preferred_release_countries", RELEASE_COUNTRIES, self.ui.country_list, self.ui.preferred_country_list) self._load_list_items("preferred_release_formats", RELEASE_FORMATS, self.ui.format_list, self.ui.preferred_format_list) def save(self): scores = [] for (release_type, release_type_slider) in self._release_type_sliders.items(): scores.append((release_type, release_type_slider.value())) config.setting["release_type_scores"] = scores self._save_list_items("preferred_release_countries", self.ui.preferred_country_list) self._save_list_items("preferred_release_formats", self.ui.preferred_format_list) def reset_preferred_types(self): for release_type_slider in self._release_type_sliders.values(): release_type_slider.reset() def add_preferred_countries(self): self._move_selected_items(self.ui.country_list, self.ui.preferred_country_list) def remove_preferred_countries(self): self._move_selected_items(self.ui.preferred_country_list, self.ui.country_list) self.ui.country_list.sortItems() def add_preferred_formats(self): self._move_selected_items(self.ui.format_list, self.ui.preferred_format_list) def remove_preferred_formats(self): self._move_selected_items(self.ui.preferred_format_list, self.ui.format_list) self.ui.format_list.sortItems() def _move_selected_items(self, list1, list2): for item in list1.selectedItems(): clone = item.clone() list2.addItem(clone) list1.takeItem(list1.row(item)) def _load_list_items(self, setting, source, list1, list2): if setting == "preferred_release_countries": source_list = [(c[0], gettext_countries(c[1])) for c in source.items()] elif setting == "preferred_release_formats": source_list = [(c[0], pgettext_attributes("medium_format", c[1])) for c in source.items()] else: source_list = [(c[0], _(c[1])) for c in source.items()] def fcmp(x): return strxfrm(x[1]) source_list.sort(key=fcmp) saved_data = config.setting[setting] move = [] for data, name in source_list: item = QtWidgets.QListWidgetItem(name) item.setData(QtCore.Qt.UserRole, data) try: i = saved_data.index(data) move.append((i, item)) except BaseException: list1.addItem(item) move.sort(key=itemgetter(0)) for i, item in move: list2.addItem(item) def _save_list_items(self, setting, list1): data = [] for i in range(list1.count()): item = list1.item(i) data.append(item.data(QtCore.Qt.UserRole)) config.setting[setting] = data
class PluginsOptionsPage(OptionsPage): NAME = "plugins" TITLE = N_("Plugins") PARENT = None SORT_ORDER = 70 ACTIVE = True options = [ config.ListOption("setting", "enabled_plugins", []), config.Option("persist", "plugins_list_state", QtCore.QByteArray()), config.Option("persist", "plugins_list_sort_section", 0), config.Option("persist", "plugins_list_sort_order", QtCore.Qt.AscendingOrder), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_PluginsOptionsPage() self.ui.setupUi(self) plugins = self.ui.plugins # fix for PICARD-1226, QT bug (https://bugreports.qt.io/browse/QTBUG-22572) workaround plugins.setStyleSheet('') plugins.itemSelectionChanged.connect(self.change_details) plugins.mimeTypes = self.mimeTypes plugins.dropEvent = self.dropEvent plugins.dragEnterEvent = self.dragEnterEvent self.ui.install_plugin.clicked.connect(self.open_plugins) self.ui.folder_open.clicked.connect(self.open_plugin_dir) self.ui.reload_list_of_plugins.clicked.connect( self.reload_list_of_plugins) self.manager = self.tagger.pluginmanager self.manager.plugin_installed.connect(self.plugin_installed) self.manager.plugin_updated.connect(self.plugin_updated) self.manager.plugin_removed.connect(self.plugin_removed) self.manager.plugin_errored.connect(self.plugin_loading_error) self._preserve = {} self._preserve_selected = None def items(self): iterator = QTreeWidgetItemIterator(self.ui.plugins, QTreeWidgetItemIterator.All) while iterator.value(): item = iterator.value() iterator += 1 yield item def find_item_by_plugin_name(self, plugin_name): for item in self.items(): if plugin_name == item.plugin.module_name: return item return None def selected_item(self): try: return self.ui.plugins.selectedItems()[COLUMN_NAME] except IndexError: return None def save_state(self): header = self.ui.plugins.header() config.persist["plugins_list_state"] = header.saveState() config.persist[ "plugins_list_sort_section"] = header.sortIndicatorSection() config.persist["plugins_list_sort_order"] = header.sortIndicatorOrder() def set_current_item(self, item, scroll=False): if scroll: self.ui.plugins.scrollToItem(item) self.ui.plugins.setCurrentItem(item) self.refresh_details(item) def restore_state(self): header = self.ui.plugins.header() header.restoreState(config.persist["plugins_list_state"]) idx = config.persist["plugins_list_sort_section"] order = config.persist["plugins_list_sort_order"] header.setSortIndicator(idx, order) self.ui.plugins.sortByColumn(idx, order) @staticmethod def is_plugin_enabled(plugin): return bool(plugin.module_name in config.setting["enabled_plugins"]) def available_plugins_name_version(self): return dict([(p.module_name, p.version) for p in self.manager.available_plugins]) def installable_plugins(self): if self.manager.available_plugins is not None: installed_plugins = [ plugin.module_name for plugin in self.installed_plugins() ] for plugin in sorted(self.manager.available_plugins, key=attrgetter('name')): if plugin.module_name not in installed_plugins: yield plugin def installed_plugins(self): return sorted(self.manager.plugins, key=attrgetter('name')) def enabled_plugins(self): return [ item.plugin.module_name for item in self.items() if item.is_enabled ] def _populate(self): self._user_interaction(False) if self.manager.available_plugins is None: available_plugins = {} self.manager.query_available_plugins(self._reload) else: available_plugins = self.available_plugins_name_version() self.ui.details.setText("") self.ui.plugins.setSortingEnabled(False) for plugin in self.installed_plugins(): new_version = None if plugin.module_name in available_plugins: latest = available_plugins[plugin.module_name] if latest.split('.') > plugin.version.split('.'): new_version = latest self.update_plugin_item(None, plugin, enabled=self.is_plugin_enabled(plugin), new_version=new_version, is_installed=True) for plugin in self.installable_plugins(): self.update_plugin_item(None, plugin, enabled=False, is_installed=False) self.ui.plugins.setSortingEnabled(True) self._user_interaction(True) header = self.ui.plugins.header() header.setStretchLastSection(False) header.setSectionResizeMode(QtWidgets.QHeaderView.Fixed) header.setSectionResizeMode(COLUMN_NAME, QtWidgets.QHeaderView.Stretch) header.setSectionResizeMode(COLUMN_VERSION, QtWidgets.QHeaderView.ResizeToContents) header.setSectionResizeMode(COLUMN_ACTIONS, QtWidgets.QHeaderView.ResizeToContents) def _remove_all(self): for item in self.items(): idx = self.ui.plugins.indexOfTopLevelItem(item) self.ui.plugins.takeTopLevelItem(idx) def restore_defaults(self): self._user_interaction(False) self._remove_all() super().restore_defaults() self.set_current_item(self.ui.plugins.topLevelItem(0), scroll=True) def load(self): self._populate() self.restore_state() def _preserve_plugins_states(self): self._preserve = { item.plugin.module_name: item.save_state() for item in self.items() } item = self.selected_item() if item: self._preserve_selected = item.plugin.module_name else: self._preserve_selected = None def _restore_plugins_states(self): for item in self.items(): plugin = item.plugin if plugin.module_name in self._preserve: item.restore_state(self._preserve[plugin.module_name]) if self._preserve_selected == plugin.module_name: self.set_current_item(item, scroll=True) def _reload(self): self._remove_all() self._populate() self._restore_plugins_states() def _user_interaction(self, enabled): self.ui.plugins.blockSignals(not enabled) self.ui.plugins_container.setEnabled(enabled) def reload_list_of_plugins(self): self.ui.details.setText(_("Reloading list of available plugins...")) self._user_interaction(False) self._preserve_plugins_states() self.manager.query_available_plugins(callback=self._reload) def plugin_loading_error(self, plugin_name, error): QtWidgets.QMessageBox.critical( self, _("Plugin '%s'") % plugin_name, _("An error occured while loading the plugin '%s':\n\n%s") % (plugin_name, error)) def plugin_installed(self, plugin): log.debug("Plugin %r installed", plugin.name) if not plugin.compatible: QtWidgets.QMessageBox.warning( self, _("Plugin '%s'") % plugin.name, _("The plugin '%s' is not compatible with this version of Picard." ) % plugin.name) return item = self.find_item_by_plugin_name(plugin.module_name) if item: self.update_plugin_item(item, plugin, make_current=True, enabled=True, is_installed=True) def plugin_updated(self, plugin_name): log.debug("Plugin %r updated", plugin_name) item = self.find_item_by_plugin_name(plugin_name) if item: plugin = item.plugin QtWidgets.QMessageBox.information( self, _("Plugin '%s'") % plugin_name, _("The plugin '%s' will be upgraded to version %s on next run of Picard." ) % (plugin.name, item.new_version)) item.upgrade_to_version = item.new_version self.update_plugin_item(item, plugin, make_current=True) def plugin_removed(self, plugin_name): log.debug("Plugin %r removed", plugin_name) item = self.find_item_by_plugin_name(plugin_name) if item: self.update_plugin_item(item, None, make_current=True, is_installed=False) def uninstall_plugin(self, item): plugin = item.plugin buttonReply = QtWidgets.QMessageBox.question( self, _("Uninstall plugin '%s'?") % plugin.name, _("Do you really want to uninstall the plugin '%s' ?") % plugin.name, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) if buttonReply == QtWidgets.QMessageBox.Yes: self.manager.remove_plugin(plugin.module_name, with_update=True) def update_plugin_item(self, item, plugin, make_current=False, enabled=None, new_version=None, is_installed=None): if item is None: item = PluginTreeWidgetItem(self.ui.plugins) if plugin is not None: item.setData(COLUMN_NAME, QtCore.Qt.UserRole, plugin) else: plugin = item.plugin if new_version is not None: item.new_version = new_version if is_installed is not None: item.is_installed = is_installed if enabled is None: enabled = item.is_enabled def update_text(): if item.new_version is not None: version = "%s → %s" % (plugin.version, item.new_version) else: version = plugin.version if item.installed_font is None: item.installed_font = item.font(COLUMN_NAME) if item.enabled_font is None: item.enabled_font = QtGui.QFont(item.installed_font) item.enabled_font.setBold(True) if item.available_font is None: item.available_font = QtGui.QFont(item.installed_font) if item.is_enabled: item.setFont(COLUMN_NAME, item.enabled_font) else: if item.is_installed: item.setFont(COLUMN_NAME, item.installed_font) else: item.setFont(COLUMN_NAME, item.available_font) item.setText(COLUMN_NAME, plugin.name) item.setText(COLUMN_VERSION, version) def toggle_enable(): item.enable(not item.is_enabled, greyout=not item.is_installed) log.debug("Plugin %r enabled: %r", item.plugin.name, item.is_enabled) update_text() reconnect(item.buttons['enable'].clicked, toggle_enable) install_enabled = not item.is_installed or bool(item.new_version) if item.upgrade_to_version: if item.upgrade_to_version != item.new_version: # case when a new version is known after a plugin was marked for update install_enabled = True else: install_enabled = False if install_enabled: if item.new_version is not None: def download_and_update(): self.download_plugin(item, update=True) reconnect(item.buttons['update'].clicked, download_and_update) item.buttons['install'].mode('hide') item.buttons['update'].mode('show') else: def download_and_install(): self.download_plugin(item) reconnect(item.buttons['install'].clicked, download_and_install) item.buttons['install'].mode('show') item.buttons['update'].mode('hide') if item.is_installed: item.buttons['install'].mode('hide') item.buttons['uninstall'].mode('show') item.enable(enabled, greyout=False) def uninstall_processor(): self.uninstall_plugin(item) reconnect(item.buttons['uninstall'].clicked, uninstall_processor) else: item.buttons['uninstall'].mode('hide') item.buttons['enable'].mode('hide') update_text() if make_current: self.set_current_item(item) actions_sort_score = 2 if item.is_installed: if item.is_enabled: actions_sort_score = 0 else: actions_sort_score = 1 item.setSortData(COLUMN_ACTIONS, actions_sort_score) item.setSortData(COLUMN_NAME, plugin.name.lower()) def v2int(elem): try: return int(elem) except ValueError: return 0 item.setSortData(COLUMN_VERSION, tuple(v2int(e) for e in plugin.version.split('.'))) return item def save(self): config.setting["enabled_plugins"] = self.enabled_plugins() self.save_state() def refresh_details(self, item): plugin = item.plugin text = [] if item.new_version is not None: if item.upgrade_to_version: label = _("Restart Picard to upgrade to new version") else: label = _("New version available") text.append("<b>" + label + ": " + item.new_version + "</b>") if plugin.description: text.append(plugin.description + "<hr width='90%'/>") if plugin.name: text.append("<b>" + _("Name") + "</b>: " + plugin.name) if plugin.author: text.append("<b>" + _("Authors") + "</b>: " + plugin.author) if plugin.license: text.append("<b>" + _("License") + "</b>: " + plugin.license) text.append("<b>" + _("Files") + "</b>: " + plugin.files_list) self.ui.details.setText("<p>%s</p>" % "<br/>\n".join(text)) def change_details(self): item = self.selected_item() if item: self.refresh_details(item) def open_plugins(self): files, _filter = QtWidgets.QFileDialog.getOpenFileNames( self, "", QtCore.QDir.homePath(), "Picard plugin (*.py *.pyc *.zip)") if files: for path in files: self.manager.install_plugin(path) def download_plugin(self, item, update=False): plugin = item.plugin self.tagger.webservice.get(PLUGINS_API['host'], PLUGINS_API['port'], PLUGINS_API['endpoint']['download'], partial(self.download_handler, update, plugin=plugin), parse_response_type=None, priority=True, important=True, queryargs={"id": plugin.module_name}) def download_handler(self, update, response, reply, error, plugin): if error: msgbox = QtWidgets.QMessageBox(self) msgbox.setText( _("The plugin '%s' could not be downloaded.") % plugin.module_name) msgbox.setInformativeText(_("Please try again later.")) msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok) msgbox.setDefaultButton(QtWidgets.QMessageBox.Ok) msgbox.exec_() log.error( "Error occurred while trying to download the plugin: '%s'" % plugin.module_name) return self.manager.install_plugin( None, update=update, plugin_name=plugin.module_name, plugin_data=response, ) @staticmethod def open_plugin_dir(): if sys.platform == 'win32': url = 'file:///' + USER_PLUGIN_DIR else: url = 'file://' + USER_PLUGIN_DIR QtGui.QDesktopServices.openUrl( QtCore.QUrl(url, QtCore.QUrl.TolerantMode)) def mimeTypes(self): return ["text/uri-list"] def dragEnterEvent(self, event): event.setDropAction(QtCore.Qt.CopyAction) event.accept() def dropEvent(self, event): for path in [ os.path.normpath(u.toLocalFile()) for u in event.mimeData().urls() ]: self.manager.install_plugin(path)