[gnome-music/wip/jfelder/playlistview-listbox] playlistsview: Port the view to ListBox



commit ce8f044afc335554a37731cce78a31b62932cfe1
Author: Jean Felder <jfelder src gnome org>
Date:   Fri May 31 10:07:42 2019 +0200

    playlistsview: Port the view to ListBox

 data/org.gnome.Music.css           |  11 ++-
 data/org.gnome.Music.gresource.xml |   1 +
 data/ui/SongRow.ui                 | 113 +++++++++++++++++++++
 gnomemusic/player.py               |  71 ++++++++++++++
 gnomemusic/views/playlistsview.py  | 195 +++++++++----------------------------
 gnomemusic/widgets/songrow.py      | 182 ++++++++++++++++++++++++++++++++++
 meson.build                        |   2 +-
 7 files changed, 424 insertions(+), 151 deletions(-)
---
diff --git a/data/org.gnome.Music.css b/data/org.gnome.Music.css
index 6caaa519..5cd56c1c 100644
--- a/data/org.gnome.Music.css
+++ b/data/org.gnome.Music.css
@@ -96,15 +96,20 @@ box#ArtistAlbumsWidget .artist-label {
 }
 
 /* PlaylistDialog */
-.playlistdialog-row {
+.playlistdialog-row,
+.song-row {
     border-bottom: 1px solid rgba(0, 0, 0, 0.1);
 }
 
-.playlistdialog-row:selected {
+.playlistdialog-row:selected,
+.song-row:hover,
+.song-row:selected {
     color: @theme_fg_color;
     background-color: @theme_insensitive_bg_color;
 }
 
-.playlistdialog-row:selected label {
+.playlistdialog-row:selected label,
+.song-row:hover label,
+.song-row:selected label {
     color: @theme_text_color;
 }
diff --git a/data/org.gnome.Music.gresource.xml b/data/org.gnome.Music.gresource.xml
index 3992ffa4..c018d18c 100644
--- a/data/org.gnome.Music.gresource.xml
+++ b/data/org.gnome.Music.gresource.xml
@@ -24,6 +24,7 @@
     <file preprocess="xml-stripblanks">ui/SelectionBarMenuButton.ui</file>
     <file preprocess="xml-stripblanks">ui/SelectionToolbar.ui</file>
     <file preprocess="xml-stripblanks">ui/SidebarRow.ui</file>
+    <file preprocess="xml-stripblanks">ui/SongRow.ui</file>
     <file preprocess="xml-stripblanks">ui/SongWidget.ui</file>
     <file preprocess="xml-stripblanks">ui/TwoLineTip.ui</file>
     <file preprocess="xml-stripblanks">ui/Window.ui</file>
diff --git a/data/ui/SongRow.ui b/data/ui/SongRow.ui
new file mode 100644
index 00000000..f4915ae1
--- /dev/null
+++ b/data/ui/SongRow.ui
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="SongRow" parent="GtkListBoxRow">
+    <property name="can_focus">False</property>
+    <property name="visible">True</property>
+    <child>
+      <object class="GtkBox" id="hbox">
+        <property name="can_focus">False</property>
+        <property name="height-request">48</property>
+        <property name="margin-left">20</property>
+        <property name="margin-right">20</property>
+        <property name="spacing">64</property>
+        <property name="visible">True</property>
+        <child>
+          <object class="GtkBox" id="merde0">
+            <property name="spacing">20</property>
+            <property name="visible">True</property>
+            <child>
+              <object class="GtkImage" id="_play_icon">
+                <property name="can_focus">False</property>
+                <property name="icon_size">1</property>
+                <property name="visible">True</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="_title_label">
+                <property name="can_focus">False</property>
+                <property name="ellipsize">end</property>
+                <property name="halign">start</property>
+                <property name="hexpand">True</property>
+                <property name="valign">center</property>
+                <property name="visible">True</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox" id="star_duration_box">
+            <property name="hexpand">False</property>
+            <property name="margin-start">10</property>
+            <property name="margin-end">10</property>
+            <property name="spacing">64</property>
+            <property name="vexpand">True</property>
+            <property name="visible">True</property>
+            <child>
+             <object class="GtkEventBox" id="_star_eventbox">
+               <property name="visible">True</property>
+               <property name="can_focus">False</property>
+               <signal name="button-release-event" handler="_on_star_toggle" swapped="no"/>
+                <signal name="enter-notify-event" handler="_on_star_hover" swapped="no"/>
+                <signal name="leave-notify-event" handler="_on_star_unhover" swapped="no"/>
+               <child>
+                 <object class="StarImage" id="_star_image">
+                    <property name="can_focus">False</property>
+                    <property name="halign">start</property>
+                    <property name="valign">center</property>
+                    <property name="visible">True</property>
+                 </object>
+               </child>
+             </object>
+           </child>
+            <child>
+              <object class="GtkLabel" id="_duration_label">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="valign">center</property>
+                <property name="single_line_mode">True</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox" id="merde1">
+            <property name="visible">True</property>
+            <child>
+              <object class="GtkLabel" id="_artist_label">
+                <property name="can_focus">False</property>
+                <property name="ellipsize">end</property>
+                <property name="halign">start</property>
+                <property name="hexpand">True</property>
+                <property name="valign">center</property>
+                <property name="visible">True</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox" id="merde2">
+            <property name="visible">True</property>
+            <child>
+              <object class="GtkLabel" id="_album_label">
+                <property name="can_focus">False</property>
+                <property name="ellipsize">end</property>
+                <property name="halign">start</property>
+                <property name="hexpand">True</property>
+                <property name="valign">center</property>
+                <property name="visible">True</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+<object class="GtkSizeGroup">
+  <property name="mode">horizontal</property>
+  <widgets>
+    <widget name="merde0"/>
+    <widget name="merde1"/>
+    <widget name="merde2"/>
+  </widgets>
+</object>
+</interface>
diff --git a/gnomemusic/player.py b/gnomemusic/player.py
index 25ce9b66..d5671370 100644
--- a/gnomemusic/player.py
+++ b/gnomemusic/player.py
@@ -177,6 +177,67 @@ class PlayerPlaylist(GObject.GObject):
             self._validate_next_song()
         return True
 
+    @log
+    def set_playlist_from_listbox(self, playlist_type, playlist_id, listbox):
+        """Set a new playlist or change the song being played
+
+        If no song is requested (through model_iter), a song will be
+        automatically selected:
+        * the first song in a linear mode
+        * a random song in shuffle mode
+
+        :param PlayerPlaylist.Type playlist_type: playlist type
+        :param string playlist_id: unique identifer to recognize the playlist
+        :param GtkListStore model: list of songs to play
+        :param GtkTreeIter model_iter: requested song
+
+        :return: True if the playlist has been updated. False otherwise
+        :rtype: bool
+        """
+        selected_row = listbox.get_selected_row()
+        if selected_row:
+            self._current_index = selected_row.get_index()
+        else:
+            if self.props.repeat_mode == RepeatMode.SHUFFLE:
+                self._current_index = randrange(len(listbox))
+            else:
+                self._current_index = 0
+
+        # Playlist is the same. Check that the requested song is valid.
+        # If not, try to get the next valid one
+        if (playlist_type == self._type
+                and playlist_id == self._id):
+            if not self._current_song_is_valid():
+                self.next()
+            else:
+                self._validate_song(self._current_index)
+                self._validate_next_song()
+            return False
+
+        self._validation_indexes = defaultdict(list)
+        self._type = playlist_type
+        self._id = playlist_id
+
+        self._songs = []
+        for row in listbox:
+            self._songs.append([row.props.media, ValidationStatus.PENDING])
+
+        if self.props.repeat_mode == RepeatMode.SHUFFLE:
+            self._shuffle_indexes = list(range(len(self._songs)))
+            shuffle(self._shuffle_indexes)
+            self._shuffle_indexes.remove(self._current_index)
+            self._shuffle_indexes.insert(0, self._current_index)
+
+        # If the playlist has already been played, check that the requested
+        # song is valid. If it has never been played, validate the current
+        # song and the next song to display an error icon on failure.
+        if not self._current_song_is_valid():
+            self.next()
+        else:
+            self._validate_song(self._current_index)
+            self._validate_next_song()
+        return True
+
     @log
     def set_song(self, song_offset):
         """Change playlist index.
@@ -710,6 +771,16 @@ class Player(GObject.GObject):
         if playlist_changed:
             self.emit('playlist-changed')
 
+    @log
+    def set_playlist_from_listbox(
+            self, playlist_type, playlist_id, listbox):
+        playlist_changed = self._playlist.set_playlist_from_listbox(
+            playlist_type, playlist_id, listbox)
+
+        if playlist_changed:
+            self.emit('playlist-changed')
+
+
     @log
     def playlist_change_position(self, prev_pos, new_pos):
         """Change order of a song in the playlist.
diff --git a/gnomemusic/views/playlistsview.py b/gnomemusic/views/playlistsview.py
index aad54a63..9ffb7079 100644
--- a/gnomemusic/views/playlistsview.py
+++ b/gnomemusic/views/playlistsview.py
@@ -36,6 +36,7 @@ from gnomemusic.widgets.playlistcontextmenu import PlaylistContextMenu
 from gnomemusic.widgets.playlistcontrols import PlaylistControls
 from gnomemusic.widgets.playlistdialog import PlaylistDialog
 from gnomemusic.widgets.sidebarrow import SidebarRow
+from gnomemusic.widgets.songrow import SongRow
 import gnomemusic.utils as utils
 
 playlists = Playlists.get_default()
@@ -66,8 +67,6 @@ class PlaylistsView(BaseView):
 
         self._view.get_style_context().add_class('songs-list')
 
-        self._add_list_renderers()
-
         self._pl_ctrls = PlaylistControls()
         self._pl_ctrls.connect('playlist-renamed', self._on_playlist_renamed)
 
@@ -113,8 +112,7 @@ class PlaylistsView(BaseView):
         self._grid.child_set_property(sidebar_container, 'top-attach', 0)
         self._grid.child_set_property(sidebar_container, 'height', 2)
 
-        self._iter_to_clean = None
-        self._iter_to_clean_model = None
+        self._current_row = None
         self._current_playlist = None
         self._current_playlist_index = None
         self._plays_songs_on_activation = False
@@ -122,10 +120,7 @@ class PlaylistsView(BaseView):
         self._songs_todelete = {}
         self._songs_count = 0
 
-        self.model.connect('row-inserted', self._on_song_inserted)
-        self.model.connect('row-deleted', self._on_song_deleted)
-
-        self.player.connect('song-changed', self._update_model)
+        self.player.connect('song-changed', self._update_listbox)
         self.player.connect('song-validated', self._on_song_validated)
         playlists.connect('playlist-created', self._on_playlist_created)
         playlists.connect('playlist-updated', self._on_playlist_update)
@@ -144,17 +139,17 @@ class PlaylistsView(BaseView):
         view_container = Gtk.ScrolledWindow(hexpand=True, vexpand=True)
         self._box.pack_start(view_container, True, True, 0)
 
-        self._view = Gtk.TreeView()
-        self._view.set_headers_visible(False)
-        self._view.set_valign(Gtk.Align.START)
-        self._view.set_model(self.model)
-        self._view.set_activate_on_single_click(True)
-        self._view.get_selection().set_mode(Gtk.SelectionMode.SINGLE)
+        self._view = Gtk.ListBox()
+        self._view.props.activate_on_single_click = True
+        self._view.props.valign = Gtk.Align.FILL
+        self._view.props.vexpand = True
 
-        self._view.connect('row-activated', self._on_song_activated)
-        self._view.connect('drag-begin', self._drag_begin)
-        self._view.connect('drag-end', self._drag_end)
         self._song_drag = {'active': False}
+        self._view.connect('drag-begin', self._drag_begin)  # ?
+        self._view.connect('drag-end', self._drag_end)  # ?
+        self._view.connect('row-activated', self._on_song_activated)  # ok
+        # self._view.connect('row-deleted', self._on_song_deleted)
+        # self._view.connect('row-inserted', self._on_song_inserted)
 
         self._controller = Gtk.GestureMultiPress().new(self._view)
         self._controller.props.propagation_phase = Gtk.PropagationPhase.CAPTURE
@@ -164,110 +159,25 @@ class PlaylistsView(BaseView):
         view_container.add(self._view)
 
     @log
-    def _add_list_renderers(self):
-        now_playing_symbol_renderer = Gtk.CellRendererPixbuf(
-            xpad=0, xalign=0.5, yalign=0.5)
-        column_now_playing = Gtk.TreeViewColumn()
-        column_now_playing.set_fixed_width(48)
-        column_now_playing.pack_start(now_playing_symbol_renderer, False)
-        column_now_playing.set_cell_data_func(
-            now_playing_symbol_renderer, self._on_list_widget_icon_render,
-            None)
-        self._view.append_column(column_now_playing)
-
-        title_renderer = Gtk.CellRendererText(
-            xpad=0, xalign=0.0, yalign=0.5, height=48,
-            ellipsize=Pango.EllipsizeMode.END)
-        column_title = Gtk.TreeViewColumn("Title", title_renderer, text=2)
-        column_title.set_expand(True)
-        self._view.append_column(column_title)
-
-        column_star = Gtk.TreeViewColumn()
-        self._view.append_column(column_star)
-        self._star_handler.add_star_renderers(column_star)
-
-        duration_renderer = Gtk.CellRendererText(xpad=32, xalign=1.0)
-        column_duration = Gtk.TreeViewColumn()
-        column_duration.pack_start(duration_renderer, False)
-        column_duration.set_cell_data_func(
-            duration_renderer, self._on_list_widget_duration_render, None)
-        self._view.append_column(column_duration)
-
-        artist_renderer = Gtk.CellRendererText(
-            xpad=32, ellipsize=Pango.EllipsizeMode.END)
-        column_artist = Gtk.TreeViewColumn("Artist", artist_renderer, text=3)
-        column_artist.set_expand(True)
-        self._view.append_column(column_artist)
-
-        album_renderer = Gtk.CellRendererText(
-            xpad=32, ellipsize=Pango.EllipsizeMode.END)
-        column_album = Gtk.TreeViewColumn()
-        column_album.set_expand(True)
-        column_album.pack_start(album_renderer, True)
-        column_album.set_cell_data_func(
-            album_renderer, self._on_list_widget_album_render, None)
-        self._view.append_column(column_album)
-
-    def _on_list_widget_duration_render(self, col, cell, model, _iter, data):
-        if not model.iter_is_valid(_iter):
-            return
-
-        item = model[_iter][5]
-        if item:
-            duration = item.get_duration()
-            cell.set_property('text', utils.seconds_to_string(duration))
-
-    def _on_list_widget_album_render(self, coll, cell, model, _iter, data):
-        if not model.iter_is_valid(_iter):
-            return
-
-        item = model[_iter][5]
-        if item:
-            cell.set_property('text', utils.get_album_title(item))
-
-    def _on_list_widget_icon_render(self, col, cell, model, _iter, data):
-        if not self.player.playing_playlist(
-                PlayerPlaylist.Type.PLAYLIST, self._current_playlist.get_id()):
-            cell.set_visible(False)
-            return
-
-        if not model.iter_is_valid(_iter):
-            return
-
-        current_song = self.player.props.current_song
-        if model[_iter][11] == ValidationStatus.FAILED:
-            cell.set_property('icon-name', self._error_icon_name)
-            cell.set_visible(True)
-        elif model[_iter][5].get_id() == current_song.get_id():
-            cell.set_property('icon-name', self._now_playing_icon_name)
-            cell.set_visible(True)
-        else:
-            cell.set_visible(False)
-
-    @log
-    def _update_model(self, player):
-        """Updates model when the song changes
+    def _update_listbox(self, player):
+        """Updates listbox when the song changes
 
         :param Player player: The main player object
         """
         if self._current_playlist is None:
             return
 
-        if self._iter_to_clean:
-            self._iter_to_clean_model[self._iter_to_clean][10] = False
         if not player.playing_playlist(
                 PlayerPlaylist.Type.PLAYLIST, self._current_playlist.get_id()):
             return False
 
-        index = self.player.props.current_song_index
-        iter_ = self.model.get_iter_from_string(str(index))
-        self.model[iter_][10] = True
-        path = self.model.get_path(iter_)
-        self._view.scroll_to_cell(path, None, False, 0., 0.)
-        if self.model[iter_][8] != self._error_icon_name:
-            self._iter_to_clean = iter_.copy()
-            self._iter_to_clean_model = self.model
+        if self._current_row:
+            self._current_row.props.state = SongRow.State.UNPLAYED
 
+        index = self.player.props.current_song_index
+        self._current_row = self._view.get_row_at_index(index)
+        self._current_row.props.state = SongRow.State.PLAYING
+        # self._view.scroll_to_cell(path, None, False, 0., 0.)
         return False
 
     @log
@@ -330,35 +240,33 @@ class PlaylistsView(BaseView):
                 PlayerPlaylist.Type.PLAYLIST, self._current_playlist.get_id()):
             return
 
-        iter_ = self.model.get_iter_from_string(str(index))
-        self.model[iter_][11] = status
+        row = self._view.get_row_at_index(index)
+        if status == ValidationStatus.FAILED:
+            row.props.state = SongRow.State.UNPLAYABLE
+        elif (status == ValidationStatus.SUCCEEDED
+              and row.props.state != SongRow.State.PLAYING):
+            row.props.state = SongRow.State.UNPLAYED
 
     @log
-    def _on_song_activated(self, widget, path, column):
+    def _on_song_activated(self, klass, row):
         """Action performed when clicking on a song
 
         clicking on star column toggles favorite
         clicking on an other columns launches player
         Action is not performed if drag and drop is active
 
-        :param Gtk.Tree treeview: self._view
-        :param Gtk.TreePath path: activated row index
-        :param Gtk.TreeViewColumn column: activated column
+        :param Gtk.ListBox klass: self._view
+        :param Gtk.ListboxRow row: activated row
         """
         def activate_song():
             if self._song_drag['active']:
                 return GLib.SOURCE_REMOVE
 
-            if self._star_handler.star_renderer_click:
-                self._star_handler.star_renderer_click = False
-                return GLib.SOURCE_REMOVE
-
-            _iter = None
-            if path:
-                _iter = self.model.get_iter(path)
             playlist_id = self._current_playlist.get_id()
-            self.player.set_playlist(
-                PlayerPlaylist.Type.PLAYLIST, playlist_id, self.model, _iter)
+            self.player.set_playlist_from_listbox(
+                PlayerPlaylist.Type.PLAYLIST, playlist_id, self._view)
+            current_index = self.player.props.current_song_index
+            self._current_row = self._view.get_row_at_index(current_index)
             self.player.play()
 
             return GLib.SOURCE_REMOVE
@@ -537,11 +445,10 @@ class PlaylistsView(BaseView):
         self._current_playlist_index = row.get_index()
 
         # if the active queue has been set by this playlist,
-        # use it as model, otherwise build the liststore
-        self._view.set_model(None)
-        self.model.clear()
-        self._iter_to_clean = None
-        self._iter_to_clean_model = None
+        # use it as model, otherwise build the listbox
+        for row in self._view:
+            self._view.remove(row)
+        self._current_row = None
         self._update_songs_count(0)
         self._pl_ctrls.props.display_songs_count = False
         grilo.populate_playlist_songs(playlist, self._add_song)
@@ -550,13 +457,13 @@ class PlaylistsView(BaseView):
         self._playlist_delete_action.set_enabled(not protected_pl)
         self._playlist_rename_action.set_enabled(not protected_pl)
         self._remove_song_action.set_enabled(not protected_pl)
-        self._view.set_reorderable(not protected_pl)
+        # self._view.set_reorderable(not protected_pl)
 
     @log
     def _add_song(self, source, param, song, remaining=0, data=None):
         """Grilo.populate_playlist_songs callback.
 
-        Add all playlists found by Grilo to self._model
+        Add all songs found by Grilo to the ListBox
 
         :param GrlTrackerSource source: tracker source
         :param int param: param
@@ -564,9 +471,8 @@ class PlaylistsView(BaseView):
         :param int remaining: next playlist_id or zero if None
         :param data: associated data
         """
-        self._add_song_to_model(song, self.model)
+        self._add_song_to_listbox(song)
         if remaining == 0:
-            self._view.set_model(self.model)
             self._pl_ctrls.props.display_songs_count = True
             if self._plays_songs_on_activation:
                 first_iter = self.model.get_iter_first()
@@ -577,27 +483,22 @@ class PlaylistsView(BaseView):
                 self._plays_songs_on_activation = False
 
     @log
-    def _add_song_to_model(self, song, model, index=-1):
+    def _add_song_to_listbox(self, song, index=-1):
         """Add song to a playlist
         :param Grl.Media song: song to add
-        :param Gtk.ListStore model: model
+        :param int index: insertion index in the ListBox
         """
         if not song:
             return None
 
-        title = utils.get_media_title(song)
-        song.set_title(title)
-        artist = utils.get_artist_name(song)
-        iter_ = model.insert_with_valuesv(
-            index, [2, 3, 5, 9],
-            [title, artist, song, song.get_favourite()])
-
+        song_row = SongRow(song)
+        self._view.insert(song_row, index)
         self._update_songs_count(self._songs_count + 1)
-        return iter_
+        return song_row
 
     @log
     def _on_play_activate(self, menuitem, data=None):
-        self._view.emit('row-activated', None, None)
+        self._view.emit('row-activated', None)
 
     @log
     def _is_current_playlist(self, playlist):
@@ -685,7 +586,7 @@ class PlaylistsView(BaseView):
             if not self._is_current_playlist(song_todelete['playlist']):
                 return
 
-            iter_ = self._add_song_to_model(
+            iter_ = self._add_song_to_listbox(
                 song_todelete['song'], self.model, song_todelete['index'])
 
             playlist_id = self._current_playlist.get_id()
@@ -746,7 +647,7 @@ class PlaylistsView(BaseView):
     @log
     def _on_song_added_to_playlist(self, playlists, playlist, item):
         if self._is_current_playlist(playlist):
-            iter_ = self._add_song_to_model(item, self.model)
+            iter_ = self._add_song_to_listbox(item, self.model)
             playlist_id = self._current_playlist.get_id()
             if self.player.playing_playlist(
                     PlayerPlaylist.Type.PLAYLIST, playlist_id):
diff --git a/gnomemusic/widgets/songrow.py b/gnomemusic/widgets/songrow.py
new file mode 100644
index 00000000..0c8717fc
--- /dev/null
+++ b/gnomemusic/widgets/songrow.py
@@ -0,0 +1,182 @@
+# Copyright 2019 The GNOME Music developers
+#
+# GNOME Music is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# GNOME Music is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with GNOME Music; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# The GNOME Music authors hereby grant permission for non-GPL compatible
+# GStreamer plugins to be used and distributed together with GStreamer
+# and GNOME Music.  This permission is above and beyond the permissions
+# granted by the GPL license by which GNOME Music is covered.  If you
+# modify this code, you may extend this exception to your version of the
+# code, but you are not obligated to do so.  If you do not wish to do so,
+# delete this exception statement from your version.
+
+from enum import IntEnum
+
+from gi.repository import Gdk, GObject, Grl, Gtk
+
+from gnomemusic import utils
+from gnomemusic.grilo import grilo
+from gnomemusic.playlists import Playlists, SmartPlaylists
+from gnomemusic.widgets.starimage import StarImage  # noqa: F401
+
+
+@Gtk.Template(resource_path="/org/gnome/Music/ui/SongRow.ui")
+class SongRow(Gtk.ListBoxRow):
+    """The single song row used in PlaylistView
+
+    Contains
+     * selection check box (optional)
+     * play icon (depending on state)
+     * song title
+     * favorite/star picker
+     * song duration
+     * song album name
+     * song artist
+    """
+
+    __gtype_name__ = "SongRow"
+
+    __gsignals__ = {
+        'selection-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
+    }
+
+    selected = GObject.Property(type=bool, default=False)
+
+    _playlists = Playlists.get_default()
+
+    _album_label = Gtk.Template.Child()
+    _artist_label = Gtk.Template.Child()
+    _duration_label = Gtk.Template.Child()
+    _play_icon = Gtk.Template.Child()
+    # _select_button = Gtk.Template.Child()
+    _star_image = Gtk.Template.Child()
+    _title_label = Gtk.Template.Child()
+
+    class State(IntEnum):
+        """The state of the SongWidget
+        """
+        UNPLAYABLE = 0
+        UNPLAYED = 1
+        PLAYING = 2
+
+    def __repr__(self):
+        return '<SongRow>'
+
+    def __init__(self, media):
+        super().__init__()
+
+        self._media = media
+        self._selection_mode = False
+        self._state = SongRow.State.UNPLAYED
+
+        self.get_style_context().add_class('song-row')
+
+        title = utils.get_media_title(media)
+        self._title_label.props.label = title
+
+        time = utils.seconds_to_string(media.get_duration())
+        self._duration_label.props.label = time
+
+        self._star_image.props.favorite = media.get_favourite()
+
+        artist = utils.get_artist_name(media)
+        self._artist_label.props.label = artist
+
+        album = utils.get_album_title(media)
+        self._album_label.props.label = album
+
+        # self.bind_property(
+        #     'selected', self._select_button, 'active',
+        #     GObject.BindingFlags.BIDIRECTIONAL
+        #     | GObject.BindingFlags.SYNC_CREATE)
+
+    # @Gtk.Template.Callback()
+    # def _on_selection_changed(self, klass, value):
+    #     self.emit('selection-changed')
+
+    @Gtk.Template.Callback()
+    def _on_star_toggle(self, widget, event):
+        (_, button) = event.get_button()
+        if button != Gdk.BUTTON_PRIMARY:
+            return False
+
+        favorite = not self._star_image.favorite
+        self._star_image.props.favorite = favorite
+
+        # TODO: Rework and stop updating widgets from here directly.
+        grilo.set_favorite(self._media, favorite)
+        self._playlists.update_smart_playlist(SmartPlaylists.Favorites)
+
+        return True
+
+    @Gtk.Template.Callback()
+    def _on_star_hover(self, widget, event):
+        self._star_image.props.hover = True
+
+    @Gtk.Template.Callback()
+    def _on_star_unhover(self, widget, event):
+        self._star_image.props.hover = False
+
+    @GObject.Property(type=bool, default=False)
+    def selection_mode(self):
+        """Selection mode
+
+        :returns: Selection mode
+        :rtype: bool
+        """
+        return self._selection_mode
+
+    @selection_mode.setter
+    def selection_mode(self, value):
+        """Set the selection mode
+
+        :param bool value: Selection mode
+        """
+        self._selection_mode = value
+        self._select_button.set_visible(value)
+
+        if not value:
+            self.props.selected = False
+
+    @GObject.Property(type=Grl.Media, flags=GObject.ParamFlags.READABLE)
+    def media(self):
+        return self._media
+
+    @GObject.Property(type=int, default=0)
+    def state(self):
+        """State of the widget
+
+        :returns: Widget state
+        :rtype: SongWidget.State
+        """
+        return self._state
+
+    @state.setter
+    def state(self, value):
+        """Set state of the of widget
+
+        This influences the look of the widgets label and if there is a
+        song play indicator being shown.
+
+        :param SongWidget.State value: Widget state
+        """
+        self._state = value
+        if value == SongRow.State.UNPLAYABLE:
+            self._play_icon.props.icon_name = "dialog-error-symbolic"
+        elif value == SongRow.State.PLAYING:
+            self._play_icon.props.icon_name = "media-playback-start-symbolic"
+        else:
+            self._play_icon.props.icon_name = None
+            self._play_icon.props.icon_size = 1
diff --git a/meson.build b/meson.build
index f040bc22..2cfa9b97 100644
--- a/meson.build
+++ b/meson.build
@@ -45,7 +45,7 @@ dependency('libsoup-2.4')
 dependency('tracker-sparql-2.0', version: '>= 1.99.1')
 dependency('pygobject-3.0', version: '>= 3.29.1')
 dependency('py3cairo', version: '>= 1.14.0')
-dependency('grilo-0.3', version: '>= 0.3.8')
+dependency('grilo-0.3', version: '>= 0.3.7')
 dependency('grilo-plugins-0.3', version: '>= 0.3.8')
 
 subproject('libgd',


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]