import pygame
+from .focus import FocusRootMixin
+
class EventMethodDispatcher:
MODS = (pygame.KMOD_CTRL, pygame.KMOD_ALT, pygame.KMOD_META, pygame.KMOD_SHIFT)
if hasattr(self, method_name):
getattr(self, method_name)(ev)
+ def update(self):
+ pass
+
+ def draw(self):
+ pass
+
class Parent(EventMethodDispatcher):
- def __init__(self):
+ root: "Root"
+ running: bool
+ stop_event: bool
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
self.children = []
def handle_event(self, ev):
- Child.handle_event(self, ev)
- self.handle_event_children(ev)
-
- def handle_event_children(self, ev):
- root = self.root
- if not root.running or root.stop_event:
+ super().handle_event(ev)
+ if not self.running or self.stop_event:
return
for child in self.children:
child.handle_event(ev)
- if not root.running or root.stop_event:
+ if not self.running or self.stop_event:
break
def update(self):
+ super().update()
for child in self.children:
child.update()
def draw(self):
+ super().draw()
for child in self.children:
child.draw()
class Child(EventMethodDispatcher):
def __init__(self, parent):
- self.parent = parent
+ self._parent = parent
+ if parent is None:
+ return
+ for parent in (parent, getattr(parent, "parent", None)):
+ if isinstance(parent, Parent):
+ parent.children.append(self)
+ break
+
+ @property
+ def parent(self):
+ return self._parent
+
+ @parent.setter
+ def parent(self, parent):
+ if self._parent is not None:
+ self._parent.children.remove(self)
+ self._parent = parent
+ if parent is not None:
+ parent.children.append(self)
+ if "root" in self.__dict__:
+ self.__dict__.pop("root")
@cached_property
def root(self):
parent = parent.root or parent.parent
return parent.surf
- def draw(self):
- pass
-
- def update(self):
- pass
-
+ @property
+ def running(self):
+ return self.root.running
-class ChildAndParent(Parent, Child):
- def __init__(self, parent):
- Parent.__init__(self)
- Child.__init__(self, parent)
+ @property
+ def stop_event(self):
+ return self.root.stop_event
-class Root(Parent):
+class Root(FocusRootMixin, Parent):
BACKGROUND_COLOR: pygame.Color
def __init__(self, surf, font=None):
self.dirty = False
self.surf = surf
self.clock = pygame.time.Clock()
- self.cursor = None
self.stop_event = False
self.root = self
handle_activeevent = handle_windowexposed
def key_escape(self):
- if self.cursor is None:
+ if self.active:
self.running = False
KEY_METHODS = {frozenset(set()): {pygame.K_ESCAPE: key_escape}}
self.surf.fill(self.BACKGROUND_COLOR)
super().draw()
- def update(self):
- super().update()
- if self.cursor is not None:
- self.cursor.update()
-
def run(self):
while True:
for ev in pygame.event.get():
import pygame
+from .base import Parent
from .button import Button
from .modal import Modal
def __init__(self, parent, rect, entries, callback):
super().__init__(parent)
self.callback = callback
- self.children.extend(
- (
- Button(
- self,
- pygame.Rect(
- (rect.left, rect.bottom + i * rect.height), rect.size,
- ),
- entry,
- partial(self.choose, i),
- )
- for i, entry in enumerate(entries)
+ for i, entry in enumerate(entries):
+ Button(
+ self,
+ pygame.Rect(
+ (rect.left, rect.bottom + i * rect.height), rect.size,
+ ),
+ entry,
+ partial(self.choose, i),
)
- )
self.buttons_rect = pygame.Rect(
rect.bottomleft, (rect.width, rect.height * len(entries))
)
self.deactivate()
-class DropDown(Button):
+class DropDown(Parent, Button):
def __init__(self, parent, rect, value, entries, callback, is_active=False):
- self.dropdown_menu = DropDownMenu(parent, rect, entries, callback)
- super().__init__(parent, rect, value, self.dropdown_menu.activate, is_active)
+ super().__init__(parent, rect, value, self.activate, is_active)
+ self.dropdown_menu = DropDownMenu(self, rect, entries, callback)
+
+ def activate(self):
+ self.dropdown_menu.activate()
--- /dev/null
+import pygame
+
+
+class FocusRootMixin:
+ surf: pygame.Surface
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.focus_stack: list[FocusableMixin | FocusRootMixin] = [self]
+
+ @property
+ def active(self):
+ return self.focus_stack[-1] is self
+
+ def handle_event(self, ev):
+ focused = self.focus_stack[-1]
+ (super() if focused is self else focused).handle_event(ev)
+
+
+class FocusableMixin:
+ root: FocusRootMixin
+
+ @property
+ def active(self):
+ return self.root.focus_stack[-1] is self
+
+ def activate(self):
+ focus_stack = self.root.focus_stack
+ assert self not in focus_stack
+ focus_stack.append(self)
+ self.dirty = True
+
+ def deactivate(self):
+ focus_stack = self.root.focus_stack
+ assert self.active
+ focus_stack.pop()
+ self.dirty = True
def __init__(self, parent, rect, message):
super().__init__(parent)
self.rect = rect
- self.label = Label(self.parent, pygame.Rect(rect.center, (10, 10)), "")
- self.children.extend((self.label, *self.get_buttons()))
+ self.label = Label(self, pygame.Rect(rect.center, (10, 10)), "")
+ self.add_buttons()
self.message = message
- def get_buttons(self):
+ def add_buttons(self):
rect = self.rect
- yield Button(
- self.parent,
+ Button(
+ self,
pygame.Rect(
(rect.left + rect.width // 3, rect.centery + 64),
(rect.width // 3, 128),
)
def draw(self):
- super().draw()
pygame.draw.rect(self.surf, "black", self.rect)
pygame.draw.rect(self.surf, "gray", self.rect, 1)
+ super().draw()
@property
def message(self):
import pygame
-from .base import Child
+from .base import Child,Parent
+from .focus import FocusableMixin
-class Modal(Child):
+class Modal(FocusableMixin, Parent, Child):
def __init__(self, parent):
super().__init__(parent)
- self.backsurf = None
- self.children = []
- self.active = False
- self.draw = self._check_active(self.draw)
+ self.draw = self._check_active(self._wrap_draw(self.draw))
self.update = self._check_active(self.update)
self.handle_event = self._check_active(self.handle_event)
self.activate = self._check_active(self.activate, False)
return None
return inner
- def draw(self):
- self.surf.blit(self.backsurf, (0, 0))
-
- @staticmethod
- def tinted_copy(surf):
- surf = surf.copy()
- tintsurf = pygame.Surface(surf.get_size(), pygame.SRCALPHA, 32)
- tintsurf.fill(pygame.Color(0x00000080))
- surf.blit(tintsurf, (0, 0))
- return surf
-
- def swap_children(self):
- children = self.children.copy()
- self.children.clear()
- self.children.extend(self.root.children)
- self.root.children.clear()
- self.root.children.extend(children)
+ def _wrap_draw(self, method):
+ def inner():
+ tintsurf = pygame.Surface(self.surf.get_size(), pygame.SRCALPHA, 32)
+ tintsurf.fill(pygame.Color(0x80))
+ self.surf.blit(tintsurf, (0, 0))
+ return method()
+ return inner
def activate(self):
- self.backsurf = self.tinted_copy(self.surf)
- self.swap_children()
- self.root.children.insert(0, self)
- self.active = True
+ super().activate()
self.root.stop_event = True
self.dirty = True
def deactivate(self):
- self.backsurf = None
- self.swap_children()
- self.active = False
+ super().deactivate()
self.root.stop_event = True
self.dirty = True
+
+ def draw(self):
+ super().draw()
import pygame
-from .base import ChildAndParent
+from .base import Child, Parent
-class Scroll(ChildAndParent):
+class Scroll(Parent, Child):
def __init__(self, parent, rect, surf_size):
super().__init__(parent)
self.rect = rect
- self.surf = pygame.Surface(surf_size, pygame.SRCALPHA, 32)
+ self.__dict__["surf"] = pygame.Surface(surf_size, pygame.SRCALPHA, 32)
self.scroll_x = 0
self.scroll_y = 0
pygame.draw.rect(self.parent.surf, "gray", self.rect)
self.root.surf.blit(self.get_subsurf(), self.rect.topleft)
- def handle_event_children(self, ev):
+ def handle_event(self, ev):
if hasattr(ev, "pos"):
if not self.rect.collidepoint(ev.pos):
return
)
},
)
- super().handle_event_children(ev)
+ super().handle_event(ev)
import pygame
-from .base import ChildAndParent
+from .base import Child, Parent
from .button import Button
from .text_input import TextInput
self.dirty = True
-class Spinner(ChildAndParent):
+class Spinner(Parent, Child):
def __init__(self, parent, rect, callback, value=0):
super().__init__(parent)
self.callback = callback
self.value = value
button_size = (rect.height // 2, rect.height // 2)
- self.children.extend(
- (
- TextInput(
- self,
- pygame.Rect(
- rect.topleft, (rect.width - button_size[0], rect.height)
- ),
- self.call_callback,
- str(value),
- re.compile(r"[-+]?\d*").fullmatch,
- ),
- RepeatButton(
- self,
- pygame.Rect(
- (rect.right - button_size[0], rect.top),
- button_size,
- ),
- "^",
- partial(self.spin_callback, 1),
- ),
- RepeatButton(
- self,
- pygame.Rect(
- (rect.right - button_size[0], rect.top + button_size[1]),
- button_size,
- ),
- "v",
- partial(self.spin_callback, -1),
- ),
- )
+ self.text_input = TextInput(
+ self,
+ pygame.Rect(
+ rect.topleft, (rect.width - button_size[0], rect.height)
+ ),
+ self.call_callback,
+ str(value),
+ re.compile(r"[-+]?\d*").fullmatch,
+ )
+ RepeatButton(
+ self,
+ pygame.Rect(
+ (rect.right - button_size[0], rect.top),
+ button_size,
+ ),
+ "^",
+ partial(self.spin_callback, 1),
+ )
+ RepeatButton(
+ self,
+ pygame.Rect(
+ (rect.right - button_size[0], rect.top + button_size[1]),
+ button_size,
+ ),
+ "v",
+ partial(self.spin_callback, -1),
)
def call_callback(self, value):
if int_value != self.value:
self.value = int_value
self.callback(int_value)
- text_input = self.children[0]
str_value = str(self.value)
- if str_value != text_input.value:
- text_input.value = str(self.value)
+ if str_value != self.text_input.value:
+ self.text_input.value = str(self.value)
self.root.dirty = True
def spin_callback(self, value):
self.value += value
- self.children[0].value = str(self.value)
+ self.text_input.value = str(self.value)
self.callback(self.value)
self.root.dirty = True
import pygame
-from .base import ChildAndParent
+from .base import Child, Parent
from .button import Button
-class TabBar(ChildAndParent):
+class TabBar(Parent, Child):
def __init__(self, parent, rect, labels, groups, active):
super().__init__(parent)
self.labels = labels
+ # ...why do we have to reparent these items?
+ for group in groups:
+ for item in group:
+ item.parent = self
self.groups = groups
- num_names = len(groups)
+ self.active = active
+ num_labels = len(labels)
+ self.children.clear()
self.buttons = [
Button(
self,
pygame.Rect(
- (rect.left + rect.width * i // num_names, rect.top),
- (rect.width // num_names, rect.height),
+ (rect.left + rect.width * i // num_labels, rect.top),
+ (rect.width // num_labels, rect.height),
),
labels[i],
partial(self.update_children, i),
- i == active,
+ False,
)
- for i in range(len(groups))
+ for i in range(num_labels)
]
- self.children.extend(self.buttons)
- self.active = active
self.children.extend(self.groups[active])
- def update_children(self, name):
- if self.active == name:
+ def update_children(self, i):
+ if self.active == i:
return
- self.active = name
+ self.active = i
for i, (button, group) in enumerate(zip(self.buttons, self.groups)):
is_group_active = i == self.active
if button.is_active != is_group_active:
elif is_child_active:
self.children.remove(item)
self.dirty = True
+ assert len(self.children) == len(set(self.children))
from contextlib import contextmanager
+from dataclasses import dataclass
from functools import partial
from math import floor
from time import time
+from typing import Optional, Callable
import pygame
from .base import Child
+from .focus import FocusableMixin
-class Cursor(Child):
+@dataclass
+class Cursor:
DELAY_MS = 500
REPEAT_MS = 100
- def __init__(self, text_input, x_offset):
- super().__init__(text_input.parent)
- self.text_input = text_input
- self.old_value = text_input.value
+ old_value: str
+ pos: Optional[int] = None
+ key_callback: Optional[Callable] = None
+ key: Optional[int] = None
+ repeat_ts: Optional[float] = None
+
+ def press_key(self, key_callback, key):
+ self.key_callback = key_callback
+ self.key = key
+ self.repeat_ts = time() + self.DELAY_MS / 1000
+
+ def release_key(self):
self.key_callback = None
self.key = None
self.repeat_ts = None
- self.pos = self.pos_from_offset(x_offset)
- # I would rather have one cursor object on the current focus that activates
- # itself by appending self to the current_focus stack.
- self.root.children.append(self)
- def remove(self):
- self.root.children.remove(self)
- self.text_input.cursor = None
- self.dirty = True
- return self.old_value
+ def repeat_key_callback(self):
+ repeat_offset = floor((time() - self.repeat_ts) * 1000 / self.REPEAT_MS) + 1
+ if repeat_offset < 1:
+ return
+ for _ in range(repeat_offset):
+ self.key_callback()
+ self.repeat_ts += self.REPEAT_MS * repeat_offset / 1000
- def pos_from_offset(self, x_offset):
- value = self.text_input.value
- a, a_x = 0, 0
- b, b_x = len(value), self.font.size(value)[0]
- if x_offset <= a_x:
- return a
- elif x_offset >= b_x:
- return b
- while b - a > 1:
- c = a + (b - a) // 2
- c_x = self.font.size(value[:c])[0]
- if c_x < x_offset:
- a, a_x = c, c_x
- else:
- b, b_x = c, c_x
- if abs(a_x - x_offset) < abs(b_x - x_offset):
- return a
- return b
- @contextmanager
- def check_dirty(self):
- old = self.pos, self.value
- yield
- if (self.pos, self.value) != old:
- self.text_input.dirty = True
+def search_same_isspace_backward(value, pos):
+ if pos == 0:
+ return 0
+ pos -= 1
+ isspace = value[pos].isspace()
+ while pos > 0:
+ pos -= 1
+ if value[pos].isspace() != isspace:
+ return pos + 1
+ return 0
- def handle_keydown(self, ev):
- if (key_method := self.get_key_method(ev.key, ev.mod)) is not None:
- self.key_callback = key_method
- elif ev.unicode and ev.unicode.isprintable():
- self.key_callback = partial(self.key_printable_unicode, ev.unicode)
- else:
- return
- with self.check_dirty():
- self.key_callback()
- self.key = ev.key
- self.repeat_ts = time() + self.DELAY_MS / 1000
- def handle_keyup(self, ev):
- if ev.key == self.key:
- self.key_release()
+def search_same_isspace_forward(value, pos):
+ len_value = len(value)
+ if pos == len_value:
+ return len_value
+ isspace = value[pos].isspace()
+ pos += 1
+ while pos < len_value and value[pos].isspace() == isspace:
+ pos += 1
+ return pos
- @property
- def value(self):
- return self.text_input.value
- @value.setter
- def value(self, value):
- self.text_input.value = value
+class TextInput(FocusableMixin, Child):
+ def __init__(self, parent, rect, callback, value="", value_filter=None):
+ super().__init__(parent)
+ self.rect = rect
+ self.callback = callback
+ self.value = value
+ self.value_filter = value_filter
+ self.offset = 0
+ self.cursor = None
@staticmethod
def maybe_scroll_font_surface(font, value_to_cursor, fs, width, height):
return fs, offset, x - offset
def get_font_surface(self):
- if self.pos > len(self.value):
- self.pos = len(self.value)
+ if self.cursor.pos > len(self.value):
+ self.cursor.pos = len(self.value)
fs = self.font.render(self.value, True, "gray")
- rect = self.text_input.rect
+ rect = self.rect
if self.value:
- fs, self.text_input.offset, x = self.maybe_scroll_font_surface(
+ fs, self.offset, x = self.maybe_scroll_font_surface(
self.font,
- self.value[:self.pos],
+ self.value[:self.cursor.pos],
fs,
rect.width - 24,
rect.height,
return fs
def update(self):
- if self.key_callback is None:
+ if getattr(self.cursor, "key_callback", None) is None:
return
- repeat_offset = floor((time() - self.repeat_ts) * 1000 / self.REPEAT_MS)
- if repeat_offset < 0:
- return
- repeat_offset += 1
with self.check_dirty():
- for _ in range(repeat_offset):
- self.key_callback()
- self.repeat_ts += self.REPEAT_MS * repeat_offset / 1000
+ self.cursor.repeat_key_callback()
- def key_backspace(self):
- if self.pos > 0:
- self.pos -= 1
- self.key_delete()
+ def draw(self):
+ pygame.draw.rect(self.surf, "black", self.rect)
+ if self.active:
+ fs = self.get_font_surface()
+ else:
+ fs = self.font.render(self.value, True, "gray")
+ self.surf.subsurface(self.rect).blit(
+ fs, (16, (self.rect.height - fs.get_height()) // 2)
+ )
+ pygame.draw.rect(self.surf, "gray", self.rect, 1)
- def key_delete(self):
+ def pos_from_offset(self, x_offset):
value = self.value
- if self.pos < len(value):
- self.value = "".join((value[:self.pos], value[self.pos + 1:]))
+ if x_offset is None:
+ return len(value)
+ a, a_x = 0, 0
+ b, b_x = len(value), self.font.size(value)[0]
+ if x_offset <= a_x:
+ return a
+ elif x_offset >= b_x:
+ return b
+ while b - a > 1:
+ c = a + (b - a) // 2
+ c_x = self.font.size(value[:c])[0]
+ if c_x < x_offset:
+ a, a_x = c, c_x
+ else:
+ b, b_x = c, c_x
+ if abs(a_x - x_offset) < abs(b_x - x_offset):
+ return a
+ return b
- def key_prev_word(self):
- value = self.value
- pos = self.pos
- for _ in range(2):
- if pos == 0:
- continue
- n = 1
- isspace = value[pos - n].isspace()
- while n < pos:
- n += 1
- if value[pos - n].isspace() != isspace:
- n -= 1
- break
- pos -= n
- if pos != self.pos:
- self.pos = pos
-
- def key_next_word(self):
- value = self.value
- value_len = len(value)
- pos = self.pos
- for _ in range(2):
- if pos == value_len:
- continue
- n = 0
- isspace = value[pos].isspace()
- while pos + n < value_len and value[pos + n].isspace() == isspace:
- n += 1
- pos += n
- if pos != self.pos:
- self.pos = pos
+ def handle_mousebuttondown(self, ev):
+ if ev.button != 1:
+ return
+ if self.rect.collidepoint(ev.pos):
+ self.activate()
+ with self.check_dirty():
+ self.cursor.pos = self.pos_from_offset(
+ ev.pos[0] - self.rect.left - 16 + self.offset
+ )
+ elif self.active:
+ self.deactivate(True)
+
+ @contextmanager
+ def check_dirty(self):
+ old = getattr(self.cursor, "pos", -1), self.value
+ yield
+ if (getattr(self.cursor, "pos", -2), self.value) != old:
+ self.dirty = True
def key_left(self):
- self.pos = max(self.pos - 1, 0)
+ self.cursor.pos = max(self.cursor.pos - 1, 0)
def key_right(self):
- self.pos = min(self.pos + 1, len(self.value))
+ self.cursor.pos = min(self.cursor.pos + 1, len(self.value))
def key_home(self):
- self.pos = 0
+ self.cursor.pos = 0
def key_end(self):
- self.pos = len(self.value)
+ self.cursor.pos = len(self.value)
- def key_printable_unicode(self, unicode):
+ def key_backspace(self):
+ if self.cursor.pos > 0:
+ self.cursor.pos -= 1
+ self.key_delete()
+
+ def key_delete(self):
value = self.value
- len_old_value = len(value)
- value = "".join((value[:self.pos], unicode, value[self.pos:]))
- if self.text_input.value_filter:
- try:
- result = self.text_input.value_filter(value)
- except ValueError:
- result = None
- if isinstance(result, str):
- value = result
- elif not result:
- return
+ if self.cursor.pos < len(value):
+ self.value = f"{value[:self.cursor.pos]}{value[self.cursor.pos + 1:]}"
+
+ def filter_value(self, value):
+ if not self.value_filter:
+ return value
+ try:
+ return self.value_filter(value)
+ except ValueError:
+ return None
+
+ def key_printable_unicode(self, unicode):
+ len_old_value = len(self.value)
+ value = f"{self.value[:self.cursor.pos]}{unicode}{self.value[self.cursor.pos:]}"
+ result = self.filter_value(value)
+ if isinstance(result, str):
+ value = result
+ elif not value:
+ return
self.value = value
- if len(value) > len_old_value:
- self.pos += len(unicode)
+ len_value = len(self.value)
+ if len_value > len_old_value:
+ self.cursor.pos += len_value - len_old_value
- def key_release(self):
- self.key_callback = None
- self.key = None
- self.repeat_ts = None
+ def key_skip_word(self, func):
+ pos = self.cursor.pos
+ for _ in range(2):
+ pos = func(self.value, pos)
+ if pos != self.cursor.pos:
+ self.cursor.pos = pos
- def key_blur(self, restore=False):
- self.text_input.blur(restore)
+ def activate(self):
+ if self.active:
+ return
+ super().activate()
+ self.cursor = Cursor(self.value)
+
+ def deactivate(self, restore=False):
+ if not self.active:
+ return
+ if restore:
+ self.value = self.cursor.old_value
+ else:
+ self.callback(self.value)
+ self.cursor = None
+ super().deactivate()
KEY_METHODS = {
frozenset(set()): {
pygame.K_END: key_end,
pygame.K_BACKSPACE: key_backspace,
pygame.K_DELETE: key_delete,
- pygame.K_KP_ENTER: key_blur,
- pygame.K_RETURN: key_blur,
- pygame.K_ESCAPE: partial(key_blur, restore=True),
+ pygame.K_KP_ENTER: deactivate,
+ pygame.K_RETURN: deactivate,
+ pygame.K_ESCAPE: partial(deactivate, restore=True),
},
frozenset({pygame.KMOD_CTRL}): {
- pygame.K_LEFT: key_prev_word,
- pygame.K_RIGHT: key_next_word,
+ pygame.K_LEFT: partial(key_skip_word, func=search_same_isspace_backward),
+ pygame.K_RIGHT: partial(key_skip_word, func=search_same_isspace_forward),
},
}
-
-class TextInput(Child):
- def __init__(self, parent, rect, callback, value="", value_filter=None):
- super().__init__(parent)
- self.rect = rect
- self.callback = callback
- self.value = value
- self.value_filter = value_filter
- self.offset = 0
-
- @property
- def cursor(self):
- cursor = self.root.cursor
- if cursor is not None and cursor.text_input is self:
- return cursor
- return None
-
- @cursor.setter
- def cursor(self, value):
- self.root.cursor = value
-
- def draw(self):
- pygame.draw.rect(self.surf, "black", self.rect)
- if self.cursor is not None:
- fs = self.cursor.get_font_surface()
+ def handle_keydown(self, ev):
+ if not self.active:
+ return
+ if (key_method := self.get_key_method(ev.key, ev.mod)) is not None:
+ key_callback = key_method
+ elif ev.unicode and ev.unicode.isprintable():
+ key_callback = partial(self.key_printable_unicode, ev.unicode)
else:
- fs = self.font.render(self.value, True, "gray")
- self.surf.subsurface(self.rect).blit(
- fs, (16, (self.rect.height - fs.get_height()) // 2)
- )
- pygame.draw.rect(self.surf, "gray", self.rect, 1)
-
- def focus(self, x_offset):
- cursor = self.cursor
- x_offset = x_offset - self.rect.left - 16 + self.offset
- if cursor is not None:
- if cursor.text_input is self:
- new_pos = cursor.pos_from_offset(x_offset)
- if new_pos != cursor.pos:
- cursor.pos = new_pos
- self.dirty = True
- return
- cursor.text_input.blur(True)
- self.dirty = True
- self.cursor = Cursor(self, x_offset)
-
- def blur(self, restore=False):
- if self.cursor is not None:
- old_value = self.cursor.remove()
- if restore:
- self.value = old_value
- elif self.value != old_value:
- self.callback(self.value)
- self.offset = 0
+ return
+ with self.check_dirty():
+ key_callback()
+ if self.active:
+ self.cursor.press_key(key_callback, ev.key)
- def handle_mousebuttondown(self, ev):
- if ev.button == 1:
- if self.rect.collidepoint(ev.pos):
- self.focus(ev.pos[0])
- elif self.cursor is not None:
- self.blur(True)
+ def handle_keyup(self, ev):
+ if ev.key == getattr(self.cursor, "key", None):
+ self.cursor.release_key()
),
self.xrandr_conf.is_active("vertical"),
)
- self.children.extend(
- (
- FPSWidget(self),
- self.single_button,
- self.double_button,
- self.vertical_button,
- bt_switch,
- Icon(
- self,
- bluetooth.fit(
- (bt_switch.rect.right + 68, bt_switch.rect.centery - 88),
- (8, 8),
- ),
- ),
- touch_switch,
- Icon(
- self,
- touchscreen.fit(
- (touch_switch.rect.right + 68, touch_switch.rect.centery - 88),
- (8, 8),
- ),
- ),
- stylus_switch,
- Icon(
- self,
- stylus.fit(
- (
- stylus_switch.rect.right + 68,
- stylus_switch.rect.centery - 88,
- ),
- (8, 8),
- ),
- ),
- Button(
- self,
- pygame.Rect((784, touch_switch.rect.top), (384, 128)),
- "Re-apply",
- partial(self.xinput_conf.reapply_by_type, "touchpad")
- ),
- Button(
- self,
- pygame.Rect((784, stylus_switch.rect.top), (384, 128)),
- "Re-apply",
- partial(self.xinput_conf.reapply_by_type, "stylus")
- ),
- Button(
- self,
- pygame.Rect((window_size[0] - 128, 0), (128, 96)),
- "×",
- self.quit,
- ),
- Button(
- self,
- pygame.Rect((window_size[0] - 256, 0), (128, 96)),
- "_",
- self.iconify,
- ),
- Button(
- self,
- pygame.Rect((window_size[0] - 384, 0), (128, 96)),
- "»",
- self.next_display,
+ FPSWidget(self)
+ Icon(
+ self,
+ bluetooth.fit(
+ (bt_switch.rect.right + 68, bt_switch.rect.centery - 88),
+ (8, 8),
+ ),
+ )
+ Icon(
+ self,
+ touchscreen.fit(
+ (touch_switch.rect.right + 68, touch_switch.rect.centery - 88),
+ (8, 8),
+ ),
+ )
+ Icon(
+ self,
+ stylus.fit(
+ (
+ stylus_switch.rect.right + 68,
+ stylus_switch.rect.centery - 88,
),
- )
+ (8, 8),
+ ),
+ )
+ Button(
+ self,
+ pygame.Rect((784, touch_switch.rect.top), (384, 128)),
+ "Re-apply",
+ partial(self.xinput_conf.reapply_by_type, "touchpad")
+ )
+ Button(
+ self,
+ pygame.Rect((784, stylus_switch.rect.top), (384, 128)),
+ "Re-apply",
+ partial(self.xinput_conf.reapply_by_type, "stylus")
+ )
+ Button(
+ self,
+ pygame.Rect((window_size[0] - 128, 0), (128, 96)),
+ "×",
+ self.quit,
+ )
+ Button(
+ self,
+ pygame.Rect((window_size[0] - 256, 0), (128, 96)),
+ "_",
+ self.iconify,
+ )
+ Button(
+ self,
+ pygame.Rect((window_size[0] - 384, 0), (128, 96)),
+ "»",
+ self.next_display,
)
def get_icon(self):