Beispiel #1
0
 def Query(self, text):
     if text is not None:
         query = Query(text, star=SongList.star)
         if query.is_parsable:
             return [self.__dict(s) for s in itervalues(self.library)
                     if query.search(s)]
     return None
Beispiel #2
0
    def GetSubsearchResultSet(self, previous_results, terms):
        query = Query("")
        for term in terms:
            query &= Query(term)

        songs = get_songs_for_ids(app.library, previous_results)
        ids = [get_song_id(s) for s in songs if query.search(s)]
        return ids
Beispiel #3
0
    def activate(self, widget=None, resort=True):
        songs = self._get_playlist_songs()

        text = self._get_text()
        # TODO: remove static dependency on Query
        if Query.is_parsable(text):
            self._query = Query(text, SongList.star)
            songs = self._query.filter(songs)
        GLib.idle_add(self.songs_selected, songs, resort)
Beispiel #4
0
def QueryValidator(string):
    """Returns True/False for a query, None for a text only query"""

    type_ = Query.get_type(string)
    if type_ == QueryType.VALID:
        # in case of an empty but valid query we say it's "text"
        if Query.match_all(string):
            return None
        return True
    elif type_ == QueryType.INVALID:
        return False
    return None
Beispiel #5
0
    def test_extension(self):
        self.failUnless(Query.is_valid("@(name)"))
        self.failUnless(Query.is_valid("@(name: extension body)"))
        self.failUnless(Query.is_valid("@(name: body (with (nested) parens))"))
        self.failUnless(Query.is_valid(r"@(name: body \\ with \) escapes)"))

        self.failIf(Query.is_valid("@()"))
        self.failIf(Query.is_valid(r"@(invalid %name!\\)"))
        self.failIf(Query.is_valid("@(name: mismatched ( parenthesis)"))
        self.failIf(Query.is_valid(r"@(\()"))
        self.failIf(Query.is_valid("@(name:unclosed body"))
        self.failIf(Query.is_valid("@ )"))
Beispiel #6
0
    def restore(self):
        text = config.get("browsers", "query_text").decode("utf-8")
        entry = self.__search
        entry.set_text(text)

        # update_filter expects a parsable query
        if Query.is_parsable(text):
            self.__update_filter(entry, text, scroll_up=False, restore=True)

        keys = config.get("browsers", "albums").split("\n")

        # FIXME: If albums is "" then it could be either all albums or
        # no albums. If it's "" and some other stuff, assume no albums,
        # otherwise all albums.
        self.__inhibit()
        if keys == [""]:
            self.view.set_cursor((0,))
        else:

            def select_fun(row):
                album = row[0]
                if not album:  # all
                    return False
                return album.str_key in keys
            self.view.select_by_func(select_fun)
        self.__uninhibit()
Beispiel #7
0
 def _get_songs(self):
     try:
         self._query = Query(self._text, star=SongList.star)
     except Query.error:
         pass
     else:
         return self._query.filter(self._library)
Beispiel #8
0
 def filter_text(self, text):
     self.__search.set_text(text)
     if Query.is_parsable(text):
         self.__update_filter(self.__search, text)
         self.__inhibit()
         self.view.set_cursor((0,))
         self.__uninhibit()
         self.activate()
Beispiel #9
0
    def __update_filter(self, entry, text):
        self.__filter = None
        if not Query.match_all(text):
            tags = self.__model.tags + ["album"]
            self.__filter = Query(text, star=tags).search
        self.__bg_filter = background_filter()

        self.view.get_model().refilter()
Beispiel #10
0
    def test_numcmp(self):
        self.failUnless(Query.is_valid("#(t < 3)"))
        self.failUnless(Query.is_valid("#(t <= 3)"))
        self.failUnless(Query.is_valid("#(t > 3)"))
        self.failUnless(Query.is_valid("#(t >= 3)"))
        self.failUnless(Query.is_valid("#(t = 3)"))
        self.failUnless(Query.is_valid("#(t != 3)"))

        self.failIf(Query.is_valid("#(t !> 3)"))
        self.failIf(Query.is_valid("#(t >> 3)"))
Beispiel #11
0
 def test_not(self):
     self.failUnless(Query.is_valid('t = !/a/'))
     self.failUnless(Query.is_valid('t = !!/a/'))
     self.failUnless(Query.is_valid('!t = "a"'))
     self.failUnless(Query.is_valid('!!t = "a"'))
     self.failUnless(Query.is_valid('t = !|(/a/, !"b")'))
     self.failUnless(Query.is_valid('t = !!|(/a/, !"b")'))
     self.failUnless(Query.is_valid('!|(t = /a/)'))
Beispiel #12
0
 def activate(self):
     text = self._get_text()
     if Query.is_parsable(text):
         star = dict.fromkeys(SongList.star)
         star.update(self.__star)
         self._filter = Query(text, star.keys()).search
         songs = filter(self._filter, self._library)
         bg = background_filter()
         if bg:
             songs = filter(bg, songs)
         self._panes[0].fill(songs)
