[boxes] introduce new boxes system
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Wed, 25 Aug 2010 10:01:11 +0200
changeset 6140 65a619eb31c4
parent 6139 f76599a96238
child 6141 b8287e54b528
[boxes] introduce new boxes system * separate box content generation from its layout * refactor css classes to allow moving boxes and still get consistent ui On the way to contentnavigation/boxes unification (in a later patch) * * * some fixes for the previous (default_new_boxes_system) patch * * * some fixes for the previous (default_new_boxes_system) patch
devtools/testlib.py
selectors.py
tags.py
view.py
vregistry.py
web/action.py
web/box.py
web/component.py
web/data/actionBoxHeader.png
web/data/boxHeader.png
web/data/contextFreeBoxHeader.png
web/data/contextualBoxHeader.png
web/data/cubicweb.calendar.css
web/data/cubicweb.css
web/data/cubicweb.form.css
web/data/cubicweb.login.css
web/data/cubicweb.old.css
web/data/cubicweb.tableview.css
web/data/incontextBoxHeader.png
web/data/uiprops.py
web/htmlwidgets.py
web/views/ajaxedit.py
web/views/basecomponents.py
web/views/basecontrollers.py
web/views/basetemplates.py
web/views/bookmark.py
web/views/boxes.py
web/views/facets.py
web/views/idownloadable.py
web/views/primary.py
web/views/treeview.py
web/views/xmlrss.py
--- a/devtools/testlib.py	Wed Aug 25 09:43:12 2010 +0200
+++ b/devtools/testlib.py	Wed Aug 25 10:01:11 2010 +0200
@@ -903,7 +903,8 @@
         for action in self.list_actions_for(rset):
             yield InnerTest(self._testname(rset, action.__regid__, 'action'), self._test_action, action)
         for box in self.list_boxes_for(rset):
-            yield InnerTest(self._testname(rset, box.__regid__, 'box'), box.render)
+            w = [].append
+            yield InnerTest(self._testname(rset, box.__regid__, 'box'), box.render, w)
 
     @staticmethod
     def _testname(rset, objid, objtype):
--- a/selectors.py	Wed Aug 25 09:43:12 2010 +0200
+++ b/selectors.py	Wed Aug 25 10:01:11 2010 +0200
@@ -60,9 +60,9 @@
 
 .. sourcecode:: python
 
-  class RSSIconBox(ExtResourcesBoxTemplate):
+  class RSSIconBox(box.Box):
     ''' just display the RSS icon on uniform result set '''
-    __select__ = ExtResourcesBoxTemplate.__select__ & non_final_entity()
+    __select__ = box.Box.__select__ & non_final_entity()
 
 It takes into account:
 
@@ -1220,6 +1220,15 @@
     return 1
 
 
+@objectify_selector
+@lltrace
+def contextual(cls, req, view=None, **kwargs):
+    """Return 1 if view's contextual property is true"""
+    if view is not None and view.contextual:
+        return 1
+    return 0
+
+
 class match_view(ExpectedValueSelector):
     """Return 1 if a view is specified an as its registry id is in one of the
     expected view id given to the initializer.
@@ -1231,6 +1240,18 @@
         return 1
 
 
+class match_context(ExpectedValueSelector):
+
+    @lltrace
+    def __call__(self, cls, req, context=None, **kwargs):
+        try:
+            if not context in self.expected:
+                return 0
+        except AttributeError:
+            return 1 # class doesn't care about search state, accept it
+        return 1
+
+
 @objectify_selector
 @lltrace
 def match_context_prop(cls, req, context=None, **kwargs):
--- a/tags.py	Wed Aug 25 09:43:12 2010 +0200
+++ b/tags.py	Wed Aug 25 10:01:11 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""helper classes to generate simple (X)HTML tags
+"""helper classes to generate simple (X)HTML tags"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from cubicweb.uilib import simple_sgml_tag, sgml_attributes
--- a/view.py	Wed Aug 25 09:43:12 2010 +0200
+++ b/view.py	Wed Aug 25 10:01:11 2010 +0200
@@ -504,8 +504,13 @@
 
     build_js = build_update_js_call # expect updatable component by default
 
+    @property
+    def domid(self):
+        return domid(self.__regid__)
+
+    @deprecated('[3.10] use .domid property')
     def div_id(self):
-        return ''
+        return self.domid
 
 
 class Component(ReloadableMixIn, View):
@@ -513,14 +518,20 @@
     __registry__ = 'components'
     __select__ = yes()
 
-    # XXX huummm, much probably useless
+    # XXX huummm, much probably useless (should be...)
     htmlclass = 'mainRelated'
+    @property
+    def cssclass(self):
+        return '%s %s' % (self.htmlclass, domid(self.__regid__))
+
+    # XXX should rely on ReloadableMixIn.domid
+    @property
+    def domid(self):
+        return '%sComponent' % domid(self.__regid__)
+
+    @deprecated('[3.10] use .cssclass property')
     def div_class(self):
-        return '%s %s' % (self.htmlclass, self.__regid__)
-
-    # XXX a generic '%s%s' % (self.__regid__, self.__registry__.capitalize()) would probably be nicer
-    def div_id(self):
-        return '%sComponent' % self.__regid__
+        return self.cssclass
 
 
 class Adapter(AppObject):
--- a/vregistry.py	Wed Aug 25 09:43:12 2010 +0200
+++ b/vregistry.py	Wed Aug 25 10:01:11 2010 +0200
@@ -467,7 +467,7 @@
         self.load_module(module)
 
     def load_module(self, module):
-        self.info('loading %s', module)
+        self.info('loading %s from %s', module.__name__, module.__file__)
         if hasattr(module, 'registration_callback'):
             module.registration_callback(self)
         else:
--- a/web/action.py	Wed Aug 25 09:43:12 2010 +0200
+++ b/web/action.py	Wed Aug 25 10:01:11 2010 +0200
@@ -45,35 +45,31 @@
         for action in self.actual_actions():
             menu.append(box.box_action(action))
 
+    def html_class(self):
+        if self._cw.selected(self.url()):
+            return 'selected'
+
+    def build_action(self, title, url, **kwargs):
+        return UnregisteredAction(self._cw, title, url, **kwargs)
+
     def url(self):
         """return the url associated with this action"""
         raise NotImplementedError
 
-    def html_class(self):
-        if self._cw.selected(self.url()):
-            return 'selected'
-        if self.category:
-            return 'box' + self.category.capitalize()
-
-    def build_action(self, title, path, **kwargs):
-        return UnregisteredAction(self._cw, self.cw_rset, title, path, **kwargs)
-
 
 class UnregisteredAction(Action):
-    """non registered action used to build boxes. Unless you set them
-    explicitly, .vreg and .schema attributes at least are None.
-    """
+    """non registered action, used to build boxes"""
     category = None
     id = None
 
-    def __init__(self, req, rset, title, path, **kwargs):
-        Action.__init__(self, req, rset=rset)
+    def __init__(self, req, title, url, **kwargs):
+        Action.__init__(self, req)
         self.title = req._(title)
-        self._path = path
+        self._url = url
         self.__dict__.update(kwargs)
 
     def url(self):
-        return self._path
+        return self._url
 
 
 class LinkToEntityAction(Action):
--- a/web/box.py	Wed Aug 25 09:43:12 2010 +0200
+++ b/web/box.py	Wed Aug 25 10:01:11 2010 +0200
@@ -21,13 +21,15 @@
 _ = unicode
 
 from logilab.mtconverter import xml_escape
+from logilab.common.deprecation import class_deprecated, class_renamed
 
-from cubicweb import Unauthorized, role as get_role, target as get_target
+from cubicweb import Unauthorized, role as get_role, target as get_target, tags
 from cubicweb.schema import display_name
 from cubicweb.selectors import (no_cnx, one_line_rset,  primary_view,
                                 match_context_prop, partial_relation_possible,
                                 partial_has_related_entities)
