def test_upgrade_to_v2_5_0_dev_2(self):
     Option("persist", "splitter_state", QByteArray())
     Option("persist", "bottom_splitter_state", QByteArray())
     self.config.persist["splitter_state"] = b'foo'
     self.config.persist["bottom_splitter_state"] = b'bar'
     upgrade_to_v2_5_0_dev_2(self.config)
     self.assertEqual(b'', self.config.persist['splitter_state'])
     self.assertEqual(b'', self.config.persist['bottom_splitter_state'])
Exemple #2
0
 def save(self):
     if not self.ui.enable_cleanup.checkState() == QtCore.Qt.CheckState.Checked:
         return
     to_remove = set(self.selected_options())
     if to_remove and QtWidgets.QMessageBox.question(
         self,
         _('Confirm Remove'),
         _("Are you sure you want to remove the selected option settings?"),
     ) == QtWidgets.QMessageBox.StandardButton.Yes:
         config = get_config()
         for item in to_remove:
             Option.add_if_missing('setting', item, None)
             log.warning("Removing option setting '%s' from the INI file.", item)
             config.setting.remove(item)
Exemple #3
0
def upgrade_to_v2_7_0_dev_3(config):
    """Save file naming scripts to dictionary.
    """
    from picard.script import get_file_naming_script_presets
    from picard.script.serializer import (
        FileNamingScript,
        ScriptImportError,
    )
    Option("setting", "file_renaming_scripts", {})
    ListOption("setting", "file_naming_scripts", [])
    TextOption("setting", "file_naming_format", DEFAULT_FILE_NAMING_FORMAT)
    TextOption("setting", "selected_file_naming_script_id", "")
    scripts = {}
    for item in config.setting["file_naming_scripts"]:
        try:
            script_item = FileNamingScript().create_from_yaml(
                item, create_new_id=False)
            scripts[script_item["id"]] = script_item.to_dict()
        except ScriptImportError:
            log.error("Error converting file naming script")
    script_list = set(scripts.keys()) | set(
        map(lambda item: item["id"], get_file_naming_script_presets()))
    if config.setting["selected_file_naming_script_id"] not in script_list:
        script_item = FileNamingScript(
            script=config.setting["file_naming_format"],
            title=_("Primary file naming script"),
            readonly=False,
            deletable=True,
        )
        scripts[script_item["id"]] = script_item.to_dict()
        config.setting["selected_file_naming_script_id"] = script_item["id"]
    config.setting["file_renaming_scripts"] = scripts
    config.setting.remove("file_naming_scripts")
    config.setting.remove("file_naming_format")
Exemple #4
0
class FileTreeView(BaseTreeView):

    header_state = Option("persist", "file_view_header_state",
                          QtCore.QByteArray())
    header_locked = BoolOption("persist", "file_view_header_locked", False)

    def __init__(self, window, parent=None):
        super().__init__(window, parent)
        self.setAccessibleName(_("file view"))
        self.setAccessibleDescription(
            _("Contains unmatched files and clusters"))
        self.unmatched_files = ClusterItem(self.tagger.unclustered_files,
                                           False, self)
        self.unmatched_files.update()
        self.unmatched_files.setExpanded(True)
        self.clusters = ClusterItem(self.tagger.clusters, False, self)
        self.set_clusters_text()
        self.clusters.setExpanded(True)
        self.tagger.cluster_added.connect(self.add_file_cluster)
        self.tagger.cluster_removed.connect(self.remove_file_cluster)

    def add_file_cluster(self, cluster, parent_item=None):
        self.add_cluster(cluster, parent_item)
        self.set_clusters_text()

    def remove_file_cluster(self, cluster):
        cluster.item.setSelected(False)
        self.clusters.removeChild(cluster.item)
        self.set_clusters_text()

    def set_clusters_text(self):
        self.clusters.setText(
            MainPanel.TITLE_COLUMN,
            '%s (%d)' % (_("Clusters"), len(self.tagger.clusters)))
Exemple #5
0
    def setUp(self):
        super().setUp()

        self.tmp_directory = self.mktmpdir()

        self.configpath = os.path.join(self.tmp_directory, 'test.ini')
        shutil.copy(os.path.join('test', 'data', 'test.ini'), self.configpath)
        self.addCleanup(os.remove, self.configpath)

        self.config = Config.from_file(None, self.configpath)
        self.addCleanup(self.cleanup_config_obj)

        self.config.application["version"] = "testing"
        logging.disable(logging.ERROR)
        Option.registry = {}

        ListOption('profiles', self.PROFILES_KEY, [])
        Option('profiles', self.SETTINGS_KEY, {})

        # Get valid profile option settings for testing
        option_settings = list(UserProfileGroups.get_all_settings_list())
        self.test_setting_0 = option_settings[0]
        self.test_setting_1 = option_settings[1]
        self.test_setting_2 = option_settings[2]
        self.test_setting_3 = option_settings[3]

        TextOption("setting", self.test_setting_0, "abc")
        BoolOption("setting", self.test_setting_1, True)
        IntOption("setting", self.test_setting_2, 42)
        TextOption("setting", self.test_setting_3, "xyz")
Exemple #6
0
class AlbumTreeView(BaseTreeView):

    header_state = Option("persist", "album_view_header_state",
                          QtCore.QByteArray())
    header_locked = BoolOption("persist", "album_view_header_locked", False)

    def __init__(self, window, parent=None):
        super().__init__(window, parent)
        self.setAccessibleName(_("album view"))
        self.setAccessibleDescription(_("Contains albums and matched files"))
        self.tagger.album_added.connect(self.add_album)
        self.tagger.album_removed.connect(self.remove_album)

    def add_album(self, album):
        if isinstance(album, NatAlbum):
            item = NatAlbumItem(album, True)
            self.insertTopLevelItem(0, item)
        else:
            item = AlbumItem(album, True, self)
        item.setIcon(MainPanel.TITLE_COLUMN, AlbumItem.icon_cd)
        for i, column in enumerate(MainPanel.columns):
            font = item.font(i)
            font.setBold(True)
            item.setFont(i, font)
            item.setText(i, album.column(column[1]))
        self.add_cluster(album.unmatched_files, item)

    def remove_album(self, album):
        album.item.setSelected(False)
        self.takeTopLevelItem(self.indexOfTopLevelItem(album.item))
Exemple #7
0
    def test_var_opt_set_read_back(self):
        Option("setting", "var_option", set(["a", "b"]))

        # set option to "def", and read back
        self.config.setting["var_option"] = set(["c", "d"])
        self.assertEqual(self.config.setting["var_option"], set(["c", "d"]))
        self.assertIs(type(self.config.setting["var_option"]), set)
Exemple #8
0
 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)
Exemple #9
0
 def upgrade_persisted_splitter(new_persist_key, key_map):
     _p = config.persist
     splitter_dict = {}
     for (old_splitter_key, new_splitter_key) in key_map:
         if _p.__contains__(old_splitter_key):
             if _p[old_splitter_key] is not None:
                 splitter_dict[new_splitter_key] = bytearray(_p[old_splitter_key])
             _p.remove(old_splitter_key)
     Option("persist", new_persist_key, {})
     _p[new_persist_key] = splitter_dict
Exemple #10
0
 def test_var_opt_convert(self):
     opt = Option("setting", "var_option", set())
     self.assertEqual(opt.convert(["a", "b", "a"]), {"a", "b"})
