1# Copyright 2019 The GNOME Music Developers 2# 3# GNOME Music is free software; you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published by 5# the Free Software Foundation; either version 2 of the License, or 6# (at your option) any later version. 7# 8# GNOME Music is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License along 14# with GNOME Music; if not, write to the Free Software Foundation, Inc., 15# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 16# 17# The GNOME Music authors hereby grant permission for non-GPL compatible 18# GStreamer plugins to be used and distributed together with GStreamer 19# and GNOME Music. This permission is above and beyond the permissions 20# granted by the GPL license by which GNOME Music is covered. If you 21# modify this code, you may extend this exception to your version of the 22# code, but you are not obligated to do so. If you do not wish to do so, 23# delete this exception statement from your version. 24 25import math 26 27from gettext import gettext as _ 28from gi.repository import Gdk, GLib, GObject, Gtk 29 30from gnomemusic.widgets.headerbar import HeaderBar 31from gnomemusic.widgets.albumcover import AlbumCover 32from gnomemusic.widgets.albumwidget import AlbumWidget 33 34 35@Gtk.Template(resource_path="/org/gnome/Music/ui/AlbumsView.ui") 36class AlbumsView(Gtk.Stack): 37 """Gridlike view of all albums 38 39 Album activation switches to AlbumWidget. 40 """ 41 42 __gtype_name__ = "AlbumsView" 43 44 icon_name = GObject.Property( 45 type=str, default="media-optical-cd-audio-symbolic", 46 flags=GObject.ParamFlags.READABLE) 47 search_mode_active = GObject.Property(type=bool, default=False) 48 selection_mode = GObject.Property(type=bool, default=False) 49 title = GObject.Property( 50 type=str, default=_("Albums"), flags=GObject.ParamFlags.READABLE) 51 52 _album_scrolled_window = Gtk.Template.Child() 53 _scrolled_window = Gtk.Template.Child() 54 _flowbox = Gtk.Template.Child() 55 _flowbox_long_press = Gtk.Template.Child() 56 57 def __init__(self, application): 58 """Initialize AlbumsView 59 60 :param application: The Application object 61 """ 62 super().__init__(transition_type=Gtk.StackTransitionType.CROSSFADE) 63 64 self.props.name = "albums" 65 66 self._application = application 67 self._window = application.props.window 68 self._headerbar = self._window._headerbar 69 self._adjustment_timeout_id = 0 70 self._viewport = self._scrolled_window.get_child() 71 self._widget_counter = 1 72 self._ctrl_hold = False 73 74 model = self._application.props.coremodel.props.albums_sort 75 self._flowbox.bind_model(model, self._create_widget) 76 self._flowbox.set_hadjustment(self._scrolled_window.get_hadjustment()) 77 self._flowbox.set_vadjustment(self._scrolled_window.get_vadjustment()) 78 self._flowbox.connect("child-activated", self._on_child_activated) 79 80 self.bind_property( 81 "selection-mode", self._window, "selection-mode", 82 GObject.BindingFlags.DEFAULT) 83 84 self._window.connect( 85 "notify::selection-mode", self._on_selection_mode_changed) 86 87 self._album_widget = AlbumWidget(self._application) 88 self._album_widget.bind_property( 89 "selection-mode", self, "selection-mode", 90 GObject.BindingFlags.BIDIRECTIONAL) 91 92 self._album_scrolled_window.add(self._album_widget) 93 94 self.connect( 95 "notify::search-mode-active", self._on_search_mode_changed) 96 97 self._scrolled_window.props.vadjustment.connect( 98 "value-changed", self._on_vadjustment_changed) 99 self._scrolled_window.props.vadjustment.connect( 100 "changed", self._on_vadjustment_changed) 101 102 def _on_vadjustment_changed(self, adjustment): 103 if self._adjustment_timeout_id != 0: 104 GLib.source_remove(self._adjustment_timeout_id) 105 self._adjustment_timeout_id = 0 106 107 self._adjustment_timeout_id = GLib.timeout_add( 108 200, self._retrieve_covers, adjustment.props.value, 109 priority=GLib.PRIORITY_DEFAULT - 10) 110 111 def _retrieve_covers(self, old_adjustment): 112 adjustment = self._scrolled_window.props.vadjustment.props.value 113 114 if old_adjustment != adjustment: 115 return GLib.SOURCE_CONTINUE 116 117 first_cover = self._flowbox.get_child_at_index(0) 118 if first_cover is None: 119 return GLib.SOURCE_REMOVE 120 121 cover_size, _ = first_cover.get_allocated_size() 122 if cover_size.width == 0 or cover_size.height == 0: 123 return GLib.SOURCE_REMOVE 124 125 viewport_size, _ = self._viewport.get_allocated_size() 126 127 h_space = self._flowbox.get_column_spacing() 128 v_space = self._flowbox.get_row_spacing() 129 nr_cols = ( 130 (viewport_size.width + h_space) // (cover_size.width + h_space)) 131 132 top_left_cover = self._flowbox.get_child_at_index( 133 nr_cols * (adjustment // (cover_size.height + v_space))) 134 135 covers_col = math.ceil(viewport_size.width / cover_size.width) 136 covers_row = math.ceil(viewport_size.height / cover_size.height) 137 138 children = self._flowbox.get_children() 139 retrieve_list = [] 140 for i, albumcover in enumerate(children): 141 if top_left_cover == albumcover: 142 retrieve_covers = covers_row * covers_col 143 retrieve_list = children[i:i + retrieve_covers] 144 break 145 146 for albumcover in retrieve_list: 147 albumcover.retrieve() 148 149 self._adjustment_timeout_id = 0 150 151 return GLib.SOURCE_REMOVE 152 153 def _on_selection_mode_changed(self, widget, data=None): 154 selection_mode = self._window.props.selection_mode 155 if (selection_mode == self.props.selection_mode 156 or self.get_parent().get_visible_child() != self): 157 return 158 159 self.props.selection_mode = selection_mode 160 if not self.props.selection_mode: 161 self.deselect_all() 162 self._flowbox.props.selection_mode = Gtk.SelectionMode.NONE 163 164 def _on_search_mode_changed(self, klass, param): 165 if (not self.props.search_mode_active 166 and self._headerbar.props.stack.props.visible_child == self 167 and self.get_visible_child() == self._album_widget): 168 self._set_album_headerbar(self._album_widget.props.corealbum) 169 170 def _create_widget(self, corealbum): 171 album_widget = AlbumCover(corealbum) 172 173 self.bind_property( 174 "selection-mode", album_widget, "selection-mode", 175 GObject.BindingFlags.SYNC_CREATE 176 | GObject.BindingFlags.BIDIRECTIONAL) 177 178 # NOTE: Adding SYNC_CREATE here will trigger all the nested 179 # models to be created. This will slow down initial start, 180 # but will improve initial 'select all' speed. 181 album_widget.bind_property( 182 "selected", corealbum, "selected", 183 GObject.BindingFlags.BIDIRECTIONAL) 184 185 GLib.timeout_add( 186 self._widget_counter * 250, album_widget.retrieve, 187 priority=GLib.PRIORITY_LOW) 188 self._widget_counter = self._widget_counter + 1 189 190 return album_widget 191 192 def _back_button_clicked(self, widget, data=None): 193 self._headerbar.state = HeaderBar.State.MAIN 194 self.props.visible_child = self._scrolled_window 195 196 def _on_child_activated(self, widget, child, user_data=None): 197 corealbum = child.props.corealbum 198 if self.props.selection_mode: 199 return 200 201 # Update and display the album widget if not in selection mode 202 self._album_widget.props.corealbum = corealbum 203 204 self._set_album_headerbar(corealbum) 205 self.set_visible_child(self._album_scrolled_window) 206 207 def _set_album_headerbar(self, corealbum): 208 self._headerbar.props.state = HeaderBar.State.CHILD 209 self._headerbar.props.title = corealbum.props.title 210 self._headerbar.props.subtitle = corealbum.props.artist 211 212 @Gtk.Template.Callback() 213 def _on_flowbox_press_begin(self, gesture, sequence): 214 event = gesture.get_last_event(sequence) 215 ok, state = event.get_state() 216 if ((ok is True 217 and state == Gdk.ModifierType.CONTROL_MASK) 218 or self.props.selection_mode is True): 219 self._flowbox.props.selection_mode = Gtk.SelectionMode.MULTIPLE 220 if state == Gdk.ModifierType.CONTROL_MASK: 221 self._ctrl_hold = True 222 223 @Gtk.Template.Callback() 224 def _on_flowbox_press_cancel(self, gesture, sequence): 225 self._flowbox.props.selection_mode = Gtk.SelectionMode.NONE 226 227 @Gtk.Template.Callback() 228 def _on_selected_children_changed(self, flowbox): 229 if self._flowbox.props.selection_mode == Gtk.SelectionMode.NONE: 230 return 231 232 if self.props.selection_mode is False: 233 self.props.selection_mode = True 234 235 rubberband_selection = len(self._flowbox.get_selected_children()) > 1 236 with self._application.props.coreselection.freeze_notify(): 237 if (rubberband_selection 238 and not self._ctrl_hold): 239 self.deselect_all() 240 for child in self._flowbox.get_selected_children(): 241 if (self._ctrl_hold is True 242 or not rubberband_selection): 243 child.props.selected = not child.props.selected 244 else: 245 child.props.selected = True 246 247 self._ctrl_hold = False 248 self._flowbox.props.selection_mode = Gtk.SelectionMode.NONE 249 250 def _toggle_all_selection(self, selected): 251 """Selects or deselects all items. 252 """ 253 with self._application.props.coreselection.freeze_notify(): 254 if self.get_visible_child() == self._album_widget: 255 if selected is True: 256 self._album_widget.select_all() 257 else: 258 self._album_widget.deselect_all() 259 else: 260 for child in self._flowbox.get_children(): 261 child.props.selected = selected 262 263 def select_all(self): 264 self._toggle_all_selection(True) 265 266 def deselect_all(self): 267 self._toggle_all_selection(False) 268