~ubuntu-branches/debian/wheezy/quodlibet/wheezy

« back to all changes in this revision

Viewing changes to browsers/albums.py

  • Committer: Bazaar Package Importer
  • Author(s): Luca Falavigna
  • Date: 2009-01-30 23:55:34 UTC
  • mto: (18.1.1 squeeze) (2.1.9 sid)
  • mto: This revision was merged to the branch mainline in revision 23.
  • Revision ID: james.westby@ubuntu.com-20090130235534-45857nfsgobw4apc
Tags: upstream-2.0
ImportĀ upstreamĀ versionĀ 2.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- coding: utf-8 -*-
2
 
# Copyright 2004-2005 Joe Wreschnig, Michael Urman, IƱigo Serna
3
 
#
4
 
# This program is free software; you can redistribute it and/or modify
5
 
# it under the terms of the GNU General Public License version 2 as
6
 
# published by the Free Software Foundation
7
 
#
8
 
# $Id: albums.py 4024 2007-04-27 02:00:16Z piman $
9
 
 
10
 
import os
11
 
 
12
 
import gobject
13
 
import gtk
14
 
import pango
15
 
 
16
 
import config
17
 
import const
18
 
import qltk
19
 
import stock
20
 
import util
21
 
 
22
 
from browsers._base import Browser
23
 
from formats import PEOPLE
24
 
from parse import Query, XMLFromPattern
25
 
from qltk.ccb import ConfigCheckButton
26
 
from qltk.completion import EntryWordCompletion
27
 
from qltk.entry import ValidatingEntry
28
 
from qltk.songsmenu import SongsMenu
29
 
from qltk.textedit import PatternEditBox
30
 
from qltk.views import AllTreeView
31
 
from util import copool, tag
32
 
 
33
 
ELPOEP = list(PEOPLE); ELPOEP.reverse()
34
 
EMPTY = _("Songs not in an album")
35
 
PATTERN = r"""\<b\><title|\<i\><title>\</i\>|%s>\</b\><date| (<date>)>
36
 
\<small\><~discs|<~discs> - ><~tracks> - <~long-length>\</small\>
37
 
<people>""" % EMPTY
38
 
 
39
 
class AlbumTagCompletion(EntryWordCompletion):
40
 
    def __init__(self):
41
 
        super(AlbumTagCompletion, self).__init__()
42
 
        try: model = self.__model
43
 
        except AttributeError:
44
 
            model = type(self).__model = gtk.ListStore(str)
45
 
            self.__refreshmodel()
46
 
        self.set_model(model)
47
 
        self.set_text_column(0)
48
 
 
49
 
    def __refreshmodel(self, *args):
50
 
        for tag in ["title", "album", "date", "people", "artist", "genre"]:
51
 
            self.__model.append(row=[tag])
52
 
        for tag in ["tracks", "discs", "length", "date"]:
53
 
            self.__model.append(row=["#(" + tag])
54
 
        for tag in ["rating", "playcount", "skipcount"]:
55
 
            for suffix in ["avg", "max", "min", "sum"]:
56
 
                self.__model.append(row=["#(%s:%s" % (tag, suffix)])
57
 
 
58
 
class Preferences(qltk.Window):
59
 
    def __init__(self):
60
 
        super(Preferences, self).__init__()
61
 
        self.set_border_width(12)
62
 
        self.set_title(_("Album List Preferences") + " - Quod Libet")
63
 
        self.add(gtk.VBox(spacing=6))
64
 
        self.set_default_size(300, 200)
65
 
        self.connect_object('delete-event', Preferences.__delete_event, self)
66
 
 
67
 
        cb = ConfigCheckButton(
68
 
            _("Show album _covers"), "browsers", "album_covers")
69
 
        cb.set_active(config.getboolean("browsers", "album_covers"))
70
 
        cb.connect('toggled', lambda s: AlbumList.toggle_covers())
71
 
        self.child.pack_start(cb, expand=False)
