/* * 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.scalars; import java.util.List; import com.google.common.collect.Lists; import org.apache.wicket.Component; import org.apache.wicket.MarkupContainer; import org.apache.wicket.RestartResponseException; import org.apache.wicket.ajax.AjaxEventBehavior; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior; import org.apache.wicket.behavior.AttributeAppender; import org.apache.wicket.behavior.Behavior; import org.apache.wicket.feedback.ComponentFeedbackMessageFilter; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.form.LabeledWebMarkupContainer; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; import org.apache.isis.applib.annotation.ActionLayout; import org.apache.isis.applib.annotation.PromptStyle; import org.apache.isis.applib.annotation.Where; import org.apache.isis.core.metamodel.adapter.ObjectAdapter; import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager; import org.apache.isis.core.metamodel.adapter.version.ConcurrencyException; import org.apache.isis.core.metamodel.facets.members.cssclass.CssClassFacet; import org.apache.isis.core.metamodel.facets.objectvalue.labelat.LabelAtFacet; import org.apache.isis.core.runtime.system.context.IsisContext; import org.apache.isis.viewer.wicket.model.links.LinkAndLabel; 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.InlinePromptContext; 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.LinkAndLabelUtil; import org.apache.isis.viewer.wicket.ui.components.property.PropertyEditFormPanel; import org.apache.isis.viewer.wicket.ui.components.property.PropertyEditPanel; import org.apache.isis.viewer.wicket.ui.components.propertyheader.PropertyEditPromptHeaderPanel; import org.apache.isis.viewer.wicket.ui.components.scalars.isisapplib.IsisBlobOrClobPanelAbstract; import org.apache.isis.viewer.wicket.ui.components.scalars.primitive.BooleanPanel; import org.apache.isis.viewer.wicket.ui.components.scalars.reference.ReferencePanel; import org.apache.isis.viewer.wicket.ui.components.scalars.valuechoices.ValueChoicesSelect2Panel; import org.apache.isis.viewer.wicket.ui.pages.entity.EntityPage; 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 de.agilecoders.wicket.core.markup.html.bootstrap.common.NotificationPanel; public abstract class ScalarPanelAbstract2 extends PanelAbstract<ScalarModel> implements ScalarModelSubscriber2 { private static final long serialVersionUID = 1L; protected static final String ID_SCALAR_TYPE_CONTAINER = "scalarTypeContainer"; protected static final String ID_SCALAR_IF_COMPACT = "scalarIfCompact"; protected static final String ID_SCALAR_IF_REGULAR = "scalarIfRegular"; protected static final String ID_SCALAR_NAME = "scalarName"; protected static final String ID_SCALAR_VALUE = "scalarValue"; /** * as per {@link #inlinePromptLink} */ protected static final String ID_SCALAR_VALUE_INLINE_PROMPT_LINK = "scalarValueInlinePromptLink"; protected static final String ID_SCALAR_VALUE_INLINE_PROMPT_LABEL = "scalarValueInlinePromptLabel"; /** * as per {@link #scalarIfRegularInlinePromptForm}. */ public static final String ID_SCALAR_IF_REGULAR_INLINE_PROMPT_FORM = "scalarIfRegularInlinePromptForm"; private static final String ID_EDIT_PROPERTY = "editProperty"; private static final String ID_FEEDBACK = "feedback"; private static final String ID_ASSOCIATED_ACTION_LINKS_BELOW = "associatedActionLinksBelow"; private static final String ID_ASSOCIATED_ACTION_LINKS_RIGHT = "associatedActionLinksRight"; public static class InlinePromptConfig { private final boolean supported; private final Component componentToHideIfAny; public static InlinePromptConfig supported() { return new InlinePromptConfig(true, null); } public static InlinePromptConfig notSupported() { return new InlinePromptConfig(false, null); } public static InlinePromptConfig supportedAndHide(final Component componentToHideIfAny) { return new InlinePromptConfig(true, componentToHideIfAny); } private InlinePromptConfig(final boolean supported, final Component componentToHideIfAny) { this.supported = supported; this.componentToHideIfAny = componentToHideIfAny; } boolean isSupported() { return supported; } Component getComponentToHideIfAny() { return componentToHideIfAny; } } // /////////////////////////////////////////////////////////////////// protected final ScalarModel scalarModel; private Component scalarIfCompact; private MarkupContainer scalarIfRegular; private WebMarkupContainer scalarTypeContainer; /** * Populated * Used by most subclasses ({@link ScalarPanelAbstract2}, {@link ReferencePanel}, {@link ValueChoicesSelect2Panel}) but not all ({@link IsisBlobOrClobPanelAbstract}, {@link BooleanPanel}) */ private WebMarkupContainer scalarIfRegularInlinePromptForm; WebMarkupContainer inlinePromptLink; public ScalarPanelAbstract2(final String id, final ScalarModel scalarModel) { super(id, scalarModel); this.scalarModel = scalarModel; } // /////////////////////////////////////////////////////////////////// @Override protected void onInitialize() { super.onInitialize(); buildGuiAndCallHooks(); setOutputMarkupId(true); } private void buildGuiAndCallHooks() { try { buildGui(); } catch (ConcurrencyException ex) { // // this has to be here because it's the first method called when editing a property // on a potentially stale model. // // there is similar code for invoking actions (ActionLink) // IsisContext.getSessionFactory().getCurrentSession().getAuthenticationSession().getMessageBroker().addMessage(ex.getMessage()); final ObjectAdapter parentAdapter = getModel().getParentEntityModel().load(); throw new RestartResponseException(new EntityPage(parentAdapter)); } final ScalarModel scalarModel = getModel(); final String disableReasonIfAny = scalarModel.disable(getRendering().getWhere()); if (scalarModel.isViewMode()) { onInitializeWhenViewMode(); } else { if (disableReasonIfAny != null) { onInitializeWhenDisabled(disableReasonIfAny); } else { onInitializeWhenEnabled(); } } } /** * Mandatory hook; simply determines the CSS that is added to the outermost 'scalarTypeContainer' div. */ protected abstract String getScalarPanelType(); /** * Mandatory hook for implementations to indicate whether it supports the {@link PromptStyle#INLINE} inline prompt, * and if so, how. * * <p> * For those that do, both {@link #createInlinePromptForm()} and * {@link #createInlinePromptLink()} must return non-null values (and their corresponding markup * must define the corresponding elements). * </p> * * <p> * Implementations that support inline prompts are: ({@link ScalarPanelAbstract2}, {@link ReferencePanel} and * {@link ValueChoicesSelect2Panel}; those that don't are {@link IsisBlobOrClobPanelAbstract} and {@link BooleanPanel}. * </p> */ protected abstract InlinePromptConfig getInlinePromptConfig(); /** * Builds GUI lazily prior to first render. * * <p> * This design allows the panel to be configured first. * * @see #onBeforeRender() */ private void buildGui() { scalarTypeContainer = new WebMarkupContainer(ID_SCALAR_TYPE_CONTAINER); scalarTypeContainer.setOutputMarkupId(true); scalarTypeContainer.add(new CssClassAppender(Model.of(getScalarPanelType()))); addOrReplace(scalarTypeContainer); this.scalarIfCompact = createComponentForCompact(); this.scalarIfRegular = createComponentForRegular(); scalarIfRegular.setOutputMarkupId(true); scalarTypeContainer.addOrReplace(scalarIfCompact, scalarIfRegular); final InlinePromptConfig inlinePromptConfig = getInlinePromptConfig(); if(inlinePromptConfig.isSupported()) { this.scalarIfRegularInlinePromptForm = createInlinePromptForm(); scalarTypeContainer.addOrReplace(scalarIfRegularInlinePromptForm); inlinePromptLink = createInlinePromptLink(); scalarIfRegular.add(inlinePromptLink); // even if this particular scalarModel (property) is not configured for inline edits, it's possible that // one of the associated actions is. Thus we set the prompt context scalarModel.setInlinePromptContext( new InlinePromptContext( getComponentForRegular(), scalarIfRegularInlinePromptForm, scalarTypeContainer)); // and we configure the prompt link if _this_ property is configured for inline edits... final PromptStyle promptStyle = this.scalarModel.getPromptStyle(); if(promptStyle == PromptStyle.INLINE) { configureInlinePromptLinkCallback(inlinePromptLink); } Component componentToHideIfAny = null; if (scalarModel.canEnterEditMode() && promptStyle == PromptStyle.INLINE) { componentToHideIfAny = inlinePromptConfig.getComponentToHideIfAny(); } else { componentToHideIfAny = inlinePromptLink; } if(componentToHideIfAny != null) { componentToHideIfAny.setVisibilityAllowed(false); } } if(scalarModel.getKind() == ScalarModel.Kind.PROPERTY && scalarModel.getMode() == EntityModel.Mode.VIEW && (scalarModel.getPromptStyle() != PromptStyle.INLINE || !scalarModel.canEnterEditMode())) { getScalarValueComponent().add(new AttributeAppender("tabindex", "-1")); } final List<LinkAndLabel> actionLinks = LinkAndLabelUtil.asActionLinksForAssociation(this.scalarModel, getDeploymentCategory()); addPositioningCssTo(scalarIfRegular, actionLinks); addActionLinksBelowAndRight(scalarIfRegular, actionLinks); addEditPropertyTo(scalarIfRegular); addFeedbackOnlyTo(scalarIfRegular, getScalarValueComponent()); getRendering().buildGui(this); addCssFromMetaModel(); notifyOnChange(this); addFormComponentBehaviourToUpdateSubscribers(); } /** * Optional hook. */ protected void onInitializeWhenViewMode() { } /** * Optional hook. */ protected void onInitializeWhenDisabled(final String disableReason) { } /** * Optional hook. */ protected void onInitializeWhenEnabled() { } private void addCssFromMetaModel() { final String cssForMetaModel = getModel().getLongName(); if (cssForMetaModel != null) { add(new AttributeAppender("class", Model.of(cssForMetaModel), " ")); } ScalarModel model = getModel(); final CssClassFacet facet = model.getFacet(CssClassFacet.class); if(facet != null) { final ObjectAdapter parentAdapter = model.getParentEntityModel().load(AdapterManager.ConcurrencyChecking.NO_CHECK); final String cssClass = facet.cssClass(parentAdapter); CssClassAppender.appendCssClassTo(this, cssClass); } } // ////////////////////////////////////// static class ScalarUpdatingBehavior extends AjaxFormComponentUpdatingBehavior { private static final long serialVersionUID = 1L; private final ScalarPanelAbstract2 scalarPanel; private ScalarUpdatingBehavior(final ScalarPanelAbstract2 scalarPanel) { super("change"); this.scalarPanel = scalarPanel; } @Override protected void onUpdate(AjaxRequestTarget target) { for (ScalarModelSubscriber2 subscriber : scalarPanel.subscribers) { subscriber.onUpdate(target, scalarPanel); } } @Override protected void onError(AjaxRequestTarget target, RuntimeException e) { super.onError(target, e); for (ScalarModelSubscriber2 subscriber : scalarPanel.subscribers) { subscriber.onError(target, scalarPanel); } } } private final List<ScalarModelSubscriber2> subscribers = Lists.newArrayList(); public void notifyOnChange(final ScalarModelSubscriber2 subscriber) { subscribers.add(subscriber); } private void addFormComponentBehaviourToUpdateSubscribers() { Component scalarValueComponent = getScalarValueComponent(); if(scalarValueComponent == null) { return; } for (Behavior b : scalarValueComponent.getBehaviors(ScalarUpdatingBehavior.class)) { scalarValueComponent.remove(b); } scalarValueComponent.add(new ScalarUpdatingBehavior(this)); } // ////////////////////////////////////// @Override public void onUpdate( final AjaxRequestTarget target, final ScalarPanelAbstract2 scalarPanel) { if(getModel().getKind() == ScalarModel.Kind.PARAMETER) { target.appendJavaScript( String.format("Wicket.Event.publish(Isis.Topic.FOCUS_FIRST_PARAMETER, '%s')", getMarkupId())); } } @Override public void onError( final AjaxRequestTarget target, final ScalarPanelAbstract2 scalarPanel) { } // /////////////////////////////////////////////////////////////////// public enum Rendering { /** * Does not show labels, eg for use in tables */ COMPACT { @Override public String getLabelCaption(final LabeledWebMarkupContainer labeledContainer) { return ""; } @Override public void buildGui(final ScalarPanelAbstract2 panel) { panel.getComponentForRegular().setVisible(false); } @Override public Where getWhere() { return Where.PARENTED_TABLES; } }, /** * Does show labels, eg for use in forms. */ REGULAR { @Override public String getLabelCaption(final LabeledWebMarkupContainer labeledContainer) { return labeledContainer.getLabel().getObject(); } @Override public void buildGui(final ScalarPanelAbstract2 panel) { panel.scalarIfCompact.setVisible(false); } @Override public Where getWhere() { return Where.OBJECT_FORMS; } }; public abstract String getLabelCaption(LabeledWebMarkupContainer labeledContainer); public abstract void buildGui(ScalarPanelAbstract2 panel); public abstract Where getWhere(); private static Rendering renderingFor(EntityModel.RenderingHint renderingHint) { return renderingHint.isInTable()? Rendering.COMPACT: Rendering.REGULAR; } } protected Rendering getRendering() { return Rendering.renderingFor(scalarModel.getRenderingHint()); } // /////////////////////////////////////////////////////////////////// protected Component getComponentForRegular() { return scalarIfRegular; } /** * Mandatory hook method to build the component to render the model when in * {@link Rendering#REGULAR regular} format. * * <p> * Is added to {@link #scalarTypeContainer}. * </p> */ protected abstract MarkupContainer createComponentForRegular(); /** * Mandatory hook method to build the component to render the model when in * {@link Rendering#COMPACT compact} format. * * <p> * Is added to {@link #scalarTypeContainer}. * </p> */ protected abstract Component createComponentForCompact(); /** * Returns a container holding an empty form. This can be switched out using {@link #switchFormForInlinePrompt(AjaxRequestTarget)}. */ private WebMarkupContainer createInlinePromptForm() { // (placeholder initially, create dynamically when needed - otherwise infinite loop because form references regular) WebMarkupContainer scalarIfRegularInlinePromptForm = new WebMarkupContainer( ID_SCALAR_IF_REGULAR_INLINE_PROMPT_FORM); scalarIfRegularInlinePromptForm.setOutputMarkupId(true); scalarIfRegularInlinePromptForm.setVisible(false); return scalarIfRegularInlinePromptForm; } private WebMarkupContainer createInlinePromptLink() { final IModel<String> inlinePromptModel = obtainInlinePromptModel(); if(inlinePromptModel == null) { throw new IllegalStateException(this.getClass().getName() + ": obtainInlinePromptModel() returning null is not compatible with supportsInlinePrompt() returning true "); } final WebMarkupContainer inlinePromptLink = new WebMarkupContainer(ID_SCALAR_VALUE_INLINE_PROMPT_LINK); inlinePromptLink.setOutputMarkupId(true); configureInlinePromptLink(inlinePromptLink); final Component editInlineLinkLabel = createInlinePromptComponent(ID_SCALAR_VALUE_INLINE_PROMPT_LABEL, inlinePromptModel ); inlinePromptLink.add(editInlineLinkLabel); return inlinePromptLink; } protected void configureInlinePromptLink(final WebMarkupContainer inlinePromptLink) { final String append = obtainInlinePromptLinkCssIfAny(); if(append != null) { inlinePromptLink.add(new CssClassAppender(append)); } } protected String obtainInlinePromptLinkCssIfAny() { return "form-control input-sm"; } protected Component createInlinePromptComponent( final String id, final IModel<String> inlinePromptModel) { return new Label(id, inlinePromptModel); } // /////////////////////////////////////////////////////////////////// /** * Components returning true for {@link #getInlinePromptConfig()} are required to override and return a non-null value. */ protected IModel<String> obtainInlinePromptModel() { return null; } private void configureInlinePromptLinkCallback(final WebMarkupContainer inlinePromptLink) { inlinePromptLink.add(new AjaxEventBehavior("click") { @Override protected void onEvent(final AjaxRequestTarget target) { scalarModel.toEditMode(); switchFormForInlinePrompt(target); getComponentForRegular().setVisible(false); scalarIfRegularInlinePromptForm.setVisible(true); target.add(scalarTypeContainer); } @Override public boolean isEnabled(final Component component) { return true; } }); } private void switchFormForInlinePrompt(final AjaxRequestTarget target) { scalarIfRegularInlinePromptForm = (PropertyEditFormPanel) getComponentFactoryRegistry().addOrReplaceComponent( scalarTypeContainer, ID_SCALAR_IF_REGULAR_INLINE_PROMPT_FORM, ComponentType.PROPERTY_EDIT_FORM, scalarModel); onSwitchFormForInlinePrompt(scalarIfRegularInlinePromptForm, target); } /** * Optional hook. */ protected void onSwitchFormForInlinePrompt( final WebMarkupContainer inlinePromptForm, final AjaxRequestTarget target) { } // /////////////////////////////////////////////////////////////////// protected void addEditPropertyTo( final MarkupContainer scalarIfRegularFormGroup) { if(scalarModel.canEnterEditMode() && (scalarModel.getPromptStyle() == PromptStyle.DIALOG || !getInlinePromptConfig().isSupported())) { final WebMarkupContainer editProperty = new WebMarkupContainer(ID_EDIT_PROPERTY); editProperty.setOutputMarkupId(true); scalarIfRegularFormGroup.addOrReplace(editProperty); editProperty.add(new AjaxEventBehavior("click") { protected void onEvent(AjaxRequestTarget target) { final ActionPrompt prompt = ActionPromptProvider.Util .getFrom(ScalarPanelAbstract2.this).getActionPrompt(); PropertyEditPromptHeaderPanel titlePanel = new PropertyEditPromptHeaderPanel(prompt.getTitleId(), ScalarPanelAbstract2.this.scalarModel); final PropertyEditPanel propertyEditPanel = (PropertyEditPanel) getComponentFactoryRegistry().createComponent( ComponentType.PROPERTY_EDIT_PROMPT, prompt.getContentId(), ScalarPanelAbstract2.this.scalarModel); propertyEditPanel.setShowHeader(false); prompt.setTitle(titlePanel, target); prompt.setPanel(propertyEditPanel, target); prompt.showPrompt(target); } }); } else { Components.permanentlyHide(scalarIfRegularFormGroup, ID_EDIT_PROPERTY); } } /** * Mandatory hook, used to determine which component to attach feedback to. * @return */ protected abstract Component getScalarValueComponent(); private void addFeedbackOnlyTo(final MarkupContainer markupContainer, final Component component) { markupContainer.addOrReplace(new NotificationPanel(ID_FEEDBACK, component, new ComponentFeedbackMessageFilter(component))); } private void addActionLinksBelowAndRight( final MarkupContainer labelIfRegular, final List<LinkAndLabel> linkAndLabels) { final List<LinkAndLabel> linksBelow = LinkAndLabel.positioned(linkAndLabels, ActionLayout.Position.BELOW); AdditionalLinksPanel.addAdditionalLinks(labelIfRegular, ID_ASSOCIATED_ACTION_LINKS_BELOW, linksBelow, AdditionalLinksPanel.Style.INLINE_LIST); final List<LinkAndLabel> linksRight = LinkAndLabel.positioned(linkAndLabels, ActionLayout.Position.RIGHT); AdditionalLinksPanel.addAdditionalLinks(labelIfRegular, ID_ASSOCIATED_ACTION_LINKS_RIGHT, linksRight, AdditionalLinksPanel.Style.DROPDOWN); } /** * Applies the {@literal @}{@link LabelAtFacet} and also CSS based on * whether any of the associated actions have {@literal @}{@link ActionLayout layout} positioned to * the {@link ActionLayout.Position#RIGHT right}. * * @param markupContainer The form group element * @param entityActionLinks */ private void addPositioningCssTo(final MarkupContainer markupContainer, final List<LinkAndLabel> entityActionLinks) { CssClassAppender.appendCssClassTo(markupContainer, determinePropParamLayoutCss(getModel())); CssClassAppender.appendCssClassTo(markupContainer, determineActionLayoutPositioningCss(entityActionLinks)); } private static String determinePropParamLayoutCss(ScalarModel model) { final LabelAtFacet facet = model.getFacet(LabelAtFacet.class); if (facet != null) { switch (facet.label()) { case LEFT: return "label-left"; case RIGHT: return "label-right"; case NONE: return "label-none"; case TOP: return "label-top"; } } return "label-left"; } private static String determineActionLayoutPositioningCss(List<LinkAndLabel> entityActionLinks) { boolean actionsPositionedOnRight = hasActionsPositionedOn(entityActionLinks, ActionLayout.Position.RIGHT); return actionsPositionedOnRight ? "actions-right" : null; } private static boolean hasActionsPositionedOn(final List<LinkAndLabel> entityActionLinks, final ActionLayout.Position position) { for (LinkAndLabel entityActionLink : entityActionLinks) { if(entityActionLink.getPosition() == position) { return true; } } return false; } // /////////////////////////////////////////////////////////////////// /** * Repaints this panel of just some of its children * * @param target The Ajax request handler */ public void repaint(AjaxRequestTarget target) { target.add(this); } }