[gnome-music] Rewrite the album widgets



commit e7fe2b7cf2e65eb9692a0f1c9e4319aa852dc29a
Author: Marinus Schraal <mschraal src gnome org>
Date:   Thu Oct 13 16:29:54 2016 +0200

    Rewrite the album widgets
    
    Complete rewrite of the artists-album and album widgets to use only
    native GTK+ widgets and share more components, dropping the use of libgd
    here in the process. Also implements multi-disc album separation.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=750827
    https://bugzilla.gnome.org/show_bug.cgi?id=772128
    https://bugzilla.gnome.org/show_bug.cgi?id=761891

 data/ArtistAlbumWidget.ui                |   84 +++---
 data/TrackWidget.ui                      |   61 +++-
 data/application.css                     |    8 +
 gnomemusic/grilo.py                      |   11 +
 gnomemusic/query.py                      |    1 +
 gnomemusic/utils.py                      |   14 +
 gnomemusic/views/albumsview.py           |    8 +-
 gnomemusic/views/artistsview.py          |    3 +-
 gnomemusic/views/searchview.py           |    9 +-
 gnomemusic/widgets/Makefile.am           |    1 +
 gnomemusic/widgets/albumwidget.py        |  285 +++++++++----------
 gnomemusic/widgets/artistalbumswidget.py |   59 +++--
 gnomemusic/widgets/artistalbumwidget.py  |  245 +++++++---------
 gnomemusic/widgets/disclistboxwidget.py  |  473 ++++++++++++++++++++++++++++++
 gnomemusic/window.py                     |    4 +-
 po/POTFILES.in                           |    1 +
 16 files changed, 898 insertions(+), 369 deletions(-)
---
diff --git a/data/ArtistAlbumWidget.ui b/data/ArtistAlbumWidget.ui
index 637d78f..7174764 100644
--- a/data/ArtistAlbumWidget.ui
+++ b/data/ArtistAlbumWidget.ui
@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.20.0 -->
 <interface>
-  <!-- interface-requires gtk+ 3.12 -->
+  <requires lib="gtk+" version="3.12"/>
   <object class="GtkBox" id="ArtistAlbumWidget">
     <property name="visible">True</property>
     <property name="can_focus">False</property>
@@ -16,7 +17,6 @@
       <packing>
         <property name="expand">False</property>
         <property name="fill">True</property>
-        <property name="padding">0</property>
         <property name="position">0</property>
       </packing>
     </child>
@@ -33,18 +33,18 @@
               <object class="GtkLabel" id="title">
                 <property name="visible">True</property>
                 <property name="can_focus">False</property>
-                <property name="xalign">0</property>
-                <property name="yalign">0</property>
                 <property name="margin_start">6</property>
                 <property name="margin_end">6</property>
                 <property name="ellipsize">middle</property>
-                <style>
-                  <class name="title"/>
-                </style>
+                <property name="xalign">0</property>
+                <property name="yalign">0</property>
                 <attributes>
                   <attribute name="weight" value="bold"/>
                   <attribute name="foreground" value="#bababdbdb6b6"/>
                 </attributes>
+                <style>
+                  <class name="title"/>
+                </style>
               </object>
               <packing>
                 <property name="expand">False</property>
@@ -76,42 +76,14 @@
           </packing>
         </child>
         <child>
-          <object class="GtkGrid" id="grid1">
+          <object class="DiscListBox" id="disclistbox">
             <property name="visible">True</property>
             <property name="can_focus">False</property>
-            <property name="margin_top">24</property>
-            <property name="column_homogeneous">True</property>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
+            <property name="selection_mode">none</property>
           </object>
           <packing>
             <property name="expand">False</property>
-            <property name="fill">False</property>
+            <property name="fill">True</property>
             <property name="position">1</property>
           </packing>
         </child>
@@ -123,6 +95,42 @@
       </packing>
     </child>
   </object>
+  <object class="GtkBox" id="disc">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="orientation">vertical</property>
+    <property name="margin_top">16</property>
+    <child>
+      <object class="GtkLabel" id="disclabel">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">start</property>
+        <property name="margin_start">60</property>
+        <property name="margin_bottom">4</property>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="DiscSongsFlowBox" id="discsongsflowbox">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="valign">start</property>
+        <property name="orientation">vertical</property>
+        <property name="homogeneous">True</property>
+        <property name="max_children_per_line">0</property>
+        <property name="selection_mode">none</property>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+  </object>
   <object class="GtkListStore" id="liststore1">
     <columns>
       <!-- column-name gchararray1 -->
diff --git a/data/TrackWidget.ui b/data/TrackWidget.ui
index 8b69b17..02e8632 100644
--- a/data/TrackWidget.ui
+++ b/data/TrackWidget.ui
@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.20.0 -->
 <interface>
-  <!-- interface-requires gtk+ 3.0 -->
+  <requires lib="gtk+" version="3.10"/>
   <object class="GtkEventBox" id="eventbox1">
     <property name="visible">True</property>
     <property name="can_focus">False</property>
@@ -33,13 +34,12 @@
               </packing>
             </child>
             <child>
-              <placeholder/>
-            </child>
-            <child>
               <object class="GtkCheckButton" id="select">
                 <property name="visible">True</property>
                 <property name="can_focus">False</property>
+                <property name="receives_default">False</property>
                 <property name="no_show_all">True</property>
+                <property name="draw_indicator">True</property>
               </object>
               <packing>
                 <property name="expand">False</property>
@@ -51,12 +51,13 @@
               <object class="GtkLabel" id="num">
                 <property name="visible">True</property>
                 <property name="can_focus">False</property>
-                <property name="xalign">1</property>
+                <property name="halign">end</property>
                 <property name="justify">right</property>
               </object>
               <packing>
                 <property name="expand">False</property>
                 <property name="fill">True</property>
+                <property name="pack_type">end</property>
                 <property name="position">2</property>
               </packing>
             </child>
@@ -77,40 +78,72 @@
               <object class="GtkLabel" id="title">
                 <property name="visible">True</property>
                 <property name="can_focus">False</property>
+                <property name="halign">start</property>
                 <property name="valign">start</property>
-                <property name="xalign">0</property>
                 <property name="ellipsize">end</property>
-                <property name="max-width-chars">90</property>
+                <property name="max_width_chars">90</property>
               </object>
               <packing>
                 <property name="expand">True</property>
                 <property name="fill">True</property>
                 <property name="padding">9</property>
-                <property name="position">2</property>
+                <property name="position">0</property>
               </packing>
             </child>
             <child>
               <object class="GtkLabel" id="duration">
                 <property name="visible">True</property>
                 <property name="can_focus">False</property>
-                <property name="valign">start</property>
-                <property name="xalign">1</property>
-                <property name="yalign">0</property>
+                <property name="no_show_all">True</property>
+                <property name="halign">end</property>
+                <property name="valign">center</property>
+                <property name="single_line_mode">True</property>
               </object>
               <packing>
                 <property name="expand">False</property>
-                <property name="fill">False</property>
-                <property name="pack_type">end</property>
-                <property name="position">3</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
               </packing>
             </child>
           </object>
           <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkEventBox" id="starevent">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="no_show_all">True</property>
+            <property name="halign">end</property>
+            <property name="valign">center</property>
+            <property name="visible_window">True</property>
+            <child>
+              <object class="StarStack" id="starstack">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="valign">center</property>
+                <property name="transition_type">crossfade</property>
+                <child>
+                  <placeholder/>
+                </child>
+                <child>
+                  <placeholder/>
+                </child>
+              </object>
+            </child>
+          </object>
+          <packing>
             <property name="expand">False</property>
             <property name="fill">True</property>
             <property name="position">2</property>
           </packing>
         </child>
