From 963ae5174de8ec335d2e8ddf3ed8f2be5a297e19 Mon Sep 17 00:00:00 2001 From: Bruno Sinou Date: Wed, 4 Nov 2015 10:27:10 +0000 Subject: [PATCH] Add a first draft of form management abilities git-svn-id: https://svn.argeo.org/commons/trunk@8541 4cfe0d0a-d680-48aa-b62c-e0a02a3f76cc --- .../src/org/argeo/cms/forms/EditableLink.java | 74 ++ .../forms/EditableMultiStringProperty.java | 267 +++++++ .../argeo/cms/forms/EditablePropertyDate.java | 304 ++++++++ .../cms/forms/EditablePropertyString.java | 74 ++ .../org/argeo/cms/forms/FormConstants.java | 7 + .../org/argeo/cms/forms/FormEditorHeader.java | 114 +++ .../org/argeo/cms/forms/FormPageViewer.java | 662 ++++++++++++++++++ .../src/org/argeo/cms/forms/FormStyle.java | 26 + .../src/org/argeo/cms/forms/FormUtils.java | 201 ++++++ .../internal/text/MarkupValidatorCopy.java | 316 +++++---- 10 files changed, 1893 insertions(+), 152 deletions(-) create mode 100644 org.argeo.cms/src/org/argeo/cms/forms/EditableLink.java create mode 100644 org.argeo.cms/src/org/argeo/cms/forms/EditableMultiStringProperty.java create mode 100644 org.argeo.cms/src/org/argeo/cms/forms/EditablePropertyDate.java create mode 100644 org.argeo.cms/src/org/argeo/cms/forms/EditablePropertyString.java create mode 100644 org.argeo.cms/src/org/argeo/cms/forms/FormConstants.java create mode 100644 org.argeo.cms/src/org/argeo/cms/forms/FormEditorHeader.java create mode 100644 org.argeo.cms/src/org/argeo/cms/forms/FormPageViewer.java create mode 100644 org.argeo.cms/src/org/argeo/cms/forms/FormStyle.java create mode 100644 org.argeo.cms/src/org/argeo/cms/forms/FormUtils.java diff --git a/org.argeo.cms/src/org/argeo/cms/forms/EditableLink.java b/org.argeo.cms/src/org/argeo/cms/forms/EditableLink.java new file mode 100644 index 000000000..17e3255b1 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/forms/EditableLink.java @@ -0,0 +1,74 @@ +package org.argeo.cms.forms; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.argeo.cms.viewers.EditablePart; +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 + EditablePart { + private static final long serialVersionUID = 5055000749992803591L; + + private String type; + private String message; + private boolean readOnly; + + public EditableLink(Composite parent, int style, Node node, + String propertyName, String type, String message) + throws RepositoryException { + super(parent, style, node, propertyName, message); + this.message = message; + this.type = type; + + readOnly = SWT.READ_ONLY == (style & SWT.READ_ONLY); + if (node.hasProperty(propertyName)) { + this.setStyle(FormStyle.propertyText.style()); + this.setText(node.getProperty(propertyName).getString()); + } 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 (FormUtils.notEmpty(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 (FormUtils.notEmpty(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/org.argeo.cms/src/org/argeo/cms/forms/EditableMultiStringProperty.java b/org.argeo.cms/src/org/argeo/cms/forms/EditableMultiStringProperty.java new file mode 100644 index 000000000..629630b6b --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/forms/EditableMultiStringProperty.java @@ -0,0 +1,267 @@ +package org.argeo.cms.forms; + +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.argeo.cms.util.CmsUtils; +import org.argeo.cms.viewers.EditablePart; +import org.argeo.cms.widgets.StyledControl; +import org.argeo.eclipse.ui.EclipseUiUtils; +import org.eclipse.jface.dialogs.MessageDialog; +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 StyledControl implements + EditablePart { + private static final long serialVersionUID = -7044614381252178595L; + + private String propertyName; + private String message; + // TODO implement the ability to provide a list of legal 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, Node node, + String propertyName, List values, String[] possibleValues, + String addValueMsg, SelectionListener removeValueSelectionListener) + throws RepositoryException { + super(parent, style, node, true); + + this.propertyName = propertyName; + this.values = values; + this.possibleValues = possibleValues; + 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(CmsUtils.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); + CmsUtils.markup(label); + CmsUtils.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); + CmsUtils.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); + CmsUtils.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 (FormUtils.notEmpty(value)) + return; + + if (values.contains(value)) + errMsg = "Dupplicated value: " + value + + ", please correct and try again"; + if (errMsg != null) + MessageDialog.openError(this.getShell(), "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); + CmsUtils.style(lbl, style); + CmsUtils.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() { + getControl().setData(STYLE, FormStyle.propertyText.style()); + super.startEditing(); + } + + public synchronized void stopEditing() { + getControl().setData(STYLE, FormStyle.propertyMessage.style()); + super.stopEditing(); + } + + public String getPropertyName() { + return propertyName; + } +} \ No newline at end of file diff --git a/org.argeo.cms/src/org/argeo/cms/forms/EditablePropertyDate.java b/org.argeo.cms/src/org/argeo/cms/forms/EditablePropertyDate.java new file mode 100644 index 000000000..83580df63 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/forms/EditablePropertyDate.java @@ -0,0 +1,304 @@ +package org.argeo.cms.forms; + +import java.text.DateFormat; +import java.util.Calendar; +import java.util.GregorianCalendar; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.argeo.cms.util.CmsUtils; +import org.argeo.cms.viewers.EditablePart; +import org.argeo.cms.widgets.StyledControl; +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 StyledControl implements EditablePart { + private static final long serialVersionUID = 2500215515778162468L; + + // Context + private String propertyName; + private String message; + private DateFormat 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, Node node, + String propertyName, String message, DateFormat dateFormat) + throws RepositoryException { + super(parent, style, node, false); + + this.propertyName = propertyName; + this.message = message; + this.dateFormat = dateFormat; + + if (node.hasProperty(propertyName)) { + this.setStyle(FormStyle.propertyText.style()); + this.setText(dateFormat.format(node.getProperty(propertyName) + .getDate().getTime())); + } 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 (FormUtils.notEmpty(text)) + lbl.setText(message); + else + lbl.setText(text); + } else if (child instanceof Text) { + Text txt = (Text) child; + if (FormUtils.notEmpty(text)) { + txt.setText(""); + } else + txt.setText(text); + } + } + + public synchronized void startEditing() { + // if (dateTxt != null && !dateTxt.isDisposed()) + getControl().setData(STYLE, FormStyle.propertyText.style()); + super.startEditing(); + } + + public synchronized void stopEditing() { + if (FormUtils.notEmpty(dateTxt.getText())) + getControl().setData(STYLE, FormStyle.propertyMessage.style()); + else + getControl().setData(STYLE, FormStyle.propertyText.style()); + super.stopEditing(); + } + + public String 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(CmsUtils.fillWidth()); + CmsUtils.style(lbl, style); + CmsUtils.markup(lbl); + if (mouseListener != null) + lbl.addMouseListener(mouseListener); + return lbl; + } + + private Control createCustomEditableControl(Composite box, String style) { + box.setLayoutData(CmsUtils.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); + CmsUtils.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); + CmsUtils.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 + CmsUtils.markup(CalendarPopup.this); + CmsUtils.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); + 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.getTime()); + dateTxt.setText(dateStr); + } + + protected void populate() { + setLayout(EclipseUiUtils.noSpaceGridLayout()); + + dateTimeCtl = new DateTime(this, SWT.CALENDAR); + dateTimeCtl.setLayoutData(EclipseUiUtils.fillAll()); + + Calendar calendar = FormUtils.parseDate(dateFormat, + dateTxt.getText()); + + if (calendar != null) + dateTimeCtl.setDate(calendar.get(Calendar.YEAR), + calendar.get(Calendar.MONTH), + calendar.get(Calendar.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/org.argeo.cms/src/org/argeo/cms/forms/EditablePropertyString.java b/org.argeo.cms/src/org/argeo/cms/forms/EditablePropertyString.java new file mode 100644 index 000000000..e86312922 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/forms/EditablePropertyString.java @@ -0,0 +1,74 @@ +package org.argeo.cms.forms; + +import static org.argeo.cms.forms.FormStyle.propertyMessage; +import static org.argeo.cms.forms.FormStyle.propertyText; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.argeo.cms.viewers.EditablePart; +import org.argeo.cms.widgets.EditableText; +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 + EditablePart { + private static final long serialVersionUID = 5055000749992803591L; + + private String propertyName; + private String message; + + public EditablePropertyString(Composite parent, int style, Node node, + String propertyName, String message) throws RepositoryException { + super(parent, style, node, true); + + this.propertyName = propertyName; + this.message = message; + + if (node.hasProperty(propertyName)) { + this.setStyle(propertyText.style()); + this.setText(node.getProperty(propertyName).getString()); + } 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 (FormUtils.notEmpty(text)) + lbl.setText(message + " "); + else + lbl.setText(text); + } else if (child instanceof Text) { + Text txt = (Text) child; + if (FormUtils.notEmpty(text)) { + txt.setText(""); + txt.setMessage(message + " "); + } else + txt.setText(text.replaceAll("
", "\n")); + } + } + + public synchronized void startEditing() { + getControl().setData(STYLE, propertyText.style()); + super.startEditing(); + } + + public synchronized void stopEditing() { + if (FormUtils.notEmpty(((Text) getControl()).getText())) + getControl().setData(STYLE, propertyMessage.style()); + else + getControl().setData(STYLE, propertyText.style()); + super.stopEditing(); + } + + public String getPropertyName() { + return propertyName; + } +} \ No newline at end of file diff --git a/org.argeo.cms/src/org/argeo/cms/forms/FormConstants.java b/org.argeo.cms/src/org/argeo/cms/forms/FormConstants.java new file mode 100644 index 000000000..18df3e47f --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/forms/FormConstants.java @@ -0,0 +1,7 @@ +package org.argeo.cms.forms; + +/** Constants used in the various CMS Forms */ +public interface FormConstants { + // DATAKEYS + public final static String LINKED_VALUE = "LinkedValue"; +} diff --git a/org.argeo.cms/src/org/argeo/cms/forms/FormEditorHeader.java b/org.argeo.cms/src/org/argeo/cms/forms/FormEditorHeader.java new file mode 100644 index 000000000..c12e2f000 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/forms/FormEditorHeader.java @@ -0,0 +1,114 @@ +package org.argeo.cms.forms; + +import java.util.Observable; +import java.util.Observer; + +import javax.jcr.Node; + +import org.argeo.cms.CmsEditable; +import org.argeo.cms.util.CmsUtils; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; + +/** Add life cycle management abilities to an editable form page */ +public class FormEditorHeader implements SelectionListener, Observer { + private static final long serialVersionUID = 7392898696542484282L; + + // private final Node context; + private final CmsEditable cmsEditable; + private Button publishBtn; + + // Should we provide here the ability to switch from read only to edition + // mode? + // private Button editBtn; + // private boolean readOnly; + + // TODO add information about the current node status, typically if it is + // dirty or not + + private Composite parent; + private Composite display; + private Object layoutData; + + public FormEditorHeader(Composite parent, int style, Node context, + CmsEditable cmsEditable) { + this.cmsEditable = cmsEditable; + this.parent = parent; + // readOnly = SWT.READ_ONLY == (style & SWT.READ_ONLY); + // this.context = context; + if (this.cmsEditable instanceof Observable) + ((Observable) this.cmsEditable).addObserver(this); + refresh(); + } + + public void setLayoutData(Object layoutData) { + this.layoutData = layoutData; + if (display != null && !display.isDisposed()) + display.setLayoutData(layoutData); + } + + protected void refresh() { + if (display != null && !display.isDisposed()) + display.dispose(); + + display = new Composite(parent, SWT.NONE); + display.setLayoutData(layoutData); + + CmsUtils.style(display, FormStyle.header.style()); + display.setBackgroundMode(SWT.INHERIT_FORCE); + + display.setLayout(CmsUtils.noSpaceGridLayout()); + + publishBtn = createSimpleBtn(display, getPublishButtonLabel()); + display.moveAbove(null); + parent.layout(); + } + + private Button createSimpleBtn(Composite parent, String label) { + Button button = new Button(parent, SWT.FLAT | SWT.PUSH); + button.setText(label); + CmsUtils.style(button, FormStyle.header.style()); + button.addSelectionListener(this); + return button; + } + + private String getPublishButtonLabel() { + // Rather check if the current node differs from what has been + // previously committed + // For the time being, we always reach here, the underlying CmsEditable + // is always editing. + if (cmsEditable.isEditing()) + return " Publish "; + else + return " Edit "; + } + + @Override + public void widgetSelected(SelectionEvent e) { + if (e.getSource() == publishBtn) { + // For the time being, the underlying CmsEditable + // is always editing when we reach this point + if (cmsEditable.isEditing()) { + // we always leave the node in a check outed state + cmsEditable.stopEditing(); + cmsEditable.startEditing(); + } else { + cmsEditable.startEditing(); + } + } + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + } + + @Override + public void update(Observable o, Object arg) { + if (o == cmsEditable) { + refresh(); + } + } +} \ No newline at end of file diff --git a/org.argeo.cms/src/org/argeo/cms/forms/FormPageViewer.java b/org.argeo.cms/src/org/argeo/cms/forms/FormPageViewer.java new file mode 100644 index 000000000..7eddbbc2e --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/forms/FormPageViewer.java @@ -0,0 +1,662 @@ +package org.argeo.cms.forms; + +import java.io.IOException; +import java.io.InputStream; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.argeo.ArgeoException; +import org.argeo.cms.CmsEditable; +import org.argeo.cms.CmsException; +import org.argeo.cms.CmsImageManager; +import org.argeo.cms.CmsNames; +import org.argeo.cms.internal.text.MarkupValidatorCopy; +import org.argeo.cms.text.Img; +import org.argeo.cms.util.CmsUtils; +import org.argeo.cms.viewers.AbstractPageViewer; +import org.argeo.cms.viewers.EditablePart; +import org.argeo.cms.viewers.Section; +import org.argeo.cms.viewers.SectionPart; +import org.argeo.cms.widgets.EditableImage; +import org.argeo.cms.widgets.StyledControl; +import org.argeo.eclipse.ui.EclipseUiUtils; +import org.argeo.jcr.JcrUtils; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.rap.addons.fileupload.FileDetails; +import org.eclipse.rap.addons.fileupload.FileUploadEvent; +import org.eclipse.rap.addons.fileupload.FileUploadHandler; +import org.eclipse.rap.addons.fileupload.FileUploadListener; +import org.eclipse.rap.addons.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 Log log = LogFactory.getLog(FormPageViewer.class); + private static final long serialVersionUID = 5277789504209413500L; + + private final Section mainSection; + + // TODO manage within the CSS + private int labelColWidth = 150; + private int sectionSeparatorHeight = 10; + private int sectionBodyVIndent = 30; + private int sectionBodyHSpacing = 15; + private int sectionBodyVSpacing = 15; + private int rowLayoutHSpacing = 8; + + // Context cached in the viewer + // The reference to translate from text to calendar and reverse + private DateFormat dateFormat = new SimpleDateFormat( + FormUtils.DEFAULT_SHORT_DATE_FORMAT); + private CmsImageManager imageManager; + private FileUploadListener fileUploadListener; + + public FormPageViewer(Section mainSection, int style, + CmsEditable cmsEditable) throws RepositoryException { + super(mainSection, style, cmsEditable); + this.mainSection = mainSection; + + if (getCmsEditable().canEdit()) { + fileUploadListener = new FUL(); + } + } + + @Override + protected void prepare(EditablePart part, Object caretPosition) { + if (part instanceof Img) { + ((Img) part).setFileUploadListener(fileUploadListener); + } + } + + /** To be overridden.Save the edited part. */ + protected void save(EditablePart part) throws RepositoryException { + Node node = null; + if (part instanceof EditableMultiStringProperty) { + EditableMultiStringProperty ept = (EditableMultiStringProperty) part; + // SWT : View + List values = ept.getValues(); + // JCR : Model + node = ept.getNode(); + String propName = ept.getPropertyName(); + if (values.isEmpty()) { + if (node.hasProperty(propName)) + node.getProperty(propName).remove(); + } else { + node.setProperty(propName, values.toArray(new String[0])); + } + // => Viewer : Controller + } else if (part instanceof EditablePropertyString) { + EditablePropertyString ept = (EditablePropertyString) part; + // SWT : View + String txt = ((Text) ept.getControl()).getText(); + // JCR : Model + node = ept.getNode(); + String propName = ept.getPropertyName(); + if (FormUtils.notEmpty(txt)) { + if (node.hasProperty(propName)) + node.getProperty(propName).remove(); + } else { + setPropertySilently(node, propName, txt); + // node.setProperty(propName, txt); + } + // node.getSession().save(); + // => Viewer : Controller + } else if (part instanceof EditablePropertyDate) { + EditablePropertyDate ept = (EditablePropertyDate) part; + Calendar cal = FormUtils.parseDate(dateFormat, + ((Text) ept.getControl()).getText()); + node = ept.getNode(); + String propName = ept.getPropertyName(); + if (cal == null) { + if (node.hasProperty(propName)) + node.getProperty(propName).remove(); + } else { + node.setProperty(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); + node.getSession().save(); + } + } + + @Override + protected void updateContent(EditablePart part) throws RepositoryException { + if (part instanceof EditableMultiStringProperty) { + EditableMultiStringProperty ept = (EditableMultiStringProperty) part; + // SWT : View + Node node = ept.getNode(); + String propName = ept.getPropertyName(); + List valStrings = new ArrayList(); + if (node.hasProperty(propName)) { + Value[] values = node.getProperty(propName).getValues(); + for (Value val : values) + valStrings.add(val.getString()); + } + ept.setValues(valStrings); + } else if (part instanceof EditablePropertyString) { + // || part instanceof EditableLink + EditablePropertyString ept = (EditablePropertyString) part; + // JCR : Model + Node node = ept.getNode(); + String propName = ept.getPropertyName(); + if (node.hasProperty(propName)) { + String value = node.getProperty(propName).getString(); + ept.setText(value); + } else + ept.setText(""); + // => Viewer : Controller + } else if (part instanceof EditablePropertyDate) { + EditablePropertyDate ept = (EditablePropertyDate) part; + // JCR : Model + Node node = ept.getNode(); + String propName = ept.getPropertyName(); + if (node.hasProperty(propName)) + ept.setText(dateFormat.format(node.getProperty(propName) + .getDate().getTime())); + else + ept.setText(""); + } else if (part instanceof SectionPart) { + SectionPart sectionPart = (SectionPart) part; + Node partNode = sectionPart.getNode(); + // 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 CmsException("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; + EditablePart 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(); + EditablePart 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(EditablePart part) { + if (part instanceof SectionPart) { + 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 = CmsUtils.getCmsView().getImageManager(); + return imageManager; + } + + // LOCAL UI HELPERS + protected Section createSectionIfNeeded(Composite body, Node node) + throws RepositoryException { + Section section = null; + if (node != null) { + section = new Section(body, SWT.NO_FOCUS, node); + section.setLayoutData(CmsUtils.fillWidth()); + section.setLayout(CmsUtils.noSpaceGridLayout()); + } + return section; + } + + protected void createSimpleLT(Composite bodyRow, Node node, + String propName, String label, String msg) + throws RepositoryException { + if (getCmsEditable().canEdit() || node.hasProperty(propName)) { + createPropertyLbl(bodyRow, label); + EditablePropertyString eps = new EditablePropertyString(bodyRow, + SWT.WRAP | SWT.LEFT, node, propName, msg); + eps.setMouseListener(getMouseListener()); + eps.setFocusListener(getFocusListener()); + eps.setLayoutData(CmsUtils.fillWidth()); + } + } + + protected void createMultiStringLT(Composite bodyRow, Node node, + String propName, String label, String msg) + throws RepositoryException { + boolean canEdit = getCmsEditable().canEdit(); + if (canEdit || node.hasProperty(propName)) { + createPropertyLbl(bodyRow, label); + + List valueStrings = new ArrayList(); + + if (node.hasProperty(propName)) { + Value[] values = node.getProperty(propName).getValues(); + for (Value value : values) + valueStrings.add(value.getString()); + } + + // 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(CmsUtils.fillWidth()); + } + } + + protected Label createPropertyLbl(Composite parent, String value) { + return createPropertyLbl(parent, value, SWT.TOP); + } + + protected Label createPropertyLbl(Composite parent, String value, int vAlign) { + Label label = new Label(parent, SWT.RIGHT | SWT.WRAP); + label.setText(value + " "); + CmsUtils.style(label, FormStyle.propertyLabel.style()); + GridData gd = new GridData(SWT.RIGHT, vAlign, false, false); + 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); + CmsUtils.style(label, style); + return label; + } + + protected Composite createRowLayoutComposite(Composite parent) + throws RepositoryException { + Composite bodyRow = new Composite(parent, SWT.NO_FOCUS); + bodyRow.setLayoutData(CmsUtils.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 createSectionBody(Composite parent, int nbOfCol) { + // The separator line. Ugly workaround that should be better managed via + // css + Composite header = new Composite(parent, SWT.NO_FOCUS); + CmsUtils.style(header, FormStyle.sectionHeader.style()); + GridData gd = CmsUtils.fillWidth(); + gd.verticalIndent = sectionSeparatorHeight; + gd.heightHint = 0; + header.setLayoutData(gd); + + Composite bodyRow = new Composite(parent, SWT.NO_FOCUS); + gd = CmsUtils.fillWidth(); + gd.verticalIndent = sectionBodyVIndent; + bodyRow.setLayoutData(gd); + GridLayout gl = new GridLayout(nbOfCol, false); + gl.horizontalSpacing = sectionBodyHSpacing; + gl.verticalSpacing = sectionBodyVSpacing; + bodyRow.setLayout(gl); + CmsUtils.style(bodyRow, FormStyle.section.style()); + + return bodyRow; + } + + protected Composite createAddImgComposite(final Section section, + Composite parent, final Node parentNode) throws RepositoryException { + + 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 implements + CmsNames { + + private Node context; + private Section section; + private String name; + + public FormFileUploadReceiver(Section section, Node 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(); + try { + imageManager().uploadImage(context, name, stream); + // TODO clean refresh strategy + section.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + try { + FormPageViewer.this.refresh(section); + section.layout(); + section.getParent().layout(); + } catch (RepositoryException re) { + throw new ArgeoException("unable to refresh " + + "image section for " + context); + } + } + }); + } catch (RepositoryException re) { + throw new ArgeoException("unable to upload image " + name + + " at " + context); + } + } + } + + protected void addListeners(StyledControl control) { + control.setMouseListener(getMouseListener()); + control.setFocusListener(getFocusListener()); + } + + protected Img createImgComposite(Composite parent, Node node, + Point preferredSize) throws RepositoryException { + Img img = new Img(parent, SWT.NONE, node, preferredSize) { + private static final long serialVersionUID = 1297900641952417540L; + + @Override + protected void setContainerLayoutData(Composite composite) { + composite.setLayoutData(CmsUtils.grabWidth(SWT.CENTER, + SWT.DEFAULT)); + } + + @Override + protected void setControlLayoutData(Control control) { + control.setLayoutData(CmsUtils.grabWidth(SWT.CENTER, + SWT.DEFAULT)); + } + }; + img.setLayoutData(CmsUtils.grabWidth(SWT.CENTER, SWT.DEFAULT)); + updateContent(img); + addListeners(img); + return img; + } + + protected Composite addDeleteAbility(final Section section, + final Node sessionNode, int topWeight, int rightWeight) { + Composite comp = new Composite(section, SWT.NONE); + comp.setLayoutData(CmsUtils.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); + CmsUtils.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(); + Section parSection = section.getParentSection(); + sessionNode.remove(); + session.save(); + refresh(parSection); + layout(parSection); + } catch (RepositoryException re) { + throw new ArgeoException("Unable to delete " + + sessionNode, re); + } + + } + + } + }); + } + return body; + } + + // LOCAL HELPERS FOR NODE MANAGEMENT + protected Node getOrCreateNode(Node parent, String nodeType, String nodeName) + 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); + EditablePart 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); + try { + save(emsp); + // TODO workaround to force refresh + edit(emsp, 0); + cancelEdit(); + } catch (RepositoryException e1) { + throw new ArgeoException( + "Unable to remove value " + obj, e1); + } + layout(emsp); + } + } + } + } + }; + } + + protected void setPropertySilently(Node node, String propName, String value) + throws RepositoryException { + try { + // 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.setProperty(propName, value); + } catch (ValueFormatException vfe) { + log.warn("Cannot set [" + value + "] on prop " + propName + "of " + + node + " - " + vfe.getMessage()); + } + } +} \ No newline at end of file diff --git a/org.argeo.cms/src/org/argeo/cms/forms/FormStyle.java b/org.argeo.cms/src/org/argeo/cms/forms/FormStyle.java new file mode 100644 index 000000000..01f120826 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/forms/FormStyle.java @@ -0,0 +1,26 @@ +package org.argeo.cms.forms; + +/** Syles used */ +public enum FormStyle { + // Main + form, title, + // main part + header, headerBtn, headerCombo, section, sectionHeader, + // Property fields + propertyLabel, propertyText, propertyMessage, + // Date + popupCalendar, + // Buttons + starOverlay, deleteOverlay, updateOverlay, deleteOverlaySmall, calendar, delete, + // Contacts + email, address, phone, website, + // Social Media + facebook, twitter, linkedIn, instagram; + + public String style() { + return form.name() + '_' + name(); + } + + // TODO clean button style management + public final static String BUTTON_SUFFIX = "_btn"; +} diff --git a/org.argeo.cms/src/org/argeo/cms/forms/FormUtils.java b/org.argeo.cms/src/org/argeo/cms/forms/FormUtils.java new file mode 100644 index 000000000..400aa735c --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/forms/FormUtils.java @@ -0,0 +1,201 @@ +package org.argeo.cms.forms; + +import java.text.DateFormat; +import java.text.ParseException; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.argeo.ArgeoException; +import org.argeo.cms.CmsView; +import org.argeo.cms.util.CmsUtils; +import org.eclipse.jface.fieldassist.ControlDecoration; +import org.eclipse.jface.fieldassist.FieldDecorationRegistry; +import org.eclipse.jface.viewers.DoubleClickEvent; +import org.eclipse.jface.viewers.IDoubleClickListener; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +/** Utilitary methods to ease implementation of CMS forms */ +public class FormUtils { + private final static Log log = LogFactory.getLog(FormUtils.class); + + public final static String DEFAULT_SHORT_DATE_FORMAT = "dd/MM/yyyy"; + + /** Simply checks if a string is not null nor empty */ + public static boolean notEmpty(String stringToTest) { + return stringToTest == null || "".equals(stringToTest.trim()); + } + + /** Best effort to convert a String to a calendar. Fails silently */ + public static Calendar parseDate(DateFormat dateFormat, String calStr) { + Calendar cal = null; + if (notEmpty(calStr)) { + try { + Date date = dateFormat.parse(calStr); + cal = new GregorianCalendar(); + cal.setTime(date); + } catch (ParseException pe) { + // Silent + log.warn("Unable to parse date: " + calStr + " - msg: " + + pe.getMessage()); + } + } + return cal; + } + + /** 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 = CmsUtils.getCmsView(); + Node node = (Node) ((IStructuredSelection) event.getSelection()) + .getFirstElement(); + try { + cmsView.navigateTo(node.getPath()); + } catch (RepositoryException e) { + throw new ArgeoException("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 + */ + 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 + */ + 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() { + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/internal/text/MarkupValidatorCopy.java b/org.argeo.cms/src/org/argeo/cms/internal/text/MarkupValidatorCopy.java index 39ce3c374..e28d3704c 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/text/MarkupValidatorCopy.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/text/MarkupValidatorCopy.java @@ -10,163 +10,175 @@ import java.util.Map; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; +import org.argeo.cms.forms.FormPageViewer; import org.eclipse.rap.rwt.SingletonUtil; import org.eclipse.swt.widgets.Widget; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.helpers.DefaultHandler; -/** Copy of RAP v2.3 since it is in an internal package. */ -class MarkupValidatorCopy { - - // Used by Eclipse Scout project - public static final String MARKUP_VALIDATION_DISABLED - = "org.eclipse.rap.rwt.markupValidationDisabled"; - - private static final String DTD = createDTD(); - private static final Map SUPPORTED_ELEMENTS = createSupportedElementsMap(); - private final SAXParser saxParser; - - public static MarkupValidatorCopy getInstance() { - return SingletonUtil.getSessionInstance( MarkupValidatorCopy.class ); - } - - public MarkupValidatorCopy() { - saxParser = createSAXParser(); - } - - public void validate( String text ) { - StringBuilder markup = new StringBuilder(); - markup.append( DTD ); - markup.append( "" ); - markup.append( text ); - markup.append( "" ); - InputSource inputSource = new InputSource( new StringReader( markup.toString() ) ); - try { - saxParser.parse( inputSource, new MarkupHandler() ); - } catch( RuntimeException exception ) { - throw exception; - } catch( Exception exception ) { - throw new IllegalArgumentException( "Failed to parse markup text", exception ); - } - } - - public static boolean isValidationDisabledFor( Widget widget ) { - return Boolean.TRUE.equals( widget.getData( MARKUP_VALIDATION_DISABLED ) ); - } - - private static SAXParser createSAXParser() { - SAXParser result = null; - SAXParserFactory parserFactory = SAXParserFactory.newInstance(); - try { - result = parserFactory.newSAXParser(); - } catch( Exception exception ) { - throw new RuntimeException( "Failed to create SAX parser", exception ); - } - return result; - } - - private static String createDTD() { - StringBuilder result = new StringBuilder(); - result.append( "" ); - result.append( "" ); - result.append( "" ); - result.append( "" ); - result.append( "" ); - result.append( "" ); - result.append( "" ); - result.append( "" ); - result.append( "" ); - result.append( "" ); - result.append( "]>" ); - return result.toString(); - } - - private static Map createSupportedElementsMap() { - Map result = new HashMap(); - result.put( "html", new String[ 0 ] ); - result.put( "br", new String[ 0 ] ); - result.put( "b", new String[] { "style" } ); - result.put( "strong", new String[] { "style" } ); - result.put( "i", new String[] { "style" } ); - result.put( "em", new String[] { "style" } ); - result.put( "sub", new String[] { "style" } ); - result.put( "sup", new String[] { "style" } ); - result.put( "big", new String[] { "style" } ); - result.put( "small", new String[] { "style" } ); - result.put( "del", new String[] { "style" } ); - result.put( "ins", new String[] { "style" } ); - result.put( "code", new String[] { "style" } ); - result.put( "samp", new String[] { "style" } ); - result.put( "kbd", new String[] { "style" } ); - result.put( "var", new String[] { "style" } ); - result.put( "cite", new String[] { "style" } ); - result.put( "dfn", new String[] { "style" } ); - result.put( "q", new String[] { "style" } ); - result.put( "abbr", new String[] { "style", "title" } ); - result.put( "span", new String[] { "style" } ); - result.put( "img", new String[] { "style", "src", "width", "height", "title", "alt" } ); - result.put( "a", new String[] { "style", "href", "target", "title" } ); - return result; - } - - private static class MarkupHandler extends DefaultHandler { - - @Override - public void startElement( String uri, String localName, String name, Attributes attributes ) { - checkSupportedElements( name, attributes ); - checkSupportedAttributes( name, attributes ); - checkMandatoryAttributes( name, attributes ); - } - - private static void checkSupportedElements( String elementName, Attributes attributes ) { - if( !SUPPORTED_ELEMENTS.containsKey( elementName ) ) { - throw new IllegalArgumentException( "Unsupported element in markup text: " + elementName ); - } - } - - private static void checkSupportedAttributes( String elementName, Attributes attributes ) { - if( attributes.getLength() > 0 ) { - List supportedAttributes = Arrays.asList( SUPPORTED_ELEMENTS.get( elementName ) ); - int index = 0; - String attributeName = attributes.getQName( index ); - while( attributeName != null ) { - if( !supportedAttributes.contains( attributeName ) ) { - String message = "Unsupported attribute \"{0}\" for element \"{1}\" in markup text"; - message = MessageFormat.format( message, new Object[] { attributeName, elementName } ); - throw new IllegalArgumentException( message ); - } - index++; - attributeName = attributes.getQName( index ); - } - } - } - - private static void checkMandatoryAttributes( String elementName, Attributes attributes ) { - checkIntAttribute( elementName, attributes, "img", "width" ); - checkIntAttribute( elementName, attributes, "img", "height" ); - } - - private static void checkIntAttribute( String elementName, - Attributes attributes, - String checkedElementName, - String checkedAttributeName ) - { - if( checkedElementName.equals( elementName ) ) { - String attribute = attributes.getValue( checkedAttributeName ); - try { - Integer.parseInt( attribute ); - } catch( NumberFormatException exception ) { - String message - = "Mandatory attribute \"{0}\" for element \"{1}\" is missing or not a valid integer"; - Object[] arguments = new Object[] { checkedAttributeName, checkedElementName }; - message = MessageFormat.format( message, arguments ); - throw new IllegalArgumentException( message ); - } - } - } - - } +/** + * Copy of RAP v2.3 since it is in an internal package. + * + * FIXME made public to enable validation from the {@link FormPageViewer} + */ +public class MarkupValidatorCopy { + + // Used by Eclipse Scout project + public static final String MARKUP_VALIDATION_DISABLED = "org.eclipse.rap.rwt.markupValidationDisabled"; + + private static final String DTD = createDTD(); + private static final Map SUPPORTED_ELEMENTS = createSupportedElementsMap(); + private final SAXParser saxParser; + + public static MarkupValidatorCopy getInstance() { + return SingletonUtil.getSessionInstance(MarkupValidatorCopy.class); + } + + public MarkupValidatorCopy() { + saxParser = createSAXParser(); + } + + public void validate(String text) { + StringBuilder markup = new StringBuilder(); + markup.append(DTD); + markup.append(""); + markup.append(text); + markup.append(""); + InputSource inputSource = new InputSource(new StringReader( + markup.toString())); + try { + saxParser.parse(inputSource, new MarkupHandler()); + } catch (RuntimeException exception) { + throw exception; + } catch (Exception exception) { + throw new IllegalArgumentException("Failed to parse markup text", + exception); + } + } + + public static boolean isValidationDisabledFor(Widget widget) { + return Boolean.TRUE.equals(widget.getData(MARKUP_VALIDATION_DISABLED)); + } + + private static SAXParser createSAXParser() { + SAXParser result = null; + SAXParserFactory parserFactory = SAXParserFactory.newInstance(); + try { + result = parserFactory.newSAXParser(); + } catch (Exception exception) { + throw new RuntimeException("Failed to create SAX parser", exception); + } + return result; + } + + private static String createDTD() { + StringBuilder result = new StringBuilder(); + result.append(""); + result.append(""); + result.append(""); + result.append(""); + result.append(""); + result.append(""); + result.append(""); + result.append(""); + result.append(""); + result.append(""); + result.append("]>"); + return result.toString(); + } + + private static Map createSupportedElementsMap() { + Map result = new HashMap(); + result.put("html", new String[0]); + result.put("br", new String[0]); + result.put("b", new String[] { "style" }); + result.put("strong", new String[] { "style" }); + result.put("i", new String[] { "style" }); + result.put("em", new String[] { "style" }); + result.put("sub", new String[] { "style" }); + result.put("sup", new String[] { "style" }); + result.put("big", new String[] { "style" }); + result.put("small", new String[] { "style" }); + result.put("del", new String[] { "style" }); + result.put("ins", new String[] { "style" }); + result.put("code", new String[] { "style" }); + result.put("samp", new String[] { "style" }); + result.put("kbd", new String[] { "style" }); + result.put("var", new String[] { "style" }); + result.put("cite", new String[] { "style" }); + result.put("dfn", new String[] { "style" }); + result.put("q", new String[] { "style" }); + result.put("abbr", new String[] { "style", "title" }); + result.put("span", new String[] { "style" }); + result.put("img", new String[] { "style", "src", "width", "height", + "title", "alt" }); + result.put("a", new String[] { "style", "href", "target", "title" }); + return result; + } + + private static class MarkupHandler extends DefaultHandler { + + @Override + public void startElement(String uri, String localName, String name, + Attributes attributes) { + checkSupportedElements(name, attributes); + checkSupportedAttributes(name, attributes); + checkMandatoryAttributes(name, attributes); + } + + private static void checkSupportedElements(String elementName, + Attributes attributes) { + if (!SUPPORTED_ELEMENTS.containsKey(elementName)) { + throw new IllegalArgumentException( + "Unsupported element in markup text: " + elementName); + } + } + + private static void checkSupportedAttributes(String elementName, + Attributes attributes) { + if (attributes.getLength() > 0) { + List supportedAttributes = Arrays + .asList(SUPPORTED_ELEMENTS.get(elementName)); + int index = 0; + String attributeName = attributes.getQName(index); + while (attributeName != null) { + if (!supportedAttributes.contains(attributeName)) { + String message = "Unsupported attribute \"{0}\" for element \"{1}\" in markup text"; + message = MessageFormat.format(message, new Object[] { + attributeName, elementName }); + throw new IllegalArgumentException(message); + } + index++; + attributeName = attributes.getQName(index); + } + } + } + + private static void checkMandatoryAttributes(String elementName, + Attributes attributes) { + checkIntAttribute(elementName, attributes, "img", "width"); + checkIntAttribute(elementName, attributes, "img", "height"); + } + + private static void checkIntAttribute(String elementName, + Attributes attributes, String checkedElementName, + String checkedAttributeName) { + if (checkedElementName.equals(elementName)) { + String attribute = attributes.getValue(checkedAttributeName); + try { + Integer.parseInt(attribute); + } catch (NumberFormatException exception) { + String message = "Mandatory attribute \"{0}\" for element \"{1}\" is missing or not a valid integer"; + Object[] arguments = new Object[] { checkedAttributeName, + checkedElementName }; + message = MessageFormat.format(message, arguments); + throw new IllegalArgumentException(message); + } + } + } + + } } -- 2.30.2