478 lines
13 KiB
Python
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
|