72
 
 
73
 
        vbox = gtk.VBox(spacing=6)
74
 
        label = gtk.Label()
75
 
        label.set_alignment(0.0, 0.5)
76
 
        edit = PatternEditBox(PATTERN)
77
 
        edit.text = AlbumList._Album._pattern_text
78
 
        edit.apply.connect('clicked', self.__set_pattern, edit)
79
 
        edit.buffer.connect_object(
80
 
            'changed', self.__preview_pattern, edit, label)
81
 
        vbox.pack_start(label, expand=False)
82
 
        vbox.pack_start(edit)
83
 
        self.__preview_pattern(edit, label)
84
 
        f = qltk.Frame(_("Album Display"), child=vbox)
85
 
        self.child.pack_start(f)
86
 
 
87
 
        self.child.show_all()
88
 
 
89
 
    def __delete_event(self, event):
90
 
        self.hide()
91
 
        return True
92
 
 
93
 
    def __set_pattern(self, apply, edit):
94
 
        AlbumList.refresh_pattern(edit.text)
95
 
 
96
 
    def __preview_pattern(self, edit, label):
97
 
        album = AlbumList._Album(
98
 
            util.tag("album"), util.tag("labelid"),
99
 
            util.tag("musicbrainz_albumid"))
100
 
        album.date = "2004-10-31"
101
 
        album.length = 6319
102
 
        album.discs = 2
103
 
        album.tracks = 5
104
 
        album.people = [tag("artist"), tag("performer"), tag("arranger")]
105
 
        album.genre = tag("genre")
106
 
        try: text = XMLFromPattern(edit.text) % album
107
 
        except:
108
 
            text = _("Invalid pattern")
109
 
            edit.apply.set_sensitive(False)
110
 
        try: pango.parse_markup(text, u"\u0000")
111
 
        except gobject.GError:
112
 
            text = _("Invalid pattern")
113
 
            edit.apply.set_sensitive(False)
114
 
        else: edit.apply.set_sensitive(True)
115
 
        label.set_markup(text)
116
 
 
117
 
class AlbumList(Browser, gtk.VBox, util.InstanceTracker):
118
 
    expand = qltk.RHPaned
119
 
    __gsignals__ = Browser.__gsignals__
120
 
    __model = None
121
 
 
122
 
    name = _("Album List")
123
 
    accelerated_name = _("_Album List")
124
 
    priority = 4
125
 
 
126
 
    @classmethod
127
 
    def init(klass, library):
128
 
        pattern_fn = os.path.join(const.USERDIR, "album_pattern")
129
 
        try:
130
 
            klass._Album._pattern_text = file(pattern_fn).read().rstrip()
131
 
        except EnvironmentError: pass
132
 
        else:
133
 
            klass._Album._pattern = XMLFromPattern(klass._Album._pattern_text)
134
 
        try:
135
 
            klass._Album.cover = gtk.gdk.pixbuf_new_from_file_at_size(
136
 
                stock.NO_ALBUM, 48, 48)
137
 
        except RuntimeError:
138
 
            klass._Album.cover = None
139
 
 
140
 
    @classmethod
141
 
    def toggle_covers(klass):
142
 
        on = config.getboolean("browsers", "album_covers")
143
 
        for albumlist in klass.instances():
144
 
            albumlist.__cover_column.set_visible(on)
145
 
 
146
 
    @classmethod
147
 
    def refresh_pattern(klass, pattern_text):
148
 
        if pattern_text == klass._Album._pattern_text: return
149
 
        klass._Album._pattern_text = pattern_text
150
 
        klass._Album._pattern = XMLFromPattern(pattern_text)
151
 
        for album in [row[0] for row in klass.__model if row[0] is not None]:
152
 
            album.finalize(cover=False)
153
 
        pattern_fn = os.path.join(const.USERDIR, "album_pattern")
154
 
        f = file(pattern_fn, "w")
155
 
        f.write(pattern_text  + "\n")
156
 
        f.close()
157
 
 
158
 
    @classmethod
