Lightningbeam/GUI/Generic/GComponents.py

478 lines
13 KiB
Python

#
# Python GUI - Components - Generic
#
import os
from GUI.Properties import Properties, overridable_property
from GUI import MessageHandler
from GUI.Geometry import add_pt, sub_pt, rect_size, rect_sized, rect_topleft
from GUI import application
_user_tab_stop = os.environ.get("PYGUI_KEYBOARD_NAVIGATION") or None
# Allow "False", "True", "0", "1"
if _user_tab_stop is not None:
_user_tab_stop = _user_tab_stop.strip().capitalize()
try:
_user_tab_stop = {"False": False, "True": True}[_user_tab_stop]
except KeyError:
try:
_user_tab_stop = int(_user_tab_stop)
except ValueError:
sys.stderr.write("PYGUI_KEYBOARD_NAVIGATION: Unrecognized value %r"
% _user_tab_stop)
_user_tab_stop = None
class Component(Properties, MessageHandler):
"""Component is an abstract class representing a user
interface component."""
left = overridable_property('left', "Position of left edge relative to container.")
top = overridable_property('top', "Position of top edge relative to container.")
right = overridable_property('right', "Position of right edge relative to container.")
bottom = overridable_property('bottom', "Position of bottom edge relative to container.")
x = overridable_property('x', "Horizontal position relative to container.")
y = overridable_property('y', "Vertical position relative to container.")
width = overridable_property('width')
height = overridable_property('height')
position = overridable_property('position', "Position relative to container.")
size = overridable_property('size')
bounds = overridable_property('bounds', "Bounding rectangle in container's coordinates.")
container = overridable_property('container',
"Container which contains this Component. Setting this property has the "
"effect of removing the component from its previous container, if any, "
"and adding it to the new one, if any.")
# visible = overridable_property('visible',
# "Whether the component is currently shown.")
tab_stop = overridable_property('tab_stop',
"Whether tab key can navigate into this control.")
anchor = overridable_property('anchor', "A string of 'ltrb' controlling behaviour when container is resized.")
border = overridable_property('border', "True if the component should have a border.")
_is_scrollable = False # Overridden by scrollable subclasses
_generic_tabbing = True # Whether to use generic tab navigation code
_default_tab_stop = False
_user_tab_stop_override = False # Whether user preference overrides _default_tab_stop
_tab_stop = None
#
# Class variables defined by implementations:
#
# _has_local_coords bool True if component has a local coordinate system
#
_container = None
_border = False
hmove = 0
vmove = 0
hstretch = 0
vstretch = 0
def __init__(self, tab_stop = None, **kwds):
Properties.__init__(self, **kwds)
if tab_stop is None:
tab_stop = self._get_default_tab_stop()
self.tab_stop = tab_stop
def destroy(self):
self.container = None
#
# Geometry properties
#
# Default implementations of position and size properties
# in terms of the bounds property. A minimal implementation
# need only implement get_bounds and set_bounds.
#
# It is the implementation's responsibility to call _resized()
# whenever the size of the component changes, either by
# explicit assignment to geometry properties or by the user
# resizing the containing window. It should not be called if
# setting a geometry property does not cause the size to change.
#
def get_left(self):
return self.position[0]
def set_left(self, v):
l, t, r, b = self.bounds
self.bounds = (v, t, r, b)
def get_top(self):
return self.bounds[1]
def set_top(self, v):
l, t, r, b = self.bounds
self.bounds = (l, v, r, b)
def get_right(self):
return self.bounds[2]
def set_right(self, v):
l, t, r, b = self.bounds
self.bounds = (l, t, v, b)
def get_bottom(self):
return self.bounds[3]
def set_bottom(self, v):
l, t, r, b = self.bounds
self.bounds = (l, t, r, v)
def get_x(self):
return self.bounds[0]
def set_x(self, v):
l, t, r, b = self.bounds
self.bounds = (v, t, v + r - l, b)
def get_y(self):
return self.bounds[1]
def set_y(self, v):
l, t, r, b = self.bounds
self.bounds = (l, v, r, v + b - t)
def get_position(self):
l, t, r, b = self.bounds
return (l, t)
def set_position(self, (x, y)):
l, t, r, b = self.bounds
self.bounds = (x, y, x + r - l, y + b - t)
def get_width(self):
l, t, r, b = self.bounds
return r - l
def set_width(self, v):
l, t, r, b = self.bounds
self.bounds = (l, t, l + v, b)
def get_height(self):
l, t, r, b = self.bounds
return b - t
def set_height(self, v):
l, t, r, b = self.bounds
self.bounds = (l, t, r, t + v)
def get_size(self):
l, t, r, b = self.bounds
return (r - l, b - t)
def set_size(self, (w, h)):
l, t, r, b = self.bounds
self.bounds = (l, t, l + w, t + h)
#
# Container management
#
def get_container(self):
return self._container
def set_container(self, new_container):
if self._container != new_container:
self._change_container(new_container)
def _change_container(self, new_container):
old_container = self._container
if old_container:
self._container = None
old_container._remove(self)
if new_container:
self._container = new_container
new_container._add(self)
#
# Message dispatching
#
def become_target(self):
"""Arrange for this object to be the first to handle messages
dispatched to the containing Window. If the component is not
contained in a Window, the effect is undefined."""
raise NotImplementedError
def is_target(self):
"""Return true if this is the current target within the containing
Window. If the component is not contained in a Window, the result
is undefined."""
return self.window and self.window.target is self
#
# Message handling
#
def next_handler(self):
return self._container
#
# Visibility control
#
# def show(self):
# """Make the Component visible (provided its container is visible)."""
# self.visible = 1
#
# def hide(self):
# """Make the Component invisible."""
# self.visible = 0
#
# Border
#
def get_border(self):
return self._border
def set_border(self, x):
self._border = x
#
# Resizing
#
def get_anchor(self):
if self.hmove:
s1 = 'r'
elif self.hstretch:
s1 = 'lr'
else:
s1 = 'l'
if self.vmove:
s2 = 'b'
elif self.vstretch:
s2 = 'tb'
else:
s2 = 't'
return s1 + s2
def set_anchor(self, s):
if 'r' in s:
if 'l' in s:
self.hstretch = True
self.hmove = False
else:
self.hstretch = False
self.hmove = True
else:
self.hstretch = False
self.hmove = False
if 'b' in s:
if 't' in s:
self.vstretch = True
self.vmove = False
else:
self.vstretch = False
self.vmove = True
else:
self.vstretch = False
self.vmove = False
def get_auto_layout(self):
return self._auto_layout
def set_auto_layout(self, x):
self._auto_layout = x
def _resized(self, delta):
# Called whenever the size of the component changes for
# any reason.
pass
def container_resized(self, delta):
"""Called whenever the component's container changes size and the
container's auto_layout property is true. The default implementation
repositions and resizes this component according to its resizing
options."""
dw, dh = delta
left, top, right, bottom = self.bounds
if self.hmove:
left += dw
right += dw
elif self.hstretch:
right += dw
if self.vmove:
top += dh
bottom += dh
elif self.vstretch:
bottom += dh
self.bounds = (left, top, right, bottom)
#
# Update region maintenance
#
def invalidate(self):
"""Mark the whole Component as needing to be redrawn."""
self.invalidate_rect(self.viewed_rect())
# def invalidate_rect(self, r):
# print "GComponent.invalidate_rect:", self, r ###
# container = self._container
# if container:
# container.invalidate_rect(r)
# def _invalidate_in_container(self):
# container = self._container
# if container:
# container._invalidate_subcomponent(self)
#
# Coordinate transformation
#
def local_to_global(self, p):
p = self.local_to_container(p)
parent = self._container
if parent:
return parent.local_to_global(p)
else:
return p
def global_to_local(self, p):
parent = self._container
if parent:
p = parent.global_to_local(p)
return self.container_to_local(p)
def local_to_container(self, p):
if self._has_local_coords:
return add_pt(p, self.local_to_container_offset())
else:
return p
def container_to_local(self, p):
if self._has_local_coords:
return sub_pt(p, self.local_to_container_offset())
else:
return p
def local_to_container_offset(self):
if self._has_local_coords:
return self.position
else:
return (0, 0)
def transform_from(self, other, p):
return transform_coords(other, self, p)
def transform_to(self, other, p):
return transform_coords(self, other, p)
#
# Placement specification support
#
def __add__(self, offset):
return (self, offset)
def __sub__(self, offset):
return (self, -offset)
#
# Tabbing
#
# def get_tabbable(self):
# return self._tabbable
#
# def set_tabbable(self, value):
# if self._tabbable <> value:
# self._tabbable = value
# self._invalidate_tab_chain()
def get_tab_stop(self):
return self._tab_stop
def set_tab_stop(self, x):
if self._tab_stop <> x:
self._tab_stop = x
self._invalidate_tab_chain()
def _get_default_tab_stop(self):
if self._user_tab_stop_override:
result = _user_tab_stop
else:
result = None
if result is None:
result = self._default_tab_stop
return result
def _tab_out(self):
pass
def _tab_in(self):
self.become_target()
def _build_tab_chain(self, chain):
if self._tab_stop:
chain.append(self)
def _invalidate_tab_chain(self):
window = self.window
if window:
window._invalidate_tab_chain()
def _is_targetable(self):
return True
#
# Other
#
window = overridable_property('window', """The Window ultimately containing
this Component, or None.""")
def get_window(self):
container = self._container
if container:
return container.window
else:
return None
def reset_blink(self):
application().reset_blink()
def viewed_rect(self):
"""Returns the rectangle in local coordinates that is
currently visible within the component."""
if self._has_local_coords:
width, height = self.size
return (0, 0, width, height)
else:
return self.bounds
def broadcast(self, message, *args):
"""Traverse the component hierarchy, calling each component's handler for
the given message, if any."""
method = getattr(self, message, None)
if method:
method(*args)
def _dispatch_mouse_event(self, event):
self._handle_mouse_event(event)
def _handle_mouse_event(self, event):
self.handle(event.kind, event)
def transform_coords(from_component, to_component, p):
if from_component:
g = from_component.local_to_global(p)
else:
g = p
if to_component:
return to_component.global_to_local(g)
else:
return g