Exemple #11
0
class MetadataBox(QtWidgets.QTableWidget):

    options = (
        Option("persist", "metadatabox_header_state", QtCore.QByteArray()),
        BoolOption("persist", "show_changes_first", False)
    )

    COLUMN_ORIG = 1
    COLUMN_NEW = 2

    def __init__(self, parent):
        super().__init__(parent)
        config = get_config()
        self.parent = parent
        self.setAccessibleName(_("metadata view"))
        self.setAccessibleDescription(_("Displays original and new tags for the selected files"))
        self.setColumnCount(3)
        self.setHorizontalHeaderLabels((_("Tag"), _("Original Value"), _("New Value")))
        self.horizontalHeader().setStretchLastSection(True)
        self.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
        self.horizontalHeader().setSectionsClickable(False)
        self.verticalHeader().setDefaultSectionSize(21)
        self.verticalHeader().setVisible(False)
        self.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
        self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
        self.setTabKeyNavigation(False)
        self.setStyleSheet("QTableWidget {border: none;}")
        self.setAttribute(QtCore.Qt.WA_MacShowFocusRect, 1)
        self.setItemDelegate(TableTagEditorDelegate(self))
        self.setWordWrap(False)
        self.files = set()
        self.tracks = set()
        self.objects = set()
        self.selection_mutex = QtCore.QMutex()
        self.selection_dirty = False
        self.editing = None  # the QTableWidgetItem being edited
        self.clipboard = [""]
        self.add_tag_action = QtWidgets.QAction(_("Add New Tag..."), parent)
        self.add_tag_action.triggered.connect(partial(self.edit_tag, ""))
        self.changes_first_action = QtWidgets.QAction(_("Show Changes First"), parent)
        self.changes_first_action.setCheckable(True)
        self.changes_first_action.setChecked(config.persist["show_changes_first"])
        self.changes_first_action.toggled.connect(self.toggle_changes_first)
        self.browser_integration = BrowserIntegration()
        # TR: Keyboard shortcut for "Add New Tag..."
        self.add_tag_shortcut = QtWidgets.QShortcut(QtGui.QKeySequence(_("Alt+Shift+A")), self, partial(self.edit_tag, ""))
        self.add_tag_action.setShortcut(self.add_tag_shortcut.key())
        # TR: Keyboard shortcut for "Edit..." (tag)
        self.edit_tag_shortcut = QtWidgets.QShortcut(QtGui.QKeySequence(_("Alt+Shift+E")), self, partial(self.edit_selected_tag))
        # TR: Keyboard shortcut for "Remove" (tag)
        self.remove_tag_shortcut = QtWidgets.QShortcut(QtGui.QKeySequence(_("Alt+Shift+R")), self, self.remove_selected_tags)
        self.preserved_tags = PreservedTags()
        self._single_file_album = False
        self._single_track_album = False
        self.tagger.clipboard().dataChanged.connect(self.update_clipboard)

    def get_file_lookup(self):
        """Return a FileLookup object."""
        config = get_config()
        return FileLookup(self, config.setting["server_host"],
                          config.setting["server_port"],
                          self.browser_integration.port)

    def lookup_tags(self):
        lookup = self.get_file_lookup()
        LOOKUP_TAGS = {
            "musicbrainz_recordingid": lookup.recording_lookup,
            "musicbrainz_trackid": lookup.track_lookup,
            "musicbrainz_albumid": lookup.album_lookup,
            "musicbrainz_workid": lookup.work_lookup,
            "musicbrainz_artistid": lookup.artist_lookup,
            "musicbrainz_albumartistid": lookup.artist_lookup,
            "musicbrainz_releasegroupid": lookup.release_group_lookup,
            "musicbrainz_discid": lookup.discid_lookup,
            "acoustid_id": lookup.acoust_lookup
        }
        return LOOKUP_TAGS

    def open_link(self, values, tag):
        lookup = self.lookup_tags()
        lookup_func = lookup[tag]
        for v in values:
            lookup_func(v)

    def edit(self, index, trigger, event):
        if index.column() != self.COLUMN_NEW:
            return False
        item = self.itemFromIndex(index)
        if item.flags() & QtCore.Qt.ItemIsEditable and \
           trigger in (QtWidgets.QAbstractItemView.DoubleClicked,
                       QtWidgets.QAbstractItemView.EditKeyPressed,
                       QtWidgets.QAbstractItemView.AnyKeyPressed):
            tag = self.tag_diff.tag_names[item.row()]
            values = self.tag_diff.new[tag]
            if len(values) > 1:
                self.edit_tag(tag)
                return False
            else:
                self.editing = item
                item.setText(values[0])
                return super().edit(index, trigger, event)
        return False

    def keyPressEvent(self, event):
        if event.matches(QtGui.QKeySequence.Copy):
            self.copy_value()
        elif event.matches(QtGui.QKeySequence.Paste):
            self.paste_value()
        else:
            super().keyPressEvent(event)

    def copy_value(self):
        item = self.currentItem()
        if item:
            column = item.column()
            tag = self.tag_diff.tag_names[item.row()]
            value = None
            if column == self.COLUMN_ORIG:
                value = self.tag_diff.orig[tag]
            elif column == self.COLUMN_NEW:
                value = self.tag_diff.new[tag]
            if tag == '~length':
                value = [format_time(value or 0), ]
            if value is not None:
                self.tagger.clipboard().setText(MULTI_VALUED_JOINER.join(value))
                self.clipboard = value

    def paste_value(self):
        item = self.currentItem()
        if item:
            column = item.column()
            tag = self.tag_diff.tag_names[item.row()]
            if column == self.COLUMN_NEW and self.tag_is_editable(tag):
                self.set_tag_values(tag, self.clipboard)
                self.update()

    def update_clipboard(self):
        clipboard = self.tagger.clipboard().text().split(MULTI_VALUED_JOINER)
        if clipboard:
            self.clipboard = clipboard

    def closeEditor(self, editor, hint):
        super().closeEditor(editor, hint)
        tag = self.tag_diff.tag_names[self.editing.row()]
        old = self.tag_diff.new[tag]
        new = [self._get_editor_value(editor)]
        if old == new:
            self.editing.setText(old[0])
        else:
            self.set_tag_values(tag, new)
        self.editing = None
        self.update(drop_album_caches=tag == 'album')

    @staticmethod
    def _get_editor_value(editor):
        if hasattr(editor, 'text'):
            return editor.text()
        elif hasattr(editor, 'toPlainText'):
            return editor.toPlainText()
        return ''

    def contextMenuEvent(self, event):
        menu = QtWidgets.QMenu(self)
        if self.objects:
            tags = self.selected_tags()
            single_tag = len(tags) == 1
            if single_tag:
                selected_tag = tags[0]
                editable = self.tag_is_editable(selected_tag)
                edit_tag_action = QtWidgets.QAction(_("Edit..."), self.parent)
                edit_tag_action.triggered.connect(partial(self.edit_tag, selected_tag))
                edit_tag_action.setShortcut(self.edit_tag_shortcut.key())
                edit_tag_action.setEnabled(editable)
                menu.addAction(edit_tag_action)
                if selected_tag not in self.preserved_tags:
                    add_to_preserved_tags_action = QtWidgets.QAction(_("Add to 'Preserve Tags' List"), self.parent)
                    add_to_preserved_tags_action.triggered.connect(partial(self.preserved_tags.add, selected_tag))
                    add_to_preserved_tags_action.setEnabled(editable)
                    menu.addAction(add_to_preserved_tags_action)
                else:
                    remove_from_preserved_tags_action = QtWidgets.QAction(_("Remove from 'Preserve Tags' List"), self.parent)
                    remove_from_preserved_tags_action.triggered.connect(partial(self.preserved_tags.discard, selected_tag))
                    remove_from_preserved_tags_action.setEnabled(editable)
                    menu.addAction(remove_from_preserved_tags_action)
            removals = []
            useorigs = []
            item = self.currentItem()
            if item:
                column = item.column()
                for tag in tags:
                    if tag in self.lookup_tags().keys():
                        if (column == self.COLUMN_ORIG or column == self.COLUMN_NEW) and single_tag and item.text():
                            if column == self.COLUMN_ORIG:
                                values = self.tag_diff.orig[tag]
                            else:
                                values = self.tag_diff.new[tag]
                            lookup_action = QtWidgets.QAction(_("Lookup in &Browser"), self.parent)
                            lookup_action.triggered.connect(partial(self.open_link, values, tag))
                            menu.addAction(lookup_action)
                    if self.tag_is_removable(tag):
                        removals.append(partial(self.remove_tag, tag))
                    status = self.tag_diff.status[tag] & TagStatus.CHANGED
                    if status == TagStatus.CHANGED or status == TagStatus.REMOVED:
                        file_tracks = []
                        track_albums = set()
                        for file in self.files:
                            objects = [file]
                            if file.parent in self.tracks and len(self.files & set(file.parent.files)) == 1:
                                objects.append(file.parent)
                                file_tracks.append(file.parent)
                                track_albums.add(file.parent.album)
                            orig_values = list(file.orig_metadata.getall(tag)) or [""]
                            useorigs.append(partial(self.set_tag_values, tag, orig_values, objects))
                        for track in set(self.tracks)-set(file_tracks):
                            objects = [track]
                            orig_values = list(track.orig_metadata.getall(tag)) or [""]
                            useorigs.append(partial(self.set_tag_values, tag, orig_values, objects))
                            track_albums.add(track.album)
                        for album in track_albums:
                            objects = [album]
                            orig_values = list(album.orig_metadata.getall(tag)) or [""]
                            useorigs.append(partial(self.set_tag_values, tag, orig_values, objects))
                remove_tag_action = QtWidgets.QAction(_("Remove"), self.parent)
                remove_tag_action.triggered.connect(partial(self._apply_update_funcs, removals))
                remove_tag_action.setShortcut(self.remove_tag_shortcut.key())
                remove_tag_action.setEnabled(bool(removals))
                menu.addAction(remove_tag_action)
                if useorigs:
                    name = ngettext("Use Original Value", "Use Original Values", len(useorigs))
                    use_orig_value_action = QtWidgets.QAction(name, self.parent)
                    use_orig_value_action.triggered.connect(partial(self._apply_update_funcs, useorigs))
                    menu.addAction(use_orig_value_action)
                    menu.addSeparator()
                if single_tag:
                    menu.addSeparator()
                    copy_action = QtWidgets.QAction(icontheme.lookup('edit-copy', icontheme.ICON_SIZE_MENU), _("&Copy"), self)
                    copy_action.triggered.connect(self.copy_value)
                    copy_action.setShortcut(QtGui.QKeySequence.Copy)
                    menu.addAction(copy_action)
                    paste_action = QtWidgets.QAction(icontheme.lookup('edit-paste', icontheme.ICON_SIZE_MENU), _("&Paste"), self)
                    paste_action.triggered.connect(self.paste_value)
                    paste_action.setShortcut(QtGui.QKeySequence.Paste)
                    paste_action.setEnabled(editable)
                    menu.addAction(paste_action)
            if single_tag or removals or useorigs:
                menu.addSeparator()
            menu.addAction(self.add_tag_action)
            menu.addSeparator()
        menu.addAction(self.changes_first_action)
        menu.exec_(event.globalPos())
        event.accept()

    def _apply_update_funcs(self, funcs):
        for f in funcs:
            f()
        self.parent.update_selection(new_selection=False, drop_album_caches=True)

    def edit_tag(self, tag):
        EditTagDialog(self.parent, tag).exec_()

    def edit_selected_tag(self):
        tags = self.selected_tags(filter_func=self.tag_is_editable)
        if len(tags) == 1:
            self.edit_tag(tags[0])

    def toggle_changes_first(self, checked):
        config = get_config()
        config.persist["show_changes_first"] = checked
        self.update()

    def set_tag_values(self, tag, values, objects=None):
        if objects is None:
            objects = self.objects
        with self.parent.ignore_selection_changes:
            if values == [""]:
                values = []
            if not values and self.tag_is_removable(tag):
                for obj in objects:
                    del obj.metadata[tag]
                    obj.update()
            elif values:
                for obj in objects:
                    obj.metadata[tag] = values
                    obj.update()

    def remove_tag(self, tag):
        self.set_tag_values(tag, [])

    def remove_selected_tags(self):
        for tag in self.selected_tags(filter_func=self.tag_is_removable):
            if self.tag_is_removable(tag):
                self.remove_tag(tag)
        self.parent.update_selection(new_selection=False, drop_album_caches=True)

    def tag_is_removable(self, tag):
        return self.tag_diff.status[tag] & TagStatus.NOTREMOVABLE == 0

    def tag_is_editable(self, tag):
        return self.tag_diff.status[tag] & TagStatus.READONLY == 0

    def selected_tags(self, filter_func=None):
        tags = set(self.tag_diff.tag_names[item.row()]
                   for item in self.selectedItems())
        if filter_func:
            tags = filter(filter_func, tags)
        return list(tags)

    def _update_selection(self):
        files = set()
        tracks = set()
        objects = set()
        for obj in self.parent.selected_objects:
            if isinstance(obj, File):
                files.add(obj)
            elif isinstance(obj, Track):
                tracks.add(obj)
                files.update(obj.files)
            elif isinstance(obj, Cluster) and obj.can_edit_tags():
                objects.add(obj)
                files.update(obj.files)
            elif isinstance(obj, Album):
                objects.add(obj)
                tracks.update(obj.tracks)
                for track in obj.tracks:
                    files.update(track.files)
        objects.update(files)
        objects.update(tracks)
        self.selection_dirty = False

        self.selection_mutex.lock()
        self.files = files
        self.tracks = tracks
        self.objects = objects
        self.selection_mutex.unlock()

    @throttle(100)
    def update(self, drop_album_caches=False):
        if self.editing:
            return
        new_selection = self.selection_dirty
        if self.selection_dirty:
            self._update_selection()
        thread.run_task(partial(self._update_tags, new_selection, drop_album_caches), self._update_items,
            thread_pool=self.tagger.priority_thread_pool)

    def _update_tags(self, new_selection=True, drop_album_caches=False):
        self.selection_mutex.lock()
        files = self.files
        tracks = self.tracks
        self.selection_mutex.unlock()

        if not (files or tracks):
            return None

        if new_selection or drop_album_caches:
            self._single_file_album = len(set([file.metadata["album"] for file in files])) == 1
            self._single_track_album = len(set([track.metadata["album"] for track in tracks])) == 1

        while not new_selection:  # Just an if with multiple exit points
            # If we are dealing with the same selection
            # skip updates unless it we are dealing with a single file/track
            if len(files) == 1:
                break
            if len(tracks) == 1:
                break
            # Or if we are dealing with a single cluster/album
            if self._single_file_album:
                break
            if self._single_track_album:
                break
            return self.tag_diff

        self.colors = {
            TagStatus.NOCHANGE: self.palette().color(QtGui.QPalette.Text),
            TagStatus.REMOVED: QtGui.QBrush(interface_colors.get_qcolor('tagstatus_removed')),
            TagStatus.ADDED: QtGui.QBrush(interface_colors.get_qcolor('tagstatus_added')),
            TagStatus.CHANGED: QtGui.QBrush(interface_colors.get_qcolor('tagstatus_changed'))
        }

        config = get_config()
        tag_diff = TagDiff(max_length_diff=config.setting["ignore_track_duration_difference_under"])
        orig_tags = tag_diff.orig
        new_tags = tag_diff.new
        tag_diff.objects = len(files)

        clear_existing_tags = config.setting["clear_existing_tags"]
        top_tags = config.setting['metadatabox_top_tags']
        top_tags_set = set(top_tags)

        for file in files:
            new_metadata = file.new_metadata
            orig_metadata = file.orig_metadata
            tags = set(list(new_metadata.keys()) + list(orig_metadata.keys()))

            for name in filter(lambda x: not x.startswith("~") and file.supports_tag(x), tags):
                new_values = new_metadata.getall(name)
                orig_values = orig_metadata.getall(name)

                if not clear_existing_tags and not new_values:
                    new_values = list(orig_values or [""])

                removed = name in new_metadata.deleted_tags
                tag_diff.add(name, orig_values, new_values, True, removed, top_tags=top_tags_set)

            tag_diff.add("~length", str(orig_metadata.length), str(new_metadata.length),
                         removable=False, readonly=True)

        for track in tracks:
            if track.num_linked_files == 0:
                for name, new_values in track.metadata.rawitems():
                    if not name.startswith("~"):
                        if name in track.orig_metadata:
                            orig_values = track.orig_metadata.getall(name)
                        else:
                            orig_values = new_values
                        tag_diff.add(name, orig_values, new_values, True)

                length = str(track.metadata.length)
                tag_diff.add("~length", length, length, removable=False, readonly=True)

                tag_diff.objects += 1

        all_tags = set(list(orig_tags.keys()) + list(new_tags.keys()))
        common_tags = [tag for tag in top_tags if tag in all_tags]
        tag_names = common_tags + sorted(all_tags.difference(common_tags),
                                         key=lambda x: display_tag_name(x).lower())

        if config.persist["show_changes_first"]:
            tags_by_status = {}

            for tag in tag_names:
                tags_by_status.setdefault(tag_diff.tag_status(tag), []).append(tag)

            for status in (TagStatus.CHANGED, TagStatus.ADDED,
                           TagStatus.REMOVED, TagStatus.NOCHANGE):
                tag_diff.tag_names += tags_by_status.pop(status, [])
        else:
            tag_diff.tag_names = [
                tag for tag in tag_names if
                tag_diff.status[tag] != TagStatus.EMPTY]

        return tag_diff

    def _update_items(self, result=None, error=None):
        if self.editing:
            return

        if not (self.files or self.tracks):
            result = None

        self.tag_diff = result

        if result is None:
            self.setRowCount(0)
            return

        self.setRowCount(len(result.tag_names))

        orig_flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
        new_flags = orig_flags | QtCore.Qt.ItemIsEditable

        for i, name in enumerate(result.tag_names):
            tag_item = self.item(i, 0)
            orig_item = self.item(i, 1)
            new_item = self.item(i, 2)
            if not tag_item:
                tag_item = QtWidgets.QTableWidgetItem()
                tag_item.setFlags(orig_flags)
                font = tag_item.font()
                font.setBold(True)
                tag_item.setFont(font)
                self.setItem(i, 0, tag_item)
            if not orig_item:
                orig_item = QtWidgets.QTableWidgetItem()
                orig_item.setFlags(orig_flags)
                self.setItem(i, 1, orig_item)
            if not new_item:
                new_item = QtWidgets.QTableWidgetItem()
                self.setItem(i, 2, new_item)
            tag_item.setText(display_tag_name(name))
            self.set_item_value(orig_item, self.tag_diff.orig, name)
            if name == "~length":
                new_item.setFlags(orig_flags)
            else:
                new_item.setFlags(new_flags)
            self.set_item_value(new_item, self.tag_diff.new, name)

            font = new_item.font()
            if result.tag_status(name) == TagStatus.REMOVED:
                font.setStrikeOut(True)
            else:
                font.setStrikeOut(False)

            new_item.setFont(font)

            color = self.colors.get(result.tag_status(name),
                                    self.colors[TagStatus.NOCHANGE])
            orig_item.setForeground(color)
            new_item.setForeground(color)

            alignment = QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop
            tag_item.setTextAlignment(alignment)
            orig_item.setTextAlignment(alignment)
            new_item.setTextAlignment(alignment)

            # Adjust row height to content size
            self.setRowHeight(i, self.sizeHintForRow(i))

    def set_item_value(self, item, tags, name):
        text, italic = tags.display_value(name)
        item.setData(QtCore.Qt.UserRole, name)
        item.setText(text)
        font = item.font()
        font.setItalic(italic)
        item.setFont(font)

    @restore_method
    def restore_state(self):
        config = get_config()
        state = config.persist["metadatabox_header_state"]
        header = self.horizontalHeader()
        header.restoreState(state)
        header.setSectionResizeMode(QtWidgets.QHeaderView.Interactive)

    def save_state(self):
        config = get_config()
        header = self.horizontalHeader()
        state = header.saveState()
        config.persist["metadatabox_header_state"] = state
Exemple #12
0
 def test_var_opt_convert(self):
     opt = Option("setting", "var_option", set())
     self.assertEqual(opt.convert(["a", "b", "a"]), {"a", "b"})
