1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 from constant import DEFAULT_FONT_SIZE, MENU_ITEM_RADIUS, ALIGN_START, ALIGN_MIDDLE, WIDGET_POS_RIGHT_CENTER, WIDGET_POS_TOP_LEFT
24 from draw import draw_vlinear, draw_pixbuf, draw_text, draw_hlinear
25 from line import HSeparator
26 from theme import ui_theme
27 from window import Window
28 import gobject
29 import gtk
30 from utils import (is_in_rect, get_content_size, propagate_expose,
31 get_widget_root_coordinate, get_screen_size,
32 alpha_color_hex_to_cairo, get_window_shadow_size)
33
34 __all__ = ["Menu", "MenuItem"]
35
36 menu_grab_window = gtk.Window(gtk.WINDOW_POPUP)
37 menu_grab_window.move(0, 0)
38 menu_grab_window.set_default_size(0, 0)
39 menu_grab_window.show()
40 menu_active_item = None
41
42 root_menus = []
43
45 menu_grab_window.grab_add()
46 gtk.gdk.pointer_grab(
47 menu_grab_window.window,
48 True,
49 gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK | gtk.gdk.ENTER_NOTIFY_MASK | gtk.gdk.LEAVE_NOTIFY_MASK,
50 None, None, gtk.gdk.CURRENT_TIME)
51
65
67 '''Is press on menu grab window.'''
68 for toplevel in gtk.window_list_toplevels():
69 if isinstance(window, gtk.Window):
70 if window == toplevel:
71 return True
72 elif isinstance(window, gtk.gdk.Window):
73 if window == toplevel.window:
74 return True
75
76 return False
77
92
94 global menu_active_item
95
96 if event and event.window:
97 event_widget = event.window.get_user_data()
98 if isinstance(event_widget, Menu):
99 menu_item = event_widget.get_menu_item_at_coordinate(event.get_root_coords())
100 if menu_item and isinstance(menu_item.item_box, gtk.Button):
101 if menu_active_item:
102 menu_active_item.set_state(gtk.STATE_NORMAL)
103
104 menu_item.item_box.set_state(gtk.STATE_PRELIGHT)
105 menu_active_item = menu_item.item_box
106
107 enter_notify_event = gtk.gdk.Event(gtk.gdk.ENTER_NOTIFY)
108 enter_notify_event.window = event.window
109 enter_notify_event.time = event.time
110 enter_notify_event.send_event = True
111 enter_notify_event.x_root = event.x_root
112 enter_notify_event.y_root = event.y_root
113 enter_notify_event.x = event.x
114 enter_notify_event.y = event.y
115 enter_notify_event.state = event.state
116
117 menu_item.item_box.event(enter_notify_event)
118
119 menu_item.item_box.queue_draw()
120
121 menu_grab_window.connect("button-press-event", menu_grab_window_button_press)
122 menu_grab_window.connect("motion-notify-event", menu_grab_window_motion_notify)
123
125 '''
126 Menu.
127
128 @undocumented: realize_menu
129 @undocumented: hide_menu
130 @undocumented: get_menu_item_at_coordinate
131 @undocumented: get_menu_items
132 @undocumented: init_menu
133 @undocumented: get_submenus
134 @undocumented: get_menu_icon_info
135 @undocumented: adjust_menu_position
136 @undocumented: show_submenu
137 @undocumented: hide_submenu
138 @undocumented: get_root_menu
139 '''
140
154 '''
155 Initialize Menu class.
156
157 @param items: A list of item, item format: (item_icon, itemName, item_node).
158 @param is_root_menu: Default is False for submenu, you should set it as True if you build root menu.
159 @param select_scale: Default is False, it will use parant's width if it set True.
160 @param x_align: Horizontal alignment value.
161 @param y_align: Vertical alignment value.
162 @param font_size: Menu font size, default is DEFAULT_FONT_SIZE
163 @param padding_x: Horizontal padding value, default is 3 pixel.
164 @param padding_y: Vertical padding value, default is 3 pixel.
165 @param item_padding_x: Horizontal item padding value, default is 6 pixel.
166 @param item_padding_y: Vertical item padding value, default is 3 pixel.
167 @param shadow_visible: Whether show window shadow, default is True.
168 @param menu_min_width: Minimum width of menu.
169 '''
170 global root_menus
171
172
173 Window.__init__(self, shadow_visible=shadow_visible, window_type=gtk.WINDOW_POPUP)
174 self.set_can_focus(True)
175 self.draw_mask = self.draw_menu_mask
176 self.is_root_menu = is_root_menu
177 self.select_scale = select_scale
178 self.x_align = x_align
179 self.y_align = y_align
180 self.submenu = None
181 self.root_menu = None
182 self.offset_x = 0
183 self.offset_y = 0
184 self.padding_x = padding_x
185 self.padding_y = padding_y
186 self.item_padding_x = item_padding_x
187 self.item_padding_y = item_padding_y
188 self.menu_min_width = menu_min_width
189
190
191 self.set_skip_pager_hint(True)
192 self.set_skip_taskbar_hint(True)
193 self.set_keep_above(True)
194
195
196 self.item_box = gtk.VBox()
197 self.item_align = gtk.Alignment()
198 self.item_align.set_padding(padding_y, padding_y, padding_x, padding_x)
199 self.item_align.add(self.item_box)
200 self.window_frame.add(self.item_align)
201 self.menu_items = []
202
203 if items:
204 (icon_width, icon_height, have_submenu, submenu_width, submenu_height) = self.get_menu_icon_info(items)
205
206 for item in items:
207 menu_item = MenuItem(
208 item, font_size, self.select_scale, self.show_submenu, self.hide_submenu,
209 self.get_root_menu, self.get_menu_items,
210 icon_width, icon_height,
211 have_submenu, submenu_width, submenu_height,
212 padding_x, padding_y,
213 item_padding_x, item_padding_y, self.menu_min_width)
214 self.menu_items.append(menu_item)
215 self.item_box.pack_start(menu_item.item_box, False, False)
216
217 self.connect("show", self.init_menu)
218 self.connect("hide", self.hide_menu)
219 self.connect("realize", self.realize_menu)
220
222 '''
223 Internal callback for `hide` signal.
224 '''
225
226 self.move(-1000000, -1000000)
227
229 '''
230 Internal callback for `realize` signal.
231 '''
232
233 self.move(-1000000, -1000000)
234
235
236 self.window.set_back_pixmap(None, False)
237
256
258 '''
259 Internal function to get menu item at coordinate, return None if haven't any menu item at given coordinate.
260 '''
261 match_menu_item = None
262 for menu_item in self.menu_items:
263 item_rect = menu_item.item_box.get_allocation()
264 (item_x, item_y) = get_widget_root_coordinate(menu_item.item_box, WIDGET_POS_TOP_LEFT)
265 if is_in_rect((x, y), (item_x, item_y, item_rect.width, item_rect.height)):
266 match_menu_item = menu_item
267 break
268
269 return match_menu_item
270
272 '''
273 Internal function to get menu items.
274 '''
275 return self.menu_items
276
278 '''
279 Internal callback for `show` signal.
280 '''
281 global root_menus
282
283 if self.is_root_menu:
284 menu_grab_window_focus_out()
285
286 if not gtk.gdk.pointer_is_grabbed():
287 menu_grab_window_focus_in()
288
289 if self.is_root_menu and not self in root_menus:
290 root_menus.append(self)
291
292 self.adjust_menu_position()
293
295 '''
296 Internal function to get submenus.
297 '''
298 if self.submenu:
299 return [self.submenu] + self.submenu.get_submenus()
300 else:
301 return []
302
304 '''
305 Internal function to get menu icon information.
306 '''
307 have_submenu = False
308 icon_width = 16
309 icon_height = 16
310 submenu_width = 16
311 submenu_height = 15
312
313 for item in items:
314 if item:
315 (item_icons, item_content, item_node) = item[0:3]
316 if isinstance(item_node, Menu):
317 have_submenu = True
318
319 if have_submenu:
320 break
321
322 return (icon_width, icon_height, have_submenu, submenu_width, submenu_height)
323
325 '''
326 Show menu with given position.
327
328 @param x: X coordinate of menu.
329 @param y: Y coordinate of menu.
330 @param offset_x: Offset x when haven't enough space to show menu, default is 0.
331 @param offset_y: Offset y when haven't enough space to show menu, default is 0.
332 '''
333
334 self.expect_x = x
335 self.expect_y = y
336 self.offset_x = offset_x
337 self.offset_y = offset_y
338
339
340 self.show_all()
341
343 '''
344 Internal function to realize menu position.
345 '''
346
347 (screen_width, screen_height) = get_screen_size(self)
348
349 menu_width = menu_height = 0
350 for menu_item in self.menu_items:
351 if isinstance(menu_item.item_box, gtk.Button):
352 item_box_width = menu_item.item_box_width
353 if item_box_width > menu_width:
354 menu_width = item_box_width
355
356 menu_height += menu_item.item_box_height
357 (shadow_x, shadow_y) = get_window_shadow_size(self)
358 menu_width += (self.padding_x + shadow_x) * 2
359 menu_height += (self.padding_y + shadow_y) * 2
360
361 if self.x_align == ALIGN_START:
362 dx = self.expect_x
363 elif self.x_align == ALIGN_MIDDLE:
364 dx = self.expect_x - menu_width / 2
365 else:
366 dx = self.expect_x - menu_width
367
368 if self.y_align == ALIGN_START:
369 dy = self.expect_y
370 elif self.y_align == ALIGN_MIDDLE:
371 dy = self.expect_y - menu_height / 2
372 else:
373 dy = self.expect_y - menu_height
374
375 if self.expect_x + menu_width > screen_width:
376 dx = self.expect_x - menu_width + self.offset_x
377 if self.expect_y + menu_height > screen_height:
378 dy = self.expect_y - menu_height + self.offset_y
379
380 self.move(dx, dy)
381
383 '''
384 Hide menu.
385 '''
386
387 self.hide_submenu()
388
389
390 self.hide_all()
391
392
393 self.submenu = None
394 self.root_menu = None
395
397 '''
398 Internal function to show submenu.
399 '''
400 if self.submenu != submenu:
401
402 self.hide_submenu()
403
404
405 self.submenu = submenu
406 self.submenu.root_menu = self.get_root_menu()
407
408
409 rect = self.get_allocation()
410 self.submenu.show(coordinate, (-rect.width + self.shadow_radius * 2, offset_y))
411
413 '''
414 Internal function to hide submenu.
415 '''
416 if self.submenu:
417
418 self.submenu.hide()
419 self.submenu = None
420
421
422 for menu_item in self.menu_items:
423 menu_item.submenu_active = False
424
426 '''
427 Internal to get root menu.
428 '''
429 if self.root_menu:
430 return self.root_menu
431 else:
432 return self
433
434 gobject.type_register(Menu)
435
437 '''
438 Menu item for L{ I{Menu} <Menu>}.
439
440 @undocumented: create_separator_item
441 @undocumented: create_menu_item
442 @undocumented: realize_item_box
443 @undocumented: wrap_menu_clicked_action
444 @undocumented: expose_menu_item
445 @undocumented: enter_notify_menu_item
446 '''
447
463 '''
464 Initialize MenuItem class.
465
466 @param item: item format: (item_icon, itemName, item_node).
467 @param font_size: Menu font size.
468 @param select_scale: Default is False, it will use parant's width if it set True.
469 @param show_submenu_callback: Callback when show submenus.
470 @param hide_submenu_callback: Callback when hide submenus.
471 @param get_root_menu_callback: Callback to get root menu.
472 @param get_menu_items_callback: Callback to get menu items.
473 @param icon_width: Icon width.
474 @param icon_height: Icon height.
475 @param have_submenu: Whether have submenu.
476 @param submenu_width: Width of submenu.
477 @param submenu_height: Height of submenu.
478 @param menu_padding_x: Horizontal padding of menu.
479 @param menu_padding_y: Vertical padding of menu.
480 @param item_padding_x: Horizontal padding of item.
481 @param item_padding_y: Vertical padding of item.
482 @param min_width: Minimum width.
483 '''
484
485 self.item = item
486 self.font_size = font_size
487 self.select_scale = select_scale
488 self.menu_padding_x = menu_padding_x
489 self.menu_padding_y = menu_padding_y
490 self.item_padding_x = item_padding_x
491 self.item_padding_y = item_padding_y
492 self.show_submenu_callback = show_submenu_callback
493 self.hide_submenu_callback = hide_submenu_callback
494 self.get_root_menu_callback = get_root_menu_callback
495 self.get_menu_items_callback = get_menu_items_callback
496 self.icon_width = icon_width
497 self.icon_height = icon_height
498 self.have_submenu = have_submenu
499 self.submenu_width = submenu_width
500 self.submenu_height = submenu_height
501 self.submenu_active = False
502 self.min_width = min_width
503 self.arrow_padding_x = 5
504
505
506 if self.item:
507 self.create_menu_item()
508 else:
509 self.create_separator_item()
510
512 '''
513 Internal function to create separator item.
514 '''
515 self.item_box = HSeparator(
516 ui_theme.get_shadow_color("h_separator").get_color_info(),
517 self.item_padding_x,
518 self.item_padding_y)
519 self.item_box_height = self.item_padding_y * 2 + 1
520
522 '''
523 Internal function to create menu item.
524 '''
525
526 (item_icons, item_content, item_node) = self.item[0:3]
527
528
529 self.item_box = gtk.Button()
530
531
532 self.item_box.connect("expose-event", self.expose_menu_item)
533 self.item_box.connect("enter-notify-event", lambda w, e: self.enter_notify_menu_item(w))
534
535
536 self.item_box.connect("button-press-event", self.wrap_menu_clicked_action)
537
538 self.item_box.connect("realize", lambda w: self.realize_item_box(w, item_content))
539
541 '''
542 Internal callback for `realize` signal.
543 '''
544
545 (width, height) = get_content_size(item_content, self.font_size)
546 self.item_box_height = self.item_padding_y * 2 + max(int(height), self.icon_height)
547 if self.select_scale:
548 self.item_box_width = widget.get_parent().get_parent().allocation.width
549 else:
550 self.item_box_width = self.item_padding_x * 3 + self.icon_width + int(width)
551
552 if self.have_submenu:
553 self.item_box_width += self.item_padding_x + self.submenu_width + self.arrow_padding_x * 2
554
555 self.item_box_width = max(self.item_box_width, self.min_width)
556
557 self.item_box.set_size_request(self.item_box_width, self.item_box_height)
558
560 '''
561 Internal function to wrap clicked menu action.
562 '''
563 item_node = self.item[2]
564 if not isinstance(item_node, Menu):
565
566 menu_grab_window_focus_out()
567
568
569 if item_node:
570 if len(self.item) > 3:
571 item_node(*self.item[3:])
572 else:
573 item_node()
574
576 '''
577 Internal callback for `expose` signal.
578 '''
579
580 cr = widget.window.cairo_create()
581 rect = widget.allocation
582 font_color = ui_theme.get_color("menu_font").get_color()
583 (item_icons, item_content, item_node) = self.item[0:3]
584
585
586 if self.submenu_active or widget.state in [gtk.STATE_PRELIGHT, gtk.STATE_ACTIVE]:
587
588 draw_vlinear(cr, rect.x, rect.y, rect.width, rect.height,
589 ui_theme.get_shadow_color("menu_item_select").get_color_info(),
590 MENU_ITEM_RADIUS)
591
592
593 font_color = ui_theme.get_color("menu_select_font").get_color()
594
595
596 pixbuf = None
597 pixbuf_width = 0
598 if item_icons:
599 (item_normal_dpixbuf, item_hover_dpixbuf) = item_icons
600 if self.submenu_active or widget.state in [gtk.STATE_PRELIGHT, gtk.STATE_ACTIVE]:
601 if item_hover_dpixbuf == None:
602 pixbuf = item_normal_dpixbuf.get_pixbuf()
603 else:
604 pixbuf = item_hover_dpixbuf.get_pixbuf()
605 else:
606 pixbuf = item_normal_dpixbuf.get_pixbuf()
607 pixbuf_width += pixbuf.get_width()
608 draw_pixbuf(cr, pixbuf, rect.x + self.item_padding_x, rect.y + (rect.height - pixbuf.get_height()) / 2)
609
610
611 draw_text(cr, item_content,
612 rect.x + self.item_padding_x * 2 + self.icon_width,
613 rect.y,
614 rect.width,
615 rect.height,
616 self.font_size, font_color,
617 )
618
619
620 if isinstance(item_node, Menu):
621 if self.submenu_active or widget.state in [gtk.STATE_PRELIGHT, gtk.STATE_ACTIVE]:
622 submenu_pixbuf = ui_theme.get_pixbuf("menu/arrow_hover.png").get_pixbuf()
623 else:
624 submenu_pixbuf = ui_theme.get_pixbuf("menu/arrow_normal.png").get_pixbuf()
625 draw_pixbuf(cr, submenu_pixbuf,
626 rect.x + rect.width - self.item_padding_x - submenu_pixbuf.get_width() - self.arrow_padding_x,
627 rect.y + (rect.height - submenu_pixbuf.get_height()) / 2)
628
629
630 propagate_expose(widget, event)
631
632 return True
633
635 '''
636 Internal callback for `enter-notify-event` signal.
637 '''
638
639 for menu_item in self.get_menu_items_callback():
640 if menu_item != self and menu_item.submenu_active:
641 menu_item.submenu_active = False
642 menu_item.item_box.queue_draw()
643
644 (item_icons, item_content, item_node) = self.item[0:3]
645 if isinstance(item_node, Menu):
646 menu_window = self.item_box.get_toplevel()
647 (menu_window_x, menu_window_y) = get_widget_root_coordinate(menu_window, WIDGET_POS_RIGHT_CENTER)
648 (item_x, item_y) = get_widget_root_coordinate(self.item_box)
649 self.show_submenu_callback(
650 item_node,
651 (menu_window_x - menu_window.shadow_radius,
652 item_y - widget.get_allocation().height - menu_window.shadow_radius),
653 self.item_box.allocation.height + menu_window.shadow_radius)
654
655 self.submenu_active = True
656 else:
657 self.hide_submenu_callback()
658