-from cubicweb.view import View, ReloadableMixIn
+from cubicweb.appobject import AppObject
+from cubicweb.view import View, ReloadableMixIn, Component
 from cubicweb.uilib import domid, js
 from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs
 from cubicweb.web.htmlwidgets import (BoxLink, BoxWidget, SideBoxWidget,
@@ -35,20 +37,84 @@
 from cubicweb.web.action import UnregisteredAction
 
 
-class BoxTemplate(View):
-    """base template for boxes, usually a (contextual) list of possible
+def sort_by_category(actions, categories_in_order=None):
+    """return a list of (category, actions_sorted_by_title)"""
+    result = []
+    actions_by_cat = {}
+    for action in actions:
+        actions_by_cat.setdefault(action.category, []).append(
+            (action.title, action) )
+    for key, values in actions_by_cat.items():
+        actions_by_cat[key] = [act for title, act in sorted(values)]
+    if categories_in_order:
+        for cat in categories_in_order:
+            if cat in actions_by_cat:
+                result.append( (cat, actions_by_cat[cat]) )
+    for item in sorted(actions_by_cat.items()):
+        result.append(item)
+    return result
+
 
-    actions. Various classes attributes may be used to control the box
-    rendering.
+class EditRelationMixIn(ReloadableMixIn):
+    def box_item(self, entity, etarget, rql, label):
+        """builds HTML link to edit relation between `entity` and `etarget`"""
+        role, target = get_role(self), get_target(self)
+        args = {role[0] : entity.eid, target[0] : etarget.eid}
+        url = self._cw.user_rql_callback((rql, args))
+        # for each target, provide a link to edit the relation
+        return u'[<a href="%s">%s</a>] %s' % (xml_escape(url), label,
+                                              etarget.view('incontext'))
+
+    def related_boxitems(self, entity):
+        rql = 'DELETE S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype
+        return [self.box_item(entity, etarget, rql, u'-')
+                for etarget in self.related_entities(entity)]
+
+    def related_entities(self, entity):
+        return entity.related(self.rtype, get_role(self), entities=True)
 
-    You may override on of the formatting callbacks is this is not necessary
-    for your custom box.
+    def unrelated_boxitems(self, entity):
+        rql = 'SET S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype
+        return [self.box_item(entity, etarget, rql, u'+')
+                for etarget in self.unrelated_entities(entity)]
 
-    Classes inheriting from this class usually only have to override call
-    to fetch desired actions, and then to do something like  ::
+    def unrelated_entities(self, entity):
+        """returns the list of unrelated entities, using the entity's
+        appropriate vocabulary function
+        """
+        skip = set(unicode(e.eid) for e in entity.related(self.rtype, get_role(self),
+                                                          entities=True))
+        skip.add(None)
+        skip.add(INTERNAL_FIELD_VALUE)
+        filteretype = getattr(self, 'etype', None)
+        entities = []
+        form = self._cw.vreg['forms'].select('edition', self._cw,
+                                             rset=self.cw_rset,
+                                             row=self.cw_row or 0)
+        field = form.field_by_name(self.rtype, get_role(self), entity.e_schema)
+        for _, eid in field.vocabulary(form):
+            if eid not in skip:
+                entity = self._cw.entity_from_eid(eid)
+                if filteretype is None or entity.__regid__ == filteretype:
+                    entities.append(entity)
+        return entities
 
-        box.render(self.w)
+
+# generic classes for the new box system #######################################
+
+from cubicweb.selectors import match_context, contextual
+
+class EmptyComponent(Exception):
+    """some selectable component has actually no content and should not be
+    rendered
     """
+
+class Layout(Component):
+    __regid__ = 'layout'
+    __abstract__ = True
+
+
+class Box(AppObject): # XXX ContextComponent
     __registry__ = 'boxes'
     __select__ = ~no_cnx() & match_context_prop()
 
@@ -64,34 +130,289 @@
                            help=_('context where this box should be displayed')),
         }
     context = 'left'
-    htmlitemclass = 'boxItem'
+    contextual = False
+    title = None
+    # XXX support kwargs for compat with old boxes which gets the view as
+    # argument
+    def render(self, w, **kwargs):
+        getlayout = self._cw.vreg['components'].select
+        try:
+            # XXX ensure context is given when the component is reloaded through
+            # ajax
+            context = self.cw_extra_kwargs['context']
+        except KeyError:
+            context = self.cw_propval('context')
+        layout = getlayout('layout', self._cw, rset=self.cw_rset,
+                           row=self.cw_row, col=self.cw_col,
+                           view=self, context=context)
+        layout.render(w)
+
+    def init_rendering(self):
+        """init rendering callback: that's the good time to check your component
+        has some content to display. If not, you can still raise
+        :exc:`EmptyComponent` to inform it should be skipped.
+
+        Also, :exc:`Unauthorized` will be catched, logged, then the component
+        will be skipped.
+        """
+        self.items = []
+
+    @property
+    def domid(self):
+        """return the HTML DOM identifier for this component"""
+        return domid(self.__regid__)
+
+    @property
+    def cssclass(self):
+        """return the CSS class name for this component"""
+        return domid(self.__regid__)
+
+    def render_title(self, w):
+        """return the title for this component"""
+        if self.title is None:
+            raise NotImplementedError()
+        w(self._cw._(self.title))
+
+    def render_body(self, w):
+        """return the body (content) for this component"""
+        raise NotImplementedError()
+
+    def render_items(self, w, items=None, klass=u'boxListing'):
+        if items is None:
+            items = self.items
+        assert items
+        w(u'<ul class="%s">' % klass)
+        for item in items:
+            if hasattr(item, 'render'):
+                item.render(w) # XXX display <li> by itself
+            else:
+                w(u'<li>')
+                w(item)
+                w(u'</li>')
+        w(u'</ul>')
+
+    def append(self, item):
+        self.items.append(item)
+
+    def box_action(self, action): # XXX action_link
+        return self.build_link(self._cw._(action.title), action.url())
+
+    def build_link(self, title, url, **kwargs):
+        if self._cw.selected(url):
+            try:
+                kwargs['klass'] += ' selected'
+            except KeyError:
+                kwargs['klass'] = 'selected'
+        return tags.a(title, href=url, **kwargs)
+
+
+class EntityBox(Box): # XXX ContextEntityComponent
+    """base class for boxes related to a single entity"""
+    __select__ = Box.__select__ & one_line_rset()
+    context = 'incontext'
+    contextual = True
+
+    def __init__(self, *args, **kwargs):
+        super(EntityBox, self).__init__(*args, **kwargs)
+        try:
+            entity = kwargs['entity']
+        except KeyError:
+            entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
+        self.entity = entity
+
+    @property
+    def domid(self):
+        return domid(self.__regid__) + unicode(self.entity.eid)
+
+
+# high level abstract box classes ##############################################
+
+
+class RQLBox(Box):
+    """abstract box for boxes displaying the content of a rql query not
+    related to the current result set.
+    """
+    rql  = None
+
+    def to_display_rql(self):
+        assert self.rql is not None, self.__regid__
+        return (self.rql,)
+
+    def init_rendering(self):
+        rset = self._cw.execute(*self.to_display_rql())
+        if not rset:
+            raise EmptyComponent()
+        if len(rset[0]) == 2:
+            self.items = []
+            for i, (eid, label) in enumerate(rset):
+                entity = rset.get_entity(i, 0)
+                self.items.append(self.build_link(label, entity.absolute_url()))
+        else:
+            self.items = [self.build_link(e.dc_title(), e.absolute_url())
+                          for e in rset.entities()]
+
+    def render_body(self, w):
+        self.render_items(w)
+
+
+class EditRelationBox(EditRelationMixIn, EntityBox):
+    """base class for boxes which let add or remove entities linked by a given
+    relation
+
+    subclasses should define at least id, rtype and target class attributes.
+    """
+    def render_title(self, w):
+        return display_name(self._cw, self.rtype, get_role(self),
+                            context=self.entity.__regid__)
+
+    def render_body(self, w):
+        self._cw.add_js('cubicweb.ajax.js')
+        related = self.related_boxitems(self.entity)
+        unrelated = self.unrelated_boxitems(self.entity)
+        self.items.extend(related)
+        if related and unrelated:
+            self.items.append(BoxSeparator())
+        self.items.extend(unrelated)
+        self.render_items(w)
+
+
+class AjaxEditRelationBox(EntityBox):
+    __select__ = EntityBox.__select__ & (
+        partial_relation_possible(action='add') | partial_has_related_entities())
+
+    # view used to display related entties
+    item_vid = 'incontext'
+    # values separator when multiple values are allowed
+    separator = ','
+    # msgid of the message to display when some new relation has been added/removed
+    added_msg = None
+    removed_msg = None
+
+    # class attributes below *must* be set in concret classes (additionaly to
+    # rtype / role [/ target_etype]. They should correspond to js_* methods on
+    # the json controller
+
+    # function(eid)
+    # -> expected to return a list of values to display as input selector
+    #    vocabulary
+    fname_vocabulary = None
+
+    # function(eid, value)
+    # -> handle the selector's input (eg create necessary entities and/or
+    # relations). If the relation is multiple, you'll get a list of value, else
+    # a single string value.
+    fname_validate = None
+
+    # function(eid, linked entity eid)
+    # -> remove the relation
+    fname_remove = None
+
+    def __init__(self, *args, **kwargs):
+        super(AjaxEditRelationBox, self).__init__(*args, **kwargs)
+        self.rdef = self.entity.e_schema.rdef(self.rtype, self.role, self.target_etype)
+
+    def render_title(self, w):
+        w(self.rdef.rtype.display_name(self._cw, self.role,
+                                       context=self.entity.__regid__))
+
+    def render_body(self, w):
+        req = self._cw
+        entity = self.entity
+        related = entity.related(self.rtype, self.role)
+        if self.role == 'subject':
+            mayadd = self.rdef.has_perm(req, 'add', fromeid=entity.eid)
+            maydel = self.rdef.has_perm(req, 'delete', fromeid=entity.eid)
+        else:
+            mayadd = self.rdef.has_perm(req, 'add', toeid=entity.eid)
+            maydel = self.rdef.has_perm(req, 'delete', toeid=entity.eid)
+        if mayadd or maydel:
+            req.add_js(('cubicweb.ajax.js', 'cubicweb.ajax.box.js'))
+        _ = req._
+        if related:
+            w(u'<table>')
+            for rentity in related.entities():
+                # for each related entity, provide a link to remove the relation
+                subview = rentity.view(self.item_vid)
+                if maydel:
+                    jscall = unicode(js.ajaxBoxRemoveLinkedEntity(
+                        self.__regid__, entity.eid, rentity.eid,
+                        self.fname_remove,
+                        self.removed_msg and _(self.removed_msg)))
+                    w(u'<tr><td>[<a href="javascript: %s">-</a>]</td>'
+                      '<td class="tagged"> %s</td></tr>' % (xml_escape(jscall),
+                                                            subview))
+                else:
+                    w(u'<tr><td class="tagged">%s</td></tr>' % (subview))
+            w(u'</table>')
+        else:
+            w(_('no related entity'))
+        if mayadd:
+            req.add_js('jquery.autocomplete.js')
+            req.add_css('jquery.autocomplete.css')
+            multiple = self.rdef.role_cardinality(self.role) in '*+'
+            w(u'<table><tr><td>')
+            jscall = unicode(js.ajaxBoxShowSelector(
+                self.__regid__, entity.eid, self.fname_vocabulary,
+                self.fname_validate, self.added_msg and _(self.added_msg),
+                _(stdmsgs.BUTTON_OK[0]), _(stdmsgs.BUTTON_CANCEL[0]),
+                multiple and self.separator))
+            w('<a class="button sglink" href="javascript: %s">%s</a>' % (
+                xml_escape(jscall),
+                multiple and _('add_relation') or _('update_relation')))
+            w(u'</td><td>')
+            w(u'<div id="%sHolder"></div>' % self.domid)
+            w(u'</td></tr></table>')
+
+
+# old box system, deprecated ###################################################
+
+class BoxTemplate(View):
+    """base template for boxes, usually a (contextual) list of possible
+
+    actions. Various classes attributes may be used to control the box
+    rendering.
+
+    You may override on of the formatting callbacks is this is not necessary
+    for your custom box.
+
+    Classes inheriting from this class usually only have to override call
+    to fetch desired actions, and then to do something like  ::
+
+        box.render(self.w)
+    """
+    __metaclass__ = class_deprecated
+    __deprecation_warning__ = '*BoxTemplate classes are deprecated, use *Box instead'
+
+    __registry__ = 'boxes'
+    __select__ = ~no_cnx() & match_context_prop()
+
+    categories_in_order = ()
+    cw_property_defs = {
+        _('visible'): dict(type='Boolean', default=True,
+                           help=_('display the box or not')),
+        _('order'):   dict(type='Int', default=99,
+                           help=_('display order of the box')),
+        # XXX 'incontext' boxes are handled by the default primary view
+        _('context'): dict(type='String', default='left',
+                           vocabulary=(_('left'), _('incontext'), _('right')),
+                           help=_('context where this box should be displayed')),
+        }
+    context = 'left'
 
     def sort_actions(self, actions):
         """return a list of (category, actions_sorted_by_title)"""
-        result = []
-        actions_by_cat = {}
-        for action in actions:
-            actions_by_cat.setdefault(action.category, []).append(
-                (action.title, action) )
-        for key, values in actions_by_cat.items():
-            actions_by_cat[key] = [act for title, act in sorted(values)]
-        for cat in self.categories_in_order:
-            if cat in actions_by_cat:
-                result.append( (cat, actions_by_cat[cat]) )
-        for item in sorted(actions_by_cat.items()):
-            result.append(item)
-        return result
+        return sort_by_category(actions, self.categories_in_order)
 
-    def mk_action(self, title, path, escape=True, **kwargs):
+    def mk_action(self, title, url, escape=True, **kwargs):
         """factory function to create dummy actions compatible with the
         .format_actions method
         """
         if escape:
             title = xml_escape(title)
-        return self.box_action(self._action(title, path, **kwargs))
+        return self.box_action(self._action(title, url, **kwargs))
 
-    def _action(self, title, path, **kwargs):
-        return UnregisteredAction(self._cw, self.cw_rset, title, path, **kwargs)
+    def _action(self, title, url, **kwargs):
+        return UnregisteredAction(self._cw, title, url, **kwargs)
 
     # formating callbacks
 
@@ -101,18 +422,14 @@
         return u''
 
     def box_action(self, action):
-        cls = getattr(action, 'html_class', lambda: None)() or self.htmlitemclass
+        klass = getattr(action, 'html_class', lambda: None)()
         return BoxLink(action.url(), self._cw._(action.title),
-                       cls, self.boxitem_link_tooltip(action))
+                       klass, self.boxitem_link_tooltip(action))
 
 
 class RQLBoxTemplate(BoxTemplate):
     """abstract box for boxes displaying the content of a rql query not
     related to the current result set.
-
-    It rely on etype, rtype (both optional, usable to control registration
-    according to application schema and display according to connected
-    user's rights) and rql attributes
     """
 
     rql  = None
@@ -156,21 +473,7 @@
         self.cell_call(row, col, **kwargs)
 
 
-class RelatedEntityBoxTemplate(EntityBoxTemplate):
-    __select__ = EntityBoxTemplate.__select__ & partial_has_related_entities()
-
-    def cell_call(self, row, col, **kwargs):
-        entity = self.cw_rset.get_entity(row, col)
-        limit = self._cw.property_value('navigation.related-limit') + 1
-        role = get_role(self)
-        self.w(u'<div class="sideBox">')
-        self.wview('sidebox', entity.related(self.rtype, role, limit=limit),
-                   title=display_name(self._cw, self.rtype, role,
-                                      context=entity.__regid__))
-        self.w(u'</div>')
-
-
-class EditRelationBoxTemplate(ReloadableMixIn, EntityBoxTemplate):
+class EditRelationBoxTemplate(EditRelationMixIn, EntityBoxTemplate):
     """base class for boxes which let add or remove entities linked
     by a given relation
 
@@ -181,7 +484,8 @@
     def cell_call(self, row, col, view=None, **kwargs):
         self._cw.add_js('cubicweb.ajax.js')
         entity = self.cw_rset.get_entity(row, col)
-        title = display_name(self._cw, self.rtype, get_role(self), context=entity.__regid__)
+        title = display_name(self._cw, self.rtype, get_role(self),
+                             context=entity.__regid__)
         box = SideBoxWidget(title, self.__regid__)
         related = self.related_boxitems(entity)
         unrelated = self.unrelated_boxitems(entity)
@@ -191,144 +495,13 @@
         box.extend(unrelated)
         box.render(self.w)
 
-    def div_id(self):
-        return self.__regid__
-
     def box_item(self, entity, etarget, rql, label):
-        """builds HTML link to edit relation between `entity` and `etarget`
-        """
-        role, target = get_role(self), get_target(self)
-        args = {role[0] : entity.eid, target[0] : etarget.eid}
-        url = self._cw.user_rql_callback((rql, args))
-        # for each target, provide a link to edit the relation
-        label = u'[<a href="%s">%s</a>] %s' % (xml_escape(url), label,
-                                               etarget.view('incontext'))
+        label = super(EditRelationBoxTemplate, self).box_item(
+            entity, etarget, rql, label)
         return RawBoxItem(label, liclass=u'invisible')
 
-    def related_boxitems(self, entity):
-        rql = 'DELETE S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype
-        related = []
-        for etarget in self.related_entities(entity):
-            related.append(self.box_item(entity, etarget, rql, u'-'))
-        return related
-
-    def unrelated_boxitems(self, entity):
-        rql = 'SET S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype
-        unrelated = []
-        for etarget in self.unrelated_entities(entity):
-            unrelated.append(self.box_item(entity, etarget, rql, u'+'))
-        return unrelated
-
-    def related_entities(self, entity):
-        return entity.related(self.rtype, get_role(self), entities=True)
-
-    def unrelated_entities(self, entity):
-        """returns the list of unrelated entities, using the entity's
-        appropriate vocabulary function
-        """
-        skip = set(unicode(e.eid) for e in entity.related(self.rtype, get_role(self),
-                                                          entities=True))
-        skip.add(None)
-        skip.add(INTERNAL_FIELD_VALUE)
-        filteretype = getattr(self, 'etype', None)
-        entities = []
-        form = self._cw.vreg['forms'].select('edition', self._cw,
-                                             rset=self.cw_rset,
-                                             row=self.cw_row or 0)
-        field = form.field_by_name(self.rtype, get_role(self), entity.e_schema)
-        for _, eid in field.vocabulary(form):
-            if eid not in skip:
-                entity = self._cw.entity_from_eid(eid)
-                if filteretype is None or entity.__regid__ == filteretype:
-                    entities.append(entity)
-        return entities
-
 
-class AjaxEditRelationBoxTemplate(EntityBoxTemplate):
-    __select__ = EntityBoxTemplate.__select__ & partial_relation_possible()
-
-    # view used to display related entties
-    item_vid = 'incontext'
-    # values separator when multiple values are allowed
-    separator = ','
-    # msgid of the message to display when some new relation has been added/removed
-    added_msg = None
-    removed_msg = None
-
-    # class attributes below *must* be set in concret classes (additionaly to
-    # rtype / role [/ target_etype]. They should correspond to js_* methods on
-    # the json controller
-
-    # function(eid)
-    # -> expected to return a list of values to display as input selector
-    #    vocabulary
-    fname_vocabulary = None
-
-    # function(eid, value)
-    # -> handle the selector's input (eg create necessary entities and/or
-    # relations). If the relation is multiple, you'll get a list of value, else
-    # a single string value.
-    fname_validate = None
-
-    # function(eid, linked entity eid)
-    # -> remove the relation
-    fname_remove = None
+AjaxEditRelationBoxTemplate = class_renamed(
+    'AjaxEditRelationBoxTemplate', AjaxEditRelationBox,
+    '[3.10] AjaxEditRelationBoxTemplate has been renamed to AjaxEditRelationBox')
 
-    def cell_call(self, row, col, **kwargs):
-        req = self._cw
-        entity = self.cw_rset.get_entity(row, col)
-        related = entity.related(self.rtype, self.role)
-        rdef = entity.e_schema.rdef(self.rtype, self.role, self.target_etype)
-        if self.role == 'subject':
-            mayadd = rdef.has_perm(req, 'add', fromeid=entity.eid)
-            maydel = rdef.has_perm(req, 'delete', fromeid=entity.eid)
-        else:
-            mayadd = rdef.has_perm(req, 'add', toeid=entity.eid)
-            maydel = rdef.has_perm(req, 'delete', toeid=entity.eid)
-        if not (related or mayadd):
-            return
-        if mayadd or maydel:
-            req.add_js(('cubicweb.ajax.js', 'cubicweb.ajax.box.js'))
-        _ = req._
-        w = self.w
-        divid = domid(self.__regid__) + unicode(entity.eid)
-        w(u'<div class="sideBox" id="%s%s">' % (domid(self.__regid__), entity.eid))
-        w(u'<div class="sideBoxTitle"><span>%s</span></div>' %
-               rdef.rtype.display_name(req, self.role, context=entity.__regid__))
-        w(u'<div class="sideBox"><div class="sideBoxBody">')
-        if related:
-            w(u'<table>')
-            for rentity in related.entities():
-                # for each related entity, provide a link to remove the relation
-                subview = rentity.view(self.item_vid)
-                if maydel:
-                    jscall = unicode(js.ajaxBoxRemoveLinkedEntity(
-                        self.__regid__, entity.eid, rentity.eid,
-                        self.fname_remove,
-                        self.removed_msg and _(self.removed_msg)))
-                    w(u'<tr><td>[<a href="javascript: %s">-</a>]</td>'
-                      '<td class="tagged">%s</td></tr>' % (xml_escape(jscall),
-                                                           subview))
-                else:
-                    w(u'<tr><td class="tagged">%s</td></tr>' % (subview))
-            w(u'</table>')
-        else:
-            w(_('no related entity'))
-        if mayadd:
-            req.add_js('jquery.autocomplete.js')
-            req.add_css('jquery.autocomplete.css')
-            multiple = rdef.role_cardinality(self.role) in '*+'
-            w(u'<table><tr><td>')
-            jscall = unicode(js.ajaxBoxShowSelector(
-                self.__regid__, entity.eid, self.fname_vocabulary,
-                self.fname_validate, self.added_msg and _(self.added_msg),
-                _(stdmsgs.BUTTON_OK[0]), _(stdmsgs.BUTTON_CANCEL[0]),
-                multiple and self.separator))
-            w('<a class="button sglink" href="javascript: %s">%s</a>' % (
-                xml_escape(jscall),
-                multiple and _('add_relation') or _('update_relation')))
-            w(u'</td><td>')
-            w(u'<div id="%sHolder"></div>' % divid)
-            w(u'</td></tr></table>')
-        w(u'</div>\n')
-        w(u'</div></div>\n')
--- a/web/component.py	Wed Aug 25 09:43:12 2010 +0200
+++ b/web/component.py	Wed Aug 25 10:01:11 2010 +0200
@@ -197,7 +197,7 @@
             rset = self._cw.execute(self.rql(), {'x': eid})
         if not rset.rowcount:
             return
-        self.w(u'<div class="%s">' % self.div_class())
+        self.w(u'<div class="%s">' % self.cssclass)
         self.w(u'<h4>%s</h4>\n' % self._cw._(self.title).capitalize())
         self.wview(self.vid, rset)
         self.w(u'</div>')
Binary file web/data/actionBoxHeader.png has changed
Binary file web/data/boxHeader.png has changed
Binary file web/data/contextFreeBoxHeader.png has changed
Binary file web/data/contextualBoxHeader.png has changed
--- a/web/data/cubicweb.calendar.css	Wed Aug 25 09:43:12 2010 +0200
+++ b/web/data/cubicweb.calendar.css	Wed Aug 25 10:01:11 2010 +0200
@@ -230,7 +230,7 @@
 .calendar th.month {
  font-weight:bold;
  padding-bottom:0.2em;
- background: %(actionBoxTitleBgColor)s;
+ background: %(incontextBoxBodyBgColor)s;
 }
 
 .calendar th.month a{
--- a/web/data/cubicweb.css	Wed Aug 25 09:43:12 2010 +0200
+++ b/web/data/cubicweb.css	Wed Aug 25 10:01:11 2010 +0200
@@ -31,19 +31,22 @@
 /* h3 { font-size:1.30769em; } */
 
 /* scale traditional */
-h1 { font-size: %(h1FontSize)s; }
+h1,
+.vtitle { font-size: %(h1FontSize)s; }
 h2 { font-size: %(h2FontSize)s; }
 h3 { font-size: %(h3FontSize)s; }
 
 /* paddings */
-h1 {
+h1,
+.vtitle {
   border-bottom: %(h1BorderBottomStyle)s;
   padding: %(h1Padding)s;
   margin: %(h1Margin)s;
   color: %(h1Color)s;
 }
 
-h1.plain {
+h1.plain,
+.vtitle {
  border-bottom: none;
 }
 
@@ -100,7 +103,7 @@
 }
 
 ol ol,
-ul ul{
+ul ul {
   margin-left: 8px;
   margin-bottom : 0px;
 }
@@ -113,7 +116,7 @@
   margin-left: 1.5em;
 }
 
-img{
+img {
   border: none;
 }
 
@@ -139,7 +142,7 @@
   border: 1px inset %(headerBgColor)s;
 }
 
-hr{
+hr {
   border: none;
   border-bottom: 1px solid %(defaultColor)s;
   height: 1px;
@@ -234,16 +237,11 @@
 /* Popup on login box and userActionBox */
 div.popupWrapper {
   position: relative;
-  z-index: 100;
 }
 
 div.popup {
   position: absolute;
   background: #fff;
-  /* background-color: #f0eff0; */
-  /* background-image: url(popup.png); */
-  /* background-repeat: repeat-x; */
-  /* background-positon: top left; */
   border: 1px solid %(listingBorderColor)s;
   border-top: none;
   text-align: left;
@@ -261,12 +259,13 @@
   margin: %(defaultLayoutMargin)s;
 }
 
-table#mainLayout #navColumnLeft {
+table#mainLayout td#navColumnLeft {
   width: 16em;
   padding-right: %(defaultLayoutMargin)s;
+
 }
 
-table#mainLayout #navColumnRight {
+table#mainLayout td#navColumnRight {
   width: 16em;
   padding-left: %(defaultLayoutMargin)s;
 }
@@ -301,28 +300,15 @@
   color: %(defaultColor)s;
 }
 
-/* rql bar */
-
-div#rqlinput {
-  margin-bottom: %(defaultLayoutMargin)s;
-}
-
-input#rql{
-  padding: 0.25em 0.3em;
-  width: 99%;
-}
-
-/* boxes */
+/* XXX old boxes, deprecated */
 
 div.boxFrame {
   width: 100%;
 }
 
 div.boxTitle {
-  overflow: hidden;
-  font-weight: bold;
   color: #fff;
-  background: %(boxTitleBg)s;
+  background: %(contextualBoxTitleBgColor)s;
 }
 
 div.boxTitle span,
@@ -331,14 +317,7 @@
   white-space: nowrap;
 }
 
