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'])
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)
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")
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)))
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")
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))
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)
def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_CoverOptionsPage() self.ui.setupUi(self) self.ui.cover_image_filename.setPlaceholderText(Option.get('setting', 'cover_image_filename').default) self.ui.save_images_to_files.clicked.connect(self.update_ca_providers_groupbox_state) self.ui.save_images_to_tags.clicked.connect(self.update_ca_providers_groupbox_state) self.ui.save_only_one_front_image.toggled.connect(self.ui.image_type_as_filename.setDisabled) self.move_view = MoveableListView(self.ui.ca_providers_list, self.ui.up_button, self.ui.down_button)
def 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
def test_var_opt_convert(self): opt = Option("setting", "var_option", set()) self.assertEqual(opt.convert(["a", "b", "a"]), {"a", "b"})
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
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()
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)
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)
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"])
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)
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"]))
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)
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)
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)
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)
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
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"]))
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()
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())
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)
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)
class OptionsDialog(PicardDialog, SingletonDialog): autorestore = False options = [ TextOption("persist", "options_last_active_page", ""), ListOption("persist", "options_pages_tree_state", []), Option("persist", "options_splitter", QtCore.QByteArray()), ] def add_pages(self, parent, default_page, parent_item): pages = [(p.SORT_ORDER, p.NAME, p) for p in self.pages if p.PARENT == parent] items = [] for foo, bar, page in sorted(pages): item = HashableTreeWidgetItem(parent_item) item.setText(0, _(page.TITLE)) if page.ACTIVE: self.item_to_page[item] = page self.page_to_item[page.NAME] = item self.ui.pages_stack.addWidget(page) else: item.setFlags(QtCore.Qt.ItemIsEnabled) self.add_pages(page.NAME, default_page, item) if page.NAME == default_page: self.default_item = item items.append(item) if not self.default_item and not parent: self.default_item = items[0] def __init__(self, default_page=None, parent=None): super().__init__(parent) self.setWindowModality(QtCore.Qt.ApplicationModal) self.setAttribute(QtCore.Qt.WA_DeleteOnClose) from picard.ui.ui_options import Ui_Dialog self.ui = Ui_Dialog() self.ui.setupUi(self) self.ui.reset_all_button = QtWidgets.QPushButton( _("&Restore all Defaults")) self.ui.reset_all_button.setToolTip( _("Reset all of Picard's settings")) self.ui.reset_button = QtWidgets.QPushButton(_("Restore &Defaults")) self.ui.reset_button.setToolTip( _("Reset all settings for current option page")) ok = StandardButton(StandardButton.OK) ok.setText(_("Make It So!")) self.ui.buttonbox.addButton(ok, QtWidgets.QDialogButtonBox.AcceptRole) self.ui.buttonbox.addButton(StandardButton(StandardButton.CANCEL), QtWidgets.QDialogButtonBox.RejectRole) self.ui.buttonbox.addButton(StandardButton(StandardButton.HELP), QtWidgets.QDialogButtonBox.HelpRole) self.ui.buttonbox.addButton(self.ui.reset_all_button, QtWidgets.QDialogButtonBox.ActionRole) self.ui.buttonbox.addButton(self.ui.reset_button, QtWidgets.QDialogButtonBox.ActionRole) self.ui.buttonbox.accepted.connect(self.accept) self.ui.buttonbox.rejected.connect(self.reject) self.ui.reset_all_button.clicked.connect(self.confirm_reset_all) self.ui.reset_button.clicked.connect(self.confirm_reset) self.ui.buttonbox.helpRequested.connect(self.show_help) self.pages = [] for Page in page_classes: try: page = Page(self.ui.pages_stack) self.pages.append(page) except Exception: log.exception('Failed initializing options page %r', page) self.item_to_page = {} self.page_to_item = {} self.default_item = None if not default_page: config = get_config() default_page = config.persist["options_last_active_page"] self.add_pages(None, default_page, self.ui.pages_tree) # work-around to set optimal option pane width self.ui.pages_tree.expandAll() max_page_name = self.ui.pages_tree.sizeHintForColumn( 0) + 2 * self.ui.pages_tree.frameWidth() self.ui.splitter.setSizes( [max_page_name, self.geometry().width() - max_page_name]) self.ui.pages_tree.setHeaderLabels([""]) self.ui.pages_tree.header().hide() self.ui.pages_tree.itemSelectionChanged.connect(self.switch_page) self.restoreWindowState() self.finished.connect(self.saveWindowState) for page in self.pages: try: page.load() except Exception: log.exception('Failed loading options page %r', page) self.disable_page(page.NAME) self.ui.pages_tree.setCurrentItem(self.default_item) def switch_page(self): items = self.ui.pages_tree.selectedItems() if items: config = get_config() page = self.item_to_page[items[0]] config.persist["options_last_active_page"] = page.NAME self.ui.pages_stack.setCurrentWidget(page) def disable_page(self, name): item = self.page_to_item[name] item.setDisabled(True) @property def help_url(self): current_page = self.ui.pages_stack.currentWidget() url = current_page.HELP_URL # If URL is empty, use the first non empty parent help URL. while current_page.PARENT and not url: current_page = self.item_to_page[self.page_to_item[ current_page.PARENT]] url = current_page.HELP_URL if not url: url = 'doc_options' # key in PICARD_URLS return url def accept(self): for page in self.pages: try: page.check() except OptionsCheckError as e: self._show_page_error(page, e) return except Exception as e: log.exception('Failed checking options page %r', page) self._show_page_error(page, e) return for page in self.pages: try: page.save() except Exception as e: log.exception('Failed saving options page %r', page) self._show_page_error(page, e) return super().accept() def _show_page_error(self, page, error): if not isinstance(error, OptionsCheckError): error = OptionsCheckError(_('Unexpected error'), str(error)) self.ui.pages_tree.setCurrentItem(self.page_to_item[page.NAME]) page.display_error(error) def saveWindowState(self): expanded_pages = [] for page, item in self.page_to_item.items(): index = self.ui.pages_tree.indexFromItem(item) is_expanded = self.ui.pages_tree.isExpanded(index) expanded_pages.append((page, is_expanded)) config = get_config() config.persist["options_pages_tree_state"] = expanded_pages config.persist["options_splitter"] = self.ui.splitter.saveState() @restore_method def restoreWindowState(self): config = get_config() pages_tree_state = config.persist["options_pages_tree_state"] if not pages_tree_state: self.ui.pages_tree.expandAll() else: for page, is_expanded in pages_tree_state: try: item = self.page_to_item[page] except KeyError: continue item.setExpanded(is_expanded) self.restore_geometry() self.ui.splitter.restoreState(config.persist["options_splitter"]) def restore_all_defaults(self): for page in self.pages: page.restore_defaults() def restore_page_defaults(self): self.ui.pages_stack.currentWidget().restore_defaults() def confirm_reset(self): msg = _("You are about to reset your options for this page.") self._show_dialog(msg, self.restore_page_defaults) def confirm_reset_all(self): msg = _("Warning! This will reset all of your settings.") self._show_dialog(msg, self.restore_all_defaults) def _show_dialog(self, msg, function): message_box = QtWidgets.QMessageBox(self) message_box.setIcon(QtWidgets.QMessageBox.Warning) message_box.setWindowModality(QtCore.Qt.WindowModal) message_box.setWindowTitle(_("Confirm Reset")) message_box.setText(_("Are you sure?") + "\n\n" + msg) message_box.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) if message_box.exec_() == QtWidgets.QMessageBox.Yes: function()
def __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)
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)