Example #1
0
class AlbumsView(Gtk.Stack):
    """Gridlike view of all albums

    Album activation switches to AlbumWidget.
    """

    __gtype_name__ = "AlbumsView"

    search_mode_active = GObject.Property(type=bool, default=False)
    selection_mode = GObject.Property(type=bool, default=False)
    title = GObject.Property(type=str,
                             default=_("Albums"),
                             flags=GObject.ParamFlags.READABLE)

    _scrolled_window = Gtk.Template.Child()
    _flowbox = Gtk.Template.Child()
    _flowbox_long_press = Gtk.Template.Child()

    def __init__(self, application):
        """Initialize AlbumsView

        :param application: The Application object
        """
        super().__init__(transition_type=Gtk.StackTransitionType.CROSSFADE)

        self.props.name = "albums"

        self._application = application
        self._window = application.props.window
        self._headerbar = self._window._headerbar
        self._adjustment_timeout_id = 0
        self._viewport = self._scrolled_window.get_child()
        self._widget_counter = 1
        self._ctrl_hold = False

        model = self._application.props.coremodel.props.albums_sort
        self._flowbox.bind_model(model, self._create_widget)
        self._flowbox.set_hadjustment(self._scrolled_window.get_hadjustment())
        self._flowbox.set_vadjustment(self._scrolled_window.get_vadjustment())
        self._flowbox.connect("child-activated", self._on_child_activated)

        self.bind_property("selection-mode", self._window, "selection-mode",
                           GObject.BindingFlags.DEFAULT)

        self._window.connect("notify::selection-mode",
                             self._on_selection_mode_changed)

        self._album_widget = AlbumWidget(self._application)
        self._album_widget.bind_property("selection-mode", self,
                                         "selection-mode",
                                         GObject.BindingFlags.BIDIRECTIONAL)

        self.add(self._album_widget)

        self.connect("notify::search-mode-active",
                     self._on_search_mode_changed)

        self._scrolled_window.props.vadjustment.connect(
            "value-changed", self._on_vadjustment_changed)
        self._scrolled_window.props.vadjustment.connect(
            "changed", self._on_vadjustment_changed)

    def _on_vadjustment_changed(self, adjustment):
        if self._adjustment_timeout_id != 0:
            GLib.source_remove(self._adjustment_timeout_id)
            self._adjustment_timeout_id = 0

        self._adjustment_timeout_id = GLib.timeout_add(
            200,
            self._retrieve_covers,
            adjustment.props.value,
            priority=GLib.PRIORITY_LOW)

    def _retrieve_covers(self, old_adjustment):
        adjustment = self._scrolled_window.props.vadjustment.props.value

        if old_adjustment != adjustment:
            return GLib.SOURCE_CONTINUE

        first_cover = self._flowbox.get_child_at_index(0)
        if first_cover is None:
            return GLib.SOURCE_REMOVE

        cover_size, _ = first_cover.get_allocated_size()
        if cover_size.width == 0 or cover_size.height == 0:
            return GLib.SOURCE_REMOVE

        viewport_size, _ = self._viewport.get_allocated_size()

        h_space = self._flowbox.get_column_spacing()
        v_space = self._flowbox.get_row_spacing()
        nr_cols = ((viewport_size.width + h_space) //
                   (cover_size.width + h_space))

        top_left_cover = self._flowbox.get_child_at_index(
            nr_cols * (adjustment // (cover_size.height + v_space)))

        covers_col = math.ceil(viewport_size.width / cover_size.width)
        covers_row = math.ceil(viewport_size.height / cover_size.height)

        children = self._flowbox.get_children()
        retrieve_list = []
        for i, albumcover in enumerate(children):
            if top_left_cover == albumcover:
                retrieve_covers = covers_row * covers_col
                retrieve_list = children[i:i + retrieve_covers]
                break

        for albumcover in retrieve_list:
            albumcover.retrieve()

        self._adjustment_timeout_id = 0

        return GLib.SOURCE_REMOVE

    def _on_selection_mode_changed(self, widget, data=None):
        selection_mode = self._window.props.selection_mode
        if (selection_mode == self.props.selection_mode
                or self.get_parent().get_visible_child() != self):
            return

        self.props.selection_mode = selection_mode
        if not self.props.selection_mode:
            self.deselect_all()

    def _on_search_mode_changed(self, klass, param):
        if (not self.props.search_mode_active
                and self._headerbar.props.stack.props.visible_child == self
                and self.get_visible_child() == self._album_widget):
            self._set_album_headerbar(self._album_widget.props.album)

    def _create_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 'select all' speed.
        album_widget.bind_property("selected", corealbum, "selected",
                                   GObject.BindingFlags.BIDIRECTIONAL)

        GLib.timeout_add(self._widget_counter * 250,
                         album_widget.retrieve,
                         priority=GLib.PRIORITY_LOW)
        self._widget_counter = self._widget_counter + 1

        return album_widget

    def _back_button_clicked(self, widget, data=None):
        self._headerbar.state = HeaderBar.State.MAIN
        self.props.visible_child = self._scrolled_window

    def _on_child_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._set_album_headerbar(corealbum)
        self.set_visible_child(self._album_widget)

    def _set_album_headerbar(self, corealbum):
        self._headerbar.props.state = HeaderBar.State.CHILD
        self._headerbar.props.title = corealbum.props.title
        self._headerbar.props.subtitle = corealbum.props.artist

    @Gtk.Template.Callback()
    def _on_flowbox_press_begin(self, gesture, sequence):
        event = gesture.get_last_event(sequence)
        ok, state = event.get_state()
        if ((ok is True and state == Gdk.ModifierType.CONTROL_MASK)
                or self.props.selection_mode is True):
            self._flowbox.props.selection_mode = Gtk.SelectionMode.MULTIPLE
            if state == Gdk.ModifierType.CONTROL_MASK:
                self._ctrl_hold = True

    @Gtk.Template.Callback()
    def _on_flowbox_press_cancel(self, gesture, sequence):
        self._flowbox.props.selection_mode = Gtk.SelectionMode.NONE

    @Gtk.Template.Callback()
    def _on_selected_children_changed(self, flowbox):
        if self._flowbox.props.selection_mode == Gtk.SelectionMode.NONE:
            return

        if self.props.selection_mode is False:
            self.props.selection_mode = True

        rubberband_selection = len(self._flowbox.get_selected_children()) > 1
        with self._application.props.coreselection.freeze_notify():
            if (rubberband_selection and not self._ctrl_hold):
                self.deselect_all()
            for child in self._flowbox.get_selected_children():
                if (self._ctrl_hold is True or not rubberband_selection):
                    child.props.selected = not child.props.selected
                else:
                    child.props.selected = True

        self._ctrl_hold = False
        self._flowbox.props.selection_mode = Gtk.SelectionMode.NONE

    def _toggle_all_selection(self, selected):
        """Selects or deselects all items.
        """
        with self._application.props.coreselection.freeze_notify():
            if self.get_visible_child() == self._album_widget:
                if selected is True:
                    self._album_widget.select_all()
                else:
                    self._album_widget.deselect_all()
            else:
                for child in self._flowbox.get_children():
                    child.props.selected = selected

    def select_all(self):
        self._toggle_all_selection(True)

    def deselect_all(self):
        self._toggle_all_selection(False)
Example #2
0
class AlbumsView(BaseView):

    search_mode_active = GObject.Property(type=bool, default=False)

    def __repr__(self):
        return '<AlbumsView>'

    @log
    def __init__(self, window, player):
        super().__init__('albums', _("Albums"), window)

        self.player = player

        self._album_widget = AlbumWidget(player)
        self._album_widget.bind_property(
            "selection-mode", self, "selection-mode",
            GObject.BindingFlags.BIDIRECTIONAL)
        self._album_widget.bind_property(
            "selected-items-count", self, "selected-items-count")

        self.add(self._album_widget)
        self.albums_selected = []
        self.all_items = []
        self.items_selected = []
        self.items_selected_callback = None

        self.connect(
            "notify::search-mode-active", self._on_search_mode_changed)

    @log
    def _on_changes_pending(self, data=None):
        if (self._init and not self.props.selection_mode):
            self._offset = 0
            self._populate()
            grilo.changes_pending['Albums'] = False

    @log
    def _on_selection_mode_changed(self, widget, data=None):
        super()._on_selection_mode_changed(widget, data)

        if (not self.props.selection_mode
                and grilo.changes_pending['Albums']):
            self._on_changes_pending()

    @log
    def _on_search_mode_changed(self, klass, param):
        if (not self.props.search_mode_active
                and self._headerbar.props.stack.props.visible_child == self
                and self.get_visible_child() == self._album_widget):
            self._set_album_headerbar(self._album_widget.props.album)

    @log
    def _setup_view(self):
        self._view = 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)

        self._view.get_style_context().add_class('content-view')
        self._view.connect('child-activated', self._on_child_activated)

        scrolledwin = Gtk.ScrolledWindow()
        scrolledwin.add(self._view)
        scrolledwin.show()

        self._box.add(scrolledwin)

    @log
    def _back_button_clicked(self, widget, data=None):
        self._headerbar.state = HeaderBar.State.MAIN
        self.set_visible_child(self._grid)

    @log
    def _on_child_activated(self, widget, child, user_data=None):
        if self.props.selection_mode:
            return

        item = child.props.media
        # Update and display the album widget if not in selection mode
        self._album_widget.update(item)

        self._set_album_headerbar(item)
        self.set_visible_child(self._album_widget)

    @log
    def _set_album_headerbar(self, album):
        self._headerbar.props.state = HeaderBar.State.CHILD
        self._headerbar.props.title = utils.get_album_title(album)
        self._headerbar.props.subtitle = utils.get_artist_name(album)

    @log
    def _populate(self, data=None):
        self._window.notifications_popup.push_loading()
        grilo.populate_albums(self._offset, self._add_item)
        self._init = True

    @log
    def get_selected_songs(self, callback):
        # FIXME: we call into private objects with full knowledge of
        # what is there
        if self._headerbar.props.state == HeaderBar.State.CHILD:
            callback(self._album_widget._disc_listbox.get_selected_items())
        else:
            self.items_selected = []
            self.items_selected_callback = callback
            self.albums_index = 0
            if len(self.albums_selected):
                self._get_selected_album_songs()

    @log
    def _add_item(self, source, param, item, remaining=0, data=None):
        if item:
            # Store all items to optimize 'Select All' action
            self.all_items.append(item)

            # Add to the flowbox
            child = self._create_album_item(item)
            self._view.add(child)
            self._offset += 1
        elif remaining == 0:
            self._view.show()
            self._window.notifications_popup.pop_loading()
            self._init = False

    def _create_album_item(self, item):
        child = AlbumCover(item)

        child.connect('notify::selected', self._on_selection_changed)

        self.bind_property(
            'selection-mode', child, 'selection-mode',
            GObject.BindingFlags.BIDIRECTIONAL)

        return child

    @log
    def _on_selection_changed(self, child, data=None):
        if (child.props.selected
                and child.props.media not in self.albums_selected):
            self.albums_selected.append(child.props.media)
        elif (not child.props.selected
                and child.props.media in self.albums_selected):
            self.albums_selected.remove(child.props.media)

        self.props.selected_items_count = len(self.albums_selected)

    @log
    def _get_selected_album_songs(self):
        grilo.populate_album_songs(
            self.albums_selected[self.albums_index],
            self._add_selected_item)
        self.albums_index += 1

    @log
    def _add_selected_item(self, source, param, item, remaining=0, data=None):
        if item:
            self.items_selected.append(item)
        if remaining == 0:
            if self.albums_index < self.props.selected_items_count:
                self._get_selected_album_songs()
            else:
                self.items_selected_callback(self.items_selected)

    def _toggle_all_selection(self, selected):
        """
        Selects or unselects all items without sending the notify::active
        signal for performance purposes.
        """
        for child in self._view.get_children():
            child.props.selected = selected

    @log
    def select_all(self):
        self.albums_selected = list(self.all_items)
        self._toggle_all_selection(True)

    @log
    def unselect_all(self):
        self.albums_selected = []
        self._toggle_all_selection(False)
Example #3
0
class AlbumsView(BaseView):

    search_mode_active = GObject.Property(type=bool, default=False)

    def __repr__(self):
        return '<AlbumsView>'

    @log
    def __init__(self, window, player):
        self._window = window
        super().__init__('albums', _("Albums"), window)

        self.player = player
        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.albums_selected = []
        self.all_items = []
        self.items_selected = []
        self.items_selected_callback = None

        self.connect(
            "notify::search-mode-active", self._on_search_mode_changed)

    @log
    def _on_selection_mode_changed(self, widget, data=None):
        super()._on_selection_mode_changed(widget, data)

        if not self.props.selection_mode:
            self._on_changes_pending()

    @log
    def _on_search_mode_changed(self, klass, param):
        if (not self.props.search_mode_active
                and self._headerbar.props.stack.props.visible_child == self
                and self.get_visible_child() == self._album_widget):
            self._set_album_headerbar(self._album_widget.props.album)

    @log
    def _setup_view(self):
        self._view = 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._view.get_style_context().add_class('content-view')
        self._view.connect('child-activated', self._on_child_activated)

        scrolledwin = Gtk.ScrolledWindow()
        scrolledwin.add(self._view)
        scrolledwin.show()

        self._box.add(scrolledwin)

        self._model = self._window._app.props.coremodel.props.albums_sort
        self._view.bind_model(self._model, self._create_widget)

        self._view.show()

    @log
    def _create_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

    @log
    def _back_button_clicked(self, widget, data=None):
        self._headerbar.state = HeaderBar.State.MAIN
        self.set_visible_child(self._grid)

    @log
    def _on_child_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._set_album_headerbar(corealbum)
        self.set_visible_child(self._album_widget)

    @log
    def _set_album_headerbar(self, corealbum):
        self._headerbar.props.state = HeaderBar.State.CHILD
        self._headerbar.props.title = corealbum.props.title
        self._headerbar.props.subtitle = corealbum.props.artist

    @log
    def _populate(self, data=None):
        # self._window.notifications_popup.push_loading()
        self._init = True
        self._view.show()

    def _toggle_all_selection(self, selected):
        """
        Selects or unselects all items without sending the notify::active
        signal for performance purposes.
        """
        with self._window._app.props.coreselection.freeze_notify():
            for child in self._view.get_children():
                child.props.selected = selected
                child.props.corealbum.props.selected = selected

    def select_all(self):
        self._toggle_all_selection(True)

    def unselect_all(self):
        self._toggle_all_selection(False)
Example #4
0
class SearchView(Gtk.Stack):
    """Gridlike view of search results.

    Three sections: artists, albums, songs.
    """

    __gtype_name__ = "SearchView"

    class State(IntEnum):
        """The different states of SearchView
        """
        MAIN = 0
        ALL_ALBUMS = 1
        ALL_ARTISTS = 2
        ALBUM = 3
        ARTIST = 4

    search_state = GObject.Property(type=int, default=Search.State.NONE)
    selection_mode = GObject.Property(type=bool, default=False)
    state = GObject.Property(type=int, default=State.MAIN)
    title = GObject.Property(type=str,
                             default="",
                             flags=GObject.ParamFlags.READABLE)

    _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 __init__(self, application):
        """Initialize SearchView

        :param GtkApplication application: The Application object
        """
        super().__init__(transition_type=Gtk.StackTransitionType.CROSSFADE)

        self.props.name = "search"

        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(self._application)
        self._album_widget.bind_property("selection-mode", self,
                                         "selection-mode",
                                         GObject.BindingFlags.BIDIRECTIONAL)

        self.add(self._album_widget)

        self._scrolled_artist_window = None

        self._search_mode_active = False

    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)
        album_widget.retrieve()

        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 'select 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 set_child_visible(child):
            child.props.visible = True

        self._album_flowbox.foreach(set_child_visible)

    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 set_child_visible(child):
            child.props.visible = True

        self._artist_flowbox.foreach(set_child_visible)

    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):
        if widget.props.select_click:
            widget.props.select_click = False
            return

        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

        if self.props.selection_mode:
            widget.props.select_click = True
            widget.props.selected = not widget.props.selected
            widget.props.coresong.props.selected = widget.props.selected
            return

        (_, button) = event.get_button()
        if (button == Gdk.BUTTON_PRIMARY and not self.props.selection_mode):
            self._coremodel.props.active_core_object = widget.props.coresong
            self._player.play(widget.props.coresong)

        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._artist_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:
            corealbum.props.selected = not corealbum.props.selected
            return

        # Update and display the album widget if not in selection mode
        self._album_widget.update(corealbum)

        self.props.state = SearchView.State.ALBUM
        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

        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

    @Gtk.Template.Callback()
    def _on_all_artists_clicked(self, widget, event, user_data=None):
        self.props.state = SearchView.State.ALL_ARTISTS
        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.props.state = SearchView.State.ALL_ALBUMS
        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):
        def child_select(child):
            child.props.selected = value

        if self.props.state == SearchView.State.MAIN:
            with self._model.freeze_notify():

                def song_select(child):
                    song_widget = child.get_child()
                    song_widget.props.coresong.props.selected = value

                self._songs_listbox.foreach(song_select)
                self._album_flowbox.foreach(child_select)
                self._artist_flowbox.foreach(child_select)
        elif self.props.state == SearchView.State.ALL_ALBUMS:
            with self._model.freeze_notify():
                self._album_all_flowbox.foreach(child_select)
        elif self.props.state == SearchView.State.ALL_ARTISTS:
            with self._model.freeze_notify():
                self._artist_all_flowbox.foreach(child_select)
        elif self.props.state == SearchView.State.ALBUM:
            view = self.get_visible_child()
            if value is True:
                view.select_all()
            else:
                view.deselect_all()
        elif self.props.state == SearchView.State.ARTIST:
            view = self.get_visible_child().get_child().get_child()
            if value is True:
                view.select_all()
            else:
                view.deselect_all()

    def select_all(self):
        self._select_all(True)

    def deselect_all(self):
        self._select_all(False)

    def _back_button_clicked(self, widget, data=None):
        if self.get_visible_child() == self._search_results:
            return
        elif self.get_visible_child() == self._scrolled_artist_window:
            self.remove(self._scrolled_artist_window)
            self._scrolled_artist_window.destroy()
            self._scrolled_artist_window = None

        self.set_visible_child(self._search_results)
        self.props.search_mode_active = True
        self.props.state = SearchView.State.MAIN
        self._headerbar.props.state = HeaderBar.State.MAIN

    def _on_selection_mode_changed(self, widget, data=None):
        if (not self.props.selection_mode
                and self.get_parent().get_visible_child() == self):
            self.deselect_all()

    @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