-div.searchBoxFrame div.boxTitle,
-div.greyBoxFrame div.boxTitle {
-  background: %(actionBoxTitleBg)s;
-}
-
-div.sideBoxTitle span,
-div.searchBoxFrame div.boxTitle span,
-div.greyBoxFrame div.boxTitle span {
+div.sideBoxTitle span {
   color: %(defaultColor)s;
 }
 
@@ -352,34 +331,13 @@
   border-top: none;
 }
 
-a.boxMenu {
-  display: block;
-  padding: 1px 9px 1px 3px;
-  background: transparent %(bulletDownImg)s;
-}
-
-a.boxMenu:hover {
-  background: %(sideBoxBodyBgColor)s %(bulletDownImg)s;
-  cursor: pointer;
-}
-
-a.popupMenu {
-  background: transparent url("puce_down_black.png") 2% 6px no-repeat;
-  padding-left: 2em;
-}
-
-div.searchBoxFrame div.boxContent {
-  padding: 4px 4px 3px;
-  background: #f0eff0 url("gradient-grey-up.png") left top repeat-x;
-}
-
 div.shadow{
   height: 14px;
   background: url("shadow.gif") no-repeat top right;
 }
 
 div.sideBoxTitle {
-  background: %(actionBoxTitleBg)s;
+  background: %(incontextBoxBodyBg)s;
   display: block;
   font-weight: bold;
 }