159
 
    def _init_model(klass, library):
160
 
        klass.__model = model = klass._AlbumStore(object)
161
 
        library.connect('removed', klass.__remove_songs, model)
162
 
        library.connect('changed', klass.__changed_songs, model)
163
 
        library.connect('added', klass.__add_songs, model)
164
 
        klass.__add_songs(library, library.values(), model)
165
 
        model.append(row=[None])
166
 
 
167
 
    @classmethod
168
 
    def __changed_songs(klass, library, changed, model):
169
 
        to_update = klass.__remove_songs(library, changed, model, False)
170
 
        to_update.update(klass.__add_songs(library, changed, model, False))
171
 
        klass.__update(to_update, model)
172
 
 
173
 
    @classmethod
174
 
    def __update(klass, changed, model):
175
 
        to_change = []
176
 
        to_remove = []
177
 
        for row in model:
178
 
            album = row[0]
179
 
            if album is not None and album.key in changed:
180
 
                if album.songs:
181
 
                    to_change.append((row.path, row.iter))
182
 
                    album.finalize()
183
 
                else:
184
 
                    to_remove.append(row.iter)
185
 
                    album._model = album._iter = None
186
 
        if to_change: map(model.row_changed, *zip(*to_change))
187
 
        if to_remove: map(model.remove, to_remove)
188
 
 
189
 
    @classmethod
190
 
    def __remove_songs(klass, library, removed, model, update=True):
191
 
        changed = set()
192
 
        for row in model:
193
 
            album = row[0]
194
 
            if album is not None and True in map(album.remove, removed):
195
 
                changed.add(album.key)
196
 
        if update: klass.__update(changed, model)
197
 
        else: return changed
198
 
 
199
 
    @classmethod
200
 
    def __add_songs(klass, library, added, model, update=True):
201
 
        albums = model.get_albums()
202
 
        changed = set() # Keys of changed albums
203
 
        new = [] # Added album instances
204
 
        for song in added:
205
 
            labelid = song.get("labelid", "")
206
 
            mbid = song.get("musicbrainz_albumid", "")
207
 
            key = song.album_key
208
 
            if key not in albums:
209
 
                new_album = klass._Album(song("album"), labelid, mbid)
210
 
                albums[key] = new_album
211
 
                new.append(new_album)
212
 
            albums[key].songs.add(song)
213
 
            changed.add(key)
214
 
        for album in new:
215
 
            model.append(row=[album])
216
 
        if update: klass.__update(changed, model)
217
 
        else: return changed
218
 
 
219
 
    # Something like an AudioFile, but for a whole album.
220
 
    class _Album(object):
221
 
        __pending_covers = []
222
 
 
223
 
        _pattern_text = PATTERN
224
 
        _pattern = XMLFromPattern(PATTERN)
225
 
 
226
 
        length = 0
227
 
        discs = 1
228
 
        tracks = 0
229
 
        date = ""
230
 
        markup = ""
231
 
 
232
 
        def __init__(self, title, labelid, mbid):
233
 
            self.people = []
234
 
            self.songs = set()
235
 
            self.title = title
236
 
            # The key uniquely identifies the album; this way, albums
237
 
            # with different MBIDs or different label IDs but the same
238
 
            # title are different, and albums with different MBIDs
239
 
            # but the same label ID are the same (since MB uses separate
240
 
            # MBIDs for each disc).
241
 
            self.key = (title, labelid or mbid)
242
 
 
243
 
        def get(self, key, default="", connector=u" - "):
244
 
            if "~" in key[1:]:
245
 
                return connector.join(map(self.get, util.tagsplit(key)))
246
 
            elif key == "~#length": return self.length
247
 
            elif key == "~#tracks": return self.tracks
248
 
            elif key == "~#discs": return self.discs
249
 
            elif key == "~length": return util.format_time(self.length)
250
 
            elif key == "date": return self.date
251
 
            elif key == "~long-length":
252
 
                return util.format_time_long(self.length)
