blob: 6624f0b864590650981ee3b812a1176d25be90d7 [file] [log] [blame]
/*
* 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.pivot.wtk;
import java.util.Iterator;
import java.util.Locale;
import org.apache.pivot.beans.DefaultProperty;
import org.apache.pivot.collections.ArrayList;
import org.apache.pivot.collections.Dictionary;
import org.apache.pivot.collections.Sequence;
import org.apache.pivot.json.JSONSerializer;
import org.apache.pivot.serialization.SerializationException;
import org.apache.pivot.util.ImmutableIterator;
import org.apache.pivot.util.ListenerList;
/**
* A container that arranges field components in a form layout. Each field has
* an optional text label associated with it and may be flagged as requiring
* attention using one of several flag types and an optional flag message (for
* use during form validation, for example).
*/
@DefaultProperty("sections")
public class Form extends Container {
/**
* Class representing a form section. A section is a grouping of components
* within a form.
*/
public static class Section implements Sequence<Component>, Iterable<Component> {
private Form form = null;
private String heading = null;
private ArrayList<Component> fields = new ArrayList<>();
public Form getForm() {
return form;
}
public String getHeading() {
return heading;
}
public void setHeading(String heading) {
String previousHeading = this.heading;
if (previousHeading != heading) {
this.heading = heading;
if (form != null) {
form.formListeners.sectionHeadingChanged(this);
}
}
}
@Override
public int add(Component field) {
int index = getLength();
insert(field, index);
return index;
}
@Override
public void insert(Component field, int index) {
if (field == null) {
throw new IllegalArgumentException();
}
if (field.getParent() != null) {
throw new IllegalArgumentException("Field already has a parent.");
}
fields.insert(field, index);
field.setAttribute(Attribute.SECTION, this);
if (form != null) {
form.add(field);
form.formListeners.fieldInserted(this, index);
}
}
@Override
public Component update(int index, Component field) {
throw new UnsupportedOperationException();
}
@Override
public int remove(Component field) {
int index = fields.indexOf(field);
if (index != -1) {
remove(index, 1);
}
return index;
}
@Override
public Sequence<Component> remove(int index, int count) {
Sequence<Component> removed = fields.remove(index, count);
for (int i = 0, n = removed.getLength(); i < n; i++) {
Component field = removed.get(i);
field.setAttribute(Attribute.SECTION, null);
if (form != null) {
form.remove(field);
}
}
if (form != null) {
form.formListeners.fieldsRemoved(this, index, removed);
}
return removed;
}
@Override
public Component get(int index) {
return fields.get(index);
}
@Override
public int indexOf(Component field) {
return fields.indexOf(field);
}
@Override
public int getLength() {
return fields.getLength();
}
@Override
public Iterator<Component> iterator() {
return new ImmutableIterator<>(fields.iterator());
}
}
/**
* Section sequence implementation.
*/
public final class SectionSequence implements Sequence<Section>, Iterable<Section> {
private SectionSequence() {
}
@Override
public int add(Section section) {
int index = getLength();
insert(section, index);
return index;
}
@Override
public void insert(Section section, int index) {
if (section.form != null) {
throw new IllegalArgumentException("section already has a form.");
}
sections.insert(section, index);
section.form = Form.this;
for (int i = 0, n = section.getLength(); i < n; i++) {
Form.this.add(section.get(i));
}
formListeners.sectionInserted(Form.this, index);
}
@Override
public Section update(int index, Section section) {
throw new UnsupportedOperationException();
}
@Override
public int remove(Section section) {
int index = sections.indexOf(section);
if (index != -1) {
remove(index, 1);
}
return index;
}
@Override
public Sequence<Section> remove(int index, int count) {
Sequence<Section> removed = sections.remove(index, count);
for (int i = 0, n = removed.getLength(); i < n; i++) {
Section section = removed.get(i);
section.form = null;
for (Component field : section) {
Form.this.remove(field);
}
}
formListeners.sectionsRemoved(Form.this, index, removed);
return removed;
}
@Override
public Section get(int index) {
return sections.get(index);
}
@Override
public int indexOf(Section item) {
return sections.indexOf(item);
}
@Override
public int getLength() {
return sections.getLength();
}
@Override
public Iterator<Section> iterator() {
return new ImmutableIterator<>(sections.iterator());
}
}
/**
* Represents an message alert associated with a form field.
*/
public static class Flag {
/**
* The flag's message type.
*/
private MessageType messageType = null;
/**
* The flag message. May be <tt>null</tt>.
*/
private String message = null;
public static final String MESSAGE_TYPE_KEY = "messageType";
public static final String MESSAGE_KEY = "message";
/**
* Creates a new flag with a type of "error" and no message.
*/
public Flag() {
this(MessageType.ERROR, null);
}
/**
* Creates a new flag with the given message type and no message.
*
* @param messageType The type of the flag.
*/
public Flag(MessageType messageType) {
this(messageType, null);
}
/**
* Creates a new flag with the given type and message.
*
* @param messageType The type of the flag.
* @param message The message text associated with the flag, or
* <tt>null</tt> for no message.
*/
public Flag(MessageType messageType, String message) {
if (messageType == null) {
throw new IllegalArgumentException("messageType is null.");
}
this.messageType = messageType;
this.message = message;
}
/**
* Returns the flag's message type.
*
* @return The message type of the flag.
*/
public MessageType getMessageType() {
return messageType;
}
/**
* Sets the flag's message type.
*
* @param messageType The new message type for this flag.
* @throws IllegalArgumentException if the message type is {@code null}.
*/
public void setMessageType(MessageType messageType) {
if (messageType == null) {
throw new IllegalArgumentException();
}
this.messageType = messageType;
}
/**
* Returns the flag message.
*
* @return The message text associated with the flag, or <tt>null</tt>
* if there is no message.
*/
public String getMessage() {
return message;
}
/**
* Sets the flag message.
*
* @param message The message text associated with the flag, or
* <tt>null</tt> if there is no message.
*/
public void setMessage(String message) {
this.message = message;
}
public static Flag decode(String flag) {
Dictionary<String, ?> map;
try {
map = JSONSerializer.parseMap(flag);
} catch (SerializationException exception) {
throw new IllegalArgumentException(exception);
}
String messageType = (String) map.get(MESSAGE_TYPE_KEY);
if (messageType == null) {
throw new IllegalArgumentException(MESSAGE_TYPE_KEY + " is required.");
}
Flag value = new Flag(MessageType.valueOf(messageType.toUpperCase(Locale.ENGLISH)),
(String) map.get(MESSAGE_KEY));
return value;
}
}
private enum Attribute {
SECTION, LABEL, REQUIRED, FLAG;
}
private static class FormListenerList extends WTKListenerList<FormListener> implements
FormListener {
@Override
public void sectionInserted(Form form, int index) {
for (FormListener listener : this) {
listener.sectionInserted(form, index);
}
}
@Override
public void sectionsRemoved(Form form, int index, Sequence<Section> removed) {
for (FormListener listener : this) {
listener.sectionsRemoved(form, index, removed);
}
}
@Override
public void sectionHeadingChanged(Form.Section section) {
for (FormListener listener : this) {
listener.sectionHeadingChanged(section);
}
}
@Override
public void fieldInserted(Section section, int index) {
for (FormListener listener : this) {
listener.fieldInserted(section, index);
}
}
@Override
public void fieldsRemoved(Section section, int index, Sequence<Component> fields) {
for (FormListener listener : this) {
listener.fieldsRemoved(section, index, fields);
}
}
}
private static class FormAttributeListenerList extends WTKListenerList<FormAttributeListener>
implements FormAttributeListener {
@Override
public void labelChanged(Form form, Component component, String previousLabel) {
for (FormAttributeListener listener : this) {
listener.labelChanged(form, component, previousLabel);
}
}
@Override
public void requiredChanged(Form form, Component field) {
for (FormAttributeListener listener : this) {
listener.requiredChanged(form, field);
}
}
@Override
public void flagChanged(Form form, Component component, Form.Flag previousFlag) {
for (FormAttributeListener listener : this) {
listener.flagChanged(form, component, previousFlag);
}
}
}
private ArrayList<Section> sections = new ArrayList<>();
private SectionSequence sectionSequence = new SectionSequence();
private FormListenerList formListeners = new FormListenerList();
private FormAttributeListenerList formAttributeListeners = new FormAttributeListenerList();
/**
* Creates a new form.
*/
public Form() {
super();
installSkin(Form.class);
}
/**
* Returns the form's field sequence.
*
* @return The form's field sequence.
*/
public SectionSequence getSections() {
return sectionSequence;
}
/**
* Clears all field flags.
*/
public void clearFlags() {
for (Section section : sections) {
for (Component field : section) {
setFlag(field, (Flag) null);
}
}
}
/**
* Returns the number of fields that are flagged with a given message type.
*
* @param messageType The message type to count, or <tt>null</tt> to return
* the count of all flagged fields regardless of message type.
* @return The number of flagged fields.
*/
public int getFlaggedFieldCount(MessageType messageType) {
int count = 0;
for (Section section : sections) {
for (Component field : section) {
Flag flag = getFlag(field);
if (flag != null && (messageType == null || flag.getMessageType() == messageType)) {
count++;
}
}
}
return count;
}
/**
* Ensures that the first field with the given flag type is visible.
*
* @param messageType The message type, or <tt>null</tt> to scroll the first
* flag of any type to visible.
*/
public void scrollFirstFlagToVisible(MessageType messageType) {
Flag flag = null;
for (Section section : sections) {
for (Component field : section) {
flag = getFlag(field);
if (flag != null && (messageType == null || flag.getMessageType() == messageType)) {
field.scrollAreaToVisible(0, 0, field.getWidth(), field.getHeight());
break;
}
}
if (flag != null) {
break;
}
}
}
@Override
public Sequence<Component> remove(int index, int count) {
for (int i = index, n = index + count; i < n; i++) {
Component component = get(i);
for (Section section : sections) {
if (section.indexOf(component) >= 0) {
throw new UnsupportedOperationException();
}
}
}
// Call the base method to remove the components
return super.remove(index, count);
}
/**
* Returns the form listener list.
*
* @return The form listener list.
*/
public ListenerList<FormListener> getFormListeners() {
return formListeners;
}
/**
* Returns the form attribute listener list.
*
* @return The form attribute listener list.
*/
public ListenerList<FormAttributeListener> getFormAttributeListeners() {
return formAttributeListeners;
}
/**
* Finds the {@link Form.Section} that the given component belongs to. Only
* finds the section if the component is a direct child of the section.
*
* @param component The component in question.
* @return The section this component belongs to.
* @see #getEnclosingSection getEnclosingSection(Component)
*/
public static Section getSection(Component component) {
return (Section) component.getAttribute(Attribute.SECTION);
}
/**
* Finds the {@link Form.Section} that the given component belongs to. Will
* search up the parent hierarchy in case the component is nested inside
* other containers inside the form.
*
* @param component The component in question.
* @return The form section this component (or one of its parents) belongs
* to or <code>null</code> if the component does not belong to a form.
* @see #getSection getSection(Component)
*/
public static Section getEnclosingSection(Component component) {
Section section = (Section) component.getAttribute(Attribute.SECTION);
if (section == null) {
for (Container parent = component.getParent(); parent != null
&& (section = (Section) parent.getAttribute(Attribute.SECTION)) == null;) {
parent = parent.getParent();
}
}
return section;
}
public static String getLabel(Component component) {
return (String) component.getAttribute(Attribute.LABEL);
}
public static void setLabel(Component component, String label) {
String previousLabel = (String) component.setAttribute(Attribute.LABEL, label);
if (previousLabel != label) {
Container parent = component.getParent();
if (parent instanceof Form) {
Form form = (Form) parent;
form.formAttributeListeners.labelChanged(form, component, previousLabel);
}
}
}
public static boolean isRequired(Component component) {
Boolean value = (Boolean) component.getAttribute(Attribute.REQUIRED);
return (value == null) ? false : value.booleanValue();
}
public static void setRequired(Component component, boolean required) {
Boolean previousValue = (Boolean) component.setAttribute(Attribute.REQUIRED, new Boolean(required));
boolean previousRequired = (previousValue == null) ? false : previousValue.booleanValue();
if (previousRequired != required) {
Container parent = component.getParent();
if (parent instanceof Form) {
Form form = (Form) parent;
form.formAttributeListeners.requiredChanged(form, component);
}
}
}
public static Flag getFlag(Component component) {
return (Flag) component.getAttribute(Attribute.FLAG);
}
public static void setFlag(Component component, Flag flag) {
Flag previousFlag = (Flag) component.setAttribute(Attribute.FLAG, flag);
if (previousFlag != flag) {
Container parent = component.getParent();
if (parent instanceof Form) {
Form form = (Form) parent;
form.formAttributeListeners.flagChanged(form, component, previousFlag);
}
}
}
public static final void setFlag(Component component, String flag) {
if (flag == null) {
throw new IllegalArgumentException("flag is null.");
}
setFlag(component, Flag.decode(flag));
}
public static final void clearFlag(Component component) {
setFlag(component, (Flag) null);
}
}