#!/usr/bin/python3
"""
Panel - Displays a window with various widgets at the top of the screen.
This panel is based on the original C implementation in ToaruOS, but is focused
on providing a more easily extended widget system. Each element of the panel is
a widget object which can independently draw and receive mouse events.
"""
import calendar
import configparser
import html
import json
import math
import os
import signal
import sys
import shlex
import subprocess
import time
import cairo
import yutani
import text_region
import toaru_fonts
import fswait
#import toaru_webp
from menu_bar import MenuEntryAction, MenuEntrySubmenu, MenuEntryDivider, MenuWindow
from icon_cache import get_icon
import toaru_theme
PANEL_HEIGHT=28
def create_from_bmp(path):
return yutani.Sprite.from_file(path).get_cairo_surface()
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 BaseWidget(object):
"""Base class for a panel widget."""
width = 0
def draw(self, window, offset, remaining, ctx):
pass
def focus_enter(self):
pass
def focus_leave(self):
pass
def mouse_action(self, msg):
return False
class FillWidget(BaseWidget):
"""Fills the panel with blank space. Only one such panel element should exist at a time."""
width = -1
class CalendarMenuEntry(MenuEntryDivider):
height = 130
width = 200
def __init__(self):
self.font = toaru_fonts.Font(toaru_fonts.FONT_MONOSPACE,13,toaru_theme.menu_entry_text)
self.tr = text_region.TextRegion(0,0,self.width-20,self.height,font=self.font)
self.calendar = calendar.TextCalendar(calendar.SUNDAY)
t = time.localtime(current_time)
self.tr.set_line_height(17)
self.tr.set_text(self.calendar.formatmonth(t.tm_year,t.tm_mon))
for tu in self.tr.text_units:
if tu.string == str(t.tm_mday):
tu.set_font(toaru_fonts.Font(toaru_fonts.FONT_MONOSPACE_BOLD,13,toaru_theme.menu_entry_text))
break
def draw(self, window, offset, ctx):
self.window = window
self.tr.move(20,offset)
self.tr.draw(window)
class ClockWidget(BaseWidget):
"""Displays a simple clock"""
text_y_offset = 4
width = 80
color = toaru_theme.panel_widget_foreground
font_size = 16
alignment = 0
time_format = '%H:%M:%S'
hilight = toaru_theme.panel_widget_hilight
def __init__(self):
self.font = toaru_fonts.Font(toaru_fonts.FONT_SANS_SERIF, self.font_size, self.color)
self.tr = text_region.TextRegion(0,0,self.width,PANEL_HEIGHT-self.text_y_offset,font=self.font)
self.tr.set_alignment(self.alignment)
self.offset = 0
def draw(self, window, offset, remaining, ctx):
self.offset = offset
self.window = window
self.tr.move(offset,self.text_y_offset)
self.tr.set_richtext(time.strftime(self.time_format,time.localtime(current_time)))
self.tr.draw(window)
def focus_enter(self):
self.font.font_color = self.hilight
def focus_leave(self):
self.font.font_color = self.color
def mouse_action(self, msg):
if msg.command == yutani.MouseEvent.CLICK or close_enough(msg):
def _pass(action):
pass
menu_entries = [
CalendarMenuEntry(),
]
menu = MenuWindow(menu_entries,(self.offset-120,self.window.height),root=self.window)
class DateWidget(ClockWidget):
"""Displays the weekday and date on separate lines."""
text_y_offset = 4
color = toaru_theme.panel_widget_foreground
width = 70
font_size = 9
alignment = 2
time_format = '%A\n%B %e'
class LogOutWidget(BaseWidget):
"""Simple button widget that ends the user session when clicked."""
# TODO: Present a log out / restart menu instead.
width = 28
path = '/usr/share/icons/panel-shutdown.bmp'
def __init__(self):
self.icon = create_from_bmp(self.path)
self.icon_hilight = create_from_bmp(self.path)
tmp = cairo.Context(self.icon_hilight)
tmp.set_operator(cairo.OPERATOR_ATOP)
tmp.rectangle(0,0,24,24)
tmp.set_source_rgb(*toaru_theme.as_rgb_tuple(toaru_theme.panel_widget_hilight))
tmp.paint()
self.hilighted = False
def draw(self, window, offset, remaining, ctx):
self.offset = offset
self.window = window
if self.hilighted:
ctx.set_source_surface(self.icon_hilight,offset+2,2)
else:
ctx.set_source_surface(self.icon,offset+2,2)
ctx.paint()
def focus_enter(self):
self.hilighted = True
def focus_leave(self):
self.hilighted = False
def mouse_action(self, msg):
if msg.command == yutani.MouseEvent.CLICK or close_enough(msg):
menu_entries = [
MenuEntryAction("Log Out","exit",logout_callback,None),
]
menu = MenuWindow(menu_entries,(self.offset-120,self.window.height),root=self.window)
menu.move(self.window.width - menu.width, self.window.height)
return False
class RestartMenuWidget(LogOutWidget):
def mouse_action(self, msg):
if msg.command == yutani.MouseEvent.CLICK or close_enough(msg):
def exit(action):
if 'callback' in dir(self):
self.callback()
else:
sys.exit(0)
menu_entries = [
MenuEntryAction("Restart","exit",exit,None),
]
menu = MenuWindow(menu_entries,(self.offset-120,self.window.height),root=self.window)
menu.move(self.window.width - menu.width, self.window.height)
return False
class LabelWidget(BaseWidget):
"""Provides a menu of applications to launch."""
text_y_offset = 4
text_x_offset = 10
color = toaru_theme.panel_widget_foreground
hilight = toaru_theme.panel_widget_hilight
def __init__(self, text):
self.width = 140
self.font = toaru_fonts.Font(toaru_fonts.FONT_SANS_SERIF_BOLD, 14, self.color)
self.tr = text_region.TextRegion(0,0,self.width-self.text_x_offset*2,PANEL_HEIGHT-self.text_y_offset,font=self.font)
self.tr.set_text(text)
def draw(self, window, offset, remaining, ctx):
self.window = window
self.tr.move(offset+self.text_x_offset,self.text_y_offset)
self.tr.draw(window)
def focus_enter(self):
self.font.font_color = self.hilight
def focus_leave(self):
self.font.font_color = self.color
def activate(self):
pass # Extend this as needed
def mouse_action(self,msg):
if msg.command == yutani.MouseEvent.CLICK or close_enough(msg):
self.activate()
class MouseModeWidget(BaseWidget):
"""Controls the mouse mode in VMs."""
width = 28
color = toaru_theme.as_rgb_tuple(toaru_theme.panel_widget_foreground)
hilight_color = toaru_theme.as_rgb_tuple(toaru_theme.panel_widget_hilight)
icon_names = ['mouse-status', 'mouse-relative']
check_time = 10
def __init__(self):
self.icons = {}
self.icons_hilight = {}
for name in self.icon_names:
self.icons[name] = create_from_bmp(f'/usr/share/icons/24/{name}.bmp')
tmp = cairo.Context(self.icons[name])
tmp.set_operator(cairo.OPERATOR_ATOP)
tmp.rectangle(0,0,24,24)
tmp.set_source_rgb(*self.color)
tmp.paint()
self.icons_hilight[name] = create_from_bmp(f'/usr/share/icons/24/{name}.bmp')
tmp = cairo.Context(self.icons_hilight[name])
tmp.set_operator(cairo.OPERATOR_ATOP)
tmp.rectangle(0,0,24,24)
tmp.set_source_rgb(*self.hilight_color)
tmp.paint()
self.hilighted = False
self.absolute = True
if not os.path.exists('/dev/vmmouse') and not os.path.exists('/dev/absmouse'):
self.width = 0
def focus_enter(self):
self.hilighted = True
def focus_leave(self):
self.hilighted = False
def draw(self, window, offset, remaining, ctx):
if not self.width:
return
source = 'mouse-status' if self.absolute else 'mouse-relative'
if self.hilighted:
ctx.set_source_surface(self.icons_hilight[source],offset,2)
else:
ctx.set_source_surface(self.icons[source],offset,2)
ctx.paint()
def mouse_action(self, msg):
if msg.command == yutani.MouseEvent.CLICK or close_enough(msg):
if self.absolute:
launch_app('toggle_vmware_mouse.py relative')
self.absolute = False
else:
launch_app('toggle_vmware_mouse.py absolute')
self.absolute = True
return True
class WeatherWidget(BaseWidget):
"""Collects weather information from an API."""
text_y_offset = 4
width = 50
color = toaru_theme.panel_widget_foreground
font_size = 14
alignment = 0
check_time = 7200 # 2hr
update_time = 60
icon_width = 24
hilight = toaru_theme.panel_widget_hilight
icon_color = toaru_theme.as_rgb_tuple(toaru_theme.panel_widget_foreground)
hilight_color = toaru_theme.as_rgb_tuple(toaru_theme.panel_widget_hilight)
data_path = '/tmp/weather.json'
icons_path = '/usr/share/icons/weather/'
def __init__(self):
self.font = toaru_fonts.Font(toaru_fonts.FONT_SANS_SERIF, self.font_size, self.color)
self.tr = text_region.TextRegion(0,0,self.width-self.icon_width,PANEL_HEIGHT-self.text_y_offset,font=self.font)
self.tr.set_alignment(self.alignment)
self.offset = 0
self.last_check = 0
self.last_update = 0
self.icon = None
self.hilighted = False
def draw(self, window, offset, remaining, ctx):
self.check()
self.offset = offset
self.window = window
if self.icon:
icon = self.icon_hilight if self.hilighted else self.icon
ctx.save()
ctx.translate(offset,0)
if icon.get_width() != self.icon_width:
ctx.scale(self.icon_width/icon.get_width(),self.icon_width/icon.get_width())
ctx.set_source_surface(icon,0,0)
ctx.paint()
ctx.restore()
self.tr.move(offset + self.icon_width,self.text_y_offset)
else:
self.tr.move(offset,self.text_y_offset)
self.tr.draw(window)
def focus_enter(self):
self.font.font_color = self.hilight
self.hilighted = True
def focus_leave(self):
self.font.font_color = self.color
self.hilighted = False
def check(self):
if current_time - self.last_check > self.check_time:
# Every so often, try to colect new weather data.
self.last_check = current_time
subprocess.Popen(['/usr/share/python-demos/weather_tool.py'])
self.update_time = 1 # Try to collect data every seconds.
if current_time - self.last_update > self.update_time:
self.last_update = current_time
try:
with open(self.data_path,'r') as f:
weather = json.loads(f.read())
except:
self.icon = None
self.tr.set_richtext("")
self.width = 0
if current_time - self.last_check > 10:
self.update_time = 60 # It's taken more than ten seconds to collect, give up for now.
return
self.last_modified = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime(os.path.getmtime(self.data_path)))
# We succeeded, so we don't need to try again until we run the update tool again.
# In case the update tool runs on its own (triggered update from change in city, etc),
# we'll check every minute.
self.update_time = 60
self.tr.set_richtext(f"{weather['temp_r']}°")
self.width = self.icon_width + self.tr.get_offset_at_index(-1)[1][1]
if weather['conditions'] and os.path.exists(f"{self.icons_path}{weather['icon']}.bmp"):
self.icon = create_from_bmp(f"{self.icons_path}{weather['icon']}.bmp")
tmp = cairo.Context(self.icon)
tmp.set_operator(cairo.OPERATOR_ATOP)
tmp.rectangle(0,0,24,24)
tmp.set_source_rgb(*self.icon_color)
tmp.paint()
self.icon_hilight = create_from_bmp(f"{self.icons_path}{weather['icon']}.bmp")
tmp = cairo.Context(self.icon_hilight)
tmp.set_operator(cairo.OPERATOR_ATOP)
tmp.rectangle(0,0,24,24)
tmp.set_source_rgb(*self.hilight_color)
tmp.paint()
else:
self.icon = None
self.weather = weather
def mouse_action(self, msg):
if msg.command == yutani.MouseEvent.CLICK or close_enough(msg):
def _pass(action):
pass
def open_browser(site):
launch_app(f"help-browser.py {site}")
def refresh_weather(action):
self.last_check = 0
self.check()
menu_entries = [
MenuEntryAction(f"Weather for {self.weather['city']}",None,_pass,None,rich=True),
MenuEntryAction(self.last_modified,None,_pass,None),
MenuEntryDivider(),
MenuEntryAction(f"{self.weather['temp']:.2f}° - {self.weather['conditions']}",None,_pass,None,rich=True),
MenuEntryAction(f"Humidity: {self.weather['humidity']}%",None,_pass,None,rich=True),
MenuEntryAction(f"Clouds: {self.weather['clouds']}%",None,_pass,None,rich=True),
MenuEntryDivider(),
MenuEntryAction(f"Refresh...","refresh",refresh_weather,None),
MenuEntryAction(f"Configure...","config",launch_app,"weather_tool.py --config"),
MenuEntryDivider(),
MenuEntryAction(f"Weather data provided by",None,_pass,None),
MenuEntryAction(f"OpenWeatherMap.org",None,open_browser,"http://openweathermap.org/",rich=True),
]
menu = MenuWindow(menu_entries,(self.offset-120,self.window.height),root=self.window)
class VolumeWidget(BaseWidget):
"""Volume control widget."""
width = 28
color = toaru_theme.as_rgb_tuple(toaru_theme.panel_widget_foreground)
hilight_color = toaru_theme.as_rgb_tuple(toaru_theme.panel_widget_hilight)
icon_names = ['volume-mute','volume-low','volume-medium','volume-full']
check_time = 10
def __init__(self):
self.icons = {}
self.icons_hilight = {}
for name in self.icon_names:
self.icons[name] = create_from_bmp(f'/usr/share/icons/24/{name}.bmp')
tmp = cairo.Context(self.icons[name])
tmp.set_operator(cairo.OPERATOR_ATOP)
tmp.rectangle(0,0,24,24)
tmp.set_source_rgb(*self.color)
tmp.paint()
self.icons_hilight[name] = create_from_bmp(f'/usr/share/icons/24/{name}.bmp')
tmp = cairo.Context(self.icons_hilight[name])
tmp.set_operator(cairo.OPERATOR_ATOP)
tmp.rectangle(0,0,24,24)
tmp.set_source_rgb(*self.hilight_color)
tmp.paint()
try:
self.mixer_fd = open('/dev/mixer')
except:
self.mixer_fd = None
self.volume = self.get_volume()
self.muted = False
self.previous_volume = 0
self.hilighted = False
self.last_check = 0
def focus_enter(self):
self.hilighted = True
def focus_leave(self):
self.hilighted = False
def draw(self, window, offset, remaining, ctx):
self.check()
if self.volume < 10:
source = 'volume-mute'
elif self.volume < 0x547ae147:
source = 'volume-low'
elif self.volume < 0xa8f5c28e:
source = 'volume-medium'
else:
source = 'volume-full'
if self.hilighted:
ctx.set_source_surface(self.icons_hilight[source],offset,2)
else:
ctx.set_source_surface(self.icons[source],offset,2)
ctx.paint()
def check(self):
if current_time - self.last_check > self.check_time:
self.last_check = current_time
self.volume = self.get_volume()
def get_volume(self):
"""Get the current mixer master volume."""
import fcntl
import struct
knob = bytearray(struct.pack("III", 0, 0, 0)) # VOLUME_DEVICE_ID, VOLUME_KNOB_ID,
try:
fcntl.ioctl(self.mixer_fd, 2, knob, True)
_,_,value = struct.unpack("III", knob)
return value
except:
return 0
def set_volume(self):
"""Set the mixer master volume to the widget's volume level."""
import fcntl
import struct
try:
knob = struct.pack("III", 0, 0, self.volume) # VOLUME_DEVICE_ID, VOLUME_KNOB_ID, volume_level
fcntl.ioctl(self.mixer_fd, 3, knob)
except:
pass
def volume_up(self):
self.volume += 0x8000000
if self.volume >= 0x100000000:
self.volume = 0xf8000000
self.set_volume()
def volume_down(self):
self.volume -= 0x8000000
if self.volume < 0:
self.volume = 0
self.set_volume()
def mouse_action(self, msg):
if msg.command == yutani.MouseEvent.CLICK or close_enough(msg):
if self.muted:
self.muted = False
self.volume = self.previous_volume
self.set_volume()
else:
self.muted = True
self.previous_volume = self.volume
self.volume = 0
self.set_volume()
return True
else:
if msg.buttons & yutani.MouseButton.SCROLL_UP:
self.volume_up()
return True
elif msg.buttons & yutani.MouseButton.SCROLL_DOWN:
self.volume_down()
return True
class NetworkWidget(BaseWidget):
"""Volume control widget."""
width = 28
color = toaru_theme.as_rgb_tuple(toaru_theme.panel_widget_foreground)
hilight_color = toaru_theme.as_rgb_tuple(toaru_theme.panel_widget_hilight)
icon_names = ['net-active','net-disconnected']
check_time = 10
def __init__(self):
self.icons = {}
self.icons_hilight = {}
for name in self.icon_names:
self.icons[name] = create_from_bmp(f'/usr/share/icons/24/{name}.bmp')
tmp = cairo.Context(self.icons[name])
tmp.set_operator(cairo.OPERATOR_ATOP)
tmp.rectangle(0,0,24,24)
tmp.set_source_rgb(*self.color)
tmp.paint()
self.icons_hilight[name] = create_from_bmp(f'/usr/share/icons/24/{name}.bmp')
tmp = cairo.Context(self.icons_hilight[name])
tmp.set_operator(cairo.OPERATOR_ATOP)
tmp.rectangle(0,0,24,24)
tmp.set_source_rgb(*self.hilight_color)
tmp.paint()
self.hilighted = False
self.ip = None
self.mac = None
self.gw = None
self.device = None
self.dns = None
self.last_check = 0
self.status = 0
def focus_enter(self):
self.hilighted = True
def focus_leave(self):
self.hilighted = False
def update(self):
self.last_check = current_time
with open('/proc/netif','r') as f:
lines = f.readlines()
if len(lines) < 4 or "no network" in lines[0]:
self.status = 0
else:
self.status = 1
_,self.ip = lines[0].strip().split('\t')
_,self.mac = lines[1].strip().split('\t')
_,self.device = lines[2].strip().split('\t')
_,self.dns = lines[3].strip().split('\t')
_,self.gw = lines[4].strip().split('\t')
def check(self):
if current_time - self.last_check > self.check_time:
self.update()
def draw(self, window, offset, remaining, ctx):
self.check()
self.offset = offset
self.window = window
if self.status == 1:
source = 'net-active'
else:
source = 'net-disconnected'
if self.hilighted:
ctx.set_source_surface(self.icons_hilight[source],offset,2)
else:
ctx.set_source_surface(self.icons[source],offset,2)
ctx.paint()
def mouse_action(self, msg):
if msg.command == yutani.MouseEvent.CLICK or close_enough(msg):
def _pass(action):
pass
self.update()
if self.status == 1:
menu_entries = [
MenuEntryAction(f"Network Status",None,_pass,None,rich=True),
MenuEntryDivider(),
MenuEntryAction(f"IP: {self.ip}",None,_pass,None,rich=True),
MenuEntryAction(f"Primary DNS: {self.dns}",None,_pass,None,rich=True),
MenuEntryAction(f"Gateway: {self.gw}",None,_pass,None,rich=True),
MenuEntryAction(f"MAC: {self.mac}",None,_pass,None,rich=True),
MenuEntryAction(f"Device: {self.device}",None,_pass,None,rich=True),
]
else:
menu_entries = [
MenuEntryAction(f"No network.",None,_pass,None),
]
menu = MenuWindow(menu_entries,(self.offset-100,PANEL_HEIGHT),root=self.window)
class WindowListWidget(FillWidget):
"""Displays a list of windows with icons and titles."""
text_y_offset = 5
color = toaru_theme.panel_widget_foreground
hilight = toaru_theme.panel_widget_hilight
icon_width = 48
def __init__(self):
self.font = toaru_fonts.Font(toaru_fonts.FONT_SANS_SERIF, 13, self.color)
self.font.set_shadow(toaru_theme.panel_window_shadow)
self.font_hilight = toaru_fonts.Font(toaru_fonts.FONT_SANS_SERIF, 13, self.hilight)
self.font_hilight.set_shadow(toaru_theme.panel_window_shadow)
self.gradient = cairo.LinearGradient(0,0,0,PANEL_HEIGHT)
self.gradient.add_color_stop_rgba(*toaru_theme.panel_window_gradient_top)
self.gradient.add_color_stop_rgba(*toaru_theme.panel_window_gradient_low)
self.divider = cairo.LinearGradient(0,0,0,PANEL_HEIGHT)
self.divider.add_color_stop_rgba(*toaru_theme.panel_window_divider_top)
self.divider.add_color_stop_rgba(*toaru_theme.panel_window_divider_mid)
self.divider.add_color_stop_rgba(*toaru_theme.panel_window_divider_low)
self.hovered = None
self.unit_width = None
self.offset = 0
def draw(self, window, offset, remaining, ctx):
global windows
if not len(windows):
return
self.window = window
self.offset = offset
available_width = remaining - offset
self.unit_width = min(int(available_width / len(windows)),180)
icon_width = self.icon_width
if self.unit_width < 56:
self.unit_width = 32
icon_width = 24
i = 0
for w in windows:
if w.flags & 1:
ctx.set_source(self.gradient)
ctx.rectangle(offset+4,0,self.unit_width-4,PANEL_HEIGHT)
ctx.fill()
icon = get_icon(w.icon, icon_width)
ctx.save()
ctx.translate(offset + self.unit_width - icon_width - 2,0)
ctx.rectangle(0,0,icon_width + 4,PANEL_HEIGHT-2)
ctx.clip()
if icon.get_width() != icon_width:
ctx.scale(icon_width/icon.get_width(),icon_width/icon.get_width())
ctx.set_source_surface(icon,0,0)
if self.unit_width < 48:
ctx.paint()
else:
ctx.paint_with_alpha(0.7)
if i < len(windows) - 1:
ctx.rectangle(icon_width + 3,0,1,PANEL_HEIGHT)
ctx.set_source(self.divider)
ctx.fill()
ctx.restore()
offset_left = 4
if self.unit_width > 48:
font = self.font
if self.hovered == w.wid:
font = self.font_hilight
tr = text_region.TextRegion(offset+4+offset_left,self.text_y_offset,self.unit_width - 6 - offset_left,PANEL_HEIGHT-self.text_y_offset,font=font)
tr.set_one_line()
tr.set_ellipsis()
tr.set_text(w.name)
tr.draw(window)
offset += self.unit_width
i += 1
def focus_leave(self):
self.hovered = None
def mouse_action(self, msg):
if not len(windows):
return
msg.new_x -= self.offset
hovered_index = int(msg.new_x / self.unit_width)
previously_hovered = self.hovered
if hovered_index < len(windows):
self.hovered = windows[hovered_index].wid
if msg.command == yutani.MouseEvent.CLICK or close_enough(msg):
yctx.focus_window(self.hovered)
elif msg.buttons & yutani.MouseButton.BUTTON_RIGHT:
if not self.window.menus:
def move_window(window):
yutani.yutani_lib.yutani_window_drag_start_wid(yutani.yutani_ctx._ptr,window)
#def close_window(window):
# print("Should close window",window)
menu_entries = [
MenuEntryAction("Move",None,move_window,self.hovered),
#MenuEntryAction("Close",None,close_window,self.hovered)
]
menu = MenuWindow(menu_entries,(msg.new_x+self.offset,PANEL_HEIGHT),root=self.window)
else:
self.hovered = None
return self.hovered != previously_hovered
class ApplicationsMenuWidget(BaseWidget):
"""Provides a menu of applications to launch."""
text_y_offset = 4
text_x_offset = 10
color = toaru_theme.panel_widget_foreground
hilight = toaru_theme.panel_widget_hilight
def __init__(self):
self.width = 140
self.font = toaru_fonts.Font(toaru_fonts.FONT_SANS_SERIF_BOLD, 14, self.color)
self.tr = text_region.TextRegion(0,0,self.width-self.text_x_offset*2,PANEL_HEIGHT-self.text_y_offset,font=self.font)
self.tr.set_text("Applications")
self.reinit_menus()
def extra(self, name):
if not os.path.exists(f'/usr/share/menus/{name}'):
return []
else:
out = []
for icon in os.listdir(f'/usr/share/menus/{name}'):
with open(f'/usr/share/menus/{name}/{icon}','r') as f:
icon,command,title = f.read().strip().split(',')
out.append(MenuEntryAction(title,icon,launch_app,command))
return out
def reinit_menus(self):
accessories = [
MenuEntryAction("Calculator","calculator",launch_app,"calculator.py"),
MenuEntryAction("Clock Widget","clock",launch_app,"clock.py"),
MenuEntryAction("File Browser","folder",launch_app,"file_browser.py"),
MenuEntryAction("Terminal","utilities-terminal",launch_app,"terminal"),
]
accessories.extend(self.extra('accessories'))
demos = [
MenuEntrySubmenu("Cairo",[
MenuEntryAction("Cairo Demo","cairo-demo",launch_app,"cairo-demo"),
MenuEntryAction("Cairo Snow","snow",launch_app,"make-it-snow"),
MenuEntryAction("Pixman Demo","pixman-demo",launch_app,"pixman-demo"),
]),
MenuEntryAction("Draw Lines","drawlines",launch_app,"drawlines"),
MenuEntryAction("Julia Fractals","julia",launch_app,"julia"),
MenuEntryAction("Plasma","plasma",launch_app,"plasma"),
]
demos.extend(self.extra('demos'))
games = [
MenuEntryAction("Mines","mines",launch_app,"mines.py"),
]
games.extend(self.extra('games'))
graphics = [
MenuEntryAction("ToaruPaint","applications-painting",launch_app,"painting.py"),
]
graphics.extend(self.extra('graphics'))
settings = [
MenuEntryAction("Package Manager","package",launch_app,"gsudo package_manager.py"),
MenuEntryAction("Select Wallpaper","select-wallpaper",launch_app,"select_wallpaper.py"),
]
settings.extend(self.extra('settings'))
self.menu_entries = [
MenuEntrySubmenu("Accessories",accessories),
MenuEntrySubmenu("Demos",demos),
MenuEntrySubmenu("Games",games),
MenuEntrySubmenu("Graphics",graphics),
MenuEntrySubmenu("Settings",settings),
MenuEntryDivider(),
MenuEntryAction("Help","help",launch_app,"help-browser.py"),
MenuEntryAction("About ToaruOS","star",launch_app,"about-applet.py"),
MenuEntryAction("Log Out","exit",logout_callback,""),
]
def draw(self, window, offset, remaining, ctx):
self.window = window
self.tr.move(offset+self.text_x_offset,self.text_y_offset)
self.tr.draw(window)
def focus_enter(self):
self.font.font_color = self.hilight
def focus_leave(self):
self.font.font_color = self.color
def activate(self):
menu = MenuWindow(self.menu_entries,(0,self.window.height),root=self.window)
def mouse_action(self,msg):
if msg.command == yutani.MouseEvent.CLICK or close_enough(msg):
self.activate()
class PanelWindow(yutani.Window):
"""The panel itself."""
def __init__(self, widgets):
self.widgets = widgets
flags = yutani.WindowFlag.FLAG_NO_STEAL_FOCUS | yutani.WindowFlag.FLAG_DISALLOW_DRAG | yutani.WindowFlag.FLAG_DISALLOW_RESIZE
super(PanelWindow, self).__init__(yutani.yutani_ctx._ptr.contents.display_width,PANEL_HEIGHT,doublebuffer=True,flags=flags)
self.move(0,0)
self.set_stack(yutani.WindowStackOrder.ZORDER_TOP)
self.focused_widget = None
self.menus = {}
self.hovered_menu = None
# Panel background
self.background = create_from_bmp('/usr/share/panel.bmp')
self.background_pattern = cairo.SurfacePattern(self.background)
self.background_pattern.set_extend(cairo.EXTEND_REPEAT)
self.visible = True
def toggle_visibility(self):
if not self.visible:
for i in range(PANEL_HEIGHT-1,-1,-1):
self.move(0,-i)
yutani.usleep(10000)
self.visible = True
else:
for i in range(1,PANEL_HEIGHT,1):
self.move(0,-i)
yutani.usleep(10000)
self.visible = False
def draw(self):
"""Draw the window."""
surface = self.get_cairo_surface()
ctx = cairo.Context(surface)
something_changed = False
for i in range(2):
# Minor hack - if a widget changes its width during the draw call
# and it's on the right side, we need to redraw again. We'll go through
# one draw call with everything, in case multiple widgets changed widths
# and then redraw them all again. If something changes the second time,
# well, tough luck.
ctx.set_operator(cairo.OPERATOR_SOURCE)
ctx.set_source(self.background_pattern)
ctx.paint()
ctx.set_operator(cairo.OPERATOR_OVER)
offset = 0
index = 0
for widget in self.widgets:
index += 1
remaining = 0 if widget.width != -1 else self.width - sum([widget.width for widget in self.widgets[index:]])
before = widget.width
widget.draw(self, offset, remaining, ctx)
if widget.width != before:
something_changed = True
offset = offset + widget.width if widget.width != -1 else remaining
if not something_changed:
break
self.flip()
def finish_resize(self, msg):
self.resize_accept(msg.width, msg.height)
self.reinit()
self.draw()
self.resize_done()
self.flip()
def mouse_event(self, msg):
redraw = False
if (msg.command == 5 or msg.new_y >= self.height) and self.focused_widget:
self.focused_widget.focus_leave()
self.focused_widget = None
redraw = True
elif msg.new_y < self.height:
widget_under_mouse = None
offset = 0
index = 0
for widget in self.widgets:
index += 1
remaining = 0 if widget.width != -1 else self.width - sum([widget.width for widget in self.widgets[index:]])
if msg.new_x >= offset and msg.new_x < (offset + widget.width if widget.width != -1 else remaining):
widget_under_mouse = widget
break
offset = offset + widget.width if widget.width != -1 else remaining
if widget_under_mouse != self.focused_widget:
if self.focused_widget:
self.focused_widget.focus_leave()
redraw = True
self.focused_widget = widget_under_mouse
if self.focused_widget:
self.focused_widget.focus_enter()
redraw = True
elif widget_under_mouse:
if widget_under_mouse.mouse_action(msg):
self.draw()
if redraw:
self.draw()
def keyboard_event(self, msg):
pass
class WallpaperIcon(object):
icon_width = 48
width = 100
height = 80
def __init__(self, icon, name, action, data):
self.name = name
self.action = action
self.data = data
self.x = 0
self.y = 0
self.hilighted = False
self.icon = get_icon(icon,self.icon_width)
self.icon_hilight = cairo.ImageSurface(self.icon.get_format(),self.icon.get_width(),self.icon.get_height())
tmp = cairo.Context(self.icon_hilight)
tmp.set_operator(cairo.OPERATOR_SOURCE)
tmp.set_source_surface(self.icon,0,0)
tmp.paint()
tmp.set_operator(cairo.OPERATOR_ATOP)
tmp.rectangle(0,0,self.icon_hilight.get_width(),self.icon_hilight.get_height())
tmp.set_source_rgba(*toaru_theme.desktop_icon_hilight)
tmp.paint()
self.font = toaru_fonts.Font(toaru_fonts.FONT_SANS_SERIF, 13, toaru_theme.desktop_icon_text)
self.font.set_shadow(toaru_theme.desktop_icon_shadow)
self.tr = text_region.TextRegion(0,0,self.width,15,font=self.font)
self.tr.set_alignment(2)
self.tr.set_text(self.name)
self.tr.set_one_line()
def draw(self, window, offset, ctx, animating=False):
x, y = offset
self.x = x
self.y = y
self.tr.move(x,y+self.icon_width+5)
self.tr.draw(window)
left_pad = int((self.width - self.icon_width)/2)
icon = self.icon_hilight if self.hilighted else self.icon
ctx.save()
ctx.translate(x+left_pad,y)
if icon.get_width() != self.icon_width:
ctx.scale(self.icon_width/icon.get_width(),self.icon_width/icon.get_width())
ctx.set_source_surface(icon,0,0)
ctx.paint()
ctx.restore()
if animating and animating < 0.5:
ctx.save()
ctx.translate(x+left_pad,y)
if icon.get_width() != self.icon_width:
ctx.scale(self.icon_width/icon.get_width(),self.icon_width/icon.get_width())
scale = 1.0 + animating/0.8
n = (self.icon_width - self.icon_width * scale) / 2
ctx.translate(n,0)
ctx.scale(scale,scale)
ctx.set_source_surface(icon,0,0)
ctx.paint_with_alpha(1.0 - animating/0.5)
ctx.restore()
def focus_enter(self):
self.hilighted = True
def focus_leave(self):
self.hilighted = False
def mouse_action(self, msg):
if msg.command == yutani.MouseEvent.CLICK or close_enough(msg):
self.action(self)
class WallpaperWindow(yutani.Window):
"""Manages the desktop wallpaper window."""
fallback = '/usr/share/wallpaper.bmp'
def __init__(self):
w = yutani.yutani_ctx._ptr.contents.display_width
h = yutani.yutani_ctx._ptr.contents.display_height
flags = yutani.WindowFlag.FLAG_NO_STEAL_FOCUS | yutani.WindowFlag.FLAG_DISALLOW_DRAG | yutani.WindowFlag.FLAG_DISALLOW_RESIZE
super(WallpaperWindow, self).__init__(w,h,doublebuffer=True,flags=flags)
self.move(0,0)
self.set_stack(yutani.WindowStackOrder.ZORDER_BOTTOM)
# TODO get the user's selected wallpaper
self.background = self.load_wallpaper()
self.icons = self.load_icons()
self.focused_icon = None
self.animations = {}
def animate_new(self):
self.new_background = self.load_wallpaper()
self.animations[self] = time.time()
def add_animation(self, icon):
self.animations[icon] = time.time()
def animate(self):
tick = time.time()
self.draw(self.animations.keys())
ditch = []
for icon in self.animations:
if icon == self:
continue
if tick - self.animations[icon] > 0.5:
ditch.append(icon)
for icon in ditch:
del self.animations[icon]
def load_icons(self):
home = os.environ['HOME']
path = f'{home}/.desktop'
if not os.path.exists(path):
path = '/etc/default.desktop'
icons = []
with open(path) as f:
for line in f:
icons.append(line.strip().split(','))
wallpaper = self
def launch_application(self):
wallpaper.add_animation(self)
launch_app(self.data)
out = []
for icon in icons:
out.append(WallpaperIcon(icon[0],icon[2],launch_application,icon[1]))
return out
def load_wallpaper(self, path=None):
if not path:
home = os.environ['HOME']
conf = f'{home}/.desktop.conf'
if not os.path.exists(conf):
path = self.fallback
else:
with open(conf,'r') as f:
conf_str = '[desktop]\n' + f.read()
c = configparser.ConfigParser()
c.read_string(conf_str)
path = c['desktop'].get('wallpaper',self.fallback)
#if path.endswith('.webp') and toaru_webp.exists():
# return toaru_webp.load_webp(path)
return create_from_bmp(path)
def finish_resize(self, msg):
self.resize_accept(msg.width, msg.height)
self.reinit()
self.draw()
self.resize_done()
self.flip()
def draw(self, clips=None):
"""Draw the window."""
surface = self.get_cairo_surface()
ctx = cairo.Context(surface)
ctx.save()
if clips:
for clip in clips:
ctx.rectangle(clip.x,clip.y,clip.width,clip.height)
if self.animations:
for clip in self.animations:
ctx.rectangle(clip.x,clip.y,clip.width,clip.height)
ctx.clip()
ctx.set_operator(cairo.OPERATOR_SOURCE)
x = self.width / self.background.get_width()
y = self.height / self.background.get_height()
nh = int(x * self.background.get_height())
nw = int(y * self.background.get_width())
if (nw > self.width):
ctx.translate((self.width - nw) / 2, 0)
ctx.scale(y,y)
else:
ctx.translate(0,(self.height - nh) / 2)
ctx.scale(x,x)
ctx.set_source_surface(self.background,0,0)
ctx.paint()
ctx.restore()
ctx.set_operator(cairo.OPERATOR_OVER)
clear_animation = False
if self in self.animations:
ctx.save()
x = self.width / self.new_background.get_width()
y = self.height / self.new_background.get_height()
nh = int(x * self.new_background.get_height())
nw = int(y * self.new_background.get_width())
if (nw > self.width):
ctx.translate((self.width - nw) / 2, 0)
ctx.scale(y,y)
else:
ctx.translate(0,(self.height - nh) / 2)
ctx.scale(x,x)
ctx.set_source_surface(self.new_background,0,0)
diff = time.time()-self.animations[self]
if diff >= 1.0:
self.background = self.new_background
clear_animation = True
ctx.paint()
else:
ctx.paint_with_alpha(diff/1.0)
ctx.restore()
offset_x = 20
offset_y = 50
last_width = 0
for icon in self.icons:
if offset_y > self.height - icon.height:
offset_y = 50
offset_x += last_width
last_width = 0
if icon.width > last_width:
last_width = icon.width
if not clips or icon in clips or icon in self.animations or self in self.animations:
icon.draw(self,(offset_x,offset_y),ctx,time.time()-self.animations[icon] if icon in self.animations else False)
offset_y += icon.height
if clear_animation:
del self.animations[self]
self.flip()
def mouse_event(self, msg):
redraw = False
clips = []
if (msg.command == 5 or msg.new_y >= self.height) and self.focused_icon:
self.focused_icon.focus_leave()
clips.append(self.focused_icon)
self.focused_icon = None
redraw = True
else:
icon_under_mouse = None
offset_x = 20
offset_y = 50
last_width = 0
for icon in self.icons:
if offset_y > self.height - icon.height:
offset_y = 50
offset_x += last_width
last_width = 0
if icon.width > last_width:
last_width = icon.width
if msg.new_x >= offset_x and msg.new_x < offset_x + icon.width and msg.new_y >= offset_y and msg.new_y < offset_y + icon.height:
icon_under_mouse = icon
break
offset_y += icon.height
if icon_under_mouse != self.focused_icon:
if self.focused_icon:
self.focused_icon.focus_leave()
redraw = True
clips.append(self.focused_icon)
self.focused_icon = icon_under_mouse
if self.focused_icon:
self.focused_icon.focus_enter()
redraw = True
clips.append(self.focused_icon)
elif icon_under_mouse:
if icon_under_mouse.mouse_action(msg):
self.draw()
clips.append(icon_under_mouse)
if redraw:
self.draw(clips)
class AlttabWindow(yutani.Window):
"""Displays the currently selected window for Alt-Tab switching."""
icon_width = 48
color = toaru_theme.alt_tab_text
def __init__(self):
flags = yutani.WindowFlag.FLAG_NO_STEAL_FOCUS | yutani.WindowFlag.FLAG_DISALLOW_DRAG | yutani.WindowFlag.FLAG_DISALLOW_RESIZE
super(AlttabWindow,self).__init__(300,115,doublebuffer=True,flags=flags)
w = yutani.yutani_ctx._ptr.contents.display_width
h = yutani.yutani_ctx._ptr.contents.display_height
self.move(int((w-self.width)/2),int((h-self.height)/2))
self.font = toaru_fonts.Font(toaru_fonts.FONT_SANS_SERIF_BOLD, 14, self.color)
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_source_rgba(0,0,0,0)
ctx.fill()
ctx.set_operator(cairo.OPERATOR_OVER)
rounded_rectangle(ctx,0,0,self.width,self.height,10)
ctx.set_source_rgba(*toaru_theme.alt_tab_background)
ctx.fill()
if new_focused >= 0 and new_focused < len(windows_zorder):
w = windows_zorder[new_focused]
icon = get_icon(w.icon,self.icon_width)
ctx.save()
ctx.translate(int((self.width-self.icon_width)/2),20)
if icon.get_width() != self.icon_width:
ctx.scale(self.icon_width/icon.get_width(),self.icon_width/icon.get_width())
ctx.set_source_surface(icon,0,0)
ctx.paint()
ctx.restore()
font = self.font
tr = text_region.TextRegion(0,70,self.width,30,font=font)
tr.set_one_line()
tr.set_ellipsis()
tr.set_alignment(2)
tr.set_text(w.name)
tr.draw(self)
self.flip()
class ApplicationRunnerWindow(yutani.Window):
"""Displays the currently selected window for Alt-Tab switching."""
icon_width = 48
color = toaru_theme.alt_tab_text
def __init__(self):
super(ApplicationRunnerWindow,self).__init__(400,115,doublebuffer=True)
w = yutani.yutani_ctx._ptr.contents.display_width
h = yutani.yutani_ctx._ptr.contents.display_height
self.move(int((w-self.width)/2),int((h-self.height)/2))
self.font = toaru_fonts.Font(toaru_fonts.FONT_SANS_SERIF_BOLD, 16, self.color)
self.data = ""
self.complete = ""
self.completed = False
self.bins = []
for d in os.environ.get("PATH").split(":"):
if os.path.exists(d):
self.bins.extend(os.listdir(d))
def close(self):
global app_runner
app_runner = None
super(ApplicationRunnerWindow,self).close()
def try_complete(self):
if not self.data:
self.complete = ""
self.completed = False
return
for b in sorted(self.bins):
if b.startswith(self.data):
self.complete = b[len(self.data):]
self.completed = True
return
self.completed = False
self.complete = ""
def key_action(self, msg):
if not msg.event.action == yutani.KeyAction.ACTION_DOWN:
return
if msg.event.keycode == yutani.Keycode.ESCAPE:
self.close()
return
if msg.event.keycode == yutani.Keycode.DEL:
self.complete = ""
self.completed = False
self.draw()
return
if msg.event.key == b'\x00':
return
if msg.event.key == b'\n':
if self.data:
launch_app(self.data + self.complete, terminal=bool(msg.event.modifiers & yutani.Modifier.MOD_LEFT_SHIFT))
self.close()
return
if msg.event.key == b'\b':
if self.data:
self.data = self.data[:-1]
self.try_complete()
else:
self.data += msg.event.key.decode('utf-8')
self.try_complete()
self.draw()
def match_icon(self):
icons = {
"calculator.py": "calculator",
"clock-win": "clock",
"file_browser.py": "file-browser",
"make-it-snow": "snow",
"game": "applications-simulation",
"draw": "applications-painting",
"about-applet.py": "star",
"help-browser.py": "help",
"terminal": "utilities-terminal",
}
x = (self.data+self.complete).split(" ")[0]
if x in icons:
return get_icon(icons[x],self.icon_width) # Odd names
elif self.completed:
return get_icon(x,self.icon_width) # Fallback
return None
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_source_rgba(0,0,0,0)
ctx.fill()
ctx.set_operator(cairo.OPERATOR_OVER)
rounded_rectangle(ctx,0,0,self.width,self.height,10)
ctx.set_source_rgba(*toaru_theme.alt_tab_background)
ctx.fill()
icon = self.match_icon()
if icon:
ctx.save()
ctx.translate(20,20)
if icon.get_width() != self.icon_width:
ctx.scale(self.icon_width/icon.get_width(),self.icon_width/icon.get_width())
ctx.set_source_surface(icon,0,0)
ctx.paint()
ctx.restore()
font = self.font
tr = text_region.TextRegion(0,20,self.width,30,font=font)
tr.set_one_line()
tr.set_ellipsis()
tr.set_alignment(2)
tr.set_richtext(html.escape(self.data) + '' + html.escape(self.complete) + '')
tr.draw(self)
self.flip()
def rounded_rectangle(ctx,x,y,w,h,r):
degrees = math.pi / 180
ctx.new_sub_path()
ctx.arc(x + w - r, y + r, r, -90 * degrees, 0 * degrees)
ctx.arc(x + w - r, y + h - r, r, 0 * degrees, 90 * degrees)
ctx.arc(x + r, y + h - r, r, 90 * degrees, 180 * degrees)
ctx.arc(x + r, y + r, r, 180 * degrees, 270 * degrees)
ctx.close_path()
def launch_app(item,terminal=False):
"""Launch an application in the background."""
try:
if terminal:
subprocess.Popen(['/bin/terminal',item])
else:
subprocess.Popen(shlex.split(item))
except:
pass
def logout_callback(item):
"""Request the active session be stopped."""
yctx.session_end()
def finish_alt_tab(msg):
"""When Alt is released, call this to close the alt-tab window and focus the requested window."""
global tabbing, new_focused, alttab
w = windows_zorder[new_focused]
yctx.focus_window(w.wid)
tabbing = False
new_focused = -1
alttab.close()
def reload_wallpaper(signum, frame):
"""Respond to SIGUSR1 by reloading the wallpaper."""
wallpaper.animate_new()
appmenu.reinit_menus()
def alt_tab(msg):
"""When Alt+Tab or Alt+Shift+Tab are pressed, call this to set the active alt-tab window."""
global tabbing, new_focused, alttab
direction = 1 if (msg.event.modifiers & yutani.Modifier.MOD_LEFT_SHIFT) else -1
if len(windows_zorder) < 1:
return
if tabbing:
new_focused = new_focused + direction
else:
new_focused = len(windows_zorder) - 1 + direction
alttab = AlttabWindow()
if new_focused < 0:
new_focused = len(windows_zorder)-1
elif new_focused >= len(windows_zorder):
new_focused = 0
tabbing = True
alttab.draw()
def set_binds():
# Show terminal
yctx.key_bind(ord('t'), yutani.Modifier.MOD_LEFT_CTRL | yutani.Modifier.MOD_LEFT_ALT, yutani.KeybindFlag.BIND_STEAL)
# Application runner
yctx.key_bind(yutani.Keycode.F2, yutani.Modifier.MOD_LEFT_ALT, yutani.KeybindFlag.BIND_STEAL)
# Menu
yctx.key_bind(yutani.Keycode.F1, yutani.Modifier.MOD_LEFT_ALT, yutani.KeybindFlag.BIND_STEAL)
# Hide/show panel
yctx.key_bind(yutani.Keycode.F11, yutani.Modifier.MOD_LEFT_CTRL, yutani.KeybindFlag.BIND_STEAL)
# Alt-tab forward and backward
yctx.key_bind(ord("\t"), yutani.Modifier.MOD_LEFT_ALT, yutani.KeybindFlag.BIND_STEAL)
yctx.key_bind(ord("\t"), yutani.Modifier.MOD_LEFT_ALT | yutani.Modifier.MOD_LEFT_SHIFT, yutani.KeybindFlag.BIND_STEAL)
# Release alt
yctx.key_bind(yutani.Keycode.LEFT_ALT, 0, yutani.KeybindFlag.BIND_PASSTHROUGH)
def reset_zorder(signum, frame):
wallpaper.set_stack(yutani.WindowStackOrder.ZORDER_BOTTOM)
panel.set_stack(yutani.WindowStackOrder.ZORDER_TOP)
set_binds()
def maybe_animate():
global current_time
tick = int(time.time())
if tick != current_time:
try:
os.waitpid(-1,os.WNOHANG)
except ChildProcessError:
pass
current_time = tick
panel.draw()
if wallpaper.animations:
wallpaper.animate()
if __name__ == '__main__':
yctx = yutani.Yutani()
appmenu = ApplicationsMenuWidget()
widgets = [appmenu,WindowListWidget(),MouseModeWidget(),VolumeWidget(),NetworkWidget(),WeatherWidget(),DateWidget(),ClockWidget(),LogOutWidget()]
panel = PanelWindow(widgets)
wallpaper = WallpaperWindow()
wallpaper.draw()
app_runner = None
# Tabbing
tabbing = False
alttab = None
new_focused = -1
yctx.subscribe()
set_binds()
def update_window_list():
yctx.query_windows()
while 1:
ad = yctx.wait_for(yutani.Message.MSG_WINDOW_ADVERTISE)
if ad.size == 0:
return
yield ad
windows_zorder = [x for x in update_window_list()]
windows = sorted(windows_zorder, key=lambda window: window.wid)
current_time = int(time.time())
panel.draw()
signal.signal(signal.SIGUSR1, reload_wallpaper)
with open('/tmp/.wallpaper.pid','w') as f:
f.write(str(os.getpid())+'\n')
signal.signal(signal.SIGUSR2, reset_zorder)
fds = [yutani.yutani_ctx]
while 1:
# Poll for events.
fd = fswait.fswait(fds,500 if not wallpaper.animations else 20)
maybe_animate()
while yutani.yutani_ctx.query():
msg = yutani.yutani_ctx.poll()
if msg.type == yutani.Message.MSG_SESSION_END:
# All applications should attempt to exit on SESSION_END.
panel.close()
wallpaper.close()
msg.free()
break
elif msg.type == yutani.Message.MSG_NOTIFY:
# Update the window list.
windows_zorder = [x for x in update_window_list()]
windows = sorted(windows_zorder, key=lambda window: window.wid)
panel.draw()
elif msg.type == yutani.Message.MSG_KEY_EVENT:
if app_runner and msg.wid == app_runner.wid:
app_runner.key_action(msg)
msg.free()
continue
if not app_runner and \
(msg.event.modifiers & yutani.Modifier.MOD_LEFT_ALT) and \
(msg.event.keycode == yutani.Keycode.F2) and \
(msg.event.action == yutani.KeyAction.ACTION_DOWN):
app_runner = ApplicationRunnerWindow()
app_runner.draw()
if not panel.menus and \
(msg.event.modifiers & yutani.Modifier.MOD_LEFT_ALT) and \
(msg.event.keycode == yutani.Keycode.F1) and \
(msg.event.action == yutani.KeyAction.ACTION_DOWN):
appmenu.activate()
# Ctrl-Alt-T: Open Terminal
if (msg.event.modifiers & yutani.Modifier.MOD_LEFT_CTRL) and \
(msg.event.modifiers & yutani.Modifier.MOD_LEFT_ALT) and \
(msg.event.keycode == ord('t')) and \
(msg.event.action == yutani.KeyAction.ACTION_DOWN):
launch_app('terminal')
# Ctrl-F11: Toggle visibility of panel
if (msg.event.modifiers & yutani.Modifier.MOD_LEFT_CTRL) and \
(msg.event.keycode == yutani.Keycode.F11) and \
(msg.event.action == yutani.KeyAction.ACTION_DOWN):
panel.toggle_visibility()
# Release alt while alt-tabbing
if tabbing and (msg.event.keycode == 0 or msg.event.keycode == yutani.Keycode.LEFT_ALT) and \
(msg.event.modifiers == 0) and (msg.event.action == yutani.KeyAction.ACTION_UP):
finish_alt_tab(msg)
# Alt-Tab and Alt-Shift-Tab: Switch window focus.
if (msg.event.modifiers & yutani.Modifier.MOD_LEFT_ALT) and \
(msg.event.keycode == ord("\t")) and \
(msg.event.action == yutani.KeyAction.ACTION_DOWN):
alt_tab(msg)
if msg.wid in panel.menus:
panel.menus[msg.wid].keyboard_event(msg)
elif msg.type == yutani.Message.MSG_WELCOME:
# Display size has changed.
panel.resize(msg.display_width, PANEL_HEIGHT)
wallpaper.resize(msg.display_width, msg.display_height)
elif msg.type == yutani.Message.MSG_RESIZE_OFFER:
# Resize the window.
if msg.wid == panel.wid:
panel.finish_resize(msg)
elif msg.wid == wallpaper.wid:
wallpaper.finish_resize(msg)
elif msg.type == yutani.Message.MSG_WINDOW_MOUSE_EVENT:
if msg.wid == panel.wid:
panel.mouse_event(msg)
elif msg.wid == wallpaper.wid:
wallpaper.mouse_event(msg)
if msg.wid in panel.menus:
m = panel.menus[msg.wid]
if msg.new_x >= 0 and msg.new_x < m.width and msg.new_y >= 0 and msg.new_y < m.height:
panel.hovered_menu = m
elif panel.hovered_menu == m:
panel.hovered_menu = None
m.mouse_action(msg)
elif msg.type == yutani.Message.MSG_WINDOW_FOCUS_CHANGE:
if msg.wid in panel.menus and msg.focused == 0:
panel.menus[msg.wid].leave_menu()
msg.free()