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, Gio, GLib, GObject, Pango 14 15from gettext import gettext as _ 16 17from lollypop.view_tracks_album import AlbumTracksView 18from lollypop.define import ArtSize, App, ViewType, MARGIN_SMALL 19from lollypop.define import ArtBehaviour 20from lollypop.utils import emit_signal 21 22 23class AlbumRow(Gtk.ListBoxRow): 24 """ 25 Album row 26 """ 27 28 __gsignals__ = { 29 "activated": (GObject.SignalFlags.RUN_FIRST, 30 None, (GObject.TYPE_PYOBJECT,)), 31 "populated": (GObject.SignalFlags.RUN_FIRST, None, ()), 32 "track-removed": (GObject.SignalFlags.RUN_FIRST, None, 33 (GObject.TYPE_PYOBJECT,)), 34 } 35 36 def get_best_height(widget): 37 """ 38 Helper to pass object it's height request 39 @param widget as Gtk.Widget 40 """ 41 ctx = widget.get_pango_context() 42 layout = Pango.Layout.new(ctx) 43 layout.set_text("a", 1) 44 font_height = int(MARGIN_SMALL * 2 + 45 2 * layout.get_pixel_size()[1]) 46 cover_height = MARGIN_SMALL * 2 + ArtSize.SMALL 47 # Don't understand what is this magic value 48 # May not work properly without Adwaita 49 if font_height > cover_height: 50 return font_height + 4 51 else: 52 return cover_height + 4 53 54 def __init__(self, album, height, view_type): 55 """ 56 Init row widgets 57 @param album as Album 58 @param height as int 59 @param view_type as ViewType 60 @param parent as AlbumListView 61 """ 62 Gtk.ListBoxRow.__init__(self) 63 self.__view_type = view_type 64 self.__revealer = None 65 self.__artwork = None 66 self.__gesture_list = None 67 self.__album = album 68 self.__cancellable = Gio.Cancellable() 69 self.set_sensitive(False) 70 context_style = self.get_style_context() 71 context_style.add_class("albumrow") 72 context_style.add_class("albumrow-collapsed") 73 self.set_property("height-request", height) 74 self.connect("destroy", self.__on_destroy) 75 self.__tracks_view = self.__get_new_tracks_view() 76 77 def populate(self): 78 """ 79 Populate widget content 80 """ 81 if self.__artwork is not None: 82 self.emit("populated") 83 return 84 self.__artwork = Gtk.Image.new() 85 App().art_helper.set_frame(self.__artwork, "small-cover-frame", 86 ArtSize.SMALL, ArtSize.SMALL) 87 self.set_sensitive(True) 88 # Issue with gtk3-3.24.24, tooltip is flashing when in 89 # information popover 90 self.set_property("has-tooltip", 91 self.get_ancestor(Gtk.Popover) is None) 92 self.connect("query-tooltip", self.__on_query_tooltip) 93 if self.__album.artists: 94 artists = GLib.markup_escape_text(", ".join(self.__album.artists)) 95 else: 96 artists = _("Compilation") 97 self.__artist_label = Gtk.Label.new("<b>%s</b>" % artists) 98 self.__artist_label.set_use_markup(True) 99 self.__artist_label.set_property("halign", Gtk.Align.START) 100 self.__artist_label.set_hexpand(True) 101 self.__artist_label.set_ellipsize(Pango.EllipsizeMode.END) 102 self.__title_label = Gtk.Label.new(self.__album.name) 103 self.__title_label.set_ellipsize(Pango.EllipsizeMode.END) 104 self.__title_label.set_property("halign", Gtk.Align.START) 105 self.__title_label.get_style_context().add_class("dim-label") 106 if self.__view_type & (ViewType.PLAYBACK | ViewType.PLAYLISTS): 107 button = Gtk.Button.new_from_icon_name("list-remove-symbolic", 108 Gtk.IconSize.BUTTON) 109 if self.__view_type & ViewType.PLAYBACK: 110 button.set_tooltip_text(_("Remove from playback")) 111 else: 112 button.set_tooltip_text(_("Remove from playlist")) 113 button.connect("clicked", self.__on_remove_clicked) 114 else: 115 button = Gtk.Button.new_from_icon_name( 116 "media-playback-start-symbolic", 117 Gtk.IconSize.MENU) 118 button.set_tooltip_text(_("Play this album")) 119 button.connect("clicked", self.__on_play_clicked) 120 button.set_relief(Gtk.ReliefStyle.NONE) 121 button.get_style_context().add_class("menu-button") 122 button.set_property("valign", Gtk.Align.CENTER) 123 header = Gtk.Grid.new() 124 header.set_column_spacing(MARGIN_SMALL) 125 header.show() 126 header.set_margin_start(MARGIN_SMALL) 127 # This to align button with row button 128 header.set_margin_end(MARGIN_SMALL * 2 + 3) 129 header.set_margin_top(2) 130 header.set_margin_bottom(2) 131 header.attach(self.__artwork, 0, 0, 1, 2) 132 header.attach(self.__artist_label, 1, 0, 1, 1) 133 header.attach(self.__title_label, 1, 1, 1, 1) 134 header.attach(button, 2, 0, 1, 2) 135 136 self.__revealer = Gtk.Revealer.new() 137 self.__revealer.show() 138 self.__revealer.add(self.__tracks_view) 139 140 box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) 141 box.pack_start(header, 0, True, True) 142 box.pack_start(self.__revealer, 1, False, False) 143 self.add(box) 144 self.set_artwork() 145 self.set_selection() 146 147 def reveal(self, reveal=None): 148 """ 149 Reveal/Unreveal tracks 150 @param reveal as bool or None to just change state 151 """ 152 if self.__artwork is None: 153 self.populate() 154 if self.__revealer.get_reveal_child() and reveal is not True: 155 self.__revealer.set_reveal_child(False) 156 self.get_style_context().add_class("albumrow-collapsed") 157 if self.album.id == App().player.current_track.album.id: 158 self.set_state_flags(Gtk.StateFlags.VISITED, True) 159 else: 160 if not self.__tracks_view.is_populated: 161 self.__tracks_view.populate() 162 self.__revealer.set_reveal_child(True) 163 self.get_style_context().remove_class("albumrow-collapsed") 164 self.unset_state_flags(Gtk.StateFlags.VISITED) 165 166 def set_selection(self): 167 """ 168 Show play indicator 169 """ 170 if self.__artwork is None: 171 return 172 selected = self.album.id == App().player.current_track.album.id and\ 173 App().player.current_track.id in self.album.track_ids 174 if self.__revealer.get_reveal_child(): 175 self.set_state_flags(Gtk.StateFlags.NORMAL, True) 176 elif selected: 177 self.set_state_flags(Gtk.StateFlags.VISITED, True) 178 else: 179 self.set_state_flags(Gtk.StateFlags.NORMAL, True) 180 181 def reset(self): 182 """ 183 Get a new track view 184 """ 185 if self.__artwork is None: 186 return 187 self.__tracks_view.destroy() 188 self.__tracks_view = self.__get_new_tracks_view() 189 self.__tracks_view.populate() 190 self.__revealer.add(self.__tracks_view) 191 192 def set_artwork(self): 193 """ 194 Set album artwork 195 """ 196 if self.__artwork is None: 197 return 198 App().art_helper.set_album_artwork(self.__album, 199 ArtSize.SMALL, 200 ArtSize.SMALL, 201 self.__artwork.get_scale_factor(), 202 ArtBehaviour.CACHE | 203 ArtBehaviour.CROP_SQUARE, 204 self.__on_album_artwork) 205 206 @property 207 def revealed(self): 208 """ 209 True if revealed 210 @return bool 211 """ 212 return self.__revealer is not None and\ 213 self.__revealer.get_reveal_child() 214 215 @property 216 def tracks_view(self): 217 """ 218 Get tracks view 219 @return TracksView 220 """ 221 return self.__tracks_view 222 223 @property 224 def listbox(self): 225 """ 226 Get listbox 227 @return Gtk.ListBox 228 """ 229 if self.__tracks_view.boxes: 230 return self.__tracks_view.boxes[0] 231 else: 232 return Gtk.ListBox.new() 233 234 @property 235 def children(self): 236 """ 237 Get track rows 238 @return [TrackRow] 239 """ 240 if self.__tracks_view.boxes: 241 return self.__tracks_view.boxes[0].get_children() 242 else: 243 return [] 244 245 @property 246 def is_populated(self): 247 """ 248 Return True if populated 249 @return bool 250 """ 251 return not self.revealed or self.__tracks_view.is_populated 252 253 @property 254 def name(self): 255 """ 256 Get row name 257 @return str 258 """ 259 if self.__artwork is None: 260 return "" 261 else: 262 return self.__title_label.get_text() +\ 263 self.__artist_label.get_text() 264 265 @property 266 def album(self): 267 """ 268 Get album 269 @return Album 270 """ 271 return self.__album 272 273####################### 274# PRIVATE # 275####################### 276 def __get_new_tracks_view(self): 277 """ 278 Get a new track view 279 @return AlbumTracksView 280 """ 281 tracks_view = AlbumTracksView(self.__album, 282 self.__view_type | 283 ViewType.SINGLE_COLUMN) 284 tracks_view.connect("activated", 285 self.__on_tracks_view_activated) 286 tracks_view.connect("populated", 287 self.__on_tracks_view_populated) 288 tracks_view.connect("track-removed", 289 self.__on_tracks_view_track_removed) 290 tracks_view.show() 291 return tracks_view 292 293 def __on_album_artwork(self, surface): 294 """ 295 Set album artwork 296 @param surface as str 297 """ 298 if self.__artwork is None: 299 return 300 if surface is None: 301 self.__artwork.set_from_icon_name("folder-music-symbolic", 302 Gtk.IconSize.BUTTON) 303 else: 304 self.__artwork.set_from_surface(surface) 305 self.show_all() 306 # TracksView will emit populated 307 if not self.revealed: 308 emit_signal(self, "populated") 309 310 def __on_query_tooltip(self, widget, x, y, keyboard, tooltip): 311 """ 312 Show tooltip if needed 313 @param widget as Gtk.Widget 314 @param x as int 315 @param y as int 316 @param keyboard as bool 317 @param tooltip as Gtk.Tooltip 318 """ 319 layout_title = self.__title_label.get_layout() 320 layout_artist = self.__artist_label.get_layout() 321 if layout_title.is_ellipsized() or layout_artist.is_ellipsized(): 322 artist = GLib.markup_escape_text(self.__artist_label.get_text()) 323 title = GLib.markup_escape_text(self.__title_label.get_text()) 324 self.set_tooltip_markup("<b>%s</b>\n%s" % (artist, title)) 325 else: 326 self.set_tooltip_text("") 327 328 def __on_destroy(self, widget): 329 """ 330 Destroyed widget 331 @param widget as Gtk.Widget 332 """ 333 self.__cancellable.cancel() 334 self.__artwork = None 335 336 def __on_tracks_view_activated(self, view, track): 337 """ 338 Pass signal 339 """ 340 emit_signal(self, "activated", track) 341 342 def __on_tracks_view_track_removed(self, view, row): 343 """ 344 Remove row 345 @param view as TracksView 346 @param row as TrackRow 347 """ 348 row.destroy() 349 emit_signal(self, "track-removed", row.track) 350 351 def __on_tracks_view_populated(self, view): 352 """ 353 Populate remaining discs 354 @param view as TracksView 355 @param disc_number as int 356 """ 357 if self.revealed and not self.__tracks_view.is_populated: 358 self.__tracks_view.populate() 359 else: 360 emit_signal(self, "populated") 361 362 def __on_remove_clicked(self, button): 363 """ 364 Remove album from playback/playlist 365 @param button as Gtk.Button 366 """ 367 if not self.get_state_flags() & Gtk.StateFlags.PRELIGHT: 368 return True 369 self.destroy() 370 371 def __on_play_clicked(self, button): 372 """ 373 Play album 374 @param button as Gtk.Button 375 """ 376 if not self.get_state_flags() & Gtk.StateFlags.PRELIGHT: 377 return True 378 App().player.play_album(self.__album) 379