Exemple #1
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