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