Exemple #13
0
class InterfaceColorsOptionsPage(OptionsPage):

    NAME = "interface_colors"
    TITLE = N_("Colors")
    PARENT = "interface"
    SORT_ORDER = 30
    ACTIVE = True
    HELP_URL = '/config/options_interface_colors.html'

    options = [
        Option("setting", "interface_colors",
               InterfaceColors(dark_theme=False).get_colors()),
        Option("setting", "interface_colors_dark",
               InterfaceColors(dark_theme=True).get_colors()),
    ]

    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_InterfaceColorsOptionsPage()
        self.ui.setupUi(self)
        self.new_colors = {}
        self.colors_list = QtWidgets.QVBoxLayout()
        self.ui.colors.setLayout(self.colors_list)

    def update_color_selectors(self):
        if self.colors_list:
            delete_items_of_layout(self.colors_list)

        def color_changed(color_key, color_value):
            interface_colors.set_color(color_key, color_value)

        for color_key, color_value in interface_colors.get_colors().items():
            widget = QtWidgets.QWidget()

            hlayout = QtWidgets.QHBoxLayout()
            hlayout.setContentsMargins(0, 0, 0, 0)

            label = QtWidgets.QLabel(
                interface_colors.get_color_description(color_key))
            label.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
                                QtWidgets.QSizePolicy.Minimum)
            hlayout.addWidget(label)

            button = ColorButton(color_value)
            button.color_changed.connect(partial(color_changed, color_key))
            hlayout.addWidget(button, 0, QtCore.Qt.AlignRight)

            widget.setLayout(hlayout)
            self.colors_list.addWidget(widget)

        spacerItem1 = QtWidgets.QSpacerItem(20, 40,
                                            QtWidgets.QSizePolicy.Minimum,
                                            QtWidgets.QSizePolicy.Expanding)
        self.colors_list.addItem(spacerItem1)

    def load(self):
        interface_colors.load_from_config()
        self.update_color_selectors()

    def save(self):
        if interface_colors.save_to_config():
            dialog = QtWidgets.QMessageBox(
                QtWidgets.QMessageBox.Information, _('Colors changed'),
                _('You have changed the interface colors. You may have to restart Picard in order for the changes to take effect.'
                  ), QtWidgets.QMessageBox.Ok, self)
            dialog.exec_()

    def restore_defaults(self):
        interface_colors.set_default_colors()
        self.update_color_selectors()
Exemple #14
0
 def __init__(self):
     Option("persist", self.opt_name(), QtCore.QByteArray())
     Option("persist", self.splitters_name(), {})
     if getattr(self, 'finished', None):
         self.finished.connect(self.save_geometry)