253
 
            elif key in ["cover", "~cover"]: return (self.cover and "y") or ""
254
 
            elif key in ["title", "album"]: return self.title
255
 
            elif key == "people":
256
 
                return "\n".join(self.people)
257
 
            elif key.startswith("~#") and key[-4:-3] != ":": key += ":avg"
258
 
            elif key == "~tracks":
259
 
                return ngettext(
260
 
                    "%d track", "%d tracks", self.tracks) % self.tracks
261
 
            elif key == "~discs":
262
 
                if self.discs > 1:
263
 
                    return ngettext(
264
 
                        "%d disc", "%d discs", self.discs) % self.discs
265
 
                else: return default
266
 
 
267
 
            if key.startswith("~#") and key[-4:-3] == ":":
268
 
                # Using key:<func> runs the resulting list of values
269
 
                # through the function before returning it.
270
 
                func = key[-3:]
271
 
                key = key[:-4]
272
 
                func = {"max": max, "min": min, "sum": sum,
273
 
                        "avg": lambda s: float(sum(s)) / len(s)}.get(func)
274
 
                if func: return func([song(key, 0) for song in self.songs])
275
 
 
276
 
            # Otherwise, if the tag isn't one provided by the album
277
 
            # object, look in songs for it.
278
 
            values = set()
279
 
            for song in self.songs: values.update(song.list(key))
280
 
            value = u"\n".join(list(values))
281
 
            return value or default
282
 
 
283
 
        __call__ = get
284
 
        def comma(self, *args):
285
 
            return self.get(*args).replace("\n", ", ")
286
 
 
287
 
        # All songs added, cache info.
288
 
        def finalize(self, cover=True):
289
 
            self.tracks = len(self.songs)
290
 
            self.length = 0
291
 
            people = {}
292
 
            for song in self.songs:
293
 
                # Rank people by "relevance" -- artists before composers
294
 
                # before performers, then by number of appearances.
295
 
                for w, key in enumerate(ELPOEP):
296
 
                    for person in song.list(key):
297
 
                        people[person] = people.get(person, 0) - 1000 ** w
298
 
 
299
 
                self.discs = max(self.discs, song("~#disc", 0))
300
 
                self.length += song.get("~#length", 0)
301
 
 
302
 
            self.people = sorted(people.keys(), key=people.__getitem__)[:100]
303
 
 
304
 
            if not self.title:
305
 
                self.date = ""
306
 
                self.discs = 1
307
 
            else: self.date = song.comma("date")
308
 
 
309
 
            self.markup = self._pattern % self
310
 
            self._model[self._iter][0] = self
311
 
 
312
 
        def remove(self, song):
313
 
            try: self.songs.remove(song)
314
 
            except KeyError: return False
315
 
            else: return True
316
 
 
317
 
    # An auto-searching entry; it wraps is a TreeModelFilter whose parent
318
 
    # is the album list.
319
 
    class FilterEntry(ValidatingEntry):
320
 
        def __init__(self, model):
321
 
            ValidatingEntry.__init__(self, Query.is_valid_color)
322
 
            self.connect_object('changed', self.__filter_changed, model)
323
 
            self.set_completion(AlbumTagCompletion())
324
 
            self.__refill_id = None
325
 
            self.__filter = None
326
 
            self.inhibit = False
327
 
            model.set_visible_func(self.__parse)
328
 
 
329
 
        def __parse(self, model, iter):
330
 
            if self.__filter is None: return True
331
 
            elif model[iter][0] is None: return True
332
 
            else: return self.__filter(model[iter][0])
333
 
 
334
 
        def __filter_changed(self, model):
335
 
            if self.__refill_id is not None:
336
 
                gobject.source_remove(self.__refill_id)
337
 
                self.__refill_id = None
338
 
            text = self.get_text().decode('utf-8')
339
 
            if Query.is_parsable(text):
340
 
                if not text: self.__filter = None
341
 
                else:
342
 
                    self.__filter = Query(
343
 
                        text, star=["people", "album"]).search
