# -*- coding: utf-8 -*-

# Copyright © 2012 Gerd Kohlberger <lowfi@chello.at>
# Copyright © 2011-2014, 2016 marmuta <marmvta@gmail.com>
#
# This file is part of Onboard.
#
# Onboard is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Onboard is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

""" Classes for recursive layout definition """

from __future__ import division, print_function, unicode_literals

from Onboard.utils import Rect, TreeItem

### Config Singleton ###
from Onboard.Config import Config
config = Config()
########################


class KeyContext(object):
    """
    Transforms logical coordinates to canvas coordinates and vice versa.
    """
    def __init__(self):
        # logical rectangle as defined by the keyboard layout,
        # never changed after loading.
        self.initial_log_rect = Rect(0.0, 0.0, 1.0, 1.0)  # includes border

        # logical rectangle as defined by the keyboard layout
        self.log_rect = Rect(0.0, 0.0, 1.0, 1.0)  # includes border

        # canvas rectangle in drawing units
        self.canvas_rect = Rect(0.0, 0.0, 1.0, 1.0)

    def __repr__(self):
        return "log={} canvas={}".format(list(self.log_rect),
                                          list(self.canvas_rect))

    def log_to_canvas(self, coord):
        return (self.log_to_canvas_x(coord[0]), \
                self.log_to_canvas_y(coord[1]))

    def log_to_canvas_rect(self, rect):
        if rect.is_empty():
            return Rect()
        return Rect(self.log_to_canvas_x(rect.x),
                    self.log_to_canvas_y(rect.y),
                    self.scale_log_to_canvas_x(rect.w),
                    self.scale_log_to_canvas_y(rect.h))

    def log_to_canvas_x(self, x):
        canvas_rect = self.canvas_rect
        log_rect = self.log_rect
        return canvas_rect.x + (x - log_rect.x) * canvas_rect.w / log_rect.w

    def log_to_canvas_y(self, y):
        canvas_rect = self.canvas_rect
        log_rect = self.log_rect
        return canvas_rect.y + (y - log_rect.y) * canvas_rect.h / log_rect.h

    def scale_log_to_canvas(self, coord):
        return (self.scale_log_to_canvas_x(coord[0]), \
                self.scale_log_to_canvas_y(coord[1]))

    def scale_log_to_canvas_x(self, x):
        return x * self.canvas_rect.w / self.log_rect.w

    def scale_log_to_canvas_y(self, y):
        return y * self.canvas_rect.h / self.log_rect.h


    def canvas_to_log(self, coord):
        return (self.canvas_to_log_x(coord[0]), \
                self.canvas_to_log_y(coord[1]))

    def canvas_to_log_rect(self, rect):
        return Rect(self.canvas_to_log_x(rect.x),
                    self.canvas_to_log_y(rect.y),
                    self.scale_canvas_to_log_x(rect.w),
                    self.scale_canvas_to_log_y(rect.h))

    def canvas_to_log_x(self, x):
        canvas_rect = self.canvas_rect
        log_rect = self.log_rect
        return (x - canvas_rect.x) * log_rect.w / canvas_rect.w + log_rect.x

    def canvas_to_log_y(self, y):
        canvas_rect = self.canvas_rect
        log_rect = self.log_rect
        return (y - canvas_rect.y) * log_rect.h / canvas_rect.h + log_rect.y


    def scale_canvas_to_log_x(self, x):
        return x * self.log_rect.w / self.canvas_rect.w

    def scale_canvas_to_log_y(self, y):
        return y * self.log_rect.h / self.canvas_rect.h

    def log_to_canvas_path(self, path):
        result = path.copy()
        log_to_canvas_x = self.log_to_canvas_x
        log_to_canvas_y = self.log_to_canvas_y
        for op, coords in result.segments:
            for i in range(0, len(coords), 2):
                coords[i]   = log_to_canvas_x(coords[i])
                coords[i+1] = log_to_canvas_y(coords[i+1])
        return result

    ##### Speed-optimized overloads #####

    def log_to_canvas(self, coord):
        canvas_rect = self.canvas_rect
        log_rect = self.log_rect
        return canvas_rect.x + (coord[0] - log_rect.x) * \
                             canvas_rect.w / log_rect.w, \
               canvas_rect.y + (coord[1] - log_rect.y) * \
                             canvas_rect.h / log_rect.h

    def log_to_canvas_rect(self, rect):
        """ ~50% faster than the above. """
        w = rect.w
        h = rect.h
        if w <= 0 or h <= 0:
            return Rect()

        canvas_rect = self.canvas_rect
        log_rect = self.log_rect
        scale_w = canvas_rect.w / log_rect.w
        scale_h = canvas_rect.h / log_rect.h

        return Rect(canvas_rect.x + (rect.x - log_rect.x) * scale_w,
                    canvas_rect.y + (rect.y - log_rect.y) * scale_h,
                    w * scale_w,
                    h * scale_h)

    def scale_log_to_canvas(self, coord):
        canvas_rect = self.canvas_rect
        log_rect = self.log_rect
        return coord[0] * canvas_rect.w / log_rect.w, \
               coord[1] * canvas_rect.h / log_rect.h