@@ -400,11 +358,11 @@
 
 div.sideBoxBody {
   padding: 0.2em 5px;
-  background: %(sideBoxBodyBg)s;
+  background: %(incontextBoxBodyBg)s;
 }
 
 div.sideBoxBody a {
-  color: %(sideBoxBodyColor)s;
+  color: %(incontextBoxBodyColor)s;
 }
 
 div.sideBoxBody a:hover {
@@ -415,6 +373,174 @@
   padding-right: 1em;
 }
 
+/* boxes */
+
+div.boxTitle {
+  overflow: hidden;
+  font-weight: bold;
+}
+
+div.boxTitle span {
+  padding: 0px 0.5em;
+  white-space: nowrap;
+}
+
+div.boxBody {
+  padding: 5px;
+  border-top: none;
+  background-color: %(leftrightBoxBodyBgColor)s;
+}
+
+div.boxBody a {
+  color: %(leftrightBoxBodyColor)s;
+}
+
+div.boxBody a:hover {
+  text-decoration: none;
+  cursor: pointer;
+  background-color: %(leftrightBoxBodyHoverBgColor)s;
+}
+
+/* boxes contextual customization */
+
+.contextFreeBox div.boxTitle {
+  background: %(contextFreeBoxTitleBg)s;
+  color: %(contextFreeBoxTitleColor)s;
+}
+
+.contextualBox div.boxTitle {
+  background: %(contextualBoxTitleBg)s;
+  color: %(contextualBoxTitleColor)s;
+}
+
+.primaryRight div.boxTitle {
+  background: %(incontextBoxTitleBg)s;
+  color: %(incontextBoxTitleColor)s;
+}
+
+.primaryRight div.boxBody {
+  padding: 0.2em 5px;
+  background: %(incontextBoxBodyBgColor)s;
+}
+
+.primaryRight div.boxBody a {
+  color: %(incontextBoxBodyColor)s;
+}
+
+.primaryRight div.boxBody a:hover {
+  background-color: %(incontextBoxBodyHoverBgColor)s;
+}
+
+.primaryRight div.boxFooter {
+  margin-bottom: 1em;
+}
+
+#navColumnLeft div.boxFooter, #navColumnRight div.boxFooter{
+  height: 14px;
+  background: url("shadow.gif") no-repeat top right;
+}
+
+/* boxes lists and menus */
+
+ul.boxListing {
+  margin: 0;
+  padding: 0;
+}
+
+ul.boxListing ul {
+  padding: 1px 3px;
+}
+
+ul.boxListing a {
+  color: %(defaultColor)s;
+  display: block;
+  padding: 1px 9px 1px 3px;
+}
+
+ul.boxListing li {
+  margin: 0px;
+  padding: 0px;
+  background-image: none;
+}
+
+ul.boxListing ul li {
+  margin: 0px;
+  padding-left: 8px;
+}
+
+ul.boxListing ul li a {
+  padding-left: 10px;
+  background-image: url("bullet_orange.png");
+  background-repeat: no-repeat;
+  background-position: 0 6px;
+}
+
+ul.boxListing .selected {
+  color: %(aColor)s;
+  font-weight: bold;
+}
+
+ul.boxListing a.boxMenu:hover {
+  border-top: medium none;
+  background: %(leftrightBoxBodyHoverBgColor)s;
+}
+
+a.boxMenu,
+ul.boxListing a.boxMenu{
+  display: block;
+  padding: 1px 3px;
+  background: transparent %(bulletDownImg)s;
+}
+
+ul.boxListing a.boxMenu:hover {
+  border-top: medium none;
+  background: %(leftrightBoxBodyHoverBgColor)s %(bulletDownImg)s;
+}
+
+a.boxMenu:hover {
+  cursor: pointer;
+}
+
+a.popupMenu {
+  background: transparent url("puce_down_black.png") 2% 6px no-repeat;
+  padding-left: 2em;
+}
+
+/* custom boxes */
+
+.search_box div.boxBody {
+  padding: 4px 4px 3px;
+  background: #f0eff0 url("gradient-grey-up.png") left top repeat-x;
+}
+
+.bookmarks_box ul.boxListing div a{
+  background: #fff;
+  display: inline;
+  padding: 0;
+}
+.bookmarks_box ul.boxListing div a:hover{
+  border-bottom: 1px solid #000;
+}
+
+.download_box div.boxTitle {
+  background : #8fbc8f !important;
+}
+
+.download_box div.boxBody {
+  background : #eefed9;
+}
+
+/* search box and rql bar */
+
+div#rqlinput {
+  margin-bottom: %(defaultLayoutMargin)s;
+}
+
+input#rql{
+  padding: 0.25em 0.3em;
+  width: 99%;
+}
+
 input.rqlsubmit{
   display: block;
   width: 20px;
@@ -424,7 +550,7 @@
 }
 
 input#norql{
-  width:13em;
+  width:155px;
   margin-right: 2px;
 }
 
@@ -435,7 +561,7 @@
 }
 
 div#userActionsBox {
-  width: 14em;
+  width: 15em;
   text-align: right;
 }
 
@@ -445,20 +571,6 @@
   padding-right: 2em;
 }
 
-/* download box XXX move to its own file? */
-div.downloadBoxTitle{
- background : #8fbc8f;
- font-weight: bold;
-}
-
-div.downloadBox{
- font-weight: bold;
-}
-
-div.downloadBox div.sideBoxBody{
- background : #eefed9;
-}
-
 /**************/
 /* navigation */
 /**************/
@@ -565,7 +677,7 @@
 
 div#appMsg {
   margin-bottom: %(defaultLayoutMargin)s;
-  border: 1px solid %(actionBoxTitleBgColor)s;
+  border: 1px solid %(incontextBoxTitleBgColor)s;
 }
 
 .message {
@@ -578,7 +690,7 @@
   padding-left: 25px;
   background: %(msgBgColor)s url("critical.png") 2px center no-repeat;
   color: %(errorMsgColor)s;
-  border: 1px solid %(actionBoxTitleBgColor)s;
+  border: 1px solid %(incontextBoxTitleBgColor)s;
 }
 
 /* search-associate message */
@@ -733,7 +845,7 @@
 input.button{
   margin: 1em 1em 0px 0px;
   border: 1px solid %(buttonBorderColor)s;
-  border-color: %(buttonBorderColor)s %(actionBoxTitleBgColor)s %(actionBoxTitleBgColor)s %(buttonBorderColor)s;
+  border-color: %(buttonBorderColor)s %(incontextBoxTitleBgColor)s %(incontextBoxTitleBgColor)s %(buttonBorderColor)s;
 }
 
 /* FileItemInnerView  jquery.treeview.css */
@@ -753,74 +865,20 @@
 
 ul.startup li,
 ul.section li {
-  margin-left:0px
-}
-
-ul.boxListing {
-  margin: 0px;
-  padding: 0px 3px;
-}
-
-ul.boxListing li,
-ul.boxListing ul li {
-  margin: 0px;
-  padding: 0px;
-  background-image: none;
-}
-
-ul.boxListing ul {
-  padding: 1px 3px;
-}
-
-ul.boxListing a {
-  color: %(defaultColor)s;
-  display:block;
-  padding: 1px 9px 1px 3px;
-}
-
-ul.boxListing .selected {
-  color: %(aColor)s;
-  font-weight: bold;
-}
-
-ul.boxListing a.boxMenu:hover {
-  border-top: medium none;
-  background: %(sideBoxBodyBgColor)s %(bulletDownImg)s;
-}
-
-ul.boxListing a.boxBookmark {
-  padding-left: 3px;
-  background-image: none;
-  background:#fff;
+  margin-left: 0px
 }
 
 ul.simple li,
-ul.boxListing ul li ,
 .popupWrapper ul li {
   background: transparent url("bullet_orange.png") no-repeat 0% 6px;
 }
 
-ul.boxListing a.boxBookmark:hover,
-ul.boxListing a:hover,
-ul.boxListing ul li a:hover {
-  text-decoration: none;
-  background: %(sideBoxBodyBg)s;
-}
-
-ul.boxListing ul li a:hover{
-  background-color: transparent;
-}
-
-ul.boxListing ul li a {
-  padding: 1px 3px 0px 10px;
-}
-
 ul.simple li {
   padding-left: 8px;
 }
 
 .popupWrapper ul {
-  padding:0.2em 0.3em;
+  padding: 0.2em 0.3em;
   margin-bottom: 0px;
 }
 
@@ -853,20 +911,12 @@
 .validateButton {
   margin: 1em 1em 0px 0px;
   border: 1px solid %(buttonBorderColor)s;
-  border-color: %(buttonBorderColor)s %(actionBoxTitleBgColor)s %(actionBoxTitleBgColor)s %(buttonBorderColor)s;
+  border-color: %(buttonBorderColor)s %(incontextBoxTitleBgColor)s %(incontextBoxTitleBgColor)s %(buttonBorderColor)s;
   background: %(buttonBgColor)s url("button.png") bottom left repeat-x;
 }
 
 /********************************/
-/* placement of alt. view icons */
-/********************************/
-
-.otherView {
-  float: right;
-}
-
-/********************************/
-/* rest releted classes         */
+/* rest related classes         */
 /********************************/
 
 img.align-right {
--- a/web/data/cubicweb.form.css	Wed Aug 25 09:43:12 2010 +0200
+++ b/web/data/cubicweb.form.css	Wed Aug 25 10:01:11 2010 +0200
@@ -229,6 +229,6 @@
   margin: 1em 1em 0px 0px;
   border-width: 1px;
   border-style: solid;
-  border-color: %(buttonBorderColor)s %(actionBoxTitleBgColor)s %(actionBoxTitleBgColor)s %(buttonBorderColor)s;
+  border-color: %(buttonBorderColor)s %(incontextBoxBodyBgColor)s %(incontextBoxBodyBgColor)s %(buttonBorderColor)s;
   background: %(buttonBgColor)s %(buttonBgImg)s;
 }
--- a/web/data/cubicweb.login.css	Wed Aug 25 09:43:12 2010 +0200
+++ b/web/data/cubicweb.login.css	Wed Aug 25 10:01:11 2010 +0200
@@ -30,7 +30,7 @@
   margin-left: -14em;
   width: 28em;
   background: #fff;
-  border: 2px solid %(actionBoxTitleBgColor)s;
+  border: 2px solid %(incontextBoxBodyBgColor)s;
   padding-bottom: 0.5em;
   text-align: center;
 }
@@ -80,7 +80,7 @@
 
 .loginButton {
   border: 1px solid #edecd2;
-  border-color: #edecd2 %(actionBoxTitleBgColor)s %(actionBoxTitleBgColor)s  #edecd2;
+  border-color: #edecd2 %(incontextBoxBodyBgColor)s %(incontextBoxBodyBgColor)s  #edecd2;
   margin: 2px 0px 0px;
   background: #f0eff0 url("gradient-grey-up.png") left top repeat-x;
 }
--- a/web/data/cubicweb.old.css	Wed Aug 25 09:43:12 2010 +0200
+++ b/web/data/cubicweb.old.css	Wed Aug 25 10:01:11 2010 +0200
@@ -22,7 +22,8 @@
   font-family: Verdana, sans-serif;
 }
 