Exemple #15
0
class MainWindow(QtGui.QMainWindow):

    options = [
        Option("persist", "window_state", QtCore.QByteArray(),
               QtCore.QVariant.toByteArray),
        Option("persist", "window_position", QtCore.QPoint(),
               QtCore.QVariant.toPoint),
        Option("persist", "window_size", QtCore.QSize(780, 560),
               QtCore.QVariant.toSize),
        BoolOption("persist", "window_maximized", False),
        BoolOption("persist", "view_cover_art", False),
        BoolOption("persist", "view_file_browser", False),
        TextOption("persist", "current_directory", ""),
    ]

    def __init__(self, parent=None):
        QtGui.QMainWindow.__init__(self, parent)
        self.selected_objects = []
        self.tagger.selected_metadata_changed.connect(self.updateSelection)
        self.setupUi()

    def setupUi(self):
        self.setWindowTitle(_("MusicBrainz Picard"))
        icon = QtGui.QIcon()
        icon.addFile(":/images/16x16/picard.png", QtCore.QSize(16, 16))
        icon.addFile(":/images/24x24/picard.png", QtCore.QSize(24, 24))
        icon.addFile(":/images/32x32/picard.png", QtCore.QSize(32, 32))
        icon.addFile(":/images/48x48/picard.png", QtCore.QSize(48, 48))
        icon.addFile(":/images/128x128/picard.png", QtCore.QSize(128, 128))
        icon.addFile(":/images/256x256/picard.png", QtCore.QSize(256, 256))
        self.setWindowIcon(icon)

        self.create_actions()
        self.create_statusbar()
        self.create_toolbar()
        self.create_menus()

        centralWidget = QtGui.QWidget(self)
        self.setCentralWidget(centralWidget)

        self.panel = MainPanel(self, centralWidget)
        self.file_browser = FileBrowser(self.panel)
        if not self.show_file_browser_action.isChecked():
            self.file_browser.hide()
        self.panel.insertWidget(0, self.file_browser)
        self.panel.restore_state()

        self.orig_metadata_box = MetadataBox(self, _("Original Metadata"), True)
        self.orig_metadata_box.disable()
        self.metadata_box = MetadataBox(self, _("New Metadata"), False)
        self.metadata_box.disable()

        self.cover_art_box = CoverArtBox(self)
        if not self.show_cover_art_action.isChecked():
            self.cover_art_box.hide()

        bottomLayout = QtGui.QHBoxLayout()
        bottomLayout.addWidget(self.orig_metadata_box, 1)
        bottomLayout.addWidget(self.metadata_box, 1)
        bottomLayout.addWidget(self.cover_art_box, 0)

        mainLayout = QtGui.QVBoxLayout()
        mainLayout.addWidget(self.panel, 1)
        mainLayout.addLayout(bottomLayout, 0)

        centralWidget.setLayout(mainLayout)

        # FIXME: use QApplication's clipboard
        self._clipboard = []

        for function in ui_init:
            function(self)

        self.restoreWindowState()

    def closeEvent(self, event):
        if self.config.setting["quit_confirmation"] and not self.show_quit_confirmation():
            event.ignore()
            return
        self.saveWindowState()
        event.accept()

    def show_quit_confirmation(self):
        unsaved_files = sum(a.get_num_unsaved_files() for a in self.tagger.albums.itervalues())
        QMessageBox = QtGui.QMessageBox

        if unsaved_files > 0:
            msg = QMessageBox(self)
            msg.setIcon(QMessageBox.Question)
            msg.setWindowModality(QtCore.Qt.WindowModal)
            msg.setWindowTitle(_(u"Unsaved Changes"))
            msg.setText(_(u"Are you sure you want to quit Picard?"))
            txt = ungettext(
                "There is %d unsaved file. Closing Picard will lose all unsaved changes.",
                "There are %d unsaved files. Closing Picard will lose all unsaved changes.",
                unsaved_files) % unsaved_files
            msg.setInformativeText(txt)
            cancel = msg.addButton(QMessageBox.Cancel)
            msg.setDefaultButton(cancel)
            msg.addButton(_(u"&Quit Picard"), QMessageBox.YesRole)
            ret = msg.exec_()

            if ret == QMessageBox.Cancel:
                return False

        return True

    def saveWindowState(self):
        self.config.persist["window_state"] = self.saveState()
        isMaximized = int(self.windowState()) & QtCore.Qt.WindowMaximized != 0
        if isMaximized:
            # FIXME: this doesn't include the window frame
            geom = self.normalGeometry()
            self.config.persist["window_position"] = geom.topLeft()
            self.config.persist["window_size"] = geom.size()
        else:
            pos = self.pos()
            if not pos.isNull():
                self.config.persist["window_position"] = pos
            self.config.persist["window_size"] = self.size()
        self.config.persist["window_maximized"] = isMaximized
        self.config.persist["view_cover_art"] = self.show_cover_art_action.isChecked()
        self.config.persist["view_file_browser"] = self.show_file_browser_action.isChecked()
        self.file_browser.save_state()
        self.panel.save_state()

    def restoreWindowState(self):
        self.restoreState(self.config.persist["window_state"])
        pos = self.config.persist["window_position"]
        size = self.config.persist["window_size"]
        self._desktopgeo = self.tagger.desktop().screenGeometry()
        if pos.x() > 0 and pos.y() > 0 and pos.x()+size.width() < self._desktopgeo.width() and pos.y()+size.height() < self._desktopgeo.height():
            self.move(pos)
        if size.width() <= 0 or size.height() <= 0:
            size = QtCore.QSize(780, 560)
        self.resize(size)
        if self.config.persist["window_maximized"]:
            self.setWindowState(QtCore.Qt.WindowMaximized)
        self.file_browser.restore_state()

    def create_statusbar(self):
        """Creates a new status bar."""
        self.statusBar().showMessage("Ready")
        self.file_counts_label = QtGui.QLabel()
        self.statusBar().addPermanentWidget(self.file_counts_label)
        self.connect(self.tagger, QtCore.SIGNAL("file_state_changed"), self.update_statusbar)
        self.update_statusbar(0)

    def update_statusbar(self, num_pending_files):
        """Updates the status bar information."""
        self.file_counts_label.setText(_(" Files: %(files)d, Pending Files: %(pending)d ")
            % {"files": self.tagger.num_files(), "pending": num_pending_files})

    def set_statusbar_message(self, message, *args, **kwargs):
        """Set the status bar message."""
        try:
            if message:
                self.log.debug(repr(message.replace('%%s', '%%r')), *args)
        except:
            pass
        self.tagger.thread_pool.call_from_thread(
            self._set_statusbar_message, message, *args, **kwargs)

    def _set_statusbar_message(self, message, *args, **kwargs):
        if message:
            if args:
                message = _(message) % args
            else:
                message = _(message)
        self.statusBar().showMessage(message, kwargs.get('timeout', 0))

    def clear_statusbar_message(self):
        """Set the status bar message."""
        self.statusBar().clearMessage()

    def _on_submit(self):
        if self.tagger.use_acoustid:
            if not self.config.setting["acoustid_apikey"]:
                QtGui.QMessageBox.warning(self,
                    _(u"Submission Error"),
                    _(u"You need to configure your AcoustID API key before you can submit fingerprints."))
            else:
                self.tagger.acoustidmanager.submit()
        else:
            self.tagger.puidmanager.submit()

    def create_actions(self):

        self.options_action = QtGui.QAction(icontheme.lookup('preferences-desktop'), _("&Options..."), self)
        self.connect(self.options_action, QtCore.SIGNAL("triggered()"), self.show_options)

        self.cut_action = QtGui.QAction(icontheme.lookup('edit-cut', icontheme.ICON_SIZE_MENU), _(u"&Cut"), self)
        self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
        self.cut_action.setEnabled(False)
        self.connect(self.cut_action, QtCore.SIGNAL("triggered()"), self.cut)

        self.paste_action = QtGui.QAction(icontheme.lookup('edit-paste', icontheme.ICON_SIZE_MENU), _(u"&Paste"), self)
        self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
        self.paste_action.setEnabled(False)
        self.connect(self.paste_action, QtCore.SIGNAL("triggered()"), self.paste)

        self.help_action = QtGui.QAction(_("&Help..."), self)

        self.help_action.setShortcut(QtGui.QKeySequence.HelpContents)
        self.connect(self.help_action, QtCore.SIGNAL("triggered()"), self.show_help)

        self.about_action = QtGui.QAction(_("&About..."), self)
        self.connect(self.about_action, QtCore.SIGNAL("triggered()"), self.show_about)

        self.donate_action = QtGui.QAction(_("&Donate..."), self)
        self.connect(self.donate_action, QtCore.SIGNAL("triggered()"), self.open_donation_page)

        self.report_bug_action = QtGui.QAction(_("&Report a Bug..."), self)
        self.connect(self.report_bug_action, QtCore.SIGNAL("triggered()"), self.open_bug_report)

        self.support_forum_action = QtGui.QAction(_("&Support Forum..."), self)
        self.connect(self.support_forum_action, QtCore.SIGNAL("triggered()"), self.open_support_forum)

        self.add_files_action = QtGui.QAction(icontheme.lookup('document-open'), _(u"&Add Files..."), self)
        self.add_files_action.setStatusTip(_(u"Add files to the tagger"))
        # TR: Keyboard shortcut for "Add Files..."
        self.add_files_action.setShortcut(QtGui.QKeySequence.Open)
        self.connect(self.add_files_action, QtCore.SIGNAL("triggered()"), self.add_files)

        self.add_directory_action = QtGui.QAction(icontheme.lookup('folder'), _(u"A&dd Folder..."), self)
        self.add_directory_action.setStatusTip(_(u"Add a folder to the tagger"))
        # TR: Keyboard shortcut for "Add Directory..."
        self.add_directory_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+D")))
        self.connect(self.add_directory_action, QtCore.SIGNAL("triggered()"),
                     self.add_directory)

        self.save_action = QtGui.QAction(icontheme.lookup('document-save'), _(u"&Save"), self)
        self.save_action.setStatusTip(_(u"Save selected files"))
        # TR: Keyboard shortcut for "Save"
        self.save_action.setShortcut(QtGui.QKeySequence.Save)
        self.save_action.setEnabled(False)
        self.connect(self.save_action, QtCore.SIGNAL("triggered()"), self.save)

        self.submit_action = QtGui.QAction(icontheme.lookup('picard-submit'), _(u"S&ubmit"), self)
        self.submit_action.setStatusTip(_(u"Submit fingerprints"))
        self.submit_action.setEnabled(False)
        self.connect(self.submit_action, QtCore.SIGNAL("triggered()"), self._on_submit)

        self.exit_action = QtGui.QAction(_(u"E&xit"), self)
        # TR: Keyboard shortcut for "Exit"
        self.exit_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+Q")))
        self.connect(self.exit_action, QtCore.SIGNAL("triggered()"), self.close)

        self.remove_action = QtGui.QAction(icontheme.lookup('list-remove'), _(u"&Remove"), self)
        self.remove_action.setStatusTip(_(u"Remove selected files/albums"))
        self.remove_action.setShortcut(QtGui.QKeySequence.Delete)
        self.remove_action.setEnabled(False)
        self.connect(self.remove_action, QtCore.SIGNAL("triggered()"), self.remove)

        self.show_file_browser_action = QtGui.QAction(_(u"File &Browser"), self)
        self.show_file_browser_action.setCheckable(True)
        if self.config.persist["view_file_browser"]:
            self.show_file_browser_action.setChecked(True)
        self.show_file_browser_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+B")))
        self.connect(self.show_file_browser_action, QtCore.SIGNAL("triggered()"), self.show_file_browser)

        self.show_cover_art_action = QtGui.QAction(_(u"&Cover Art"), self)
        self.show_cover_art_action.setCheckable(True)
        if self.config.persist["view_cover_art"]:
            self.show_cover_art_action.setChecked(True)
        self.connect(self.show_cover_art_action, QtCore.SIGNAL("triggered()"), self.show_cover_art)

        self.search_action = QtGui.QAction(icontheme.lookup('system-search'), _(u"Search"), self)
        self.connect(self.search_action, QtCore.SIGNAL("triggered()"), self.search)

        self.cd_lookup_action = QtGui.QAction(icontheme.lookup('media-optical'), _(u"&CD Lookup..."), self)
        self.cd_lookup_action.setToolTip(_(u"Lookup CD"))
        self.cd_lookup_action.setStatusTip(_(u"Lookup CD"))
        # TR: Keyboard shortcut for "Lookup CD"
        self.cd_lookup_action.setShortcut(QtGui.QKeySequence(_("Ctrl+K")))
        self.connect(self.cd_lookup_action, QtCore.SIGNAL("triggered()"), self.tagger.lookup_cd)

        self.analyze_action = QtGui.QAction(icontheme.lookup('picard-analyze'), _(u"&Scan"), self)
        self.analyze_action.setEnabled(False)
        # TR: Keyboard shortcut for "Analyze"
        self.analyze_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+Y")))
        self.connect(self.analyze_action, QtCore.SIGNAL("triggered()"), self.analyze)

        self.cluster_action = QtGui.QAction(icontheme.lookup('picard-cluster'), _(u"Cl&uster"), self)
        self.cluster_action.setEnabled(False)
        # TR: Keyboard shortcut for "Cluster"
        self.cluster_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+U")))
        self.connect(self.cluster_action, QtCore.SIGNAL("triggered()"), self.cluster)

        self.autotag_action = QtGui.QAction(icontheme.lookup('picard-auto-tag'), _(u"&Lookup"), self)
        self.autotag_action.setToolTip(_(u"Lookup metadata"))
        self.autotag_action.setStatusTip(_(u"Lookup metadata"))
        self.autotag_action.setEnabled(False)
        # TR: Keyboard shortcut for "Lookup"
        self.autotag_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+L")))
        self.connect(self.autotag_action, QtCore.SIGNAL("triggered()"), self.autotag)

        self.edit_tags_action = QtGui.QAction(icontheme.lookup('picard-edit-tags'), _(u"&Details..."), self)
        self.edit_tags_action.setEnabled(False)
        # TR: Keyboard shortcut for "Details"
        self.edit_tags_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+I")))
        self.connect(self.edit_tags_action, QtCore.SIGNAL("triggered()"), self.edit_tags)

        self.refresh_action = QtGui.QAction(icontheme.lookup('view-refresh', icontheme.ICON_SIZE_MENU), _("&Refresh"), self)
        self.connect(self.refresh_action, QtCore.SIGNAL("triggered()"), self.refresh)

        self.enable_renaming_action = QtGui.QAction(_(u"&Rename Files"), self)
        self.enable_renaming_action.setCheckable(True)
        self.enable_renaming_action.setChecked(self.config.setting["rename_files"])
        self.connect(self.enable_renaming_action, QtCore.SIGNAL("triggered(bool)"), self.toggle_rename_files)

        self.enable_moving_action = QtGui.QAction(_(u"&Move Files"), self)
        self.enable_moving_action.setCheckable(True)
        self.enable_moving_action.setChecked(self.config.setting["move_files"])
        self.connect(self.enable_moving_action, QtCore.SIGNAL("triggered(bool)"), self.toggle_move_files)

        self.enable_tag_saving_action = QtGui.QAction(_(u"Save &Tags"), self)
        self.enable_tag_saving_action.setCheckable(True)
        self.enable_tag_saving_action.setChecked(not self.config.setting["dont_write_tags"])
        self.connect(self.enable_tag_saving_action, QtCore.SIGNAL("triggered(bool)"), self.toggle_tag_saving)

        self.tags_from_filenames_action = QtGui.QAction(_(u"Tags From &File Names..."), self)
        self.connect(self.tags_from_filenames_action, QtCore.SIGNAL("triggered()"), self.open_tags_from_filenames)

        self.view_log_action = QtGui.QAction(_(u"View &Log..."), self)
        self.connect(self.view_log_action, QtCore.SIGNAL("triggered()"), self.show_log)

        self.connect(self.tagger.xmlws, QtCore.SIGNAL("authentication_required"), self.show_password_dialog)
        self.connect(self.tagger.xmlws, QtCore.SIGNAL("proxyAuthentication_required"), self.show_proxy_dialog)

        self.open_file_action = QtGui.QAction(_(u"&Open..."), self)
        self.open_file_action.setStatusTip(_(u"Open the file"))
        self.connect(self.open_file_action, QtCore.SIGNAL("triggered()"), self.open_file)

        self.open_folder_action = QtGui.QAction(_(u"Open &Folder..."), self)
        self.open_folder_action.setStatusTip(_(u"Open the containing folder"))
        self.connect(self.open_folder_action, QtCore.SIGNAL("triggered()"), self.open_folder)

    def toggle_rename_files(self, checked):
        self.config.setting["rename_files"] = checked

    def toggle_move_files(self, checked):
        self.config.setting["move_files"] = checked

    def toggle_tag_saving(self, checked):
        self.config.setting["dont_write_tags"] = not checked

    def open_tags_from_filenames(self):
        files = self.tagger.get_files_from_objects(self.selected_objects)
        if not files:
            files = self.tagger.unmatched_files.files
        if files:
            dialog = TagsFromFileNamesDialog(files, self)
            dialog.exec_()

    def create_menus(self):
        menu = self.menuBar().addMenu(_(u"&File"))
        menu.addAction(self.add_files_action)
        menu.addAction(self.add_directory_action)
        menu.addSeparator()
        menu.addAction(self.save_action)
        menu.addAction(self.submit_action)
        menu.addSeparator()
        menu.addAction(self.exit_action)
        menu = self.menuBar().addMenu(_(u"&Edit"))
        menu.addAction(self.cut_action)
        menu.addAction(self.paste_action)
        menu.addSeparator()
        menu.addAction(self.edit_tags_action)
        menu.addAction(self.remove_action)
        menu = self.menuBar().addMenu(_(u"&View"))
        menu.addAction(self.show_file_browser_action)
        menu.addAction(self.show_cover_art_action)
        menu.addSeparator()
        menu.addAction(self.toolbar.toggleViewAction())
        menu.addAction(self.search_toolbar.toggleViewAction())
        menu = self.menuBar().addMenu(_(u"&Options"))
        menu.addAction(self.enable_renaming_action)
        menu.addAction(self.enable_moving_action)
        menu.addAction(self.enable_tag_saving_action)
        menu.addSeparator()
        menu.addAction(self.options_action)
        menu = self.menuBar().addMenu(_(u"&Tools"))
        menu.addAction(self.cd_lookup_action)
        menu.addAction(self.autotag_action)
        menu.addAction(self.analyze_action)
        menu.addAction(self.cluster_action)
        menu.addSeparator()
        menu.addAction(self.tags_from_filenames_action)
        self.menuBar().addSeparator()
        menu = self.menuBar().addMenu(_(u"&Help"))
        menu.addAction(self.help_action)
        menu.addSeparator()
        menu.addAction(self.support_forum_action)
        menu.addAction(self.report_bug_action)
        menu.addAction(self.view_log_action)
        menu.addSeparator()
        menu.addAction(self.donate_action)
        menu.addAction(self.about_action)

    def update_toolbar_style(self):
        if self.config.setting["toolbar_show_labels"]:
            self.toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon)
        else:
            self.toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly)
        self.cd_lookup_action.setEnabled(len(get_cdrom_drives()) > 0)

    def create_toolbar(self):
        self.toolbar = toolbar = self.addToolBar(_(u"&Toolbar"))
        self.update_toolbar_style()
        toolbar.setObjectName("main_toolbar")
        toolbar.addAction(self.add_files_action)
        toolbar.addAction(self.add_directory_action)
        toolbar.addSeparator()
        toolbar.addAction(self.save_action)
        toolbar.addAction(self.submit_action)
        toolbar.addSeparator()

        toolbar.addAction(self.cd_lookup_action)
        drives = get_cdrom_drives()
        if len(drives) > 1:
            self.cd_lookup_menu = QtGui.QMenu()
            for drive in drives:
                self.cd_lookup_menu.addAction(drive)
            self.connect(self.cd_lookup_menu, QtCore.SIGNAL("triggered(QAction*)"), self.tagger.lookup_cd)
            button = toolbar.widgetForAction(self.cd_lookup_action)
            button.setPopupMode(QtGui.QToolButton.MenuButtonPopup)
            button.setMenu(self.cd_lookup_menu)

        toolbar.addAction(self.cluster_action)
        toolbar.addAction(self.autotag_action)
        toolbar.addAction(self.analyze_action)
        toolbar.addAction(self.edit_tags_action)
        toolbar.addAction(self.remove_action)
        self.search_toolbar = toolbar = self.addToolBar(_(u"&Search Bar"))
        toolbar.setObjectName("search_toolbar")
        search_panel = QtGui.QWidget(toolbar)
        hbox = QtGui.QHBoxLayout(search_panel)
        self.search_combo = QtGui.QComboBox(search_panel)
        self.search_combo.addItem(_(u"Album"), QtCore.QVariant("album"))
        self.search_combo.addItem(_(u"Artist"), QtCore.QVariant("artist"))
        self.search_combo.addItem(_(u"Track"), QtCore.QVariant("track"))
        hbox.addWidget(self.search_combo, 0)
        self.search_edit = QtGui.QLineEdit(search_panel)
        self.connect(self.search_edit, QtCore.SIGNAL("returnPressed()"), self.search)
        hbox.addWidget(self.search_edit, 0)
        self.search_button = QtGui.QToolButton(search_panel)
        self.search_button.setAutoRaise(True)
        self.search_button.setDefaultAction(self.search_action)
        self.search_button.setIconSize(QtCore.QSize(22, 22))
        hbox.addWidget(self.search_button)
        toolbar.addWidget(search_panel)

    def enable_submit(self, enabled):
        """Enable/disable the 'Submit PUIDs' action."""
        self.submit_action.setEnabled(enabled)

    def enable_cluster(self, enabled):
        """Enable/disable the 'Cluster' action."""
        self.cluster_action.setEnabled(enabled)

    def search(self):
        """Search for album, artist or track on the MusicBrainz website."""
        text = unicode(self.search_edit.text())
        type = unicode(self.search_combo.itemData(
                       self.search_combo.currentIndex()).toString())
        self.tagger.search(text, type, self.config.setting["use_adv_search_syntax"])

    def add_files(self):
        """Add files to the tagger."""
        current_directory = self.config.persist["current_directory"] or QtCore.QDir.homePath()
        current_directory = find_existing_path(unicode(current_directory))
        formats = []
        extensions = []
        for exts, name in supported_formats():
            exts = ["*" + e for e in exts]
            formats.append("%s (%s)" % (name, " ".join(exts)))
            extensions.extend(exts)
        formats.sort()
        extensions.sort()
        formats.insert(0, _("All Supported Formats") + " (%s)" % " ".join(extensions))
        files = QtGui.QFileDialog.getOpenFileNames(self, "", current_directory, u";;".join(formats))
        if files:
            files = map(unicode, files)
            self.config.persist["current_directory"] = os.path.dirname(files[0])
            self.tagger.add_files(files)

    def add_directory(self):
        """Add directory to the tagger."""
        current_directory = self.config.persist["current_directory"] or QtCore.QDir.homePath()
        current_directory = find_existing_path(unicode(current_directory))

        dir_list = []
        if not self.config.setting["toolbar_multiselect"]:
            directory = QtGui.QFileDialog.getExistingDirectory(self, "", current_directory)
            if directory:
                dir_list.append(directory)
        else:
            # Use a custom file selection dialog to allow the selection of multiple directories
            file_dialog = QtGui.QFileDialog(self, "", current_directory)
            file_dialog.setFileMode(QtGui.QFileDialog.DirectoryOnly)
            if sys.platform == "darwin": # The native dialog doesn't allow selecting >1 directory
                file_dialog.setOption(QtGui.QFileDialog.DontUseNativeDialog)
            tree_view = file_dialog.findChild(QtGui.QTreeView)
            tree_view.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
            list_view = file_dialog.findChild(QtGui.QListView, "listView")
            list_view.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)

            if file_dialog.exec_() == QtGui.QDialog.Accepted:
                dir_list = file_dialog.selectedFiles()

        if len(dir_list) == 1:
            self.config.persist["current_directory"] = dir_list[0]
        elif len(dir_list) > 1:
            (parent, dir) = os.path.split(str(dir_list[0]))
            self.config.persist["current_directory"] = parent

        for directory in dir_list:
            directory = unicode(directory)
            self.tagger.add_directory(directory)

    def show_about(self):
        self.show_options("about")

    def show_options(self, page=None):
        dialog = OptionsDialog(page, self)
        dialog.exec_()

    def show_help(self):
        webbrowser2.open("http://musicbrainz.org/doc/Picard_Documentation")

    def show_log(self):
        from picard.ui.logview import LogView
        w = LogView(self)
        w.show()

    def open_bug_report(self):
        webbrowser2.open("http://musicbrainz.org/doc/Picard_Troubleshooting")

    def open_support_forum(self):
        webbrowser2.open("http://forums.musicbrainz.org/viewforum.php?id=2")

    def open_donation_page(self):
        webbrowser2.open('http://metabrainz.org/donate/index.html')

    def save(self):
        """Tell the tagger to save the selected objects."""
        self.tagger.save(self.panel.selected_objects())

    def remove(self):
        """Tell the tagger to remove the selected objects."""
        self.tagger.remove(self.panel.selected_objects())

    def analyze(self):
        if not self.config.setting['enable_fingerprinting']:
            if self.show_analyze_settings_info():
                self.show_options("fingerprinting")
            if not self.config.setting['enable_fingerprinting']:
                return
        return self.tagger.analyze(self.panel.selected_objects())

    def open_file(self):
        files = self.tagger.get_files_from_objects(self.selected_objects)
        for file in files:
            url = QtCore.QUrl.fromLocalFile(file.filename)
            QtGui.QDesktopServices.openUrl(url)

    def open_folder(self):
        files = self.tagger.get_files_from_objects(self.selected_objects)
        for file in files:
            url = QtCore.QUrl.fromLocalFile(os.path.dirname(file.filename))
            QtGui.QDesktopServices.openUrl(url)

    def show_analyze_settings_info(self):
        ret = QtGui.QMessageBox.question(self,
            _(u"Configuration Required"),
            _(u"Audio fingerprinting is not yet configured. Would you like to configure it now?"),
            QtGui.QMessageBox.Yes | QtGui.QMessageBox.No,
            QtGui.QMessageBox.Yes)
        return ret == QtGui.QMessageBox.Yes

    def edit_tags(self, objs=None):
        if not objs:
            objs = self.selected_objects
        objs = self.tagger.get_files_from_objects(objs)
        dialog = TagEditor(objs, self)
        dialog.exec_()

    def cluster(self):
        self.tagger.cluster(self.panel.selected_objects())

    def refresh(self):
        self.tagger.refresh(self.panel.selected_objects())

    def update_actions(self):
        can_remove = False
        can_save = False
        can_edit_tags = False
        can_analyze = False
        can_refresh = False
        can_autotag = False
        for obj in self.selected_objects:
            if obj is None:
                continue
            if obj.can_analyze():
                can_analyze = True
            if obj.can_save():
                can_save = True
            if obj.can_remove():
                can_remove = True
            if obj.can_edit_tags():
                can_edit_tags = True
            if obj.can_refresh():
                can_refresh = True
            if obj.can_autotag():
                can_autotag = True
            if can_save and can_remove and can_edit_tags and can_refresh \
                    and can_autotag:
                break
        self.remove_action.setEnabled(can_remove)
        self.save_action.setEnabled(can_save)
        self.edit_tags_action.setEnabled(can_edit_tags)
        self.analyze_action.setEnabled(can_analyze)
        self.refresh_action.setEnabled(can_refresh)
        self.autotag_action.setEnabled(can_autotag)
        self.cut_action.setEnabled(bool(self.selected_objects))

    def updateSelection(self, objects=None):
        if objects is not None:
            self.selected_objects = objects
        else:
            objects = self.selected_objects

        self.update_actions()

        orig_metadata = None
        metadata = None
        is_album = False
        statusBar = u""
        file = None
        obj = None
        if len(objects) == 1:
            obj = objects[0]
            if isinstance(obj, File):
                orig_metadata = obj.orig_metadata
                metadata = obj.metadata
                statusBar = obj.filename
                if obj.state == obj.ERROR:
                    statusBar += _(" (Error: %s)") % obj.error
                file = obj
            elif isinstance(obj, Track):
                if obj.num_linked_files == 1:
                    file = obj.linked_files[0]
                    orig_metadata = file.orig_metadata
                    metadata = file.metadata
                    statusBar = "%s (%d%%)" % (file.filename, file.similarity * 100)
                    if file.state == file.ERROR:
                        statusBar += _(" (Error: %s)") % file.error
                elif obj.num_linked_files == 0:
                    metadata = obj.metadata
                else:
                    metadata = obj.metadata
                    #Show dup zaper
            elif isinstance(obj, Cluster):
                orig_metadata = obj.metadata
                is_album = True
            elif isinstance(obj, Album):
                metadata = obj.metadata
                is_album = True

        self.orig_metadata_box.set_metadata(orig_metadata, is_album)
        self.metadata_box.set_metadata(metadata, is_album, file=file)
        self.cover_art_box.set_metadata(metadata, obj)
        self.set_statusbar_message(statusBar)

    def show_cover_art(self):
        """Show/hide the cover art box."""
        if self.show_cover_art_action.isChecked():
            self.cover_art_box.show()
        else:
            self.cover_art_box.hide()

    def show_file_browser(self):
        """Show/hide the file browser."""
        if self.show_file_browser_action.isChecked():
            sizes = self.panel.sizes()
            if sizes[0] == 0:
                sizes[0] = sum(sizes) / 4
                self.panel.setSizes(sizes)
            self.file_browser.show()
        else:
            self.file_browser.hide()

    def show_password_dialog(self, reply, authenticator):
        dialog = PasswordDialog(authenticator, reply, parent=self)
        dialog.exec_()

    def show_proxy_dialog(self, proxy, authenticator):
        dialog = ProxyDialog(authenticator, proxy, parent=self)
        dialog.exec_()

    def autotag(self):
        self.tagger.autotag(self.panel.selected_objects())

    def cut(self):
        self._clipboard = self.panel.selected_objects()
        self.paste_action.setEnabled(bool(self._clipboard))

    def paste(self):
        selected_objects = self.panel.selected_objects()
        if not selected_objects:
            target = self.tagger.unmatched_files
        else:
            target = selected_objects[0]
        self.panel.views[0].drop_files(self.tagger.get_files_from_objects(self._clipboard), target)
        self._clipboard = []
        self.paste_action.setEnabled(False)
