def _artist_activated(self, widget, event): coreartist = widget.coreartist mod_mask = Gtk.accelerator_get_default_mod_mask() if ((event.get_state() & mod_mask) == Gdk.ModifierType.CONTROL_MASK and not self.props.selection_mode): self.props.selection_mode = True return (_, button) = event.get_button() if (button == Gdk.BUTTON_PRIMARY and not self.props.selection_mode): # self.emit('song-activated', widget) self._artist_albums_widget = ArtistAlbumsWidget( coreartist, self.player, self._window, False) self.add(self._artist_albums_widget) self._artist_albums_widget.show() self.bind_property('selection-mode', self._artist_albums_widget, 'selection-mode', GObject.BindingFlags.BIDIRECTIONAL) self._headerbar.props.state = HeaderBar.State.SEARCH self._headerbar.props.title = coreartist.artist self._headerbar.props.subtitle = None self.set_visible_child(self._artist_albums_widget) self.props.search_mode_active = False # FIXME: Need to ignore the event from the checkbox. # if self.props.selection_mode: # widget.props.selected = not widget.props.selected return True
def _on_artist_activated(self, widget, child, user_data=None): coreartist = child.props.coreartist if self.props.selection_mode: return artist_albums_widget = ArtistAlbumsWidget(coreartist, self._application) # FIXME: Recreating a view here. Alternate solution is used # in AlbumsView: one view created and an update function. # Settle on one design. self._scrolled_artist_window = Gtk.ScrolledWindow() self._scrolled_artist_window.add(artist_albums_widget) self._scrolled_artist_window.props.visible = True self.add(self._scrolled_artist_window) artist_albums_widget.show() self.bind_property("selection-mode", artist_albums_widget, "selection-mode", GObject.BindingFlags.BIDIRECTIONAL) self.props.state = SearchView.State.ARTIST self._headerbar.props.state = HeaderBar.State.SEARCH self._headerbar.props.title = coreartist.props.artist self._headerbar.props.subtitle = None self.set_visible_child(self._scrolled_artist_window) self.props.search_mode_active = False
def _on_artist_activated(self, sidebar, row, data=None): """Initializes new artist album widgets""" artist_tile = row.get_child() if self.props.selection_mode: artist_tile.props.selected = not artist_tile.props.selected return # Prepare a new artist_albums_widget here coreartist = artist_tile.props.coreartist if coreartist.props.artist in self._loaded_artists: scroll_vadjustment = self._view_container.props.vadjustment scroll_vadjustment.props.value = 0. self._view.set_visible_child_name(coreartist.props.artist) return if self._loading_id > 0: self._artist_albums.disconnect(self._loading_id) self._loading_id = 0 self._artist_albums = ArtistAlbumsWidget(coreartist, self.player, self._window, False) self._loading_id = self._artist_albums.connect( "ready", self._on_artist_albums_ready, coreartist.props.artist) self._view.set_visible_child_name("empty-frame") self._window.notifications_popup.push_loading() return
def _on_item_activated(self, treeview, path, column): if self._star_handler.star_renderer_click: self._star_handler.star_renderer_click = False return if self.props.selection_mode: return try: child_path = self._filter_model.convert_path_to_child_path(path) except TypeError: return _iter = self.model.get_iter(child_path) if self.model[_iter][12] == 'album': title = self.model[_iter][2] artist = self.model[_iter][3] item = self.model[_iter][5] self._album_widget.update(item) self._headerbar.props.state = HeaderBar.State.SEARCH self._headerbar.props.title = title self._headerbar.props.subtitle = artist self.set_visible_child(self._album_widget) self.props.search_state = Search.State.NONE elif self.model[_iter][12] == 'artist': artist = self.model[_iter][2] albums = self._artists[artist.casefold()]['albums'] self._artist_albums_widget = ArtistAlbumsWidget( artist, albums, self.player, self._window, True) self.add(self._artist_albums_widget) self._artist_albums_widget.show() self._artist_albums_widget.bind_property( 'selected-items-count', self, 'selected-items-count') self.bind_property( 'selection-mode', self._artist_albums_widget, 'selection-mode', GObject.BindingFlags.BIDIRECTIONAL) self._headerbar.props.state = HeaderBar.State.SEARCH self._headerbar.props.title = artist self._headerbar.props.subtitle = None self.set_visible_child(self._artist_albums_widget) self.props.search_state = Search.State.NONE elif self.model[_iter][12] == 'song': if self.model[_iter][11] != ValidationStatus.FAILED: c_iter = self._songs_model.convert_child_iter_to_iter(_iter)[1] self.player.set_playlist( PlayerPlaylist.Type.SEARCH_RESULT, None, self._songs_model, c_iter) self.player.play() else: # Headers if self._view.row_expanded(path): self._view.collapse_row(path) else: self._view.expand_row(path, False)
def _on_item_activated(self, treeview, path, column): if self._star_handler.star_renderer_click: self._star_handler.star_renderer_click = False return if self.props.selection_mode: return try: child_path = self._filter_model.convert_path_to_child_path(path) except TypeError: return _iter = self.model.get_iter(child_path) if self.model[_iter][12] == 'album': title = self.model[_iter][2] artist = self.model[_iter][3] item = self.model[_iter][5] self._album_widget.update(item) self._header_bar.props.state = HeaderBar.State.SEARCH self._header_bar.props.title = title self._header_bar.props.subtitle = artist self.set_visible_child(self._album_widget) self._header_bar.searchbar.reveal(False) elif self.model[_iter][12] == 'artist': artist = self.model[_iter][2] albums = self._artists[artist.casefold()]['albums'] self._artist_albums_widget = ArtistAlbumsWidget( artist, albums, self.player, self._header_bar, self._selection_toolbar, self._window, True) self.add(self._artist_albums_widget) self._artist_albums_widget.show() self._header_bar.props.state = HeaderBar.State.SEARCH self._header_bar.props.title = artist self.set_visible_child(self._artist_albums_widget) self._header_bar.searchbar.reveal(False) elif self.model[_iter][12] == 'song': if self.model[_iter][11] != DiscoveryStatus.FAILED: c_iter = self._songs_model.convert_child_iter_to_iter(_iter)[1] self.player.set_playlist('Search Results', None, self._songs_model, c_iter) self.player.play() else: # Headers if self._view.row_expanded(path): self._view.collapse_row(path) else: self._view.expand_row(path, False)
def _on_item_activated(self, widget, id, path): if self._star_handler.star_renderer_click: self._star_handler.star_renderer_click = False return try: child_path = self._filter_model.convert_path_to_child_path(path) except TypeError: return _iter = self.model.get_iter(child_path) if self.model[_iter][11] == 'album': title = self.model[_iter][2] artist = self.model[_iter][3] item = self.model[_iter][5] self._album_widget.update( artist, title, item, self._header_bar, self._selection_toolbar) self._header_bar.set_state(ToolbarState.SEARCH_VIEW) self._header_bar.header_bar.set_title(title) self._header_bar.header_bar.sub_title = artist self.set_visible_child(self._album_widget) self._header_bar.searchbar.reveal(False) elif self.model[_iter][11] == 'artist': artist = self.model[_iter][2] albums = self._artists[artist.casefold()]['albums'] self._artist_albums_widget = ArtistAlbumsWidget( artist, albums, self.player, self._header_bar, self._selection_toolbar, self._window, True) self.add(self._artist_albums_widget) self._artist_albums_widget.show() self._header_bar.set_state(ToolbarState.SEARCH_VIEW) self._header_bar.header_bar.set_title(artist) self.set_visible_child(self._artist_albums_widget) self._header_bar.searchbar.reveal(False) elif self.model[_iter][11] == 'song': if self.model[_iter][12] != DiscoveryStatus.FAILED: c_iter = self._songs_model.convert_child_iter_to_iter(_iter)[1] self.player.set_playlist( 'Search Results', None, self._songs_model, c_iter, 5, 12) self.player.set_playing(True) else: # Headers if self._view.get_generic_view().row_expanded(path): self._view.get_generic_view().collapse_row(path) else: self._view.get_generic_view().expand_row(path, False)
def _on_artist_activated(self, sidebar, row, data=None): """Initializes new artist album widgets""" artist_tile = row.get_child() if self.props.selection_mode: artist_tile.props.selected = not artist_tile.props.selected return # Prepare a new artist_albums_widget here coreartist = artist_tile.props.coreartist if coreartist.props.artist in self._loaded_artists: scroll_vadjustment = self._view_container.props.vadjustment scroll_vadjustment.props.value = 0. self._view.set_visible_child_name(coreartist.props.artist) return self._artist_albums = ArtistAlbumsWidget(coreartist, self._application, False) artist_albums_frame = Gtk.Frame(shadow_type=Gtk.ShadowType.NONE, hexpand=True) artist_albums_frame.add(self._artist_albums) artist_albums_frame.show() self._view.add_named(artist_albums_frame, coreartist.props.artist) scroll_vadjustment = self._view_container.props.vadjustment scroll_vadjustment.props.value = 0. self._view.set_visible_child(artist_albums_frame) self._loaded_artists.append(coreartist.props.artist)
def _on_artist_activated(self, widget, child, user_data=None): coreartist = child.props.coreartist if self.props.selection_mode: return self._artist_albums_widget = ArtistAlbumsWidget( coreartist, self._application, False) # FIXME: Adding scrolled windows without removing them. scrolled_window = Gtk.ScrolledWindow() scrolled_window.add(self._artist_albums_widget) scrolled_window.props.visible = True self.add(scrolled_window) self._artist_albums_widget.show() self.bind_property( "selection-mode", self._artist_albums_widget, "selection-mode", GObject.BindingFlags.BIDIRECTIONAL) self._headerbar.props.state = HeaderBar.State.SEARCH self._headerbar.props.title = coreartist.props.artist self._headerbar.props.subtitle = None self.set_visible_child(scrolled_window) self.props.search_mode_active = False
def _on_item_activated(self, widget, id, path): if self.star_handler.star_renderer_click: self.star_handler.star_renderer_click = False return try: child_path = self.filter_model.convert_path_to_child_path(path) except TypeError: return _iter = self.model.get_iter(child_path) if self.model[_iter][11] == 'album': title = self.model.get_value(_iter, 2) artist = self.model.get_value(_iter, 3) item = self.model.get_value(_iter, 5) self._albumWidget.update(artist, title, item, self.header_bar, self.selection_toolbar) self.header_bar.set_state(ToolbarState.SEARCH_VIEW) title = utils.get_media_title(item) self.header_bar.header_bar.set_title(title) self.header_bar.header_bar.sub_title = artist self.set_visible_child(self._albumWidget) self.header_bar.searchbar.show_bar(False) elif self.model[_iter][11] == 'artist': artist = self.model.get_value(_iter, 2) albums = self._artists[artist.casefold()]['albums'] self._artistAlbumsWidget = ArtistAlbumsWidget( artist, albums, self.player, self.header_bar, self.selection_toolbar, self.window, True ) self.add(self._artistAlbumsWidget) self._artistAlbumsWidget.show() self.header_bar.set_state(ToolbarState.SEARCH_VIEW) self.header_bar.header_bar.set_title(artist) self.set_visible_child(self._artistAlbumsWidget) self.header_bar.searchbar.show_bar(False) elif self.model[_iter][11] == 'song': if self.model.get_value(_iter, 12) != DiscoveryStatus.FAILED: child_iter = self.songs_model.convert_child_iter_to_iter(_iter)[1] self.player.set_playlist('Search Results', None, self.songs_model, child_iter, 5, 12) self.player.set_playing(True) else: # Headers if self.view.get_generic_view().row_expanded(path): self.view.get_generic_view().collapse_row(path) else: self.view.get_generic_view().expand_row(path, False)
def _on_item_activated(self, widget, item_id, path): if self.star_handler.star_renderer_click: self.star_handler.star_renderer_click = False return try: _iter = self.model.get_iter(path) except TypeError: return self._last_selection = _iter artist = self.model.get_value(_iter, 2) albums = self._artists[artist.casefold()]['albums'] widget = self._artists[artist.casefold()]['widget'] if widget: if widget.model == self.player.running_playlist('Artist', widget.artist): self._artistAlbumsWidget = widget.get_parent() GLib.idle_add(self.artistAlbumsStack.set_visible_child, self._artistAlbumsWidget) return elif widget.get_parent() == self._artistAlbumsWidget: return else: widget.get_parent().destroy() # Prepare a new artistAlbumsWidget here new_artistAlbumsWidget = Gtk.Frame( shadow_type=Gtk.ShadowType.NONE, hexpand=True ) self.artistAlbumsStack.add(new_artistAlbumsWidget) artistAlbums = None artistAlbums = ArtistAlbumsWidget( artist, albums, self.player, self.header_bar, self.selection_toolbar, self.window ) self._artists[artist.casefold()]['widget'] = artistAlbums new_artistAlbumsWidget.add(artistAlbums) new_artistAlbumsWidget.show() # Replace previous widget self._artistAlbumsWidget = new_artistAlbumsWidget GLib.idle_add(self.artistAlbumsStack.set_visible_child, new_artistAlbumsWidget)
def _on_item_activated(self, widget, item_id, path): """Initializes new artist album widgets""" try: itr = self.model.get_iter(path) except ValueError as err: logger.warning("Error: {}, {}".format(err.__class__, err)) return self._last_selection = itr artist = self.model[itr][2] albums = self._artists[artist.casefold()]['albums'] widget = self._artists[artist.casefold()]['widget'] if widget: artist_widget_model = self.player.running_playlist( 'Artist', widget.artist) artist_stack = self._artist_albums_stack # FIXME: calling to private model if widget._model == artist_widget_model: self._artist_albums_widget = widget.get_parent() GLib.idle_add(self._artist_albums_stack.set_visible_child, self._artist_albums_widget) return elif widget.get_parent() == artist_stack: return else: widget.get_parent().destroy() # Prepare a new artist_albums_widget here new_artist_albums_widget = Gtk.Frame(shadow_type=Gtk.ShadowType.NONE, hexpand=True) self._artist_albums_stack.add(new_artist_albums_widget) artist_albums = ArtistAlbumsWidget(artist, albums, self.player, self._header_bar, self._selection_toolbar, self._window) self._artists[artist.casefold()]['widget'] = artist_albums new_artist_albums_widget.add(artist_albums) new_artist_albums_widget.show() # Replace previous widget self._artist_albums_widget = new_artist_albums_widget GLib.idle_add(self._artist_albums_stack.set_visible_child, new_artist_albums_widget)
def _on_artist_activated(self, sidebar, row, data=None, untouched=False): """Initializes new artist album widgets""" # On application start the first row of ArtistView is activated # to show an intial artist. When this happens while any of the # views are in selection mode, this artist will be incorrectly # selected. # When selecting items check that the current visible view is # ArtistsView, to circumvent this issue. if (self.props.selection_mode and self._window.props.active_view is self): return if untouched is False: self._untouched_list = False selected_row = self._sidebar.get_selected_row() self._selected_artist = selected_row.props.coreartist # Prepare a new artist_albums_widget here coreartist = row.props.coreartist if coreartist.props.artist in self._loaded_artists: scroll_vadjustment = self._artist_container.props.vadjustment scroll_vadjustment.props.value = 0. self._artist_view.set_visible_child_name(coreartist.props.artist) return artist_albums = ArtistAlbumsWidget(coreartist, self._application) self.bind_property( "selection-mode", artist_albums, "selection-mode", GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL) self._artist_view.add_named(artist_albums, coreartist.props.artist) scroll_vadjustment = self._artist_container.props.vadjustment scroll_vadjustment.props.value = 0. self._artist_view.set_visible_child(artist_albums) self._loaded_artists.append(coreartist.props.artist)
def _on_artist_activated(self, sidebar, row, data=None): """Initializes new artist album widgets""" if self.props.selection_mode: row.check.props.active = not row.check.props.active return self._last_selected_row = row artist = row.artist albums = self._artists[artist.casefold()]['albums'] widget = self._artists[artist.casefold()]['widget'] if widget: if self.player.playing_playlist('Artist', widget.props.artist): self._artist_albums_widget = widget.get_parent() GLib.idle_add(self._view.set_visible_child, self._artist_albums_widget) return elif widget.get_parent() == self._view: return else: widget.get_parent().destroy() # Prepare a new artist_albums_widget here new_artist_albums_widget = Gtk.Frame(shadow_type=Gtk.ShadowType.NONE, hexpand=True) self._view.add(new_artist_albums_widget) artist_albums = ArtistAlbumsWidget(artist, albums, self.player, self._header_bar, self._selection_toolbar, self._window) self._artists[artist.casefold()]['widget'] = artist_albums new_artist_albums_widget.add(artist_albums) new_artist_albums_widget.show() # Replace previous widget self._artist_albums_widget = new_artist_albums_widget GLib.idle_add(self._view.set_visible_child, new_artist_albums_widget)
class SearchView(BaseView): __gsignals__ = { 'no-music-found': (GObject.SignalFlags.RUN_FIRST, None, ()) } def __repr__(self): return '<SearchView>' @log def __init__(self, window, player): BaseView.__init__(self, 'search', None, window, Gd.MainViewType.LIST) self._items = {} self.isStarred = None self.iter_to_clean = None scale = self.get_scale_factor() loading_icon_surface = DefaultIcon(scale).get(DefaultIcon.Type.loading, ArtSize.small) self._loading_icon = Gdk.pixbuf_get_from_surface( loading_icon_surface, 0, 0, loading_icon_surface.get_width(), loading_icon_surface.get_height()) self._add_list_renderers() self.player = player self.head_iters = [None, None, None, None] self.songs_model = self.model self.previous_view = None self.connect('no-music-found', self._no_music_found_callback) self.albums_selected = [] self._albums = {} self._albumWidget = AlbumWidget(player, self) self.add(self._albumWidget) self.artists_albums_selected = [] self._artists = {} self._artistAlbumsWidget = None self.view.get_generic_view().set_show_expanders(False) self.items_selected = [] self.items_selected_callback = None self.found_items_number = None @log def _no_music_found_callback(self, view): self.window._stack.set_visible_child_name('emptysearch') emptysearch = self.window._stack.get_child_by_name('emptysearch') emptysearch._artistAlbumsWidget = self._artistAlbumsWidget @log def _back_button_clicked(self, widget, data=None): self.header_bar.searchbar.show_bar(True, False) if self.get_visible_child() == self._artistAlbumsWidget: self._artistAlbumsWidget.destroy() self._artistAlbumsWidget = None elif self.get_visible_child() == self._grid: self.window.views[0].set_visible_child(self.window.views[0]._grid) self.set_visible_child(self._grid) self.window.toolbar.set_state(ToolbarState.MAIN) @log def _on_item_activated(self, widget, id, path): if self.star_handler.star_renderer_click: self.star_handler.star_renderer_click = False return try: child_path = self.filter_model.convert_path_to_child_path(path) except TypeError: return _iter = self.model.get_iter(child_path) if self.model[_iter][11] == 'album': title = self.model.get_value(_iter, 2) artist = self.model.get_value(_iter, 3) item = self.model.get_value(_iter, 5) self._albumWidget.update(artist, title, item, self.header_bar, self.selection_toolbar) self.header_bar.set_state(ToolbarState.SEARCH_VIEW) title = utils.get_media_title(item) self.header_bar.header_bar.set_title(title) self.header_bar.header_bar.sub_title = artist self.set_visible_child(self._albumWidget) self.header_bar.searchbar.show_bar(False) elif self.model[_iter][11] == 'artist': artist = self.model.get_value(_iter, 2) albums = self._artists[artist.casefold()]['albums'] self._artistAlbumsWidget = ArtistAlbumsWidget( artist, albums, self.player, self.header_bar, self.selection_toolbar, self.window, True ) self.add(self._artistAlbumsWidget) self._artistAlbumsWidget.show() self.header_bar.set_state(ToolbarState.SEARCH_VIEW) self.header_bar.header_bar.set_title(artist) self.set_visible_child(self._artistAlbumsWidget) self.header_bar.searchbar.show_bar(False) elif self.model[_iter][11] == 'song': if self.model.get_value(_iter, 12) != DiscoveryStatus.FAILED: child_iter = self.songs_model.convert_child_iter_to_iter(_iter)[1] self.player.set_playlist('Search Results', None, self.songs_model, child_iter, 5, 12) self.player.set_playing(True) else: # Headers if self.view.get_generic_view().row_expanded(path): self.view.get_generic_view().collapse_row(path) else: self.view.get_generic_view().expand_row(path, False) @log def _on_selection_mode_changed(self, widget, data=None): if self._artistAlbumsWidget is not None and self.get_visible_child() == self._artistAlbumsWidget: self._artistAlbumsWidget.set_selection_mode(self.header_bar._selectionMode) @log def _add_search_item(self, source, param, item, remaining=0, data=None): if not item: if grilo._search_callback_counter == 0 and grilo.search_source: self.emit('no-music-found') return if data != self.model: return artist = utils.get_artist_name(item) album = utils.get_album_title(item) composer = item.get_composer() key = '%s-%s' % (artist, album) if key not in self._albums: self._albums[key] = Grl.Media() self._albums[key].set_title(album) self._albums[key].add_artist(artist) self._albums[key].set_composer(composer) self._albums[key].set_source(source.get_id()) self._albums[key].tracks = [] self._add_item(source, None, self._albums[key], 0, [self.model, 'album']) self._add_item(source, None, self._albums[key], 0, [self.model, 'artist']) self._albums[key].tracks.append(item) self._add_item(source, None, item, 0, [self.model, 'song']) @log def _add_item(self, source, param, item, remaining=0, data=None): if data is None: return model, category = data self.found_items_number = ( self.model.iter_n_children(self.head_iters[0]) + self.model.iter_n_children(self.head_iters[1]) + self.model.iter_n_children(self.head_iters[2]) + self.model.iter_n_children(self.head_iters[3])) if category == 'song' and self.found_items_number == 0 and remaining == 0: if grilo.search_source: self.emit('no-music-found') # We need to remember the view before the search view if self.window.curr_view != self.window.views[5] and \ self.window.prev_view != self.window.views[5]: self.previous_view = self.window.prev_view if remaining == 0: self.window.pop_loading_notification() self.view.show() if not item or model != self.model: return self._offset += 1 title = utils.get_media_title(item) item.set_title(title) artist = utils.get_artist_name(item) # FIXME: Can't be None in treemodel composer = item.get_composer() or "" group = 3 try: group = {'album': 0, 'artist': 1, 'song': 2}[category] except: pass # FIXME: HiDPI icon lookups return a surface that can't be # scaled by GdkPixbuf, so it results in a * scale factor sized # icon for the search view. _iter = None if category == 'album': _iter = self.model.insert_with_values( self.head_iters[group], -1, [0, 2, 3, 4, 5, 9, 11, 13], [str(item.get_id()), title, artist, self._loading_icon, item, 2, category, composer]) self.cache.lookup(item, ArtSize.small, self._on_lookup_ready, _iter) elif category == 'song': _iter = self.model.insert_with_values( self.head_iters[group], -1, [0, 2, 3, 4, 5, 9, 11, 13], [str(item.get_id()), title, artist, self._loading_icon, item, 2 if source.get_id() != 'grl-tracker-source' \ else item.get_favourite(), category, composer]) self.cache.lookup(item, ArtSize.small, self._on_lookup_ready, _iter) else: if not artist.casefold() in self._artists: _iter = self.model.insert_with_values( self.head_iters[group], -1, [0, 2, 4, 5, 9, 11, 13], [str(item.get_id()), artist, self._loading_icon, item, 2, category, composer]) self.cache.lookup(item, ArtSize.small, self._on_lookup_ready, _iter) self._artists[artist.casefold()] = {'iter': _iter, 'albums': []} self._artists[artist.casefold()]['albums'].append(item) if self.model.iter_n_children(self.head_iters[group]) == 1: path = self.model.get_path(self.head_iters[group]) path = self.filter_model.convert_child_path_to_path(path) self.view.get_generic_view().expand_row(path, False) @log def _add_list_renderers(self): list_widget = self.view.get_generic_view() list_widget.set_halign(Gtk.Align.CENTER) list_widget.set_size_request(530, -1) cols = list_widget.get_columns() title_renderer = Gtk.CellRendererText( xpad=12, xalign=0.0, yalign=0.5, height=32, ellipsize=Pango.EllipsizeMode.END, weight=Pango.Weight.BOLD ) list_widget.add_renderer(title_renderer, self._on_list_widget_title_render, None) cols[0].add_attribute(title_renderer, 'text', 2) self.star_handler.add_star_renderers(list_widget, cols, hidden=False) cells = cols[0].get_cells() cols[0].reorder(cells[0], -1) cols[0].set_cell_data_func(cells[0], self._on_list_widget_selection_render, None) def _on_list_widget_selection_render(self, col, cell, model, _iter, data): cell.set_visible(self.view.get_selection_mode() and model.iter_parent(_iter) is not None) def _on_list_widget_title_render(self, col, cell, model, _iter, data): cells = col.get_cells() cells[0].set_visible(model.iter_parent(_iter) is not None) cells[1].set_visible(model.iter_parent(_iter) is not None) cells[2].set_visible(model.iter_parent(_iter) is None) @log def populate(self): self._init = True self.window.push_loading_notification() self.header_bar.set_state(ToolbarState.MAIN) @log def get_selected_tracks(self, callback): if self.get_visible_child() == self._albumWidget: callback(self._albumWidget.view.get_selected_items()) elif self.get_visible_child() == self._artistAlbumsWidget: items = [] # FIXME: calling into private model for row in self._artistAlbumsWidget._model: if row[6]: items.append(row[5]) callback(items) else: self.items_selected = [] self.items_selected_callback = callback self._get_selected_albums() @log def _get_selected_albums(self): self.albums_index = 0 self.albums_selected = [self.model[child_path][5] for child_path in [self.filter_model.convert_path_to_child_path(path) for path in self.view.get_selection()] if self.model[child_path][11] == 'album'] if len(self.albums_selected): self._get_selected_albums_songs() else: self._get_selected_artists() @log def _get_selected_albums_songs(self): grilo.populate_album_songs( self.albums_selected[self.albums_index], self._add_selected_albums_songs) self.albums_index += 1 @log def _add_selected_albums_songs(self, source, param, item, remaining=0, data=None): if item: self.items_selected.append(item) if remaining == 0: if self.albums_index < len(self.albums_selected): self._get_selected_albums_songs() else: self._get_selected_artists() @log def _get_selected_artists(self): self.artists_albums_index = 0 self.artists_selected = [self._artists[self.model[child_path][2].casefold()] for child_path in [self.filter_model.convert_path_to_child_path(path) for path in self.view.get_selection()] if self.model[child_path][11] == 'artist'] self.artists_albums_selected = [] for artist in self.artists_selected: self.artists_albums_selected.extend(artist['albums']) if len(self.artists_albums_selected): self._get_selected_artists_albums_songs() else: self._get_selected_songs() @log def _get_selected_artists_albums_songs(self): grilo.populate_album_songs( self.artists_albums_selected[self.artists_albums_index], self._add_selected_artists_albums_songs) self.artists_albums_index += 1 @log def _add_selected_artists_albums_songs(self, source, param, item, remaining=0, data=None): if item: self.items_selected.append(item) if remaining == 0: if self.artists_albums_index < len(self.artists_albums_selected): self._get_selected_artists_albums_songs() else: self._get_selected_songs() @log def _get_selected_songs(self): self.items_selected.extend([self.model[child_path][5] for child_path in [self.filter_model.convert_path_to_child_path(path) for path in self.view.get_selection()] if self.model[child_path][11] == 'song']) self.items_selected_callback(self.items_selected) @log def _filter_visible_func(self, model, _iter, data=None): return model.iter_parent(_iter) is not None or model.iter_has_child(_iter) @log def set_search_text(self, search_term, fields_filter): query_matcher = { 'album': { 'search_all': Query.get_albums_with_any_match, 'search_artist': Query.get_albums_with_artist_match, 'search_composer': Query.get_albums_with_composer_match, 'search_album': Query.get_albums_with_album_match, 'search_track': Query.get_albums_with_track_match, }, 'artist': { 'search_all': Query.get_artists_with_any_match, 'search_artist': Query.get_artists_with_artist_match, 'search_composer': Query.get_artists_with_composer_match, 'search_album': Query.get_artists_with_album_match, 'search_track': Query.get_artists_with_track_match, }, 'song': { 'search_all': Query.get_songs_with_any_match, 'search_artist': Query.get_songs_with_artist_match, 'search_composer': Query.get_songs_with_composer_match, 'search_album': Query.get_songs_with_album_match, 'search_track': Query.get_songs_with_track_match, }, } self.model = Gtk.TreeStore( GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING, # item title or header text GObject.TYPE_STRING, # artist for albums and songs GdkPixbuf.Pixbuf, # album art GObject.TYPE_OBJECT, # item GObject.TYPE_BOOLEAN, GObject.TYPE_INT, GObject.TYPE_STRING, GObject.TYPE_INT, GObject.TYPE_BOOLEAN, GObject.TYPE_STRING, # type GObject.TYPE_INT, GObject.TYPE_STRING, # composer ) self.filter_model = self.model.filter_new(None) self.filter_model.set_visible_func(self._filter_visible_func) self.view.set_model(self.filter_model) self._albums = {} self._artists = {} if search_term == "": return albums_iter = self.model.insert_with_values(None, -1, [2, 9], [_("Albums"), 2]) artists_iter = self.model.insert_with_values(None, -1, [2, 9], [_("Artists"), 2]) songs_iter = self.model.insert_with_values(None, -1, [2, 9], [_("Songs"), 2]) playlists_iter = self.model.insert_with_values(None, -1, [2, 9], [_("Playlists"), 2]) self.head_iters = [albums_iter, artists_iter, songs_iter, playlists_iter] self.songs_model = self.model.filter_new(self.model.get_path(songs_iter)) # Use queries for Tracker if not grilo.search_source or \ grilo.search_source.get_id() == 'grl-tracker-source': for category in ('album', 'artist', 'song'): query = query_matcher[category][fields_filter](search_term) grilo.populate_custom_query(query, self._add_item, -1, [self.model, category]) if not grilo.search_source or \ grilo.search_source.get_id() != 'grl-tracker-source': # nope, can't do - reverting to Search grilo.search(search_term, self._add_search_item, self.model)
class ArtistsView(BaseView): """Main view of all available artists Consists of a list of artists on the left side and an overview of all albums by this artist on the right side. """ def __repr__(self): return '<ArtistsView>' @log def __init__(self, window, player): """Initialize :param GtkWidget window: The main window :param player: The main player object """ self._sidebar = Gtk.ListBox() sidebar_container = Gtk.ScrolledWindow() sidebar_container.add(self._sidebar) super().__init__('artists', _("Artists"), window, sidebar_container) self.player = player self._artists = {} self._window = window self._coremodel = window._app.props.coremodel self._model = self._coremodel.props.artists_sort self._model.connect_after("items-changed", self._on_model_items_changed) self._sidebar.bind_model(self._model, self._create_widget) self._loaded_id = self._coremodel.connect("artists-loaded", self._on_artists_loaded) sidebar_container.props.width_request = 220 sidebar_container.get_style_context().add_class('sidebar') self._sidebar.props.selection_mode = Gtk.SelectionMode.SINGLE self._sidebar.connect('row-activated', self._on_artist_activated) self._ctrl = Gtk.GestureMultiPress().new(self._sidebar) self._ctrl.props.propagation_phase = Gtk.PropagationPhase.CAPTURE self._ctrl.props.button = Gdk.BUTTON_PRIMARY self._ctrl.connect("released", self._on_sidebar_clicked) self._loaded_artists = [] self._loading_id = 0 self.show_all() def _create_widget(self, coreartist): row = ArtistTile(coreartist) row.props.text = coreartist.props.artist self.bind_property("selection-mode", row, "selection-mode") return row def _on_model_items_changed(self, model, position, removed, added): if removed == 0: return removed_artist = None artists = [coreartist.props.artist for coreartist in model] for artist in self._loaded_artists: if artist not in artists: removed_artist = artist break if removed_artist is None: return self._loaded_artists.remove(removed_artist) if self._view.get_visible_child_name() == removed_artist: row_next = (self._sidebar.get_row_at_index(position) or self._sidebar.get_row_at_index(position - 1)) if row_next: self._sidebar.select_row(row_next) row_next.emit("activate") removed_frame = self._view.get_child_by_name(removed_artist) self._view.remove(removed_frame) def _on_artists_loaded(self, klass): self._coremodel.disconnect(self._loaded_id) first_row = self._sidebar.get_row_at_index(0) self._sidebar.select_row(first_row) first_row.emit("activate") @log def _setup_view(self): self._view_container = Gtk.ScrolledWindow(hexpand=True, vexpand=True) self._box.pack_start(self._view_container, True, True, 0) self._view = Gtk.Stack( transition_type=Gtk.StackTransitionType.CROSSFADE, vhomogeneous=False) self._view_container.add(self._view) empty_frame = Gtk.Frame(shadow_type=Gtk.ShadowType.NONE, hexpand=True) empty_frame.show() self._view.add_named(empty_frame, "empty-frame") @log def _on_changes_pending(self, data=None): if (self._init and not self.props.selection_mode): self._artists.clear() self._offset = 0 self._populate() @log def _on_artist_activated(self, sidebar, row, data=None): """Initializes new artist album widgets""" artist_tile = row.get_child() if self.props.selection_mode: artist_tile.props.selected = not artist_tile.props.selected return # Prepare a new artist_albums_widget here coreartist = artist_tile.props.coreartist if coreartist.props.artist in self._loaded_artists: scroll_vadjustment = self._view_container.props.vadjustment scroll_vadjustment.props.value = 0. self._view.set_visible_child_name(coreartist.props.artist) return if self._loading_id > 0: self._artist_albums.disconnect(self._loading_id) self._loading_id = 0 self._artist_albums = ArtistAlbumsWidget(coreartist, self.player, self._window, False) self._loading_id = self._artist_albums.connect( "ready", self._on_artist_albums_ready, coreartist.props.artist) self._view.set_visible_child_name("empty-frame") self._window.notifications_popup.push_loading() return def _on_artist_albums_ready(self, widget, artist): artist_albums_frame = Gtk.Frame(shadow_type=Gtk.ShadowType.NONE, hexpand=True) artist_albums_frame.add(self._artist_albums) artist_albums_frame.show() self._view.add_named(artist_albums_frame, artist) scroll_vadjustment = self._view_container.props.vadjustment scroll_vadjustment.props.value = 0. self._view.set_visible_child(artist_albums_frame) self._window.notifications_popup.pop_loading() self._loaded_artists.append(artist) self._artist_albums.disconnect(self._loading_id) self._loading_id = 0 self._artist_albums = None return @log def _populate(self, data=None): """Populates the view""" pass @log def _on_sidebar_clicked(self, gesture, n_press, x, y): success, state = Gtk.get_current_event_state() modifiers = Gtk.accelerator_get_default_mod_mask() if ((state & modifiers) == Gdk.ModifierType.CONTROL_MASK and not self.props.selection_mode): self.props.selection_mode = True @log def _on_selection_changed(self, widget, value, data=None): return @log def _on_selection_mode_changed(self, widget, data=None): super()._on_selection_mode_changed(widget, data) self._view.props.sensitive = not self.props.selection_mode if self.props.selection_mode: self._sidebar.props.selection_mode = Gtk.SelectionMode.NONE else: self._sidebar.props.selection_mode = Gtk.SelectionMode.SINGLE @log def _toggle_all_selection(self, selected): def toggle_selection(child): tile = child.get_child() tile.props.selected = selected self._sidebar.foreach(toggle_selection) @log def select_all(self): self._toggle_all_selection(True) @log def unselect_all(self): self._toggle_all_selection(False)
class SearchView(BaseView): search_state = GObject.Property(type=int, default=Search.State.NONE) def __repr__(self): return '<SearchView>' @log def __init__(self, window, player): self._coremodel = window._app.props.coremodel self._model = self._coremodel.props.songs_search self._album_model = self._coremodel.props.albums_search self._artist_model = self._coremodel.props.artists_search super().__init__('search', None, window) self.player = player self.previous_view = None self._album_widget = AlbumWidget(player, self) self._album_widget.bind_property("selection-mode", self, "selection-mode", GObject.BindingFlags.BIDIRECTIONAL) self.add(self._album_widget) self._artist_albums_widget = None self._search_mode_active = False # self.connect("notify::search-state", self._on_search_state_changed) @log def _setup_view(self): view_container = Gtk.ScrolledWindow(hexpand=True, vexpand=True) self._box.pack_start(view_container, True, True, 0) self._songs_listbox = Gtk.ListBox() self._songs_listbox.bind_model(self._model, self._create_song_widget) self._album_flowbox = Gtk.FlowBox( homogeneous=True, hexpand=True, halign=Gtk.Align.FILL, valign=Gtk.Align.START, selection_mode=Gtk.SelectionMode.NONE, margin=18, row_spacing=12, column_spacing=6, min_children_per_line=1, max_children_per_line=20, visible=True) self._album_flowbox.get_style_context().add_class('content-view') self._album_flowbox.bind_model(self._album_model, self._create_album_widget) self._album_flowbox.connect("child-activated", self._on_album_activated) self._artist_listbox = Gtk.ListBox() self._artist_listbox.bind_model(self._artist_model, self._create_artist_widget) self._all_results_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self._all_results_box.pack_start(self._album_flowbox, True, True, 0) self._all_results_box.pack_start(self._artist_listbox, True, True, 0) self._all_results_box.pack_start(self._songs_listbox, True, True, 0) # self._ctrl = Gtk.GestureMultiPress().new(self._view) # self._ctrl.props.propagation_phase = Gtk.PropagationPhase.CAPTURE # self._ctrl.connect("released", self._on_view_clicked) view_container.add(self._all_results_box) self._box.show_all() def _create_song_widget(self, coresong): song_widget = SongWidget(coresong) self.bind_property( "selection-mode", song_widget, "selection-mode", GObject.BindingFlags.BIDIRECTIONAL | GObject.BindingFlags.SYNC_CREATE) song_widget.connect('button-release-event', self._song_activated) song_widget.show_all() return song_widget def _create_album_widget(self, corealbum): album_widget = AlbumCover(corealbum) self.bind_property( "selection-mode", album_widget, "selection-mode", GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL) # NOTE: Adding SYNC_CREATE here will trigger all the nested # models to be created. This will slow down initial start, # but will improve initial 'selecte all' speed. album_widget.bind_property("selected", corealbum, "selected", GObject.BindingFlags.BIDIRECTIONAL) return album_widget def _create_artist_widget(self, coreartist): artist_tile = ArtistTile(coreartist) artist_tile.props.text = coreartist.props.artist artist_tile.connect('button-release-event', self._artist_activated) self.bind_property( "selection-mode", artist_tile, "selection-mode", GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL) return artist_tile def _song_activated(self, widget, event): mod_mask = Gtk.accelerator_get_default_mod_mask() if ((event.get_state() & mod_mask) == Gdk.ModifierType.CONTROL_MASK and not self.props.selection_mode): self.props.selection_mode = True return (_, button) = event.get_button() if (button == Gdk.BUTTON_PRIMARY and not self.props.selection_mode): # self.emit('song-activated', widget) self._coremodel.set_playlist_model( PlayerPlaylist.Type.SEARCH_RESULT, self._model) self.player.play(widget.props.coresong) # FIXME: Need to ignore the event from the checkbox. # if self.props.selection_mode: # widget.props.selected = not widget.props.selected return True def _on_album_activated(self, widget, child, user_data=None): corealbum = child.props.corealbum if self.props.selection_mode: return # Update and display the album widget if not in selection mode self._album_widget.update(corealbum) self._headerbar.props.state = HeaderBar.State.SEARCH self._headerbar.props.title = corealbum.props.title self._headerbar.props.subtitle = corealbum.props.artist self.props.search_mode_active = False self.set_visible_child(self._album_widget) def _artist_activated(self, widget, event): coreartist = widget.coreartist mod_mask = Gtk.accelerator_get_default_mod_mask() if ((event.get_state() & mod_mask) == Gdk.ModifierType.CONTROL_MASK and not self.props.selection_mode): self.props.selection_mode = True return (_, button) = event.get_button() if (button == Gdk.BUTTON_PRIMARY and not self.props.selection_mode): # self.emit('song-activated', widget) self._artist_albums_widget = ArtistAlbumsWidget( coreartist, self.player, self._window, False) self.add(self._artist_albums_widget) self._artist_albums_widget.show() self.bind_property('selection-mode', self._artist_albums_widget, 'selection-mode', GObject.BindingFlags.BIDIRECTIONAL) self._headerbar.props.state = HeaderBar.State.SEARCH self._headerbar.props.title = coreartist.artist self._headerbar.props.subtitle = None self.set_visible_child(self._artist_albums_widget) self.props.search_mode_active = False # FIXME: Need to ignore the event from the checkbox. # if self.props.selection_mode: # widget.props.selected = not widget.props.selected return True def _child_select(self, child, value): widget = child.get_child() widget.props.selected = value def _select_all(self, value): with self._model.freeze_notify(): def song_select(child): song_widget = child.get_child() song_widget.props.selected = value def album_select(child): child.props.selected = value def artist_select(child): artist_widget = child.get_child() artist_widget.props.selected = value self._songs_listbox.foreach(song_select) self._album_flowbox.foreach(album_select) self._artist_listbox.foreach(artist_select) def select_all(self): self._select_all(True) def unselect_all(self): self._select_all(False) @log def _back_button_clicked(self, widget, data=None): if self.get_visible_child() == self._artist_albums_widget: self._artist_albums_widget.destroy() self._artist_albums_widget = None elif self.get_visible_child() == self._grid: self._window.views[View.ALBUM].set_visible_child( self._window.views[View.ALBUM]._grid) self.set_visible_child(self._grid) self.props.search_mode_active = True self._headerbar.props.state = HeaderBar.State.MAIN @log def _on_view_clicked(self, gesture, n_press, x, y): """Ctrl+click on self._view triggers selection mode.""" _, state = Gtk.get_current_event_state() modifiers = Gtk.accelerator_get_default_mod_mask() if (state & modifiers == Gdk.ModifierType.CONTROL_MASK and not self.props.selection_mode): self.props.selection_mode = True if (self.selection_mode and not self._star_handler.star_renderer_click): path, col, cell_x, cell_y = self._view.get_path_at_pos(x, y) iter_ = self.model.get_iter(path) self.model[iter_][6] = not self.model[iter_][6] selected_iters = self._get_selected_iters() self.props.selected_items_count = len(selected_iters) @log def _on_selection_mode_changed(self, widget, data=None): super()._on_selection_mode_changed(widget, data) @log def _on_search_state_changed(self, klass, param): # If a search is triggered when selection mode is activated, # reset the number of selected items. if (self.props.selection_mode and self.props.search_state != Search.State.NONE): self.props.selected_items_count = 0 @GObject.Property(type=bool, default=False) def search_mode_active(self): """Get search mode status. :returns: the search mode status :rtype: bool """ return self._search_mode_active @search_mode_active.setter def search_mode_active(self, value): """Set search mode status. :param bool mode: new search mode """ # FIXME: search_mode_active should not change search_state. # This is necessary because Search state cannot interact with # the child views. self._search_mode_active = value if (not self._search_mode_active and self.get_visible_child() == self._grid): self.props.search_state = Search.State.NONE @log def _populate(self, data=None): self._init = True self._headerbar.props.state = HeaderBar.State.MAIN
class SearchView(BaseView): __gsignals__ = { 'no-music-found': (GObject.SignalFlags.RUN_FIRST, None, ()) } def __repr__(self): return '<SearchView>' @log def __init__(self, window, player): super().__init__('search', None, window, Gd.MainViewType.LIST) self._add_list_renderers() self.player = player self._head_iters = [None, None, None, None] self._filter_model = None self.previous_view = None self.connect('no-music-found', self._no_music_found_callback) self._albums_selected = [] self._albums = {} self._albums_index = 0 self._album_widget = AlbumWidget(player, self) self.add(self._album_widget) self._artists_albums_selected = [] self._artists_albums_index = 0 self._artists = {} self._artist_albums_widget = None self._view.get_generic_view().set_show_expanders(False) self._items_selected = [] self._items_selected_callback = None self._items_found = None @log def _no_music_found_callback(self, view): # FIXME: call into private members self._window._stack.set_visible_child_name('emptysearch') emptysearch = self._window._stack.get_child_by_name('emptysearch') emptysearch._artist_albums_widget = self._artist_albums_widget @log def _back_button_clicked(self, widget, data=None): self._header_bar.searchbar.reveal(True, False) if self.get_visible_child() == self._artist_albums_widget: self._artist_albums_widget.destroy() self._artist_albums_widget = None elif self.get_visible_child() == self._grid: self._window.views[View.ALBUM].set_visible_child( self._window.views[View.ALBUM]._grid) self.set_visible_child(self._grid) self._window.toolbar.set_state(ToolbarState.MAIN) @log def _on_item_activated(self, widget, id, path): if self._star_handler.star_renderer_click: self._star_handler.star_renderer_click = False return try: child_path = self._filter_model.convert_path_to_child_path(path) except TypeError: return _iter = self.model.get_iter(child_path) if self.model[_iter][11] == 'album': title = self.model[_iter][2] artist = self.model[_iter][3] item = self.model[_iter][5] self._album_widget.update( artist, title, item, self._header_bar, self._selection_toolbar) self._header_bar.set_state(ToolbarState.SEARCH_VIEW) self._header_bar.header_bar.set_title(title) self._header_bar.header_bar.sub_title = artist self.set_visible_child(self._album_widget) self._header_bar.searchbar.reveal(False) elif self.model[_iter][11] == 'artist': artist = self.model[_iter][2] albums = self._artists[artist.casefold()]['albums'] self._artist_albums_widget = ArtistAlbumsWidget( artist, albums, self.player, self._header_bar, self._selection_toolbar, self._window, True) self.add(self._artist_albums_widget) self._artist_albums_widget.show() self._header_bar.set_state(ToolbarState.SEARCH_VIEW) self._header_bar.header_bar.set_title(artist) self.set_visible_child(self._artist_albums_widget) self._header_bar.searchbar.reveal(False) elif self.model[_iter][11] == 'song': if self.model[_iter][12] != DiscoveryStatus.FAILED: c_iter = self._songs_model.convert_child_iter_to_iter(_iter)[1] self.player.set_playlist( 'Search Results', None, self._songs_model, c_iter, 5, 12) self.player.set_playing(True) else: # Headers if self._view.get_generic_view().row_expanded(path): self._view.get_generic_view().collapse_row(path) else: self._view.get_generic_view().expand_row(path, False) @log def _on_selection_mode_changed(self, widget, data=None): if (self._artist_albums_widget is not None and self.get_visible_child() == self._artist_albums_widget): self._artist_albums_widget.set_selection_mode( self._header_bar.selection_mode) @log def _add_search_item(self, source, param, item, remaining=0, data=None): if not item: if (grilo._search_callback_counter == 0 and grilo.search_source): self.emit('no-music-found') return if data != self.model: return artist = utils.get_artist_name(item) album = utils.get_album_title(item) key = '%s-%s' % (artist, album) if key not in self._albums: self._albums[key] = Grl.Media() self._albums[key].set_title(album) self._albums[key].add_artist(artist) self._albums[key].set_source(source.get_id()) self._albums[key].songs = [] self._add_item( source, None, self._albums[key], 0, [self.model, 'album']) self._add_item( source, None, self._albums[key], 0, [self.model, 'artist']) self._albums[key].songs.append(item) self._add_item(source, None, item, 0, [self.model, 'song']) @log def _retrieval_finished(self, klass, model, _iter): if not model[_iter][13]: return model[_iter][13] = klass.surface @log def _add_item(self, source, param, item, remaining=0, data=None): if data is None: return model, category = data self._items_found = ( self.model.iter_n_children(self._head_iters[0]) + self.model.iter_n_children(self._head_iters[1]) + self.model.iter_n_children(self._head_iters[2]) + self.model.iter_n_children(self._head_iters[3]) ) if (category == 'song' and self._items_found == 0 and remaining == 0): if grilo.search_source: self.emit('no-music-found') # We need to remember the view before the search view emptysearchview = self._window.views[View.EMPTY_SEARCH] if (self._window.curr_view != emptysearchview and self._window.prev_view != emptysearchview): self.previous_view = self._window.prev_view if remaining == 0: self._window.notifications_popup.pop_loading() self._view.show() if not item or model != self.model: return self._offset += 1 title = utils.get_media_title(item) item.set_title(title) artist = utils.get_artist_name(item) group = 3 try: group = {'album': 0, 'artist': 1, 'song': 2}[category] except: pass _iter = None if category == 'album': _iter = self.model.insert_with_values( self._head_iters[group], -1, [0, 2, 3, 5, 9, 11], [str(item.get_id()), title, artist, item, 2, category]) elif category == 'song': # FIXME: source specific hack if source.get_id() != 'grl-tracker-source': fav = 2 else: fav = item.get_favourite() _iter = self.model.insert_with_values( self._head_iters[group], -1, [0, 2, 3, 5, 9, 11], [str(item.get_id()), title, artist, item, fav, category]) else: if not artist.casefold() in self._artists: _iter = self.model.insert_with_values( self._head_iters[group], -1, [0, 2, 5, 9, 11], [str(item.get_id()), artist, item, 2, category]) self._artists[artist.casefold()] = { 'iter': _iter, 'albums': [] } self._artists[artist.casefold()]['albums'].append(item) # FIXME: Figure out why iter can be None here, seems illogical. if _iter is not None: scale = self._view.get_scale_factor() art = Art(Art.Size.SMALL, item, scale) self.model[_iter][13] = art.surface art.connect( 'finished', self._retrieval_finished, self.model, _iter) art.lookup() if self.model.iter_n_children(self._head_iters[group]) == 1: path = self.model.get_path(self._head_iters[group]) path = self._filter_model.convert_child_path_to_path(path) self._view.get_generic_view().expand_row(path, False) @log def _add_list_renderers(self): list_widget = self._view.get_generic_view() list_widget.set_halign(Gtk.Align.CENTER) list_widget.set_size_request(530, -1) cols = list_widget.get_columns() title_renderer = Gtk.CellRendererText( xpad=12, xalign=0.0, yalign=0.5, height=32, ellipsize=Pango.EllipsizeMode.END, weight=Pango.Weight.BOLD) list_widget.add_renderer( title_renderer, self._on_list_widget_title_render, None) cols[0].add_attribute(title_renderer, 'text', 2) # Add our own surface renderer, instead of the one provided by # Gd. This avoids us having to set the model to a cairo.Surface # which is currently not a working solution in pygobject. # https://gitlab.gnome.org/GNOME/pygobject/issues/155 pixbuf_renderer = Gtk.CellRendererPixbuf( xalign=0.5, yalign=0.5, xpad=12, ypad=2) list_widget.add_renderer( pixbuf_renderer, self._on_list_widget_pixbuf_renderer, None) cols[0].add_attribute(pixbuf_renderer, 'surface', 13) self._star_handler.add_star_renderers(list_widget, cols[0]) cells = cols[0].get_cells() # With the bugfix in libgd 9117650bda, the search results # stopped aligning at the top. With the artists results not # having a second line of text, this looks off. # Revert to old behaviour by forcing the alignment to be top. # FIXME: Revisit this when rewriting the searchview. gd_twolines_renderer = cells[2] gd_twolines_renderer.props.yalign = 0 cols[0].reorder(cells[0], -1) cols[0].reorder(cells[4], 1) cols[0].set_cell_data_func( cells[0], self._on_list_widget_selection_render, None) @log def _on_list_widget_pixbuf_renderer(self, col, cell, model, _iter, data): if not model[_iter][13]: return cell.set_property("surface", model[_iter][13]) @log def _on_list_widget_selection_render(self, col, cell, model, _iter, data): if (self._view.get_selection_mode() and model.iter_parent(_iter) is not None): cell.set_visible(True) else: cell.set_visible(False) @log def _on_list_widget_title_render(self, col, cell, model, _iter, data): cells = col.get_cells() cells[0].set_visible(False) cells[1].set_visible(model.iter_parent(_iter) is not None) cells[2].set_visible(model.iter_parent(_iter) is not None) cells[3].set_visible(model.iter_parent(_iter) is None) @log def populate(self): self._init = True self._window.notifications_popup.push_loading() self._header_bar.set_state(ToolbarState.MAIN) @log def get_selected_songs(self, callback): if self.get_visible_child() == self._album_widget: callback(self._album_widget.view.get_selected_items()) elif self.get_visible_child() == self._artist_albums_widget: items = [] # FIXME: calling into private model for row in self._artist_albums_widget._model: if row[6]: items.append(row[5]) callback(items) else: self._items_selected = [] self._items_selected_callback = callback self._get_selected_albums() @log def _get_selected_albums(self): paths = [ self._filter_model.convert_path_to_child_path(path) for path in self._view.get_selection()] self._albums_selected = [ self.model[child_path][5] for child_path in paths if self.model[child_path][11] == 'album'] if len(self._albums_selected): self._get_selected_albums_songs() else: self._get_selected_artists() @log def _get_selected_albums_songs(self): grilo.populate_album_songs( self._albums_selected[self._albums_index], self._add_selected_albums_songs) self._albums_index += 1 @log def _add_selected_albums_songs( self, source, param, item, remaining=0, data=None): if item: self._items_selected.append(item) if remaining == 0: if self._albums_index < len(self._albums_selected): self._get_selected_albums_songs() else: self._get_selected_artists() @log def _get_selected_artists(self): artists_selected = [ self._artists[self.model[child_path][2].casefold()] for child_path in [ self._filter_model.convert_path_to_child_path(path) for path in self._view.get_selection()] if self.model[child_path][11] == 'artist'] self._artists_albums_selected = [] for artist in artists_selected: self._artists_albums_selected.extend(artist['albums']) if len(self._artists_albums_selected): self._get_selected_artists_albums_songs() else: self._get_selected_songs() @log def _get_selected_artists_albums_songs(self): grilo.populate_album_songs( self._artists_albums_selected[self._artists_albums_index], self._add_selected_artists_albums_songs) self._artists_albums_index += 1 @log def _add_selected_artists_albums_songs( self, source, param, item, remaining=0, data=None): if item: self._items_selected.append(item) if remaining == 0: artist_albums = len(self._artists_albums_selected) if self._artists_albums_index < artist_albums: self._get_selected_artists_albums_songs() else: self._get_selected_songs() @log def _get_selected_songs(self): self._items_selected.extend([ self.model[child_path][5] for child_path in [ self._filter_model.convert_path_to_child_path(path) for path in self._view.get_selection()] if self.model[child_path][11] == 'song']) self._items_selected_callback(self._items_selected) @log def _filter_visible_func(self, model, _iter, data=None): top_level = model.iter_parent(_iter) is None visible = (not top_level or model.iter_has_child(_iter)) return visible @log def set_search_text(self, search_term, fields_filter): query_matcher = { 'album': { 'search_all': Query.get_albums_with_any_match, 'search_artist': Query.get_albums_with_artist_match, 'search_composer': Query.get_albums_with_composer_match, 'search_album': Query.get_albums_with_album_match, 'search_track': Query.get_albums_with_track_match, }, 'artist': { 'search_all': Query.get_artists_with_any_match, 'search_artist': Query.get_artists_with_artist_match, 'search_composer': Query.get_artists_with_composer_match, 'search_album': Query.get_artists_with_album_match, 'search_track': Query.get_artists_with_track_match, }, 'song': { 'search_all': Query.get_songs_with_any_match, 'search_artist': Query.get_songs_with_artist_match, 'search_composer': Query.get_songs_with_composer_match, 'search_album': Query.get_songs_with_album_match, 'search_track': Query.get_songs_with_track_match, }, } self.model = Gtk.TreeStore( GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING, # item title or header text GObject.TYPE_STRING, # artist for albums and songs GdkPixbuf.Pixbuf, # Gd placeholder album art GObject.TYPE_OBJECT, # item GObject.TYPE_BOOLEAN, GObject.TYPE_INT, GObject.TYPE_STRING, GObject.TYPE_INT, GObject.TYPE_BOOLEAN, GObject.TYPE_STRING, # type GObject.TYPE_INT, object # album art surface ) self._filter_model = self.model.filter_new(None) self._filter_model.set_visible_func(self._filter_visible_func) self._view.set_model(self._filter_model) self._albums = {} self._artists = {} if search_term == "": return albums_iter = self.model.insert_with_values( None, -1, [2, 9], [_("Albums"), 2]) artists_iter = self.model.insert_with_values( None, -1, [2, 9], [_("Artists"), 2]) songs_iter = self.model.insert_with_values( None, -1, [2, 9], [_("Songs"), 2]) playlists_iter = self.model.insert_with_values( None, -1, [2, 9], [_("Playlists"), 2]) self._head_iters = [ albums_iter, artists_iter, songs_iter, playlists_iter ] self._songs_model = self.model.filter_new( self.model.get_path(songs_iter)) # Use queries for Tracker if (not grilo.search_source or grilo.search_source.get_id() == 'grl-tracker-source'): for category in ('album', 'artist', 'song'): query = query_matcher[category][fields_filter](search_term) grilo.populate_custom_query( query, self._add_item, -1, [self.model, category]) if (not grilo.search_source or grilo.search_source.get_id() != 'grl-tracker-source'): # nope, can't do - reverting to Search grilo.search(search_term, self._add_search_item, self.model)
class SearchView(Gtk.Stack): """Gridlike view of search results. Three sections: artists, albums, songs. """ __gtype_name__ = "SearchView" search_state = GObject.Property(type=int, default=Search.State.NONE) selected_items_count = GObject.Property(type=int, default=0, minimum=0) selection_mode = GObject.Property(type=bool, default=False) _album_header = Gtk.Template.Child() _album_flowbox = Gtk.Template.Child() _album_all_flowbox = Gtk.Template.Child() _all_search_results = Gtk.Template.Child() _artist_header = Gtk.Template.Child() _artist_all_flowbox = Gtk.Template.Child() _artist_flowbox = Gtk.Template.Child() _search_results = Gtk.Template.Child() _songs_header = Gtk.Template.Child() _songs_listbox = Gtk.Template.Child() _view_all_albums = Gtk.Template.Child() _view_all_artists = Gtk.Template.Child() def __repr__(self): return '<SearchView>' @log def __init__(self, application, player=None): """Initialize SearchView :param GtkApplication application: The Application object """ super().__init__(transition_type=Gtk.StackTransitionType.CROSSFADE) # FIXME: Make these properties. self.name = "search" self.title = None self._application = application self._coremodel = application.props.coremodel self._model = self._coremodel.props.songs_search self._album_model = self._coremodel.props.albums_search self._album_filter = self._coremodel.props.albums_search_filter self._album_filter.set_filter_func( self._core_filter, self._album_model, 12) self._artist_model = self._coremodel.props.artists_search self._artist_filter = self._coremodel.props.artists_search_filter self._artist_filter.set_filter_func( self._core_filter, self._artist_model, 6) self._model.connect_after( "items-changed", self._on_model_items_changed) self._songs_listbox.bind_model(self._model, self._create_song_widget) self._on_model_items_changed(self._model, 0, 0, 0) self._album_filter.connect_after( "items-changed", self._on_album_model_items_changed) self._album_flowbox.bind_model( self._album_filter, self._create_album_widget) self._album_flowbox.connect( "size-allocate", self._on_album_flowbox_size_allocate) self._on_album_model_items_changed(self._album_filter, 0, 0, 0) self._artist_filter.connect_after( "items-changed", self._on_artist_model_items_changed) self._artist_flowbox.bind_model( self._artist_filter, self._create_artist_widget) self._artist_flowbox.connect( "size-allocate", self._on_artist_flowbox_size_allocate) self._on_artist_model_items_changed(self._artist_filter, 0, 0, 0) self._player = self._application.props.player self._window = application.props.window self._headerbar = self._window._headerbar self.connect("notify::selection-mode", self._on_selection_mode_changed) self.bind_property( 'selection-mode', self._window, 'selection-mode', GObject.BindingFlags.BIDIRECTIONAL) self._album_widget = AlbumWidget(player, self) self._album_widget.bind_property( "selection-mode", self, "selection-mode", GObject.BindingFlags.BIDIRECTIONAL) self.add(self._album_widget) self._artist_albums_widget = None self._search_mode_active = False # self.connect("notify::search-state", self._on_search_state_changed) def _core_filter(self, coreitem, coremodel, nr_items): if coremodel.get_n_items() <= 5: return True for i in range(nr_items): if coremodel.get_item(i) == coreitem: return True return False def _create_song_widget(self, coresong): song_widget = SongWidget(coresong, False, True) song_widget.props.show_song_number = False self.bind_property( "selection-mode", song_widget, "selection-mode", GObject.BindingFlags.BIDIRECTIONAL | GObject.BindingFlags.SYNC_CREATE) song_widget.connect('button-release-event', self._song_activated) return song_widget def _create_album_widget(self, corealbum): album_widget = AlbumCover(corealbum) self.bind_property( "selection-mode", album_widget, "selection-mode", GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL) # NOTE: Adding SYNC_CREATE here will trigger all the nested # models to be created. This will slow down initial start, # but will improve initial 'selecte all' speed. album_widget.bind_property( "selected", corealbum, "selected", GObject.BindingFlags.BIDIRECTIONAL) return album_widget def _create_artist_widget(self, coreartist): artist_tile = ArtistSearchTile(coreartist) self.bind_property( "selection-mode", artist_tile, "selection-mode", GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL) return artist_tile def _on_album_model_items_changed(self, model, position, removed, added): items_found = model.get_n_items() > 0 self._album_header.props.visible = items_found self._album_flowbox.props.visible = items_found self._check_visibility() nr_albums = self._album_model.get_n_items() self._view_all_albums.props.visible = (nr_albums > model.get_n_items()) def _on_artist_model_items_changed(self, model, position, removed, added): items_found = model.get_n_items() > 0 self._artist_header.props.visible = items_found self._artist_flowbox.props.visible = items_found self._check_visibility() nr_artists = self._artist_model.get_n_items() self._view_all_artists.props.visible = ( nr_artists > model.get_n_items()) def _on_model_items_changed(self, model, position, removed, added): items_found = model.get_n_items() > 0 self._songs_header.props.visible = items_found self._songs_listbox.props.visible = items_found self._check_visibility() def _check_visibility(self): if not self.props.search_mode_active: return items_found = (self._model.get_n_items() > 0 or self._artist_model.get_n_items() > 0 or self._album_model.get_n_items() > 0) if items_found: self.props.search_state = Search.State.RESULT else: self.props.search_state = Search.State.NO_RESULT def _song_activated(self, widget, event): mod_mask = Gtk.accelerator_get_default_mod_mask() if ((event.get_state() & mod_mask) == Gdk.ModifierType.CONTROL_MASK and not self.props.selection_mode): self.props.selection_mode = True return (_, button) = event.get_button() if (button == Gdk.BUTTON_PRIMARY and not self.props.selection_mode): # self.emit('song-activated', widget) self._coremodel.set_player_model( PlayerPlaylist.Type.SEARCH_RESULT, self._model) self._player.play(widget.props.coresong) # FIXME: Need to ignore the event from the checkbox. # if self.props.selection_mode: # widget.props.selected = not widget.props.selected return True def _on_album_flowbox_size_allocate(self, widget, allocation, data=None): nb_children = self._album_filter.get_n_items() if nb_children == 0: return first_child = self._album_flowbox.get_child_at_index(0) child_height = first_child.get_allocation().height if allocation.height > 2.5 * child_height: for i in range(nb_children - 1, -1, -1): child = self._album_flowbox.get_child_at_index(i) if child.props.visible is True: child.props.visible = False return children_hidden = False for idx in range(nb_children): child = self._album_flowbox.get_child_at_index(idx) if not child.props.visible: children_hidden = True break if children_hidden is False: return last_visible_child = self._album_flowbox.get_child_at_index(idx - 1) first_row_last = self._album_flowbox.get_child_at_index((idx - 1) // 2) second_row_pos = last_visible_child.get_allocation().x first_row_pos = first_row_last.get_allocation().x child_width = last_visible_child.get_allocation().width nb_children_to_add = (first_row_pos - second_row_pos) // child_width nb_children_to_add = min(nb_children_to_add + idx, nb_children) for i in range(idx, nb_children_to_add): child = self._album_flowbox.get_child_at_index(i) child.props.visible = True def _on_artist_flowbox_size_allocate(self, widget, allocation, data=None): nb_children = self._artist_filter.get_n_items() if nb_children == 0: return first_child = self._album_flowbox.get_child_at_index(0) # FIXME: It looks like it is possible that the widget is not # yet created, resulting in a crash with first_child being # None. # Look for a cleaner solution. if first_child is None: return child_height = first_child.get_allocation().height if allocation.height > 1.5 * child_height: for i in range(nb_children - 1, -1, -1): child = self._artist_flowbox.get_child_at_index(i) if child.props.visible is True: child.props.visible = False return children_hidden = False for idx in range(nb_children): child = self._artist_flowbox.get_child_at_index(idx) if not child.props.visible: children_hidden = True break if children_hidden is False: return last_child = self._artist_flowbox.get_child_at_index(idx - 1) last_child_allocation = last_child.get_allocation() child_width = last_child_allocation.width if (last_child_allocation.x + 2 * child_width) < allocation.width: child = self._artist_flowbox.get_child_at_index(idx) child.props.visible = True @Gtk.Template.Callback() def _on_album_activated(self, widget, child, user_data=None): corealbum = child.props.corealbum if self.props.selection_mode: return # Update and display the album widget if not in selection mode self._album_widget.update(corealbum) self._headerbar.props.state = HeaderBar.State.SEARCH self._headerbar.props.title = corealbum.props.title self._headerbar.props.subtitle = corealbum.props.artist self.set_visible_child(self._album_widget) self.props.search_mode_active = False @Gtk.Template.Callback() def _on_artist_activated(self, widget, child, user_data=None): coreartist = child.props.coreartist if self.props.selection_mode: return self._artist_albums_widget = ArtistAlbumsWidget( coreartist, self._application, False) # FIXME: Adding scrolled windows without removing them. scrolled_window = Gtk.ScrolledWindow() scrolled_window.add(self._artist_albums_widget) scrolled_window.props.visible = True self.add(scrolled_window) self._artist_albums_widget.show() self.bind_property( "selection-mode", self._artist_albums_widget, "selection-mode", GObject.BindingFlags.BIDIRECTIONAL) self._headerbar.props.state = HeaderBar.State.SEARCH self._headerbar.props.title = coreartist.props.artist self._headerbar.props.subtitle = None self.set_visible_child(scrolled_window) self.props.search_mode_active = False @Gtk.Template.Callback() def _on_all_artists_clicked(self, widget, event, user_data=None): self._headerbar.props.state = HeaderBar.State.SEARCH self._headerbar.props.title = _("Artists Results") self._headerbar.props.subtitle = None self._artist_all_flowbox.props.visible = True self._album_all_flowbox.props.visible = False self._artist_all_flowbox.bind_model( self._artist_model, self._create_artist_widget) self.props.visible_child = self._all_search_results self.props.search_mode_active = False @Gtk.Template.Callback() def _on_all_albums_clicked(self, widget, event, user_data=None): self._headerbar.props.state = HeaderBar.State.SEARCH self._headerbar.props.title = _("Albums Results") self._headerbar.props.subtitle = None self._artist_all_flowbox.props.visible = False self._album_all_flowbox.props.visible = True self._album_all_flowbox.bind_model( self._album_model, self._create_album_widget) self.props.visible_child = self._all_search_results self.props.search_mode_active = False def _select_all(self, value): with self._model.freeze_notify(): def song_select(child): song_widget = child.get_child() song_widget.props.selected = value def album_select(child): child.props.selected = value def artist_select(child): child.props.selected = value self._songs_listbox.foreach(song_select) self._album_flowbox.foreach(album_select) self._artist_flowbox.foreach(artist_select) def select_all(self): self._select_all(True) def unselect_all(self): self._select_all(False) @log def _back_button_clicked(self, widget, data=None): if self.get_visible_child() == self._artist_albums_widget: self._artist_albums_widget.destroy() self._artist_albums_widget = None elif self.get_visible_child() == self._search_results: self._window.views[View.ALBUM].set_visible_child( self._window.views[View.ALBUM]._grid) self.set_visible_child(self._search_results) self.props.search_mode_active = True self._headerbar.props.state = HeaderBar.State.MAIN @log def _on_selection_mode_changed(self, widget, data=None): if not self.props.selection_mode: self.unselect_all() @log def _on_search_state_changed(self, klass, param): # If a search is triggered when selection mode is activated, # reset the number of selected items. if (self.props.selection_mode and self.props.search_state != Search.State.NONE): self.props.selected_items_count = 0 @GObject.Property(type=bool, default=False) def search_mode_active(self): """Get search mode status. :returns: the search mode status :rtype: bool """ return self._search_mode_active @search_mode_active.setter def search_mode_active(self, value): """Set search mode status. :param bool mode: new search mode """ # FIXME: search_mode_active should not change search_state. # This is necessary because Search state cannot interact with # the child views. self._search_mode_active = value if (not self._search_mode_active and self.get_visible_child() == self._search_results): self.props.search_state = Search.State.NONE
class SearchView(BaseView): __gsignals__ = { 'no-music-found': (GObject.SignalFlags.RUN_FIRST, None, ()) } def __repr__(self): return '<SearchView>' @log def __init__(self, window, player): BaseView.__init__(self, 'search', None, window, Gd.MainViewType.LIST) self._items = {} self.isStarred = None self.iter_to_clean = None scale = self.get_scale_factor() loading_icon_surface = DefaultIcon(scale).get(DefaultIcon.Type.loading, ArtSize.small) self._loading_icon = Gdk.pixbuf_get_from_surface( loading_icon_surface, 0, 0, loading_icon_surface.get_width(), loading_icon_surface.get_height()) self._add_list_renderers() self.player = player self.head_iters = [None, None, None, None] self.songs_model = self.model self.previous_view = None self.connect('no-music-found', self._no_music_found_callback) self.albums_selected = [] self._albums = {} self._albumWidget = AlbumWidget(player, self) self.add(self._albumWidget) self.artists_albums_selected = [] self._artists = {} self._artistAlbumsWidget = None self._view.get_generic_view().set_show_expanders(False) self.items_selected = [] self.items_selected_callback = None self.found_items_number = None @log def _no_music_found_callback(self, view): self._window._stack.set_visible_child_name('emptysearch') emptysearch = self._window._stack.get_child_by_name('emptysearch') emptysearch._artistAlbumsWidget = self._artistAlbumsWidget @log def _back_button_clicked(self, widget, data=None): self._header_bar.searchbar.show_bar(True, False) if self.get_visible_child() == self._artistAlbumsWidget: self._artistAlbumsWidget.destroy() self._artistAlbumsWidget = None elif self.get_visible_child() == self._grid: self._window.views[0].set_visible_child( self._window.views[0]._grid) self.set_visible_child(self._grid) self._window.toolbar.set_state(ToolbarState.MAIN) @log def _on_item_activated(self, widget, id, path): if self._star_handler.star_renderer_click: self._star_handler.star_renderer_click = False return try: child_path = self.filter_model.convert_path_to_child_path(path) except TypeError: return _iter = self.model.get_iter(child_path) if self.model[_iter][11] == 'album': title = self.model.get_value(_iter, 2) artist = self.model.get_value(_iter, 3) item = self.model.get_value(_iter, 5) self._albumWidget.update(artist, title, item, self._header_bar, self._selection_toolbar) self._header_bar.set_state(ToolbarState.SEARCH_VIEW) title = utils.get_media_title(item) self._header_bar.header_bar.set_title(title) self._header_bar.header_bar.sub_title = artist self.set_visible_child(self._albumWidget) self._header_bar.searchbar.show_bar(False) elif self.model[_iter][11] == 'artist': artist = self.model.get_value(_iter, 2) albums = self._artists[artist.casefold()]['albums'] self._artistAlbumsWidget = ArtistAlbumsWidget( artist, albums, self.player, self._header_bar, self._selection_toolbar, self._window, True) self.add(self._artistAlbumsWidget) self._artistAlbumsWidget.show() self._header_bar.set_state(ToolbarState.SEARCH_VIEW) self._header_bar.header_bar.set_title(artist) self.set_visible_child(self._artistAlbumsWidget) self._header_bar.searchbar.show_bar(False) elif self.model[_iter][11] == 'song': if self.model.get_value(_iter, 12) != DiscoveryStatus.FAILED: child_iter = self.songs_model.convert_child_iter_to_iter( _iter)[1] self.player.set_playlist('Search Results', None, self.songs_model, child_iter, 5, 12) self.player.set_playing(True) else: # Headers if self._view.get_generic_view().row_expanded(path): self._view.get_generic_view().collapse_row(path) else: self._view.get_generic_view().expand_row(path, False) @log def _on_selection_mode_changed(self, widget, data=None): if self._artistAlbumsWidget is not None and self.get_visible_child( ) == self._artistAlbumsWidget: self._artistAlbumsWidget.set_selection_mode( self._header_bar._selectionMode) @log def _add_search_item(self, source, param, item, remaining=0, data=None): if not item: if grilo._search_callback_counter == 0 and grilo.search_source: self.emit('no-music-found') return if data != self.model: return artist = utils.get_artist_name(item) album = utils.get_album_title(item) composer = item.get_composer() key = '%s-%s' % (artist, album) if key not in self._albums: self._albums[key] = Grl.Media() self._albums[key].set_title(album) self._albums[key].add_artist(artist) self._albums[key].set_composer(composer) self._albums[key].set_source(source.get_id()) self._albums[key].songs = [] self._add_item(source, None, self._albums[key], 0, [self.model, 'album']) self._add_item(source, None, self._albums[key], 0, [self.model, 'artist']) self._albums[key].songs.append(item) self._add_item(source, None, item, 0, [self.model, 'song']) @log def _add_item(self, source, param, item, remaining=0, data=None): if data is None: return model, category = data self.found_items_number = ( self.model.iter_n_children(self.head_iters[0]) + self.model.iter_n_children(self.head_iters[1]) + self.model.iter_n_children(self.head_iters[2]) + self.model.iter_n_children(self.head_iters[3])) if category == 'song' and self.found_items_number == 0 and remaining == 0: if grilo.search_source: self.emit('no-music-found') # We need to remember the view before the search view if self._window.curr_view != self._window.views[5] and \ self._window.prev_view != self._window.views[5]: self.previous_view = self._window.prev_view if remaining == 0: self._window.pop_loading_notification() self._view.show() if not item or model != self.model: return self._offset += 1 title = utils.get_media_title(item) item.set_title(title) artist = utils.get_artist_name(item) # FIXME: Can't be None in treemodel composer = item.get_composer() or "" group = 3 try: group = {'album': 0, 'artist': 1, 'song': 2}[category] except: pass # FIXME: HiDPI icon lookups return a surface that can't be # scaled by GdkPixbuf, so it results in a * scale factor sized # icon for the search view. _iter = None if category == 'album': _iter = self.model.insert_with_values( self.head_iters[group], -1, [0, 2, 3, 4, 5, 9, 11, 13], [ str(item.get_id()), title, artist, self._loading_icon, item, 2, category, composer ]) self._cache.lookup(item, ArtSize.small, self._on_lookup_ready, _iter) elif category == 'song': _iter = self.model.insert_with_values( self.head_iters[group], -1, [0, 2, 3, 4, 5, 9, 11, 13], [str(item.get_id()), title, artist, self._loading_icon, item, 2 if source.get_id() != 'grl-tracker-source' \ else item.get_favourite(), category, composer]) self._cache.lookup(item, ArtSize.small, self._on_lookup_ready, _iter) else: if not artist.casefold() in self._artists: _iter = self.model.insert_with_values( self.head_iters[group], -1, [0, 2, 4, 5, 9, 11, 13], [ str(item.get_id()), artist, self._loading_icon, item, 2, category, composer ]) self._cache.lookup(item, ArtSize.small, self._on_lookup_ready, _iter) self._artists[artist.casefold()] = { 'iter': _iter, 'albums': [] } self._artists[artist.casefold()]['albums'].append(item) if self.model.iter_n_children(self.head_iters[group]) == 1: path = self.model.get_path(self.head_iters[group]) path = self.filter_model.convert_child_path_to_path(path) self._view.get_generic_view().expand_row(path, False) @log def _add_list_renderers(self): list_widget = self._view.get_generic_view() list_widget.set_halign(Gtk.Align.CENTER) list_widget.set_size_request(530, -1) cols = list_widget.get_columns() title_renderer = Gtk.CellRendererText( xpad=12, xalign=0.0, yalign=0.5, height=32, ellipsize=Pango.EllipsizeMode.END, weight=Pango.Weight.BOLD) list_widget.add_renderer(title_renderer, self._on_list_widget_title_render, None) cols[0].add_attribute(title_renderer, 'text', 2) self._star_handler.add_star_renderers(list_widget, cols[0]) cells = cols[0].get_cells() cols[0].reorder(cells[0], -1) cols[0].set_cell_data_func(cells[0], self._on_list_widget_selection_render, None) def _on_list_widget_selection_render(self, col, cell, model, _iter, data): cell.set_visible(self._view.get_selection_mode() and model.iter_parent(_iter) is not None) def _on_list_widget_title_render(self, col, cell, model, _iter, data): cells = col.get_cells() cells[0].set_visible(model.iter_parent(_iter) is not None) cells[1].set_visible(model.iter_parent(_iter) is not None) cells[2].set_visible(model.iter_parent(_iter) is None) @log def populate(self): self._init = True self._window.push_loading_notification() self._header_bar.set_state(ToolbarState.MAIN) @log def get_selected_songs(self, callback): if self.get_visible_child() == self._albumWidget: callback(self._albumWidget.view.get_selected_items()) elif self.get_visible_child() == self._artistAlbumsWidget: items = [] # FIXME: calling into private model for row in self._artistAlbumsWidget._model: if row[6]: items.append(row[5]) callback(items) else: self.items_selected = [] self.items_selected_callback = callback self._get_selected_albums() @log def _get_selected_albums(self): self.albums_index = 0 self.albums_selected = [ self.model[child_path][5] for child_path in [ self.filter_model.convert_path_to_child_path(path) for path in self._view.get_selection() ] if self.model[child_path][11] == 'album' ] if len(self.albums_selected): self._get_selected_albums_songs() else: self._get_selected_artists() @log def _get_selected_albums_songs(self): grilo.populate_album_songs(self.albums_selected[self.albums_index], self._add_selected_albums_songs) self.albums_index += 1 @log def _add_selected_albums_songs(self, source, param, item, remaining=0, data=None): if item: self.items_selected.append(item) if remaining == 0: if self.albums_index < len(self.albums_selected): self._get_selected_albums_songs() else: self._get_selected_artists() @log def _get_selected_artists(self): self.artists_albums_index = 0 self.artists_selected = [ self._artists[self.model[child_path][2].casefold()] for child_path in [ self.filter_model.convert_path_to_child_path(path) for path in self._view.get_selection() ] if self.model[child_path][11] == 'artist' ] self.artists_albums_selected = [] for artist in self.artists_selected: self.artists_albums_selected.extend(artist['albums']) if len(self.artists_albums_selected): self._get_selected_artists_albums_songs() else: self._get_selected_songs() @log def _get_selected_artists_albums_songs(self): grilo.populate_album_songs( self.artists_albums_selected[self.artists_albums_index], self._add_selected_artists_albums_songs) self.artists_albums_index += 1 @log def _add_selected_artists_albums_songs(self, source, param, item, remaining=0, data=None): if item: self.items_selected.append(item) if remaining == 0: if self.artists_albums_index < len(self.artists_albums_selected): self._get_selected_artists_albums_songs() else: self._get_selected_songs() @log def _get_selected_songs(self): self.items_selected.extend([ self.model[child_path][5] for child_path in [ self.filter_model.convert_path_to_child_path(path) for path in self._view.get_selection() ] if self.model[child_path][11] == 'song' ]) self.items_selected_callback(self.items_selected) @log def _filter_visible_func(self, model, _iter, data=None): return model.iter_parent(_iter) is not None or model.iter_has_child( _iter) @log def set_search_text(self, search_term, fields_filter): query_matcher = { 'album': { 'search_all': Query.get_albums_with_any_match, 'search_artist': Query.get_albums_with_artist_match, 'search_composer': Query.get_albums_with_composer_match, 'search_album': Query.get_albums_with_album_match, 'search_track': Query.get_albums_with_track_match, }, 'artist': { 'search_all': Query.get_artists_with_any_match, 'search_artist': Query.get_artists_with_artist_match, 'search_composer': Query.get_artists_with_composer_match, 'search_album': Query.get_artists_with_album_match, 'search_track': Query.get_artists_with_track_match, }, 'song': { 'search_all': Query.get_songs_with_any_match, 'search_artist': Query.get_songs_with_artist_match, 'search_composer': Query.get_songs_with_composer_match, 'search_album': Query.get_songs_with_album_match, 'search_track': Query.get_songs_with_track_match, }, } self.model = Gtk.TreeStore( GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING, # item title or header text GObject.TYPE_STRING, # artist for albums and songs GdkPixbuf.Pixbuf, # album art GObject.TYPE_OBJECT, # item GObject.TYPE_BOOLEAN, GObject.TYPE_INT, GObject.TYPE_STRING, GObject.TYPE_INT, GObject.TYPE_BOOLEAN, GObject.TYPE_STRING, # type GObject.TYPE_INT, GObject.TYPE_STRING, # composer ) self.filter_model = self.model.filter_new(None) self.filter_model.set_visible_func(self._filter_visible_func) self._view.set_model(self.filter_model) self._albums = {} self._artists = {} if search_term == "": return albums_iter = self.model.insert_with_values(None, -1, [2, 9], [_("Albums"), 2]) artists_iter = self.model.insert_with_values(None, -1, [2, 9], [_("Artists"), 2]) songs_iter = self.model.insert_with_values(None, -1, [2, 9], [_("Songs"), 2]) playlists_iter = self.model.insert_with_values(None, -1, [2, 9], [_("Playlists"), 2]) self.head_iters = [ albums_iter, artists_iter, songs_iter, playlists_iter ] self.songs_model = self.model.filter_new( self.model.get_path(songs_iter)) # Use queries for Tracker if not grilo.search_source or \ grilo.search_source.get_id() == 'grl-tracker-source': for category in ('album', 'artist', 'song'): query = query_matcher[category][fields_filter](search_term) grilo.populate_custom_query(query, self._add_item, -1, [self.model, category]) if not grilo.search_source or \ grilo.search_source.get_id() != 'grl-tracker-source': # nope, can't do - reverting to Search grilo.search(search_term, self._add_search_item, self.model)
class SearchView(BaseView): search_state = GObject.Property(type=int, default=Search.State.NONE) def __repr__(self): return '<SearchView>' @log def __init__(self, window, player): super().__init__('search', None, window) # FIXME: Searchbar handling does not belong here. self._searchbar = window._searchbar self._add_list_renderers() self.player = player self._head_iters = [None, None, None, None] self._filter_model = None self.previous_view = None self._albums_selected = [] self._albums = {} self._albums_index = 0 self._album_widget = AlbumWidget(player, self) self.add(self._album_widget) self._artists_albums_selected = [] self._artists_albums_index = 0 self._artists = {} self._artist_albums_widget = None self._items_selected = [] self._items_selected_callback = None self._items_found = None @log def _setup_view(self): view_container = Gtk.ScrolledWindow(hexpand=True, vexpand=True) self._box.pack_start(view_container, True, True, 0) self._view = Gtk.TreeView( activate_on_single_click=True, can_focus=False, halign=Gtk.Align.CENTER, headers_visible=False, show_expanders=False, width_request=530) self._view.get_style_context().add_class('view') self._view.get_style_context().add_class('content-view') self._view.get_selection().props.mode = Gtk.SelectionMode.NONE self._view.connect('row-activated', self._on_item_activated) self._ctrl = Gtk.GestureMultiPress().new(self._view) self._ctrl.props.propagation_phase = Gtk.PropagationPhase.CAPTURE self._ctrl.connect("released", self._on_view_clicked) view_container.add(self._view) @log def _back_button_clicked(self, widget, data=None): self._searchbar.reveal(True, False) if self.get_visible_child() == self._artist_albums_widget: self._artist_albums_widget.destroy() self._artist_albums_widget = None elif self.get_visible_child() == self._grid: self._window.views[View.ALBUM].set_visible_child( self._window.views[View.ALBUM]._grid) self.set_visible_child(self._grid) self._headerbar.props.state = HeaderBar.State.MAIN @log def _on_item_activated(self, treeview, path, column): if self._star_handler.star_renderer_click: self._star_handler.star_renderer_click = False return if self.props.selection_mode: return try: child_path = self._filter_model.convert_path_to_child_path(path) except TypeError: return _iter = self.model.get_iter(child_path) if self.model[_iter][12] == 'album': title = self.model[_iter][2] artist = self.model[_iter][3] item = self.model[_iter][5] self._album_widget.update(item) self._headerbar.props.state = HeaderBar.State.SEARCH self._headerbar.props.title = title self._headerbar.props.subtitle = artist self.set_visible_child(self._album_widget) self.props.search_state = Search.State.NONE elif self.model[_iter][12] == 'artist': artist = self.model[_iter][2] albums = self._artists[artist.casefold()]['albums'] self._artist_albums_widget = ArtistAlbumsWidget( artist, albums, self.player, self._window, True) self.add(self._artist_albums_widget) self._artist_albums_widget.show() self._artist_albums_widget.bind_property( 'selected-items-count', self, 'selected-items-count') self.bind_property( 'selection-mode', self._artist_albums_widget, 'selection-mode', GObject.BindingFlags.BIDIRECTIONAL) self._headerbar.props.state = HeaderBar.State.SEARCH self._headerbar.props.title = artist self._headerbar.props.subtitle = None self.set_visible_child(self._artist_albums_widget) self.props.search_state = Search.State.NONE elif self.model[_iter][12] == 'song': if self.model[_iter][11] != ValidationStatus.FAILED: c_iter = self._songs_model.convert_child_iter_to_iter(_iter)[1] self.player.set_playlist( PlayerPlaylist.Type.SEARCH_RESULT, None, self._songs_model, c_iter) self.player.play() else: # Headers if self._view.row_expanded(path): self._view.collapse_row(path) else: self._view.expand_row(path, False) @log def _on_view_clicked(self, gesture, n_press, x, y): """Ctrl+click on self._view triggers selection mode.""" _, state = Gtk.get_current_event_state() modifiers = Gtk.accelerator_get_default_mod_mask() if (state & modifiers == Gdk.ModifierType.CONTROL_MASK and not self.props.selection_mode): self._on_selection_mode_request() if (self.selection_mode and not self._star_handler.star_renderer_click): path, col, cell_x, cell_y = self._view.get_path_at_pos(x, y) iter_ = self.model.get_iter(path) self.model[iter_][6] = not self.model[iter_][6] selected_iters = self._get_selected_iters() self.props.selected_items_count = len(selected_iters) @log def _get_selected_iters(self): iters = [] for row in self.model: iter_child = self.model.iter_children(row.iter) while iter_child is not None: if self.model[iter_child][6]: iters.append(iter_child) iter_child = self.model.iter_next(iter_child) return iters @log def _on_selection_mode_changed(self, widget, data=None): super()._on_selection_mode_changed(widget, data) col = self._view.get_columns()[0] cells = col.get_cells() cells[4].props.visible = self.props.selection_mode col.queue_resize() @log def _add_search_item(self, source, param, item, remaining=0, data=None): if not item: if (grilo._search_callback_counter == 0 and grilo.search_source): self.props.search_state = Search.State.NO_RESULT return if data != self.model: return artist = utils.get_artist_name(item) album = utils.get_album_title(item) key = '%s-%s' % (artist, album) if key not in self._albums: self._albums[key] = Grl.Media() self._albums[key].set_title(album) self._albums[key].add_artist(artist) self._albums[key].set_source(source.get_id()) self._albums[key].songs = [] self._add_item( source, None, self._albums[key], 0, [self.model, 'album']) self._add_item( source, None, self._albums[key], 0, [self.model, 'artist']) self._albums[key].songs.append(item) self._add_item(source, None, item, 0, [self.model, 'song']) @log def _retrieval_finished(self, klass, model, _iter): if not model[_iter][13]: return model[_iter][13] = klass.surface @log def _add_item(self, source, param, item, remaining=0, data=None): if data is None: return model, category = data self._items_found = ( self.model.iter_n_children(self._head_iters[0]) + self.model.iter_n_children(self._head_iters[1]) + self.model.iter_n_children(self._head_iters[2]) + self.model.iter_n_children(self._head_iters[3]) ) # We need to remember the view before the search view emptysearchview = self._window.views[View.EMPTY] if (self._window.curr_view != emptysearchview and self._window.prev_view != emptysearchview): self.previous_view = self._window.prev_view if self._items_found == 0: self.props.search_state = Search.State.NO_RESULT else: self.props.search_state = Search.State.RESULT if remaining == 0: self._window.notifications_popup.pop_loading() self._view.show() if not item or model != self.model: return self._offset += 1 title = utils.get_media_title(item) item.set_title(title) artist = utils.get_artist_name(item) group = 3 try: group = {'album': 0, 'artist': 1, 'song': 2}[category] except KeyError: pass _iter = None if category == 'album': _iter = self.model.insert_with_values( self._head_iters[group], -1, [0, 2, 3, 5, 9, 12], [str(item.get_id()), title, artist, item, 2, category]) elif category == 'song': # FIXME: source specific hack if source.get_id() != 'grl-tracker-source': fav = 2 else: fav = item.get_favourite() _iter = self.model.insert_with_values( self._head_iters[group], -1, [0, 2, 3, 5, 9, 12], [str(item.get_id()), title, artist, item, fav, category]) else: if not artist.casefold() in self._artists: _iter = self.model.insert_with_values( self._head_iters[group], -1, [0, 2, 5, 9, 12], [str(item.get_id()), artist, item, 2, category]) self._artists[artist.casefold()] = { 'iter': _iter, 'albums': [] } self._artists[artist.casefold()]['albums'].append(item) # FIXME: Figure out why iter can be None here, seems illogical. if _iter is not None: scale = self._view.get_scale_factor() art = Art(Art.Size.SMALL, item, scale) self.model[_iter][13] = art.surface art.connect( 'finished', self._retrieval_finished, self.model, _iter) art.lookup() if self.model.iter_n_children(self._head_iters[group]) == 1: path = self.model.get_path(self._head_iters[group]) path = self._filter_model.convert_child_path_to_path(path) self._view.expand_row(path, False) @log def _add_list_renderers(self): column = Gtk.TreeViewColumn() # Add our own surface renderer, instead of the one provided by # Gd. This avoids us having to set the model to a cairo.Surface # which is currently not a working solution in pygobject. # https://gitlab.gnome.org/GNOME/pygobject/issues/155 pixbuf_renderer = Gtk.CellRendererPixbuf( xalign=0.5, yalign=0.5, xpad=12, ypad=2) column.pack_start(pixbuf_renderer, False) column.set_cell_data_func( pixbuf_renderer, self._on_list_widget_pixbuf_renderer) column.add_attribute(pixbuf_renderer, 'surface', 13) # With the bugfix in libgd 9117650bda, the search results # stopped aligning at the top. With the artists results not # having a second line of text, this looks off. # Revert to old behaviour by forcing the alignment to be top. two_lines_renderer = Gd.TwoLinesRenderer( wrap_mode=Pango.WrapMode.WORD_CHAR, xpad=12, xalign=0.0, yalign=0, text_lines=2) column.pack_start(two_lines_renderer, True) column.set_cell_data_func( two_lines_renderer, self._on_list_widget_two_lines_renderer) column.add_attribute(two_lines_renderer, 'text', 2) column.add_attribute(two_lines_renderer, 'line_two', 3) title_renderer = Gtk.CellRendererText( xpad=12, xalign=0.0, yalign=0.5, height=32, ellipsize=Pango.EllipsizeMode.END, weight=Pango.Weight.BOLD) column.pack_start(title_renderer, False) column.set_cell_data_func( title_renderer, self._on_list_widget_title_renderer) column.add_attribute(title_renderer, 'text', 2) self._star_handler.add_star_renderers(column) selection_renderer = Gtk.CellRendererToggle(xpad=12, xalign=1.0) column.pack_start(selection_renderer, False) column.set_cell_data_func( selection_renderer, self._on_list_widget_selection_renderer) column.add_attribute(selection_renderer, 'active', 6) self._view.append_column(column) @log def _is_header(self, model, iter_): return model.iter_parent(iter_) is None @log def _on_list_widget_title_renderer(self, col, cell, model, iter_, data): cell.props.visible = self._is_header(model, iter_) @log def _on_list_widget_pixbuf_renderer(self, col, cell, model, iter_, data): if (not model[iter_][13] or self._is_header(model, iter_)): cell.props.visible = False return cell.props.surface = model[iter_][13] cell.props.visible = True @log def _on_list_widget_two_lines_renderer( self, col, cell, model, iter_, data): if self._is_header(model, iter_): cell.props.visible = False return cell.props.visible = True @log def _on_list_widget_selection_renderer( self, col, cell, model, iter_, data): if (self.props.selection_mode and not self._is_header(model, iter_)): cell.props.visible = True else: cell.props.visible = False @log def populate(self): self._init = True self._headerbar.props.state = HeaderBar.State.MAIN @log def get_selected_songs(self, callback): if self.get_visible_child() == self._album_widget: callback(self._album_widget.get_selected_songs()) elif self.get_visible_child() == self._artist_albums_widget: callback(self._artist_albums_widget.get_selected_songs()) else: self._albums_index = 0 self._artists_albums_index = 0 self._items_selected = [] self._items_selected_callback = callback self._get_selected_albums() @log def _get_selected_albums(self): selected_iters = self._get_selected_iters() self._albums_selected = [ self.model[iter_][5] for iter_ in selected_iters if self.model[iter_][12] == 'album'] if len(self._albums_selected): self._get_selected_albums_songs() else: self._get_selected_artists() @log def _get_selected_albums_songs(self): grilo.populate_album_songs( self._albums_selected[self._albums_index], self._add_selected_albums_songs) self._albums_index += 1 @log def _add_selected_albums_songs( self, source, param, item, remaining=0, data=None): if item: self._items_selected.append(item) if remaining == 0: if self._albums_index < len(self._albums_selected): self._get_selected_albums_songs() else: self._get_selected_artists() @log def _get_selected_artists(self): selected_iters = self._get_selected_iters() artists_selected = [ self._artists[self.model[iter_][2].casefold()] for iter_ in selected_iters if self.model[iter_][12] == 'artist'] self._artists_albums_selected = [] for artist in artists_selected: self._artists_albums_selected.extend(artist['albums']) if len(self._artists_albums_selected): self._get_selected_artists_albums_songs() else: self._get_selected_songs() @log def _get_selected_artists_albums_songs(self): grilo.populate_album_songs( self._artists_albums_selected[self._artists_albums_index], self._add_selected_artists_albums_songs) self._artists_albums_index += 1 @log def _add_selected_artists_albums_songs( self, source, param, item, remaining=0, data=None): if item: self._items_selected.append(item) if remaining == 0: artist_albums = len(self._artists_albums_selected) if self._artists_albums_index < artist_albums: self._get_selected_artists_albums_songs() else: self._get_selected_songs() @log def _get_selected_songs(self): selected_iters = self._get_selected_iters() self._items_selected.extend([ self.model[iter_][5] for iter_ in selected_iters if self.model[iter_][12] == 'song']) self._items_selected_callback(self._items_selected) @log def _filter_visible_func(self, model, _iter, data=None): top_level = model.iter_parent(_iter) is None visible = (not top_level or model.iter_has_child(_iter)) return visible @log def set_search_text(self, search_term, fields_filter): query_matcher = { 'album': { 'search_all': Query.get_albums_with_any_match, 'search_artist': Query.get_albums_with_artist_match, 'search_composer': Query.get_albums_with_composer_match, 'search_album': Query.get_albums_with_album_match, 'search_track': Query.get_albums_with_track_match, }, 'artist': { 'search_all': Query.get_artists_with_any_match, 'search_artist': Query.get_artists_with_artist_match, 'search_composer': Query.get_artists_with_composer_match, 'search_album': Query.get_artists_with_album_match, 'search_track': Query.get_artists_with_track_match, }, 'song': { 'search_all': Query.get_songs_with_any_match, 'search_artist': Query.get_songs_with_artist_match, 'search_composer': Query.get_songs_with_composer_match, 'search_album': Query.get_songs_with_album_match, 'search_track': Query.get_songs_with_track_match, }, } self.model = Gtk.TreeStore( GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING, # item title or header text GObject.TYPE_STRING, # artist for albums and songs GdkPixbuf.Pixbuf, # Gd placeholder album art GObject.TYPE_OBJECT, # item GObject.TYPE_BOOLEAN, GObject.TYPE_INT, GObject.TYPE_STRING, GObject.TYPE_INT, GObject.TYPE_BOOLEAN, GObject.TYPE_INT, # validation status GObject.TYPE_STRING, # type object # album art surface ) self._filter_model = self.model.filter_new(None) self._filter_model.set_visible_func(self._filter_visible_func) self._view.set_model(self._filter_model) self._albums = {} self._artists = {} if search_term == "": return albums_iter = self.model.insert_with_values( None, -1, [2, 9], [_("Albums"), 2]) artists_iter = self.model.insert_with_values( None, -1, [2, 9], [_("Artists"), 2]) songs_iter = self.model.insert_with_values( None, -1, [2, 9], [_("Songs"), 2]) playlists_iter = self.model.insert_with_values( None, -1, [2, 9], [_("Playlists"), 2]) self._head_iters = [ albums_iter, artists_iter, songs_iter, playlists_iter ] self._songs_model = self.model.filter_new( self.model.get_path(songs_iter)) # Use queries for Tracker if (not grilo.search_source or grilo.search_source.get_id() == 'grl-tracker-source'): for category in ('album', 'artist', 'song'): query = query_matcher[category][fields_filter](search_term) self._window.notifications_popup.push_loading() grilo.populate_custom_query( query, self._add_item, -1, [self.model, category]) if (not grilo.search_source or grilo.search_source.get_id() != 'grl-tracker-source'): # nope, can't do - reverting to Search grilo.search(search_term, self._add_search_item, self.model)