-h1 {
+h1,
+.vtitle {
   font-size: 188%;
   margin: 0.2em 0px 0.3em;
   border-bottom: 1px solid #000;
@@ -243,10 +244,6 @@
 }
 
 /* Popup on login box and userActionBox */
-div.popupWrapper{
- position:relative;
- z-index:100;
-}
 
 div.popup {
   position: absolute;
@@ -303,18 +300,23 @@
 div#rqlinput {
   border: 1px solid #cfceb7;
   margin-bottom: 8px;
-  padding: 3px;
+  padding: 1px;
   background: #cfceb7;
+  width: 100%;
+}
+
+input#rql {
+  width: 99%;
 }
 
-input#rql{
-  width: 95%;
+input.rqlsubmit{
+  display: block;
+  width: 20px;
+  height: 20px;
+  background: %(buttonBgColor)s url("go.png") 50% 50% no-repeat;
+  vertical-align: bottom;
 }
-
-/* boxes */
-div.navboxes {
- margin-top: 8px;
-}
+/* old boxes, deprecated */
 
 div.boxFrame {
   width: 100%;
@@ -324,25 +326,17 @@
   padding-top: 0px;
   padding-bottom: 0.2em;
   font: bold 100% Georgia;
-  overflow: hidden;
   color: #fff;
   background: #ff9900 url("search.png") left bottom repeat-x;
 }
 
-div.searchBoxFrame div.boxTitle,
-div.greyBoxFrame div.boxTitle {
-  background: #cfceb7;
-}
-
 div.boxTitle span,
 div.sideBoxTitle span {
   padding: 0px 5px;
   white-space: nowrap;
 }
 
-div.sideBoxTitle span,
-div.searchBoxFrame div.boxTitle span,
-div.greyBoxFrame div.boxTitle span {
+div.sideBoxTitle span {
   color: #222211;
 }
 
@@ -356,85 +350,6 @@
   border-top: none;
 }
 
-ul.boxListing {
-  margin: 0px;
-  padding: 0px 3px;
-}
-
-ul.boxListing li,
-ul.boxListing ul li {
-  display: inline;
-  margin: 0px;
-  padding: 0px;
-  background-image: none;
-}
-
-ul.boxListing ul {
-  margin: 0px 0px 0px 7px;
-  padding: 1px 3px;
-}
-
-ul.boxListing a {
-  color: #000;
-  display: block;
-  padding: 1px 9px 1px 3px;
-}
-
-ul.boxListing .selected {
-  color: #FF4500;
-  font-weight: bold;
-}
-
-ul.boxListing a.boxBookmark:hover,
-ul.boxListing a:hover,
-ul.boxListing ul li a:hover {
-  text-decoration: none;
-  background: #eeedd9;
-  color: #111100;
-}
-
-ul.boxListing a.boxMenu:hover {
-                                background: #eeedd9 url(puce_down.png) no-repeat scroll 98% 6px;
-                                cursor:pointer;
-                                border-top:medium none;
-                                }
-a.boxMenu {
-  background: transparent url("puce_down.png") 98% 6px no-repeat;
-  display: block;
-  padding: 1px 9px 1px 3px;
-}
-
-
-a.popupMenu {
-  background: transparent url("puce_down_black.png") 2% 6px no-repeat;
-  padding-left: 2em;
-}
-
-ul.boxListing ul li a:hover {
-  background: #eeedd9  url("bullet_orange.png") 0% 6px no-repeat;
-}
-
-a.boxMenu:hover {
-  background: #eeedd9 url("puce_down.png") 98% 6px no-repeat;
-  cursor: pointer;
-}
-
-ul.boxListing a.boxBookmark {
-  padding-left: 3px;
-  background-image:none;
-  background:#fff;
-}
-
-ul.boxListing ul li a {
-  background: #fff url("bullet_orange.png") 0% 6px no-repeat;
-  padding: 1px 3px 0px 10px;
-}
-
-div.searchBoxFrame div.boxContent {
-  padding: 4px 4px 3px;
-  background: #f0eff0 url("gradient-grey-up.png") left top repeat-x;
-}
-
 div.shadow{
   height: 14px;
   background: url("shadow.gif") no-repeat top right;
@@ -474,16 +389,164 @@
   padding-right: 1em;
 }
 
-input.rqlsubmit{
-  background: #fffff8 url("go.png") 50% 50% no-repeat;
-  width: 20px;
-  height: 20px;
-  margin: 0px;
+/* boxes */
+
+div.navboxes {
+  padding-top: 0.5em;
+}
+
+div.boxTitle {
+  overflow: hidden;
+  font-weight: bold;
+}
+
+div.boxTitle span {
+  padding: 0px 0.5em;
+  white-space: nowrap;
+}
+
+div.boxBody {
+  padding: 3px 3px;
+  border-top: none;
+  background-color: %(leftrightBoxBodyBgColor)s;
+}
+
+div.boxBody a {
+  color: %(leftrightBoxBodyColor)s;
+}
+
+div.boxBody a:hover {
+  text-decoration: none;
+  cursor: pointer;
+  background-color: %(leftrightBoxBodyHoverBgColor)s;
+}
+
+/* boxes contextual customization */
+
+.contextFreeBox div.boxTitle {
+  background: %(contextFreeBoxTitleBg)s;
+  color: %(contextFreeBoxTitleColor)s;
+}
+
+.contextualBox div.boxTitle {
+  background: %(contextualBoxTitleBg)s;
+  color: %(contextualBoxTitleColor)s;
+}
+
+.primaryRight div.boxTitle {
+  background: %(incontextBoxTitleBg)s;
+  color: %(incontextBoxTitleColor)s;
+}
+
+.primaryRight div.boxBody {
+  padding: 0.2em 5px;
+  background: %(incontextBoxBodyBgColor)s;
+}
+
+.primaryRight div.boxBody a {
+  color: %(incontextBoxBodyColor)s;
+}
+
+.primaryRight div.boxBody a:hover {
+  background-color: %(incontextBoxBodyHoverBgColor)s;
+}
+
+.primaryRight div.boxFooter {
+  margin-bottom: 1em;
+}
+
+#navColumnLeft div.boxFooter, #navColumnRight div.boxFooter{
+  height: 14px;
+  background: url("shadow.gif") no-repeat top right;
+}
+
+/* boxes lists and menus */
+
+ul.boxListing {
+  margin: 0;
+  padding: 0;
 }
 
-input#norql{
-  width:13em;
-  margin-right: 2px;
+ul.boxListing ul {
+  padding: 1px 3px;
+}
+
+ul.boxListing a {
+  color: %(defaultColor)s;
+  display: block;
+  padding: 1px 3px;
+}
+
+ul.boxListing li {
+  margin: 0px;
+  padding: 0px;
+  background-image: none;
+}
+
+ul.boxListing ul li {
+  margin: 0px;
+  padding-left: 1em;
+}
+
+ul.boxListing ul li a {
+  padding-left: 10px;
+  background-image: url("bullet_orange.png");
+  background-repeat: no-repeat;
+  background-position: 0 6px;
+}
+
+ul.boxListing .selected {
+  color: %(aColor)s;
+  font-weight: bold;
+}
+
+ul.boxListing a.boxMenu:hover {
+  border-top: medium none;
+  background: %(leftrightBoxBodyHoverBgColor)s;
+}
+
+a.boxMenu,
+ul.boxListing a.boxMenu{
+  display: block;
+  padding: 1px 3px;
+  background: transparent %(bulletDownImg)s;
+}
+
+ul.boxListing a.boxMenu:hover {
+  border-top: medium none;
+  background: %(leftrightBoxBodyHoverBgColor)s %(bulletDownImg)s;
+}
+
+a.boxMenu:hover {
+  cursor: pointer;
+}
+
+a.popupMenu {
+  background: transparent url("puce_down_black.png") 2% 6px no-repeat;
+  padding-left: 2em;
+}
+
+
+/* custom boxes */
+
+.search_box div.boxBody {
+  padding: 4px 4px 3px;
+  background: #f0eff0 url("gradient-grey-up.png") left top repeat-x;
+}
+
+.bookmarks_box ul.boxListing div a {
+  background: #fff;
+  display: inline;
+  padding: 0;
+}
+
+.download_box div.boxTitle {
+  background : #8fbc8f !important;
+}
+
+.download_box div.boxBody {
+  background : #eefed9;
+  vertical-align: center;
 }
 
 /* user actions menu */
@@ -503,20 +566,6 @@
   padding-right: 2em;
 }
 
-/* download box XXX move to its own file? */
-div.downloadBoxTitle{
- background : #8FBC8F;
- font-weight: bold;
-}
-
-div.downloadBox{
- font-weight: bold;
-}
-
-div.downloadBox div.sideBoxBody{
- background : #EEFED9;
-}
-
 /**************/
 /* navigation */
 /**************/
@@ -852,11 +901,3 @@
   border-color:#edecd2 #cfceb7 #cfceb7  #edecd2;
   background: #fffff8 url("button.png") bottom left repeat-x;
 }
-
-/********************************/
-/* placement of alt. view icons */
-/********************************/
-
-.otherView {
-  float: right;
-}
--- a/web/data/cubicweb.tableview.css	Wed Aug 25 09:43:12 2010 +0200
+++ b/web/data/cubicweb.tableview.css	Wed Aug 25 10:01:11 2010 +0200
@@ -6,7 +6,7 @@
   font-weight: bold;
   background: #ebe8d9 url("button.png") repeat-x;
   padding: 0.3em;
-  border-bottom: 1px solid %(actionBoxTitleBgColor)s;
+  border-bottom: 1px solid %(incontextBoxBodyBgColor)s;
   text-align: left;
 }
 
Binary file web/data/incontextBoxHeader.png has changed
--- a/web/data/uiprops.py	Wed Aug 25 09:43:12 2010 +0200
+++ b/web/data/uiprops.py	Wed Aug 25 10:01:11 2010 +0200
@@ -103,18 +103,36 @@
 pageContentPadding = '1em'
 pageMinHeight = '800px'
 
-# boxes
-boxTitleBg = lazystr('%(headerBgColor)s url("boxHeader.png") repeat-x 50%% 50%%')
-boxBodyBgColor = '#efefde'
+# boxes ########################################################################
+
+# title of contextFree / contextual boxes
+contextFreeBoxTitleBgColor = '#CFCEB7'
+contextFreeBoxTitleBg = lazystr('%(contextFreeBoxTitleBgColor)s url("contextFreeBoxHeader.png") repeat-x 50%% 50%%')
+contextFreeBoxTitleColor = '#000'
+
+contextualBoxTitleBgColor = '#FF9900'
+contextualBoxTitleBg = lazystr('%(contextualBoxTitleBgColor)s url("contextualBoxHeader.png") repeat-x 50%% 50%%')
+contextualBoxTitleColor = '#FFF'
 
-# action, search, sideBoxes
-actionBoxTitleBgColor = '#cfceb7'
-actionBoxTitleBg = lazystr('%(actionBoxTitleBgColor)s url("actionBoxHeader.png") repeat-x 50%% 50%%')
-sideBoxBodyBgColor = '#f8f8ee'
-sideBoxBodyBg = lazystr('%(sideBoxBodyBgColor)s')
-sideBoxBodyColor = '#555544'
+# title of 'incontext' boxes (eg displayed insinde the primary view)
+incontextBoxTitleBgColor = lazystr('%(contextFreeBoxTitleBgColor)s')
+incontextBoxTitleBg = lazystr('%(incontextBoxTitleBgColor)s url("incontextBoxHeader.png") repeat-x 50%% 50%%')
+incontextBoxTitleColor = '#000'
 
