#
#		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