344
 
                self.__refill_id = gobject.timeout_add(
345
 
                    500, self.__refilter, model)
346
 
 
347
 
        def __refilter(self, model):
348
 
            self.inhibit = True
349
 
            model.refilter()
350
 
            self.inhibit = False
351
 
 
352
 
    # Sorting, either by people or album title. It wraps a TreeModelSort
353
 
    # whose parent is the album list.
354
 
    class SortCombo(gtk.ComboBox):
355
 
        def __init__(self, model):
356
 
            # Contains string to display, function to do sorting
357
 
            cbmodel = gtk.ListStore(str)
358
 
            gtk.ComboBox.__init__(self, cbmodel)
359
 
            cell = gtk.CellRendererText()
360
 
            self.pack_start(cell, True)
361
 
            self.add_attribute(cell, 'text', 0)
362
 
            model.set_sort_func(100, self.__compare_title)
363
 
            model.set_sort_func(101, self.__compare_artist)
364
 
            model.set_sort_func(102, self.__compare_date)
365
 
 
366
 
            for text in [
367
 
                _("Sort by title"), _("Sort by artist"), _("Sort by date")
368
 
                ]: cbmodel.append(row=[text])
369
 
 
370
 
            self.connect_object('changed', self.__set_cmp_func, model)
371
 
            try: active = config.getint('browsers', 'album_sort')
372
 
            except: active = 0
373
 
            self.set_active(active)
374
 
 
375
 
        def __set_cmp_func(self, model):
376
 
            active = self.get_active()
377
 
            config.set("browsers", "album_sort", str(active))
378
 
            model.set_sort_column_id(100 + active, gtk.SORT_ASCENDING)
379
 
 
380
 
        def __compare_title(self, model, i1, i2):
381
 
            a1, a2 = model[i1][0], model[i2][0]
382
 
            if (a1 and a2) is None: return cmp(a1, a2)
383
 
            elif not a1.title: return 1
384
 
            elif not a2.title: return -1
385
 
            else: return cmp(a1.key, a2.key)
386
 
 
387
 
        def __compare_artist(self, model, i1, i2):
388
 
            a1, a2 = model[i1][0], model[i2][0]
389
 
            if (a1 and a2) is None: return cmp(a1, a2)
390
 
            elif not a1.title: return 1
391
 
            elif not a2.title: return -1
392
 
            elif not a1.people and a2.people: return 1
393
 
            elif not a2.people and a1.people: return -1
394
 
            else: return (cmp(a1.people and a1.people[0],
395
 
                              a2.people and a2.people[0]) or
396
 
                          cmp(a1.date or "ZZZZ", a2.date or "ZZZZ") or
397
 
                          cmp(a1.key, a2.key))
398
 
 
399
 
        def __compare_date(self, model, i1, i2):
400
 
            a1, a2 = model[i1][0], model[i2][0]
401
 
            if (a1 and a2) is None: return cmp(a1, a2)
402
 
            elif not a1.title: return 1
403
 
            elif not a2.title: return -1
404
 
            elif not a1.date and a2.date: return 1
405
 
            elif not a2.date and a1.date: return -1
406
 
            else: return (cmp(a1.date, a2.date) or cmp(a1.key, a2.key))
407
 
 
408
 
    class _AlbumStore(gtk.ListStore):
409
 
        __gsignals__ = { "row-changed": "override" }
410
 
 
411
 
        def __init__(self, *args, **kwargs):
412
 
            super(AlbumList._AlbumStore, self).__init__(*args, **kwargs)
413
 
            self.__pending_covers = []
414
 
 
415
 
        def do_row_changed(self, path, iter):
416
 
            album = self[iter][0]
417
 
            if album is None:
418
 
                return
419
 
            album._model = self
420
 
            album._iter = iter
421
 
            if album.title and album.cover is type(album).cover:
422
 
                if not self.__pending_covers:
423
 
                    copool.add(self.__scan_covers)