Example #5
0
class AlbumsView(BaseView):

    search_mode_active = GObject.Property(type=bool, default=False)

    def __repr__(self):
        return '<AlbumsView>'

    @log
    def __init__(self, window, player):
        super().__init__('albums', _("Albums"), window)

        self.player = player

        self._album_widget = AlbumWidget(player)
        self._album_widget.bind_property("selection-mode", self,
                                         "selection-mode",
                                         GObject.BindingFlags.BIDIRECTIONAL)
        self._album_widget.bind_property("selected-items-count", self,
                                         "selected-items-count")

        self.add(self._album_widget)
        self.albums_selected = []
        self.all_items = []
        self.items_selected = []
        self.items_selected_callback = None

        self.connect("notify::search-mode-active",
                     self._on_search_mode_changed)

    @log
    def _on_changes_pending(self, data=None):
        if (self._init and not self.props.selection_mode):
            self._offset = 0
            self._populate()
            grilo.changes_pending['Albums'] = False

    @log
    def _on_selection_mode_changed(self, widget, data=None):
        super()._on_selection_mode_changed(widget, data)

        if (not self.props.selection_mode and grilo.changes_pending['Albums']):
            self._on_changes_pending()

    @log
    def _on_search_mode_changed(self, klass, param):
        if (not self.props.search_mode_active
                and self._headerbar.props.stack.props.visible_child == self
                and self.get_visible_child() == self._album_widget):
            self._set_album_headerbar(self._album_widget.props.album)

    @log
    def _setup_view(self):
        self._view = 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)

        self._view.get_style_context().add_class('content-view')
        self._view.connect('child-activated', self._on_child_activated)

        scrolledwin = Gtk.ScrolledWindow()
        scrolledwin.add(self._view)
        scrolledwin.show()

        self._box.add(scrolledwin)

    @log
    def _back_button_clicked(self, widget, data=None):
        self._headerbar.state = HeaderBar.State.MAIN
        self.set_visible_child(self._grid)

    @log
    def _on_child_activated(self, widget, child, user_data=None):
        if self.props.selection_mode:
            return

        item = child.props.media
        # Update and display the album widget if not in selection mode
        self._album_widget.update(item)

        self._set_album_headerbar(item)
        self.set_visible_child(self._album_widget)

    @log
    def _set_album_headerbar(self, album):
        self._headerbar.props.state = HeaderBar.State.CHILD
        self._headerbar.props.title = utils.get_album_title(album)
        self._headerbar.props.subtitle = utils.get_artist_name(album)

    @log
    def _populate(self, data=None):
        self._window.notifications_popup.push_loading()
        grilo.populate_albums(self._offset, self._add_item)
        self._init = True

    @log
    def get_selected_songs(self, callback):
        # FIXME: we call into private objects with full knowledge of
        # what is there
        if self._headerbar.props.state == HeaderBar.State.CHILD:
            callback(self._album_widget._disc_listbox.get_selected_items())
        else:
            self.items_selected = []
            self.items_selected_callback = callback
            self.albums_index = 0
            if len(self.albums_selected):
                self._get_selected_album_songs()

    @log
    def _add_item(self, source, param, item, remaining=0, data=None):
        if item:
            # Store all items to optimize 'Select All' action
            self.all_items.append(item)

            # Add to the flowbox
            child = self._create_album_item(item)
            self._view.add(child)
            self._offset += 1
        elif remaining == 0:
            self._view.show()
            self._window.notifications_popup.pop_loading()
            self._init = False

    def _create_album_item(self, item):
        child = AlbumCover(item)

        child.connect('notify::selected', self._on_selection_changed)

        self.bind_property('selection-mode', child, 'selection-mode',
                           GObject.BindingFlags.BIDIRECTIONAL)

        return child

    @log
    def _on_selection_changed(self, child, data=None):
        if (child.props.selected
                and child.props.media not in self.albums_selected):
            self.albums_selected.append(child.props.media)
        elif (not child.props.selected
              and child.props.media in self.albums_selected):
            self.albums_selected.remove(child.props.media)

        self.props.selected_items_count = len(self.albums_selected)

    @log
    def _get_selected_album_songs(self):
        grilo.populate_album_songs(self.albums_selected[self.albums_index],
                                   self._add_selected_item)
        self.albums_index += 1

    @log
    def _add_selected_item(self, source, param, item, remaining=0, data=None):
        if item:
            self.items_selected.append(item)
        if remaining == 0:
            if self.albums_index < self.props.selected_items_count:
                self._get_selected_album_songs()
            else:
                self.items_selected_callback(self.items_selected)

    def _toggle_all_selection(self, selected):
        """
        Selects or unselects all items without sending the notify::active
        signal for performance purposes.
        """
        for child in self._view.get_children():
            child.props.selected = selected

    @log
    def select_all(self):
        self.albums_selected = list(self.all_items)
        self._toggle_all_selection(True)

    @log
    def unselect_all(self):
        self.albums_selected = []
        self._toggle_all_selection(False)
