diff --git a/picard/album.py b/picard/album.py index 63e10bb947..4c8565f1ea 100644 --- a/picard/album.py +++ b/picard/album.py @@ -319,11 +319,11 @@ def update(self, update_tracks=True): def _add_file(self, track, file): self._files += 1 - self.update(False) + self.update(update_tracks=False) def _remove_file(self, track, file): self._files -= 1 - self.update(False) + self.update(update_tracks=False) def match_files(self, files, use_trackid=True): """Match files to tracks on this album, based on metadata similarity or trackid.""" @@ -381,7 +381,7 @@ def can_remove(self): return True def can_edit_tags(self): - return False + return True def can_analyze(self): return False @@ -392,6 +392,9 @@ def can_autotag(self): def can_refresh(self): return True + def can_view_info(self): + return self._files > 0 + def get_num_matched_tracks(self): num = 0 for track in self.tracks: diff --git a/picard/cluster.py b/picard/cluster.py index 7a37d16bfe..5e7474e2a8 100644 --- a/picard/cluster.py +++ b/picard/cluster.py @@ -30,20 +30,27 @@ class Cluster(QtCore.QObject, Item): + # Weights for different elements when comparing a cluster to a release + comparison_weights = { + 'album': 17, + 'artist': 6, + 'totaltracks': 5, + 'releasecountry': 2, + 'format': 2, + } + def __init__(self, name, artist="", special=False, related_album=None, hide_if_empty=False): QtCore.QObject.__init__(self) self.item = None self.metadata = Metadata() self.metadata['album'] = name - self.metadata['artist'] = artist + self.metadata['albumartist'] = artist self.metadata['totaltracks'] = 0 self.special = special self.hide_if_empty = hide_if_empty self.related_album = related_album self.files = [] self.lookup_task = None - # Weights for different elements when comparing a cluster to a release - self.comparison_weights = { 'album' : 17, 'artist' : 6, 'totaltracks' : 5, 'releasecountry': 2, 'format': 2 } def __repr__(self): return '' % self.metadata['album'] @@ -75,10 +82,6 @@ def remove_file(self, file): if not self.special and self.get_num_files() == 0: self.tagger.remove_cluster(self) - def update_file(self, file): - if file.item: - file.item.update() - def update(self): if self.item: self.item.update() @@ -103,7 +106,7 @@ def can_remove(self): def can_edit_tags(self): """Return if this object supports tag editing.""" - return False + return True def can_analyze(self): """Return if this object can be fingerprinted.""" @@ -122,6 +125,8 @@ def column(self, column): return '' elif column == '~length': return format_time(self.metadata.length) + elif column == 'artist': + return self.metadata['albumartist'] return self.metadata[column] def _compare_to_release(self, release): @@ -140,9 +145,9 @@ def _compare_to_release(self, release): """ total = 0.0 parts = [] - w = self.comparison_weights + w = Cluster.comparison_weights - a = self.metadata['artist'] + a = self.metadata['albumartist'] b = artist_credit_from_node(release.artist_credit[0], self.config)[0] parts.append((similarity2(a, b), w["artist"])) total += w["artist"] @@ -183,8 +188,8 @@ def lookup_metadata(self): """ Try to identify the cluster using the existing metadata. """ self.tagger.window.set_statusbar_message(N_("Looking up the metadata for cluster %s..."), self.metadata['album']) self.lookup_task = self.tagger.xmlws.find_releases(self._lookup_finished, - artist=self.metadata.get('artist', ''), - release=self.metadata.get('album', ''), + artist=self.metadata['albumartist'], + release=self.metadata['album'], tracks=str(len(self.files)), limit=25) @@ -263,6 +268,9 @@ def remove_file(self, file): def lookup_metadata(self): self.tagger.autotag(self.files) + def can_edit_tags(self): + return False + class ClusterList(list, Item): """A list of clusters.""" diff --git a/picard/const.py b/picard/const.py index 66b86fe11d..f7b6e6e494 100644 --- a/picard/const.py +++ b/picard/const.py @@ -25,16 +25,6 @@ PUID_SUBMIT_HOST = "musicbrainz.org" PUID_SUBMIT_PORT = 80 -# Amazon associate and developer ids -AMAZON_STORE_ASSOCIATE_IDS = { - u'amazon.ca': u'musicbrainz01-20', - u'amazon.co.jp': u'musicbrainz-22', - u'amazon.co.uk': u'musicbrainz0c-21', - u'amazon.com': u'musicbrainz0d-20', - u'amazon.de': u'musicbrainz00-21', - u'amazon.fr': u'musicbrainz0e-21', -} - # MusicDNS client ID MUSICDNS_KEY = "0736ac2cd889ef77f26f6b5e3fb8a09c" diff --git a/picard/file.py b/picard/file.py index 5cadfdb3d2..fe880425ac 100644 --- a/picard/file.py +++ b/picard/file.py @@ -112,6 +112,7 @@ def _copy_metadata(self, metadata): filename, extension = os.path.splitext(self.base_filename) self.metadata.copy(metadata) self.metadata['~extension'] = extension[1:].lower() + self.metadata['~length'] = format_time(self.metadata.length) if 'title' not in self.metadata: self.metadata['title'] = filename if 'tracknumber' not in self.metadata: @@ -180,6 +181,7 @@ def _saving_finished(self, next, result=None, error=None): temp_info[info] = self.orig_metadata[info] self.orig_metadata.copy(self.metadata) self.orig_metadata.length = length + self.orig_metadata['~length'] = format_time(length) for k, v in temp_info.items(): self.orig_metadata[k] = v self.metadata.changed = False @@ -365,9 +367,9 @@ def update(self, signal=True): self.similarity = 1.0 if self.state in (File.CHANGED, File.NORMAL): self.state = File.NORMAL - if signal: + if signal and self.item: self.log.debug("Updating file %r", self) - self.parent.update_file(self) + self.item.update() def can_save(self): """Return if this object can be saved.""" @@ -391,6 +393,9 @@ def can_autotag(self): def can_refresh(self): return False + def can_view_info(self): + return True + def _info(self, metadata, file): if hasattr(file.info, 'length'): metadata.length = int(file.info.length * 1000) @@ -407,7 +412,6 @@ def _info(self, metadata, file): def get_state(self): return self._state - # in order to significantly speed up performance, the number of pending # files is cached num_pending_files = 0 @@ -427,12 +431,9 @@ def set_state(self, state, update=False): def column(self, column): m = self.metadata - if column == '~length': - return format_time(m.length) - elif column == "title" and not m["title"]: + if column == "title" and not m["title"]: return self.base_filename - else: - return m[column] + return m[column] def _compare_to_track(self, track): """ diff --git a/picard/mbxml.py b/picard/mbxml.py index 2efcf6ff72..cc19fcb73c 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -211,6 +211,7 @@ def track_to_metadata(node, track, config): m.length = int(nodes[0].text) elif name == 'artist_credit': artist_credit_to_metadata(nodes[0], m, config) + m['~length'] = format_time(m.length) def recording_to_metadata(node, track, config): @@ -238,6 +239,7 @@ def recording_to_metadata(node, track, config): add_isrcs_to_metadata(nodes[0], m) elif name == 'user_rating': m['~rating'] = nodes[0].text + m['~length'] = format_time(m.length) def medium_to_metadata(node, m): diff --git a/picard/resources.py b/picard/resources.py index e8df3bcd30..d66155f110 100644 --- a/picard/resources.py +++ b/picard/resources.py @@ -2,8 +2,8 @@ # Resource object code # -# Created: Thu Sep 15 13:39:32 2011 -# by: The Resource Compiler for PyQt (Qt v4.7.2) +# Created: Sat Dec 17 14:09:48 2011 +# by: The Resource Compiler for PyQt (Qt v4.7.4) # # WARNING! All changes made in this file will be lost! @@ -36,6 +36,123 @@ \x9b\x3c\x71\x95\x28\x7f\xf3\x9d\xed\x85\x7d\x96\xfa\xc9\xc9\x79\ \xf7\x20\x68\x4d\x1f\x7a\xed\x1f\x2d\x49\x58\xba\x4e\x19\x3c\x81\ \x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x07\x22\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xff\x61\ +\x00\x00\x03\x1e\x69\x43\x43\x50\x49\x43\x43\x20\x50\x72\x6f\x66\ +\x69\x6c\x65\x00\x00\x78\x01\x85\x54\xdf\x6b\xd3\x50\x14\xfe\xda\ +\x65\x9d\xb0\xe1\x8b\x3a\x67\x11\x09\x3e\x68\x91\x6e\x64\x53\x74\ +\x43\x9c\xb6\x6b\x57\xba\xcd\x5a\xea\x36\xb7\x21\x48\x9b\xa6\x6d\ +\x5c\x9a\xc6\x24\xed\x7e\xb0\x07\xd9\x8b\x6f\x3a\xc5\x77\xf1\x07\ +\x3e\xf9\x07\x0c\xd9\x83\x6f\x7b\x92\x0d\xc6\x14\x61\xf8\xac\x88\ +\x22\x4c\xf6\x22\xb3\x9e\x9b\x34\x4d\x27\x53\x03\xb9\xf7\xbb\xdf\ +\xf9\xee\x39\x27\xe7\xe4\x5e\xa0\xf9\x71\x5a\xd3\x14\x2f\x0f\x14\ +\x55\x53\x4f\xc5\xc2\xfc\xc4\xe4\x14\xdf\xf2\x01\x5e\x1c\x43\x2b\ +\xfc\x68\x4d\x8b\x86\x16\x4a\x26\x47\x40\x0f\xd3\xb2\x79\xef\xb3\ +\xf3\x0e\x1e\xc6\x6c\x74\xee\x6f\xdf\xab\xfe\x63\xd5\x9a\x95\x0c\ +\x11\xf0\x1c\x20\xbe\x94\x35\xc4\x22\xe1\x59\xa0\x69\x5c\xd4\x74\ +\x13\xe0\xd6\x89\xef\x9d\x31\x35\xc2\xcd\x4c\x73\x58\xa7\x04\x09\ +\x1f\x67\x38\x6f\x63\x81\xe1\x8c\x8d\x23\x96\x66\x34\x35\x40\x9a\ +\x09\xc2\x07\xc5\x42\x3a\x4b\xb8\x40\x38\x98\x69\xe0\xf3\x0d\xd8\ +\xce\x81\x14\xe4\x27\x26\xa9\x92\x2e\x8b\x3c\xab\x45\x52\x2f\xe5\ +\x64\x45\xb2\x0c\xf6\xf0\x1f\x73\x83\xf2\x5f\xb0\xa8\x94\xe9\x9b\ +\xad\xe7\x10\x8d\x6d\x9a\x19\x4e\xd1\x7c\x8a\xde\x1f\x39\x7d\x70\ +\x8c\xe6\x00\xd5\xc1\x3f\x5f\x18\xbd\x41\xb8\x9d\x70\x58\x36\xe3\ +\xa3\x35\x7e\x42\xcd\x24\xae\x11\x26\xbd\xe7\xee\x74\x69\x98\xed\ +\x65\x9a\x97\x59\x29\x12\x25\x1c\x24\xbc\x62\x54\xae\x33\x6c\x69\ +\xe6\x0b\x03\x89\x9a\xe6\xd3\xed\xf4\x50\x92\xb0\x9f\x34\xbf\x34\ +\x33\x59\xf3\xe3\xed\x50\x95\x04\xeb\x31\xc5\xf5\xf6\x4b\x46\xf4\ +\xba\xbd\xd7\xdb\x91\x93\x07\xe3\x35\x3e\xa7\x29\xd6\x7f\x40\xfe\ +\xbd\xf7\xf5\x72\x8a\xe5\x79\x92\xf0\xeb\xb4\x1e\x8d\xd5\xf4\x5b\ +\x92\x3a\x56\xdb\xdb\xe4\xcd\xa6\x23\xc3\xc4\x77\x51\x3f\x03\x48\ +\x42\x82\x8e\x1c\x64\x28\xe0\x91\x42\x0c\x61\x9a\x63\xc4\xaa\xf8\ +\x4c\x16\x19\x22\x4a\xa4\xd2\x69\x74\x54\x79\xb2\x38\xd6\x3b\x28\ +\x93\x96\xed\x1c\x47\x78\xc9\x5f\x0e\xb8\x5e\x16\xf5\x5b\xb2\xb8\ +\xf6\xe0\xfb\x9e\xdd\x25\xd7\x8e\xbc\x15\x85\xc5\xb7\xa3\xd8\x51\ +\xed\xb5\x81\xe9\xba\xb2\x13\x9a\x1b\x7f\x75\x61\xa5\xa3\x6e\xe1\ +\x37\xb9\xe5\x9b\x1b\x6d\xab\x0b\x08\x51\xfe\x8a\xe5\xb1\x48\x5e\ +\x65\xca\x4f\x82\x51\xd7\x75\x36\xe6\x90\x53\x97\xfc\x75\x0b\xcf\ +\x32\x94\xee\x25\x76\x12\x58\x0c\xba\xac\xf0\x5e\xf8\x2a\x6c\x0a\ +\x4f\x85\x17\xc2\x97\xbf\xd4\xc8\xce\xde\xad\x11\xcb\x80\x71\x2c\ +\x3e\xab\x9e\x53\xcd\xc6\xec\x25\xd2\x4c\xd2\xeb\x64\xb8\xbf\x8a\ +\xf5\x42\xc6\x18\xf9\x90\x31\x43\x5a\x9d\xbe\x24\x4d\x9c\x8a\x39\ +\xf2\xda\x50\x0b\x27\x06\x77\x82\xeb\xe6\xe2\x5c\x2f\xd7\x07\x9e\ +\xbb\xcc\x5d\xe1\xfa\xb9\x08\xad\x2e\x72\x23\x8e\xc2\x17\xf5\x45\ +\x7c\x21\xf0\xbe\x33\xbe\x3e\x5f\xb7\x6f\x88\x61\xa7\xdb\xbe\xd3\ +\x64\xeb\xa3\x31\x5a\xeb\xbb\xd3\x91\xba\xa2\xb1\x7a\x94\x8f\xdb\ +\x27\xf6\x3d\x8e\xaa\x13\x19\xb2\xb1\xbe\xb1\x7e\x56\x08\x2b\xb4\ +\xa2\x63\x6a\x4a\xb3\x74\x4f\x00\x03\x25\x6d\x4e\x97\xf3\x05\x93\ +\xef\x11\x84\x0b\x7c\x88\xae\x2d\x89\x8f\xab\x62\x57\x90\x4f\x2b\ +\x0a\x6f\x99\x0c\x5e\x97\x0c\x49\xaf\x48\xd9\x2e\xb0\x3b\x8f\xed\ +\x03\xb6\x53\xd6\x5d\xe6\x69\x5f\x73\x39\xf3\x2a\x70\xe9\x1b\xfd\ +\xc3\xeb\x2e\x37\x55\x06\x5e\x19\xc0\xd1\x73\x2e\x17\xa0\x33\x75\ +\xe4\x09\xb0\x7c\x5e\x2c\xeb\x15\xdb\x1f\x3c\x9e\xb7\x80\x91\x3b\ +\xdb\x63\xad\x3d\x6d\x61\xba\x8b\x3e\x56\xab\xdb\x74\x2e\x5b\x1e\ +\x01\xbb\x0f\xab\xd5\x9f\xcf\xaa\xd5\xdd\xe7\xe4\x7f\x0b\x78\xa3\ +\xfc\x06\xa9\x23\x0a\xd6\xc2\xa1\x5f\x32\x00\x00\x00\x09\x70\x48\ +\x59\x73\x00\x00\x04\x9d\x00\x00\x04\x9d\x01\x7c\x34\x6b\xa1\x00\ +\x00\x01\x64\x69\x54\x58\x74\x58\x4d\x4c\x3a\x63\x6f\x6d\x2e\x61\ +\x64\x6f\x62\x65\x2e\x78\x6d\x70\x00\x00\x00\x00\x00\x3c\x78\x3a\ +\x78\x6d\x70\x6d\x65\x74\x61\x20\x78\x6d\x6c\x6e\x73\x3a\x78\x3d\ +\x22\x61\x64\x6f\x62\x65\x3a\x6e\x73\x3a\x6d\x65\x74\x61\x2f\x22\ +\x20\x78\x3a\x78\x6d\x70\x74\x6b\x3d\x22\x58\x4d\x50\x20\x43\x6f\ +\x72\x65\x20\x34\x2e\x34\x2e\x30\x22\x3e\x0a\x20\x20\x20\x3c\x72\ +\x64\x66\x3a\x52\x44\x46\x20\x78\x6d\x6c\x6e\x73\x3a\x72\x64\x66\ +\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\ +\x6f\x72\x67\x2f\x31\x39\x39\x39\x2f\x30\x32\x2f\x32\x32\x2d\x72\ +\x64\x66\x2d\x73\x79\x6e\x74\x61\x78\x2d\x6e\x73\x23\x22\x3e\x0a\ +\x20\x20\x20\x20\x20\x20\x3c\x72\x64\x66\x3a\x44\x65\x73\x63\x72\ +\x69\x70\x74\x69\x6f\x6e\x20\x72\x64\x66\x3a\x61\x62\x6f\x75\x74\ +\x3d\x22\x22\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\ +\x78\x6d\x6c\x6e\x73\x3a\x78\x6d\x70\x3d\x22\x68\x74\x74\x70\x3a\ +\x2f\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x78\ +\x61\x70\x2f\x31\x2e\x30\x2f\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\ +\x20\x20\x20\x3c\x78\x6d\x70\x3a\x43\x72\x65\x61\x74\x6f\x72\x54\ +\x6f\x6f\x6c\x3e\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\x65\ +\x2e\x6f\x72\x67\x3c\x2f\x78\x6d\x70\x3a\x43\x72\x65\x61\x74\x6f\ +\x72\x54\x6f\x6f\x6c\x3e\x0a\x20\x20\x20\x20\x20\x20\x3c\x2f\x72\ +\x64\x66\x3a\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\ +\x20\x20\x20\x3c\x2f\x72\x64\x66\x3a\x52\x44\x46\x3e\x0a\x3c\x2f\ +\x78\x3a\x78\x6d\x70\x6d\x65\x74\x61\x3e\x0a\xad\x12\xec\x34\x00\ +\x00\x02\x3a\x49\x44\x41\x54\x38\x11\xa5\x52\xcf\x6b\x13\x41\x14\ +\x7e\xf3\x63\xb3\x9b\x34\xbf\xda\x34\x39\xd8\x42\xa1\x92\x56\x6d\ +\x69\x63\x44\xa9\x91\x50\x44\x2f\x1a\x28\x52\xaa\x3d\x88\xf7\x9a\ +\x93\x9e\xfc\x1f\x14\x4f\xa5\xe8\xc1\x73\x4b\x23\x55\x04\x45\xec\ +\x45\x2a\x08\x16\x63\x52\x53\x45\x8a\x54\x4b\x3d\x08\x26\x4d\x62\ +\xd3\x68\xb2\x99\x9d\x75\x26\xb0\x25\xc1\x26\x08\xbe\x65\x78\x6f\ +\xde\x7e\xdf\xb7\xef\xdb\x19\x64\x9a\x26\xfc\x4f\xe0\x76\xe4\xb7\ +\x0b\x23\xe3\x72\xb5\xc3\xd0\x76\x2f\x83\xe1\x2b\xb3\x60\x8a\x07\ +\x60\xb4\x25\x4e\x5a\x38\x68\xa5\x13\xa1\x18\x2f\x2e\x33\x5e\x7c\ +\xc6\x64\x7d\x10\x46\xf6\x5a\x5a\xe8\x1b\x9e\xb8\x83\x78\x91\x80\ +\x58\xbd\xc7\x2e\xdc\x6e\x35\x01\x92\x2a\x56\x3c\x9f\x0d\xfa\x5d\ +\x0a\x1d\xb2\x77\x75\x4c\x86\xce\xdd\x8c\x83\x91\x13\x02\x3a\x70\ +\xe4\x31\x3e\xac\xdc\x9f\x2b\x15\x2a\x4b\x25\x4e\xd7\x63\xd7\x33\ +\x05\x8b\x83\x56\xe6\x82\x47\x83\x63\x17\x1f\x77\x06\x8e\xf8\x08\ +\xa6\x4e\xa2\xa9\x1a\x46\x18\x6a\x95\x6f\x60\x1a\x65\x61\x4f\x07\ +\x40\x00\x54\x09\x00\x37\x74\x60\x7a\xa5\x6c\x30\xf6\xb3\xf8\x63\ +\x7b\xe7\xcb\xda\xea\x74\x7d\x82\x8d\xa7\x67\x67\x0e\x9f\xba\x76\ +\x97\xa0\x5d\x07\x17\x24\x83\xfd\x02\xce\x04\x99\x57\x84\x88\x0e\ +\x08\x71\x51\xef\x02\x11\x99\x2a\x2a\x30\x08\x96\xb6\x32\x2f\x6f\ +\x0d\x4e\xbc\xbe\xb7\x6f\x61\xe3\xc9\xd8\x54\x5f\x78\xea\x81\x82\ +\xb3\x1e\x56\xdb\xab\x8b\x18\xb5\x32\x70\x29\xc2\xf2\x80\x4c\x03\ +\xa8\xcd\x06\x06\x1e\xc8\x7f\x5d\x4f\xc5\x47\x2f\xbf\x5b\x94\x36\ +\xf6\x05\xe4\x26\x3d\x3f\x72\x7e\x30\x72\x69\x91\xf0\xad\x2e\x26\ +\xa6\xa8\x55\x4b\x60\x54\x0b\xd2\x81\xf8\x32\x01\xb0\x0d\xe5\xb6\ +\x33\xc9\xab\xc3\xd3\x6b\xcb\x12\x2f\xa3\x49\x40\x36\x0a\x99\x99\ +\x4d\x55\xd9\xec\x67\x35\x61\xc3\x60\x62\x74\x00\x4c\x10\x60\x4c\ +\xe0\x77\xb5\xe7\x73\xe0\x64\x62\x40\xe2\xac\x68\x3a\x46\x71\x0a\ +\x6e\x8c\x4a\xdd\x18\x74\xe1\x17\x81\x62\xb3\x83\x4d\xd3\x80\x12\ +\x0a\x84\x12\x50\x54\xe4\x97\x18\x8b\x2c\x73\xd3\x4d\xf4\xb9\xb5\ +\xa8\xc3\x65\x77\x03\x73\x80\x49\xfc\x95\x7c\x76\x2f\x69\x22\x44\ +\x7c\xdd\xae\x30\xc5\xdf\x55\xa2\x69\x5e\x9f\x53\x3d\x2d\x78\x2f\ +\x2c\x91\xa6\x09\x3c\x01\x6f\x14\xd3\x43\x7a\x2e\xef\x7e\xf5\x69\ +\x35\x15\xeb\x39\xf3\x28\xda\x1b\x59\x8a\x7c\x4c\xa6\x26\xb3\x3b\ +\x9d\x6f\x88\xda\x5f\x73\x7a\x3b\xa2\x16\xb9\x9e\x1b\xaf\xe8\xfb\ +\x87\x27\xe2\x99\xc4\xf1\xf1\xc6\x5e\x63\x9d\x5e\x08\xc5\xd2\xf3\ +\xa1\x1b\x8d\xbd\xbf\x7e\x62\x93\xfa\x3f\x6c\xfe\x00\x30\x2a\x2f\ +\x9b\x6a\x8a\xe5\x15\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\ +\x82\ \x00\x00\x40\x8c\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -1084,6 +1201,120 @@ \x01\x03\x83\x2f\x12\x7b\x33\x71\x56\x8c\x2a\xc0\x12\xd4\xb0\xd0\ \x43\x06\x8c\x14\xc7\x26\x00\xbe\x5b\x05\xd6\xa8\x8e\xff\x70\x00\ \x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x06\xf4\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xff\x61\ +\x00\x00\x03\x1e\x69\x43\x43\x50\x49\x43\x43\x20\x50\x72\x6f\x66\ +\x69\x6c\x65\x00\x00\x78\x01\x85\x54\xdf\x6b\xd3\x50\x14\xfe\xda\ +\x65\x9d\xb0\xe1\x8b\x3a\x67\x11\x09\x3e\x68\x91\x6e\x64\x53\x74\ +\x43\x9c\xb6\x6b\x57\xba\xcd\x5a\xea\x36\xb7\x21\x48\x9b\xa6\x6d\ +\x5c\x9a\xc6\x24\xed\x7e\xb0\x07\xd9\x8b\x6f\x3a\xc5\x77\xf1\x07\ +\x3e\xf9\x07\x0c\xd9\x83\x6f\x7b\x92\x0d\xc6\x14\x61\xf8\xac\x88\ +\x22\x4c\xf6\x22\xb3\x9e\x9b\x34\x4d\x27\x53\x03\xb9\xf7\xbb\xdf\ +\xf9\xee\x39\x27\xe7\xe4\x5e\xa0\xf9\x71\x5a\xd3\x14\x2f\x0f\x14\ +\x55\x53\x4f\xc5\xc2\xfc\xc4\xe4\x14\xdf\xf2\x01\x5e\x1c\x43\x2b\ +\xfc\x68\x4d\x8b\x86\x16\x4a\x26\x47\x40\x0f\xd3\xb2\x79\xef\xb3\ +\xf3\x0e\x1e\xc6\x6c\x74\xee\x6f\xdf\xab\xfe\x63\xd5\x9a\x95\x0c\ +\x11\xf0\x1c\x20\xbe\x94\x35\xc4\x22\xe1\x59\xa0\x69\x5c\xd4\x74\ +\x13\xe0\xd6\x89\xef\x9d\x31\x35\xc2\xcd\x4c\x73\x58\xa7\x04\x09\ +\x1f\x67\x38\x6f\x63\x81\xe1\x8c\x8d\x23\x96\x66\x34\x35\x40\x9a\ +\x09\xc2\x07\xc5\x42\x3a\x4b\xb8\x40\x38\x98\x69\xe0\xf3\x0d\xd8\ +\xce\x81\x14\xe4\x27\x26\xa9\x92\x2e\x8b\x3c\xab\x45\x52\x2f\xe5\ +\x64\x45\xb2\x0c\xf6\xf0\x1f\x73\x83\xf2\x5f\xb0\xa8\x94\xe9\x9b\ +\xad\xe7\x10\x8d\x6d\x9a\x19\x4e\xd1\x7c\x8a\xde\x1f\x39\x7d\x70\ +\x8c\xe6\x00\xd5\xc1\x3f\x5f\x18\xbd\x41\xb8\x9d\x70\x58\x36\xe3\ +\xa3\x35\x7e\x42\xcd\x24\xae\x11\x26\xbd\xe7\xee\x74\x69\x98\xed\ +\x65\x9a\x97\x59\x29\x12\x25\x1c\x24\xbc\x62\x54\xae\x33\x6c\x69\ +\xe6\x0b\x03\x89\x9a\xe6\xd3\xed\xf4\x50\x92\xb0\x9f\x34\xbf\x34\ +\x33\x59\xf3\xe3\xed\x50\x95\x04\xeb\x31\xc5\xf5\xf6\x4b\x46\xf4\ +\xba\xbd\xd7\xdb\x91\x93\x07\xe3\x35\x3e\xa7\x29\xd6\x7f\x40\xfe\ +\xbd\xf7\xf5\x72\x8a\xe5\x79\x92\xf0\xeb\xb4\x1e\x8d\xd5\xf4\x5b\ +\x92\x3a\x56\xdb\xdb\xe4\xcd\xa6\x23\xc3\xc4\x77\x51\x3f\x03\x48\ +\x42\x82\x8e\x1c\x64\x28\xe0\x91\x42\x0c\x61\x9a\x63\xc4\xaa\xf8\ +\x4c\x16\x19\x22\x4a\xa4\xd2\x69\x74\x54\x79\xb2\x38\xd6\x3b\x28\ +\x93\x96\xed\x1c\x47\x78\xc9\x5f\x0e\xb8\x5e\x16\xf5\x5b\xb2\xb8\ +\xf6\xe0\xfb\x9e\xdd\x25\xd7\x8e\xbc\x15\x85\xc5\xb7\xa3\xd8\x51\ +\xed\xb5\x81\xe9\xba\xb2\x13\x9a\x1b\x7f\x75\x61\xa5\xa3\x6e\xe1\ +\x37\xb9\xe5\x9b\x1b\x6d\xab\x0b\x08\x51\xfe\x8a\xe5\xb1\x48\x5e\ +\x65\xca\x4f\x82\x51\xd7\x75\x36\xe6\x90\x53\x97\xfc\x75\x0b\xcf\ +\x32\x94\xee\x25\x76\x12\x58\x0c\xba\xac\xf0\x5e\xf8\x2a\x6c\x0a\ +\x4f\x85\x17\xc2\x97\xbf\xd4\xc8\xce\xde\xad\x11\xcb\x80\x71\x2c\ +\x3e\xab\x9e\x53\xcd\xc6\xec\x25\xd2\x4c\xd2\xeb\x64\xb8\xbf\x8a\ +\xf5\x42\xc6\x18\xf9\x90\x31\x43\x5a\x9d\xbe\x24\x4d\x9c\x8a\x39\ +\xf2\xda\x50\x0b\x27\x06\x77\x82\xeb\xe6\xe2\x5c\x2f\xd7\x07\x9e\ +\xbb\xcc\x5d\xe1\xfa\xb9\x08\xad\x2e\x72\x23\x8e\xc2\x17\xf5\x45\ +\x7c\x21\xf0\xbe\x33\xbe\x3e\x5f\xb7\x6f\x88\x61\xa7\xdb\xbe\xd3\ +\x64\xeb\xa3\x31\x5a\xeb\xbb\xd3\x91\xba\xa2\xb1\x7a\x94\x8f\xdb\ +\x27\xf6\x3d\x8e\xaa\x13\x19\xb2\xb1\xbe\xb1\x7e\x56\x08\x2b\xb4\ +\xa2\x63\x6a\x4a\xb3\x74\x4f\x00\x03\x25\x6d\x4e\x97\xf3\x05\x93\ +\xef\x11\x84\x0b\x7c\x88\xae\x2d\x89\x8f\xab\x62\x57\x90\x4f\x2b\ +\x0a\x6f\x99\x0c\x5e\x97\x0c\x49\xaf\x48\xd9\x2e\xb0\x3b\x8f\xed\ +\x03\xb6\x53\xd6\x5d\xe6\x69\x5f\x73\x39\xf3\x2a\x70\xe9\x1b\xfd\ +\xc3\xeb\x2e\x37\x55\x06\x5e\x19\xc0\xd1\x73\x2e\x17\xa0\x33\x75\ +\xe4\x09\xb0\x7c\x5e\x2c\xeb\x15\xdb\x1f\x3c\x9e\xb7\x80\x91\x3b\ +\xdb\x63\xad\x3d\x6d\x61\xba\x8b\x3e\x56\xab\xdb\x74\x2e\x5b\x1e\ +\x01\xbb\x0f\xab\xd5\x9f\xcf\xaa\xd5\xdd\xe7\xe4\x7f\x0b\x78\xa3\ +\xfc\x06\xa9\x23\x0a\xd6\xc2\xa1\x5f\x32\x00\x00\x00\x09\x70\x48\ +\x59\x73\x00\x00\x04\x9d\x00\x00\x04\x9d\x01\x7c\x34\x6b\xa1\x00\ +\x00\x01\x64\x69\x54\x58\x74\x58\x4d\x4c\x3a\x63\x6f\x6d\x2e\x61\ +\x64\x6f\x62\x65\x2e\x78\x6d\x70\x00\x00\x00\x00\x00\x3c\x78\x3a\ +\x78\x6d\x70\x6d\x65\x74\x61\x20\x78\x6d\x6c\x6e\x73\x3a\x78\x3d\ +\x22\x61\x64\x6f\x62\x65\x3a\x6e\x73\x3a\x6d\x65\x74\x61\x2f\x22\ +\x20\x78\x3a\x78\x6d\x70\x74\x6b\x3d\x22\x58\x4d\x50\x20\x43\x6f\ +\x72\x65\x20\x34\x2e\x34\x2e\x30\x22\x3e\x0a\x20\x20\x20\x3c\x72\ +\x64\x66\x3a\x52\x44\x46\x20\x78\x6d\x6c\x6e\x73\x3a\x72\x64\x66\ +\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\ +\x6f\x72\x67\x2f\x31\x39\x39\x39\x2f\x30\x32\x2f\x32\x32\x2d\x72\ +\x64\x66\x2d\x73\x79\x6e\x74\x61\x78\x2d\x6e\x73\x23\x22\x3e\x0a\ +\x20\x20\x20\x20\x20\x20\x3c\x72\x64\x66\x3a\x44\x65\x73\x63\x72\ +\x69\x70\x74\x69\x6f\x6e\x20\x72\x64\x66\x3a\x61\x62\x6f\x75\x74\ +\x3d\x22\x22\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\ +\x78\x6d\x6c\x6e\x73\x3a\x78\x6d\x70\x3d\x22\x68\x74\x74\x70\x3a\ +\x2f\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x78\ +\x61\x70\x2f\x31\x2e\x30\x2f\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\ +\x20\x20\x20\x3c\x78\x6d\x70\x3a\x43\x72\x65\x61\x74\x6f\x72\x54\ +\x6f\x6f\x6c\x3e\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\x65\ +\x2e\x6f\x72\x67\x3c\x2f\x78\x6d\x70\x3a\x43\x72\x65\x61\x74\x6f\ +\x72\x54\x6f\x6f\x6c\x3e\x0a\x20\x20\x20\x20\x20\x20\x3c\x2f\x72\ +\x64\x66\x3a\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\ +\x20\x20\x20\x3c\x2f\x72\x64\x66\x3a\x52\x44\x46\x3e\x0a\x3c\x2f\ +\x78\x3a\x78\x6d\x70\x6d\x65\x74\x61\x3e\x0a\xad\x12\xec\x34\x00\ +\x00\x02\x0c\x49\x44\x41\x54\x38\x11\xa5\x52\xbf\xab\xda\x50\x14\ +\x3e\xc9\x8d\x26\x31\xf5\x3d\xf5\x35\xab\x43\x87\x82\xed\xd0\xc1\ +\xc5\x16\xe4\x81\x54\x10\x5e\x5d\x1e\xd5\x82\xfd\x0f\x1c\xc4\xc5\ +\xd5\x49\xf0\x47\x1d\x84\xd2\xad\xff\xc0\x1b\xa4\xa3\xd0\x14\x87\ +\x52\x51\x5c\x8b\x53\xe9\x16\x71\x30\x6d\x29\xea\x7b\xb9\x49\x8c\ +\xbd\x47\xf0\xf1\x42\x55\x0a\x3d\x70\xc8\xbd\x39\xdf\xf7\x9d\x5f\ +\x97\xdb\x6c\x36\xf0\x3f\xc6\x1f\x23\x57\xab\xd5\x73\xf4\x63\x18\ +\xe1\x58\x30\x9d\x4e\xbf\x65\x15\x62\x89\x4f\x0e\xe2\x30\xbe\xcf\ +\x1b\x8d\xc6\x85\xae\xeb\x0e\x3a\x9e\xf7\x61\xf0\xdf\xc1\x16\x12\ +\x89\xc4\x1b\x06\x20\xae\xeb\x92\x78\x3c\xde\x3a\x54\x01\x87\x2a\ +\x3b\x2b\x95\x4a\xaa\x28\x8a\x8f\xc3\xe1\xf0\x65\xa1\x50\x28\x5a\ +\x96\x45\xb6\x59\x78\x7e\xdd\xed\x76\xdf\x19\x86\xd1\xf5\xf9\x7c\ +\x5f\xeb\xf5\xfa\xaf\x1d\x87\x2b\x97\xcb\x31\xd6\xeb\x87\x68\x34\ +\x7a\x46\x08\xb9\x27\x07\x64\x89\xf0\x04\x16\x8b\x05\x38\x6b\x07\ +\xdc\xb5\x0b\x3c\xcf\x83\x2c\xcb\xc0\x04\x81\x52\xba\xb2\xa8\xf5\ +\x5b\x9f\xea\x3f\x06\x83\xc1\x2b\x32\x1c\x0e\x8d\x5e\xaf\xe7\xc4\ +\x1e\xc5\x5e\x08\x44\x90\xa9\x49\x61\xb5\x5a\x81\x69\x9a\x40\x2d\ +\xba\x25\xd9\xb6\x0d\xcb\xe5\x12\xc9\xc0\x92\xf8\x99\xc1\x64\x32\ +\xa9\xb3\x0d\xf5\x6e\x5b\xe8\x74\x3a\x2f\x53\xa9\xd4\x7b\x8e\x70\ +\xa7\xf4\x86\x02\x92\x58\x26\x70\x5c\x67\x7b\x06\xd6\xa9\x24\x49\ +\x28\xf0\x73\x34\x1a\x15\x2b\x95\xca\x15\xb6\x71\x2b\x80\x97\x5a\ +\xad\xf6\x3c\x93\xc9\x5c\xb1\xc1\x45\xa8\x4d\x01\x85\x6e\xae\xaf\ +\x19\x8a\x03\xd1\x2f\x82\x28\x89\xc6\x78\x3c\x7e\xcd\xc8\x1f\x11\ +\x8f\xe6\x11\xc0\x1f\xda\x27\xed\xbb\xdf\xe7\x7f\x80\xe5\xda\x6b\ +\x7b\xbb\x62\x81\x17\x30\x33\xb6\xf5\x2d\x9b\xcd\x3e\x44\xdc\xce\ +\x3c\x6b\x64\x5b\x38\xe1\x80\xbb\x8f\x41\x24\x04\xa4\x00\x28\xb2\ +\x02\x82\x5f\x00\x36\x7d\x90\x64\x49\x45\xcc\x8e\x8c\x5f\x8f\x40\ +\x30\x18\x4c\x46\xce\x22\x27\x6c\x95\x38\x75\x73\x3e\x9f\x7f\x61\ +\xab\x1b\x32\x21\x8a\x5b\x08\x9d\x86\x42\x0c\xf3\xf4\xa0\x00\x5b\ +\x65\x92\x91\xad\xd9\x6c\xf6\x59\xd3\xb4\x8b\x7c\x3e\x9f\xcc\xe5\ +\x72\xcf\xfa\xfd\xfe\xe5\x74\x3a\x1d\x29\x8a\x62\xab\xaa\x9a\xbc\ +\x2b\xe0\x79\xc6\xed\x76\xbb\xd8\x6a\xb5\xce\xf1\xf1\xec\x73\x7c\ +\xd2\xcd\x66\xb3\x7c\x37\xf6\xd7\x10\x3d\xea\xff\x70\xf9\x03\x24\ +\xeb\x32\x12\xe4\xb3\x60\xa5\x00\x00\x00\x00\x49\x45\x4e\x44\xae\ +\x42\x60\x82\ \x00\x00\x00\xbc\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -8530,6 +8761,10 @@ \x00\xe0\x1d\x47\ \x00\x66\ \x00\x69\x00\x6c\x00\x65\x00\x2d\x00\x70\x00\x65\x00\x6e\x00\x64\x00\x69\x00\x6e\x00\x67\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x08\ +\x0a\x85\x58\x07\ +\x00\x73\ +\x00\x74\x00\x61\x00\x72\x00\x2e\x00\x70\x00\x6e\x00\x67\ \x00\x12\ \x08\xc2\x5a\x27\ \x00\x43\ @@ -8556,6 +8791,10 @@ \x00\x37\xfb\x78\ \x00\x34\ \x00\x38\x00\x78\x00\x34\x00\x38\ +\x00\x0d\ +\x0d\xdc\xf1\x67\ +\x00\x73\ +\x00\x74\x00\x61\x00\x72\x00\x2d\x00\x67\x00\x72\x00\x61\x00\x79\x00\x2e\x00\x70\x00\x6e\x00\x67\ \x00\x0c\ \x0e\xf2\x04\x27\ \x00\x6d\ @@ -8717,61 +8956,63 @@ qt_resource_struct = "\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x18\x00\x00\x00\x02\ -\x00\x00\x01\xf4\x00\x00\x00\x00\x00\x01\x00\x00\x48\xfb\ -\x00\x00\x02\x56\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x2c\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x1f\ -\x00\x00\x00\x12\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1e\ -\x00\x00\x00\xca\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1d\ -\x00\x00\x00\xda\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1c\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1a\x00\x00\x00\x02\ +\x00\x00\x02\x2a\x00\x00\x00\x00\x00\x01\x00\x00\x57\x19\ +\x00\x00\x02\x8c\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x2e\ +\x00\x00\x01\xce\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x21\ +\x00\x00\x00\x12\x00\x02\x00\x00\x00\x01\x00\x00\x00\x20\ +\x00\x00\x00\xe0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1f\ +\x00\x00\x00\xf0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1e\ \x00\x00\x00\x22\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x02\xa8\x00\x00\x00\x00\x00\x01\x00\x00\x4e\x9f\ -\x00\x00\x02\x84\x00\x00\x00\x00\x00\x01\x00\x00\x4d\xcc\ -\x00\x00\x00\x72\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1b\ -\x00\x00\x00\xb6\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1a\ -\x00\x00\x01\x82\x00\x00\x00\x00\x00\x01\x00\x00\x45\xd8\ -\x00\x00\x00\x86\x00\x00\x00\x00\x00\x01\x00\x00\x42\x11\ +\x00\x00\x02\xde\x00\x00\x00\x00\x00\x01\x00\x00\x5c\xbd\ +\x00\x00\x02\xba\x00\x00\x00\x00\x00\x01\x00\x00\x5b\xea\ +\x00\x00\x00\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1d\ +\x00\x00\x00\xcc\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1c\ +\x00\x00\x01\xb8\x00\x00\x00\x00\x00\x01\x00\x00\x53\xf6\ +\x00\x00\x00\x9c\x00\x00\x00\x00\x00\x01\x00\x00\x49\x37\ +\x00\x00\x00\x5e\x00\x00\x00\x00\x00\x01\x00\x00\x08\xa7\ +\x00\x00\x02\x5e\x00\x00\x00\x00\x00\x01\x00\x00\x5a\x50\ \x00\x00\x00\x48\x00\x00\x00\x00\x00\x01\x00\x00\x01\x81\ -\x00\x00\x02\x28\x00\x00\x00\x00\x00\x01\x00\x00\x4c\x32\ -\x00\x00\x01\xc6\x00\x00\x00\x00\x00\x01\x00\x00\x48\x3f\ -\x00\x00\x01\x54\x00\x00\x00\x00\x00\x01\x00\x00\x45\x0d\ -\x00\x00\x01\x08\x00\x00\x00\x00\x00\x01\x00\x00\x43\x81\ -\x00\x00\x02\xc8\x00\x00\x00\x00\x00\x01\x00\x00\x4f\x54\ -\x00\x00\x02\x66\x00\x00\x00\x00\x00\x01\x00\x00\x4c\xfe\ -\x00\x00\x02\x0a\x00\x00\x00\x00\x00\x01\x00\x00\x4b\x60\ -\x00\x00\x01\xa8\x00\x00\x00\x00\x00\x01\x00\x00\x47\x7f\ -\x00\x00\x01\x36\x00\x00\x00\x00\x00\x01\x00\x00\x44\x3a\ -\x00\x00\x00\xea\x00\x00\x00\x00\x00\x01\x00\x00\x42\xc1\ -\x00\x00\x04\x0c\x00\x00\x00\x00\x00\x01\x00\x00\xc3\xa8\ -\x00\x00\x04\x0c\x00\x00\x00\x00\x00\x01\x00\x01\xae\xf5\ -\x00\x00\x04\x0c\x00\x00\x00\x00\x00\x01\x00\x00\xa5\xc8\ -\x00\x00\x04\x0c\x00\x00\x00\x00\x00\x01\x00\x00\xb9\x5b\ -\x00\x00\x04\x0c\x00\x00\x00\x00\x00\x01\x00\x02\x06\x32\ -\x00\x00\x03\x96\x00\x00\x00\x00\x00\x01\x00\x00\x83\x88\ -\x00\x00\x03\x2a\x00\x00\x00\x00\x00\x01\x00\x00\x7b\x6d\ -\x00\x00\x05\x76\x00\x00\x00\x00\x00\x01\x00\x00\x91\x3e\ -\x00\x00\x05\xc2\x00\x00\x00\x00\x00\x01\x00\x00\x99\x0a\ -\x00\x00\x04\xca\x00\x00\x00\x00\x00\x01\x00\x00\x71\xc6\ -\x00\x00\x04\xf2\x00\x00\x00\x00\x00\x01\x00\x00\x78\x15\ -\x00\x00\x03\xd8\x00\x00\x00\x00\x00\x01\x00\x00\x8c\x24\ -\x00\x00\x05\x4c\x00\x00\x00\x00\x00\x01\x00\x00\x8e\x28\ -\x00\x00\x03\xbe\x00\x00\x00\x00\x00\x01\x00\x00\x88\xfd\ -\x00\x00\x04\x26\x00\x00\x00\x00\x00\x01\x00\x00\x9b\xe1\ -\x00\x00\x05\x1e\x00\x00\x00\x00\x00\x01\x00\x00\x7f\x5a\ -\x00\x00\x05\x9a\x00\x00\x00\x00\x00\x01\x00\x00\x94\x13\ -\x00\x00\x04\x7c\x00\x00\x00\x00\x00\x01\x00\x00\xa1\x46\ -\x00\x00\x03\x96\x00\x00\x00\x00\x00\x01\x00\x00\x5a\x44\ -\x00\x00\x03\x2a\x00\x00\x00\x00\x00\x01\x00\x00\x52\x6b\ -\x00\x00\x03\xd8\x00\x00\x00\x00\x00\x01\x00\x00\x5f\xe1\ -\x00\x00\x03\x78\x00\x00\x00\x00\x00\x01\x00\x00\x57\x19\ -\x00\x00\x04\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x6e\x32\ -\x00\x00\x04\x0c\x00\x00\x00\x00\x00\x01\x00\x00\x61\x9d\ -\x00\x00\x03\xbe\x00\x00\x00\x00\x00\x01\x00\x00\x5d\xeb\ -\x00\x00\x04\x5a\x00\x00\x00\x00\x00\x01\x00\x00\x68\x6a\ -\x00\x00\x02\xf6\x00\x00\x00\x00\x00\x01\x00\x00\x50\x1b\ -\x00\x00\x04\x26\x00\x00\x00\x00\x00\x01\x00\x00\x65\x16\ -\x00\x00\x03\x52\x00\x00\x00\x00\x00\x01\x00\x00\x54\x88\ -\x00\x00\x04\x7c\x00\x00\x00\x00\x00\x01\x00\x00\x6a\x9f\ +\x00\x00\x01\xfc\x00\x00\x00\x00\x00\x01\x00\x00\x56\x5d\ +\x00\x00\x01\x8a\x00\x00\x00\x00\x00\x01\x00\x00\x53\x2b\ +\x00\x00\x01\x3e\x00\x00\x00\x00\x00\x01\x00\x00\x51\x9f\ +\x00\x00\x02\xfe\x00\x00\x00\x00\x00\x01\x00\x00\x5d\x72\ +\x00\x00\x01\x00\x00\x00\x00\x00\x00\x01\x00\x00\x49\xe7\ +\x00\x00\x02\x9c\x00\x00\x00\x00\x00\x01\x00\x00\x5b\x1c\ +\x00\x00\x02\x40\x00\x00\x00\x00\x00\x01\x00\x00\x59\x7e\ +\x00\x00\x01\xde\x00\x00\x00\x00\x00\x01\x00\x00\x55\x9d\ +\x00\x00\x01\x6c\x00\x00\x00\x00\x00\x01\x00\x00\x52\x58\ +\x00\x00\x01\x20\x00\x00\x00\x00\x00\x01\x00\x00\x50\xdf\ +\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x00\xd1\xc6\ +\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x01\xbd\x13\ +\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x00\xb3\xe6\ +\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x00\xc7\x79\ +\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x02\x14\x50\ +\x00\x00\x03\xcc\x00\x00\x00\x00\x00\x01\x00\x00\x91\xa6\ +\x00\x00\x03\x60\x00\x00\x00\x00\x00\x01\x00\x00\x89\x8b\ +\x00\x00\x05\xac\x00\x00\x00\x00\x00\x01\x00\x00\x9f\x5c\ +\x00\x00\x05\xf8\x00\x00\x00\x00\x00\x01\x00\x00\xa7\x28\ +\x00\x00\x05\x00\x00\x00\x00\x00\x00\x01\x00\x00\x7f\xe4\ +\x00\x00\x05\x28\x00\x00\x00\x00\x00\x01\x00\x00\x86\x33\ +\x00\x00\x04\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x9a\x42\ +\x00\x00\x05\x82\x00\x00\x00\x00\x00\x01\x00\x00\x9c\x46\ +\x00\x00\x03\xf4\x00\x00\x00\x00\x00\x01\x00\x00\x97\x1b\ +\x00\x00\x04\x5c\x00\x00\x00\x00\x00\x01\x00\x00\xa9\xff\ +\x00\x00\x05\x54\x00\x00\x00\x00\x00\x01\x00\x00\x8d\x78\ +\x00\x00\x05\xd0\x00\x00\x00\x00\x00\x01\x00\x00\xa2\x31\ +\x00\x00\x04\xb2\x00\x00\x00\x00\x00\x01\x00\x00\xaf\x64\ +\x00\x00\x03\xcc\x00\x00\x00\x00\x00\x01\x00\x00\x68\x62\ +\x00\x00\x03\x60\x00\x00\x00\x00\x00\x01\x00\x00\x60\x89\ +\x00\x00\x04\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x6d\xff\ +\x00\x00\x03\xae\x00\x00\x00\x00\x00\x01\x00\x00\x65\x37\ +\x00\x00\x04\xda\x00\x00\x00\x00\x00\x01\x00\x00\x7c\x50\ +\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x00\x6f\xbb\ +\x00\x00\x03\xf4\x00\x00\x00\x00\x00\x01\x00\x00\x6c\x09\ +\x00\x00\x04\x90\x00\x00\x00\x00\x00\x01\x00\x00\x76\x88\ +\x00\x00\x03\x2c\x00\x00\x00\x00\x00\x01\x00\x00\x5e\x39\ +\x00\x00\x04\x5c\x00\x00\x00\x00\x00\x01\x00\x00\x73\x34\ +\x00\x00\x03\x88\x00\x00\x00\x00\x00\x01\x00\x00\x62\xa6\ +\x00\x00\x04\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x78\xbd\ " def qInitResources(): diff --git a/picard/tagger.py b/picard/tagger.py index 0557cf9ecb..79bd14eac4 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -176,19 +176,15 @@ def __init__(self, args, localedir, autoupdate, debug=False): self.puidmanager = PUIDManager() self.acoustidmanager = AcoustIDManager() - self.browser_integration = BrowserIntegration() self.files = {} - self.clusters = ClusterList() self.albums = {} self.mbid_redirects = {} - self.unmatched_files = UnmatchedFiles() - self.window = MainWindow() - self.nats = None + self.window = MainWindow() def setup_gettext(self, localedir): """Setup locales, load translations, install gettext functions.""" @@ -409,12 +405,10 @@ def lookup(self, metadata): def get_files_from_objects(self, objects, save=False): """Return list of files from list of albums, clusters, tracks or files.""" - files = [] + files = set() for obj in objects: - for file in obj.iterfiles(save): - if file not in files: - files.append(file) - return files + files.update(obj.iterfiles(save)) + return list(files) def _file_saved(self, result=None, error=None): if error is None: diff --git a/picard/track.py b/picard/track.py index 6ba56347ef..5f7d215f32 100644 --- a/picard/track.py +++ b/picard/track.py @@ -25,6 +25,7 @@ from picard.mbxml import recording_to_metadata from picard.script import ScriptParser from picard.const import VARIOUS_ARTISTS_ID +from picard.ui.item import Item import traceback @@ -35,7 +36,7 @@ } -class Track(DataObject): +class Track(DataObject, Item): def __init__(self, id, album=None): DataObject.__init__(self, id) @@ -82,9 +83,6 @@ def remove_file(self, file): self.album._remove_file(self, file) self.update() - def update_file(self, file): - self.update() - def update(self): if self.item: self.item.update() @@ -112,33 +110,17 @@ def can_remove(self): def can_edit_tags(self): """Return if this object supports tag editing.""" - for file in self.linked_files: - if file.can_edit_tags(): - return True - return False - - def can_analyze(self): - """Return if this object can be fingerprinted.""" - return False - - def can_autotag(self): - return False + return True - def can_refresh(self): - return False + def can_view_info(self): + return self.num_linked_files > 0 def column(self, column): - if self.num_linked_files == 1: - m = self.linked_files[0].metadata - else: - m = self.metadata + m = self.metadata if column == 'title': prefix = "%s-" % m['discnumber'] if m['totaldiscs'] != "1" else "" return u"%s%s %s" % (prefix, m['tracknumber'].zfill(2), m['title']) - elif column == '~length': - return format_time(m.length) - else: - return m[column] + return m[column] def _customize_metadata(self): tm = self.metadata @@ -199,12 +181,8 @@ def can_refresh(self): return True def column(self, column): - if self.num_linked_files == 1: - m = self.linked_files[0].metadata - else: - m = self.metadata if column == "title": - return m["title"] + return self.metadata["title"] return Track.column(self, column) def load(self): diff --git a/picard/ui/coverartbox.py b/picard/ui/coverartbox.py index 5a568a29ac..ad8b19a28a 100644 --- a/picard/ui/coverartbox.py +++ b/picard/ui/coverartbox.py @@ -23,7 +23,6 @@ from picard.track import Track from picard.file import File from picard.util import webbrowser2, encode_filename -from picard.const import AMAZON_STORE_ASSOCIATE_IDS class ActiveLabel(QtGui.QLabel): @@ -34,6 +33,7 @@ class ActiveLabel(QtGui.QLabel): def __init__(self, active=True, *args): QtGui.QLabel.__init__(self, *args) + self.setMargin(0) self.setActive(active) self.setAcceptDrops(True) @@ -65,21 +65,21 @@ def dropEvent(self, event): class CoverArtBox(QtGui.QGroupBox): def __init__(self, parent): - QtGui.QGroupBox.__init__(self, " ") + QtGui.QGroupBox.__init__(self, "") self.layout = QtGui.QVBoxLayout() self.layout.setSpacing(0) - self.layout.setContentsMargins (7,7,0,0) + self.layout.setContentsMargins(0, 0, 0, 0) # Kills off any borders self.setStyleSheet('''QGroupBox{background-color:none;border:1px;}''') self.setFlat(True) - self.asin = None + self.release = None self.data = None self.item = None self.shadow = QtGui.QPixmap(":/images/CoverArtShadow.png") self.coverArt = ActiveLabel(False, parent) self.coverArt.setPixmap(self.shadow) self.coverArt.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter) - self.coverArt.clicked.connect(self.open_amazon) + self.coverArt.clicked.connect(self.open_release_page) self.coverArt.imageDropped.connect(self.fetch_remote_image) self.layout.addWidget(self.coverArt, 0) self.setLayout(self.layout) @@ -103,7 +103,7 @@ def __set_data(self, data, force=False, pixmap=None): pixmap.loadFromData(self.data[1]) if not pixmap.isNull(): cover = QtGui.QPixmap(self.shadow) - pixmap = pixmap.scaled(121,121 , QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation) + pixmap = pixmap.scaled(121, 121, QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation) painter = QtGui.QPainter(cover) painter.drawPixmap(1, 1, pixmap) painter.end() @@ -115,24 +115,21 @@ def set_metadata(self, metadata, item): if metadata and metadata.images: data = metadata.images[0] self.__set_data(data) + release = None if metadata: - asin = metadata.get("asin", None) + release = metadata.get("musicbrainz_albumid", None) + if release: + self.coverArt.setActive(True) + self.coverArt.setToolTip(_(u"View release on MusicBrainz")) else: - asin = None - if asin != self.asin: - if asin: - self.coverArt.setActive(True) - self.coverArt.setToolTip(_(u"Buy the album on Amazon")) - else: - self.coverArt.setActive(False) - self.coverArt.setToolTip("") - self.asin = asin - - def open_amazon(self): - # TODO: make this configurable - store = "amazon.com" - url = "http://%s/exec/obidos/ASIN/%s/%s?v=glance&s=music" % ( - store, self.asin, AMAZON_STORE_ASSOCIATE_IDS[store]) + self.coverArt.setActive(False) + self.coverArt.setToolTip("") + self.release = release + + def open_release_page(self): + host = self.config.setting["server_host"] + port = self.config.setting["server_port"] + url = "http://%s:%s/release/%s" % (host, port, self.release) webbrowser2.open(url) def fetch_remote_image(self, url): @@ -182,4 +179,3 @@ def load_remote_image(self, mime, data): elif isinstance(self.item, File): file = self.item file.metadata.add_image(mime, data) - diff --git a/picard/ui/infodialog.py b/picard/ui/infodialog.py new file mode 100644 index 0000000000..c2131db66f --- /dev/null +++ b/picard/ui/infodialog.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# +# Picard, the next-generation MusicBrainz tagger +# Copyright (C) 2006 Lukáš Lalinský +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +import os.path +from PyQt4 import QtCore, QtGui +from picard.util import format_time, encode_filename +from picard.ui.util import StandardButton +from picard.ui.ui_infodialog import Ui_InfoDialog + + +class InfoDialog(QtGui.QDialog): + + def __init__(self, objects, parent=None): + QtGui.QDialog.__init__(self, parent) + self.ui = Ui_InfoDialog() + self.ui.setupUi(self) + self.ui.buttonBox.accepted.connect(self.accept) + self.ui.buttonBox.rejected.connect(self.reject) + self.files = self.tagger.get_files_from_objects(objects) + title = _("Info") + " - " + total = len(self.files) + if total == 1: + title += self.files[0].base_filename + else: + title += "%d files" % total + self.setWindowTitle(title) + self.load_info() + + def load_info(self): + total = len(self.files) + if total == 1: + file = self.files[0] + info = [] + info.append((_('Filename:'), file.filename)) + if '~format' in file.orig_metadata: + info.append((_('Format:'), file.orig_metadata['~format'])) + try: + size = os.path.getsize(encode_filename(file.filename)) + if size < 1024: + size = '%d B' % size + elif size < 1024 * 1024: + size = '%0.1f kB' % (size / 1024.0) + else: + size = '%0.1f MB' % (size / 1024.0 / 1024.0) + info.append((_('Size:'), size)) + except: + pass + if file.orig_metadata.length: + info.append((_('Length:'), format_time(file.orig_metadata.length))) + if '~#bitrate' in file.orig_metadata: + info.append((_('Bitrate:'), '%d kbps' % file.orig_metadata['~#bitrate'])) + if '~#sample_rate' in file.orig_metadata: + info.append((_('Sample rate:'), '%d Hz' % file.orig_metadata['~#sample_rate'])) + if '~#bits_per_sample' in file.orig_metadata: + info.append((_('Bits per sample:'), str(file.orig_metadata['~#bits_per_sample']))) + if '~#channels' in file.orig_metadata: + ch = file.orig_metadata['~#channels'] + if ch == 1: ch = _('Mono') + elif ch == 2: ch = _('Stereo') + else: ch = str(ch) + info.append((_('Channels:'), ch)) + text = '
'.join(map(lambda i: '%s
%s' % i, info)) + self.ui.info.setText(text) + else: + self.ui.info.setText(ungettext("%d file", "%d files", total) % total) diff --git a/picard/ui/item.py b/picard/ui/item.py index 4936af0083..263c796825 100644 --- a/picard/ui/item.py +++ b/picard/ui/item.py @@ -42,3 +42,6 @@ def can_autotag(self): def can_refresh(self): """Return if this object can be refreshed.""" return False + + def can_view_info(self): + return False diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py index bae57b9da7..8d46ec2dd8 100644 --- a/picard/ui/itemviews.py +++ b/picard/ui/itemviews.py @@ -28,6 +28,7 @@ from picard.config import Option, TextOption from picard.plugin import ExtensionPoint from picard.const import RELEASE_COUNTRIES +from picard.ui.ratingwidget import RatingWidget class BaseAction(QtGui.QAction): @@ -88,14 +89,14 @@ def __init__(self, window, parent=None): QtGui.QSplitter.__init__(self, parent) self.window = window self.create_icons() - self.views = [FileTreeView(window, self), AlbumTreeView(window, self)] self.views[0].itemSelectionChanged.connect(self.update_selection_0) self.views[1].itemSelectionChanged.connect(self.update_selection_1) self._selected_view = 0 self._ignore_selection_changes = False - TreeItem.window = window + self._selected_objects = set() + TreeItem.selected_metadata_changed = self.tagger.selected_metadata_changed TreeItem.base_color = self.palette().base().color() TreeItem.text_color = self.palette().text().color() TrackItem.track_colors = { @@ -111,6 +112,9 @@ def __init__(self, window, parent=None): File.ERROR: self.config.setting["color_error"], } + def selected_objects(self): + return list(self._selected_objects) + def save_state(self): self.config.persist["splitter_state"] = self.saveState() for view in self.views: @@ -149,13 +153,12 @@ def create_icons(self): ] self.icon_plugins = icontheme.lookup('applications-system', icontheme.ICON_SIZE_MENU) - def selected_objects(self): - return [i.obj for i in self.views[self._selected_view].selectedItems()] - def update_selection(self, i, j): self._selected_view = i self.views[j].clearSelection() - self.window.updateSelection(self.selected_objects()) + self._selected_objects.clear() + self._selected_objects.update(item.obj for item in self.views[i].selectedItems()) + self.window.update_selection(self.selected_objects()) def update_selection_0(self): if not self._ignore_selection_changes: @@ -216,7 +219,7 @@ def contextMenuEvent(self, event): plugin_actions = None menu = QtGui.QMenu(self) if isinstance(obj, Track): - menu.addAction(self.window.edit_tags_action) + menu.addAction(self.window.view_info_action) plugin_actions = list(_track_actions) if obj.num_linked_files == 1: menu.addAction(self.window.open_file_action) @@ -226,6 +229,9 @@ def contextMenuEvent(self, event): if isinstance(obj, NonAlbumTrack): menu.addAction(self.window.refresh_action) elif isinstance(obj, Cluster): + if type(obj) == Cluster: + menu.addAction(self.window.view_info_action) + menu.addSeparator() menu.addAction(self.window.autotag_action) menu.addAction(self.window.analyze_action) if isinstance(obj, UnmatchedFiles): @@ -236,7 +242,7 @@ def contextMenuEvent(self, event): menu.addAction(self.window.analyze_action) plugin_actions = list(_cluster_actions) elif isinstance(obj, File): - menu.addAction(self.window.edit_tags_action) + menu.addAction(self.window.view_info_action) menu.addAction(self.window.open_file_action) menu.addAction(self.window.open_folder_action) menu.addSeparator() @@ -244,6 +250,8 @@ def contextMenuEvent(self, event): menu.addAction(self.window.analyze_action) plugin_actions = list(_file_actions) elif isinstance(obj, Album): + menu.addAction(self.window.view_info_action) + menu.addSeparator() menu.addAction(self.window.refresh_action) plugin_actions = list(_album_actions) @@ -278,6 +286,14 @@ def _add_other_versions(): kwargs = {"release-group": obj.rgid, "limit": 100} self.tagger.xmlws.browse_releases(obj._release_group_request_finished, **kwargs) + if self.config.setting["enable_ratings"] and \ + len(self.window.selected_objects) == 1 and isinstance(obj, Track): + menu.addSeparator() + action = QtGui.QWidgetAction(menu) + action.setDefaultWidget(RatingWidget(menu, obj)) + menu.addAction(action) + menu.addSeparator() + if plugin_actions: plugin_menu = QtGui.QMenu(_("&Plugins"), menu) plugin_menu.addActions(plugin_actions) @@ -449,8 +465,8 @@ def dropMimeData(self, parent, index, data, action): def activate_item(self, index): obj = self.itemFromIndex(index).obj - if obj.can_edit_tags(): - self.window.edit_tags([obj]) + if obj.can_view_info(): + self.window.view_info([obj]) def add_cluster(self, cluster, parent_item=None): if parent_item is None: @@ -477,6 +493,7 @@ def __init__(self, window, parent=None): self.tagger.cluster_removed.connect(self.remove_cluster) def remove_cluster(self, cluster): + cluster.item.setSelected(False) self.clusters.removeChild(cluster.item) @@ -498,6 +515,7 @@ def add_album(self, album): self.add_cluster(album.unmatched_files, item) def remove_album(self, album): + album.item.setSelected(False) self.takeTopLevelItem(self.indexOfTopLevelItem(album.item)) @@ -532,6 +550,8 @@ def update(self): album = self.obj.related_album if self.obj.special and album and album.loaded: album.item.update(update_tracks=False) + if self.isSelected(): + TreeItem.selected_metadata_changed.emit() def add_file(self, file): self.add_files([file]) @@ -548,6 +568,7 @@ def add_files(self, files): self.addChildren(items) def remove_file(self, file): + file.item.setSelected(False) self.removeChild(file.item) self.update() if self.obj.hide_if_empty and not self.obj.files: @@ -584,9 +605,8 @@ def update(self, update_tracks=True): self.setIcon(0, AlbumItem.icon_cd_saved if album.is_complete() else AlbumItem.icon_cd) for i, column in enumerate(MainPanel.columns): self.setText(i, album.column(column[1])) - selection = TreeItem.window.selected_objects - if len(selection) == 1 and album in selection: - TreeItem.window.updateSelection() + if self.isSelected(): + TreeItem.selected_metadata_changed.emit() class TrackItem(TreeItem): @@ -595,6 +615,7 @@ def update(self, update_album=True): track = self.obj if track.num_linked_files == 1: file = track.linked_files[0] + file.item = None color = TrackItem.track_colors[file.state] bgcolor = get_match_color(file.similarity, TreeItem.base_color) icon = FileItem.decide_file_icon(file) @@ -617,7 +638,7 @@ def update(self, update_album=True): item.update() if newnum > oldnum: # add new items items = [] - for i in xrange(oldnum, newnum): + for i in xrange(newnum - 1, oldnum - 1, -1): item = FileItem(track.linked_files[i], False) item.update() items.append(item) @@ -628,6 +649,8 @@ def update(self, update_album=True): self.setText(i, track.column(column[1])) self.setForeground(i, color) self.setBackground(i, bgcolor) + if self.isSelected(): + TreeItem.selected_metadata_changed.emit() if update_album: self.parent().update(update_tracks=False) @@ -643,6 +666,8 @@ def update(self): self.setText(i, file.column(column[1])) self.setForeground(i, color) self.setBackground(i, bgcolor) + if self.isSelected(): + TreeItem.selected_metadata_changed.emit() @staticmethod def decide_file_icon(file): diff --git a/picard/ui/mainwindow.py b/picard/ui/mainwindow.py index 27dce611ff..d5562ecab5 100644 --- a/picard/ui/mainwindow.py +++ b/picard/ui/mainwindow.py @@ -34,7 +34,7 @@ from picard.ui.filebrowser import FileBrowser from picard.ui.tagsfromfilenames import TagsFromFileNamesDialog from picard.ui.options.dialog import OptionsDialog -from picard.ui.tageditor import TagEditor +from picard.ui.infodialog import InfoDialog from picard.ui.passworddialog import PasswordDialog from picard.util import icontheme, webbrowser2, find_existing_path from picard.util.cdrom import get_cdrom_drives @@ -63,7 +63,7 @@ class MainWindow(QtGui.QMainWindow): def __init__(self, parent=None): QtGui.QMainWindow.__init__(self, parent) self.selected_objects = [] - self.tagger.selected_metadata_changed.connect(self.updateSelection) + self.tagger.selected_metadata_changed.connect(self.update_selection) self.setupUi() def setupUi(self): @@ -92,21 +92,20 @@ def setupUi(self): self.panel.insertWidget(0, self.file_browser) self.panel.restore_state() - self.orig_metadata_box = MetadataBox(self, _("Original Metadata"), True) - self.orig_metadata_box.disable() - self.metadata_box = MetadataBox(self, _("New Metadata"), False) - self.metadata_box.disable() + self.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.addWidget(self.orig_metadata_box, 1) + bottomLayout.setContentsMargins(0, 0, 0, 0) bottomLayout.addWidget(self.metadata_box, 1) bottomLayout.addWidget(self.cover_art_box, 0) mainLayout = QtGui.QVBoxLayout() + mainLayout.setContentsMargins(0, 0, 0, 0) + mainLayout.setSpacing(0) mainLayout.addWidget(self.panel, 1) mainLayout.addLayout(bottomLayout, 0) @@ -170,6 +169,7 @@ def saveWindowState(self): self.config.persist["view_file_browser"] = self.show_file_browser_action.isChecked() self.file_browser.save_state() self.panel.save_state() + self.metadata_box.save_state() def restoreWindowState(self): self.restoreState(self.config.persist["window_state"]) @@ -232,7 +232,6 @@ def _on_submit(self): self.tagger.puidmanager.submit() def create_actions(self): - self.options_action = QtGui.QAction(icontheme.lookup('preferences-desktop'), _("&Options..."), self) self.connect(self.options_action, QtCore.SIGNAL("triggered()"), self.show_options) @@ -342,11 +341,11 @@ def create_actions(self): self.autotag_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+L"))) self.connect(self.autotag_action, QtCore.SIGNAL("triggered()"), self.autotag) - self.edit_tags_action = QtGui.QAction(icontheme.lookup('picard-edit-tags'), _(u"&Details..."), self) - self.edit_tags_action.setEnabled(False) - # TR: Keyboard shortcut for "Details" - self.edit_tags_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+I"))) - self.connect(self.edit_tags_action, QtCore.SIGNAL("triggered()"), self.edit_tags) + self.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.connect(self.view_info_action, QtCore.SIGNAL("triggered()"), self.view_info) self.refresh_action = QtGui.QAction(icontheme.lookup('view-refresh', icontheme.ICON_SIZE_MENU), _("&Refresh"), self) self.connect(self.refresh_action, QtCore.SIGNAL("triggered()"), self.refresh) @@ -413,7 +412,7 @@ def create_menus(self): menu.addAction(self.cut_action) menu.addAction(self.paste_action) menu.addSeparator() - menu.addAction(self.edit_tags_action) + menu.addAction(self.view_info_action) menu.addAction(self.remove_action) menu = self.menuBar().addMenu(_(u"&View")) menu.addAction(self.show_file_browser_action) @@ -477,7 +476,7 @@ def create_toolbar(self): toolbar.addAction(self.cluster_action) toolbar.addAction(self.autotag_action) toolbar.addAction(self.analyze_action) - toolbar.addAction(self.edit_tags_action) + toolbar.addAction(self.view_info_action) toolbar.addAction(self.remove_action) self.search_toolbar = toolbar = self.addToolBar(_(u"&Search Bar")) toolbar.setObjectName("search_toolbar") @@ -592,11 +591,11 @@ def open_donation_page(self): def save(self): """Tell the tagger to save the selected objects.""" - self.tagger.save(self.panel.selected_objects()) + self.tagger.save(self.selected_objects) def remove(self): """Tell the tagger to remove the selected objects.""" - self.tagger.remove(self.panel.selected_objects()) + self.tagger.remove(self.selected_objects) def analyze(self): if not self.config.setting['enable_fingerprinting']: @@ -604,7 +603,7 @@ def analyze(self): self.show_options("fingerprinting") if not self.config.setting['enable_fingerprinting']: return - return self.tagger.analyze(self.panel.selected_objects()) + return self.tagger.analyze(self.selected_objects) def open_file(self): files = self.tagger.get_files_from_objects(self.selected_objects) @@ -626,26 +625,25 @@ def show_analyze_settings_info(self): QtGui.QMessageBox.Yes) return ret == QtGui.QMessageBox.Yes - def edit_tags(self, objs=None): - if not objs: - objs = self.selected_objects - objs = self.tagger.get_files_from_objects(objs) - dialog = TagEditor(objs, self) + def view_info(self, objects=None): + if objects is None: + objects = self.selected_objects + dialog = InfoDialog(objects, self) dialog.exec_() def cluster(self): - self.tagger.cluster(self.panel.selected_objects()) + self.tagger.cluster(self.selected_objects) def refresh(self): - self.tagger.refresh(self.panel.selected_objects()) + self.tagger.refresh(self.selected_objects) def update_actions(self): can_remove = False can_save = False - can_edit_tags = False can_analyze = False can_refresh = False can_autotag = False + can_view_info = False for obj in self.selected_objects: if obj is None: continue @@ -655,24 +653,24 @@ def update_actions(self): can_save = True if obj.can_remove(): can_remove = True - if obj.can_edit_tags(): - can_edit_tags = True + if obj.can_view_info(): + can_view_info = True if obj.can_refresh(): can_refresh = True if obj.can_autotag(): can_autotag = True - if can_save and can_remove and can_edit_tags and can_refresh \ + if can_save and can_remove and can_view_info and can_refresh \ and can_autotag: break self.remove_action.setEnabled(can_remove) self.save_action.setEnabled(can_save) - self.edit_tags_action.setEnabled(can_edit_tags) + self.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.cut_action.setEnabled(bool(self.selected_objects)) - def updateSelection(self, objects=None): + def update_selection(self, objects=None): if objects is not None: self.selected_objects = objects else: @@ -680,45 +678,31 @@ def updateSelection(self, objects=None): self.update_actions() - orig_metadata = None metadata = None - is_album = False - statusBar = u"" - file = None + statusbar = u"" obj = None + if len(objects) == 1: - obj = objects[0] + obj = list(objects)[0] if isinstance(obj, File): - orig_metadata = obj.orig_metadata metadata = obj.metadata - statusBar = obj.filename + statusbar = obj.filename if obj.state == obj.ERROR: - statusBar += _(" (Error: %s)") % obj.error - file = obj + statusbar += _(" (Error: %s)") % obj.error elif isinstance(obj, Track): + metadata = obj.metadata if obj.num_linked_files == 1: file = obj.linked_files[0] - orig_metadata = file.orig_metadata - metadata = file.metadata - statusBar = "%s (%d%%)" % (file.filename, file.similarity * 100) - if file.state == file.ERROR: - statusBar += _(" (Error: %s)") % file.error - elif obj.num_linked_files == 0: - metadata = obj.metadata - else: - metadata = obj.metadata - #Show dup zaper - elif isinstance(obj, Cluster): - orig_metadata = obj.metadata - is_album = True - elif isinstance(obj, Album): + statusbar = "%s (%d%%)" % (file.filename, file.similarity * 100) + if file.state == File.ERROR: + statusbar += _(" (Error: %s)") % file.error + elif obj.can_edit_tags(): metadata = obj.metadata - is_album = True - self.orig_metadata_box.set_metadata(orig_metadata, is_album) - self.metadata_box.set_metadata(metadata, is_album, file=file) + self.metadata_box.update_selection() + self.metadata_box.update() self.cover_art_box.set_metadata(metadata, obj) - self.set_statusbar_message(statusBar) + self.set_statusbar_message(statusbar) def show_cover_art(self): """Show/hide the cover art box.""" @@ -747,14 +731,14 @@ def show_proxy_dialog(self, proxy, authenticator): dialog.exec_() def autotag(self): - self.tagger.autotag(self.panel.selected_objects()) + self.tagger.autotag(self.selected_objects) def cut(self): - self._clipboard = self.panel.selected_objects() + self._clipboard = self.selected_objects self.paste_action.setEnabled(bool(self._clipboard)) def paste(self): - selected_objects = self.panel.selected_objects() + selected_objects = self.selected_objects if not selected_objects: target = self.tagger.unmatched_files else: diff --git a/picard/ui/metadatabox.py b/picard/ui/metadatabox.py index 9abc4cf9ee..c73947d9e3 100644 --- a/picard/ui/metadatabox.py +++ b/picard/ui/metadatabox.py @@ -19,104 +19,359 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from PyQt4 import QtCore, QtGui -from picard.util import format_time +from picard.album import Album +from picard.cluster import Cluster +from picard.track import Track +from picard.file import File +from picard.config import TextOption +from picard.util.tags import display_tag_name +from picard.ui.ui_edittagdialog import Ui_EditTagDialog -class MetadataBox(QtGui.QGroupBox): - def __init__(self, parent, title, read_only=False): - QtGui.QGroupBox.__init__(self, title) - self.metadata = None - self.read_only = read_only +class EditTagDialog(QtGui.QDialog): - from picard.ui.ui_metadata import Ui_Form - self.ui = Ui_Form() + def __init__(self, parent, values): + QtGui.QDialog.__init__(self, parent) + self.ui = Ui_EditTagDialog() self.ui.setupUi(self) + self.values = values + self.list = self.ui.value_list + for value in values[0]: + item = QtGui.QListWidgetItem(value) + item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable) + self.list.addItem(item) + self.list.setCurrentItem(self.list.item(0), QtGui.QItemSelectionModel.SelectCurrent) + self.ui.add_value.clicked.connect(self.add_value) + self.ui.remove_value.clicked.connect(self.remove_value) - self.ui.title.setReadOnly(self.read_only) - self.ui.artist.setReadOnly(self.read_only) - self.ui.album.setReadOnly(self.read_only) - self.ui.tracknumber.setReadOnly(self.read_only) - self.ui.date.setReadOnly(self.read_only) - - self.connect(self.ui.lookup, QtCore.SIGNAL("clicked()"), self.lookup) - self.connect(self.ui.title, QtCore.SIGNAL("textEdited(QString)"), self.update_metadata_title) - self.connect(self.ui.album, QtCore.SIGNAL("textEdited(QString)"), self.update_metadata_album) - self.connect(self.ui.artist, QtCore.SIGNAL("textEdited(QString)"), self.update_metadata_artist) - self.connect(self.ui.tracknumber, QtCore.SIGNAL("textEdited(QString)"), self.update_metadata_tracknum) - self.connect(self.ui.date, QtCore.SIGNAL("textEdited(QString)"), self.update_metadata_date) - - self.disable() - - def enable(self, is_album): - if not is_album: - self.ui.title.setDisabled(False) - self.ui.tracknumber.setDisabled(False) - else: - self.ui.title.setDisabled(True) - self.ui.tracknumber.setDisabled(True) - self.ui.artist.setDisabled(False) - self.ui.album.setDisabled(False) - self.ui.length.setDisabled(False) - self.ui.date.setDisabled(False) - self.ui.lookup.setDisabled(False) - - def disable(self): - self.ui.title.setDisabled(True) - self.ui.artist.setDisabled(True) - self.ui.album.setDisabled(True) - self.ui.tracknumber.setDisabled(True) - self.ui.length.setDisabled(True) - self.ui.date.setDisabled(True) - self.ui.lookup.setDisabled(True) + def add_value(self): + item = QtGui.QListWidgetItem() + item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable) + self.list.addItem(item) + self.list.editItem(item) + + def remove_value(self): + vals = self.list + vals.takeItem(vals.row(vals.currentItem())) + + def accept(self): + values = [] + for i in xrange(self.list.count()): + value = unicode(self.list.item(i).text()) + if value: + values.append(value) + self.values[0] = tuple(values) + QtGui.QDialog.accept(self) + + +class TagCounter(dict): + + def __init__(self): + self._counts = {} + self._different = set() + + def add(self, tag, values): + if tag not in self._different: + vals = self.setdefault(tag, set()) + vals.add(tuple(sorted(values))) + if len(vals) > 1: + self._different.add(tag) + self[tag] = [("",)] + self._counts[tag] = self._counts.get(tag, 0) + 1 + + def different(self, tag): + return tag in self._different + + def count(self, tag): + return self._counts.get(tag, 0) def clear(self): - self.ui.title.clear() - self.ui.artist.clear() - self.ui.album.clear() - self.ui.length.clear() - self.ui.tracknumber.clear() - self.ui.date.clear() - - def set_metadata(self, metadata, is_album=False, file=None): - self.metadata = metadata - self.obj = file - if metadata: - if is_album: - self.ui.album.setText(metadata['album']) - self.ui.title.clear() - self.ui.tracknumber.clear() + dict.clear(self) + self._counts.clear() + self._different.clear() + return self + + +class MetadataBox(QtGui.QTableWidget): + + options = ( + TextOption("persist", "metadata_box_sizes", "200 380 380") + ) + + common_tags = ( + "title", + "artist", + "album", + "tracknumber", + "~length", + "date", + ) + + def __init__(self, parent): + QtGui.QTableWidget.__init__(self, parent) + self.parent = parent + self.setColumnCount(3) + self.setHorizontalHeaderLabels((N_("Tag"), N_("Original value"), N_("New value"))) + self.verticalHeader().setDefaultSectionSize(18) + self.verticalHeader().setVisible(False) + self.horizontalHeader().setStretchLastSection(True) + self.horizontalHeader().setClickable(False) + self.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) + self.setStyleSheet("border: none; font-size: 11px;") + self.restore_state() + self.itemChanged.connect(self.item_changed) + self._item_signals = True + self.colors = { + "default": self.palette().color(QtGui.QPalette.Text), + "removed": QtGui.QBrush(QtGui.QColor("red")), + "added": QtGui.QBrush(QtGui.QColor("green")), + "changed": QtGui.QBrush(QtGui.QColor("darkgoldenrod")), + } + self.files = set() + self.tracks = set() + self.objects = set() + self.orig_tags = TagCounter() + self.new_tags = TagCounter() + self.update_mutex = QtCore.QMutex() + self.selection_mutex = QtCore.QMutex() + self.updating = False + self.update_pending = False + self.selection_dirty = False + + def edit(self, index, trigger, event): + if index.column() != 2: + return False + if trigger in (QtGui.QAbstractItemView.DoubleClicked, + QtGui.QAbstractItemView.EditKeyPressed, + QtGui.QAbstractItemView.AnyKeyPressed): + item = self.itemFromIndex(index) + tag = self.tag_names[item.row()] + values = self.new_tags[tag] + if len(values) == 1 and len(values[0]) > 1: + dialog = EditTagDialog(self.parent, values) + if dialog.exec_(): + self.set_item_value(item, values) + self._item_signals = False + self.set_row_colors(item.row()) + self._item_signals = True + return False else: - self.ui.album.setText(metadata['album']) - self.ui.title.setText(metadata['title']) - self.ui.tracknumber.setText(metadata['tracknumber']) - self.ui.artist.setText(metadata['artist']) - self.ui.length.setText(format_time(metadata.length)) - self.ui.date.setText(metadata['date']) - self.enable(is_album) + return QtGui.QTableWidget.edit(self, index, trigger, event) + return False + + def update_selection(self): + self.selection_mutex.lock() + self.selection_dirty = True + self.selection_mutex.unlock() + + def _update_selection(self): + files = self.files + tracks = self.tracks + objects = self.objects + files.clear() + tracks.clear() + objects.clear() + for obj in self.parent.panel._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 + + def update(self): + self.update_mutex.lock() + self._update() + self.update_mutex.unlock() + + def _update(self): + if not self.updating: + self.updating = True + self.update_pending = False + self.tagger.other_queue.put((self._update_tags, self._update_items, QtCore.Qt.LowEventPriority)) else: - self.clear() - self.disable() + self.update_pending = True + + def _update_tags(self): + self.selection_mutex.lock() + if self.selection_dirty: + self._update_selection() + self.selection_mutex.unlock() + + files = self.files + tracks = self.tracks + if not (files or tracks): + return None + orig_tags = self.orig_tags.clear() + new_tags = self.new_tags.clear() + orig_total = 0 + + for file in files: + for name, values in file.orig_metadata._items.iteritems(): + if not name.startswith("~") or name == "~length": + orig_tags.add(name, values) + for name, values in file.metadata._items.iteritems(): + if not name.startswith("~") or name == "~length": + new_tags.add(name, values) + orig_total += 1 + + new_total = orig_total + for track in tracks: + if track.num_linked_files == 0: + for name, values in track.metadata._items.iteritems(): + if not name.startswith("~") or name == "~length": + new_tags.add(name, values) + new_total += 1 + + common_tags = MetadataBox.common_tags + all_tags = set(orig_tags.keys() + new_tags.keys()) + self.tag_names = [t for t in common_tags if t in all_tags] + sorted(all_tags.difference(common_tags)) + return (orig_total, new_total) + + def _update_items(self, result=None, error=None): + if result is None: + self.orig_tags.clear() + self.new_tags.clear() + self.tag_names = None + self.setRowCount(0) + + self.update_mutex.lock() + self.updating = False + if self.update_pending: + self._update() + self.update_mutex.unlock() + return + + self._item_signals = False + orig_total, new_total = result + orig_tags = self.orig_tags + new_tags = self.new_tags + tag_names = self.tag_names + self.setRowCount(len(tag_names)) + set_item_value = self.set_item_value - def lookup(self): - """Tell the tagger to lookup the metadata.""" - self.tagger.lookup(self.metadata) + for i, name in enumerate(tag_names): + tag_item = self.item(i, 0) + if not tag_item: + tag_item = QtGui.QTableWidgetItem() + tag_item.setFlags(QtCore.Qt.ItemIsEnabled) + font = tag_item.font() + font.setBold(True) + tag_item.setFont(font) + self.setItem(i, 0, tag_item) + tag_item.setText(display_tag_name(name)) - def _update_metadata(self, name, text): - if self.metadata and self.obj: - self.metadata[name] = unicode(text) - self.obj.update() + orig_values = list(orig_tags.get(name, [("",)])) + new_values = list(new_tags.get(name, [("",)])) + orig_tags[name] = orig_values + new_tags[name] = new_values - def update_metadata_title(self, text): - self._update_metadata('title', text) + orig_item = self.item(i, 1) + if not orig_item: + orig_item = QtGui.QTableWidgetItem() + orig_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) + self.setItem(i, 1, orig_item) + set_item_value(orig_item, orig_values, orig_tags.different(name), orig_tags.count(name), orig_total) - def update_metadata_album(self, text): - self._update_metadata('album', text) + new_item = self.item(i, 2) + if not new_item: + new_item = QtGui.QTableWidgetItem() + self.setItem(i, 2, new_item) + if name == "~length": + new_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) + set_item_value(new_item, new_values, new_tags.different(name), new_tags.count(name), new_total) + self.set_row_colors(i) - def update_metadata_artist(self, text): - self._update_metadata('artist', text) + self._item_signals = True + + self.update_mutex.lock() + self.updating = False + if self.update_pending: + self._update() + self.update_mutex.unlock() + + def set_item_value(self, item, values, different=False, count=0, total=0): + font = item.font() + missing = total - count + if different or (count > 0 and missing > 0): + if missing > 0: + item.setText(ungettext("(missing from %d item)", "(missing from %d items)", missing) % missing) + else: + item.setText(_("(different across %d items)") % total) + font.setItalic(True) + else: + value = values[0] + if len(value) > 1: + item.setText("; ".join(value)) + else: + item.setText(value[0]) + font.setItalic(False) + item.setFont(font) + + def set_row_colors(self, row): + orig_item = self.item(row, 1) + new_item = self.item(row, 2) + tag = self.tag_names[row] + orig_values = self.orig_tags[tag] + new_values = self.new_tags[tag] + orig_blank = orig_values == [("",)] and not self.orig_tags.different(tag) + new_blank = new_values == [("",)] and not self.new_tags.different(tag) + if new_blank and not orig_blank: + orig_item.setForeground(self.colors["removed"]) + elif orig_blank and not new_blank: + new_item.setForeground(self.colors["added"]) + elif not (orig_blank or new_blank) and orig_values != new_values: + orig_item.setForeground(self.colors["changed"]) + new_item.setForeground(self.colors["changed"]) + else: + orig_item.setForeground(self.colors["default"]) + new_item.setForeground(self.colors["default"]) + + def item_changed(self, item): + if not self._item_signals: + return + self._item_signals = False + self.tagger.selected_metadata_changed.disconnect(self.parent.update_selection) + tag = self.tag_names[item.row()] + values = self.new_tags[tag] + if len(values) == 1 and len(values[0]) > 1: + # The tag editor dialog already updated self.new_tags + value = values[0] + else: + value = unicode(item.text()) + new_values = self.new_tags[tag] = [(value,)] + font = item.font() + font.setItalic(False) + item.setFont(font) + self.set_row_colors(item.row()) + for obj in self.objects: + obj.metadata[tag] = value + obj.update() + self.tagger.selected_metadata_changed.connect(self.parent.update_selection) + self._item_signals = True - def update_metadata_tracknum(self, text): - self._update_metadata('tracknumber', text) + def restore_state(self): + sizes = self.config.persist["metadata_box_sizes"].split(" ") + header = self.horizontalHeader() + try: + for i in range(header.count()): + header.resizeSection(i, int(sizes[i])) + except IndexError: + pass - def update_metadata_date(self, text): - self._update_metadata('date', text) + def save_state(self): + sizes = [] + header = self.horizontalHeader() + for i in range(header.count()): + sizes.append(str(header.sectionSize(i))) + self.config.persist["metadata_box_sizes"] = " ".join(sizes) diff --git a/picard/ui/ratingwidget.py b/picard/ui/ratingwidget.py index e68853b163..e25a14fea8 100644 --- a/picard/ui/ratingwidget.py +++ b/picard/ui/ratingwidget.py @@ -22,100 +22,74 @@ class RatingWidget(QtGui.QWidget): - def __init__(self, parent=None, rating=0, maximum=5): - super(RatingWidget, self).__init__(parent) - self._maximum = maximum - self._rating = rating + def __init__(self, parent, track): + QtGui.QWidget.__init__(self, parent) + self._track = track + self._maximum = self.config.setting["rating_steps"] - 1 + self._rating = int(track.metadata["~rating"] or 0) self._highlight = 0 - - self._starSize = 15 - self._starSpacing = 4 - + self._star_pixmap = QtGui.QPixmap(":/images/star.png") + self._star_gray_pixmap = QtGui.QPixmap(":/images/star-gray.png") + self._star_size = 16 + self._star_spacing = 2 + self._offset = 16 + self._width = self._maximum * (self._star_size + self._star_spacing) + self._offset + self._height = self._star_size + 6 + self.setMaximumSize(self._width, self._height) + self.setMinimumSize(self._width, self._height) + self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed)) self.setMouseTracking(True) - self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, - QtGui.QSizePolicy.Minimum)) - - def setRating(self, rating): - assert 0 <= rating <= self._maximum - if rating != self._rating: - self._rating = rating - self.update() - - def getRating(self): - return self._rating - + + def sizeHint(self): + return QtCore.QSize(self._width, self._height) + def _setHighlight(self, highlight): assert 0 <= highlight <= self._maximum if highlight != self._highlight: self._highlight = highlight self.update() - - def setMaximum(self, maximum): - assert maximum > 0 - self._maximum = maximum - if self._rating > self._maximum: - self._rating = self._maximum - self.updateGeometry() - self.update() - - def getMaximum(self): - return self._maximum - - def sizeHint(self): - return self.minimumSizeHint() - - def minimumSizeHint(self): - return QtCore.QSize(self._maximum * (self._starSize + self._starSpacing), - self._starSize) - + def mousePressEvent(self, event): if event.button() == QtCore.Qt.LeftButton: - rating = self._getRatingFromPosition(event.x()) + x = event.x() + if x < self._offset: + return + rating = self._getRatingFromPosition(x) if self._rating == rating: rating = 0 - self.setRating(rating) + self._rating = rating + self._update_track() + self.update() event.accept() - else: - QWidget.mousePressEvent(self, event) - + def mouseMoveEvent(self, event): self._setHighlight(self._getRatingFromPosition(event.x())) event.accept() - + def leaveEvent(self, event): self._setHighlight(0) event.accept() - + def _getRatingFromPosition(self, position): - rating = int(position / (self._starSize + self._starSpacing)) + 1 + rating = int((position - self._offset) / (self._star_size + self._star_spacing)) + 1 if rating > self._maximum: rating = self._maximum return rating - + + def _update_track(self): + track = self._track + track.metadata["~rating"] = unicode(self._rating) + if self.config.setting["submit_ratings"]: + ratings = {("recording", track.id): self._rating} + self.tagger.xmlws.submit_ratings(ratings, None) + def paintEvent(self, event=None): - # FIXME: Draw prettier stars, maybe use some nice bitmaps painter = QtGui.QPainter(self) - painter.setRenderHint(QtGui.QPainter.Antialiasing) - painter.setBrush(self.palette().color(QtGui.QPalette.Light)) - offset = 0 - for i in range(1, self._maximum+1): - if (i <= self._rating): - painter.setBrush(self.palette().color(QtGui.QPalette.Highlight)) - elif (i <= self._highlight): - painter.setBrush(self.palette().color(QtGui.QPalette.Mid)) + offset = self._offset + for i in range(1, self._maximum + 1): + if i <= self._rating or i <= self._highlight: + pixmap = self._star_pixmap else: - painter.setBrush(self.palette().color(QtGui.QPalette.Light)) - self._drawStar(painter, offset) - offset += self._starSize + self._starSpacing - - def _drawStar(self, painter, offset): - painter.drawPolygon(QtCore.QPointF(offset + self._starSize*0.50, 0), - QtCore.QPointF(offset + self._starSize*0.63, self._starSize*0.37), - QtCore.QPointF(offset + self._starSize, self._starSize*0.38), - QtCore.QPointF(offset + self._starSize*0.71, self._starSize*0.62), - QtCore.QPointF(offset + self._starSize*0.82, self._starSize), - QtCore.QPointF(offset + self._starSize*0.50, self._starSize*0.78), - QtCore.QPointF(offset + self._starSize*0.18, self._starSize), - QtCore.QPointF(offset + self._starSize*0.30, self._starSize*0.62), - QtCore.QPointF(offset + 0, self._starSize*0.38), - QtCore.QPointF(offset + self._starSize*0.37, self._starSize*0.37)) + pixmap = self._star_gray_pixmap + painter.drawPixmap(offset, 3, pixmap) + offset += self._star_size + self._star_spacing diff --git a/picard/ui/tageditor.py b/picard/ui/tageditor.py deleted file mode 100644 index 2e691e8eac..0000000000 --- a/picard/ui/tageditor.py +++ /dev/null @@ -1,267 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Picard, the next-generation MusicBrainz tagger -# Copyright (C) 2006 Lukáš Lalinský -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -import os.path -from PyQt4 import QtCore, QtGui -from picard.track import Track -from picard.metadata import Metadata -from picard.util import format_time, encode_filename -from picard.ui.util import StandardButton -from picard.util.tags import tag_names, display_tag_name -from picard.ui.ui_tageditor import Ui_TagEditorDialog -from picard.ui.ui_edittagdialog import Ui_EditTagDialog - - -class EditTagDialog(QtGui.QDialog): - """Single tag editor.""" - - def __init__(self, name, value, parent=None): - QtGui.QDialog.__init__(self, parent) - self.ui = Ui_EditTagDialog() - self.ui.setupUi(self) - self.ui.buttonbox.addButton(StandardButton(StandardButton.OK), QtGui.QDialogButtonBox.AcceptRole) - self.ui.buttonbox.addButton(StandardButton(StandardButton.CANCEL), QtGui.QDialogButtonBox.RejectRole) - self.connect(self.ui.buttonbox, QtCore.SIGNAL('accepted()'), self, QtCore.SLOT('accept()')) - self.connect(self.ui.buttonbox, QtCore.SIGNAL('rejected()'), self, QtCore.SLOT('reject()')) - self.ui.name.addItems(sorted(tag_names.keys())) - if name: - self.ui.name.setEditText(name) - if value: - self.ui.value.document().setPlainText(value) - self.ui.value.selectAll() - self.ui.value.setFocus(QtCore.Qt.OtherFocusReason) - else: - self.ui.name.lineEdit().selectAll() - - def accept(self): - self.name = unicode(self.ui.name.currentText()) - self.value = self.ui.value.document().toPlainText() - QtGui.QDialog.accept(self) - - -class TagEditor(QtGui.QDialog): - - def __init__(self, files, parent=None): - QtGui.QDialog.__init__(self, parent) - - self.ui = Ui_TagEditorDialog() - self.ui.setupUi(self) - - title = _("Details") + " - " - total = len(files) - if total == 1: - title += files[0].base_filename - else: - title += ungettext("%d file", "%d files", total) % total - self.setWindowTitle(title) - - self.ui.buttonbox.addButton(StandardButton(StandardButton.OK), QtGui.QDialogButtonBox.AcceptRole) - self.ui.buttonbox.addButton(StandardButton(StandardButton.CANCEL), QtGui.QDialogButtonBox.RejectRole) - self.connect(self.ui.buttonbox, QtCore.SIGNAL('accepted()'), self, QtCore.SLOT('accept()')) - self.connect(self.ui.buttonbox, QtCore.SIGNAL('rejected()'), self, QtCore.SLOT('reject()')) - - self.connect(self.ui.tags_add, QtCore.SIGNAL('clicked()'), self.add_tag) - self.connect(self.ui.tags_edit, QtCore.SIGNAL('clicked()'), self.edit_tag) - self.connect(self.ui.tags_delete, QtCore.SIGNAL('clicked()'), self.delete_tag) - self.connect(self.ui.tags, QtCore.SIGNAL("itemActivated (QTreeWidgetItem*, int)"), self.edit_tag) - - self.ui.tags.setSortingEnabled(True) - self.ui.tags.sortByColumn(0, QtCore.Qt.AscendingOrder) - - if self.config.setting['enable_ratings']: - self.ui.rating.setMaximum(self.config.setting['rating_steps'] - 1) - else: - self.ui.ratingLabel.hide() - self.ui.rating.hide() - - self.changed = set() - self.files = files - self.load() - self.load_info() - - def accept(self): - self.save() - self.tagger.selected_metadata_changed.emit() - QtGui.QDialog.accept(self) - - def load(self): - all_tag_names = set() - common_tags = None - counts = dict() - - for file in self.files: - tags = set() - for name, values in file.metadata.rawitems(): - if not name.startswith("~"): - tags.add((name, tuple(sorted(values)))) - all_tag_names.add(name) - counts[name] = counts.get(name, 0) + 1 - if common_tags is None: - common_tags = tags - else: - common_tags = common_tags.intersection(tags) - - common_tag_names = set([a for (a, b) in common_tags]) - different_tag_names = all_tag_names.difference(common_tag_names) - - total = len(self.files) - for name in different_tag_names: - item = QtGui.QTreeWidgetItem(self.ui.tags) - item.setText(0, display_tag_name(name)) - item.setData(0, QtCore.Qt.UserRole, QtCore.QVariant(name)) - font = item.font(1) - font.setItalic(True) - item.setFont(1, font) - missing = total - counts[name] - if not missing: - value = ungettext("(different across %d file)", - "(different across %d files)", total) % total - else: - value = ungettext("(missing from %d file)", - "(missing from %d files)", missing) % missing - item.setText(1, value) - - for name, values in common_tags: - for value in values: - item = QtGui.QTreeWidgetItem(self.ui.tags) - item.setText(0, display_tag_name(name)) - item.setData(0, QtCore.Qt.UserRole, QtCore.QVariant(name)) - item.setText(1, value) - - if len(self.files) == 1: - if self.config.setting['enable_ratings']: - ratings = self.files[0].metadata.getall('~rating') - if len(ratings) > 0: - self.ui.rating.setRating(int(ratings[0])) - for mime, data in self.files[0].metadata.images: - item = QtGui.QListWidgetItem() - pixmap = QtGui.QPixmap() - pixmap.loadFromData(data) - icon = QtGui.QIcon(pixmap) - item.setIcon(icon) - self.ui.artwork_list.addItem(item) - - def save(self): - metadata = Metadata() - for i in range(self.ui.tags.topLevelItemCount()): - item = self.ui.tags.topLevelItem(i) - name = unicode(item.data(0, QtCore.Qt.UserRole).toString()) - if name in self.changed: - value = unicode(item.text(1)) - metadata.add(name, value) - - # Rate the different tracks - if self.config.setting['enable_ratings']: - rating = self.ui.rating.getRating() - metadata['~rating'] = unicode(rating) - tracks = set([file.parent for file in self.files - if isinstance(file.parent, Track)]) - ratings = {} - for track in tracks: - ratings[('recording', track.id)] = rating - track.metadata['~rating'] = rating - if self.config.setting['submit_ratings']: - self.tagger.xmlws.submit_ratings(ratings, None) - - for file in self.files: - for name in self.changed: - try: - del file.metadata[name] - except KeyError: - pass - file.metadata.update(metadata) - file.update() - - def add_tag(self): - dialog = EditTagDialog('', None, self) - if dialog.exec_(): - name = dialog.name - value = dialog.value - item = QtGui.QTreeWidgetItem(self.ui.tags) - item.setText(0, display_tag_name(name)) - item.setData(0, QtCore.Qt.UserRole, QtCore.QVariant(name)) - item.setText(1, value) - self.changed.add(name) - - def edit_tag(self, item=None, column=None): - if item is None: - items = self.ui.tags.selectedItems() - if not items: - return - item = items[0] - name = unicode(item.data(0, QtCore.Qt.UserRole).toString()) - value = item.text(1) - dialog = EditTagDialog(name, value, self) - if dialog.exec_(): - if value != dialog.value or name != dialog.name: - name = dialog.name - value = dialog.value - item.setText(0, display_tag_name(name)) - item.setData(0, QtCore.Qt.UserRole, QtCore.QVariant(name)) - font = item.font(1) - font.setItalic(False) - item.setFont(1, font) - item.setText(1, value) - self.changed.add(name) - - def delete_tag(self): - items = self.ui.tags.selectedItems() - for item in items: - name = unicode(item.data(0, QtCore.Qt.UserRole).toString()) - index = self.ui.tags.indexOfTopLevelItem(item) - self.ui.tags.takeTopLevelItem(index) - self.changed.add(name) - - def load_info(self): - total = len(self.files) - if total == 1: - file = self.files[0] - info = [] - info.append((_('Filename:'), file.filename)) - if '~format' in file.orig_metadata: - info.append((_('Format:'), file.orig_metadata['~format'])) - try: - size = os.path.getsize(encode_filename(file.filename)) - if size < 1024: - size = '%d B' % size - elif size < 1024 * 1024: - size = '%0.1f kB' % (size / 1024.0) - else: - size = '%0.1f MB' % (size / 1024.0 / 1024.0) - info.append((_('Size:'), size)) - except: - pass - if file.orig_metadata.length: - info.append((_('Length:'), format_time(file.orig_metadata.length))) - if '~#bitrate' in file.orig_metadata: - info.append((_('Bitrate:'), '%d kbps' % file.orig_metadata['~#bitrate'])) - if '~#sample_rate' in file.orig_metadata: - info.append((_('Sample rate:'), '%d Hz' % file.orig_metadata['~#sample_rate'])) - if '~#bits_per_sample' in file.orig_metadata: - info.append((_('Bits per sample:'), str(file.orig_metadata['~#bits_per_sample']))) - if '~#channels' in file.orig_metadata: - ch = file.orig_metadata['~#channels'] - if ch == 1: ch = _('Mono') - elif ch == 2: ch = _('Stereo') - else: ch = str(ch) - info.append((_('Channels:'), ch)) - text = '
'.join(map(lambda i: '%s
%s' % i, info)) - self.ui.info.setText(text) - else: - self.ui.info.setText(ungettext("%d file", "%d files", total) % total) diff --git a/picard/ui/ui_edittagdialog.py b/picard/ui/ui_edittagdialog.py index 2f8ffc0eac..522048a096 100644 --- a/picard/ui/ui_edittagdialog.py +++ b/picard/ui/ui_edittagdialog.py @@ -2,8 +2,8 @@ # Form implementation generated from reading ui file 'ui/edittagdialog.ui' # -# Created: Thu Sep 15 13:39:10 2011 -# by: PyQt4 UI code generator 4.8.3 +# Created: Fri Dec 16 23:07:11 2011 +# by: PyQt4 UI code generator 4.8.5 # # WARNING! All changes made in this file will be lost! @@ -17,30 +17,63 @@ class Ui_EditTagDialog(object): def setupUi(self, EditTagDialog): EditTagDialog.setObjectName(_fromUtf8("EditTagDialog")) - EditTagDialog.resize(384, 225) - self.vboxlayout = QtGui.QVBoxLayout(EditTagDialog) - self.vboxlayout.setMargin(9) - self.vboxlayout.setSpacing(6) - self.vboxlayout.setObjectName(_fromUtf8("vboxlayout")) - self.name = QtGui.QComboBox(EditTagDialog) - self.name.setEditable(True) - self.name.setObjectName(_fromUtf8("name")) - self.vboxlayout.addWidget(self.name) - self.value = QtGui.QTextEdit(EditTagDialog) - self.value.setTabChangesFocus(True) - self.value.setAcceptRichText(False) - self.value.setObjectName(_fromUtf8("value")) - self.vboxlayout.addWidget(self.value) + EditTagDialog.setWindowModality(QtCore.Qt.ApplicationModal) + EditTagDialog.resize(436, 240) + EditTagDialog.setFocusPolicy(QtCore.Qt.StrongFocus) + EditTagDialog.setWindowTitle(_("Edit tag")) + EditTagDialog.setModal(True) + self.verticalLayout = QtGui.QVBoxLayout(EditTagDialog) + self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) + self.value_list = QtGui.QListWidget(EditTagDialog) + self.value_list.setFocusPolicy(QtCore.Qt.StrongFocus) + self.value_list.setTabKeyNavigation(False) + self.value_list.setProperty("showDropIndicator", False) + self.value_list.setObjectName(_fromUtf8("value_list")) + self.verticalLayout.addWidget(self.value_list) + self.horizontalLayout = QtGui.QHBoxLayout() + self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) + self.add_value = QtGui.QPushButton(EditTagDialog) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(100) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.add_value.sizePolicy().hasHeightForWidth()) + self.add_value.setSizePolicy(sizePolicy) + self.add_value.setMinimumSize(QtCore.QSize(100, 0)) + self.add_value.setText(_("Add value")) + self.add_value.setAutoDefault(False) + self.add_value.setObjectName(_fromUtf8("add_value")) + self.horizontalLayout.addWidget(self.add_value) + self.remove_value = QtGui.QPushButton(EditTagDialog) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(120) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.remove_value.sizePolicy().hasHeightForWidth()) + self.remove_value.setSizePolicy(sizePolicy) + self.remove_value.setMinimumSize(QtCore.QSize(120, 0)) + self.remove_value.setText(_("Remove value")) + self.remove_value.setAutoDefault(False) + self.remove_value.setObjectName(_fromUtf8("remove_value")) + self.horizontalLayout.addWidget(self.remove_value) + spacerItem = QtGui.QSpacerItem(33, 17, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.horizontalLayout.addItem(spacerItem) self.buttonbox = QtGui.QDialogButtonBox(EditTagDialog) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(150) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.buttonbox.sizePolicy().hasHeightForWidth()) + self.buttonbox.setSizePolicy(sizePolicy) + self.buttonbox.setMinimumSize(QtCore.QSize(150, 0)) self.buttonbox.setOrientation(QtCore.Qt.Horizontal) + self.buttonbox.setStandardButtons(QtGui.QDialogButtonBox.Cancel|QtGui.QDialogButtonBox.Save) self.buttonbox.setObjectName(_fromUtf8("buttonbox")) - self.vboxlayout.addWidget(self.buttonbox) + self.horizontalLayout.addWidget(self.buttonbox) + self.verticalLayout.addLayout(self.horizontalLayout) self.retranslateUi(EditTagDialog) + QtCore.QObject.connect(self.buttonbox, QtCore.SIGNAL(_fromUtf8("accepted()")), EditTagDialog.accept) + QtCore.QObject.connect(self.buttonbox, QtCore.SIGNAL(_fromUtf8("rejected()")), EditTagDialog.reject) QtCore.QMetaObject.connectSlotsByName(EditTagDialog) - EditTagDialog.setTabOrder(self.buttonbox, self.name) - EditTagDialog.setTabOrder(self.name, self.value) def retranslateUi(self, EditTagDialog): - EditTagDialog.setWindowTitle(_("Edit Tag")) + pass diff --git a/picard/ui/ui_infodialog.py b/picard/ui/ui_infodialog.py new file mode 100644 index 0000000000..b1fe5dae25 --- /dev/null +++ b/picard/ui/ui_infodialog.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ui/infodialog.ui' +# +# Created: Sat Dec 17 17:07:31 2011 +# by: PyQt4 UI code generator 4.8.5 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_InfoDialog(object): + def setupUi(self, InfoDialog): + InfoDialog.setObjectName(_fromUtf8("InfoDialog")) + InfoDialog.resize(535, 436) + self.vboxlayout = QtGui.QVBoxLayout(InfoDialog) + self.vboxlayout.setObjectName(_fromUtf8("vboxlayout")) + self.tabWidget = QtGui.QTabWidget(InfoDialog) + self.tabWidget.setObjectName(_fromUtf8("tabWidget")) + self.info_tab = QtGui.QWidget() + self.info_tab.setObjectName(_fromUtf8("info_tab")) + self.vboxlayout1 = QtGui.QVBoxLayout(self.info_tab) + self.vboxlayout1.setObjectName(_fromUtf8("vboxlayout1")) + self.info = QtGui.QLabel(self.info_tab) + self.info.setText(_fromUtf8("")) + self.info.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) + self.info.setWordWrap(True) + self.info.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByKeyboard|QtCore.Qt.TextSelectableByMouse) + self.info.setObjectName(_fromUtf8("info")) + self.vboxlayout1.addWidget(self.info) + self.tabWidget.addTab(self.info_tab, _fromUtf8("")) + self.artwork_tab = QtGui.QWidget() + self.artwork_tab.setObjectName(_fromUtf8("artwork_tab")) + self.vboxlayout2 = QtGui.QVBoxLayout(self.artwork_tab) + self.vboxlayout2.setObjectName(_fromUtf8("vboxlayout2")) + self.artwork_list = QtGui.QListWidget(self.artwork_tab) + self.artwork_list.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) + self.artwork_list.setIconSize(QtCore.QSize(170, 170)) + self.artwork_list.setMovement(QtGui.QListView.Static) + self.artwork_list.setFlow(QtGui.QListView.LeftToRight) + self.artwork_list.setProperty("isWrapping", False) + self.artwork_list.setResizeMode(QtGui.QListView.Fixed) + self.artwork_list.setSpacing(10) + self.artwork_list.setViewMode(QtGui.QListView.IconMode) + self.artwork_list.setObjectName(_fromUtf8("artwork_list")) + self.vboxlayout2.addWidget(self.artwork_list) + self.tabWidget.addTab(self.artwork_tab, _fromUtf8("")) + self.vboxlayout.addWidget(self.tabWidget) + self.buttonBox = QtGui.QDialogButtonBox(InfoDialog) + self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel|QtGui.QDialogButtonBox.Ok) + self.buttonBox.setObjectName(_fromUtf8("buttonBox")) + self.vboxlayout.addWidget(self.buttonBox) + + self.retranslateUi(InfoDialog) + self.tabWidget.setCurrentIndex(0) + QtCore.QMetaObject.connectSlotsByName(InfoDialog) + InfoDialog.setTabOrder(self.tabWidget, self.artwork_list) + InfoDialog.setTabOrder(self.artwork_list, self.buttonBox) + + def retranslateUi(self, InfoDialog): + self.tabWidget.setTabText(self.tabWidget.indexOf(self.info_tab), _("&Info")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.artwork_tab), _("A&rtwork")) + diff --git a/picard/ui/ui_metadata.py b/picard/ui/ui_metadata.py deleted file mode 100644 index 62b3de0d11..0000000000 --- a/picard/ui/ui_metadata.py +++ /dev/null @@ -1,138 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'ui/metadata.ui' -# -# Created: Thu Sep 15 13:39:09 2011 -# by: PyQt4 UI code generator 4.8.3 -# -# WARNING! All changes made in this file will be lost! - -from PyQt4 import QtCore, QtGui - -try: - _fromUtf8 = QtCore.QString.fromUtf8 -except AttributeError: - _fromUtf8 = lambda s: s - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName(_fromUtf8("Form")) - Form.resize(380, 143) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth()) - Form.setSizePolicy(sizePolicy) - Form.setTitle(_fromUtf8("")) - self.vboxlayout = QtGui.QVBoxLayout(Form) - self.vboxlayout.setSpacing(6) - self.vboxlayout.setMargin(9) - self.vboxlayout.setObjectName(_fromUtf8("vboxlayout")) - self.gridlayout = QtGui.QGridLayout() - self.gridlayout.setMargin(0) - self.gridlayout.setHorizontalSpacing(2) - self.gridlayout.setVerticalSpacing(3) - self.gridlayout.setObjectName(_fromUtf8("gridlayout")) - self.label = QtGui.QLabel(Form) - self.label.setObjectName(_fromUtf8("label")) - self.gridlayout.addWidget(self.label, 0, 0, 1, 1) - self.title = QtGui.QLineEdit(Form) - self.title.setObjectName(_fromUtf8("title")) - self.gridlayout.addWidget(self.title, 0, 1, 1, 9) - self.label_2 = QtGui.QLabel(Form) - self.label_2.setObjectName(_fromUtf8("label_2")) - self.gridlayout.addWidget(self.label_2, 1, 0, 1, 1) - self.artist = QtGui.QLineEdit(Form) - self.artist.setObjectName(_fromUtf8("artist")) - self.gridlayout.addWidget(self.artist, 1, 1, 1, 9) - self.label_3 = QtGui.QLabel(Form) - self.label_3.setObjectName(_fromUtf8("label_3")) - self.gridlayout.addWidget(self.label_3, 2, 0, 1, 1) - self.album = QtGui.QLineEdit(Form) - self.album.setObjectName(_fromUtf8("album")) - self.gridlayout.addWidget(self.album, 2, 1, 1, 9) - self.label_4 = QtGui.QLabel(Form) - self.label_4.setObjectName(_fromUtf8("label_4")) - self.gridlayout.addWidget(self.label_4, 3, 0, 1, 1) - self.tracknumber = QtGui.QLineEdit(Form) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(1) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.tracknumber.sizePolicy().hasHeightForWidth()) - self.tracknumber.setSizePolicy(sizePolicy) - self.tracknumber.setMinimumSize(QtCore.QSize(25, 0)) - self.tracknumber.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.tracknumber.setObjectName(_fromUtf8("tracknumber")) - self.gridlayout.addWidget(self.tracknumber, 3, 1, 1, 1) - self.label_5 = QtGui.QLabel(Form) - self.label_5.setObjectName(_fromUtf8("label_5")) - self.gridlayout.addWidget(self.label_5, 3, 3, 1, 1) - self.length = QtGui.QLineEdit(Form) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(3) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.length.sizePolicy().hasHeightForWidth()) - self.length.setSizePolicy(sizePolicy) - self.length.setMinimumSize(QtCore.QSize(35, 0)) - self.length.setInputMask(_fromUtf8("")) - self.length.setAlignment(QtCore.Qt.AlignCenter) - self.length.setReadOnly(True) - self.length.setObjectName(_fromUtf8("length")) - self.gridlayout.addWidget(self.length, 3, 4, 1, 1) - self.label_6 = QtGui.QLabel(Form) - self.label_6.setObjectName(_fromUtf8("label_6")) - self.gridlayout.addWidget(self.label_6, 3, 6, 1, 1) - self.date = QtGui.QLineEdit(Form) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(5) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.date.sizePolicy().hasHeightForWidth()) - self.date.setSizePolicy(sizePolicy) - self.date.setMinimumSize(QtCore.QSize(65, 0)) - self.date.setAlignment(QtCore.Qt.AlignCenter) - self.date.setObjectName(_fromUtf8("date")) - self.gridlayout.addWidget(self.date, 3, 7, 1, 1) - spacerItem = QtGui.QSpacerItem(4, 20, QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Minimum) - self.gridlayout.addItem(spacerItem, 3, 8, 1, 1) - self.lookup = QtGui.QPushButton(Form) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(10) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.lookup.sizePolicy().hasHeightForWidth()) - self.lookup.setSizePolicy(sizePolicy) - self.lookup.setMinimumSize(QtCore.QSize(75, 0)) - self.lookup.setObjectName(_fromUtf8("lookup")) - self.gridlayout.addWidget(self.lookup, 3, 9, 1, 1) - spacerItem1 = QtGui.QSpacerItem(4, 20, QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Minimum) - self.gridlayout.addItem(spacerItem1, 3, 2, 1, 1) - spacerItem2 = QtGui.QSpacerItem(4, 20, QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Minimum) - self.gridlayout.addItem(spacerItem2, 3, 5, 1, 1) - self.vboxlayout.addLayout(self.gridlayout) - spacerItem3 = QtGui.QSpacerItem(20, 0, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) - self.vboxlayout.addItem(spacerItem3) - self.label.setBuddy(self.title) - self.label_2.setBuddy(self.artist) - self.label_3.setBuddy(self.album) - self.label_4.setBuddy(self.tracknumber) - self.label_5.setBuddy(self.length) - self.label_6.setBuddy(self.date) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - Form.setTabOrder(self.title, self.artist) - Form.setTabOrder(self.artist, self.album) - Form.setTabOrder(self.album, self.tracknumber) - Form.setTabOrder(self.tracknumber, self.length) - Form.setTabOrder(self.length, self.date) - Form.setTabOrder(self.date, self.lookup) - - def retranslateUi(self, Form): - self.label.setText(_("Title:")) - self.label_2.setText(_("Artist:")) - self.label_3.setText(_("Album:")) - self.label_4.setText(_("Track:")) - self.label_5.setText(_("Length:")) - self.label_6.setText(_("Date:")) - self.date.setInputMask(_("0000-00-00; ")) - self.lookup.setText(_("Lookup")) - diff --git a/picard/ui/ui_tageditor.py b/picard/ui/ui_tageditor.py deleted file mode 100644 index 97be405fa4..0000000000 --- a/picard/ui/ui_tageditor.py +++ /dev/null @@ -1,133 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'ui/tageditor.ui' -# -# Created: Thu Sep 15 13:39:10 2011 -# by: PyQt4 UI code generator 4.8.3 -# -# WARNING! All changes made in this file will be lost! - -from PyQt4 import QtCore, QtGui - -try: - _fromUtf8 = QtCore.QString.fromUtf8 -except AttributeError: - _fromUtf8 = lambda s: s - -class Ui_TagEditorDialog(object): - def setupUi(self, TagEditorDialog): - TagEditorDialog.setObjectName(_fromUtf8("TagEditorDialog")) - TagEditorDialog.resize(535, 436) - self.vboxlayout = QtGui.QVBoxLayout(TagEditorDialog) - self.vboxlayout.setSpacing(6) - self.vboxlayout.setMargin(9) - self.vboxlayout.setObjectName(_fromUtf8("vboxlayout")) - self.tabWidget = QtGui.QTabWidget(TagEditorDialog) - self.tabWidget.setObjectName(_fromUtf8("tabWidget")) - self.tab_4 = QtGui.QWidget() - self.tab_4.setObjectName(_fromUtf8("tab_4")) - self.vboxlayout1 = QtGui.QVBoxLayout(self.tab_4) - self.vboxlayout1.setSpacing(6) - self.vboxlayout1.setMargin(9) - self.vboxlayout1.setObjectName(_fromUtf8("vboxlayout1")) - self.tags = QtGui.QTreeWidget(self.tab_4) - self.tags.setRootIsDecorated(False) - self.tags.setObjectName(_fromUtf8("tags")) - self.vboxlayout1.addWidget(self.tags) - self.horizontalLayout = QtGui.QHBoxLayout() - self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) - self.ratingLabel = QtGui.QLabel(self.tab_4) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.ratingLabel.sizePolicy().hasHeightForWidth()) - self.ratingLabel.setSizePolicy(sizePolicy) - self.ratingLabel.setMinimumSize(QtCore.QSize(0, 0)) - self.ratingLabel.setObjectName(_fromUtf8("ratingLabel")) - self.horizontalLayout.addWidget(self.ratingLabel) - self.rating = RatingWidget(self.tab_4) - self.rating.setEnabled(True) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.rating.sizePolicy().hasHeightForWidth()) - self.rating.setSizePolicy(sizePolicy) - self.rating.setMinimumSize(QtCore.QSize(0, 0)) - self.rating.setObjectName(_fromUtf8("rating")) - self.horizontalLayout.addWidget(self.rating) - self.vboxlayout1.addLayout(self.horizontalLayout) - self.hboxlayout = QtGui.QHBoxLayout() - self.hboxlayout.setSpacing(6) - self.hboxlayout.setMargin(0) - self.hboxlayout.setObjectName(_fromUtf8("hboxlayout")) - self.tags_add = QtGui.QPushButton(self.tab_4) - self.tags_add.setObjectName(_fromUtf8("tags_add")) - self.hboxlayout.addWidget(self.tags_add) - self.tags_edit = QtGui.QPushButton(self.tab_4) - self.tags_edit.setObjectName(_fromUtf8("tags_edit")) - self.hboxlayout.addWidget(self.tags_edit) - self.tags_delete = QtGui.QPushButton(self.tab_4) - self.tags_delete.setObjectName(_fromUtf8("tags_delete")) - self.hboxlayout.addWidget(self.tags_delete) - spacerItem = QtGui.QSpacerItem(151, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.hboxlayout.addItem(spacerItem) - self.vboxlayout1.addLayout(self.hboxlayout) - self.tabWidget.addTab(self.tab_4, _fromUtf8("")) - self.tab_2 = QtGui.QWidget() - self.tab_2.setObjectName(_fromUtf8("tab_2")) - self.vboxlayout2 = QtGui.QVBoxLayout(self.tab_2) - self.vboxlayout2.setSpacing(6) - self.vboxlayout2.setMargin(9) - self.vboxlayout2.setObjectName(_fromUtf8("vboxlayout2")) - self.artwork_list = QtGui.QListWidget(self.tab_2) - self.artwork_list.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) - self.artwork_list.setIconSize(QtCore.QSize(170, 170)) - self.artwork_list.setMovement(QtGui.QListView.Static) - self.artwork_list.setFlow(QtGui.QListView.LeftToRight) - self.artwork_list.setProperty(_fromUtf8("isWrapping"), False) - self.artwork_list.setResizeMode(QtGui.QListView.Fixed) - self.artwork_list.setSpacing(10) - self.artwork_list.setViewMode(QtGui.QListView.IconMode) - self.artwork_list.setObjectName(_fromUtf8("artwork_list")) - self.vboxlayout2.addWidget(self.artwork_list) - self.tabWidget.addTab(self.tab_2, _fromUtf8("")) - self.tab_5 = QtGui.QWidget() - self.tab_5.setObjectName(_fromUtf8("tab_5")) - self.vboxlayout3 = QtGui.QVBoxLayout(self.tab_5) - self.vboxlayout3.setSpacing(6) - self.vboxlayout3.setMargin(9) - self.vboxlayout3.setObjectName(_fromUtf8("vboxlayout3")) - self.info = QtGui.QLabel(self.tab_5) - self.info.setText(_fromUtf8("")) - self.info.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) - self.info.setWordWrap(True) - self.info.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByKeyboard|QtCore.Qt.TextSelectableByMouse) - self.info.setObjectName(_fromUtf8("info")) - self.vboxlayout3.addWidget(self.info) - self.tabWidget.addTab(self.tab_5, _fromUtf8("")) - self.vboxlayout.addWidget(self.tabWidget) - self.buttonbox = QtGui.QDialogButtonBox(TagEditorDialog) - self.buttonbox.setOrientation(QtCore.Qt.Horizontal) - self.buttonbox.setObjectName(_fromUtf8("buttonbox")) - self.vboxlayout.addWidget(self.buttonbox) - - self.retranslateUi(TagEditorDialog) - self.tabWidget.setCurrentIndex(0) - QtCore.QMetaObject.connectSlotsByName(TagEditorDialog) - TagEditorDialog.setTabOrder(self.tags, self.tags_add) - TagEditorDialog.setTabOrder(self.tags_add, self.tags_delete) - TagEditorDialog.setTabOrder(self.tags_delete, self.tabWidget) - TagEditorDialog.setTabOrder(self.tabWidget, self.artwork_list) - - def retranslateUi(self, TagEditorDialog): - self.tags.headerItem().setText(0, _("Name")) - self.tags.headerItem().setText(1, _("Value")) - self.ratingLabel.setText(_("Rating:")) - self.tags_add.setText(_("&Add...")) - self.tags_edit.setText(_("&Edit...")) - self.tags_delete.setText(_("&Delete")) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_4), _("&Metadata")) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), _("A&rtwork")) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_5), _("&Info")) - -from picard.ui.ratingwidget import RatingWidget diff --git a/picard/util/tags.py b/picard/util/tags.py index 06115ae200..0fb4f374d3 100644 --- a/picard/util/tags.py +++ b/picard/util/tags.py @@ -78,6 +78,8 @@ 'mixer': N_('Mixer'), 'language': N_('Language'), 'script': N_('Script'), + '~length': N_('Length'), + '~rating': N_('Rating'), } def display_tag_name(name): diff --git a/resources/images/star-gray.png b/resources/images/star-gray.png new file mode 100644 index 0000000000..0c5ec7c988 Binary files /dev/null and b/resources/images/star-gray.png differ diff --git a/resources/images/star.png b/resources/images/star.png new file mode 100644 index 0000000000..ae629cdcc6 Binary files /dev/null and b/resources/images/star.png differ diff --git a/resources/picard.qrc b/resources/picard.qrc index 65b5e480d3..4b36323c0e 100644 --- a/resources/picard.qrc +++ b/resources/picard.qrc @@ -5,6 +5,8 @@ images/file.png images/file-pending.png images/note.png + images/star.png + images/star-gray.png images/16x16/picard.png images/24x24/picard.png images/32x32/picard.png @@ -13,7 +15,7 @@ images/256x256/picard.png images/22x22/picard-analyze.png images/16x16/media-optical.png - images/16x16/media-optical-saved.png + images/16x16/media-optical-saved.png images/22x22/media-optical.png images/22x22/media-optical-saved.png images/22x22/list-remove.png diff --git a/ui/edittagdialog.ui b/ui/edittagdialog.ui index c1211d75cb..e6f3bbb88d 100644 --- a/ui/edittagdialog.ui +++ b/ui/edittagdialog.ui @@ -1,55 +1,162 @@ - + + EditTagDialog - - + + + Qt::ApplicationModal + + 0 0 - 384 - 225 + 436 + 240 - - Edit Tag + + Qt::StrongFocus + + + Edit tag + + + true - - - 9 - - - 6 - + - - - true + + + Qt::StrongFocus - - - - - - true + + false - + false - - - Qt::Horizontal - - + + + + + + 100 + 0 + + + + + 100 + 0 + + + + Add value + + + false + + + + + + + + 120 + 0 + + + + + 120 + 0 + + + + Remove value + + + false + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 33 + 17 + + + + + + + + + 150 + 0 + + + + + 150 + 0 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Save + + + + - - buttonbox - name - value - - + + + buttonbox + accepted() + EditTagDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonbox + rejected() + EditTagDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + diff --git a/ui/infodialog.ui b/ui/infodialog.ui new file mode 100644 index 0000000000..6c647c229f --- /dev/null +++ b/ui/infodialog.ui @@ -0,0 +1,98 @@ + + + InfoDialog + + + + 0 + 0 + 535 + 436 + + + + + + + 0 + + + + &Info + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + A&rtwork + + + + + + QAbstractItemView::SingleSelection + + + + 170 + 170 + + + + QListView::Static + + + QListView::LeftToRight + + + false + + + QListView::Fixed + + + 10 + + + QListView::IconMode + + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + tabWidget + artwork_list + buttonBox + + + + diff --git a/ui/metadata.ui b/ui/metadata.ui deleted file mode 100644 index 56b235db58..0000000000 --- a/ui/metadata.ui +++ /dev/null @@ -1,270 +0,0 @@ - - - Form - - - - 0 - 0 - 380 - 143 - - - - - 0 - 0 - - - - - - - - 6 - - - 9 - - - - - 2 - - - 3 - - - 0 - - - - - Title: - - - title - - - - - - - - - - Artist: - - - artist - - - - - - - - - - Album: - - - album - - - - - - - - - - Track: - - - tracknumber - - - - - - - - 1 - 0 - - - - - 25 - 0 - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Length: - - - length - - - - - - - - 3 - 0 - - - - - 35 - 0 - - - - - - - Qt::AlignCenter - - - true - - - - - - - Date: - - - date - - - - - - - - 5 - 0 - - - - - 65 - 0 - - - - 0000-00-00; - - - Qt::AlignCenter - - - - - - - Qt::Horizontal - - - QSizePolicy::MinimumExpanding - - - - 4 - 20 - - - - - - - - - 10 - 0 - - - - - 75 - 0 - - - - Lookup - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 4 - 20 - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 4 - 20 - - - - - - - - - - Qt::Vertical - - - - 20 - 0 - - - - - - - - title - artist - album - tracknumber - length - date - lookup - - - - diff --git a/ui/tageditor.ui b/ui/tageditor.ui deleted file mode 100644 index 43656d97a3..0000000000 --- a/ui/tageditor.ui +++ /dev/null @@ -1,245 +0,0 @@ - - TagEditorDialog - - - - 0 - 0 - 535 - 436 - - - - - 6 - - - 9 - - - - - 0 - - - - &Metadata - - - - 6 - - - 9 - - - - - false - - - - Name - - - - - Value - - - - - - - - - - - 0 - 0 - - - - - 0 - 0 - - - - Rating: - - - - - - - true - - - - 0 - 0 - - - - - 0 - 0 - - - - - - - - - - 6 - - - 0 - - - - - &Add... - - - - - - - &Edit... - - - - - - - &Delete - - - - - - - Qt::Horizontal - - - - 151 - 20 - - - - - - - - tags - rating - horizontalLayoutWidget - ratingLabel - - - - A&rtwork - - - - 6 - - - 9 - - - - - QAbstractItemView::SingleSelection - - - - 170 - 170 - - - - QListView::Static - - - QListView::LeftToRight - - - false - - - QListView::Fixed - - - 10 - - - QListView::IconMode - - - - - - - - &Info - - - - 6 - - - 9 - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - - - - Qt::Horizontal - - - - - - - - RatingWidget - QWidget -
picard/ui/ratingwidget
- 1 -
-
- - tags - tags_add - tags_delete - tabWidget - artwork_list - - - -