class AlbumTreeView(BaseTreeView): header_state = config.Option("persist", "album_view_header_state", QtCore.QByteArray()) 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(0, 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))
class FileTreeView(BaseTreeView): header_state = config.Option("persist", "file_view_header_state", QtCore.QByteArray()) 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(0, '%s (%d)' % (_("Clusters"), len(self.tagger.clusters)))
class HistoryView(LogViewCommon): options = [ config.Option("persist", "historyview_position", QtCore.QPoint()), config.Option("persist", "historyview_size", QtCore.QSize(LogViewCommon.WIDTH, LogViewCommon.HEIGHT)), ] def __init__(self, parent=None): super().__init__(log.history_tail, _("Activity History"), w=self.WIDTH, h=self.HEIGHT, parent=parent) self.restoreWindowState("historyview_position", "historyview_size") def closeEvent(self, event): self.saveWindowState("historyview_position", "historyview_size") super().closeEvent(event)
class HistoryView(LogViewCommon): options = [ config.Option("persist", "historyview_position", QtCore.QPoint()), config.Option("persist", "historyview_size", QtCore.QSize(560, 400)), ] def __init__(self, parent=None): title = _("Activity History") logger = log.history_logger LogViewCommon.__init__(self, title, logger, parent=parent) self.restoreWindowState("historyview_position", "historyview_size") def _formatted_log_line(self, level, time, msg): return log.formatted_log_line(level, time, msg, level_prefixes=False) def closeEvent(self, event): self.saveWindowState("historyview_position", "historyview_size") event.accept()
class LogView(LogViewCommon): options = [ config.Option("persist", "logview_position", QtCore.QPoint()), config.Option("persist", "logview_size", QtCore.QSize(560, 400)), ] def __init__(self, parent=None): title = _("Log") logger = log.main_logger LogViewCommon.__init__(self, title, logger, parent=parent) self.restoreWindowState("logview_position", "logview_size") cb = QtWidgets.QCheckBox(_('Debug mode'), self) cb.setChecked(QtCore.QObject.tagger._debug) cb.stateChanged.connect(self.toggleDebug) self.vbox.addWidget(cb) def toggleDebug(self, state): QtCore.QObject.tagger.debug(state == QtCore.Qt.Checked) def closeEvent(self, event): self.saveWindowState("logview_position", "logview_size") event.accept()
class TrackSearchDialog(SearchDialog): dialog_header_state = "tracksearchdialog_header_state" options = [ config.Option("persist", dialog_header_state, QtCore.QByteArray()) ] def __init__(self, parent): super().__init__(parent, accept_button_title=_("Load into Picard")) 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, 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': string_(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 config.setting["use_adv_search_syntax"]: 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_: sorted_results = sorted((self.file_.orig_metadata.compare_to_track( track, File.comparison_weights) for track in tracks), reverse=True, key=itemgetter(0)) tracks = [item[3] for item in sorted_results] 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", sort=BY_DURATION) 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", sort=BY_NUMBER) 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 = country_list_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, arg): self.load_selection(arg) 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._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._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 AlbumSearchDialog(SearchDialog): dialog_header_state = "albumsearchdialog_header_state" options = [ config.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 ArtistSearchDialog(SearchDialog): options = [ config.Option("persist", "artistsearchdialog_window_size", QtCore.QSize(720, 360)), config.Option("persist", "artistsearchdialog_header_state", QtCore.QByteArray()) ] def __init__(self, parent): super(ArtistSearchDialog, self).__init__( parent, accept_button_title=_("Show in browser")) self.setWindowTitle(_("Artist Search Dialog")) self.table_headers = [ _("Name"), _("Type"), _("Gender"), _("Area"), _("Begin"), _("Begin Area"), _("End"), _("End Area"), ] def search(self, text): self.retry_params = (self.search, text) self.search_box.search_edit.setText(text) self.show_progress() self.tagger.xmlws.find_artists(self.handle_reply, query=text, search=True, limit=QUERY_LIMIT) def retry(self): self.retry_params[0](self.retry_params[1]) def handle_reply(self, document, http, error): if error: self.network_error(http, error) return try: artists = document.metadata[0].artist_list[0].artist except (AttributeError, IndexError): self.no_results() return del self.search_results[:] self.parse_artists_from_xml(artists) self.display_results() def parse_artists_from_xml(self, artist_xml): for node in artist_xml: artist = Metadata() artist_to_metadata(node, artist) self.search_results.append(artist) def display_results(self): self.show_table(self.table_headers) for row, artist in enumerate(self.search_results): table_item = QtWidgets.QTableWidgetItem self.table.insertRow(row) self.table.setItem(row, 0, table_item(artist.get("name", ""))) self.table.setItem(row, 1, table_item(artist.get("type", ""))) self.table.setItem(row, 2, table_item(artist.get("gender", ""))) self.table.setItem(row, 3, table_item(artist.get("area", ""))) self.table.setItem(row, 4, table_item(artist.get("begindate", ""))) self.table.setItem(row, 5, table_item(artist.get("beginarea", ""))) self.table.setItem(row, 6, table_item(artist.get("enddate", ""))) self.table.setItem(row, 7, table_item(artist.get("endarea", ""))) def accept_event(self, row): self.load_in_browser(row) def load_in_browser(self, row): self.tagger.search(self.search_results[row]["musicbrainz_artistid"], "artist") def restore_state(self): size = config.persist["artistsearchdialog_window_size"] if size: self.resize(size) self.search_box.restore_checkbox_state() def restore_table_header_state(self): header = self.table.horizontalHeader() state = config.persist["artistsearchdialog_header_state"] if state: header.restoreState(state) header.setSectionResizeMode(QtWidgets.QHeaderView.Interactive) def save_state(self): if self.table: self.save_table_header_state() config.persist["artistsearchdialog_window_size"] = self.size() def save_table_header_state(self): state = self.table.horizontalHeader().saveState() config.persist["artistsearchdialog_header_state"] = state
class TrackSearchDialog(SearchDialog): options = [ config.Option("persist", "tracksearchdialog_window_size", QtCore.QSize(720, 360)), config.Option("persist", "tracksearchdialog_header_state", QtCore.QByteArray()) ] def __init__(self, parent): super(TrackSearchDialog, self).__init__( parent, accept_button_title=_("Load into Picard")) self.file_ = None self.setWindowTitle(_("Track Search Results")) self.table_headers = [ _("Name"), _("Length"), _("Artist"), _("Release"), _("Date"), _("Country"), _("Type") ] def search(self, text): """Perform search using query provided by the user.""" self.retry_params = Retry(self.search, text) self.search_box.search_edit.setText(text) self.show_progress() self.tagger.xmlws.find_tracks(self.handle_reply, query=text, search=True, 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': string_(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 config.setting["use_adv_search_syntax"]: 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.search_edit.setText(query_str) self.show_progress() self.tagger.xmlws.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.metadata[0].recording_list[0].recording except (AttributeError, IndexError): self.no_results_found() return if self.file_: sorted_results = sorted( (self.file_.orig_metadata.compare_to_track( track, File.comparison_weights) for track in tracks), reverse=True, key=itemgetter(0)) tracks = [item[3] for item in sorted_results] del self.search_results[:] # Clear existing data self.parse_tracks_from_xml(tracks) self.display_results() def display_results(self): self.show_table(self.table_headers) for row, obj in enumerate(self.search_results): track = obj[0] table_item = QtWidgets.QTableWidgetItem self.table.insertRow(row) self.table.setItem(row, 0, table_item(track.get("title", ""))) self.table.setItem(row, 1, table_item(track.get("~length", ""))) self.table.setItem(row, 2, table_item(track.get("artist", ""))) self.table.setItem(row, 3, table_item(track.get("album", ""))) self.table.setItem(row, 4, table_item(track.get("date", ""))) self.table.setItem(row, 5, table_item(track.get("country", ""))) self.table.setItem(row, 6, table_item(track.get("releasetype", ""))) def parse_tracks_from_xml(self, tracks_xml): for node in tracks_xml: if "release_list" in node.children and "release" in node.release_list[0].children: for rel_node in node.release_list[0].release: track = Metadata() recording_to_metadata(node, track) release_to_metadata(rel_node, track) rg_node = rel_node.release_group[0] release_group_to_metadata(rg_node, track) countries = country_list_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["album"] = _("Standalone Recording") self.search_results.append((track, node)) def accept_event(self, arg): self.load_selection(arg) 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._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_: album = self.file_.parent.album self.tagger.move_file_to_nat(track["musicbrainz_recordingid"]) if album._files == 0: self.tagger.remove_album(album) else: self.tagger.load_nat(track["musicbrainz_recordingid"], node) def restore_state(self): size = config.persist["tracksearchdialog_window_size"] if size: self.resize(size) self.search_box.restore_checkbox_state() def restore_table_header_state(self): header = self.table.horizontalHeader() state = config.persist["tracksearchdialog_header_state"] if state: header.restoreState(state) header.setSectionResizeMode(QtWidgets.QHeaderView.Interactive) def save_state(self): if self.table: self.save_table_header_state() config.persist["tracksearchdialog_window_size"] = self.size() def save_table_header_state(self): state = self.table.horizontalHeader().saveState() config.persist["tracksearchdialog_header_state"] = state
class MainWindow(QtGui.QMainWindow): selection_updated = QtCore.pyqtSignal(object) options = [ config.Option("persist", "window_state", QtCore.QByteArray()), config.Option("persist", "window_position", QtCore.QPoint()), config.Option("persist", "window_size", QtCore.QSize(780, 560)), config.Option("persist", "bottom_splitter_state", QtCore.QByteArray()), config.BoolOption("persist", "window_maximized", False), config.BoolOption("persist", "view_cover_art", True), config.BoolOption("persist", "view_file_browser", False), config.TextOption("persist", "current_directory", ""), ] def __init__(self, parent=None): QtGui.QMainWindow.__init__(self, parent) self.selected_objects = [] self.ignore_selection_changes = False 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() mainLayout = QtGui.QSplitter(QtCore.Qt.Vertical) mainLayout.setContentsMargins(0, 0, 0, 0) mainLayout.setHandleWidth(1) self.panel = MainPanel(self, mainLayout) 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.metadata_box = MetadataBox(self) self.cover_art_box = CoverArtBox(self) if not self.show_cover_art_action.isChecked(): self.cover_art_box.hide() bottomLayout = QtGui.QHBoxLayout() bottomLayout.setContentsMargins(0, 0, 0, 0) bottomLayout.setSpacing(0) bottomLayout.addWidget(self.metadata_box, 1) bottomLayout.addWidget(self.cover_art_box, 0) bottom = QtGui.QWidget() bottom.setLayout(bottomLayout) mainLayout.addWidget(self.panel) mainLayout.addWidget(bottom) self.setCentralWidget(mainLayout) # accessibility self.set_tab_order() # FIXME: use QApplication's clipboard self._clipboard = [] for function in ui_init: function(self) def keyPressEvent(self, event): if event.matches(QtGui.QKeySequence.Delete): if self.metadata_box.hasFocus(): self.metadata_box.remove_selected_tags() else: self.remove() else: QtGui.QMainWindow.keyPressEvent(self, event) def show(self): self.restoreWindowState() QtGui.QMainWindow.show(self) self.metadata_box.restore_state() def closeEvent(self, event): if 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): 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() config.persist["window_position"] = geom.topLeft() config.persist["window_size"] = geom.size() else: pos = self.pos() if not pos.isNull(): config.persist["window_position"] = pos config.persist["window_size"] = self.size() config.persist["window_maximized"] = isMaximized config.persist[ "view_cover_art"] = self.show_cover_art_action.isChecked() config.persist[ "view_file_browser"] = self.show_file_browser_action.isChecked() config.persist["bottom_splitter_state"] = self.centralWidget( ).saveState() self.file_browser.save_state() self.panel.save_state() self.metadata_box.save_state() def restoreWindowState(self): self.restoreState(config.persist["window_state"]) pos = config.persist["window_position"] size = 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 config.persist["window_maximized"]: self.setWindowState(QtCore.Qt.WindowMaximized) bottom_splitter_state = config.persist["bottom_splitter_state"] if bottom_splitter_state.isEmpty(): self.centralWidget().setSizes([366, 194]) else: self.centralWidget().restoreState(bottom_splitter_state) self.file_browser.restore_state() def create_statusbar(self): """Creates a new status bar.""" self.statusBar().showMessage(_("Ready")) self.infostatus = InfoStatus(self) self.listening_label = QtGui.QLabel() self.listening_label.setVisible(False) self.listening_label.setToolTip("<qt/>" + _( "Picard listens on this port to integrate with your browser. When " "you \"Search\" or \"Open in Browser\" from Picard, clicking the " "\"Tagger\" button on the web page loads the release into Picard.") ) self.statusBar().addPermanentWidget(self.infostatus) self.statusBar().addPermanentWidget(self.listening_label) self.tagger.tagger_stats_changed.connect(self.update_statusbar_stats) self.tagger.listen_port_changed.connect( self.update_statusbar_listen_port) self.update_statusbar_stats() @throttle(100) def update_statusbar_stats(self): """Updates the status bar information.""" self.infostatus.setFiles(len(self.tagger.files)) self.infostatus.setAlbums(len(self.tagger.albums)) self.infostatus.setPendingFiles(File.num_pending_files) ws = self.tagger.xmlws self.infostatus.setPendingRequests(ws.num_pending_web_requests) def update_statusbar_listen_port(self, listen_port): if listen_port: self.listening_label.setVisible(True) self.listening_label.setText( _(" Listening on port %(port)d ") % {"port": listen_port}) else: self.listening_label.setVisible(False) def set_statusbar_message(self, message, *args, **kwargs): """Set the status bar message. *args are passed to % operator, if args[0] is a mapping it is used for named place holders values >>> w.set_statusbar_message("File %(filename)s", {'filename': 'x.txt'}) Keyword arguments: `echo` parameter defaults to `log.debug`, called before message is translated, it can be disabled passing None or replaced by ie. `log.error`. If None, skipped. `translate` is a method called on message before it is sent to history log and status bar, it defaults to `_()`. If None, skipped. `timeout` defines duration of the display in milliseconds `history` is a method called with translated message as argument, it defaults to `log.history_info`. If None, skipped. Empty messages are never passed to echo and history functions but they are sent to status bar (ie. to clear it). """ def isdict(obj): return hasattr(obj, 'keys') and hasattr(obj, '__getitem__') echo = kwargs.get('echo', log.debug) # _ is defined using __builtin__.__dict__, so setting it as default named argument # value doesn't work as expected translate = kwargs.get('translate', _) timeout = kwargs.get('timeout', 0) history = kwargs.get('history', log.history_info) if len(args) == 1 and isdict(args[0]): # named place holders mparms = args[0] else: # simple place holders, ensure compatibility mparms = args if message: if echo: echo(message % mparms) if translate: message = translate(message) message = message % mparms if history: history(message) thread.to_main(self.statusBar().showMessage, message, timeout) def _on_submit(self): if self.tagger.use_acoustid: if not 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() def create_actions(self): self.options_action = QtGui.QAction( icontheme.lookup('preferences-desktop'), _("&Options..."), self) self.options_action.setMenuRole(QtGui.QAction.PreferencesRole) self.options_action.triggered.connect(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.cut_action.triggered.connect(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.paste_action.triggered.connect(self.paste) self.help_action = QtGui.QAction(_("&Help..."), self) self.help_action.setShortcut(QtGui.QKeySequence.HelpContents) self.help_action.triggered.connect(self.show_help) self.about_action = QtGui.QAction(_("&About..."), self) self.about_action.setMenuRole(QtGui.QAction.AboutRole) self.about_action.triggered.connect(self.show_about) self.donate_action = QtGui.QAction(_("&Donate..."), self) self.donate_action.triggered.connect(self.open_donation_page) self.report_bug_action = QtGui.QAction(_("&Report a Bug..."), self) self.report_bug_action.triggered.connect(self.open_bug_report) self.support_forum_action = QtGui.QAction(_("&Support Forum..."), self) self.support_forum_action.triggered.connect(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.add_files_action.triggered.connect(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.add_directory_action.triggered.connect(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.save_action.triggered.connect(self.save) self.submit_action = QtGui.QAction(icontheme.lookup('picard-submit'), _(u"S&ubmit"), self) self.submit_action.setStatusTip(_(u"Submit acoustic fingerprints")) self.submit_action.setEnabled(False) self.submit_action.triggered.connect(self._on_submit) self.exit_action = QtGui.QAction(_(u"E&xit"), self) self.exit_action.setMenuRole(QtGui.QAction.QuitRole) # TR: Keyboard shortcut for "Exit" self.exit_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+Q"))) self.exit_action.triggered.connect(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.setEnabled(False) self.remove_action.triggered.connect(self.remove) self.browser_lookup_action = QtGui.QAction( icontheme.lookup('lookup-musicbrainz'), _(u"Lookup in &Browser"), self) self.browser_lookup_action.setStatusTip( _(u"Lookup selected item on MusicBrainz website")) self.browser_lookup_action.setEnabled(False) self.browser_lookup_action.triggered.connect(self.browser_lookup) self.show_file_browser_action = QtGui.QAction(_(u"File &Browser"), self) self.show_file_browser_action.setCheckable(True) if config.persist["view_file_browser"]: self.show_file_browser_action.setChecked(True) self.show_file_browser_action.setShortcut( QtGui.QKeySequence(_(u"Ctrl+B"))) self.show_file_browser_action.triggered.connect(self.show_file_browser) self.show_cover_art_action = QtGui.QAction(_(u"&Cover Art"), self) self.show_cover_art_action.setCheckable(True) if config.persist["view_cover_art"]: self.show_cover_art_action.setChecked(True) self.show_cover_art_action.triggered.connect(self.show_cover_art) self.search_action = QtGui.QAction(icontheme.lookup('system-search'), _(u"Search"), self) self.search_action.triggered.connect(self.search) self.cd_lookup_action = QtGui.QAction( icontheme.lookup('media-optical'), _(u"Lookup &CD..."), self) self.cd_lookup_action.setStatusTip( _(u"Lookup the details of the CD in your drive")) # TR: Keyboard shortcut for "Lookup CD" self.cd_lookup_action.setShortcut(QtGui.QKeySequence(_("Ctrl+K"))) self.cd_lookup_action.triggered.connect(self.tagger.lookup_cd) self.analyze_action = QtGui.QAction(icontheme.lookup('picard-analyze'), _(u"&Scan"), self) self.analyze_action.setStatusTip( _(u"Use AcoustID audio fingerprint to identify the files by the actual music, even if they have no metadata" )) self.analyze_action.setEnabled(False) # TR: Keyboard shortcut for "Analyze" self.analyze_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+Y"))) self.analyze_action.triggered.connect(self.analyze) self.cluster_action = QtGui.QAction(icontheme.lookup('picard-cluster'), _(u"Cl&uster"), self) self.cluster_action.setStatusTip( _(u"Cluster files into album clusters")) self.cluster_action.setEnabled(False) # TR: Keyboard shortcut for "Cluster" self.cluster_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+U"))) self.cluster_action.triggered.connect(self.cluster) self.autotag_action = QtGui.QAction( icontheme.lookup('picard-auto-tag'), _(u"&Lookup"), self) tip = _(u"Lookup selected items in MusicBrainz") self.autotag_action.setToolTip(tip) self.autotag_action.setStatusTip(tip) self.autotag_action.setEnabled(False) # TR: Keyboard shortcut for "Lookup" self.autotag_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+L"))) self.autotag_action.triggered.connect(self.autotag) self.view_info_action = QtGui.QAction( icontheme.lookup('picard-edit-tags'), _(u"&Info..."), self) self.view_info_action.setEnabled(False) # TR: Keyboard shortcut for "Info" self.view_info_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+I"))) self.view_info_action.triggered.connect(self.view_info) self.refresh_action = QtGui.QAction( icontheme.lookup('view-refresh', icontheme.ICON_SIZE_MENU), _("&Refresh"), self) self.refresh_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+R"))) self.refresh_action.triggered.connect(self.refresh) self.enable_renaming_action = QtGui.QAction(_(u"&Rename Files"), self) self.enable_renaming_action.setCheckable(True) self.enable_renaming_action.setChecked(config.setting["rename_files"]) self.enable_renaming_action.triggered.connect(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(config.setting["move_files"]) self.enable_moving_action.triggered.connect(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 config.setting["dont_write_tags"]) self.enable_tag_saving_action.triggered.connect(self.toggle_tag_saving) self.tags_from_filenames_action = QtGui.QAction( _(u"Tags From &File Names..."), self) self.tags_from_filenames_action.triggered.connect( self.open_tags_from_filenames) self.tags_from_filenames_action.setEnabled(False) self.open_collection_in_browser_action = QtGui.QAction( _(u"&Open My Collections in Browser"), self) self.open_collection_in_browser_action.triggered.connect( self.open_collection_in_browser) self.open_collection_in_browser_action.setEnabled( config.setting["username"] != u'') self.view_log_action = QtGui.QAction(_(u"View Error/Debug &Log"), self) self.view_log_action.triggered.connect(self.show_log) self.view_history_action = QtGui.QAction(_(u"View Activity &History"), self) self.view_history_action.triggered.connect(self.show_history) xmlws_manager = self.tagger.xmlws.manager xmlws_manager.authenticationRequired.connect(self.show_password_dialog) xmlws_manager.proxyAuthenticationRequired.connect( self.show_proxy_dialog) self.play_file_action = QtGui.QAction(icontheme.lookup('play-music'), _(u"&Play file"), self) self.play_file_action.setStatusTip( _(u"Play the file in your default media player")) self.play_file_action.setEnabled(False) self.play_file_action.triggered.connect(self.play_file) self.open_folder_action = QtGui.QAction( icontheme.lookup('folder', icontheme.ICON_SIZE_MENU), _(u"Open Containing &Folder"), self) self.open_folder_action.setStatusTip( _(u"Open the containing folder in your file explorer")) self.open_folder_action.setEnabled(False) self.open_folder_action.triggered.connect(self.open_folder) def toggle_rename_files(self, checked): config.setting["rename_files"] = checked def toggle_move_files(self, checked): config.setting["move_files"] = checked def toggle_tag_saving(self, checked): config.setting["dont_write_tags"] = not checked def get_selected_or_unmatched_files(self): files = self.tagger.get_files_from_objects(self.selected_objects) if not files: files = self.tagger.unmatched_files.files return files def open_tags_from_filenames(self): files = self.get_selected_or_unmatched_files() if files: dialog = TagsFromFileNamesDialog(files, self) dialog.exec_() def open_collection_in_browser(self): self.tagger.collection_lookup() def create_menus(self): menu = self.menuBar().addMenu(_(u"&File")) menu.addAction(self.add_directory_action) menu.addAction(self.add_files_action) menu.addSeparator() menu.addAction(self.play_file_action) menu.addAction(self.open_folder_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.view_info_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_toggle_action) menu.addAction(self.search_toolbar_toggle_action) 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.refresh_action) menu.addAction(self.cd_lookup_action) menu.addAction(self.autotag_action) menu.addAction(self.analyze_action) menu.addAction(self.cluster_action) menu.addAction(self.browser_lookup_action) menu.addSeparator() menu.addAction(self.tags_from_filenames_action) menu.addAction(self.open_collection_in_browser_action) self.menuBar().addSeparator() menu = self.menuBar().addMenu(_(u"&Help")) menu.addAction(self.help_action) menu.addSeparator() menu.addAction(self.view_history_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 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 and discid is not None) def create_toolbar(self): self.toolbar = toolbar = self.addToolBar(_(u"Actions")) self.toolbar_toggle_action = self.toolbar.toggleViewAction() self.update_toolbar_style() toolbar.setObjectName("main_toolbar") def add_toolbar_action(action): toolbar.addAction(action) widget = toolbar.widgetForAction(action) widget.setFocusPolicy(QtCore.Qt.TabFocus) widget.setAttribute(QtCore.Qt.WA_MacShowFocusRect) add_toolbar_action(self.add_directory_action) add_toolbar_action(self.add_files_action) toolbar.addSeparator() add_toolbar_action(self.play_file_action) toolbar.addSeparator() add_toolbar_action(self.save_action) add_toolbar_action(self.submit_action) toolbar.addSeparator() add_toolbar_action(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.cd_lookup_menu.triggered.connect(self.tagger.lookup_cd) button = toolbar.widgetForAction(self.cd_lookup_action) button.setPopupMode(QtGui.QToolButton.MenuButtonPopup) button.setMenu(self.cd_lookup_menu) add_toolbar_action(self.cluster_action) add_toolbar_action(self.autotag_action) add_toolbar_action(self.analyze_action) add_toolbar_action(self.view_info_action) add_toolbar_action(self.remove_action) add_toolbar_action(self.browser_lookup_action) self.search_toolbar = toolbar = self.addToolBar(_(u"Search")) self.search_toolbar_toggle_action = self.search_toolbar.toggleViewAction( ) 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"), "album") self.search_combo.addItem(_(u"Artist"), "artist") self.search_combo.addItem(_(u"Track"), "track") hbox.addWidget(self.search_combo, 0) self.search_edit = ButtonLineEdit(search_panel) self.search_edit.returnPressed.connect(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)) self.search_button.setAttribute(QtCore.Qt.WA_MacShowFocusRect) hbox.addWidget(self.search_button) toolbar.addWidget(search_panel) def set_tab_order(self): tab_order = self.setTabOrder tw = self.toolbar.widgetForAction # toolbar tab_order(tw(self.add_directory_action), tw(self.add_files_action)) tab_order(tw(self.add_files_action), tw(self.play_file_action)) tab_order(tw(self.play_file_action), tw(self.save_action)) tab_order(tw(self.save_action), tw(self.submit_action)) tab_order(tw(self.submit_action), tw(self.cd_lookup_action)) tab_order(tw(self.cd_lookup_action), tw(self.cluster_action)) tab_order(tw(self.cluster_action), tw(self.autotag_action)) tab_order(tw(self.autotag_action), tw(self.analyze_action)) tab_order(tw(self.analyze_action), tw(self.view_info_action)) tab_order(tw(self.view_info_action), tw(self.remove_action)) tab_order(tw(self.remove_action), tw(self.browser_lookup_action)) tab_order(tw(self.browser_lookup_action), self.search_combo) tab_order(self.search_combo, self.search_edit) tab_order(self.search_edit, self.search_button) # panels tab_order(self.search_button, self.file_browser) tab_order(self.file_browser, self.panel.views[0]) tab_order(self.panel.views[0], self.panel.views[1]) tab_order(self.panel.views[1], self.metadata_box) def enable_submit(self, enabled): """Enable/disable the 'Submit fingerprints' 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 = self.search_edit.text() type = self.search_combo.itemData(self.search_combo.currentIndex()) self.tagger.search(text, type, config.setting["use_adv_search_syntax"]) def add_files(self): """Add files to the tagger.""" current_directory = find_starting_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) 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 = find_starting_directory() dir_list = [] if not 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: config.persist["current_directory"] = dir_list[0] self.set_statusbar_message( N_("Adding directory: '%(directory)s' ..."), {'directory': dir_list[0]}) elif len(dir_list) > 1: (parent, dir) = os.path.split(str(dir_list[0])) config.persist["current_directory"] = parent self.set_statusbar_message( N_("Adding multiple directories from '%(directory)s' ..."), {'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.goto('documentation') def show_log(self): from picard.ui.logview import LogView LogView(self).show() def show_history(self): from picard.ui.logview import HistoryView HistoryView(self).show() def open_bug_report(self): webbrowser2.goto('troubleshooting') def open_support_forum(self): webbrowser2.goto('forum') def open_donation_page(self): webbrowser2.goto('donate') def save(self): """Tell the tagger to save the selected objects.""" self.tagger.save(self.selected_objects) def remove(self): """Tell the tagger to remove the selected objects.""" self.panel.remove(self.selected_objects) def analyze(self): if not config.setting['fingerprinting_system']: if self.show_analyze_settings_info(): self.show_options("fingerprinting") if not config.setting['fingerprinting_system']: return return self.tagger.analyze(self.selected_objects) def play_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 view_info(self): if isinstance(self.selected_objects[0], Album): album = self.selected_objects[0] dialog = AlbumInfoDialog(album, self) elif isinstance(self.selected_objects[0], Cluster): cluster = self.selected_objects[0] dialog = ClusterInfoDialog(cluster, self) else: file = self.tagger.get_files_from_objects(self.selected_objects)[0] dialog = FileInfoDialog(file, self) dialog.exec_() def cluster(self): self.tagger.cluster(self.selected_objects) self.update_actions() def refresh(self): self.tagger.refresh(self.selected_objects) def browser_lookup(self): self.tagger.browser_lookup(self.selected_objects[0]) @throttle(100) def update_actions(self): can_remove = False can_save = False can_analyze = False can_refresh = False can_autotag = False single = self.selected_objects[0] if len( self.selected_objects) == 1 else None can_view_info = bool(single and single.can_view_info()) can_browser_lookup = bool(single and single.can_browser_lookup()) have_files = len( self.tagger.get_files_from_objects(self.selected_objects)) > 0 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_refresh(): can_refresh = True if obj.can_autotag(): can_autotag = True # Skip further loops if all values now True. if can_analyze and can_save and can_remove and can_refresh and can_autotag: break self.remove_action.setEnabled(can_remove) self.save_action.setEnabled(can_save) self.view_info_action.setEnabled(can_view_info) self.analyze_action.setEnabled(can_analyze) self.refresh_action.setEnabled(can_refresh) self.autotag_action.setEnabled(can_autotag) self.browser_lookup_action.setEnabled(can_browser_lookup) self.play_file_action.setEnabled(have_files) self.open_folder_action.setEnabled(have_files) self.cut_action.setEnabled(bool(self.selected_objects)) files = self.get_selected_or_unmatched_files() self.tags_from_filenames_action.setEnabled(bool(files)) def update_selection(self, objects=None): if self.ignore_selection_changes: return if objects is not None: self.selected_objects = objects else: objects = self.selected_objects self.update_actions() metadata = None obj = None if len(objects) == 1: obj = list(objects)[0] if isinstance(obj, File): metadata = obj.metadata if obj.state == obj.ERROR: msg = N_("%(filename)s (error: %(error)s)") mparms = {'filename': obj.filename, 'error': obj.error} else: msg = N_("%(filename)s") mparms = { 'filename': obj.filename, } self.set_statusbar_message(msg, mparms, echo=None, history=None) elif isinstance(obj, Track): metadata = obj.metadata if obj.num_linked_files == 1: file = obj.linked_files[0] if file.state == File.ERROR: msg = N_( "%(filename)s (%(similarity)d%%) (error: %(error)s)" ) mparms = { 'filename': file.filename, 'similarity': file.similarity * 100, 'error': file.error } else: msg = N_("%(filename)s (%(similarity)d%%)") mparms = { 'filename': file.filename, 'similarity': file.similarity * 100, } self.set_statusbar_message(msg, mparms, echo=None, history=None) elif obj.can_edit_tags(): metadata = obj.metadata self.metadata_box.selection_dirty = True self.metadata_box.update() self.cover_art_box.set_metadata(metadata, obj) self.selection_updated.emit(objects) def show_cover_art(self): """Show/hide the cover art box.""" if self.show_cover_art_action.isChecked(): self.cover_art_box.show() self.metadata_box.resize_columns() 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): if reply.url().host() == config.setting['server_host']: ret = QtGui.QMessageBox.question( self, _(u"Authentication Required"), _(u"Picard needs authorization to access your personal data on the MusicBrainz server. Would you like to log in now?" ), QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.Yes) if ret == QtGui.QMessageBox.Yes: pass else: 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.selected_objects) def cut(self): self._clipboard = self.selected_objects self.paste_action.setEnabled(bool(self._clipboard)) def paste(self): selected_objects = self.selected_objects if not selected_objects: target = self.tagger.unmatched_files else: target = selected_objects[0] self.tagger.move_files( self.tagger.get_files_from_objects(self._clipboard), target) self._clipboard = [] self.paste_action.setEnabled(False)
class PluginsOptionsPage(OptionsPage): NAME = "plugins" TITLE = N_("Plugins") PARENT = None SORT_ORDER = 70 ACTIVE = True options = [ config.ListOption("setting", "enabled_plugins", []), config.Option("persist", "plugins_list_state", QtCore.QByteArray()), config.Option("persist", "plugins_list_sort_section", 0), config.Option("persist", "plugins_list_sort_order", QtCore.Qt.AscendingOrder), config.Option("persist", "plugins_list_selected", ""), ] def __init__(self, parent=None): super(PluginsOptionsPage, self).__init__(parent) self.ui = Ui_PluginsOptionsPage() self.ui.setupUi(self) self.items = {} self.ui.plugins.itemSelectionChanged.connect(self.change_details) self.ui.plugins.mimeTypes = self.mimeTypes self.ui.plugins.dropEvent = self.dropEvent self.ui.plugins.dragEnterEvent = self.dragEnterEvent if sys.platform == "win32": self.loader = "file:///%s" else: self.loader = "file://%s" self.ui.install_plugin.clicked.connect(self.open_plugins) self.ui.folder_open.clicked.connect(self.open_plugin_dir) self.ui.reload_list_of_plugins.clicked.connect( self.reload_list_of_plugins) self.tagger.pluginmanager.plugin_installed.connect( self.plugin_installed) self.tagger.pluginmanager.plugin_updated.connect(self.plugin_updated) self.ui.plugins.header().setStretchLastSection(False) self.ui.plugins.header().setSectionResizeMode( 0, QtWidgets.QHeaderView.Stretch) self.ui.plugins.header().setSectionResizeMode( 1, QtWidgets.QHeaderView.Stretch) self.ui.plugins.header().resizeSection(2, 100) self.ui.plugins.setSortingEnabled(True) def save_state(self): header = self.ui.plugins.header() config.persist["plugins_list_state"] = header.saveState() config.persist[ "plugins_list_sort_section"] = header.sortIndicatorSection() config.persist["plugins_list_sort_order"] = header.sortIndicatorOrder() try: selected = self.items[self.ui.plugins.selectedItems() [0]].module_name except IndexError: selected = "" config.persist["plugins_list_selected"] = selected def restore_state(self, restore_selection=False): header = self.ui.plugins.header() header.restoreState(config.persist["plugins_list_state"]) idx = config.persist["plugins_list_sort_section"] order = config.persist["plugins_list_sort_order"] header.setSortIndicator(idx, order) self.ui.plugins.sortByColumn(idx, order) selected = restore_selection and config.persist["plugins_list_selected"] if selected: for i, p in self.items.items(): if selected == p.module_name: self.ui.plugins.setCurrentItem(i) self.ui.plugins.scrollToItem(i) break else: self.ui.plugins.setCurrentItem(self.ui.plugins.topLevelItem(0)) def _populate(self): self.ui.details.setText("<b>" + _("No plugins installed.") + "</b>") self._user_interaction(False) plugins = sorted(self.tagger.pluginmanager.plugins, key=attrgetter('name')) enabled_plugins = config.setting["enabled_plugins"] available_plugins = dict([ (p.module_name, p.version) for p in self.tagger.pluginmanager.available_plugins ]) installed = [] for plugin in plugins: if plugin.module_name in enabled_plugins: plugin.enabled = True if plugin.module_name in available_plugins.keys(): latest = available_plugins[plugin.module_name] if latest.split('.') > plugin.version.split('.'): plugin.new_version = latest plugin.can_be_updated = True self.add_plugin_item(plugin) installed.append(plugin.module_name) for plugin in sorted(self.tagger.pluginmanager.available_plugins, key=attrgetter('name')): if plugin.module_name not in installed: plugin.can_be_downloaded = True self.add_plugin_item(plugin) self._user_interaction(True) def _remove_all(self): for i, p in self.items.items(): idx = self.ui.plugins.indexOfTopLevelItem(i) self.ui.plugins.takeTopLevelItem(idx) self.items = {} def restore_defaults(self): # Plugin manager has to be updated for plugin in self.tagger.pluginmanager.plugins: plugin.enabled = False # Remove previous entries self._user_interaction(False) self._remove_all() super(PluginsOptionsPage, self).restore_defaults() def load(self): self._populate() self.restore_state() def _reload(self): self._populate() self.restore_state(restore_selection=True) def _user_interaction(self, enabled): self.ui.plugins.blockSignals(not enabled) self.ui.plugins_container.setEnabled(enabled) def reload_list_of_plugins(self): self.ui.details.setText("") self._user_interaction(False) self.save_state() self._remove_all() self.tagger.pluginmanager.query_available_plugins( callback=self._reload) def plugin_installed(self, plugin): if not plugin.compatible: msgbox = QtWidgets.QMessageBox(self) msgbox.setText( _("The plugin '%s' is not compatible with this version of Picard." ) % plugin.name) msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok) msgbox.setDefaultButton(QtWidgets.QMessageBox.Ok) msgbox.exec_() return plugin.new_version = "" plugin.enabled = False plugin.can_be_updated = False plugin.can_be_downloaded = False for i, p in self.items.items(): if plugin.module_name == p.module_name: if i.checkState(0) == QtCore.Qt.Checked: plugin.enabled = True self.add_plugin_item(plugin, item=i) self.ui.plugins.setCurrentItem(i) self.change_details() break else: self.add_plugin_item(plugin) def plugin_updated(self, plugin_name): for i, p in self.items.items(): if plugin_name == p.module_name: p.can_be_updated = False p.can_be_downloaded = False p.marked_for_update = True msgbox = QtWidgets.QMessageBox(self) msgbox.setText( _("The plugin '%s' will be upgraded to version %s on next run of Picard." ) % (p.name, p.new_version)) msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok) msgbox.setDefaultButton(QtWidgets.QMessageBox.Ok) msgbox.exec_() self.add_plugin_item(p, item=i) self.ui.plugins.setCurrentItem(i) self.change_details() break def add_plugin_item(self, plugin, item=None): if item is None: item = PluginTreeWidgetItem(self.ui.plugins) item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable) item.setText(0, plugin.name) item.setSortData(0, plugin.name.lower()) if plugin.enabled: item.setCheckState(0, QtCore.Qt.Checked) else: item.setCheckState(0, QtCore.Qt.Unchecked) if plugin.marked_for_update: item.setText(1, plugin.new_version) else: item.setText(1, plugin.version) label = None if plugin.can_be_updated: label = _("Update") elif plugin.can_be_downloaded: label = _("Install") item.setFlags(item.flags() ^ QtCore.Qt.ItemIsUserCheckable) if label is not None: button = QtWidgets.QPushButton(label) button.setMaximumHeight( button.fontMetrics().boundingRect(label).height() + 7) self.ui.plugins.setItemWidget(item, 2, button) def download_button_process(): self.ui.plugins.setCurrentItem(item) self.download_plugin() button.released.connect(download_button_process) else: # Note: setText() don't work after it was set to a button if plugin.marked_for_update: label = _("Updated") else: label = _("Installed") self.ui.plugins.setItemWidget(item, 2, QtWidgets.QLabel(label)) item.setSortData(2, label) self.ui.plugins.header().resizeSections( QtWidgets.QHeaderView.ResizeToContents) self.items[item] = plugin return item def save(self): enabled_plugins = [] for item, plugin in self.items.items(): if item.checkState(0) == QtCore.Qt.Checked: enabled_plugins.append(plugin.module_name) config.setting["enabled_plugins"] = enabled_plugins self.save_state() def change_details(self): try: plugin = self.items[self.ui.plugins.selectedItems()[0]] except IndexError: return text = [] if plugin.new_version: if plugin.marked_for_update: text.append("<b>" + _("Restart Picard to upgrade to new version") + ": " + plugin.new_version + "</b>") else: text.append("<b>" + _("New version available") + ": " + plugin.new_version + "</b>") if plugin.description: text.append(plugin.description + "<hr width='90%'/>") if plugin.name: text.append("<b>" + _("Name") + "</b>: " + plugin.name) if plugin.author: text.append("<b>" + _("Authors") + "</b>: " + plugin.author) if plugin.license: text.append("<b>" + _("License") + "</b>: " + plugin.license) text.append("<b>" + _("Files") + "</b>: " + plugin.files_list) self.ui.details.setText("<p>%s</p>" % "<br/>\n".join(text)) def open_plugins(self): files, _filter = QtWidgets.QFileDialog.getOpenFileNames( self, "", QtCore.QDir.homePath(), "Picard plugin (*.py *.pyc *.zip)") if files: for path in files: self.tagger.pluginmanager.install_plugin(path) def download_plugin(self): selected = self.ui.plugins.selectedItems()[0] plugin = self.items[selected] self.tagger.webservice.get(PLUGINS_API['host'], PLUGINS_API['port'], PLUGINS_API['endpoint']['download'], partial(self.download_handler, plugin=plugin), parse_response_type=None, priority=True, important=True, queryargs={"id": plugin.module_name}) def download_handler(self, response, reply, error, plugin): if error: msgbox = QtWidgets.QMessageBox(self) msgbox.setText( _("The plugin '%s' could not be downloaded.") % plugin.module_name) msgbox.setInformativeText(_("Please try again later.")) msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok) msgbox.setDefaultButton(QtWidgets.QMessageBox.Ok) msgbox.exec_() log.error( "Error occurred while trying to download the plugin: '%s'" % plugin.module_name) return self.tagger.pluginmanager.install_plugin( None, plugin_name=plugin.module_name, plugin_data=response) def open_plugin_dir(self): QtGui.QDesktopServices.openUrl( QtCore.QUrl(self.loader % USER_PLUGIN_DIR, QtCore.QUrl.TolerantMode)) def mimeTypes(self): return ["text/uri-list"] def dragEnterEvent(self, event): event.setDropAction(QtCore.Qt.CopyAction) event.accept() def dropEvent(self, event): for path in [ os.path.normpath(u.toLocalFile()) for u in event.mimeData().urls() ]: self.tagger.pluginmanager.install_plugin(path)
class OptionsDialog(PicardDialog): autorestore = False options = [ config.Option("persist", "options_splitter", QtCore.QByteArray()), ] def add_pages(self, parent, default_page, parent_item): pages = [(p.SORT_ORDER, p.NAME, p) for p in self.pages if p.PARENT == parent] items = [] for foo, bar, page in sorted(pages): item = HashableTreeWidgetItem(parent_item) item.setText(0, _(page.TITLE)) if page.ACTIVE: self.item_to_page[item] = page self.page_to_item[page.NAME] = item self.ui.pages_stack.addWidget(page) else: item.setFlags(QtCore.Qt.ItemIsEnabled) self.add_pages(page.NAME, default_page, item) if page.NAME == default_page: self.default_item = item items.append(item) if not self.default_item and not parent: self.default_item = items[0] def __init__(self, default_page=None, parent=None): super().__init__(parent) self.setWindowModality(QtCore.Qt.ApplicationModal) from picard.ui.ui_options import Ui_Dialog self.ui = Ui_Dialog() self.ui.setupUi(self) self.ui.reset_all_button = QtWidgets.QPushButton(_("&Restore all Defaults")) self.ui.reset_all_button.setToolTip(_("Reset all of Picard's settings")) self.ui.reset_button = QtWidgets.QPushButton(_("Restore &Defaults")) self.ui.reset_button.setToolTip(_("Reset all settings for current option page")) ok = StandardButton(StandardButton.OK) ok.setText(_("Make It So!")) self.ui.buttonbox.addButton(ok, QtWidgets.QDialogButtonBox.AcceptRole) self.ui.buttonbox.addButton(StandardButton(StandardButton.CANCEL), QtWidgets.QDialogButtonBox.RejectRole) self.ui.buttonbox.addButton(StandardButton(StandardButton.HELP), QtWidgets.QDialogButtonBox.HelpRole) self.ui.buttonbox.addButton(self.ui.reset_all_button, QtWidgets.QDialogButtonBox.ActionRole) self.ui.buttonbox.addButton(self.ui.reset_button, QtWidgets.QDialogButtonBox.ActionRole) self.ui.buttonbox.accepted.connect(self.accept) self.ui.buttonbox.rejected.connect(self.reject) self.ui.reset_all_button.clicked.connect(self.confirm_reset_all) self.ui.reset_button.clicked.connect(self.confirm_reset) self.ui.buttonbox.helpRequested.connect(self.help) self.pages = [] for Page in page_classes: 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) # 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.pages_tree.collapseAll() 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: 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.goto('doc_options') def accept(self): for page in self.pages: try: page.check() except OptionsCheckError as e: self.ui.pages_tree.setCurrentItem(self.page_to_item[page.NAME]) page.display_error(e) return for page in self.pages: page.save() super().accept() def saveWindowState(self): config.persist["options_splitter"] = self.ui.splitter.saveState() @restore_method def restoreWindowState(self): 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() message_box.setIcon(QtWidgets.QMessageBox.Warning) message_box.setWindowModality(QtCore.Qt.WindowModal) message_box.setWindowTitle(_("Confirm Reset")) message_box.setText(_("Are you sure?") + "\n\n" + msg) message_box.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) if message_box.exec_() == QtWidgets.QMessageBox.Yes: function()
class InterfaceColorsOptionsPage(OptionsPage): NAME = "interface_colors" TITLE = N_("Colors") PARENT = "interface" SORT_ORDER = 30 ACTIVE = True HELP_URL = '/config/options_interface_colors.html' options = [ config.Option("setting", "interface_colors", InterfaceColors(dark_theme=False).get_colors()), config.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()
class OptionsDialog(PicardDialog): options = [ config.Option("persist", "options_position", QtCore.QPoint()), config.Option("persist", "options_size", QtCore.QSize(560, 400)), config.Option("persist", "options_splitter", QtCore.QByteArray()), ] def add_pages(self, parent, default_page, parent_item): pages = [(p.SORT_ORDER, p.NAME, p) for p in self.pages if p.PARENT == parent] items = [] for foo, bar, page in sorted(pages): item = 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): PicardDialog.__init__(self, parent) from picard.ui.ui_options import Ui_Dialog self.ui = Ui_Dialog() self.ui.setupUi(self) self.ui.reset_all_button = QtGui.QPushButton(_("&Restore all Defaults")) self.ui.reset_button = QtGui.QPushButton(_("Restore &Defaults")) 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.ui.buttonbox.addButton(self.ui.reset_all_button, QtGui.QDialogButtonBox.ActionRole) self.ui.buttonbox.addButton(self.ui.reset_button, QtGui.QDialogButtonBox.ActionRole) self.ui.buttonbox.accepted.connect(self.accept) self.ui.buttonbox.rejected.connect(self.reject) self.ui.reset_all_button.clicked.connect(self.confirm_reset_all) self.ui.reset_button.clicked.connect(self.confirm_reset) self.ui.buttonbox.helpRequested.connect(self.help) self.pages = [] for Page in page_classes: 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.ui.pages_tree.itemSelectionChanged.connect(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.goto('doc_options') def accept(self): for page in self.pages: try: page.check() except OptionsCheckError as 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) def closeEvent(self, event): self.saveWindowState() event.accept() def saveWindowState(self): pos = self.pos() if not pos.isNull(): config.persist["options_position"] = pos config.persist["options_size"] = self.size() config.persist["options_splitter"] = self.ui.splitter.saveState() def restoreWindowState(self): pos = config.persist["options_position"] if pos.x() > 0 and pos.y() > 0: self.move(pos) self.resize(config.persist["options_size"]) 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 = QtGui.QMessageBox() message_box.setIcon(QtGui.QMessageBox.Warning) message_box.setWindowModality(QtCore.Qt.WindowModal) message_box.setText("Are you sure?") message_box.setInformativeText(msg) message_box.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) if message_box.exec_() == QtGui.QMessageBox.Yes: function()
class TagsFromFileNamesDialog(QtGui.QDialog): options = [ config.TextOption("persist", "tags_from_filenames_format", ""), config.Option("persist", "tags_from_filenames_position", QtCore.QPoint(), QtCore.QVariant.toPoint), config.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 = 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.ui.buttonbox.accepted.connect(self.accept) self.ui.buttonbox.rejected.connect(self.reject) self.ui.preview.clicked.connect(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() 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(): config.persist["tags_from_filenames_position"] = pos config.persist["tags_from_filenames_size"] = self.size() def restoreWindowState(self): pos = config.persist["tags_from_filenames_position"] if pos.x() > 0 and pos.y() > 0: self.move(pos) self.resize(config.persist["tags_from_filenames_size"])
class SearchEngineLookupOptionsPage(OptionsPage): NAME = "search_engine_lookup_options" TITLE = "Search Engine Lookup" PARENT = "plugins" HELP_URL = "https://github.com/metabrainz/picard-plugins/blob/2.0/plugins/search_engine_lookup/README.md" options = [ config.Option("setting", KEY_PROVIDERS, DEFAULT_PROVIDERS.copy()), config.TextOption("setting", KEY_PROVIDER, DEFAULT_PROVIDER), config.TextOption("setting", KEY_EXTRA, DEFAULT_EXTRA_WORDS), ] def __init__(self, parent=None): super(SearchEngineLookupOptionsPage, self).__init__(parent) self.ui = Ui_SearchEngineLookupOptionsPage() self.ui.setupUi(self) self.setup_actions() self.provider = '' self.providers = {} self.additional_words = '' def setup_actions(self): self.ui.list_providers.itemChanged.connect(self.select_provider) self.ui.pb_add.clicked.connect(self.add_provider) self.ui.pb_edit.clicked.connect(self.edit_provider) self.ui.pb_delete.clicked.connect(self.delete_provider) self.ui.pb_test.clicked.connect(self.test_provider) def load(self): # Settings for search engine providers self.providers = config.setting[KEY_PROVIDERS] or DEFAULT_PROVIDERS.copy() # Settings for search engine provider self.provider = config.setting[KEY_PROVIDER] if self.provider not in self.providers: # Assign an arbitrary valid value to self.provider self.provider = list(self.providers)[0] # Settings for search extra words self.additional_words = config.setting[KEY_EXTRA] self.ui.le_additional_words.setText(self.additional_words) # Display list of providers self.update_list() def select_provider(self, list_item): if list_item.checkState() == QtCore.Qt.Checked: # New provider selected self.provider = list_item.data(QtCore.Qt.UserRole) self.update_list(current_item=self.provider) else: # Attempt to deselect the current provider leaving none selected list_item.setCheckState(QtCore.Qt.Checked) def add_provider(self): provider_id = uuid4() self.edit_provider_dialog(provider_id) def edit_provider(self): current_item = self.ui.list_providers.currentItem() provider = current_item.text() provider_id = current_item.data(QtCore.Qt.UserRole) url = self.providers[provider_id]['url'] self.edit_provider_dialog(provider_id, provider, url) def edit_provider_dialog(self, provider_id='', provider='', url=''): # List of titles currently used and not allowed. Omit current title from the list when editing. titles = [x['name'] for x in self.providers.values() if x['name'] != provider] dialog = SearchEngineEditDialog(self, provider, url, titles) temp = dialog.exec_() if temp == QtWidgets.QDialog.Accepted: data = dialog.get_output() if data: new_provider, new_url = data self.providers[provider_id] = {'name': new_provider, 'url': new_url} self.update_list(provider_id) def delete_provider(self): current_item = self.ui.list_providers.currentItem() provider = current_item.text() provider_id = current_item.data(QtCore.Qt.UserRole) if current_item.checkState() or provider_id == self.provider: QtWidgets.QMessageBox.critical( self, _('Deletion Error'), _('You cannot delete the currently selected search provider.'), QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok ) else: if QtWidgets.QMessageBox.warning( self, _('Confirm Deletion'), _('You are about to permanently delete the search provider "{provider_name}". Continue?').format(provider_name=provider), QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel ) == QtWidgets.QMessageBox.Ok: self.providers.pop(provider_id, None) self.update_list() def test_provider(self): current_item = self.ui.list_providers.currentItem() parts = ('The Beatles Abby Road ' + self.additional_words).strip().split() url = self.providers[current_item.data(QtCore.Qt.UserRole)]['url'].replace(r'%search%', quote_plus(' '.join(parts))) _open(url) def update_list(self, current_item=None): current_row = -1 self.ui.list_providers.clear() for counter, provider_id in enumerate(self.providers): item = QtWidgets.QListWidgetItem() item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable) item.setText(self.providers[provider_id]['name']) item.setCheckState(QtCore.Qt.Checked if provider_id == self.provider else QtCore.Qt.Unchecked) item.setData(QtCore.Qt.UserRole, provider_id) self.ui.list_providers.addItem(item) if current_item and provider_id == current_item: current_row = counter current_row = max(current_row, 0) self.ui.list_providers.setCurrentRow(current_row) self.ui.list_providers.sortItems() def save(self): self._set_settings(config.setting) def _set_settings(self, settings): settings[KEY_PROVIDER] = self.provider.strip() settings[KEY_EXTRA] = self.additional_words.strip() settings[KEY_PROVIDERS] = self.providers or DEFAULT_PROVIDERS.copy()
class CDLookupDialog(PicardDialog): autorestore = False dialog_header_state = "cdlookupdialog_header_state" options = [ config.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(l): return "\n".join(l) 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() 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.persist[self.dialog_header_state] = state log.debug("save_state: %s" % self.dialog_header_state)
class ScriptingOptionsPage(OptionsPage): NAME = "scripting" TITLE = N_("Scripting") PARENT = None SORT_ORDER = 85 ACTIVE = True HELP_URL = '/config/options_scripting.html' options = [ config.BoolOption("setting", "enable_tagger_scripts", False), config.ListOption("setting", "list_of_scripts", []), config.IntOption("persist", "last_selected_script_pos", 0), config.Option("persist", "scripting_splitter", QtCore.QByteArray()), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_ScriptingOptionsPage() self.ui.setupUi(self) self.ui.tagger_script.setEnabled(False) self.ui.splitter.setStretchFactor(0, 1) self.ui.splitter.setStretchFactor(1, 2) self.move_view = MoveableListView(self.ui.script_list, self.ui.move_up_button, self.ui.move_down_button) self.ui.scripting_documentation_button.clicked.connect( self.show_scripting_documentation) self.scripting_documentation_shown = None def show_scripting_documentation(self): if not self.scripting_documentation_shown: self.scriptdoc_dialog = ScriptingDocumentationDialog(parent=self) self.scriptdoc_dialog.show() else: self.scriptdoc_dialog.raise_() self.scriptdoc_dialog.activateWindow() def enable_tagger_scripts_toggled(self, on): if on and self.ui.script_list.count() == 0: self.ui.script_list.add_script() def script_selected(self): items = self.ui.script_list.selectedItems() if items: item = items[0] self.ui.tagger_script.setEnabled(True) self.ui.tagger_script.setText(item.script) self.ui.tagger_script.setFocus(QtCore.Qt.OtherFocusReason) else: self.ui.tagger_script.setEnabled(False) self.ui.tagger_script.setText("") def live_update_and_check(self): items = self.ui.script_list.selectedItems() if items: script = items[0] script.script = self.ui.tagger_script.toPlainText() self.ui.script_error.setStyleSheet("") self.ui.script_error.setText("") try: self.check() except OptionsCheckError as e: self.ui.script_error.setStyleSheet(self.STYLESHEET_ERROR) self.ui.script_error.setText(e.info) return def check(self): parser = ScriptParser() try: parser.eval(self.ui.tagger_script.toPlainText()) except Exception as e: raise ScriptCheckError(_("Script Error"), str(e)) def restore_defaults(self): # Remove existing scripts self.ui.script_list.clear() self.ui.tagger_script.setText("") super().restore_defaults() def load(self): self.ui.enable_tagger_scripts.setChecked( config.setting["enable_tagger_scripts"]) for pos, name, enabled, text in config.setting["list_of_scripts"]: list_item = ScriptListWidgetItem(name, enabled, text) self.ui.script_list.addItem(list_item) # Select the last selected script item last_selected_script_pos = config.persist["last_selected_script_pos"] last_selected_script = self.ui.script_list.item( last_selected_script_pos) if last_selected_script: self.ui.script_list.setCurrentItem(last_selected_script) last_selected_script.setSelected(True) self.restore_state() def _all_scripts(self): for row in range(0, self.ui.script_list.count()): item = self.ui.script_list.item(row) yield item.get_all() @restore_method def restore_state(self): # Preserve previous splitter position self.ui.splitter.restoreState(config.persist["scripting_splitter"]) def save(self): config.setting[ "enable_tagger_scripts"] = self.ui.enable_tagger_scripts.isChecked( ) config.setting["list_of_scripts"] = list(self._all_scripts()) config.persist[ "last_selected_script_pos"] = self.ui.script_list.currentRow() config.persist["scripting_splitter"] = self.ui.splitter.saveState() def display_error(self, error): # Ignore scripting errors, those are handled inline if not isinstance(error, ScriptCheckError): super().display_error(error)
class ArtistSearchDialog(SearchDialog): dialog_header_state = "artistsearchdialog_header_state" options = [ config.Option("persist", dialog_header_state, QtCore.QByteArray()) ] def __init__(self, parent): super().__init__(parent, accept_button_title=_("Show in browser"), search_type="artist") self.setWindowTitle(_("Artist Search Dialog")) self.columns = [ ('name', _("Name")), ('type', _("Type")), ('gender', _("Gender")), ('area', _("Area")), ('begindate', _("Begin")), ('beginarea', _("Begin Area")), ('enddate', _("End")), ('endarea', _("End Area")), ('score', _("Score")), ] def search(self, text): self.retry_params = Retry(self.search, text) self.search_box_text(text) self.show_progress() self.tagger.mb_api.find_artists(self.handle_reply, query=text, search=True, limit=QUERY_LIMIT) 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: artists = document['artists'] except (KeyError, TypeError): self.no_results() return del self.search_results[:] self.parse_artists(artists) self.display_results() def parse_artists(self, artists): for node in artists: artist = Metadata() artist_to_metadata(node, artist) artist['score'] = node['score'] self.search_results.append(artist) def display_results(self): self.prepare_table() for row, artist in enumerate(self.search_results): self.table.insertRow(row) self.set_table_item(row, 'name', artist, "name") self.set_table_item(row, 'type', artist, "type") self.set_table_item(row, 'gender', artist, "gender") self.set_table_item(row, 'area', artist, "area") self.set_table_item(row, 'begindate', artist, "begindate") self.set_table_item(row, 'beginarea', artist, "beginarea") self.set_table_item(row, 'enddate', artist, "enddate") self.set_table_item(row, 'endarea', artist, "endarea") self.set_table_item(row, 'score', artist, "score") self.show_table(sort_column='score') def accept_event(self, rows): for row in rows: self.load_in_browser(row) def load_in_browser(self, row): self.tagger.search(self.search_results[row]["musicbrainz_artistid"], "artist")
class ScriptingOptionsPage(OptionsPage): NAME = "scripting" TITLE = N_("Scripting") PARENT = None SORT_ORDER = 85 ACTIVE = True options = [ config.BoolOption("setting", "enable_tagger_scripts", False), config.ListOption("setting", "list_of_scripts", []), config.IntOption("persist", "last_selected_script_pos", 0), config.Option("persist", "scripting_splitter", QtCore.QByteArray()), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_ScriptingOptionsPage() self.ui.setupUi(self) self.highlighter = TaggerScriptSyntaxHighlighter( self.ui.tagger_script.document()) self.ui.tagger_script.setEnabled(False) self.ui.splitter.setStretchFactor(0, 1) self.ui.splitter.setStretchFactor(1, 2) font = QtGui.QFont('Monospace') font.setStyleHint(QtGui.QFont.TypeWriter) self.ui.tagger_script.setFont(font) self.move_view = MoveableListView(self.ui.script_list, self.ui.move_up_button, self.ui.move_down_button) def script_selected(self): items = self.ui.script_list.selectedItems() if items: item = items[0] self.ui.tagger_script.setEnabled(True) self.ui.tagger_script.setText(item.script) else: self.ui.tagger_script.setEnabled(False) self.ui.tagger_script.setText("") def live_update_and_check(self): items = self.ui.script_list.selectedItems() if items: script = items[0] script.script = self.ui.tagger_script.toPlainText() self.ui.script_error.setStyleSheet("") self.ui.script_error.setText("") try: self.check() except OptionsCheckError as e: self.ui.script_error.setStyleSheet(self.STYLESHEET_ERROR) self.ui.script_error.setText(e.info) return def check(self): parser = ScriptParser() try: parser.eval(self.ui.tagger_script.toPlainText()) except Exception as e: raise ScriptCheckError(_("Script Error"), str(e)) def restore_defaults(self): # Remove existing scripts self.ui.script_list.clear() self.ui.tagger_script.setText("") super().restore_defaults() def load(self): self.ui.enable_tagger_scripts.setChecked( config.setting["enable_tagger_scripts"]) for pos, name, enabled, text in config.setting["list_of_scripts"]: list_item = ScriptListWidgetItem(name, enabled, text) self.ui.script_list.addItem(list_item) # Select the last selected script item last_selected_script_pos = config.persist["last_selected_script_pos"] last_selected_script = self.ui.script_list.item( last_selected_script_pos) if last_selected_script: self.ui.script_list.setCurrentItem(last_selected_script) last_selected_script.setSelected(True) self.restore_state() args = { "picard-doc-scripting-url": PICARD_URLS['doc_scripting'], } text = _('<a href="%(picard-doc-scripting-url)s">Open Scripting' ' Documentation in your browser</a>') % args self.ui.scripting_doc_link.setText(text) def _all_scripts(self): for row in range(0, self.ui.script_list.count()): item = self.ui.script_list.item(row) yield item.get_all() @restore_method def restore_state(self): # Preserve previous splitter position self.ui.splitter.restoreState(config.persist["scripting_splitter"]) def save(self): config.setting[ "enable_tagger_scripts"] = self.ui.enable_tagger_scripts.isChecked( ) config.setting["list_of_scripts"] = list(self._all_scripts()) config.persist[ "last_selected_script_pos"] = self.ui.script_list.currentRow() config.persist["scripting_splitter"] = self.ui.splitter.saveState() def display_error(self, error): # Ignore scripting errors, those are handled inline if not isinstance(error, ScriptCheckError): super().display_error(error)
class BaseTreeView(QtWidgets.QTreeWidget): options = [ config.Option("setting", "color_modified", QtGui.QColor(QtGui.QPalette.WindowText)), config.Option("setting", "color_saved", QtGui.QColor(0, 128, 0)), config.Option("setting", "color_error", QtGui.QColor(200, 0, 0)), config.Option("setting", "color_pending", QtGui.QColor(128, 128, 128)), ] def __init__(self, window, parent=None): super().__init__(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(QtWidgets.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 = QtWidgets.QAction(_("&Expand all"), self) self.expand_all_action.triggered.connect(self.expandAll) self.collapse_all_action = QtWidgets.QAction(_("&Collapse all"), self) self.collapse_all_action.triggered.connect(self.collapseAll) self.select_all_action = QtWidgets.QAction(_("Select &all"), self) self.select_all_action.triggered.connect(self.selectAll) self.select_all_action.setShortcut(QtGui.QKeySequence(_("Ctrl+A"))) 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 can_view_info = self.window.view_info_action.isEnabled() menu = QtWidgets.QMenu(self) if isinstance(obj, Track): if can_view_info: menu.addAction(self.window.view_info_action) plugin_actions = list(_track_actions) if obj.num_linked_files == 1: menu.addAction(self.window.play_file_action) menu.addAction(self.window.open_folder_action) menu.addAction(self.window.track_search_action) plugin_actions.extend(_file_actions) menu.addAction(self.window.browser_lookup_action) menu.addSeparator() if isinstance(obj, NonAlbumTrack): menu.addAction(self.window.refresh_action) elif isinstance(obj, Cluster): if can_view_info: menu.addAction(self.window.view_info_action) menu.addAction(self.window.browser_lookup_action) menu.addSeparator() menu.addAction(self.window.autotag_action) menu.addAction(self.window.analyze_action) if isinstance(obj, UnclusteredFiles): menu.addAction(self.window.cluster_action) else: menu.addAction(self.window.album_search_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(_clusterlist_actions) elif isinstance(obj, File): if can_view_info: menu.addAction(self.window.view_info_action) menu.addAction(self.window.play_file_action) menu.addAction(self.window.open_folder_action) menu.addAction(self.window.browser_lookup_action) menu.addSeparator() menu.addAction(self.window.autotag_action) menu.addAction(self.window.analyze_action) menu.addAction(self.window.track_search_action) plugin_actions = list(_file_actions) elif isinstance(obj, Album): if can_view_info: menu.addAction(self.window.view_info_action) menu.addAction(self.window.browser_lookup_action) menu.addSeparator() menu.addAction(self.window.refresh_action) plugin_actions = list(_album_actions) menu.addAction(self.window.save_action) menu.addAction(self.window.remove_action) bottom_separator = False if isinstance(obj, Album) and not isinstance(obj, NatAlbum) and obj.loaded: releases_menu = QtWidgets.QMenu(_("&Other versions"), menu) menu.addSeparator() menu.addMenu(releases_menu) loading = releases_menu.addAction(_('Loading...')) loading.setDisabled(True) bottom_separator = True if len(self.selectedIndexes()) == len(MainPanel.columns): def _add_other_versions(): releases_menu.removeAction(loading) heading = releases_menu.addAction(obj.release_group.version_headings) heading.setDisabled(True) font = heading.font() font.setBold(True) heading.setFont(font) versions = obj.release_group.versions albumtracks = obj.get_num_total_files() if obj.get_num_total_files() else len(obj.tracks) preferred_countries = set(config.setting["preferred_release_countries"]) preferred_formats = set(config.setting["preferred_release_formats"]) matches = ("trackmatch", "countrymatch", "formatmatch") priorities = {} for version in versions: priority = { "trackmatch": "0" if version['totaltracks'] == albumtracks else "?", "countrymatch": "0" if len(preferred_countries) == 0 or preferred_countries & set(version['countries'] or '') else "?", "formatmatch": "0" if len(preferred_formats) == 0 or preferred_formats & set(version['formats'] or '') else "?", } priorities[version['id']] = "".join(priority[k] for k in matches) versions.sort(key=lambda version: priorities[version['id']] + version['name']) priority = normal = False for version in versions: if not normal and "?" in priorities[version['id']]: if priority: releases_menu.addSeparator() normal = True else: priority = True action = releases_menu.addAction(version["name"]) action.setCheckable(True) if obj.id == version["id"]: action.setChecked(True) action.triggered.connect(partial(obj.switch_release_version, version["id"])) if obj.release_group.loaded: _add_other_versions() else: obj.release_group.load_versions(_add_other_versions) releases_menu.setEnabled(True) else: releases_menu.setEnabled(False) if config.setting["enable_ratings"] and \ len(self.window.selected_objects) == 1 and isinstance(obj, Track): menu.addSeparator() action = QtWidgets.QWidgetAction(menu) action.setDefaultWidget(RatingWidget(menu, obj)) menu.addAction(action) menu.addSeparator() # Using type here is intentional. isinstance will return true for the # NatAlbum instance, which can't be part of a collection. selected_albums = [a for a in self.window.selected_objects if type(a) == Album] if selected_albums: if not bottom_separator: menu.addSeparator() menu.addMenu(CollectionMenu(selected_albums, _("Collections"), menu)) scripts = config.setting["list_of_scripts"] if plugin_actions or scripts: menu.addSeparator() if plugin_actions: plugin_menu = QtWidgets.QMenu(_("P&lugins"), menu) plugin_menu.setIcon(self.panel.icon_plugins) menu.addMenu(plugin_menu) plugin_menus = {} for action in plugin_actions: action_menu = plugin_menu for index in range(1, len(action.MENU) + 1): key = tuple(action.MENU[:index]) if key in plugin_menus: action_menu = plugin_menus[key] else: action_menu = plugin_menus[key] = action_menu.addMenu(key[-1]) action_menu.addAction(action) if scripts: scripts_menu = ScriptsMenu(scripts, _("&Run scripts"), menu) scripts_menu.setIcon(self.panel.icon_plugins) menu.addMenu(scripts_menu) if isinstance(obj, Cluster) or isinstance(obj, ClusterList) or isinstance(obj, Album): menu.addSeparator() menu.addAction(self.expand_all_action) menu.addAction(self.collapse_all_action) menu.addAction(self.select_all_action) menu.exec_(event.globalPos()) event.accept() @restore_method def restore_state(self): sizes = config.persist[self.view_sizes.name] 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): cols = range(self.numHeaderSections - 1) sizes = " ".join(string_(self.header().sectionSize(i)) for i in cols) config.persist[self.view_sizes.name] = 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.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.exec_(QtCore.Qt.MoveAction) def mimeData(self, items): """Return MIME data for specified items.""" album_ids = [] files = [] url = QtCore.QUrl.fromLocalFile for item in items: obj = item.obj if isinstance(obj, Album): album_ids.append(string_(obj.id)) elif obj.iterfiles: files.extend([url(f.filename) for f in obj.iterfiles()]) mimeData = QtCore.QMimeData() mimeData.setData("application/picard.album-list", "\n".join(album_ids).encode()) if files: mimeData.setUrls(files) return mimeData @staticmethod def drop_urls(urls, target): files = [] new_files = [] for url in urls: log.debug("Dropped the URL: %r", url.toString(QtCore.QUrl.RemoveUserInfo)) if url.scheme() == "file" or not url.scheme(): filename = os.path.normpath(os.path.realpath(url.toLocalFile().rstrip("\0"))) file = BaseTreeView.tagger.files.get(filename) if file: files.append(file) elif os.path.isdir(encode_filename(filename)): BaseTreeView.tagger.add_directory(filename) else: new_files.append(filename) elif url.scheme() in ("http", "https"): path = url.path() match = re.search(r"/(release|recording)/([0-9a-z\-]{36})", path) if match: entity = match.group(1) mbid = match.group(2) if entity == "release": BaseTreeView.tagger.load_album(mbid) elif entity == "recording": BaseTreeView.tagger.load_nat(mbid) if files: BaseTreeView.tagger.move_files(files, target) if new_files: BaseTreeView.tagger.add_files(new_files, target=target) def dropEvent(self, event): return QtWidgets.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 log.debug("Drop target = %r", target) handled = False # text/uri-list urls = data.urls() if urls: self.drop_urls(urls, target) handled = True # application/picard.album-list albums = data.data("application/picard.album-list") if albums: if isinstance(self, FileTreeView) and target is None: target = self.tagger.unclustered_files albums = [self.tagger.load_album(id) for id in string_(albums).split("\n")] self.tagger.move_files(self.tagger.get_files_from_objects(albums), target) handled = True return handled def activate_item(self, index): obj = self.itemFromIndex(index).obj # Double-clicking albums or clusters should expand them. The album info can be # viewed by using the toolbar button. if not isinstance(obj, (Album, Cluster)) and obj.can_view_info(): self.window.view_info() 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) def moveCursor(self, action, modifiers): if action in (QtWidgets.QAbstractItemView.MoveUp, QtWidgets.QAbstractItemView.MoveDown): item = self.currentItem() if item and not item.isSelected(): self.setCurrentItem(item) return QtWidgets.QTreeWidget.moveCursor(self, action, modifiers)
class MainWindow(QtGui.QMainWindow): options = [ config.Option("persist", "window_state", QtCore.QByteArray(), QtCore.QVariant.toByteArray), config.Option("persist", "window_position", QtCore.QPoint(), QtCore.QVariant.toPoint), config.Option("persist", "window_size", QtCore.QSize(780, 560), QtCore.QVariant.toSize), config.Option("persist", "bottom_splitter_state", QtCore.QByteArray(), QtCore.QVariant.toByteArray), config.BoolOption("persist", "window_maximized", False), config.BoolOption("persist", "view_cover_art", False), config.BoolOption("persist", "view_file_browser", False), config.TextOption("persist", "current_directory", ""), ] def __init__(self, parent=None): QtGui.QMainWindow.__init__(self, parent) self.selected_objects = [] self.ignore_selection_changes = False 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() mainLayout = QtGui.QSplitter(QtCore.Qt.Vertical) mainLayout.setContentsMargins(0, 0, 0, 0) mainLayout.setHandleWidth(1) self.panel = MainPanel(self, mainLayout) 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.metadata_box = MetadataBox(self) self.cover_art_box = CoverArtBox(self) if not self.show_cover_art_action.isChecked(): self.cover_art_box.hide() bottomLayout = QtGui.QHBoxLayout() bottomLayout.setContentsMargins(0, 0, 0, 0) bottomLayout.setSpacing(0) bottomLayout.addWidget(self.metadata_box, 1) bottomLayout.addWidget(self.cover_art_box, 0) bottom = QtGui.QWidget() bottom.setLayout(bottomLayout) mainLayout.addWidget(self.panel) mainLayout.addWidget(bottom) self.setCentralWidget(mainLayout) # accessibility self.set_tab_order() # FIXME: use QApplication's clipboard self._clipboard = [] for function in ui_init: function(self) def keyPressEvent(self, event): if event.matches(QtGui.QKeySequence.Delete): if self.metadata_box.hasFocus(): self.metadata_box.remove_selected_tags() else: self.remove() else: QtGui.QMainWindow.keyPressEvent(self, event) def show(self): self.restoreWindowState() QtGui.QMainWindow.show(self) self.metadata_box.restore_state() def closeEvent(self, event): if 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): 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() config.persist["window_position"] = geom.topLeft() config.persist["window_size"] = geom.size() else: pos = self.pos() if not pos.isNull(): config.persist["window_position"] = pos config.persist["window_size"] = self.size() config.persist["window_maximized"] = isMaximized config.persist[ "view_cover_art"] = self.show_cover_art_action.isChecked() config.persist[ "view_file_browser"] = self.show_file_browser_action.isChecked() config.persist["bottom_splitter_state"] = self.centralWidget( ).saveState() self.file_browser.save_state() self.panel.save_state() self.metadata_box.save_state() def restoreWindowState(self): self.restoreState(config.persist["window_state"]) pos = config.persist["window_position"] size = 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 config.persist["window_maximized"]: self.setWindowState(QtCore.Qt.WindowMaximized) bottom_splitter_state = config.persist["bottom_splitter_state"] if bottom_splitter_state.isEmpty(): self.centralWidget().setSizes([366, 194]) else: self.centralWidget().restoreState(bottom_splitter_state) self.file_browser.restore_state() def create_statusbar(self): """Creates a new status bar.""" self.statusBar().showMessage(_("Ready")) self.infostatus = InfoStatus(self) self.listening_label = QtGui.QLabel() self.listening_label.setVisible(False) self.listening_label.setToolTip( _("Picard listens on a port to integrate with your browser and downloads release" " information when you click the \"Tagger\" buttons on the MusicBrainz website" )) self.statusBar().addPermanentWidget(self.infostatus) self.statusBar().addPermanentWidget(self.listening_label) self.tagger.tagger_stats_changed.connect(self.update_statusbar_stats) self.tagger.listen_port_changed.connect( self.update_statusbar_listen_port) self.update_statusbar_stats() @throttle(250) def update_statusbar_stats(self): """Updates the status bar information.""" self.infostatus.setFiles(len(self.tagger.files)) self.infostatus.setAlbums(len(self.tagger.albums)) self.infostatus.setPendingFiles(File.num_pending_files) ws = self.tagger.xmlws self.infostatus.setPendingRequests( max(ws.num_pending_web_requests, ws.num_active_requests)) def update_statusbar_listen_port(self, listen_port): if listen_port: self.listening_label.setVisible(True) self.listening_label.setText( _(" Listening on port %(port)d ") % {"port": listen_port}) else: self.listening_label.setVisible(False) def set_statusbar_message(self, message, *args, **kwargs): """Set the status bar message.""" if message: try: log.debug(repr(message.replace('%%s', '%%r')), *args) except: pass if args: message = _(message) % args else: message = _(message) log.history_info(message) thread.to_main(self.statusBar().showMessage, message, kwargs.get("timeout", 0)) def _on_submit(self): if self.tagger.use_acoustid: if not 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() def create_actions(self): self.options_action = QtGui.QAction( icontheme.lookup('preferences-desktop'), _("&Options..."), self) self.options_action.setMenuRole(QtGui.QAction.PreferencesRole) self.options_action.triggered.connect(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.cut_action.triggered.connect(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.paste_action.triggered.connect(self.paste) self.help_action = QtGui.QAction(_("&Help..."), self) self.help_action.setShortcut(QtGui.QKeySequence.HelpContents) self.help_action.triggered.connect(self.show_help) self.about_action = QtGui.QAction(_("&About..."), self) self.about_action.setMenuRole(QtGui.QAction.AboutRole) self.about_action.triggered.connect(self.show_about) self.donate_action = QtGui.QAction(_("&Donate..."), self) self.donate_action.triggered.connect(self.open_donation_page) self.report_bug_action = QtGui.QAction(_("&Report a Bug..."), self) self.report_bug_action.triggered.connect(self.open_bug_report) self.support_forum_action = QtGui.QAction(_("&Support Forum..."), self) self.support_forum_action.triggered.connect(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.add_files_action.triggered.connect(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.add_directory_action.triggered.connect(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.save_action.triggered.connect(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.submit_action.triggered.connect(self._on_submit) self.exit_action = QtGui.QAction(_(u"E&xit"), self) self.exit_action.setMenuRole(QtGui.QAction.QuitRole) # TR: Keyboard shortcut for "Exit" self.exit_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+Q"))) self.exit_action.triggered.connect(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.setEnabled(False) self.remove_action.triggered.connect(self.remove) self.browser_lookup_action = QtGui.QAction( icontheme.lookup('lookup-musicbrainz'), _(u"Lookup in &Browser"), self) self.browser_lookup_action.setStatusTip( _(u"Lookup selected item on MusicBrainz website")) self.browser_lookup_action.setEnabled(False) self.browser_lookup_action.triggered.connect(self.browser_lookup) self.show_file_browser_action = QtGui.QAction(_(u"File &Browser"), self) self.show_file_browser_action.setCheckable(True) if config.persist["view_file_browser"]: self.show_file_browser_action.setChecked(True) self.show_file_browser_action.setShortcut( QtGui.QKeySequence(_(u"Ctrl+B"))) self.show_file_browser_action.triggered.connect(self.show_file_browser) self.show_cover_art_action = QtGui.QAction(_(u"&Cover Art"), self) self.show_cover_art_action.setCheckable(True) if config.persist["view_cover_art"]: self.show_cover_art_action.setChecked(True) self.show_cover_art_action.triggered.connect(self.show_cover_art) self.search_action = QtGui.QAction(icontheme.lookup('system-search'), _(u"Search"), self) self.search_action.triggered.connect(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.cd_lookup_action.triggered.connect(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.analyze_action.triggered.connect(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.cluster_action.triggered.connect(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.autotag_action.triggered.connect(self.autotag) self.view_info_action = QtGui.QAction( icontheme.lookup('picard-edit-tags'), _(u"&Info..."), self) self.view_info_action.setEnabled(False) # TR: Keyboard shortcut for "Info" self.view_info_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+I"))) self.view_info_action.triggered.connect(self.view_info) self.refresh_action = QtGui.QAction( icontheme.lookup('view-refresh', icontheme.ICON_SIZE_MENU), _("&Refresh"), self) self.refresh_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+R"))) self.refresh_action.triggered.connect(self.refresh) self.enable_renaming_action = QtGui.QAction(_(u"&Rename Files"), self) self.enable_renaming_action.setCheckable(True) self.enable_renaming_action.setChecked(config.setting["rename_files"]) self.enable_renaming_action.triggered.connect(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(config.setting["move_files"]) self.enable_moving_action.triggered.connect(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 config.setting["dont_write_tags"]) self.enable_tag_saving_action.triggered.connect(self.toggle_tag_saving) self.tags_from_filenames_action = QtGui.QAction( _(u"Tags From &File Names..."), self) self.tags_from_filenames_action.triggered.connect( self.open_tags_from_filenames) self.view_log_action = QtGui.QAction(_(u"View &Log..."), self) self.view_log_action.triggered.connect(self.show_log) self.view_history_action = QtGui.QAction(_(u"View Status &History..."), self) self.view_history_action.triggered.connect(self.show_history) xmlws_manager = self.tagger.xmlws.manager xmlws_manager.authenticationRequired.connect(self.show_password_dialog) xmlws_manager.proxyAuthenticationRequired.connect( self.show_proxy_dialog) self.open_file_action = QtGui.QAction(_(u"&Open..."), self) self.open_file_action.setStatusTip(_(u"Open the file")) self.open_file_action.triggered.connect(self.open_file) self.open_folder_action = QtGui.QAction(_(u"Open &Folder..."), self) self.open_folder_action.setStatusTip(_(u"Open the containing folder")) self.open_folder_action.triggered.connect(self.open_folder) def toggle_rename_files(self, checked): config.setting["rename_files"] = checked def toggle_move_files(self, checked): config.setting["move_files"] = checked def toggle_tag_saving(self, checked): 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.view_info_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.addSeparator() menu.addAction(self.refresh_action) 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.addAction(self.browser_lookup_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.addAction(self.view_history_action) menu.addSeparator() menu.addAction(self.donate_action) menu.addAction(self.about_action) def update_toolbar_style(self): if 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 and discid is not None) def create_toolbar(self): self.toolbar = toolbar = self.addToolBar(_(u"Actions")) self.update_toolbar_style() toolbar.setObjectName("main_toolbar") def add_toolbar_action(action): toolbar.addAction(action) widget = toolbar.widgetForAction(action) widget.setFocusPolicy(QtCore.Qt.TabFocus) widget.setAttribute(QtCore.Qt.WA_MacShowFocusRect) add_toolbar_action(self.add_files_action) add_toolbar_action(self.add_directory_action) toolbar.addSeparator() add_toolbar_action(self.save_action) add_toolbar_action(self.submit_action) toolbar.addSeparator() add_toolbar_action(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.cd_lookup_menu.triggered.connect(self.tagger.lookup_cd) button = toolbar.widgetForAction(self.cd_lookup_action) button.setPopupMode(QtGui.QToolButton.MenuButtonPopup) button.setMenu(self.cd_lookup_menu) add_toolbar_action(self.cluster_action) add_toolbar_action(self.autotag_action) add_toolbar_action(self.analyze_action) add_toolbar_action(self.view_info_action) add_toolbar_action(self.remove_action) add_toolbar_action(self.browser_lookup_action) self.search_toolbar = toolbar = self.addToolBar(_(u"Search")) 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.search_edit.returnPressed.connect(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)) self.search_button.setAttribute(QtCore.Qt.WA_MacShowFocusRect) hbox.addWidget(self.search_button) toolbar.addWidget(search_panel) def set_tab_order(self): tab_order = self.setTabOrder tw = self.toolbar.widgetForAction # toolbar tab_order(tw(self.add_files_action), tw(self.add_directory_action)) tab_order(tw(self.add_directory_action), tw(self.save_action)) tab_order(tw(self.save_action), tw(self.submit_action)) tab_order(tw(self.submit_action), tw(self.cd_lookup_action)) tab_order(tw(self.cd_lookup_action), tw(self.cluster_action)) tab_order(tw(self.cluster_action), tw(self.autotag_action)) tab_order(tw(self.autotag_action), tw(self.analyze_action)) tab_order(tw(self.analyze_action), tw(self.view_info_action)) tab_order(tw(self.view_info_action), tw(self.remove_action)) tab_order(tw(self.remove_action), tw(self.browser_lookup_action)) tab_order(tw(self.browser_lookup_action), self.search_combo) tab_order(self.search_combo, self.search_edit) tab_order(self.search_edit, self.search_button) # panels tab_order(self.search_button, self.file_browser) tab_order(self.file_browser, self.panel.views[0]) tab_order(self.panel.views[0], self.panel.views[1]) tab_order(self.panel.views[1], self.metadata_box) def enable_submit(self, enabled): """Enable/disable the 'Submit fingerprints' 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, config.setting["use_adv_search_syntax"]) def add_files(self): """Add files to the tagger.""" current_directory = 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) 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 = config.persist[ "current_directory"] or QtCore.QDir.homePath() current_directory = find_existing_path(unicode(current_directory)) dir_list = [] if not 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: config.persist["current_directory"] = dir_list[0] elif len(dir_list) > 1: (parent, dir) = os.path.split(str(dir_list[0])) 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 LogView(self).show() def show_history(self): from picard.ui.logview import HistoryView HistoryView(self).show() def confirm_va_removal(self): return QtGui.QMessageBox.question( self, _("Various Artists file naming scheme removal"), _("""The separate file naming scheme for various artists albums has been removed in this version of Picard. You currently do not use the this option, but have a separate file naming scheme defined. Do you want to remove it or merge it with your file naming scheme for single artist albums?"""), _("Merge"), _("Remove")) def show_va_removal_notice(self): QtGui.QMessageBox.information( self, _("Various Artists file naming scheme removal"), _("""The separate file naming scheme for various artists albums has been removed in this version of Picard. Your file naming scheme has automatically been merged with that of single artist albums."""), QtGui.QMessageBox.Ok) 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') def save(self): """Tell the tagger to save the selected objects.""" self.tagger.save(self.selected_objects) def remove(self): """Tell the tagger to remove the selected objects.""" self.panel.remove(self.selected_objects) def analyze(self): if not config.setting['fingerprinting_system']: if self.show_analyze_settings_info(): self.show_options("fingerprinting") if not config.setting['fingerprinting_system']: return return self.tagger.analyze(self.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 view_info(self): if isinstance(self.selected_objects[0], Album): album = self.selected_objects[0] dialog = AlbumInfoDialog(album, self) else: file = self.tagger.get_files_from_objects(self.selected_objects)[0] dialog = FileInfoDialog(file, self) dialog.exec_() def cluster(self): self.tagger.cluster(self.selected_objects) def refresh(self): self.tagger.refresh(self.selected_objects) def browser_lookup(self): self.tagger.browser_lookup(self.selected_objects[0]) @throttle(100) def update_actions(self): can_remove = False can_save = False can_analyze = False can_refresh = False can_autotag = False single = self.selected_objects[0] if len( self.selected_objects) == 1 else None can_view_info = bool(single and single.can_view_info()) can_browser_lookup = bool(single and single.can_browser_lookup()) 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_refresh(): can_refresh = True if obj.can_autotag(): can_autotag = True if can_save and can_remove and can_refresh and can_autotag: break self.remove_action.setEnabled(can_remove) self.save_action.setEnabled(can_save) self.view_info_action.setEnabled(can_view_info) self.analyze_action.setEnabled(can_analyze) self.refresh_action.setEnabled(can_refresh) self.autotag_action.setEnabled(can_autotag) self.browser_lookup_action.setEnabled(can_browser_lookup) self.cut_action.setEnabled(bool(self.selected_objects)) def update_selection(self, objects=None): if self.ignore_selection_changes: return if objects is not None: self.selected_objects = objects else: objects = self.selected_objects self.update_actions() metadata = None statusbar = u"" obj = None if len(objects) == 1: obj = list(objects)[0] if isinstance(obj, File): metadata = obj.metadata statusbar = obj.filename if obj.state == obj.ERROR: statusbar += _(" (Error: %s)") % obj.error elif isinstance(obj, Track): metadata = obj.metadata if obj.num_linked_files == 1: file = obj.linked_files[0] statusbar = "%s (%d%%)" % (file.filename, file.similarity * 100) if file.state == File.ERROR: statusbar += _(" (Error: %s)") % file.error elif obj.can_edit_tags(): metadata = obj.metadata self.metadata_box.selection_dirty = True self.metadata_box.update() 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() self.metadata_box.shrink_columns() 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.selected_objects) def cut(self): self._clipboard = self.selected_objects self.paste_action.setEnabled(bool(self._clipboard)) def paste(self): selected_objects = self.selected_objects if not selected_objects: target = self.tagger.unmatched_files else: target = selected_objects[0] self.tagger.move_files( self.tagger.get_files_from_objects(self._clipboard), target) self._clipboard = [] self.paste_action.setEnabled(False)
class MetadataBox(QtGui.QTableWidget): options = ( config.Option("persist", "metadatabox_header_state", QtCore.QByteArray()), config.BoolOption("persist", "show_changes_first", False) ) def __init__(self, parent): QtGui.QTableWidget.__init__(self, parent) 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().setResizeMode(QtGui.QHeaderView.Stretch) self.horizontalHeader().setClickable(False) self.verticalHeader().setDefaultSectionSize(21) self.verticalHeader().setVisible(False) self.setHorizontalScrollMode(QtGui.QAbstractItemView.ScrollPerPixel) self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) self.setTabKeyNavigation(False) self.setStyleSheet("QTableWidget {border: none;}") self.setAttribute(QtCore.Qt.WA_MacShowFocusRect, 1) self.colors = { TagStatus.NoChange: self.palette().color(QtGui.QPalette.Text), TagStatus.Removed: QtGui.QBrush(QtGui.QColor("red")), TagStatus.Added: QtGui.QBrush(QtGui.QColor("green")), TagStatus.Changed: QtGui.QBrush(QtGui.QColor("darkgoldenrod")) } 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 = QtGui.QAction(_(u"Add New Tag..."), parent) self.add_tag_action.triggered.connect(partial(self.edit_tag, "")) self.changes_first_action = QtGui.QAction(_(u"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 = QtGui.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 = QtGui.QShortcut(QtGui.QKeySequence(_("Alt+Shift+E")), self, partial(self.edit_selected_tag)) # TR: Keyboard shortcut for "Remove" (tag) self.remove_tag_shortcut = QtGui.QShortcut(QtGui.QKeySequence(_("Alt+Shift+R")), self, self.remove_selected_tags) def get_file_lookup(self): """Return a FileLookup object.""" 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.recordingLookup, "musicbrainz_trackid": lookup.trackLookup, "musicbrainz_albumid": lookup.albumLookup, "musicbrainz_workid": lookup.workLookup, "musicbrainz_artistid": lookup.artistLookup, "musicbrainz_albumartistid": lookup.artistLookup, "musicbrainz_releasegroupid": lookup.releaseGroupLookup, "acoustid_id": lookup.acoustLookup } 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() != 2: return False item = self.itemFromIndex(index) if item.flags() & QtCore.Qt.ItemIsEditable and \ trigger in (QtGui.QAbstractItemView.DoubleClicked, QtGui.QAbstractItemView.EditKeyPressed, QtGui.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 QtGui.QTableWidget.edit(self, index, trigger, event) return False def event(self, e): item = self.currentItem() if (item and e.type() == QtCore.QEvent.KeyPress and e.modifiers() == QtCore.Qt.ControlModifier): column = item.column() tag = self.tag_diff.tag_names[item.row()] if e.key() == QtCore.Qt.Key_C: if column == 1: self.clipboard = list(self.tag_diff.orig[tag]) elif column == 2: self.clipboard = list(self.tag_diff.new[tag]) elif e.key() == QtCore.Qt.Key_V and column == 2 and tag != "~length": self.set_tag_values(tag, list(self.clipboard)) return QtGui.QTableWidget.event(self, e) def closeEditor(self, editor, hint): QtGui.QTableWidget.closeEditor(self, editor, hint) tag = self.tag_diff.tag_names[self.editing.row()] old = self.tag_diff.new[tag] new = [unicode(editor.text())] if old == new: self.editing.setText(old[0]) else: self.set_tag_values(tag, new) self.editing = None self.update() def contextMenuEvent(self, event): menu = QtGui.QMenu(self) if self.objects: tags = self.selected_tags() if len(tags) == 1: edit_tag_action = QtGui.QAction(_(u"Edit..."), self.parent) edit_tag_action.triggered.connect(partial(self.edit_tag, list(tags)[0])) edit_tag_action.setShortcut(self.edit_tag_shortcut.key()) menu.addAction(edit_tag_action) removals = [] useorigs = [] item = self.currentItem() column = item.column() for tag in tags: if tag in self.lookup_tags().keys(): if (column == 1 or column == 2) and len(tags) == 1 and item.text(): if column == 1: values = self.tag_diff.orig[tag] else: values = self.tag_diff.new[tag] lookup_action = QtGui.QAction(_(u"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: for file in self.files: objects = [file] if file.parent in self.tracks and len(self.files & set(file.parent.linked_files)) == 1: objects.append(file.parent) orig_values = list(file.orig_metadata.getall(tag)) or [""] useorigs.append(partial(self.set_tag_values, tag, orig_values, objects)) if removals: remove_tag_action = QtGui.QAction(_(u"Remove"), self.parent) remove_tag_action.triggered.connect(lambda: [f() for f in removals]) remove_tag_action.setShortcut(self.remove_tag_shortcut.key()) menu.addAction(remove_tag_action) if useorigs: name = ungettext("Use Original Value", "Use Original Values", len(useorigs)) use_orig_value_action = QtGui.QAction(name, self.parent) use_orig_value_action.triggered.connect(lambda: [f() for f in useorigs]) menu.addAction(use_orig_value_action) menu.addSeparator() if len(tags) == 1 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 edit_tag(self, tag): EditTagDialog(self.parent, tag).exec_() def edit_selected_tag(self): tags = self.selected_tags() if len(tags) == 1: self.edit_tag(list(tags)[0]) def toggle_changes_first(self, checked): config.persist["show_changes_first"] = checked self.update() def set_tag_values(self, tag, values, objects=None): if objects is None: objects = self.objects self.parent.ignore_selection_changes = True if values != [""] or self.tag_is_removable(tag): for obj in objects: obj.metadata[tag] = values obj.update() self.update() self.parent.ignore_selection_changes = False def remove_tag(self, tag): self.set_tag_values(tag, [""]) def remove_selected_tags(self): for tag in self.selected_tags(): if self.tag_is_removable(tag): self.remove_tag(tag) def tag_is_removable(self, tag): return self.tag_diff.status[tag] & TagStatus.NotRemovable == 0 def selected_tags(self): tags = set(self.tag_diff.tag_names[item.row()] for item in self.selectedItems()) tags.discard("~length") return 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.linked_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.linked_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): if self.editing: return if self.selection_dirty: self._update_selection() thread.run_task(self._update_tags, self._update_items) def _update_tags(self): self.selection_mutex.lock() files = self.files tracks = self.tracks self.selection_mutex.unlock() if not (files or tracks): return None tag_diff = TagDiff() orig_tags = tag_diff.orig new_tags = tag_diff.new # existing_tags are orig_tags that would not be overwritten by # any new_tags, assuming clear_existing_tags is disabled. existing_tags = set() tag_diff.objects = len(files) clear_existing_tags = config.setting["clear_existing_tags"] for file in files: new_metadata = file.new_metadata orig_metadata = file.orig_metadata tags = set(new_metadata.keys() + 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 ((new_values and name not in existing_tags) or clear_existing_tags): new_values = list(orig_values or [""]) existing_tags.add(name) removed = name in new_metadata.deleted_tags tag_diff.add(name, orig_values, new_values, True, removed) tag_diff.add("~length", str(orig_metadata.length), str(new_metadata.length), False) for track in tracks: if track.num_linked_files == 0: for name, values in dict.iteritems(track.metadata): if not name.startswith("~"): tag_diff.add(name, values, values, True) length = str(track.metadata.length) tag_diff.add("~length", length, length, False) tag_diff.objects += 1 all_tags = set(orig_tags.keys() + new_tags.keys()) 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): length = name == "~length" tag_item = self.item(i, 0) orig_item = self.item(i, 1) new_item = self.item(i, 2) if not tag_item: tag_item = QtGui.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 = QtGui.QTableWidgetItem() orig_item.setFlags(orig_flags) self.setItem(i, 1, orig_item) if not new_item: new_item = QtGui.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) new_item.setFlags(orig_flags if length else 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) def set_item_value(self, item, tags, name): text, italic = tags.display_value(name) item.setText(text) font = item.font() font.setItalic(italic) item.setFont(font) def restore_state(self): state = config.persist["metadatabox_header_state"] header = self.horizontalHeader() header.restoreState(state) header.setResizeMode(QtGui.QHeaderView.Interactive) def save_state(self): header = self.horizontalHeader() state = header.saveState() config.persist["metadatabox_header_state"] = state
class MainPanel(QtWidgets.QSplitter): options = [ config.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.persist["splitter_state"] = self.saveState() for view in self._views: view.save_state() @restore_method def restore_state(self): 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()
class MainPanel(QtWidgets.QSplitter): options = [ config.Option("persist", "splitter_state", QtCore.QByteArray()), ] columns = [ (N_('Title'), 'title'), (N_('Length'), '~length'), (N_('Artist'), 'artist'), ] def __init__(self, window, parent=None): super().__init__(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() TreeItem.text_color_secondary = self.palette() \ .brush(QtGui.QPalette.Disabled, QtGui.QPalette.Text).color() TrackItem.track_colors = { 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 = { 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 save_state(self): config.persist["splitter_state"] = self.saveState() for view in self.views: view.save_state() @restore_method def restore_state(self): 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.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, i, j): self._selected_view = i self.views[j].clearSelection() self.window.update_selection( [item.obj for item in self.views[i].selectedItems()]) 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 update_current_view(self): self.update_selection(self._selected_view, abs(self._selected_view - 1)) def remove(self, objects): self._ignore_selection_changes = True self.tagger.remove(objects) self._ignore_selection_changes = False view = self.views[self._selected_view] index = view.currentIndex() if index.isValid(): # select the current index view.setCurrentIndex(index) else: self.update_current_view()
class ScriptingOptionsPage(OptionsPage): NAME = "scripting" TITLE = N_("Scripting") PARENT = None SORT_ORDER = 85 ACTIVE = True options = [ config.BoolOption("setting", "enable_tagger_scripts", False), config.ListOption("setting", "list_of_scripts", []), config.IntOption("persist", "last_selected_script_pos", 0), config.Option("persist", "scripting_splitter", QtCore.QByteArray()), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_ScriptingOptionsPage() self.ui.setupUi(self) self.highlighter = TaggerScriptSyntaxHighlighter( self.ui.tagger_script.document()) self.ui.tagger_script.textChanged.connect(self.live_update_and_check) self.ui.script_name.textChanged.connect(self.script_name_changed) self.ui.add_script.clicked.connect(self.add_to_list_of_scripts) self.ui.script_list.itemSelectionChanged.connect(self.script_selected) self.ui.tagger_script.setEnabled(False) self.ui.script_name.setEnabled(False) self.listitem_to_scriptitem = {} self.list_of_scripts = [] self.last_selected_script_pos = 0 self.ui.splitter.setStretchFactor(0, 1) self.ui.splitter.setStretchFactor(1, 2) def script_name_changed(self): items = self.ui.script_list.selectedItems() if items: script = self.listitem_to_scriptitem[items[0]] script.name = self.ui.script_name.text() list_widget = self.ui.script_list.itemWidget(items[0]) list_widget.update_name(script.name) self.list_of_scripts[script.pos] = script.get_all() def script_selected(self): items = self.ui.script_list.selectedItems() if items: self.ui.tagger_script.setEnabled(True) self.ui.script_name.setEnabled(True) script = self.listitem_to_scriptitem[items[0]] self.ui.tagger_script.setText(script.text) self.ui.script_name.setText(script.name) self.last_selected_script_pos = script.pos def setSignals(self, list_widget, item): list_widget.set_up_connection( lambda: self.move_script(self.ui.script_list.row(item), 1)) list_widget.set_down_connection( lambda: self.move_script(self.ui.script_list.row(item), -1)) list_widget.set_remove_connection( lambda: self.remove_from_list_of_scripts( self.ui.script_list.row(item))) list_widget.set_checkbox_connection(lambda: self.update_check_state( item, list_widget.checkbox_state())) list_widget.set_rename_connection(lambda: self.rename_script(item)) def rename_script(self, item): item.setSelected(True) self.ui.script_name.setFocus() self.ui.script_name.selectAll() def update_check_state(self, item, checkbox_state): script = self.listitem_to_scriptitem[item] script.enabled = checkbox_state self.list_of_scripts[script.pos] = script.get_all() def add_to_list_of_scripts(self): count = self.ui.script_list.count() numbered_name = _(DEFAULT_NUMBERED_SCRIPT_NAME) % (count + 1) script = ScriptItem(pos=count, name=numbered_name) list_item = HashableListWidgetItem() list_widget = AdvancedScriptItem(numbered_name) self.setSignals(list_widget, list_item) self.ui.script_list.addItem(list_item) self.ui.script_list.setItemWidget(list_item, list_widget) self.listitem_to_scriptitem[list_item] = script self.list_of_scripts.append(script.get_all()) list_item.setSelected(True) def update_script_positions(self): for i, script in enumerate(self.list_of_scripts): self.list_of_scripts[i] = (i, script[1], script[2], script[3]) item = self.ui.script_list.item(i) self.listitem_to_scriptitem[item].pos = i def remove_from_list_of_scripts(self, row): item = self.ui.script_list.item(row) confirm_remove = QtWidgets.QMessageBox() msg = _("Are you sure you want to remove this script?") reply = confirm_remove.question(confirm_remove, _('Confirm Remove'), msg, QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) if item and reply == QtWidgets.QMessageBox.Yes: item = self.ui.script_list.takeItem(row) script = self.listitem_to_scriptitem[item] del self.listitem_to_scriptitem[item] del self.list_of_scripts[script.pos] del script item = None # update positions of other items self.update_script_positions() if not self.ui.script_list: self.ui.tagger_script.setText("") self.ui.tagger_script.setEnabled(False) self.ui.script_name.setText("") self.ui.script_name.setEnabled(False) # update position of last_selected_script if row == self.last_selected_script_pos: self.last_selected_script_pos = 0 # workaround to remove residue on UI if not self.ui.script_list.selectedItems(): current_item = self.ui.script_list.currentItem() if current_item: current_item.setSelected(True) else: item = self.ui.script_list.item(0) item.setSelected(True) elif row < self.last_selected_script_pos: self.last_selected_script_pos -= 1 def move_script(self, row, step): item1 = self.ui.script_list.item(row) item2 = self.ui.script_list.item(row - step) if item1 and item2: # make changes in the ui list_item = self.ui.script_list.takeItem(row) script = self.listitem_to_scriptitem[list_item] # list_widget has to be set again list_widget = AdvancedScriptItem(name=script.name, state=script.enabled) self.setSignals(list_widget, list_item) self.ui.script_list.insertItem(row - step, list_item) self.ui.script_list.setItemWidget(list_item, list_widget) # make changes in the picklable list script1 = self.listitem_to_scriptitem[item1] script2 = self.listitem_to_scriptitem[item2] # workaround since tuples are immutable indices = script1.pos, script2.pos self.list_of_scripts = [ i for j, i in enumerate(self.list_of_scripts) if j not in indices ] new_script1 = (script1.pos - step, script1.name, script1.enabled, script1.text) new_script2 = (script2.pos + step, script2.name, script2.enabled, script2.text) self.list_of_scripts.append(new_script1) self.list_of_scripts.append(new_script2) self.list_of_scripts = sorted(self.list_of_scripts, key=lambda x: x[0]) # corresponding mapping support also has to be updated self.listitem_to_scriptitem[item1] = ScriptItem( script1.pos - step, script1.name, script1.enabled, script1.text) self.listitem_to_scriptitem[item2] = ScriptItem( script2.pos + step, script2.name, script2.enabled, script2.text) def live_update_and_check(self): items = self.ui.script_list.selectedItems() if items: script = self.listitem_to_scriptitem[items[0]] script.text = self.ui.tagger_script.toPlainText() self.list_of_scripts[script.pos] = script.get_all() self.ui.script_error.setStyleSheet("") self.ui.script_error.setText("") try: self.check() except OptionsCheckError as e: self.ui.script_error.setStyleSheet(self.STYLESHEET_ERROR) self.ui.script_error.setText(e.info) return def check(self): parser = ScriptParser() try: parser.eval(self.ui.tagger_script.toPlainText()) except Exception as e: raise OptionsCheckError(_("Script Error"), string_(e)) def restore_defaults(self): # Remove existing scripts self.ui.script_list.clear() self.ui.script_name.setText("") self.ui.tagger_script.setText("") super().restore_defaults() def load(self): self.ui.enable_tagger_scripts.setChecked( config.setting["enable_tagger_scripts"]) self.list_of_scripts = config.setting["list_of_scripts"] for s_pos, s_name, s_enabled, s_text in self.list_of_scripts: script = ScriptItem(s_pos, s_name, s_enabled, s_text) list_item = HashableListWidgetItem() list_widget = AdvancedScriptItem(name=s_name, state=s_enabled) self.setSignals(list_widget, list_item) self.ui.script_list.addItem(list_item) self.ui.script_list.setItemWidget(list_item, list_widget) self.listitem_to_scriptitem[list_item] = script # Select the last selected script item self.last_selected_script_pos = config.persist[ "last_selected_script_pos"] last_selected_script = self.ui.script_list.item( self.last_selected_script_pos) if last_selected_script: last_selected_script.setSelected(True) # Preserve previous splitter position self.ui.splitter.restoreState(config.persist["scripting_splitter"]) args = { "picard-doc-scripting-url": PICARD_URLS['doc_scripting'], } text = _('<a href="%(picard-doc-scripting-url)s">Open Scripting' ' Documentation in your browser</a>') % args self.ui.scripting_doc_link.setText(text) def save(self): config.setting[ "enable_tagger_scripts"] = self.ui.enable_tagger_scripts.isChecked( ) config.setting["list_of_scripts"] = self.list_of_scripts config.persist[ "last_selected_script_pos"] = self.last_selected_script_pos config.persist["scripting_splitter"] = self.ui.splitter.saveState() def display_error(self, error): pass
class AlbumSearchDialog(SearchDialog): options = [ config.Option("persist", "albumsearchdialog_window_size", QtCore.QSize(720, 360)), config.Option("persist", "albumsearchdialog_header_state", QtCore.QByteArray()) ] def __init__(self, parent): super(AlbumSearchDialog, self).__init__( parent, accept_button_title=_("Load into Picard")) self.cluster = None self.setWindowTitle(_("Album Search Results")) self.table_headers = [ _("Name"), _("Artist"), _("Format"), _("Tracks"), _("Date"), _("Country"), _("Labels"), _("Catalog #s"), _("Barcode"), _("Language"), _("Type"), _("Status"), _("Cover") ] def search(self, text): """Perform search using query provided by the user.""" self.retry_params = Retry(self.search, text) self.search_box.search_edit.setText(text) self.show_progress() self.tagger.xmlws.find_releases(self.handle_reply, query=text, search=True, 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": string_(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 config.setting["use_adv_search_syntax"]: 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.search_edit.setText(query_str) self.show_progress() self.tagger.xmlws.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.metadata[0].release_list[0].release except (AttributeError, IndexError): self.no_results_found() return del self.search_results[:] self.parse_releases_from_xml(releases) self.display_results() self.fetch_coverarts() def fetch_coverarts(self): """Queue cover art jsons from CAA server for each album in search results. """ for row, release in enumerate(self.search_results): caa_path = "/release/%s" % release["musicbrainz_albumid"] self.tagger.xmlws.download( CAA_HOST, CAA_PORT, caa_path, partial(self._caa_json_downloaded, row) ) def _caa_json_downloaded(self, row, 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. """ if not self.table: return cover_cell = self.table.cellWidget(row, len(self.table_headers)-1) if error: cover_cell.not_found() return try: caa_data = load_json(data) except ValueError: cover_cell.not_found() return front = None for image in caa_data["images"]: if image["front"]: front = image break if front: url = front["thumbnails"]["small"] coverartimage = CaaThumbnailCoverArtImage(url=url) self.tagger.xmlws.download( coverartimage.host, coverartimage.port, coverartimage.path, partial(self._cover_downloaded, row), ) else: cover_cell.not_found() def _cover_downloaded(self, row, data, http, error): """Handle cover art query reply from CAA server. If server returns the cover image succesfully, update the cover art cell of particular release. Args: row -- Album's row in results table """ cover_cell = self.table.cellWidget(row, len(self.table_headers)-1) if error: cover_cell.not_found() else: pixmap = QtGui.QPixmap() try: pixmap.loadFromData(data) cover_cell.update(pixmap) except: cover_cell.not_found() def parse_releases_from_xml(self, release_xml): for node in release_xml: release = Metadata() release_to_metadata(node, release) rg_node = node.release_group[0] release_group_to_metadata(rg_node, release) if "medium_list" in node.children: medium_list = node.medium_list[0] release["format"] = media_formats_from_node(medium_list) release["tracks"] = medium_list.track_count[0].text countries = country_list_from_node(node) if countries: release["country"] = ", ".join(countries) self.search_results.append(release) def display_results(self): self.show_table(self.table_headers) self.table.verticalHeader().setDefaultSectionSize(100) for row, release in enumerate(self.search_results): table_item = QtWidgets.QTableWidgetItem self.table.insertRow(row) self.table.setItem(row, 0, table_item(release.get("album", ""))) self.table.setItem(row, 1, table_item(release.get("albumartist", ""))) self.table.setItem(row, 2, table_item(release.get("format", ""))) self.table.setItem(row, 3, table_item(release.get("tracks", ""))) self.table.setItem(row, 4, table_item(release.get("date", ""))) self.table.setItem(row, 5, table_item(release.get("country", ""))) self.table.setItem(row, 6, table_item(release.get("label", ""))) self.table.setItem(row, 7, table_item(release.get("catalognumber", ""))) self.table.setItem(row, 8, table_item(release.get("barcode", ""))) self.table.setItem(row, 9, table_item(release.get("~releaselanguage", ""))) self.table.setItem(row, 10, table_item(release.get("releasetype", ""))) self.table.setItem(row, 11, table_item(release.get("releasestatus", ""))) self.table.setCellWidget(row, 12, CoverArt(self.table)) def accept_event(self, arg): self.load_selection(arg) 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.tagger.get_files_from_objects([self.cluster]) self.tagger.move_files_to_album(files, release["musicbrainz_albumid"], album) def restore_state(self): size = config.persist["albumsearchdialog_window_size"] if size: self.resize(size) self.search_box.restore_checkbox_state() def restore_table_header_state(self): header = self.table.horizontalHeader() state = config.persist["albumsearchdialog_header_state"] if state: header.restoreState(state) header.setSectionResizeMode(QtWidgets.QHeaderView.Interactive) def save_state(self): if self.table: self.save_table_header_state() config.persist["albumsearchdialog_window_size"] = self.size() def save_table_header_state(self): state = self.table.horizontalHeader().saveState() config.persist["albumsearchdialog_header_state"] = state
class PluginsOptionsPage(OptionsPage): NAME = "plugins" TITLE = N_("Plugins") PARENT = None SORT_ORDER = 70 ACTIVE = True options = [ config.ListOption("setting", "enabled_plugins", []), config.Option("persist", "plugins_list_state", QtCore.QByteArray()), config.Option("persist", "plugins_list_sort_section", 0), config.Option("persist", "plugins_list_sort_order", QtCore.Qt.AscendingOrder), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_PluginsOptionsPage() self.ui.setupUi(self) plugins = self.ui.plugins # fix for PICARD-1226, QT bug (https://bugreports.qt.io/browse/QTBUG-22572) workaround plugins.setStyleSheet('') plugins.itemSelectionChanged.connect(self.change_details) plugins.mimeTypes = self.mimeTypes plugins.dropEvent = self.dropEvent plugins.dragEnterEvent = self.dragEnterEvent self.ui.install_plugin.clicked.connect(self.open_plugins) self.ui.folder_open.clicked.connect(self.open_plugin_dir) self.ui.reload_list_of_plugins.clicked.connect( self.reload_list_of_plugins) self.manager = self.tagger.pluginmanager self.manager.plugin_installed.connect(self.plugin_installed) self.manager.plugin_updated.connect(self.plugin_updated) self.manager.plugin_removed.connect(self.plugin_removed) self.manager.plugin_errored.connect(self.plugin_loading_error) self._preserve = {} self._preserve_selected = None def items(self): iterator = QTreeWidgetItemIterator(self.ui.plugins, QTreeWidgetItemIterator.All) while iterator.value(): item = iterator.value() iterator += 1 yield item def find_item_by_plugin_name(self, plugin_name): for item in self.items(): if plugin_name == item.plugin.module_name: return item return None def selected_item(self): try: return self.ui.plugins.selectedItems()[COLUMN_NAME] except IndexError: return None def save_state(self): header = self.ui.plugins.header() config.persist["plugins_list_state"] = header.saveState() config.persist[ "plugins_list_sort_section"] = header.sortIndicatorSection() config.persist["plugins_list_sort_order"] = header.sortIndicatorOrder() def set_current_item(self, item, scroll=False): if scroll: self.ui.plugins.scrollToItem(item) self.ui.plugins.setCurrentItem(item) self.refresh_details(item) def restore_state(self): header = self.ui.plugins.header() header.restoreState(config.persist["plugins_list_state"]) idx = config.persist["plugins_list_sort_section"] order = config.persist["plugins_list_sort_order"] header.setSortIndicator(idx, order) self.ui.plugins.sortByColumn(idx, order) @staticmethod def is_plugin_enabled(plugin): return bool(plugin.module_name in config.setting["enabled_plugins"]) def available_plugins_name_version(self): return dict([(p.module_name, p.version) for p in self.manager.available_plugins]) def installable_plugins(self): if self.manager.available_plugins is not None: installed_plugins = [ plugin.module_name for plugin in self.installed_plugins() ] for plugin in sorted(self.manager.available_plugins, key=attrgetter('name')): if plugin.module_name not in installed_plugins: yield plugin def installed_plugins(self): return sorted(self.manager.plugins, key=attrgetter('name')) def enabled_plugins(self): return [ item.plugin.module_name for item in self.items() if item.is_enabled ] def _populate(self): self._user_interaction(False) if self.manager.available_plugins is None: available_plugins = {} self.manager.query_available_plugins(self._reload) else: available_plugins = self.available_plugins_name_version() self.ui.details.setText("") self.ui.plugins.setSortingEnabled(False) for plugin in self.installed_plugins(): new_version = None if plugin.module_name in available_plugins: latest = available_plugins[plugin.module_name] if latest.split('.') > plugin.version.split('.'): new_version = latest self.update_plugin_item(None, plugin, enabled=self.is_plugin_enabled(plugin), new_version=new_version, is_installed=True) for plugin in self.installable_plugins(): self.update_plugin_item(None, plugin, enabled=False, is_installed=False) self.ui.plugins.setSortingEnabled(True) self._user_interaction(True) header = self.ui.plugins.header() header.setStretchLastSection(False) header.setSectionResizeMode(QtWidgets.QHeaderView.Fixed) header.setSectionResizeMode(COLUMN_NAME, QtWidgets.QHeaderView.Stretch) header.setSectionResizeMode(COLUMN_VERSION, QtWidgets.QHeaderView.ResizeToContents) header.setSectionResizeMode(COLUMN_ACTIONS, QtWidgets.QHeaderView.ResizeToContents) def _remove_all(self): for item in self.items(): idx = self.ui.plugins.indexOfTopLevelItem(item) self.ui.plugins.takeTopLevelItem(idx) def restore_defaults(self): self._user_interaction(False) self._remove_all() super().restore_defaults() self.set_current_item(self.ui.plugins.topLevelItem(0), scroll=True) def load(self): self._populate() self.restore_state() def _preserve_plugins_states(self): self._preserve = { item.plugin.module_name: item.save_state() for item in self.items() } item = self.selected_item() if item: self._preserve_selected = item.plugin.module_name else: self._preserve_selected = None def _restore_plugins_states(self): for item in self.items(): plugin = item.plugin if plugin.module_name in self._preserve: item.restore_state(self._preserve[plugin.module_name]) if self._preserve_selected == plugin.module_name: self.set_current_item(item, scroll=True) def _reload(self): self._remove_all() self._populate() self._restore_plugins_states() def _user_interaction(self, enabled): self.ui.plugins.blockSignals(not enabled) self.ui.plugins_container.setEnabled(enabled) def reload_list_of_plugins(self): self.ui.details.setText(_("Reloading list of available plugins...")) self._user_interaction(False) self._preserve_plugins_states() self.manager.query_available_plugins(callback=self._reload) def plugin_loading_error(self, plugin_name, error): QtWidgets.QMessageBox.critical( self, _("Plugin '%s'") % plugin_name, _("An error occured while loading the plugin '%s':\n\n%s") % (plugin_name, error)) def plugin_installed(self, plugin): log.debug("Plugin %r installed", plugin.name) if not plugin.compatible: QtWidgets.QMessageBox.warning( self, _("Plugin '%s'") % plugin.name, _("The plugin '%s' is not compatible with this version of Picard." ) % plugin.name) return item = self.find_item_by_plugin_name(plugin.module_name) if item: self.update_plugin_item(item, plugin, make_current=True, enabled=True, is_installed=True) def plugin_updated(self, plugin_name): log.debug("Plugin %r updated", plugin_name) item = self.find_item_by_plugin_name(plugin_name) if item: plugin = item.plugin QtWidgets.QMessageBox.information( self, _("Plugin '%s'") % plugin_name, _("The plugin '%s' will be upgraded to version %s on next run of Picard." ) % (plugin.name, item.new_version)) item.upgrade_to_version = item.new_version self.update_plugin_item(item, plugin, make_current=True) def plugin_removed(self, plugin_name): log.debug("Plugin %r removed", plugin_name) item = self.find_item_by_plugin_name(plugin_name) if item: self.update_plugin_item(item, None, make_current=True, is_installed=False) def uninstall_plugin(self, item): plugin = item.plugin buttonReply = QtWidgets.QMessageBox.question( self, _("Uninstall plugin '%s'?") % plugin.name, _("Do you really want to uninstall the plugin '%s' ?") % plugin.name, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) if buttonReply == QtWidgets.QMessageBox.Yes: self.manager.remove_plugin(plugin.module_name, with_update=True) def update_plugin_item(self, item, plugin, make_current=False, enabled=None, new_version=None, is_installed=None): if item is None: item = PluginTreeWidgetItem(self.ui.plugins) if plugin is not None: item.setData(COLUMN_NAME, QtCore.Qt.UserRole, plugin) else: plugin = item.plugin if new_version is not None: item.new_version = new_version if is_installed is not None: item.is_installed = is_installed if enabled is None: enabled = item.is_enabled def update_text(): if item.new_version is not None: version = "%s → %s" % (plugin.version, item.new_version) else: version = plugin.version if item.installed_font is None: item.installed_font = item.font(COLUMN_NAME) if item.enabled_font is None: item.enabled_font = QtGui.QFont(item.installed_font) item.enabled_font.setBold(True) if item.available_font is None: item.available_font = QtGui.QFont(item.installed_font) if item.is_enabled: item.setFont(COLUMN_NAME, item.enabled_font) else: if item.is_installed: item.setFont(COLUMN_NAME, item.installed_font) else: item.setFont(COLUMN_NAME, item.available_font) item.setText(COLUMN_NAME, plugin.name) item.setText(COLUMN_VERSION, version) def toggle_enable(): item.enable(not item.is_enabled, greyout=not item.is_installed) log.debug("Plugin %r enabled: %r", item.plugin.name, item.is_enabled) update_text() reconnect(item.buttons['enable'].clicked, toggle_enable) install_enabled = not item.is_installed or bool(item.new_version) if item.upgrade_to_version: if item.upgrade_to_version != item.new_version: # case when a new version is known after a plugin was marked for update install_enabled = True else: install_enabled = False if install_enabled: if item.new_version is not None: def download_and_update(): self.download_plugin(item, update=True) reconnect(item.buttons['update'].clicked, download_and_update) item.buttons['install'].mode('hide') item.buttons['update'].mode('show') else: def download_and_install(): self.download_plugin(item) reconnect(item.buttons['install'].clicked, download_and_install) item.buttons['install'].mode('show') item.buttons['update'].mode('hide') if item.is_installed: item.buttons['install'].mode('hide') item.buttons['uninstall'].mode('show') item.enable(enabled, greyout=False) def uninstall_processor(): self.uninstall_plugin(item) reconnect(item.buttons['uninstall'].clicked, uninstall_processor) else: item.buttons['uninstall'].mode('hide') item.buttons['enable'].mode('hide') update_text() if make_current: self.set_current_item(item) actions_sort_score = 2 if item.is_installed: if item.is_enabled: actions_sort_score = 0 else: actions_sort_score = 1 item.setSortData(COLUMN_ACTIONS, actions_sort_score) item.setSortData(COLUMN_NAME, plugin.name.lower()) def v2int(elem): try: return int(elem) except ValueError: return 0 item.setSortData(COLUMN_VERSION, tuple(v2int(e) for e in plugin.version.split('.'))) return item def save(self): config.setting["enabled_plugins"] = self.enabled_plugins() self.save_state() def refresh_details(self, item): plugin = item.plugin text = [] if item.new_version is not None: if item.upgrade_to_version: label = _("Restart Picard to upgrade to new version") else: label = _("New version available") text.append("<b>" + label + ": " + item.new_version + "</b>") if plugin.description: text.append(plugin.description + "<hr width='90%'/>") if plugin.name: text.append("<b>" + _("Name") + "</b>: " + plugin.name) if plugin.author: text.append("<b>" + _("Authors") + "</b>: " + plugin.author) if plugin.license: text.append("<b>" + _("License") + "</b>: " + plugin.license) text.append("<b>" + _("Files") + "</b>: " + plugin.files_list) self.ui.details.setText("<p>%s</p>" % "<br/>\n".join(text)) def change_details(self): item = self.selected_item() if item: self.refresh_details(item) def open_plugins(self): files, _filter = QtWidgets.QFileDialog.getOpenFileNames( self, "", QtCore.QDir.homePath(), "Picard plugin (*.py *.pyc *.zip)") if files: for path in files: self.manager.install_plugin(path) def download_plugin(self, item, update=False): plugin = item.plugin self.tagger.webservice.get(PLUGINS_API['host'], PLUGINS_API['port'], PLUGINS_API['endpoint']['download'], partial(self.download_handler, update, plugin=plugin), parse_response_type=None, priority=True, important=True, queryargs={"id": plugin.module_name}) def download_handler(self, update, response, reply, error, plugin): if error: msgbox = QtWidgets.QMessageBox(self) msgbox.setText( _("The plugin '%s' could not be downloaded.") % plugin.module_name) msgbox.setInformativeText(_("Please try again later.")) msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok) msgbox.setDefaultButton(QtWidgets.QMessageBox.Ok) msgbox.exec_() log.error( "Error occurred while trying to download the plugin: '%s'" % plugin.module_name) return self.manager.install_plugin( None, update=update, plugin_name=plugin.module_name, plugin_data=response, ) @staticmethod def open_plugin_dir(): if sys.platform == 'win32': url = 'file:///' + USER_PLUGIN_DIR else: url = 'file://' + USER_PLUGIN_DIR QtGui.QDesktopServices.openUrl( QtCore.QUrl(url, QtCore.QUrl.TolerantMode)) def mimeTypes(self): return ["text/uri-list"] def dragEnterEvent(self, event): event.setDropAction(QtCore.Qt.CopyAction) event.accept() def dropEvent(self, event): for path in [ os.path.normpath(u.toLocalFile()) for u in event.mimeData().urls() ]: self.manager.install_plugin(path)
def __init__(self): config.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 MainWindow(QtWidgets.QMainWindow, PreserveGeometry): defaultsize = QtCore.QSize(780, 560) autorestore = False selection_updated = QtCore.pyqtSignal(object) options = [ config.Option("persist", "window_state", QtCore.QByteArray()), config.Option("persist", "bottom_splitter_state", QtCore.QByteArray()), config.BoolOption("persist", "window_maximized", False), config.BoolOption("persist", "view_cover_art", True), config.BoolOption("persist", "view_toolbar", True), config.BoolOption("persist", "view_file_browser", False), config.TextOption("persist", "current_directory", ""), ] def __init__(self, parent=None): super().__init__(parent) self.selected_objects = [] self.ignore_selection_changes = False self.toolbar = None 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() mainLayout = QtWidgets.QSplitter(QtCore.Qt.Vertical) mainLayout.setContentsMargins(0, 0, 0, 0) mainLayout.setHandleWidth(1) self.panel = MainPanel(self, mainLayout) 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.metadata_box = MetadataBox(self) self.cover_art_box = CoverArtBox(self) if not self.show_cover_art_action.isChecked(): self.cover_art_box.hide() self.logDialog = LogView(self) self.historyDialog = HistoryView(self) bottomLayout = QtWidgets.QHBoxLayout() bottomLayout.setContentsMargins(0, 0, 0, 0) bottomLayout.setSpacing(0) bottomLayout.addWidget(self.metadata_box, 1) bottomLayout.addWidget(self.cover_art_box, 0) bottom = QtWidgets.QWidget() bottom.setLayout(bottomLayout) mainLayout.addWidget(self.panel) mainLayout.addWidget(bottom) self.setCentralWidget(mainLayout) # accessibility self.set_tab_order() for function in ui_init: function(self) def keyPressEvent(self, event): if event.matches(QtGui.QKeySequence.Delete): if self.metadata_box.hasFocus(): self.metadata_box.remove_selected_tags() else: self.remove() else: super().keyPressEvent(event) def show(self): self.restoreWindowState() super().show() self.auto_update_check() self.metadata_box.restore_state() def closeEvent(self, event): if 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.values()) QMessageBox = QtWidgets.QMessageBox if unsaved_files > 0: msg = QMessageBox(self) msg.setIcon(QMessageBox.Question) msg.setWindowModality(QtCore.Qt.WindowModal) msg.setWindowTitle(_("Unsaved Changes")) msg.setText(_("Are you sure you want to quit Picard?")) txt = ngettext( "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(_("&Quit Picard"), QMessageBox.YesRole) ret = msg.exec_() if ret == QMessageBox.Cancel: return False return True def saveWindowState(self): config.persist["window_state"] = self.saveState() isMaximized = int(self.windowState()) & QtCore.Qt.WindowMaximized != 0 self.save_geometry() config.persist["window_maximized"] = isMaximized config.persist[ "view_cover_art"] = self.show_cover_art_action.isChecked() config.persist["view_toolbar"] = self.show_toolbar_action.isChecked() config.persist[ "view_file_browser"] = self.show_file_browser_action.isChecked() config.persist["bottom_splitter_state"] = self.centralWidget( ).saveState() self.file_browser.save_state() self.panel.save_state() self.metadata_box.save_state() @restore_method def restoreWindowState(self): self.restoreState(config.persist["window_state"]) self.restore_geometry() if config.persist["window_maximized"]: self.setWindowState(QtCore.Qt.WindowMaximized) bottom_splitter_state = config.persist["bottom_splitter_state"] if bottom_splitter_state.isEmpty(): self.centralWidget().setSizes([366, 194]) else: self.centralWidget().restoreState(bottom_splitter_state) self.file_browser.restore_state() def create_statusbar(self): """Creates a new status bar.""" self.statusBar().showMessage(_("Ready")) self.infostatus = InfoStatus(self) self.listening_label = QtWidgets.QLabel() self.listening_label.setVisible(False) self.listening_label.setToolTip("<qt/>" + _( "Picard listens on this port to integrate with your browser. When " "you \"Search\" or \"Open in Browser\" from Picard, clicking the " "\"Tagger\" button on the web page loads the release into Picard.") ) self.statusBar().addPermanentWidget(self.infostatus) self.statusBar().addPermanentWidget(self.listening_label) self.tagger.tagger_stats_changed.connect(self.update_statusbar_stats) self.tagger.listen_port_changed.connect( self.update_statusbar_listen_port) self.update_statusbar_stats() @throttle(100) def update_statusbar_stats(self): """Updates the status bar information.""" self.infostatus.setFiles(len(self.tagger.files)) self.infostatus.setAlbums(len(self.tagger.albums)) self.infostatus.setPendingFiles(File.num_pending_files) ws = self.tagger.webservice self.infostatus.setPendingRequests(ws.num_pending_web_requests) def update_statusbar_listen_port(self, listen_port): if listen_port: self.listening_label.setVisible(True) self.listening_label.setText( _(" Listening on port %(port)d ") % {"port": listen_port}) else: self.listening_label.setVisible(False) def set_statusbar_message(self, message, *args, **kwargs): """Set the status bar message. *args are passed to % operator, if args[0] is a mapping it is used for named place holders values >>> w.set_statusbar_message("File %(filename)s", {'filename': 'x.txt'}) Keyword arguments: `echo` parameter defaults to `log.debug`, called before message is translated, it can be disabled passing None or replaced by ie. `log.error`. If None, skipped. `translate` is a method called on message before it is sent to history log and status bar, it defaults to `_()`. If None, skipped. `timeout` defines duration of the display in milliseconds `history` is a method called with translated message as argument, it defaults to `log.history_info`. If None, skipped. Empty messages are never passed to echo and history functions but they are sent to status bar (ie. to clear it). """ def isdict(obj): return hasattr(obj, 'keys') and hasattr(obj, '__getitem__') echo = kwargs.get('echo', log.debug) # _ is defined using builtins.__dict__, so setting it as default named argument # value doesn't work as expected translate = kwargs.get('translate', _) timeout = kwargs.get('timeout', 0) history = kwargs.get('history', log.history_info) if len(args) == 1 and isdict(args[0]): # named place holders mparms = args[0] else: # simple place holders, ensure compatibility mparms = args if message: if echo: echo(message % mparms) if translate: message = translate(message) message = message % mparms if history: history(message) thread.to_main(self.statusBar().showMessage, message, timeout) def _on_submit_acoustid(self): if self.tagger.use_acoustid: if not config.setting["acoustid_apikey"]: QtWidgets.QMessageBox.warning( self, _("Submission Error"), _("You need to configure your AcoustID API key before you can submit fingerprints." )) else: self.tagger.acoustidmanager.submit() def create_actions(self): self.options_action = QtWidgets.QAction( icontheme.lookup('preferences-desktop'), _("&Options..."), self) self.options_action.setMenuRole(QtWidgets.QAction.PreferencesRole) self.options_action.triggered.connect(self.show_options) self.cut_action = QtWidgets.QAction( icontheme.lookup('edit-cut', icontheme.ICON_SIZE_MENU), _("&Cut"), self) self.cut_action.setShortcut(QtGui.QKeySequence.Cut) self.cut_action.setEnabled(False) self.cut_action.triggered.connect(self.cut) self.paste_action = QtWidgets.QAction( icontheme.lookup('edit-paste', icontheme.ICON_SIZE_MENU), _("&Paste"), self) self.paste_action.setShortcut(QtGui.QKeySequence.Paste) self.paste_action.setEnabled(False) self.paste_action.triggered.connect(self.paste) self.help_action = QtWidgets.QAction(_("&Help..."), self) self.help_action.setShortcut(QtGui.QKeySequence.HelpContents) self.help_action.triggered.connect(self.show_help) self.about_action = QtWidgets.QAction(_("&About..."), self) self.about_action.setMenuRole(QtWidgets.QAction.AboutRole) self.about_action.triggered.connect(self.show_about) self.donate_action = QtWidgets.QAction(_("&Donate..."), self) self.donate_action.triggered.connect(self.open_donation_page) self.report_bug_action = QtWidgets.QAction(_("&Report a Bug..."), self) self.report_bug_action.triggered.connect(self.open_bug_report) self.support_forum_action = QtWidgets.QAction(_("&Support Forum..."), self) self.support_forum_action.triggered.connect(self.open_support_forum) self.add_files_action = QtWidgets.QAction( icontheme.lookup('document-open'), _("&Add Files..."), self) self.add_files_action.setStatusTip(_("Add files to the tagger")) # TR: Keyboard shortcut for "Add Files..." self.add_files_action.setShortcut(QtGui.QKeySequence.Open) self.add_files_action.triggered.connect(self.add_files) self.add_directory_action = QtWidgets.QAction( icontheme.lookup('folder'), _("A&dd Folder..."), self) self.add_directory_action.setStatusTip(_("Add a folder to the tagger")) # TR: Keyboard shortcut for "Add Directory..." self.add_directory_action.setShortcut(QtGui.QKeySequence(_("Ctrl+D"))) self.add_directory_action.triggered.connect(self.add_directory) self.save_action = QtWidgets.QAction(icontheme.lookup('document-save'), _("&Save"), self) self.save_action.setStatusTip(_("Save selected files")) # TR: Keyboard shortcut for "Save" self.save_action.setShortcut(QtGui.QKeySequence.Save) self.save_action.setEnabled(False) self.save_action.triggered.connect(self.save) self.submit_acoustid_action = QtWidgets.QAction( icontheme.lookup('acoustid-fingerprinter'), _("S&ubmit AcoustIDs"), self) self.submit_acoustid_action.setStatusTip( _("Submit acoustic fingerprints")) self.submit_acoustid_action.setEnabled(False) self.submit_acoustid_action.triggered.connect(self._on_submit_acoustid) self.exit_action = QtWidgets.QAction(_("E&xit"), self) self.exit_action.setMenuRole(QtWidgets.QAction.QuitRole) # TR: Keyboard shortcut for "Exit" self.exit_action.setShortcut(QtGui.QKeySequence(_("Ctrl+Q"))) self.exit_action.triggered.connect(self.close) self.remove_action = QtWidgets.QAction(icontheme.lookup('list-remove'), _("&Remove"), self) self.remove_action.setStatusTip(_("Remove selected files/albums")) self.remove_action.setEnabled(False) self.remove_action.triggered.connect(self.remove) self.browser_lookup_action = QtWidgets.QAction( icontheme.lookup('lookup-musicbrainz'), _("Lookup in &Browser"), self) self.browser_lookup_action.setStatusTip( _("Lookup selected item on MusicBrainz website")) self.browser_lookup_action.setEnabled(False) # TR: Keyboard shortcut for "Lookup in Browser" self.browser_lookup_action.setShortcut( QtGui.QKeySequence(_("Ctrl+Shift+L"))) self.browser_lookup_action.triggered.connect(self.browser_lookup) self.album_search_action = QtWidgets.QAction( icontheme.lookup('system-search'), _("Search for similar albums..."), self) self.album_search_action.setStatusTip( _("View similar releases and optionally choose a different release" )) self.album_search_action.triggered.connect(self.show_more_albums) self.track_search_action = QtWidgets.QAction( icontheme.lookup('system-search'), _("Search for similar tracks..."), self) self.track_search_action.setStatusTip( _("View similar tracks and optionally choose a different release")) self.track_search_action.triggered.connect(self.show_more_tracks) self.show_file_browser_action = QtWidgets.QAction( _("File &Browser"), self) self.show_file_browser_action.setCheckable(True) if config.persist["view_file_browser"]: self.show_file_browser_action.setChecked(True) self.show_file_browser_action.setShortcut( QtGui.QKeySequence(_("Ctrl+B"))) self.show_file_browser_action.triggered.connect(self.show_file_browser) self.show_cover_art_action = QtWidgets.QAction(_("&Cover Art"), self) self.show_cover_art_action.setCheckable(True) if config.persist["view_cover_art"]: self.show_cover_art_action.setChecked(True) self.show_cover_art_action.triggered.connect(self.show_cover_art) self.show_toolbar_action = QtWidgets.QAction(_("&Actions"), self) self.show_toolbar_action.setCheckable(True) if config.persist["view_toolbar"]: self.show_toolbar_action.setChecked(True) self.show_toolbar_action.triggered.connect(self.show_toolbar) self.search_action = QtWidgets.QAction( icontheme.lookup('system-search'), _("Search"), self) self.search_action.setEnabled(False) self.search_action.triggered.connect(self.search) self.cd_lookup_action = QtWidgets.QAction( icontheme.lookup('media-optical'), _("Lookup &CD..."), self) self.cd_lookup_action.setStatusTip( _("Lookup the details of the CD in your drive")) # TR: Keyboard shortcut for "Lookup CD" self.cd_lookup_action.setShortcut(QtGui.QKeySequence(_("Ctrl+K"))) self.cd_lookup_action.triggered.connect(self.tagger.lookup_cd) self.cd_lookup_menu = QtWidgets.QMenu(_("Lookup &CD...")) self.cd_lookup_menu.triggered.connect(self.tagger.lookup_cd) self.cd_lookup_action.setEnabled(False) if discid is None: log.warning( "CDROM: discid library not found - Lookup CD functionality disabled" ) else: drives = get_cdrom_drives() if not drives: log.warning( "CDROM: No CD-ROM drives found - Lookup CD functionality disabled" ) else: shortcut_drive = config.setting["cd_lookup_device"].split( ",")[0] if len(drives) > 1 else "" self.cd_lookup_action.setEnabled(True) for drive in drives: action = self.cd_lookup_menu.addAction(drive) if drive == shortcut_drive: # Clear existing shortcode on main action and assign it to sub-action self.cd_lookup_action.setShortcut(QtGui.QKeySequence()) action.setShortcut(QtGui.QKeySequence(_("Ctrl+K"))) self.analyze_action = QtWidgets.QAction( icontheme.lookup('picard-analyze'), _("&Scan"), self) self.analyze_action.setStatusTip( _("Use AcoustID audio fingerprint to identify the files by the actual music, even if they have no metadata" )) self.analyze_action.setEnabled(False) self.analyze_action.setToolTip( _('Identify the file using its AcoustID audio fingerprint')) # TR: Keyboard shortcut for "Analyze" self.analyze_action.setShortcut(QtGui.QKeySequence(_("Ctrl+Y"))) self.analyze_action.triggered.connect(self.analyze) self.cluster_action = QtWidgets.QAction( icontheme.lookup('picard-cluster'), _("Cl&uster"), self) self.cluster_action.setStatusTip( _("Cluster files into album clusters")) self.cluster_action.setEnabled(False) # TR: Keyboard shortcut for "Cluster" self.cluster_action.setShortcut(QtGui.QKeySequence(_("Ctrl+U"))) self.cluster_action.triggered.connect(self.cluster) self.autotag_action = QtWidgets.QAction( icontheme.lookup('picard-auto-tag'), _("&Lookup"), self) tip = _("Lookup selected items in MusicBrainz") self.autotag_action.setToolTip(tip) self.autotag_action.setStatusTip(tip) self.autotag_action.setEnabled(False) # TR: Keyboard shortcut for "Lookup" self.autotag_action.setShortcut(QtGui.QKeySequence(_("Ctrl+L"))) self.autotag_action.triggered.connect(self.autotag) self.view_info_action = QtWidgets.QAction( icontheme.lookup('picard-edit-tags'), _("&Info..."), self) self.view_info_action.setEnabled(False) # TR: Keyboard shortcut for "Info" self.view_info_action.setShortcut(QtGui.QKeySequence(_("Ctrl+I"))) self.view_info_action.triggered.connect(self.view_info) self.refresh_action = QtWidgets.QAction( icontheme.lookup('view-refresh', icontheme.ICON_SIZE_MENU), _("&Refresh"), self) self.refresh_action.setShortcut(QtGui.QKeySequence(_("Ctrl+R"))) self.refresh_action.triggered.connect(self.refresh) self.enable_renaming_action = QtWidgets.QAction( _("&Rename Files"), self) self.enable_renaming_action.setCheckable(True) self.enable_renaming_action.setChecked(config.setting["rename_files"]) self.enable_renaming_action.triggered.connect(self.toggle_rename_files) self.enable_moving_action = QtWidgets.QAction(_("&Move Files"), self) self.enable_moving_action.setCheckable(True) self.enable_moving_action.setChecked(config.setting["move_files"]) self.enable_moving_action.triggered.connect(self.toggle_move_files) self.enable_tag_saving_action = QtWidgets.QAction( _("Save &Tags"), self) self.enable_tag_saving_action.setCheckable(True) self.enable_tag_saving_action.setChecked( not config.setting["dont_write_tags"]) self.enable_tag_saving_action.triggered.connect(self.toggle_tag_saving) self.tags_from_filenames_action = QtWidgets.QAction( _("Tags From &File Names..."), self) self.tags_from_filenames_action.triggered.connect( self.open_tags_from_filenames) self.tags_from_filenames_action.setEnabled(False) self.open_collection_in_browser_action = QtWidgets.QAction( _("&Open My Collections in Browser"), self) self.open_collection_in_browser_action.triggered.connect( self.open_collection_in_browser) self.open_collection_in_browser_action.setEnabled( config.setting["username"] != '') self.view_log_action = QtWidgets.QAction(_("View &Error/Debug Log"), self) self.view_log_action.triggered.connect(self.show_log) # TR: Keyboard shortcut for "View Error/Debug Log" self.view_log_action.setShortcut(QtGui.QKeySequence(_("Ctrl+E"))) self.view_history_action = QtWidgets.QAction( _("View Activity &History"), self) self.view_history_action.triggered.connect(self.show_history) # TR: Keyboard shortcut for "View Activity History" self.view_history_action.setShortcut(QtGui.QKeySequence(_("Ctrl+H"))) webservice_manager = self.tagger.webservice.manager webservice_manager.authenticationRequired.connect( self.show_password_dialog) webservice_manager.proxyAuthenticationRequired.connect( self.show_proxy_dialog) self.play_file_action = QtWidgets.QAction( icontheme.lookup('play-music'), _("Open in &Player"), self) self.play_file_action.setStatusTip( _("Play the file in your default media player")) self.play_file_action.setEnabled(False) self.play_file_action.triggered.connect(self.play_file) self.open_folder_action = QtWidgets.QAction( icontheme.lookup('folder', icontheme.ICON_SIZE_MENU), _("Open Containing &Folder"), self) self.open_folder_action.setStatusTip( _("Open the containing folder in your file explorer")) self.open_folder_action.setEnabled(False) self.open_folder_action.triggered.connect(self.open_folder) self.check_update_action = QtWidgets.QAction(_("&Check for Update"), self) self.check_update_action.triggered.connect(self.do_update_check) def toggle_rename_files(self, checked): config.setting["rename_files"] = checked def toggle_move_files(self, checked): config.setting["move_files"] = checked def toggle_tag_saving(self, checked): config.setting["dont_write_tags"] = not checked def get_selected_or_unmatched_files(self): files = self.tagger.get_files_from_objects(self.selected_objects) if not files: files = self.tagger.unclustered_files.files return files def open_tags_from_filenames(self): files = self.get_selected_or_unmatched_files() if files: dialog = TagsFromFileNamesDialog(files, self) dialog.exec_() def open_collection_in_browser(self): self.tagger.collection_lookup() def create_menus(self): menu = self.menuBar().addMenu(_("&File")) menu.addAction(self.add_directory_action) menu.addAction(self.add_files_action) menu.addSeparator() menu.addAction(self.play_file_action) menu.addAction(self.open_folder_action) menu.addSeparator() menu.addAction(self.save_action) menu.addAction(self.submit_acoustid_action) menu.addSeparator() menu.addAction(self.exit_action) menu = self.menuBar().addMenu(_("&Edit")) menu.addAction(self.cut_action) menu.addAction(self.paste_action) menu.addSeparator() menu.addAction(self.view_info_action) menu.addAction(self.remove_action) menu = self.menuBar().addMenu(_("&View")) menu.addAction(self.show_file_browser_action) menu.addAction(self.show_cover_art_action) menu.addSeparator() menu.addAction(self.show_toolbar_action) menu.addAction(self.search_toolbar_toggle_action) menu = self.menuBar().addMenu(_("&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(_("&Tools")) menu.addAction(self.refresh_action) if len(self.cd_lookup_menu.actions()) > 1: menu.addMenu(self.cd_lookup_menu) else: menu.addAction(self.cd_lookup_action) menu.addAction(self.autotag_action) menu.addAction(self.analyze_action) menu.addAction(self.cluster_action) menu.addAction(self.browser_lookup_action) menu.addSeparator() menu.addAction(self.tags_from_filenames_action) menu.addAction(self.open_collection_in_browser_action) self.menuBar().addSeparator() menu = self.menuBar().addMenu(_("&Help")) menu.addAction(self.help_action) menu.addSeparator() menu.addAction(self.view_history_action) menu.addSeparator() menu.addAction(self.check_update_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 config.setting["toolbar_show_labels"]: self.toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon) else: self.toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly) def create_toolbar(self): self.create_search_toolbar() self.create_action_toolbar() def create_action_toolbar(self): if self.toolbar: self.toolbar.clear() self.removeToolBar(self.toolbar) self.toolbar = toolbar = QtWidgets.QToolBar(_("Actions")) self.insertToolBar(self.search_toolbar, self.toolbar) self.update_toolbar_style() toolbar.setObjectName("main_toolbar") def add_toolbar_action(action): toolbar.addAction(action) widget = toolbar.widgetForAction(action) widget.setFocusPolicy(QtCore.Qt.TabFocus) widget.setAttribute(QtCore.Qt.WA_MacShowFocusRect) for action in config.setting['toolbar_layout']: if action == 'cd_lookup_action': add_toolbar_action(self.cd_lookup_action) if len(self.cd_lookup_menu.actions()) > 1: button = toolbar.widgetForAction(self.cd_lookup_action) button.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup) button.setMenu(self.cd_lookup_menu) elif action == 'separator': toolbar.addSeparator() else: try: add_toolbar_action(getattr(self, action)) except AttributeError: log.warning( 'Warning: Unknown action name "%r" found in config. Ignored.', action) self.show_toolbar() def create_search_toolbar(self): self.search_toolbar = toolbar = self.addToolBar(_("Search")) self.search_toolbar_toggle_action = self.search_toolbar.toggleViewAction( ) toolbar.setObjectName("search_toolbar") search_panel = QtWidgets.QWidget(toolbar) hbox = QtWidgets.QHBoxLayout(search_panel) self.search_combo = QtWidgets.QComboBox(search_panel) self.search_combo.addItem(_("Album"), "album") self.search_combo.addItem(_("Artist"), "artist") self.search_combo.addItem(_("Track"), "track") hbox.addWidget(self.search_combo, 0) self.search_edit = ButtonLineEdit(search_panel) self.search_edit.returnPressed.connect(self.trigger_search_action) self.search_edit.textChanged.connect(self.enable_search) hbox.addWidget(self.search_edit, 0) self.search_button = QtWidgets.QToolButton(search_panel) self.search_button.setAutoRaise(True) self.search_button.setDefaultAction(self.search_action) self.search_button.setIconSize(QtCore.QSize(22, 22)) self.search_button.setAttribute(QtCore.Qt.WA_MacShowFocusRect) # search button contextual menu, shortcut to toggle search options def search_button_menu(position): menu = QtWidgets.QMenu() opts = OrderedDict([('use_adv_search_syntax', N_("&Advanced search")), ('builtin_search', N_("&Builtin search"))]) def toggle_opt(opt, checked): config.setting[opt] = checked for opt, label in opts.items(): action = QtWidgets.QAction(_(label), menu) action.setCheckable(True) action.setChecked(config.setting[opt]) action.triggered.connect(partial(toggle_opt, opt)) menu.addAction(action) menu.exec_(self.search_button.mapToGlobal(position)) self.search_button.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.search_button.customContextMenuRequested.connect( search_button_menu) hbox.addWidget(self.search_button) toolbar.addWidget(search_panel) def set_tab_order(self): tab_order = self.setTabOrder tw = self.toolbar.widgetForAction prev_action = None current_action = None # Setting toolbar widget tab-orders for accessibility for action in config.setting['toolbar_layout']: if action != 'separator': try: current_action = tw(getattr(self, action)) except AttributeError: # No need to log warnings since we have already # done it once in create_toolbar pass if prev_action is not None and prev_action != current_action: tab_order(prev_action, current_action) prev_action = current_action tab_order(prev_action, self.search_combo) tab_order(self.search_combo, self.search_edit) tab_order(self.search_edit, self.search_button) # Panels tab_order(self.search_button, self.file_browser) tab_order(self.file_browser, self.panel.views[0]) tab_order(self.panel.views[0], self.panel.views[1]) tab_order(self.panel.views[1], self.metadata_box) def enable_submit(self, enabled): """Enable/disable the 'Submit fingerprints' action.""" self.submit_acoustid_action.setEnabled(enabled) def enable_cluster(self, enabled): """Enable/disable the 'Cluster' action.""" self.cluster_action.setEnabled(enabled) def enable_search(self): """Enable/disable the 'Search' action.""" if self.search_edit.text(): self.search_action.setEnabled(True) else: self.search_action.setEnabled(False) def trigger_search_action(self): if self.search_action.isEnabled(): self.search_action.trigger() def search_mbid_found(self, entity, mbid): self.search_edit.setText('%s:%s' % (entity, mbid)) def search(self): """Search for album, artist or track on the MusicBrainz website.""" text = self.search_edit.text() entity = self.search_combo.itemData(self.search_combo.currentIndex()) self.tagger.search(text, entity, config.setting["use_adv_search_syntax"], mbid_matched_callback=self.search_mbid_found) def add_files(self): """Add files to the tagger.""" current_directory = find_starting_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, _filter = QtWidgets.QFileDialog.getOpenFileNames( self, "", current_directory, ";;".join(formats)) if files: 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 = find_starting_directory() dir_list = [] if not config.setting["toolbar_multiselect"]: directory = QtWidgets.QFileDialog.getExistingDirectory( self, "", current_directory) if directory: dir_list.append(directory) else: file_dialog = MultiDirsSelectDialog(self, "", current_directory) if file_dialog.exec_() == QtWidgets.QDialog.Accepted: dir_list = file_dialog.selectedFiles() dir_count = len(dir_list) if dir_count: parent = os.path.dirname( dir_list[0]) if dir_count > 1 else dir_list[0] config.persist["current_directory"] = parent if dir_count > 1: self.set_statusbar_message( N_("Adding multiple directories from '%(directory)s' ..."), {'directory': parent}) else: self.set_statusbar_message( N_("Adding directory: '%(directory)s' ..."), {'directory': dir_list[0]}) for directory in dir_list: 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.goto('documentation') def show_log(self): self.logDialog.show() self.logDialog.raise_() self.logDialog.activateWindow() def show_history(self): self.historyDialog.show() self.historyDialog.raise_() self.historyDialog.activateWindow() def open_bug_report(self): webbrowser2.goto('troubleshooting') def open_support_forum(self): webbrowser2.goto('forum') def open_donation_page(self): webbrowser2.goto('donate') def save(self): """Tell the tagger to save the selected objects.""" self.tagger.save(self.selected_objects) def remove(self): """Tell the tagger to remove the selected objects.""" self.panel.remove(self.selected_objects) def analyze(self): if not config.setting['fingerprinting_system']: if self.show_analyze_settings_info(): self.show_options("fingerprinting") if not config.setting['fingerprinting_system']: return return self.tagger.analyze(self.selected_objects) def _openUrl(self, url): # Resolves a bug in Qt opening remote URLs - QTBUG-13359 # See https://bugreports.qt.io/browse/QTBUG-13359 if url.startswith("\\\\") or url.startswith("//"): return QtCore.QUrl(QtCore.QDir.toNativeSeparators(url)) else: return QtCore.QUrl.fromLocalFile(url) def play_file(self): files = self.tagger.get_files_from_objects(self.selected_objects) for file in files: QtGui.QDesktopServices.openUrl(self._openUrl(file.filename)) def open_folder(self): files = self.tagger.get_files_from_objects(self.selected_objects) folders = set([os.path.dirname(f.filename) for f in files]) for folder in folders: QtGui.QDesktopServices.openUrl(self._openUrl(folder)) def show_analyze_settings_info(self): ret = QtWidgets.QMessageBox.question( self, _("Configuration Required"), _("Audio fingerprinting is not yet configured. Would you like to configure it now?" ), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.Yes) return ret == QtWidgets.QMessageBox.Yes def show_more_tracks(self): obj = self.selected_objects[0] if isinstance(obj, Track): obj = obj.linked_files[0] dialog = TrackSearchDialog(self) dialog.load_similar_tracks(obj) dialog.exec_() def show_more_albums(self): obj = self.selected_objects[0] dialog = AlbumSearchDialog(self) dialog.show_similar_albums(obj) dialog.exec_() def view_info(self, default_tab=0): if isinstance(self.selected_objects[0], Album): album = self.selected_objects[0] dialog = AlbumInfoDialog(album, self) elif isinstance(self.selected_objects[0], Cluster): cluster = self.selected_objects[0] dialog = ClusterInfoDialog(cluster, self) elif isinstance(self.selected_objects[0], Track): track = self.selected_objects[0] dialog = TrackInfoDialog(track, self) else: file = self.tagger.get_files_from_objects(self.selected_objects)[0] dialog = FileInfoDialog(file, self) dialog.ui.tabWidget.setCurrentIndex(default_tab) dialog.exec_() def cluster(self): self.tagger.cluster(self.selected_objects) self.update_actions() def refresh(self): self.tagger.refresh(self.selected_objects) def browser_lookup(self): self.tagger.browser_lookup(self.selected_objects[0]) @throttle(100) def update_actions(self): can_remove = False can_save = False can_analyze = False can_refresh = False can_autotag = False single = self.selected_objects[0] if len( self.selected_objects) == 1 else None can_view_info = bool(single and single.can_view_info()) can_browser_lookup = bool(single and single.can_browser_lookup()) have_files = bool( self.tagger.get_files_from_objects(self.selected_objects)) have_objects = bool(self.selected_objects) 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_refresh(): can_refresh = True if obj.can_autotag(): can_autotag = True # Skip further loops if all values now True. if can_analyze and can_save and can_remove and can_refresh and can_autotag: break self.remove_action.setEnabled(can_remove) self.save_action.setEnabled(can_save) self.view_info_action.setEnabled(can_view_info) self.analyze_action.setEnabled(can_analyze) self.refresh_action.setEnabled(can_refresh) self.autotag_action.setEnabled(can_autotag) self.browser_lookup_action.setEnabled(can_browser_lookup) self.play_file_action.setEnabled(have_files) self.open_folder_action.setEnabled(have_files) self.cut_action.setEnabled(have_objects) files = self.get_selected_or_unmatched_files() self.tags_from_filenames_action.setEnabled(bool(files)) self.track_search_action.setEnabled(have_objects) def update_selection(self, objects=None): if self.ignore_selection_changes: return if objects is not None: self.selected_objects = objects else: objects = self.selected_objects self.update_actions() metadata = None orig_metadata = None obj = None # Clear any existing status bar messages self.set_statusbar_message("") if len(objects) == 1: obj = list(objects)[0] if isinstance(obj, File): metadata = obj.metadata orig_metadata = obj.orig_metadata if obj.state == obj.ERROR: msg = N_("%(filename)s (error: %(error)s)") mparms = {'filename': obj.filename, 'error': obj.error} else: msg = N_("%(filename)s") mparms = { 'filename': obj.filename, } self.set_statusbar_message(msg, mparms, echo=None, history=None) elif isinstance(obj, Track): metadata = obj.metadata if obj.num_linked_files == 1: file = obj.linked_files[0] orig_metadata = file.orig_metadata if file.state == File.ERROR: msg = N_( "%(filename)s (%(similarity)d%%) (error: %(error)s)" ) mparms = { 'filename': file.filename, 'similarity': file.similarity * 100, 'error': file.error } else: msg = N_("%(filename)s (%(similarity)d%%)") mparms = { 'filename': file.filename, 'similarity': file.similarity * 100, } self.set_statusbar_message(msg, mparms, echo=None, history=None) elif isinstance(obj, Album): metadata = obj.metadata orig_metadata = obj.orig_metadata elif obj.can_edit_tags(): metadata = obj.metadata self.metadata_box.selection_dirty = True self.metadata_box.update() self.cover_art_box.set_metadata(metadata, orig_metadata, obj) self.selection_updated.emit(objects) 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_toolbar(self): """Show/hide the Action toolbar.""" if self.show_toolbar_action.isChecked(): self.toolbar.show() else: self.toolbar.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): if reply.url().host() == config.setting['server_host']: ret = QtWidgets.QMessageBox.question( self, _("Authentication Required"), _("Picard needs authorization to access your personal data on the MusicBrainz server. Would you like to log in now?" ), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.Yes) if ret == QtWidgets.QMessageBox.Yes: pass else: 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.selected_objects) def cut(self): self.tagger.copy_files(self.selected_objects) self.paste_action.setEnabled(bool(self.selected_objects)) def paste(self): selected_objects = self.selected_objects if not selected_objects: target = self.tagger.unclustered_files else: target = selected_objects[0] self.tagger.paste_files(target) self.paste_action.setEnabled(False) def do_update_check(self): self.check_for_update(True) def auto_update_check(self): check_for_updates = config.setting['check_for_updates'] update_check_days = config.setting['update_check_days'] last_update_check = config.persist['last_update_check'] update_level = config.setting['update_level'] today = datetime.date.today().toordinal() do_auto_update_check = check_for_updates and update_check_days > 0 and today >= last_update_check + update_check_days log.debug( '{check_status} start-up check for program updates. Today: {today_date}, Last check: {last_check} (Check interval: {check_interval} days), Update level: {update_level} ({update_level_name})' .format( check_status='Initiating' if do_auto_update_check else 'Skipping', today_date=datetime.date.today(), last_check=str(datetime.date.fromordinal(last_update_check)) if last_update_check > 0 else 'never', check_interval=update_check_days, update_level=update_level, update_level_name=PROGRAM_UPDATE_LEVELS[update_level]['name'] if update_level in PROGRAM_UPDATE_LEVELS else 'unknown', )) if do_auto_update_check: self.check_for_update(False) def check_for_update(self, show_always): self.tagger.updatecheckmanager.check_update( show_always=show_always, update_level=config.setting['update_level'], callback=update_last_check_date)