+        <child>
+          <placeholder/>
+        </child>
       </object>
     </child>
   </object>
diff --git a/data/application.css b/data/application.css
index 8a609ab..198fc38 100644
--- a/data/application.css
+++ b/data/application.css
@@ -11,6 +11,14 @@ flowbox, treeview, widget {
     -gtk-key-bindings: unbind-ctrl-space;
 }
 
+row, list {
+    background-color: @theme_bg_color;
+}
+
+.discsongsflowbox > flowboxchild {
+    padding: 0px;
+}
+
 .cover{
     padding-left:24px;
 }
diff --git a/gnomemusic/grilo.py b/gnomemusic/grilo.py
index 74cd4f7..c9e816d 100644
--- a/gnomemusic/grilo.py
+++ b/gnomemusic/grilo.py
@@ -48,6 +48,7 @@ class Grilo(GObject.GObject):
     METADATA_KEYS = [
         Grl.METADATA_KEY_ALBUM,
         Grl.METADATA_KEY_ALBUM_ARTIST,
+        Grl.METADATA_KEY_ALBUM_DISC_NUMBER,
         Grl.METADATA_KEY_ARTIST,
         Grl.METADATA_KEY_CREATION_DATE,
         Grl.METADATA_KEY_DURATION,
@@ -280,6 +281,16 @@ class Grilo(GObject.GObject):
             song_item.set_lyrics("i'm truthy")
 
     @log
+    def set_favorite(self, song_item, favorite):
+        """Set the favorite status of a media item
+
+        :param song_item: A Grilo media item
+        :param bool favorite: Set favorite status
+        """
+        if bool(song_item.get_lyrics()) != favorite:
+            self.toggle_favorite(song_item)
+
+    @log
     def search(self, q, callback, data=None):
         options = self.options.copy()
         self._search_callback_counter = 0
diff --git a/gnomemusic/query.py b/gnomemusic/query.py
index b688341..f990c51 100644
--- a/gnomemusic/query.py
+++ b/gnomemusic/query.py
@@ -272,6 +272,7 @@ class Query():
         nmm:artistName(nmm:performer(?song)) AS ?artist
         nie:title(nmm:musicAlbum(?song)) AS ?album
         nfo:duration(?song) AS ?duration
+        nmm:setNumber(nmm:musicAlbumDisc(?song)) AS ?album_disc_number
         IF(bound(?tag), 'truth!', '') AS ?lyrics
     WHERE {
         ?song a nmm:MusicPiece ;
diff --git a/gnomemusic/utils.py b/gnomemusic/utils.py
index 997a926..4a15310 100644
--- a/gnomemusic/utils.py
+++ b/gnomemusic/utils.py
@@ -73,3 +73,17 @@ def get_media_title(item):
 
     return (item.get_title()
             or _("Untitled"))
+
+
+def seconds_to_string(duration):
+    """Convert a time in seconds to a hh:mm string
+
+    :param int duration: Time in seconds
+    :return: Time in hh:mm format
+    :rtype: string
+    """
+    seconds = duration
+    minutes = seconds // 60
+    seconds %= 60
+
+    return '{:d}:{:02d}'.format(minutes, seconds)
diff --git a/gnomemusic/views/albumsview.py b/gnomemusic/views/albumsview.py
index 76e8f85..e196f4c 100644
--- a/gnomemusic/views/albumsview.py
+++ b/gnomemusic/views/albumsview.py
@@ -129,12 +129,10 @@ class AlbumsView(BaseView):
 
     @log
     def get_selected_tracks(self, callback):
+        # FIXME: we call into private objects with full knowledge of
+        # what is there
         if self.header_bar._state == ToolbarState.CHILD_VIEW:
-            items = []
-            for path in self._albumWidget.view.get_selection():
-                _iter = self._albumWidget.model.get_iter(path)
-                items.append(self._albumWidget.model.get_value(_iter, 5))
-            callback(items)
+            callback(self._albumWidget._disc_listbox.get_selected_items())
         else:
             self.items_selected = []
             self.items_selected_callback = callback
diff --git a/gnomemusic/views/artistsview.py b/gnomemusic/views/artistsview.py
index 9d9a51e..eba4717 100644
--- a/gnomemusic/views/artistsview.py
+++ b/gnomemusic/views/artistsview.py
@@ -116,7 +116,8 @@ class ArtistsView(BaseView):
 
         widget = self._artists[artist.casefold()]['widget']
         if widget:
-            if widget.model == self.player.running_playlist('Artist', widget.artist):
+            # FIXME: internal call
+            if widget._model == self.player.running_playlist('Artist', widget.artist):
                 self._artistAlbumsWidget = widget.get_parent()
                 GLib.idle_add(self.artistAlbumsStack.set_visible_child,
                               self._artistAlbumsWidget)
diff --git a/gnomemusic/views/searchview.py b/gnomemusic/views/searchview.py
index a3c393f..b9fb4cc 100644
--- a/gnomemusic/views/searchview.py
+++ b/gnomemusic/views/searchview.py
@@ -306,14 +306,11 @@ class SearchView(BaseView):
     @log
     def get_selected_tracks(self, callback):
         if self.get_visible_child() == self._albumWidget:
-            items = []
-            for path in self._albumWidget.view.get_selection():
-                _iter = self._albumWidget.model.get_iter(path)
-                items.append(self._albumWidget.model.get_value(_iter, 5))
-            callback(items)
+            callback(self._albumWidget.view.get_selected_items())
         elif self.get_visible_child() == self._artistAlbumsWidget:
             items = []
-            for row in self._artistAlbumsWidget.model:
+            # FIXME: calling into private model
+            for row in self._artistAlbumsWidget._model:
                 if row[6]:
                     items.append(row[5])
             callback(items)
diff --git a/gnomemusic/widgets/Makefile.am b/gnomemusic/widgets/Makefile.am
index 8f4d03c..e02db58 100644
--- a/gnomemusic/widgets/Makefile.am
+++ b/gnomemusic/widgets/Makefile.am
@@ -5,5 +5,6 @@ app_PYTHON = \
        albumwidget.py \
        artistalbumswidget.py \
        artistalbumwidget.py \
+       disclistboxwidget.py \
        playlistdialog.py \
        starhandlerwidget.py
diff --git a/gnomemusic/widgets/albumwidget.py b/gnomemusic/widgets/albumwidget.py
index ab08d24..3cb3934 100644
--- a/gnomemusic/widgets/albumwidget.py
+++ b/gnomemusic/widgets/albumwidget.py
@@ -29,7 +29,8 @@ from gnomemusic import log
 from gnomemusic.albumartcache import AlbumArtCache, DefaultIcon, ArtSize
 from gnomemusic.grilo import grilo
 from gnomemusic.player import DiscoveryStatus
-from gnomemusic.widgets.starhandlerwidget import StarHandlerWidget
+from gnomemusic.playlists import Playlists
+from gnomemusic.widgets.disclistboxwidget import DiscBox, DiscListBox
 import gnomemusic.utils as utils
 
 NOW_PLAYING_ICON_NAME = 'media-playback-start-symbolic'
@@ -57,6 +58,8 @@ class AlbumWidget(Gtk.EventBox):
         """
         Gtk.EventBox.__init__(self)
 
+        self._tracks = []
+
         scale = self.get_scale_factor()
         self._cache = AlbumArtCache(scale)
         self._loading_icon_surface = DefaultIcon(scale).get(
@@ -66,35 +69,33 @@ class AlbumWidget(Gtk.EventBox):
         self._player = player
         self._iter_to_clean = None
 
-        self._ui = Gtk.Builder()
-        self._ui.add_from_resource('/org/gnome/Music/AlbumWidget.ui')
+        self._selection_mode = False
+
+        self._builder = Gtk.Builder()
+        self._builder.add_from_resource('/org/gnome/Music/AlbumWidget.ui')
         self._create_model()
-        self.view = Gd.MainView(
-            shadow_type=Gtk.ShadowType.NONE
-        )
-        self.view.set_view_type(Gd.MainViewType.LIST)
         self._album = None
         self._header_bar = None
-        self.view.connect('item-activated', self._on_item_activated)
-
-        view_box = self._ui.get_object('view')
-        self._ui.get_object('scrolledWindow').set_placement(Gtk.CornerType.
-                                                            TOP_LEFT)
-        self.view.connect('selection-mode-request',
-                          self._on_selection_mode_request)
-        child_view = self.view.get_children()[0]
-        child_view.set_margin_top(64)
-        child_view.set_margin_bottom(64)
-        child_view.set_margin_end(32)
-        self.view.remove(child_view)
-        view_box.add(child_view)
-
-        self.add(self._ui.get_object('AlbumWidget'))
-        self._star_handler = StarHandlerWidget(self, 9)
-        self._add_list_renderers()
+        self._selection_mode_allowed = True
+
+        view_box = self._builder.get_object('view')
+        self._disc_listbox = DiscListBox()
+        self._disc_listbox.set_selection_mode_allowed(True)
+        self._disc_listbox.set_margin_top(64)
+        self._disc_listbox.set_margin_bottom(64)
+        self._disc_listbox.set_margin_end(32)
+        self._disc_listbox.connect('selection-changed',
+                                   self._on_selection_changed)
+        view_box.add(self._disc_listbox)
+
+        # FIXME: Assigned to appease searchview
+        # get_selected_tracks
+        self.view = self._disc_listbox
+
+        self.add(self._builder.get_object('AlbumWidget'))
         self.get_style_context().add_class('view')
         self.get_style_context().add_class('content-view')
-        self.view.get_generic_view().get_style_context().remove_class('view')
+
         self.show_all()
 
     @log
@@ -103,91 +104,9 @@ class AlbumWidget(Gtk.EventBox):
         self._header_bar._select_button.clicked()
 
     @log
-    def _on_item_activated(self, widget, id, path):
-        """List row activated."""
-        if self._star_handler.star_renderer_click:
-            self._star_handler.star_renderer_click = False
-            return
-
-        _iter = self.model.get_iter(path)
-
-        if self.model[_iter][10] != DiscoveryStatus.FAILED:
-            if (self._iter_to_clean
-                    and self._player.playlistId == self._album):
-                item = self.model[self._iter_to_clean][5]
-                title = utils.get_media_title(item)
-                self.model[self._iter_to_clean][0] = title
-                # Hide now playing icon
-                self.model[self._iter_to_clean][6] = False
-            self._player.set_playlist('Album', self._album, self.model, _iter,
-                                      5, 11)
-            self._player.set_playing(True)
-
-    @log
-    def _add_list_renderers(self):
-        """Create the ListView columns."""
-        list_widget = self.view.get_generic_view()
-
-        cols = list_widget.get_columns()
-        cols[0].set_min_width(100)
-        cols[0].set_max_width(200)
-        cells = cols[0].get_cells()
-        cells[2].set_visible(False)
-        cells[1].set_visible(False)
-
-        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)
-        list_widget.insert_column(column_now_playing, 0)
-
-        type_renderer = Gd.StyledTextRenderer(
-            xpad=16,
-            ellipsize=Pango.EllipsizeMode.END,
-            xalign=0.0
-        )
-
-        list_widget.add_renderer(type_renderer, lambda *args: None, None)
-        cols[0].clear_attributes(type_renderer)
-        cols[0].add_attribute(type_renderer, 'markup', 0)
-
-        duration_renderer = Gd.StyledTextRenderer(
-            xpad=16,
-            ellipsize=Pango.EllipsizeMode.END,
-            xalign=1.0
-        )
-
-        duration_renderer.add_class('dim-label')
-        list_widget.add_renderer(duration_renderer, lambda *args: None, None)
-        cols[0].clear_attributes(duration_renderer)
-        cols[0].add_attribute(duration_renderer, 'markup', 1)
-
-        self._star_handler.add_star_renderers(list_widget, cols)
-
-    def _on_list_widget_icon_render(self, col, cell, model, _iter, data):
-        if not self._player.currentTrackUri:
-            cell.set_visible(False)
-            return
-
-        if model[_iter][10] == DiscoveryStatus.FAILED:
-            cell.set_property('icon-name', ERROR_ICON_NAME)
-            cell.set_visible(True)
-        elif model[_iter][5].get_url() == self._player.currentTrackUri:
-            cell.set_property('icon-name', NOW_PLAYING_ICON_NAME)
-            cell.set_visible(True)
-        else:
-            cell.set_visible(False)
-
-    @log
     def _create_model(self):
         """Create the ListStore model for this widget."""
-        self.model = Gtk.ListStore(
+        self._model = Gtk.ListStore(
             GObject.TYPE_STRING,  # title
             GObject.TYPE_STRING,
             GObject.TYPE_STRING,
@@ -212,36 +131,41 @@ class AlbumWidget(Gtk.EventBox):
         :param header_bar: The header bar object
         :param selection_toolbar: The selection toolbar object
         """
+        # reset view
+        self._tracks = []
+        self._create_model()
+        for widget in self._disc_listbox.get_children():
+            self._disc_listbox.remove(widget)
+
         self.selection_toolbar = selection_toolbar
         self._header_bar = header_bar
         self._album = album
-        self._ui.get_object('cover').set_from_surface(
+        self._builder.get_object('cover').set_from_surface(
             self._loading_icon_surface)
         self._cache.lookup(item, ArtSize.large, self._on_lookup, None)
         self._duration = 0
-        self._create_model()
+
         GLib.idle_add(grilo.populate_album_songs, item, self.add_item)
         header_bar._select_button.connect(
             'toggled', self._on_header_select_button_toggled)
         header_bar._cancel_button.connect(
             'clicked', self._on_header_cancel_button_clicked)
-        self.view.connect('view-selection-changed',
-                          self._on_view_selection_changed)
-        self.view.set_model(self.model)
+
+        # FIXME: use utils
         escaped_artist = GLib.markup_escape_text(artist)
         escaped_album = GLib.markup_escape_text(album)
-        self._ui.get_object('artist_label').set_markup(escaped_artist)
-        self._ui.get_object('title_label').set_markup(escaped_album)
+        self._builder.get_object('artist_label').set_markup(escaped_artist)
+        self._builder.get_object('title_label').set_markup(escaped_album)
         if (item.get_creation_date()):
-            self._ui.get_object('released_label_info').set_text(
+            self._builder.get_object('released_label_info').set_text(
                 str(item.get_creation_date().get_year()))
         else:
-            self._ui.get_object('released_label_info').set_text('----')
+            self._builder.get_object('released_label_info').set_text('----')
         self._player.connect('playlist-item-changed', self._update_model)
 
     @log
-    def _on_view_selection_changed(self, widget):
-        items = self.view.get_selection()
+    def _on_selection_changed(self, widget):
+        items = self._disc_listbox.get_selected_items()
         self.selection_toolbar._add_to_playlist_button.set_sensitive(
             len(items) > 0)
         if len(items) > 0:
@@ -255,7 +179,7 @@ class AlbumWidget(Gtk.EventBox):
     @log
     def _on_header_cancel_button_clicked(self, button):
         """Cancel selection mode callback."""
-        self.view.set_selection_mode(False)
+        self._disc_listbox.set_selection_mode(False)
         self._header_bar.set_selection_mode(False)
         self._header_bar.header_bar.title = self._album
 
@@ -263,22 +187,56 @@ class AlbumWidget(Gtk.EventBox):
     def _on_header_select_button_toggled(self, button):
         """Selection mode button clicked callback."""
         if button.get_active():
-            self.view.set_selection_mode(True)
+            self._selection_mode = True
+            self._disc_listbox.set_selection_mode(True)
             self._header_bar.set_selection_mode(True)
             self._player.actionbar.set_visible(False)
-            self.selection_toolbar.actionbar.set_visible(True)
-            self.selection_toolbar._add_to_playlist_button.set_sensitive(False)
             self._header_bar.header_bar.set_custom_title(
                 self._header_bar._selection_menu_button)
         else:
-            self.view.set_selection_mode(False)
+            self._selection_mode = False
+            self._disc_listbox.set_selection_mode(False)
             self._header_bar.set_selection_mode(False)
-            self._header_bar.title = self._album
-            self.selection_toolbar.actionbar.set_visible(False)
             if(self._player.get_playback_status() != 2):
                 self._player.actionbar.set_visible(True)
 
     @log
+    def _create_disc_box(self, disc_nr, disc_tracks):
+        disc_box = DiscBox(self._model)
+        disc_box.set_tracks(disc_tracks)
+        disc_box.set_disc_number(disc_nr)
+        disc_box.set_columns(1)
+        disc_box.show_song_numbers(False)
+        disc_box.connect('track-activated', self._track_activated)
+        disc_box.connect('selection-toggle', self._selection_mode_toggled)
+
+        return disc_box
+
+    @log
+    def _selection_mode_toggled(self, widget):
+        if not self._selection_mode_allowed:
+            return
+
+        self._selection_mode = not self._selection_mode
+        self._on_selection_mode_request()
+
+
+    @log
+    def _track_activated(self, widget, song_widget):
+        if not song_widget.can_be_played:
+            return
+
+        if self._selection_mode:
+            song_widget.check_button.toggled()
+            return
+
+        self._player.stop()
+        self._player.set_playlist('Artist', 'test', song_widget.model,
+                                  song_widget.itr, 5, 11)
+        self._player.set_playing(True)
+        return True
+
+    @log
     def add_item(self, source, prefs, track, remaining, data=None):
         """Add a song to the item to album list.
 
@@ -289,22 +247,30 @@ class AlbumWidget(Gtk.EventBox):
         :param data: User data
         """
         if track:
+            self._tracks.append(track)
+
             self._duration = self._duration + track.get_duration()
-            _iter = self.model.append()
-            title = utils.get_media_title(track)
-            escaped_title = GLib.markup_escape_text(title)
-            self.model[_iter][0, 1, 2, 3, 4, 5, 9] = [
-                escaped_title,
-                self._player.seconds_to_string(track.get_duration()),
-                '',
-                '',
-                None,
-                track,
-                bool(track.get_lyrics())
-            ]
-            self._ui.get_object('running_length_label_info').set_text(
+            return
+
+        discs = {}
+        for track in self._tracks:
+            disc_nr = track.get_album_disc_number()
+            if disc_nr not in discs.keys():
+                discs[disc_nr] = [track]
+            else:
+                discs[disc_nr].append(track)
+        for disc_nr in discs:
+            disc = self._create_disc_box(disc_nr, discs[disc_nr])
+            self._disc_listbox.insert(disc, -1)
+            if len(discs) == 1:
+                disc.show_disc_label(False)
+
+        if remaining == 0:
+            self._builder.get_object('running_length_label_info').set_text(
                 _("%d min") % (int(self._duration / 60) + 1))
 
+            self.show_all()
+
     @log
     def _on_lookup(self, surface, data=None):
         """Albumart retrieved callback.
@@ -313,7 +279,7 @@ class AlbumWidget(Gtk.EventBox):
         :param path: The filesystem location the pixbuf
         :param data: User data
         """
-        self._ui.get_object('cover').set_from_surface(surface)
+        self._builder.get_object('cover').set_from_surface(surface)
 
     @log
     def _update_model(self, player, playlist, current_iter):
@@ -323,29 +289,46 @@ class AlbumWidget(Gtk.EventBox):
         :param playlist: The current playlist
         :param current_iter: The current iter of the playlist model
         """
-        # self is not our playlist, return
-        if (playlist != self.model):
-            return False
+        if (playlist != self._model):
+            return True
 
         current_song = playlist[current_iter][5]
+
+        self._duration = 0
+
         song_passed = False
         _iter = playlist.get_iter_first()
-        self._duration = 0
 
         while _iter:
             song = playlist[_iter][5]
+            song_widget = song.song_widget
             self._duration += song.get_duration()
             escaped_title = GLib.markup_escape_text(utils.get_media_title(song))
+
             if (song == current_song):
-                title = '<b>%s</b>' % escaped_title
+                song_widget.now_playing_sign.show()
+                song_widget.title.set_markup("<b>{}</b>".format(escaped_title))
                 song_passed = True
             elif (song_passed):
-                title = '<span>%s</span>' % escaped_title
+                song_widget.now_playing_sign.hide()
+                song_widget.title.set_markup(
+                    "<span>{}</span>".format(escaped_title))
             else:
-                title = '<span color=\'grey\'>%s</span>' % escaped_title
-            playlist[_iter][0] = title
+                song_widget.now_playing_sign.hide()
+                song_widget.title.set_markup(
+                    "<span color=\'grey\'>{}</span>".format(escaped_title))
+
             _iter = playlist.iter_next(_iter)
-            self._ui.get_object('running_length_label_info').set_text(
-                _("%d min") % (int(self._duration / 60) + 1))
 
-        return False
+        self._builder.get_object('running_length_label_info').set_text(
+            _("%d min") % (int(self._duration / 60) + 1))
+
+        return True
+
+    @log
+    def select_all(self):
+        self._disc_listbox.select_all()
+
+    @log
+    def select_none(self):
+        self._disc_listbox.select_none()
diff --git a/gnomemusic/widgets/artistalbumswidget.py b/gnomemusic/widgets/artistalbumswidget.py
index 99a9f00..00763d5 100644
--- a/gnomemusic/widgets/artistalbumswidget.py
+++ b/gnomemusic/widgets/artistalbumswidget.py
@@ -57,15 +57,8 @@ class ArtistAlbumsWidget(Gtk.Box):
         self.ui.get_object('artist').set_label(self.artist)
         self.widgets = []
 
-        self.model = Gtk.ListStore(GObject.TYPE_STRING,   # title
-                                   GObject.TYPE_STRING,
-                                   GObject.TYPE_STRING,
-                                   GObject.TYPE_BOOLEAN,  # icon shown
-                                   GObject.TYPE_STRING,   # icon
-                                   GObject.TYPE_OBJECT,   # song object
-                                   GObject.TYPE_BOOLEAN,
-                                   GObject.TYPE_INT
-                                   )
+        self._create_model()
+
         self.row_changed_source_id = None
 
         self._hbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
@@ -94,6 +87,24 @@ class ArtistAlbumsWidget(Gtk.Box):
 
         self.player.connect('playlist-item-changed', self.update_model)
 
+    @log
+    def _create_model(self):
+        """Create the ListStore model for this widget."""
+        self._model = Gtk.ListStore(
+            GObject.TYPE_STRING,  # title
+            GObject.TYPE_STRING,
+            GObject.TYPE_STRING,
+            GObject.TYPE_STRING,
+            GObject.TYPE_STRING,    # placeholder
+            GObject.TYPE_OBJECT,  # song object
+            GObject.TYPE_BOOLEAN,  # item selected
+            GObject.TYPE_STRING,
+            GObject.TYPE_BOOLEAN,
+            GObject.TYPE_INT,  # icon shown
+            GObject.TYPE_BOOLEAN,
+            GObject.TYPE_INT
+        )
+
     def _on_last_album_displayed(self, data=None):
         self.window.notification.dismiss()
         self.show_all()
@@ -102,11 +113,12 @@ class ArtistAlbumsWidget(Gtk.Box):
     def add_album(self, album, is_last_album=False):
         self.window.notification.set_timeout(0)
         widget = ArtistAlbumWidget(
-            self.artist, album, self.player, self.model,
-            self.header_bar, self.selectionModeAllowed
+            album, self.player, self._model,
+            self.header_bar, self.selectionModeAllowed,
+            self._songsGridSizeGroup, self.header_bar
         )
         self._coverSizeGroup.add_widget(widget.cover)
-        self._songsGridSizeGroup.add_widget(widget.songsGrid)
+
         self._albumBox.pack_start(widget, False, False, 0)
         self.widgets.append(widget)
 
@@ -116,7 +128,7 @@ class ArtistAlbumsWidget(Gtk.Box):
     @log
     def update_model(self, player, playlist, currentIter):
         # this is not our playlist, return
-        if playlist != self.model:
+        if playlist != self._model:
             # TODO, only clean once, but that can wait util we have clean
             # the code a bit, and until the playlist refactoring.
             # the overhead is acceptable for now
@@ -153,15 +165,15 @@ class ArtistAlbumsWidget(Gtk.Box):
 
     @log
     def clean_model(self):
-        itr = self.model.get_iter_first()
+        itr = self._model.get_iter_first()
         while itr:
-            song = self.model.get_value(itr, 5)
+            song = self._model.get_value(itr, 5)
             song_widget = song.song_widget
             escaped_title = GLib.markup_escape_text(utils.get_media_title(song))
             if song_widget.can_be_played:
                 song_widget.now_playing_sign.hide()
             song_widget.title.set_markup('<span>%s</span>' % escaped_title)
-            itr = self.model.iter_next(itr)
+            itr = self._model.iter_next(itr)
         return False
 
     @log
@@ -171,8 +183,10 @@ class ArtistAlbumsWidget(Gtk.Box):
         self.selectionMode = selectionMode
         try:
             if self.row_changed_source_id:
-                self.model.disconnect(self.row_changed_source_id)
-            self.row_changed_source_id = self.model.connect('row-changed', self._model_row_changed)
+                self._model.disconnect(self.row_changed_source_id)
+            self.row_changed_source_id = self._model.connect(
+                'row-changed',
+                self._model_row_changed)
         except Exception as e:
             logger.warning("Exception while tracking row-changed: %s", e)
 
@@ -195,3 +209,12 @@ class ArtistAlbumsWidget(Gtk.Box):
         else:
             self.header_bar._selection_menu_label.set_text(_("Click on items to select them"))
 
+    @log
+    def select_all(self):
+        for widget in self.widgets:
+            widget._disc_listbox.select_all()
+
+    @log
+    def select_none(self):
+        for widget in self.widgets:
+            widget._disc_listbox.select_none()
diff --git a/gnomemusic/widgets/artistalbumwidget.py b/gnomemusic/widgets/artistalbumwidget.py
index 066ea73..f8af03f 100644
--- a/gnomemusic/widgets/artistalbumwidget.py
+++ b/gnomemusic/widgets/artistalbumwidget.py
@@ -22,32 +22,15 @@
 # code, but you are not obligated to do so.  If you do not wish to do so,
 # delete this exception statement from your version.
 
-import logging
-
 from gettext import gettext as _
 from gi.repository import Gdk, Gio, GLib, GObject, Gtk
 
 from gnomemusic import log
 from gnomemusic.albumartcache import AlbumArtCache, DefaultIcon, ArtSize
 from gnomemusic.grilo import grilo
-from gnomemusic.player import DiscoveryStatus
-from gnomemusic.playlists import Playlists
+from gnomemusic.widgets.disclistboxwidget import DiscBox, DiscListBox
 import gnomemusic.utils as utils
 
-logger = logging.getLogger(__name__)
-
-NOW_PLAYING_ICON_NAME = 'media-playback-start-symbolic'
-ERROR_ICON_NAME = 'dialog-error-symbolic'
-
-try:
-    settings = Gio.Settings.new('org.gnome.Music')
-    MAX_TITLE_WIDTH = settings.get_int('max-width-chars')
-except Exception as e:
-    MAX_TITLE_WIDTH = 20
-    logger.error("Error on setting widget max-width-chars: %s", str(e))
-
-playlists = Playlists.get_default()
-
 
 class ArtistAlbumWidget(Gtk.Box):
 
@@ -59,95 +42,124 @@ class ArtistAlbumWidget(Gtk.Box):
         return '<ArtistAlbumWidget>'
 
     @log
-    def __init__(self, artist, album, player, model, header_bar, selectionModeAllowed):
-        Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
+    def __init__(self, media, player, model, header_bar,
+                 selection_mode_allowed, size_group=None,
+                 selection_toolbar = None):
+        super().__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
 
+        self._size_group = size_group
         scale = self.get_scale_factor()
         self._cache = AlbumArtCache(scale)
         self._loading_icon_surface = DefaultIcon(scale).get(
             DefaultIcon.Type.loading,
-            ArtSize.large)
-
-        self.player = player
-        self.album = album
-        self.artist = artist
-        self.model = model
-        self.model.connect('row-changed', self._model_row_changed)
-        self.header_bar = header_bar
-        self.selectionMode = False
-        self.selectionModeAllowed = selectionModeAllowed
+            ArtSize.medium)
+
+        self._media = media
+        self._player = player
+        self._artist = utils.get_artist_name(self._media)
+        self._album_title = utils.get_album_title(self._media)
+        self._model = model
+        self._header_bar = header_bar
+        self._selection_mode = False
+        self._selection_mode_allowed = selection_mode_allowed
+        self._selection_toolbar = selection_toolbar
         self.songs = []
+        self._tracks = []
+
+        self._header_bar._select_button.connect(
+            'toggled', self._on_header_select_button_toggled)
+
         self.ui = Gtk.Builder()
         self.ui.add_from_resource('/org/gnome/Music/ArtistAlbumWidget.ui')
 
-        GLib.idle_add(self._update_album_art)
-
         self.cover = self.ui.get_object('cover')
         self.cover.set_from_surface(self._loading_icon_surface)
-        self.songsGrid = self.ui.get_object('grid1')
-        self.ui.get_object('title').set_label(album.get_title())
-        if album.get_creation_date():
+        self._disc_listbox = self.ui.get_object('disclistbox')
+        self._disc_listbox.set_selection_mode_allowed(
+            self._selection_mode_allowed)
+
+        self.ui.get_object('title').set_label(self._album_title)
+        creation_date = self._media.get_creation_date()
+        if creation_date:
+            year = creation_date.get_year()
             self.ui.get_object('year').set_markup(
-                '<span color=\'grey\'>(%s)</span>' %
-                str(album.get_creation_date().get_year())
-            )
-        self.tracks = []
-        grilo.populate_album_songs(album, self.add_item)
+                '<span color=\'grey\'>{}</span>'.format(year))
+
+        if self._size_group:
+            self._size_group.add_widget(self.ui.get_object('box1'))
+
         self.pack_start(self.ui.get_object('ArtistAlbumWidget'), True, True, 0)
 
+        GLib.idle_add(self._update_album_art)
+        grilo.populate_album_songs(self._media, self._add_item)
+
+
+    def create_disc_box(self, disc_nr, disc_tracks):
+        disc_box = DiscBox(self._model)
+        disc_box.set_tracks(disc_tracks)
+        disc_box.set_disc_number(disc_nr)
+        disc_box.set_columns(2)
+        disc_box.show_duration(False)
+        disc_box.show_favorites(False)
+        disc_box.connect('track-activated', self._track_activated)
+        disc_box.connect('selection-toggle', self._selection_mode_toggled)
+
+        return disc_box
+
+    def _selection_mode_toggled(self, widget):
+        if not self._selection_mode_allowed:
+            return
+
+        self._selection_mode = not self._selection_mode
+        self._on_selection_mode_request()
+
+        return True
+
+    def _on_selection_mode_request(self):
+        self._header_bar._select_button.clicked()
+
+    def _on_header_select_button_toggled(self, button):
+        """Selection mode button clicked callback."""
+        if button.get_active():
+            self._selection_mode = True
+            self._disc_listbox.set_selection_mode(True)
+            self._header_bar.set_selection_mode(True)
+            self._player.actionbar.set_visible(False)
+            self._header_bar.header_bar.set_custom_title(
+                self._header_bar._selection_menu_button)
+        else:
+            self._selection_mode = False
+            self._disc_listbox.set_selection_mode(False)
+            self._header_bar.set_selection_mode(False)
+            if(self._player.get_playback_status() != 2):
+                self._player.actionbar.set_visible(True)
+
     @log
-    def add_item(self, source, prefs, track, remaining, data=None):
+    def _add_item(self, source, prefs, track, remaining, data=None):
+        if track:
+            self._tracks.append(track)
+            return
+
+        discs = {}
+        for track in self._tracks:
+            disc_nr = track.get_album_disc_number()
+            if disc_nr not in discs.keys():
+                discs[disc_nr] = [track]
+            else:
+                discs[disc_nr].append(track)
+
+        for disc_nr in discs:
+            disc = self.create_disc_box(disc_nr, discs[disc_nr])
+            self._disc_listbox.insert(disc, -1)
+            if len(discs) == 1:
+                disc.show_disc_label(False)
+
         if remaining == 0:
-            self.songsGrid.show_all()
             self.emit("tracks-loaded")
 
-        if track:
-            self.tracks.append(track)
-        else:
-            for i, track in enumerate(self.tracks):
-                ui = Gtk.Builder()
-                ui.add_from_resource('/org/gnome/Music/TrackWidget.ui')
-                song_widget = ui.get_object('eventbox1')
-                self.songs.append(song_widget)
-                ui.get_object('num')\
-                    .set_markup('<span color=\'grey\'>%d</span>'
-                                % len(self.songs))
-                title = utils.get_media_title(track)
-                ui.get_object('title').set_text(title)
-                ui.get_object('title').set_alignment(0.0, 0.5)
-                ui.get_object('title').set_max_width_chars(MAX_TITLE_WIDTH)
-
-                self.songsGrid.attach(
-                    song_widget,
-                    int(i / (len(self.tracks) / 2)),
-                    int(i % (len(self.tracks) / 2)), 1, 1
-                )
-                track.song_widget = song_widget
-                itr = self.model.append(None)
-                song_widget._iter = itr
-                song_widget.model = self.model
-                song_widget.title = ui.get_object('title')
-                song_widget.checkButton = ui.get_object('select')
-                song_widget.checkButton.set_visible(self.selectionMode)
-                song_widget.checkButton.connect(
-                    'toggled', self._check_button_toggled, song_widget
-                )
-                self.model.set(itr,
-                               [0, 1, 2, 3, 5],
-                               [title, '', '', False, track])
-                song_widget.now_playing_sign = ui.get_object('image1')
-                song_widget.now_playing_sign.set_from_icon_name(
-                    NOW_PLAYING_ICON_NAME,
-                    Gtk.IconSize.SMALL_TOOLBAR)
-                song_widget.now_playing_sign.set_no_show_all('True')
-                song_widget.now_playing_sign.set_alignment(1, 0.6)
-                song_widget.can_be_played = True
-                song_widget.connect('button-release-event',
-                                    self.track_selected)
-
     @log
     def _update_album_art(self):
-        self._cache.lookup(self.album, ArtSize.medium, self._get_album_cover,
+        self._cache.lookup(self._media, ArtSize.medium, self._get_album_cover,
                            None)
 
     @log
@@ -155,57 +167,22 @@ class ArtistAlbumWidget(Gtk.Box):
         self.cover.set_from_surface(surface)
 
     @log
-    def track_selected(self, widget, event):
-        if not widget.can_be_played:
+    def _track_activated(self, widget, song_widget):
+        if (not song_widget.can_be_played
+                or self._selection_mode):
             return
 
-        if not self.selectionMode and \
-            (event.button == Gdk.BUTTON_SECONDARY or
-                (event.button == 1 and event.state & Gdk.ModifierType.CONTROL_MASK)):
-            if self.selectionModeAllowed:
-                self.header_bar._select_button.set_active(True)
-            else:
-                return
-
-        if self.selectionMode:
-            self.model[widget._iter][6] = not self.model[widget._iter][6]
-            return
+        self._player.stop()
+        self._player.set_playlist('Artist', self._artist, song_widget.model,
+                                  song_widget.itr, 5, 11)
+        self._player.set_playing(True)
 
-        self.player.stop()
-        self.player.set_playlist('Artist', self.artist,
-                                 widget.model, widget._iter, 5, 6)
-        self.player.set_playing(True)
+        return True
 
     @log
-    def set_selection_mode(self, selectionMode):
-        if self.selectionMode == selectionMode:
+    def set_selection_mode(self, selection_mode):
+        if self._selection_mode == selection_mode:
             return
-        self.selectionMode = selectionMode
-        for songWidget in self.songs:
-            songWidget.checkButton.set_visible(selectionMode)
-            if not selectionMode:
-                songWidget.model[songWidget._iter][6] = False
+        self._selection_mode = selection_mode
 
-    @log
-    def _check_button_toggled(self, button, songWidget):
-        if songWidget.model[songWidget._iter][6] != button.get_active():
-            songWidget.model[songWidget._iter][6] = button.get_active()
-
-    @log
-    def _model_row_changed(self, model, path, _iter):
-        if not self.selectionMode:
-            return
-        if not model[_iter][5]:
-            return
-        songWidget = model[_iter][5].song_widget
-        selected = model[_iter][6]
-
-        if model[_iter][11] == DiscoveryStatus.FAILED:
-            songWidget.now_playing_sign.set_from_icon_name(
-                ERROR_ICON_NAME,
-                Gtk.IconSize.SMALL_TOOLBAR)
-            songWidget.now_playing_sign.show()
-            songWidget.can_be_played = False
-
-        if selected != songWidget.checkButton.get_active():
-            songWidget.checkButton.set_active(selected)
+        self._disc_listbox.set_selection_mode(selection_mode)
diff --git a/gnomemusic/widgets/disclistboxwidget.py b/gnomemusic/widgets/disclistboxwidget.py
new file mode 100644
index 0000000..3b82e52
--- /dev/null
+++ b/gnomemusic/widgets/disclistboxwidget.py
@@ -0,0 +1,473 @@
+# Copyright (c) 2016 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.
+
+import logging
+
+from gettext import gettext as _
+from gi.repository import Gdk, Gio, GObject, Gtk
+
+from gnomemusic import log
+from gnomemusic.grilo import grilo
+import gnomemusic.utils as utils
+
+logger = logging.getLogger(__name__)
+
+NOW_PLAYING_ICON_NAME = 'media-playback-start-symbolic'
+ERROR_ICON_NAME = 'dialog-error-symbolic'
+
+try:
+    settings = Gio.Settings.new('org.gnome.Music')
+    MAX_TITLE_WIDTH = settings.get_int('max-width-chars')
+except Exception as e:
+    MAX_TITLE_WIDTH = 20
+    logger.error("Error on setting widget max-width-chars: %s", str(e))
+
+
+class StarStack(Gtk.Stack):
+    """Stackwidget for starring songs"""
+    __gtype_name__ = 'StarStack'
+
+    def __init__(self):
+        super().__init__(self)
+
+        starred_icon = Gtk.Image.new_from_icon_name('starred-symbolic',
+                                                    Gtk.IconSize.SMALL_TOOLBAR)
+        non_starred_icon = Gtk.Image.new_from_icon_name(
+            'non-starred-symbolic',
+            Gtk.IconSize.SMALL_TOOLBAR)
+
+        self._favorite = False
+
+        self.add_named(starred_icon, 'starred')
+        self.add_named(non_starred_icon, 'non-starred')
+        self.set_visible_child_name('non-starred')
+
+        self.show_all()
+
+    def set_favorite(self, favorite):
+        """Set favorite
+
+        Set the current widget as favorite or not.
+
+        :param bool favorite: Value to switch the widget state to
+        """
+        self._favorite = favorite
+
+        if self._favorite:
+            self.set_visible_child_name('starred')
+        else:
+            self.set_visible_child_name('non-starred')
+
+    def get_favorite(self):
+        """Return the current state of the widget
+
+        :return: The state of the widget
+        :rtype: bool
+        """
+        return self._favorite
+
+    def toggle_favorite(self):
+        """Toggle the widget state"""
+        self._favorite = not self._favorite
+
+        self.set_favorite(self._favorite)
+
+
+class DiscSongsFlowBox(Gtk.FlowBox):
+    """FlowBox containing the songs on one disc
+
+    DiscSongsFlowBox allows setting the number of columns to
+    use.
+    """
+    __gtype_name__ = 'DiscSongsFlowBox'
+
+    def __init__(self, columns=1):
+        """Initialize
+
+        :param int columns: The number of columns the widget uses
+        """
+        super().__init__(self)
+        super().set_selection_mode(Gtk.SelectionMode.NONE)
+
+        self._columns = columns
+        self.get_style_context().add_class('discsongsflowbox')
+
+    def set_columns(self, columns):
+        """Set the number of columns to use
+
+        :param int columns: The number of columns the widget uses
+        """
+        self._columns = columns
+
+        children_n = len(self.get_children())
+
+        if children_n % self._columns == 0:
+            max_per_line = children_n / self._columns
+        else:
+            max_per_line = int(children_n / self._columns) + 1
+
+        self.set_max_children_per_line(max_per_line)
+        self.set_min_children_per_line(max_per_line)
+
+
+class DiscBox(Gtk.Box):
+    """A widget which compromises one disc
+
+    DiscBox contains a disc label for the disc number on top
+    with a DiscSongsFlowBox beneath.
+    """
+    __gtype_name__ = 'DiscBox'
+
+    __gsignals__ = {
+        'selection-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
+        'selection-toggle': (GObject.SignalFlags.RUN_FIRST, None, ()),
+        'track-activated': (GObject.SignalFlags.RUN_FIRST, None, (Gtk.Widget,))
+
+    }
+
+    def __init__(self, model=None):
+        """Initialize
+
+        :param model: The TreeStore to use
+        """
+        super().__init__(self)
+
+        self._model = model
+        self._model.connect('row-changed', self._model_row_changed)
+
+        builder = Gtk.Builder()
+        builder.add_from_resource('/org/gnome/Music/ArtistAlbumWidget.ui')
+
+        self._label = builder.get_object('disclabel')
+        self._label.set_no_show_all(True)
+        self._disc_songs_flowbox = builder.get_object('discsongsflowbox')
+
+        self._selection_mode = False
+        self._selection_mode_allowed = True
+        self._selected_items = []
+        self._songs = []
+
+        self.pack_start(builder.get_object('disc'), True, True, 0)
+
+    def set_columns(self, columns):
+        """Set the number of columns used by the songs list
+
+        :param int columns: Number of columns to display
+        """
+        self._disc_songs_flowbox.set_columns(columns)
+
+    def set_disc_number(self, disc_number):
+        """Set the dics number to display
+
+        :param int disc_number: Disc number to display
+        """
+        self._label.set_markup(_("Disc {}").format(disc_number))
+        self._label.get_style_context().add_class('dim-label')
+        self._label.set_visible(True)
+
+    def show_disc_label(self, show_header):
+        """Wheter to show the disc number label
+
+        :param bool show_header: Display the disc number label
+        """
+        self._label.set_visible(False)
+        self._label.hide()
+
+    def show_duration(self, show_duration):
+        """Wheter to show the song durations
+
+        :param bool show_duration: Display the song durations
+        """
+        def child_show_duration(child):
+            child.get_child().duration.set_visible(show_duration)
+
+        self._disc_songs_flowbox.foreach(child_show_duration)
+
+    def show_favorites(self, show_favorites):
+        """Where to show the favorite switches
+
+        :param bool show_favorites: Display the favorite
+        switches
+        """
+        def child_show_favorites(child):
+            child.get_child().starevent.set_visible(show_favorites)
+
+        self._disc_songs_flowbox.foreach(child_show_favorites)
+
+    def show_song_numbers(self, show_song_number):
+        """Whether to show the song numbers
+
+        :param bool show_song_number: Display the song number
+        """
+        def child_show_song_number(child):
+            child.get_child().number.set_visible(show_song_number)
+
+        self._disc_songs_flowbox.foreach(child_show_song_number)
+
+    def set_tracks(self, tracks):
+        """Songs to display
+
+        :param list tracks: A list of Grilo media items to
+        add to the widget
+        """
+        for track in tracks:
+            song_widget = self._create_song_widget(track)
+            self._disc_songs_flowbox.insert(song_widget, -1)
+            track.song_widget = song_widget
+
+    def set_selection_mode(self, selection_mode):
+        """Set selection mode
+
+        :param bool selection_mode: Allow selection mode
+        """
+        self._selection_mode = selection_mode
+        self._disc_songs_flowbox.foreach(self._toggle_widget_selection)
+
+    def get_selected_items(self):
+        """Return all selected items
+
+        :returns: The selected items:
+        :rtype: A list if Grilo media items
+        """
+        self._selected_items = []
+        self._disc_songs_flowbox.foreach(self._get_selected)
+
+        return self._selected_items
+
+    def _get_selected(self, child):
+        song_widget = child.get_child()
+
+        if song_widget.check_button.get_active():
+            itr = song_widget.itr
+            self._selected_items.append(self._model[itr][5])
+
+    # FIXME: select all/none slow probably b/c of the row changes
+    # invocations, maybe workaround?
+    def select_all(self):
+        """Select all songs"""
+        def child_select_all(child):
+            song_widget = child.get_child()
+            self._model[song_widget.itr][6] = True
+
+        self._disc_songs_flowbox.foreach(child_select_all)
+
+    def select_none(self):
+        """Deselect all songs"""
+        def child_select_none(child):
+            song_widget = child.get_child()
+            self._model[song_widget.itr][6] = False
+
+        self._disc_songs_flowbox.foreach(child_select_none)
+
+    def _create_song_widget(self, track):
+        """Helper function to create a song widget for a
+        single song
+
+        :param track: A Grilo media item
+        :returns: A complete song widget
+        :rtype: Gtk.EventBox
+        """
+        builder = Gtk.Builder()
+        builder.add_from_resource('/org/gnome/Music/TrackWidget.ui')
+        song_widget = builder.get_object('eventbox1')
+        self._songs.append(song_widget)
+
+        title = utils.get_media_title(track)
+
+        itr = self._model.append(None)
+
+        self._model[itr][0, 1, 2, 5, 6] = [title, '', '', track, False]
+
+        song_widget.itr = itr
+        song_widget.model = self._model
+
+        song_widget.number = builder.get_object('num')
+        song_widget.number.set_markup(
+            '<span color=\'grey\'>{}</span>'.format(len(self._songs)))
+        song_widget.number.set_no_show_all(True)
+
+        song_widget.title = builder.get_object('title')
+        song_widget.title.set_text(title)
+        song_widget.title.set_max_width_chars(MAX_TITLE_WIDTH)
+
+        song_widget.duration = builder.get_object('duration')
+        time = utils.seconds_to_string(track.get_duration())
+        song_widget.duration.set_text(time)
+
+        song_widget.check_button = builder.get_object('select')
+        song_widget.check_button.set_visible(False)
+        song_widget.check_button.connect('toggled',
+                                         self._check_button_toggled,
+                                         song_widget)
+
+        song_widget.now_playing_sign = builder.get_object('image1')
+        song_widget.now_playing_sign.set_from_icon_name(
+            NOW_PLAYING_ICON_NAME,
+            Gtk.IconSize.SMALL_TOOLBAR)
+        song_widget.now_playing_sign.set_no_show_all(True)
+        song_widget.can_be_played = True
+        song_widget.connect('button-release-event', self._track_activated)
+
+        song_widget.star_stack = builder.get_object('starstack')
+        song_widget.star_stack.set_favorite(track.get_lyrics())
+        song_widget.star_stack.set_visible(True)
+
+        song_widget.starevent = builder.get_object('starevent')
+        song_widget.starevent.connect('button-release-event',
+                                      self._toggle_favorite,
+                                      song_widget)
+
+        return song_widget
+
+    def _toggle_favorite(self, widget, event, song_widget):
+        if event.button == Gdk.BUTTON_PRIMARY:
+            song_widget.star_stack.toggle_favorite()
+
+        # FIXME: ugleh. Should probably be triggered by a
+        # signal.
+        favorite = song_widget.star_stack.get_favorite()
+        grilo.set_favorite(self._model[song_widget.itr][5], favorite)
+        return True
+
+    def _check_button_toggled(self, widget, song_widget):
+        self.emit('selection-changed')
+
+        return True
+
+    def _toggle_widget_selection(self, child):
+        song_widget = child.get_child()
+        song_widget.check_button.set_visible(self._selection_mode)
+        if self._selection_mode == False:
+            if song_widget.check_button.get_active():
+                song_widget.check_button.set_active(False)
+
+    def _track_activated(self, widget, event):
+        # FIXME: don't think keys work correctly, if they did ever
+        # even.
+        if (not event.button == Gdk.BUTTON_SECONDARY
+                or (event.button == Gdk.BUTTON_PRIMARY
+                    and event.state & Gdk.ModifierType.CONTROL_MASK)):
+            self.emit('track-activated', widget)
+            if self._selection_mode:
+                itr = widget.itr
+                self._model[itr][6] = not self._model[itr][6]
+        else:
+            self.emit('selection-toggle')
+            if self._selection_mode:
+                itr = widget.itr
+                self._model[itr][6] = True
+
+        return True
+
+    @log
+    def _model_row_changed(self, model, path, itr):
+        if (not self._selection_mode
+                or not model[itr][5]):
+            return
+
+        song_widget = model[itr][5].song_widget
+        selected = model[itr][6]
+        if selected != song_widget.check_button.get_active():
+            song_widget.check_button.set_active(selected)
+
+        return True
+
+
+class DiscListBox(Gtk.ListBox):
+    """A ListBox widget containing all discs of a particular
+    album
+    """
+    __gtype_name__ = 'DiscListBox'
+
+    __gsignals__ = {
+        'selection-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
+    }
+
+    def __init__(self):
+        """Initialize"""
+        super().__init__()
+        super().set_selection_mode(Gtk.SelectionMode.NONE)
+
+        self._selection_mode = False
+        self._selection_mode_allowed = False
+        self._selected_items = []
+
+    def insert(self, widget, position):
+        """Insert a DiscBox widget"""
+        super().insert(widget, position)
+        widget.connect('selection-changed', self._on_selection_changed)
+
+    def _on_selection_changed(self, widget):
+        self.emit('selection-changed')
+
+    def get_selected_items(self):
+        """Returns all selected items for all discs
+
+        :returns: All selected items
+        :rtype: A list if Grilo media items
+        """
+        self._selected_items = []
+
+        def get_child_selected_items(child):
+            self._selected_items += child.get_child().get_selected_items()
+
+        self.foreach(get_child_selected_items)
+
+        return self._selected_items
+
+    def select_all(self):
+        """Select all songs"""
+        def child_select_all(child):
+            child.get_child().select_all()
+
+        self.foreach(child_select_all)
+
+    def select_none(self):
+        """Deselect all songs"""
+        def child_select_none(child):
+            child.get_child().select_none()
+
+        self.foreach(child_select_none)
+
+    def set_selection_mode(self, selection_mode):
+        """Set selection mode
+
+        :param bool selection_mode: Allow selection mode
+        """
+        if not self._selection_mode_allowed:
+            return
+
+        self._selection_mode = selection_mode
+
+        def set_child_selection_mode(child):
+            child.get_child().set_selection_mode(self._selection_mode)
+
+        self.foreach(set_child_selection_mode)
+
+    def set_selection_mode_allowed(self, allowed):
+        """Set if selection mode is allowed
+
+        :param bool allowed: Allow selection mode
+        """
+        self._selection_mode_allowed = allowed
diff --git a/gnomemusic/window.py b/gnomemusic/window.py
index 41b4b8c..3893b07 100644
--- a/gnomemusic/window.py
+++ b/gnomemusic/window.py
@@ -306,10 +306,10 @@ class Window(Gtk.ApplicationWindow):
     def _on_select_none(self, action, param):
         if self.toolbar._state == ToolbarState.MAIN:
             view = self._stack.get_visible_child()
+            view.unselect_all()
         else:
             view = self._stack.get_visible_child().get_visible_child()
-
-        view.unselect_all()
+            view.select_none()
 
     def _show_notification(self):
         self.notification_handler = None
diff --git a/po/POTFILES.in b/po/POTFILES.in
index ab49d21..3fc2324 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -28,6 +28,7 @@ gnomemusic/views/songsview.py
 gnomemusic/widgets/albumwidget.py
 gnomemusic/widgets/artistalbumswidget.py
 gnomemusic/widgets/artistalbumwidget.py
+gnomemusic/widgets/disclistboxwidget.py
 gnomemusic/widgets/playlistdialog.py
 gnomemusic/widgets/starhandlerwidget.py
 gnomemusic/window.py


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