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, GObject, Gdk, GdkPixbuf, GLib, Pango
14
15from lollypop.objects_album import Album
16from lollypop.define import App, ArtSize, ArtBehaviour, MARGIN
17from lollypop.utils import get_round_surface, emit_signal
18from lollypop.menu_header import HeaderType
19from lollypop.helper_signals import SignalsHelper, signals_map
20
21
22class MenuBuilder(Gtk.Stack, SignalsHelper):
23    """
24        Advanced menu model constructor
25        Does not support submenus
26    """
27
28    __gsignals__ = {
29        "hidden": (GObject.SignalFlags.RUN_FIRST, None, (bool,)),
30    }
31
32    @signals_map
33    def __init__(self, menu, scrolled=False):
34        """
35            Init menu
36            @param menu as Gio.Menu
37            @param scrolled as bool
38        """
39        Gtk.Stack.__init__(self)
40        self.__built = False
41        self.__grids = {}
42        self.__menu_queue = []
43        self.__submenu_queue = []
44        self.__widgets_queue = []
45        self.__add_menu(menu, "main", False, scrolled)
46        return [
47            (App().window.container.widget, "notify::folded",
48             "_on_container_folded")
49        ]
50
51    def add_widget(self, widget, position=-1):
52        """
53            Append widget to menu
54            @param widget as Gtk.Widget
55            @param position as negative int
56        """
57        self.__widgets_queue.append((widget, position))
58        if self.__built:
59            self.__add_widgets()
60
61#######################
62# PROTECTED           #
63#######################
64    def _on_container_folded(self, leaflet, folded):
65        """
66            Destroy self if adaptive off
67            @param leaflet as Handy.Leaflet
68            @param folded as Gparam
69        """
70        if not App().window.folded:
71            self.destroy()
72
73#######################
74# PRIVATE             #
75#######################
76    def __add_widgets(self):
77        """
78            Add pending widget to menu
79        """
80        while self.__widgets_queue:
81            (widget, position) = self.__widgets_queue.pop(0)
82            if widget.submenu_name is not None:
83                self.__add_menu_container(widget.submenu_name, True, True)
84                self.__grids[widget.submenu_name].add(widget)
85                button = Gtk.ModelButton.new()
86                button.set_label(widget.submenu_name)
87                button.get_child().set_halign(Gtk.Align.START)
88                button.set_property("menu-name", widget.submenu_name)
89                button.show()
90                parent = self.__grids["main"]
91                if widget.section is not None:
92                    self.__add_section(widget.section, "main")
93                widget = button
94            else:
95                parent = self.get_child_by_name("main")
96                if isinstance(parent, Gtk.ScrolledWindow):
97                    # scrolled -> viewport -> grid
98                    parent = parent.get_child().get_child()
99            if position < -1:
100                position = len(parent) + position + 1
101                parent.insert_row(position)
102                parent.attach(widget, 0, position, 1, 1)
103            else:
104                parent.add(widget)
105
106    def __add_menu_container(self, menu_name, submenu, scrolled, margin=10):
107        """
108            Add menu container
109            @param menu_name as str
110            @param submenu as bool
111            @param scrolled as bool
112            @param margin as int
113        """
114        grid = self.get_child_by_name(menu_name)
115        if grid is None:
116            grid = Gtk.Grid.new()
117            grid.set_orientation(Gtk.Orientation.VERTICAL)
118            grid.connect("map", self.__on_grid_map, menu_name)
119            self.__grids[menu_name] = grid
120            grid.set_property("margin", margin)
121            grid.show()
122            if scrolled:
123                scrolled = Gtk.ScrolledWindow()
124                scrolled.set_policy(Gtk.PolicyType.NEVER,
125                                    Gtk.PolicyType.AUTOMATIC)
126                scrolled.show()
127                scrolled.add(grid)
128                self.add_named(scrolled, menu_name)
129            else:
130                self.add_named(grid, menu_name)
131            if submenu:
132                button = Gtk.ModelButton.new()
133                button.get_style_context().add_class("padding")
134                button.set_property("menu-name", "main")
135                button.set_property("inverted", True)
136                button.set_label(menu_name)
137                button.get_child().set_halign(Gtk.Align.START)
138                button.show()
139                grid.add(button)
140
141    def __add_menu(self, menu, menu_name, submenu, scrolled):
142        """
143            Create container and add menu
144            @param menu as Gio.Menu
145            @param menu_name as str
146            @param submenu as bool
147            @param scrolled as bool
148        """
149        n_items = menu.get_n_items()
150        if n_items:
151            self.__add_menu_container(menu_name, submenu, scrolled, 10)
152            menu_range = list(range(0, n_items))
153            if submenu:
154                self.__submenu_queue.append((menu, menu_name, menu_range))
155            else:
156                self.__add_menu_items(menu, menu_name, menu_range)
157        else:
158            self.__add_menu_container(menu_name, submenu, scrolled, 0)
159            self.__built = True
160            self.__add_widgets()
161
162    def __add_menu_items(self, menu, menu_name, indexes):
163        """
164            Add menu items present in indexes
165            @param menu as Gio.Menu
166            @param menu_name as str
167            @param indexes as [int]
168        """
169        if indexes:
170            i = indexes.pop(0)
171            header = menu.get_item_attribute_value(i, "header")
172            action = menu.get_item_attribute_value(i, "action")
173            label = menu.get_item_attribute_value(i, "label")
174            tooltip = menu.get_item_attribute_value(i, "tooltip")
175            close = menu.get_item_attribute_value(i, "close") is not None
176            if header is not None:
177                header_type = header[0]
178                header_label = header[1]
179                if header_type == HeaderType.ALBUM:
180                    album_id = header[2]
181                    self.__add_album_header(header_label,
182                                            album_id,
183                                            menu_name)
184                elif header_type == HeaderType.ARTIST:
185                    artist_id = header[2]
186                    self.__add_artist_header(header_label, artist_id,
187                                             menu_name)
188                elif header_type == HeaderType.ROUNDED:
189                    artwork_name = header[2]
190                    self.__add_rounded_header(header_label, artwork_name,
191                                              menu_name)
192                else:
193                    icon_name = header[2]
194                    self.__add_header(header_label, icon_name, menu_name)
195                GLib.idle_add(self.__add_menu_items, menu, menu_name, indexes)
196            elif action is None:
197                link = menu.get_item_link(i, "section")
198                submenu = menu.get_item_link(i, "submenu")
199                if link is not None:
200                    self.__menu_queue.append((menu, menu_name, indexes))
201                    self.__add_section(label, menu_name)
202                    self.__add_menu(link, menu_name, False, False)
203                elif submenu is not None:
204                    self.__add_submenu(label, submenu, menu_name)
205                    GLib.idle_add(self.__add_menu_items, menu,
206                                  menu_name, indexes)
207            else:
208                target = menu.get_item_attribute_value(i, "target")
209                self.__add_item(label, action, target,
210                                tooltip, close, menu_name)
211                GLib.idle_add(self.__add_menu_items, menu, menu_name, indexes)
212        # Continue to populate queued menu
213        elif self.__menu_queue:
214            (menu, menu_name, indexes) = self.__menu_queue.pop(-1)
215            GLib.idle_add(self.__add_menu_items, menu, menu_name, indexes)
216        # Finish with submenus
217        elif self.__submenu_queue:
218            (menu, menu_name, indexes) = self.__submenu_queue.pop(-1)
219            GLib.idle_add(self.__add_menu_items, menu, menu_name, indexes)
220        else:
221            self.__built = True
222            self.__add_widgets()
223
224    def __add_item(self, text, action, target, tooltip, close, menu_name):
225        """
226            Add a Menu item
227            @param text as GLib.Variant
228            @param action as Gio.Action
229            @param target as GLib.Variant
230            @parmam tooltip as GLib.Variant
231            @param close as bool
232            @param menu_name as str
233        """
234        button = Gtk.ModelButton.new()
235        button.set_hexpand(True)
236        button.set_action_name(action.get_string())
237        button.set_label(text.get_string())
238        button.get_child().set_halign(Gtk.Align.START)
239        if close:
240            button.connect("clicked",
241                           lambda x: emit_signal(self, "hidden", True))
242        if tooltip is not None:
243            button.set_tooltip_markup(tooltip.get_string())
244            button.set_has_tooltip(True)
245        if target is not None:
246            button.set_action_target_value(target)
247        button.show()
248        self.__grids[menu_name].add(button)
249
250    def __add_section(self, text, menu_name):
251        """
252            Add section to menu
253            @param text as as GLib.Variant
254            @param menu_name as str
255        """
256        box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 4)
257        sep1 = Gtk.Separator.new(Gtk.Orientation.HORIZONTAL)
258        sep1.set_hexpand(True)
259        sep1.set_property("valign", Gtk.Align.CENTER)
260        box.add(sep1)
261        label = Gtk.Label.new(text.get_string())
262        label.get_style_context().add_class("dim-label")
263        if App().window.folded:
264            label.get_style_context().add_class("padding")
265        box.add(label)
266        sep2 = Gtk.Separator.new(Gtk.Orientation.HORIZONTAL)
267        sep2.set_property("valign", Gtk.Align.CENTER)
268        sep2.set_hexpand(True)
269        box.add(sep2)
270        box.show_all()
271        self.__grids[menu_name].add(box)
272
273    def __add_submenu(self, text, menu, menu_name):
274        """
275            Add submenu
276            @param text as GLib.Variant
277            @param menu as Gio.Menu
278            @param menu_name as str
279        """
280        submenu_name = text.get_string()
281        self.__add_menu(menu, submenu_name, True, True)
282        button = Gtk.ModelButton.new()
283        button.set_hexpand(True)
284        button.set_property("menu-name", submenu_name)
285        button.set_label(text.get_string())
286        button.get_child().set_halign(Gtk.Align.START)
287        button.show()
288        self.__grids[menu_name].add(button)
289
290    def __add_header(self, text, icon_name, menu_name):
291        """
292            Add an header for albums to close menu
293            @param text as GLib.Variant
294            @param icon_name as GLib.Variant
295            @param menu_name as str
296        """
297        button = Gtk.ModelButton.new()
298        button.set_hexpand(True)
299        button.connect("clicked", lambda x: emit_signal(self, "hidden", True))
300        button.show()
301        label = Gtk.Label.new()
302        label.set_markup(text)
303        label.set_ellipsize(Pango.EllipsizeMode.END)
304        label.show()
305        artwork = Gtk.Image.new_from_icon_name(icon_name,
306                                               Gtk.IconSize.INVALID)
307        artwork.set_pixel_size(ArtSize.SMALL)
308        artwork.show()
309        close_image = Gtk.Image.new_from_icon_name("pan-up-symbolic",
310                                                   Gtk.IconSize.BUTTON)
311        close_image.show()
312        grid = Gtk.Grid()
313        grid.set_column_spacing(MARGIN)
314        grid.add(artwork)
315        grid.add(label)
316        grid.add(close_image)
317        button.set_image(grid)
318        button.get_style_context().add_class("padding")
319        self.__grids[menu_name].add(button)
320
321    def __add_album_header(self, text, album_id, menu_name):
322        """
323            Add an header for album to close menu
324            @param text as str
325            @param album_id as int
326            @param menu_name as str
327        """
328        button = Gtk.ModelButton.new()
329        button.set_hexpand(True)
330        button.connect("clicked", lambda x: emit_signal(self, "hidden", True))
331        button.show()
332        label = Gtk.Label.new()
333        label.set_markup(text)
334        label.set_ellipsize(Pango.EllipsizeMode.END)
335        label.show()
336        artwork = Gtk.Image.new()
337        close_image = Gtk.Image.new_from_icon_name("pan-up-symbolic",
338                                                   Gtk.IconSize.BUTTON)
339        close_image.show()
340        grid = Gtk.Grid()
341        grid.set_halign(Gtk.Align.START)
342        grid.set_column_spacing(MARGIN)
343        grid.add(artwork)
344        grid.add(label)
345        grid.add(close_image)
346        button.set_image(grid)
347        button.get_style_context().add_class("padding")
348        App().art_helper.set_album_artwork(
349                Album(album_id),
350                ArtSize.SMALL,
351                ArtSize.SMALL,
352                artwork.get_scale_factor(),
353                ArtBehaviour.CACHE | ArtBehaviour.CROP_SQUARE,
354                self.__on_artwork,
355                artwork)
356        self.__grids[menu_name].add(button)
357
358    def __add_artist_header(self, text, artist_id, menu_name):
359        """
360            Add an header for artist to close menu
361            @param text as str
362            @param artist_id as int
363            @param menu_name as str
364        """
365        button = Gtk.ModelButton.new()
366        button.set_hexpand(True)
367        button.connect("clicked", lambda x: emit_signal(self, "hidden", True))
368        button.show()
369        label = Gtk.Label.new()
370        label.set_markup(text)
371        label.set_ellipsize(Pango.EllipsizeMode.END)
372        label.show()
373        artwork = Gtk.Image.new()
374        close_image = Gtk.Image.new_from_icon_name("pan-up-symbolic",
375                                                   Gtk.IconSize.BUTTON)
376        close_image.show()
377        grid = Gtk.Grid()
378        grid.set_column_spacing(MARGIN)
379        grid.add(artwork)
380        grid.add(label)
381        grid.add(close_image)
382        button.set_image(grid)
383        button.get_style_context().add_class("padding")
384        artist_name = App().artists.get_name(artist_id)
385        App().art_helper.set_artist_artwork(
386                artist_name,
387                ArtSize.SMALL,
388                ArtSize.SMALL,
389                artwork.get_scale_factor(),
390                ArtBehaviour.CACHE |
391                ArtBehaviour.CROP_SQUARE |
392                ArtBehaviour.ROUNDED,
393                self.__on_artwork,
394                artwork)
395        self.__grids[menu_name].add(button)
396
397    def __add_rounded_header(self, text, artwork_name, menu_name):
398        """
399            Add an header for rounded widgets to close menu
400            @param text as str
401            @param artwork_name as str
402            @param menu_name as str
403        """
404        def on_load_from_cache(pixbuf, artwork):
405            if pixbuf is not None:
406                scale_factor = artwork.get_scale_factor()
407                surface = Gdk.cairo_surface_create_from_pixbuf(
408                    pixbuf.scale_simple(ArtSize.MEDIUM, ArtSize.MEDIUM,
409                                        GdkPixbuf.InterpType.BILINEAR),
410                    scale_factor, None)
411                rounded = get_round_surface(surface, scale_factor,
412                                            ArtSize.MEDIUM / 4)
413                artwork.set_from_surface(rounded)
414                artwork.show()
415        button = Gtk.ModelButton.new()
416        button.set_hexpand(True)
417        button.connect("clicked", lambda x: emit_signal(self, "hidden", True))
418        button.show()
419        label = Gtk.Label.new()
420        label.set_ellipsize(Pango.EllipsizeMode.END)
421        label.set_markup(text)
422        label.show()
423        artwork = Gtk.Image.new()
424        artwork.get_style_context().add_class("light-background")
425        close_image = Gtk.Image.new_from_icon_name("pan-up-symbolic",
426                                                   Gtk.IconSize.BUTTON)
427        close_image.show()
428        grid = Gtk.Grid()
429        grid.set_column_spacing(MARGIN)
430        grid.add(artwork)
431        grid.add(label)
432        grid.add(close_image)
433        button.set_image(grid)
434        button.get_style_context().add_class("padding")
435        App().task_helper.run(
436                App().art.get_from_cache,
437                artwork_name,
438                "ROUNDED",
439                ArtSize.BANNER, ArtSize.BANNER,
440                callback=(on_load_from_cache, artwork))
441        self.__grids[menu_name].add(button)
442
443    def __on_artwork(self, surface, artwork):
444        """
445            Set artwork
446            @param surface as str
447            @param artwork as Gtk.Image
448        """
449        if surface is None:
450            artwork.set_from_icon_name("folder-music-symbolic",
451                                       Gtk.IconSize.BUTTON)
452        else:
453            artwork.set_from_surface(surface)
454        artwork.show()
455
456    def __on_grid_map(self, widget, menu_name):
457        """
458            On map, set stack order
459            @param widget as Gtk.Widget
460            @param menu_name as str
461        """
462        if menu_name == "main":
463            self.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT)
464            self.set_size_request(-1, -1)
465        else:
466            self.set_size_request(300, 400)
467            self.set_transition_type(Gtk.StackTransitionType.SLIDE_RIGHT)
468