class TagsCompatibilityAC3OptionsPage(OptionsPage): NAME = "tags_compatibility_ac3" TITLE = N_("AC3") PARENT = "tags" SORT_ORDER = 50 ACTIVE = True HELP_URL = '/config/options_tags_compatibility_ac3.html' options = [ config.BoolOption("setting", "ac3_save_ape", True), config.BoolOption("setting", "remove_ape_from_ac3", False), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_TagsCompatibilityOptionsPage() self.ui.setupUi(self) self.ui.ac3_no_tags.toggled.connect( self.ui.remove_ape_from_ac3.setEnabled) def load(self): if config.setting["ac3_save_ape"]: self.ui.ac3_save_ape.setChecked(True) else: self.ui.ac3_no_tags.setChecked(True) self.ui.remove_ape_from_ac3.setChecked( config.setting["remove_ape_from_ac3"]) self.ui.remove_ape_from_ac3.setEnabled( not config.setting["ac3_save_ape"]) def save(self): config.setting["ac3_save_ape"] = self.ui.ac3_save_ape.isChecked() config.setting[ "remove_ape_from_ac3"] = self.ui.remove_ape_from_ac3.isChecked()
class RatingsOptionsPage(OptionsPage): NAME = "ratings" TITLE = N_("Ratings") PARENT = "metadata" SORT_ORDER = 20 ACTIVE = True options = [ config.BoolOption("setting", "enable_ratings", False), config.TextOption("setting", "rating_user_email", "*****@*****.**"), config.BoolOption("setting", "submit_ratings", True), config.IntOption("setting", "rating_steps", 6), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_RatingsOptionsPage() self.ui.setupUi(self) def load(self): self.ui.enable_ratings.setChecked(config.setting["enable_ratings"]) self.ui.rating_user_email.setText(config.setting["rating_user_email"]) self.ui.submit_ratings.setChecked(config.setting["submit_ratings"]) def save(self): config.setting["enable_ratings"] = self.ui.enable_ratings.isChecked() config.setting["rating_user_email"] = self.ui.rating_user_email.text() config.setting["submit_ratings"] = self.ui.submit_ratings.isChecked()
class ProviderOptionsCaa(ProviderOptions): """ Options for Cover Art Archive cover art provider """ options = [ config.BoolOption("setting", "caa_save_single_front_image", False), config.BoolOption("setting", "caa_approved_only", False), config.BoolOption("setting", "caa_image_type_as_filename", False), config.IntOption("setting", "caa_image_size", 1), config.ListOption("setting", "caa_image_types", [u"front"]), config.BoolOption("setting", "caa_restrict_image_types", True), ] _options_ui = Ui_CaaOptions def __init__(self, parent=None): super(ProviderOptionsCaa, self).__init__(parent) self.ui.restrict_images_types.clicked.connect(self.update_caa_types) self.ui.select_caa_types.clicked.connect(self.select_caa_types) def load(self): self.ui.cb_image_size.setCurrentIndex(config.setting["caa_image_size"]) self.ui.cb_save_single_front_image.setChecked( config.setting["caa_save_single_front_image"]) self.ui.cb_approved_only.setChecked( config.setting["caa_approved_only"]) self.ui.cb_type_as_filename.setChecked( config.setting["caa_image_type_as_filename"]) self.ui.restrict_images_types.setChecked( config.setting["caa_restrict_image_types"]) self.update_caa_types() def save(self): config.setting["caa_image_size"] = \ self.ui.cb_image_size.currentIndex() config.setting["caa_save_single_front_image"] = \ self.ui.cb_save_single_front_image.isChecked() config.setting["caa_approved_only"] = \ self.ui.cb_approved_only.isChecked() config.setting["caa_image_type_as_filename"] = \ self.ui.cb_type_as_filename.isChecked() config.setting["caa_restrict_image_types"] = \ self.ui.restrict_images_types.isChecked() def update_caa_types(self): enabled = self.ui.restrict_images_types.isChecked() self.ui.select_caa_types.setEnabled(enabled) def select_caa_types(self): (types, ok) = CAATypesSelectorDialog.run(self, config.setting["caa_image_types"]) if ok: config.setting["caa_image_types"] = types
class AdvancedOptionsPage(OptionsPage): NAME = "advanced" TITLE = N_("Advanced") PARENT = None SORT_ORDER = 90 ACTIVE = True options = [ config.TextOption("setting", "ignore_regex", ""), config.BoolOption("setting", "ignore_hidden_files", False), config.BoolOption("setting", "completeness_ignore_videos", False), config.BoolOption("setting", "completeness_ignore_pregap", False), config.BoolOption("setting", "completeness_ignore_data", False), config.BoolOption("setting", "completeness_ignore_silence", False), ] def __init__(self, parent=None): super(AdvancedOptionsPage, self).__init__(parent) self.ui = Ui_AdvancedOptionsPage() self.ui.setupUi(self) self.init_regex_checker(self.ui.ignore_regex, self.ui.regex_error) def load(self): self.ui.ignore_regex.setText(config.setting["ignore_regex"]) self.ui.ignore_hidden_files.setChecked( config.setting["ignore_hidden_files"]) self.ui.completeness_ignore_videos.setChecked( config.setting["completeness_ignore_videos"]) self.ui.completeness_ignore_pregap.setChecked( config.setting["completeness_ignore_pregap"]) self.ui.completeness_ignore_data.setChecked( config.setting["completeness_ignore_data"]) self.ui.completeness_ignore_silence.setChecked( config.setting["completeness_ignore_silence"]) def save(self): config.setting["ignore_regex"] = unicode(self.ui.ignore_regex.text()) config.setting[ "ignore_hidden_files"] = self.ui.ignore_hidden_files.isChecked() config.setting[ "completeness_ignore_videos"] = self.ui.completeness_ignore_videos.isChecked( ) config.setting[ "completeness_ignore_pregap"] = self.ui.completeness_ignore_pregap.isChecked( ) config.setting[ "completeness_ignore_data"] = self.ui.completeness_ignore_data.isChecked( ) config.setting[ "completeness_ignore_silence"] = self.ui.completeness_ignore_silence.isChecked( )
class TagsOptionsPage(OptionsPage): NAME = "tags" TITLE = N_("Tags") PARENT = None SORT_ORDER = 30 ACTIVE = True HELP_URL = '/config/options_tags.html' options = [ config.BoolOption("setting", "dont_write_tags", False), config.BoolOption("setting", "preserve_timestamps", False), config.BoolOption("setting", "clear_existing_tags", False), config.BoolOption("setting", "remove_id3_from_flac", False), config.BoolOption("setting", "remove_ape_from_mp3", False), config.ListOption("setting", "preserved_tags", []), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_TagsOptionsPage() self.ui.setupUi(self) def load(self): self.ui.write_tags.setChecked(not config.setting["dont_write_tags"]) self.ui.preserve_timestamps.setChecked( config.setting["preserve_timestamps"]) self.ui.clear_existing_tags.setChecked( config.setting["clear_existing_tags"]) self.ui.remove_ape_from_mp3.setChecked( config.setting["remove_ape_from_mp3"]) self.ui.remove_id3_from_flac.setChecked( config.setting["remove_id3_from_flac"]) self.ui.preserved_tags.update(config.setting["preserved_tags"]) self.ui.preserved_tags.set_user_sortable(False) def save(self): config.setting["dont_write_tags"] = not self.ui.write_tags.isChecked() config.setting[ "preserve_timestamps"] = self.ui.preserve_timestamps.isChecked() clear_existing_tags = self.ui.clear_existing_tags.isChecked() if clear_existing_tags != config.setting["clear_existing_tags"]: config.setting["clear_existing_tags"] = clear_existing_tags self.tagger.window.metadata_box.update() config.setting[ "remove_ape_from_mp3"] = self.ui.remove_ape_from_mp3.isChecked() config.setting[ "remove_id3_from_flac"] = self.ui.remove_id3_from_flac.isChecked() config.setting["preserved_tags"] = list(self.ui.preserved_tags.tags) self.tagger.window.enable_tag_saving_action.setChecked( not config.setting["dont_write_tags"])
class AlbumTreeView(BaseTreeView): header_state = config.Option("persist", "album_view_header_state", QtCore.QByteArray()) header_locked = config.BoolOption("persist", "album_view_header_locked", False) def __init__(self, window, parent=None): super().__init__(window, parent) self.setAccessibleName(_("album view")) self.setAccessibleDescription(_("Contains albums and matched files")) self.tagger.album_added.connect(self.add_album) self.tagger.album_removed.connect(self.remove_album) def add_album(self, album): if isinstance(album, NatAlbum): item = NatAlbumItem(album, True) self.insertTopLevelItem(0, item) else: item = AlbumItem(album, True, self) item.setIcon(MainPanel.TITLE_COLUMN, AlbumItem.icon_cd) for i, column in enumerate(MainPanel.columns): font = item.font(i) font.setBold(True) item.setFont(i, font) item.setText(i, album.column(column[1])) self.add_cluster(album.unmatched_files, item) def remove_album(self, album): album.item.setSelected(False) self.takeTopLevelItem(self.indexOfTopLevelItem(album.item))
class FileTreeView(BaseTreeView): header_state = config.Option("persist", "file_view_header_state", QtCore.QByteArray()) header_locked = config.BoolOption("persist", "file_view_header_locked", False) def __init__(self, window, parent=None): super().__init__(window, parent) self.setAccessibleName(_("file view")) self.setAccessibleDescription( _("Contains unmatched files and clusters")) self.unmatched_files = ClusterItem(self.tagger.unclustered_files, False, self) self.unmatched_files.update() self.unmatched_files.setExpanded(True) self.clusters = ClusterItem(self.tagger.clusters, False, self) self.set_clusters_text() self.clusters.setExpanded(True) self.tagger.cluster_added.connect(self.add_file_cluster) self.tagger.cluster_removed.connect(self.remove_file_cluster) def add_file_cluster(self, cluster, parent_item=None): self.add_cluster(cluster, parent_item) self.set_clusters_text() def remove_file_cluster(self, cluster): cluster.item.setSelected(False) self.clusters.removeChild(cluster.item) self.set_clusters_text() def set_clusters_text(self): self.clusters.setText( MainPanel.TITLE_COLUMN, '%s (%d)' % (_("Clusters"), len(self.tagger.clusters)))
class ProxyDialog(QtGui.QDialog): options = [ config.BoolOption("persist", "save_authentication", True), ] def __init__(self, authenticator, proxy, parent=None): QtGui.QDialog.__init__(self, parent) self._authenticator = authenticator self._proxy = proxy self.ui = Ui_PasswordDialog() self.ui.setupUi(self) self.ui.info_text.setText(_("The proxy %s requires you to login. Please enter your username and password.") % config.setting["proxy_server_host"]) self.ui.save_authentication.setChecked(config.persist["save_authentication"]) self.ui.username.setText(config.setting["proxy_username"]) self.ui.password.setText(config.setting["proxy_password"]) self.ui.save_authentication.hide() self.ui.buttonbox.accepted.connect(self.set_proxy_password) def set_proxy_password(self): config.setting["proxy_username"] = unicode(self.ui.username.text()) config.setting["proxy_password"] = unicode(self.ui.password.text()) self._authenticator.setUser(unicode(self.ui.username.text())) self._authenticator.setPassword(unicode(self.ui.password.text())) self.accept()
class PasswordDialog(QtGui.QDialog): options = [ config.BoolOption("persist", "save_authentication", True), ] def __init__(self, authenticator, reply, parent=None): QtGui.QDialog.__init__(self, parent) self._authenticator = authenticator self.ui = Ui_PasswordDialog() self.ui.setupUi(self) self.ui.info_text.setText(_("The server %s requires you to login. Please enter your username and password.") % reply.url().host()) # TODO: Implement proper password storage for arbitrary servers if self._is_musicbrainz_server(reply.url().host(), reply.url().port()): self.ui.save_authentication.setChecked(config.persist["save_authentication"]) self.ui.username.setText(config.setting["username"]) self.ui.password.setText(config.setting["password"]) else: self.ui.username.setText(reply.url().userName()) self.ui.password.setText(reply.url().password()) self.ui.save_authentication.setChecked(False) self.ui.save_authentication.hide() self.ui.buttonbox.accepted.connect(self.set_new_password) def set_new_password(self): config.persist["save_authentication"] = self.ui.save_authentication.isChecked() if config.persist["save_authentication"]: config.setting["username"] = unicode(self.ui.username.text()) config.setting["password"] = rot13(unicode(self.ui.password.text())) self._authenticator.setUser(unicode(self.ui.username.text())) self._authenticator.setPassword(unicode(self.ui.password.text())) self.accept() def _is_musicbrainz_server(self, host, port): return host == config.setting["server_host"] and port == config.setting["server_port"]
class AcousticBrainzOptionsPage(OptionsPage): NAME = "acousticbrainz_tags" TITLE = "AcousticBrainz tags" PARENT = "plugins" options = [ config.BoolOption("setting", "acousticbrainz_add_simplemood", True), config.BoolOption("setting", "acousticbrainz_add_simplegenre", True), config.BoolOption("setting", "acousticbrainz_add_keybpm", False), config.BoolOption("setting", "acousticbrainz_add_fullhighlevel", False), config.BoolOption("setting", "acousticbrainz_add_sublowlevel", False) ] def __init__(self, parent=None): super(AcousticBrainzOptionsPage, self).__init__(parent) self.ui = Ui_AcousticBrainzOptionsPage() self.ui.setupUi(self) def load(self): setting = config.setting self.ui.add_simplemood.setChecked( setting["acousticbrainz_add_simplemood"]) self.ui.add_simplegenre.setChecked( setting["acousticbrainz_add_simplegenre"]) self.ui.add_fullhighlevel.setChecked( setting["acousticbrainz_add_fullhighlevel"]) self.ui.add_keybpm.setChecked(setting["acousticbrainz_add_keybpm"]) self.ui.add_sublowlevel.setChecked( setting["acousticbrainz_add_sublowlevel"]) def save(self): setting = config.setting setting[ "acousticbrainz_add_simplemood"] = self.ui.add_simplemood.isChecked( ) setting[ "acousticbrainz_add_simplegenre"] = self.ui.add_simplegenre.isChecked( ) setting["acousticbrainz_add_keybpm"] = self.ui.add_keybpm.isChecked() setting[ "acousticbrainz_add_fullhighlevel"] = self.ui.add_fullhighlevel.isChecked( ) setting[ "acousticbrainz_add_sublowlevel"] = self.ui.add_sublowlevel.isChecked( )
class ScriptingOptionsPage(OptionsPage): NAME = "scripting" TITLE = N_("Scripting") PARENT = "advanced" SORT_ORDER = 30 ACTIVE = True options = [ config.BoolOption("setting", "enable_tagger_script", False), config.TextOption("setting", "tagger_script", ""), ] def __init__(self, parent=None): super(ScriptingOptionsPage, self).__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_checker) def live_checker(self): 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(unicode(self.ui.tagger_script.toPlainText())) except Exception as e: raise OptionsCheckError(_("Script Error"), str(e)) def load(self): self.ui.enable_tagger_script.setChecked( config.setting["enable_tagger_script"]) self.ui.tagger_script.document().setPlainText( config.setting["tagger_script"]) args = { "picard-doc-scripting-url": PICARD_URLS['doc_scripting'], } text = _(u'<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_script"] = self.ui.enable_tagger_script.isChecked() config.setting["tagger_script"] = self.ui.tagger_script.toPlainText() def display_error(self, error): pass
class GenresOptionsPage(OptionsPage): NAME = "genres" TITLE = N_("Genres") PARENT = "metadata" SORT_ORDER = 20 ACTIVE = True options = [ config.BoolOption("setting", "use_genres", False), config.IntOption("setting", "max_genres", 5), config.IntOption("setting", "min_genre_usage", 90), config.TextOption("setting", "ignore_genres", "seen live, favorites, fixme, owned"), config.TextOption("setting", "join_genres", ""), config.BoolOption("setting", "only_my_genres", False), config.BoolOption("setting", "artists_genres", False), config.BoolOption("setting", "folksonomy_tags", False), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_GenresOptionsPage() self.ui.setupUi(self) def load(self): self.ui.use_genres.setChecked(config.setting["use_genres"]) self.ui.max_genres.setValue(config.setting["max_genres"]) self.ui.min_genre_usage.setValue(config.setting["min_genre_usage"]) self.ui.join_genres.setEditText(config.setting["join_genres"]) self.ui.ignore_genres.setText(config.setting["ignore_genres"]) self.ui.only_my_genres.setChecked(config.setting["only_my_genres"]) self.ui.artists_genres.setChecked(config.setting["artists_genres"]) self.ui.folksonomy_tags.setChecked(config.setting["folksonomy_tags"]) def save(self): config.setting["use_genres"] = self.ui.use_genres.isChecked() config.setting["max_genres"] = self.ui.max_genres.value() config.setting["min_genre_usage"] = self.ui.min_genre_usage.value() config.setting["join_genres"] = self.ui.join_genres.currentText() config.setting["ignore_genres"] = self.ui.ignore_genres.text() config.setting["only_my_genres"] = self.ui.only_my_genres.isChecked() config.setting["artists_genres"] = self.ui.artists_genres.isChecked() config.setting["folksonomy_tags"] = self.ui.folksonomy_tags.isChecked()
class GeneralOptionsPage(OptionsPage): NAME = "general" TITLE = N_("General") PARENT = None SORT_ORDER = 1 ACTIVE = True options = [ config.TextOption("setting", "server_host", MUSICBRAINZ_SERVERS[0]), config.IntOption("setting", "server_port", 80), config.TextOption("setting", "username", ""), config.PasswordOption("setting", "password", ""), config.BoolOption("setting", "analyze_new_files", False), config.BoolOption("setting", "ignore_file_mbids", False), ] def __init__(self, parent=None): super(GeneralOptionsPage, self).__init__(parent) self.ui = Ui_GeneralOptionsPage() self.ui.setupUi(self) self.ui.server_host.addItems(MUSICBRAINZ_SERVERS) def load(self): self.ui.server_host.setEditText(config.setting["server_host"]) self.ui.server_port.setValue(config.setting["server_port"]) self.ui.username.setText(config.setting["username"]) self.ui.password.setText(config.setting["password"]) self.ui.analyze_new_files.setChecked(config.setting["analyze_new_files"]) self.ui.ignore_file_mbids.setChecked(config.setting["ignore_file_mbids"]) def save(self): config.setting["server_host"] = unicode(self.ui.server_host.currentText()).strip() config.setting["server_port"] = self.ui.server_port.value() reload_collections = config.setting["username"] != unicode(self.ui.username.text()) \ or config.setting["password"] != unicode(self.ui.password.text()) config.setting["username"] = unicode(self.ui.username.text()) # trivially encode the password, just to not make it so apparent config.setting["password"] = rot13(unicode(self.ui.password.text())) if reload_collections: load_user_collections() config.setting["analyze_new_files"] = self.ui.analyze_new_files.isChecked() config.setting["ignore_file_mbids"] = self.ui.ignore_file_mbids.isChecked()
class TagsCompatibilityWaveOptionsPage(OptionsPage): NAME = "tags_compatibility_wave" TITLE = N_("WAVE") PARENT = "tags" SORT_ORDER = 60 ACTIVE = True HELP_URL = '/config/options_tags_compatibility_wave.html' options = [ config.BoolOption("setting", "write_wave_riff_info", True), config.BoolOption("setting", "remove_wave_riff_info", False), config.TextOption("setting", "wave_riff_info_encoding", "windows-1252"), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_TagsCompatibilityOptionsPage() self.ui.setupUi(self) def load(self): self.ui.write_wave_riff_info.setChecked( config.setting["write_wave_riff_info"]) self.ui.remove_wave_riff_info.setChecked( config.setting["remove_wave_riff_info"]) if config.setting["wave_riff_info_encoding"] == "utf-8": self.ui.wave_riff_info_enc_utf8.setChecked(True) else: self.ui.wave_riff_info_enc_cp1252.setChecked(True) def save(self): config.setting[ "write_wave_riff_info"] = self.ui.write_wave_riff_info.isChecked() config.setting[ "remove_wave_riff_info"] = self.ui.remove_wave_riff_info.isChecked( ) if self.ui.wave_riff_info_enc_utf8.isChecked(): config.setting["wave_riff_info_encoding"] = "utf-8" else: config.setting["wave_riff_info_encoding"] = "windows-1252"
class ScriptingOptionsPage(OptionsPage): NAME = "scripting" TITLE = N_("Scripting") PARENT = "advanced" SORT_ORDER = 30 ACTIVE = True options = [ config.BoolOption("setting", "enable_tagger_script", False), config.TextOption("setting", "tagger_script", ""), ] STYLESHEET_ERROR = "QWidget { background-color: #f55; color: white; font-weight:bold }" def __init__(self, parent=None): super(ScriptingOptionsPage, self).__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_checker) def live_checker(self): 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(unicode(self.ui.tagger_script.toPlainText())) except Exception as e: raise OptionsCheckError(_("Script Error"), str(e)) def load(self): self.ui.enable_tagger_script.setChecked(config.setting["enable_tagger_script"]) self.ui.tagger_script.document().setPlainText(config.setting["tagger_script"]) def save(self): config.setting["enable_tagger_script"] = self.ui.enable_tagger_script.isChecked() config.setting["tagger_script"] = self.ui.tagger_script.toPlainText() def display_error(self, error): pass
class AdvancedOptionsPage(OptionsPage): NAME = "advanced" TITLE = N_("Advanced") PARENT = None SORT_ORDER = 90 ACTIVE = True HELP_URL = '/config/options_advanced.html' options = [ config.TextOption("setting", "ignore_regex", ""), config.BoolOption("setting", "ignore_hidden_files", False), config.BoolOption("setting", "recursively_add_files", True), config.IntOption("setting", "ignore_track_duration_difference_under", 2), config.BoolOption("setting", "completeness_ignore_videos", False), config.BoolOption("setting", "completeness_ignore_pregap", False), config.BoolOption("setting", "completeness_ignore_data", False), config.BoolOption("setting", "completeness_ignore_silence", False), config.ListOption("setting", "compare_ignore_tags", []), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_AdvancedOptionsPage() self.ui.setupUi(self) self.init_regex_checker(self.ui.ignore_regex, self.ui.regex_error) def load(self): self.ui.ignore_regex.setText(config.setting["ignore_regex"]) self.ui.ignore_hidden_files.setChecked(config.setting["ignore_hidden_files"]) self.ui.recursively_add_files.setChecked(config.setting["recursively_add_files"]) self.ui.ignore_track_duration_difference_under.setValue(config.setting["ignore_track_duration_difference_under"]) self.ui.completeness_ignore_videos.setChecked(config.setting["completeness_ignore_videos"]) self.ui.completeness_ignore_pregap.setChecked(config.setting["completeness_ignore_pregap"]) self.ui.completeness_ignore_data.setChecked(config.setting["completeness_ignore_data"]) self.ui.completeness_ignore_silence.setChecked(config.setting["completeness_ignore_silence"]) self.ui.compare_ignore_tags.update(config.setting["compare_ignore_tags"]) self.ui.compare_ignore_tags.set_user_sortable(False) def save(self): config.setting["ignore_regex"] = self.ui.ignore_regex.text() config.setting["ignore_hidden_files"] = self.ui.ignore_hidden_files.isChecked() config.setting["recursively_add_files"] = self.ui.recursively_add_files.isChecked() config.setting["ignore_track_duration_difference_under"] = self.ui.ignore_track_duration_difference_under.value() config.setting["completeness_ignore_videos"] = self.ui.completeness_ignore_videos.isChecked() config.setting["completeness_ignore_pregap"] = self.ui.completeness_ignore_pregap.isChecked() config.setting["completeness_ignore_data"] = self.ui.completeness_ignore_data.isChecked() config.setting["completeness_ignore_silence"] = self.ui.completeness_ignore_silence.isChecked() tags = list(self.ui.compare_ignore_tags.tags) if tags != config.setting["compare_ignore_tags"]: config.setting["compare_ignore_tags"] = tags def restore_defaults(self): self.ui.compare_ignore_tags.clear() super().restore_defaults()
class 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 MetadataOptionsPage(OptionsPage): NAME = "metadata" TITLE = N_("Metadata") PARENT = None SORT_ORDER = 20 ACTIVE = True options = [ config.TextOption("setting", "va_name", u"Various Artists"), config.TextOption("setting", "nat_name", u"[non-album tracks]"), config.TextOption("setting", "artist_locale", u"en"), config.BoolOption("setting", "translate_artist_names", False), config.BoolOption("setting", "release_ars", True), config.BoolOption("setting", "track_ars", False), config.BoolOption("setting", "folksonomy_tags", False), config.BoolOption("setting", "convert_punctuation", True), config.BoolOption("setting", "standardize_artists", False), ] def __init__(self, parent=None): super(MetadataOptionsPage, self).__init__(parent) self.ui = Ui_MetadataOptionsPage() self.ui.setupUi(self) self.ui.va_name_default.clicked.connect(self.set_va_name_default) self.ui.nat_name_default.clicked.connect(self.set_nat_name_default) def load(self): self.ui.translate_artist_names.setChecked( config.setting["translate_artist_names"]) combo_box = self.ui.artist_locale locales = sorted(ALIAS_LOCALES.keys()) for i, loc in enumerate(locales): name = ALIAS_LOCALES[loc] if "_" in loc: name = " " + name combo_box.addItem(name, loc) if loc == config.setting["artist_locale"]: combo_box.setCurrentIndex(i) self.ui.convert_punctuation.setChecked( config.setting["convert_punctuation"]) self.ui.release_ars.setChecked(config.setting["release_ars"]) self.ui.track_ars.setChecked(config.setting["track_ars"]) self.ui.folksonomy_tags.setChecked(config.setting["folksonomy_tags"]) self.ui.va_name.setText(config.setting["va_name"]) self.ui.nat_name.setText(config.setting["nat_name"]) self.ui.standardize_artists.setChecked( config.setting["standardize_artists"]) def save(self): config.setting[ "translate_artist_names"] = self.ui.translate_artist_names.isChecked( ) config.setting["artist_locale"] = self.ui.artist_locale.itemData( self.ui.artist_locale.currentIndex()) config.setting[ "convert_punctuation"] = self.ui.convert_punctuation.isChecked() config.setting["release_ars"] = self.ui.release_ars.isChecked() config.setting["track_ars"] = self.ui.track_ars.isChecked() config.setting["folksonomy_tags"] = self.ui.folksonomy_tags.isChecked() config.setting["va_name"] = self.ui.va_name.text() nat_name = unicode(self.ui.nat_name.text()) if nat_name != config.setting["nat_name"]: config.setting["nat_name"] = nat_name self.tagger.nats.update() config.setting[ "standardize_artists"] = self.ui.standardize_artists.isChecked() def set_va_name_default(self): self.ui.va_name.setText(self.options[0].default) self.ui.va_name.setCursorPosition(0) def set_nat_name_default(self): self.ui.nat_name.setText(self.options[1].default) self.ui.nat_name.setCursorPosition(0)
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 TagsCompatibilityOptionsPage(OptionsPage): NAME = "tags_compatibility" TITLE = N_("Tag Compatibility") PARENT = "tags" SORT_ORDER = 30 ACTIVE = True options = [ config.BoolOption("setting", "write_id3v1", True), config.BoolOption("setting", "write_id3v23", True), config.TextOption("setting", "id3v2_encoding", "utf-16"), config.TextOption("setting", "id3v23_join_with", "/"), config.BoolOption("setting", "itunes_compatible_grouping", False), config.BoolOption("setting", "aac_save_ape", True), config.BoolOption("setting", "remove_ape_from_aac", False), config.BoolOption("setting", "ac3_save_ape", True), config.BoolOption("setting", "remove_ape_from_ac3", False), config.BoolOption("setting", "write_wave_riff_info", True), config.BoolOption("setting", "remove_wave_riff_info", False), config.TextOption("setting", "wave_riff_info_encoding", "windows-1252"), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_TagsCompatibilityOptionsPage() self.ui.setupUi(self) self.ui.write_id3v23.clicked.connect(self.update_encodings) self.ui.write_id3v24.clicked.connect( partial(self.update_encodings, force_utf8=True)) self.ui.aac_no_tags.toggled.connect( self.ui.remove_ape_from_aac.setEnabled) self.ui.ac3_no_tags.toggled.connect( self.ui.remove_ape_from_ac3.setEnabled) def load(self): self.ui.write_id3v1.setChecked(config.setting["write_id3v1"]) if config.setting["write_id3v23"]: self.ui.write_id3v23.setChecked(True) else: self.ui.write_id3v24.setChecked(True) if config.setting["id3v2_encoding"] == "iso-8859-1": self.ui.enc_iso88591.setChecked(True) elif config.setting["id3v2_encoding"] == "utf-16": self.ui.enc_utf16.setChecked(True) else: self.ui.enc_utf8.setChecked(True) self.ui.id3v23_join_with.setEditText( config.setting["id3v23_join_with"]) self.ui.itunes_compatible_grouping.setChecked( config.setting["itunes_compatible_grouping"]) if config.setting["aac_save_ape"]: self.ui.aac_save_ape.setChecked(True) else: self.ui.aac_no_tags.setChecked(True) self.ui.remove_ape_from_aac.setChecked( config.setting["remove_ape_from_aac"]) self.ui.remove_ape_from_aac.setEnabled( not config.setting["aac_save_ape"]) if config.setting["ac3_save_ape"]: self.ui.ac3_save_ape.setChecked(True) else: self.ui.ac3_no_tags.setChecked(True) self.ui.remove_ape_from_ac3.setChecked( config.setting["remove_ape_from_ac3"]) self.ui.remove_ape_from_ac3.setEnabled( not config.setting["ac3_save_ape"]) self.ui.write_wave_riff_info.setChecked( config.setting["write_wave_riff_info"]) self.ui.remove_wave_riff_info.setChecked( config.setting["remove_wave_riff_info"]) if config.setting["wave_riff_info_encoding"] == "utf-8": self.ui.wave_riff_info_enc_utf8.setChecked(True) else: self.ui.wave_riff_info_enc_cp1252.setChecked(True) self.update_encodings() def save(self): config.setting["write_id3v1"] = self.ui.write_id3v1.isChecked() config.setting["write_id3v23"] = self.ui.write_id3v23.isChecked() config.setting[ "id3v23_join_with"] = self.ui.id3v23_join_with.currentText() if self.ui.enc_iso88591.isChecked(): config.setting["id3v2_encoding"] = "iso-8859-1" elif self.ui.enc_utf16.isChecked(): config.setting["id3v2_encoding"] = "utf-16" else: config.setting["id3v2_encoding"] = "utf-8" config.setting[ "itunes_compatible_grouping"] = self.ui.itunes_compatible_grouping.isChecked( ) config.setting["aac_save_ape"] = self.ui.aac_save_ape.isChecked() config.setting[ "remove_ape_from_aac"] = self.ui.remove_ape_from_aac.isChecked() config.setting["ac3_save_ape"] = self.ui.ac3_save_ape.isChecked() config.setting[ "remove_ape_from_ac3"] = self.ui.remove_ape_from_ac3.isChecked() config.setting[ "write_wave_riff_info"] = self.ui.write_wave_riff_info.isChecked() config.setting[ "remove_wave_riff_info"] = self.ui.remove_wave_riff_info.isChecked( ) if self.ui.wave_riff_info_enc_utf8.isChecked(): config.setting["wave_riff_info_encoding"] = "utf-8" else: config.setting["wave_riff_info_encoding"] = "windows-1252" def update_encodings(self, force_utf8=False): if self.ui.write_id3v23.isChecked(): if self.ui.enc_utf8.isChecked(): self.ui.enc_utf16.setChecked(True) self.ui.enc_utf8.setEnabled(False) self.ui.label_id3v23_join_with.setEnabled(True) self.ui.id3v23_join_with.setEnabled(True) else: self.ui.enc_utf8.setEnabled(True) if force_utf8: self.ui.enc_utf8.setChecked(True) self.ui.label_id3v23_join_with.setEnabled(False) self.ui.id3v23_join_with.setEnabled(False)
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 FileBrowser(QtGui.QTreeView): options = [ config.TextOption("persist", "current_browser_path", ""), config.BoolOption("persist", "show_hidden_files", False), ] def __init__(self, parent): QtGui.QTreeView.__init__(self, parent) self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) self.setDragEnabled(True) self.move_files_here_action = QtGui.QAction(_("&Move Tagged Files Here"), self) self.move_files_here_action.triggered.connect(self.move_files_here) self.addAction(self.move_files_here_action) self.toggle_hidden_action = QtGui.QAction(_("Show &Hidden Files"), self) self.toggle_hidden_action.setCheckable(True) self.toggle_hidden_action.setChecked(config.persist["show_hidden_files"]) self.toggle_hidden_action.toggled.connect(self.show_hidden) self.addAction(self.toggle_hidden_action) self.set_as_starting_directory_action = QtGui.QAction(_("&Set as starting directory"), self) self.set_as_starting_directory_action.triggered.connect(self.set_as_starting_directory) self.addAction(self.set_as_starting_directory_action) self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu) self.focused = False self._set_model() def _set_model(self): self.model = QtGui.QFileSystemModel() self.model.layoutChanged.connect(self._layout_changed) self.model.setRootPath("") self._set_model_filter() filters = [] for exts, name in supported_formats(): filters.extend("*" + e for e in exts) self.model.setNameFilters(filters) # Hide unsupported files completely self.model.setNameFilterDisables(False) self.model.sort(0, QtCore.Qt.AscendingOrder) self.setModel(self.model) if sys.platform == "darwin": self.setRootIndex(self.model.index("/Volumes")) header = self.header() header.hideSection(1) header.hideSection(2) header.hideSection(3) header.setResizeMode(QtGui.QHeaderView.ResizeToContents) header.setStretchLastSection(False) header.setVisible(False) def _set_model_filter(self): filter = QtCore.QDir.AllDirs | QtCore.QDir.Files | QtCore.QDir.Drives | QtCore.QDir.NoDotAndDotDot if config.persist["show_hidden_files"]: filter |= QtCore.QDir.Hidden self.model.setFilter(filter) def _layout_changed(self): def scroll(): # XXX The currentIndex seems to change while QFileSystemModel is # populating itself (so setCurrentIndex in __init__ won't last). # The time it takes to load varies and there are no signals to find # out when it's done. As a workaround, keep restoring the state as # long as the layout is updating, and the user hasn't focused yet. if not self.focused: self._restore_state() self.scrollTo(self.currentIndex()) QtCore.QTimer.singleShot(0, scroll) def mousePressEvent(self, event): index = self.indexAt(event.pos()) if index.isValid(): self.selectionModel().setCurrentIndex(index, QtGui.QItemSelectionModel.NoUpdate) QtGui.QTreeView.mousePressEvent(self, event) def focusInEvent(self, event): self.focused = True QtGui.QTreeView.focusInEvent(self, event) def show_hidden(self, state): config.persist["show_hidden_files"] = state self._set_model_filter() def save_state(self): indexes = self.selectedIndexes() if indexes: path = self.model.filePath(indexes[0]) config.persist["current_browser_path"] = os.path.normpath(unicode(path)) def restore_state(self): pass def _restore_state(self): if config.setting["starting_directory"]: path = config.setting["starting_directory_path"] scrolltype = QtGui.QAbstractItemView.PositionAtTop else: path = config.persist["current_browser_path"] scrolltype = QtGui.QAbstractItemView.PositionAtCenter if path: index = self.model.index(find_existing_path(unicode(path))) self.setCurrentIndex(index) self.expand(index) self.scrollTo(index, scrolltype) def move_files_here(self): indexes = self.selectedIndexes() if not indexes: return path = self.model.filePath(indexes[0]) config.setting["move_files_to"] = os.path.normpath(unicode(path)) def set_as_starting_directory(self): indexes = self.selectedIndexes() if indexes: path = self.model.filePath(indexes[0]) config.setting["starting_directory_path"] = os.path.normpath(unicode(path))
class CoverOptionsPage(OptionsPage): NAME = "cover" TITLE = N_("Cover Art") PARENT = None SORT_ORDER = 35 ACTIVE = True options = [ config.BoolOption("setting", "save_images_to_tags", True), config.BoolOption("setting", "save_only_front_images_to_tags", True), config.BoolOption("setting", "save_images_to_files", False), config.TextOption("setting", "cover_image_filename", "cover"), config.BoolOption("setting", "save_images_overwrite", False), config.BoolOption("setting", "ca_provider_use_amazon", True), config.BoolOption("setting", "ca_provider_use_caa", True), config.BoolOption("setting", "ca_provider_use_whitelist", True), config.BoolOption("setting", "caa_approved_only", True), config.BoolOption("setting", "caa_image_type_as_filename", False), config.IntOption("setting", "caa_image_size", 1), config.ListOption("setting", "caa_image_types", [u"front"]), ] def __init__(self, parent=None): super(CoverOptionsPage, self).__init__(parent) self.ui = Ui_CoverOptionsPage() self.ui.setupUi(self) self.ui.save_images_to_files.clicked.connect(self.update_filename) def load(self): self.ui.save_images_to_tags.setChecked( config.setting["save_images_to_tags"]) self.ui.cb_embed_front_only.setChecked( config.setting["save_only_front_images_to_tags"]) self.ui.save_images_to_files.setChecked( config.setting["save_images_to_files"]) self.ui.cover_image_filename.setText( config.setting["cover_image_filename"]) self.ui.save_images_overwrite.setChecked( config.setting["save_images_overwrite"]) self.update_filename() self.ui.caprovider_amazon.setChecked( config.setting["ca_provider_use_amazon"]) self.ui.caprovider_caa.setChecked( config.setting["ca_provider_use_caa"]) self.ui.caprovider_whitelist.setChecked( config.setting["ca_provider_use_whitelist"]) self.ui.gb_caa.setEnabled(config.setting["ca_provider_use_caa"]) self.ui.cb_image_size.setCurrentIndex(config.setting["caa_image_size"]) widget = self.ui.caa_types_selector_1 self._selector = CAATypesSelector(widget, config.setting["caa_image_types"]) config.setting["caa_image_types"] = self._selector.get_selected_types() self.ui.cb_approved_only.setChecked( config.setting["caa_approved_only"]) self.ui.cb_type_as_filename.setChecked( config.setting["caa_image_type_as_filename"]) self.connect(self.ui.caprovider_caa, QtCore.SIGNAL("toggled(bool)"), self.ui.gb_caa.setEnabled) def save(self): config.setting[ "save_images_to_tags"] = self.ui.save_images_to_tags.isChecked() config.setting[ "save_only_front_images_to_tags"] = self.ui.cb_embed_front_only.isChecked( ) config.setting[ "save_images_to_files"] = self.ui.save_images_to_files.isChecked() config.setting["cover_image_filename"] = unicode( self.ui.cover_image_filename.text()) config.setting["ca_provider_use_amazon"] =\ self.ui.caprovider_amazon.isChecked() config.setting["ca_provider_use_caa"] =\ self.ui.caprovider_caa.isChecked() config.setting["ca_provider_use_whitelist"] =\ self.ui.caprovider_whitelist.isChecked() config.setting["caa_image_size"] =\ self.ui.cb_image_size.currentIndex() config.setting["caa_image_types"] = self._selector.get_selected_types() config.setting["caa_approved_only"] =\ self.ui.cb_approved_only.isChecked() config.setting["caa_image_type_as_filename"] = \ self.ui.cb_type_as_filename.isChecked() config.setting[ "save_images_overwrite"] = self.ui.save_images_overwrite.isChecked( ) def update_filename(self): enabled = self.ui.save_images_to_files.isChecked() self.ui.cover_image_filename.setEnabled(enabled) self.ui.save_images_overwrite.setEnabled(enabled)
class CoverOptionsPage(OptionsPage): NAME = "cover" TITLE = N_("Cover Art") PARENT = None SORT_ORDER = 35 ACTIVE = True options = [ config.BoolOption("setting", "save_images_to_tags", True), config.BoolOption("setting", "embed_only_one_front_image", True), config.BoolOption("setting", "save_images_to_files", False), config.TextOption("setting", "cover_image_filename", "cover"), config.BoolOption("setting", "save_images_overwrite", False), config.ListOption("setting", "ca_providers", [ ('Cover Art Archive', True), ('Amazon', True), ('Whitelist', True), ('CaaReleaseGroup', False), ('Local', False), ]), ] def __init__(self, parent=None): super(CoverOptionsPage, self).__init__(parent) self.ui = Ui_CoverOptionsPage() self.ui.setupUi(self) self.ui.save_images_to_files.clicked.connect(self.update_filename) self.ui.save_images_to_tags.clicked.connect(self.update_save_images_to_tags) self.provider_list_widget = SortableCheckboxListWidget() self.ui.ca_providers_list.insertWidget(0, self.provider_list_widget) self.ca_providers = [] def load_cover_art_providers(self): """Load available providers, initialize provider-specific options, restore state of each """ providers = cover_art_providers() for provider in providers: try: title = _(provider.TITLE) except AttributeError: title = provider.NAME checked = is_provider_enabled(provider.NAME) self.provider_list_widget.addItem(SortableCheckboxListItem(title, checked=checked, data=provider.NAME)) def update_providers_options(items): self.ca_providers = [(item.data, item.checked) for item in items] self.provider_list_widget.changed.connect(update_providers_options) def restore_defaults(self): # Remove previous entries self.provider_list_widget.clear() super(CoverOptionsPage, self).restore_defaults() def load(self): self.ui.save_images_to_tags.setChecked(config.setting["save_images_to_tags"]) self.ui.cb_embed_front_only.setChecked(config.setting["embed_only_one_front_image"]) self.ui.save_images_to_files.setChecked(config.setting["save_images_to_files"]) self.ui.cover_image_filename.setText(config.setting["cover_image_filename"]) self.ui.save_images_overwrite.setChecked(config.setting["save_images_overwrite"]) self.ca_providers = config.setting["ca_providers"] self.load_cover_art_providers() self.update_all() def save(self): config.setting["save_images_to_tags"] = self.ui.save_images_to_tags.isChecked() config.setting["embed_only_one_front_image"] = self.ui.cb_embed_front_only.isChecked() config.setting["save_images_to_files"] = self.ui.save_images_to_files.isChecked() config.setting["cover_image_filename"] = self.ui.cover_image_filename.text() config.setting["save_images_overwrite"] = self.ui.save_images_overwrite.isChecked() config.setting["ca_providers"] = self.ca_providers def update_all(self): self.update_filename() self.update_save_images_to_tags() def update_filename(self): enabled = self.ui.save_images_to_files.isChecked() self.ui.cover_image_filename.setEnabled(enabled) self.ui.save_images_overwrite.setEnabled(enabled) def update_save_images_to_tags(self): enabled = self.ui.save_images_to_tags.isChecked() self.ui.cb_embed_front_only.setEnabled(enabled)
class GeneralOptionsPage(OptionsPage): NAME = "general" TITLE = N_("General") PARENT = None SORT_ORDER = 1 ACTIVE = True options = [ config.TextOption("setting", "server_host", MUSICBRAINZ_SERVERS[0]), config.IntOption("setting", "server_port", 443), config.TextOption("persist", "oauth_refresh_token", ""), config.BoolOption("setting", "analyze_new_files", False), config.BoolOption("setting", "ignore_file_mbids", False), config.TextOption("persist", "oauth_refresh_token", ""), config.TextOption("persist", "oauth_refresh_token_scopes", ""), config.TextOption("persist", "oauth_access_token", ""), config.IntOption("persist", "oauth_access_token_expires", 0), config.TextOption("persist", "oauth_username", ""), config.BoolOption("setting", "check_for_updates", True), config.IntOption("setting", "update_check_days", 7), config.IntOption("setting", "update_level", 0), config.IntOption("persist", "last_update_check", 0), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_GeneralOptionsPage() self.ui.setupUi(self) self.ui.server_host.addItems(MUSICBRAINZ_SERVERS) self.ui.login.clicked.connect(self.login) self.ui.logout.clicked.connect(self.logout) self.update_login_logout() def load(self): self.ui.server_host.setEditText(config.setting["server_host"]) self.ui.server_port.setValue(config.setting["server_port"]) self.ui.analyze_new_files.setChecked( config.setting["analyze_new_files"]) self.ui.ignore_file_mbids.setChecked( config.setting["ignore_file_mbids"]) if self.tagger.autoupdate_enabled: self.ui.check_for_updates.setChecked( config.setting["check_for_updates"]) self.ui.update_level.clear() for level, description in PROGRAM_UPDATE_LEVELS.items(): # TODO: Remove temporary workaround once https://github.com/python-babel/babel/issues/415 has been resolved. babel_415_workaround = description['title'] self.ui.update_level.addItem(_(babel_415_workaround), level) self.ui.update_level.setCurrentIndex( self.ui.update_level.findData(config.setting["update_level"])) self.ui.update_check_days.setValue( config.setting["update_check_days"]) else: self.ui.update_check_groupbox.hide() def save(self): config.setting["server_host"] = self.ui.server_host.currentText( ).strip() config.setting["server_port"] = self.ui.server_port.value() config.setting[ "analyze_new_files"] = self.ui.analyze_new_files.isChecked() config.setting[ "ignore_file_mbids"] = self.ui.ignore_file_mbids.isChecked() if self.tagger.autoupdate_enabled: config.setting[ "check_for_updates"] = self.ui.check_for_updates.isChecked() config.setting["update_level"] = self.ui.update_level.currentData( QtCore.Qt.UserRole) config.setting[ "update_check_days"] = self.ui.update_check_days.value() def update_login_logout(self): if self.tagger.webservice.oauth_manager.is_logged_in(): self.ui.logged_in.setText( _("Logged in as <b>%s</b>.") % config.persist["oauth_username"]) self.ui.logged_in.show() self.ui.login.hide() self.ui.logout.show() else: self.ui.logged_in.hide() self.ui.login.show() self.ui.logout.hide() def login(self): self.tagger.mb_login(self.on_login_finished, self) def restore_defaults(self): super().restore_defaults() self.logout() def on_login_finished(self, successful): self.update_login_logout() def logout(self): self.tagger.mb_logout() self.update_login_logout()
class TagsOptionsPage(OptionsPage): NAME = "tags" TITLE = N_("Tags") PARENT = None SORT_ORDER = 30 ACTIVE = True options = [ config.BoolOption("setting", "clear_existing_tags", False), config.TextOption("setting", "preserved_tags", ""), config.BoolOption("setting", "write_id3v1", True), config.BoolOption("setting", "write_id3v23", True), config.TextOption("setting", "id3v2_encoding", "utf-16"), config.TextOption("setting", "id3v23_join_with", "/"), config.BoolOption("setting", "remove_id3_from_flac", False), config.BoolOption("setting", "remove_ape_from_mp3", False), config.BoolOption("setting", "tpe2_albumartist", False), config.BoolOption("setting", "dont_write_tags", False), config.BoolOption("setting", "preserve_timestamps", False), ] def __init__(self, parent=None): super(TagsOptionsPage, self).__init__(parent) self.ui = Ui_TagsOptionsPage() self.ui.setupUi(self) self.ui.write_id3v23.clicked.connect(self.update_encodings) self.ui.write_id3v24.clicked.connect(self.update_encodings) self.completer = QtWidgets.QCompleter(sorted(TAG_NAMES.keys()), self) self.completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) self.completer.setWidget(self.ui.preserved_tags) self.ui.preserved_tags.textEdited.connect(self.preserved_tags_edited) self.completer.activated.connect(self.completer_activated) def load(self): self.ui.write_tags.setChecked(not config.setting["dont_write_tags"]) self.ui.preserve_timestamps.setChecked( config.setting["preserve_timestamps"]) self.ui.clear_existing_tags.setChecked( config.setting["clear_existing_tags"]) self.ui.write_id3v1.setChecked(config.setting["write_id3v1"]) if config.setting["write_id3v23"]: self.ui.write_id3v23.setChecked(True) else: self.ui.write_id3v24.setChecked(True) if config.setting["id3v2_encoding"] == "iso-8859-1": self.ui.enc_iso88591.setChecked(True) elif config.setting["id3v2_encoding"] == "utf-16": self.ui.enc_utf16.setChecked(True) else: self.ui.enc_utf8.setChecked(True) self.ui.id3v23_join_with.setEditText( config.setting["id3v23_join_with"]) self.ui.remove_ape_from_mp3.setChecked( config.setting["remove_ape_from_mp3"]) self.ui.remove_id3_from_flac.setChecked( config.setting["remove_id3_from_flac"]) self.ui.preserved_tags.setText(config.setting["preserved_tags"]) self.update_encodings() def save(self): config.setting["dont_write_tags"] = not self.ui.write_tags.isChecked() config.setting[ "preserve_timestamps"] = self.ui.preserve_timestamps.isChecked() clear_existing_tags = self.ui.clear_existing_tags.isChecked() if clear_existing_tags != config.setting["clear_existing_tags"]: config.setting["clear_existing_tags"] = clear_existing_tags self.tagger.window.metadata_box.update() config.setting["write_id3v1"] = self.ui.write_id3v1.isChecked() config.setting["write_id3v23"] = self.ui.write_id3v23.isChecked() config.setting[ "id3v23_join_with"] = self.ui.id3v23_join_with.currentText() if self.ui.enc_iso88591.isChecked(): config.setting["id3v2_encoding"] = "iso-8859-1" elif self.ui.enc_utf16.isChecked(): config.setting["id3v2_encoding"] = "utf-16" else: config.setting["id3v2_encoding"] = "utf-8" config.setting[ "remove_ape_from_mp3"] = self.ui.remove_ape_from_mp3.isChecked() config.setting[ "remove_id3_from_flac"] = self.ui.remove_id3_from_flac.isChecked() config.setting["preserved_tags"] = self.ui.preserved_tags.text() self.tagger.window.enable_tag_saving_action.setChecked( not config.setting["dont_write_tags"]) def update_encodings(self): if self.ui.write_id3v23.isChecked(): if self.ui.enc_utf8.isChecked(): self.ui.enc_utf16.setChecked(True) self.ui.enc_utf8.setEnabled(False) self.ui.label_id3v23_join_with.setEnabled(True) self.ui.id3v23_join_with.setEnabled(True) else: self.ui.enc_utf8.setEnabled(True) self.ui.enc_utf8.setChecked(True) self.ui.label_id3v23_join_with.setEnabled(False) self.ui.id3v23_join_with.setEnabled(False) def preserved_tags_edited(self, text): prefix = text[:self.ui.preserved_tags.cursorPosition()].split(",")[-1] self.completer.setCompletionPrefix(prefix) if prefix: self.completer.complete() else: self.completer.popup().hide() def completer_activated(self, text): input = self.ui.preserved_tags current = input.text() i = input.cursorPosition() p = len(self.completer.completionPrefix()) input.setText("%s%s %s" % (current[:i - p], text, current[i:])) input.setCursorPosition(i - p + len(text) + 1)
class ProviderOptionsCaa(ProviderOptions): """ Options for Cover Art Archive cover art provider """ options = [ config.BoolOption("setting", "caa_save_single_front_image", False), config.BoolOption("setting", "caa_approved_only", False), config.BoolOption("setting", "caa_image_type_as_filename", False), config.IntOption("setting", "caa_image_size", _CAA_IMAGE_SIZE_DEFAULT), config.ListOption("setting", "caa_image_types", _CAA_IMAGE_TYPE_DEFAULT_INCLUDE), config.BoolOption("setting", "caa_restrict_image_types", True), config.ListOption("setting", "caa_image_types_to_omit", _CAA_IMAGE_TYPE_DEFAULT_EXCLUDE), ] _options_ui = Ui_CaaOptions def __init__(self, parent=None): super().__init__(parent) self.ui.restrict_images_types.clicked.connect(self.update_caa_types) self.ui.select_caa_types.clicked.connect(self.select_caa_types) def restore_defaults(self): self.caa_image_types = _CAA_IMAGE_TYPE_DEFAULT_INCLUDE self.caa_image_types_to_omit = _CAA_IMAGE_TYPE_DEFAULT_EXCLUDE super().restore_defaults() def load(self): self.ui.cb_image_size.clear() for item_id, item in _CAA_THUMBNAIL_SIZE_MAP.items(): self.ui.cb_image_size.addItem(_(item.label), userData=item_id) size = config.setting["caa_image_size"] index = self.ui.cb_image_size.findData(size) if index < 0: index = self.ui.cb_image_size.findData(_CAA_IMAGE_SIZE_DEFAULT) self.ui.cb_image_size.setCurrentIndex(index) self.ui.cb_save_single_front_image.setChecked(config.setting["caa_save_single_front_image"]) self.ui.cb_approved_only.setChecked(config.setting["caa_approved_only"]) self.ui.cb_type_as_filename.setChecked(config.setting["caa_image_type_as_filename"]) self.ui.restrict_images_types.setChecked( config.setting["caa_restrict_image_types"]) self.caa_image_types = config.setting["caa_image_types"] self.caa_image_types_to_omit = config.setting["caa_image_types_to_omit"] self.update_caa_types() def save(self): size = self.ui.cb_image_size.currentData() config.setting["caa_image_size"] = size config.setting["caa_save_single_front_image"] = \ self.ui.cb_save_single_front_image.isChecked() config.setting["caa_approved_only"] = \ self.ui.cb_approved_only.isChecked() config.setting["caa_image_type_as_filename"] = \ self.ui.cb_type_as_filename.isChecked() config.setting["caa_restrict_image_types"] = \ self.ui.restrict_images_types.isChecked() config.setting["caa_image_types"] = self.caa_image_types config.setting["caa_image_types_to_omit"] = self.caa_image_types_to_omit def update_caa_types(self): enabled = self.ui.restrict_images_types.isChecked() self.ui.select_caa_types.setEnabled(enabled) def select_caa_types(self): (types, types_to_omit, ok) = CAATypesSelectorDialog.run( self, self.caa_image_types, self.caa_image_types_to_omit) if ok: self.caa_image_types = types self.caa_image_types_to_omit = types_to_omit
class CoverOptionsPage(OptionsPage): NAME = "cover" TITLE = N_("Cover Art") PARENT = None SORT_ORDER = 35 ACTIVE = True HELP_URL = '/config/options_cover.html' options = [ config.BoolOption("setting", "save_images_to_tags", True), config.BoolOption("setting", "embed_only_one_front_image", True), config.BoolOption("setting", "save_images_to_files", False), config.TextOption("setting", "cover_image_filename", "cover"), config.BoolOption("setting", "save_images_overwrite", False), config.ListOption("setting", "ca_providers", [ ('Cover Art Archive', True), ('Amazon', True), ('Whitelist', True), ('CaaReleaseGroup', False), ('Local', False), ]), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_CoverOptionsPage() self.ui.setupUi(self) self.ui.save_images_to_files.clicked.connect(self.update_filename) self.ui.save_images_to_tags.clicked.connect(self.update_save_images_to_tags) self.move_view = MoveableListView(self.ui.ca_providers_list, self.ui.up_button, self.ui.down_button) def load_cover_art_providers(self): """Load available providers, initialize provider-specific options, restore state of each """ for p in cover_art_providers(): self.ui.ca_providers_list.addItem(CheckboxListItem(_(p.title), checked=p.enabled, data=p.name)) def restore_defaults(self): # Remove previous entries self.ui.ca_providers_list.clear() super().restore_defaults() def ca_providers(self): items = [] for i in range(self.ui.ca_providers_list.count()): item = self.ui.ca_providers_list.item(i) items.append((item.data, item.checked)) return items def load(self): self.ui.save_images_to_tags.setChecked(config.setting["save_images_to_tags"]) self.ui.cb_embed_front_only.setChecked(config.setting["embed_only_one_front_image"]) self.ui.save_images_to_files.setChecked(config.setting["save_images_to_files"]) self.ui.cover_image_filename.setText(config.setting["cover_image_filename"]) self.ui.save_images_overwrite.setChecked(config.setting["save_images_overwrite"]) self.load_cover_art_providers() self.ui.ca_providers_list.setCurrentRow(0) self.update_all() def save(self): config.setting["save_images_to_tags"] = self.ui.save_images_to_tags.isChecked() config.setting["embed_only_one_front_image"] = self.ui.cb_embed_front_only.isChecked() config.setting["save_images_to_files"] = self.ui.save_images_to_files.isChecked() config.setting["cover_image_filename"] = self.ui.cover_image_filename.text() config.setting["save_images_overwrite"] = self.ui.save_images_overwrite.isChecked() config.setting["ca_providers"] = self.ca_providers() def update_all(self): self.update_filename() self.update_save_images_to_tags() def update_ca_providers_groupbox_state(self): files_enabled = self.ui.save_images_to_files.isChecked() tags_enabled = self.ui.save_images_to_tags.isChecked() self.ui.ca_providers_groupbox.setEnabled(files_enabled or tags_enabled) def update_filename(self): enabled = self.ui.save_images_to_files.isChecked() self.ui.cover_image_filename.setEnabled(enabled) self.ui.save_images_overwrite.setEnabled(enabled) self.update_ca_providers_groupbox_state() def update_save_images_to_tags(self): enabled = self.ui.save_images_to_tags.isChecked() self.ui.cb_embed_front_only.setEnabled(enabled) self.update_ca_providers_groupbox_state()
class RenamingOptionsPage(OptionsPage): NAME = "filerenaming" TITLE = N_("File Naming") PARENT = None SORT_ORDER = 40 ACTIVE = True options = [ config.BoolOption("setting", "windows_compatibility", True), config.BoolOption("setting", "ascii_filenames", False), config.BoolOption("setting", "rename_files", False), config.TextOption( "setting", "file_naming_format", _DEFAULT_FILE_NAMING_FORMAT, ), config.BoolOption("setting", "move_files", False), config.TextOption("setting", "move_files_to", ""), config.BoolOption("setting", "move_additional_files", False), config.TextOption("setting", "move_additional_files_pattern", "*.jpg *.png"), config.BoolOption("setting", "delete_empty_dirs", True), ] def __init__(self, parent=None): super(RenamingOptionsPage, self).__init__(parent) self.ui = Ui_RenamingOptionsPage() self.ui.setupUi(self) self.ui.ascii_filenames.clicked.connect(self.update_examples) self.ui.windows_compatibility.clicked.connect(self.update_examples) self.ui.rename_files.clicked.connect(self.update_examples) self.ui.move_files.clicked.connect(self.update_examples) self.ui.move_files_to.editingFinished.connect(self.update_examples) self.ui.move_files.toggled.connect( partial(enabledSlot, self.toggle_file_moving)) self.ui.rename_files.toggled.connect( partial(enabledSlot, self.toggle_file_renaming)) self.ui.file_naming_format.textChanged.connect(self.check_formats) self.ui.file_naming_format_default.clicked.connect( self.set_file_naming_format_default) self.highlighter = TaggerScriptSyntaxHighlighter( self.ui.file_naming_format.document()) self.ui.move_files_to_browse.clicked.connect(self.move_files_to_browse) textEdit = self.ui.file_naming_format self.textEditPaletteNormal = textEdit.palette() self.textEditPaletteReadOnly = QPalette(self.textEditPaletteNormal) disabled_color = self.textEditPaletteNormal.color( QPalette.Inactive, QPalette.Window) self.textEditPaletteReadOnly.setColor(QPalette.Disabled, QPalette.Base, disabled_color) def toggle_file_moving(self, state): self.ui.delete_empty_dirs.setEnabled(state) self.ui.move_files_to.setEnabled(state) self.ui.move_files_to_browse.setEnabled(state) self.ui.move_additional_files.setEnabled(state) self.ui.move_additional_files_pattern.setEnabled(state) def toggle_file_renaming(self, state): self.ui.file_naming_format.setEnabled(state) self.ui.file_naming_format_default.setEnabled(state) self.ui.ascii_filenames.setEnabled(state) self.ui.file_naming_format_group.setEnabled(state) if not sys.platform == "win32": self.ui.windows_compatibility.setEnabled(state) if self.ui.file_naming_format.isEnabled(): self.ui.file_naming_format.setPalette(self.textEditPaletteNormal) else: self.ui.file_naming_format.setPalette(self.textEditPaletteReadOnly) def check_formats(self): self.test() self.update_examples() def _example_to_filename(self, file): settings = { 'windows_compatibility': self.ui.windows_compatibility.isChecked(), 'ascii_filenames': self.ui.ascii_filenames.isChecked(), 'rename_files': self.ui.rename_files.isChecked(), 'move_files': self.ui.move_files.isChecked(), 'use_va_format': False, # TODO remove 'file_naming_format': unicode(self.ui.file_naming_format.toPlainText()), 'move_files_to': os.path.normpath(unicode(self.ui.move_files_to.text())) } try: if config.setting["enable_tagger_script"]: script = config.setting["tagger_script"] parser = ScriptParser() parser.eval(script, file.metadata) filename = file._make_filename(file.filename, file.metadata, settings) if not settings["move_files"]: return os.path.basename(filename) return filename except SyntaxError: return "" except ScriptError: return "" except TypeError: return "" def update_examples(self): # TODO: Here should be more examples etc. # TODO: Would be nice to show diffs too.... example1 = self._example_to_filename(self.example_1()) example2 = self._example_to_filename(self.example_2()) self.ui.example_filename.setText(example1) self.ui.example_filename_va.setText(example2) def load(self): if sys.platform == "win32": self.ui.windows_compatibility.setChecked(True) self.ui.windows_compatibility.setEnabled(False) else: self.ui.windows_compatibility.setChecked( config.setting["windows_compatibility"]) self.ui.rename_files.setChecked(config.setting["rename_files"]) self.ui.move_files.setChecked(config.setting["move_files"]) self.ui.ascii_filenames.setChecked(config.setting["ascii_filenames"]) self.ui.file_naming_format.setPlainText( config.setting["file_naming_format"]) args = { "picard-doc-scripting-url": PICARD_URLS['doc_scripting'], } text = _(u'<a href="%(picard-doc-scripting-url)s">Open Scripting' ' Documentation in your browser</a>') % args self.ui.file_naming_format_documentation.setText(text) self.ui.move_files_to.setText(config.setting["move_files_to"]) self.ui.move_files_to.setCursorPosition(0) self.ui.move_additional_files.setChecked( config.setting["move_additional_files"]) self.ui.move_additional_files_pattern.setText( config.setting["move_additional_files_pattern"]) self.ui.delete_empty_dirs.setChecked( config.setting["delete_empty_dirs"]) self.update_examples() def check(self): self.check_format() if self.ui.move_files.isChecked() and not unicode( self.ui.move_files_to.text()).strip(): raise OptionsCheckError( _("Error"), _("The location to move files to must not be empty.")) def check_format(self): parser = ScriptParser() try: parser.eval(unicode(self.ui.file_naming_format.toPlainText())) except Exception as e: raise OptionsCheckError("", str(e)) if self.ui.rename_files.isChecked(): if not unicode(self.ui.file_naming_format.toPlainText()).strip(): raise OptionsCheckError( "", _("The file naming format must not be empty.")) def save(self): config.setting[ "windows_compatibility"] = self.ui.windows_compatibility.isChecked( ) config.setting["ascii_filenames"] = self.ui.ascii_filenames.isChecked() config.setting["rename_files"] = self.ui.rename_files.isChecked() config.setting["file_naming_format"] = unicode( self.ui.file_naming_format.toPlainText()) self.tagger.window.enable_renaming_action.setChecked( config.setting["rename_files"]) config.setting["move_files"] = self.ui.move_files.isChecked() config.setting["move_files_to"] = os.path.normpath( unicode(self.ui.move_files_to.text())) config.setting[ "move_additional_files"] = self.ui.move_additional_files.isChecked( ) config.setting["move_additional_files_pattern"] = unicode( self.ui.move_additional_files_pattern.text()) config.setting[ "delete_empty_dirs"] = self.ui.delete_empty_dirs.isChecked() self.tagger.window.enable_moving_action.setChecked( config.setting["move_files"]) def display_error(self, error): pass def set_file_naming_format_default(self): self.ui.file_naming_format.setText(self.options[3].default) # self.ui.file_naming_format.setCursorPosition(0) def example_1(self): file = File("ticket_to_ride.mp3") file.state = File.NORMAL file.metadata['album'] = 'Help!' file.metadata['title'] = 'Ticket to Ride' file.metadata['artist'] = 'The Beatles' file.metadata['artistsort'] = 'Beatles, The' file.metadata['albumartist'] = 'The Beatles' file.metadata['albumartistsort'] = 'Beatles, The' file.metadata['tracknumber'] = '7' file.metadata['totaltracks'] = '14' file.metadata['discnumber'] = '1' file.metadata['totaldiscs'] = '1' file.metadata['date'] = '1965-08-06' file.metadata['releasetype'] = ['album', 'soundtrack'] file.metadata['~primaryreleasetype'] = ['album'] file.metadata['~secondaryreleasetype'] = ['soundtrack'] file.metadata['releasestatus'] = 'official' file.metadata['releasecountry'] = 'US' file.metadata['~extension'] = 'mp3' file.metadata[ 'musicbrainz_albumid'] = '2c053984-4645-4699-9474-d2c35c227028' file.metadata[ 'musicbrainz_albumartistid'] = 'b10bbbfc-cf9e-42e0-be17-e2c3e1d2600d' file.metadata[ 'musicbrainz_artistid'] = 'b10bbbfc-cf9e-42e0-be17-e2c3e1d2600d' file.metadata[ 'musicbrainz_recordingid'] = 'ed052ae1-c950-47f2-8d2b-46e1b58ab76c' file.metadata[ 'musicbrainz_releasetrackid'] = '7668a62a-2fac-3151-a744-5707ac8c883c' return file def example_2(self): file = File("track05.mp3") file.state = File.NORMAL file.metadata['album'] = u"Coup d'État, Volume 1: Ku De Ta / Prologue" file.metadata['title'] = u"I've Got to Learn the Mambo" file.metadata['artist'] = u"Snowboy feat. James Hunter" file.metadata['artistsort'] = u"Snowboy feat. Hunter, James" file.metadata['albumartist'] = config.setting['va_name'] file.metadata['albumartistsort'] = config.setting['va_name'] file.metadata['tracknumber'] = '5' file.metadata['totaltracks'] = '13' file.metadata['discnumber'] = '2' file.metadata['totaldiscs'] = '2' file.metadata['discsubtitle'] = u"Beat Up" file.metadata['date'] = u'2005-07-04' file.metadata['releasetype'] = [u'album', u'compilation'] file.metadata['~primaryreleasetype'] = u'album' file.metadata['~secondaryreleasetype'] = u'compilation' file.metadata['releasestatus'] = u'official' file.metadata['releasecountry'] = u'AU' file.metadata['compilation'] = '1' file.metadata['~multiartist'] = '1' file.metadata['~extension'] = 'mp3' file.metadata[ 'musicbrainz_albumid'] = u'4b50c71e-0a07-46ac-82e4-cb85dc0c9bdd' file.metadata[ 'musicbrainz_recordingid'] = u'b3c487cb-0e55-477d-8df3-01ec6590f099' file.metadata[ 'musicbrainz_releasetrackid'] = u'f8649a05-da39-39ba-957c-7abf8f9012be' file.metadata[ 'musicbrainz_albumartistid'] = u'89ad4ac3-39f7-470e-963a-56509c546377' file.metadata['musicbrainz_artistid'] = [ u'7b593455-d207-482c-8c6f-19ce22c94679', u'9e082466-2390-40d1-891e-4803531f43fd' ] return file def move_files_to_browse(self): path = QtGui.QFileDialog.getExistingDirectory( self, "", self.ui.move_files_to.text()) if path: path = os.path.normpath(unicode(path)) self.ui.move_files_to.setText(path) def test(self): self.ui.renaming_error.setStyleSheet("") self.ui.renaming_error.setText("") try: self.check_format() except OptionsCheckError as e: self.ui.renaming_error.setStyleSheet(self.STYLESHEET_ERROR) self.ui.renaming_error.setText(e.info) return
class InterfaceOptionsPage(OptionsPage): NAME = "interface" TITLE = N_("User Interface") PARENT = None SORT_ORDER = 80 ACTIVE = True SEPARATOR = '—' * 5 TOOLBAR_BUTTONS = { 'add_directory_action': { 'label': N_('Add Folder'), 'icon': 'folder' }, 'add_files_action': { 'label': N_('Add Files'), 'icon': 'document-open' }, 'cluster_action': { 'label': N_('Cluster'), 'icon': 'picard-cluster' }, 'autotag_action': { 'label': N_('Lookup'), 'icon': 'picard-auto-tag' }, 'analyze_action': { 'label': N_('Scan'), 'icon': 'picard-analyze' }, 'browser_lookup_action': { 'label': N_('Lookup in Browser'), 'icon': 'lookup-musicbrainz' }, 'save_action': { 'label': N_('Save'), 'icon': 'document-save' }, 'view_info_action': { 'label': N_('Info'), 'icon': 'picard-edit-tags' }, 'remove_action': { 'label': N_('Remove'), 'icon': 'list-remove' }, 'submit_acoustid_action': { 'label': N_('Submit AcoustIDs'), 'icon': 'acoustid-fingerprinter' }, 'play_file_action': { 'label': N_('Open in Player'), 'icon': 'play-music' }, 'cd_lookup_action': { 'label': N_('Lookup CD...'), 'icon': 'media-optical' }, } ACTION_NAMES = set(TOOLBAR_BUTTONS.keys()) options = [ config.BoolOption("setting", "toolbar_show_labels", True), config.BoolOption("setting", "toolbar_multiselect", False), config.BoolOption("setting", "builtin_search", False), config.BoolOption("setting", "use_adv_search_syntax", False), config.BoolOption("setting", "quit_confirmation", True), config.TextOption("setting", "ui_language", ""), config.BoolOption("setting", "starting_directory", False), config.TextOption("setting", "starting_directory_path", _default_starting_dir), config.TextOption("setting", "load_image_behavior", "append"), config.ListOption("setting", "toolbar_layout", [ 'add_directory_action', 'add_files_action', 'separator', 'cluster_action', 'separator', 'autotag_action', 'analyze_action', 'browser_lookup_action', 'separator', 'save_action', 'view_info_action', 'remove_action', 'separator', 'cd_lookup_action', 'separator', 'submit_acoustid_action', ]), ] def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_InterfaceOptionsPage() self.ui.setupUi(self) self.ui.ui_language.addItem(_('System default'), '') language_list = [(l[0], l[1], _(l[2])) for l in UI_LANGUAGES] def fcmp(x): return locale.strxfrm(x[2]) for lang_code, native, translation in sorted(language_list, key=fcmp): if native and native != translation: name = '%s (%s)' % (translation, native) else: name = translation self.ui.ui_language.addItem(name, lang_code) self.ui.starting_directory.stateChanged.connect( partial(enabledSlot, self.ui.starting_directory_path.setEnabled)) self.ui.starting_directory.stateChanged.connect( partial(enabledSlot, self.ui.starting_directory_browse.setEnabled)) self.ui.starting_directory_browse.clicked.connect( self.starting_directory_browse) self.ui.add_button.clicked.connect(self.add_to_toolbar) self.ui.insert_separator_button.clicked.connect(self.insert_separator) self.ui.remove_button.clicked.connect(self.remove_action) self.move_view = MoveableListView(self.ui.toolbar_layout_list, self.ui.up_button, self.ui.down_button, self.update_action_buttons) self.update_buttons = self.move_view.update_buttons def load(self): self.ui.toolbar_show_labels.setChecked( config.setting["toolbar_show_labels"]) self.ui.toolbar_multiselect.setChecked( config.setting["toolbar_multiselect"]) self.ui.builtin_search.setChecked(config.setting["builtin_search"]) self.ui.use_adv_search_syntax.setChecked( config.setting["use_adv_search_syntax"]) self.ui.quit_confirmation.setChecked( config.setting["quit_confirmation"]) current_ui_language = config.setting["ui_language"] self.ui.ui_language.setCurrentIndex( self.ui.ui_language.findData(current_ui_language)) self.ui.starting_directory.setChecked( config.setting["starting_directory"]) self.ui.starting_directory_path.setText( config.setting["starting_directory_path"]) self.populate_action_list() self.ui.toolbar_layout_list.setCurrentRow(0) self.update_buttons() def save(self): config.setting[ "toolbar_show_labels"] = self.ui.toolbar_show_labels.isChecked() config.setting[ "toolbar_multiselect"] = self.ui.toolbar_multiselect.isChecked() config.setting["builtin_search"] = self.ui.builtin_search.isChecked() config.setting[ "use_adv_search_syntax"] = self.ui.use_adv_search_syntax.isChecked( ) config.setting[ "quit_confirmation"] = self.ui.quit_confirmation.isChecked() self.tagger.window.update_toolbar_style() new_language = self.ui.ui_language.itemData( self.ui.ui_language.currentIndex()) if new_language != config.setting["ui_language"]: config.setting["ui_language"] = self.ui.ui_language.itemData( self.ui.ui_language.currentIndex()) dialog = QtWidgets.QMessageBox( QtWidgets.QMessageBox.Information, _('Language changed'), _('You have changed the interface language. You have to restart Picard in order for the change to take effect.' ), QtWidgets.QMessageBox.Ok, self) dialog.exec_() config.setting[ "starting_directory"] = self.ui.starting_directory.isChecked() config.setting["starting_directory_path"] = os.path.normpath( self.ui.starting_directory_path.text()) self.update_layout_config() def restore_defaults(self): super().restore_defaults() self.update_buttons() def starting_directory_browse(self): item = self.ui.starting_directory_path path = QtWidgets.QFileDialog.getExistingDirectory( self, "", item.text()) if path: path = os.path.normpath(path) item.setText(path) def _get_icon_from_name(self, name): return self.TOOLBAR_BUTTONS[name]['icon'] def _insert_item(self, action, index=None): list_item = ToolbarListItem(action) list_item.setToolTip(_('Drag and Drop to re-order')) if action in self.TOOLBAR_BUTTONS: # TODO: Remove temporary workaround once https://github.com/python-babel/babel/issues/415 has been resolved. babel_415_workaround = self.TOOLBAR_BUTTONS[action]['label'] list_item.setText(_(babel_415_workaround)) list_item.setIcon( icontheme.lookup(self._get_icon_from_name(action), icontheme.ICON_SIZE_MENU)) else: list_item.setText(self.SEPARATOR) if index is not None: self.ui.toolbar_layout_list.insertItem(index, list_item) else: self.ui.toolbar_layout_list.addItem(list_item) return list_item def _all_list_items(self): return [ self.ui.toolbar_layout_list.item(i).action_name for i in range(self.ui.toolbar_layout_list.count()) ] def _added_actions(self): actions = self._all_list_items() return set(action for action in actions if action != 'separator') def populate_action_list(self): self.ui.toolbar_layout_list.clear() for name in config.setting['toolbar_layout']: if name in self.ACTION_NAMES or name == 'separator': self._insert_item(name) def update_action_buttons(self): self.ui.add_button.setEnabled( self._added_actions() != self.ACTION_NAMES) def add_to_toolbar(self): display_list = set.difference(self.ACTION_NAMES, self._added_actions()) selected_action, ok = AddActionDialog.get_selected_action( display_list, self) if ok: list_item = self._insert_item( selected_action, self.ui.toolbar_layout_list.currentRow() + 1) self.ui.toolbar_layout_list.setCurrentItem(list_item) self.update_buttons() def insert_separator(self): insert_index = self.ui.toolbar_layout_list.currentRow() + 1 self._insert_item('separator', insert_index) def remove_action(self): item = self.ui.toolbar_layout_list.takeItem( self.ui.toolbar_layout_list.currentRow()) del item self.update_buttons() def update_layout_config(self): config.setting['toolbar_layout'] = self._all_list_items() self._update_toolbar() def _update_toolbar(self): widget = self.parent() while not isinstance(widget, QtWidgets.QMainWindow): widget = widget.parent() # Call the main window's create toolbar method widget.create_action_toolbar() widget.set_tab_order()