Exemple #16
0
class TagsFromFileNamesDialog(QtGui.QDialog):

    options = [
        TextOption("persist", "tags_from_filenames_format", ""),
        Option("persist", "tags_from_filenames_position", QtCore.QPoint(),
               QtCore.QVariant.toPoint),
        Option("persist", "tags_from_filenames_size", QtCore.QSize(560, 400),
               QtCore.QVariant.toSize),
    ]

    def __init__(self, files, parent=None):
        QtGui.QDialog.__init__(self, parent)
        self.ui = Ui_TagsFromFileNamesDialog()
        self.ui.setupUi(self)
        items = [
            "%artist%/%album%/%tracknumber% %title%",
            "%artist%/%album%/%tracknumber% - %title%",
            "%artist%/%album - %tracknumber% - %title%",
        ]
        format = self.config.persist["tags_from_filenames_format"]
        if format and format not in items:
            items.insert(0, format)
        self.ui.format.addItems(items)
        self.ui.buttonbox.addButton(StandardButton(StandardButton.OK),
                                    QtGui.QDialogButtonBox.AcceptRole)
        self.ui.buttonbox.addButton(StandardButton(StandardButton.CANCEL),
                                    QtGui.QDialogButtonBox.RejectRole)
        self.connect(self.ui.buttonbox, QtCore.SIGNAL('accepted()'), self,
                     QtCore.SLOT('accept()'))
        self.connect(self.ui.buttonbox, QtCore.SIGNAL('rejected()'), self,
                     QtCore.SLOT('reject()'))
        self.connect(self.ui.preview, QtCore.SIGNAL('clicked()'), self.preview)
        self.ui.files.setHeaderLabels([_("File Name")])
        self.restoreWindowState()
        self.files = files
        self.items = []
        for file in files:
            item = QtGui.QTreeWidgetItem(self.ui.files)
            item.setText(0, os.path.basename(file.filename))
            self.items.append(item)
        self._tag_re = re.compile("(%\w+%)")

    def parse_format(self):
        format = unicode(self.ui.format.currentText())
        columns = []
        format_re = ['(?:^|/)']
        for part in self._tag_re.split(format):
            if part.startswith('%') and part.endswith('%'):
                name = part[1:-1]
                columns.append(name)
                if name in ('tracknumber', 'totaltracks', 'discnumber',
                            'totaldiscs'):
                    format_re.append('(?P<' + name + '>\d+)')
                elif name in ('date'):
                    format_re.append('(?P<' + name + '>\d+(?:-\d+(?:-\d+)?)?)')
                else:
                    format_re.append('(?P<' + name + '>[^/]*?)')
            else:
                format_re.append(re.escape(part))
        format_re.append('\\.(\\w+)$')
        format_re = re.compile("".join(format_re))
        return format_re, columns

    def match_file(self, file, format):
        match = format.search('/'.join(os.path.split(file.filename)))
        if match:
            result = {}
            for name, value in match.groupdict().iteritems():
                value = value.strip()
                if self.ui.replace_underscores.isChecked():
                    value = value.replace('_', ' ')
                result[name] = value
            return result
        else:
            return {}

    def preview(self):
        format, columns = self.parse_format()
        self.ui.files.setHeaderLabels([_("File Name")] +
                                      map(display_tag_name, columns))
        for item, file in zip(self.items, self.files):
            matches = self.match_file(file, format)
            for i in range(len(columns)):
                value = matches.get(columns[i], '')
                item.setText(i + 1, value)
        self.ui.files.header().resizeSections(
            QtGui.QHeaderView.ResizeToContents)
        self.ui.files.header().setStretchLastSection(True)

    def accept(self):
        format, columns = self.parse_format()
        for file in self.files:
            metadata = self.match_file(file, format)
            for name, value in metadata.iteritems():
                file.metadata[name] = value
            file.update()
        self.config.persist[
            "tags_from_filenames_format"] = self.ui.format.currentText()
        self.saveWindowState()
        QtGui.QDialog.accept(self)

    def reject(self):
        self.saveWindowState()
        QtGui.QDialog.reject(self)

    def closeEvent(self, event):
        self.saveWindowState()
        event.accept()

    def saveWindowState(self):
        pos = self.pos()
        if not pos.isNull():
            self.config.persist["tags_from_filenames_position"] = pos
        self.config.persist["tags_from_filenames_size"] = self.size()

    def restoreWindowState(self):
        pos = self.config.persist["tags_from_filenames_position"]
        if pos.x() > 0 and pos.y() > 0:
            self.move(pos)
        self.resize(self.config.persist["tags_from_filenames_size"])
Exemple #17
0
class CDLookupDialog(PicardDialog):

    autorestore = False
    dialog_header_state = "cdlookupdialog_header_state"

    options = [Option("persist", dialog_header_state, QtCore.QByteArray())]

    def __init__(self, releases, disc, parent=None):
        super().__init__(parent)
        self.releases = releases
        self.disc = disc
        self.ui = Ui_Dialog()
        self.ui.setupUi(self)
        release_list = self.ui.release_list
        release_list.setSelectionMode(
            QtWidgets.QAbstractItemView.ExtendedSelection)
        release_list.setSortingEnabled(True)
        release_list.setAlternatingRowColors(True)
        release_list.setHeaderLabels([
            _("Album"),
            _("Artist"),
            _("Date"),
            _("Country"),
            _("Labels"),
            _("Catalog #s"),
            _("Barcode")
        ])
        self.ui.submit_button.setIcon(QtGui.QIcon(":/images/cdrom.png"))
        if self.releases:

            def myjoin(values):
                return "\n".join(values)

            self.ui.results_view.setCurrentIndex(0)
            selected = None
            for release in self.releases:
                labels, catalog_numbers = label_info_from_node(
                    release['label-info'])
                dates, countries = release_dates_and_countries_from_node(
                    release)
                barcode = release['barcode'] if "barcode" in release else ""
                item = QtWidgets.QTreeWidgetItem(release_list)
                if disc.mcn and compare_barcodes(barcode, disc.mcn):
                    selected = item
                item.setText(0, release['title'])
                item.setText(
                    1,
                    artist_credit_from_node(release['artist-credit'])[0])
                item.setText(2, myjoin(dates))
                item.setText(3, myjoin(countries))
                item.setText(4, myjoin(labels))
                item.setText(5, myjoin(catalog_numbers))
                item.setText(6, barcode)
                item.setData(0, QtCore.Qt.UserRole, release['id'])
            release_list.setCurrentItem(selected
                                        or release_list.topLevelItem(0))
            self.ui.ok_button.setEnabled(True)
            for i in range(release_list.columnCount() - 1):
                release_list.resizeColumnToContents(i)
            # Sort by descending date, then ascending country
            release_list.sortByColumn(3, QtCore.Qt.AscendingOrder)
            release_list.sortByColumn(2, QtCore.Qt.DescendingOrder)
        else:
            self.ui.results_view.setCurrentIndex(1)
        self.ui.lookup_button.clicked.connect(self.lookup)
        self.ui.submit_button.clicked.connect(self.lookup)
        self.restore_geometry()
        self.restore_header_state()
        self.finished.connect(self.save_header_state)

    def accept(self):
        release_list = self.ui.release_list
        for index in release_list.selectionModel().selectedRows():
            release_id = release_list.itemFromIndex(index).data(
                0, QtCore.Qt.UserRole)
            self.tagger.load_album(release_id, discid=self.disc.id)
        super().accept()

    def lookup(self):
        lookup = self.tagger.get_file_lookup()
        lookup.disc_lookup(self.disc.submission_url)
        super().accept()

    @restore_method
    def restore_header_state(self):
        if self.ui.release_list:
            header = self.ui.release_list.header()
            config = get_config()
            state = config.persist[self.dialog_header_state]
            if state:
                header.restoreState(state)
                log.debug("restore_state: %s" % self.dialog_header_state)

    def save_header_state(self):
        if self.ui.release_list:
            state = self.ui.release_list.header().saveState()
            config = get_config()
            config.persist[self.dialog_header_state] = state
            log.debug("save_state: %s" % self.dialog_header_state)
Exemple #18
0
    def test_var_opt_set_none(self):
        Option("setting", "var_option", set(["a", "b"]))

        # set option to None
        self.config.setting["var_option"] = None
        self.assertEqual(self.config.setting["var_option"], set(["a", "b"]))
