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