class LayoutRoot:
    """
    Decorator class wrapping the root item.
    Implements extensive caching to avoid most of the expensive
    (for python) traversal of the layout tree.
    """
    def __init__(self, item):
        self.__dict__['_item'] = item    # item to decorate
        self.invalidate_caches()
        self.init_chamfer_sizes()

    def __getattr__(self, name):
        return getattr(self._item, name)

    def __setattr__(self, name, value):
        self._item.__setattr__(name, value)

    def invalidate_caches(self):
        self.invalidate_traversal_caches()
        self.invalidate_geometry_caches()

    def invalidate_traversal_caches(self):
        # speed up iterating the tree
        self._cached_items = {}
        self._cached_keys = {}
        self._cached_visible_items = {}
        self._cached_layer_items = {}
        self._cached_layer_keys = {}
        self._cached_key_groups = {}

        # cache available layers
        self._cached_layer_ids = None

    def invalidate_geometry_caches(self):
        # speed up hit testing
        self._cached_hit_rects = {}
        self._last_hit_args = None
        self._last_hit_key = None

    def fit_inside_canvas(self, canvas_border_rect):
        self._item.fit_inside_canvas(canvas_border_rect)

        # rects likely changed
        # -> invalidate geometry related caches
        self.invalidate_geometry_caches()

    def do_fit_inside_canvas(self, canvas_border_rect):
        self._item.do_fit_inside_canvas(canvas_border_rect)

        # rects likely changed
        # -> invalidate geometry related caches
        self.invalidate_geometry_caches()

    def set_visible_layers(self, layer_ids):
        """
        Show all items of layer "layer", hide all items of the other layers.
        """
        self.invalidate_caches()
        self._item.set_visible_layers(layer_ids)

    def set_item_visible(self, item, visible):
        if item.visible != visible:
            item.set_visible(visible)
            self.invalidate_caches()

    def iter_items(self):
        items = self._cached_items
        if not items:
            items = tuple(self._item.iter_items())
            self._cached_items = items
        return items

    def iter_keys(self, group_name = None):
        items = self._cached_keys.get(group_name)
        if not items:
            items = tuple(self._item.iter_keys(group_name))
            self._cached_keys[group_name] = items
        return items

    def iter_visible_items(self):
        items = self._cached_visible_items
        if not items:
            items = tuple(self._item.iter_visible_items())
            self._cached_visible_items = items
        return items

    def iter_layer_keys(self, layer_id):
        """
        Returns cached visible keys per layer, re-creates cache if necessary.
        Use iter_layer_keys if performance doesn't matter.
        """
        items = self._cached_layer_keys.get(layer_id)
        if not items:
            items = tuple(self._item.iter_layer_keys(layer_id))
            self._cached_layer_keys[layer_id] = items
        return items

    def iter_layer_items(self, layer_id = None, only_visible = True):
        args = (layer_id, only_visible)
        items = self._cached_layer_items.get(args)
        if not items:
            items = tuple(self._item.iter_layer_items(*args))
            self._cached_layer_items[args] = items
        return items

    def get_layer_ids(self):
        layer_ids = self._cached_layer_ids
        if not layer_ids:
            layer_ids = self._item.get_layer_ids()
            self._cached_layer_ids = layer_ids
        return layer_ids

    def get_key_groups(self):
        """
        Return all keys sorted by group.
        """
        key_groups = self._cached_key_groups
        if not key_groups:
            key_groups = self._item.get_key_groups()
            self._cached_key_groups = key_groups
        return key_groups

    def get_key_at(self, point, active_layer):
        """
        Find the topmost key at point.
        """
        # After motion-notify-event the query-tooltit event calls this
        # a second time with the same point. Avoid re-searching in that case.
        args = (point, active_layer)
        if self._last_hit_args == args:
            return self._last_hit_key

        key = None
        x, y = point
        hit_rects = self._get_hit_rects(active_layer)
        for x0, y0, x1, y1, k in hit_rects:
            # Inlined test, not using Rect.is_point_within for speed.
            if x >= x0 and x < x1 and \
               y >= y0 and y < y1:
                if k.geometry is None or \
                   k.get_hit_path().is_point_within(point):
                    key = k
                    break

        self._last_hit_args = args
        self._last_hit_key = key

        return key

    def _get_hit_rects(self, active_layer):
        try:
            hit_rects = self._cached_hit_rects[active_layer]
        except KeyError:
            # All visible and sensitive key items sorted in z-order.
            # Keys of the active layer have priority over non-layer keys
            # (layer switcher, hide, etc.).
            iter_layer_keys = self.iter_layer_keys
            items = list(reversed(list(iter_layer_keys(active_layer)))) + \
                    list(reversed(list(iter_layer_keys(None))))

            hit_rects = [item.get_hit_rect().to_extents() + (item,) \
                     for item in items]
            self._cached_hit_rects[active_layer] = hit_rects

        return hit_rects

    def init_chamfer_sizes(self):
        chamfer_sizes = self._calc_chamfer_sizes()
        for key in self.iter_global_keys():
            if key.chamfer_size is None:
                layer_id = key.get_layer()
                chamfer_size = chamfer_sizes.get(layer_id)
                if not chamfer_size is None:
                    key.chamfer_size = chamfer_size

    def _calc_chamfer_sizes(self):
        chamfer_sizes = {}
        for layer_id in [None] + self.get_layer_ids():
            # find the most frequent key width or height of the layer
            hist = {}
            for key in self.iter_layer_keys(layer_id):
                r = key.get_border_rect()
                s = min(r.w, r.h)
                hist[s] = hist.get(s, 0) + 1
            most_frequent_size = \
                max(list(zip(list(hist.values()), list(hist.keys()))))[1] \
                if hist else None
            chamfer_size = most_frequent_size * 0.5 \
                if not most_frequent_size is None else None
            chamfer_sizes[layer_id] = chamfer_size
        return chamfer_sizes


