Moved skin-related features into ".skin" modules
authorThierry Florac <thierry.florac@onf.fr>
Fri, 18 May 2018 15:50:16 +0200
changeset 545 ae803782cc37
parent 544 6928ddfc1c0f
child 546 213db0cb6b4c
Moved skin-related features into ".skin" modules
src/pyams_content/features/footer/__init__.py
src/pyams_content/features/footer/interfaces/__init__.py
src/pyams_content/features/footer/skin/__init__.py
src/pyams_content/features/footer/zmi/__init__.py
src/pyams_content/features/footer/zmi/templates/renderer-settings.pt
src/pyams_content/features/header/__init__.py
src/pyams_content/features/header/interfaces/__init__.py
src/pyams_content/features/header/skin/__init__.py
src/pyams_content/features/header/zmi/__init__.py
src/pyams_content/features/header/zmi/templates/renderer-settings.pt
src/pyams_content/features/renderer/__init__.py
src/pyams_content/features/renderer/interfaces/__init__.py
src/pyams_content/features/renderer/skin/__init__.py
src/pyams_content/features/renderer/zmi/__init__.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/footer/__init__.py	Fri May 18 15:50:16 2018 +0200
@@ -0,0 +1,126 @@
+#
+# Copyright (c) 2008-2018 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.features.footer.interfaces import FOOTER_RENDERERS, IFooterRenderer, IFooterSettings, IFooterTarget, \
+    FOOTER_SETTINGS_KEY, IFooterRendererSettings, FOOTER_RENDERER_SETTINGS_KEY
+from zope.traversing.interfaces import ITraversable
+
+# import packages
+from persistent import Persistent
+from pyams_content.features.renderer import RenderedContentMixin
+from pyams_utils.adapter import adapter_config, ContextAdapter, get_annotation_adapter
+from pyams_utils.inherit import BaseInheritInfo, InheritedFieldProperty
+from pyams_utils.request import check_request
+from pyams_utils.traversing import get_parent
+from pyams_utils.vocabulary import vocabulary_config
+from zope.interface import implementer, noLongerProvides, alsoProvides
+from zope.location import Location, locate
+from zope.schema.fieldproperty import FieldProperty
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
+
+
+@implementer(IFooterSettings)
+class FooterSettings(BaseInheritInfo, RenderedContentMixin, Persistent, Location):
+    """Footer settings persistent class"""
+
+    target_interface = IFooterTarget
+    adapted_interface = IFooterSettings
+
+    _renderer = FieldProperty(IFooterSettings['renderer'])
+    renderer = InheritedFieldProperty(IFooterSettings['renderer'])
+
+    renderer_interface = IFooterRenderer
+
+    def get_renderer(self, request=None, renderer_name=None):
+        if request is None:
+            request = check_request()
+        if not renderer_name:
+            renderer_name = request.params.get('form.widgets.renderer')
+            if renderer_name is None:
+                renderer_name = self.renderer or 'hidden'
+        parent = get_parent(self, IFooterTarget)
+        return request.registry.queryMultiAdapter((parent, request), self.renderer_interface,
+                                                  name=renderer_name)
+
+    def get_settings(self, renderer_name=None):
+        renderer = self.get_renderer(renderer_name=renderer_name)
+        if (renderer is None) or (renderer.settings_interface is None):
+            return None
+        settings_key = FOOTER_RENDERER_SETTINGS_KEY.format(renderer.settings_key)
+        return get_annotation_adapter(self, settings_key,
+                                      factory=lambda: IFooterRendererSettings(renderer),
+                                      name='++settings++{0}'.format(renderer.name))
+
+    @property
+    def settings(self):
+        return self.get_settings()
+
+
+@adapter_config(context=IFooterTarget, provides=IFooterSettings)
+def footer_settings_factory(context):
+    """Footer settings factory"""
+    return get_annotation_adapter(context, FOOTER_SETTINGS_KEY, FooterSettings, name='++footer++')
+
+
+@adapter_config(name='footer', context=IFooterTarget, provides=ITraversable)
+class FooterTargetNamespace(ContextAdapter):
+    """Footer target '++footer++' namespace traverser"""
+
+    def traverse(self, name, furtherpath=None):
+        return IFooterSettings(self.context)
+
+
+@adapter_config(context=IFooterSettings, provides=IFooterRendererSettings)
+def footer_settings_renderer_settings_factory(context):
+    """Footer settings renderer settings factory"""
+    return context.settings
+
+
+@adapter_config(name='settings', context=IFooterSettings, provides=ITraversable)
+class FooterSettingsRendererSettingsNamespace(ContextAdapter):
+    """Footer settings '++settings++' namespace traverser"""
+
+    def traverse(self, name, furtherpath=None):
+        if name:
+            return self.context.get_settings(renderer_name=name)
+        else:
+            return IFooterRendererSettings(self.context)
+
+
+@adapter_config(context=IFooterTarget, provides=IFooterRendererSettings)
+def footer_target_renderer_settings_factory(context):
+    """Footer target renderer settings factory"""
+    settings = IFooterSettings(context)
+    return IFooterRendererSettings(settings, None)
+
+
+@vocabulary_config(name=FOOTER_RENDERERS)
+class FooterRendererVocabulary(SimpleVocabulary):
+    """Footer renderers vocabulary"""
+
+    def __init__(self, context=None):
+        request = check_request()
+        if context is None:
+            context = request.context
+        translate = request.localizer.translate
+        registry = request.registry
+        target = get_parent(context, IFooterTarget)
+        terms = [SimpleTerm(name, title=translate(adapter.label))
+                 for name, adapter in sorted(registry.getAdapters((target, request), IFooterRenderer),
+                                             key=lambda x: x[1].weight)]
+        super(FooterRendererVocabulary, self).__init__(terms)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/footer/interfaces/__init__.py	Fri May 18 15:50:16 2018 +0200
@@ -0,0 +1,60 @@
+#
+# Copyright (c) 2008-2018 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.features.renderer.interfaces import IRenderedContent, IContentRenderer, IRendererSettings
+from pyams_utils.interfaces.inherit import IInheritInfo
+from zope.annotation.interfaces import IAttributeAnnotatable
+
+# import packages
+from zope.interface import Attribute
+from zope.schema import Choice
+
+from pyams_content import _
+
+
+FOOTER_SETTINGS_KEY = 'pyams_content.footer'
+FOOTER_RENDERER_SETTINGS_KEY = 'pyams_content.footer::{0}'
+
+FOOTER_RENDERERS = 'PyAMS.footer.renderers'
+
+
+class IFooterSettings(IInheritInfo, IRenderedContent):
+    """Footer settings interface"""
+
+    renderer = Choice(title=_("Footer template"),
+                      description=_("Presentation template used for this footer"),
+                      vocabulary=FOOTER_RENDERERS,
+                      required=False,
+                      default='hidden')
+
+    settings = Attribute("Renderer settings")
+
+
+class IFooterTarget(IAttributeAnnotatable):
+    """Footer target marker interface"""
+
+
+class IFooterRenderer(IContentRenderer):
+    """Footer renderer interface"""
+
+    name = Attribute("Renderer name")
+    settings_key = Attribute("Renderer settings key")
+
+
+class IFooterRendererSettings(IRendererSettings):
+    """Footer renderer settings interface"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/footer/skin/__init__.py	Fri May 18 15:50:16 2018 +0200
@@ -0,0 +1,53 @@
+#
+# Copyright (c) 2008-2018 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.features.footer.interfaces import IFooterTarget, IFooterRenderer, IFooterSettings
+from pyams_content.features.renderer.interfaces import HIDDEN_RENDERER_NAME
+from pyams_skin.layer import IPyAMSLayer
+
+# import packages
+from pyams_content.features.renderer.skin import BaseContentRenderer
+from pyams_utils.adapter import adapter_config
+from pyramid.decorator import reify
+
+from pyams_content import _
+
+
+class BaseFooterRenderer(BaseContentRenderer):
+    """Base footer renderer"""
+
+    @reify
+    def settings(self):
+        if self.settings_interface is None:
+            return None
+        settings = IFooterSettings(self.context)
+        while settings.inherit:
+            settings = IFooterSettings(settings.parent)
+        return settings.settings
+
+
+@adapter_config(name=HIDDEN_RENDERER_NAME, context=(IFooterTarget, IPyAMSLayer), provides=IFooterRenderer)
+class HiddenFooterRenderer(BaseFooterRenderer):
+    """Hidden footer renderer"""
+
+    name = HIDDEN_RENDERER_NAME
+    label = _("Hidden footer")
+    weight = -999
+
+    def render(self):
+        return ''
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/footer/zmi/__init__.py	Fri May 18 15:50:16 2018 +0200
@@ -0,0 +1,218 @@
+#
+# Copyright (c) 2008-2018 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.features.footer.interfaces import IFooterTarget, IFooterSettings, IFooterRenderer, \
+    IFooterRendererSettings
+from pyams_form.interfaces.form import IWidgetForm, IUncheckedEditFormButtons, IInnerSubForm, \
+    IWidgetsSuffixViewletsManager
+from pyams_portal.interfaces import MANAGE_TEMPLATE_PERMISSION
+from pyams_portal.zmi.interfaces import IPortalContextTemplatePropertiesMenu
+from pyams_skin.interfaces import IInnerPage
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces.data import IObjectData
+from pyams_utils.interfaces.inherit import IInheritInfo
+from z3c.form.interfaces import INPUT_MODE
+
+# import packages
+from pyams_form.form import AJAXEditForm
+from pyams_form.group import NamedWidgetsGroup
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.viewlet.menu import MenuItem, MenuDivider
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config
+from pyams_utils.url import absolute_url
+from pyams_viewlet.viewlet import viewlet_config, Viewlet
+from pyams_zmi.form import AdminEditForm, InnerAdminEditForm
+from pyramid.exceptions import NotFound
+from pyramid.response import Response
+from pyramid.view import view_config
+from z3c.form import field, button
+from z3c.form.browser.checkbox import SingleCheckBoxFieldWidget
+from zope.interface import implementer, alsoProvides, Interface
+
+from pyams_content import _
+
+
+@viewlet_config(name='footer-settings.menu', context=IFooterTarget, layer=IPyAMSLayer,
+                manager=IPortalContextTemplatePropertiesMenu, permission=MANAGE_TEMPLATE_PERMISSION, weight=102)
+class FooterSettingsMenu(MenuItem):
+    """Footer settings menu"""
+
+    label = _("Page footer")
+    url = '#footer-settings.html'
+
+
+class IFooterSettingsGroup(Interface):
+    """Footer settings group marker interface"""
+
+
+@pagelet_config(name='footer-settings.html', context=IFooterTarget, layer=IPyAMSLayer,
+                permission=MANAGE_TEMPLATE_PERMISSION)
+@implementer(IWidgetForm, IInnerPage)
+class FooterSettingsEditForm(AdminEditForm):
+    """Footer settings edit form"""
+
+    @property
+    def title(self):
+        return self.context.title
+
+    legend = _("Edit footer settings")
+
+    def getContent(self):
+        return IFooterSettings(self.context)
+
+    @property
+    def fields(self):
+        if self.getContent().can_inherit:
+            fields = field.Fields(IFooterSettings).select('no_inherit')
+            fields['no_inherit'].widgetFactory = SingleCheckBoxFieldWidget
+        else:
+            fields = field.Fields(Interface)
+        return fields
+
+    @property
+    def buttons(self):
+        if self.mode == INPUT_MODE:
+            return button.Buttons(IUncheckedEditFormButtons)
+        else:
+            return button.Buttons(Interface)
+
+    ajax_handler = 'footer-settings.json'
+
+    def updateGroups(self):
+        if self.getContent().can_inherit:
+            group = NamedWidgetsGroup(self, 'footer', self.widgets,
+                                      ('no_inherit', ),
+                                      legend=_("Don't inherit parent footer"),
+                                      css_class='inner',
+                                      switch=True,
+                                      checkbox_switch=True,
+                                      checkbox_mode='disable',
+                                      checkbox_field=IFooterSettings['no_inherit'])
+        else:
+            group = NamedWidgetsGroup(self, 'footer', self.widgets, (), css_class='inner')
+        alsoProvides(group, IFooterSettingsGroup)
+        self.add_group(group)
+        super(FooterSettingsEditForm, self).updateGroups()
+
+
+@view_config(name='footer-settings.json', context=IFooterTarget, request_type=IPyAMSLayer,
+             permission=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+class FooterSettingsAJAXEditForm(AJAXEditForm, FooterSettingsEditForm):
+    """Footer settings edit form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        output = super(FooterSettingsAJAXEditForm, self).get_ajax_output(changes) or {}
+        if 'no_inherit' in changes.get(IInheritInfo, ()):
+            output['status'] = 'reload'
+        return output
+
+
+@adapter_config(name='renderer', context=(IFooterTarget, IPyAMSLayer, IFooterSettingsGroup), provides=IInnerSubForm)
+class FooterSettingsRendererEditSubform(InnerAdminEditForm):
+    """Footer settings renderer edit form"""
+
+    legend = None
+
+    fields = field.Fields(IFooterSettings).select('renderer')
+    weight = 1
+
+    def __init__(self, context, request, group):
+        context = IFooterSettings(context)
+        super(FooterSettingsRendererEditSubform, self).__init__(context, request, group)
+
+    def updateWidgets(self, prefix=None):
+        super(FooterSettingsRendererEditSubform, self).updateWidgets(prefix)
+        if 'renderer' in self.widgets:
+            widget = self.widgets['renderer']
+            widget.object_data = {
+                'ams-change-handler': 'MyAMS.helpers.select2ChangeHelper',
+                'ams-change-stop-propagation': 'true',
+                'ams-select2-helper-type': 'html',
+                'ams-select2-helper-url': absolute_url(self.context, self.request,
+                                                       'get-footer-settings-renderer-form.html'),
+                'ams-select2-helper-argument': 'form.widgets.renderer',
+                'ams-select2-helper-target': '#renderer-settings-helper'
+            }
+            alsoProvides(widget, IObjectData)
+
+    def get_forms(self, include_self=True):
+        if include_self and self.request.method == 'POST':
+            data, errors = self.extractData()
+            if not errors:
+                self.applyChanges(data)
+        for form in super(FooterSettingsRendererEditSubform, self).get_forms(include_self):
+            yield form
+
+
+@adapter_config(name='footer-renderer-settings-form',
+                context=(IFooterRendererSettings, IPyAMSLayer, FooterSettingsRendererEditSubform),
+                provides=IInnerSubForm)
+@adapter_config(name='footer-renderer-settings-form',
+                context=(IFooterTarget, IPyAMSLayer, FooterSettingsAJAXEditForm),
+                provides=IInnerSubForm)
+class FooterSettingsRendererSettingsEditForm(InnerAdminEditForm):
+    """Footer settings renderer settings edit form"""
+
+    legend = _("Footer renderer settings")
+
+    def __new__(cls, context, request, view=None):
+        settings = IFooterRendererSettings(context, None)
+        if settings is None:
+            return None
+        return InnerAdminEditForm.__new__(cls)
+
+    def __init__(self, context, request, view=None):
+        context = IFooterRendererSettings(context)
+        super(FooterSettingsRendererSettingsEditForm, self).__init__(context, request, view)
+
+
+@viewlet_config(name='footer-renderer-settings', context=IFooterSettings, layer=IPyAMSLayer,
+                view=FooterSettingsRendererEditSubform, manager=IWidgetsSuffixViewletsManager)
+@template_config(template='templates/renderer-settings.pt', layer=IPyAMSLayer)
+class FooterSettingsRendererSettingsWidgetsSuffix(Viewlet):
+    """Footer settings renderer settings viewlet"""
+
+    def render_edit_form(self):
+        settings = IFooterSettings(self.context)
+        renderer = settings.get_renderer(self.request)
+        if (renderer is None) or (renderer.settings_interface is None):
+            return ''
+        renderer_settings = IFooterRendererSettings(settings)
+        form = FooterSettingsRendererSettingsEditForm(renderer_settings, self.request)
+        form.update()
+        return form.render()
+
+
+@view_config(name='get-footer-settings-renderer-form.html', context=IFooterSettings,
+             request_type=IPyAMSLayer, permission=MANAGE_TEMPLATE_PERMISSION, xhr=True)
+def get_footer_settings_renderer_form(request):
+    """Footer settings renderer settings form"""
+    renderer_name = request.params.get('form.widgets.renderer')
+    if renderer_name is None:
+        raise NotFound("No provided renderer argument")
+    if not renderer_name:
+        renderer_name = ''
+    renderer = request.registry.queryMultiAdapter((request.context, request), IFooterRenderer, name=renderer_name)
+    if (renderer is None) or (renderer.settings_interface is None):
+        return Response('')
+    settings = IFooterSettings(request.context)
+    renderer_settings = IFooterRendererSettings(settings)
+    form = FooterSettingsRendererSettingsEditForm(renderer_settings, request)
+    form.update()
+    return Response(form.render())
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/footer/zmi/templates/renderer-settings.pt	Fri May 18 15:50:16 2018 +0200
@@ -0,0 +1,3 @@
+<div id="renderer-settings-helper">
+	<tal:var replace="structure view.render_edit_form()">Edit form</tal:var>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/header/__init__.py	Fri May 18 15:50:16 2018 +0200
@@ -0,0 +1,126 @@
+#
+# Copyright (c) 2008-2018 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.features.header.interfaces import HEADER_RENDERERS, IHeaderRenderer, IHeaderSettings, IHeaderTarget, \
+    HEADER_SETTINGS_KEY, IHeaderRendererSettings, HEADER_RENDERER_SETTINGS_KEY
+from zope.traversing.interfaces import ITraversable
+
+# import packages
+from persistent import Persistent
+from pyams_content.features.renderer import RenderedContentMixin
+from pyams_utils.adapter import adapter_config, ContextAdapter, get_annotation_adapter
+from pyams_utils.inherit import BaseInheritInfo, InheritedFieldProperty
+from pyams_utils.request import check_request
+from pyams_utils.traversing import get_parent
+from pyams_utils.vocabulary import vocabulary_config
+from zope.interface import implementer, noLongerProvides, alsoProvides
+from zope.location import Location, locate
+from zope.schema.fieldproperty import FieldProperty
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
+
+
+@implementer(IHeaderSettings)
+class HeaderSettings(BaseInheritInfo, RenderedContentMixin, Persistent, Location):
+    """Header settings persistent class"""
+
+    target_interface = IHeaderTarget
+    adapted_interface = IHeaderSettings
+
+    _renderer = FieldProperty(IHeaderSettings['renderer'])
+    renderer = InheritedFieldProperty(IHeaderSettings['renderer'])
+
+    renderer_interface = IHeaderRenderer
+
+    def get_renderer(self, request=None, renderer_name=None):
+        if request is None:
+            request = check_request()
+        if not renderer_name:
+            renderer_name = request.params.get('form.widgets.renderer')
+            if renderer_name is None:
+                renderer_name = self.renderer or 'hidden'
+        parent = get_parent(self, IHeaderTarget)
+        return request.registry.queryMultiAdapter((parent, request), self.renderer_interface,
+                                                  name=renderer_name)
+
+    def get_settings(self, renderer_name=None):
+        renderer = self.get_renderer(renderer_name=renderer_name)
+        if (renderer is None) or (renderer.settings_interface is None):
+            return None
+        settings_key = HEADER_RENDERER_SETTINGS_KEY.format(renderer.settings_key)
+        return get_annotation_adapter(self, settings_key,
+                                      factory=lambda: IHeaderRendererSettings(renderer),
+                                      name='++settings++{0}'.format(renderer.name))
+
+    @property
+    def settings(self):
+        return self.get_settings()
+
+
+@adapter_config(context=IHeaderTarget, provides=IHeaderSettings)
+def header_settings_factory(context):
+    """Header settings factory"""
+    return get_annotation_adapter(context, HEADER_SETTINGS_KEY, HeaderSettings, name='++header++')
+
+
+@adapter_config(name='header', context=IHeaderTarget, provides=ITraversable)
+class HeaderTargetNamespace(ContextAdapter):
+    """Header target '++header++' namespace traverser"""
+
+    def traverse(self, name, furtherpath=None):
+        return IHeaderSettings(self.context)
+
+
+@adapter_config(context=IHeaderSettings, provides=IHeaderRendererSettings)
+def header_settings_renderer_settings_factory(context):
+    """Header settings renderer settings factory"""
+    return context.settings
+
+
+@adapter_config(name='settings', context=IHeaderSettings, provides=ITraversable)
+class HeaderSettingsRendererSettingsNamespace(ContextAdapter):
+    """Header settings '++settings++' namespace traverser"""
+
+    def traverse(self, name, furtherpath=None):
+        if name:
+            return self.context.get_settings(renderer_name=name)
+        else:
+            return IHeaderRendererSettings(self.context)
+
+
+@adapter_config(context=IHeaderTarget, provides=IHeaderRendererSettings)
+def header_target_renderer_settings_factory(context):
+    """Header target renderer settings factory"""
+    settings = IHeaderSettings(context)
+    return IHeaderRendererSettings(settings, None)
+
+
+@vocabulary_config(name=HEADER_RENDERERS)
+class HeaderRendererVocabulary(SimpleVocabulary):
+    """Header renderers vocabulary"""
+
+    def __init__(self, context=None):
+        request = check_request()
+        if context is None:
+            context = request.context
+        translate = request.localizer.translate
+        registry = request.registry
+        target = get_parent(context, IHeaderTarget)
+        terms = [SimpleTerm(name, title=translate(adapter.label))
+                 for name, adapter in sorted(registry.getAdapters((target, request), IHeaderRenderer),
+                                             key=lambda x: x[1].weight)]
+        super(HeaderRendererVocabulary, self).__init__(terms)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/header/interfaces/__init__.py	Fri May 18 15:50:16 2018 +0200
@@ -0,0 +1,60 @@
+#
+# Copyright (c) 2008-2018 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.features.renderer import IContentRenderer, IRendererSettings, IRenderedContent
+from pyams_utils.interfaces.inherit import IInheritInfo
+from zope.annotation.interfaces import IAttributeAnnotatable
+
+# import packages
+from zope.interface import Attribute
+from zope.schema import Choice
+
+from pyams_content import _
+
+
+HEADER_SETTINGS_KEY = 'pyams_content.header'
+HEADER_RENDERER_SETTINGS_KEY = 'pyams_content.header::{0}'
+
+HEADER_RENDERERS = 'PyAMS.header.renderers'
+
+
+class IHeaderSettings(IInheritInfo, IRenderedContent):
+    """Header settings interface"""
+
+    renderer = Choice(title=_("Header template"),
+                      description=_("Presentation template used for this header"),
+                      vocabulary=HEADER_RENDERERS,
+                      required=False,
+                      default='hidden')
+
+    settings = Attribute("Renderer settings")
+
+
+class IHeaderTarget(IAttributeAnnotatable):
+    """Header target marker interface"""
+
+
+class IHeaderRenderer(IContentRenderer):
+    """Header renderer interface"""
+
+    name = Attribute("Renderer name")
+    settings_key = Attribute("Renderer settings key")
+
+
+class IHeaderRendererSettings(IRendererSettings):
+    """Header renderer settings interface"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/header/skin/__init__.py	Fri May 18 15:50:16 2018 +0200
@@ -0,0 +1,53 @@
+#
+# Copyright (c) 2008-2018 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.features.header.interfaces import IHeaderTarget, IHeaderRenderer, IHeaderSettings
+from pyams_content.features.renderer.interfaces import HIDDEN_RENDERER_NAME
+from pyams_skin.layer import IPyAMSLayer
+
+# import packages
+from pyams_content.features.renderer.skin import BaseContentRenderer
+from pyams_utils.adapter import adapter_config
+from pyramid.decorator import reify
+
+from pyams_content import _
+
+
+class BaseHeaderRenderer(BaseContentRenderer):
+    """Base header renderer"""
+
+    @reify
+    def settings(self):
+        if self.settings_interface is None:
+            return None
+        settings = IHeaderSettings(self.context)
+        while settings.inherit:
+            settings = IHeaderSettings(settings.parent)
+        return settings.settings
+
+
+@adapter_config(name=HIDDEN_RENDERER_NAME, context=(IHeaderTarget, IPyAMSLayer), provides=IHeaderRenderer)
+class HiddenHeaderRenderer(BaseHeaderRenderer):
+    """Hidden header renderer"""
+
+    name = HIDDEN_RENDERER_NAME
+    label = _("Hidden header")
+    weight = -999
+
+    def render(self):
+        return ''
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/header/zmi/__init__.py	Fri May 18 15:50:16 2018 +0200
@@ -0,0 +1,224 @@
+#
+# Copyright (c) 2008-2018 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.features.header.interfaces import IHeaderTarget, IHeaderSettings, IHeaderRenderer, \
+    IHeaderRendererSettings
+from pyams_form.interfaces.form import IWidgetForm, IUncheckedEditFormButtons, IInnerSubForm, \
+    IWidgetsSuffixViewletsManager
+from pyams_portal.interfaces import MANAGE_TEMPLATE_PERMISSION
+from pyams_portal.zmi.interfaces import IPortalContextTemplatePropertiesMenu
+from pyams_skin.interfaces import IInnerPage
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces.data import IObjectData
+from pyams_utils.interfaces.inherit import IInheritInfo
+from z3c.form.interfaces import INPUT_MODE
+
+# import packages
+from pyams_form.form import AJAXEditForm
+from pyams_form.group import NamedWidgetsGroup
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.viewlet.menu import MenuItem, MenuDivider
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config
+from pyams_utils.url import absolute_url
+from pyams_viewlet.viewlet import viewlet_config, Viewlet
+from pyams_zmi.form import AdminEditForm, InnerAdminEditForm
+from pyramid.exceptions import NotFound
+from pyramid.response import Response
+from pyramid.view import view_config
+from z3c.form import field, button
+from z3c.form.browser.checkbox import SingleCheckBoxFieldWidget
+from zope.interface import implementer, alsoProvides, Interface
+
+from pyams_content import _
+
+
+@viewlet_config(name='header-settings.divider', context=IHeaderTarget, layer=IPyAMSLayer,
+                manager=IPortalContextTemplatePropertiesMenu, permission=MANAGE_TEMPLATE_PERMISSION, weight=99)
+class HeaderSettingsDivider(MenuDivider):
+    """Header settings menu divider"""
+
+
+@viewlet_config(name='header-settings.menu', context=IHeaderTarget, layer=IPyAMSLayer,
+                manager=IPortalContextTemplatePropertiesMenu, permission=MANAGE_TEMPLATE_PERMISSION, weight=100)
+class HeaderSettingsMenu(MenuItem):
+    """Header settings menu"""
+
+    label = _("Page header")
+    url = '#header-settings.html'
+
+
+class IHeaderSettingsGroup(Interface):
+    """Header settings group marker interface"""
+
+
+@pagelet_config(name='header-settings.html', context=IHeaderTarget, layer=IPyAMSLayer,
+                permission=MANAGE_TEMPLATE_PERMISSION)
+@implementer(IWidgetForm, IInnerPage)
+class HeaderSettingsEditForm(AdminEditForm):
+    """Header settings edit form"""
+
+    @property
+    def title(self):
+        return self.context.title
+
+    legend = _("Edit header settings")
+
+    def getContent(self):
+        return IHeaderSettings(self.context)
+
+    @property
+    def fields(self):
+        if self.getContent().can_inherit:
+            fields = field.Fields(IHeaderSettings).select('no_inherit')
+            fields['no_inherit'].widgetFactory = SingleCheckBoxFieldWidget
+        else:
+            fields = field.Fields(Interface)
+        return fields
+
+    @property
+    def buttons(self):
+        if self.mode == INPUT_MODE:
+            return button.Buttons(IUncheckedEditFormButtons)
+        else:
+            return button.Buttons(Interface)
+
+    ajax_handler = 'header-settings.json'
+
+    def updateGroups(self):
+        if self.getContent().can_inherit:
+            group = NamedWidgetsGroup(self, 'header', self.widgets,
+                                      ('no_inherit', ),
+                                      legend=_("Don't inherit parent header"),
+                                      css_class='inner',
+                                      switch=True,
+                                      checkbox_switch=True,
+                                      checkbox_mode='disable',
+                                      checkbox_field=IHeaderSettings['no_inherit'])
+        else:
+            group = NamedWidgetsGroup(self, 'header', self.widgets, (), css_class='inner')
+        alsoProvides(group, IHeaderSettingsGroup)
+        self.add_group(group)
+        super(HeaderSettingsEditForm, self).updateGroups()
+
+
+@view_config(name='header-settings.json', context=IHeaderTarget, request_type=IPyAMSLayer,
+             permission=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+class HeaderSettingsAJAXEditForm(AJAXEditForm, HeaderSettingsEditForm):
+    """Header settings edit form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        output = super(HeaderSettingsAJAXEditForm, self).get_ajax_output(changes) or {}
+        if 'no_inherit' in changes.get(IInheritInfo, ()):
+            output['status'] = 'reload'
+        return output
+
+
+@adapter_config(name='renderer', context=(IHeaderTarget, IPyAMSLayer, IHeaderSettingsGroup), provides=IInnerSubForm)
+class HeaderSettingsRendererEditSubform(InnerAdminEditForm):
+    """Header settings renderer edit form"""
+
+    legend = None
+
+    fields = field.Fields(IHeaderSettings).select('renderer')
+    weight = 1
+
+    def __init__(self, context, request, group):
+        context = IHeaderSettings(context)
+        super(HeaderSettingsRendererEditSubform, self).__init__(context, request, group)
+
+    def updateWidgets(self, prefix=None):
+        super(HeaderSettingsRendererEditSubform, self).updateWidgets(prefix)
+        if 'renderer' in self.widgets:
+            widget = self.widgets['renderer']
+            widget.object_data = {
+                'ams-change-handler': 'MyAMS.helpers.select2ChangeHelper',
+                'ams-change-stop-propagation': 'true',
+                'ams-select2-helper-type': 'html',
+                'ams-select2-helper-url': absolute_url(self.context, self.request,
+                                                       'get-header-settings-renderer-form.html'),
+                'ams-select2-helper-argument': 'form.widgets.renderer',
+                'ams-select2-helper-target': '#renderer-settings-helper'
+            }
+            alsoProvides(widget, IObjectData)
+
+    def get_forms(self, include_self=True):
+        if include_self and self.request.method == 'POST':
+            data, errors = self.extractData()
+            if not errors:
+                self.applyChanges(data)
+        for form in super(HeaderSettingsRendererEditSubform, self).get_forms(include_self):
+            yield form
+
+
+@adapter_config(name='header-renderer-settings-form',
+                context=(IHeaderRendererSettings, IPyAMSLayer, HeaderSettingsRendererEditSubform),
+                provides=IInnerSubForm)
+@adapter_config(name='header-renderer-settings-form',
+                context=(IHeaderTarget, IPyAMSLayer, HeaderSettingsAJAXEditForm),
+                provides=IInnerSubForm)
+class HeaderSettingsRendererSettingsEditForm(InnerAdminEditForm):
+    """Header settings renderer settings edit form"""
+
+    legend = _("Header renderer settings")
+
+    def __new__(cls, context, request, view=None):
+        settings = IHeaderRendererSettings(context, None)
+        if settings is None:
+            return None
+        return InnerAdminEditForm.__new__(cls)
+
+    def __init__(self, context, request, view=None):
+        context = IHeaderRendererSettings(context)
+        super(HeaderSettingsRendererSettingsEditForm, self).__init__(context, request, view)
+
+
+@viewlet_config(name='header-renderer-settings', context=IHeaderSettings, layer=IPyAMSLayer,
+                view=HeaderSettingsRendererEditSubform, manager=IWidgetsSuffixViewletsManager)
+@template_config(template='templates/renderer-settings.pt', layer=IPyAMSLayer)
+class HeaderSettingsRendererSettingsWidgetsSuffix(Viewlet):
+    """Header settings renderer settings viewlet"""
+
+    def render_edit_form(self):
+        settings = IHeaderSettings(self.context)
+        renderer = settings.get_renderer(self.request)
+        if (renderer is None) or (renderer.settings_interface is None):
+            return ''
+        renderer_settings = IHeaderRendererSettings(settings)
+        form = HeaderSettingsRendererSettingsEditForm(renderer_settings, self.request)
+        form.update()
+        return form.render()
+
+
+@view_config(name='get-header-settings-renderer-form.html', context=IHeaderSettings,
+             request_type=IPyAMSLayer, permission=MANAGE_TEMPLATE_PERMISSION, xhr=True)
+def get_header_settings_renderer_form(request):
+    """Header settings renderer settings form"""
+    renderer_name = request.params.get('form.widgets.renderer')
+    if renderer_name is None:
+        raise NotFound("No provided renderer argument")
+    if not renderer_name:
+        renderer_name = ''
+    renderer = request.registry.queryMultiAdapter((request.context, request), IHeaderRenderer, name=renderer_name)
+    if (renderer is None) or (renderer.settings_interface is None):
+        return Response('')
+    settings = IHeaderSettings(request.context)
+    renderer_settings = IHeaderRendererSettings(settings)
+    form = HeaderSettingsRendererSettingsEditForm(renderer_settings, request)
+    form.update()
+    return Response(form.render())
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/header/zmi/templates/renderer-settings.pt	Fri May 18 15:50:16 2018 +0200
@@ -0,0 +1,3 @@
+<div id="renderer-settings-helper">
+	<tal:var replace="structure view.render_edit_form()">Edit form</tal:var>
+</div>
--- a/src/pyams_content/features/renderer/__init__.py	Thu May 03 11:29:56 2018 +0200
+++ b/src/pyams_content/features/renderer/__init__.py	Fri May 18 15:50:16 2018 +0200
@@ -29,11 +29,12 @@
     """Renderer mixin interface"""
 
     renderer = None
