[gnome-music] albums: Use a GtkFlowBox internally



commit f659a3327ee660fa92128f9f0cb83151c6416ff6
Author: Georges Basile Stavracas Neto <georges stavracas gmail com>
Date:   Tue Aug 9 02:58:30 2016 -0300

    albums: Use a GtkFlowBox internally
    
    Replace Albums view GdMainView with GtkFlowBox.
    Also introduce a custom selection mode, instead of using the GtkFlowBox
    one. Requested by the designers to keep in line with the music designs.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=760164

 data/AlbumCover.ui             |   72 ++++++++++++++
 data/gnome-music.gresource.xml |    1 +
 gnomemusic/toolbar.py          |    8 --
 gnomemusic/view.py             |  214 ++++++++++++++++++++++++++++++++++++----
 gnomemusic/window.py           |   39 ++------
 5 files changed, 275 insertions(+), 59 deletions(-)
---
diff --git a/data/AlbumCover.ui b/data/AlbumCover.ui
new file mode 100644
index 0000000..4309a3d
--- /dev/null
+++ b/data/AlbumCover.ui
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.18"/>
+  <object class="GtkBox" id="main_box">
+    <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">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="margin_bottom">4</property>
+        <child>
+          <object class="GtkEventBox" id="events">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <child>
+              <object class="GtkImage" id="image">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="vexpand">True</property>
+                <property name="valign">end</property>
+                <property name="halign">center</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child type="overlay">
+          <object class="GtkCheckButton" id="check">
+            <property name="can_focus">True</property>
+            <property name="receives_default">False</property>
+            <property name="halign">end</property>
+            <property name="valign">end</property>
+            <property name="draw_indicator">True</property>
+            <style>
+              <class name="osd"/>
+              <class name="content-view"/>
+            </style>
+          </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>
+    <child>
+      <object class="GtkLabel" id="subtitle">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="ellipsize">middle</property>
+        <property name="max_width_chars">20</property>
+        <property name="tooltip_text" bind-source="subtitle" bind-property="label" bind-flags="default" />
+        <attributes>
+          <attribute name="scale" value="0.88"/>
+        </attributes>
+        <style>
+          <class name="dim-label"/>
+        </style>
+      </object>
+    </child>
+  </object>
+</interface>
diff --git a/data/gnome-music.gresource.xml b/data/gnome-music.gresource.xml
index 64c0a73..709058b 100644
--- a/data/gnome-music.gresource.xml
+++ b/data/gnome-music.gresource.xml
@@ -6,6 +6,7 @@
     <file alias="gtk/help-overlay.ui" preprocess="xml-stripblanks">help-overlay.ui</file>
     <file>application.css</file>
     <file>initial-state.png</file>
+    <file preprocess="xml-stripblanks">AlbumCover.ui</file>
     <file preprocess="xml-stripblanks">AlbumWidget.ui</file>
     <file preprocess="xml-stripblanks">ArtistAlbumWidget.ui</file>
     <file preprocess="xml-stripblanks">ArtistAlbumsWidget.ui</file>
diff --git a/gnomemusic/toolbar.py b/gnomemusic/toolbar.py
index 94fa13e..85eb999 100644
--- a/gnomemusic/toolbar.py
+++ b/gnomemusic/toolbar.py
@@ -108,25 +108,17 @@ class Toolbar(GObject.GObject):
 
     @log
     def set_selection_mode(self, selectionMode):
-        self._selection_handler = None
         self._selectionMode = selectionMode
         if selectionMode:
             self._select_button.hide()
             self._cancel_button.show()
             self.header_bar.get_style_context().add_class('selection-mode')
             self._cancel_button.get_style_context().remove_class('selection-mode')
-            view = self._stack_switcher.get_stack().get_visible_child()
-            self._selection_handler = view.view.connect(
-                'view-selection-changed', view._on_view_selection_changed)
         else:
             self.header_bar.get_style_context().remove_class('selection-mode')
             self._select_button.set_active(False)
             self._select_button.show()
             self._cancel_button.hide()
-            view = self._stack_switcher.get_stack().get_visible_child()
-            if self._selection_handler:
-                view.view.disconnect(self._selection_handler)
-            self._selection_handler = None
         self.emit('selection-mode-changed')
         self._update()
 
diff --git a/gnomemusic/view.py b/gnomemusic/view.py
index d9616d1..d824480 100644
--- a/gnomemusic/view.py
+++ b/gnomemusic/view.py
@@ -60,6 +60,8 @@ class ViewContainer(Gtk.Stack):
     nowPlayingIconName = 'media-playback-start-symbolic'
     errorIconName = 'dialog-error-symbolic'
 