Example #6
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)

        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._album_widget.bind_property("selection-mode", self,
                                         "selection-mode",
                                         GObject.BindingFlags.BIDIRECTIONAL)
        self._album_widget.bind_property("selected-items-count", self,
                                         "selected-items-count")

        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

        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._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):
        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_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_mode_active = False

        elif self.model[_iter][12] == 'artist':
            artist = self.model[_iter][2]
            albums = self._artists[artist.casefold()]['albums']

            self._artist_albums_widget = ArtistAlbumsWidget(
                artist, albums, self.player, self._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_mode_active = False
        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.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 _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 _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 _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, data=None):
        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)
Example #7
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):
        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
Example #8
0
class AlbumsView(Gtk.Stack):
    """Gridlike view of all albums

    Album activation switches to AlbumWidget.
    """

    __gtype_name__ = "AlbumsView"

    search_mode_active = GObject.Property(type=bool, default=False)
    selected_items_count = GObject.Property(type=int, default=0, minimum=0)
    selection_mode = GObject.Property(type=bool, default=False)

    _all_albums = Gtk.Template.Child()
    _flowbox = Gtk.Template.Child()

    def __repr__(self):
        return '<AlbumsView>'

    def __init__(self, application, player=None):
        """Initialize AlbumsView

        :param application: The Application object
        """
        super().__init__(transition_type=Gtk.StackTransitionType.CROSSFADE)

        # FIXME: Make these properties.
        self.name = "albums"
        self.title = _("Albums")

        self._window = application.props.window
        self._headerbar = self._window._headerbar

        model = self._window._app.props.coremodel.props.albums_sort
        self._flowbox.bind_model(model, self._create_widget)
        self._flowbox.connect("child-activated", self._on_child_activated)

        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(application.props.player, self)
        self._album_widget.bind_property("selection-mode", self,
                                         "selection-mode",
                                         GObject.BindingFlags.BIDIRECTIONAL)

        self.add(self._album_widget)

        self.connect("notify::search-mode-active",
                     self._on_search_mode_changed)

        self.show_all()

    def _on_selection_mode_changed(self, widget, data=None):
        if not self.props.selection_mode:
            self.unselect_all()

    def _on_search_mode_changed(self, klass, param):
        if (not self.props.search_mode_active
                and self._headerbar.props.stack.props.visible_child == self
                and self.get_visible_child() == self._album_widget):
            self._set_album_headerbar(self._album_widget.props.album)

    def _create_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 _back_button_clicked(self, widget, data=None):
        self._headerbar.state = HeaderBar.State.MAIN
        self.props.visible_child = self._all_albums

    def _on_child_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._set_album_headerbar(corealbum)
        self.set_visible_child(self._album_widget)

    def _set_album_headerbar(self, corealbum):
        self._headerbar.props.state = HeaderBar.State.CHILD
        self._headerbar.props.title = corealbum.props.title
        self._headerbar.props.subtitle = corealbum.props.artist

    def _toggle_all_selection(self, selected):
        """
        Selects or unselects all items without sending the notify::active
        signal for performance purposes.
        """
        with self._window._app.props.coreselection.freeze_notify():
            for child in self._flowbox.get_children():
                child.props.selected = selected
                child.props.corealbum.props.selected = selected

    def select_all(self):
        self._toggle_all_selection(True)

    def unselect_all(self):
        self._toggle_all_selection(False)