1022 lines
33 KiB
Python
1022 lines
33 KiB
Python
|
|
# -*- 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
|
||
|
|
|