-# table listing & co
+# body of boxes in the left or right column (whatever contextFree / contextual)
+leftrightBoxBodyBgColor = '#FFF'
+leftrightBoxBodyBg = lazystr('%(leftrightBoxBodyBgColor)s')
+leftrightBoxBodyColor = '#black'
+leftrightBoxBodyHoverBgColor = '#EEEDD9'
+
+# body of 'incontext' boxes (eg displayed insinde the primary view)
+incontextBoxBodyBgColor = '#f8f8ee'
+incontextBoxBodyBg = lazystr('%(incontextBoxBodyBgColor)s')
+incontextBoxBodyColor = '#555544'
+incontextBoxBodyHoverBgColor = lazystr('%(incontextBoxBodyBgColor)s')
+
+
+# table listing & co ###########################################################
 listingBorderColor = '#ccc'
 listingHeaderBgColor = '#efefef'
 listingHihligthedBgColor = '#fbfbfb'
--- a/web/htmlwidgets.py	Wed Aug 25 09:43:12 2010 +0200
+++ b/web/htmlwidgets.py	Wed Aug 25 10:01:11 2010 +0200
@@ -182,7 +182,10 @@
             toggle_action(ident), self.link_class, self.label))
         self._begin_menu(ident)
         for item in self.items:
-            item.render(self.w)
+            if hasattr(item, 'render'):
+                item.render(self.w)
+            else:
+                self.w(u'<li>%s</li>' % item)
         self._end_menu()
         if self.isitem:
             self.w(u'</li>')
--- a/web/views/ajaxedit.py	Wed Aug 25 09:43:12 2010 +0200
+++ b/web/views/ajaxedit.py	Wed Aug 25 10:01:11 2010 +0200
@@ -15,21 +15,19 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Set of views allowing edition of entities/relations using ajax
+"""Set of views allowing edition of entities/relations using ajax"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from cubicweb import role
+from cubicweb.view import View
 from cubicweb.selectors import match_form_params, match_kwargs
-from cubicweb.web.box import EditRelationBoxTemplate
+from cubicweb.web.box import EditRelationMixIn, EditRelationBoxTemplate
 
-class AddRelationView(EditRelationBoxTemplate):
-    """base class for view which let add entities linked
-    by a given relation
+class AddRelationView(EditRelationMixIn, View):
+    """base class for view which let add entities linked by a given relation
 
-    subclasses should define at least id, rtype and target
-    class attributes.
+    subclasses should define at least id, rtype and target class attributes.
     """
     __registry__ = 'views'
     __regid__ = 'xaddrelation'
@@ -38,7 +36,7 @@
     cw_property_defs = {} # don't want to inherit this from Box
     expected_kwargs = form_params = ('rtype', 'target')
 
-    build_js = EditRelationBoxTemplate.build_reload_js_call
+    build_js = EditRelationMixIn.build_reload_js_call
 
     def cell_call(self, row, col, rtype=None, target=None, etype=None):
         self.rtype = rtype or self._cw.form['rtype']
@@ -53,13 +51,13 @@
                 etypes = rschema.subjects(entity.e_schema)
             if len(etypes) == 1:
                 self.etype = etypes[0]
-        self.w(u'<div id="%s">' % self.__regid__)
+        self.w(u'<div id="%s">' % self.domid)
         self.w(u'<h1>%s</h1>' % self._cw._('relation %(relname)s of %(ent)s')
                % {'relname': rschema.display_name(self._cw, role(self)),
                   'ent': entity.view('incontext')})
         self.w(u'<ul>')
         for boxitem in self.unrelated_boxitems(entity):
-            boxitem.render(self.w)
+            self.w('<li class="invisible">%s</li>' % botitem)
         self.w(u'</ul></div>')
 
     def unrelated_entities(self, entity):
@@ -74,11 +72,4 @@
                                     ordermethod='fetch_order')
             self.pagination(self._cw, rset, w=self.w)
             return rset.entities()
-        # in other cases, use vocabulary functions
-        entities = []
-        # XXX to update for 3.2
-        for _, eid in entity.vocabulary(self.rtype, role(self)):
-            if eid is not None:
-                rset = self._cw.eid_rset(eid)
-                entities.append(rset.get_entity(0, 0))
-        return entities
+        super(AddRelationView, self).unrelated_entities(self)
--- a/web/views/basecomponents.py	Wed Aug 25 09:43:12 2010 +0200
+++ b/web/views/basecomponents.py	Wed Aug 25 10:01:11 2010 +0200
@@ -148,8 +148,7 @@
         self.w(u'<div id="appMsg" onclick="%s" class="%s">\n' %
                (toggle_action('appMsg'), (msgs and ' ' or 'hidden')))
         for msg in msgs:
-            self.w(u'<div class="message" id="%s">%s</div>' % (
-                self.div_id(), msg))
+            self.w(u'<div class="message" id="%s">%s</div>' % (self.domid, msg))
         self.w(u'</div>')
 
 
--- a/web/views/basecontrollers.py	Wed Aug 25 09:43:12 2010 +0200
+++ b/web/views/basecontrollers.py	Wed Aug 25 10:01:11 2010 +0200
@@ -26,7 +26,7 @@
 
 from cubicweb import (NoSelectableObject, ObjectNotFound, ValidationError,
                       AuthenticationError, typed_eid)
-from cubicweb.utils import json, json_dumps
+from cubicweb.utils import UStringIO, json, json_dumps
 from cubicweb.selectors import authenticated_user, anonymous_user, match_form_params
 from cubicweb.mail import format_mail
 from cubicweb.web import Redirect, RemoteCallFailed, DirectResponse
@@ -345,14 +345,19 @@
     def _call_view(self, view, paginate=False, **kwargs):
         divid = self._cw.form.get('divid', 'pageContent')
         # we need to call pagination before with the stream set
-        stream = view.set_stream()
+        try:
+            stream = view.set_stream()
+        except AttributeError:
+            stream = UStringIO()
+            kwargs['w'] = stream.write
+            assert not paginate
         if paginate:
             if divid == 'pageContent':
                 # mimick main template behaviour
                 stream.write(u'<div id="pageContent">')
                 vtitle = self._cw.form.get('vtitle')
                 if vtitle:
-                    stream.write(u'<h1 class="vtitle">%s</h1>\n' % vtitle)
+                    stream.write(u'<div class="vtitle">%s</div>\n' % vtitle)
             view.paginate()
             if divid == 'pageContent':
                 stream.write(u'<div id="contentmain">')
--- a/web/views/basetemplates.py	Wed Aug 25 09:43:12 2010 +0200
+++ b/web/views/basetemplates.py	Wed Aug 25 10:01:11 2010 +0200
@@ -127,7 +127,7 @@
         w(u'<div id="pageContent">\n')
         vtitle = self._cw.form.get('vtitle')
         if vtitle:
-            w(u'<h1 class="vtitle">%s</h1>\n' % xml_escape(vtitle))
+            w(u'<div class="vtitle">%s</div>\n' % xml_escape(vtitle))
         # display entity type restriction component
         etypefilter = self._cw.vreg['components'].select_or_none(
             'etypenavigation', self._cw, rset=self.cw_rset)
@@ -191,6 +191,7 @@
         boxes = list(self._cw.vreg['boxes'].poss_visible_objects(
             self._cw, rset=self.cw_rset, view=view, context=context))
         if boxes:
+            getlayout = self._cw.vreg['components'].select
             self.w(u'<td id="navColumn%s"><div class="navboxes">\n' % context.capitalize())
             for box in boxes:
                 box.render(w=self.w, view=view)
@@ -269,7 +270,7 @@
         w(u'<div id="pageContent">\n')
         vtitle = self._cw.form.get('vtitle')
         if vtitle:
-            w(u'<h1 class="vtitle">%s</h1>' % xml_escape(vtitle))
+            w(u'<div class="vtitle">%s</div>' % xml_escape(vtitle))
 
     def topleft_header(self):
         logo = self._cw.vreg['components'].select_or_none('logo', self._cw,
@@ -392,10 +393,8 @@
                                                             rset=self.cw_rset)
         footeractions = actions.get('footer', ())
         for i, action in enumerate(footeractions):
-            self.w(u'<a href="%s"' % action.url())
-            if getattr(action, 'html_class'):
-                self.w(u' class="%s"' % action.html_class())
-            self.w(u'>%s</a>' % self._cw._(action.title))
+            self.w(u'<a href="%s">%s</a>' % (action.url(),
+                                             self._cw._(action.title)))
             if i < (len(footeractions) - 1):
                 self.w(u' | ')
         self.w(u'</div>')
--- a/web/views/bookmark.py	Wed Aug 25 09:43:12 2010 +0200
+++ b/web/views/bookmark.py	Wed Aug 25 10:01:11 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Primary view for bookmarks + user's bookmarks box
+"""Primary view for bookmarks + user's bookmarks box"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from logilab.mtconverter import xml_escape
@@ -70,59 +69,55 @@
         self.w(u'</div>')
 
 
-class BookmarksBox(box.UserRQLBoxTemplate):
+class BookmarksBox(box.Box):
     """display a box containing all user's bookmarks"""
     __regid__ = 'bookmarks_box'
+
+    title = _('bookmarks')
     order = 40
-    title = _('bookmarks')
     rql = ('Any B,T,P ORDERBY lower(T) '
            'WHERE B is Bookmark,B title T, B path P, B bookmarked_by U, '
            'U eid %(x)s')
-    etype = 'Bookmark'
-    rtype = 'bookmarked_by'
 
+    def init_rendering(self):
+        ueid = self._cw.user.eid
+        self.bookmarks_rset = self._cw.execute(self.rql, {'x': ueid})
+        rschema = self._cw.vreg.schema.rschema('bookmarked_by')
+        eschema = self._cw.vreg.schema.eschema('Bookmark')
+        self.can_delete = rschema.has_perm(self._cw, 'delete', toeid=ueid)
+        self.can_edit = (eschema.has_perm(self._cw, 'add') and
+                         rschema.has_perm(self._cw, 'add', toeid=ueid))
+        if not self.bookmarks_rset and not self.can_edit:
+            raise box.EmptyComponent()
+        self.items = []
 
-    def call(self, **kwargs):
+    def render_body(self, w):
+        ueid = self._cw.user.eid
         req = self._cw
-        ueid = req.user.eid
-        try:
-            rset = req.execute(self.rql, {'x': ueid})
-        except Unauthorized:
-            # can't access to something in the query, forget this box
-            return
-        box = BoxWidget(req._(self.title), self.__regid__)
-        box.listing_class = 'sideBox'
-        rschema = self._cw.vreg.schema.rschema(self.rtype)
-        eschema = self._cw.vreg.schema.eschema(self.etype)
-        candelete = rschema.has_perm(req, 'delete', toeid=ueid)
-        if candelete:
+        if self.can_delete:
             req.add_js('cubicweb.ajax.js')
-        else:
-            dlink = None
-        for bookmark in rset.entities():
-            label = '<a href="%s">%s</a>' % (xml_escape(bookmark.action_url()),
-                                             xml_escape(bookmark.title))
-            if candelete:
+        for bookmark in self.bookmarks_rset.entities():
+            label = self.build_link(bookmark.title, bookmark.action_url())
+            if self.can_delete:
                 dlink = u'[<a href="javascript:removeBookmark(%s)" title="%s">-</a>]' % (
                     bookmark.eid, _('delete this bookmark'))
-                label = '%s %s' % (dlink, label)
-            box.append(RawBoxItem(label))
-        if eschema.has_perm(req, 'add') and rschema.has_perm(req, 'add', toeid=ueid):
-            boxmenu = BoxMenu(req._('manage bookmarks'))
+                label = '<div>%s %s</div>' % (dlink, label)
+            self.append(label)
+        if self.can_edit:
+            menu = BoxMenu(req._('manage bookmarks'))
             linkto = 'bookmarked_by:%s:subject' % ueid
             # use a relative path so that we can move the instance without
             # loosing bookmarks
             path = req.relative_path()
-            # XXX if vtitle specified in params, extract it and use it as default value
-            # for bookmark's title
+            # XXX if vtitle specified in params, extract it and use it as
+            # default value for bookmark's title
             url = req.vreg['etypes'].etype_class('Bookmark').cw_create_url(
                 req, __linkto=linkto, path=path)
