[gnome-music/wip/jfelder/smartplaylists-ui-v2: 25/29] Introduce smart playlist widgets



commit 8371b82231a5c203694507b11fecc790b3649c35
Author: Jean Felder <jfelder src gnome org>
Date:   Wed Sep 11 14:00:55 2019 +0200

    Introduce smart playlist widgets
    
    Smart Playlist are grouped together in flowboxes. Each smart playlist
    is a flowbox item.
    A play button is shown on hover. This remains while a playlist is
    being played, and turns into a pause button when hovered.
    Remove the "protected playlist" mechanism as it becomes obsolote: all
    user playlists can be edited. Button and context menu are not visible for
    the smart playlists.
    
    Closes: #49

 data/org.gnome.Music.css                   |   7 +-
 data/org.gnome.Music.gresource.xml         |   2 +
 data/ui/SmartPlaylistCover.ui              |  75 ++++++++++++++
 data/ui/SmartPlaylistsWidget.ui            |  43 ++++++++
 gnomemusic/widgets/smartplaylistcover.py   | 161 +++++++++++++++++++++++++++++
 gnomemusic/widgets/smartplaylistswidget.py |  95 +++++++++++++++++
 po/POTFILES.in                             |   1 +
 7 files changed, 383 insertions(+), 1 deletion(-)
---
diff --git a/data/org.gnome.Music.css b/data/org.gnome.Music.css
index 9b50c540..4cdcc779 100644
--- a/data/org.gnome.Music.css
+++ b/data/org.gnome.Music.css
@@ -45,7 +45,7 @@ box#ArtistAlbumsWidget .artist-label {
     font-size: smaller;
 }
 
-/* PlaylistControls */
+/* PlaylistControls and SmartPlaylistCover */
 .playlist-name-label {
   font-weight: bold;
   font-size: x-large;
@@ -55,6 +55,11 @@ box#ArtistAlbumsWidget .artist-label {
     margin: 18px 24px 18px 18px;
 }
 
