toaruos/util/python-demos/menu_bar.py

430 lines
15 KiB
Python

"""
Provides basic nested menus.
"""
import cairo
import math
import yutani
import text_region
import toaru_fonts
from icon_cache import get_icon
menu_windows = {}
def close_enough(msg):
return msg.command == yutani.MouseEvent.RAISE and \
math.sqrt((msg.new_x - msg.old_x) ** 2 + (msg.new_y - msg.old_y) ** 2) < 10
class MenuBarWidget(object):
"""Widget for display multiple menus."""
height = 24
hilight_gradient_top = (93/255,163/255,236/255)
hilight_gradient_bottom = (56/255,137/255,220/255)
def __init__(self, window, entries):
self.window = window
self.entries = entries
self.font = toaru_fonts.Font(toaru_fonts.FONT_SANS_SERIF,13,0xFFFFFFFF)
self.active_menu = None
self.active_entry = None
self.gradient = cairo.LinearGradient(0,0,0,self.height)
self.gradient.add_color_stop_rgba(0.0,*self.hilight_gradient_top,1.0)
self.gradient.add_color_stop_rgba(1.0,*self.hilight_gradient_bottom,1.0)
def draw(self, cr, x, y, width):
self.x = x
self.y = y
self.width = width
cr.save()
cr.set_source_rgb(59/255,59/255,59/255)
cr.rectangle(0,0,width,self.height)
cr.fill()
cr.restore()
menus = self.window.menus
x_, y_ = cr.user_to_device(x,y)
offset = 0
for e in self.entries:
title, _ = e
w = self.font.width(title) + 10
if self.active_menu in menus.values() and e == self.active_entry:
cr.rectangle(offset+2,0,w+2,self.height)
cr.set_source(self.gradient)
cr.fill()
tr = text_region.TextRegion(int(x_)+8+offset,int(y_)+2,w,self.height,self.font)
tr.set_one_line()
tr.set_text(title)
tr.draw(self.window)
offset += w
def mouse_event(self, msg, x, y):
offset = 0
for e in self.entries:
title, menu = e
w = self.font.width(title) + 10
if x >= offset and x < offset + w:
if msg.command == yutani.MouseEvent.CLICK or close_enough(msg):
menu = MenuWindow(menu,(self.window.x+self.window.decorator.left_width(self.window)+offset,self.window.y+self.window.decorator.top_height(self.window)+self.height),root=self.window)
self.active_menu = menu
self.active_entry = e
elif self.active_menu and self.active_menu in self.window.menus.values() and e != self.active_entry:
self.active_menu.definitely_close()
menu = MenuWindow(menu,(self.window.x+self.window.decorator.left_width(self.window)+offset,self.window.y+self.window.decorator.top_height(self.window)+self.height),root=self.window)
self.active_menu = menu
self.active_entry = e
self.window.draw()
break
offset += w
class MenuEntryAction(object):
"""Menu entry class for describing a menu entry with an action."""
# This should be calculated from the space necessary for the icon,
# but we're going to be lazy for now and just assume they're all this big.
height = 20
hilight_border_top = (54/255,128/255,205/255)
hilight_gradient_top = (93/255,163/255,236/255)
hilight_gradient_bottom = (56/255,137/255,220/55)
hilight_border_bottom = (47/255,106/255,167/255)
right_margin = 50
def __init__(self, title, icon, action=None, data=None, rich=False):
self.title = title
self.icon = get_icon(icon,16) if icon else None
self.action = action
self.data = data
self.font = toaru_fonts.Font(toaru_fonts.FONT_SANS_SERIF,13,0xFF000000)
self.font_hilight = toaru_fonts.Font(toaru_fonts.FONT_SANS_SERIF,13,0xFFFFFFFF)
self.rich = rich
self.width = self.font.width(self.title) + self.right_margin # Arbitrary bit of extra space.
# Fit width to hold title?
self.tr = text_region.TextRegion(0,0,self.width - 22, 20, self.font)
self.update_text()
self.hilight = False
self.window = None
self.gradient = cairo.LinearGradient(0,0,0,self.height-2)
self.gradient.add_color_stop_rgba(0.0,*self.hilight_gradient_top,1.0)
self.gradient.add_color_stop_rgba(1.0,*self.hilight_gradient_bottom,1.0)
def update_text(self):
if self.rich:
self.tr.set_richtext(self.title)
else:
self.tr.set_text(self.title)
self.width = self.font.width(self.title) + self.right_margin # Arbitrary bit of extra space.
if self.rich:
try:
self.width = self.tr.get_offset_at_index(-1)[1][1] + self.right_margin
except:
pass # uh, fallback to the original width
self.tr.resize(self.width - 22, 20)
def focus_enter(self,keyboard=False):
if self.window and self.window.child:
self.window.child.definitely_close()
self.tr.set_font(self.font_hilight)
self.update_text()
self.hilight = True
def focus_leave(self):
self.tr.set_font(self.font)
self.update_text()
self.hilight = False
def draw(self, window, offset, ctx):
# Here, offset = y offset, not x like in panel widgets
# eventually, this all needs to be made generic with containers and calculated window coordinates...
# but for now, as always, we're being lazy
self.window = window
self.offset = offset
if self.hilight:
ctx.rectangle(1,offset,window.width-2,1)
ctx.set_source_rgb(*self.hilight_border_top)
ctx.fill()
ctx.rectangle(1,offset+self.height-1,window.width-2,1)
ctx.set_source_rgb(*self.hilight_border_bottom)
ctx.fill()
ctx.save()
ctx.translate(0,offset+1)
ctx.rectangle(1,0,window.width-2,self.height-2)
ctx.set_source(self.gradient)
ctx.fill()
ctx.restore()
if self.icon:
ctx.save()
ctx.translate(4,offset+2)
if self.icon.get_width != 16:
ctx.scale(16/self.icon.get_width(),16/self.icon.get_width())
ctx.set_source_surface(self.icon,0,0)
ctx.paint()
ctx.restore()
self.tr.move(22,offset+2)
self.tr.draw(window)
def activate(self):
if self.action:
self.action(self.data) # Probably like launch_app("terminal")
self.window.root.hovered_menu = None
self.focus_leave()
m = [m for m in self.window.root.menus.values()]
for k in m:
k.definitely_close()
self.window.root.draw()
def close_enough(self, msg):
return close_enough(msg) and msg.old_y >= self.offset and msg.old_y < self.offset + self.height
def mouse_action(self, msg):
if msg.command == yutani.MouseEvent.CLICK or self.close_enough(msg):
self.activate()
return False
class MenuEntrySubmenu(MenuEntryAction):
"""A menu entry which opens a nested submenu."""
def __init__(self, title, entries, icon="folder"):
super(MenuEntrySubmenu,self).__init__(title,icon,None,None)
self.entries = entries
self.tick = get_icon('menu-tick', 16)
self.tick_hilight = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.tick.get_width(), self.tick.get_height())
tmp = cairo.Context(self.tick_hilight)
tmp.set_operator(cairo.OPERATOR_SOURCE)
tmp.set_source_surface(self.tick,0,0)
tmp.paint()
tmp.set_operator(cairo.OPERATOR_ATOP)
tmp.rectangle(0,0,16,16)
tmp.set_source_rgb(1,1,1)
tmp.paint()
def draw(self, window, offset, ctx):
super(MenuEntrySubmenu,self).draw(window, offset, ctx)
ctx.save()
ctx.set_source_surface(self.tick if not self.hilight else self.tick_hilight,window.width-16, self.offset + 2)
ctx.paint()
ctx.restore()
def focus_enter(self,keyboard=False):
self.tr.set_font(self.font_hilight)
self.tr.set_text(self.title)
self.hilight = True
if not keyboard:
self.activate()
def activate(self):
if self.window:
menu = MenuWindow(self.entries, (self.window.x + self.window.width - 2, self.window.y + self.offset - self.window.top_height), self.window, root=self.window.root)
def mouse_action(self, msg):
return False
class MenuEntryDivider(object):
"""A visible separator between menu entries. Does nothing."""
height = 6
width = 0
def draw(self, window, offset, ctx):
self.window = window
ctx.rectangle(2,offset+3,window.width-4,1)
ctx.set_source_rgb(0.7,0.7,0.7)
ctx.fill()
ctx.rectangle(2,offset+4,window.width-5,1)
ctx.set_source_rgb(0.98,0.98,0.98)
ctx.fill()
def focus_enter(self,keyboard=False):
if self.window and self.window.child:
self.window.child.definitely_close()
pass
def focus_leave(self):
pass
def mouse_action(self,msg):
return False
class MenuWindow(yutani.Window):
"""Nested menu window."""
# These should be part of some theming functionality, but for now we'll
# just embed them here in the MenuWindow class.
top_height = 4
bottom_height = 4
base_background = (239/255,238/255,232/255)
base_border = (109/255,111/255,112/255)
def __init__(self, entries, origin=(0,0), parent=None, root=None):
self.parent = parent
if self.parent:
self.parent.child = self
self.entries = entries
required_width = max([e.width for e in entries])
required_height = sum([e.height for e in entries]) + self.top_height + self.bottom_height
flags = yutani.WindowFlag.FLAG_ALT_ANIMATION
super(MenuWindow, self).__init__(required_width,required_height,doublebuffer=True,flags=flags)
menu_windows[self.wid] = self
self.move(*origin)
self.focused_widget = None
self.child = None
self.closed = False
self.root = root
self.root.menus[self.wid] = self
self.draw()
def draw(self):
surface = self.get_cairo_surface()
ctx = cairo.Context(surface)
ctx.set_operator(cairo.OPERATOR_SOURCE)
ctx.rectangle(0,0,self.width,self.height)
ctx.set_line_width(2)
ctx.set_source_rgb(*self.base_background)
ctx.fill_preserve()
ctx.set_source_rgb(*self.base_border)
ctx.stroke()
ctx.set_operator(cairo.OPERATOR_OVER)
offset = self.top_height
for entry in self.entries:
entry.draw(self,offset,ctx)
offset += entry.height
self.flip()
def mouse_action(self, msg):
if msg.new_y < self.top_height or msg.new_y >= self.height - self.bottom_height or \
msg.new_x < 0 or msg.new_x >= self.width:
if self.focused_widget:
self.focused_widget.focus_leave()
self.focused_widget = None
self.draw()
return
# We must have focused something
x = (msg.new_y - self.top_height)
offset = 0
new_widget = None
for entry in self.entries:
if x >= offset and x < offset + entry.height:
new_widget = entry
break
offset += entry.height
redraw = False
if new_widget:
if self.focused_widget != new_widget:
if self.focused_widget:
self.focused_widget.focus_leave()
new_widget.focus_enter(keyboard=False)
self.focused_widget = new_widget
redraw = True
if new_widget.mouse_action(msg):
redraw = True
if redraw:
self.draw()
def has_eventual_child(self, child):
"""Does this menu have the given menu as a child, or a child of a child, etc.?"""
if child is self: return True
if not self.child: return False
return self.child.has_eventual_child(child)
def keyboard_event(self, msg):
"""Handle keyboard."""
if msg.event.action != yutani.KeyAction.ACTION_DOWN:
return
if msg.event.keycode == yutani.Keycode.ESCAPE:
self.root.hovered_menu = None
self.root.menus[msg.wid].leave_menu()
return
self.root.hovered_menu = self
if msg.event.keycode == yutani.Keycode.ARROW_DOWN:
if not self.focused_widget and self.entries:
self.focused_widget = self.entries[0]
self.focused_widget.focus_enter(keyboard=True)
self.draw()
return
i = 0
for entry in self.entries:
if entry == self.focused_widget:
break
i += 1
i += 1
if i >= len(self.entries):
i = 0
self.focused_widget.focus_leave()
self.focused_widget = self.entries[i]
self.focused_widget.focus_enter(keyboard=True)
self.draw()
if msg.event.keycode == yutani.Keycode.ARROW_UP:
if not self.focused_widget and self.entries:
self.focused_widget = self.entries[0]
self.focused_widget.focus_enter(keyboard=True)
self.draw()
return
i = 0
for entry in self.entries:
if entry == self.focused_widget:
break
i += 1
i -= 1
if i < 0:
i = len(self.entries)-1
self.focused_widget.focus_leave()
self.focused_widget = self.entries[i]
self.focused_widget.focus_enter(keyboard=True)
self.draw()
if msg.event.keycode == yutani.Keycode.ARROW_LEFT:
self.root.hovered_menu = self.parent
self.definitely_close()
return
if msg.event.keycode == yutani.Keycode.ARROW_RIGHT:
if self.focused_widget and isinstance(self.focused_widget, MenuEntrySubmenu):
self.focused_widget.activate()
if msg.event.key == b'\n':
if self.focused_widget:
self.focused_widget.activate()
def close(self):
if self.wid in menu_windows:
del menu_windows[self.wid]
super(MenuWindow,self).close()
def definitely_close(self):
"""Close this menu and all of its submenus."""
if self.child:
self.child.definitely_close()
self.child = None
if self.closed:
return
if self.focused_widget:
self.focused_widget.focus_leave()
self.closed = True
wid = self.wid
self.close()
del self.root.menus[wid]
def leave_menu(self):
"""Focus has left this menu. If it is not a parent of the currently active menu, close it."""
if self.has_eventual_child(self.root.hovered_menu):
pass
else:
m = [m for m in self.root.menus.values()]
for k in m:
if not self.root.hovered_menu or (k is not self.root.hovered_menu.child and not k.has_eventual_child(self.root.hovered_menu)):
k.definitely_close()