+    selection_mode = GObject.Property(type=bool, default=False)
+
     def __repr__(self):
         return '<ViewContainer>'
 
@@ -153,22 +155,28 @@ class ViewContainer(Gtk.Stack):
         self.view.click_handler = self.view.connect('item-activated', self._on_item_activated)
         self.view.connect('selection-mode-request', self._on_selection_mode_request)
 
+        self.view.bind_property('selection-mode', self, 'selection_mode',
+                                GObject.BindingFlags.BIDIRECTIONAL)
+
+        self.view.connect('view-selection-changed', self._on_view_selection_changed)
+
         self._box.pack_start(self.view, True, True, 0)
 
     @log
     def _on_header_bar_toggled(self, button):
-        if button.get_active():
-            self.view.set_selection_mode(True)
+        self.selection_mode = button.get_active()
+
+        if self.selection_mode:
             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.selection_toolbar._remove_from_playlist_button.set_sensitive(False)
         else:
-            self.view.set_selection_mode(False)
             self.header_bar.set_selection_mode(False)
             self.player.actionbar.set_visible(self.player.currentTrack is not None)
             self.selection_toolbar.actionbar.set_visible(False)
+            self.unselect_all()
 
     @log
     def _on_cancel_button_clicked(self, button):
@@ -189,14 +197,22 @@ class ViewContainer(Gtk.Stack):
 
     @log
     def _on_view_selection_changed(self, widget):
+
+        if not self.selection_mode:
+            return
+
         items = self.view.get_selection()
+        self.update_header_from_selection(len(items))
+
+    @log
+    def update_header_from_selection(self, n_items):
         self.selection_toolbar._add_to_playlist_button.\
-            set_sensitive(len(items) > 0)
+            set_sensitive(n_items > 0)
         self.selection_toolbar._remove_from_playlist_button.\
-            set_sensitive(len(items) > 0)
-        if len(items) > 0:
+            set_sensitive(n_items > 0)
+        if n_items > 0:
             self.header_bar._selection_menu_label.set_text(
-                ngettext("Selected %d item", "Selected %d items", len(items)) % len(items))
+                ngettext("Selected %d item", "Selected %d items", n_items) % n_items)
         else:
             self.header_bar._selection_menu_label.set_text(_("Click on items to select them"))
 
@@ -261,6 +277,39 @@ class ViewContainer(Gtk.Stack):
     def _on_list_widget_star_render(self, col, cell, model, _iter, data):
         pass
 
+    @log
+    def _set_selection(self, value, parent=None):
+        count = 0
+        _iter = self.model.iter_children(parent)
+        while _iter is not None:
+            if self.model.iter_has_child(_iter):
+                count += self._set_selection(value, _iter)
+            if self.model[_iter][5]:
+                self.model[_iter][6] = value
+                count += 1
+            _iter = self.model.iter_next(_iter)
+        return count
+
+    @log
+    def select_all(self):
+        """Select all the available tracks."""
+        count = self._set_selection(True)
+
+        if count > 0:
+            self.selection_toolbar._add_to_playlist_button.set_sensitive(True)
+            self.selection_toolbar._remove_from_playlist_button.set_sensitive(True)
+
+        self.update_header_from_selection(count)
+        self.view.queue_draw()
+
+    @log
+    def unselect_all(self):
+        """Unselects all the selected tracks."""
+        self._set_selection(False)
+        self.selection_toolbar._add_to_playlist_button.set_sensitive(False)
+        self.selection_toolbar._remove_from_playlist_button.set_sensitive(False)
+        self.header_bar._selection_menu_label.set_text(_("Click on items to select them"))
+        self.queue_draw()
 
 # Class for the Empty View
 class Empty(Gtk.Stack):
@@ -316,18 +365,19 @@ class Albums(ViewContainer):
 
     @log
     def __init__(self, window, player):
-        ViewContainer.__init__(self, 'albums', _("Albums"), window, Gd.MainViewType.ICON)
+        ViewContainer.__init__(self, 'albums', _("Albums"), window, None)
         self._albumWidget = Widgets.AlbumWidget(player, self)
         self.player = player
         self.add(self._albumWidget)
         self.albums_selected = []
+        self.all_items = []
         self.items_selected = []
         self.items_selected_callback = None
         self._add_list_renderers()
 
     @log
     def _on_changes_pending(self, data=None):
