890 lines
28 KiB
Python
Executable File
890 lines
28 KiB
Python
Executable File
#!/usr/bin/python3
|
|
"""
|
|
Bindings for the Yutani graphics libraries, including the core Yutani protocol,
|
|
general graphics routines, and the system decoration library.
|
|
"""
|
|
|
|
from ctypes import *
|
|
import importlib
|
|
|
|
yutani_lib = None
|
|
yutani_gfx_lib = None
|
|
yutani_ctx = None
|
|
yutani_menu_lib = None
|
|
yutani_windows = {}
|
|
|
|
_cairo_lib = None
|
|
_cairo_module = None
|
|
_cairo_module_lib = None
|
|
|
|
_libc = CDLL('libc.so')
|
|
|
|
def usleep(microseconds):
|
|
_libc.usleep(microseconds)
|
|
|
|
class Message(object):
|
|
"""A generic event message from the Yutani server."""
|
|
class _yutani_msg_t(Structure):
|
|
_fields_ = [
|
|
('magic', c_uint32),
|
|
('type', c_uint32),
|
|
('size', c_uint32),
|
|
('data', c_char*0),
|
|
]
|
|
|
|
MSG_HELLO = 0x00000001
|
|
MSG_WINDOW_NEW = 0x00000002
|
|
MSG_FLIP = 0x00000003
|
|
MSG_KEY_EVENT = 0x00000004
|
|
MSG_MOUSE_EVENT = 0x00000005
|
|
MSG_WINDOW_MOVE = 0x00000006
|
|
MSG_WINDOW_CLOSE = 0x00000007
|
|
MSG_WINDOW_SHOW = 0x00000008
|
|
MSG_WINDOW_HIDE = 0x00000009
|
|
MSG_WINDOW_STACK = 0x0000000A
|
|
MSG_WINDOW_FOCUS_CHANGE = 0x0000000B
|
|
MSG_WINDOW_MOUSE_EVENT = 0x0000000C
|
|
MSG_FLIP_REGION = 0x0000000D
|
|
MSG_WINDOW_NEW_FLAGS = 0x0000000E
|
|
MSG_RESIZE_REQUEST = 0x00000010
|
|
MSG_RESIZE_OFFER = 0x00000011
|
|
MSG_RESIZE_ACCEPT = 0x00000012
|
|
MSG_RESIZE_BUFID = 0x00000013
|
|
MSG_RESIZE_DONE = 0x00000014
|
|
MSG_WINDOW_ADVERTISE = 0x00000020
|
|
MSG_SUBSCRIBE = 0x00000021
|
|
MSG_UNSUBSCRIBE = 0x00000022
|
|
MSG_NOTIFY = 0x00000023
|
|
MSG_QUERY_WINDOWS = 0x00000024
|
|
MSG_WINDOW_FOCUS = 0x00000025
|
|
MSG_WINDOW_DRAG_START = 0x00000026
|
|
MSG_WINDOW_WARP_MOUSE = 0x00000027
|
|
MSG_WINDOW_SHOW_MOUSE = 0x00000028
|
|
MSG_WINDOW_RESIZE_START = 0x00000029
|
|
MSG_SESSION_END = 0x00000030
|
|
MSG_KEY_BIND = 0x00000040
|
|
MSG_WINDOW_UPDATE_SHAPE = 0x00000050
|
|
MSG_GOODBYE = 0x000000F0
|
|
MSG_TIMER_REQUEST = 0x00000100 # XXX deprecated
|
|
MSG_TIMER_TICK = 0x00000101 # xxx deprecated
|
|
MSG_WELCOME = 0x00010001
|
|
MSG_WINDOW_INIT = 0x00010002
|
|
|
|
def __init__(self, msg):
|
|
self._ptr = msg
|
|
|
|
def free(self):
|
|
if self._ptr:
|
|
_libc.free(self._ptr)
|
|
self._ptr = None
|
|
|
|
@property
|
|
def type(self):
|
|
return self._ptr.contents.type
|
|
|
|
_message_types = {}
|
|
|
|
class MessageBuilder(type):
|
|
|
|
def __new__(cls, name, bases, dct):
|
|
global _message_types
|
|
new_cls = super(MessageBuilder, cls).__new__(cls, name, bases, dct)
|
|
if 'type_val' in dct:
|
|
_message_types[dct['type_val']] = new_cls
|
|
return new_cls
|
|
|
|
class MessageEx(Message, metaclass=MessageBuilder):
|
|
"""An event message with extra data available."""
|
|
type_val = None
|
|
data_struct = None
|
|
|
|
def __init__(self, msg):
|
|
Message.__init__(self, msg)
|
|
self._data_ptr = cast(byref(self._ptr.contents,Message._yutani_msg_t.data.offset), POINTER(self.data_struct))
|
|
|
|
def __getattr__(self, name):
|
|
if name in dir(self._data_ptr.contents):
|
|
return getattr(self._data_ptr.contents, name)
|
|
raise AttributeError(name)
|
|
|
|
class MessageWelcome(MessageEx):
|
|
"""Message sent by the server on display size changes."""
|
|
type_val = Message.MSG_WELCOME
|
|
class data_struct(Structure):
|
|
_fields_ = [
|
|
('display_width', c_uint32),
|
|
('display_height', c_uint32),
|
|
]
|
|
|
|
class Keycode(object):
|
|
"""Keycodes."""
|
|
NONE = 0
|
|
BACKSPACE = 8
|
|
CTRL_A = 1
|
|
CTRL_B = 2
|
|
CTRL_C = 3
|
|
CTRL_D = 4
|
|
CTRL_E = 5
|
|
CTRL_F = 6
|
|
CTRL_G = 7
|
|
CTRL_H = 8
|
|
CTRL_I = 9
|
|
CTRL_J = 10
|
|
CTRL_K = 11
|
|
CTRL_L = 12
|
|
CTRL_M = 13
|
|
CTRL_N = 14
|
|
CTRL_O = 15
|
|
CTRL_P = 16
|
|
CTRL_Q = 17
|
|
CTRL_R = 18
|
|
CTRL_S = 19
|
|
CTRL_T = 20
|
|
CTRL_U = 21
|
|
CTRL_V = 22
|
|
CTRL_W = 23
|
|
CTRL_X = 24
|
|
CTRL_Y = 25
|
|
CTRL_Z = 26
|
|
ESCAPE = 27
|
|
NORMAL_MAX = 256
|
|
ARROW_UP = 257
|
|
ARROW_DOWN = 258
|
|
ARROW_RIGHT = 259
|
|
ARROW_LEFT = 260
|
|
BAD_STATE = -1
|
|
|
|
CTRL_ARROW_UP = 261
|
|
CTRL_ARROW_DOWN = 262
|
|
CTRL_ARROW_RIGHT = 263
|
|
CTRL_ARROW_LEFT = 264
|
|
|
|
SHIFT_ARROW_UP = 265
|
|
SHIFT_ARROW_DOWN = 266
|
|
SHIFT_ARROW_RIGHT = 267
|
|
SHIFT_ARROW_LEFT = 268
|
|
|
|
LEFT_CTRL = 1001
|
|
LEFT_SHIFT = 1002
|
|
LEFT_ALT = 1003
|
|
LEFT_SUPER = 1004
|
|
|
|
RIGHT_CTRL = 1011
|
|
RIGHT_SHIFT = 1012
|
|
RIGHT_ALT = 1013
|
|
RIGHT_SUPER = 1014
|
|
|
|
F1 = 2001
|
|
F2 = 2002
|
|
F3 = 2003
|
|
F4 = 2004
|
|
F5 = 2005
|
|
F6 = 2006
|
|
F7 = 2007
|
|
F8 = 2008
|
|
F9 = 2009
|
|
F10 = 2010
|
|
F11 = 2011
|
|
F12 = 2012
|
|
|
|
PAGE_DOWN = 2013
|
|
PAGE_UP = 2014
|
|
|
|
HOME = 2015
|
|
END = 2016
|
|
DEL = 2017
|
|
INSERT = 2018
|
|
|
|
class Modifier(object):
|
|
"""Modifier key flags."""
|
|
MOD_LEFT_CTRL = 0x01
|
|
MOD_LEFT_SHIFT = 0x02
|
|
MOD_LEFT_ALT = 0x04
|
|
MOD_LEFT_SUPER = 0x08
|
|
MOD_RIGHT_CTRL = 0x10
|
|
MOD_RIGHT_SHIFT = 0x20
|
|
MOD_RIGHT_ALT = 0x40
|
|
MOD_RIGHT_SUPER = 0x80
|
|
|
|
class KeyAction(object):
|
|
"""Keyboard action (up or down)"""
|
|
ACTION_DOWN = 1
|
|
ACTION_UP = 2
|
|
|
|
class MessageKeyEvent(MessageEx):
|
|
"""Message containing key event information."""
|
|
type_val = Message.MSG_KEY_EVENT
|
|
class data_struct(Structure):
|
|
class key_event_t(Structure):
|
|
_fields_ = [
|
|
('keycode', c_uint),
|
|
('modifiers', c_uint),
|
|
('action', c_ubyte),
|
|
('key', c_char),
|
|
]
|
|
class key_event_state_t(Structure):
|
|
_fields = [
|
|
("kbd_state", c_int),
|
|
("kbd_s_state", c_int),
|
|
|
|
("k_ctrl", c_int),
|
|
("k_shift", c_int),
|
|
("k_alt", c_int),
|
|
("k_super", c_int),
|
|
|
|
("kl_ctrl", c_int),
|
|
("kl_shift", c_int),
|
|
("kl_alt", c_int),
|
|
("kl_super", c_int),
|
|
|
|
("kr_ctrl", c_int),
|
|
("kr_shift", c_int),
|
|
("kr_alt", c_int),
|
|
("kr_super", c_int),
|
|
|
|
("kbd_esc_buf", c_int),
|
|
]
|
|
_fields_ = [
|
|
('wid', c_uint32),
|
|
('event', key_event_t),
|
|
('state', key_event_state_t),
|
|
]
|
|
|
|
class MessageWindowMouseEvent(MessageEx):
|
|
"""Message containing window-relative mouse event information."""
|
|
type_val = Message.MSG_WINDOW_MOUSE_EVENT
|
|
class data_struct(Structure):
|
|
_fields_ = [
|
|
('wid', c_uint32),
|
|
('new_x', c_int32),
|
|
('new_y', c_int32),
|
|
('old_x', c_int32),
|
|
('old_y', c_int32),
|
|
('buttons', c_ubyte),
|
|
('command', c_ubyte),
|
|
]
|
|
|
|
class MessageWindowFocusChange(MessageEx):
|
|
"""Message indicating the focus state of a window has changed."""
|
|
type_val = Message.MSG_WINDOW_FOCUS_CHANGE
|
|
class data_struct(Structure):
|
|
_fields_ = [
|
|
('wid', c_uint32),
|
|
('focused', c_int),
|
|
]
|
|
|
|
class MessageWindowResize(MessageEx):
|
|
"""Message indicating the server wishes to resize this window."""
|
|
type_val = Message.MSG_RESIZE_OFFER
|
|
class data_struct(Structure):
|
|
_fields_ = [
|
|
('wid', c_uint32),
|
|
('width', c_uint32),
|
|
('height', c_uint32),
|
|
('bufid', c_uint32),
|
|
]
|
|
|
|
class MessageWindowAdvertisement(MessageEx):
|
|
"""Message containing information about a foreign window."""
|
|
type_val = Message.MSG_WINDOW_ADVERTISE
|
|
class data_struct(Structure):
|
|
_fields_ = [
|
|
('wid', c_uint32),
|
|
('flags', c_uint32),
|
|
('offsets', c_uint16 * 5),
|
|
('size', c_uint32),
|
|
('strings', c_byte * 0),
|
|
]
|
|
|
|
@property
|
|
def name(self):
|
|
return string_at(addressof(self.strings) + self.offsets[0]).decode('utf-8')
|
|
|
|
@property
|
|
def icon(self):
|
|
return string_at(addressof(self.strings) + self.offsets[1]).decode('utf-8')
|
|
|
|
class MessageWindowMove(MessageEx):
|
|
"""Message received when a window has moved containing its new coordinates."""
|
|
type_val = Message.MSG_WINDOW_MOVE
|
|
class data_struct(Structure):
|
|
_fields_ = [
|
|
('wid', c_uint32),
|
|
('x', c_int32),
|
|
('y', c_int32),
|
|
]
|
|
|
|
class Yutani(object):
|
|
"""Base Yutani communication class. Must be initialized to start a connection."""
|
|
class _yutani_t(Structure):
|
|
_fields_ = [
|
|
("sock", c_void_p), # File pointer
|
|
("display_width", c_size_t),
|
|
("display_height", c_size_t),
|
|
("windows", c_void_p), # hashmap
|
|
("queued", c_void_p), # list
|
|
("server_ident", c_char_p),
|
|
]
|
|
|
|
def __init__(self):
|
|
global yutani_lib
|
|
global yutani_ctx
|
|
global yutani_gfx_lib
|
|
global yutani_menu_lib
|
|
if not yutani_lib:
|
|
yutani_lib = CDLL("libtoaru_yutani.so")
|
|
if not yutani_gfx_lib:
|
|
yutani_gfx_lib = CDLL("libtoaru_graphics.so")
|
|
if not yutani_menu_lib:
|
|
yutani_menu_lib = CDLL("libtoaru_menu.so")
|
|
self._ptr = cast(yutani_lib.yutani_init(), POINTER(self._yutani_t))
|
|
if not self._ptr:
|
|
raise ConnectionRefusedError("Could not connect to compositor.")
|
|
yutani_ctx = self
|
|
self._fileno = _libc.fileno(self._ptr.contents.sock)
|
|
|
|
def process_menus(self, event):
|
|
return yutani_menu_lib.menu_process_event(self._ptr, event._ptr)
|
|
|
|
def poll(self, sync=True):
|
|
"""Poll for an event message."""
|
|
if sync:
|
|
result = yutani_lib.yutani_poll(self._ptr)
|
|
else:
|
|
result = yutani_lib.yutani_poll_async(self._ptr)
|
|
if not result:
|
|
return None
|
|
msg_ptr = cast(result, POINTER(Message._yutani_msg_t))
|
|
msg_class = _message_types.get(msg_ptr.contents.type, Message)
|
|
return msg_class(msg_ptr)
|
|
|
|
def wait_for(self, message):
|
|
"""Wait for a particular kind of message to be delivered."""
|
|
result = yutani_lib.yutani_wait_for(self._ptr, message)
|
|
msg_ptr = cast(result, POINTER(Message._yutani_msg_t))
|
|
msg_class = _message_types.get(msg_ptr.contents.type, Message)
|
|
return msg_class(msg_ptr)
|
|
|
|
def subscribe(self):
|
|
"""Subscribe to window information changes."""
|
|
yutani_lib.yutani_subscribe_windows(self._ptr)
|
|
|
|
def unsubscribe(self):
|
|
"""Unsubscribe from window information changes."""
|
|
yutani_lib.yutani_unsubscribe_windows(self._ptr)
|
|
|
|
def query_windows(self):
|
|
"""Request a window subsription list."""
|
|
yutani_lib.yutani_query_windows(self._ptr)
|
|
|
|
def focus_window(self, wid):
|
|
"""Request that the server change the focused window to the window with the specified wid."""
|
|
yutani_lib.yutani_focus_window(self._ptr, wid)
|
|
|
|
def session_end(self):
|
|
"""Request the end of the user session."""
|
|
yutani_lib.yutani_session_end(self._ptr)
|
|
|
|
def key_bind(self, keycode, modifiers, flags):
|
|
"""Set global key binding."""
|
|
yutani_lib.yutani_key_bind(self._ptr, keycode, modifiers, flags)
|
|
|
|
def fileno(self):
|
|
"""Act file-like and return our file descriptor number."""
|
|
return self._fileno
|
|
|
|
def query(self):
|
|
"""Asynchronously check if data is available."""
|
|
return yutani_lib.yutani_query(self._ptr)
|
|
|
|
class KeybindFlag(object):
|
|
"""Flags for global key bindings."""
|
|
BIND_PASSTHROUGH = 0 # The key bind should be received by other clients.
|
|
BIND_STEAL = 1 # The key bind should stop after being processed here.
|
|
|
|
class WindowShape(object):
|
|
"""Window shaping modes for Window.update_shape."""
|
|
# These are actually values representing the minimum required
|
|
# alpha value for a pixel to be registered as part of the given window.
|
|
# 256 is more than the maximum alpha value, so all clicks will pass through.
|
|
# 0 will catch every pixel, even if it is entirely transparent.
|
|
THRESHOLD_NONE = 0
|
|
THRESHOLD_CLEAR = 1
|
|
THRESHOLD_HALF = 127
|
|
THRESHOLD_ANY = 255
|
|
THRESHOLD_PASSTHROUGH = 256
|
|
|
|
class WindowStackOrder(object):
|
|
"""Window stack order options."""
|
|
ZORDER_MAX = 0xFFFF
|
|
ZORDER_TOP = 0xFFFF
|
|
ZORDER_BOTTOM = 0x0000
|
|
|
|
class WindowFlag(object):
|
|
"""Flags for window creation."""
|
|
FLAG_NO_STEAL_FOCUS = (1 << 0) # Don't steal focus on window creation.
|
|
FLAG_DISALLOW_DRAG = (1 << 1) # Don't allow this window to be dragged.
|
|
FLAG_DISALLOW_RESIZE = (1 << 2) # Don't allow this window to be resized.
|
|
FLAG_ALT_ANIMATION = (1 << 3) # Use the alternate animation when mapping and unmapping.
|
|
|
|
class MouseButton(object):
|
|
"""Mouse button flags."""
|
|
BUTTON_LEFT = 0x01
|
|
BUTTON_RIGHT = 0x02
|
|
BUTTON_MIDDLE = 0x04
|
|
SCROLL_UP = 0x10
|
|
SCROLL_DOWN = 0x20
|
|
|
|
class MouseEvent(object):
|
|
"""Mouse event types."""
|
|
CLICK = 0
|
|
DRAG = 1
|
|
RAISE = 2
|
|
DOWN = 3
|
|
MOVE = 4
|
|
LEAVE = 5
|
|
ENTER = 6
|
|
|
|
class CursorType(object):
|
|
"""Cursor types for show_mouse."""
|
|
RESET = -1
|
|
HIDE = 0
|
|
NORMAL = 1
|
|
DRAG = 2
|
|
RESIZE_VERTICAL = 3
|
|
RESIZE_HORIZONTAL = 4
|
|
RESIZE_UP_DOWN = 5
|
|
RESIZE_DOWN_UP = 6
|
|
|
|
class Sprite(object):
|
|
class _sprite_t(Structure):
|
|
_fields_ = [
|
|
('width', c_uint16),
|
|
('height', c_uint16),
|
|
('bitmap', POINTER(c_char)),
|
|
('masks', POINTER(c_char)),
|
|
('blank', c_uint32),
|
|
('alpha', c_uint8),
|
|
]
|
|
|
|
@property
|
|
def width(self):
|
|
return self._ptr.contents.width
|
|
|
|
@property
|
|
def height(self):
|
|
return self._ptr.contents.height
|
|
|
|
@property
|
|
def buffer(self):
|
|
return self._ptr.contents.bitmap
|
|
|
|
def get_cairo_surface(self):
|
|
class Derp(object):
|
|
pass
|
|
self._gfx = Derp()
|
|
self._gfx.contents = Derp()
|
|
self._gfx.contents.backbuffer = self._ptr.contents.bitmap
|
|
self._cairo_surface = Window.get_cairo_surface(self)
|
|
return self._cairo_surface
|
|
|
|
def from_file(path):
|
|
global yutani_gfx_lib
|
|
if not yutani_gfx_lib:
|
|
yutani_gfx_lib = CDLL("libtoaru_graphics.so")
|
|
s = Sprite()
|
|
s._contents = create_string_buffer(sizeof(Sprite._sprite_t))
|
|
s._ptr = cast(addressof(s._contents), POINTER(Sprite._sprite_t))
|
|
yutani_gfx_lib.load_sprite.argtypes = [c_void_p, c_char_p]
|
|
yutani_gfx_lib.load_sprite(s._ptr, path.encode('utf-8'))
|
|
return s
|
|
|
|
def create(width, height):
|
|
s = Sprite()
|
|
s._ptr = yutani_gfx_lib.create_sprite(width, height)
|
|
return s
|
|
|
|
def free(self):
|
|
yutani_gfx_lib.sprite_free(self._ptr)
|
|
|
|
|
|
class GraphicsBuffer(object):
|
|
"""Generic buffer for rendering."""
|
|
|
|
def __init__(self, width, height):
|
|
self.width = width
|
|
self.height = height
|
|
|
|
self._sprite = yutani_gfx_lib.create_sprite(width,height,2)
|
|
self._gfx = cast(yutani_gfx_lib.init_graphics_sprite(self._sprite),POINTER(Window._gfx_context_t))
|
|
|
|
def get_cairo_surface(self):
|
|
return Window.get_cairo_surface(self)
|
|
|
|
def get_value(self,x,y):
|
|
return cast(self._gfx.contents.backbuffer,POINTER(c_uint32))[self.width * y + x]
|
|
|
|
def destroy(self):
|
|
yutani_gfx_lib.sprite_free(self._sprite)
|
|
_libc.free(self._gfx)
|
|
|
|
|
|
class Window(object):
|
|
"""Yutani Window object."""
|
|
class _yutani_window_t(Structure):
|
|
_fields_ = [
|
|
("wid", c_uint),
|
|
("width", c_uint32),
|
|
("height", c_uint32),
|
|
("buffer", POINTER(c_uint8)),
|
|
("bufid", c_uint32),
|
|
("focused", c_uint8),
|
|
("oldbufid", c_uint32),
|
|
("userdata", c_void_p),
|
|
("x", c_int32),
|
|
("y", c_int32),
|
|
("decorator_flags", c_uint32),
|
|
("ctx", c_void_p),
|
|
]
|
|
|
|
class _gfx_context_t(Structure):
|
|
_fields_ = [
|
|
('width', c_uint16),
|
|
('height', c_uint16),
|
|
('depth', c_uint16),
|
|
('size', c_uint32),
|
|
('buffer', POINTER(c_char)),
|
|
('backbuffer', POINTER(c_char)),
|
|
]
|
|
|
|
def __init__(self, width, height, flags=0, title=None, icon=None, doublebuffer=False):
|
|
if not yutani_ctx:
|
|
raise ValueError("Not connected.")
|
|
|
|
self._ptr = cast(yutani_lib.yutani_window_create_flags(yutani_ctx._ptr, width, height, flags), POINTER(self._yutani_window_t))
|
|
|
|
yutani_windows[self.wid] = self
|
|
|
|
self.doublebuffer = doublebuffer
|
|
|
|
if doublebuffer:
|
|
self._gfx = cast(yutani_lib.init_graphics_yutani_double_buffer(self._ptr), POINTER(self._gfx_context_t))
|
|
else:
|
|
self._gfx = cast(yutani_lib.init_graphics_yutani(self._ptr), POINTER(self._gfx_context_t))
|
|
|
|
if title:
|
|
self.set_title(title, icon)
|
|
|
|
self.closed = False
|
|
|
|
def get_cairo_surface(self):
|
|
"""Obtain a pycairo.ImageSurface representing the window backbuffer."""
|
|
global _cairo_lib
|
|
global _cairo_module
|
|
global _cairo_module_lib
|
|
if not _cairo_lib:
|
|
_cairo_lib = CDLL('libcairo.so')
|
|
_cairo_module = importlib.import_module('_cairo')
|
|
_cairo_module_lib = CDLL(_cairo_module.__file__)
|
|
buffer = self._gfx.contents.backbuffer
|
|
width = self.width
|
|
height = self.height
|
|
format = _cairo_module.FORMAT_ARGB32
|
|
# int cairo_format_stride_for_width(cairo_format_t format, int width)
|
|
cfsfw = _cairo_lib.cairo_format_stride_for_width
|
|
cfsfw.argtypes = [c_int, c_int]
|
|
cfsfw.restype = c_int
|
|
# stride = cairo_format_stride_for_width(format, width)
|
|
stride = cfsfw(format, width)
|
|
# cairo_surface_t * cairo_image_surface_create_for_data(unsigned char * data, cairo_format_t format, int ...)
|
|
ciscfd = _cairo_lib.cairo_image_surface_create_for_data
|
|
ciscfd.argtypes = [POINTER(c_char), c_int, c_int, c_int, c_int]
|
|
ciscfd.restype = c_void_p
|
|
# surface = cairo_image_surface_create_for_data(buffer,format,width,height,stride)
|
|
surface = ciscfd(buffer,format,width,height,stride)
|
|
# PyObject * PycairoSurface_FromSurface(cairo_surface_t * surface, PyObject * base)
|
|
pcsfs = _cairo_module_lib.PycairoSurface_FromSurface
|
|
pcsfs.argtypes = [c_void_p, c_int]
|
|
pcsfs.restype = py_object
|
|
# return PycairoSurface_FromSurface(surface, NULL)
|
|
return pcsfs(surface, 0)
|
|
|
|
|
|
def set_title(self, title, icon=None):
|
|
"""Advertise this window with the given title and optional icon string."""
|
|
self.title = title
|
|
self.icon = icon
|
|
title_string = title.encode('utf-8') if title else None
|
|
icon_string = icon.encode('utf-8') if icon else None
|
|
|
|
if not icon:
|
|
yutani_lib.yutani_window_advertise(yutani_ctx._ptr, self._ptr, title_string)
|
|
else:
|
|
yutani_lib.yutani_window_advertise_icon(yutani_ctx._ptr, self._ptr, title_string, icon_string)
|
|
|
|
def buffer(self):
|
|
"""Obtain a reference to the graphics backbuffer representing this window's canvas."""
|
|
return cast(self._gfx.contents.backbuffer, POINTER(c_uint32))
|
|
|
|
def flip(self, region=None):
|
|
"""Flip the window buffer when double buffered and inform the server of updates."""
|
|
if self.doublebuffer:
|
|
yutani_gfx_lib.flip(self._gfx)
|
|
yutani_lib.yutani_flip(yutani_ctx._ptr, self._ptr)
|
|
|
|
def close(self):
|
|
"""Close the window."""
|
|
if self.wid in yutani_windows:
|
|
del yutani_windows[self.wid]
|
|
self.closed = True
|
|
yutani_lib.yutani_close(yutani_ctx._ptr, self._ptr)
|
|
yutani_lib.release_graphics_yutani(self._gfx)
|
|
|
|
def move(self, x, y):
|
|
"""Move the window to the requested location."""
|
|
yutani_lib.yutani_window_move(yutani_ctx._ptr, self._ptr, x, y)
|
|
|
|
def resize_accept(self, w, h):
|
|
"""Inform the server that we have accepted the offered resize."""
|
|
yutani_lib.yutani_window_resize_accept(yutani_ctx._ptr, self._ptr, w, h)
|
|
|
|
def resize_done(self):
|
|
"""Inform the server that we are done resizing and the new window may be displayed."""
|
|
yutani_lib.yutani_window_resize_done(yutani_ctx._ptr, self._ptr)
|
|
|
|
def resize_offer(self, width, height):
|
|
"""Offer alternative dimensions in response to a server offer."""
|
|
yutani_lib.yutani_window_resize_offer(yutani_ctx._ptr, self._ptr, width, height)
|
|
|
|
def resize(self, width, height):
|
|
"""Request that a window be resized to the given dimensions."""
|
|
yutani_lib.yutani_window_resize(yutani_ctx._ptr, self._ptr, width, height)
|
|
|
|
def reinit(self):
|
|
"""Reinitialize the internal graphics context for the window. Should be done after a resize_accept."""
|
|
yutani_lib.reinit_graphics_yutani(self._gfx, self._ptr)
|
|
|
|
def fill(self, color):
|
|
"""Fill the entire window with a given color."""
|
|
yutani_gfx_lib.draw_fill(self._gfx, color)
|
|
|
|
def update_shape(self, mode):
|
|
"""Set the mouse passthrough / window shaping mode. Does not affect appearance of window."""
|
|
yutani_lib.yutani_window_update_shape(yutani_ctx._ptr, self._ptr, mode)
|
|
|
|
def set_stack(self, stack):
|
|
"""Set the stack layer for the window."""
|
|
yutani_lib.yutani_set_stack(yutani_ctx._ptr, self._ptr, stack)
|
|
|
|
def drag_start(self):
|
|
"""Begin window drag."""
|
|
yutani_lib.yutani_window_drag_start(yutani_ctx._ptr, self._ptr)
|
|
|
|
@property
|
|
def x(self):
|
|
return self._ptr.contents.x
|
|
|
|
@property
|
|
def y(self):
|
|
return self._ptr.contents.y
|
|
|
|
@property
|
|
def width(self):
|
|
return self._ptr.contents.width
|
|
|
|
@property
|
|
def height(self):
|
|
return self._ptr.contents.height
|
|
|
|
# TODO: setters for width/height call resize?
|
|
|
|
@property
|
|
def wid(self):
|
|
"""The identifier of the window."""
|
|
return self._ptr.contents.wid
|
|
|
|
@property
|
|
def focused(self):
|
|
"""Whether the window is current focused."""
|
|
return self._ptr.contents.focused
|
|
|
|
@focused.setter
|
|
def focused(self, value):
|
|
self._ptr.contents.focused = value
|
|
|
|
class Decor(object):
|
|
"""Class for rendering decorations with the system decorator library."""
|
|
|
|
EVENT_OTHER = 1
|
|
EVENT_CLOSE = 2
|
|
EVENT_RESIZE = 3
|
|
EVENT_MAXIMIZE = 4
|
|
EVENT_RIGHT = 5
|
|
|
|
class decor_bound(Structure):
|
|
_fields_ = [
|
|
("top_height", c_int),
|
|
("bottom_height", c_int),
|
|
("left_width", c_int),
|
|
("right_width", c_int),
|
|
("width", c_int),
|
|
("height", c_int),
|
|
]
|
|
|
|
def __init__(self):
|
|
self.lib = CDLL("libtoaru_decorations.so")
|
|
self.lib.init_decorations()
|
|
self._bounds = self.decor_bound()
|
|
|
|
self._decor_get_bounds = cast(c_void_p.in_dll(self.lib, "decor_get_bounds").value, CFUNCTYPE(c_int, c_void_p, c_void_p))
|
|
|
|
def _get_bounds(self, window):
|
|
self._decor_get_bounds(window._ptr if window else None, byref(self._bounds))
|
|
|
|
def bounds(self,window=None):
|
|
self._get_bounds(window)
|
|
return self._bounds
|
|
|
|
def width(self,window=None):
|
|
"""The complete width of the left and right window borders."""
|
|
self._get_bounds(window)
|
|
return int(self._bounds.width)
|
|
|
|
def height(self,window=None):
|
|
"""The complete height of the top and bottom window borders."""
|
|
self._get_bounds(window)
|
|
return int(self._bounds.height)
|
|
|
|
def top_height(self,window=None):
|
|
"""The height of the top edge of the decorations."""
|
|
self._get_bounds(window)
|
|
return int(self._bounds.top_height)
|
|
|
|
def bottom_height(self,window=None):
|
|
"""The height of the bottom edge of the decorations."""
|
|
self._get_bounds(window)
|
|
return int(self._bounds.bottom_height)
|
|
|
|
def left_width(self,window=None):
|
|
"""The width of the left edge of the decorations."""
|
|
self._get_bounds(window)
|
|
return int(self._bounds.left_width)
|
|
|
|
def right_width(self,window=None):
|
|
"""The width of the right edge of the decorations."""
|
|
self._get_bounds(window)
|
|
return int(self._bounds.right_width)
|
|
|
|
def render(self, window, title=None):
|
|
"""Render decorations on this window. If a title is not provided, it will be retreived from the window object."""
|
|
if not title:
|
|
title = window.title
|
|
title_string = title.encode('utf-8') if title else None
|
|
self.lib.render_decorations(window._ptr, window._gfx, title_string)
|
|
|
|
def handle_event(self, msg):
|
|
"""Let the decorator library handle an event. Usually passed mouse events."""
|
|
return self.lib.decor_handle_event(yutani_ctx._ptr, msg._ptr)
|
|
|
|
def show_menu(self, window, event):
|
|
self.lib.decor_show_default_menu(window._ptr, window.x + event.new_x, window.y + event.new_y)
|
|
|
|
# Demo follows.
|
|
if __name__ == '__main__':
|
|
# Connect to the server.
|
|
Yutani()
|
|
|
|
# Initialize the decoration library.
|
|
d = Decor()
|
|
|
|
# Create a new window.
|
|
w = Window(300+d.width(),300+d.height(),title="Python Demo")
|
|
|
|
# Since this is Python, we can attach data to our window, such
|
|
# as its internal width (excluding the decorations).
|
|
w.int_width = 300
|
|
w.int_height = 300
|
|
|
|
# We can set window shaping...
|
|
#w.update_shape(WindowShape.THRESHOLD_HALF)
|
|
|
|
# Move the window...
|
|
w.move(100, 100)
|
|
|
|
def draw_decors():
|
|
"""Render decorations for the window."""
|
|
d.render(w)
|
|
|
|
def draw_window():
|
|
"""Draw the window."""
|
|
w.fill(0xFFFF00FF)
|
|
draw_decors()
|
|
|
|
def finish_resize(msg):
|
|
"""Accept a resize."""
|
|
|
|
# Tell the server we accept.
|
|
w.resize_accept(msg.width, msg.height)
|
|
|
|
# Reinitialize internal graphics context.
|
|
w.reinit()
|
|
|
|
# Calculate new internal dimensions.
|
|
w.int_width = msg.width - d.width()
|
|
w.int_height = msg.height - d.height()
|
|
|
|
# Redraw the window buffer.
|
|
draw_window()
|
|
|
|
# Inform the server we are done.
|
|
w.resize_done()
|
|
|
|
# And flip.
|
|
w.flip()
|
|
|
|
# Do an initial draw.
|
|
draw_window()
|
|
|
|
# Don't forget to flip. Our single-buffered window only needs
|
|
# the Yutani flip call, but the API will perform both if needed.
|
|
w.flip()
|
|
|
|
while 1:
|
|
# Poll for events.
|
|
msg = yutani_ctx.poll()
|
|
if msg.type == Message.MSG_SESSION_END:
|
|
# All applications should attempt to exit on SESSION_END.
|
|
w.close()
|
|
break
|
|
elif msg.type == Message.MSG_KEY_EVENT:
|
|
# Print key events for debugging.
|
|
print(f'W({msg.wid}) key {msg.event.key} {msg.event.action}')
|
|
if msg.event.key == b'q':
|
|
# Convention says to close windows when 'q' is pressed,
|
|
# unless we're using keyboard input "normally".
|
|
w.close()
|
|
break
|
|
elif msg.type == Message.MSG_WINDOW_FOCUS_CHANGE:
|
|
# If the focus of our window changes, redraw the borders.
|
|
if msg.wid == w.wid:
|
|
# This attribute is stored in the underlying struct
|
|
# and used by the decoration library to pick which
|
|
# version of the decorations to draw for the window.
|
|
w.focused = msg.focused
|
|
draw_decors()
|
|
w.flip()
|
|
elif msg.type == Message.MSG_RESIZE_OFFER:
|
|
# Resize the window.
|
|
finish_resize(msg)
|
|
elif msg.type == Message.MSG_WINDOW_MOUSE_EVENT:
|
|
# Handle mouse events, first by passing them
|
|
# to the decorator library for processing.
|
|
if d.handle_event(msg) == Decor.EVENT_CLOSE:
|
|
# Close the window when the 'X' button is clicked.
|
|
w.close()
|
|
break
|
|
else:
|
|
# For events that didn't get handled by the decorations,
|
|
# print a debug message with details.
|
|
print(f'W({msg.wid}) mouse {msg.new_x},{msg.new_y}')
|