424
 
                self.__pending_covers.append(album)
425
 
 
426
 
        def __scan_covers(self):
427
 
            while self.__pending_covers:
428
 
                album = self.__pending_covers.pop()
429
 
                if album._iter is None or album.cover is not type(album).cover:
430
 
                    continue
431
 
                song = list(album.songs)[0]
432
 
                cover = song.find_cover()
433
 
                if cover is not None:
434
 
                    try:
435
 
                        cover = gtk.gdk.pixbuf_new_from_file_at_size(
436
 
                            cover.name, 48, 48)
437
 
                    except StandardError:
438
 
                        continue
439
 
                    else:
440
 
                        # add a black outline
441
 
                        w, h = cover.get_width(), cover.get_height()
442
 
                        newcover = gtk.gdk.Pixbuf(
443
 
                            gtk.gdk.COLORSPACE_RGB, True, 8, w + 2, h + 2)
444
 
                        newcover.fill(0x000000ff)
445
 
                        cover.copy_area(0, 0, w, h, newcover, 1, 1)
446
 
                        album.cover = newcover
447
 
                        self[album._iter][0] = album
448
 
                yield True
449
 
 
450
 
        def get_albums(self):
451
 
            albums = [row[0] for row in self]
452
 
            try: albums.remove(None)
453
 
            except ValueError: pass
454
 
            return dict([(a.key, a) for a in albums])
455
 
 
456
 
    def __init__(self, library, player):
457
 
        super(AlbumList, self).__init__(spacing=6)
458
 
        self._register_instance()
459
 
        if self.__model is None: AlbumList._init_model(library)
460
 
        self.__save = bool(player)
461
 
 
462
 
        sw = gtk.ScrolledWindow()
463
 
        sw.set_shadow_type(gtk.SHADOW_IN)
464
 
        view = AllTreeView()
465
 
        view.set_headers_visible(False)
466
 
        model_sort = gtk.TreeModelSort(self.__model)
467
 
        model_filter = model_sort.filter_new()
468
 
 
469
 
        render = gtk.CellRendererPixbuf()
470
 
        self.__cover_column = column = gtk.TreeViewColumn("covers", render)
471
 
        column.set_visible(config.getboolean("browsers", "album_covers"))
472
 
        column.set_sizing(gtk.TREE_VIEW_COLUMN_GROW_ONLY)
473
 
        render.set_property('xpad', 2)
474
 
        render.set_property('ypad', 2)
475
 
        render.set_property('width', 56)
476
 
        render.set_property('height', 56)
477
 
 
478
 
        def cell_data_pb(column, cell, model, iter):
479
 
            album = model[iter][0]
480
 
            if album is None: cell.set_property('pixbuf', None)
481
 
            elif album.cover: cell.set_property('pixbuf', album.cover)
482
 
            else: cell.set_property('pixbuf', None)
483
 
        column.set_cell_data_func(render, cell_data_pb)
484
 
        view.append_column(column)
485
 
 
486
 
        render = gtk.CellRendererText()
487
 
        column = gtk.TreeViewColumn("albums", render)
488
 
        render.set_property('ellipsize', pango.ELLIPSIZE_END)
489
 
        def cell_data(column, cell, model, iter):
490
 
            album = model[iter][0]
491
 
            if album is None:
492
 
                text = "<b>%s</b>" % _("All Albums")
493
 
                text += "\n" + ngettext("%d album", "%d albums",
494
 
                        len(model) - 1) % (len(model) - 1)
495
 
                cell.markup = text
496
 
            else: cell.markup = model[iter][0].markup
497
 
            cell.set_property('markup', cell.markup)
498
 
        column.set_cell_data_func(render, cell_data)
499
 
        view.append_column(column)
500
 
 
501
 
        view.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
502
 
        view.set_rules_hint(True)
503
 
        view.set_search_equal_func(self.__search_func)
504
 
        view.set_search_column(0)
505
 
        sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
506
 
        sw.add(view)
