Exemple #1
0
    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
Exemple #2
0
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)
Exemple #3
0
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)
Exemple #4
0
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)
Exemple #5
0
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)
Exemple #6
0
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):

    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