Exemple #19
0
class OptionsDialog(QtGui.QDialog):

    options = [
        Option("persist", "options_position", QtCore.QPoint(),
               QtCore.QVariant.toPoint),
        Option("persist", "options_size", QtCore.QSize(560, 400),
               QtCore.QVariant.toSize),
        Option("persist", "options_splitter", QtCore.QByteArray(),
               QtCore.QVariant.toByteArray),
    ]

    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 = QtGui.QTreeWidgetItem(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):
        QtGui.QDialog.__init__(self, parent)

        from picard.ui.ui_options import Ui_Dialog
        self.ui = Ui_Dialog()
        self.ui.setupUi(self)

        self.ui.buttonbox.addButton(StandardButton(StandardButton.OK),
                                    QtGui.QDialogButtonBox.AcceptRole)
        self.ui.buttonbox.addButton(StandardButton(StandardButton.CANCEL),
                                    QtGui.QDialogButtonBox.RejectRole)
        self.ui.buttonbox.addButton(StandardButton(StandardButton.HELP),
                                    QtGui.QDialogButtonBox.HelpRole)
        self.connect(self.ui.buttonbox, QtCore.SIGNAL('accepted()'), self,
                     QtCore.SLOT('accept()'))
        self.connect(self.ui.buttonbox, QtCore.SIGNAL('rejected()'), self,
                     QtCore.SLOT('reject()'))
        self.connect(self.ui.buttonbox, QtCore.SIGNAL('helpRequested()'),
                     self.help)

        self.pages = []
        for Page in page_classes:
            page = Page(self.ui.pages_stack)
            self.pages.append(page)
        self.item_to_page = {}
        self.page_to_item = {}
        self.default_item = None
        self.add_pages(None, default_page, self.ui.pages_tree)

        self.ui.pages_tree.setHeaderLabels([""])
        self.ui.pages_tree.header().hide()
        self.connect(self.ui.pages_tree,
                     QtCore.SIGNAL("itemSelectionChanged()"), self.switch_page)

        self.restoreWindowState()

        for page in self.pages:
            page.load()
        self.ui.pages_tree.setCurrentItem(self.default_item)

    def switch_page(self):
        items = self.ui.pages_tree.selectedItems()
        if items:
            page = self.item_to_page[items[0]]
            self.ui.pages_stack.setCurrentWidget(page)

    def help(self):
        webbrowser2.open(
            'http://musicbrainz.org/doc/Picard_Documentation/Options')

    def accept(self):
        for page in self.pages:
            try:
                page.check()
            except OptionsCheckError, e:
                self.ui.pages_tree.setCurrentItem(self.page_to_item[page.NAME])
                page.display_error(e)
                return
        for page in self.pages:
            page.save()
        self.saveWindowState()
        QtGui.QDialog.accept(self)
Exemple #20
0
class AlbumSearchDialog(SearchDialog):

    dialog_header_state = "albumsearchdialog_header_state"

    options = [Option("persist", dialog_header_state, QtCore.QByteArray())]

    def __init__(self, parent, force_advanced_search=None):
        super().__init__(parent,
                         accept_button_title=_("Load into Picard"),
                         search_type="album",
                         force_advanced_search=force_advanced_search)
        self.cluster = None
        self.setWindowTitle(_("Album Search Results"))
        self.columns = [
            ('name', _("Name")),
            ('artist', _("Artist")),
            ('format', _("Format")),
            ('tracks', _("Tracks")),
            ('date', _("Date")),
            ('country', _("Country")),
            ('labels', _("Labels")),
            ('catnums', _("Catalog #s")),
            ('barcode', _("Barcode")),
            ('language', _("Language")),
            ('type', _("Type")),
            ('status', _("Status")),
            ('cover', _("Cover")),
            ('score', _("Score")),
        ]
        self.cover_cells = []
        self.fetching = False
        self.scrolled.connect(self.fetch_coverarts)

    def search(self, text):
        """Perform search using query provided by the user."""
        self.retry_params = Retry(self.search, text)
        self.search_box_text(text)
        self.show_progress()
        self.tagger.mb_api.find_releases(
            self.handle_reply,
            query=text,
            search=True,
            advanced_search=self.use_advanced_search,
            limit=QUERY_LIMIT)

    def show_similar_albums(self, cluster):
        """Perform search by using existing metadata information
        from the cluster as query."""
        self.retry_params = Retry(self.show_similar_albums, cluster)
        self.cluster = cluster
        metadata = cluster.metadata
        query = {
            "artist": metadata["albumartist"],
            "release": metadata["album"],
            "tracks": str(len(cluster.files))
        }

        # Generate query to be displayed to the user (in search box).
        # If advanced query syntax setting is enabled by user, display query in
        # advanced syntax style. Otherwise display only album title.
        if self.use_advanced_search:
            query_str = ' '.join([
                '%s:(%s)' % (item, escape_lucene_query(value))
                for item, value in query.items() if value
            ])
        else:
            query_str = query["release"]

        query["limit"] = QUERY_LIMIT
        self.search_box_text(query_str)
        self.show_progress()
        self.tagger.mb_api.find_releases(self.handle_reply, **query)

    def retry(self):
        self.retry_params.function(self.retry_params.query)

    def handle_reply(self, document, http, error):
        if error:
            self.network_error(http, error)
            return

        try:
            releases = document['releases']
        except (KeyError, TypeError):
            self.no_results_found()
            return

        del self.search_results[:]
        self.parse_releases(releases)
        self.display_results()
        self.fetch_coverarts()

    def fetch_coverarts(self):
        if self.fetching:
            return
        self.fetching = True
        for cell in self.cover_cells:
            self.fetch_coverart(cell)
        self.fetching = False

    def fetch_coverart(self, cell):
        """Queue cover art jsons from CAA server for each album in search
        results.
        """
        if cell.fetched:
            return
        if not cell.is_visible():
            return
        cell.fetched = True
        caa_path = "/release/%s" % cell.release["musicbrainz_albumid"]
        cell.fetch_task = self.tagger.webservice.get(
            CAA_HOST, CAA_PORT, caa_path,
            partial(self._caa_json_downloaded, cell))

    def _caa_json_downloaded(self, cover_cell, data, http, error):
        """Handle json reply from CAA server.
        If server replies without error, try to get small thumbnail of front
        coverart of the release.
        """
        cover_cell.fetch_task = None

        if error:
            cover_cell.not_found()
            return

        front = None
        try:
            for image in data["images"]:
                if image["front"]:
                    front = image
                    break

            if front:
                url = front["thumbnails"]["small"]
                coverartimage = CaaThumbnailCoverArtImage(url=url)
                cover_cell.fetch_task = self.tagger.webservice.download(
                    coverartimage.host, coverartimage.port, coverartimage.path,
                    partial(self._cover_downloaded, cover_cell))
            else:
                cover_cell.not_found()
        except (AttributeError, KeyError, TypeError):
            log.error("Error reading CAA response", exc_info=True)
            cover_cell.not_found()

    def _cover_downloaded(self, cover_cell, data, http, error):
        """Handle cover art query reply from CAA server.
        If server returns the cover image successfully, update the cover art
        cell of particular release.

        Args:
            row -- Album's row in results table
        """
        cover_cell.fetch_task = None

        if error:
            cover_cell.not_found()
        else:
            pixmap = QtGui.QPixmap()
            try:
                pixmap.loadFromData(data)
                cover_cell.set_pixmap(pixmap)
            except Exception as e:
                cover_cell.not_found()
                log.error(e)

    def fetch_cleanup(self):
        for cell in self.cover_cells:
            if cell.fetch_task is not None:
                log.debug("Removing cover art fetch task for %s",
                          cell.release['musicbrainz_albumid'])
                self.tagger.webservice.remove_task(cell.fetch_task)

    def closeEvent(self, event):
        if self.cover_cells:
            self.fetch_cleanup()
        super().closeEvent(event)

    def parse_releases(self, releases):
        for node in releases:
            release = Metadata()
            release_to_metadata(node, release)
            release['score'] = node['score']
            rg_node = node['release-group']
            release_group_to_metadata(rg_node, release)
            if "media" in node:
                media = node['media']
                release["format"] = media_formats_from_node(media)
                release["tracks"] = node['track-count']
            countries = countries_from_node(node)
            if countries:
                release["country"] = ", ".join(countries)
            self.search_results.append(release)

    def display_results(self):
        self.prepare_table()
        self.cover_cells = []
        for row, release in enumerate(self.search_results):
            self.table.insertRow(row)
            self.set_table_item(row, 'name', release, "album")
            self.set_table_item(row, 'artist', release, "albumartist")
            self.set_table_item(row, 'format', release, "format")
            self.set_table_item(row, 'tracks', release, "tracks")
            self.set_table_item(row, 'date', release, "date")
            self.set_table_item(row, 'country', release, "country")
            self.set_table_item(row, 'labels', release, "label")
            self.set_table_item(row, 'catnums', release, "catalognumber")
            self.set_table_item(row, 'barcode', release, "barcode")
            self.set_table_item(row, 'language', release, "~releaselanguage")
            self.set_table_item(row, 'type', release, "releasetype")
            self.set_table_item(row, 'status', release, "releasestatus")
            self.set_table_item(row, 'score', release, "score")
            self.cover_cells.append(
                CoverCell(self,
                          release,
                          row,
                          'cover',
                          on_show=self.fetch_coverart))
        self.show_table(sort_column='score')

    def accept_event(self, rows):
        for row in rows:
            self.load_selection(row)

    def load_selection(self, row):
        release = self.search_results[row]
        self.tagger.get_release_group_by_id(
            release["musicbrainz_releasegroupid"]).loaded_albums.add(
                release["musicbrainz_albumid"])
        album = self.tagger.load_album(release["musicbrainz_albumid"])
        if self.cluster:
            files = self.cluster.iterfiles()
            self.tagger.move_files_to_album(files,
                                            release["musicbrainz_albumid"],
                                            album)
Exemple #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)
Exemple #22
0
    def test_var_opt_no_config(self):
        Option("setting", "var_option", set(["a", "b"]))

        # test default, nothing in config yet
        self.assertEqual(self.config.setting["var_option"], set(["a", "b"]))
        self.assertIs(type(self.config.setting["var_option"]), set)
Exemple #23
0
class MainPanel(QtGui.QSplitter):

    options = [
        Option("persist", "splitter_state", QtCore.QByteArray(),
               QtCore.QVariant.toByteArray),
    ]

    columns = [
        (N_('Title'), 'title'),
        (N_('Length'), '~length'),
        (N_('Artist'), 'artist'),
    ]

    def __init__(self, window, parent=None):
        QtGui.QSplitter.__init__(self, parent)
        self.window = window
        self.create_icons()

        self.views = [FileTreeView(window, self), AlbumTreeView(window, self)]
        self.views[0].itemSelectionChanged.connect(self.update_selection_0)
        self.views[1].itemSelectionChanged.connect(self.update_selection_1)
        self._selected_view = 0
        self._ignore_selection_changes = False
        TreeItem.window = window

        TreeItem.base_color = self.palette().base().color()
        TreeItem.text_color = self.palette().text().color()
        TrackItem.track_colors = {
            File.NORMAL: self.config.setting["color_saved"],
            File.CHANGED: TreeItem.text_color,
            File.PENDING: self.config.setting["color_pending"],
            File.ERROR: self.config.setting["color_error"],
        }
        FileItem.file_colors = {
            File.NORMAL: TreeItem.text_color,
            File.CHANGED: self.config.setting["color_modified"],
            File.PENDING: self.config.setting["color_pending"],
            File.ERROR: self.config.setting["color_error"],
        }

    def save_state(self):
        self.config.persist["splitter_state"] = self.saveState()
        for view in self.views:
            view.save_state()

    def restore_state(self):
        self.restoreState(self.config.persist["splitter_state"])

    def create_icons(self):
        if hasattr(QtGui.QStyle, 'SP_DirIcon'):
            ClusterItem.icon_dir = self.style().standardIcon(
                QtGui.QStyle.SP_DirIcon)
        else:
            ClusterItem.icon_dir = icontheme.lookup('folder',
                                                    icontheme.ICON_SIZE_MENU)
        AlbumItem.icon_cd = icontheme.lookup('media-optical',
                                             icontheme.ICON_SIZE_MENU)
        AlbumItem.icon_cd_saved = icontheme.lookup('media-optical-saved',
                                                   icontheme.ICON_SIZE_MENU)
        TrackItem.icon_note = QtGui.QIcon(":/images/note.png")
        FileItem.icon_file = QtGui.QIcon(":/images/file.png")
        FileItem.icon_file_pending = QtGui.QIcon(":/images/file-pending.png")
        FileItem.icon_error = icontheme.lookup('dialog-error',
                                               icontheme.ICON_SIZE_MENU)
        FileItem.icon_saved = QtGui.QIcon(":/images/track-saved.png")
        FileItem.match_icons = [
            QtGui.QIcon(":/images/match-50.png"),
            QtGui.QIcon(":/images/match-60.png"),
            QtGui.QIcon(":/images/match-70.png"),
            QtGui.QIcon(":/images/match-80.png"),
            QtGui.QIcon(":/images/match-90.png"),
            QtGui.QIcon(":/images/match-100.png"),
        ]
        FileItem.match_pending_icons = [
            QtGui.QIcon(":/images/match-pending-50.png"),
            QtGui.QIcon(":/images/match-pending-60.png"),
            QtGui.QIcon(":/images/match-pending-70.png"),
            QtGui.QIcon(":/images/match-pending-80.png"),
            QtGui.QIcon(":/images/match-pending-90.png"),
            QtGui.QIcon(":/images/match-pending-100.png"),
        ]
        self.icon_plugins = icontheme.lookup('applications-system',
                                             icontheme.ICON_SIZE_MENU)

    def selected_objects(self):
        return [i.obj for i in self.views[self._selected_view].selectedItems()]

    def update_selection(self, i, j):
        self._selected_view = i
        self.views[j].clearSelection()
        self.window.updateSelection(self.selected_objects())

    def update_selection_0(self):
        if not self._ignore_selection_changes:
            self._ignore_selection_changes = True
            self.update_selection(0, 1)
            self._ignore_selection_changes = False

    def update_selection_1(self):
        if not self._ignore_selection_changes:
            self._ignore_selection_changes = True
            self.update_selection(1, 0)
            self._ignore_selection_changes = False