-        if (self._init and self.header_bar._selectionMode is False):
+        if (self._init and not self.header_bar._selectionMode):
             self._offset = 0
             self._init = True
             GLib.idle_add(self.populate)
@@ -335,31 +385,56 @@ class Albums(ViewContainer):
 
     @log
     def _on_selection_mode_changed(self, widget, data=None):
-        if self.header_bar._selectionMode is False and grilo.changes_pending['Albums'] is True:
+        if (not self.header_bar._selectionMode
+                and grilo.changes_pending['Albums']):
             self._on_changes_pending()
 
     @log
+    def _setup_view(self, view_type):
+        self.view = Gtk.FlowBox(homogeneous=True,
+                                hexpand=True,
+                                halign=Gtk.Align.FILL,
+                                selection_mode=Gtk.SelectionMode.NONE,
+                                margin=18,
+                                row_spacing=12,
+                                column_spacing=6,
+                                min_children_per_line=1,
+                                max_children_per_line=25)
+
+        self.view.connect('child-activated', self._on_child_activated)
+
+        scrolledwin = Gtk.ScrolledWindow()
+        scrolledwin.add(self.view)
+        scrolledwin.show()
+
+        self._box.add(scrolledwin)
+
+    @log
     def _back_button_clicked(self, widget, data=None):
         self.header_bar.reset_header_title()
         self.set_visible_child(self._grid)
 
     @log
-    def _on_item_activated(self, widget, id, path):
+    def _on_child_activated(self, widget, child, user_data=None):
+        item = child.media_item
+
         if self.star_handler.star_renderer_click:
             self.star_handler.star_renderer_click = False
             return
 
-        try:
-            _iter = self.model.get_iter(path)
-        except TypeError:
+        # Toggle the selection when in selection mode
+        if self.selection_mode:
+            child.check.set_active(not child.check.get_active())
             return
-        title = self.model.get_value(_iter, 2)
-        self._artist = self.model.get_value(_iter, 3)
-        item = self.model.get_value(_iter, 5)
+
+        title = AlbumArtCache.get_media_title(item)
+        self._escaped_title = title
+        self._artist = utils.get_artist_name(item)
+
         self._albumWidget.update(self._artist, title, item,
                                  self.header_bar, self.selection_toolbar)
+
         self.header_bar.set_state(ToolbarState.CHILD_VIEW)
-        self._escaped_title = AlbumArtCache.get_media_title(item)
         self.header_bar.header_bar.set_title(self._escaped_title)
         self.header_bar.header_bar.sub_title = self._artist
         self.set_visible_child(self._albumWidget)
@@ -387,12 +462,86 @@ class Albums(ViewContainer):
             self.items_selected = []
             self.items_selected_callback = callback
             self.albums_index = 0
-            self.albums_selected = [self.model.get_value(self.model.get_iter(path), 5)
-                                    for path in self.view.get_selection()]
             if len(self.albums_selected):
                 self._get_selected_album_songs()
 
     @log