class LayoutItem(TreeItem):
    """ Abstract base class for layoutable items """

    # group string of the item, label size group for keys
    group = None

    # take this item out of the size group when updating the layout.
    # Instead chose the best label size for this item alone.
    ignore_group = None

    # name of the layer the item is to be shown on, None for all layers
    layer_id = None

    # filename of the svg file where the key geometry is defined
    filename = None

    # key context for transformation between logical and canvas coordinates
    context = None

    # State of visibility. Also determines if drawing space will be
    # assigned to this item and its children.
    visible = True

    # sensitivity, aka. greying; False to stop interaction with the item
    sensitive = True

    # Border around the item. The border "shrinks" the item and
    # is invisible but still sensitive to clicks.
    border = 0.0

    # Expand item in LayoutBoxes
    # "True"  expands the item into the space of invisible siblings.
    # "False" keeps it at the size of the even distribution of all siblings.
    #         Usually this will lock the key to the aspect ratio of its
    #         svg geometry.
    expand = True

    # sublayout sub-trees
    sublayouts = None

    # parent item of sublayout roots
    sublayout_parent = None

    # override switching back to layer 0 on key press
    # True:  do switch to layer 0 on press
    # False: dont't
    # None:  maybe, hard-coded default-behavior for compatibility with <0.99
    unlatch_layer = None

    # False if the key should be ignored by the scanner
    scannable = True

    # Determines scanning order
    scan_priority = None

    # parsing helpers, only valid while loading a layout
    templates = None
    keysym_rules = None

    def __init__(self):
        self.context = KeyContext()

    def __repr__(self):
        return "{}({})".format(type(self).__name__, repr(self.id))

    def dumps(self):
        """
        Recursively dumps the layout (sub-) tree starting from self.
        Returns a multi-line string.
        """
        global _level
        if not "_level" in globals():
            _level = -1
        _level += 1
        s = "   "*_level + "{} id={} layer_id={} fn={} vis={}\n".format(
                                  object.__repr__(self),
                                  repr(self.id),
                                  repr(self.layer_id),
                                  repr(self.filename),
                                  repr(self.visible),
                                  ) + \
               "".join(item.dumps() for item in self.items)
        _level -= 1
        return s

    def set_id(self, id):
        self.id = id

    def get_rect(self):
        """ Get bounding box in logical coordinates """
        return self.get_border_rect().deflate(self.border)

    def get_border_rect(self):
        """ Get bounding rect including border in logical coordinates """
        return self.context.log_rect

    def set_border_rect(self, border_rect):
        """ Set bounding rect including border in logical coordinates """
        self.context.log_rect = border_rect

    def get_initial_border_rect(self):
        """
        Get initial bounding rect including border in logical coordinates
        """
        return self.context.initial_log_rect

    def set_initial_border_rect(self, border_rect):
        """
        Set initial bounding rect including border in logical coordinates.
        """
        self.context.initial_log_rect = border_rect

    def get_canvas_rect(self):
        """ Get bounding box in canvas coordinates """
        return self.context.log_to_canvas_rect(self.get_rect())

    def get_canvas_border_rect(self):
        """ Get bounding rect including border in canvas coordinates """
        return self.context.canvas_rect

    def get_log_aspect_ratio(self):
        """
        Return the aspect ratio of the visible logical extents
        of the layout tree.
        """
        size = self.get_log_extents()
        return size[0] / float(size[1])

    def get_log_extents(self):
        """
        Get the logical extents of the layout tree.
        Extents ignore invisible, "collapsed" items,
        ie. an invisible click column is not included.
        """
        return self.get_border_rect().get_size()

    def get_canvas_extents(self):
        """
        Get the canvas extents of the layout tree.
        """
        size = self.get_log_extents()
        return self.context.scale_log_to_canvas(size)

    def get_extra_render_size(self):
        """ Account for stroke width and antialiasing of keys and bars"""
        root = self.get_layout_root()
        return root.context.scale_log_to_canvas((2.0, 2.0))

    def fit_inside_canvas(self, canvas_border_rect):
        """
        Scale item and its children to fit inside the given canvas_rect.
        """
        # recursively update item's bounding boxes
        self.update_log_rect()

        # recursively fit inside canvas
        self.do_fit_inside_canvas(canvas_border_rect)

    def do_fit_inside_canvas(self, canvas_border_rect):
        """
        Scale item and its children to fit inside the given canvas_rect.
        """
        self.context.canvas_rect = canvas_border_rect

    def update_log_rect(self):
        for item in self.iter_depth_first():
            item._update_log_rect()

    def _update_log_rect(self):
        """
        Override this for layout items that have to calculate their
        logical rectangle.
        """
        pass

    def get_hit_rect(self):
        """ Returns true if the point lies within the items borders. """
        return self.get_canvas_border_rect().inflate(1)

    def is_point_within(self, canvas_point):
        """ Returns true if the point lies within the items borders. """
        rect = self.get_hit_rect()
        return rect.is_point_within(canvas_point)

    def set_visible(self, visible):
        self.visible = visible

    def is_visible(self):
        """ Returns visibility status """
        return self.visible

    def is_path_visible(self):
        """ Are all items in the path to the root visible? """
        item = self
        while item:
            if not item.visible:
                return False
            item = item.parent
        return True

    def has_visible_key(self):
        """
        Checks if there is any visible key in the
        subtree starting at self.
        """
        for item in self.iter_visible_items():
            if item.is_key():
                return True
        return False

    def is_path_scannable(self):
        """ Are all items in the path to the root scannable? """
        item = self
        while item:
            if not item.scannable:
                return False
            item = item.parent
        return True

    def get_path_scan_priority(self):
        """ Return the closeset scan_priority in the path to the root. """
        item = self
        while item:
            if not item.scan_priority is None:
                return item.scan_priority
            item = item.parent
        return 0

    def get_layout_root(self):
        """ Return the root layout item """
        item = self
        while item:
            if item.parent is None:
                return item
            item = item.parent

    def get_global_layout_root(self):
        """ Return the root layout item """
        item = self
        while item:
            if item.parent is None:
                return item
            item = item.parent

    def get_layer(self):
        """ Return the first layer_id on the path from the tree root to self """
        layer_id = None
        item = self
        while item:
            if not item.layer_id is None:
                layer_id = item.layer_id
            item = item.parent
        return layer_id

    def set_visible_layers(self, layer_ids):
        """
        Show all items of layers <layer_ids>, hide all items of the other layers.
        """
        if not self.layer_id is None:
            if not self.is_key():
                self.visible = self.layer_id in layer_ids

        for item in self.items:
            item.set_visible_layers(layer_ids)

    def get_layer_ids(self, _layer_ids=None):
        """
        Search the tree for layer ids and return them in order of appearance
        """
        if _layer_ids is None:
            _layer_ids = []

        if not self.layer_id is None and \
           not self.layer_id in _layer_ids:
            _layer_ids.append(self.layer_id)

        for item in self.items:
            item.get_layer_ids(_layer_ids)

        return _layer_ids

    def get_key_groups(self):
        """
        Traverse the tree and return all keys sorted by group.
        """
        key_groups = {}
        for key in self.iter_keys():
            keys = key_groups.get(key.group, [])
            keys.append(key)
            key_groups[key.group] = keys
        return key_groups

    def raise_to_top(self):
        """ raise self to the top of its siblings """
        if self.parent:
            self.parent.items.remove(self)
            self.parent.items.append(self)

    def lower_to_bottom(self):
        """ lower self to the bottom of its siblings """
        if self.parent:
            self.parent.items.remove(self)
            self.parent.items.insert(0, self)

    def raise_to_top(self):
        if self.parent:
            self.parent.items.remove(self)
            #self.parent.items.insert(0, self)
            self.parent.items.append(self)

    def get_filename(self):
        """
        Recursively finds the closeset definition of the svg filename.
        """
        if self.filename:
            return self.filename
        if self.parent:
            return self.parent.get_filename()
        return None

    def can_unlatch_layer(self):
        """
        Recursively finds the closeset definition of the
        unlatch_layer attribute.
        """
        if not self.unlatch_layer is None:
            return self.unlatch_layer
        if self.parent:
            return self.parent.can_unlatch_layer()
        return None

    def is_key(self):
        """ Returns true if self is a key. """
        return False

    def iter_visible_items(self):
        """
        Traverses top to bottom all visible layout items of the
        layout tree. Invisible paths are cut short.
        """
        if self.visible:

            yield self

            for item in self.items:
                for visible_item in item.iter_visible_items():
                    yield visible_item

    def iter_keys(self, group_name = None):
        """
        Iterates through all keys of the layout tree.
        """
        if self.is_key():
            if group_name is None or key.group == group_name:
                yield self

        for item in self.items:
            for key in item.iter_keys(group_name):
                yield key

    def iter_global_items(self):
        """
        Iterates through all items of the tree including sublayouts.
        """
        yield self

        for item in self.items:
            for child in item.iter_global_items():
                yield child

        if self.sublayouts:
            for item in self.sublayouts:
                for child in item.iter_global_items():
                    yield child

    def iter_global_keys(self, group_name = None):
        """
        Iterates through all keys of the layout tree including sublayouts.
        """
        if self.is_key():
            if group_name is None or key.group == group_name:
                yield self

        for item in self.items:
            for key in item.iter_global_keys(group_name):
                yield key

        if self.sublayouts:
            for item in self.sublayouts:
                for key in item.iter_global_keys(group_name):
                    yield key

    def iter_layer_keys(self, layer_id = None):
        """
        Iterates through all keys of the given layer.
        """
        for item in self.iter_layer_items(layer_id):
            if item.is_key():
                yield item

    def iter_layer_items(self, layer_id = None, only_visible = True,
                              _found_layer_id = None):
        """
        Iterate through all items of the given layer.
        The first layer definition found in the path to each key wins.
        layer=None iterates through all keys that don't have a layer
        specified anywhere in their path.
        """
        if only_visible and not self.visible:
            return

        if self.layer_id == layer_id:
            _found_layer_id = layer_id

        if self.layer_id and self.layer_id != _found_layer_id:
            return

        if _found_layer_id == layer_id:
            yield self

        for item in self.items:
            for item in item.iter_layer_items(layer_id, only_visible,
                                              _found_layer_id):
                yield item

    def find_instance_in_path(self, classinfo):
        """ Find an item of a certain type in the path from self to the root. """
        item = self
        while item:
            if isinstance(item, classinfo):
                return item
            item = item.parent
        return None

    def update_templates(self, templates):
        if templates:
            if self.templates is None:
                self.templates = templates
            else:
                self.templates.update(templates)

    def update_keysym_rules(self, keysym_rules):
        if keysym_rules:
            if self.keysym_rules is None:
                self.keysym_rules = keysym_rules
            else:
                self.keysym_rules.update(keysym_rules)

    def append_sublayout(self, sublayout):
        if sublayout:
            if self.sublayouts is None:
                self.sublayouts = []
            self.sublayouts.append(sublayout)

    def find_sublayout(self, id):
        """
        Look for a sublayout item upwards from self to the root.
        """
        for item in self.iter_to_root():
            sublayouts = item.sublayouts
            if sublayouts:
                for sublayout in sublayouts:
                    if sublayout.id == id:
                        return sublayout
        return None

    def iter_to_global_root(self):
        """
        Iterate through sublayouts all the way to the global layout root.
        LayoutLoader needs this to access key templates from inside of
        sublayouts.
        """
        item = self
        while item:
            yield item
            item = item.parent or item.sublayout_parent


