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, GLib
14
15from gettext import gettext as _
16
17from lollypop.define import ViewType, App, MARGIN_SMALL, Type
18from lollypop.helper_adaptive import AdaptiveHelper
19from lollypop.helper_signals import SignalsHelper, signals_map
20from lollypop.helper_filtering import FilteringHelper
21
22
23class View(Gtk.Grid, AdaptiveHelper, FilteringHelper, SignalsHelper):
24    """
25        Generic view
26    """
27
28    @signals_map
29    def __init__(self, storage_type, view_type):
30        """
31            Init view
32            @param storage_type as StorageType
33            @param view_type as ViewType
34        """
35        Gtk.Grid.__init__(self)
36        AdaptiveHelper.__init__(self)
37        FilteringHelper.__init__(self)
38        self.__storage_type = storage_type
39        self.__view_type = view_type
40        self.__scrolled_position = None
41        self.__destroyed = False
42        self.__banner = None
43        self.__placeholder = None
44        self.scrolled_value = 0
45        self.set_orientation(Gtk.Orientation.VERTICAL)
46        self.set_border_width(0)
47        self.__new_ids = []
48        self._empty_message = _("No items to show")
49        self._empty_icon_name = "emblem-music-symbolic"
50
51        if view_type & ViewType.SCROLLED:
52            self.__scrolled = Gtk.ScrolledWindow.new()
53            self.__event_controller = Gtk.EventControllerMotion.new(
54                self.__scrolled)
55            self.__event_controller.set_propagation_phase(
56                Gtk.PropagationPhase.TARGET)
57            self.__event_controller.connect("leave", self._on_view_leave)
58            self.__scrolled.get_vadjustment().connect("value-changed",
59                                                      self._on_value_changed)
60            self.__scrolled.show()
61            self.__scrolled.set_property("expand", True)
62
63        # Stack for placeholder
64        self.__stack = Gtk.Stack.new()
65        self.__stack.show()
66        self.__stack.set_transition_type(Gtk.StackTransitionType.NONE)
67
68        self.connect("destroy", self._on_destroy)
69        self.connect("map", self._on_map)
70        self.connect("unmap", self._on_unmap)
71        self.connect("realize", self._on_realize)
72        return [
73            (App().window.container.widget, "notify::folded",
74             "_on_container_folded"),
75        ]
76
77    def add_widget(self, widget, banner=None):
78        """
79            Add widget to view
80            Add banner if ViewType.OVERLAY
81            @param widget as Gtk.Widget
82        """
83        self.__stack.add_named(widget, "main")
84        if self.view_type & ViewType.OVERLAY:
85            self.__overlay = Gtk.Overlay.new()
86            self.__overlay.show()
87            if self.view_type & ViewType.SCROLLED:
88                self.__overlay.add(self.scrolled)
89                self.__scrolled.add(self.__stack)
90            else:
91                self.__overlay.add(self.__stack)
92            if banner is not None:
93                self.__overlay.add_overlay(banner)
94                self.__banner = banner
95                self.__banner.connect("scroll", self.__on_banner_scroll)
96            self.add(self.__overlay)
97        elif self.view_type & ViewType.SCROLLED:
98            self.__scrolled.add(self.__stack)
99            if banner is not None:
100                self.__banner = banner
101                self.add(self.__banner)
102            self.add(self.scrolled)
103        else:
104            if banner is not None:
105                self.__banner = banner
106                self.add(self.__banner)
107            self.add(self.__stack)
108        if banner is not None:
109            banner.connect("height-changed", self.__on_banner_height_changed)
110
111    def populate(self):
112        pass
113
114    def pause(self):
115        pass
116
117    def stop(self):
118        pass
119
120    def show_placeholder(self, show, new_text=None):
121        """
122            Show placeholder
123            @param show as bool
124        """
125        if show:
126            if self.__placeholder is not None:
127                GLib.timeout_add(200, self.__placeholder.destroy)
128            message = new_text\
129                if new_text is not None\
130                else self._empty_message
131            from lollypop.widgets_placeholder import Placeholder
132            self.__placeholder = Placeholder(message, self._empty_icon_name)
133            self.__placeholder.show()
134            self.__stack.add(self.__placeholder)
135            self.__stack.set_visible_child(self.__placeholder)
136        else:
137            self.__stack.set_visible_child_name("main")
138
139    def set_scrolled(self, scrolled):
140        """
141            Add an external scrolled window
142            @param scrolled as Gtk.ScrolledWindow
143        """
144        self.__scrolled = scrolled
145        self.__view_type |= ViewType.SCROLLED
146
147    def set_populated_scrolled_position(self, position):
148        """
149            Set scrolled position on populated
150            @param position as int
151        """
152        if self.view_type & ViewType.SCROLLED:
153            self.__scrolled_position = position
154
155    @property
156    def scrolled(self):
157        """
158            Get scrolled widget
159            @return Gtk.ScrolledWindow
160        """
161        if self.view_type & ViewType.SCROLLED:
162            return self.__scrolled
163        else:
164            return Gtk.ScrolledWindow.new()
165
166    @property
167    def banner(self):
168        """
169            Get view banner
170            @return BannerWidget
171        """
172        return self.__banner
173
174    @property
175    def children(self):
176        """
177            Get view children
178            @return [Gtk.Widget]
179        """
180        return []
181
182    @property
183    def storage_type(self):
184        """
185            Get storage type
186            @return StorageType
187        """
188        return self.__storage_type
189
190    @property
191    def view_type(self):
192        """
193            View type less sizing
194            @return ViewType
195        """
196        return self.__view_type
197
198    @property
199    def position(self):
200        """
201            Get scrolled position
202            @return float
203        """
204        if self.view_type & ViewType.SCROLLED:
205            position = self.scrolled.get_vadjustment().get_value()
206        else:
207            position = 0
208        return position
209
210    @property
211    def destroyed(self):
212        """
213            True if widget has been destroyed
214            @return bool
215        """
216        return self.__destroyed
217
218#######################
219# PROTECTED           #
220#######################
221    def _on_view_leave(self, event_controller):
222        pass
223
224    def _on_container_folded(self, leaflet, folded):
225        """
226            Handle libhandy folded status
227            @param leaflet as Handy.Leaflet
228            @param folded as Gparam
229        """
230        if self.__placeholder is not None and self.__placeholder.is_visible():
231            self.__placeholder.set_folded(App().window.folded)
232
233    def _on_value_changed(self, adj):
234        """
235            Reveal banner
236            @param adj as Gtk.Adjustment
237        """
238        if self.__banner is not None:
239            reveal = self.__should_reveal_header(adj)
240            self.__banner.set_reveal_child(reveal)
241            if reveal:
242                main_widget = self.__stack.get_child_by_name("main")
243                if main_widget is not None:
244                    main_widget.set_margin_top(self.__banner.height +
245                                               MARGIN_SMALL)
246                if self.view_type & ViewType.SCROLLED:
247                    self.scrolled.get_vscrollbar().set_margin_top(
248                        self.__banner.height)
249            elif self.view_type & ViewType.SCROLLED:
250                self.scrolled.get_vscrollbar().set_margin_top(0)
251
252    def _on_map(self, widget):
253        """
254            Set initial view state
255            @param widget as GtK.Widget
256        """
257        # Set sidebar id
258        if self.sidebar_id is None:
259            ids = App().window.container.sidebar.selected_ids
260            if ids:
261                self.set_sidebar_id(ids[0])
262                if self.sidebar_id == Type.GENRES_LIST:
263                    self.selection_ids["left"] =\
264                        App().window.container.left_list.selected_ids
265                    self.selection_ids["right"] =\
266                        App().window.container.right_list.selected_ids
267                elif self.sidebar_id == Type.ARTISTS_LIST:
268                    self.selection_ids["left"] =\
269                        App().window.container.left_list.selected_ids
270
271    def _on_unmap(self, widget):
272        pass
273
274    def _on_realize(self, widget):
275        """
276            Delayed adaptive mode
277            Restore scroll position
278            @param widget as Gtk.Widget
279        """
280        parent = widget.get_parent()
281        if self.__banner is not None and parent is not None:
282            width = parent.get_allocated_width()
283            self.__banner.update_for_width(width)
284            self.__on_banner_height_changed(self.__banner,
285                                            self.__banner.height)
286        # Wait for stack allocation to restore scrolled position
287        if self.__scrolled_position is not None:
288            self.__stack.connect("size-allocate",
289                                 self.__on_stack_size_allocated)
290
291    def _on_destroy(self, widget):
292        """
293            Clean up widget
294            @param widget as Gtk.Widget
295        """
296        self.__destroyed = True
297        self.__event_controller = None
298
299#######################
300# PRIVATE             #
301#######################
302    def __should_reveal_header(self, adj):
303        """
304            Check if we need to reveal header
305            @param adj as Gtk.Adjustment
306            @param delta as int
307            @return int
308        """
309        value = adj.get_value()
310        reveal = self.scrolled_value > value
311        self.scrolled_value = value
312        return reveal
313
314    def __on_banner_height_changed(self, banner, height):
315        """
316            Update scroll margin
317            @param banner as BannerWidget
318            @param height as int
319        """
320        if self.view_type & ViewType.OVERLAY:
321            main_widget = self.__stack.get_child_by_name("main")
322            main_widget.set_margin_top(height + MARGIN_SMALL)
323            if self.view_type & ViewType.SCROLLED:
324                self.scrolled.get_vscrollbar().set_margin_top(height)
325
326    def __on_banner_scroll(self, banner, x, y):
327        """
328            Pass to scrolled
329            @param banner as BannerWidget
330            @param x as float
331            @param y as float
332        """
333        if y > 0:
334            y = 100
335        else:
336            y = -100
337        adj = self.scrolled.get_vadjustment()
338        new_value = adj.get_value() + y
339        lower = adj.get_lower()
340        upper = adj.get_upper() - adj.get_page_size()
341        if new_value != lower and new_value != upper:
342            adj.set_value(new_value)
343
344    def __on_stack_size_allocated(self, stack, allocation):
345        """
346            Restore scrolled position
347            @param stack as Gtk.Stack
348            @param allocation as Gdk.Rectangle
349        """
350        if self.__scrolled_position is not None and\
351                allocation.height > self.__scrolled_position:
352            stack.disconnect_by_func(self.__on_stack_size_allocated)
353            self.scrolled.get_vadjustment().set_value(
354                self.__scrolled_position)
355            self.__scrolled_position = None
356