-            boxmenu.append(self.mk_action(req._('bookmark this page'), url,
-                                          category='manage', id='bookmark'))
-            if rset:
+            menu.append(self.build_link(req._('bookmark this page'), url))
+            if self.bookmarks_rset:
                 if req.user.is_in_group('managers'):
                     bookmarksrql = 'Bookmark B WHERE B bookmarked_by U, U eid %s' % ueid
-                    erset = rset
+                    erset = self.bookmarks_rset
                 else:
                     # we can't edit shared bookmarks we don't own
                     bookmarksrql = 'Bookmark B WHERE B bookmarked_by U, B owned_by U, U eid %(x)s'
@@ -130,11 +125,10 @@
                                         build_descr=False)
                     bookmarksrql %= {'x': ueid}
                 if erset:
-                    url = self._cw.build_url(vid='muledit', rql=bookmarksrql)
-                    boxmenu.append(self.mk_action(self._cw._('edit bookmarks'), url, category='manage'))
+                    url = req.build_url(vid='muledit', rql=bookmarksrql)
+                    menu.append(self.build_link(req._('edit bookmarks'), url))
             url = req.user.absolute_url(vid='xaddrelation', rtype='bookmarked_by',
                                         target='subject')
-            boxmenu.append(self.mk_action(self._cw._('pick existing bookmarks'), url, category='manage'))
-            box.append(boxmenu)
-        if not box.is_empty():
-            box.render(self.w)
+            menu.append(self.build_link(req._('pick existing bookmarks'), url))
+            self.append(menu)
+        self.render_items(w)
--- a/web/views/boxes.py	Wed Aug 25 09:43:12 2010 +0200
+++ b/web/views/boxes.py	Wed Aug 25 10:01:11 2010 +0200
@@ -18,10 +18,11 @@
 """Generic boxes for CubicWeb web client:
 
 * actions box
-* possible views box
+* search box
 
-additional (disabled by default) boxes
+Additional boxes (disabled by default):
 * schema box
+* possible views box
 * startup views box
 """
 
@@ -31,42 +32,41 @@
 from warnings import warn
 
 from logilab.mtconverter import xml_escape
+from logilab.common.deprecation import class_deprecated
 
-from cubicweb.selectors import match_user_groups, non_final_entity
+from cubicweb import Unauthorized
+from cubicweb.selectors import (match_user_groups, match_context, match_kwargs,
+                                non_final_entity, nonempty_rset)
 from cubicweb.view import EntityView
 from cubicweb.schema import display_name
-from cubicweb.web.htmlwidgets import BoxWidget, BoxMenu, BoxHtml, RawBoxItem
-from cubicweb.web.box import BoxTemplate
+from cubicweb.web import box, htmlwidgets
 
+# XXX bw compat, some cubes import this class from here
+BoxTemplate = box.BoxTemplate
+BoxHtml = htmlwidgets.BoxHtml
 
-class EditBox(BoxTemplate): # XXX rename to ActionsBox
+class EditBox(box.Box): # XXX rename to ActionsBox
     """
     box with all actions impacting the entity displayed: edit, copy, delete
     change state, add related entities
     """
     __regid__ = 'edit_box'
-    __select__ = BoxTemplate.__select__ & non_final_entity()
+    __select__ = box.Box.__select__ & non_final_entity()
 
     title = _('actions')
     order = 2
+    contextual = True
 
-    def call(self, view=None, **kwargs):
+    def init_rendering(self):
+        super(EditBox, self).init_rendering()
         _ = self._cw._
-        title = _(self.title)
-        if self.cw_rset:
-            etypes = self.cw_rset.column_types(0)
-            if len(etypes) == 1:
-                plural = self.cw_rset.rowcount > 1 and 'plural' or ''
-                etypelabel = display_name(self._cw, iter(etypes).next(), plural)
-                title = u'%s - %s' % (title, etypelabel.lower())
-        box = BoxWidget(title, self.__regid__, _class="greyBoxFrame")
         self._menus_in_order = []
         self._menus_by_id = {}
         # build list of actions
         actions = self._cw.vreg['actions'].possible_actions(self._cw, self.cw_rset,
-                                                            view=view)
+                                                            **self.cw_extra_kwargs)
         other_menu = self._get_menu('moreactions', _('more actions'))
