Source code for ajenti.ui.element

from ajenti.api import *
from ajenti.util import *


@public
[docs]def p(prop, default=None, bindtypes=[], type=unicode, public=True, doc=None): """ Creates an UI property inside an :class:`UIElement`:: @p('title') @p('category', default='Other', doc='Section category name') @p('active', default=False) class SectionPlugin (BasePlugin, UIElement): typeid = 'main:section' :param default: Default value :type default: object :param bindtypes: List of Python types that can be bound to this property :type bindtypes: list :param type: expected Python type for this value :type type: object :param public: whether this property is rendered and sent to client :type public: bool :param doc: docstring :type doc: str, None :rtype: function """ def decorator(cls): prop_obj = UIProperty(prop, default=default, bindtypes=bindtypes, type=type, public=public) if not hasattr(cls, '_properties'): cls._properties = {} cls._properties = cls._properties.copy() cls._properties[prop] = prop_obj def get(self): return self.properties[prop] def set(self, value): self.properties_dirty[prop] |= self.properties[prop] != value self.properties[prop] = value _property = property(get, set, None, doc) if not hasattr(cls, prop): setattr(cls, prop, _property) return cls return decorator
@public
[docs]def on(id, event): """ Sets the decorated method to handle indicated event:: @plugin class Hosts (SectionPlugin): def init(self): self.append(self.ui.inflate('hosts:main')) ... @on('save', 'click') def save(self): self.config.save() :param id: element ID :type id: str :param event: event name :type event: str :rtype: function """ def decorator(fx): fx._event_id = id fx._event_name = event return fx return decorator
@public class UIProperty (object): __slots__ = ['name', 'default', 'bindtypes', 'type', 'public'] def __init__(self, name, default=None, bindtypes=[], type=unicode, public=True): self.name = name self.default = default self.bindtypes = bindtypes self.type = type self.public = public def clone(self): return UIProperty( self.name, self.default, self.bindtypes, self.type, self.public, ) @public @p('visible', default=True, type=bool, doc='Visibility of the element') @p('bind', default=None, type=str, public=False, doc='Bound property name') @p('client', default=False, type=True, doc='Whether this element\'s events are only processed on client side') @p('bindtransform', default=None, type=eval, public=False, doc='Value transformation function for one-direction bindings') @p('id', default=None, type=str, public=False, doc='Element ID') @p('style', default='normal', doc='Additional CSS class') @interface @notrack
[docs]class UIElement (object): """ Base UI element class """ typeid = None """ Unique identifier or element type class, used for XML tag name """ __last_id = 0 @classmethod def __generate_id(cls): cls.__last_id += 1 return cls.__last_id def _prepare(self): #: Generated unique identifier (UID) self.uid = UIElement.__generate_id() if not hasattr(self, '_properties'): self._properties = [] self.parent = None self.children = [] self.children_changed = False self.invalidated = False self.events = {} self.event_args = {} self.context = None def __init__(self, ui, typeid=None, children=[], **kwargs): """ :param ui: UI :type ui: :class:`ajenti.ui.UI` :param typeid: type ID :type typeid: str :param children: :type children: list """ self.ui = ui self._prepare() if typeid is not None: self.typeid = typeid for c in children: self.append(c) # Copy properties from the class self.properties = {} self.properties_dirty = {} for prop in self._properties.values(): self.properties[prop.name] = prop.default self.properties_dirty[prop.name] = False for key in kwargs: self.properties[key] = kwargs[key] def __str__(self): return '<%s # %s>' % (self.typeid, self.uid) @property def property_definitions(self): return self.__class__._properties
[docs] def clone(self, set_ui=None, set_context=None): """ :returns: a deep copy of the element and its children. Property values are shallow copies. :rtype: :class:`UIElement` """ o = self.__class__.__new__(self.__class__) o._prepare() o.ui, o.typeid, o.context = (set_ui or self.ui), self.typeid, (set_context or self.context) o.events = self.events.copy() o.event_args = self.event_args.copy() o.properties = self.properties.copy() o.properties_dirty = self.properties_dirty.copy() o.children = [] for c in self.children: o.append(c.clone(set_ui=set_ui, set_context=set_context)) o.post_clone() return o
[docs] def init(self): pass
[docs] def post_clone(self): pass
[docs] def nearest(self, predicate, exclude=None, descend=True): """ Returns the nearest child which matches an arbitrary predicate lambda :param predicate: ``lambda element: bool`` :type predicate: function :param exclude: ``lambda element: bool`` - excludes matching branches from search :type exclude: function, None :param descend: whether to descend inside matching elements :type descend: bool """ r = [] q = [self] while len(q) > 0: e = q.pop(0) if exclude and exclude(e): continue if predicate(e): r.append(e) if not descend and e is not self: continue q.extend(e.children) return r
[docs] def find(self, id): """ :param id: element ID :type id: str :returns: the nearest child with given ID or ``None`` :rtype: :class:`UIElement`, None """ r = self.nearest(lambda x: x.id == id) return r[0] if len(r) > 0 else None
[docs] def find_uid(self, uid): """ :param uid: element UID :type uid: int :returns: the nearest child with given UID or ``None`` :rtype: :class:`UIElement`, None """ r = self.nearest(lambda x: x.uid == uid) return r[0] if len(r) > 0 else None
[docs] def find_type(self, typeid): """ :returns: the nearest child with given type ID or ``None`` :rtype: :class:`UIElement`, None """ r = self.nearest(lambda x: x.typeid == typeid) return r[0] if len(r) > 0 else None
[docs] def contains(self, element): """ Checks if the ``element`` is in the subtree of ``self`` :param element: element :type element: :class:`UIElement` """ return len(self.nearest(lambda x: x == element)) > 0
[docs] def path_to(self, element): """ :returns: a list of elements forming a path from ``self`` to ``element`` :rtype: list """ r = [] while element != self: r.insert(0, element) element = element.parent return r
[docs] def render(self): """ Renders this element and its subtree to JSON :rtype: dict """ attributes = { 'uid': self.uid, 'typeid': self.typeid, 'children': [c.render() for c in self.children if self.visible], } attr_defaults = { 'visible': True, 'client': False, } attr_map = { 'children': '_c', 'typeid': '_t', 'style': '_s', } result = {} for key, value in attributes.iteritems(): if attr_defaults.get(key, None) != value: result[attr_map.get(key, key)] = value for prop in self.properties: if self.property_definitions[prop].public: value = getattr(self, prop) if attr_defaults.get(prop, None) != value: result[attr_map.get(prop, prop)] = value return result
[docs] def on(self, event, handler, *args): """ Binds event with ID ``event`` to ``handler``. ``*args`` will be passed to the ``handler``. :param event: event :type event: str :param handler: handler :type handler: function """ self.events[event] = handler self.event_args[event] = args
[docs] def has_updates(self): """ Checks for pending UI updates """ if self.children_changed or self.invalidated: return True if any(self.properties_dirty.values()): return True if self.visible: for child in self.children: if child.has_updates(): return True return False
[docs] def clear_updates(self): """ Marks all pending updates as processed """ self.children_changed = False self.invalidated = False for property in self.properties: self.properties_dirty[property] = False if self.visible: for child in self.children: child.clear_updates()
[docs] def invalidate(self): self.invalidated = True
[docs] def broadcast(self, method, *args, **kwargs): """ Calls ``method`` on every member of the subtree :param method: method :type method: str """ if hasattr(self, method): getattr(self, method)(*args, **kwargs) if not self.visible: return for child in self.children: child.broadcast(method, *args, **kwargs)
[docs] def dispatch_event(self, uid, event, params=None): """ Dispatches an event to an element with given UID :param uid: element UID :type uid: int :param event: event name :type event: str :param params: event arguments :type params: dict, None """ if not self.visible: return False if self.uid == uid: self.event(event, params) return True else: for child in self.children: if child.dispatch_event(uid, event, params): for k in dir(self): v = getattr(self, k) if hasattr(v, '_event_id'): element = self.find(v._event_id) if element and element.uid == uid and v._event_name == event: getattr(self, k)(**(params or {})) return True
[docs] def event(self, event, params=None): """ Invokes handler for ``event`` on this element with given ``**params`` :param event: event name :type event: str :param params: event arguments :type params: dict, None """ self_event = event.replace('-', '_') if hasattr(self, 'on_%s' % self_event): getattr(self, 'on_%s' % self_event)(**(params or {})) if event in self.events: self.events[event](*self.event_args[event], **(params or {}))
[docs] def reverse_event(self, event, params=None): """ Raises the event on this element by feeding it to the UI root (so that ``@on`` methods in ancestors will work). :param event: event name :type event: str :param params: event arguments :type params: dict """ self.ui.dispatch_event(self.uid, event, params)
[docs] def empty(self): """ Detaches all child elements """ self.children = [] self.children_changed = True
[docs] def append(self, child): """ Appends a ``child`` :param child: child :type child: :class:`UIElement` """ if child in self.children: return self.children.append(child) child.parent = self self.children_changed = True
[docs] def delete(self): """ Detaches this element from its parent """ self.parent.remove(self)
[docs] def remove(self, child): """ Detaches the ``child`` :param child: child :type child: :class:`UIElement` """ self.children.remove(child) child.parent = None self.children_changed = True
@public @plugin class NullElement (UIElement): pass
comments powered by Disqus