+.smartplaylist-image {
+    border-radius: 50%;
+    color: white;
+}
+
 /* ArtistAlbumWidget */
 .artist-albums-widget,
 .artist-albums-widget:hover {
diff --git a/data/org.gnome.Music.gresource.xml b/data/org.gnome.Music.gresource.xml
index 80ea92f9..eb4f8440 100644
--- a/data/org.gnome.Music.gresource.xml
+++ b/data/org.gnome.Music.gresource.xml
@@ -29,6 +29,8 @@
     <file preprocess="xml-stripblanks">ui/SearchView.ui</file>
     <file preprocess="xml-stripblanks">ui/SelectionBarMenuButton.ui</file>
     <file preprocess="xml-stripblanks">ui/SelectionToolbar.ui</file>
+    <file preprocess="xml-stripblanks">ui/SmartPlaylistCover.ui</file>
+    <file preprocess="xml-stripblanks">ui/SmartPlaylistsWidget.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/SmartPlaylistCover.ui b/data/ui/SmartPlaylistCover.ui
new file mode 100644
index 00000000..3323cb1d
--- /dev/null
+++ b/data/ui/SmartPlaylistCover.ui
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="SmartPlaylistCover" parent="GtkFlowBoxChild">
+    <property name="no_show_all">True</property>
+    <child>
+      <object class="GtkBox" id="box_container">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="valign">start</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkOverlay" id="_overlay">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="margin_bottom">4</property>
+            <child>
+              <object class="GtkImage" id="_bg_image">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="vexpand">True</property>
+                <property name="valign">center</property>
+                <property name="halign">center</property>
+                <property name="icon-size">6</property>
+              </object>
+            </child>
+            <child type="overlay">
+              <object class="GtkBox">
+                <property name="hexpand">True</property>
+                <property name="vexpand">True</property>
+                <property name="visible">True</property>
+                <child>
+                  <object class="GtkImage" id="_fg_image">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="hexpand">True</property>
+                    <property name="vexpand">True</property>
+                    <property name="halign">center</property>
+                    <property name="valign">center</property>
+                    <property name="icon-size">6</property>
+                    <style>
+                      <class name="smartplaylist-image"/>
+                    </style>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel" id="_title">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="justify">center</property>
+            <property name="wrap">True</property>
+            <property name="ellipsize">middle</property>
+            <property name="max_width_chars">20</property>
+            <property name="lines">2</property>
+            <property name="tooltip_text" bind-source="_title" bind-property="label" bind-flags="default" />
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+  <object class="GtkEventControllerMotion" id="_motions_ctrlr">
+    <property name="widget">_overlay</property>
+    <property name="propagation-phase">capture</property>
+    <signal name="enter" handler="_on_smartplaylist_enter" swapped="no"/>
+    <signal name="leave" handler="_on_smartplaylist_leave" swapped="no"/>
+  </object>
+  <object class="GtkGestureMultiPress" id="_click_ctrlr">
+    <property name="widget">_overlay</property>
+    <property name="propagation-phase">capture</property>
+    <signal name="pressed" handler="_on_smartplaylist_pressed" swapped="no"/>
+  </object>
+</interface>
diff --git a/data/ui/SmartPlaylistsWidget.ui b/data/ui/SmartPlaylistsWidget.ui
new file mode 100644
index 00000000..28988462
--- /dev/null
+++ b/data/ui/SmartPlaylistsWidget.ui
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="SmartPlaylistsWidget" parent="GtkBox">
+    <property name="hexpand">True</property>
+    <property name="orientation">vertical</property>
+    <child>
+      <object class="GtkBox" id="title_box">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="valign">start</property>
+        <property name="orientation">vertical</property>
+        <style>
+          <class name="playlist-title-container"/>
+        </style>
+        <child>
+          <object class="GtkLabel" id="title_label">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="halign">start</property>
+            <property name="label" translatable="yes">Smart Playlists</property>
+            <style>
+              <class name="playlist-name-label"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkFlowBox" id="_flowbox">
+            <property name="visible">True</property>
+            <property name="homogeneous">True</property>
+            <property name="selection_mode">none</property>
+            <property name="halign">fill</property>
+            <property name="valign">start</property>
+            <property name="margin">18</property>
+            <property name="row_spacing">12</property>
+            <property name="column_spacing">6</property>
+            <property name="min_children_per_line">1</property>
+            <property name="max_children_per_line">5</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/gnomemusic/widgets/smartplaylistcover.py b/gnomemusic/widgets/smartplaylistcover.py
new file mode 100644
index 00000000..ba4f2ec4
--- /dev/null
+++ b/gnomemusic/widgets/smartplaylistcover.py
@@ -0,0 +1,161 @@
+# Copyright 2020 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, Gtk
+
+from gnomemusic.albumartcache import Art
+from gnomemusic.grilowrappers.grltrackerplaylists import SmartPlaylist
+from gnomemusic.player import PlayerPlaylist
+
+
+@Gtk.Template(resource_path="/org/gnome/Music/ui/SmartPlaylistCover.ui")
+class SmartPlaylistCover(Gtk.FlowBoxChild):
+    """ A smart playlist cover is a widget which displays
+        the playlist name and its icon.
+        A play or pause icon is displayed if the playlist
+        is playing or if the pointer is above it.
+    """
+
+    __gtype_name__ = "SmartPlaylistCover"
+
+    _play_icon = "media-playback-start-symbolic"
+    _pause_icon = "media-playback-pause-symbolic"
+
+    class State(IntEnum):
+        STOPPED = 0
+        PAUSED = 1
+        PLAYING = 2
+
+    _click_ctrlr = Gtk.Template.Child()
+    _overlay = Gtk.Template.Child()
+    _bg_image = Gtk.Template.Child()
+    _fg_image = Gtk.Template.Child()
+    _title = Gtk.Template.Child()
+    _motions_ctrlr = Gtk.Template.Child()
+
+    state = GObject.Property(type=int, default=0)
+
+    def __init__(self, smart_playlist, coremodel, player):
+        """Initialize the cover.
+
+        :param SmartPlaylit smart_playlist: the smart playlist to use
+        :param CoreModel coremodel: the CoreModel object
+        :param Player player: The Player object
+        """
+        super().__init__()
+
+        self._coremodel = coremodel
+        self._player = player
+
+        self._playlist = smart_playlist
+
+        self._title.props.label = smart_playlist.props.title
+
+        self._overlay.add_events(
+            Gdk.EventMask.ENTER_NOTIFY_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK)
+
+        self._bg_image.override_background_color(
+            Gtk.StateType.NORMAL, smart_playlist.props.color)
+        self._bg_image.props.width_request = Art.Size.MEDIUM.width
+        self._bg_image.props.height_request = Art.Size.MEDIUM.height
+        self._bg_image.props.icon_name = smart_playlist.props.icon_name
+
+        self._fg_image.override_background_color(
+            Gtk.StateType.NORMAL, Gdk.RGBA(0., 0., 0., 1.))
+        self._fg_image.props.width_request = Art.Size.MEDIUM.width / 2
+        self._fg_image.props.height_request = Art.Size.MEDIUM.height / 2
+
+        self._hover = False
+        self.connect("notify::state", self._on_state_changed)
+
+        self._playlist.props.model.connect(
+            "items-changed", self._on_model_items_changed)
+        self.stop()
+
+    def _on_model_items_changed(self, model, position, removed, added):
+        self.set_visibility()
+
+    def set_visibility(self):
+        """Display the playlist if has some songs."""
+        self.props.visible = self._playlist.props.model.get_n_items() > 0
+
+    @Gtk.Template.Callback()
+    def _on_smartplaylist_enter(self, ctrlr, x, y):
+        self._hover = True
+        self._on_state_changed(self)
+
+    @Gtk.Template.Callback()
+    def _on_smartplaylist_leave(self, ctrlr):
+        self._hover = False
+        self._on_state_changed(self)
+
+    def _on_state_changed(self, klass, value=None):
+        if self._hover is True:
+            self._fg_image.props.opacity = 0.8
+            if self.props.state == SmartPlaylistCover.State.PLAYING:
+                self._fg_image.props.icon_name = self._pause_icon
+            elif self.props.state == SmartPlaylistCover.State.PAUSED:
+                self._fg_image.props.icon_name = self._play_icon
+        else:
+            if self.props.state == SmartPlaylistCover.State.PLAYING:
+                self._fg_image.props.opacity = 0.8
+                self._fg_image.props.icon_name = self._play_icon
+            elif self.props.state == SmartPlaylistCover.State.PAUSED:
+                self._fg_image.props.opacity = 0.8
+                self._fg_image.props.icon_name = self._pause_icon
+            else:
+                self._fg_image.props.opacity = 0.0
+
+    @Gtk.Template.Callback()
+    def _on_smartplaylist_pressed(self, gesture, n_press, x, y):
+        if self.props.state == SmartPlaylistCover.State.PAUSED:
+            self._player.play()
+            self.props.state = SmartPlaylistCover.State.PLAYING
+            return
+        elif self.props.state == SmartPlaylistCover.State.PLAYING:
+            self._player.pause()
+            self.props.state = SmartPlaylistCover.State.PAUSED
+            return
+
+        def _on_playlist_loaded(klass, playlist_type):
+            self._player.play()
+            self.props.state = SmartPlaylistCover.State.PLAYING
+            self._coremodel.disconnect(signal_id)
+
+        signal_id = self._coremodel.connect(
+            "playlist-loaded", _on_playlist_loaded)
+        self._coremodel.set_player_model(
+            PlayerPlaylist.Type.PLAYLIST, self._playlist.props.model)
+
+    @GObject.Property(
+        type=SmartPlaylist, default=None, flags=GObject.ParamFlags.READABLE)
+    def playlist(self):
+        return self._playlist
+
+    def stop(self):
+        self.props.state = SmartPlaylistCover.State.STOPPED
+        self._fg_image.props.icon_name = self._play_icon
+        self._fg_image.props.opacity = 0.0
diff --git a/gnomemusic/widgets/smartplaylistswidget.py b/gnomemusic/widgets/smartplaylistswidget.py
new file mode 100644
index 00000000..c94f73f1
--- /dev/null
+++ b/gnomemusic/widgets/smartplaylistswidget.py
@@ -0,0 +1,95 @@
+# Copyright 2020 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 gi.repository import Gtk
+
+from gnomemusic.widgets.smartplaylistcover import SmartPlaylistCover
+
+
+@Gtk.Template(resource_path="/org/gnome/Music/ui/SmartPlaylistsWidget.ui")
+class SmartPlaylistsWidget(Gtk.Box):
+    """
+
+    """
+
+    __gtype_name__ = "SmartPlaylistsWidget"
+
+    _flowbox = Gtk.Template.Child()
+
+    def __init__(self, coremodel, player):
+        """FIXME! briefly describe function
+
+        :param CoreModel coremodel: Main CoreModel object
+        :param Player player: Main Player object
+        """
+        super().__init__()
+
+        self._coremodel = coremodel
+        self._smart_pls_model = self._coremodel.props.smart_playlists_sort
+
+        self._player = player
+
+        self._playing_cover = None
+
+        self._flowbox.bind_model(self._smart_pls_model, self._add_playlist)
+        self._smart_pls_model.connect(
+            "items-changed", self._on_model_items_changed)
+        self._on_model_items_changed(
+            self._smart_pls_model, 0, 0, self._smart_pls_model.get_n_items())
+
+    def _add_playlist(self, smart_playlist):
+        child = SmartPlaylistCover(
+            smart_playlist, self._coremodel, self._player)
+        child.connect("notify::state", self._on_cover_state_changed)
+
+        return child
+
+    def _on_model_items_changed(self, model, position, removed, added):
+        if added == 0:
+            return
+
+        for index in range(added):
+            cover = self._flowbox.get_child_at_index(position + index)
+            cover.set_visibility()
+
+    def _on_cover_state_changed(self, selected_cover, value):
+        if (selected_cover.props.state != SmartPlaylistCover.State.PLAYING
+                or selected_cover == self._playing_cover):
+            return
+
+        if self._playing_cover is not None:
+            self._playing_cover.stop()
+        self._playing_cover = selected_cover
+
+    def select(self, playlist):
+        """Changes the state of the corresponding cover to playing.
+           This function is called when the playlist is starts playing
+           from MPRIS.
+
+        :param SmartPlaylist playlist: new active playlist
+        """
+        for cover in self._flowbox:
+            if cover.props.playlist == playlist:
+                cover.props.state = SmartPlaylistCover.State.PLAYING
+                break
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 24138dad..1cae7428 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -17,6 +17,7 @@ data/ui/SearchHeaderBar.ui
 data/ui/SearchView.ui
 data/ui/SelectionBarMenuButton.ui
 data/ui/SelectionToolbar.ui
+data/ui/SmartPlaylistsWidget.ui
 gnomemusic/__init__.py
 gnomemusic/albumartcache.py
 gnomemusic/application.py


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