Beispiel #13
0
 def test_tag(self):
     self.failUnless(Query.is_valid('t = tag'))
     self.failUnless(Query.is_valid('t = !tag'))
     self.failUnless(Query.is_valid('t = |(tag, bar)'))
     self.failUnless(Query.is_valid('t = a"tag"'))
     self.failIf(Query.is_valid('t = a, tag'))
     self.failUnless(Query.is_valid('tag with spaces = tag'))
Beispiel #14
0
 def test_re(self):
     self.failUnless(Query.is_valid('t = /an re/'))
     self.failUnless(Query.is_valid('t = /an re/c'))
     self.failUnless(Query.is_valid('t = /an\\/re/'))
     self.failIf(Query.is_valid('t = /an/re/'))
     self.failUnless(Query.is_valid('t = /aaa/lsic'))
     self.failIf(Query.is_valid('t = /aaa/icslx'))
Beispiel #15
0
    def __save_search(self, entry, *args):
        # only save the query on focus-out if eager_search is turned on
        if args and not config.getboolean('settings', 'eager_search'):
            return

        text = self.get_text().strip()
        if text and Query.is_parsable(text):
            # Adding the active text to the model triggers a changed signal
            # (get_active is no longer -1), so inhibit
            self.__inhibit()
            self.__combo.prepend_text(text)
            self.__combo.write()
            self.__uninhibit()
Beispiel #16
0
    def test_andor(self):
        self.failUnless(Query.is_valid('a = |(/a/, /b/)'))
        self.failUnless(Query.is_valid('a = |(/b/)'))
        self.failUnless(Query.is_valid('|(a = /b/, c = /d/)'))

        self.failUnless(Query.is_valid('a = &(/a/, /b/)'))
        self.failUnless(Query.is_valid('a = &(/b/)'))
        self.failUnless(Query.is_valid('&(a = /b/, c = /d/)'))
Beispiel #17
0
    def test_filter(self):
        q = Query("artist=piman")
        self.assertEqual(q.filter([self.s1, self.s2]), [self.s1])
        self.assertEqual(q.filter(iter([self.s1, self.s2])), [self.s1])

        q = Query("")
        self.assertEqual(q.filter([self.s1, self.s2]), [self.s1, self.s2])
        self.assertEqual(
            q.filter(iter([self.s1, self.s2])), [self.s1, self.s2])
Beispiel #18
0
    def __update_filter(self, entry, text, scroll_up=True, restore=False):
        model = self.view.get_model()

        self.__filter = None
        if not Query.match_all(text):
            self.__filter = Query(text, star=["~people", "album"]).search
        self.__bg_filter = background_filter()

        self.__inhibit()

        # If we're hiding "All Albums", then there will always
        # be something to filter ­— probably there's a better
        # way to implement this

        if (not restore or self.__filter or self.__bg_filter) or (not
            config.getboolean("browsers", "covergrid_all", False)):
            model.refilter()

        self.__uninhibit()
Beispiel #19
0
    def restore(self):
        text = config.get("browsers", "query_text").decode("utf-8")
        self.__searchbar.set_text(text)
        if Query.is_parsable(text):
            self.__filter_changed(self.__searchbar, text, restore=True)

        keys = config.get("browsers", "radio").splitlines()

        def select_func(row):
            return row[self.TYPE] != self.TYPE_SEP and row[self.KEY] in keys

        self.__inhibit()
        view = self.view
        if not view.select_by_func(select_func):
            for row in view.get_model():
                if row[self.TYPE] == self.TYPE_FAV:
                    view.set_cursor(row.path)
                    break
        self.__uninhibit()
Beispiel #20
0
    def __update_filter(self, entry, text, scroll_up=True, restore=False):
        model = self.view.get_model()

        self.__filter = None
        if not Query.match_all(text):
            self.__filter = Query(text, star=["~people", "album"]).search
        self.__bg_filter = background_filter()

        self.__inhibit()

        # We could be smart and try to scroll to a selected album
        # but that introduces lots of wild scrolling. Feel free to change it.
        # Without scrolling the TV tries to stay at the same position
        # (40% down) which makes no sense, so always go to the top.
        if scroll_up:
            self.view.scroll_to_point(0, 0)

        # Don't filter on restore if there is nothing to filter
        if not restore or self.__filter or self.__bg_filter:
            model.refilter()

        self.__uninhibit()
Beispiel #21
0
    def test_numexpr(self):
        self.failUnless(Query("#(length = 224)").search(self.s1))
        self.failUnless(Query("#(length = 3:44)").search(self.s1))
        self.failUnless(
            Query("#(length = 3 minutes + 44 seconds)").search(self.s1))
        self.failUnless(Query("#(playcount > skipcount)").search(self.s1))
        self.failUnless(Query("#(playcount < 2 * skipcount)").search(self.s1))
        self.failUnless(Query("#(length > 3 minutes)").search(self.s1))
        self.failUnless(Query("#(3:00 < length < 4:00)").search(self.s1))
        self.failUnless(
            Query("#(40 seconds < length/5 < 1 minute)").search(self.s1))
        self.failUnless(Query("#(2+3 * 5 = 17)").search(self.s1))
        self.failUnless(Query("#(playcount / 0 > 0)").search(self.s1))

        self.failIf(Query("#(track + 1 != 13)").search(self.s2))
