class InterfaceOptionsPage(OptionsPage): NAME = "interface" TITLE = N_("User Interface") PARENT = None SORT_ORDER = 80 ACTIVE = True HELP_URL = '/config/options_interface.html' 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' }, 'generate_fingerprints_action': { 'label': N_("Generate Fingerprints"), 'icon': 'fingerprint' }, 'play_file_action': { 'label': N_('Open in Player'), 'icon': 'play-music' }, 'cd_lookup_action': { 'label': N_('Lookup CD...'), 'icon': 'media-optical' }, 'tags_from_filenames_action': { 'label': N_('Parse File Names...'), 'icon': 'picard-tags-from-filename' }, } ACTION_NAMES = set(TOOLBAR_BUTTONS.keys()) options = [ BoolOption("setting", "toolbar_show_labels", True), BoolOption("setting", "toolbar_multiselect", False), BoolOption("setting", "builtin_search", True), BoolOption("setting", "use_adv_search_syntax", False), BoolOption("setting", "quit_confirmation", True), TextOption("setting", "ui_language", ""), TextOption("setting", "ui_theme", str(UiTheme.DEFAULT)), BoolOption("setting", "filebrowser_horizontal_autoscroll", True), BoolOption("setting", "starting_directory", False), TextOption("setting", "starting_directory_path", _default_starting_dir), TextOption("setting", "load_image_behavior", "append"), 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', ]), ] # Those are labels for theme display _UI_THEME_LABELS = { UiTheme.DEFAULT: { 'label': N_('Default'), 'desc': N_('The default color scheme based on the operating system display settings' ), }, UiTheme.DARK: { 'label': N_('Dark'), 'desc': N_('A dark display theme'), }, UiTheme.LIGHT: { 'label': N_('Light'), 'desc': N_('A light display theme'), }, UiTheme.SYSTEM: { 'label': N_('System'), 'desc': N_('The Qt5 theme configured in the desktop environment'), }, } def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_InterfaceOptionsPage() self.ui.setupUi(self) self.ui.ui_theme.clear() for theme in AVAILABLE_UI_THEMES: label = self._UI_THEME_LABELS[theme]['label'] desc = self._UI_THEME_LABELS[theme]['desc'] self.ui.ui_theme.addItem(_(label), theme) idx = self.ui.ui_theme.findData(theme) self.ui.ui_theme.setItemData(idx, _(desc), QtCore.Qt.ToolTipRole) self.ui.ui_theme.setCurrentIndex( self.ui.ui_theme.findData(UiTheme.DEFAULT)) self.ui.ui_language.addItem(_('System default'), '') language_list = [(lang[0], lang[1], _(lang[2])) for lang 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 if not OS_SUPPORTS_THEMES: self.ui.ui_theme_container.hide() def load(self): config = get_config() 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.filebrowser_horizontal_autoscroll.setChecked( config.setting["filebrowser_horizontal_autoscroll"]) 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) current_theme = UiTheme(config.setting["ui_theme"]) self.ui.ui_theme.setCurrentIndex( self.ui.ui_theme.findData(current_theme)) self.update_buttons() def save(self): config = get_config() 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_theme_setting = str( self.ui.ui_theme.itemData(self.ui.ui_theme.currentIndex())) new_language = self.ui.ui_language.itemData( self.ui.ui_language.currentIndex()) restart_warning = None if new_theme_setting != config.setting["ui_theme"]: restart_warning_title = _('Theme changed') restart_warning = _( 'You have changed the application theme. You have to restart Picard in order for the change to take effect.' ) if new_theme_setting == str(UiTheme.SYSTEM): restart_warning += '\n\n' + _( 'Please note that using the system theme might cause the user interface to be not shown correctly. ' 'If this is the case select the "Default" theme option to use Picard\'s default theme again.' ) elif new_language != config.setting["ui_language"]: restart_warning_title = _('Language changed') restart_warning = _( 'You have changed the interface language. You have to restart Picard in order for the change to take effect.' ) if restart_warning: dialog = QtWidgets.QMessageBox(QtWidgets.QMessageBox.Information, restart_warning_title, restart_warning, QtWidgets.QMessageBox.Ok, self) dialog.exec_() config.setting["ui_theme"] = new_theme_setting config.setting["ui_language"] = self.ui.ui_language.itemData( self.ui.ui_language.currentIndex()) config.setting[ "filebrowser_horizontal_autoscroll"] = self.ui.filebrowser_horizontal_autoscroll.isChecked( ) 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() config = get_config() 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 = get_config() 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 ScriptingOptionsPage(OptionsPage): NAME = "scripting" TITLE = N_("Scripting") PARENT = None SORT_ORDER = 75 ACTIVE = True HELP_URL = '/config/options_scripting.html' options = [ BoolOption("setting", "enable_tagger_scripts", False), ListOption("setting", "list_of_scripts", []), IntOption("persist", "last_selected_script_pos", 0), ] default_script_directory = os.path.normpath( QtCore.QStandardPaths.writableLocation( QtCore.QStandardPaths.StandardLocation.DocumentsLocation)) default_script_extension = "ptsp" def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_ScriptingOptionsPage() self.ui.setupUi(self) self.ui.tagger_script.setEnabled(False) self.ui.scripting_options_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.ui.scripting_documentation_button.setToolTip( _("Show scripting documentation in new window.")) self.ui.import_button.clicked.connect(self.import_script) self.ui.import_button.setToolTip( _("Import a script file as a new script.")) self.ui.export_button.clicked.connect(self.export_script) self.ui.export_button.setToolTip( _("Export the current script to a file.")) self.FILE_TYPE_ALL = _("All files") + " (*)" self.FILE_TYPE_SCRIPT = _("Picard script files") + " (*.pts *.txt)" self.FILE_TYPE_PACKAGE = _( "Picard tagging script package") + " (*.ptsp *.yaml)" self.ui.script_list.signal_reset_selected_item.connect( self.reset_selected_item) def show_scripting_documentation(self): ScriptingDocumentationDialog.show_instance(parent=self) def output_error(self, title, fmt, filename, msg): """Log error and display error message dialog. Args: title (str): Title to display on the error dialog box fmt (str): Format for the error type being displayed filename (str): Name of the file being imported or exported msg (str): Error message to display """ log.error(fmt, filename, msg) error_message = _(fmt) % (filename, _(msg)) self.display_error(ScriptFileError(_(title), error_message)) def output_file_error(self, fmt, filename, msg): """Log file error and display error message dialog. Args: fmt (str): Format for the error type being displayed filename (str): Name of the file being imported or exported msg (str): Error message to display """ self.output_error(_("File Error"), fmt, filename, msg) def import_script(self): """Import from an external text file to a new script. Import can be either a plain text script or a Picard script package. """ try: script_item = TaggingScript().import_script(self) except ScriptImportExportError as error: self.output_file_error(error.format, error.filename, error.error_msg) return if script_item: title = _("%s (imported)") % script_item["title"] list_item = ScriptListWidgetItem(title, False, script_item["script"]) self.ui.script_list.addItem(list_item) self.ui.script_list.setCurrentRow(self.ui.script_list.count() - 1) def export_script(self): """Export the current script to an external file. Export can be either as a plain text script or a naming script package. """ items = self.ui.script_list.selectedItems() if not items: return item = items[0] script_text = item.script script_title = item.name if item.name.strip() else _("Unnamed Script") if script_text: script_item = TaggingScript(title=script_title, script=script_text) try: script_item.export_script(parent=self) except ScriptImportExportError as error: self.output_file_error(error.format, error.filename, error.error_msg) 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.FocusReason.OtherFocusReason) self.ui.export_button.setEnabled(True) else: self.ui.tagger_script.setEnabled(False) self.ui.tagger_script.setText("") self.ui.export_button.setEnabled(False) def live_update_and_check(self): items = self.ui.script_list.selectedItems() if not items: return 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: script.has_error = True self.ui.script_error.setStyleSheet(self.STYLESHEET_ERROR) self.ui.script_error.setText(e.info) return script.has_error = False def reset_selected_item(self): widget = self.ui.script_list widget.setCurrentRow(widget.bad_row) 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): config = get_config() self.ui.enable_tagger_scripts.setChecked( config.setting["enable_tagger_scripts"]) self.ui.script_list.clear() 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) 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() def save(self): config = get_config() 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() def display_error(self, error): # Ignore scripting errors, those are handled inline if not isinstance(error, ScriptCheckError): super().display_error(error)
def test_list_opt_no_config(self): ListOption("setting", "list_option", ["a", "b"]) # test default, nothing in config yet self.assertEqual(self.config.setting["list_option"], ["a", "b"]) self.assertIs(type(self.config.setting["list_option"]), list)
class MetadataOptionsPage(OptionsPage): NAME = "metadata" TITLE = N_("Metadata") PARENT = None SORT_ORDER = 20 ACTIVE = True HELP_URL = '/config/options_metadata.html' options = [ TextOption("setting", "va_name", "Various Artists"), TextOption("setting", "nat_name", "[non-album tracks]"), ListOption("setting", "artist_locales", ["en"]), BoolOption("setting", "translate_artist_names", False), BoolOption("setting", "translate_artist_names_script_exception", False), ListOption("setting", "artist_script_exceptions", []), IntOption("setting", "artist_script_exception_weighting", 0), BoolOption("setting", "release_ars", True), BoolOption("setting", "track_ars", False), BoolOption("setting", "convert_punctuation", True), BoolOption("setting", "standardize_artists", False), BoolOption("setting", "standardize_instruments", True), BoolOption("setting", "guess_tracknumber_and_title", True), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_MetadataOptionsPage() self.ui.setupUi(self) self.ui.va_name_default.clicked.connect(self.set_va_name_default) self.ui.nat_name_default.clicked.connect(self.set_nat_name_default) self.ui.select_locales.clicked.connect(self.open_locale_selector) self.ui.translate_artist_names.stateChanged.connect( self.set_enabled_states) self.ui.translate_artist_names_script_exception.stateChanged.connect( self.set_enabled_states) def load(self): config = get_config() self.ui.translate_artist_names.setChecked( config.setting["translate_artist_names"]) self.current_locales = config.setting["artist_locales"] self.make_locales_text() self.ui.translate_artist_names_script_exception.setChecked( config.setting["translate_artist_names_script_exception"]) self.ui.ignore_tx_scripts.clear() for script_id in SCRIPTS: enabled = script_id in config.setting["artist_script_exceptions"] item = QtWidgets.QListWidgetItem(_(SCRIPTS[script_id])) item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable) item.setData(QtCore.Qt.UserRole, script_id) item.setCheckState( QtCore.Qt.Checked if enabled else QtCore.Qt.Unchecked) self.ui.ignore_tx_scripts.addItem(item) self.ui.minimum_weighting.setValue( config.setting["artist_script_exception_weighting"]) self.ui.convert_punctuation.setChecked( config.setting["convert_punctuation"]) self.ui.release_ars.setChecked(config.setting["release_ars"]) self.ui.track_ars.setChecked(config.setting["track_ars"]) self.ui.va_name.setText(config.setting["va_name"]) self.ui.nat_name.setText(config.setting["nat_name"]) self.ui.standardize_artists.setChecked( config.setting["standardize_artists"]) self.ui.standardize_instruments.setChecked( config.setting["standardize_instruments"]) self.ui.guess_tracknumber_and_title.setChecked( config.setting["guess_tracknumber_and_title"]) self.set_enabled_states() def make_locales_text(self): def translated_locales(): for locale in self.current_locales: yield _(ALIAS_LOCALES[locale]) self.ui.selected_locales.setText('; '.join(translated_locales())) def save(self): config = get_config() config.setting[ "translate_artist_names"] = self.ui.translate_artist_names.isChecked( ) config.setting["artist_locales"] = self.current_locales config.setting[ "translate_artist_names_script_exception"] = self.ui.translate_artist_names_script_exception.isChecked( ) script_exceptions = [] for idx in range(self.ui.ignore_tx_scripts.count()): item = self.ui.ignore_tx_scripts.item(idx) if item.checkState() == QtCore.Qt.Checked: script_exceptions.append(item.data(QtCore.Qt.UserRole)) config.setting["artist_script_exceptions"] = script_exceptions config.setting["artist_script_exception_weighting"] = min( 100, max(0, self.ui.minimum_weighting.value())) config.setting[ "convert_punctuation"] = self.ui.convert_punctuation.isChecked() config.setting["release_ars"] = self.ui.release_ars.isChecked() config.setting["track_ars"] = self.ui.track_ars.isChecked() config.setting["va_name"] = self.ui.va_name.text() nat_name = self.ui.nat_name.text() if nat_name != config.setting["nat_name"]: config.setting["nat_name"] = nat_name if self.tagger.nats is not None: self.tagger.nats.update() config.setting[ "standardize_artists"] = self.ui.standardize_artists.isChecked() config.setting[ "standardize_instruments"] = self.ui.standardize_instruments.isChecked( ) config.setting[ "guess_tracknumber_and_title"] = self.ui.guess_tracknumber_and_title.isChecked( ) def set_va_name_default(self): self.ui.va_name.setText(self.options[0].default) self.ui.va_name.setCursorPosition(0) def set_nat_name_default(self): self.ui.nat_name.setText(self.options[1].default) self.ui.nat_name.setCursorPosition(0) def set_enabled_states(self): translate_checked = self.ui.translate_artist_names.isChecked() translate_exception_checked = self.ui.translate_artist_names_script_exception.isChecked( ) self.ui.select_locales.setEnabled(translate_checked) self.ui.selected_locales.setEnabled(translate_checked) self.ui.translate_artist_names_script_exception.setEnabled( translate_checked) self.ui.ignore_script_frame.setEnabled(translate_checked and translate_exception_checked) def open_locale_selector(self): dialog = MultiLocaleSelector(self) dialog.show()
class ProfileEditorDialog(SingletonDialog, PicardDialog): """User Profile Editor Page """ TITLE = N_("Option profile editor") STYLESHEET_ERROR = OptionsPage.STYLESHEET_ERROR help_url = PICARD_URLS["doc_profile_edit"] PROFILES_KEY = SettingConfigSection.PROFILES_KEY SETTINGS_KEY = SettingConfigSection.SETTINGS_KEY POSITION_KEY = "last_selected_profile_pos" EXPANDED_KEY = "profile_settings_tree_expanded_list" TREEWIDGETITEM_COLUMN = 0 options = [ IntOption("persist", POSITION_KEY, 0), ListOption("persist", EXPANDED_KEY, []) ] signal_save = QtCore.pyqtSignal() def __init__(self, parent=None): """Option profile editor. """ super().__init__(parent) self.main_window = parent self.ITEMS_TEMPLATES = { True: "\n ■ %s", False: "\n □ %s", } self.setWindowTitle(_(self.TITLE)) self.displaying = False self.loading = True self.ui = Ui_ProfileEditorDialog() self.ui.setupUi(self) self.setModal(True) self.make_buttons() self.ui.profile_editor_splitter.setStretchFactor(1, 1) self.move_view = MoveableListView(self.ui.profile_list, self.ui.move_up_button, self.ui.move_down_button) self.ui.profile_list.itemChanged.connect(self.profile_item_changed) self.ui.profile_list.currentItemChanged.connect(self.current_item_changed) self.ui.profile_list.itemSelectionChanged.connect(self.item_selection_changed) self.ui.profile_list.itemChanged.connect(self.profile_data_changed) self.ui.settings_tree.itemChanged.connect(self.save_profile) self.ui.settings_tree.itemExpanded.connect(self.update_current_expanded_items_list) self.ui.settings_tree.itemCollapsed.connect(self.update_current_expanded_items_list) self.current_profile_id = None self.expanded_sections = [] self.building_tree = False self.load() self.loading = False def make_buttons(self): """Make buttons and add them to the button bars. """ self.make_it_so_button = QtWidgets.QPushButton(_("Make It So!")) self.make_it_so_button.setToolTip(_("Save all profile information to the user settings")) self.ui.buttonbox.addButton(self.make_it_so_button, QtWidgets.QDialogButtonBox.AcceptRole) self.ui.buttonbox.accepted.connect(self.make_it_so) self.new_profile_button = QtWidgets.QPushButton(_('New')) self.new_profile_button.setToolTip(_("Create a new profile")) self.new_profile_button.clicked.connect(self.new_profile) self.ui.profile_list_buttonbox.addButton(self.new_profile_button, QtWidgets.QDialogButtonBox.ActionRole) self.copy_profile_button = QtWidgets.QPushButton(_('Copy')) self.copy_profile_button.setToolTip(_("Copy to a new profile")) self.copy_profile_button.clicked.connect(self.copy_profile) self.ui.profile_list_buttonbox.addButton(self.copy_profile_button, QtWidgets.QDialogButtonBox.ActionRole) self.delete_profile_button = QtWidgets.QPushButton(_('Delete')) self.delete_profile_button.setToolTip(_("Delete the profile")) self.delete_profile_button.clicked.connect(self.delete_profile) self.ui.profile_list_buttonbox.addButton(self.delete_profile_button, QtWidgets.QDialogButtonBox.ActionRole) self.cancel_button = QtWidgets.QPushButton(_('Cancel')) self.cancel_button.setToolTip(_("Close the profile editor without saving changes to the profiles")) self.ui.buttonbox.addButton(self.cancel_button, QtWidgets.QDialogButtonBox.RejectRole) self.ui.buttonbox.rejected.connect(self.close) self.ui.buttonbox.addButton(QtWidgets.QDialogButtonBox.Help) self.ui.buttonbox.helpRequested.connect(self.show_help) def load(self): """Load initial configuration. """ config = get_config() # Use deepcopy() to avoid changes made locally from being cascaded into `config.profiles` # before the user clicks "Make It So!" self.profile_settings = deepcopy(config.profiles[self.SETTINGS_KEY]) for profile in config.profiles[self.PROFILES_KEY]: list_item = ProfileListWidgetItem(profile['title'], profile['enabled'], profile['id']) self.ui.profile_list.addItem(list_item) self.all_profiles = list(self._all_profiles()) # Select the last selected profile item last_selected_profile_pos = config.persist[self.POSITION_KEY] self.expanded_sections = config.persist[self.EXPANDED_KEY] last_selected_profile = self.ui.profile_list.item(last_selected_profile_pos) settings = None if last_selected_profile: self.ui.profile_list.setCurrentItem(last_selected_profile) last_selected_profile.setSelected(True) id = last_selected_profile.profile_id self.current_profile_id = id settings = self.get_settings_for_profile(id) self.make_setting_tree(settings=settings) def get_settings_for_profile(self, id): """Get the settings for the specified profile ID. Automatically adds an empty settings dictionary if there is no settings dictionary found for the ID. Args: id (str): ID of the profile Returns: dict: Profile settings """ # Add empty settings dictionary if no dictionary found for the profile. # This happens when a new profile is created. if id not in self.profile_settings: self.profile_settings[id] = {} return self.profile_settings[id] def get_current_selected_item(self): """Gets the profile item currently selected in the profiles list. Returns: ProfileListWidgetItem: Currently selected item """ items = self.ui.profile_list.selectedItems() if items: return items[0] return None def update_current_expanded_items_list(self): if self.building_tree: return self.expanded_sections = [] for i in range(self.ui.settings_tree.topLevelItemCount()): tl_item = self.ui.settings_tree.topLevelItem(i) if tl_item.isExpanded(): self.expanded_sections.append(tl_item.text(self.TREEWIDGETITEM_COLUMN)) def profile_selected(self, update_settings=True): """Update working profile information for the selected item in the profiles list. Args: update_settings (bool, optional): Update settings tree. Defaults to True. """ item = self.get_current_selected_item() if item: id = item.profile_id self.current_profile_id = id if update_settings: settings = self.get_settings_for_profile(id) self.make_setting_tree(settings=settings) else: self.current_profile_id = None self.make_setting_tree(settings=None) def make_setting_tree(self, settings=None): """Update the profile settings tree based on the settings provided. If no settings are provided, displays an empty tree. Args: settings (dict, optional): Dictionary of settings for the profile. Defaults to None. """ self.set_button_states() self.ui.settings_tree.clear() self.ui.settings_tree.setHeaderItem(QtWidgets.QTreeWidgetItem()) self.ui.settings_tree.setHeaderLabels([_("Settings to include in profile")]) if settings is None: return self.building_tree = True for id, group in UserProfileGroups.SETTINGS_GROUPS.items(): title = group["title"] group_settings = group["settings"] widget_item = QtWidgets.QTreeWidgetItem([title]) widget_item.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsAutoTristate) widget_item.setCheckState(self.TREEWIDGETITEM_COLUMN, QtCore.Qt.Unchecked) for setting in group_settings: child_item = QtWidgets.QTreeWidgetItem([_(setting.title)]) child_item.setData(0, QtCore.Qt.UserRole, setting.name) child_item.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsUserCheckable) state = QtCore.Qt.Checked if settings and setting.name in settings else QtCore.Qt.Unchecked child_item.setCheckState(self.TREEWIDGETITEM_COLUMN, state) if settings and setting.name in settings and settings[setting.name] is not None: value = settings[setting.name] else: value = None child_item.setToolTip(self.TREEWIDGETITEM_COLUMN, self.make_setting_value_text(setting.name, value)) widget_item.addChild(child_item) self.ui.settings_tree.addTopLevelItem(widget_item) if title in self.expanded_sections: widget_item.setExpanded(True) self.building_tree = False def make_setting_value_text(self, key, value): if value is None: profile = self.get_controlling_profile(key) return _("Value not set (set in \"%s\" profile)") % profile if key == "selected_file_naming_script_id": return self.get_file_naming_script_name(value) if key == "list_of_scripts": return self.get_list_of_scripts() if key == "ca_providers": return self.get_list_of_ca_providers() if isinstance(value, str): return '"%s"' % value if isinstance(value, bool) or isinstance(value, int) or isinstance(value, float): return str(value) if isinstance(value, set) or isinstance(value, tuple) or isinstance(value, list) or isinstance(value, dict): return _("List of %i items") % len(value) return _("Unknown value format") def get_file_naming_script_name(self, script_id): config = get_config() scripts = config.setting["file_renaming_scripts"] if script_id in scripts: return scripts[script_id]["title"] presets = {x["id"]: x["title"] for x in get_file_naming_script_presets()} if script_id in presets: return presets[script_id] return _("Unknown script") def get_list_of_scripts(self): config = get_config() scripts = config.setting["list_of_scripts"] if scripts: value_text = _("Tagging scripts (%i found):") % len(scripts) for (pos, name, enabled, script) in scripts: value_text += self.ITEMS_TEMPLATES[enabled] % name else: value_text = _("No scripts in list") return value_text def get_list_of_ca_providers(self): config = get_config() providers = config.setting["ca_providers"] value_text = _("CA providers (%i found):") % len(providers) for (name, enabled) in providers: value_text += self.ITEMS_TEMPLATES[enabled] % name return value_text def get_controlling_profile(self, key): below_current_profile_flag = False for profile in self.all_profiles: if below_current_profile_flag: if profile["enabled"]: settings = self.profile_settings[profile["id"]] if key in settings and settings[key] is not None: return profile["title"] elif profile["id"] == self.current_profile_id: below_current_profile_flag = True return _("Default") def profile_data_changed(self): """Update the profile settings values displayed. """ self.all_profiles = list(self._all_profiles()) settings = self.get_settings_for_profile(id) for i in range(self.ui.settings_tree.topLevelItemCount()): group = self.ui.settings_tree.topLevelItem(i) for j in range(group.childCount()): child = group.child(j) key = child.data(self.TREEWIDGETITEM_COLUMN, QtCore.Qt.UserRole) if key in settings: value = settings[key] else: value = None child.setToolTip(self.TREEWIDGETITEM_COLUMN, self.make_setting_value_text(key, value)) def profile_item_changed(self, item): """Check title is not blank and remove leading and trailing spaces. Args: item (ProfileListWidgetItem): Item that changed """ if not self.loading: text = item.text().strip() if not text: QtWidgets.QMessageBox( QtWidgets.QMessageBox.Warning, _("Invalid Title"), _("The profile title cannot be blank."), QtWidgets.QMessageBox.Ok, self ).exec_() item.setText(_("Unnamed profile")) elif text != item.text(): # Remove leading and trailing spaces from new title. item.setText(text) def current_item_changed(self, new_item, old_item): """Update the display when a new item is selected in the profile list. Args: new_item (ProfileListWidgetItem): Newly selected item old_item (ProfileListWidgetItem): Previously selected item """ if self.loading: return self.save_profile() self.all_profiles = list(self._all_profiles()) self.set_current_item(new_item) self.profile_selected() def item_selection_changed(self): """Set tree list highlight bar to proper line if selection change canceled. """ item = self.ui.profile_list.currentItem() if item: item.setSelected(True) def set_current_item(self, item): """Sets the specified item as the current selection in the profiles list. Args: item (ProfileListWidgetItem): Item to set as current selection """ self.loading = True self.ui.profile_list.setCurrentItem(item) self.loading = False def save_profile(self): """Save changes to the currently selected profile. """ if not self.current_profile_id: return checked_items = set(self.get_checked_items_from_tree()) settings = set(self.profile_settings[self.current_profile_id].keys()) # Add new items to settings for item in checked_items.difference(settings): self.profile_settings[self.current_profile_id][item] = None # Remove unchecked items from settings for item in settings.difference(checked_items): del self.profile_settings[self.current_profile_id][item] def copy_profile(self): """Make a copy of the currently selected profile. """ item = self.get_current_selected_item() id = str(uuid.uuid4()) settings = deepcopy(self.profile_settings[self.current_profile_id]) self.profile_settings[id] = settings name = _("%s (copy)") % item.name self.ui.profile_list.add_profile(name=name, profile_id=id) def new_profile(self): """Add a new profile with no settings selected. """ self.ui.profile_list.add_profile() def delete_profile(self): """Delete the current profile. """ self.ui.profile_list.remove_selected_profile() self.profile_selected() def make_it_so(self): """Save any changes to the current profile's settings, save all updated profile information to the user settings, and close the profile editor dialog. """ all_profiles = list(self._all_profiles()) all_profile_ids = set(x['id'] for x in all_profiles) keys = set(self.profile_settings.keys()) for id in keys.difference(all_profile_ids): del self.profile_settings[id] config = get_config() config.profiles[self.PROFILES_KEY] = all_profiles config.profiles[self.SETTINGS_KEY] = self.profile_settings self.main_window.enable_renaming_action.setChecked(config.setting["rename_files"]) self.main_window.enable_moving_action.setChecked(config.setting["move_files"]) self.main_window.enable_tag_saving_action.setChecked(not config.setting["dont_write_tags"]) self.close() def closeEvent(self, event): """Custom close event handler to save editor settings. """ config = get_config() config.persist[self.POSITION_KEY] = self.ui.profile_list.currentRow() config.persist[self.EXPANDED_KEY] = self.expanded_sections super().closeEvent(event) def _all_profiles(self): """Get all profiles from the profiles list in order from top to bottom. Yields: dict: Profile information in a format for saving to the user settings """ for row in range(self.ui.profile_list.count()): item = self.ui.profile_list.item(row) yield item.get_dict() def set_button_states(self): """Set the enabled / disabled states of the buttons. """ state = self.current_profile_id is not None self.copy_profile_button.setEnabled(state) self.delete_profile_button.setEnabled(state) def get_checked_items_from_tree(self): """Get the keys for the settings that are checked in the profile settings tree. Yields: str: Settings key """ for i in range(self.ui.settings_tree.topLevelItemCount()): tl_item = self.ui.settings_tree.topLevelItem(i) for j in range(tl_item.childCount()): item = tl_item.child(j) if item.checkState(self.TREEWIDGETITEM_COLUMN) == QtCore.Qt.Checked: yield item.data(self.TREEWIDGETITEM_COLUMN, QtCore.Qt.UserRole)
class AdvancedOptionsPage(OptionsPage): NAME = "advanced" TITLE = N_("Advanced") PARENT = None SORT_ORDER = 90 ACTIVE = True HELP_URL = '/config/options_advanced.html' options = [ TextOption("setting", "ignore_regex", ""), BoolOption("setting", "ignore_hidden_files", False), BoolOption("setting", "recursively_add_files", True), IntOption("setting", "ignore_track_duration_difference_under", 2), BoolOption("setting", "completeness_ignore_videos", False), BoolOption("setting", "completeness_ignore_pregap", False), BoolOption("setting", "completeness_ignore_data", False), BoolOption("setting", "completeness_ignore_silence", False), 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): config = get_config() 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 = get_config() 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 ReleasesOptionsPage(OptionsPage): NAME = "releases" TITLE = N_("Preferred Releases") PARENT = "metadata" SORT_ORDER = 10 ACTIVE = True HELP_URL = '/config/options_releases.html' options = [ ListOption("setting", "release_type_scores", _release_type_scores), ListOption("setting", "preferred_release_countries", []), 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') reset_types_btn = QtWidgets.QPushButton(self.ui.type_group) reset_types_btn.setText(_("Reset all")) sizePolicy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( reset_types_btn.sizePolicy().hasHeightForWidth()) reset_types_btn.setSizePolicy(sizePolicy) r, c = next(griditer) self.ui.gridLayout.addWidget(reset_types_btn, r, c, 1, 2) reset_types_btn.clicked.connect(self.reset_preferred_types) self.setTabOrder(reset_types_btn, self.ui.country_list) self.setTabOrder(self.ui.country_list, self.ui.preferred_country_list) self.setTabOrder(self.ui.preferred_country_list, self.ui.add_countries) self.setTabOrder(self.ui.add_countries, self.ui.remove_countries) self.setTabOrder(self.ui.remove_countries, self.ui.format_list) self.setTabOrder(self.ui.format_list, self.ui.preferred_format_list) self.setTabOrder(self.ui.preferred_format_list, self.ui.add_formats) self.setTabOrder(self.ui.add_formats, self.ui.remove_formats) 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.SelectionMode.ExtendedSelection) self.ui.preferred_country_list.setSelectionMode( QtWidgets.QAbstractItemView.SelectionMode.ExtendedSelection) self.ui.format_list.setSelectionMode( QtWidgets.QAbstractItemView.SelectionMode.ExtendedSelection) self.ui.preferred_format_list.setSelectionMode( QtWidgets.QAbstractItemView.SelectionMode.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): config = get_config() 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): config = get_config() 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) config = get_config() saved_data = config.setting[setting] move = [] for data, name in source_list: item = QtWidgets.QListWidgetItem(name) item.setData(QtCore.Qt.ItemDataRole.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.ItemDataRole.UserRole)) config = get_config() config.setting[setting] = data
def test_list_opt_set_empty(self): ListOption("setting", "list_option", ["a", "b"]) # set option to empty list self.config.setting["list_option"] = [] self.assertEqual(self.config.setting["list_option"], [])
class MetadataOptionsPage(OptionsPage): NAME = "metadata" TITLE = N_("Metadata") PARENT = None SORT_ORDER = 20 ACTIVE = True HELP_URL = '/config/options_metadata.html' options = [ TextOption("setting", "va_name", "Various Artists"), TextOption("setting", "nat_name", "[non-album tracks]"), ListOption("setting", "artist_locales", ["en"]), BoolOption("setting", "translate_artist_names", False), BoolOption("setting", "translate_artist_names_script_exception", False), ListOption("setting", "script_exceptions", []), BoolOption("setting", "release_ars", True), BoolOption("setting", "track_ars", False), BoolOption("setting", "convert_punctuation", False), BoolOption("setting", "standardize_artists", False), BoolOption("setting", "standardize_instruments", True), BoolOption("setting", "guess_tracknumber_and_title", True), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_MetadataOptionsPage() self.ui.setupUi(self) self.ui.va_name_default.clicked.connect(self.set_va_name_default) self.ui.nat_name_default.clicked.connect(self.set_nat_name_default) self.ui.select_locales.clicked.connect(self.open_locale_selector) self.ui.select_scripts.clicked.connect(self.open_script_selector) self.ui.translate_artist_names.stateChanged.connect( self.set_enabled_states) self.ui.translate_artist_names_script_exception.stateChanged.connect( self.set_enabled_states) def load(self): config = get_config() self.ui.translate_artist_names.setChecked( config.setting["translate_artist_names"]) self.current_locales = config.setting["artist_locales"] self.make_locales_text() self.current_scripts = config.setting["script_exceptions"] self.make_scripts_text() self.ui.translate_artist_names_script_exception.setChecked( config.setting["translate_artist_names_script_exception"]) self.ui.convert_punctuation.setChecked( config.setting["convert_punctuation"]) self.ui.release_ars.setChecked(config.setting["release_ars"]) self.ui.track_ars.setChecked(config.setting["track_ars"]) self.ui.va_name.setText(config.setting["va_name"]) self.ui.nat_name.setText(config.setting["nat_name"]) self.ui.standardize_artists.setChecked( config.setting["standardize_artists"]) self.ui.standardize_instruments.setChecked( config.setting["standardize_instruments"]) self.ui.guess_tracknumber_and_title.setChecked( config.setting["guess_tracknumber_and_title"]) self.set_enabled_states() def make_locales_text(self): def translated_locales(): for locale in self.current_locales: yield _(ALIAS_LOCALES[locale]) self.ui.selected_locales.setText('; '.join(translated_locales())) def make_scripts_text(self): def translated_scripts(): for script in self.current_scripts: yield ScriptExceptionSelector.make_label(script[0], script[1]) self.ui.selected_scripts.setText('; '.join(translated_scripts())) def save(self): config = get_config() config.setting[ "translate_artist_names"] = self.ui.translate_artist_names.isChecked( ) config.setting["artist_locales"] = self.current_locales config.setting[ "translate_artist_names_script_exception"] = self.ui.translate_artist_names_script_exception.isChecked( ) config.setting["script_exceptions"] = self.current_scripts config.setting[ "convert_punctuation"] = self.ui.convert_punctuation.isChecked() config.setting["release_ars"] = self.ui.release_ars.isChecked() config.setting["track_ars"] = self.ui.track_ars.isChecked() config.setting["va_name"] = self.ui.va_name.text() nat_name = self.ui.nat_name.text() if nat_name != config.setting["nat_name"]: config.setting["nat_name"] = nat_name if self.tagger.nats is not None: self.tagger.nats.update() config.setting[ "standardize_artists"] = self.ui.standardize_artists.isChecked() config.setting[ "standardize_instruments"] = self.ui.standardize_instruments.isChecked( ) config.setting[ "guess_tracknumber_and_title"] = self.ui.guess_tracknumber_and_title.isChecked( ) def set_va_name_default(self): self.ui.va_name.setText(self.options[0].default) self.ui.va_name.setCursorPosition(0) def set_nat_name_default(self): self.ui.nat_name.setText(self.options[1].default) self.ui.nat_name.setCursorPosition(0) def set_enabled_states(self): translate_checked = self.ui.translate_artist_names.isChecked() translate_exception_checked = self.ui.translate_artist_names_script_exception.isChecked( ) self.ui.select_locales.setEnabled(translate_checked) self.ui.selected_locales.setEnabled(translate_checked) self.ui.translate_artist_names_script_exception.setEnabled( translate_checked) select_scripts_enabled = translate_checked and translate_exception_checked self.ui.selected_scripts.setEnabled(select_scripts_enabled) self.ui.select_scripts.setEnabled(select_scripts_enabled) def open_locale_selector(self): dialog = MultiLocaleSelector(self) dialog.show() def open_script_selector(self): dialog = ScriptExceptionSelector(self) dialog.show()
class ScriptEditorPage(PicardDialog): """File Naming Script Editor Page """ TITLE = N_("File naming script editor") STYLESHEET_ERROR = OptionsPage.STYLESHEET_ERROR options = [ TextOption( "setting", "file_naming_format", DEFAULT_FILE_NAMING_FORMAT, ), ListOption( "setting", "file_naming_scripts", [], ), TextOption( "setting", "selected_file_naming_script_id", "", ), ] signal_save = QtCore.pyqtSignal() signal_update = QtCore.pyqtSignal() signal_selection_changed = QtCore.pyqtSignal() default_script_directory = os.path.normpath( QtCore.QStandardPaths.writableLocation( QtCore.QStandardPaths.DocumentsLocation)) default_script_filename = "picard_naming_script.pnsp" def __init__(self, parent=None, examples=None): """Stand-alone file naming script editor. Args: parent (QMainWindow or OptionsPage, optional): Parent object. Defaults to None. examples (ScriptEditorExamples, required): Object containing examples to display. Defaults to None. """ super().__init__(parent) self.examples = examples self.FILE_TYPE_ALL = _("All Files") + " (*)" self.FILE_TYPE_SCRIPT = _("Picard Script Files") + " (*.pts *.txt)" self.FILE_TYPE_PACKAGE = _( "Picard Naming Script Package") + " (*.pnsp *.json)" self.SCRIPT_TITLE_SYSTEM = _("System: %s") self.SCRIPT_TITLE_USER = _("User: %s") # TODO: Make this work properly so that it can be accessed from both the main window and the options window. # self.setWindowFlags(QtCore.Qt.Window) self.setWindowModality(QtCore.Qt.WindowModal) self.setWindowTitle(self.TITLE) self.displaying = False self.loading = True self.ui = Ui_ScriptEditor() self.ui.setupUi(self) self.make_menu() # Button tooltips self.ui.file_naming_editor_close.setToolTip( self.close_action.toolTip()) self.ui.file_naming_editor_save.setToolTip(self.save_action.toolTip()) self.ui.file_naming_editor_reset.setToolTip( self.reset_action.toolTip()) self.ui.label.setWordWrap(False) self.installEventFilter(self) self.ui.file_naming_editor_save.clicked.connect(self.save_script) self.ui.file_naming_editor_close.clicked.connect(self.close_window) self.ui.file_naming_editor_reset.clicked.connect(self.reset_script) self.ui.file_naming_format.setEnabled(True) # Add scripting documentation to parent frame. doc_widget = ScriptingDocumentationWidget(self, include_link=False) self.ui.documentation_frame_layout.addWidget(doc_widget) self.ui.file_naming_format.textChanged.connect(self.check_formats) self._sampled_example_files = [] self.ui.example_filename_after.itemSelectionChanged.connect( self.match_before_to_after) self.ui.example_filename_before.itemSelectionChanged.connect( self.match_after_to_before) self.ui.preset_naming_scripts.currentIndexChanged.connect( partial(self.select_script, skip_check=False)) self.synchronize_vertical_scrollbars( (self.ui.example_filename_before, self.ui.example_filename_after)) self.wordwrap = True self.toggle_wordwrap() # Force update to display self.sidebar = True self.toggle_documentation() # Force update to display self.examples_current_row = -1 self.script_metadata_changed = False # self.select_script() self.load() self.loading = False def make_menu(self): """Build the menu bar. """ main_menu = QtWidgets.QMenuBar() base_font = self.ui.file_naming_editor_close.font() main_menu.setStyleSheet( "QMenuBar { font-family: %s; font-size: %upt; }" % (base_font.family(), base_font.pointSize())) # File menu settings file_menu = main_menu.addMenu(_('&File')) file_menu.setToolTipsVisible(True) self.import_action = QtWidgets.QAction(_("&Import a script file"), self) self.import_action.setToolTip(_("Import a file as a new script")) self.import_action.setIcon(icontheme.lookup('document-open')) self.import_action.triggered.connect(self.import_script) file_menu.addAction(self.import_action) self.export_action = QtWidgets.QAction(_("&Export a script file"), self) self.export_action.setToolTip(_("Export the script to a file")) self.export_action.setIcon(icontheme.lookup('document-save')) self.export_action.triggered.connect(self.export_script) file_menu.addAction(self.export_action) self.close_action = QtWidgets.QAction(_("E&xit / Close editor"), self) self.close_action.setToolTip(_("Close the script editor")) self.close_action.triggered.connect(self.close_window) file_menu.addAction(self.close_action) # Script menu settings script_menu = main_menu.addMenu(_('&Script')) script_menu.setToolTipsVisible(True) self.details_action = QtWidgets.QAction(_("Edit Script &Metadata"), self) self.details_action.setToolTip(_("Display the details for the script")) self.details_action.triggered.connect(self.view_script_details) self.details_action.setShortcut(QtGui.QKeySequence(_("Ctrl+M"))) script_menu.addAction(self.details_action) self.add_action = QtWidgets.QAction(_("Add a &new script"), self) self.add_action.setToolTip(_("Create a new file naming script")) self.add_action.setIcon(icontheme.lookup('add-item')) self.add_action.triggered.connect(self.new_script) script_menu.addAction(self.add_action) self.copy_action = QtWidgets.QAction(_("&Copy the current script"), self) self.copy_action.setToolTip( _("Save a copy of the script as a new script")) self.copy_action.setIcon(icontheme.lookup('edit-copy')) self.copy_action.triggered.connect(self.copy_script) script_menu.addAction(self.copy_action) self.delete_action = QtWidgets.QAction(_("&Delete the current script"), self) self.delete_action.setToolTip(_("Delete the script")) self.delete_action.setIcon(icontheme.lookup('list-remove')) self.delete_action.triggered.connect(self.delete_script) script_menu.addAction(self.delete_action) self.reset_action = QtWidgets.QAction(_("&Revert the current script"), self) self.reset_action.setToolTip( _("Revert the script to the last saved value")) self.reset_action.setIcon(icontheme.lookup('view-refresh')) self.reset_action.triggered.connect(self.reset_script) script_menu.addAction(self.reset_action) self.save_action = QtWidgets.QAction(_("&Save the current script"), self) self.save_action.setToolTip(_("Save changes to the script")) self.save_action.setIcon(icontheme.lookup('document-save')) self.save_action.setShortcut(QtGui.QKeySequence(_("Ctrl+S"))) self.save_action.triggered.connect(self.save_script) script_menu.addAction(self.save_action) # Display menu settings display_menu = main_menu.addMenu(_('&Display')) display_menu.setToolTipsVisible(True) self.examples_action = QtWidgets.QAction( _("&Reload random example files"), self) self.examples_action.setToolTip( _(self.examples.tooltip_text) % self.examples.max_samples) self.examples_action.setIcon(icontheme.lookup('view-refresh')) self.examples_action.triggered.connect(self.update_example_files) display_menu.addAction(self.examples_action) self.wrap_action = QtWidgets.QAction(_("&Word wrap script"), self) self.wrap_action.setToolTip(_("Word wrap long lines in the editor")) self.wrap_action.triggered.connect(self.toggle_wordwrap) self.wrap_action.setShortcut(QtGui.QKeySequence(_("Ctrl+W"))) self.wrap_action.setCheckable(True) display_menu.addAction(self.wrap_action) self.docs_action = QtWidgets.QAction(_("&Show documentation"), self) self.docs_action.setToolTip( _("View the scripting documentation in a sidebar")) self.docs_action.triggered.connect(self.toggle_documentation) self.docs_action.setShortcut(QtGui.QKeySequence(_("Ctrl+H"))) self.docs_action.setCheckable(True) display_menu.addAction(self.docs_action) # Help menu settings help_menu = main_menu.addMenu(_('&Help')) help_menu.setToolTipsVisible(True) self.docs_browse_action = QtWidgets.QAction(_("&Open in browser"), self) self.docs_browse_action.setToolTip( _("Open the scripting documentation in your browser")) self.docs_browse_action.setIcon(icontheme.lookup('lookup-musicbrainz')) self.docs_browse_action.triggered.connect(self.docs_browser) help_menu.addAction(self.docs_browse_action) self.ui.layout_for_menubar.addWidget(main_menu) def load(self): """Load initial configuration. """ config = get_config() self.examples.settings = config.setting self.naming_scripts = config.setting["file_naming_scripts"] self.selected_script_id = config.setting[ "selected_file_naming_script_id"] self.selected_script_index = 0 idx = self.populate_script_selector() self.ui.preset_naming_scripts.blockSignals(True) self.ui.preset_naming_scripts.setCurrentIndex(idx) self.ui.preset_naming_scripts.blockSignals(False) self.select_script(skip_check=True) def docs_browser(self): """Open the scriping documentation in a browser. """ webbrowser2.open('doc_scripting') @staticmethod def synchronize_vertical_scrollbars(widgets): """Synchronize position of vertical scrollbars and selections for listed widgets. """ # Set highlight colors for selected list items example_style = widgets[0].palette() highlight_bg = example_style.color(QPalette.Active, QPalette.Highlight) highlight_fg = example_style.color(QPalette.Active, QPalette.HighlightedText) stylesheet = "QListView::item:selected { color: " + highlight_fg.name( ) + "; background-color: " + highlight_bg.name() + "; }" def _sync_scrollbar_vert(widget, value): widget.blockSignals(True) widget.verticalScrollBar().setValue(value) widget.blockSignals(False) widgets = set(widgets) for widget in widgets: for other in widgets - {widget}: widget.verticalScrollBar().valueChanged.connect( partial(_sync_scrollbar_vert, other)) widget.setStyleSheet(stylesheet) def eventFilter(self, object, event): """Process selected events. """ evtype = event.type() if evtype in {QtCore.QEvent.WindowActivate, QtCore.QEvent.FocusIn}: self.update_examples() return False def close_window(self): """Close the window. """ self.close() def closeEvent(self, event): """Custom close event handler to check for unsaved changes. """ if self.unsaved_changes_confirmation(): if self.has_changed(): self.select_script(skip_check=True) event.accept() else: event.ignore() def populate_script_selector(self): """Populate the script selection combo box. Returns: int: The index of the selected script in the combo box. """ if not self.selected_script_id: script_item = FileNamingScript( script=get_config().setting["file_naming_format"], title=_("Primary file naming script"), readonly=False, deletable=True, ) self.naming_scripts.insert(0, script_item.to_json()) self.selected_script_id = script_item['id'] self.ui.preset_naming_scripts.blockSignals(True) self.ui.preset_naming_scripts.clear() def _add_and_check(idx, count, title, item): self.ui.preset_naming_scripts.addItem(title, item) if item['id'] == self.selected_script_id: idx = count count += 1 return idx, count idx = 0 count = 0 # Use separate counter rather than `i` in case ScriptImportError triggers, resulting in an incorrect index count. for i in range(len(self.naming_scripts)): try: script_item = FileNamingScript().create_from_json( self.naming_scripts[i], create_new_id=False) except ScriptImportError: pass else: self.naming_scripts[i] = script_item.to_json( ) # Ensure scripts are stored with id codes idx, count = _add_and_check( idx, count, self.SCRIPT_TITLE_USER % script_item["title"], script_item) for script_item in get_file_naming_script_presets(): idx, count = _add_and_check(idx, count, script_item['title'], script_item) self.ui.preset_naming_scripts.blockSignals(False) self.update_scripts_list() return idx def toggle_documentation(self): """Toggle the display of the scripting documentation sidebar. """ self.sidebar = not self.sidebar self.ui.documentation_frame.setVisible(self.sidebar) def view_script_details(self): """View and edit (if not readonly) the metadata associated with the script. """ selected_item = self.get_selected_item() details_page = ScriptDetailsEditor(self, selected_item) details_page.signal_save.connect(self.update_from_details) details_page.show() details_page.raise_() details_page.activateWindow() def has_changed(self): """Check if the current script has pending edits to the title or script that have not been saved. Returns: bool: True if there are unsaved changes, otherwise false. """ script_item = self.ui.preset_naming_scripts.itemData( self.selected_script_index) return self.ui.script_title.text().strip() != script_item['title'] or \ self.get_script() != script_item['script'] or \ self.script_metadata_changed def update_from_details(self): """Update the script selection combo box and script list after updates from the script details dialog. """ selected_item = self.get_selected_item() self.update_combo_box_item( self.ui.preset_naming_scripts.currentIndex(), selected_item) self.ui.script_title.setText(selected_item['title']) self.script_metadata_changed = True def _insert_item(self, script_item): """Insert a new item into the script selection combo box and update the script list in the settings. Args: script_item (FileNamingScript): File naming scrip to insert. """ self.ui.preset_naming_scripts.blockSignals(True) idx = len(self.naming_scripts) self.ui.preset_naming_scripts.insertItem( idx, self.SCRIPT_TITLE_USER % script_item['title'], script_item) self.ui.preset_naming_scripts.setCurrentIndex(idx) self.ui.preset_naming_scripts.blockSignals(False) self.update_scripts_list() self.select_script(skip_check=True) def new_script(self): """Add a new (empty) script to the script selection combo box and script list. """ if self.unsaved_changes_confirmation(): script_item = FileNamingScript(script='$noop()') self._insert_item(script_item) def copy_script(self): """Add a copy of the script as a new editable script to the script selection combo box and script list. """ if self.unsaved_changes_confirmation(): selected = self.ui.preset_naming_scripts.currentIndex() script_item = self.ui.preset_naming_scripts.itemData(selected) new_item = script_item.copy() self._insert_item(new_item) def update_script_in_settings(self, script_item): self.signal_save.emit() # config = get_config() # config.setting["file_naming_format"] = script_item['script'] # config.setting["selected_file_naming_script_id"] = self.selected_script_id def update_scripts_list(self): """Refresh the script list in the settings based on the contents of the script selection combo box. """ self.naming_scripts = [] for idx in range(self.ui.preset_naming_scripts.count()): script_item = self.ui.preset_naming_scripts.itemData(idx) # Only add items that can be removed -- no presets if script_item.deletable: self.naming_scripts.append(script_item.to_json()) # config = get_config() # config.setting["file_naming_scripts"] = self.naming_scripts def get_selected_item(self): """Get the selected item from the script selection combo box. Returns: FileNamingScript: The selected script. """ selected = self.ui.preset_naming_scripts.currentIndex() return self.ui.preset_naming_scripts.itemData(selected) def unsaved_changes_confirmation(self): """Check if there are unsaved changes and as the user to confirm the action resulting in their loss. Returns: bool: True if no unsaved changes or user confirms the action, otherwise False. """ if not self.loading and self.has_changed() and not confirmation_dialog( self, _("There are unsaved changes to the current script. Do you want to continue and lose these changes?" )): self.ui.preset_naming_scripts.blockSignals(True) self.ui.preset_naming_scripts.setCurrentIndex( self.selected_script_index) self.ui.preset_naming_scripts.blockSignals(False) return False return True def select_script(self, skip_check=False): """Load the current script from the combo box into the editor. Args: skip_check (bool): Skip the check for unsaved edits. Defaults to False. """ if skip_check or self.unsaved_changes_confirmation(): script_item = self.get_selected_item() self.ui.script_title.setText(script_item['title']) self.set_script(script_item['script']) self.selected_script_id = script_item['id'] self.selected_script_index = self.ui.preset_naming_scripts.currentIndex( ) self.script_metadata_changed = False self.update_script_in_settings(script_item) self.set_button_states() self.update_examples() self.signal_selection_changed.emit() def update_combo_box_item(self, idx, script_item): """Update the title and item data for the specified script selection combo box item. Args: idx (int): Index of the item to update script_item (FileNamingScript): Updated script information """ self.ui.preset_naming_scripts.setItemData(idx, script_item) self.ui.preset_naming_scripts.setItemText( idx, self.SCRIPT_TITLE_USER % script_item['title']) self.update_script_in_settings(script_item) self.update_scripts_list() def set_button_states(self, save_enabled=True): """Set the button states based on the readonly and deletable attributes of the currently selected item in the script selection combo box. Args: save_enabled (bool, optional): Allow updates to be saved to this item. Defaults to True. """ selected = self.ui.preset_naming_scripts.currentIndex() if selected < 0: return script_item = self.get_selected_item() readonly = script_item['readonly'] self.ui.script_title.setReadOnly(readonly or selected < 1) # Buttons self.ui.file_naming_format.setReadOnly(readonly) self.ui.file_naming_editor_save.setEnabled(save_enabled and not readonly) self.ui.file_naming_editor_reset.setEnabled(not readonly) # Menu items self.save_action.setEnabled(save_enabled and not readonly) self.reset_action.setEnabled(not readonly) self.add_action.setEnabled(save_enabled) self.copy_action.setEnabled(save_enabled) self.delete_action.setEnabled(script_item['deletable'] and save_enabled) self.import_action.setEnabled(save_enabled) self.export_action.setEnabled(save_enabled) @staticmethod def synchronize_selected_example_lines(current_row, source, target): """Matches selected item in target to source""" if source.currentRow() != current_row: current_row = source.currentRow() target.blockSignals(True) target.setCurrentRow(current_row) target.blockSignals(False) def match_after_to_before(self): """Sets the selected item in the 'after' list to the corresponding item in the 'before' list. """ self.synchronize_selected_example_lines( self.examples_current_row, self.ui.example_filename_before, self.ui.example_filename_after) def match_before_to_after(self): """Sets the selected item in the 'before' list to the corresponding item in the 'after' list. """ self.synchronize_selected_example_lines( self.examples_current_row, self.ui.example_filename_after, self.ui.example_filename_before) def delete_script(self): """Removes the currently selected script from the script selection combo box and script list. """ if confirmation_dialog( self, _('Are you sure that you want to delete the script?')): idx = self.ui.preset_naming_scripts.currentIndex() self.ui.preset_naming_scripts.blockSignals(True) self.ui.preset_naming_scripts.removeItem(idx) if idx >= self.ui.preset_naming_scripts.count(): idx = self.ui.preset_naming_scripts.count() - 1 self.ui.preset_naming_scripts.setCurrentIndex(idx) self.ui.preset_naming_scripts.blockSignals(False) self.update_scripts_list() self.select_script(skip_check=True) def save_script(self): """Saves changes to the current script to the script list and combo box item. """ selected = self.ui.preset_naming_scripts.currentIndex() self.signal_save.emit() title = str(self.ui.script_title.text()).strip() if title: script_item = self.ui.preset_naming_scripts.itemData(selected) script_item.title = title script_item.script = self.get_script() self.update_combo_box_item(selected, script_item) dialog = QtWidgets.QMessageBox( QtWidgets.QMessageBox.Information, _("Save Script"), _("Changes to the script have been saved."), QtWidgets.QMessageBox.Ok, self) dialog.exec_() else: self.display_error( OptionsCheckError(_("Error"), _("The script title must not be empty."))) def get_script(self): """Provides the text of the file naming script currently loaded into the editor. Returns: str: File naming script """ return str(self.ui.file_naming_format.toPlainText()).strip() def set_script(self, script_text): """Sets the text of the file naming script into the editor and settings. Args: script_text (str): File naming script text to set in the editor. """ self.ui.file_naming_format.setPlainText(str(script_text).strip()) def update_example_files(self): """Update the before and after file naming examples list. """ self.examples.update_sample_example_files() self.display_examples() def update_examples(self): """Update the before and after file naming examples using the current file naming script in the editor. """ override = {'file_naming_format': self.get_script()} self.examples.update_examples(override) self.display_examples() @staticmethod def update_example_listboxes(before_listbox, after_listbox, examples): """Update the contents of the file naming examples before and after listboxes. Args: before_listbox (QListBox): The before listbox after_listbox (QListBox): The after listbox examples (ScriptEditorExamples): The object to use for the examples """ before_listbox.clear() after_listbox.clear() for before, after in sorted(examples, key=lambda x: x[1]): before_listbox.addItem(before) after_listbox.addItem(after) def display_examples(self): """Update the display of the before and after file naming examples. """ self.examples_current_row = -1 examples = self.examples.get_examples() self.update_example_listboxes(self.ui.example_filename_before, self.ui.example_filename_after, examples) self.signal_update.emit() def toggle_wordwrap(self): """Toggles wordwrap in the script editing textbox. """ self.wordwrap = not self.wordwrap if self.wordwrap: self.ui.file_naming_format.setLineWrapMode( QtWidgets.QTextEdit.WidgetWidth) else: self.ui.file_naming_format.setLineWrapMode( QtWidgets.QTextEdit.NoWrap) def output_error(self, title, fmt, filename, msg): """Log error and display error message dialog. Args: title (str): Title to display on the error dialog box fmt (str): Format for the error type being displayed filename (str): Name of the file being imported or exported msg (str): Error message to display """ log.error(fmt, filename, msg) error_message = _(fmt) % (filename, _(msg)) self.display_error(ScriptFileError(_(title), error_message)) def output_file_error(self, fmt, filename, msg): """Log file error and display error message dialog. Args: fmt (str): Format for the error type being displayed filename (str): Name of the file being imported or exported msg (str): Error message to display """ self.output_error(_("File Error"), fmt, filename, msg) def import_script(self): """Import from an external text file to a new script. Import can be either a plain text script or a naming script package. """ FILE_ERROR_IMPORT = N_('Error importing "%s". %s.') FILE_ERROR_DECODE = N_('Error decoding "%s". %s.') if not self.unsaved_changes_confirmation(): return dialog_title = _("Import Script File") dialog_file_types = self.FILE_TYPE_PACKAGE + ";;" + self.FILE_TYPE_SCRIPT + ";;" + self.FILE_TYPE_ALL options = QtWidgets.QFileDialog.Options() options |= QtWidgets.QFileDialog.DontUseNativeDialog filename, file_type = QtWidgets.QFileDialog.getOpenFileName( self, dialog_title, self.default_script_directory, dialog_file_types, options=options) if filename: log.debug('Importing naming script file: %s' % filename) try: with open(filename, 'r', encoding='utf8') as i_file: file_content = i_file.read() except OSError as error: self.output_file_error(FILE_ERROR_IMPORT, filename, error.strerror) return if not file_content.strip(): self.output_file_error(FILE_ERROR_IMPORT, filename, _('The file was empty')) return if file_type == self.FILE_TYPE_PACKAGE: try: script_item = FileNamingScript().create_from_json( file_content) except ScriptImportError as error: self.output_file_error(FILE_ERROR_DECODE, filename, error) return else: script_item = FileNamingScript(title=_("Imported from %s") % filename, script=file_content.strip()) self._insert_item(script_item) def export_script(self): """Export the current script to an external file. Export can be either as a plain text script or a naming script package. """ FILE_ERROR_EXPORT = N_('Error exporting file "%s". %s.') script_item = self.get_selected_item() script_text = self.get_script() if script_text: default_path = os.path.normpath( os.path.join(self.default_script_directory, self.default_script_filename)) dialog_title = _("Export Script File") dialog_file_types = self.FILE_TYPE_PACKAGE + ";;" + self.FILE_TYPE_SCRIPT + ";;" + self.FILE_TYPE_ALL options = QtWidgets.QFileDialog.Options() options |= QtWidgets.QFileDialog.DontUseNativeDialog filename, file_type = QtWidgets.QFileDialog.getSaveFileName( self, dialog_title, default_path, dialog_file_types, options=options) if filename: # Fix issue where Qt may set the extension twice (name, ext) = os.path.splitext(filename) if ext and str(name).endswith('.' + ext): filename = name log.debug('Exporting naming script file: %s' % filename) if file_type == self.FILE_TYPE_PACKAGE: script_text = script_item.to_json(indent=4) try: with open(filename, 'w', encoding='utf8') as o_file: o_file.write(script_text) except OSError as error: self.output_file_error(FILE_ERROR_EXPORT, filename, error.strerror) else: dialog = QtWidgets.QMessageBox( QtWidgets.QMessageBox.Information, _("Export Script"), _("Script successfully exported to %s") % filename, QtWidgets.QMessageBox.Ok, self) dialog.exec_() def reset_script(self): """Reset the script to the last saved value. """ if self.has_changed(): if confirmation_dialog( self, _("Are you sure that you want to reset the script to its last saved value?" )): self.select_script(skip_check=True) else: dialog = QtWidgets.QMessageBox( QtWidgets.QMessageBox.Information, _("Revert Script"), _("There have been no changes made since the last time the script was saved." ), QtWidgets.QMessageBox.Ok, self) dialog.exec_() def check_formats(self): """Checks for valid file naming script and settings, and updates the examples. """ self.test() self.update_examples() def check_format(self): """Parse the file naming script and check for errors. """ config = get_config() parser = ScriptParser() script_text = self.get_script() try: parser.eval(script_text) except Exception as e: raise ScriptCheckError("", str(e)) if config.setting["rename_files"]: if not self.get_script(): raise ScriptCheckError( "", _("The file naming format must not be empty.")) def display_error(self, error): """Display an error message for the specified error. Args: error (Exception): The exception to display. """ # Ignore scripting errors, those are handled inline if not isinstance(error, ScriptCheckError): dialog = QtWidgets.QMessageBox(QtWidgets.QMessageBox.Warning, error.title, error.info, QtWidgets.QMessageBox.Ok, self) dialog.exec_() def test(self): """Parse the script and display any errors. """ self.ui.renaming_error.setStyleSheet("") self.ui.renaming_error.setText("") save_enabled = True try: self.check_format() except ScriptCheckError as e: self.ui.renaming_error.setStyleSheet(self.STYLESHEET_ERROR) self.ui.renaming_error.setText(e.info) save_enabled = False self.set_button_states(save_enabled=save_enabled)
class ProfilesOptionsPage(OptionsPage): NAME = "profiles" TITLE = N_("Option Profiles") PARENT = None SORT_ORDER = 10 ACTIVE = True HELP_URL = '/config/options_profiles.html' PROFILES_KEY = SettingConfigSection.PROFILES_KEY SETTINGS_KEY = SettingConfigSection.SETTINGS_KEY POSITION_KEY = "last_selected_profile_pos" EXPANDED_KEY = "profile_settings_tree_expanded_list" TREEWIDGETITEM_COLUMN = 0 options = [ IntOption("persist", POSITION_KEY, 0), ListOption("persist", EXPANDED_KEY, []) ] signal_refresh = QtCore.pyqtSignal() def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_ProfileEditorDialog() self.ui.setupUi(self) self.make_buttons() self.ui.profile_editor_splitter.setStretchFactor(1, 1) self.move_view = MoveableListView(self.ui.profile_list, self.ui.move_up_button, self.ui.move_down_button) self.ui.profile_list.itemChanged.connect(self.profile_item_changed) self.ui.profile_list.currentItemChanged.connect(self.current_item_changed) self.ui.profile_list.itemChanged.connect(self.reload_all_page_settings) self.ui.settings_tree.itemChanged.connect(self.set_profile_settings_changed) self.ui.settings_tree.itemExpanded.connect(self.update_current_expanded_items_list) self.ui.settings_tree.itemCollapsed.connect(self.update_current_expanded_items_list) self.current_profile_id = None self.expanded_sections = [] self.building_tree = False self.loading = False self.settings_changed = False self.ui.settings_tree.installEventFilter(self) def eventFilter(self, object, event): """Process selected events. """ event_type = event.type() if event_type == QtCore.QEvent.FocusOut and object == self.ui.settings_tree: if self.settings_changed: self.settings_changed = False self.update_values_in_profile_options() self.reload_all_page_settings() return False def make_buttons(self): """Make buttons and add them to the button bars. """ self.new_profile_button = QtWidgets.QPushButton(_('New')) self.new_profile_button.setToolTip(_("Create a new profile")) self.new_profile_button.clicked.connect(self.new_profile) self.ui.profile_list_buttonbox.addButton(self.new_profile_button, QtWidgets.QDialogButtonBox.ActionRole) self.copy_profile_button = QtWidgets.QPushButton(_('Copy')) self.copy_profile_button.setToolTip(_("Copy to a new profile")) self.copy_profile_button.clicked.connect(self.copy_profile) self.ui.profile_list_buttonbox.addButton(self.copy_profile_button, QtWidgets.QDialogButtonBox.ActionRole) self.delete_profile_button = QtWidgets.QPushButton(_('Delete')) self.delete_profile_button.setToolTip(_("Delete the profile")) self.delete_profile_button.clicked.connect(self.delete_profile) self.ui.profile_list_buttonbox.addButton(self.delete_profile_button, QtWidgets.QDialogButtonBox.ActionRole) def restore_defaults(self): """Remove all profiles and profile settings. """ self.ui.profile_list.clear() self.profile_settings = {} self.profile_selected() self.update_config_overrides() self.reload_all_page_settings() def load(self): """Load initial configuration. """ self.loading = True config = get_config() # Use deepcopy() to avoid changes made locally from being cascaded into `config.profiles` # before the user clicks "Make It So!" self.profile_settings = deepcopy(config.profiles[self.SETTINGS_KEY]) self.ui.profile_list.clear() for profile in config.profiles[self.PROFILES_KEY]: list_item = ProfileListWidgetItem(profile['title'], profile['enabled'], profile['id']) self.ui.profile_list.addItem(list_item) # Select the last selected profile item last_selected_profile_pos = config.persist[self.POSITION_KEY] self.expanded_sections = config.persist[self.EXPANDED_KEY] last_selected_profile = self.ui.profile_list.item(last_selected_profile_pos) settings = None if last_selected_profile: self.ui.profile_list.setCurrentItem(last_selected_profile) last_selected_profile.setSelected(True) id = last_selected_profile.profile_id self.current_profile_id = id settings = self.get_settings_for_profile(id) self.make_setting_tree(settings=settings) self.update_config_overrides() self.loading = False def update_config_overrides(self, reset=False): """Update the profile overrides used in `config.settings` when retrieving or saving a setting. Args: reset (bool, optional): Remove the profile overrides. Defaults to False. """ config = get_config() if reset: config.setting.set_profiles_override(None) config.setting.set_settings_override(None) else: config.setting.set_profiles_override(self._clean_and_get_all_profiles()) config.setting.set_settings_override(self.profile_settings) def get_settings_for_profile(self, id): """Get the settings for the specified profile ID. Automatically adds an empty settings dictionary if there is no settings dictionary found for the ID. Args: id (str): ID of the profile Returns: dict: Profile settings """ # Add empty settings dictionary if no dictionary found for the profile. # This happens when a new profile is created. if id not in self.profile_settings: self.profile_settings[id] = {} return self.profile_settings[id] def _all_profiles(self): """Get all profiles from the profiles list in order from top to bottom. Yields: dict: Profile information in a format for saving to the user settings """ for row in range(self.ui.profile_list.count()): item = self.ui.profile_list.item(row) yield item.get_dict() def make_setting_tree(self, settings=None): """Update the profile settings tree based on the settings provided. If no settings are provided, displays an empty tree. Args: settings (dict, optional): Dictionary of settings for the profile. Defaults to None. """ self.set_button_states() self.ui.settings_tree.clear() self.ui.settings_tree.setHeaderItem(QtWidgets.QTreeWidgetItem()) self.ui.settings_tree.setHeaderLabels([_("Settings to include in profile")]) if settings is None: return self.building_tree = True for id, group in UserProfileGroups.SETTINGS_GROUPS.items(): title = group["title"] group_settings = group["settings"] widget_item = QtWidgets.QTreeWidgetItem([title]) widget_item.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsAutoTristate) widget_item.setCheckState(self.TREEWIDGETITEM_COLUMN, QtCore.Qt.Unchecked) for setting in group_settings: child_item = QtWidgets.QTreeWidgetItem([_(setting.title)]) child_item.setData(0, QtCore.Qt.UserRole, setting.name) child_item.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsUserCheckable) state = QtCore.Qt.Checked if settings and setting.name in settings else QtCore.Qt.Unchecked child_item.setCheckState(self.TREEWIDGETITEM_COLUMN, state) if setting.name in settings and settings[setting.name] is not None: value = settings[setting.name] else: value = None child_item.setToolTip(self.TREEWIDGETITEM_COLUMN, self.make_setting_value_text(setting.name, value)) widget_item.addChild(child_item) self.ui.settings_tree.addTopLevelItem(widget_item) if title in self.expanded_sections: widget_item.setExpanded(True) self.building_tree = False def _get_naming_script(self, config, value): if value in config.setting["file_renaming_scripts"]: return config.setting["file_renaming_scripts"][value]["title"] presets = {x["id"]: x["title"] for x in get_file_naming_script_presets()} if value in presets: return presets[value] return _("Unknown script") def _get_scripts_list(self, config, key, template, none_text): if not config.setting[key]: return _("No scripts in list") flag = False scripts = config.setting[key] value_text = _("Enabled tagging scripts of %i found:") % len(scripts) for (pos, name, enabled, script) in scripts: if enabled: flag = True value_text += template % name if not flag: value_text += " %s" % none_text return value_text def _get_ca_providers_list(self, config, key, template, none_text): flag = False providers = config.setting[key] value_text = _("Enabled providers of %i listed:") % len(providers) for (name, enabled) in providers: if enabled: flag = True value_text += template % name if not flag: value_text += " %s" % none_text return value_text def make_setting_value_text(self, key, value): ITEMS_TEMPLATE = "\n - %s" NONE_TEXT = _("None") config = get_config() if value is None: return NONE_TEXT if key == "selected_file_naming_script_id": return self._get_naming_script(config, value) if key == "list_of_scripts": return self._get_scripts_list(config, key, ITEMS_TEMPLATE, NONE_TEXT) if key == "ca_providers": return self._get_ca_providers_list(config, key, ITEMS_TEMPLATE, NONE_TEXT) if isinstance(value, str): return '"%s"' % value if type(value) in {bool, int, float}: return str(value) if type(value) in {set, tuple, list, dict}: return _("List of %i items") % len(value) return _("Unknown value format") def update_current_expanded_items_list(self): """Update the list of expanded sections in the settings tree for persistent settings. """ if self.building_tree: return self.expanded_sections = [] for i in range(self.ui.settings_tree.topLevelItemCount()): tl_item = self.ui.settings_tree.topLevelItem(i) if tl_item.isExpanded(): self.expanded_sections.append(tl_item.text(self.TREEWIDGETITEM_COLUMN)) def get_current_selected_item(self): """Gets the profile item currently selected in the profiles list. Returns: ProfileListWidgetItem: Currently selected item """ items = self.ui.profile_list.selectedItems() if items: return items[0] return None def profile_selected(self, update_settings=True): """Update working profile information for the selected item in the profiles list. Args: update_settings (bool, optional): Update settings tree. Defaults to True. """ item = self.get_current_selected_item() if item: id = item.profile_id self.current_profile_id = id if update_settings: settings = self.get_settings_for_profile(id) self.make_setting_tree(settings=settings) else: self.current_profile_id = None self.make_setting_tree(settings=None) def reload_all_page_settings(self): """Trigger a reload of the settings and highlights for all pages containing options that can be managed in a profile. """ self.signal_refresh.emit() def update_values_in_profile_options(self): """Update the current profile's settings dictionary from the settings tree. Note that this update is delayed to avoid losing a profile's option setting value when a selected option (with an associated value) is de-selected and then re-selected. """ if not self.current_profile_id: return checked_items = set(self.get_checked_items_from_tree()) settings = set(self.profile_settings[self.current_profile_id].keys()) # Add new items to settings for item in checked_items.difference(settings): self.profile_settings[self.current_profile_id][item] = None # Remove unchecked items from settings for item in settings.difference(checked_items): del self.profile_settings[self.current_profile_id][item] def profile_item_changed(self, item): """Check title is not blank and remove leading and trailing spaces. Args: item (ProfileListWidgetItem): Item that changed """ if not self.loading: text = item.text().strip() if not text: QtWidgets.QMessageBox( QtWidgets.QMessageBox.Warning, _("Invalid Title"), _("The profile title cannot be blank."), QtWidgets.QMessageBox.Ok, self ).exec_() item.setText(self.ui.profile_list.unique_profile_name()) elif text != item.text(): # Remove leading and trailing spaces from new title. item.setText(text) self.update_config_overrides() self.reload_all_page_settings() def current_item_changed(self, new_item, old_item): """Update the display when a new item is selected in the profile list. Args: new_item (ProfileListWidgetItem): Newly selected item old_item (ProfileListWidgetItem): Previously selected item """ if self.loading: return # Set self.loading to avoid looping through the `.currentItemChanged` event. self.loading = True self.ui.profile_list.setCurrentItem(new_item) self.loading = False self.profile_selected() def get_checked_items_from_tree(self): """Get the keys for the settings that are checked in the profile settings tree. Yields: str: Settings key """ for i in range(self.ui.settings_tree.topLevelItemCount()): tl_item = self.ui.settings_tree.topLevelItem(i) for j in range(tl_item.childCount()): item = tl_item.child(j) if item.checkState(self.TREEWIDGETITEM_COLUMN) == QtCore.Qt.Checked: yield item.data(self.TREEWIDGETITEM_COLUMN, QtCore.Qt.UserRole) def set_profile_settings_changed(self): """Set flag to trigger option page updates later (when focus is lost from the settings tree) to avoid updating after each change to the settings selected for a profile. """ if self.current_profile_id: self.settings_changed = True def copy_profile(self): """Make a copy of the currently selected profile. """ item = self.get_current_selected_item() id = str(uuid.uuid4()) settings = deepcopy(self.profile_settings[self.current_profile_id]) self.profile_settings[id] = settings base_title = "%s %s" % (get_base_title(item.name), _(DEFAULT_COPY_TEXT)) name = self.ui.profile_list.unique_profile_name(base_title) self.ui.profile_list.add_profile(name=name, profile_id=id) self.update_config_overrides() self.reload_all_page_settings() def new_profile(self): """Add a new profile with no settings selected. """ self.ui.profile_list.add_profile() self.update_config_overrides() self.reload_all_page_settings() def delete_profile(self): """Delete the current profile. """ self.ui.profile_list.remove_selected_profile() self.profile_selected() self.update_config_overrides() self.reload_all_page_settings() def _clean_and_get_all_profiles(self): """Returns the list of profiles, adds any missing profile settings, and removes any "orphan" profile settings (i.e. settings dictionaries not associated with an existing profile). Returns: list: List of profiles suitable for storing in `config.profiles`. """ all_profiles = list(self._all_profiles()) all_profile_ids = set(x['id'] for x in all_profiles) keys = set(self.profile_settings.keys()) # Add any missing profile settings for id in all_profile_ids.difference(keys): self.profile_settings[id] = {} # Remove any "orphan" profile settings for id in keys.difference(all_profile_ids): del self.profile_settings[id] return all_profiles def save(self): """Save any changes to the current profile's settings, and save all updated profile information to the user settings. """ config = get_config() config.profiles[self.PROFILES_KEY] = self._clean_and_get_all_profiles() config.profiles[self.SETTINGS_KEY] = self.profile_settings config.persist[self.POSITION_KEY] = self.ui.profile_list.currentRow() config.persist[self.EXPANDED_KEY] = self.expanded_sections def set_button_states(self): """Set the enabled / disabled states of the buttons. """ state = self.current_profile_id is not None self.copy_profile_button.setEnabled(state) self.delete_profile_button.setEnabled(state)
class OptionsDialog(PicardDialog, SingletonDialog): autorestore = False options = [ TextOption("persist", "options_last_active_page", ""), ListOption("persist", "options_pages_tree_state", []), 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.show_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: config = get_config() 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: config = get_config() 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) @property def help_url(self): current_page = self.ui.pages_stack.currentWidget() url = current_page.HELP_URL # If URL is empty, use the first non empty parent help URL. while current_page.PARENT and not url: current_page = self.item_to_page[self.page_to_item[ current_page.PARENT]] url = current_page.HELP_URL if not url: url = 'doc_options' # key in PICARD_URLS return 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 = get_config() config.persist["options_pages_tree_state"] = expanded_pages config.persist["options_splitter"] = self.ui.splitter.saveState() @restore_method def restoreWindowState(self): config = get_config() 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()
def test_upgrade_to_v1_3_0_dev_2_skip_list(self): ListOption('setting', 'preserved_tags', []) self.config.setting['preserved_tags'] = ['foo'] upgrade_to_v1_3_0_dev_2(self.config) self.assertEqual(['foo'], self.config.setting['preserved_tags'])
def test_list_opt_convert(self): opt = ListOption("setting", "list_option", []) self.assertEqual(opt.convert("123"), ['1', '2', '3'])
class CoverOptionsPage(OptionsPage): NAME = "cover" TITLE = N_("Cover Art") PARENT = None SORT_ORDER = 35 ACTIVE = True HELP_URL = '/config/options_cover.html' options = [ BoolOption("setting", "save_images_to_tags", True), BoolOption("setting", "embed_only_one_front_image", True), BoolOption("setting", "save_images_to_files", False), TextOption("setting", "cover_image_filename", DEFAULT_COVER_IMAGE_FILENAME), BoolOption("setting", "save_images_overwrite", False), BoolOption("setting", "save_only_one_front_image", False), BoolOption("setting", "image_type_as_filename", False), ListOption("setting", "ca_providers", [ ('Cover Art Archive', True), ('UrlRelationships', True), ('CaaReleaseGroup', True), ('Local', False), ]), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_CoverOptionsPage() self.ui.setupUi(self) self.ui.cover_image_filename.setPlaceholderText( Option.get('setting', 'cover_image_filename').default) self.ui.save_images_to_files.clicked.connect( self.update_ca_providers_groupbox_state) self.ui.save_images_to_tags.clicked.connect( self.update_ca_providers_groupbox_state) self.ui.save_only_one_front_image.toggled.connect( self.ui.image_type_as_filename.setDisabled) 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 """ self.ui.ca_providers_list.clear() 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): config = get_config() 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.ui.save_only_one_front_image.setChecked( config.setting["save_only_one_front_image"]) self.ui.image_type_as_filename.setChecked( config.setting["image_type_as_filename"]) self.load_cover_art_providers() self.ui.ca_providers_list.setCurrentRow(0) self.update_ca_providers_groupbox_state() def save(self): config = get_config() 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[ "save_only_one_front_image"] = self.ui.save_only_one_front_image.isChecked( ) config.setting[ "image_type_as_filename"] = self.ui.image_type_as_filename.isChecked( ) config.setting["ca_providers"] = self.ca_providers() 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 test_list_opt_set_none(self): ListOption("setting", "list_option", ["a", "b"]) # set option to None self.config.setting["list_option"] = None self.assertEqual(self.config.setting["list_option"], [])
def test_upgrade_to_v2_4_0_beta_3(self): ListOption("setting", "preserved_tags", []) self.config.setting['preserved_tags'] = 'foo,bar' upgrade_to_v2_4_0_beta_3(self.config) self.assertEqual(['foo', 'bar'], self.config.setting['preserved_tags'])
def test_list_opt_direct_invalid(self): ListOption("setting", "list_option", ["a", "b"]) # store invalid list value in config file directly self.config.setValue('setting/list_option', 'efg') self.assertEqual(self.config.setting["list_option"], ["a", "b"])
class ProviderOptionsCaa(ProviderOptions): """ Options for Cover Art Archive cover art provider """ HELP_URL = '/config/options_cover_art_archive.html' options = [ BoolOption("setting", "caa_approved_only", False), IntOption("setting", "caa_image_size", _CAA_IMAGE_SIZE_DEFAULT), ListOption("setting", "caa_image_types", _CAA_IMAGE_TYPE_DEFAULT_INCLUDE), BoolOption("setting", "caa_restrict_image_types", True), 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) config = get_config() 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_approved_only.setChecked( config.setting["caa_approved_only"]) 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): config = get_config() size = self.ui.cb_image_size.currentData() config.setting["caa_image_size"] = size config.setting["caa_approved_only"] = \ self.ui.cb_approved_only.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 ScriptingOptionsPage(OptionsPage): NAME = "scripting" TITLE = N_("Scripting") PARENT = None SORT_ORDER = 85 ACTIVE = True HELP_URL = '/config/options_scripting.html' options = [ BoolOption("setting", "enable_tagger_scripts", False), ListOption("setting", "list_of_scripts", []), IntOption("persist", "last_selected_script_pos", 0), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_ScriptingOptionsPage() self.ui.setupUi(self) self.ui.tagger_script.setEnabled(False) self.ui.scripting_options_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) def show_scripting_documentation(self): ScriptingDocumentationDialog.show_instance(parent=self) 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): config = get_config() 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) 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() def save(self): config = get_config() 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() def display_error(self, error): # Ignore scripting errors, those are handled inline if not isinstance(error, ScriptCheckError): super().display_error(error)
class PluginsOptionsPage(OptionsPage): NAME = "plugins" TITLE = N_("Plugins") PARENT = None SORT_ORDER = 70 ACTIVE = True HELP_URL = '/config/options_plugins.html' options = [ ListOption("setting", "enabled_plugins", []), Option("persist", "plugins_list_state", QtCore.QByteArray()), Option("persist", "plugins_list_sort_section", 0), 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 = get_config() 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() config = get_config() 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): config = get_config() 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 > plugin.version: 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): if self.deleted: return 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 occurred 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) else: self._reload() item = self.find_item_by_plugin_name(plugin.module_name) if item: self.set_current_item(item, scroll=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.to_string(short=True))) 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: if self.manager.is_available(plugin_name): self.update_plugin_item(item, None, make_current=True, is_installed=False) else: # Remove local plugin self.ui.plugins.invisibleRootItem().removeChild(item) 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.to_string(short=True), item.new_version.to_string(short=True)) else: version = plugin.version.to_string(short=True) 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' if plugin.is_user_installed else 'hide') 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.enable(False) 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, plugin.version) return item def save(self): config = get_config() 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") version_str = item.new_version.to_string(short=True) text.append("<b>{0}: {1}</b>".format(label, version_str)) if plugin.description: text.append(plugin.description + "<hr width='90%'/>") infos = [ (_("Name"), plugin.name), (_("Authors"), plugin.author), (_("License"), plugin.license), (_("Files"), plugin.files_list), ] for label, value in infos: if value: text.append("<b>{0}:</b> {1}".format(label, value)) self.ui.details.setText("<p>{0}</p>".format("<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, "version": plugin.version.to_string(short=True) }) 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(): QtGui.QDesktopServices.openUrl( QtCore.QUrl.fromLocalFile(USER_PLUGIN_DIR)) 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)
class OptionsDialog(PicardDialog, SingletonDialog): options = [ TextOption("persist", "options_last_active_page", ""), ListOption("persist", "options_pages_tree_state", []), ] 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.show_help) self.ui.attached_profiles_button = QtWidgets.QPushButton( _("Attached Profiles")) self.ui.attached_profiles_button.setToolTip( _("Show which profiles are attached to the options on this page")) self.ui.buttonbox.addButton(self.ui.attached_profiles_button, QtWidgets.QDialogButtonBox.ActionRole) self.ui.attached_profiles_button.clicked.connect( self.show_attached_profiles_dialog) config = get_config() 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.dialog_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) self.profile_page = self.get_page('profiles') self.profile_page.signal_refresh.connect( self.update_from_profile_changes) self.first_enter = True self.installEventFilter(self) self.highlight_enabled_profile_options() current_page = self.item_to_page[self.ui.pages_tree.currentItem()] self.set_profiles_button_and_highlight(current_page) def page_has_profile_options(self, page): try: name = page.PARENT if page.PARENT in UserProfileGroups.SETTINGS_GROUPS else page.NAME except AttributeError: return False return name in UserProfileGroups.get_setting_groups_list() def show_attached_profiles_dialog(self): window_title = _("Profiles Attached to Options") items = self.ui.pages_tree.selectedItems() if not items: return page = self.item_to_page[items[0]] if not self.page_has_profile_options(page): message_box = QtWidgets.QMessageBox(self) message_box.setIcon(QtWidgets.QMessageBox.Information) message_box.setWindowModality(QtCore.Qt.WindowModal) message_box.setWindowTitle(window_title) message_box.setText( _("The options on this page are not currently available to be managed using profiles." )) message_box.setStandardButtons(QtWidgets.QMessageBox.Ok) return message_box.exec_() override_profiles = self.profile_page._clean_and_get_all_profiles() override_settings = self.profile_page.profile_settings profile_dialog = AttachedProfilesDialog( parent=self, option_group=page.NAME, override_profiles=override_profiles, override_settings=override_settings) profile_dialog.show() profile_dialog.raise_() profile_dialog.activateWindow() def _get_profile_title_from_id(self, profile_id): config = get_config() for item in config.profiles[SettingConfigSection.PROFILES_KEY]: if item["id"] == profile_id: return item["title"] return _('Unknown profile') def update_from_profile_changes(self): self.highlight_enabled_profile_options(load_settings=True) def get_working_profile_data(self): profile_page = self.get_page('profiles') working_profiles = profile_page._clean_and_get_all_profiles() if working_profiles is None: working_profiles = [] working_settings = profile_page.profile_settings return working_profiles, working_settings def highlight_enabled_profile_options(self, load_settings=False): working_profiles, working_settings = self.get_working_profile_data() HighlightColors = namedtuple('HighlightColors', ('fg', 'bg')) HIGHLIGHT_FMT = "#%s { color: %s; background-color: %s; }" if theme.is_dark_theme: option_colors = HighlightColors('#FFFFFF', '#000080') else: option_colors = HighlightColors('#000000', '#F9F906') for page in self.pages: page_name = page.PARENT if page.PARENT in UserProfileGroups.SETTINGS_GROUPS else page.NAME if page_name in UserProfileGroups.SETTINGS_GROUPS: if load_settings: page.load() for opt in UserProfileGroups.SETTINGS_GROUPS[page_name][ 'settings']: for opt_field in opt.fields: style = HIGHLIGHT_FMT % (opt_field, option_colors.fg, option_colors.bg) try: obj = getattr(page.ui, opt_field) except AttributeError: continue self._check_and_highlight_option( obj, opt.name, working_profiles, working_settings, style) def _check_and_highlight_option(self, obj, option_name, working_profiles, working_settings, style): obj.setStyleSheet(None) obj.setToolTip(None) for item in working_profiles: if item["enabled"]: profile_id = item["id"] profile_title = item["title"] if profile_id in working_settings: profile_settings = working_settings[profile_id] else: profile_settings = {} if option_name in profile_settings: tooltip = _("This option will be saved to profile: %s" ) % profile_title try: obj.setStyleSheet(style) obj.setToolTip(tooltip) except AttributeError: pass break def eventFilter(self, object, event): """Process selected events. """ evtype = event.type() if evtype == QtCore.QEvent.Enter: if self.first_enter: self.first_enter = False if self.tagger and self.tagger.window.script_editor_dialog is not None: self.get_page('filerenaming').show_script_editing_page() self.activateWindow() return False def get_page(self, name): return self.item_to_page[self.page_to_item[name]] def page_has_attached_profiles(self, page, enabled_profiles_only=False): if not self.page_has_profile_options(page): return False working_profiles, working_settings = self.get_working_profile_data() page_name = page.PARENT if page.PARENT in UserProfileGroups.SETTINGS_GROUPS else page.NAME for opt in UserProfileGroups.SETTINGS_GROUPS[page_name]['settings']: for item in working_profiles: if enabled_profiles_only and not item["enabled"]: continue profile_id = item["id"] if opt.name in working_settings[profile_id]: return True return False def set_profiles_button_and_highlight(self, page): if self.page_has_attached_profiles(page): self.ui.attached_profiles_button.setDisabled(False) else: self.ui.attached_profiles_button.setDisabled(True) self.ui.pages_stack.setCurrentWidget(page) def switch_page(self): items = self.ui.pages_tree.selectedItems() if items: config = get_config() page = self.item_to_page[items[0]] config.persist["options_last_active_page"] = page.NAME self.set_profiles_button_and_highlight(page) def disable_page(self, name): item = self.page_to_item[name] item.setDisabled(True) @property def help_url(self): current_page = self.ui.pages_stack.currentWidget() url = current_page.HELP_URL # If URL is empty, use the first non empty parent help URL. while current_page.PARENT and not url: current_page = self.item_to_page[self.page_to_item[ current_page.PARENT]] url = current_page.HELP_URL if not url: url = 'doc_options' # key in PICARD_URLS return 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 = get_config() config.persist["options_pages_tree_state"] = expanded_pages config.setting.set_profiles_override() config.setting.set_settings_override() @restore_method def restoreWindowState(self): config = get_config() 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) def restore_all_defaults(self): for page in self.pages: page.restore_defaults() self.highlight_enabled_profile_options() def restore_page_defaults(self): self.ui.pages_stack.currentWidget().restore_defaults() self.highlight_enabled_profile_options() 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()
def test_list_opt_not_list(self): ListOption("setting", "list_option", ["a", "b"]) # set option to invalid value self.config.setting["list_option"] = 'invalid' self.assertEqual(self.config.setting["list_option"], ["a", "b"])
class RenamingOptionsPage(OptionsPage): NAME = "filerenaming" TITLE = N_("File Naming") PARENT = None SORT_ORDER = 40 ACTIVE = True HELP_URL = '/config/options_filerenaming.html' options = [ BoolOption("setting", "windows_compatibility", True), BoolOption("setting", "ascii_filenames", False), BoolOption("setting", "rename_files", False), TextOption( "setting", "file_naming_format", DEFAULT_FILE_NAMING_FORMAT, ), BoolOption("setting", "move_files", False), TextOption("setting", "move_files_to", _default_music_dir), BoolOption("setting", "move_additional_files", False), TextOption("setting", "move_additional_files_pattern", "*.jpg *.png"), BoolOption("setting", "delete_empty_dirs", True), ListOption( "setting", "file_naming_scripts", [], ), TextOption( "setting", "selected_file_naming_script_id", "", ), ] def __init__(self, parent=None): super().__init__(parent) self.script_text = "" self.ui = Ui_RenamingOptionsPage() self.ui.setupUi(self) self.ui.ascii_filenames.clicked.connect( self.update_examples_from_local) self.ui.windows_compatibility.clicked.connect( self.update_examples_from_local) self.ui.rename_files.clicked.connect(self.update_examples_from_local) self.ui.move_files.clicked.connect(self.update_examples_from_local) self.ui.move_files_to.editingFinished.connect( self.update_examples_from_local) self.ui.move_files.toggled.connect( partial(enabledSlot, self.toggle_file_moving)) self.ui.rename_files.toggled.connect( partial(enabledSlot, self.toggle_file_renaming)) self.ui.open_script_editor.clicked.connect( self.show_script_editing_page) self.ui.move_files_to_browse.clicked.connect(self.move_files_to_browse) self.ui.naming_script_selector.currentIndexChanged.connect( self.update_selector_in_editor) self.ui.example_filename_after.itemSelectionChanged.connect( self.match_before_to_after) self.ui.example_filename_before.itemSelectionChanged.connect( self.match_after_to_before) script_edit = self.ui.move_additional_files_pattern self.script_palette_normal = script_edit.palette() self.script_palette_readonly = QPalette(self.script_palette_normal) disabled_color = self.script_palette_normal.color( QPalette.Inactive, QPalette.Window) self.script_palette_readonly.setColor(QPalette.Disabled, QPalette.Base, disabled_color) self.ui.example_filename_sample_files_button.clicked.connect( self.update_example_files) self.examples = ScriptEditorExamples(tagger=self.tagger) self.ui.example_selection_note.setText( _(self.examples.notes_text) % self.examples.max_samples) self.ui.example_filename_sample_files_button.setToolTip( _(self.examples.tooltip_text) % self.examples.max_samples) self.script_editor_page = ScriptEditorPage(parent=self, examples=self.examples) self.script_editor_page.signal_save.connect(self.save_from_editor) self.script_editor_page.signal_update.connect(self.update_from_editor) self.script_editor_page.signal_selection_changed.connect( self.update_selector_from_editor) self.update_selector_from_editor() # Sync example lists vertical scrolling and selection colors self.script_editor_page.synchronize_vertical_scrollbars( (self.ui.example_filename_before, self.ui.example_filename_after)) self.current_row = -1 def update_selector_from_editor(self): """Update the script selector combo box from the script editor page. """ self.ui.naming_script_selector.blockSignals(True) self.ui.naming_script_selector.clear() for i in range( self.script_editor_page.ui.preset_naming_scripts.count()): title = self.script_editor_page.ui.preset_naming_scripts.itemText( i) script = self.script_editor_page.ui.preset_naming_scripts.itemData( i) self.ui.naming_script_selector.addItem(title, script) self.ui.naming_script_selector.setCurrentIndex( self.script_editor_page.ui.preset_naming_scripts.currentIndex()) self.ui.naming_script_selector.blockSignals(False) def update_selector_in_editor(self): """Update the selection in the script editor page to match local selection. """ self.script_editor_page.ui.preset_naming_scripts.setCurrentIndex( self.ui.naming_script_selector.currentIndex()) def match_after_to_before(self): """Sets the selected item in the 'after' list to the corresponding item in the 'before' list. """ self.script_editor_page.synchronize_selected_example_lines( self.current_row, self.ui.example_filename_before, self.ui.example_filename_after) def match_before_to_after(self): """Sets the selected item in the 'before' list to the corresponding item in the 'after' list. """ self.script_editor_page.synchronize_selected_example_lines( self.current_row, self.ui.example_filename_after, self.ui.example_filename_before) def show_script_editing_page(self): self.script_editor_page.show() self.script_editor_page.raise_() self.script_editor_page.activateWindow() self.update_examples_from_local() def show_scripting_documentation(self): ScriptingDocumentationDialog.show_instance(parent=self) def toggle_file_moving(self, state): self.toggle_file_naming_format() self.ui.delete_empty_dirs.setEnabled(state) self.ui.move_files_to.setEnabled(state) self.ui.move_files_to_browse.setEnabled(state) self.ui.move_additional_files.setEnabled(state) self.ui.move_additional_files_pattern.setEnabled(state) def toggle_file_renaming(self, state): self.toggle_file_naming_format() def toggle_file_naming_format(self): active = self.ui.move_files.isChecked( ) or self.ui.rename_files.isChecked() self.ui.open_script_editor.setEnabled(active) self.ui.ascii_filenames.setEnabled(active) if not IS_WIN: self.ui.windows_compatibility.setEnabled(active) def save_from_editor(self): self.script_text = self.script_editor_page.get_script() def update_from_editor(self): self.display_examples() def check_formats(self): self.test() self.update_examples_from_local() def update_example_files(self): self.examples.update_sample_example_files() self.script_editor_page.display_examples() def update_examples_from_local(self): override = { 'ascii_filenames': self.ui.ascii_filenames.isChecked(), 'move_files': self.ui.move_files.isChecked(), 'move_files_to': os.path.normpath(self.ui.move_files_to.text()), 'rename_files': self.ui.rename_files.isChecked(), 'windows_compatibility': self.ui.windows_compatibility.isChecked(), } self.examples.update_examples(override=override) self.script_editor_page.display_examples() def display_examples(self): self.current_row = -1 examples = self.examples.get_examples() self.script_editor_page.update_example_listboxes( self.ui.example_filename_before, self.ui.example_filename_after, examples) def load(self): config = get_config() if IS_WIN: self.ui.windows_compatibility.setChecked(True) self.ui.windows_compatibility.setEnabled(False) else: self.ui.windows_compatibility.setChecked( config.setting["windows_compatibility"]) self.ui.rename_files.setChecked(config.setting["rename_files"]) self.ui.move_files.setChecked(config.setting["move_files"]) self.ui.ascii_filenames.setChecked(config.setting["ascii_filenames"]) self.script_text = config.setting["file_naming_format"] self.ui.move_files_to.setText(config.setting["move_files_to"]) self.ui.move_files_to.setCursorPosition(0) self.ui.move_additional_files.setChecked( config.setting["move_additional_files"]) self.ui.move_additional_files_pattern.setText( config.setting["move_additional_files_pattern"]) self.ui.delete_empty_dirs.setChecked( config.setting["delete_empty_dirs"]) self.script_editor_page.load() self.update_examples_from_local() def check(self): self.check_format() if self.ui.move_files.isChecked( ) and not self.ui.move_files_to.text().strip(): raise OptionsCheckError( _("Error"), _("The location to move files to must not be empty.")) def check_format(self): parser = ScriptParser() try: parser.eval(self.script_text) except Exception as e: raise ScriptCheckError("", str(e)) if self.ui.rename_files.isChecked(): if not self.script_text.strip(): raise ScriptCheckError( "", _("The file naming format must not be empty.")) def save(self): config = get_config() config.setting[ "windows_compatibility"] = self.ui.windows_compatibility.isChecked( ) config.setting["ascii_filenames"] = self.ui.ascii_filenames.isChecked() config.setting["rename_files"] = self.ui.rename_files.isChecked() config.setting["file_naming_format"] = self.script_text.strip() self.tagger.window.enable_renaming_action.setChecked( config.setting["rename_files"]) config.setting["move_files"] = self.ui.move_files.isChecked() config.setting["move_files_to"] = os.path.normpath( self.ui.move_files_to.text()) config.setting[ "move_additional_files"] = self.ui.move_additional_files.isChecked( ) config.setting[ "move_additional_files_pattern"] = self.ui.move_additional_files_pattern.text( ) config.setting[ "delete_empty_dirs"] = self.ui.delete_empty_dirs.isChecked() config.setting[ "file_naming_scripts"] = self.script_editor_page.naming_scripts config.setting[ "selected_file_naming_script_id"] = self.script_editor_page.selected_script_id self.tagger.window.enable_moving_action.setChecked( config.setting["move_files"]) def display_error(self, error): # Ignore scripting errors, those are handled inline if not isinstance(error, ScriptCheckError): super().display_error(error) def move_files_to_browse(self): path = QtWidgets.QFileDialog.getExistingDirectory( self, "", self.ui.move_files_to.text()) if path: path = os.path.normpath(path) self.ui.move_files_to.setText(path) def test(self): self.ui.renaming_error.setStyleSheet("") self.ui.renaming_error.setText("") try: self.check_format() except ScriptCheckError as e: self.ui.renaming_error.setStyleSheet(self.STYLESHEET_ERROR) self.ui.renaming_error.setText(e.info) return