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