Esempio n. 1
0
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()
Esempio n. 2
0
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)
Esempio n. 3
0
    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)
Esempio n. 4
0
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()
Esempio n. 5
0
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)
Esempio n. 6
0
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()
Esempio n. 7
0
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
Esempio n. 8
0
    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"], [])
Esempio n. 9
0
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()
Esempio n. 10
0
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)
Esempio n. 11
0
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)
Esempio n. 12
0
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()
Esempio n. 13
0
 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'])
Esempio n. 14
0
 def test_list_opt_convert(self):
     opt = ListOption("setting", "list_option", [])
     self.assertEqual(opt.convert("123"), ['1', '2', '3'])
Esempio n. 15
0
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)
Esempio n. 16
0
    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"], [])
Esempio n. 17
0
 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'])
Esempio n. 18
0
    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"])
Esempio n. 19
0
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
Esempio n. 20
0
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)
Esempio n. 21
0
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)
Esempio n. 22
0
 def test_list_opt_convert(self):
     opt = ListOption("setting", "list_option", [])
     self.assertEqual(opt.convert("123"), ['1', '2', '3'])
Esempio n. 23
0
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()
Esempio n. 24
0
    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"])
Esempio n. 25
0
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