+    renderer_interface = IContentRenderer
 
     def get_renderer(self, request=None):
         if request is None:
             request = check_request()
-        return request.registry.queryMultiAdapter((self, request), IContentRenderer, name=self.renderer)
+        return request.registry.queryMultiAdapter((self, request), self.renderer_interface, name=self.renderer)
 
 
 @adapter_config(context=IRenderedContent, provides=IContentRenderer)
--- a/src/pyams_content/features/renderer/interfaces/__init__.py	Thu May 03 11:29:56 2018 +0200
+++ b/src/pyams_content/features/renderer/interfaces/__init__.py	Fri May 18 15:50:16 2018 +0200
@@ -23,6 +23,9 @@
 from zope.interface import Interface, Attribute
 
 
+HIDDEN_RENDERER_NAME = 'hidden'
+
+
 class IRenderedContent(IAttributeAnnotatable):
     """Generic interface for any rendered content"""
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/renderer/skin/__init__.py	Fri May 18 15:50:16 2018 +0200
@@ -0,0 +1,71 @@
+#
+# Copyright (c) 2008-2018 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.features.renderer.interfaces import IContentRenderer, IRendererSettings, IRenderedContent, \
+    HIDDEN_RENDERER_NAME
+from pyams_i18n.interfaces import II18n
+from pyams_skin.layer import IPyAMSLayer
+
+# import packages
+from pyams_template.template import get_view_template
+from pyams_utils.adapter import ContextRequestAdapter, adapter_config
+from pyramid.decorator import reify
+from zope.interface import implementer
+
+from pyams_content import _
+
+
+@implementer(IContentRenderer)
+class BaseContentRenderer(ContextRequestAdapter):
+    """Base content renderer"""
+
+    label = None
+    weight = 0
+    settings_interface = None
+
+    language = None
+    context_attrs = ()
+    i18n_context_attrs = ()
+
+    @reify
+    def settings(self):
+        if self.settings_interface is None:
+            return None
+        return IRendererSettings(self.context)
+
+    def update(self):
+        for attr in self.context_attrs:
+            setattr(self, attr, getattr(self.context, attr, None))
+        if self.i18n_context_attrs:
+            i18n = II18n(self.context, None)
+            if i18n is not None:
+                for attr in self.i18n_context_attrs:
+                    setattr(self, attr, i18n.get_attribute(attr, lang=self.language, request=self.request))
+
+    render = get_view_template()
+
+
+@adapter_config(name=HIDDEN_RENDERER_NAME, context=(IRenderedContent, IPyAMSLayer), provides=IContentRenderer)
+class HiddenContentRenderer(BaseContentRenderer):
+    """Hidden content renderer"""
+
+    label = _("Hidden content")
+    weight = -999
+
+    def render(self):
+        return ''
--- a/src/pyams_content/features/renderer/zmi/__init__.py	Thu May 03 11:29:56 2018 +0200
+++ b/src/pyams_content/features/renderer/zmi/__init__.py	Fri May 18 15:50:16 2018 +0200
@@ -18,20 +18,16 @@
 # import interfaces
 from pyams_content.features.renderer.interfaces import IRenderedContent, IContentRenderer, IRendererSettings
 from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