Exemple #24
0
    def test_var_opt_invalid_value(self):
        Option("setting", "var_option", set(["a", "b"]))

        # store invalid value in config file directly
        self.config.setValue('setting/var_option', object)
        self.assertEqual(self.config.setting["var_option"], set(["a", "b"]))
Exemple #25
0
class MainPanel(QtWidgets.QSplitter):

    options = [
        Option("persist", "splitter_state", QtCore.QByteArray()),
    ]

    columns = [
        (N_('Title'), 'title'),
        (N_('Length'), '~length'),
        (N_('Artist'), 'artist'),
        (N_('Album Artist'), 'albumartist'),
        (N_('Composer'), 'composer'),
        (N_('Album'), 'album'),
        (N_('Disc Subtitle'), 'discsubtitle'),
        (N_('Track No.'), 'tracknumber'),
        (N_('Disc No.'), 'discnumber'),
        (N_('Catalog No.'), 'catalognumber'),
        (N_('Barcode'), 'barcode'),
        (N_('Media'), 'media'),
        (N_('Genre'), 'genre'),
        (N_('Fingerprint status'), '~fingerprint'),
        (N_('Date'), 'date'),
        (N_('Original Release Date'), 'originaldate'),
    ]

    _column_indexes = {column[1]: i for i, column in enumerate(columns)}

    TITLE_COLUMN = _column_indexes['title']
    TRACKNUMBER_COLUMN = _column_indexes['tracknumber']
    DISCNUMBER_COLUMN = _column_indexes['discnumber']
    LENGTH_COLUMN = _column_indexes['~length']
    FINGERPRINT_COLUMN = _column_indexes['~fingerprint']

    NAT_SORT_COLUMNS = [
        _column_indexes['title'],
        _column_indexes['album'],
        _column_indexes['discsubtitle'],
        _column_indexes['tracknumber'],
        _column_indexes['discnumber'],
        _column_indexes['catalognumber'],
    ]

    def __init__(self, window, parent=None):
        super().__init__(parent)
        self.setChildrenCollapsible(False)
        self.window = window
        self.create_icons()
        self._views = [FileTreeView(window, self), AlbumTreeView(window, self)]
        self._selected_view = self._views[0]
        self._ignore_selection_changes = False

        def _view_update_selection(view):
            if not self._ignore_selection_changes:
                self._ignore_selection_changes = True
                self._update_selection(view)
                self._ignore_selection_changes = False

        for view in self._views:
            view.itemSelectionChanged.connect(
                partial(_view_update_selection, view))

        TreeItem.window = window
        TreeItem.base_color = self.palette().base().color()
        TreeItem.text_color = self.palette().text().color()
        TreeItem.text_color_secondary = self.palette() \
            .brush(QtGui.QPalette.Disabled, QtGui.QPalette.Text).color()
        TrackItem.track_colors = defaultdict(
            lambda: TreeItem.text_color, {
                File.NORMAL: interface_colors.get_qcolor('entity_saved'),
                File.CHANGED: TreeItem.text_color,
                File.PENDING: interface_colors.get_qcolor('entity_pending'),
                File.ERROR: interface_colors.get_qcolor('entity_error'),
            })
        FileItem.file_colors = defaultdict(
            lambda: TreeItem.text_color, {
                File.NORMAL: TreeItem.text_color,
                File.CHANGED: TreeItem.text_color,
                File.PENDING: interface_colors.get_qcolor('entity_pending'),
                File.ERROR: interface_colors.get_qcolor('entity_error'),
            })

    def set_processing(self, processing=True):
        self._ignore_selection_changes = processing

    def tab_order(self, tab_order, before, after):
        prev = before
        for view in self._views:
            tab_order(prev, view)
            prev = view
        tab_order(prev, after)

    def save_state(self):
        config = get_config()
        config.persist["splitter_state"] = self.saveState()
        for view in self._views:
            view.save_state()

    @restore_method
    def restore_state(self):
        config = get_config()
        self.restoreState(config.persist["splitter_state"])

    def create_icons(self):
        if hasattr(QtWidgets.QStyle, 'SP_DirIcon'):
            ClusterItem.icon_dir = self.style().standardIcon(
                QtWidgets.QStyle.SP_DirIcon)
        else:
            ClusterItem.icon_dir = icontheme.lookup('folder',
                                                    icontheme.ICON_SIZE_MENU)
        AlbumItem.icon_cd = icontheme.lookup('media-optical',
                                             icontheme.ICON_SIZE_MENU)
        AlbumItem.icon_cd_modified = icontheme.lookup('media-optical-modified',
                                                      icontheme.ICON_SIZE_MENU)
        AlbumItem.icon_cd_saved = icontheme.lookup('media-optical-saved',
                                                   icontheme.ICON_SIZE_MENU)
        AlbumItem.icon_cd_saved_modified = icontheme.lookup(
            'media-optical-saved-modified', icontheme.ICON_SIZE_MENU)
        AlbumItem.icon_error = icontheme.lookup('media-optical-error',
                                                icontheme.ICON_SIZE_MENU)
        TrackItem.icon_audio = QtGui.QIcon(":/images/track-audio.png")
        TrackItem.icon_video = QtGui.QIcon(":/images/track-video.png")
        TrackItem.icon_data = QtGui.QIcon(":/images/track-data.png")
        TrackItem.icon_error = icontheme.lookup('dialog-error',
                                                icontheme.ICON_SIZE_MENU)
        FileItem.icon_file = QtGui.QIcon(":/images/file.png")
        FileItem.icon_file_pending = QtGui.QIcon(":/images/file-pending.png")
        FileItem.icon_error = icontheme.lookup('dialog-error',
                                               icontheme.ICON_SIZE_MENU)
        FileItem.icon_saved = QtGui.QIcon(":/images/track-saved.png")
        FileItem.icon_fingerprint = icontheme.lookup('fingerprint',
                                                     icontheme.ICON_SIZE_MENU)
        FileItem.icon_fingerprint_gray = icontheme.lookup(
            'fingerprint-gray', icontheme.ICON_SIZE_MENU)
        FileItem.match_icons = [
            QtGui.QIcon(":/images/match-50.png"),
            QtGui.QIcon(":/images/match-60.png"),
            QtGui.QIcon(":/images/match-70.png"),
            QtGui.QIcon(":/images/match-80.png"),
            QtGui.QIcon(":/images/match-90.png"),
            QtGui.QIcon(":/images/match-100.png"),
        ]
        FileItem.match_icons_info = [
            N_("Bad match"),
            N_("Poor match"),
            N_("Ok match"),
            N_("Good match"),
            N_("Great match"),
            N_("Excellent match"),
        ]
        FileItem.match_pending_icons = [
            QtGui.QIcon(":/images/match-pending-50.png"),
            QtGui.QIcon(":/images/match-pending-60.png"),
            QtGui.QIcon(":/images/match-pending-70.png"),
            QtGui.QIcon(":/images/match-pending-80.png"),
            QtGui.QIcon(":/images/match-pending-90.png"),
            QtGui.QIcon(":/images/match-pending-100.png"),
        ]
        self.icon_plugins = icontheme.lookup('applications-system',
                                             icontheme.ICON_SIZE_MENU)

    def _update_selection(self, selected_view):
        for view in self._views:
            if view != selected_view:
                view.clearSelection()
            else:
                self._selected_view = view
                self.window.update_selection(
                    [item.obj for item in view.selectedItems()])

    def update_current_view(self):
        self._update_selection(self._selected_view)

    def remove(self, objects):
        self._ignore_selection_changes = True
        self.tagger.remove(objects)
        self._ignore_selection_changes = False

        view = self._selected_view
        index = view.currentIndex()
        if index.isValid():
            # select the current index
            view.setCurrentIndex(index)
        else:
            self.update_current_view()

    def set_sorting(self, sort=True):
        for view in self._views:
            view.setSortingEnabled(sort)

    def collapse_clusters(self, collapse=True):
        if collapse:
            self._views[0].collapseAll()
        else:
            self._views[0].expandAll()
Exemple #26
0
    def test_var_opt_set_empty(self):
        Option("setting", "var_option", set(["a", "b"]))

        # set option to ""
        self.config.setting["var_option"] = set()
        self.assertEqual(self.config.setting["var_option"], set())
Exemple #27
0
class TrackSearchDialog(SearchDialog):

    dialog_header_state = "tracksearchdialog_header_state"

    options = [Option("persist", dialog_header_state, QtCore.QByteArray())]

    def __init__(self, parent):
        super().__init__(parent,
                         accept_button_title=_("Load into Picard"),
                         search_type="track")
        self.file_ = None
        self.setWindowTitle(_("Track Search Results"))
        self.columns = [
            ('name', _("Name")),
            ('length', _("Length")),
            ('artist', _("Artist")),
            ('release', _("Release")),
            ('date', _("Date")),
            ('country', _("Country")),
            ('type', _("Type")),
            ('score', _("Score")),
        ]

    def search(self, text):
        """Perform search using query provided by the user."""
        self.retry_params = Retry(self.search, text)
        self.search_box_text(text)
        self.show_progress()
        self.tagger.mb_api.find_tracks(
            self.handle_reply,
            query=text,
            search=True,
            advanced_search=self.use_advanced_search,
            limit=QUERY_LIMIT)

    def load_similar_tracks(self, file_):
        """Perform search using existing metadata information
        from the file as query."""
        self.retry_params = Retry(self.load_similar_tracks, file_)
        self.file_ = file_
        metadata = file_.orig_metadata
        query = {
            'track': metadata['title'],
            'artist': metadata['artist'],
            'release': metadata['album'],
            'tnum': metadata['tracknumber'],
            'tracks': metadata['totaltracks'],
            'qdur': str(metadata.length // 2000),
            'isrc': metadata['isrc'],
        }

        # Generate query to be displayed to the user (in search box).
        # If advanced query syntax setting is enabled by user, display query in
        # advanced syntax style. Otherwise display only track title.
        if self.use_advanced_search:
            query_str = ' '.join([
                '%s:(%s)' % (item, escape_lucene_query(value))
                for item, value in query.items() if value
            ])
        else:
            query_str = query["track"]

        query["limit"] = QUERY_LIMIT
        self.search_box_text(query_str)
        self.show_progress()
        self.tagger.mb_api.find_tracks(self.handle_reply, **query)

    def retry(self):
        self.retry_params.function(self.retry_params.query)

    def handle_reply(self, document, http, error):
        if error:
            self.network_error(http, error)
            return

        try:
            tracks = document['recordings']
        except (KeyError, TypeError):
            self.no_results_found()
            return

        if self.file_:
            metadata = self.file_.orig_metadata

            def candidates():
                for track in tracks:
                    yield metadata.compare_to_track(track,
                                                    File.comparison_weights)

            tracks = [
                result.track for result in sort_by_similarity(candidates)
            ]

        del self.search_results[:]  # Clear existing data
        self.parse_tracks(tracks)
        self.display_results()

    def display_results(self):
        self.prepare_table()
        for row, obj in enumerate(self.search_results):
            track = obj[0]
            self.table.insertRow(row)
            self.set_table_item(row, 'name', track, "title")
            self.set_table_item(row,
                                'length',
                                track,
                                "~length",
                                sortkey=track.length)
            self.set_table_item(row, 'artist', track, "artist")
            self.set_table_item(row, 'release', track, "album")
            self.set_table_item(row, 'date', track, "date")
            self.set_table_item(row, 'country', track, "country")
            self.set_table_item(row, 'type', track, "releasetype")
            self.set_table_item(row, 'score', track, "score")
        self.show_table(sort_column='score')

    def parse_tracks(self, tracks):
        for node in tracks:
            if "releases" in node:
                for rel_node in node['releases']:
                    track = Metadata()
                    recording_to_metadata(node, track)
                    track['score'] = node['score']
                    release_to_metadata(rel_node, track)
                    rg_node = rel_node['release-group']
                    release_group_to_metadata(rg_node, track)
                    countries = countries_from_node(rel_node)
                    if countries:
                        track["country"] = ", ".join(countries)
                    self.search_results.append((track, node))
            else:
                # This handles the case when no release is associated with a track
                # i.e. the track is an NAT
                track = Metadata()
                recording_to_metadata(node, track)
                track['score'] = node['score']
                track["album"] = _("Standalone Recording")
                self.search_results.append((track, node))

    def accept_event(self, rows):
        for row in rows:
            self.load_selection(row)

    def load_selection(self, row):
        """Load the album corresponding to the selected track.
        If the search is performed for a file, also associate the file to
        corresponding track in the album.
        """

        track, node = self.search_results[row]
        if track.get("musicbrainz_albumid"):
            # The track is not an NAT
            self.tagger.get_release_group_by_id(
                track["musicbrainz_releasegroupid"]).loaded_albums.add(
                    track["musicbrainz_albumid"])
            if self.file_:
                # Search is performed for a file.
                # Have to move that file from its existing album to the new one.
                if isinstance(self.file_.parent, Track):
                    album = self.file_.parent.album
                    self.tagger.move_file_to_track(
                        self.file_, track["musicbrainz_albumid"],
                        track["musicbrainz_recordingid"])
                    if album.get_num_total_files() == 0:
                        # Remove album if it has no more files associated
                        self.tagger.remove_album(album)
                else:
                    self.tagger.move_file_to_track(
                        self.file_, track["musicbrainz_albumid"],
                        track["musicbrainz_recordingid"])
            else:
                # No files associated. Just a normal search.
                self.tagger.load_album(track["musicbrainz_albumid"])
        else:
            if self.file_ and getattr(self.file_.parent, 'album', None):
                album = self.file_.parent.album
                self.tagger.move_file_to_nat(self.file_,
                                             track["musicbrainz_recordingid"],
                                             node)
                if album.get_num_total_files() == 0:
                    self.tagger.remove_album(album)
            else:
                self.tagger.load_nat(track["musicbrainz_recordingid"], node)
                self.tagger.move_file_to_nat(self.file_,
                                             track["musicbrainz_recordingid"],
                                             node)
Exemple #28
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),
        Option("persist", "scripting_splitter", QtCore.QByteArray()),
    ]

    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_ScriptingOptionsPage()
        self.ui.setupUi(self)
        self.ui.tagger_script.setEnabled(False)
        self.ui.splitter.setStretchFactor(0, 1)
        self.ui.splitter.setStretchFactor(1, 2)
        self.move_view = MoveableListView(self.ui.script_list, self.ui.move_up_button,
                                          self.ui.move_down_button)
        self.ui.scripting_documentation_button.clicked.connect(self.show_scripting_documentation)
        self.scripting_documentation_shown = None

    def show_scripting_documentation(self):
        if not self.scripting_documentation_shown:
            self.scriptdoc_dialog = ScriptingDocumentationDialog(parent=self)
            self.scriptdoc_dialog.show()
        else:
            self.scriptdoc_dialog.raise_()
            self.scriptdoc_dialog.activateWindow()

    def enable_tagger_scripts_toggled(self, on):
        if on and self.ui.script_list.count() == 0:
            self.ui.script_list.add_script()

    def script_selected(self):
        items = self.ui.script_list.selectedItems()
        if items:
            item = items[0]
            self.ui.tagger_script.setEnabled(True)
            self.ui.tagger_script.setText(item.script)
            self.ui.tagger_script.setFocus(QtCore.Qt.OtherFocusReason)
        else:
            self.ui.tagger_script.setEnabled(False)
            self.ui.tagger_script.setText("")

    def live_update_and_check(self):
        items = self.ui.script_list.selectedItems()
        if items:
            script = items[0]
            script.script = self.ui.tagger_script.toPlainText()
        self.ui.script_error.setStyleSheet("")
        self.ui.script_error.setText("")
        try:
            self.check()
        except OptionsCheckError as e:
            self.ui.script_error.setStyleSheet(self.STYLESHEET_ERROR)
            self.ui.script_error.setText(e.info)
            return

    def check(self):
        parser = ScriptParser()
        try:
            parser.eval(self.ui.tagger_script.toPlainText())
        except Exception as e:
            raise ScriptCheckError(_("Script Error"), str(e))

    def restore_defaults(self):
        # Remove existing scripts
        self.ui.script_list.clear()
        self.ui.tagger_script.setText("")
        super().restore_defaults()

    def load(self):
        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)

        self.restore_state()

    def _all_scripts(self):
        for row in range(0, self.ui.script_list.count()):
            item = self.ui.script_list.item(row)
            yield item.get_all()

    @restore_method
    def restore_state(self):
        # Preserve previous splitter position
        config = get_config()
        self.ui.splitter.restoreState(config.persist["scripting_splitter"])

    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()
        config.persist["scripting_splitter"] = self.ui.splitter.saveState()

    def display_error(self, error):
        # Ignore scripting errors, those are handled inline
        if not isinstance(error, ScriptCheckError):
            super().display_error(error)
