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
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
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)
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
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("@ )"))
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()
def _get_songs(self): try: self._query = Query(self._text, star=SongList.star) except Query.error: pass else: return self._query.filter(self._library)
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()
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()
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)"))
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/)'))
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)
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'))
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'))
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()
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/)'))
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])
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()
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()
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()
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))
def test_black(self): for p in ["a test", "more test hooray"]: self.failUnlessEqual(QueryType.TEXT, Query.get_type(p))
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//)"))
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("&()"))
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))"))
def test_inequality(self): self.failUnless(Query("album!=foo").search(self.s1)) self.failIf(Query("album!=foo").search(self.s2))
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))
def test_gte(self): self.failUnless(Query("#(track >= 11)").search(self.s2))
def test_re_escape(self): af = AudioFile({"foo": "\""}) assert Query('foo="\\""').search(af) af = AudioFile({"foo": "/"}) assert Query('foo=/\\//').search(af)
def test_empty(self): self.failIf(Query("foobar = /./").search(self.s1))
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))
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)
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')
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'))
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)
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)
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)
def test_trailing(self): self.failIf(Query.is_valid('t = /an re/)')) self.failIf(Query.is_valid('|(a, b = /a/, c, d = /q/) woo'))
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))
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)
def test_red(self): for p in ["a = /w", "|(sa#"]: self.failUnlessEqual(QueryType.INVALID, Query.get_type(p))
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
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)
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)
def filter_text(self, text): self.__searchbar.set_text(text) if Query.is_parsable(text): self.__filter_changed(self.__searchbar, text) self.activate()
def __filter_changed(self, bar, text, restore=False): self.__filter = Query(text, self.STAR) if not restore: self.activate()
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))
def test_nesting(self): self.failUnless(Query("|(s, t = &(/a/, /b/),!#(2 > q > 3))").valid)
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)"))
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))
def filter_text(self, text): self.__search.set_text(text) if Query.is_parsable(text): self.__update_filter(self.__search, text) self.activate()
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))