1# Copyright (c) 2014-2021 Cedric Bellegarde <cedric.bellegarde@adishatz.org> 2# This program is free software: you can redistribute it and/or modify 3# it under the terms of the GNU General Public License as published by 4# the Free Software Foundation, either version 3 of the License, or 5# (at your option) any later version. 6# This program is distributed in the hope that it will be useful, 7# but WITHOUT ANY WARRANTY; without even the implied warranty of 8# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9# GNU General Public License for more details. 10# You should have received a copy of the GNU General Public License 11# along with this program. If not, see <http://www.gnu.org/licenses/>. 12 13from gi.repository import Gtk 14 15from lollypop.utils import popup_widget 16from lollypop.view_lazyloading import LazyLoadingView 17from lollypop.define import App, ViewType, MARGIN, StorageType 18from lollypop.widgets_row_album import AlbumRow 19from lollypop.widgets_listbox import ListBox 20from lollypop.helper_gestures import GesturesHelper 21from lollypop.helper_signals import SignalsHelper, signals_map 22 23 24class AlbumsListView(LazyLoadingView, SignalsHelper, GesturesHelper): 25 """ 26 View showing albums 27 """ 28 29 @signals_map 30 def __init__(self, genre_ids, artist_ids, view_type): 31 """ 32 Init widget 33 @param genre_ids as int 34 @param artist_ids as int 35 @param view_type as ViewType 36 """ 37 LazyLoadingView.__init__(self, StorageType.ALL, view_type) 38 self.__genre_ids = genre_ids 39 self.__artist_ids = artist_ids 40 self.__reveals = [] 41 # Calculate default album height based on current pango context 42 # We may need to listen to screen changes 43 self.__height = AlbumRow.get_best_height(self) 44 self._box = ListBox() 45 self._box.set_margin_bottom(MARGIN) 46 self._box.set_margin_end(MARGIN) 47 self._box.get_style_context().add_class("trackswidget") 48 self._box.set_vexpand(True) 49 self._box.set_selection_mode(Gtk.SelectionMode.NONE) 50 self._box.show() 51 GesturesHelper.__init__(self, self._box) 52 if view_type & ViewType.DND: 53 from lollypop.helper_dnd import DNDHelper 54 self.__dnd_helper = DNDHelper(self._box, view_type) 55 self.__dnd_helper.connect("dnd-insert", self.__on_dnd_insert) 56 return [ 57 (App().player, "current-changed", "_on_current_changed"), 58 (App().album_art, "album-artwork-changed", "_on_artwork_changed") 59 ] 60 61 def add_reveal_albums(self, albums): 62 """ 63 Add albums to reveal list 64 @param albums as [Album] 65 """ 66 self.__reveals += list(albums) 67 68 def add_value(self, album): 69 """ 70 Insert item 71 @param album as Album 72 """ 73 # Merge album if previous is same 74 if self.children and self.children[-1].album.id == album.id: 75 track_ids = self.children[-1].album.track_ids 76 for track in album.tracks: 77 if track.id not in track_ids: 78 self.children[-1].tracks_view.append_row(track) 79 else: 80 LazyLoadingView.populate(self, [album]) 81 82 def insert_row(self, row, index): 83 """ 84 Insert row at index 85 @param row as AlbumRow 86 @param index as int 87 """ 88 row.connect("activated", self._on_row_activated) 89 row.connect("destroy", self._on_row_destroy) 90 row.connect("track-removed", self._on_track_removed) 91 row.show() 92 self._box.insert(row, index) 93 94 def populate(self, albums): 95 """ 96 Populate widget with album rows 97 @param albums as [Album] 98 """ 99 for child in self._box.get_children(): 100 self._box.remove(child) 101 LazyLoadingView.populate(self, albums) 102 103 def clear(self): 104 """ 105 Clear the view 106 """ 107 self.stop() 108 self.__reveals = [] 109 for child in self._box.get_children(): 110 self._box.remove(child) 111 112 def jump_to_current(self): 113 """ 114 Scroll to album 115 """ 116 y = self.__get_current_ordinate() 117 if y is not None: 118 self.scrolled.get_vadjustment().set_value(y) 119 120 def destroy(self): 121 """ 122 Force destroying the box 123 Help freeing memory, no idea why 124 """ 125 self._box.destroy() 126 LazyLoadingView.destroy(self) 127 128 @property 129 def args(self): 130 """ 131 Get default args for __class__ 132 @return {} 133 """ 134 return {"genre_ids": self.__genre_ids, 135 "artist_ids": self.__artist_ids, 136 "view_type": self.view_type & ~ViewType.SMALL} 137 138 @property 139 def dnd_helper(self): 140 """ 141 Get Drag & Drop helper 142 @return DNDHelper 143 """ 144 return self.__dnd_helper 145 146 @property 147 def box(self): 148 """ 149 Get album list box 150 @return Gtk.ListBox 151 """ 152 return self._box 153 154 @property 155 def children(self): 156 """ 157 Get view children 158 @return [AlbumRow] 159 """ 160 return self._box.get_children() 161 162####################### 163# PROTECTED # 164####################### 165 def _get_child(self, album): 166 """ 167 Get an album view widget 168 @param album as Album 169 @return AlbumRow 170 """ 171 if self.destroyed: 172 return None 173 row = AlbumRow(album, self.__height, self.view_type) 174 row.connect("activated", self._on_row_activated) 175 row.connect("destroy", self._on_row_destroy) 176 row.connect("track-removed", self._on_track_removed) 177 row.show() 178 self._box.add(row) 179 return row 180 181 def _on_current_changed(self, player): 182 """ 183 Update children state 184 @param player as Player 185 """ 186 for child in self._box.get_children(): 187 child.set_selection() 188 189 def _on_artwork_changed(self, artwork, album_id): 190 """ 191 Update children artwork if matching album id 192 @param artwork as Artwork 193 @param album_id as int 194 """ 195 for child in self._box.get_children(): 196 if child.album.id == album_id: 197 child.set_artwork() 198 199 def _on_primary_long_press_gesture(self, x, y): 200 """ 201 Show row menu 202 @param x as int 203 @param y as int 204 """ 205 self.__popup_menu(x, y) 206 207 def _on_primary_press_gesture(self, x, y, event): 208 """ 209 Activate current row 210 @param x as int 211 @param y as int 212 @param event as Gdk.Event 213 """ 214 row = self._box.get_row_at_y(y) 215 if row is None: 216 return 217 self._box.set_selection_mode(Gtk.SelectionMode.NONE) 218 row.reveal() 219 220 def _on_secondary_press_gesture(self, x, y, event): 221 """ 222 Show row menu 223 @param x as int 224 @param y as int 225 @param event as Gdk.Event 226 """ 227 self._on_primary_long_press_gesture(x, y) 228 229 def _on_populated(self, widget): 230 """ 231 Add another album/disc 232 @param widget as AlbumWidget/TracksView 233 """ 234 if widget.album in self.__reveals: 235 widget.reveal() 236 self.__reveals.remove(widget.album) 237 else: 238 LazyLoadingView._on_populated(self, widget) 239 240 def _on_row_activated(self, row, track): 241 pass 242 243 def _on_row_destroy(self, row): 244 pass 245 246 def _on_track_removed(self, row, track): 247 pass 248 249####################### 250# PRIVATE # 251####################### 252 def __get_current_ordinate(self): 253 """ 254 If current track in widget, return it ordinate, 255 @return y as int 256 """ 257 y = None 258 for child in self._box.get_children(): 259 if child.album == App().player.current_track.album: 260 child.reveal(True) 261 y = child.translate_coordinates(self._box, 0, 0)[1] 262 return y 263 264 def __popup_menu(self, x, y): 265 """ 266 Popup menu for album 267 @param x as int 268 @param y as int 269 """ 270 row = self._box.get_row_at_y(y) 271 if row is None: 272 return 273 # First check it's not a track gesture 274 if row.revealed: 275 for track_row in row.listbox.get_children(): 276 coordinates = track_row.translate_coordinates(self._box, 0, 0) 277 if coordinates is not None and\ 278 coordinates[1] < y and\ 279 coordinates[1] + track_row.get_allocated_height() > y: 280 track_row.popup_menu(self._box, x, y) 281 return 282 # Then handle album gesture 283 from lollypop.menu_objects import AlbumMenu 284 from lollypop.widgets_menu import MenuBuilder 285 menu = AlbumMenu(row.album, ViewType.ALBUM, self.view_type) 286 menu_widget = MenuBuilder(menu) 287 menu_widget.show() 288 popup_widget(menu_widget, self._box, x, y, row) 289 290 def __reveal_row(self, row): 291 """ 292 Reveal row if style always present 293 """ 294 style_context = row.get_style_context() 295 if style_context.has_class("drag-down"): 296 row.reveal(True) 297 298 def __on_dnd_insert(self, dnd_helper, row, index): 299 """ 300 Insert row at index 301 @param dnd_helper as DNDHelper 302 @param row as AlbumRow 303 @param index as int 304 """ 305 self.insert_row(row, index) 306