diff options
14 files changed, 2308 insertions, 24 deletions
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/ui/VForm.java b/compatibility-client/src/main/java/com/vaadin/v7/client/ui/VForm.java new file mode 100644 index 0000000000..b5adc5cd8f --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/v7/client/ui/VForm.java @@ -0,0 +1,141 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed 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 com.vaadin.v7.client.ui; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.ComplexPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.VErrorMessage; +import com.vaadin.client.ui.Icon; +import com.vaadin.client.ui.ShortcutActionHandler; + +public class VForm extends ComplexPanel implements KeyDownHandler { + + public static final String CLASSNAME = "v-form"; + + /** For internal use only. May be removed or replaced in the future. */ + public String id; + + /** For internal use only. May be removed or replaced in the future. */ + public Widget lo; + + /** For internal use only. May be removed or replaced in the future. */ + public Element legend = DOM.createLegend(); + + /** For internal use only. May be removed or replaced in the future. */ + public Element caption = DOM.createSpan(); + + /** For internal use only. May be removed or replaced in the future. */ + public Element desc = DOM.createDiv(); + + /** For internal use only. May be removed or replaced in the future. */ + public Icon icon; + + /** For internal use only. May be removed or replaced in the future. */ + public VErrorMessage errorMessage = new VErrorMessage(); + + /** For internal use only. May be removed or replaced in the future. */ + public Element fieldContainer = DOM.createDiv(); + + /** For internal use only. May be removed or replaced in the future. */ + public Element footerContainer = DOM.createDiv(); + + /** For internal use only. May be removed or replaced in the future. */ + public Element fieldSet = DOM.createFieldSet(); + + /** For internal use only. May be removed or replaced in the future. */ + public Widget footer; + + /** For internal use only. May be removed or replaced in the future. */ + public ApplicationConnection client; + + /** For internal use only. May be removed or replaced in the future. */ + public ShortcutActionHandler shortcutHandler; + + /** For internal use only. May be removed or replaced in the future. */ + public HandlerRegistration keyDownRegistration; + + public VForm() { + setElement(DOM.createDiv()); + getElement().appendChild(fieldSet); + setStyleName(CLASSNAME); + fieldSet.appendChild(legend); + legend.appendChild(caption); + + fieldSet.appendChild(desc); // Adding description for initial padding + // measurements, removed later if no + // description is set + + fieldSet.appendChild(fieldContainer); + errorMessage.setVisible(false); + + fieldSet.appendChild(errorMessage.getElement()); + fieldSet.appendChild(footerContainer); + + errorMessage.setOwner(this); + } + + @Override + public void setStyleName(String style) { + super.setStyleName(style); + updateStyleNames(); + } + + @Override + public void setStylePrimaryName(String style) { + super.setStylePrimaryName(style); + updateStyleNames(); + } + + protected void updateStyleNames() { + fieldContainer.setClassName(getStylePrimaryName() + "-content"); + errorMessage.setStyleName(getStylePrimaryName() + "-errormessage"); + desc.setClassName(getStylePrimaryName() + "-description"); + footerContainer.setClassName(getStylePrimaryName() + "-footer"); + } + + @Override + public void onKeyDown(KeyDownEvent event) { + shortcutHandler.handleKeyboardEvent(Event.as(event.getNativeEvent())); + } + + public void setFooterWidget(Widget footerWidget) { + if (footer != null) { + remove(footer); + } + if (footerWidget != null) { + super.add(footerWidget, footerContainer); + } + footer = footerWidget; + } + + public void setLayoutWidget(Widget newLayoutWidget) { + if (lo != null) { + remove(lo); + } + if (newLayoutWidget != null) { + super.add(newLayoutWidget, fieldContainer); + } + lo = newLayoutWidget; + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/ui/form/FormConnector.java b/compatibility-client/src/main/java/com/vaadin/v7/client/ui/form/FormConnector.java new file mode 100644 index 0000000000..e3f04aaa59 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/v7/client/ui/form/FormConnector.java @@ -0,0 +1,236 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed 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 com.vaadin.v7.client.ui.form; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ComponentConnector; +import com.vaadin.client.ConnectorHierarchyChangeEvent; +import com.vaadin.client.LayoutManager; +import com.vaadin.client.Paintable; +import com.vaadin.client.TooltipInfo; +import com.vaadin.client.UIDL; +import com.vaadin.client.VCaption; +import com.vaadin.client.ui.AbstractComponentContainerConnector; +import com.vaadin.client.ui.ShortcutActionHandler; +import com.vaadin.client.ui.layout.ElementResizeEvent; +import com.vaadin.client.ui.layout.ElementResizeListener; +import com.vaadin.client.ui.layout.MayScrollChildren; +import com.vaadin.shared.ui.ComponentStateUtil; +import com.vaadin.shared.ui.Connect; +import com.vaadin.v7.client.ui.VForm; +import com.vaadin.v7.shared.form.FormState; +import com.vaadin.v7.ui.Form; + +@Connect(Form.class) +public class FormConnector extends AbstractComponentContainerConnector + implements Paintable, MayScrollChildren { + + private final ElementResizeListener footerResizeListener = new ElementResizeListener() { + @Override + public void onElementResize(ElementResizeEvent e) { + com.vaadin.v7.client.ui.VForm form = getWidget(); + + LayoutManager lm = getLayoutManager(); + int footerHeight = 0; + if (form.footer != null) { + footerHeight += lm.getOuterHeight(form.footer.getElement()); + } + + if (form.errorMessage.isVisible()) { + footerHeight += lm + .getOuterHeight(form.errorMessage.getElement()); + footerHeight -= lm.getMarginTop(form.errorMessage.getElement()); + form.errorMessage.getElement().getStyle() + .setMarginTop(-footerHeight, Unit.PX); + form.footerContainer.getStyle().clearMarginTop(); + } else { + form.footerContainer.getStyle().setMarginTop(-footerHeight, + Unit.PX); + } + + form.fieldContainer.getStyle().setPaddingBottom(footerHeight, + Unit.PX); + } + }; + + @Override + protected void init() { + getLayoutManager().addElementResizeListener( + getWidget().errorMessage.getElement(), footerResizeListener); + } + + @Override + public void onUnregister() { + VForm form = getWidget(); + getLayoutManager().removeElementResizeListener( + form.errorMessage.getElement(), footerResizeListener); + if (form.footer != null) { + getLayoutManager().removeElementResizeListener( + form.footer.getElement(), footerResizeListener); + } + } + + @Override + public boolean delegateCaptionHandling() { + return false; + } + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + getWidget().client = client; + getWidget().id = uidl.getId(); + + if (!isRealUpdate(uidl)) { + return; + } + + boolean legendEmpty = true; + if (getState().caption != null) { + VCaption.setCaptionText(getWidget().caption, getState()); + legendEmpty = false; + } else { + getWidget().caption.setInnerText(""); + } + if (getWidget().icon != null) { + getWidget().legend.removeChild(getWidget().icon.getElement()); + } + if (getIconUri() != null) { + getWidget().icon = client.getIcon(getIconUri()); + getWidget().legend.insertFirst(getWidget().icon.getElement()); + + legendEmpty = false; + } + if (legendEmpty) { + getWidget().addStyleDependentName("nocaption"); + } else { + getWidget().removeStyleDependentName("nocaption"); + } + + if (null != getState().errorMessage) { + getWidget().errorMessage.updateMessage(getState().errorMessage); + getWidget().errorMessage.setVisible(true); + } else { + getWidget().errorMessage.setVisible(false); + } + + if (ComponentStateUtil.hasDescription(getState())) { + getWidget().desc.setInnerHTML(getState().description); + if (getWidget().desc.getParentElement() == null) { + getWidget().fieldSet.insertAfter(getWidget().desc, + getWidget().legend); + } + } else { + getWidget().desc.setInnerHTML(""); + if (getWidget().desc.getParentElement() != null) { + getWidget().fieldSet.removeChild(getWidget().desc); + } + } + + // also recalculates size of the footer if undefined size form - see + // #3710 + client.runDescendentsLayout(getWidget()); + + // We may have actions attached + if (uidl.getChildCount() >= 1) { + UIDL childUidl = uidl.getChildByTagName("actions"); + if (childUidl != null) { + if (getWidget().shortcutHandler == null) { + getWidget().shortcutHandler = new ShortcutActionHandler( + getConnectorId(), client); + getWidget().keyDownRegistration = getWidget() + .addDomHandler(getWidget(), KeyDownEvent.getType()); + } + getWidget().shortcutHandler.updateActionMap(childUidl); + } + } else if (getWidget().shortcutHandler != null) { + getWidget().keyDownRegistration.removeHandler(); + getWidget().shortcutHandler = null; + getWidget().keyDownRegistration = null; + } + } + + @Override + public void updateCaption(ComponentConnector component) { + // NOP form don't render caption for neither field layout nor footer + // layout + } + + @Override + public VForm getWidget() { + return (VForm) super.getWidget(); + } + + @Override + public boolean isReadOnly() { + return super.isReadOnly() || getState().propertyReadOnly; + } + + @Override + public FormState getState() { + return (FormState) super.getState(); + } + + private ComponentConnector getFooter() { + return (ComponentConnector) getState().footer; + } + + private ComponentConnector getLayout() { + return (ComponentConnector) getState().layout; + } + + @Override + public void onConnectorHierarchyChange( + ConnectorHierarchyChangeEvent connectorHierarchyChangeEvent) { + Widget newFooterWidget = null; + ComponentConnector footer = getFooter(); + + if (footer != null) { + newFooterWidget = footer.getWidget(); + Widget currentFooter = getWidget().footer; + if (currentFooter != null) { + // Remove old listener + getLayoutManager().removeElementResizeListener( + currentFooter.getElement(), footerResizeListener); + } + getLayoutManager().addElementResizeListener( + newFooterWidget.getElement(), footerResizeListener); + } + getWidget().setFooterWidget(newFooterWidget); + + Widget newLayoutWidget = null; + ComponentConnector newLayout = getLayout(); + if (newLayout != null) { + newLayoutWidget = newLayout.getWidget(); + } + getWidget().setLayoutWidget(newLayoutWidget); + } + + @Override + public TooltipInfo getTooltipInfo(Element element) { + // Form shows its description and error message + // as a part of the actual layout + return null; + } + + @Override + public boolean hasTooltip() { + return false; + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/DateField.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/DateField.java index b1e314d4de..14318cea8d 100644 --- a/compatibility-server/src/main/java/com/vaadin/v7/ui/DateField.java +++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/DateField.java @@ -18,6 +18,7 @@ package com.vaadin.v7.ui; import java.text.SimpleDateFormat; import java.util.Calendar; +import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.Locale; @@ -71,7 +72,7 @@ public class DateField extends AbstractField<Date> implements /** * Resolution identifier: seconds. - * + * * @deprecated As of 7.0, use {@link Resolution#SECOND} */ @Deprecated @@ -600,6 +601,16 @@ public class DateField extends AbstractField<Date> implements fireValueChange(false); } + /* + * Because of our custom implementation of isValid(), that + * also checks the parsingSucceeded flag, we must also + * notify the form (if this is used in one) that the + * validity of this field has changed. + * + * Normally fields validity doesn't change without value + * change and form depends on this implementation detail. + */ + notifyFormOfValidityChange(); markAsDirty(); } } else if (newDate != oldDate @@ -693,6 +704,38 @@ public class DateField extends AbstractField<Date> implements super.setValue(newValue, repaintIsNotNeeded); } + /** + * Detects if this field is used in a Form (logically) and if so, notifies + * it (by repainting it) that the validity of this field might have changed. + */ + private void notifyFormOfValidityChange() { + Component parenOfDateField = getParent(); + boolean formFound = false; + while (parenOfDateField != null || formFound) { + if (parenOfDateField instanceof Form) { + Form f = (Form) parenOfDateField; + Collection<?> visibleItemProperties = f.getItemPropertyIds(); + for (Object fieldId : visibleItemProperties) { + Field<?> field = f.getField(fieldId); + if (equals(field)) { + /* + * this datefield is logically in a form. Do the same + * thing as form does in its value change listener that + * it registers to all fields. + */ + f.markAsDirty(); + formFound = true; + break; + } + } + } + if (formFound) { + break; + } + parenOfDateField = parenOfDateField.getParent(); + } + } + @Override protected void setInternalValue(Date newValue) { // Also set the internal dateString diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/DefaultFieldFactory.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/DefaultFieldFactory.java index 53035ba087..da6441f48f 100644 --- a/compatibility-server/src/main/java/com/vaadin/v7/ui/DefaultFieldFactory.java +++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/DefaultFieldFactory.java @@ -1,12 +1,12 @@ /* * Copyright 2000-2016 Vaadin Ltd. - * + * * Licensed 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 @@ -15,31 +15,32 @@ */ package com.vaadin.v7.ui; -import java.text.Normalizer.Form; import java.util.Date; import com.vaadin.shared.util.SharedUtil; import com.vaadin.ui.Component; import com.vaadin.v7.data.Container; +import com.vaadin.v7.data.Item; import com.vaadin.v7.data.Property; /** - * This class contains a basic implementation for {@link TableFieldFactory}. The - * class is singleton, use {@link #get()} method to get reference to the - * instance. - * + * This class contains a basic implementation for both {@link FormFieldFactory} + * and {@link TableFieldFactory}. The class is singleton, use {@link #get()} + * method to get reference to the instance. + * * <p> * There are also some static helper methods available for custom built field * factories. - * + * */ -public class DefaultFieldFactory implements TableFieldFactory { +public class DefaultFieldFactory + implements FormFieldFactory, TableFieldFactory { private static final DefaultFieldFactory instance = new DefaultFieldFactory(); /** * Singleton method to get an instance of DefaultFieldFactory. - * + * * @return an instance of DefaultFieldFactory */ public static DefaultFieldFactory get() { @@ -50,6 +51,15 @@ public class DefaultFieldFactory implements TableFieldFactory { } @Override + public Field<?> createField(Item item, Object propertyId, + Component uiContext) { + Class<?> type = item.getItemProperty(propertyId).getType(); + Field<?> field = createFieldByPropertyType(type); + field.setCaption(createCaptionByPropertyId(propertyId)); + return field; + } + + @Override public Field createField(Container container, Object itemId, Object propertyId, Component uiContext) { Property containerProperty = container.getContainerProperty(itemId, @@ -63,7 +73,7 @@ public class DefaultFieldFactory implements TableFieldFactory { /** * If name follows method naming conventions, convert the name to spaced * upper case text. For example, convert "firstName" to "First Name" - * + * * @param propertyId * @return the formatted caption string */ @@ -74,18 +84,18 @@ public class DefaultFieldFactory implements TableFieldFactory { /** * Creates fields based on the property type. * <p> - * The default field type is {@link TextField}. Other field types generated - * by this method: + * The default field type is {@link LegacyTextField}. Other field types + * generated by this method: * <p> * <b>Boolean</b>: {@link CheckBox}.<br/> - * <b>Date</b>: {@link DateField}(resolution: day).<br/> + * <b>Date</b>: {@link LegacyDateField}(resolution: day).<br/> * <b>Item</b>: {@link Form}. <br/> - * <b>default field type</b>: {@link TextField}. + * <b>default field type</b>: {@link LegacyTextField}. * <p> - * + * * @param type * the type of the property - * @return the most suitable generic {@link Field} for given type + * @return the most suitable generic {@link LegacyField} for given type */ public static Field<?> createFieldByPropertyType(Class<?> type) { // Null typed properties can not be edited @@ -93,6 +103,11 @@ public class DefaultFieldFactory implements TableFieldFactory { return null; } + // Item field + if (com.vaadin.v7.data.Item.class.isAssignableFrom(type)) { + return new Form(); + } + // Date field if (Date.class.isAssignableFrom(type)) { final DateField df = new DateField(); diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/Form.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/Form.java new file mode 100644 index 0000000000..e9bdf8771e --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/Form.java @@ -0,0 +1,1421 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed 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 com.vaadin.v7.ui; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; + +import com.vaadin.event.Action; +import com.vaadin.event.Action.Handler; +import com.vaadin.event.Action.ShortcutNotifier; +import com.vaadin.event.ActionManager; +import com.vaadin.server.AbstractErrorMessage; +import com.vaadin.server.CompositeErrorMessage; +import com.vaadin.server.ErrorMessage; +import com.vaadin.server.PaintException; +import com.vaadin.server.PaintTarget; +import com.vaadin.server.UserError; +import com.vaadin.ui.AbstractComponent; +import com.vaadin.ui.Component; +import com.vaadin.ui.ComponentContainer; +import com.vaadin.ui.CustomLayout; +import com.vaadin.ui.FormLayout; +import com.vaadin.ui.GridLayout; +import com.vaadin.ui.HasComponents; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Layout; +import com.vaadin.ui.LegacyComponent; +import com.vaadin.v7.data.Buffered; +import com.vaadin.v7.data.Item; +import com.vaadin.v7.data.Property; +import com.vaadin.v7.data.Validatable; +import com.vaadin.v7.data.Validator; +import com.vaadin.v7.data.fieldgroup.FieldGroup; +import com.vaadin.v7.data.util.BeanItem; +import com.vaadin.v7.shared.form.FormState; + +/** + * Form component provides easy way of creating and managing sets fields. + * + * <p> + * <code>Form</code> is a container for fields implementing {@link Field} + * interface. It provides support for any layouts and provides buffering + * interface for easy connection of commit and discard buttons. All the form + * fields can be customized by adding validators, setting captions and icons, + * setting immediateness, etc. Also direct mechanism for replacing existing + * fields with selections is given. + * </p> + * + * <p> + * <code>Form</code> provides customizable editor for classes implementing + * {@link com.vaadin.data.Item} interface. Also the form itself implements this + * interface for easier connectivity to other items. To use the form as editor + * for an item, just connect the item to form with + * {@link Form#setItemDataSource(Item)}. If only a part of the item needs to be + * edited, {@link Form#setItemDataSource(Item,Collection)} can be used instead. + * After the item has been connected to the form, the automatically created + * fields can be customized and new fields can be added. If you need to connect + * a class that does not implement {@link com.vaadin.data.Item} interface, most + * properties of any class following bean pattern, can be accessed trough + * {@link com.vaadin.data.util.BeanItem}. + * </p> + * + * @author Vaadin Ltd. + * @since 3.0 + * @deprecated As of 7.0, use {@link FieldGroup} instead of {@link Form} for + * more flexibility. + */ +@Deprecated +public class Form extends AbstractField<Object> + implements Item.Editor, com.vaadin.v7.data.Buffered, Item, Validatable, + Action.Notifier, HasComponents, LegacyComponent { + + private Object propertyValue; + + /** + * Item connected to this form as datasource. + */ + private Item itemDatasource; + + /** + * Ordered list of property ids in this editor. + */ + private final LinkedList<Object> propertyIds = new LinkedList<>(); + + /** + * Current buffered source exception. + */ + private Buffered.SourceException currentBufferedSourceException = null; + + /** + * Is the form in buffered mode. + */ + private boolean buffered = false; + + /** + * Mapping from propertyName to corresponding field. + */ + private final HashMap<Object, Field<?>> fields = new HashMap<>(); + + /** + * Form may act as an Item, its own properties are stored here. + */ + private final HashMap<Object, Property<?>> ownProperties = new HashMap<>(); + + /** + * Field factory for this form. + */ + private FormFieldFactory fieldFactory; + + /** + * Visible item properties. + */ + private Collection<?> visibleItemProperties; + + /** + * Form needs to repaint itself if child fields value changes due possible + * change in form validity. + * + * TODO introduce ValidityChangeEvent (#6239) and start using it instead. + * See e.g. DateField#notifyFormOfValidityChange(). + */ + private final ValueChangeListener fieldValueChangeListener = new ValueChangeListener() { + @Override + public void valueChange(Property.ValueChangeEvent event) { + markAsDirty(); + } + }; + + /** + * If this is true, commit implicitly calls setValidationVisible(true). + */ + private boolean validationVisibleOnCommit = true; + + // special handling for gridlayout; remember initial cursor pos + private int gridlayoutCursorX = -1; + private int gridlayoutCursorY = -1; + + /** + * Keeps track of the Actions added to this component, and manages the + * painting and handling as well. Note that the extended AbstractField is a + * {@link ShortcutNotifier} and has a actionManager that delegates actions + * to the containing window. This one does not delegate. + */ + private ActionManager ownActionManager = new ActionManager(this); + + /** + * Constructs a new form with default layout. + * + * <p> + * By default the form uses {@link FormLayout}. + * </p> + */ + public Form() { + this(null); + setValidationVisible(false); + } + + /** + * Constructs a new form with given {@link Layout}. + * + * @param formLayout + * the layout of the form. + */ + public Form(Layout formLayout) { + this(formLayout, DefaultFieldFactory.get()); + } + + /** + * Constructs a new form with given {@link Layout} and + * {@link FormFieldFactory}. + * + * @param formLayout + * the layout of the form. + * @param fieldFactory + * the TableFieldFactory of the form. + */ + public Form(Layout formLayout, FormFieldFactory fieldFactory) { + super(); + setLayout(formLayout); + setFooter(new HorizontalLayout()); + setFormFieldFactory(fieldFactory); + setValidationVisible(false); + setWidth(100, UNITS_PERCENTAGE); + } + + @Override + protected FormState getState() { + return (FormState) super.getState(); + } + + @Override + protected FormState getState(boolean markAsDirty) { + return (FormState) super.getState(markAsDirty); + } + + /* Documented in interface */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + if (ownActionManager != null) { + ownActionManager.paintActions(null, target); + } + } + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + // Actions + if (ownActionManager != null) { + ownActionManager.handleActions(variables, this); + } + } + + /** + * The error message of a Form is the error of the first field with a + * non-empty error. + * + * Empty error messages of the contained fields are skipped, because an + * empty error indicator would be confusing to the user, especially if there + * are errors that have something to display. This is also the reason why + * the calculation of the error message is separate from validation, because + * validation fails also on empty errors. + */ + @Override + public ErrorMessage getErrorMessage() { + + // Reimplement the checking of validation error by using + // getErrorMessage() recursively instead of validate(). + ErrorMessage validationError = null; + if (isValidationVisible()) { + for (final Iterator<Object> i = propertyIds.iterator(); i + .hasNext();) { + Object f = fields.get(i.next()); + if (f instanceof AbstractComponent) { + AbstractComponent field = (AbstractComponent) f; + + validationError = field.getErrorMessage(); + if (validationError != null) { + // Show caption as error for fields with empty errors + if ("".equals(validationError.toString())) { + validationError = new UserError(field.getCaption()); + } + break; + } else if (f instanceof Field + && !((Field<?>) f).isValid()) { + // Something is wrong with the field, but no proper + // error is given. Generate one. + validationError = new UserError(field.getCaption()); + break; + } + } + } + } + + // Return if there are no errors at all + if (getComponentError() == null && validationError == null + && currentBufferedSourceException == null) { + return null; + } + + // Throw combination of the error types + return new CompositeErrorMessage( + new ErrorMessage[] { getComponentError(), validationError, + AbstractErrorMessage.getErrorMessageForException( + currentBufferedSourceException) }); + } + + /** + * Controls the making validation visible implicitly on commit. + * + * Having commit() call setValidationVisible(true) implicitly is the default + * behaviour. You can disable the implicit setting by setting this property + * as false. + * + * It is useful, because you usually want to start with the form free of + * errors and only display them after the user clicks Ok. You can disable + * the implicit setting by setting this property as false. + * + * @param makeVisible + * If true (default), validation is made visible when commit() is + * called. If false, the visibility is left as it is. + */ + public void setValidationVisibleOnCommit(boolean makeVisible) { + validationVisibleOnCommit = makeVisible; + } + + /** + * Is validation made automatically visible on commit? + * + * See setValidationVisibleOnCommit(). + * + * @return true if validation is made automatically visible on commit. + */ + public boolean isValidationVisibleOnCommit() { + return validationVisibleOnCommit; + } + + /* + * Commit changes to the data source Don't add a JavaDoc comment here, we + * use the default one from the interface. + */ + @Override + public void commit() + throws Buffered.SourceException, Validator.InvalidValueException { + + LinkedList<SourceException> problems = null; + + // Only commit on valid state if so requested + if (!isInvalidCommitted() && !isValid()) { + /* + * The values are not ok and we are told not to commit invalid + * values + */ + if (validationVisibleOnCommit) { + setValidationVisible(true); + } + + // Find the first invalid value and throw the exception + validate(); + } + + // Try to commit all + for (final Iterator<Object> i = propertyIds.iterator(); i.hasNext();) { + try { + final Field<?> f = (fields.get(i.next())); + // Commit only non-readonly fields. + if (!f.isReadOnly()) { + f.commit(); + } + } catch (final Buffered.SourceException e) { + if (problems == null) { + problems = new LinkedList<>(); + } + problems.add(e); + } + } + + // No problems occurred + if (problems == null) { + if (currentBufferedSourceException != null) { + currentBufferedSourceException = null; + markAsDirty(); + } + return; + } + + // Commit problems + final Throwable[] causes = new Throwable[problems.size()]; + int index = 0; + for (final Iterator<SourceException> i = problems.iterator(); i + .hasNext();) { + causes[index++] = i.next(); + } + final Buffered.SourceException e = new Buffered.SourceException(this, + causes); + currentBufferedSourceException = e; + markAsDirty(); + throw e; + } + + /* + * Discards local changes and refresh values from the data source Don't add + * a JavaDoc comment here, we use the default one from the interface. + */ + @Override + public void discard() throws Buffered.SourceException { + + LinkedList<SourceException> problems = null; + + // Try to discard all changes + for (final Iterator<Object> i = propertyIds.iterator(); i.hasNext();) { + try { + (fields.get(i.next())).discard(); + } catch (final Buffered.SourceException e) { + if (problems == null) { + problems = new LinkedList<>(); + } + problems.add(e); + } + } + + // No problems occurred + if (problems == null) { + if (currentBufferedSourceException != null) { + currentBufferedSourceException = null; + markAsDirty(); + } + return; + } + + // Discards problems occurred + final Throwable[] causes = new Throwable[problems.size()]; + int index = 0; + for (final Iterator<SourceException> i = problems.iterator(); i + .hasNext();) { + causes[index++] = i.next(); + } + final Buffered.SourceException e = new Buffered.SourceException(this, + causes); + currentBufferedSourceException = e; + markAsDirty(); + throw e; + } + + /* + * Is the object modified but not committed? Don't add a JavaDoc comment + * here, we use the default one from the interface. + */ + @Override + public boolean isModified() { + for (final Iterator<Object> i = propertyIds.iterator(); i.hasNext();) { + final Field<?> f = fields.get(i.next()); + if (f != null && f.isModified()) { + return true; + } + + } + return false; + } + + /* + * Sets the editor's buffered mode to the specified status. Don't add a + * JavaDoc comment here, we use the default one from the interface. + */ + @Override + public void setBuffered(boolean buffered) { + if (buffered != this.buffered) { + this.buffered = buffered; + for (final Iterator<Object> i = propertyIds.iterator(); i + .hasNext();) { + (fields.get(i.next())).setBuffered(buffered); + } + } + } + + /** + * Adds a new property to form and create corresponding field. + * + * @see com.vaadin.data.Item#addItemProperty(Object, Property) + */ + @Override + public boolean addItemProperty(Object id, Property property) { + + // Checks inputs + if (id == null || property == null) { + throw new NullPointerException("Id and property must be non-null"); + } + + // Checks that the property id is not reserved + if (propertyIds.contains(id)) { + return false; + } + + propertyIds.add(id); + ownProperties.put(id, property); + + // Gets suitable field + final Field<?> field = fieldFactory.createField(this, id, this); + if (field == null) { + return false; + } + + // Configures the field + bindPropertyToField(id, property, field); + + // Register and attach the created field + addField(id, field); + + return true; + } + + /** + * Registers the field with the form and adds the field to the form layout. + * + * <p> + * The property id must not be already used in the form. + * </p> + * + * <p> + * This field is added to the layout using the + * {@link #attachField(Object, Field)} method. + * </p> + * + * @param propertyId + * the Property id the the field. + * @param field + * the field which should be added to the form. + */ + public void addField(Object propertyId, Field<?> field) { + registerField(propertyId, field); + attachField(propertyId, field); + markAsDirty(); + } + + /** + * Register the field with the form. All registered fields are validated + * when the form is validated and also committed when the form is committed. + * + * <p> + * The property id must not be already used in the form. + * </p> + * + * + * @param propertyId + * the Property id of the field. + * @param field + * the Field that should be registered + */ + private void registerField(Object propertyId, Field<?> field) { + if (propertyId == null || field == null) { + return; + } + + fields.put(propertyId, field); + field.addListener(fieldValueChangeListener); + if (!propertyIds.contains(propertyId)) { + // adding a field directly + propertyIds.addLast(propertyId); + } + + // Update the buffered mode and immediate to match the + // form. + // Should this also include invalidCommitted (#3993)? + field.setBuffered(buffered); + if (isImmediate() && field instanceof AbstractComponent) { + ((AbstractComponent) field).setImmediate(true); + } + } + + /** + * Adds the field to the form layout. + * <p> + * The field is added to the form layout in the default position (the + * position used by {@link Layout#addComponent(Component)}. If the + * underlying layout is a {@link CustomLayout} the field is added to the + * CustomLayout location given by the string representation of the property + * id using {@link CustomLayout#addComponent(Component, String)}. + * </p> + * + * <p> + * Override this method to control how the fields are added to the layout. + * </p> + * + * @param propertyId + * @param field + */ + protected void attachField(Object propertyId, Field field) { + if (propertyId == null || field == null) { + return; + } + + Layout layout = getLayout(); + if (layout instanceof CustomLayout) { + ((CustomLayout) layout).addComponent(field, propertyId.toString()); + } else { + layout.addComponent(field); + } + + } + + /** + * The property identified by the property id. + * + * <p> + * The property data source of the field specified with property id is + * returned. If there is a (with specified property id) having no data + * source, the field is returned instead of the data source. + * </p> + * + * @see com.vaadin.data.Item#getItemProperty(Object) + */ + @Override + public Property getItemProperty(Object id) { + final Field<?> field = fields.get(id); + if (field == null) { + // field does not exist or it is not (yet) created for this property + return ownProperties.get(id); + } + final Property<?> property = field.getPropertyDataSource(); + + if (property != null) { + return property; + } else { + return field; + } + } + + /** + * Gets the field identified by the propertyid. + * + * @param propertyId + * the id of the property. + */ + public Field getField(Object propertyId) { + return fields.get(propertyId); + } + + /* Documented in interface */ + @Override + public Collection<?> getItemPropertyIds() { + return Collections.unmodifiableCollection(propertyIds); + } + + /** + * Removes the property and corresponding field from the form. + * + * @see com.vaadin.data.Item#removeItemProperty(Object) + */ + @Override + public boolean removeItemProperty(Object id) { + ownProperties.remove(id); + + final Field<?> field = fields.get(id); + + if (field != null) { + propertyIds.remove(id); + fields.remove(id); + detachField(field); + field.removeListener(fieldValueChangeListener); + return true; + } + + return false; + } + + /** + * Called when a form field is detached from a Form. Typically when a new + * Item is assigned to Form via {@link #setItemDataSource(Item)}. + * <p> + * Override this method to control how the fields are removed from the + * layout. + * </p> + * + * @param field + * the field to be detached from the forms layout. + */ + protected void detachField(final Field field) { + Component p = field.getParent(); + if (p instanceof ComponentContainer) { + ((ComponentContainer) p).removeComponent(field); + } + } + + /** + * Removes all properties and fields from the form. + * + * @return the Success of the operation. Removal of all fields succeeded if + * (and only if) the return value is <code>true</code>. + */ + public boolean removeAllProperties() { + final Object[] properties = propertyIds.toArray(); + boolean success = true; + + for (int i = 0; i < properties.length; i++) { + if (!removeItemProperty(properties[i])) { + success = false; + } + } + + return success; + } + + /* Documented in the interface */ + @Override + public Item getItemDataSource() { + return itemDatasource; + } + + /** + * Sets the item datasource for the form. + * + * <p> + * Setting item datasource clears any fields, the form might contain and + * adds all the properties as fields to the form. + * </p> + * + * @see com.vaadin.data.Item.Viewer#setItemDataSource(Item) + */ + @Override + public void setItemDataSource(Item newDataSource) { + setItemDataSource(newDataSource, newDataSource != null + ? newDataSource.getItemPropertyIds() : null); + } + + /** + * Set the item datasource for the form, but limit the form contents to + * specified properties of the item. + * + * <p> + * Setting item datasource clears any fields, the form might contain and + * adds the specified the properties as fields to the form, in the specified + * order. + * </p> + * + * @see com.vaadin.data.Item.Viewer#setItemDataSource(Item) + */ + public void setItemDataSource(Item newDataSource, + Collection<?> propertyIds) { + + if (getLayout() instanceof GridLayout) { + GridLayout gl = (GridLayout) getLayout(); + if (gridlayoutCursorX == -1) { + // first setItemDataSource, remember initial cursor + gridlayoutCursorX = gl.getCursorX(); + gridlayoutCursorY = gl.getCursorY(); + } else { + // restore initial cursor + gl.setCursorX(gridlayoutCursorX); + gl.setCursorY(gridlayoutCursorY); + } + } + + // Removes all fields first from the form + removeAllProperties(); + + // Sets the datasource + itemDatasource = newDataSource; + + // If the new datasource is null, just set null datasource + if (itemDatasource == null) { + markAsDirty(); + return; + } + + // Adds all the properties to this form + for (final Iterator<?> i = propertyIds.iterator(); i.hasNext();) { + final Object id = i.next(); + final Property<?> property = itemDatasource.getItemProperty(id); + if (id != null && property != null) { + final Field<?> f = fieldFactory.createField(itemDatasource, id, + this); + if (f != null) { + bindPropertyToField(id, property, f); + addField(id, f); + } + } + } + } + + /** + * Binds an item property to a field. The default behavior is to bind + * property straight to Field. If Property.Viewer type property (e.g. + * PropertyFormatter) is already set for field, the property is bound to + * that Property.Viewer. + * + * @param propertyId + * @param property + * @param field + * @since 6.7.3 + */ + protected void bindPropertyToField(final Object propertyId, + final Property property, final Field field) { + // check if field has a property that is Viewer set. In that case we + // expect developer has e.g. PropertyFormatter that he wishes to use and + // assign the property to the Viewer instead. + boolean hasFilterProperty = field.getPropertyDataSource() != null + && (field.getPropertyDataSource() instanceof Property.Viewer); + if (hasFilterProperty) { + ((Property.Viewer) field.getPropertyDataSource()) + .setPropertyDataSource(property); + } else { + field.setPropertyDataSource(property); + } + } + + /** + * Gets the layout of the form. + * + * <p> + * By default form uses <code>OrderedLayout</code> with <code>form</code> + * -style. + * </p> + * + * @return the Layout of the form. + */ + public Layout getLayout() { + return (Layout) getState(false).layout; + } + + /** + * Sets the layout of the form. + * + * <p> + * If set to null then Form uses a FormLayout by default. + * </p> + * + * @param layout + * the layout of the form. + */ + public void setLayout(Layout layout) { + + // Use orderedlayout by default + if (layout == null) { + layout = new FormLayout(); + } + + // reset cursor memory + gridlayoutCursorX = -1; + gridlayoutCursorY = -1; + + // Move fields from previous layout + if (getLayout() != null) { + final Object[] properties = propertyIds.toArray(); + for (int i = 0; i < properties.length; i++) { + Field<?> f = getField(properties[i]); + detachField(f); + if (layout instanceof CustomLayout) { + ((CustomLayout) layout).addComponent(f, + properties[i].toString()); + } else { + layout.addComponent(f); + } + } + + getLayout().setParent(null); + } + + // Replace the previous layout + layout.setParent(this); + getState().layout = layout; + } + + /** + * Sets the form field to be selectable from static list of changes. + * + * <p> + * The list values and descriptions are given as array. The value-array must + * contain the current value of the field and the lengths of the arrays must + * match. Null values are not supported. + * </p> + * + * Note: since Vaadin 7.0, returns an {@link AbstractSelect} instead of a + * {@link Select}. + * + * @param propertyId + * the id of the property. + * @param values + * @param descriptions + * @return the select property generated + */ + public AbstractSelect replaceWithSelect(Object propertyId, Object[] values, + Object[] descriptions) { + + // Checks the parameters + if (propertyId == null || values == null || descriptions == null) { + throw new NullPointerException("All parameters must be non-null"); + } + if (values.length != descriptions.length) { + throw new IllegalArgumentException( + "Value and description list are of different size"); + } + + // Gets the old field + final Field<?> oldField = fields.get(propertyId); + if (oldField == null) { + throw new IllegalArgumentException("Field with given propertyid '" + + propertyId.toString() + "' can not be found."); + } + final Object value = oldField.getPropertyDataSource() == null + ? oldField.getValue() + : oldField.getPropertyDataSource().getValue(); + + // Checks that the value exists and check if the select should + // be forced in multiselect mode + boolean found = false; + boolean isMultiselect = false; + for (int i = 0; i < values.length && !found; i++) { + if (values[i] == value + || (value != null && value.equals(values[i]))) { + found = true; + } + } + if (value != null && !found) { + if (value instanceof Collection) { + for (final Iterator<?> it = ((Collection<?>) value) + .iterator(); it.hasNext();) { + final Object val = it.next(); + found = false; + for (int i = 0; i < values.length && !found; i++) { + if (values[i] == val + || (val != null && val.equals(values[i]))) { + found = true; + } + } + if (!found) { + throw new IllegalArgumentException( + "Currently selected value '" + val + + "' of property '" + + propertyId.toString() + + "' was not found"); + } + } + isMultiselect = true; + } else { + throw new IllegalArgumentException( + "Current value '" + value + "' of property '" + + propertyId.toString() + "' was not found"); + } + } + + // Creates the new field matching to old field parameters + final AbstractSelect newField = isMultiselect ? new ListSelect() + : new Select(); + newField.setCaption(oldField.getCaption()); + newField.setReadOnly(oldField.isReadOnly()); + newField.setBuffered(oldField.isBuffered()); + + // Creates the options list + newField.addContainerProperty("desc", String.class, ""); + newField.setItemCaptionPropertyId("desc"); + for (int i = 0; i < values.length; i++) { + Object id = values[i]; + final Item item; + if (id == null) { + id = newField.addItem(); + item = newField.getItem(id); + newField.setNullSelectionItemId(id); + } else { + item = newField.addItem(id); + } + + if (item != null) { + item.getItemProperty("desc") + .setValue(descriptions[i].toString()); + } + } + + // Sets the property data source + final Property<?> property = oldField.getPropertyDataSource(); + oldField.setPropertyDataSource(null); + newField.setPropertyDataSource(property); + + // Replaces the old field with new one + getLayout().replaceComponent(oldField, newField); + fields.put(propertyId, newField); + newField.addListener(fieldValueChangeListener); + oldField.removeListener(fieldValueChangeListener); + + return newField; + } + + /** + * Checks the validity of the Form and all of its fields. + * + * @see com.vaadin.legacy.data.Validatable#validate() + */ + @Override + public void validate() throws Validator.InvalidValueException { + super.validate(); + for (final Iterator<Object> i = propertyIds.iterator(); i.hasNext();) { + (fields.get(i.next())).validate(); + } + } + + /** + * Checks the validabtable object accept invalid values. + * + * @see com.vaadin.legacy.data.Validatable#isInvalidAllowed() + */ + @Override + public boolean isInvalidAllowed() { + return true; + } + + /** + * Should the validabtable object accept invalid values. + * + * @see com.vaadin.legacy.data.Validatable#setInvalidAllowed(boolean) + */ + @Override + public void setInvalidAllowed(boolean invalidValueAllowed) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /** + * Sets the component's to read-only mode to the specified state. + * + * @see com.vaadin.ui.Component#setReadOnly(boolean) + */ + @Override + public void setReadOnly(boolean readOnly) { + super.setReadOnly(readOnly); + for (final Iterator<?> i = propertyIds.iterator(); i.hasNext();) { + (fields.get(i.next())).setReadOnly(readOnly); + } + } + + /** + * Sets the field factory used by this Form to genarate Fields for + * properties. + * + * {@link FormFieldFactory} is used to create fields for form properties. + * {@link DefaultFieldFactory} is used by default. + * + * @param fieldFactory + * the new factory used to create the fields. + * @see Field + * @see FormFieldFactory + */ + public void setFormFieldFactory(FormFieldFactory fieldFactory) { + this.fieldFactory = fieldFactory; + } + + /** + * Get the field factory of the form. + * + * @return the FormFieldFactory Factory used to create the fields. + */ + public FormFieldFactory getFormFieldFactory() { + return fieldFactory; + } + + /** + * Gets the field type. + * + * @see com.vaadin.legacy.ui.AbstractField#getType() + */ + @Override + public Class<?> getType() { + if (getPropertyDataSource() != null) { + return getPropertyDataSource().getType(); + } + return Object.class; + } + + /** + * Sets the internal value. + * + * This is relevant when the Form is used as Field. + * + * @see com.vaadin.legacy.ui.AbstractField#setInternalValue(java.lang.Object) + */ + @Override + protected void setInternalValue(Object newValue) { + // Stores the old value + final Object oldValue = propertyValue; + + // Sets the current Value + super.setInternalValue(newValue); + propertyValue = newValue; + + // Ignores form updating if data object has not changed. + if (oldValue != newValue) { + setFormDataSource(newValue, getVisibleItemProperties()); + } + } + + /** + * Gets the first focusable field in form. If there are enabled, + * non-read-only fields, the first one of them is returned. Otherwise, the + * field for the first property (or null if none) is returned. + * + * @return the Field. + */ + private Field<?> getFirstFocusableField() { + Collection<?> itemPropertyIds = getItemPropertyIds(); + if (itemPropertyIds != null && itemPropertyIds.size() > 0) { + for (Object id : itemPropertyIds) { + if (id != null) { + Field<?> field = getField(id); + if (field.isConnectorEnabled() && !field.isReadOnly()) { + return field; + } + } + } + // fallback: first field if none of the fields is enabled and + // writable + Object id = itemPropertyIds.iterator().next(); + if (id != null) { + return getField(id); + } + } + return null; + } + + /** + * Updates the internal form datasource. + * + * Method setFormDataSource. + * + * @param data + * @param properties + */ + protected void setFormDataSource(Object data, Collection<?> properties) { + + // If data is an item use it. + Item item = null; + if (data instanceof Item) { + item = (Item) data; + } else if (data != null) { + item = new BeanItem<>(data); + } + + // Sets the datasource to form + if (item != null && properties != null) { + // Shows only given properties + this.setItemDataSource(item, properties); + } else { + // Shows all properties + this.setItemDataSource(item); + } + } + + /** + * Returns the visibleProperties. + * + * @return the Collection of visible Item properites. + */ + public Collection<?> getVisibleItemProperties() { + return visibleItemProperties; + } + + /** + * Sets the visibleProperties. + * + * @param visibleProperties + * the visibleProperties to set. + */ + public void setVisibleItemProperties(Collection<?> visibleProperties) { + visibleItemProperties = visibleProperties; + Object value = getValue(); + if (value == null) { + value = itemDatasource; + } + setFormDataSource(value, getVisibleItemProperties()); + } + + /** + * Sets the visibleProperties. + * + * @param visibleProperties + * the visibleProperties to set. + */ + public void setVisibleItemProperties(Object... visibleProperties) { + LinkedList<Object> v = new LinkedList<>(); + for (int i = 0; i < visibleProperties.length; i++) { + v.add(visibleProperties[i]); + } + setVisibleItemProperties(v); + } + + /** + * Focuses the first field in the form. + * + * @see com.vaadin.ui.Component.Focusable#focus() + */ + @Override + public void focus() { + final Field<?> f = getFirstFocusableField(); + if (f != null) { + f.focus(); + } + } + + /** + * Sets the Tabulator index of this Focusable component. + * + * @see com.vaadin.ui.Component.Focusable#setTabIndex(int) + */ + @Override + public void setTabIndex(int tabIndex) { + super.setTabIndex(tabIndex); + for (final Iterator<?> i = getItemPropertyIds().iterator(); i + .hasNext();) { + (getField(i.next())).setTabIndex(tabIndex); + } + } + + /** + * Setting the form to be immediate also sets all the fields of the form to + * the same state. + */ + @Override + public void setImmediate(boolean immediate) { + super.setImmediate(immediate); + for (Iterator<Field<?>> i = fields.values().iterator(); i.hasNext();) { + Field<?> f = i.next(); + if (f instanceof AbstractComponent) { + ((AbstractComponent) f).setImmediate(immediate); + } + } + } + + /** + * {@inheritDoc} + * <p> + * A Form is empty if all of its fields are empty. + * + */ + @Override + public boolean isEmpty() { + + for (Iterator<Field<?>> i = fields.values().iterator(); i.hasNext();) { + Field<?> f = i.next(); + if (f instanceof AbstractField) { + if (!((AbstractField<?>) f).isEmpty()) { + return false; + } + } + } + + return true; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.AbstractField#clear() + */ + @Override + public void clear() { + for (Iterator<Field<?>> i = fields.values().iterator(); i.hasNext();) { + Field<?> f = i.next(); + if (f instanceof AbstractField) { + ((AbstractField<?>) f).clear(); + } + } + } + + /** + * Adding validators directly to form is not supported. + * + * Add the validators to form fields instead. + */ + @Override + public void addValidator(Validator validator) { + throw new UnsupportedOperationException(); + } + + /** + * Returns a layout that is rendered below normal form contents. This area + * can be used for example to include buttons related to form contents. + * + * @return layout rendered below normal form contents or null if no footer + * is used + */ + public Layout getFooter() { + return (Layout) getState(false).footer; + } + + /** + * Sets the layout that is rendered below normal form contents. No footer is + * rendered if this is set to null, . + * + * @param footer + * the new footer layout + */ + public void setFooter(Layout footer) { + if (getFooter() != null) { + getFooter().setParent(null); + } + getState().footer = footer; + if (footer != null) { + footer.setParent(this); + } + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + if (getParent() != null && !getParent().isEnabled()) { + // some ancestor still disabled, don't update children + return; + } else { + getLayout().markAsDirtyRecursive(); + } + } + + /* + * ACTIONS + */ + + /** + * Gets the {@link ActionManager} responsible for handling {@link Action}s + * added to this Form.<br/> + * Note that Form has another ActionManager inherited from + * {@link AbstractField}. The ownActionManager handles Actions attached to + * this Form specifically, while the ActionManager in AbstractField + * delegates to the containing Window (i.e global Actions). + * + * @return + */ + protected ActionManager getOwnActionManager() { + if (ownActionManager == null) { + ownActionManager = new ActionManager(this); + } + return ownActionManager; + } + + @Override + public void addActionHandler(Handler actionHandler) { + getOwnActionManager().addActionHandler(actionHandler); + } + + @Override + public void removeActionHandler(Handler actionHandler) { + if (ownActionManager != null) { + ownActionManager.removeActionHandler(actionHandler); + } + } + + /** + * Removes all action handlers + */ + public void removeAllActionHandlers() { + if (ownActionManager != null) { + ownActionManager.removeAllActionHandlers(); + } + } + + @Override + public <T extends Action & com.vaadin.event.Action.Listener> void addAction( + T action) { + getOwnActionManager().addAction(action); + } + + @Override + public <T extends Action & com.vaadin.event.Action.Listener> void removeAction( + T action) { + if (ownActionManager != null) { + ownActionManager.removeAction(action); + } + } + + @Override + public Iterator<Component> iterator() { + return new ComponentIterator(); + } + + /** + * Modifiable and Serializable Iterator for the components, used by + * {@link Form#getComponentIterator()}. + */ + private class ComponentIterator + implements Iterator<Component>, Serializable { + + int i = 0; + + @Override + public boolean hasNext() { + if (i < getComponentCount()) { + return true; + } + return false; + } + + @Override + public Component next() { + if (!hasNext()) { + return null; + } + i++; + if (i == 1) { + if (getLayout() != null) { + return getLayout(); + } + if (getFooter() != null) { + return getFooter(); + } + } else if (i == 2) { + if (getFooter() != null) { + return getFooter(); + } + } + return null; + } + + @Override + public void remove() { + if (i == 1) { + if (getLayout() != null) { + setLayout(null); + i = 0; + } else { + setFooter(null); + } + } else if (i == 2) { + setFooter(null); + } + } + } + + /** + * @deprecated As of 7.0, use {@link #iterator()} instead. + */ + @Deprecated + public Iterator<Component> getComponentIterator() { + return iterator(); + } + + public int getComponentCount() { + int count = 0; + if (getLayout() != null) { + count++; + } + if (getFooter() != null) { + count++; + } + + return count; + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/FormFieldFactory.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/FormFieldFactory.java new file mode 100644 index 0000000000..a2c963c0a4 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/FormFieldFactory.java @@ -0,0 +1,56 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed 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 com.vaadin.v7.ui; + +import java.io.Serializable; + +import com.vaadin.ui.Component; +import com.vaadin.v7.data.Item; +import com.vaadin.v7.data.fieldgroup.FieldGroup; + +/** + * Factory interface for creating new Field-instances based on {@link Item}, + * property id and uiContext (the component responsible for displaying fields). + * Currently this interface is used by {@link Form}, but might later be used by + * some other components for {@link Field} generation. + * + * <p> + * + * @author Vaadin Ltd. + * @since 6.0 + * @see TableFieldFactory + * @deprecated As of 7.0, use {@link FieldGroup} instead of {@link Form} for + * more flexibility. + */ +@Deprecated +public interface FormFieldFactory extends Serializable { + /** + * Creates a field based on the item, property id and the component (most + * commonly {@link Form}) where the Field will be presented. + * + * @param item + * the item where the property belongs to. + * @param propertyId + * the Id of the property. + * @param uiContext + * the component where the field is presented, most commonly this + * is {@link Form}. uiContext will not necessary be the parent + * component of the field, but the one that is responsible for + * creating it. + * @return the field suitable for editing the specified data. + */ + Field<?> createField(Item item, Object propertyId, Component uiContext); +} diff --git a/compatibility-server/src/test/java/com/vaadin/v7/tests/server/SerializationTest.java b/compatibility-server/src/test/java/com/vaadin/v7/tests/server/SerializationTest.java index dd0148fdc9..23b3c4fc0d 100644 --- a/compatibility-server/src/test/java/com/vaadin/v7/tests/server/SerializationTest.java +++ b/compatibility-server/src/test/java/com/vaadin/v7/tests/server/SerializationTest.java @@ -17,6 +17,7 @@ import com.vaadin.v7.data.Property; import com.vaadin.v7.data.util.IndexedContainer; import com.vaadin.v7.data.util.MethodProperty; import com.vaadin.v7.data.validator.RegexpValidator; +import com.vaadin.v7.ui.Form; public class SerializationTest { @@ -29,6 +30,19 @@ public class SerializationTest { } @Test + public void testForm() throws Exception { + Form f = new Form(); + String propertyId = "My property"; + f.addItemProperty(propertyId, + new MethodProperty<>(new Data(), "dummyGetterAndSetter")); + f.replaceWithSelect(propertyId, new Object[] { "a", "b", null }, + new String[] { "Item a", "ITem b", "Null item" }); + + serializeAndDeserialize(f); + + } + + @Test public void testIndedexContainerItemIds() throws Exception { IndexedContainer ic = new IndexedContainer(); ic.addContainerProperty("prop1", String.class, null); @@ -43,22 +57,20 @@ public class SerializationTest { @Test public void testMethodPropertyGetter() throws Exception { - MethodProperty<?> mp = new MethodProperty<Object>(new Data(), - "dummyGetter"); + MethodProperty<?> mp = new MethodProperty<>(new Data(), "dummyGetter"); serializeAndDeserialize(mp); } @Test public void testMethodPropertyGetterAndSetter() throws Exception { - MethodProperty<?> mp = new MethodProperty<Object>(new Data(), + MethodProperty<?> mp = new MethodProperty<>(new Data(), "dummyGetterAndSetter"); serializeAndDeserialize(mp); } @Test public void testMethodPropertyInt() throws Exception { - MethodProperty<?> mp = new MethodProperty<Object>(new Data(), - "dummyInt"); + MethodProperty<?> mp = new MethodProperty<>(new Data(), "dummyInt"); serializeAndDeserialize(mp); } diff --git a/compatibility-server/src/test/java/com/vaadin/v7/tests/server/component/form/FormTest.java b/compatibility-server/src/test/java/com/vaadin/v7/tests/server/component/form/FormTest.java new file mode 100644 index 0000000000..0b30afbe0c --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/v7/tests/server/component/form/FormTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed 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 com.vaadin.v7.tests.server.component.form; + +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.v7.ui.Form; +import com.vaadin.v7.ui.TextField; + +/** + * Test for {@link Form}. + * + * @author Vaadin Ltd + */ +public class FormTest { + + @Test + public void testFocus() { + Form form = new Form(); + final boolean firstFieldIsFocused[] = new boolean[1]; + TextField field1 = new TextField() { + @Override + public boolean isConnectorEnabled() { + return false; + } + + @Override + public void focus() { + firstFieldIsFocused[0] = true; + } + }; + + final boolean secondFieldIsFocused[] = new boolean[1]; + TextField field2 = new TextField() { + @Override + public boolean isConnectorEnabled() { + return true; + } + + @Override + public void focus() { + secondFieldIsFocused[0] = true; + } + }; + form.addField("a", field1); + form.addField("b", field2); + form.focus(); + + Assert.assertTrue("Field with enabled connector is not focused", + secondFieldIsFocused[0]); + Assert.assertFalse("Field with disabled connector is focused", + firstFieldIsFocused[0]); + } +} diff --git a/compatibility-shared/src/main/java/com/vaadin/v7/shared/form/FormState.java b/compatibility-shared/src/main/java/com/vaadin/v7/shared/form/FormState.java new file mode 100644 index 0000000000..f351bbbe36 --- /dev/null +++ b/compatibility-shared/src/main/java/com/vaadin/v7/shared/form/FormState.java @@ -0,0 +1,27 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed 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 com.vaadin.v7.shared.form; + +import com.vaadin.shared.Connector; +import com.vaadin.v7.shared.AbstractFieldState; + +public class FormState extends AbstractFieldState { + { + primaryStyleName = "v-form"; + } + public Connector layout; + public Connector footer; +} diff --git a/server/src/main/java/com/vaadin/server/ComponentSizeValidator.java b/server/src/main/java/com/vaadin/server/ComponentSizeValidator.java index 241aea2078..fa3b269fae 100644 --- a/server/src/main/java/com/vaadin/server/ComponentSizeValidator.java +++ b/server/src/main/java/com/vaadin/server/ComponentSizeValidator.java @@ -36,6 +36,7 @@ import com.vaadin.ui.ComponentContainer; import com.vaadin.ui.CustomComponent; import com.vaadin.ui.GridLayout; import com.vaadin.ui.GridLayout.Area; +import com.vaadin.ui.HasComponents; import com.vaadin.ui.Panel; import com.vaadin.ui.TabSheet; import com.vaadin.ui.UI; @@ -89,11 +90,39 @@ public class ComponentSizeValidator implements Serializable { errors = validateComponentRelativeSizes(it.next(), errors, parent); } + } else if (isForm(component)) { + HasComponents form = (HasComponents) component; + for (Iterator<Component> iterator = form.iterator(); iterator + .hasNext();) { + Component child = iterator.next(); + errors = validateComponentRelativeSizes(child, errors, parent); + } } return errors; } + /** + * Comparability form component which is defined in the different jar. + * + * TODO : Normally this logic shouldn't be here. But it means that the whole + * this class has wrong design and impementation and should be refactored. + */ + private static boolean isForm(Component component) { + if (!(component instanceof HasComponents)) { + return false; + } + Class<?> clazz = component.getClass(); + while (clazz != null) { + if (component.getClass().getName() + .equals("com.vaadin.v7.ui.Form")) { + return true; + } + clazz = clazz.getSuperclass(); + } + return false; + } + private static void printServerError(String msg, Stack<ComponentInfo> attributes, boolean widthError, PrintStream errorStream) { @@ -448,6 +477,12 @@ public class ComponentSizeValidator implements Serializable { // Other components define row height return true; } + } else if (isForm(parent)) { + /* + * If some other part of the form is not relative it determines + * the component width + */ + return formHasNonRelativeWidthComponent(parent); } if (parent instanceof Panel || parent instanceof AbstractSplitPanel @@ -477,6 +512,24 @@ public class ComponentSizeValidator implements Serializable { } } + /** + * Comparability form component which is defined in the different jar. + * + * TODO : Normally this logic shouldn't be here. But it means that the whole + * this class has wrong design and impementation and should be refactored. + */ + private static boolean formHasNonRelativeWidthComponent(Component form) { + HasComponents parent = (HasComponents) form; + for (Iterator<Component> iterator = parent.iterator(); iterator + .hasNext();) { + if (!hasRelativeWidth(iterator.next())) { + return true; + } + } + + return false; + } + private static boolean hasRelativeHeight(Component component) { return (component.getHeightUnits() == Unit.PERCENTAGE && component.getHeight() > 0); diff --git a/uitest/src/main/java/com/vaadin/v7/tests/components/form/FormTooltips.java b/uitest/src/main/java/com/vaadin/v7/tests/components/form/FormTooltips.java new file mode 100644 index 0000000000..75688dfdb9 --- /dev/null +++ b/uitest/src/main/java/com/vaadin/v7/tests/components/form/FormTooltips.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012 Vaadin Ltd. + * + * Licensed 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 com.vaadin.v7.tests.components.form; + +import java.util.Arrays; + +import com.vaadin.server.UserError; +import com.vaadin.server.VaadinRequest; +import com.vaadin.tests.components.AbstractTestUI; +import com.vaadin.tests.data.bean.Person; +import com.vaadin.tests.data.bean.Sex; +import com.vaadin.v7.data.util.BeanItem; +import com.vaadin.v7.ui.Form; +import com.vaadin.v7.ui.TextField; + +public class FormTooltips extends AbstractTestUI { + + @Override + protected void setup(VaadinRequest request) { + final Form form = new Form(); + form.setId("tooltipForm"); + form.setDescription("Some description"); + form.setItemDataSource( + new BeanItem<>( + new Person("foo", "bar", "baz", 12, Sex.MALE, null)), + Arrays.asList(new String[] { "firstName", "lastName", "age" })); + ((TextField) form.getField("firstName")) + .setDescription("Fields own tooltip"); + + form.setComponentError(new UserError("Form error")); + addComponent(form); + + } + + @Override + protected String getTestDescription() { + return "The 'first name' should show its own tooltip, the other fields should show no tooltip"; + } + + @Override + protected Integer getTicketNumber() { + return 9173; + } + +} diff --git a/uitest/src/main/java/com/vaadin/v7/tests/components/window/UndefinedHeightSubWindowAndContent.java b/uitest/src/main/java/com/vaadin/v7/tests/components/window/UndefinedHeightSubWindowAndContent.java new file mode 100644 index 0000000000..4a747771ac --- /dev/null +++ b/uitest/src/main/java/com/vaadin/v7/tests/components/window/UndefinedHeightSubWindowAndContent.java @@ -0,0 +1,60 @@ +package com.vaadin.v7.tests.components.window; + +import com.vaadin.tests.components.TestBase; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.Window; +import com.vaadin.v7.data.Validator; +import com.vaadin.v7.ui.Form; +import com.vaadin.v7.ui.TextField; + +public class UndefinedHeightSubWindowAndContent extends TestBase { + + @Override + protected void setup() { + Window subWindow = new Window("No scrollbars!"); + subWindow.setWidth("300px"); + subWindow.center(); + subWindow.setModal(true); + VerticalLayout layout = new VerticalLayout(); + layout.setWidth("100%"); + subWindow.setContent(layout); + + final Form form = new Form(); + form.setFooter(null); + form.setImmediate(true); + form.setValidationVisible(true); + form.setCaption("This is a form"); + form.setDescription("How do you do?"); + final TextField field1 = new TextField("Write here"); + field1.setImmediate(true); + field1.addValidator(new Validator() { + + @Override + public void validate(Object value) throws InvalidValueException { + if (!isValid(value)) { + throw new InvalidValueException("FAIL!"); + } + } + + public boolean isValid(Object value) { + return field1.getValue().equals("valid"); + } + }); + form.addField("Field 1", field1); + layout.addComponent(form); + + getMainWindow().addWindow(subWindow); + subWindow.bringToFront(); + } + + @Override + protected String getDescription() { + return "When both window and its content have undefined height, window must not reserve space for a scroll bar when it is not needed."; + } + + @Override + protected Integer getTicketNumber() { + return 8852; + } + +} diff --git a/uitest/src/test/java/com/vaadin/v7/tests/components/form/FormTooltipsTest.java b/uitest/src/test/java/com/vaadin/v7/tests/components/form/FormTooltipsTest.java new file mode 100644 index 0000000000..710fd5a8c1 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/v7/tests/components/form/FormTooltipsTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed 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 com.vaadin.v7.tests.components.form; + +import org.junit.Test; +import org.openqa.selenium.WebElement; + +import com.vaadin.testbench.By; +import com.vaadin.testbench.elements.FormElement; +import com.vaadin.testbench.elements.TextFieldElement; +import com.vaadin.tests.tb3.TooltipTest; + +public class FormTooltipsTest extends TooltipTest { + + @Test + public void testTooltipConfiguration() throws Exception { + openTestURL(); + // first name tooltip + + WebElement fieldElement = $(FormElement.class).first() + .$(TextFieldElement.class).first(); + checkTooltip(fieldElement, "Fields own tooltip"); + clearTooltip(); + checkTooltipNotPresent(); + + // first name caption tooltip + checkTooltip($(FormElement.class).first() + .findElement(By.className("v-caption")), "Fields own tooltip"); + + clearTooltip(); + checkTooltipNotPresent(); + + // Form should not have a description tooltip + checkTooltip($(FormElement.class).first(), null); + + // Form error message should not have a tooltip + checkTooltip(By.className("v-form-errormessage"), null); + + // last name should have no tooltip + checkTooltip($(TextFieldElement.class).get(1), null); + + // last name caption should have no tooltip + checkTooltip($(FormElement.class).first() + .findElements(By.className("v-caption")).get(1), null); + } + +} diff --git a/uitest/src/test/java/com/vaadin/v7/tests/components/window/UndefinedHeightSubWindowAndContentTest.java b/uitest/src/test/java/com/vaadin/v7/tests/components/window/UndefinedHeightSubWindowAndContentTest.java new file mode 100644 index 0000000000..09489d4566 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/v7/tests/components/window/UndefinedHeightSubWindowAndContentTest.java @@ -0,0 +1,34 @@ +package com.vaadin.v7.tests.components.window; + +import org.junit.Assert; +import org.junit.Test; +import org.openqa.selenium.Keys; + +import com.vaadin.testbench.customelements.WindowElement; +import com.vaadin.testbench.elements.TextFieldElement; +import com.vaadin.tests.tb3.MultiBrowserTest; + +public class UndefinedHeightSubWindowAndContentTest extends MultiBrowserTest { + + @Test + public void testUndefinedHeight() { + openTestURL(); + + TextFieldElement textField = $(TextFieldElement.class).first(); + + textField.click(); + textField.sendKeys("invalid", Keys.ENTER); + + WindowElement window = $(WindowElement.class).first(); + int height = window.getSize().getHeight(); + Assert.assertTrue("Window height with validation failure", + 161 <= height && height <= 164); + + textField.setValue("valid"); + textField.sendKeys(Keys.ENTER); + height = window.getSize().getHeight(); + Assert.assertTrue("Window height with validation success", + 136 <= height && height <= 139); + } + +} |