507
 
        e = self.FilterEntry(model_filter)
508
 
        hb2 = gtk.HBox()
509
 
        hb2.pack_start(e)
510
 
        hb2.pack_start(qltk.ClearButton(e), expand=False)
511
 
 
512
 
        if player: view.connect('row-activated', self.__play_selection, player)
513
 
        view.get_selection().connect('changed', self.__selection_changed, e)
514
 
 
515
 
        targets = [("text/x-quodlibet-songs", gtk.TARGET_SAME_APP, 1),
516
 
                   ("text/uri-list", 0, 2)]
517
 
        view.drag_source_set(
518
 
            gtk.gdk.BUTTON1_MASK, targets, gtk.gdk.ACTION_COPY)
519
 
        view.connect("drag-data-get", self.__drag_data_get)
520
 
        view.connect_object('popup-menu', self.__popup, view, library)
521
 
 
522
 
        hb = gtk.HBox(spacing=6)
523
 
        hb.pack_start(self.SortCombo(model_sort), expand=False)
524
 
        hb.pack_start(hb2)
525
 
        prefs = gtk.Button()
526
 
        prefs.add(
527
 
            gtk.image_new_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU))
528
 
        prefs.connect('clicked', self.__preferences)
529
 
        hb.pack_start(prefs, expand=False)
530
 
        self.pack_start(hb, expand=False)
531
 
        self.pack_start(sw, expand=True)
532
 
        view.set_model(model_filter)
533
 
        self.show_all()
534
 
 
535
 
    def __search_func(self, model, column, key, iter):
536
 
        try: value = model[iter][0].title
537
 
        except AttributeError: return True
538
 
        else:
539
 
            key = key.decode('utf-8')
540
 
            return not (value.startswith(key) or value.lower().startswith(key))
541
 
        
542
 
    def __popup(self, view, library):
543
 
        songs = self.__get_selected_songs(view.get_selection())
544
 
        menu = SongsMenu(library, songs)
545
 
 
546
 
        button = gtk.ImageMenuItem(gtk.STOCK_REFRESH)
547
 
        button.connect('activate', self.__refresh_album, view.get_selection())
548
 
        menu.prepend(gtk.SeparatorMenuItem())
549
 
        menu.prepend(button)
550
 
        menu.show_all()
551
 
        return view.popup_menu(menu, 0, gtk.get_current_event_time())
552
 
 
553
 
    def __refresh_album(self, menuitem, selection):
554
 
        model, rows = selection.get_selected_rows()
555
 
        albums = [model[row][0] for row in rows]
556
 
        if None in albums:
557
 
            albums = [model[row][0] for row in model]
558
 
        for album in albums:
559
 
            album.cover = type(album).cover
560
 
            album.finalize()
561
 
 
562
 
    def __get_selected_albums(self, selection):
563
 
        model, rows = selection.get_selected_rows()
564
 
        if not model or not rows: return set([])
565
 
        albums = [model[row][0] for row in rows]
566
 
        if None in albums: return None
567
 
        else: return albums
568
 
 
569
 
    def __get_selected_songs(self, selection):
570
 
        model, rows = selection.get_selected_rows()
571
 
        if not model or not rows: return []
572
 
        albums = [model[row][0] for row in rows]
573
 
        if None in albums:
574
 
            albums = [row[0] for row in model if row[0]]
575
 
        # Sort first by how the albums appear in the model itself,
576
 
        # then within the album using the default order.
577
 
        songs = []
578
 
        for album in albums:
579
 
            songs.extend(sorted(album.songs))
580
 
        return songs
581
 
 
582
 
    def __drag_data_get(self, view, ctx, sel, tid, etime):
583
 
        songs = self.__get_selected_songs(view.get_selection())
584
 
        if tid == 1:
585
 
            filenames = [song("~filename") for song in songs]
586
 
            sel.set("text/x-quodlibet-songs", 8, "\x00".join(filenames))
587
 
        else: sel.set_uris([song("~uri") for song in songs])