-from pyams_i18n.interfaces import II18n
 from pyams_skin.layer import IPyAMSLayer
 
 # import packages
 from pyams_form.form import AJAXEditForm
 from pyams_pagelet.pagelet import pagelet_config
-from pyams_template.template import get_view_template
-from pyams_utils.adapter import ContextRequestAdapter, adapter_config
 from pyams_viewlet.viewlet import BaseContentProvider
 from pyams_zmi.form import AdminDialogEditForm
-from pyramid.decorator import reify
 from pyramid.view import view_config
 from z3c.form import field
-from zope.interface import implementer, Interface
+from zope.interface import Interface
 
 from pyams_content import _
 
@@ -65,35 +61,6 @@
 # Base content renderer
 #
 
-@implementer(IContentRenderer)
-class BaseContentRenderer(ContextRequestAdapter):
-    """Base content renderer"""
-
-    label = None
-    weight = 0
-    settings_interface = None
-
-    language = None
-    context_attrs = ()
-    i18n_context_attrs = ()
-
-    @reify
-    def settings(self):
-        if self.settings_interface is None:
-            return None
-        return IRendererSettings(self.context)
-
-    def update(self):
-        for attr in self.context_attrs:
-            setattr(self, attr, getattr(self.context, attr, None))
-        if self.i18n_context_attrs:
-            i18n = II18n(self.context, None)
-            if i18n is not None:
-                for attr in self.i18n_context_attrs:
-                    setattr(self, attr, i18n.get_attribute(attr, lang=self.language, request=self.request))
-
-    render = get_view_template()
-
 
 @pagelet_config(name='renderer-properties.html', context=IRenderedContent, layer=IPyAMSLayer,
                 permission=MANAGE_CONTENT_PERMISSION)
@@ -121,18 +88,3 @@
              permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
 class RendererPropertiesAJAXEditForm(AJAXEditForm, RendererPropertiesEditForm):
     """Renderer properties edit form, JSON renderer"""
-
-
-#
-# Default common renderers
-#
-
-@adapter_config(name='hidden', context=(IRenderedContent, IPyAMSLayer), provides=IContentRenderer)
-class HiddenContentRenderer(BaseContentRenderer):
-    """Hidden content renderer"""
-
-    label = _("Hidden content")
-    weight = -999
-
-    def render(self):
-        return ''