Exemple #29
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()
Exemple #30
0
 def __init__(self):
     Option("persist", self.opt_name(), QtCore.QByteArray())
     if self.autorestore:
         self.restore_geometry()
     if getattr(self, 'finished', None):
         self.finished.connect(self.save_geometry)
Exemple #31
0
class BaseTreeView(QtGui.QTreeWidget):

    options = [
        TextOption("persist", "file_view_sizes", "250 40 100"),
        TextOption("persist", "album_view_sizes", "250 40 100"),
        Option("setting", "color_modified",
               QtGui.QColor(QtGui.QPalette.WindowText), QtGui.QColor),
        Option("setting", "color_saved", QtGui.QColor(0, 128, 0),
               QtGui.QColor),
        Option("setting", "color_error", QtGui.QColor(200, 0, 0),
               QtGui.QColor),
        Option("setting", "color_pending", QtGui.QColor(128, 128, 128),
               QtGui.QColor),
    ]

    def __init__(self, window, parent=None):
        QtGui.QTreeWidget.__init__(self, parent)
        self.window = window
        self.panel = parent

        self.numHeaderSections = len(MainPanel.columns)
        self.setHeaderLabels([_(h) for h, n in MainPanel.columns])
        self.restore_state()

        self.setAcceptDrops(True)
        self.setDragEnabled(True)
        self.setDropIndicatorShown(True)
        self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)

        # enable sorting, but don't actually use it by default
        # XXX it would be nice to be able to go to the 'no sort' mode, but the
        #     internal model that QTreeWidget uses doesn't support it
        self.header().setSortIndicator(-1, QtCore.Qt.AscendingOrder)
        self.setSortingEnabled(True)

        self.expand_all_action = QtGui.QAction(_("&Expand all"), self)
        self.expand_all_action.triggered.connect(self.expandAll)
        self.collapse_all_action = QtGui.QAction(_("&Collapse all"), self)
        self.collapse_all_action.triggered.connect(self.collapseAll)
        self.doubleClicked.connect(self.activate_item)

    def contextMenuEvent(self, event):
        item = self.itemAt(event.pos())
        if not item:
            return
        obj = item.obj

        plugin_actions = None
        menu = QtGui.QMenu(self)
        if isinstance(obj, Track):
            menu.addAction(self.window.edit_tags_action)
            plugin_actions = list(_track_actions)
            if obj.num_linked_files == 1:
                menu.addAction(self.window.open_file_action)
                menu.addAction(self.window.open_folder_action)
                plugin_actions.extend(_file_actions)
            menu.addSeparator()
            if isinstance(obj, NonAlbumTrack):
                menu.addAction(self.window.refresh_action)
        elif isinstance(obj, Cluster):
            menu.addAction(self.window.autotag_action)
            menu.addAction(self.window.analyze_action)
            if isinstance(obj, UnmatchedFiles):
                menu.addAction(self.window.cluster_action)
            plugin_actions = list(_cluster_actions)
        elif isinstance(obj, ClusterList):
            menu.addAction(self.window.autotag_action)
            menu.addAction(self.window.analyze_action)
            plugin_actions = list(_cluster_actions)
        elif isinstance(obj, File):
            menu.addAction(self.window.edit_tags_action)
            menu.addAction(self.window.open_file_action)
            menu.addAction(self.window.open_folder_action)
            menu.addSeparator()
            menu.addAction(self.window.autotag_action)
            menu.addAction(self.window.analyze_action)
            plugin_actions = list(_file_actions)
        elif isinstance(obj, Album):
            menu.addAction(self.window.refresh_action)
            plugin_actions = list(_album_actions)

        menu.addAction(self.window.save_action)
        menu.addAction(self.window.remove_action)

        if isinstance(obj,
                      Album) and not isinstance(obj, NatAlbum) and obj.loaded:
            releases_menu = QtGui.QMenu(_("&Other versions"), menu)
            menu.addSeparator()
            menu.addMenu(releases_menu)
            loading = releases_menu.addAction(_('Loading...'))
            loading.setEnabled(False)

            def _add_other_versions():
                releases_menu.removeAction(loading)
                actions = []
                for i, version in enumerate(obj.other_versions):
                    keys = ("date", "country", "labels", "catnums", "tracks",
                            "format")
                    name = " / ".join([version[k] for k in keys
                                       if version[k]]).replace("&", "&&")
                    if name == version["tracks"]:
                        name = "%s / %s" % (_('[no release info]'), name)
                    action = releases_menu.addAction(name)
                    action.setCheckable(True)
                    if obj.id == version["mbid"]:
                        action.setChecked(True)
                    action.triggered.connect(
                        partial(obj.switch_release_version, version["mbid"]))

            if obj.rgloaded:
                _add_other_versions()
            elif obj.rgid:
                obj.release_group_loaded.connect(_add_other_versions)
                kwargs = {"release-group": obj.rgid, "limit": 100}
                self.tagger.xmlws.browse_releases(
                    obj._release_group_request_finished, **kwargs)

        if plugin_actions:
            plugin_menu = QtGui.QMenu(_("&Plugins"), menu)
            plugin_menu.addActions(plugin_actions)
            plugin_menu.setIcon(self.panel.icon_plugins)
            menu.addSeparator()
            menu.addMenu(plugin_menu)

        if isinstance(obj, Cluster) or isinstance(
                obj, ClusterList) or isinstance(obj, Album):
            menu.addAction(self.expand_all_action)
            menu.addAction(self.collapse_all_action)

        menu.exec_(event.globalPos())
        event.accept()

    def restore_state(self):
        if self.__class__.__name__ == "FileTreeView":
            sizes = self.config.persist["file_view_sizes"]
        else:
            sizes = self.config.persist["album_view_sizes"]
        header = self.header()
        sizes = sizes.split(" ")
        try:
            for i in range(self.numHeaderSections - 1):
                header.resizeSection(i, int(sizes[i]))
        except IndexError:
            pass

    def save_state(self):
        sizes = []
        header = self.header()
        for i in range(self.numHeaderSections - 1):
            sizes.append(str(self.header().sectionSize(i)))
        sizes = " ".join(sizes)
        if self.__class__.__name__ == "FileTreeView":
            self.config.persist["file_view_sizes"] = sizes
        else:
            self.config.persist["album_view_sizes"] = sizes

    def supportedDropActions(self):
        return QtCore.Qt.CopyAction | QtCore.Qt.MoveAction

    def mimeTypes(self):
        """List of MIME types accepted by this view."""
        return [
            "text/uri-list", "application/picard.file-list",
            "application/picard.album-list"
        ]

    def dragEnterEvent(self, event):
        if event.mimeData().hasUrls():
            event.setDropAction(QtCore.Qt.CopyAction)
            event.accept()
        else:
            event.acceptProposedAction()

    def startDrag(self, supportedActions):
        """Start drag, *without* using pixmap."""
        items = self.selectedItems()
        if items:
            drag = QtGui.QDrag(self)
            drag.setMimeData(self.mimeData(items))
            drag.start(supportedActions)

    def mimeData(self, items):
        """Return MIME data for specified items."""
        album_ids = []
        file_ids = []
        for item in items:
            obj = item.obj
            if isinstance(obj, Album):
                album_ids.append(str(obj.id))
            elif isinstance(obj, Track):
                for file in obj.linked_files:
                    file_ids.append(str(file.id))
            elif isinstance(obj, File):
                file_ids.append(str(obj.id))
            elif isinstance(obj, Cluster):
                for file in obj.files:
                    file_ids.append(str(file.id))
            elif isinstance(obj, ClusterList):
                for cluster in obj:
                    for file in cluster.files:
                        file_ids.append(str(file.id))
        mimeData = QtCore.QMimeData()
        mimeData.setData("application/picard.album-list", "\n".join(album_ids))
        mimeData.setData("application/picard.file-list", "\n".join(file_ids))
        return mimeData

    def drop_files(self, files, target):
        if isinstance(target, (Track, Cluster)):
            for file in files:
                file.move(target)
        elif isinstance(target, File):
            for file in files:
                file.move(target.parent)
        elif isinstance(target, Album):
            self.tagger.move_files_to_album(files, album=target)
        elif isinstance(target, ClusterList):
            self.tagger.cluster(files)

    def drop_albums(self, albums, target):
        files = self.tagger.get_files_from_objects(albums)
        if isinstance(target, Cluster):
            for file in files:
                file.move(target)
        elif isinstance(target, Album):
            self.tagger.move_files_to_album(files, album=target)
        elif isinstance(target, ClusterList):
            self.tagger.cluster(files)

    def drop_urls(self, urls, target):
        # URL -> Unmatched Files
        # TODO: use the drop target to move files to specific albums/tracks/clusters
        files = []
        for url in urls:
            if url.scheme() == "file" or not url.scheme():
                filename = unicode(url.toLocalFile())
                if os.path.isdir(encode_filename(filename)):
                    self.tagger.add_directory(filename)
                else:
                    files.append(filename)
            elif url.scheme() == "http":
                path = unicode(url.path())
                match = re.search(r"/(release|recording)/([0-9a-z\-]{36})",
                                  path)
                if not match:
                    continue
                entity = match.group(1)
                mbid = match.group(2)
                if entity == "release":
                    self.tagger.load_album(mbid)
                elif entity == "recording":
                    self.tagger.load_nat(mbid)
        if files:
            self.tagger.add_files(files)

    def dropEvent(self, event):
        return QtGui.QTreeView.dropEvent(self, event)

    def dropMimeData(self, parent, index, data, action):
        target = None
        if parent:
            if index == parent.childCount():
                item = parent
            else:
                item = parent.child(index)
            if item is not None:
                target = item.obj
        self.log.debug("Drop target = %r", target)
        handled = False
        # text/uri-list
        urls = data.urls()
        if urls:
            if target is None:
                target = self.tagger.unmatched_files
            self.drop_urls(urls, target)
            handled = True
        # application/picard.file-list
        files = data.data("application/picard.file-list")
        if files:
            files = [
                self.tagger.get_file_by_id(int(file_id))
                for file_id in str(files).split("\n")
            ]
            self.drop_files(files, target)
            handled = True
        # application/picard.album-list
        albums = data.data("application/picard.album-list")
        if albums:
            albums = [
                self.tagger.load_album(id) for id in str(albums).split("\n")
            ]
            self.drop_albums(albums, target)
            handled = True
        return handled

    def activate_item(self, index):
        obj = self.itemFromIndex(index).obj
        if obj.can_edit_tags():
            self.window.edit_tags([obj])

    def add_cluster(self, cluster, parent_item=None):
        if parent_item is None:
            parent_item = self.clusters
        cluster_item = ClusterItem(cluster, not cluster.special, parent_item)
        if cluster.hide_if_empty and not cluster.files:
            cluster_item.update()
            cluster_item.setHidden(True)
        else:
            cluster_item.add_files(cluster.files)