-        for category, defaultmenu in (('mainactions', box),
+        for category, defaultmenu in (('mainactions', self),
                                       ('moreactions', other_menu),
                                       ('addrelated', None)):
             for action in actions.get(category, ()):
@@ -81,16 +81,28 @@
                     menu = defaultmenu
                 action.fill_menu(self, menu)
         # if we've nothing but actions in the other_menu, add them directly into the box
-        if box.is_empty() and len(self._menus_by_id) == 1 and not other_menu.is_empty():
-            box.items = other_menu.items
-            other_menu.items = []
+        if not self.items and len(self._menus_by_id) == 1 and not other_menu.is_empty():
+            self.items = other_menu.items
         else: # ensure 'more actions' menu appears last
             self._menus_in_order.remove(other_menu)
             self._menus_in_order.append(other_menu)
-        for submenu in self._menus_in_order:
-            self.add_submenu(box, submenu)
-        if not box.is_empty():
-            box.render(self.w)
+            for submenu in self._menus_in_order:
+                self.add_submenu(self, submenu)
+        if not self.items:
+            raise box.EmptyComponent()
+
+    def render_title(self, w):
+        title = self._cw._(self.title)
+        if self.cw_rset:
+            etypes = self.cw_rset.column_types(0)
+            if len(etypes) == 1:
+                plural = self.cw_rset.rowcount > 1 and 'plural' or ''
+                etypelabel = display_name(self._cw, iter(etypes).next(), plural)
+                title = u'%s - %s' % (title, etypelabel.lower())
+        w(title)
+
+    def render_body(self, w):
+        self.render_items(w)
 
     def _get_menu(self, id, title=None, label_prefix=None):
         try:
@@ -98,7 +110,7 @@
         except KeyError:
             if title is None:
                 title = self._cw._(id)
-            self._menus_by_id[id] = menu = BoxMenu(title)
+            self._menus_by_id[id] = menu = htmlwidgets.BoxMenu(title)
             menu.label_prefix = label_prefix
             self._menus_in_order.append(menu)
             return menu
@@ -108,19 +120,22 @@
         if len(submenu.items) == 1 and not appendanyway:
             boxlink = submenu.items[0]
             if submenu.label_prefix:
-                boxlink.label = u'%s %s' % (submenu.label_prefix, boxlink.label)
+                # XXX iirk
+                if hasattr(boxlink, 'label'):
+                    boxlink.label = u'%s %s' % (submenu.label_prefix, boxlink.label)
+                else:
+                    submenu.items[0] = u'%s %s' % (submenu.label_prefix, boxlink)
             box.append(boxlink)
         elif submenu.items:
             box.append(submenu)
         elif appendanyway:
-            box.append(RawBoxItem(xml_escape(submenu.label)))
+            box.append(xml_escape(submenu.label))
 
 
-class SearchBox(BoxTemplate):
+class SearchBox(box.Box):
     """display a box with a simple search form"""
     __regid__ = 'search_box'
 
-    visible = True # enabled by default
     title = _('search')
     order = 0
     formdef = u"""<form action="%s">
@@ -130,74 +145,123 @@
 <input type="hidden" name="subvid" value="tsearch" />
 </td><td>
 <input tabindex="%s" type="submit" id="rqlboxsubmit" class="rqlsubmit" value="" />
-</td></tr></table>
-</form>"""
+ </td></tr></table>
+ </form>"""
 
-    def call(self, view=None, **kwargs):
-        req = self._cw
-        if req.form.pop('__fromsearchbox', None):
-            rql = req.form.get('rql', '')
+    def render_title(self, w):
+        w(u"""<span onclick="javascript: toggleVisibility('rqlinput')">%s</span>"""
+          % self._cw._(self.title))
+
+    def render_body(self, w):
+        if self._cw.form.pop('__fromsearchbox', None):
+            rql = self._cw.form.get('rql', '')
         else:
             rql = ''
-        form = self.formdef % (req.build_url('view'), req.next_tabindex(),
-                               xml_escape(rql), req.next_tabindex())
-        title = u"""<span onclick="javascript: toggleVisibility('rqlinput')">%s</span>""" % req._(self.title)
-        box = BoxWidget(title, self.__regid__, _class="searchBoxFrame", islist=False, escape=False)
-        box.append(BoxHtml(form))
-        box.render(self.w)
+        w(self.formdef % (self._cw.build_url('view'), self._cw.next_tabindex(),
+                          xml_escape(rql), self._cw.next_tabindex()))
 
 
 # boxes disabled by default ###################################################
 
-class PossibleViewsBox(BoxTemplate):
+class PossibleViewsBox(box.Box):
     """display a box containing links to all possible views"""
     __regid__ = 'possible_views_box'
-    __select__ = BoxTemplate.__select__ & match_user_groups('users', 'managers')
 
     visible = False
     title = _('possible views')
     order = 10
 
-    def call(self, **kwargs):
-        box = BoxWidget(self._cw._(self.title), self.__regid__)
-        views = [v for v in self._cw.vreg['views'].possible_views(self._cw,
-                                                              rset=self.cw_rset)
-                 if v.category != 'startupview']
-        for category, views in self.sort_actions(views):
-            menu = BoxMenu(category)
+    def init_rendering(self):
+        self.views = [v for v in self._cw.vreg['views'].possible_views(self._cw,
+                                                                       rset=self.cw_rset)
+                      if v.category != 'startupview']
+        if not self.views:
+            raise box.EmptyComponent()
+        self.items = []
+
+    def render_body(self, w):
+        for category, views in box.sort_by_category(self.views):
+            menu = htmlwidgets.BoxMenu(category)
             for view in views:
                 menu.append(self.box_action(view))
-            box.append(menu)
-        if not box.is_empty():
-            box.render(self.w)
+            self.append(menu)
+        self.render_items(w)
 
 
-class StartupViewsBox(BoxTemplate):
+class StartupViewsBox(PossibleViewsBox):
     """display a box containing links to all startup views"""
     __regid__ = 'startup_views_box'
+
     visible = False # disabled by default
     title = _('startup views')
     order = 70
 
-    def call(self, **kwargs):
-        box = BoxWidget(self._cw._(self.title), self.__regid__)
-        for view in self._cw.vreg['views'].possible_views(self._cw, None):
-            if view.category == 'startupview':
-                box.append(self.box_action(view))
-        if not box.is_empty():
-            box.render(self.w)
+    def init_rendering(self):
+        self.views = [v for v in self._cw.vreg['views'].possible_views(self._cw)
+                      if v.category == 'startupview']
+        if not self.views:
+            raise box.EmptyComponent()
+        self.items = []
 
 
-# helper classes ##############################################################
+class RsetBox(box.Box):
+    """helper view class to display an rset in a sidebox"""
+    __select__ = nonempty_rset() & match_kwargs('title', 'vid')
+    __regid__ = 'rsetbox'
+    cw_property_defs = {}
+    context = 'incontext'
+
+    @property
+    def domid(self):
+        return super(RsetBox, self).domid + unicode(abs(id(self)))
+    def render_title(self, w):
+        w(self.cw_extra_kwargs['title'])
+
+    def render_body(self, w):
+        self._cw.view(self.cw_extra_kwargs['vid'], self.cw_rset, w=w)
+
+ # helper classes ##############################################################
 
 class SideBoxView(EntityView):
     """helper view class to display some entities in a sidebox"""
+    __metaclass__ = class_deprecated
+    __deprecation_warning__ = 'SideBoxView is deprecated, use RsetBox instead'
+
     __regid__ = 'sidebox'
 
-    def call(self, boxclass='sideBox', title=u''):
+    def call(self, **kwargs):
         """display a list of entities by calling their <item_vid> view"""
-        if title:
-            self.w(u'<div class="sideBoxTitle"><span>%s</span></div>' % title)
-        self.w(u'<div class="%s"><div class="sideBoxBody">' % boxclass)
-        self.wview('autolimited', self.cw_rset, **self.cw_extra_kwargs)
-        self.w(u'</div>\n</div>\n')
+        box = self._cw.vreg['boxes'].select('rsetbox', self._cw, rset=self.cw_rset,
+                                            vid='autolimited', title=title,
+                                            **self.cw_extra_kwargs)
+        box.render(self.w)
+
+
+class ContextualBoxLayout(box.Layout):
+    __select__ = match_context('incontext', 'left', 'right') & box.contextual()
+    # predefined class in cubicweb.css: contextualBox | contextFreeBox
+    # XXX: navigationBox | actionBox
+    cssclass = 'contextualBox'
+
+    def render(self, w):
+        view = self.cw_extra_kwargs['view']
+        try:
+            view.init_rendering()
+        except Unauthorized, ex:
+            self.warning("can't render %s: %s", view, ex)
+            return
+        except box.EmptyComponent:
+            return
+        w(u'<div class="%s %s" id="%s">' % (self.cssclass, view.cssclass,
+                                            view.domid))
+        w(u'<div class="boxTitle"><span>')
+        view.render_title(w)
+        w(u'</span></div>\n<div class="boxBody">')
+        view.render_body(w)
+        # boxFooter div is a CSS place holder (for shadow for example)
+        w(u'</div><div class="boxFooter"></div></div>\n')
+
+
+class ContextFreeBoxLayout(ContextualBoxLayout):
+    __select__ = match_context('incontext', 'left', 'right') & ~box.contextual()
+    cssclass = 'contextFreeBox'
--- a/web/views/facets.py	Wed Aug 25 09:43:12 2010 +0200
+++ b/web/views/facets.py	Wed Aug 25 10:01:11 2010 +0200
@@ -25,7 +25,7 @@
 from cubicweb.selectors import (non_final_entity, multi_lines_rset,
                                 match_context_prop, yes, relation_possible)
 from cubicweb.utils import json_dumps
-from cubicweb.web.box import BoxTemplate
+from cubicweb.web import box
 from cubicweb.web.facet import (AbstractFacet, FacetStringWidget, RelationFacet,
                                 prepare_facets_rqlst, filter_hiddens, _cleanup_rqlst,
                                 _prepare_vocabulary_rqlst)
@@ -38,13 +38,13 @@
     return 0
 
 
-class FilterBox(BoxTemplate):
+class FilterBox(box.Box):
     """filter results of a query"""
     __regid__ = 'filter_box'
     __select__ = (((non_final_entity() & multi_lines_rset())
                    | contextview_selector()
                    ) & match_context_prop())
-    context = 'left'
+    context = 'left' # XXX doesn't support 'incontext', only 'left' or 'right'
     title = _('boxes_filter_box')
     visible = True # functionality provided by the search box by default
     order = 1
@@ -61,7 +61,8 @@
         """
         return {}
 
-    def _get_context(self, view):
+    def _get_context(self):
+        view = self.cw_extra_kwargs.get('view')
         context = getattr(view, 'filter_box_context_info', lambda: None)()
         if context:
             rset, vid, divid, paginate = context
@@ -71,14 +72,15 @@
             paginate = view and view.paginable
         return rset, vid, divid, paginate
 
-    def call(self, view=None):
+    def render(self, w, **kwargs):
         req = self._cw
         req.add_js( self.needs_js )
         req.add_css( self.needs_css)
         if self.roundcorners:
             req.html_headers.add_onload('jQuery(".facet").corner("tl br 10px");')
-        rset, vid, divid, paginate = self._get_context(view)
-        if rset.rowcount < 2: # XXX done by selectors, though maybe necessary when rset has been hijacked
+        rset, vid, divid, paginate = self._get_context()
+        # XXX done by selectors, though maybe necessary when rset has been hijacked
+        if rset.rowcount < 2:
             return
         rqlst = rset.syntax_tree()
         # union not yet supported
@@ -97,7 +99,6 @@
             return
         if vid is None:
             vid = req.form.get('vid')
-        w = self.w
         if self.bk_linkbox_template and req.vreg.schema['Bookmark'].has_perm(req, 'add'):
             w(self.bookmark_link(rset))
         w(u'<form method="post" id="%sForm" cubicweb:facetargs="%s" action="">'  % (
@@ -110,7 +111,7 @@
                 hiddens[param] = req.form[param]
         filter_hiddens(w, **hiddens)
         for wdg in widgets:
-            wdg.render(w=self.w)
+            wdg.render(w=w)
         w(u'</fieldset>\n</form>\n')
 
     def bookmark_link(self, rset):
--- a/web/views/idownloadable.py	Wed Aug 25 09:43:12 2010 +0200
+++ b/web/views/idownloadable.py	Wed Aug 25 10:01:11 2010 +0200
@@ -42,22 +42,25 @@
     w(u'<a href="%s"><img src="%s" alt="%s"/> %s</a>'
       % (xml_escape(entity.cw_adapt_to('IDownloadable').download_url()),
          req.uiprops['DOWNLOAD_ICON'],
-         _('download icon'), xml_escape(label or entity.dc_title())))
+         req._('download icon'), xml_escape(label or entity.dc_title())))
     w(u'%s</div>' % footer)
     w(u'</div></div>\n')
 
 
-class DownloadBox(box.EntityBoxTemplate):
+class DownloadBox(box.EntityBox):
     __regid__ = 'download_box'
     # no download box for images
-    # XXX primary_view selector ?
-    __select__ = (one_line_rset() & match_context_prop()
-                  & adaptable('IDownloadable') & ~has_mimetype('image/'))
+    __select__ = (box.EntityBox.__select__ &
+                  adaptable('IDownloadable') & ~has_mimetype('image/'))
+
     order = 10
+    title = _('download')
 
-    def cell_call(self, row, col, title=None, label=None, **kwargs):
-        entity = self.cw_rset.get_entity(row, col)
-        download_box(self.w, entity, title, label)
+    def render_body(self, w):
+        w(u'<a href="%s"><img src="%s" alt="%s"/> %s</a>'
+          % (xml_escape(self.entity.cw_adapt_to('IDownloadable').download_url()),
+             self._cw.uiprops['DOWNLOAD_ICON'],
+             self._cw._('download icon'), xml_escape(self.entity.dc_title())))
 
 
 class DownloadView(EntityView):
--- a/web/views/primary.py	Wed Aug 25 09:43:12 2010 +0200
+++ b/web/views/primary.py	Wed Aug 25 10:01:11 2010 +0200
@@ -194,42 +194,45 @@
                 try:
                     label, rset, vid, dispctrl  = box
                 except ValueError:
-                    warn('[3.5] box views should now be defined as a 4-uple (label, rset, vid, dispctrl), '
-                         'please update %s' % self.__class__.__name__,
-                         DeprecationWarning)
                     label, rset, vid = box
                     dispctrl = {}
+                warn('[3.10] box views should now be a RsetBox instance, '
+                     'please update %s' % self.__class__.__name__,
+                     DeprecationWarning)
                 self.w(u'<div class="sideBox">')
                 self.wview(vid, rset, title=label, initargs={'dispctrl': dispctrl})
                 self.w(u'</div>')
             else:
-                try:
-                    box.render(w=self.w, row=self.cw_row)
-                except NotImplementedError:
-                    # much probably a context insensitive box, which only implements
-                    # .call() and not cell_call()
+                 try:
+                     box.render(w=self.w, row=self.cw_row)
+                 except NotImplementedError:
+                    # much probably a context insensitive box, which only
+                    # implements .call() and not cell_call()
+                    # XXX shouldn't occurs with the new box system
                     box.render(w=self.w)
 
     def _prepare_side_boxes(self, entity):
         sideboxes = []
+        boxesreg = self._cw.vreg['boxes']
         for rschema, tschemas, role, dispctrl in self._section_def(entity, 'sideboxes'):
             rset = self._relation_rset(entity, rschema, role, dispctrl)
             if not rset:
                 continue
             label = self._rel_label(entity, rschema, role, dispctrl)
-            vid = dispctrl.get('vid', 'sidebox')
-            sideboxes.append( (label, rset, vid, dispctrl) )
-        sideboxes += self._cw.vreg['boxes'].poss_visible_objects(
-            self._cw, rset=self.cw_rset, row=self.cw_row, view=self,
-            context='incontext')
+            vid = dispctrl.get('vid', 'autolimited')
+            box = boxesreg.select('rsetbox', self._cw, rset=rset,
+                                  vid=vid, title=label, dispctrl=dispctrl,
+                                  context='incontext')
+            sideboxes.append(box)
+        sideboxes += boxesreg.poss_visible_objects(
+             self._cw, rset=self.cw_rset, row=self.cw_row, view=self,
+             context='incontext')
         # XXX since we've two sorted list, it may be worth using bisect
         def get_order(x):
-            if isinstance(x, tuple):
-                # x is a view box (label, rset, vid, dispctrl)
-                # default to 1000 so view boxes occurs after component boxes
-                return x[-1].get('order', 1000)
-            # x is a component box
-            return x.cw_propval('order')
+            if 'order' in x.cw_property_defs:
+                return x.cw_propval('order')
+            # default to 9999 so view boxes occurs after component boxes
+            return x.cw_extra_kwargs.get('dispctrl', {}).get('order', 9999)
         return sorted(sideboxes, key=get_order)
 
     def _section_def(self, entity, where):
--- a/web/views/treeview.py	Wed Aug 25 09:43:12 2010 +0200
+++ b/web/views/treeview.py	Wed Aug 25 10:01:11 2010 +0200
@@ -110,7 +110,7 @@
     __regid__ = 'treeview'
     itemvid = 'treeitemview'
     subvid = 'oneline'
-    css_classes = 'treeview widget'
+    cssclass = 'treeview widget'
     title = _('tree view')
 
     def _init_params(self, subvid, treeid, initial_load, initial_thru_ajax, morekwargs):
@@ -144,7 +144,7 @@
         if toplevel:
             self._init_headers(treeid, toplevel_thru_ajax)
             ulid = ' id="tree-%s"' % treeid
-        self.w(u'<ul%s class="%s">' % (ulid, self.css_classes))
+        self.w(u'<ul%s class="%s">' % (ulid, self.cssclass))
         # XXX force sorting on x.sortvalue() (which return dc_title by default)
         # we need proper ITree & co specification to avoid this.
         # (pb when type ambiguity at the other side of the tree relation,
@@ -171,7 +171,7 @@
     """specific version of the treeview to display file trees
     """
     __regid__ = 'filetree'
-    css_classes = 'treeview widget filetree'
+    cssclass = 'treeview widget filetree'
     title = _('file tree view')
 
     def call(self, subvid=None, treeid=None, initial_load=True, **kwargs):
--- a/web/views/xmlrss.py	Wed Aug 25 09:43:12 2010 +0200
+++ b/web/views/xmlrss.py	Wed Aug 25 10:01:11 2010 +0200
@@ -148,25 +148,25 @@
         return entity.cw_adapt_to('IFeed').rss_feed_url()
 
 
-class RSSIconBox(box.BoxTemplate):
+class RSSIconBox(box.Box):
     """just display the RSS icon on uniform result set"""
     __regid__ = 'rss'
-    __select__ = (box.BoxTemplate.__select__
+    __select__ = (box.Box.__select__
                   & appobject_selectable('components', 'rss_feed_url'))
 
     visible = False
     order = 999
 
-    def call(self, **kwargs):
+    def render(self, w, **kwargs):
         try:
             rss = self._cw.uiprops['RSS_LOGO']
         except KeyError:
             self.error('missing RSS_LOGO external resource')
             return
         urlgetter = self._cw.vreg['components'].select('rss_feed_url', self._cw,
-                                                   rset=self.cw_rset)
+                                                       rset=self.cw_rset)
         url = urlgetter.feed_url()
-        self.w(u'<a href="%s"><img src="%s" alt="rss"/></a>\n' % (xml_escape(url), rss))
+        w(u'<a href="%s"><img src="%s" alt="rss"/></a>\n' % (xml_escape(url), rss))
 
 
 class RSSView(XMLView):