588
 
        
589
 
    def __play_selection(self, view, indices, col, player):
590
 
        player.reset()
591
 
 
592
 
    def __preferences(self, button):
593
 
        try: prefs = AlbumList.__prefs_win
594
 
        except AttributeError: prefs = AlbumList.__prefs_win = Preferences()
595
 
        win = qltk.get_top_parent(self)
596
 
        top, left = win.get_position()
597
 
        w, h = win.get_size()
598
 
        dw, dh = prefs.get_size()
599
 
        prefs.move((left + w // 2) - dw // 2, (top + h // 2) - dh // 2)
600
 
        prefs.present()
601
 
 
602
 
    def filter(self, key, values):
603
 
        assert(key == "album")
604
 
        if not values: values = [""]
605
 
        view = self.get_children()[1].child
606
 
        selection = view.get_selection()
607
 
        selection.unselect_all()
608
 
        model = view.get_model()
609
 
        first = None
610
 
        for row in model:
611
 
            if row[0] is not None and row[0].title in values:
612
 
                selection.select_path(row.path)
613
 
                if first is None:
614
 
                    view.set_cursor(row.path)
615
 
                    first = row.path[0]
616
 
        if first:
617
 
            view.scroll_to_cell(first, use_align=True, row_align=0.5)
618
 
 
619
 
    def unfilter(self):
620
 
        view = self.get_children()[1].child
621
 
        selection = view.get_selection()
622
 
        selection.unselect_all()
623
 
        selection.select_path((0,))
624
 
 
625
 
    def activate(self):
626
 
        self.get_children()[1].child.get_selection().emit('changed')
627
 
 
628
 
    def can_filter(self, key):
629
 
        return (key == "album")
630
 
 
631
 
    def list(self, key):
632
 
        assert (key == "album")
633
 
        view = self.get_children()[1].child
634
 
        model = view.get_model()
635
 
        return [row[0].title for row in model if row[0]]
636
 
 
637
 
    def restore(self):
638
 
        albums = config.get("browsers", "albums").split("\n")
639
 
        view = self.get_children()[1].child
640
 
        selection = view.get_selection()
641
 
        # FIXME: If albums is "" then it could be either all albums or
642
 
        # no albums. If it's "" and some other stuff, assume no albums,
643
 
        # otherwise all albums.
644
 
        selection.unselect_all()
645
 
        if albums == [""]:  selection.select_path((0,))
646
 
        else:
647
 
            model = selection.get_tree_view().get_model()
648
 
            first = None
649
 
            for row in model:
650
 
                if row[0] is not None and row[0].title in albums:
651
 
                    selection.select_path(row.path)
652
 
                    first = first or row.path
653
 
 
654
 
            if first:
655
 
                view.scroll_to_cell(first[0], use_align=True, row_align=0.5)
656
 
 
657
 
    def scroll(self, song):
658
 
        view = self.get_children()[1].child
659
 
        model = view.get_model()
660
 
        values = song.list("album")
661
 
        album_key = song.album_key
662
 
        for row in model:
663
 
            if row[0] is not None and row[0].key == album_key:
664
 
                view.scroll_to_cell(row.path[0], use_align=True, row_align=0.5)
665
 
                break
666
 
 
667
 
    def __selection_changed(self, selection, sort):
668
 
        if sort.inhibit: return
669
 
        songs = self.__get_selected_songs(selection)
670
 
        albums = self.__get_selected_albums(selection)
671
 
        if not songs: return
672
 
        self.emit('songs-selected', songs, None)
673
 
        if self.__save:
674
 
            if albums is None: config.set("browsers", "albums", "")
675
 
            else:
676
 
                confval = "\n".join([a.title for a in albums])
677
 
                # Since ConfigParser strips a trailing \n...
678
 
                if confval and confval[-1] == "\n":
679
 
                    confval = "\n" + confval[:-1]
680
 
                config.set("browsers", "albums", confval)
681
 
 
682
 
browsers = [AlbumList]