From: Mathieu Baudier Date: Fri, 23 Jun 2023 09:20:09 +0000 (+0200) Subject: Migrate forms UI to ACR X-Git-Tag: v2.3.15~7 X-Git-Url: https://git.argeo.org/?p=gpl%2Fargeo-suite.git;a=commitdiff_plain;h=ccf5f2a0cce5a063b22385334dbf8d19355805a5 Migrate forms UI to ACR --- diff --git a/swt/org.argeo.app.swt/src/org/argeo/app/swt/forms/EditableLink.java b/swt/org.argeo.app.swt/src/org/argeo/app/swt/forms/EditableLink.java new file mode 100644 index 0000000..9d14ba0 --- /dev/null +++ b/swt/org.argeo.app.swt/src/org/argeo/app/swt/forms/EditableLink.java @@ -0,0 +1,70 @@ +package org.argeo.app.swt.forms; + +import javax.xml.namespace.QName; + +import org.argeo.api.acr.Content; +import org.argeo.cms.swt.SwtEditablePart; +import org.argeo.eclipse.ui.EclipseUiUtils; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +/** Editable String that displays a browsable link when read-only */ +public class EditableLink extends EditablePropertyString implements SwtEditablePart { + private static final long serialVersionUID = 5055000749992803591L; + + private String type; + private String message; + private boolean readOnly; + + public EditableLink(Composite parent, int style, Content node, QName propertyName, String type, String message) { + super(parent, style, node, propertyName, message); + this.message = message; + this.type = type; + + readOnly = SWT.READ_ONLY == (style & SWT.READ_ONLY); + if (node.containsKey(propertyName)) { + this.setStyle(FormStyle.propertyText.style()); + this.setText(node.attr(propertyName)); + } else { + this.setStyle(FormStyle.propertyMessage.style()); + this.setText(""); + } + } + + public void setText(String text) { + Control child = getControl(); + if (child instanceof Label) { + Label lbl = (Label) child; + if (EclipseUiUtils.isEmpty(text)) + lbl.setText(message); + else if (readOnly) + setLinkValue(lbl, text); + else + // if canEdit() we put only the value with no link + // to avoid glitches of the edition life cycle + lbl.setText(text); + } else if (child instanceof Text) { + Text txt = (Text) child; + if (EclipseUiUtils.isEmpty(text)) { + txt.setText(""); + txt.setMessage(message); + } else + txt.setText(text); + } + } + + private void setLinkValue(Label lbl, String text) { + if (FormStyle.email.style().equals(type)) + lbl.setText(FormUtils.getMailLink(text)); + else if (FormStyle.phone.style().equals(type)) + lbl.setText(FormUtils.getPhoneLink(text)); + else if (FormStyle.website.style().equals(type)) + lbl.setText(FormUtils.getUrlLink(text)); + else if (FormStyle.facebook.style().equals(type) || FormStyle.instagram.style().equals(type) + || FormStyle.linkedIn.style().equals(type) || FormStyle.twitter.style().equals(type)) + lbl.setText(FormUtils.getUrlLink(text)); + } +} \ No newline at end of file diff --git a/swt/org.argeo.app.swt/src/org/argeo/app/swt/forms/EditableMultiStringProperty.java b/swt/org.argeo.app.swt/src/org/argeo/app/swt/forms/EditableMultiStringProperty.java new file mode 100644 index 0000000..6145366 --- /dev/null +++ b/swt/org.argeo.app.swt/src/org/argeo/app/swt/forms/EditableMultiStringProperty.java @@ -0,0 +1,261 @@ +package org.argeo.app.swt.forms; + +import java.util.List; + +import javax.xml.namespace.QName; + +import org.argeo.api.acr.Content; +import org.argeo.cms.swt.CmsSwtUtils; +import org.argeo.cms.swt.SwtEditablePart; +import org.argeo.cms.swt.acr.ContentStyledControl; +import org.argeo.cms.swt.dialogs.CmsMessageDialog; +import org.argeo.eclipse.ui.EclipseUiUtils; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.events.TraverseEvent; +import org.eclipse.swt.events.TraverseListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.layout.RowLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +/** Display, add or remove values from a list in a CMS context */ +public class EditableMultiStringProperty extends ContentStyledControl implements SwtEditablePart { + private static final long serialVersionUID = -7044614381252178595L; + + private QName propertyName; + private String message; + // TODO implement the ability to provide a list of possible values +// private String[] possibleValues; + private boolean canEdit; + private SelectionListener removeValueSL; + private List values; + + // TODO manage within the CSS + private int rowSpacing = 5; + private int rowMarging = 0; + private int oneValueMargingRight = 5; + private int btnWidth = 16; + private int btnHeight = 16; + private int btnHorizontalIndent = 3; + + public EditableMultiStringProperty(Composite parent, int style, Content node, QName propertyName, + List values, String[] possibleValues, String addValueMsg, + SelectionListener removeValueSelectionListener) { + super(parent, style, node); + + this.propertyName = propertyName; + this.values = values; +// this.possibleValues = new String[] { "Un", "Deux", "Trois" }; + this.message = addValueMsg; + this.canEdit = removeValueSelectionListener != null; + this.removeValueSL = removeValueSelectionListener; + } + + public List getValues() { + return values; + } + + public void setValues(List values) { + this.values = values; + } + + // Row layout items do not need explicit layout data + protected void setControlLayoutData(Control control) { + } + + /** To be overridden */ + protected void setContainerLayoutData(Composite composite) { + composite.setLayoutData(CmsSwtUtils.fillWidth()); + } + + @Override + public Control getControl() { + return super.getControl(); + } + + @Override + protected Control createControl(Composite box, String style) { + Composite row = new Composite(box, SWT.NO_FOCUS); + row.setLayoutData(EclipseUiUtils.fillAll()); + + RowLayout rl = new RowLayout(SWT.HORIZONTAL); + rl.wrap = true; + rl.spacing = rowSpacing; + rl.marginRight = rl.marginLeft = rl.marginBottom = rl.marginTop = rowMarging; + row.setLayout(rl); + + if (values != null) { + for (final String value : values) { + if (canEdit) + createRemovableValue(row, SWT.SINGLE, value); + else + createValueLabel(row, SWT.SINGLE, value); + } + } + + if (!canEdit) + return row; + else if (isEditing()) + return createText(row, style); + else + return createLabel(row, style); + } + + /** + * Override to provide specific layout for the existing values, typically adding + * a pound (#) char for tags or anchor info for browsable links. We assume the + * parent composite already has a layout and it is the caller responsibility to + * apply corresponding layout data + */ + protected Label createValueLabel(Composite parent, int style, String value) { + Label label = new Label(parent, style); + label.setText("#" + value); + CmsSwtUtils.markup(label); + CmsSwtUtils.style(label, FormStyle.propertyText.style()); + return label; + } + + private Composite createRemovableValue(Composite parent, int style, String value) { + Composite valCmp = new Composite(parent, SWT.NO_FOCUS); + GridLayout gl = EclipseUiUtils.noSpaceGridLayout(new GridLayout(2, false)); + gl.marginRight = oneValueMargingRight; + valCmp.setLayout(gl); + + createValueLabel(valCmp, SWT.WRAP, value); + + Button deleteBtn = new Button(valCmp, SWT.FLAT); + deleteBtn.setData(FormConstants.LINKED_VALUE, value); + deleteBtn.addSelectionListener(removeValueSL); + CmsSwtUtils.style(deleteBtn, FormStyle.delete.style() + FormStyle.BUTTON_SUFFIX); + GridData gd = new GridData(); + gd.heightHint = btnHeight; + gd.widthHint = btnWidth; + gd.horizontalIndent = btnHorizontalIndent; + deleteBtn.setLayoutData(gd); + + return valCmp; + } + + protected Text createText(Composite box, String style) { + final Text text = new Text(box, getStyle()); + // The "add new value" text is not meant to change, so we can set it on + // creation + text.setMessage(message); + CmsSwtUtils.style(text, style); + text.setFocus(); + + text.addTraverseListener(new TraverseListener() { + private static final long serialVersionUID = 1L; + + public void keyTraversed(TraverseEvent e) { + if (e.keyCode == SWT.CR) { + addValue(text); + e.doit = false; + } + } + }); + + // The OK button does not work with the focusOut listener + // because focus out is called before the OK button is pressed + + // // we must call layout() now so that the row data can compute the + // height + // // of the other controls. + // text.getParent().layout(); + // int height = text.getSize().y; + // + // Button okBtn = new Button(box, SWT.BORDER | SWT.PUSH | SWT.BOTTOM); + // okBtn.setText("OK"); + // RowData rd = new RowData(SWT.DEFAULT, height - 2); + // okBtn.setLayoutData(rd); + // + // okBtn.addSelectionListener(new SelectionAdapter() { + // private static final long serialVersionUID = 2780819012423622369L; + // + // @Override + // public void widgetSelected(SelectionEvent e) { + // addValue(text); + // } + // }); + + return text; + } + + /** Performs the real addition, overwrite to make further sanity checks */ + protected void addValue(Text text) { + String value = text.getText(); + String errMsg = null; + + if (EclipseUiUtils.isEmpty(value)) + return; + + if (values.contains(value)) + errMsg = "Dupplicated value: " + value + ", please correct and try again"; + if (errMsg != null) + CmsMessageDialog.openError("Addition not allowed: " + errMsg); + else { + values.add(value); + Composite newCmp = createRemovableValue(text.getParent(), SWT.SINGLE, value); + newCmp.moveAbove(text); + text.setText(""); + newCmp.getParent().layout(); + } + } + + protected Label createLabel(Composite box, String style) { + if (canEdit) { + Label lbl = new Label(box, getStyle()); + lbl.setText(message); + CmsSwtUtils.style(lbl, style); + CmsSwtUtils.markup(lbl); + if (mouseListener != null) + lbl.addMouseListener(mouseListener); + return lbl; + } + return null; + } + + protected void clear(boolean deep) { + Control child = getControl(); + if (deep) + super.clear(deep); + else { + child.getParent().dispose(); + } + } + + public void setText(String text) { + Control child = getControl(); + if (child instanceof Label) { + Label lbl = (Label) child; + if (canEdit) + lbl.setText(text); + else + lbl.setText(""); + } else if (child instanceof Text) { + Text txt = (Text) child; + txt.setText(text); + } + } + + public synchronized void startEditing() { + CmsSwtUtils.style(getControl(), FormStyle.propertyText.style()); +// getControl().setData(STYLE, FormStyle.propertyText.style()); + super.startEditing(); + } + + public synchronized void stopEditing() { + CmsSwtUtils.style(getControl(), FormStyle.propertyMessage.style()); +// getControl().setData(STYLE, FormStyle.propertyMessage.style()); + super.stopEditing(); + } + + public QName getPropertyName() { + return propertyName; + } +} \ No newline at end of file diff --git a/swt/org.argeo.app.swt/src/org/argeo/app/swt/forms/EditablePropertyDate.java b/swt/org.argeo.app.swt/src/org/argeo/app/swt/forms/EditablePropertyDate.java new file mode 100644 index 0000000..fb78fbd --- /dev/null +++ b/swt/org.argeo.app.swt/src/org/argeo/app/swt/forms/EditablePropertyDate.java @@ -0,0 +1,305 @@ +package org.argeo.app.swt.forms; + +import java.text.DateFormat; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoField; +import java.time.temporal.TemporalAccessor; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Optional; + +import javax.xml.namespace.QName; + +import org.argeo.api.acr.Content; +import org.argeo.cms.swt.CmsSwtUtils; +import org.argeo.cms.swt.SwtEditablePart; +import org.argeo.cms.swt.acr.ContentStyledControl; +import org.argeo.eclipse.ui.EclipseUiUtils; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.ShellAdapter; +import org.eclipse.swt.events.ShellEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.DateTime; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +/** CMS form part to display and edit a date */ +public class EditablePropertyDate extends ContentStyledControl implements SwtEditablePart { + private static final long serialVersionUID = 2500215515778162468L; + + // Context + private QName propertyName; + private String message; + private DateTimeFormatter dateFormat; + + // UI Objects + private Text dateTxt; + private Button openCalBtn; + + // TODO manage within the CSS + private int fieldBtnSpacing = 5; + + /** + * + * @param parent + * @param style + * @param node + * @param propertyName + * @param message + * @param dateFormat provide a {@link DateFormat} as contract to be able to + * read/write dates as strings + * @throws RepositoryException + */ + public EditablePropertyDate(Composite parent, int style, Content node, QName propertyName, String message, + DateTimeFormatter dateFormat) { + super(parent, style, node); + + this.propertyName = propertyName; + this.message = message; + this.dateFormat = dateFormat; + + Optional instant = node.get(propertyName, Instant.class); + if (instant.isPresent()) { + this.setStyle(FormStyle.propertyText.style()); + this.setText(dateFormat.format(instant.get())); + } else { + this.setStyle(FormStyle.propertyMessage.style()); + this.setText(message); + } + } + + public void setText(String text) { + Control child = getControl(); + if (child instanceof Label) { + Label lbl = (Label) child; + if (EclipseUiUtils.isEmpty(text)) + lbl.setText(message); + else + lbl.setText(text); + } else if (child instanceof Text) { + Text txt = (Text) child; + if (EclipseUiUtils.isEmpty(text)) { + txt.setText(""); + } else + txt.setText(text); + } + } + + public synchronized void startEditing() { + // if (dateTxt != null && !dateTxt.isDisposed()) + CmsSwtUtils.style(getControl(), FormStyle.propertyText); +// getControl().setData(STYLE, FormStyle.propertyText.style()); + super.startEditing(); + } + + public synchronized void stopEditing() { + if (EclipseUiUtils.isEmpty(dateTxt.getText())) + CmsSwtUtils.style(getControl(), FormStyle.propertyMessage); +// getControl().setData(STYLE, FormStyle.propertyMessage.style()); + else + CmsSwtUtils.style(getControl(), FormStyle.propertyText); +// getControl().setData(STYLE, FormStyle.propertyText.style()); + super.stopEditing(); + } + + public QName getPropertyName() { + return propertyName; + } + + @Override + protected Control createControl(Composite box, String style) { + if (isEditing()) { + return createCustomEditableControl(box, style); + } else + return createLabel(box, style); + } + + protected Label createLabel(Composite box, String style) { + Label lbl = new Label(box, getStyle() | SWT.WRAP); + lbl.setLayoutData(CmsSwtUtils.fillWidth()); + CmsSwtUtils.style(lbl, style); + CmsSwtUtils.markup(lbl); + if (mouseListener != null) + lbl.addMouseListener(mouseListener); + return lbl; + } + + private Control createCustomEditableControl(Composite box, String style) { + box.setLayoutData(CmsSwtUtils.fillWidth()); + Composite dateComposite = new Composite(box, SWT.NONE); + GridLayout gl = EclipseUiUtils.noSpaceGridLayout(new GridLayout(2, false)); + gl.horizontalSpacing = fieldBtnSpacing; + dateComposite.setLayout(gl); + dateTxt = new Text(dateComposite, SWT.BORDER); + CmsSwtUtils.style(dateTxt, style); + dateTxt.setLayoutData(new GridData(120, SWT.DEFAULT)); + dateTxt.setToolTipText( + "Enter a date with form \"" + FormUtils.DEFAULT_SHORT_DATE_FORMAT + "\" or use the calendar"); + openCalBtn = new Button(dateComposite, SWT.FLAT); + CmsSwtUtils.style(openCalBtn, FormStyle.calendar.style() + FormStyle.BUTTON_SUFFIX); + GridData gd = new GridData(SWT.CENTER, SWT.CENTER, false, false); + gd.heightHint = 17; + openCalBtn.setLayoutData(gd); + // openCalBtn.setImage(PeopleRapImages.CALENDAR_BTN); + + openCalBtn.addSelectionListener(new SelectionAdapter() { + private static final long serialVersionUID = 1L; + + public void widgetSelected(SelectionEvent event) { + CalendarPopup popup = new CalendarPopup(dateTxt); + popup.open(); + } + }); + + // dateTxt.addFocusListener(new FocusListener() { + // private static final long serialVersionUID = 1L; + // + // @Override + // public void focusLost(FocusEvent event) { + // String newVal = dateTxt.getText(); + // // Enable reset of the field + // if (FormUtils.notNull(newVal)) + // calendar = null; + // else { + // try { + // Calendar newCal = parseDate(newVal); + // // DateText.this.setText(newCal); + // calendar = newCal; + // } catch (ParseException pe) { + // // Silent. Manage error popup? + // if (calendar != null) + // EditablePropertyDate.this.setText(calendar); + // } + // } + // } + // + // @Override + // public void focusGained(FocusEvent event) { + // } + // }); + return dateTxt; + } + + protected void clear(boolean deep) { + Control child = getControl(); + if (deep || child instanceof Label) + super.clear(deep); + else { + child.getParent().dispose(); + } + } + +// /** Enable setting a custom tooltip on the underlying text */ +// @Deprecated +// public void setToolTipText(String toolTipText) { +// dateTxt.setToolTipText(toolTipText); +// } +// +// @Deprecated +// /** Enable setting a custom message on the underlying text */ +// public void setMessage(String message) { +// dateTxt.setMessage(message); +// } +// +// @Deprecated +// public void setText(Calendar cal) { +// String newValueStr = ""; +// if (cal != null) +// newValueStr = dateFormat.format(cal.getTime()); +// if (!newValueStr.equals(dateTxt.getText())) +// dateTxt.setText(newValueStr); +// } + + // UTILITIES TO MANAGE THE CALENDAR POPUP + // TODO manage the popup shell in a cleaner way + private class CalendarPopup extends Shell { + private static final long serialVersionUID = 1L; + private DateTime dateTimeCtl; + + public CalendarPopup(Control source) { + super(source.getDisplay(), SWT.NO_TRIM | SWT.BORDER | SWT.ON_TOP); + populate(); + // Add border and shadow style + CmsSwtUtils.markup(CalendarPopup.this); + CmsSwtUtils.style(CalendarPopup.this, FormStyle.popupCalendar.style()); + pack(); + layout(); + setLocation(source.toDisplay((source.getLocation().x - 2), (source.getSize().y) + 3)); + + addShellListener(new ShellAdapter() { + private static final long serialVersionUID = 5178980294808435833L; + + @Override + public void shellDeactivated(ShellEvent e) { + close(); + dispose(); + } + }); + open(); + } + + private void setProperty() { + // Direct set does not seems to work. investigate + // cal.set(dateTimeCtl.getYear(), dateTimeCtl.getMonth(), + // dateTimeCtl.getDay(), 12, 0); + // TODO use modern time API + Calendar cal = new GregorianCalendar(); + cal.set(Calendar.YEAR, dateTimeCtl.getYear()); + cal.set(Calendar.MONTH, dateTimeCtl.getMonth()); + cal.set(Calendar.DAY_OF_MONTH, dateTimeCtl.getDay()); + String dateStr = dateFormat.format(cal.toInstant()); + dateTxt.setText(dateStr); + } + + protected void populate() { + setLayout(EclipseUiUtils.noSpaceGridLayout()); + + dateTimeCtl = new DateTime(this, SWT.CALENDAR); + dateTimeCtl.setLayoutData(EclipseUiUtils.fillAll()); + + TemporalAccessor calendar = FormUtils.parseDate(dateFormat, dateTxt.getText()); + + if (calendar != null) + dateTimeCtl.setDate(calendar.get(ChronoField.YEAR), calendar.get(ChronoField.MONTH_OF_YEAR), + calendar.get(ChronoField.DAY_OF_MONTH)); + + dateTimeCtl.addSelectionListener(new SelectionAdapter() { + private static final long serialVersionUID = -8414377364434281112L; + + @Override + public void widgetSelected(SelectionEvent e) { + setProperty(); + } + }); + + dateTimeCtl.addMouseListener(new MouseListener() { + private static final long serialVersionUID = 1L; + + @Override + public void mouseUp(MouseEvent e) { + } + + @Override + public void mouseDown(MouseEvent e) { + } + + @Override + public void mouseDoubleClick(MouseEvent e) { + setProperty(); + close(); + dispose(); + } + }); + } + } +} \ No newline at end of file diff --git a/swt/org.argeo.app.swt/src/org/argeo/app/swt/forms/EditablePropertyString.java b/swt/org.argeo.app.swt/src/org/argeo/app/swt/forms/EditablePropertyString.java new file mode 100644 index 0000000..7820bfe --- /dev/null +++ b/swt/org.argeo.app.swt/src/org/argeo/app/swt/forms/EditablePropertyString.java @@ -0,0 +1,87 @@ +package org.argeo.app.swt.forms; + +import static org.argeo.app.swt.forms.FormStyle.propertyMessage; +import static org.argeo.app.swt.forms.FormStyle.propertyText; + +import javax.xml.namespace.QName; + +import org.argeo.api.acr.Content; +import org.argeo.cms.swt.CmsSwtUtils; +import org.argeo.cms.swt.SwtEditablePart; +import org.argeo.cms.swt.widgets.EditableText; +import org.argeo.cms.ux.acr.ContentPart; +import org.argeo.eclipse.ui.EclipseUiUtils; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +/** Editable String in a CMS context */ +public class EditablePropertyString extends EditableText implements SwtEditablePart, ContentPart { + private static final long serialVersionUID = 5055000749992803591L; + + private QName propertyName; + private String message; + + // encode the '&' character in rap + private final static String AMPERSAND = "&"; + private final static String AMPERSAND_REGEX = "&(?![#a-zA-Z0-9]+;)"; + + public EditablePropertyString(Composite parent, int style, Content node, QName propertyName, String message) { + super(parent, style); + // setUseTextAsLabel(true); + this.propertyName = propertyName; + this.message = message; + setData(node); + + if (node.containsKey(propertyName)) { + this.setStyle(propertyText.style()); + this.setText(node.attr(propertyName)); + } else { + this.setStyle(propertyMessage.style()); + this.setText(message + " "); + } + } + + public void setText(String text) { + Control child = getControl(); + if (child instanceof Label) { + Label lbl = (Label) child; + if (EclipseUiUtils.isEmpty(text)) + lbl.setText(message + " "); + else + // TODO enhance this + lbl.setText(text.replaceAll(AMPERSAND_REGEX, AMPERSAND)); + } else if (child instanceof Text) { + Text txt = (Text) child; + if (EclipseUiUtils.isEmpty(text)) { + txt.setText(""); + txt.setMessage(message + " "); + } else + txt.setText(text.replaceAll("
", "\n")); + } + } + + public synchronized void startEditing() { + CmsSwtUtils.style(getControl(), FormStyle.propertyText); + super.startEditing(); + } + + public synchronized void stopEditing() { + if (EclipseUiUtils.isEmpty(((Text) getControl()).getText())) + CmsSwtUtils.style(getControl(), FormStyle.propertyMessage); + else + CmsSwtUtils.style(getControl(), FormStyle.propertyText); + super.stopEditing(); + } + + public QName getPropertyName() { + return propertyName; + } + + @Override + public Content getContent() { + return (Content) getData(); + } + +} \ No newline at end of file diff --git a/swt/org.argeo.app.swt/src/org/argeo/app/swt/forms/FormConstants.java b/swt/org.argeo.app.swt/src/org/argeo/app/swt/forms/FormConstants.java new file mode 100644 index 0000000..1d45f46 --- /dev/null +++ b/swt/org.argeo.app.swt/src/org/argeo/app/swt/forms/FormConstants.java @@ -0,0 +1,7 @@ +package org.argeo.app.swt.forms; + +/** Constants used in the various CMS Forms */ +public interface FormConstants { + // DATAKEYS + public final static String LINKED_VALUE = "LinkedValue"; +} diff --git a/swt/org.argeo.app.swt/src/org/argeo/app/swt/forms/FormPageViewer.java b/swt/org.argeo.app.swt/src/org/argeo/app/swt/forms/FormPageViewer.java new file mode 100644 index 0000000..840855e --- /dev/null +++ b/swt/org.argeo.app.swt/src/org/argeo/app/swt/forms/FormPageViewer.java @@ -0,0 +1,567 @@ +package org.argeo.app.swt.forms; + +import java.io.IOException; +import java.io.InputStream; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.namespace.QName; + +import org.argeo.api.acr.Content; +import org.argeo.api.cms.CmsLog; +import org.argeo.api.cms.ux.Cms2DSize; +import org.argeo.api.cms.ux.CmsEditable; +import org.argeo.api.cms.ux.CmsImageManager; +import org.argeo.cms.swt.CmsSwtUtils; +import org.argeo.cms.swt.SwtEditablePart; +import org.argeo.cms.swt.acr.AbstractPageViewer; +import org.argeo.cms.swt.acr.Img; +import org.argeo.cms.swt.acr.SwtSection; +import org.argeo.cms.swt.acr.SwtSectionPart; +import org.argeo.cms.swt.widgets.EditableImage; +import org.argeo.cms.swt.widgets.StyledControl; +import org.argeo.eclipse.ui.EclipseUiUtils; +import org.eclipse.rap.fileupload.FileDetails; +import org.eclipse.rap.fileupload.FileUploadEvent; +import org.eclipse.rap.fileupload.FileUploadHandler; +import org.eclipse.rap.fileupload.FileUploadListener; +import org.eclipse.rap.fileupload.FileUploadReceiver; +import org.eclipse.rap.rwt.service.ServerPushSession; +import org.eclipse.rap.rwt.widgets.FileUpload; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.FocusEvent; +import org.eclipse.swt.events.FocusListener; +import org.eclipse.swt.events.MouseAdapter; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.layout.RowLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +/** Manage life cycle of a form page that is linked to a given node */ +public class FormPageViewer extends AbstractPageViewer { + private final static CmsLog log = CmsLog.getLog(FormPageViewer.class); + + private final SwtSection mainSection; + + // TODO manage within the CSS + private Integer labelColWidth = null; + private int rowLayoutHSpacing = 8; + + // Context cached in the viewer + // The reference to translate from text to calendar and reverse + private DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern(FormUtils.DEFAULT_SHORT_DATE_FORMAT); + // new SimpleDateFormat(FormUtils.DEFAULT_SHORT_DATE_FORMAT); + private CmsImageManager imageManager; + private FileUploadListener fileUploadListener; + + public FormPageViewer(SwtSection mainSection, int style, CmsEditable cmsEditable) { + super(mainSection, style, cmsEditable); + this.mainSection = mainSection; + + if (getCmsEditable().canEdit()) { + fileUploadListener = new FUL(); + } + } + + @Override + protected void prepare(SwtEditablePart part, Object caretPosition) { + if (part instanceof Img) { + // ((Img) part).setFileUploadListener(fileUploadListener); + } + } + + /** To be overridden.Save the edited part. */ + protected void save(SwtEditablePart part) { + Content node = null; + if (part instanceof EditableMultiStringProperty ept) { + List values = ept.getValues(); + node = ept.getContent(); + QName propName = ept.getPropertyName(); + if (values.isEmpty()) { + if (node.containsKey(propName)) + node.remove(propName); + } else { + node.put(propName, values); +// node.setProperty(propName, values.toArray(new String[0])); + } + // => Viewer : Controller + } else if (part instanceof EditablePropertyString ept) { + String txt = ((Text) ept.getControl()).getText(); + node = ept.getContent(); + QName propName = ept.getPropertyName(); + if (EclipseUiUtils.isEmpty(txt)) { + node.remove(propName); + } else { + setPropertySilently(node, propName, txt); + // node.setProperty(propName, txt); + } + // node.getSession().save(); + // => Viewer : Controller + } else if (part instanceof EditablePropertyDate) { + EditablePropertyDate ept = (EditablePropertyDate) part; + // FIXME deal with no value set + TemporalAccessor cal = FormUtils.parseDate(dateFormat, ((Text) ept.getControl()).getText()); + node = ept.getContent(); + QName propName = ept.getPropertyName(); + if (cal == null) { + node.remove(propName); + } else { + node.put(propName, cal); + } + // node.getSession().save(); + // => Viewer : Controller + } + // TODO: make this configurable, sometimes we do not want to save the + // current session at this stage +// if (node != null && node.getSession().hasPendingChanges()) { +// JcrUtils.updateLastModified(node, true); +// node.getSession().save(); +// } + } + + @Override + protected void updateContent(SwtEditablePart part) { + if (part instanceof EditableMultiStringProperty ept) { + Content node = ept.getContent(); + QName propName = ept.getPropertyName(); + List valStrings = new ArrayList(); + if (node.containsKey(propName)) { + for (String val : node.getMultiple(propName, String.class)) + valStrings.add(val); + } + ept.setValues(valStrings); + } else if (part instanceof EditablePropertyString ept) { + // || part instanceof EditableLink + Content node = ept.getContent(); + QName propName = ept.getPropertyName(); + ept.setText(node.get(propName, String.class).orElse("")); + } else if (part instanceof EditablePropertyDate ept) { + Content node = ept.getContent(); + QName propName = ept.getPropertyName(); + if (node.containsKey(propName)) + ept.setText(dateFormat.format(node.get(propName, Instant.class).get())); + else + ept.setText(""); + } else if (part instanceof SwtSectionPart sectionPart) { + Content partNode = sectionPart.getContent(); + // use control AFTER setting style, since it may have been reset + if (part instanceof EditableImage) { + EditableImage editableImage = (EditableImage) part; + imageManager().load(partNode, part.getControl(), editableImage.getPreferredImageSize()); + } + } + } + + // FILE UPLOAD LISTENER + protected class FUL implements FileUploadListener { + + public FUL() { + } + + public void uploadProgress(FileUploadEvent event) { + // TODO Monitor upload progress + } + + public void uploadFailed(FileUploadEvent event) { + throw new IllegalStateException("Upload failed " + event, event.getException()); + } + + public void uploadFinished(FileUploadEvent event) { + for (FileDetails file : event.getFileDetails()) { + if (log.isDebugEnabled()) + log.debug("Received: " + file.getFileName()); + } + mainSection.getDisplay().syncExec(new Runnable() { + @Override + public void run() { + saveEdit(); + } + }); + FileUploadHandler uploadHandler = (FileUploadHandler) event.getSource(); + uploadHandler.dispose(); + } + } + + // FOCUS OUT LISTENER + protected FocusListener createFocusListener() { + return new FocusOutListener(); + } + + private class FocusOutListener implements FocusListener { + private static final long serialVersionUID = -6069205786732354186L; + + @Override + public void focusLost(FocusEvent event) { + saveEdit(); + } + + @Override + public void focusGained(FocusEvent event) { + // does nothing; + } + } + + // MOUSE LISTENER + @Override + protected MouseListener createMouseListener() { + return new ML(); + } + + private class ML extends MouseAdapter { + private static final long serialVersionUID = 8526890859876770905L; + + @Override + public void mouseDoubleClick(MouseEvent e) { + if (e.button == 1) { + Control source = (Control) e.getSource(); + if (getCmsEditable().canEdit()) { + if (getCmsEditable().isEditing() && !(getEdited() instanceof Img)) { + if (source == mainSection) + return; + SwtEditablePart part = findDataParent(source); + upload(part); + } else { + getCmsEditable().startEditing(); + } + } + } + } + + @Override + public void mouseDown(MouseEvent e) { + if (getCmsEditable().isEditing()) { + if (e.button == 1) { + Control source = (Control) e.getSource(); + SwtEditablePart composite = findDataParent(source); + Point point = new Point(e.x, e.y); + if (!(composite instanceof Img)) + edit(composite, source.toDisplay(point)); + } else if (e.button == 3) { + // EditablePart composite = findDataParent((Control) e + // .getSource()); + // if (styledTools != null) + // styledTools.show(composite, new Point(e.x, e.y)); + } + } + } + + protected synchronized void upload(SwtEditablePart part) { + if (part instanceof SwtSectionPart) { + if (part instanceof Img) { + if (getEdited() == part) + return; + edit(part, null); + layout(part.getControl()); + } + } + } + } + + @Override + public Control getControl() { + return mainSection; + } + + protected CmsImageManager imageManager() { + if (imageManager == null) + imageManager = CmsSwtUtils.getCmsView(mainSection).getImageManager(); + return imageManager; + } + + // LOCAL UI HELPERS + protected SwtSection createSectionIfNeeded(Composite body, Content node) { + SwtSection section = null; + if (node != null) { + section = new SwtSection(body, SWT.NO_FOCUS, node); + section.setLayoutData(CmsSwtUtils.fillWidth()); + section.setLayout(CmsSwtUtils.noSpaceGridLayout()); + } + return section; + } + + protected void createSimpleLT(Composite bodyRow, Content node, QName propName, String label, String msg) { + if (getCmsEditable().canEdit() || node.containsKey(propName)) { + createPropertyLbl(bodyRow, label); + EditablePropertyString eps = new EditablePropertyString(bodyRow, SWT.WRAP | SWT.LEFT, node, propName, msg); + eps.setMouseListener(getMouseListener()); + eps.setFocusListener(getFocusListener()); + eps.setLayoutData(CmsSwtUtils.fillWidth()); + } + } + + protected void createMultiStringLT(Composite bodyRow, Content node, QName propName, String label, String msg) { + boolean canEdit = getCmsEditable().canEdit(); + if (canEdit || node.containsKey(propName)) { + createPropertyLbl(bodyRow, label); + + List valueStrings = new ArrayList(); + + if (node.containsKey(propName)) { + for (String value : node.getMultiple(propName, String.class)) + valueStrings.add(value); + } + + // TODO use a drop down to display possible values to the end user + EditableMultiStringProperty emsp = new EditableMultiStringProperty(bodyRow, SWT.SINGLE | SWT.LEAD, node, + propName, valueStrings, new String[] { "Implement this" }, msg, + canEdit ? getRemoveValueSelListener() : null); + addListeners(emsp); + // emsp.setMouseListener(getMouseListener()); + emsp.setStyle(FormStyle.propertyMessage.style()); + emsp.setLayoutData(CmsSwtUtils.fillWidth()); + } + } + + protected Label createPropertyLbl(Composite parent, String value) { + return createPropertyLbl(parent, value, SWT.NONE); + } + + protected Label createPropertyLbl(Composite parent, String value, int vAlign) { + // boolean isSmall = CmsView.getCmsView(parent).getUxContext().isSmall(); + Label label = new Label(parent, SWT.LEAD | SWT.WRAP); + label.setText(value + " "); + CmsSwtUtils.style(label, FormStyle.propertyLabel.style()); + GridData gd = new GridData(SWT.LEAD, vAlign, false, false); + if (labelColWidth != null) + gd.widthHint = labelColWidth; + label.setLayoutData(gd); + return label; + } + + protected Label newStyledLabel(Composite parent, String style, String value) { + Label label = new Label(parent, SWT.NONE); + label.setText(value); + CmsSwtUtils.style(label, style); + return label; + } + + protected Composite createRowLayoutComposite(Composite parent) { + Composite bodyRow = new Composite(parent, SWT.NO_FOCUS); + bodyRow.setLayoutData(CmsSwtUtils.fillWidth()); + RowLayout rl = new RowLayout(SWT.WRAP); + rl.type = SWT.HORIZONTAL; + rl.spacing = rowLayoutHSpacing; + rl.marginHeight = rl.marginWidth = 0; + rl.marginTop = rl.marginBottom = rl.marginLeft = rl.marginRight = 0; + bodyRow.setLayout(rl); + return bodyRow; + } + + protected Composite createAddImgComposite(SwtSection section, Composite parent, Content parentNode) { + + Composite body = new Composite(parent, SWT.NO_FOCUS); + body.setLayout(new GridLayout()); + + FormFileUploadReceiver receiver = new FormFileUploadReceiver(section, parentNode, null); + final FileUploadHandler currentUploadHandler = new FileUploadHandler(receiver); + if (fileUploadListener != null) + currentUploadHandler.addUploadListener(fileUploadListener); + + // Button creation + final FileUpload fileUpload = new FileUpload(body, SWT.BORDER); + fileUpload.setText("Import an image"); + fileUpload.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, true)); + fileUpload.addSelectionListener(new SelectionAdapter() { + private static final long serialVersionUID = 4869523412991968759L; + + @Override + public void widgetSelected(SelectionEvent e) { + ServerPushSession pushSession = new ServerPushSession(); + pushSession.start(); + String uploadURL = currentUploadHandler.getUploadUrl(); + fileUpload.submit(uploadURL); + } + }); + + return body; + } + + protected class FormFileUploadReceiver extends FileUploadReceiver { + + private Content context; + private SwtSection section; + private String name; + + public FormFileUploadReceiver(SwtSection section, Content context, String name) { + this.context = context; + this.section = section; + this.name = name; + } + + @Override + public void receive(InputStream stream, FileDetails details) throws IOException { + + if (name == null) + name = details.getFileName(); + + // TODO clean image name more carefully + String cleanedName = name.replaceAll("[^a-zA-Z0-9-.]", ""); + // We add a unique prefix to workaround the cache issue: when + // deleting and re-adding a new image with same name, the end user + // browser will use the cache and the image will remain unchanged + // for a while + cleanedName = System.currentTimeMillis() % 100000 + "_" + cleanedName; + + imageManager().uploadImage(context, context, cleanedName, stream, details.getContentType()); + // TODO clean refresh strategy + section.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + FormPageViewer.this.refresh(section); + section.layout(); + section.getParent().layout(); + } + }); + } + } + + protected void addListeners(StyledControl control) { + control.setMouseListener(getMouseListener()); + control.setFocusListener(getFocusListener()); + } + + protected Img createImgComposite(Composite parent, Content node, Point preferredSize) { + Img img = new Img(parent, SWT.NONE, node, new Cms2DSize(preferredSize.x, preferredSize.y)) { + private static final long serialVersionUID = 1297900641952417540L; + + @Override + protected void setContainerLayoutData(Composite composite) { + composite.setLayoutData(CmsSwtUtils.grabWidth(SWT.CENTER, SWT.DEFAULT)); + } + + @Override + protected void setControlLayoutData(Control control) { + control.setLayoutData(CmsSwtUtils.grabWidth(SWT.CENTER, SWT.DEFAULT)); + } + }; + img.setLayoutData(CmsSwtUtils.grabWidth(SWT.CENTER, SWT.DEFAULT)); + updateContent(img); + addListeners(img); + return img; + } + + protected Composite addDeleteAbility(final SwtSection section, Content sessionNode, int topWeight, + int rightWeight) { + Composite comp = new Composite(section, SWT.NONE); + comp.setLayoutData(CmsSwtUtils.fillAll()); + comp.setLayout(new FormLayout()); + + // The body to be populated + Composite body = new Composite(comp, SWT.NO_FOCUS); + body.setLayoutData(EclipseUiUtils.fillFormData()); + + if (getCmsEditable().canEdit()) { + // the delete button + Button deleteBtn = new Button(comp, SWT.FLAT); + CmsSwtUtils.style(deleteBtn, FormStyle.deleteOverlay.style()); + FormData formData = new FormData(); + formData.right = new FormAttachment(rightWeight, 0); + formData.top = new FormAttachment(topWeight, 0); + deleteBtn.setLayoutData(formData); + deleteBtn.moveAbove(body); + + deleteBtn.addSelectionListener(new SelectionAdapter() { + private static final long serialVersionUID = 4304223543657238462L; + + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); +// if (MessageDialog.openConfirm(section.getShell(), "Confirm deletion", +// "Are you really you want to remove this?")) { +// Session session; +// try { +// session = sessionNode.getSession(); +// SwtSection parSection = section.getParentSection(); +// sessionNode.remove(); +// session.save(); +// refresh(parSection); +// layout(parSection); +// } catch (RepositoryException re) { +// throw new JcrException("Unable to delete " + sessionNode, re); +// } +// +// } + + } + }); + } + return body; + } + +// // LOCAL HELPERS FOR NODE MANAGEMENT +// private Node getOrCreateNode(Node parent, String nodeName, String nodeType) throws RepositoryException { +// Node node = null; +// if (getCmsEditable().canEdit() && !parent.hasNode(nodeName)) { +// node = JcrUtils.mkdirs(parent, nodeName, nodeType); +// parent.getSession().save(); +// } +// +// if (getCmsEditable().canEdit() || parent.hasNode(nodeName)) +// node = parent.getNode(nodeName); +// +// return node; +// } + + private SelectionListener getRemoveValueSelListener() { + return new SelectionAdapter() { + private static final long serialVersionUID = 9022259089907445195L; + + @Override + public void widgetSelected(SelectionEvent e) { + Object source = e.getSource(); + if (source instanceof Button) { + Button btn = (Button) source; + Object obj = btn.getData(FormConstants.LINKED_VALUE); + SwtEditablePart ep = findDataParent(btn); + if (ep != null && ep instanceof EditableMultiStringProperty) { + EditableMultiStringProperty emsp = (EditableMultiStringProperty) ep; + List values = emsp.getValues(); + if (values.contains(obj)) { + values.remove(values.indexOf(obj)); + emsp.setValues(values); + save(emsp); + // TODO workaround to force refresh + edit(emsp, 0); + cancelEdit(); + layout(emsp); + } + } + } + } + }; + } + + protected void setPropertySilently(Content node, QName propName, String value) { + // TODO Clean this: + // Format strings to replace \n + value = value.replaceAll("\n", "
"); + // Do not make the update if validation fails +// try { +// MarkupValidatorCopy.getInstance().validate(value); +// } catch (Exception e) { +// log.warn("Cannot set [" + value + "] on prop " + propName + "of " + node +// + ", String cannot be validated - " + e.getMessage()); +// return; +// } + // TODO check if the newly created property is of the correct type, + // otherwise the property will be silently created with a STRING + // property type. + node.put(propName, value); + } +} \ No newline at end of file diff --git a/swt/org.argeo.app.swt/src/org/argeo/app/swt/forms/FormUtils.java b/swt/org.argeo.app.swt/src/org/argeo/app/swt/forms/FormUtils.java new file mode 100644 index 0000000..6ce8b54 --- /dev/null +++ b/swt/org.argeo.app.swt/src/org/argeo/app/swt/forms/FormUtils.java @@ -0,0 +1,173 @@ +package org.argeo.app.swt.forms; + +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.TemporalAccessor; + +import org.argeo.api.cms.CmsLog; +import org.argeo.eclipse.ui.EclipseUiUtils; + +/** Utilitary methods to ease implementation of CMS forms */ +public class FormUtils { + private final static CmsLog log = CmsLog.getLog(FormUtils.class); + + public final static String DEFAULT_SHORT_DATE_FORMAT = "dd/MM/yyyy"; + + /** Best effort to convert a String to a calendar. Fails silently */ + public static TemporalAccessor parseDate(DateTimeFormatter dateFormat, String calStr) { + if (EclipseUiUtils.notEmpty(calStr)) { + try { + return dateFormat.parse(calStr); +// cal = new GregorianCalendar(); +// cal.setTime(date); + } catch (DateTimeParseException pe) { + // Silent + log.warn("Unable to parse date: " + calStr + " - msg: " + pe.getMessage()); + throw pe; + } + } + return null; + } + +// /** Add a double click listener on tables that display a JCR node list */ +// public static void addCanonicalDoubleClickListener(final TableViewer v) { +// v.addDoubleClickListener(new IDoubleClickListener() { +// +// @Override +// public void doubleClick(DoubleClickEvent event) { +// CmsView cmsView = CmsUiUtils.getCmsView(); +// Node node = (Node) ((IStructuredSelection) event.getSelection()) +// .getFirstElement(); +// try { +// cmsView.navigateTo(node.getPath()); +// } catch (RepositoryException e) { +// throw new CmsException("Unable to get path for node " +// + node + " before calling navigateTo(path)", e); +// } +// } +// }); +// } + + // MANAGE ERROR DECORATION + +// public static ControlDecoration addDecoration(final Text text) { +// final ControlDecoration dynDecoration = new ControlDecoration(text, +// SWT.LEFT); +// Image icon = getDecorationImage(FieldDecorationRegistry.DEC_ERROR); +// dynDecoration.setImage(icon); +// dynDecoration.setMarginWidth(3); +// dynDecoration.hide(); +// return dynDecoration; +// } +// +// public static void refreshDecoration(Text text, ControlDecoration deco, +// boolean isValid, boolean clean) { +// if (isValid || clean) { +// text.setBackground(null); +// deco.hide(); +// } else { +// text.setBackground(new Color(text.getDisplay(), 250, 200, 150)); +// deco.show(); +// } +// } +// +// public static Image getDecorationImage(String image) { +// FieldDecorationRegistry registry = FieldDecorationRegistry.getDefault(); +// return registry.getFieldDecoration(image).getImage(); +// } +// +// public static void addCompulsoryDecoration(Label label) { +// final ControlDecoration dynDecoration = new ControlDecoration(label, +// SWT.RIGHT | SWT.TOP); +// Image icon = getDecorationImage(FieldDecorationRegistry.DEC_REQUIRED); +// dynDecoration.setImage(icon); +// dynDecoration.setMarginWidth(3); +// } + + // TODO the read only generation of read only links for various contact type + // should be factorised in the cms Utils. + /** + * Creates the read-only HTML snippet to display in a label with styling enabled + * in order to provide a click-able phone number + */ + public static String getPhoneLink(String value) { + return getPhoneLink(value, value); + } + + /** + * Creates the read-only HTML snippet to display in a label with styling enabled + * in order to provide a click-able phone number + * + * @param value + * @param label a potentially distinct label + * @return the link + */ + public static String getPhoneLink(String value, String label) { + StringBuilder builder = new StringBuilder(); + builder.append("").append(label).append(""); + return builder.toString(); + } + + /** + * Creates the read-only HTML snippet to display in a label with styling enabled + * in order to provide a click-able mail + */ + public static String getMailLink(String value) { + return getMailLink(value, value); + } + + /** + * Creates the read-only HTML snippet to display in a label with styling enabled + * in order to provide a click-able mail + * + * @param value + * @param label a potentially distinct label + * @return the link + */ + public static String getMailLink(String value, String label) { + StringBuilder builder = new StringBuilder(); + value = replaceAmpersand(value); + builder.append("").append(label).append(""); + return builder.toString(); + } + + /** + * Creates the read-only HTML snippet to display in a label with styling enabled + * in order to provide a click-able link + */ + public static String getUrlLink(String value) { + return getUrlLink(value, value); + } + + /** + * Creates the read-only HTML snippet to display in a label with styling enabled + * in order to provide a click-able link + */ + public static String getUrlLink(String value, String label) { + StringBuilder builder = new StringBuilder(); + value = replaceAmpersand(value); + label = replaceAmpersand(label); + if (!(value.startsWith("http://") || value.startsWith("https://"))) + value = "http://" + value; + builder.append("" + label + ""); + return builder.toString(); + } + + private static String AMPERSAND = "&"; + + /** + * Cleans a String by replacing any '&' by its HTML encoding '&' to + * avoid SAXParseException while rendering HTML with RWT + */ + public static String replaceAmpersand(String value) { + value = value.replaceAll("&(?![#a-zA-Z0-9]+;)", AMPERSAND); + return value; + } + + // Prevents instantiation + private FormUtils() { + } +}