class LayoutBox(LayoutItem):
    """
    Container for distributing items along a single horizontal or
    vertical axis. Items touch, but don't overlap.
    """

    # Spread out child items horizontally or vertically.
    horizontal = True

    # distance between items
    spacing = 1

    # Don't extend bounding box into invisibles
    compact = False

    def __init__(self, horizontal = True):
        super(LayoutBox, self).__init__()
        if self.horizontal != horizontal:
            self.horizontal = horizontal

    def _update_log_rect(self):
        self.context.log_rect = self._calc_bounds()

    def _calc_bounds(self):
        """
        Calculate the bounding rectangle over all items of this panel.
        Include invisible items to stretch the visible ones into their
        space too.
        """
        compact = self.compact
        bounds = None
        for item in self.items:
            if not compact or item.visible:
                rect = item.get_border_rect()
                if not rect.is_empty():
                    if bounds is None:
                        bounds = rect
                    else:
                        bounds = bounds.union(rect)

        if bounds is None:
            return Rect()
        return bounds

    def do_fit_inside_canvas(self, canvas_border_rect):
        """ Scale items to fit inside the given canvas_rect """

        LayoutItem.do_fit_inside_canvas(self, canvas_border_rect)

        axis = 0 if self.horizontal else 1
        items = self.items

        # get canvas rectangle without borders
        canvas_rect = self.get_canvas_rect()

        # Find the combined length of all items, including
        # invisible ones (logical coordinates).
        length = 0.0
        for i, item in enumerate(items):
            rect = item.get_border_rect()
            if not rect.is_empty():
                if i:
                    length += self.spacing
                length += rect[axis+2]

        # Find the stretch factor, that fills the available canvas space with
        # evenly distributed, all visible items.
        fully_visible_scale = canvas_rect[axis+2] / length \
                              if length else 1.0
        canvas_spacing = fully_visible_scale * self.spacing

        # Transform items into preliminary canvas space, drop invisibles
        # and find the total lengths of expandable and non-expandable
        # items (preliminary canvas coordinates).
        length_expandables = 0.0
        num_expandables = 0
        length_nonexpandables = 0.0
        num_nonexpandables = 0
        for i, item in enumerate(items):
            length = item.get_border_rect()[axis+2]
            if length and item.has_visible_key():
                length *= fully_visible_scale
                if item.expand:
                    length_expandables += length
                    num_expandables += 1
                else:
                    length_nonexpandables += length
                    num_nonexpandables += 1

        # Calculate a second stretch factor for expandable and actually
        # visible items. This takes care of the part of the canvas_rect,
        # that isn't covered by the first factor yet.
        # All calculation is done in preliminary canvas coordinates.
        length_target = canvas_rect[axis+2] - length_nonexpandables - \
                   canvas_spacing * (num_nonexpandables + num_expandables - 1)
        expandable_scale = length_target / length_expandables \
                           if length_expandables else 1.0

        # Calculate the final canvas rectangles and traverse
        # the tree recursively.
        position = 0.0
        for i, item in enumerate(items):
            rect = item.get_border_rect()
            if item.has_visible_key():
                length  = rect[axis+2]
                spacing = canvas_spacing
            else:
                length  = 0.0
                spacing = 0.0

            scale = fully_visible_scale
            if item.expand:
                scale *= expandable_scale
            canvas_length = length * scale

            # set the final canvas rect
            r = Rect(*canvas_rect)
            r[axis]   = canvas_rect[axis] + position
            r[axis+2] = canvas_length
            item.do_fit_inside_canvas(r)

            position += canvas_length + spacing

    def get_log_extents(self):
        """
        Get the logical extents of the layout tree.
        Extents ignore invisible, "collapsed" items,
        ie. an invisible click column is not included.
        """
        rect = None
        for item in self.items:
            r = item.get_border_rect()
            if rect is None:
                rect = r.copy()
            else:
                if self.horizontal:
                    rect.w += r.w
                else:
                    rect.h += r.h

        return rect.get_size()


