495 lines
16 KiB
Python
495 lines
16 KiB
Python
"""Display different types of interactive widgets.
|
|
"""
|
|
|
|
import pyglet
|
|
|
|
from pyglet.event import EventDispatcher
|
|
from pyglet.graphics import Group
|
|
from pyglet.text.caret import Caret
|
|
from pyglet.text.layout import IncrementalTextLayout
|
|
|
|
|
|
class WidgetBase(EventDispatcher):
|
|
|
|
def __init__(self, x, y, width, height):
|
|
self._x = x
|
|
self._y = y
|
|
self._width = width
|
|
self._height = height
|
|
self._bg_group = None
|
|
self._fg_group = None
|
|
self.enabled = True
|
|
|
|
def update_groups(self, order):
|
|
pass
|
|
|
|
@property
|
|
def x(self):
|
|
"""X coordinate of the widget.
|
|
|
|
:type: int
|
|
"""
|
|
return self._x
|
|
|
|
@x.setter
|
|
def x(self, value):
|
|
self._x = value
|
|
self._update_position()
|
|
|
|
@property
|
|
def y(self):
|
|
"""Y coordinate of the widget.
|
|
|
|
:type: int
|
|
"""
|
|
return self._y
|
|
|
|
@y.setter
|
|
def y(self, value):
|
|
self._y = value
|
|
self._update_position()
|
|
|
|
@property
|
|
def position(self):
|
|
"""The x, y coordinate of the widget as a tuple.
|
|
|
|
:type: tuple(int, int)
|
|
"""
|
|
return self._x, self._y
|
|
|
|
@position.setter
|
|
def position(self, values):
|
|
self._x, self._y = values
|
|
self._update_position()
|
|
|
|
@property
|
|
def width(self):
|
|
"""Width of the widget.
|
|
|
|
:type: int
|
|
"""
|
|
return self._width
|
|
|
|
@property
|
|
def height(self):
|
|
"""Height of the widget.
|
|
|
|
:type: int
|
|
"""
|
|
return self._height
|
|
|
|
@property
|
|
def aabb(self):
|
|
"""Bounding box of the widget.
|
|
|
|
Expressed as (x, y, x + width, y + height)
|
|
|
|
:type: (int, int, int, int)
|
|
"""
|
|
return self._x, self._y, self._x + self._width, self._y + self._height
|
|
|
|
@property
|
|
def value(self):
|
|
"""Query or set the Widget's value.
|
|
|
|
This property allows you to set the value of a Widget directly, without any
|
|
user input. This could be used, for example, to restore Widgets to a
|
|
previous state, or if some event in your program is meant to naturally
|
|
change the same value that the Widget controls. Note that events are not
|
|
dispatched when changing this property.
|
|
"""
|
|
raise NotImplementedError("Value depends on control type!")
|
|
|
|
@value.setter
|
|
def value(self, value):
|
|
raise NotImplementedError("Value depends on control type!")
|
|
|
|
def _check_hit(self, x, y):
|
|
return self._x < x < self._x + self._width and self._y < y < self._y + self._height
|
|
|
|
def _update_position(self):
|
|
raise NotImplementedError("Unable to reposition this Widget")
|
|
|
|
def on_mouse_press(self, x, y, buttons, modifiers):
|
|
pass
|
|
|
|
def on_mouse_release(self, x, y, buttons, modifiers):
|
|
pass
|
|
|
|
def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
|
|
pass
|
|
|
|
def on_mouse_motion(self, x, y, dx, dy):
|
|
pass
|
|
|
|
def on_mouse_scroll(self, x, y, mouse, direction):
|
|
pass
|
|
|
|
def on_text(self, text):
|
|
pass
|
|
|
|
def on_text_motion(self, motion):
|
|
pass
|
|
|
|
def on_text_motion_select(self, motion):
|
|
pass
|
|
|
|
|
|
class PushButton(WidgetBase):
|
|
"""Instance of a push button.
|
|
|
|
Triggers the event 'on_press' when it is clicked by the mouse.
|
|
Triggers the event 'on_release' when the mouse is released.
|
|
"""
|
|
|
|
def __init__(self, x, y, pressed, depressed, hover=None, batch=None, group=None):
|
|
"""Create a push button.
|
|
|
|
:Parameters:
|
|
`x` : int
|
|
X coordinate of the push button.
|
|
`y` : int
|
|
Y coordinate of the push button.
|
|
`pressed` : `~pyglet.image.AbstractImage`
|
|
Image to display when the button is pressed.
|
|
`depresseed` : `~pyglet.image.AbstractImage`
|
|
Image to display when the button isn't pressed.
|
|
`hover` : `~pyglet.image.AbstractImage`
|
|
Image to display when the button is being hovered over.
|
|
`batch` : `~pyglet.graphics.Batch`
|
|
Optional batch to add the push button to.
|
|
`group` : `~pyglet.graphics.Group`
|
|
Optional parent group of the push button.
|
|
"""
|
|
super().__init__(x, y, depressed.width, depressed.height)
|
|
self._pressed_img = pressed
|
|
self._depressed_img = depressed
|
|
self._hover_img = hover or depressed
|
|
|
|
# TODO: add `draw` method or make Batch required.
|
|
self._batch = batch or pyglet.graphics.Batch()
|
|
self._user_group = group
|
|
bg_group = Group(order=0, parent=group)
|
|
self._sprite = pyglet.sprite.Sprite(self._depressed_img, x, y, batch=batch, group=bg_group)
|
|
|
|
self._pressed = False
|
|
|
|
def _update_position(self):
|
|
self._sprite.position = self._x, self._y, 0
|
|
|
|
@property
|
|
def value(self):
|
|
return self._pressed
|
|
|
|
@value.setter
|
|
def value(self, value):
|
|
assert type(value) is bool, "This Widget's value must be True or False."
|
|
self._pressed = value
|
|
self._sprite.image = self._pressed_img if self._pressed else self._depressed_img
|
|
|
|
def update_groups(self, order):
|
|
self._sprite.group = Group(order=order + 1, parent=self._user_group)
|
|
|
|
def on_mouse_press(self, x, y, buttons, modifiers):
|
|
if not self.enabled or not self._check_hit(x, y):
|
|
return
|
|
self._sprite.image = self._pressed_img
|
|
self._pressed = True
|
|
self.dispatch_event('on_press')
|
|
|
|
def on_mouse_release(self, x, y, buttons, modifiers):
|
|
if not self.enabled or not self._pressed:
|
|
return
|
|
self._sprite.image = self._hover_img if self._check_hit(x, y) else self._depressed_img
|
|
self._pressed = False
|
|
self.dispatch_event('on_release')
|
|
|
|
def on_mouse_motion(self, x, y, dx, dy):
|
|
if not self.enabled or self._pressed:
|
|
return
|
|
self._sprite.image = self._hover_img if self._check_hit(x, y) else self._depressed_img
|
|
|
|
def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
|
|
if not self.enabled or self._pressed:
|
|
return
|
|
self._sprite.image = self._hover_img if self._check_hit(x, y) else self._depressed_img
|
|
|
|
|
|
PushButton.register_event_type('on_press')
|
|
PushButton.register_event_type('on_release')
|
|
|
|
|
|
class ToggleButton(PushButton):
|
|
"""Instance of a toggle button.
|
|
|
|
Triggers the event 'on_toggle' when the mouse is pressed or released.
|
|
"""
|
|
|
|
def _get_release_image(self, x, y):
|
|
return self._hover_img if self._check_hit(x, y) else self._depressed_img
|
|
|
|
def on_mouse_press(self, x, y, buttons, modifiers):
|
|
if not self.enabled or not self._check_hit(x, y):
|
|
return
|
|
self._pressed = not self._pressed
|
|
self._sprite.image = self._pressed_img if self._pressed else self._get_release_image(x, y)
|
|
self.dispatch_event('on_toggle', self._pressed)
|
|
|
|
def on_mouse_release(self, x, y, buttons, modifiers):
|
|
if not self.enabled or self._pressed:
|
|
return
|
|
self._sprite.image = self._get_release_image(x, y)
|
|
|
|
|
|
ToggleButton.register_event_type('on_toggle')
|
|
|
|
|
|
class Slider(WidgetBase):
|
|
"""Instance of a slider made of a base and a knob image.
|
|
|
|
Triggers the event 'on_change' when the knob position is changed.
|
|
The knob position can be changed by dragging with the mouse, or
|
|
scrolling the mouse wheel.
|
|
"""
|
|
|
|
def __init__(self, x, y, base, knob, edge=0, batch=None, group=None):
|
|
"""Create a slider.
|
|
|
|
:Parameters:
|
|
`x` : int
|
|
X coordinate of the slider.
|
|
`y` : int
|
|
Y coordinate of the slider.
|
|
`base` : `~pyglet.image.AbstractImage`
|
|
Image to display as the background to the slider.
|
|
`knob` : `~pyglet.image.AbstractImage`
|
|
Knob that moves to show the position of the slider.
|
|
`edge` : int
|
|
Pixels from the maximum and minimum position of the slider,
|
|
to the edge of the base image.
|
|
`batch` : `~pyglet.graphics.Batch`
|
|
Optional batch to add the slider to.
|
|
`group` : `~pyglet.graphics.Group`
|
|
Optional parent group of the slider.
|
|
"""
|
|
super().__init__(x, y, base.width, knob.height)
|
|
self._edge = edge
|
|
self._base_img = base
|
|
self._knob_img = knob
|
|
self._half_knob_width = knob.width / 2
|
|
self._half_knob_height = knob.height / 2
|
|
self._knob_img.anchor_y = knob.height / 2
|
|
|
|
self._min_knob_x = x + edge
|
|
self._max_knob_x = x + base.width - knob.width - edge
|
|
|
|
self._user_group = group
|
|
bg_group = Group(order=0, parent=group)
|
|
fg_group = Group(order=1, parent=group)
|
|
self._base_spr = pyglet.sprite.Sprite(self._base_img, x, y, batch=batch, group=bg_group)
|
|
self._knob_spr = pyglet.sprite.Sprite(self._knob_img, x+edge, y+base.height/2, batch=batch, group=fg_group)
|
|
|
|
self._value = 0
|
|
self._in_update = False
|
|
|
|
def _update_position(self):
|
|
self._base_spr.position = self._x, self._y, 0
|
|
self._knob_spr.position = self._x + self._edge, self._y + self._base_img.height / 2, 0
|
|
|
|
@property
|
|
def value(self):
|
|
return self._value
|
|
|
|
@value.setter
|
|
def value(self, value):
|
|
assert type(value) in (int, float), "This Widget's value must be an int or float."
|
|
self._value = value
|
|
x = (self._max_knob_x - self._min_knob_x) * value / 100 + self._min_knob_x + self._half_knob_width
|
|
self._knob_spr.x = max(self._min_knob_x, min(x - self._half_knob_width, self._max_knob_x))
|
|
|
|
def update_groups(self, order):
|
|
self._base_spr.group = Group(order=order + 1, parent=self._user_group)
|
|
self._knob_spr.group = Group(order=order + 2, parent=self._user_group)
|
|
|
|
@property
|
|
def _min_x(self):
|
|
return self._x + self._edge
|
|
|
|
@property
|
|
def _max_x(self):
|
|
return self._x + self._width - self._edge
|
|
|
|
@property
|
|
def _min_y(self):
|
|
return self._y - self._half_knob_height
|
|
|
|
@property
|
|
def _max_y(self):
|
|
return self._y + self._half_knob_height + self._base_img.height / 2
|
|
|
|
def _check_hit(self, x, y):
|
|
return self._min_x < x < self._max_x and self._min_y < y < self._max_y
|
|
|
|
def _update_knob(self, x):
|
|
self._knob_spr.x = max(self._min_knob_x, min(x - self._half_knob_width, self._max_knob_x))
|
|
self._value = abs(((self._knob_spr.x - self._min_knob_x) * 100) / (self._min_knob_x - self._max_knob_x))
|
|
self.dispatch_event('on_change', self._value)
|
|
|
|
def on_mouse_press(self, x, y, buttons, modifiers):
|
|
if not self.enabled:
|
|
return
|
|
if self._check_hit(x, y):
|
|
self._in_update = True
|
|
self._update_knob(x)
|
|
|
|
def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
|
|
if not self.enabled:
|
|
return
|
|
if self._in_update:
|
|
self._update_knob(x)
|
|
|
|
def on_mouse_scroll(self, x, y, mouse, direction):
|
|
if not self.enabled:
|
|
return
|
|
if self._check_hit(x, y):
|
|
self._update_knob(self._knob_spr.x + self._half_knob_width + direction)
|
|
|
|
def on_mouse_release(self, x, y, buttons, modifiers):
|
|
if not self.enabled:
|
|
return
|
|
self._in_update = False
|
|
|
|
|
|
Slider.register_event_type('on_change')
|
|
|
|
|
|
class TextEntry(WidgetBase):
|
|
"""Instance of a text entry widget.
|
|
|
|
Allows the user to enter and submit text.
|
|
"""
|
|
|
|
def __init__(self, text, x, y, width,
|
|
color=(255, 255, 255, 255), text_color=(0, 0, 0, 255), caret_color=(0, 0, 0),
|
|
batch=None, group=None):
|
|
"""Create a text entry widget.
|
|
|
|
:Parameters:
|
|
`text` : str
|
|
Initial text to display.
|
|
`x` : int
|
|
X coordinate of the text entry widget.
|
|
`y` : int
|
|
Y coordinate of the text entry widget.
|
|
`width` : int
|
|
The width of the text entry widget.
|
|
`color` : (int, int, int, int)
|
|
The color of the outline box in RGBA format.
|
|
`text_color` : (int, int, int, int)
|
|
The color of the text in RGBA format.
|
|
`caret_color` : (int, int, int)
|
|
The color of the caret in RGB format.
|
|
`batch` : `~pyglet.graphics.Batch`
|
|
Optional batch to add the text entry widget to.
|
|
`group` : `~pyglet.graphics.Group`
|
|
Optional parent group of text entry widget.
|
|
"""
|
|
self._doc = pyglet.text.document.UnformattedDocument(text)
|
|
self._doc.set_style(0, len(self._doc.text), dict(color=text_color))
|
|
font = self._doc.get_font()
|
|
height = font.ascent - font.descent
|
|
|
|
self._user_group = group
|
|
bg_group = Group(order=0, parent=group)
|
|
fg_group = Group(order=1, parent=group)
|
|
|
|
# Rectangular outline with 2-pixel pad:
|
|
self._pad = p = 2
|
|
self._outline = pyglet.shapes.Rectangle(x-p, y-p, width+p+p, height+p+p, color[:3], batch, bg_group)
|
|
self._outline.opacity = color[3]
|
|
|
|
# Text and Caret:
|
|
self._layout = IncrementalTextLayout(self._doc, width, height, multiline=False, batch=batch, group=fg_group)
|
|
self._layout.x = x
|
|
self._layout.y = y
|
|
self._caret = Caret(self._layout, color=caret_color)
|
|
self._caret.visible = False
|
|
|
|
self._focus = False
|
|
|
|
super().__init__(x, y, width, height)
|
|
|
|
def _update_position(self):
|
|
self._layout.position = self._x, self._y, 0
|
|
self._outline.position = self._x - self._pad, self._y - self._pad
|
|
|
|
@property
|
|
def value(self):
|
|
return self._doc.text
|
|
|
|
@value.setter
|
|
def value(self, value):
|
|
assert type(value) is str, "This Widget's value must be a string."
|
|
self._doc.text = value
|
|
|
|
def _check_hit(self, x, y):
|
|
return self._x < x < self._x + self._width and self._y < y < self._y + self._height
|
|
|
|
def _set_focus(self, value):
|
|
self._focus = value
|
|
self._caret.visible = value
|
|
|
|
def update_groups(self, order):
|
|
self._outline.group = Group(order=order + 1, parent=self._user_group)
|
|
self._layout.group = Group(order=order + 2, parent=self._user_group)
|
|
|
|
def on_mouse_motion(self, x, y, dx, dy):
|
|
if not self.enabled:
|
|
return
|
|
|
|
def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
|
|
if not self.enabled:
|
|
return
|
|
if self._focus:
|
|
self._caret.on_mouse_drag(x, y, dx, dy, buttons, modifiers)
|
|
|
|
def on_mouse_press(self, x, y, buttons, modifiers):
|
|
if not self.enabled:
|
|
return
|
|
if self._check_hit(x, y):
|
|
self._set_focus(True)
|
|
self._caret.on_mouse_press(x, y, buttons, modifiers)
|
|
else:
|
|
self._set_focus(False)
|
|
|
|
def on_text(self, text):
|
|
if not self.enabled:
|
|
return
|
|
if self._focus:
|
|
# Commit on Enter/Return:
|
|
if text in ('\r', '\n'):
|
|
self.dispatch_event('on_commit', self._layout.document.text)
|
|
self._set_focus(False)
|
|
return
|
|
self._caret.on_text(text)
|
|
|
|
def on_text_motion(self, motion):
|
|
if not self.enabled:
|
|
return
|
|
if self._focus:
|
|
self._caret.on_text_motion(motion)
|
|
|
|
def on_text_motion_select(self, motion):
|
|
if not self.enabled:
|
|
return
|
|
if self._focus:
|
|
self._caret.on_text_motion_select(motion)
|
|
|
|
def on_commit(self, text):
|
|
if not self.enabled:
|
|
return
|
|
"""Text has been commited via Enter/Return key."""
|
|
|
|
|
|
TextEntry.register_event_type('on_commit')
|