Beispiel #22
0
 def test_black(self):
     for p in ["a test", "more test hooray"]:
         self.failUnlessEqual(QueryType.TEXT, Query.get_type(p))
Beispiel #23
0
 def test_nonsense(self):
     self.failIf(Query.is_valid('a string'))
     self.failIf(Query.is_valid('t = #(a > b)'))
     self.failIf(Query.is_valid("=a= = /b/"))
     self.failIf(Query.is_valid("a = &(/b//"))
     self.failIf(Query.is_valid("(a = &(/b//)"))
Beispiel #24
0
 def test_black(self):
     for p in ["a test", "more test hooray"]:
         self.failUnlessEqual(QueryType.TEXT, Query.get_type(p))
Beispiel #25
0
 def test_emptylist(self):
     self.failIf(Query.is_valid("a = &()"))
     self.failIf(Query.is_valid("a = |()"))
     self.failIf(Query.is_valid("|()"))
     self.failIf(Query.is_valid("&()"))
Beispiel #26
0
 def test_numcmp_func(self):
     self.assertTrue(Query.is_valid("#(t:min < 3)"))
     self.assertTrue(
         Query.is_valid("&(#(playcount:min = 0), #(added < 1 month ago))"))
Beispiel #27
0
 def test_inequality(self):
     self.failUnless(Query("album!=foo").search(self.s1))
     self.failIf(Query("album!=foo").search(self.s2))
Beispiel #28
0
 def test_not(self):
     for s in ["album = !hate", "artist = !pi"]:
         self.failIf(Query(s).search(self.s1))
         self.failUnless(Query(s).search(self.s2))
Beispiel #29
0
 def test_numcmp_func(self):
     self.assertTrue(Query.is_valid("#(t:min < 3)"))
     self.assertTrue(
         Query.is_valid("&(#(playcount:min = 0), #(added < 1 month ago))"))
Beispiel #30
0
 def test_gte(self):
     self.failUnless(Query("#(track >= 11)").search(self.s2))
Beispiel #31
0
 def test_re_escape(self):
     af = AudioFile({"foo": "\""})
     assert Query('foo="\\""').search(af)
     af = AudioFile({"foo": "/"})
     assert Query('foo=/\\//').search(af)
Beispiel #32
0
 def test_empty(self):
     self.failIf(Query("foobar = /./").search(self.s1))
Beispiel #33
0
 def test_2007_07_27_synth_search(self):
     song = AudioFile({"~filename": fsnative(u"foo/64K/bar.ogg")})
     query = Query("~dirname = !64K")
     self.failIf(query.search(song), "%r, %r" % (query, song))
Beispiel #34
0
 def test_str(self):
     self.failUnless(Query('t = "a str"').valid)
     self.failUnless(Query('t = "a str"c').valid)
     self.failUnless(Query('t = "a\\"str"').valid)
Beispiel #35
0
 def test_match_diacriticals_invalid_or_unsupported(self):
     # these fall back to test dumb searches:
     # invalid regex
     Query(u'/Sigur [r-zos/d')
     # group refs unsupported for diacritic matching
     Query(u'/(<)?(\w+@\w+(?:\.\w+)+)(?(1)>)/d')
Beispiel #36
0
 def test_taglist(self):
     self.failUnless(Query.is_valid('a, b = /a/'))
     self.failUnless(Query.is_valid('a, b, c = |(/a/)'))
     self.failUnless(Query.is_valid('|(a, b = /a/, c, d = /q/)'))
     self.failIf(Query.is_valid('a = /a/, b'))
Beispiel #37
0
    def test_numexpr(self):
        self.failUnless(Query("#(t < 3*4)").valid)
        self.failUnless(Query("#(t * (1+r) < 7)").valid)
        self.failUnless(Query("#(0 = t)").valid)
        self.failUnless(Query("#(t < r < 9)").valid)
        self.failUnless(Query("#((t-9)*r < -(6*2) = g*g-1)").valid)
        self.failUnless(Query("#(t + 1 + 2 + -4 * 9 > g*(r/4 + 6))").valid)
        self.failUnless(Query("#(date < 2010-4)").valid)
        self.failUnless(Query("#(date < 2010 - 4)").valid)
        self.failUnless(Query("#(date > 0000)").valid)
        self.failUnless(Query("#(date > 00004)").valid)
        self.failUnless(Query("#(t > 3 minutes)").valid)
        self.failUnless(Query("#(added > today)").valid)
        self.failUnless(Query("#(length < 5:00)").valid)
        self.failUnless(Query("#(filesize > 5M)").valid)
        self.failUnless(Query("#(added < 7 days ago)").valid)

        self.failIf(Query("#(3*4)").valid)
        self.failIf(Query("#(t = 3 + )").valid)
        self.failIf(Query("#(t = -)").valid)
        self.failIf(Query("#(-4 <)").valid)
        self.failIf(Query("#(t < ()").valid)
        self.failIf(Query("#((t +) - 1 > 8)").valid)
        self.failIf(Query("#(t += 8)").valid)