class LayoutPanel(LayoutItem):
    """
    Group of keys layed out at fixed positions relative to each other.
    """

    # Don't extend bounding box into invisibles
    compact = False

    def do_fit_inside_canvas(self, canvas_border_rect):
        """
        Scale panel to fit inside the given canvas_rect.
        """
        LayoutItem.do_fit_inside_canvas(self, canvas_border_rect)

        # Setup children's transformations, take care of the border.
        if self.get_border_rect().is_empty():
            # Clear all item's transformations if there are no visible items.
            for item in self.items:
                item.context.canvas_rect = Rect()
        else:
            context = KeyContext()
            context.log_rect = self.get_border_rect()
            context.canvas_rect = self.get_canvas_rect() # exclude border

            for item in self.items:
                rect = context.log_to_canvas_rect(item.context.log_rect)
                item.do_fit_inside_canvas(rect)

    def _update_log_rect(self):
        self.context.log_rect = self._calc_bounds()

    def _calc_bounds(self):
        """ Calculate the bounding rectangle over all items of this panel """
        # If there is no visible item return an empty rect
        if all(not item.is_visible() for item in self.items):
            return Rect()

        compact = self.compact
        bounds = None
        for item in self.items:
            if not compact or item.visible:
                rect = item.get_border_rect()
                if not rect.is_empty():
                    if bounds is None:
                        bounds = rect
                    else:
                        bounds = bounds.union(rect)

        if bounds is None:
            return Rect()
        return bounds