+    def _add_item(self, source, param, item, remaining=0, data=None):
+        self.window.notification.set_timeout(0)
+
+        if item:
+            # Store all items to optimize 'Select All' action
+            self.all_items.append(item)
+
+            # Add to the flowbox
+            child = self._create_album_item(item)
+            self.view.add(child)
+        elif remaining == 0:
+                self.window.notification.dismiss()
+                self.view.show()
+
+    def _create_album_item(self, item):
+        artist = utils.get_artist_name(item)
+        title = AlbumArtCache.get_media_title(item)
+
+        builder = Gtk.Builder.new_from_resource('/org/gnome/Music/AlbumCover.ui')
+
+        child = Gtk.FlowBoxChild()
+        child.image = builder.get_object('image')
+        child.check = builder.get_object('check')
+        child.title = builder.get_object('title')
+        child.subtitle = builder.get_object('subtitle')
+        child.events = builder.get_object('events')
+        child.media_item = item
+
+        child.title.set_label(title)
+        child.subtitle.set_label(artist)
+        child.image.set_from_pixbuf(self._loading_icon)
+        # In the case of off-sized icons (eg. provided in the soundfile)
+        # keep the size request equal to all other icons to get proper
+        # alignment with GtkFlowBox.
+        child.image.set_property("width-request", self._iconWidth)
+        child.image.set_property("height-request", self._iconHeight)
+
+        child.events.connect('button-release-event',
+                             self._on_album_event_triggered,
+                             child)
+
+        child.check_handler_id = child.check.connect('notify::active',
+                                                     self._on_child_toggled,
+                                                     child)
+
+        child.check.bind_property('visible', self, 'selection_mode',
+                                  GObject.BindingFlags.BIDIRECTIONAL)
+
+        child.add(builder.get_object('main_box'))
+        child.show()
+
+        self.cache.lookup(item, self._iconWidth, self._iconHeight, self._on_lookup_ready,
+                          child, artist, title)
+
+        return child
+
+    @log
+    def _on_album_event_triggered(self, evbox, event, child):
+        if event.button is 3:
+            self._on_selection_mode_request()
+            if self.selection_mode:
+                child.check.set_active(True)
+
+    def _on_lookup_ready(self, icon, path, child):
+        child.image.set_from_pixbuf(icon)
+
+    @log
+    def _on_child_toggled(self, check, pspec, child):
+        if check.get_active() and not child.media_item in self.albums_selected:
+            self.albums_selected.append(child.media_item)
+        elif not check.get_active() and child.media_item in self.albums_selected:
+            self.albums_selected.remove(child.media_item)
+
+        self.update_header_from_selection(len(self.albums_selected))
+
+    @log
     def _get_selected_album_songs(self):
         grilo.populate_album_songs(
             self.albums_selected[self.albums_index],
@@ -409,6 +558,31 @@ class Albums(ViewContainer):
             else:
                 self.items_selected_callback(self.items_selected)
 
+    def _toggle_all_selection(self, selected):
+        """
+        Selects or unselects all items without sending the notify::active
+        signal for performance purposes.
+        """
+        for child in self.view.get_children():
+            GObject.signal_handler_block(child.check, child.check_handler_id)
+
+            # Set the checkbutton state without emiting the signal
+            child.check.set_active(selected)
+
+            GObject.signal_handler_unblock(child.check, child.check_handler_id)
+
+        self.update_header_from_selection(len(self.albums_selected))
+
+    @log
+    def select_all(self):
+        self.albums_selected = list(self.all_items)
+        self._toggle_all_selection(True)
+
+    @log
+    def unselect_all(self):
+        self.albums_selected = []
+        self._toggle_all_selection(False)
+
 
 class Songs(ViewContainer):
 
diff --git a/gnomemusic/window.py b/gnomemusic/window.py
index fb1bb07..a619423 100644
--- a/gnomemusic/window.py
+++ b/gnomemusic/window.py
@@ -284,47 +284,24 @@ class Window(Gtk.ApplicationWindow):
         self.views[0].populate()
 
     @log
-    def _set_selection(self, model, value, parent=None):
-        count = 0
-        _iter = model.iter_children(parent)
-        while _iter is not None:
-            if model.iter_has_child(_iter):
-                count += self._set_selection(model, value, _iter)
-            if model[_iter][5]:
-                model.set(_iter, [6], [value])
-                count += 1
-            _iter = model.iter_next(_iter)
-        return count
-
-    @log
     def _on_select_all(self, action, param):
         if self.toolbar._selectionMode is False:
             return
         if self.toolbar._state == ToolbarState.MAIN:
-            model = self._stack.get_visible_child().model
+            view = self._stack.get_visible_child()
         else:
-            model = self._stack.get_visible_child().get_visible_child().model
-        count = self._set_selection(model, True)
-        if count > 0:
-            self.toolbar._selection_menu_label.set_text(
-                ngettext("Selected %d item", "Selected %d items", count) % count)
-            self.selection_toolbar._add_to_playlist_button.set_sensitive(True)
-            self.selection_toolbar._remove_from_playlist_button.set_sensitive(True)
-        elif count == 0:
-            self.toolbar._selection_menu_label.set_text(_("Click on items to select them"))
-        self._stack.get_visible_child().queue_draw()
+            view = self._stack.get_visible_child().get_visible_child()
+
+        view.select_all()
 
     @log
     def _on_select_none(self, action, param):
         if self.toolbar._state == ToolbarState.MAIN:
-            model = self._stack.get_visible_child().model
+            view = self._stack.get_visible_child()
         else:
-            model = self._stack.get_visible_child().get_visible_child().model
-        self._set_selection(model, False)
-        self.selection_toolbar._add_to_playlist_button.set_sensitive(False)
-        self.selection_toolbar._remove_from_playlist_button.set_sensitive(False)
-        self.toolbar._selection_menu_label.set_text(_("Click on items to select them"))
-        self._stack.get_visible_child().queue_draw()
+            view = self._stack.get_visible_child().get_visible_child()
+
+        view.unselect_all()
 
     def _show_notification(self):
         self.notification_handler = None


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