Beispiel #38
0
 def test_nonsense(self):
     self.failIf(Query.is_valid('a string'))
     self.failIf(Query.is_valid('t = #(a > b)'))
     self.failIf(Query.is_valid("=a= = /b/"))
     self.failIf(Query.is_valid("a = &(/b//"))
     self.failIf(Query.is_valid("(a = &(/b//)"))
Beispiel #39
0
 def test_list(self):
     self.failUnless(Query("#(t < 3, t > 9)").valid)
     self.failUnless(Query("t = &(/a/, /b/)").valid)
     self.failUnless(Query("s, t = |(/a/, /b/)").valid)
     self.failUnless(Query("|(t = /a/, s = /b/)").valid)
Beispiel #40
0
 def test_basic_tag(self):
     assert Query("album=foo").search(self.s2)
     assert not Query("album=.").search(self.s2)
     assert Query("album=/./").search(self.s2)
Beispiel #41
0
 def test_trailing(self):
     self.failIf(Query.is_valid('t = /an re/)'))
     self.failIf(Query.is_valid('|(a, b = /a/, c, d = /q/) woo'))
Beispiel #42
0
 def test_emptylist(self):
     self.failIf(Query.is_valid("a = &()"))
     self.failIf(Query.is_valid("a = |()"))
     self.failIf(Query.is_valid("|()"))
     self.failIf(Query.is_valid("&()"))
Beispiel #43
0
 def test_green(self):
     for p in ["a = /b/", "&(a = b, c = d)", "/abc/", "!x", "!&(abc, def)"]:
         self.failUnlessEqual(QueryType.VALID, Query.get_type(p))
Beispiel #44
0
 def test_trinary(self):
     self.failUnless(Query("#(2 < t < 3)").valid)
     self.failUnless(Query("#(2 >= t > 3)").valid)
     # useless, but valid
     self.failUnless(Query("#(5 > t = 2)").valid)
Beispiel #45
0
 def test_red(self):
     for p in ["a = /w", "|(sa#"]:
         self.failUnlessEqual(QueryType.INVALID, Query.get_type(p))
Beispiel #46
0
 def song_excluded(self, song):
     if self.exclude and Query(self.exclude).search(song):
         print_d("%s is excluded by %s" %
                 (song("~artist~title"), self.exclude))
         return True
     return False
Beispiel #47
0
 def test_red(self):
     for p in ["a = /w", "|(sa#"]:
         self.failUnlessEqual(QueryType.INVALID, Query.get_type(p))
Beispiel #48
0
 def __filter_changed(self, *args):
     self.__deferred_changed.abort()
     text = self.get_text()
     if Query.is_parsable(text):
         GLib.idle_add(self.emit, 'query-changed', text)
Beispiel #49
0
 def test_green(self):
     for p in ["a = /b/", "&(a = b, c = d)", "/abc/", "!x", "!&(abc, def)"]:
         self.failUnlessEqual(QueryType.VALID, Query.get_type(p))
