You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by da...@apache.org on 2015/02/19 17:58:40 UTC

[08/51] [partial] isis git commit: ISIS-1049: moving viewer-wicket into core.

http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/properties/EntityPropertiesForm.java
----------------------------------------------------------------------
diff --git a/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/properties/EntityPropertiesForm.java b/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/properties/EntityPropertiesForm.java
new file mode 100644
index 0000000..cb2dc45
--- /dev/null
+++ b/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/properties/EntityPropertiesForm.java
@@ -0,0 +1,761 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.viewer.wicket.ui.components.entity.properties;
+
+import de.agilecoders.wicket.core.markup.html.bootstrap.common.NotificationPanel;
+import de.agilecoders.wicket.core.util.Attributes;
+
+import java.util.List;
+import java.util.Map;
+import com.google.common.collect.Lists;
+import org.apache.wicket.Component;
+import org.apache.wicket.MarkupContainer;
+import org.apache.wicket.Session;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
+import org.apache.wicket.ajax.markup.html.form.AjaxButton;
+import org.apache.wicket.feedback.ComponentFeedbackMessageFilter;
+import org.apache.wicket.markup.ComponentTag;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.form.Button;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.form.FormComponent;
+import org.apache.wicket.markup.repeater.RepeatingView;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.ResourceModel;
+import org.apache.wicket.request.Response;
+import org.apache.wicket.util.string.Strings;
+import org.apache.wicket.util.visit.IVisit;
+import org.apache.wicket.util.visit.IVisitor;
+import org.apache.isis.applib.annotation.ActionLayout;
+import org.apache.isis.applib.annotation.MemberGroupLayout.ColumnSpans;
+import org.apache.isis.applib.annotation.Where;
+import org.apache.isis.applib.filter.Filter;
+import org.apache.isis.applib.filter.Filters;
+import org.apache.isis.applib.services.exceprecog.ExceptionRecognizer;
+import org.apache.isis.applib.services.exceprecog.ExceptionRecognizerComposite;
+import org.apache.isis.core.commons.authentication.MessageBroker;
+import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
+import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager.ConcurrencyChecking;
+import org.apache.isis.core.metamodel.adapter.version.ConcurrencyException;
+import org.apache.isis.core.metamodel.facets.object.membergroups.MemberGroupLayoutFacet;
+import org.apache.isis.core.metamodel.runtimecontext.ServicesInjector;
+import org.apache.isis.core.metamodel.spec.ObjectSpecification;
+import org.apache.isis.core.metamodel.spec.ObjectSpecifications;
+import org.apache.isis.core.metamodel.spec.ObjectSpecifications.MemberGroupLayoutHint;
+import org.apache.isis.core.metamodel.spec.feature.Contributed;
+import org.apache.isis.core.metamodel.spec.feature.ObjectAction;
+import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation;
+import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
+import org.apache.isis.core.runtime.memento.Memento;
+import org.apache.isis.core.runtime.system.DeploymentType;
+import org.apache.isis.core.runtime.system.context.IsisContext;
+import org.apache.isis.core.runtime.system.transaction.IsisTransactionManager;
+import org.apache.isis.viewer.wicket.model.links.LinkAndLabel;
+import org.apache.isis.viewer.wicket.model.mementos.PropertyMemento;
+import org.apache.isis.viewer.wicket.model.models.ActionPrompt;
+import org.apache.isis.viewer.wicket.model.models.ActionPromptProvider;
+import org.apache.isis.viewer.wicket.model.models.EntityModel;
+import org.apache.isis.viewer.wicket.model.models.ScalarModel;
+import org.apache.isis.viewer.wicket.ui.ComponentType;
+import org.apache.isis.viewer.wicket.ui.components.actionmenu.entityactions.AdditionalLinksPanel;
+import org.apache.isis.viewer.wicket.ui.components.actionmenu.entityactions.EntityActionUtil;
+import org.apache.isis.viewer.wicket.ui.components.widgets.containers.UiHintPathSignificantWebMarkupContainer;
+import org.apache.isis.viewer.wicket.ui.components.widgets.formcomponent.CancelHintRequired;
+import org.apache.isis.viewer.wicket.ui.errors.JGrowlBehaviour;
+import org.apache.isis.viewer.wicket.ui.pages.entity.EntityPage;
+import org.apache.isis.viewer.wicket.ui.panels.FormAbstract;
+import org.apache.isis.viewer.wicket.ui.panels.IFormSubmitterWithPreValidateHook;
+import org.apache.isis.viewer.wicket.ui.util.Components;
+import org.apache.isis.viewer.wicket.ui.util.CssClassAppender;
+
+public class EntityPropertiesForm extends FormAbstract<ObjectAdapter> implements ActionPromptProvider {
+
+    private static final long serialVersionUID = 1L;
+
+    private static final String ID_MEMBER_GROUP = "memberGroup";
+    private static final String ID_MEMBER_GROUP_NAME = "memberGroupName";
+
+    private static final String ID_ASSOCIATED_ACTION_LINKS_PANEL = "associatedActionLinksPanel";
+    private static final String ID_ASSOCIATED_ACTION_LINKS_PANEL_DROPDOWN = "associatedActionLinksPanelDropDown";
+
+    private static final String ID_LEFT_COLUMN = "leftColumn";
+    private static final String ID_MIDDLE_COLUMN = "middleColumn";
+    private static final String ID_RIGHT_COLUMN = "rightColumn";
+    
+    private static final String ID_ENTITY_COLLECTIONS = "entityCollections";
+    private static final String ID_ENTITY_COLLECTIONS_OVERFLOW = "entityCollectionsOverflow";
+    
+    private static final String ID_PROPERTIES = "properties";
+    private static final String ID_PROPERTY = "property";
+
+    private static final String ID_EDIT_BUTTON = "edit";
+    private static final String ID_OK_BUTTON = "ok";
+    private static final String ID_CANCEL_BUTTON = "cancel";
+
+    private static final String ID_FEEDBACK = "feedback";
+
+    private final Component owningPanel;
+
+    private Button editButton;
+    private Button okButton;
+    private Button cancelButton;
+
+    private NotificationPanel feedback;
+    
+    private boolean renderedFirstField;
+
+    public EntityPropertiesForm(
+            final String id,
+            final EntityModel entityModel,
+            final Component owningPanel) {
+
+        super(id, entityModel);
+        this.owningPanel = owningPanel; // for repainting
+
+        buildGui();
+        
+        // add any concurrency exception that might have been propagated into the entity model 
+        // as a result of a previous action invocation
+        final String concurrencyExceptionIfAny = entityModel.getAndClearConcurrencyExceptionIfAny();
+        if(concurrencyExceptionIfAny != null) {
+            error(concurrencyExceptionIfAny);
+        }
+    }
+
+    private void buildGui() {
+
+        final EntityModel entityModel = (EntityModel) getModel();
+        final ColumnSpans columnSpans = entityModel.getObject().getSpecification().getFacet(MemberGroupLayoutFacet.class).getColumnSpans();
+
+        renderedFirstField = false;
+        
+        // left column
+        MarkupContainer leftColumn = new WebMarkupContainer(ID_LEFT_COLUMN);
+        add(leftColumn);
+        
+        boolean addedProperties;
+        if(columnSpans.getLeft() > 0) {
+            addedProperties = addPropertiesInColumn(leftColumn, MemberGroupLayoutHint.LEFT, columnSpans);
+            addButtons(leftColumn);
+            addFeedbackGui(leftColumn);
+        } else {
+            Components.permanentlyHide(this, ID_LEFT_COLUMN);
+            addedProperties = false;
+        }
+        if(!addedProperties) {
+            // a bit hacky...
+            Components.permanentlyHide(this,
+                    ID_EDIT_BUTTON, ID_OK_BUTTON, ID_CANCEL_BUTTON,
+                    ID_FEEDBACK);
+        }
+        
+        // middle column
+        if(columnSpans.getMiddle() > 0) {
+            MarkupContainer middleColumn = new WebMarkupContainer(ID_MIDDLE_COLUMN);
+            add(middleColumn);
+            addPropertiesInColumn(middleColumn, MemberGroupLayoutHint.MIDDLE, columnSpans);
+        } else {
+            Components.permanentlyHide(this, ID_MIDDLE_COLUMN);
+        }
+
+        // right column
+        if(columnSpans.getRight() > 0) {
+            MarkupContainer rightColumn = new WebMarkupContainer(ID_RIGHT_COLUMN);
+            add(rightColumn);
+            addPropertiesInColumn(rightColumn, MemberGroupLayoutHint.RIGHT, columnSpans);
+        } else {
+            Components.permanentlyHide(this, ID_RIGHT_COLUMN);
+        }
+
+        // collections
+        if(columnSpans.getCollections() > 0) {
+            final String idCollectionsToShow;
+            final String idCollectionsToHide;
+            int collectionSpan;
+            if (columnSpans.exceedsRow())  {
+                idCollectionsToShow = ID_ENTITY_COLLECTIONS_OVERFLOW;
+                idCollectionsToHide = ID_ENTITY_COLLECTIONS;
+                collectionSpan = 12;
+            } else {
+                idCollectionsToShow = ID_ENTITY_COLLECTIONS;
+                idCollectionsToHide = ID_ENTITY_COLLECTIONS_OVERFLOW;
+                collectionSpan = columnSpans.getCollections();
+            }
+
+            final Component collectionsColumn = getComponentFactoryRegistry().addOrReplaceComponent(this, idCollectionsToShow, ComponentType.ENTITY_COLLECTIONS, entityModel);
+            addClassForSpan(collectionsColumn, collectionSpan);
+            
+            Components.permanentlyHide(this, idCollectionsToHide);
+        } else {
+            Components.permanentlyHide(this, ID_ENTITY_COLLECTIONS);
+            Components.permanentlyHide(this, ID_ENTITY_COLLECTIONS_OVERFLOW);
+        }
+
+    }
+
+    private boolean addPropertiesInColumn(MarkupContainer markupContainer, MemberGroupLayoutHint hint, ColumnSpans columnSpans) {
+        final int span = hint.from(columnSpans);
+        
+        final EntityModel entityModel = (EntityModel) getModel();
+        final ObjectAdapter adapter = entityModel.getObject();
+        final ObjectSpecification objSpec = adapter.getSpecification();
+
+        final List<ObjectAssociation> associations = visibleProperties(adapter, objSpec, Where.OBJECT_FORMS);
+
+        final RepeatingView memberGroupRv = new RepeatingView(ID_MEMBER_GROUP);
+        markupContainer.add(memberGroupRv);
+
+        Map<String, List<ObjectAssociation>> associationsByGroup = ObjectAssociation.Util.groupByMemberOrderName(associations);
+        
+        final List<String> groupNames = ObjectSpecifications.orderByMemberGroups(objSpec, associationsByGroup.keySet(), hint);
+
+
+        for(String groupName: groupNames) {
+            final List<ObjectAssociation> associationsInGroup = associationsByGroup.get(groupName);
+            if(associationsInGroup==null) {
+                continue;
+            }
+
+            final WebMarkupContainer memberGroupRvContainer = new WebMarkupContainer(memberGroupRv.newChildId());
+            memberGroupRv.add(memberGroupRvContainer);
+            memberGroupRvContainer.add(new Label(ID_MEMBER_GROUP_NAME, groupName));
+
+            final List<LinkAndLabel> memberGroupActions = Lists.newArrayList();
+
+            final RepeatingView propertyRv = new RepeatingView(ID_PROPERTIES);
+            memberGroupRvContainer.add(propertyRv);
+
+            @SuppressWarnings("unused")
+            Component component;
+            for (final ObjectAssociation association : associationsInGroup) {
+                final WebMarkupContainer propertyRvContainer = new UiHintPathSignificantWebMarkupContainer(propertyRv.newChildId());
+                propertyRv.add(propertyRvContainer);
+
+                addPropertyToForm(entityModel, (OneToOneAssociation) association, propertyRvContainer, memberGroupActions);
+            }
+
+            final List<LinkAndLabel> actionsPanel = LinkAndLabel.positioned(memberGroupActions, ActionLayout.Position.PANEL);
+            final List<LinkAndLabel> actionsPanelDropDown = LinkAndLabel.positioned(memberGroupActions, ActionLayout.Position.PANEL_DROPDOWN);
+
+            AdditionalLinksPanel.addAdditionalLinks(
+                    memberGroupRvContainer, ID_ASSOCIATED_ACTION_LINKS_PANEL,
+                    actionsPanel,
+                    AdditionalLinksPanel.Style.INLINE_LIST);
+            AdditionalLinksPanel.addAdditionalLinks(
+                    memberGroupRvContainer, ID_ASSOCIATED_ACTION_LINKS_PANEL_DROPDOWN,
+                    actionsPanelDropDown,
+                    AdditionalLinksPanel.Style.DROPDOWN);
+        }
+        
+        addClassForSpan(markupContainer, span);
+        return !groupNames.isEmpty();
+    }
+
+    private void addPropertyToForm(
+            final EntityModel entityModel,
+            final OneToOneAssociation association,
+            final WebMarkupContainer container,
+            final List<LinkAndLabel> entityActions) {
+        final OneToOneAssociation otoa = association;
+        final PropertyMemento pm = new PropertyMemento(otoa);
+
+        final ScalarModel scalarModel = entityModel.getPropertyModel(pm);
+        final Component component = getComponentFactoryRegistry().addOrReplaceComponent(container, ID_PROPERTY, ComponentType.SCALAR_NAME_AND_VALUE, scalarModel);
+
+        final List<ObjectAction> associatedActions = EntityActionUtil.getObjectActionsForAssociation(entityModel, otoa, getDeploymentType());
+
+        entityActions.addAll(EntityActionUtil.asLinkAndLabelsForAdditionalLinksPanel(entityModel, associatedActions));
+
+        if(!renderedFirstField) {
+            component.add(new CssClassAppender("first-field"));
+            renderedFirstField = true;
+        }
+    }
+
+
+    private List<ObjectAssociation> visibleProperties(final ObjectAdapter adapter, final ObjectSpecification objSpec, Where where) {
+        return objSpec.getAssociations(Contributed.INCLUDED, visiblePropertyFilter(adapter, where));
+    }
+
+    @SuppressWarnings("unchecked")
+    private Filter<ObjectAssociation> visiblePropertyFilter(final ObjectAdapter adapter, Where where) {
+        return Filters.and(ObjectAssociation.Filters.PROPERTIES, ObjectAssociation.Filters.dynamicallyVisible(getAuthenticationSession(), adapter, where));
+    }
+
+    @Override
+    protected void onComponentTag(ComponentTag tag) {
+        super.onComponentTag(tag);
+
+        Attributes.addClass(tag, "form-horizontal");
+    }
+
+    @Override
+    public ActionPrompt getActionPrompt() {
+        return ActionPromptProvider.Util.getFrom(this).getActionPrompt();
+    }
+
+    abstract class AjaxButtonWithOnError extends AjaxButton {
+
+        public AjaxButtonWithOnError(String id, IModel<String> model) {
+            super(id, model);
+        }
+
+        @Override
+        protected void onError(AjaxRequestTarget target, Form<?> form) {
+            super.onError(target, form);
+            toEditMode(target);
+        }
+
+        /**
+         * Render the 'type' attribute even for invisible buttons to avoid
+         * <a href="https://github.com/twbs/bootlint/wiki/W007">Bootlint W007</a>
+         *
+         * @param tag The component tag to render
+         * @param response The response to write to
+         */
+        // TODO mgrigorov Move this to Wicket Bootstrap project
+        @Override
+        protected void renderPlaceholderTag(ComponentTag tag, Response response) {
+            String ns = Strings.isEmpty(tag.getNamespace()) ? null : tag.getNamespace() + ':';
+
+            response.write("<");
+            if (ns != null)
+            {
+                response.write(ns);
+            }
+            response.write(tag.getName());
+            response.write(" id=\"");
+            response.write(getAjaxRegionMarkupId());
+
+            String type = tag.getAttribute("type");
+            if (!Strings.isEmpty(type)) {
+                response.write("\" type=\""+type);
+            }
+
+            response.write("\" style=\"display:none\"></");
+            if (ns != null)
+            {
+                response.write(ns);
+            }
+            response.write(tag.getName());
+            response.write(">");
+        }
+    }
+
+    public class AjaxButtonForValidate extends AjaxButtonWithOnError implements IFormSubmitterWithPreValidateHook {
+        private static final long serialVersionUID = 1L;
+        public AjaxButtonForValidate(String id, IModel<String> model) {
+            super(id, model);
+        }
+
+        @Override
+        public String preValidate() {
+            // attempt to load with concurrency checking, catching recognized exceptions
+            try {
+                getEntityModel().load(ConcurrencyChecking.CHECK); // could have also just called #getObject(), since CHECK is the default
+
+            } catch(ConcurrencyException ex){
+                String recognizedErrorMessage = recognizeException(ex);
+                if(recognizedErrorMessage == null) {
+                    throw ex;
+                }
+
+                // reload
+                getEntityModel().load(ConcurrencyChecking.NO_CHECK);
+
+                getForm().clearInput();
+                getEntityModel().resetPropertyModels();
+
+                toViewMode(null);
+                toEditMode(null);
+
+                return recognizedErrorMessage;
+            }
+
+            return null;
+        }
+
+        @Override
+        public void validate() {
+            // add in any error message that we might have recognized from above
+            EntityPropertiesForm form = EntityPropertiesForm.this;
+            String preValidationErrorIfAny = form.getPreValidationErrorIfAny();
+
+            if(preValidationErrorIfAny != null) {
+                feedbackOrNotifyAnyRecognizedError(preValidationErrorIfAny, form);
+                // skip validation, because would relate to old values
+
+                final EntityPage entityPage = new EntityPage(EntityPropertiesForm.this.getModelObject(), null);
+                EntityPropertiesForm.this.setResponsePage(entityPage);
+            } else {
+                // run Wicket's validation
+                super.validate();
+            }
+        }
+
+        @Override
+        protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
+
+            if (getForm().hasError()) {
+                // stay in edit mode
+                return;
+            }
+
+            doPreApply();
+            if (applyFormChangesElse()) return;
+            final Object redirectIfAny = doPostApply();
+
+            if (flushChangesElse(target)) return;
+
+            getEntityModel().resetPropertyModels();
+
+            toViewMode(null);
+
+            // "redirect-after-post"
+            //
+            // RequestCycle.get().getActiveRequestHandler() indicates this is handled by the ListenerInterfaceRequestHandler
+            // which renders page at end.
+            //
+            // it's necessary to zap the page parameters (so mapping is to just wicket/page?nn)
+            // otherwise (what I think happens) is that the httpServletResponse.sendRedirect ends up being to the same URL,
+            // and this is rejected as invalid either by the browser or by the servlet container (perhaps only if running remotely).
+            //
+
+            final ObjectAdapter objectAdapter;
+            if(redirectIfAny != null) {
+                objectAdapter = getPersistenceSession().getAdapterManager().adapterFor(redirectIfAny);
+            } else {
+                // we obtain the adapter from the entity model because (if a view model) then the entity model may contain
+                // a different adapter (the cloned view model) to the one with which we started with.
+                objectAdapter = getEntityModel().getObjectAdapterMemento().getObjectAdapter(ConcurrencyChecking.NO_CHECK);
+            }
+
+            final EntityPage entityPage = new EntityPage(objectAdapter, null);
+            EntityPropertiesForm.this.setResponsePage(entityPage);
+        }
+
+        /**
+         * Optional hook to override.
+         *
+         * <p>
+         * If a non-null value is returned, then transition to it (ie eg the finish() transition for a wizard).
+         * </p>
+         */
+        protected void doPreApply() {
+        }
+
+        /**
+         * Optional hook to override.
+         *
+         * <p>
+         * If a non-null value is returned, then transition to it (ie eg the finish() transition for a wizard).
+         * </p>
+         */
+        protected Object doPostApply() {
+            return null;
+        }
+
+    }
+
+    abstract class AjaxButtonForCancel extends AjaxButtonWithOnError {
+
+        public AjaxButtonForCancel(String id, IModel<String> model) {
+            super(id, model);
+            setDefaultFormProcessing(false);
+        }
+    }
+
+
+    private void addButtons(MarkupContainer markupContainer) {
+
+        // edit button
+        editButton = new AjaxButtonWithOnError(ID_EDIT_BUTTON, new ResourceModel("editLabel")) {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public void validate() {
+
+                // same logic as in cancelButton; should this be factored out?
+                try {
+                    getEntityModel().load(ConcurrencyChecking.CHECK);
+                } catch(ConcurrencyException ex) {
+                    getMessageBroker().addMessage("Object changed by " + ex.getOid().getVersion().getUser() + ", automatically reloading");
+                    getEntityModel().load(ConcurrencyChecking.NO_CHECK);
+                }
+                
+                super.validate();
+            }
+            
+            @Override
+            public void onSubmit(final AjaxRequestTarget target, final Form<?> form) {
+                getEntityModel().resetPropertyModels();
+                toEditMode(target);
+            }
+
+            @Override
+            protected void updateAjaxAttributes(AjaxRequestAttributes attributes) {
+                super.updateAjaxAttributes(attributes);
+                attributes.getAjaxCallListeners().add(new org.apache.wicket.ajax.attributes.AjaxCallListener(){
+
+                    private static final long serialVersionUID = 1L;
+
+                    @Override
+                    public CharSequence getSuccessHandler(Component component) {
+                        // scroll to the top of the entity panel
+                        return "$('html, body').animate({"
+                               + "        scrollTop: $('.entityIconAndTitlePanel').offset().top"
+                               + "    }, 1000);";
+                    }
+                });
+            }
+        };
+        editButton.add(new Label("editLabel", editButton.getModel()));
+        markupContainer.add(editButton);
+
+
+        // ok button
+        okButton = new AjaxButtonForValidate(ID_OK_BUTTON, new ResourceModel("okLabel"));
+        markupContainer.add(okButton);
+
+
+        // cancel button
+        cancelButton = new AjaxButtonForCancel(ID_CANCEL_BUTTON, new ResourceModel("cancelLabel")) {
+            private static final long serialVersionUID = 1L;
+            
+            @Override
+            public void validate() {
+
+                // same logic as in editButton; should this be factored out?
+                try {
+                    getEntityModel().load(ConcurrencyChecking.CHECK);
+                } catch(ConcurrencyException ex) {
+                    getMessageBroker().addMessage("Object changed by " + ex.getOid().getVersion().getUser() + ", automatically reloading");
+                    getEntityModel().load(ConcurrencyChecking.NO_CHECK);
+                }
+                super.validate();
+            }
+            
+            @Override
+            protected void onSubmit(final AjaxRequestTarget target, final Form<?> form) {
+                Session.get().getFeedbackMessages().clear();
+                getForm().clearInput();
+                getForm().visitFormComponentsPostOrder(new IVisitor<FormComponent<?>, Void>() {
+
+                    @Override
+                    public void component(FormComponent<?> formComponent, IVisit<Void> visit) {
+                        if (formComponent instanceof CancelHintRequired) {
+                            final CancelHintRequired cancelHintRequired = (CancelHintRequired) formComponent;
+                            cancelHintRequired.onCancel();
+                        }
+                    }
+                });
+
+                try {
+                    getEntityModel().resetPropertyModels();
+                } catch(RuntimeException ex) {
+                    throw ex;
+                }
+                toViewMode(target);
+            }
+        };
+
+        markupContainer.add(cancelButton);
+
+        okButton.setOutputMarkupPlaceholderTag(true);
+        editButton.setOutputMarkupPlaceholderTag(true);
+        cancelButton.setOutputMarkupPlaceholderTag(true);
+        
+        // flush any JGrowl messages (typically concurrency exceptions) if they are added.
+        okButton.add(new JGrowlBehaviour());
+        editButton.add(new JGrowlBehaviour());
+        cancelButton.add(new JGrowlBehaviour());
+    }
+
+    // to perform object-level validation, we must apply the changes first
+    // contrast this with ActionPanel (for validating actionarguments) where
+    // we do the validation prior to the execution of the action
+    private boolean applyFormChangesElse() {
+        final ObjectAdapter adapter = getEntityModel().getObject();
+        final Memento snapshotToRollbackToIfInvalid = new Memento(adapter);
+
+        getEntityModel().apply();
+        final String invalidReasonIfAny = getEntityModel().getReasonInvalidIfAny();
+        if (invalidReasonIfAny != null) {
+            error(invalidReasonIfAny);
+            snapshotToRollbackToIfInvalid.recreateObject();
+            toEditMode(null);
+            return true;
+        }
+        return false;
+    }
+
+    private boolean flushChangesElse(AjaxRequestTarget target) {
+        try {
+            this.getTransactionManager().flushTransaction();
+        } catch(RuntimeException ex) {
+
+            // There's no need to abort the transaction here, as it will have already been done
+            // (in IsisTransactionManager#executeWithinTransaction(...)).
+
+            String message = recognizeExceptionAndNotify(ex, this);
+            if(message == null) {
+                throw ex;
+            }
+
+            toEditMode(target);
+            return true;
+        }
+        return false;
+    }
+
+
+    private String recognizeExceptionAndNotify(RuntimeException ex, Component feedbackComponentIfAny) {
+        
+        // see if the exception is recognized as being a non-serious error
+        
+        String recognizedErrorMessageIfAny = recognizeException(ex);
+        feedbackOrNotifyAnyRecognizedError(recognizedErrorMessageIfAny, feedbackComponentIfAny);
+
+        return recognizedErrorMessageIfAny;
+    }
+
+    private void feedbackOrNotifyAnyRecognizedError(String recognizedErrorMessageIfAny, Component feedbackComponentIfAny) {
+        if(recognizedErrorMessageIfAny == null) {
+            return;
+        }
+        
+        if(feedbackComponentIfAny != null) {
+            feedbackComponentIfAny.error(recognizedErrorMessageIfAny);
+        }
+        getMessageBroker().addWarning(recognizedErrorMessageIfAny);
+
+        // we clear the abort cause because we've handled rendering the exception
+        getTransactionManager().getTransaction().clearAbortCause();
+    }
+
+    private String recognizeException(RuntimeException ex) {
+        
+        // REVIEW: this code is similar to stuff in EntityPropertiesForm, perhaps move up to superclass?
+        // REVIEW: similar code also in WebRequestCycleForIsis; combine?
+        
+        final List<ExceptionRecognizer> exceptionRecognizers = getServicesInjector().lookupServices(ExceptionRecognizer.class);
+        final String message = new ExceptionRecognizerComposite(exceptionRecognizers).recognize(ex);
+        return message;
+    }
+
+    private void requestRepaintPanel(final AjaxRequestTarget target) {
+        if (target != null) {
+            target.add(owningPanel);
+            // TODO: is it necessary to add these too?
+            target.add(editButton, okButton, cancelButton, feedback);
+        }
+    }
+
+    private EntityModel getEntityModel() {
+        return (EntityModel) getModel();
+    }
+
+    void toViewMode(final AjaxRequestTarget target) {
+
+        getEntityModel().toViewMode();
+
+        setVisible(editButton, isAnythingEditable());
+        setVisible(okButton, false);
+        setVisible(cancelButton, false);
+
+        requestRepaintPanel(target);
+    }
+
+    private void setVisible(Button b, boolean editable) {
+        if(b != null) {
+            b.setVisible(editable);
+        }
+    }
+
+    private boolean isAnythingEditable() {
+        final EntityModel entityModel = (EntityModel) getModel();
+        final ObjectAdapter adapter = entityModel.getObject();
+
+        return !enabledAssociations(adapter, adapter.getSpecification()).isEmpty();
+    }
+    
+    private List<ObjectAssociation> enabledAssociations(final ObjectAdapter adapter, final ObjectSpecification objSpec) {
+        return objSpec.getAssociations(Contributed.EXCLUDED, enabledAssociationFilter(adapter));
+    }
+
+    @SuppressWarnings("unchecked")
+    private Filter<ObjectAssociation> enabledAssociationFilter(final ObjectAdapter adapter) {
+        return Filters.and(ObjectAssociation.Filters.PROPERTIES, ObjectAssociation.Filters.enabled(getAuthenticationSession(), adapter, Where.OBJECT_FORMS));
+    }
+
+    private void toEditMode(final AjaxRequestTarget target) {
+        getEntityModel().toEditMode();
+
+        editButton.setVisible(false);
+        okButton.setVisible(true);
+        cancelButton.setVisible(true);
+
+        requestRepaintPanel(target);
+    }
+
+    private void addFeedbackGui(final MarkupContainer markupContainer) {
+        feedback = new NotificationPanel(ID_FEEDBACK, this, new ComponentFeedbackMessageFilter(this));
+        feedback.setOutputMarkupPlaceholderTag(true);
+        markupContainer.addOrReplace(feedback);
+
+        // to avoid potential XSS attacks, no longer escape model strings
+        // (risk is low but could just happen: error message being rendered might accidentally or deliberately contain rogue Javascript)
+        // feedback.setEscapeModelStrings(false);
+
+        final ObjectAdapter adapter = getEntityModel().getObject();
+        if (adapter == null) {
+            feedback.error("cannot locate object:" + getEntityModel().getObjectAdapterMemento().toString());
+        }
+    }
+
+    
+    private static void addClassForSpan(final Component component, final int numGridCols) {
+        component.add(new CssClassAppender("col-xs-"+numGridCols));
+    }
+
+
+
+    ///////////////////////////////////////////////////////
+    // Dependencies (from context)
+    ///////////////////////////////////////////////////////
+    
+    protected IsisTransactionManager getTransactionManager() {
+        return IsisContext.getTransactionManager();
+    }
+
+    protected ServicesInjector getServicesInjector() {
+        return IsisContext.getPersistenceSession().getServicesInjector();
+    }
+
+    protected MessageBroker getMessageBroker() {
+        return getAuthenticationSession().getMessageBroker();
+    }
+
+    protected DeploymentType getDeploymentType() {
+        return IsisContext.getDeploymentType();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/properties/EntityPropertiesForm.properties
----------------------------------------------------------------------
diff --git a/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/properties/EntityPropertiesForm.properties b/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/properties/EntityPropertiesForm.properties
new file mode 100644
index 0000000..71edcd9
--- /dev/null
+++ b/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/properties/EntityPropertiesForm.properties
@@ -0,0 +1,22 @@
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+#
+
+okLabel=OK
+cancelLabel=Cancel
+editLabel=Edit

http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/properties/EntityPropertiesPanel.html
----------------------------------------------------------------------
diff --git a/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/properties/EntityPropertiesPanel.html b/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/properties/EntityPropertiesPanel.html
new file mode 100644
index 0000000..e65c157
--- /dev/null
+++ b/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/properties/EntityPropertiesPanel.html
@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+  
+         http://www.apache.org/licenses/LICENSE-2.0
+         
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<html xmlns:wicket="http://wicket.apache.org">
+    <body>
+        <wicket:panel>
+            <div class="entityPropertiesPanel entityPropertiesComponentType">
+                <form wicket:id="entityProperties" class="inputForm" role="form">
+                    <div class="row" style="padding-bottom: 20px">
+                        <div wicket:id="leftColumn">
+                            <div class="inputFormTable properties">
+                                <fieldset wicket:id="memberGroup" class="memberGroup myBlockContainer">
+                                    <div class="panel panel-default">
+                                        <div class="panel-heading">
+                                            <span wicket:id="memberGroupName" class="panel-title">[group name]</span>
+                                            <div class="pull-right additionalLinks">
+                                                <div wicket:id="associatedActionLinksPanel"></div>
+                                                <div wicket:id="associatedActionLinksPanelDropDown"></div>
+                                            </div>
+                                        </div>
+                                        <div class="properties panel-body">
+                                            <div wicket:id="properties">
+                                                <div wicket:id="property" class="property">[property]</div>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </fieldset>
+                            </div>
+                            <div class="feedbackPanel">
+                                <span wicket:id="feedback"></span>
+                            </div>
+                            <div class="buttons">
+                                <button type="submit" class="edit btn btn-primary btn-md" wicket:id="edit"><span class="fa fa-edit"></span> <wicket:container wicket:id="editLabel"></wicket:container></button>
+                                <input type="submit" class="ok btn btn-primary btn-md" wicket:id="ok"/>
+                                <input type="submit" class="cancel btn btn-default btn-md" wicket:id="cancel"/>
+                            </div>
+                        </div>
+                        <div wicket:id="middleColumn">
+                            <div class="inputFormTable properties">
+                                <fieldset wicket:id="memberGroup" class="memberGroup myBlockContainer">
+                                    <div class="panel panel-default">
+                                        <div class="panel-heading">
+                                            <span wicket:id="memberGroupName" class="panel-title">[group name]</span>
+                                            <div class="pull-right additionalLinks">
+                                                <div wicket:id="associatedActionLinksPanel"></div>
+                                                <div wicket:id="associatedActionLinksPanelDropDown"></div>
+                                            </div>
+                                        </div>
+                                        <div class="properties panel-body">
+                                            <div wicket:id="properties">
+                                                <div wicket:id="property" class="property">[property]</div>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </fieldset>
+                            </div>
+                        </div>
+                        <div wicket:id="rightColumn">
+                            <div class="inputFormTable properties">
+                                <fieldset wicket:id="memberGroup" class="memberGroup myBlockContainer">
+                                    <div class="panel panel-default">
+                                        <div class="panel-heading">
+                                            <span wicket:id="memberGroupName" class="panel-title">[group name]</span>
+                                            <div class="pull-right additionalLinks">
+                                                <div wicket:id="associatedActionLinksPanel"></div>
+                                                <div wicket:id="associatedActionLinksPanelDropDown"></div>
+                                            </div>
+                                        </div>
+                                        <div class="properties panel-body">
+                                            <div wicket:id="properties">
+                                                <div wicket:id="property" class="property">[property]</div>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </fieldset>
+                            </div>
+                        </div>
+                        <div wicket:id="entityCollections"></div>
+                    </div>
+                    <div class="row">
+                        <div wicket:id="entityCollectionsOverflow"></div>
+                    </div>
+
+                </form>
+            </div>
+        </wicket:panel>
+    </body>
+</html>

http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/properties/EntityPropertiesPanel.java
----------------------------------------------------------------------
diff --git a/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/properties/EntityPropertiesPanel.java b/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/properties/EntityPropertiesPanel.java
new file mode 100644
index 0000000..0f49919
--- /dev/null
+++ b/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/properties/EntityPropertiesPanel.java
@@ -0,0 +1,60 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package org.apache.isis.viewer.wicket.ui.components.entity.properties;
+
+import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
+import org.apache.isis.viewer.wicket.model.models.EntityModel;
+import org.apache.isis.viewer.wicket.ui.panels.PanelAbstract;
+
+/**
+ * {@link PanelAbstract Panel} representing the properties of an entity, as per
+ * the provided {@link EntityModel}.
+ */
+public class EntityPropertiesPanel extends PanelAbstract<EntityModel> {
+
+    private static final long serialVersionUID = 1L;
+
+    private static final String ID_ENTITY_PROPERTIES = "entityProperties";
+
+    private EntityPropertiesForm form;
+
+    public EntityPropertiesPanel(final String id, final EntityModel entityModel) {
+        super(id, entityModel);
+        buildGui();
+        form.toViewMode(null);
+    }
+
+
+    private void buildGui() {
+        buildEntityPropertiesAndOrCollectionsGui();
+        setOutputMarkupId(true);
+    }
+
+    private void buildEntityPropertiesAndOrCollectionsGui() {
+        final EntityModel model = getModel();
+        final ObjectAdapter adapter = model.getObject();
+        if (adapter != null) {
+            form = new EntityPropertiesForm(ID_ENTITY_PROPERTIES, model, this);
+            addOrReplace(form);
+        } else {
+            permanentlyHide(ID_ENTITY_PROPERTIES);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/properties/EntityPropertiesPanelFactory.java
----------------------------------------------------------------------
diff --git a/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/properties/EntityPropertiesPanelFactory.java b/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/properties/EntityPropertiesPanelFactory.java
new file mode 100644
index 0000000..a236bd4
--- /dev/null
+++ b/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/properties/EntityPropertiesPanelFactory.java
@@ -0,0 +1,46 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package org.apache.isis.viewer.wicket.ui.components.entity.properties;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.model.IModel;
+
+import org.apache.isis.viewer.wicket.model.models.EntityModel;
+import org.apache.isis.viewer.wicket.ui.ComponentFactory;
+import org.apache.isis.viewer.wicket.ui.ComponentType;
+import org.apache.isis.viewer.wicket.ui.components.entity.EntityComponentFactoryAbstract;
+
+/**
+ * {@link ComponentFactory} for {@link EntityPropertiesPanel}.
+ */
+public class EntityPropertiesPanelFactory extends EntityComponentFactoryAbstract {
+
+    private static final long serialVersionUID = 1L;
+
+    public EntityPropertiesPanelFactory() {
+        super(ComponentType.ENTITY_PROPERTIES, EntityPropertiesPanel.class);
+    }
+
+    @Override
+    public Component createComponent(final String id, final IModel<?> model) {
+        final EntityModel entityModel = (EntityModel) model;
+        return new EntityPropertiesPanel(id, entityModel);
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/selector/links/EntityLinksSelectorPanel.html
----------------------------------------------------------------------
diff --git a/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/selector/links/EntityLinksSelectorPanel.html b/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/selector/links/EntityLinksSelectorPanel.html
new file mode 100644
index 0000000..988f0a3
--- /dev/null
+++ b/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/selector/links/EntityLinksSelectorPanel.html
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns:wicket="http://wicket.apache.org">
+<body>
+<wicket:panel>
+    <div class="linksSelectorPanel">
+        <div class="btn-group viewLinks" wicket:id="views">
+            <button type="button" class="btn btn-xs btn-info">
+                <span wicket:id="viewButtonIcon" class="ViewLinkItem"></span>
+                <span wicket:id="viewButtonTitle" class="ViewLinkItemTitle"></span>
+            </button>
+            <button type="button" class="btn btn-xs btn-info dropdown-toggle" data-toggle="dropdown">
+                <span class="caret"></span>
+            </button>
+            <ul wicket:id="viewList" class="dropdown-menu dropdown-menu-right" role="menu">
+                <li wicket:id="viewItem" class="viewItem">
+                    <a href="#" wicket:id="viewLink">
+                        <span wicket:id="viewItemIcon" class="ViewLinkItem"></span> <span wicket:id="viewItemTitle" class="ViewLinkItemTitle">[link title]</span>
+                    </a>
+                </li>
+            </ul>
+        </div>
+
+        <span wicket:id="additionalLinks"></span>
+
+        <div class="views">
+            <div wicket:id="entity-0" class="entityLinksSelectorPanel entityComponentType"></div>
+            <div wicket:id="entity-1" class="entityLinksSelectorPanel entityComponentType"></div>
+            <div wicket:id="entity-2" class="entityLinksSelectorPanel entityComponentType"></div>
+            <div wicket:id="entity-3" class="entityLinksSelectorPanel entityComponentType"></div>
+            <div wicket:id="entity-4" class="entityLinksSelectorPanel entityComponentType"></div>
+            <div wicket:id="entity-5" class="entityLinksSelectorPanel entityComponentType"></div>
+            <div wicket:id="entity-6" class="entityLinksSelectorPanel entityComponentType"></div>
+            <div wicket:id="entity-7" class="entityLinksSelectorPanel entityComponentType"></div>
+            <div wicket:id="entity-8" class="entityLinksSelectorPanel entityComponentType"></div>
+            <div wicket:id="entity-9" class="entityLinksSelectorPanel entityComponentType"></div>
+        </div>
+    </div>
+</wicket:panel>
+</body>
+</html>

http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/selector/links/EntityLinksSelectorPanel.java
----------------------------------------------------------------------
diff --git a/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/selector/links/EntityLinksSelectorPanel.java b/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/selector/links/EntityLinksSelectorPanel.java
new file mode 100644
index 0000000..2bd7cf1
--- /dev/null
+++ b/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/selector/links/EntityLinksSelectorPanel.java
@@ -0,0 +1,387 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package org.apache.isis.viewer.wicket.ui.components.entity.selector.links;
+
+import de.agilecoders.wicket.core.markup.html.bootstrap.button.Buttons;
+
+import java.util.ArrayList;
+import java.util.List;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Lists;
+import org.apache.wicket.AttributeModifier;
+import org.apache.wicket.Component;
+import org.apache.wicket.MarkupContainer;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.markup.html.AjaxLink;
+import org.apache.wicket.event.Broadcast;
+import org.apache.wicket.markup.ComponentTag;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.AbstractLink;
+import org.apache.wicket.markup.html.list.ListItem;
+import org.apache.wicket.markup.html.list.ListView;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+import org.apache.isis.core.commons.lang.StringExtensions;
+import org.apache.isis.viewer.wicket.model.hints.IsisUiHintEvent;
+import org.apache.isis.viewer.wicket.model.hints.UiHintContainer;
+import org.apache.isis.viewer.wicket.model.hints.UiHintPathSignificant;
+import org.apache.isis.viewer.wicket.model.links.LinkAndLabel;
+import org.apache.isis.viewer.wicket.model.links.LinksProvider;
+import org.apache.isis.viewer.wicket.model.models.EntityModel;
+import org.apache.isis.viewer.wicket.ui.CollectionContentsAsFactory;
+import org.apache.isis.viewer.wicket.ui.ComponentFactory;
+import org.apache.isis.viewer.wicket.ui.ComponentType;
+import org.apache.isis.viewer.wicket.ui.components.actionmenu.entityactions.AdditionalLinksPanel;
+import org.apache.isis.viewer.wicket.ui.panels.PanelAbstract;
+import org.apache.isis.viewer.wicket.ui.util.Components;
+import org.apache.isis.viewer.wicket.ui.util.CssClassAppender;
+import org.apache.isis.viewer.wicket.ui.util.CssClassRemover;
+
+/**
+ * Provides a list of links for selecting other views that support
+ * {@link ComponentType#ENTITY} with a backing {@link EntityModel}.
+ *
+ * <p>
+ *     TODO: this code could be simplified
+ *     (pushed down common code here and for the CollectionsSelectorPanel in order to do so);
+ *     haven't simplified this yet because currently there is only one view, so the markup
+ *     rendered by this component 'collapses' to just show that underlying view.
+ * </p>
+ */
+public class EntityLinksSelectorPanel extends PanelAbstract<EntityModel> implements UiHintPathSignificant {
+
+
+    private static final long serialVersionUID = 1L;
+
+    private static final String INVISIBLE_CLASS = "link-selector-panel-invisible";
+    private static final int MAX_NUM_UNDERLYING_VIEWS = 10;
+
+    private static final String ID_ADDITIONAL_LINKS = "additionalLinks";
+
+    private static final String ID_VIEWS = "views";
+    private static final String ID_VIEW_LIST = "viewList";
+    private static final String ID_VIEW_LINK = "viewLink";
+    private static final String ID_VIEW_ITEM = "viewItem";
+    private static final String ID_VIEW_ITEM_TITLE = "viewItemTitle";
+    private static final String ID_VIEW_ITEM_ICON = "viewItemIcon";
+
+    private static final String UIHINT_VIEW = "view";
+    private static final String ID_VIEW_BUTTON_TITLE = "viewButtonTitle";
+    private static final String ID_VIEW_BUTTON_ICON = "viewButtonIcon";
+
+    private final ComponentType componentType;
+    private final String underlyingIdPrefix;
+
+    private ComponentFactory selectedComponentFactory;
+    protected Component selectedComponent;
+
+
+    public EntityLinksSelectorPanel(final String id, final EntityModel model, final ComponentFactory factory) {
+        super(id, model);
+        this.underlyingIdPrefix = ComponentType.ENTITY.toString();
+        this.componentType = factory.getComponentType();
+    }
+
+
+    protected int determineInitialFactory(List<ComponentFactory> componentFactories, IModel<?> model) {
+        return 0;
+    }
+
+    @Override
+    public UiHintContainer getUiHintContainer() {
+        // disables hinting by this component
+        return null;
+    }
+
+
+    /**
+     * Build UI only after added to parent.
+     */
+    public void onInitialize() {
+        super.onInitialize();
+        ComponentFactory componentFactory = getComponentFactoryRegistry().findComponentFactoryElseFailFast(getComponentType(), getModel());
+        addAdditionalLinks(getModel());
+        addUnderlyingViews(underlyingIdPrefix, getModel(), componentFactory);
+    }
+
+    protected void addAdditionalLinks(final EntityModel model) {
+        if(!(model instanceof LinksProvider)) {
+            permanentlyHide(ID_ADDITIONAL_LINKS);
+            return;
+        }
+        LinksProvider linksProvider = (LinksProvider) model;
+        List<LinkAndLabel> links = linksProvider.getLinks();
+
+        addAdditionalLinks(this, links);
+    }
+
+    protected void addAdditionalLinks(MarkupContainer markupContainer, List<LinkAndLabel> links) {
+        if(links == null || links.isEmpty()) {
+            Components.permanentlyHide(markupContainer, ID_ADDITIONAL_LINKS);
+            return;
+        }
+        links = Lists.newArrayList(links); // copy, to serialize any lazy evaluation
+
+        AdditionalLinksPanel.addAdditionalLinks(
+                markupContainer, ID_ADDITIONAL_LINKS,
+                links,
+                AdditionalLinksPanel.Style.INLINE_LIST);
+    }
+
+    private void addUnderlyingViews(final String underlyingIdPrefix, final EntityModel model, final ComponentFactory factory) {
+        final List<ComponentFactory> componentFactories = findOtherComponentFactories(model, factory);
+
+        final int selected = honourViewHintElseDefault(componentFactories, model);
+
+        final EntityLinksSelectorPanel selectorPanel = this;
+
+        // create all, hide the one not selected
+        final Component[] underlyingViews = new Component[MAX_NUM_UNDERLYING_VIEWS];
+        int i = 0;
+        final EntityModel emptyModel = dummyOf(model);
+        for (ComponentFactory componentFactory : componentFactories) {
+            final String underlyingId = underlyingIdPrefix + "-" + i;
+
+            Component underlyingView = componentFactory.createComponent(underlyingId,i==selected? model: emptyModel);
+            underlyingViews[i++] = underlyingView;
+            selectorPanel.addOrReplace(underlyingView);
+        }
+
+        // hide any unused placeholders
+        while(i<MAX_NUM_UNDERLYING_VIEWS) {
+            String underlyingId = underlyingIdPrefix + "-" + i;
+            permanentlyHide(underlyingId);
+            i++;
+        }
+
+        // selector
+        if (componentFactories.size() <= 1) {
+            permanentlyHide(ID_VIEWS);
+        } else {
+            final Model<ComponentFactory> componentFactoryModel = new Model<>();
+
+            selectorPanel.selectedComponentFactory = componentFactories.get(selected);
+            componentFactoryModel.setObject(selectorPanel.selectedComponentFactory);
+
+            final WebMarkupContainer views = new WebMarkupContainer(ID_VIEWS);
+
+            final Label viewButtonTitle = new Label(ID_VIEW_BUTTON_TITLE, "Hidden");
+            views.addOrReplace(viewButtonTitle);
+
+            final Label viewButtonIcon = new Label(ID_VIEW_BUTTON_ICON, "");
+            views.addOrReplace(viewButtonIcon);
+
+            final WebMarkupContainer container = new WebMarkupContainer(ID_VIEW_LIST);
+
+            views.addOrReplace(container);
+            views.setOutputMarkupId(true);
+
+            this.setOutputMarkupId(true);
+
+            final ListView<ComponentFactory> listView = new ListView<ComponentFactory>(ID_VIEW_ITEM, componentFactories) {
+
+                private static final long serialVersionUID = 1L;
+
+                @Override
+                protected void populateItem(ListItem<ComponentFactory> item) {
+
+                    final int underlyingViewNum = item.getIndex();
+
+                    final ComponentFactory componentFactory = item.getModelObject();
+                    final AbstractLink link = new AjaxLink<Void>(ID_VIEW_LINK) {
+                        private static final long serialVersionUID = 1L;
+                        @Override
+                        public void onClick(AjaxRequestTarget target) {
+                            EntityLinksSelectorPanel linksSelectorPanel = EntityLinksSelectorPanel.this;
+                            linksSelectorPanel.setViewHintAndBroadcast(underlyingViewNum, target);
+
+                            final EntityModel dummyModel = dummyOf(model);
+                            for(int i=0; i<MAX_NUM_UNDERLYING_VIEWS; i++) {
+                                final Component component = underlyingViews[i];
+                                if(component == null) {
+                                    continue;
+                                }
+                                final boolean isSelected = i == underlyingViewNum;
+                                applyCssVisibility(component, isSelected);
+                                component.setDefaultModel(isSelected? model: dummyModel);
+                            }
+
+                            selectorPanel.selectedComponentFactory = componentFactory;
+                            selectorPanel.selectedComponent = underlyingViews[underlyingViewNum];
+                            selectorPanel.onSelect(target);
+                            target.add(selectorPanel, views);
+                        }
+
+                        @Override
+                        protected void onComponentTag(ComponentTag tag) {
+                            super.onComponentTag(tag);
+                            Buttons.fixDisabledState(this, tag);
+                        }
+                    };
+
+                    IModel<String> title = nameFor(componentFactory);
+                    Label viewItemTitleLabel = new Label(ID_VIEW_ITEM_TITLE, title);
+                    link.add(viewItemTitleLabel);
+
+                    Label viewItemIcon = new Label(ID_VIEW_ITEM_ICON, "");
+                    link.add(viewItemIcon);
+
+                    boolean isEnabled = componentFactory != selectorPanel.selectedComponentFactory;
+                    if (!isEnabled) {
+                        viewButtonTitle.setDefaultModel(title);
+                        IModel<String> cssClass = cssClassFor(componentFactory, viewButtonIcon);
+                        viewButtonIcon.add(AttributeModifier.replace("class", "ViewLinkItem " + cssClass.getObject()));
+                        link.setVisible(false);
+                    } else {
+                        IModel<String> cssClass = cssClassFor(componentFactory, viewItemIcon);
+                        viewItemIcon.add(new CssClassAppender(cssClass));
+                    }
+
+                    item.add(link);
+                }
+
+                private IModel<String> cssClassFor(final ComponentFactory componentFactory, Label viewIcon) {
+                    IModel<String> cssClass = null;
+                    if (componentFactory instanceof CollectionContentsAsFactory) {
+                        CollectionContentsAsFactory collectionContentsAsFactory = (CollectionContentsAsFactory) componentFactory;
+                        cssClass = collectionContentsAsFactory.getCssClass();
+                        viewIcon.setDefaultModelObject("");
+                        viewIcon.setEscapeModelStrings(true);
+                    }
+                    if (cssClass == null) {
+                        String name = componentFactory.getName();
+                        cssClass = Model.of(StringExtensions.asLowerDashed(name));
+                        // Small hack: if there is no specific CSS class then we assume that background-image is used
+                        // the span.ViewItemLink should have some content to show it
+                        // FIX: find a way to do this with CSS (width and height don't seems to help)
+                        viewIcon.setDefaultModelObject("&#160;&#160;&#160;&#160;&#160;");
+                        viewIcon.setEscapeModelStrings(false);
+                    }
+                    return cssClass;
+                }
+
+                private IModel<String> nameFor(final ComponentFactory componentFactory) {
+                    IModel<String> name = null;
+                    if (componentFactory instanceof CollectionContentsAsFactory) {
+                        CollectionContentsAsFactory collectionContentsAsFactory = (CollectionContentsAsFactory) componentFactory;
+                        name = collectionContentsAsFactory.getTitleLabel();
+                    }
+                    if (name == null) {
+                        name = Model.of(componentFactory.getName());
+                    }
+                    return name;
+                }
+            };
+            container.add(listView);
+            addOrReplace(views);
+        }
+
+        for(i=0; i<MAX_NUM_UNDERLYING_VIEWS; i++) {
+            Component component = underlyingViews[i];
+            if(component != null) {
+                if(i != selected) {
+                    component.add(new CssClassAppender(INVISIBLE_CLASS));
+                } else {
+                    selectedComponent = component;
+                }
+            }
+        }
+    }
+
+
+
+    protected void setViewHintAndBroadcast(int viewNum, AjaxRequestTarget target) {
+        final UiHintContainer uiHintContainer = getUiHintContainer();
+        if(uiHintContainer == null) {
+            return;
+        }
+        uiHintContainer.setHint(this, UIHINT_VIEW, ""+viewNum);
+        send(getPage(), Broadcast.EXACT, new IsisUiHintEvent(uiHintContainer, target));
+    }
+
+    /**
+     * Overrideable hook.
+     */
+    protected void onSelect(AjaxRequestTarget target) {
+    }
+
+    /**
+     * Ask for a dummy (empty) {@link Model} to pass into those components that are rendered but will be
+     * made invisible using CSS styling.
+     */
+    protected EntityModel dummyOf(EntityModel model) {
+        return model;
+    }
+
+    protected static void applyCssVisibility(final Component component, final boolean visible) {
+        if(component == null) {
+            return;
+        }
+        AttributeModifier modifier = visible ? new CssClassRemover(INVISIBLE_CLASS) : new CssClassAppender(INVISIBLE_CLASS);
+        component.add(modifier);
+    }
+
+    protected int honourViewHintElseDefault(final List<ComponentFactory> componentFactories, final IModel<?> model) {
+        // honour hints ...
+        final UiHintContainer hintContainer = getUiHintContainer();
+        if(hintContainer != null) {
+            String viewStr = hintContainer.getHint(this, UIHINT_VIEW);
+            if(viewStr != null) {
+                try {
+                    int view = Integer.parseInt(viewStr);
+                    if(view >= 0 && view < componentFactories.size()) {
+                        return view;
+                    }
+                } catch(NumberFormatException ex) {
+                    // ignore
+                }
+            }
+        }
+
+        // ... else default
+        int initialFactory = determineInitialFactory(componentFactories, model);
+        if(hintContainer != null) {
+            hintContainer.setHint(this, UIHINT_VIEW, ""+initialFactory);
+            // don't broadcast (no AjaxRequestTarget, still configuring initial setup)
+        }
+        return initialFactory;
+    }
+
+
+    private List<ComponentFactory> findOtherComponentFactories(final EntityModel model, final ComponentFactory ignoreFactory) {
+        final List<ComponentFactory> componentFactories = getComponentFactoryRegistry().findComponentFactories(componentType, model);
+        ArrayList<ComponentFactory> otherFactories = Lists.newArrayList(Collections2.filter(componentFactories, new Predicate<ComponentFactory>() {
+            @Override
+            public boolean apply(final ComponentFactory input) {
+                return input != ignoreFactory;
+            }
+        }));
+        return ordered(otherFactories);
+    }
+
+    protected List<ComponentFactory> ordered(List<ComponentFactory> otherFactories) {
+        return otherFactories;
+    }
+
+
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/selector/links/EntityLinksSelectorPanelFactory.java
----------------------------------------------------------------------
diff --git a/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/selector/links/EntityLinksSelectorPanelFactory.java b/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/selector/links/EntityLinksSelectorPanelFactory.java
new file mode 100644
index 0000000..30b2ce3
--- /dev/null
+++ b/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/entity/selector/links/EntityLinksSelectorPanelFactory.java
@@ -0,0 +1,47 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package org.apache.isis.viewer.wicket.ui.components.entity.selector.links;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.model.IModel;
+
+import org.apache.isis.viewer.wicket.model.models.EntityModel;
+import org.apache.isis.viewer.wicket.ui.ComponentFactory;
+import org.apache.isis.viewer.wicket.ui.ComponentType;
+import org.apache.isis.viewer.wicket.ui.components.entity.EntityComponentFactoryAbstract;
+
+/**
+ * {@link ComponentFactory} for {@link EntityLinksSelectorPanel}.
+ */
+public class EntityLinksSelectorPanelFactory extends EntityComponentFactoryAbstract {
+
+    private static final long serialVersionUID = 1L;
+
+    public EntityLinksSelectorPanelFactory() {
+        super(ComponentType.ENTITY, EntityLinksSelectorPanel.class);
+    }
+
+    @Override
+    public Component createComponent(final String id, final IModel<?> model) {
+        final EntityModel entityModel = (EntityModel) model;
+        return new EntityLinksSelectorPanel(id, entityModel, this);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/footer/FooterPanel.html
----------------------------------------------------------------------
diff --git a/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/footer/FooterPanel.html b/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/footer/FooterPanel.html
new file mode 100644
index 0000000..58b746b
--- /dev/null
+++ b/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/footer/FooterPanel.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html xmlns:wicket="http://wicket.apache.org">
+    <body>
+        <wicket:panel>
+
+            <footer class="navbar navbar-default navbar-fixed-bottom" role="navigation">
+
+            <div class="container-fluid">
+                <div class="navbar-header">
+                    <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-collapse-footer">
+                        <span class="sr-only">Toggle navigation</span>
+                        <span class="icon-bar"></span>
+                        <span class="icon-bar"></span>
+                        <span class="icon-bar"></span>
+                    </button>
+                </div>
+
+                <div class="collapse navbar-collapse" id="navbar-collapse-footer">
+                    <form class="nav navbar-nav navbar-form navbar-left">
+                        <span wicket:id="breadcrumbs"></span>
+                    </form>
+
+                    <ul class="nav navbar-nav">
+                    </ul>
+                    <ul class="nav navbar-nav navbar-right">
+                        <p class="navbar-text powered-by">Powered by:</p>
+                        <li><a href="http://isis.apache.org" target="_blank">Apache Isis &#8482;</a></li>
+                        <li>
+                            <a wicket:id="aboutLink" class="navbar-right">
+                                <span wicket:id="aboutMessage">About</span>
+                                <i wicket:id="devModeWarning" class="fa fa-lg fa-warning text-danger" title="Running in Prototype (Development) mode!"/>
+                            </a>
+
+                        </li>
+                        <li wicket:id="themePicker"></li>
+                    </ul>
+                </div>
+            </div>
+
+            </footer>
+        </wicket:panel>
+    </body>
+</html>

http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/footer/FooterPanel.java
----------------------------------------------------------------------
diff --git a/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/footer/FooterPanel.java b/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/footer/FooterPanel.java
new file mode 100644
index 0000000..0287ebe
--- /dev/null
+++ b/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/footer/FooterPanel.java
@@ -0,0 +1,80 @@
+package org.apache.isis.viewer.wicket.ui.components.footer;
+
+import org.apache.isis.viewer.wicket.model.common.PageParametersUtils;
+import org.apache.isis.viewer.wicket.ui.components.widgets.breadcrumbs.BreadcrumbPanel;
+import org.apache.isis.viewer.wicket.ui.components.widgets.themepicker.ThemeChooser;
+import org.apache.isis.viewer.wicket.ui.pages.about.AboutPage;
+import org.apache.isis.viewer.wicket.ui.panels.PanelAbstract;
+import org.apache.wicket.MarkupContainer;
+import org.apache.wicket.markup.html.WebComponent;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.model.ResourceModel;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+
+/**
+ * A panel for the default page footer
+ */
+public class FooterPanel extends PanelAbstract<Model<String>> {
+
+    private static final String ID_BREADCRUMBS = "breadcrumbs";
+    private static final String ID_ABOUT_LINK = "aboutLink";
+    private static final String ID_ABOUT_MESSAGE = "aboutMessage";
+    private static final String ID_THEME_PICKER = "themePicker";
+
+    /**
+     * Constructor.
+     *
+     * @param id The component id
+     */
+    public FooterPanel(String id) {
+        super(id);
+    }
+
+    @Override
+    protected void onInitialize() {
+        super.onInitialize();
+
+        addBreadcrumbs();
+        addAboutLink();
+        addThemePicker();
+    }
+
+    private void addBreadcrumbs() {
+        final BreadcrumbPanel breadcrumbPanel = new BreadcrumbPanel(ID_BREADCRUMBS);
+        addOrReplace(breadcrumbPanel);
+    }
+
+    @Override
+    protected void onConfigure() {
+        super.onConfigure();
+
+        PageParameters parameters = getPage().getPageParameters();
+        setVisible(parameters.get(PageParametersUtils.ISIS_NO_FOOTER_PARAMETER_NAME).isNull());
+    }
+
+    private void addAboutLink() {
+        final BookmarkablePageLink<Void> aboutLink = new BookmarkablePageLink<>(ID_ABOUT_LINK, AboutPage.class);
+        add(aboutLink);
+
+        final Label aboutLabel = new Label(ID_ABOUT_MESSAGE, new ResourceModel("aboutLabel"));
+        aboutLink.add(aboutLabel);
+        addDevModeWarning(aboutLink);
+    }
+
+    /**
+     * Adds a component that shows a warning sign next to "About" link in development mode
+     * @param container The parent component
+     */
+    private void addDevModeWarning(final MarkupContainer container) {
+        final WebComponent devModeWarning = new WebComponent("devModeWarning");
+        devModeWarning.setVisible(getApplication().usesDevelopmentConfig());
+        container.add(devModeWarning);
+    }
+
+    private void addThemePicker() {
+        final ThemeChooser themeChooser = new ThemeChooser(ID_THEME_PICKER);
+        addOrReplace(themeChooser);
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/footer/FooterPanelFactory.java
----------------------------------------------------------------------
diff --git a/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/footer/FooterPanelFactory.java b/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/footer/FooterPanelFactory.java
new file mode 100644
index 0000000..902a21a
--- /dev/null
+++ b/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/footer/FooterPanelFactory.java
@@ -0,0 +1,48 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package org.apache.isis.viewer.wicket.ui.components.footer;
+
+import org.apache.isis.viewer.wicket.ui.ComponentFactoryAbstract;
+import org.apache.isis.viewer.wicket.ui.ComponentType;
+import org.apache.wicket.Component;
+import org.apache.wicket.model.IModel;
+
+/**
+ * {@link org.apache.isis.viewer.wicket.ui.ComponentFactory} to create container for the page footer.
+ */
+public class FooterPanelFactory extends ComponentFactoryAbstract {
+
+    private static final long serialVersionUID = 1L;
+
+    public FooterPanelFactory() {
+        super(ComponentType.FOOTER, FooterPanel.class);
+    }
+
+    @Override
+    public ApplicationAdvice appliesTo(final IModel<?> model) {
+        return ApplicationAdvice.APPLIES;
+    }
+
+    @Override
+    public Component createComponent(final String id, final IModel<?> model) {
+        return new FooterPanel(id);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/header/HeaderPanel.html
----------------------------------------------------------------------
diff --git a/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/header/HeaderPanel.html b/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/header/HeaderPanel.html
new file mode 100644
index 0000000..82378f5
--- /dev/null
+++ b/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/header/HeaderPanel.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html xmlns:wicket="http://wicket.apache.org">
+    <body>
+        <wicket:panel>
+            <header class="navbar navbar-default navbar-fixed-top" role="navigation">
+
+            <div class="container-fluid">
+                <div class="navbar-header">
+                    <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target=".navbar-collapse.app-actions">
+                        <span class="sr-only">Toggle navigation</span>
+                        <span class="icon-bar"></span>
+                        <span class="icon-bar"></span>
+                        <span class="icon-bar"></span>
+                    </button>
+                    <a class="navbar-brand" wicket:id="applicationName">
+                        <span wicket:id="brandText"></span>
+                        <img alt="Brand" wicket:id="brandLogo"/>
+                    </a>
+                </div>
+                <div class="navbar-header navbar-right">
+                    <ul class="nav navbar-nav tertiary">
+                        <li class="dropdown">
+                            <a href="#" class="dropdown-toggle" data-toggle="dropdown">
+                                <i class="fa fa-fw fa-user"></i> <span wicket:id="userName"></span><span class="caret"></span>
+                            </a>
+                            <ul wicket:id="tertiaryMenuBar" class="dropdown-menu" role="menu" style="min-width: 0px;">
+                            </ul>
+                        </li>
+                    </ul>
+                </div>
+                <div class="navbar-collapse app-actions collapse navbar-right">
+                    <ul class="nav navbar-nav" wicket:id="secondaryMenuBar"></ul>
+                </div>
+                <div class="navbar-collapse app-actions collapse">
+                    <ul class="nav navbar-nav" wicket:id="primaryMenuBar"></ul>
+                </div>
+            </div>
+
+            </header>
+        </wicket:panel>
+    </body>
+</html>

http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/header/HeaderPanel.java
----------------------------------------------------------------------
diff --git a/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/header/HeaderPanel.java b/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/header/HeaderPanel.java
new file mode 100644
index 0000000..17a98fb
--- /dev/null
+++ b/core/viewer-wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/header/HeaderPanel.java
@@ -0,0 +1,145 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.viewer.wicket.ui.components.header;
+
+import java.util.Locale;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+import org.apache.wicket.Component;
+import org.apache.wicket.MarkupContainer;
+import org.apache.wicket.Page;
+import org.apache.wicket.behavior.AttributeAppender;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.apache.isis.applib.annotation.DomainServiceLayout;
+import org.apache.isis.applib.services.userprof.UserProfileService;
+import org.apache.isis.viewer.wicket.model.common.PageParametersUtils;
+import org.apache.isis.viewer.wicket.model.models.ServiceActionsModel;
+import org.apache.isis.viewer.wicket.ui.ComponentType;
+import org.apache.isis.viewer.wicket.ui.components.widgets.navbar.BrandLogo;
+import org.apache.isis.viewer.wicket.ui.components.widgets.navbar.BrandName;
+import org.apache.isis.viewer.wicket.ui.components.widgets.navbar.Placement;
+import org.apache.isis.viewer.wicket.ui.pages.error.ErrorPage;
+import org.apache.isis.viewer.wicket.ui.panels.PanelAbstract;
+import org.apache.isis.viewer.wicket.ui.util.Components;
+
+/**
+ * A panel for the default page header
+ */
+public class HeaderPanel extends PanelAbstract<Model<String>> {
+
+    private static final String ID_USER_NAME = "userName";
+
+    private static final String ID_PRIMARY_MENU_BAR = "primaryMenuBar";
+    private static final String ID_SECONDARY_MENU_BAR = "secondaryMenuBar";
+    private static final String ID_TERTIARY_MENU_BAR = "tertiaryMenuBar";
+
+    @Inject
+    @Named("applicationName")
+    private String applicationName;
+
+    @Inject(optional = true)
+    @Named("brandLogo")
+    private String brandLogo;
+
+    /**
+     * Constructor.
+     *
+     * @param id The component id
+     */
+    public HeaderPanel(String id) {
+        super(id);
+    }
+
+    @Override
+    protected void onInitialize() {
+        super.onInitialize();
+
+        addApplicationName();
+        addUserName();
+        addServiceActionMenuBars();
+    }
+
+    @Override
+    protected void onConfigure() {
+        super.onConfigure();
+
+        PageParameters parameters = getPage().getPageParameters();
+        setVisible(parameters.get(PageParametersUtils.ISIS_NO_HEADER_PARAMETER_NAME).isNull());
+    }
+
+    protected void addApplicationName() {
+        Class<? extends Page> homePage = getApplication().getHomePage();
+        final BookmarkablePageLink<Void> applicationNameLink = new BookmarkablePageLink<>("applicationName", homePage);
+
+        final Placement placement = Placement.HEADER;
+        final BrandLogo brandImage = new BrandLogo("brandLogo", placement);
+        final BrandName brandName = new BrandName("brandText", Placement.HEADER);
+        applicationNameLink.add(brandName, brandImage);
+
+        add(applicationNameLink);
+    }
+
+
+    protected void addUserName() {
+        final UserProfileService userProfileService = getUserProfileService();
+        final Label userName = new Label(ID_USER_NAME, userProfileService.userProfileName());
+        add(userName);
+    }
+
+    private UserProfileService getUserProfileService() {
+        return new UserProfileService() {
+            @Override
+            public String userProfileName() {
+                if(getPage() instanceof ErrorPage) {
+                    return getAuthenticationSession().getUserName();
+                }
+                try {
+                    final UserProfileService userProfileService = lookupService(UserProfileService.class);
+                    final String userProfileName = userProfileService != null ? userProfileService.userProfileName() : null;
+                    return userProfileName != null? userProfileName: getAuthenticationSession().getUserName();
+                } catch (final Exception e) {
+                    return getAuthenticationSession().getUserName();
+                }
+            }
+        };
+    }
+
+    protected void addServiceActionMenuBars() {
+        if (getPage() instanceof ErrorPage) {
+            Components.permanentlyHide(this, ID_PRIMARY_MENU_BAR);
+            Components.permanentlyHide(this, ID_SECONDARY_MENU_BAR);
+            addMenuBar(this, ID_TERTIARY_MENU_BAR, DomainServiceLayout.MenuBar.TERTIARY);
+        } else {
+            addMenuBar(this, ID_PRIMARY_MENU_BAR, DomainServiceLayout.MenuBar.PRIMARY);
+            addMenuBar(this, ID_SECONDARY_MENU_BAR, DomainServiceLayout.MenuBar.SECONDARY);
+            addMenuBar(this, ID_TERTIARY_MENU_BAR, DomainServiceLayout.MenuBar.TERTIARY);
+        }
+    }
+
+    private void addMenuBar(final MarkupContainer container, final String id, final DomainServiceLayout.MenuBar menuBar) {
+        final ServiceActionsModel model = new ServiceActionsModel(menuBar);
+        Component menuBarComponent = getComponentFactoryRegistry().createComponent(ComponentType.SERVICE_ACTIONS, id, model);
+        menuBarComponent.add(AttributeAppender.append("class", menuBar.name().toLowerCase(Locale.ENGLISH)));
+        container.add(menuBarComponent);
+
+    }
+}