Beispiel #50
0
class PlaylistsBrowser(Browser, DisplayPatternMixin):

    name = _("Playlists")
    accelerated_name = _("_Playlists")
    keys = ["Playlists", "PlaylistsBrowser"]
    priority = 2
    replaygain_profiles = ["track"]
    __last_render = None
    _PATTERN_FN = os.path.join(quodlibet.get_user_dir(), "playlist_pattern")
    _DEFAULT_PATTERN_TEXT = DEFAULT_PATTERN_TEXT

    def pack(self, songpane):
        self._main_box.pack1(self, True, False)
        self._rh_box = rhbox = Gtk.VBox(spacing=6)
        align = Align(self._sb_box, left=0, right=6, top=6)
        rhbox.pack_start(align, False, True, 0)
        rhbox.pack_start(songpane, True, True, 0)
        self._main_box.pack2(rhbox, True, False)
        rhbox.show()
        align.show_all()
        return self._main_box

    def unpack(self, container, songpane):
        self._rh_box.remove(songpane)
        container.remove(self._rh_box)
        container.remove(self)

    @classmethod
    def init(klass, library):
        klass.library = library
        model = klass.__lists.get_model()
        for playlist in os.listdir(PLAYLISTS):
            try:
                playlist = FileBackedPlaylist(
                    PLAYLISTS,
                    FileBackedPlaylist.unquote(playlist),
                    library=library)
                model.append(row=[playlist])
            except EnvironmentError:
                print_w("Invalid Playlist '%s'" % playlist)
                pass

        klass._ids = [
            library.connect('removed', klass.__removed),
            library.connect('added', klass.__added),
            library.connect('changed', klass.__changed),
        ]
        klass.load_pattern()

    @classmethod
    def deinit(cls, library):
        model = cls.__lists.get_model()
        model.clear()

        for id_ in cls._ids:
            library.disconnect(id_)
        del cls._ids

    @classmethod
    def playlists(klass):
        return [row[0] for row in klass.__lists]

    @classmethod
    def changed(klass, playlist, refresh=True):
        model = klass.__lists
        for row in model:
            if row[0] is playlist:
                if refresh:
                    print_d("Refreshing playlist %s..." % row[0])
                    klass.__lists.row_changed(row.path, row.iter)
                playlist.write()
                break
        else:
            model.get_model().append(row=[playlist])
            playlist.write()

    @classmethod
    def __removed(klass, library, songs):
        for playlist in klass.playlists():
            if playlist.remove_songs(songs):
                klass.changed(playlist)

    @classmethod
    def __added(klass, library, songs):
        filenames = {song("~filename") for song in songs}
        for playlist in klass.playlists():
            if playlist.add_songs(filenames, library):
                klass.changed(playlist)

    @classmethod
    def __changed(klass, library, songs):
        for playlist in klass.playlists():
            for song in songs:
                if song in playlist.songs:
                    klass.changed(playlist)
                    break

    def cell_data(self, col, cell, model, iter, data):
        playlist = model[iter][0]
        cell.markup = markup = self.display_pattern % playlist
        if self.__last_render == markup:
            return
        self.__last_render = markup
        cell.markup = markup
        cell.set_property('markup', markup)

    def Menu(self, songs, library, items):
        model, iters = self.__get_selected_songs()
        remove = qltk.MenuItem(_("_Remove from Playlist"), Icons.LIST_REMOVE)
        qltk.add_fake_accel(remove, "Delete")
        connect_obj(remove, 'activate', self.__remove, iters, model)
        playlist_iter = self.__view.get_selection().get_selected()[1]
        remove.set_sensitive(bool(playlist_iter))
        items.append([remove])
        menu = super(PlaylistsBrowser, self).Menu(songs, library, items)
        return menu

    def __get_selected_songs(self):
        songlist = qltk.get_top_parent(self).songlist
        model, rows = songlist.get_selection().get_selected_rows()
        iters = map(model.get_iter, rows)
        return model, iters

    __lists = ObjectModelSort(model=ObjectStore())
    __lists.set_default_sort_func(ObjectStore._sort_on_value)

    def __init__(self, library):
        self.library = library
        super(PlaylistsBrowser, self).__init__(spacing=6)
        self.set_orientation(Gtk.Orientation.VERTICAL)
        self.__render = self.__create_cell_renderer()
        self.__view = view = self.__create_playlists_view(self.__render)
        self.__embed_in_scrolledwin(view)
        self.__configure_buttons(library)
        self.__configure_dnd(view, library)
        self.__connect_signals(view, library)
        self._sb_box = self.__create_searchbar(library)
        self._main_box = self.__create_box()
        self.show_all()
        self._query = None

        for child in self.get_children():
            child.show_all()

    def __destroy(self, *args):
        del self._sb_box

    def __create_box(self):
        box = qltk.ConfigRHPaned("browsers", "playlistsbrowser_pos", 0.4)
        box.show_all()
        return box

    def __create_searchbar(self, library):
        self.accelerators = Gtk.AccelGroup()
        completion = LibraryTagCompletion(library.librarian)
        sbb = SearchBarBox(completion=completion,
                           accel_group=self.accelerators)
        sbb.connect('query-changed', self.__text_parse)
        sbb.connect('focus-out', self.__focus)
        return sbb

    def __embed_in_scrolledwin(self, view):
        swin = ScrolledWindow()
        swin.set_shadow_type(Gtk.ShadowType.IN)
        swin.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
        swin.add(view)
        self.pack_start(swin, True, True, 0)

    def __configure_buttons(self, library):
        new_pl = qltk.Button(_("_New"), Icons.DOCUMENT_NEW, Gtk.IconSize.MENU)
        new_pl.connect('clicked', self.__new_playlist)
        import_pl = qltk.Button(_("_Import"), Icons.LIST_ADD,
                                Gtk.IconSize.MENU)
        import_pl.connect('clicked', self.__import, library)
        hb = Gtk.HBox(spacing=6)
        hb.set_homogeneous(False)
        hb.pack_start(new_pl, True, True, 0)
        hb.pack_start(import_pl, True, True, 0)
        hb2 = Gtk.HBox(spacing=0)
        hb2.pack_start(hb, True, True, 0)
        hb2.pack_start(PreferencesButton(self), False, False, 6)
        self.pack_start(Align(hb2, left=3, bottom=3), False, False, 0)

    def __create_playlists_view(self, render):
        view = RCMHintedTreeView()
        view.set_enable_search(True)
        view.set_search_column(0)
        view.set_search_equal_func(
            lambda model, col, key, iter, data: not model[iter][col].name.
            lower().startswith(key.lower()), None)
        col = Gtk.TreeViewColumn("Playlists", render)
        col.set_cell_data_func(render, self.cell_data)
        view.append_column(col)
        view.set_model(self.__lists)
        view.set_rules_hint(True)
        view.set_headers_visible(False)
        return view

    def __configure_dnd(self, view, library):
        targets = [("text/x-quodlibet-songs", Gtk.TargetFlags.SAME_APP,
                    DND_QL), ("text/uri-list", 0, DND_URI_LIST),
                   ("text/x-moz-url", 0, DND_MOZ_URL)]
        targets = [Gtk.TargetEntry.new(*t) for t in targets]
        view.drag_dest_set(Gtk.DestDefaults.ALL, targets, Gdk.DragAction.COPY)
        view.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, targets[:2],
                             Gdk.DragAction.COPY)
        view.connect('drag-data-received', self.__drag_data_received, library)
        view.connect('drag-data-get', self._drag_data_get)
        view.connect('drag-motion', self.__drag_motion)
        view.connect('drag-leave', self.__drag_leave)

    def __connect_signals(self, view, library):
        view.connect('row-activated', lambda *x: self.songs_activated())
        view.connect('popup-menu', self.__popup_menu, library)
        view.get_selection().connect('changed', self.activate)
        model = view.get_model()
        s = model.connect('row-changed', self.__check_current)
        connect_obj(self, 'destroy', model.disconnect, s)
        self.connect('key-press-event', self.__key_pressed)

    def __create_cell_renderer(self):
        render = Gtk.CellRendererText()
        render.set_padding(3, 3)
        render.set_property('ellipsize', Pango.EllipsizeMode.END)
        render.connect('editing-started', self.__start_editing)
        render.connect('edited', self.__edited)
        return render

    def key_pressed(self, event):
        if qltk.is_accel(event, "Delete"):
            self.__handle_songlist_delete()
            return True
        return False

    def __handle_songlist_delete(self, *args):
        model, iters = self.__get_selected_songs()
        self.__remove(iters, model)

    def __key_pressed(self, widget, event):
        if qltk.is_accel(event, "Delete"):
            model, iter = self.__view.get_selection().get_selected()
            if not iter:
                return False

            playlist = model[iter][0]
            dialog = ConfirmRemovePlaylistDialog(self, playlist)
            if dialog.run() == Gtk.ResponseType.YES:
                playlist.delete()
                model.get_model().remove(
                    model.convert_iter_to_child_iter(iter))
            return True
        elif qltk.is_accel(event, "F2"):
            model, iter = self.__view.get_selection().get_selected()
            if iter:
                self._start_rename(model.get_path(iter))
            return True
        return False

    def __check_current(self, model, path, iter):
        model, citer = self.__view.get_selection().get_selected()
        if citer and model.get_path(citer) == path:
            songlist = qltk.get_top_parent(self).songlist
            self.activate(resort=not songlist.is_sorted())

    def __drag_motion(self, view, ctx, x, y, time):
        targets = [t.name() for t in ctx.list_targets()]
        if "text/x-quodlibet-songs" in targets:
            view.set_drag_dest(x, y, into_only=True)
            return True
        else:
            # Highlighting the view itself doesn't work.
            view.get_parent().drag_highlight()
            return True

    def __drag_leave(self, view, ctx, time):
        view.get_parent().drag_unhighlight()

    def __remove(self, iters, smodel):
        def song_at(itr):
            return smodel[smodel.get_path(itr)][0]

        def remove_from_model(iters, smodel):
            for it in iters:
                smodel.remove(it)

        model, iter = self.__view.get_selection().get_selected()
        if iter:
            playlist = model[iter][0]
            removals = [song_at(iter_remove) for iter_remove in iters]
            if self._query is None or not self.get_filter_text():
                # Calling playlist.remove_songs(songs) won't remove the
                # right ones if there are duplicates
                remove_from_model(iters, smodel)
                self.__rebuild_playlist_from_songs_model(playlist, smodel)
                # Emit manually
                self.library.emit('changed', removals)
            else:
                print_d("Removing %d song(s) from %s" %
                        (len(removals), playlist))
                playlist.remove_songs(removals, True)
                remove_from_model(iters, smodel)
            self.changed(playlist)
            self.activate()

    def __rebuild_playlist_from_songs_model(self, playlist, smodel):
        playlist.inhibit = True
        playlist.clear()
        playlist.extend([row[0] for row in smodel])
        playlist.inhibit = False

    def __drag_data_received(self, view, ctx, x, y, sel, tid, etime, library):
        # TreeModelSort doesn't support GtkTreeDragDestDrop.
        view.emit_stop_by_name('drag-data-received')
        model = view.get_model()
        if tid == DND_QL:
            filenames = qltk.selection_get_filenames(sel)
            songs = listfilter(None, [library.get(f) for f in filenames])
            if not songs:
                Gtk.drag_finish(ctx, False, False, etime)
                return
            try:
                path, pos = view.get_dest_row_at_pos(x, y)
            except TypeError:
                playlist = FileBackedPlaylist.from_songs(
                    PLAYLISTS, songs, library)
                GLib.idle_add(self._select_playlist, playlist)
            else:
                playlist = model[path][0]
                playlist.extend(songs)
            self.changed(playlist)
            Gtk.drag_finish(ctx, True, False, etime)
        else:
            if tid == DND_URI_LIST:
                uri = sel.get_uris()[0]
                name = os.path.basename(uri)
            elif tid == DND_MOZ_URL:
                data = sel.get_data()
                uri, name = data.decode('utf16', 'replace').split('\n')
            else:
                Gtk.drag_finish(ctx, False, False, etime)
                return
            name = name or os.path.basename(uri) or _("New Playlist")
            uri = uri.encode('utf-8')
            try:
                sock = urlopen(uri)
                f = NamedTemporaryFile()
                f.write(sock.read())
                f.flush()
                if uri.lower().endswith('.pls'):
                    playlist = parse_pls(f.name, library=library)
                elif uri.lower().endswith('.m3u'):
                    playlist = parse_m3u(f.name, library=library)
                else:
                    raise IOError
                library.add_filename(playlist)
                if name:
                    playlist.rename(name)
                self.changed(playlist)
                Gtk.drag_finish(ctx, True, False, etime)
            except IOError:
                Gtk.drag_finish(ctx, False, False, etime)
                qltk.ErrorMessage(
                    qltk.get_top_parent(self), _("Unable to import playlist"),
                    _("Quod Libet can only import playlists in the M3U "
                      "and PLS formats.")).run()

    def _drag_data_get(self, view, ctx, sel, tid, etime):
        model, iters = self.__view.get_selection().get_selected_rows()
        songs = []
        for itr in iters:
            if itr:
                songs += model[itr][0].songs
        if tid == 0:
            qltk.selection_set_songs(sel, songs)
        else:
            sel.set_uris([song("~uri") for song in songs])

    def _select_playlist(self, playlist, scroll=False):
        view = self.__view
        model = view.get_model()
        for row in model:
            if row[0] is playlist:
                view.get_selection().select_iter(row.iter)
                if scroll:
                    view.scroll_to_cell(row.path,
                                        use_align=True,
                                        row_align=0.5)

    def __popup_menu(self, view, library):
        model, itr = view.get_selection().get_selected()
        if itr is None:
            return
        songs = list(model[itr][0])
        songs = [s for s in songs if isinstance(s, AudioFile)]
        menu = SongsMenu(library,
                         songs,
                         playlists=False,
                         remove=False,
                         ratings=False)
        menu.preseparate()

        def _remove(model, itr):
            playlist = model[itr][0]
            dialog = ConfirmRemovePlaylistDialog(self, playlist)
            if dialog.run() == Gtk.ResponseType.YES:
                playlist.delete()
                model.get_model().remove(model.convert_iter_to_child_iter(itr))

        rem = MenuItem(_("_Delete"), Icons.EDIT_DELETE)
        connect_obj(rem, 'activate', _remove, model, itr)
        menu.prepend(rem)

        def _rename(path):
            self._start_rename(path)

        ren = qltk.MenuItem(_("_Rename"), Icons.EDIT)
        qltk.add_fake_accel(ren, "F2")
        connect_obj(ren, 'activate', _rename, model.get_path(itr))
        menu.prepend(ren)

        playlist = model[itr][0]
        PLAYLIST_HANDLER.populate_menu(menu, library, self, [playlist])
        menu.show_all()
        return view.popup_menu(menu, 0, Gtk.get_current_event_time())

    def _start_rename(self, path):
        view = self.__view
        self.__render.set_property('editable', True)
        view.set_cursor(path, view.get_columns()[0], start_editing=True)

    def __focus(self, widget, *args):
        qltk.get_top_parent(widget).songlist.grab_focus()

    def __text_parse(self, bar, text):
        self.activate()

    def _set_text(self, text):
        self._sb_box.set_text(text)

    def activate(self, widget=None, resort=True):
        songs = self._get_playlist_songs()

        text = self.get_filter_text()
        # TODO: remove static dependency on Query
        if Query.is_parsable(text):
            self._query = Query(text, SongList.star)
            songs = self._query.filter(songs)
        GLib.idle_add(self.songs_selected, songs, resort)

    @classmethod
    def refresh_all(cls):
        model = cls.__lists.get_model()
        for iter_, value in model.iterrows():
            model.row_changed(model.get_path(iter_), iter_)

    @property
    def model(self):
        return self.__lists.get_model()

    def _get_playlist_songs(self):
        model, iter = self.__view.get_selection().get_selected()
        songs = iter and list(model[iter][0]) or []
        return [s for s in songs if isinstance(s, AudioFile)]

    def can_filter_text(self):
        return True

    def filter_text(self, text):
        self._set_text(text)
        self.activate()

    def get_filter_text(self):
        return self._sb_box.get_text()

    def can_filter(self, key):
        # TODO: special-case the ~playlists tag maybe?
        return super(PlaylistsBrowser, self).can_filter(key)

    def finalize(self, restore):
        config.set("browsers", "query_text", "")

    def unfilter(self):
        self.filter_text("")

    def active_filter(self, song):
        return (song in self._get_playlist_songs()
                and (self._query is None or self._query.search(song)))

    def save(self):
        model, iter = self.__view.get_selection().get_selected()
        name = iter and model[iter][0].name or ""
        config.set("browsers", "playlist", name)
        text = self.get_filter_text()
        config.set("browsers", "query_text", text)

    def __new_playlist(self, activator):
        playlist = FileBackedPlaylist.new(PLAYLISTS)
        self.model.append(row=[playlist])
        self._select_playlist(playlist, scroll=True)

        model, iter = self.__view.get_selection().get_selected()
        path = model.get_path(iter)
        GLib.idle_add(self._start_rename, path)

    def __start_editing(self, render, editable, path):
        editable.set_text(self.__lists[path][0].name)

    def __edited(self, render, path, newname):
        return self._rename(path, newname)

    def _rename(self, path, newname):
        playlist = self.__lists[path][0]
        try:
            playlist.rename(newname)
        except ValueError as s:
            qltk.ErrorMessage(None, _("Unable to rename playlist"), s).run()
        else:
            row = self.__lists[path]
            child_model = self.model
            child_model.remove(
                self.__lists.convert_iter_to_child_iter(row.iter))
            child_model.append(row=[playlist])
            self._select_playlist(playlist, scroll=True)

    def __import(self, activator, library):
        filt = lambda fn: fn.endswith(".pls") or fn.endswith(".m3u")
        from quodlibet.qltk.chooser import FileChooser
        chooser = FileChooser(self, _("Import Playlist"), filt, get_home_dir())
        files = chooser.run()
        chooser.destroy()
        for filename in files:
            if filename.endswith(".m3u"):
                playlist = parse_m3u(filename, library=library)
            elif filename.endswith(".pls"):
                playlist = parse_pls(filename, library=library)
            else:
                qltk.ErrorMessage(
                    qltk.get_top_parent(self), _("Unable to import playlist"),
                    _("Quod Libet can only import playlists in the M3U "
                      "and PLS formats.")).run()
                return
            self.changed(playlist)
            library.add(playlist)

    def restore(self):
        try:
            name = config.get("browsers", "playlist")
        except config.Error as e:
            print_d("Couldn't get last playlist from config: %s" % e)
        else:
            self.__view.select_by_func(lambda r: r[0].name == name, one=True)
        try:
            text = config.get("browsers", "query_text")
        except config.Error as e:
            print_d("Couldn't get last search string from config: %s" % e)
        else:
            self._set_text(text)

    can_reorder = True

    def scroll(self, song):
        self.__view.iter_select_by_func(lambda r: song in r[0])

    def reordered(self, songs):
        model, iter = self.__view.get_selection().get_selected()
        playlist = None
        if iter:
            playlist = model[iter][0]
            playlist[:] = songs
        elif songs:
            playlist = FileBackedPlaylist.from_songs(PLAYLISTS, songs)
            GLib.idle_add(self._select_playlist, playlist)
        if playlist:
            self.changed(playlist, refresh=False)
Beispiel #51
0
 def test_trailing(self):
     self.failIf(Query.is_valid('t = /an re/)'))
     self.failIf(Query.is_valid('|(a, b = /a/, c, d = /q/) woo'))
Beispiel #52
0
 def filter_text(self, text):
     self.__searchbar.set_text(text)
     if Query.is_parsable(text):
         self.__filter_changed(self.__searchbar, text)
         self.activate()
Beispiel #53
0
 def test_taglist(self):
     self.failUnless(Query.is_valid('a, b = /a/'))
     self.failUnless(Query.is_valid('a, b, c = |(/a/)'))
     self.failUnless(Query.is_valid('|(a, b = /a/, c, d = /q/)'))
     self.failIf(Query.is_valid('a = /a/, b'))
Beispiel #54
0
    def __filter_changed(self, bar, text, restore=False):
        self.__filter = Query(text, self.STAR)

        if not restore:
            self.activate()
Beispiel #55
0
 def test_abbrs(self):
     for s in ["b = /i hate/", "a = /pi*/", "t = /x.y/"]:
         self.failUnless(Query(s).search(self.s1))
         self.failIf(Query(s).search(self.s2))
Beispiel #56
0
 def test_nesting(self):
     self.failUnless(Query("|(s, t = &(/a/, /b/),!#(2 > q > 3))").valid)
Beispiel #57
0
 def test_trinary(self):
     self.failUnless(Query.is_valid("#(2 < t < 3)"))
     self.failUnless(Query.is_valid("#(2 >= t > 3)"))
     # useless, but valid
     self.failUnless(Query.is_valid("#(5 > t = 2)"))
Beispiel #58
0
 def test_str(self):
     for k in self.s2.keys():
         v = self.s2[k]
         self.failUnless(Query('%s = "%s"' % (k, v)).search(self.s2))
         self.failIf(Query('%s = !"%s"' % (k, v)).search(self.s2))
Beispiel #59
0
 def filter_text(self, text):
     self.__search.set_text(text)
     if Query.is_parsable(text):
         self.__update_filter(self.__search, text)
         self.activate()
Beispiel #60
0
 def test_match_diacriticals_dumb(self):
     self.assertTrue(Query(u'Angstrom').search(self.s4))
     self.assertTrue(Query(u'Ångström').search(self.s4))
     self.assertTrue(Query(u'Ångstrom').search(self.s4))
     self.assertFalse(Query(u'Ängström').search(self.s4))