001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.ui.components.editablegroup;
029
030import java.util.List;
031
032import com.google.common.base.Supplier;
033import com.google.common.collect.Lists;
034import com.vaadin.ui.AbstractComponent;
035import com.vaadin.ui.AbstractOrderedLayout;
036import com.vaadin.ui.Button;
037import com.vaadin.ui.Component;
038import com.vaadin.ui.Component.ErrorEvent;
039import com.vaadin.ui.Component.Event;
040import com.vaadin.ui.Component.Listener;
041import com.vaadin.ui.Layout;
042import com.vaadin.v7.ui.Label;
043
044/**
045 * Manages a group of widgets used as a multivalue input.<p>
046 *
047 * This class is not itself a widget, it just coordinates the other widgets actually used to display the multivalue widget group.
048 */
049public class CmsEditableGroup {
050
051    /**
052     * Empty handler which shows or hides an 'Add' button to add new rows, depending on whether the group is empty.
053     */
054    public static class AddButtonEmptyHandler implements CmsEditableGroup.I_EmptyHandler {
055
056        /** The 'Add' button. */
057        private Button m_addButton;
058
059        /** The group. */
060        private CmsEditableGroup m_group;
061
062        /**
063         * Creates a new instance.
064         *
065         * @param addButtonText the text for the Add button
066         */
067        public AddButtonEmptyHandler(String addButtonText) {
068
069            m_addButton = new Button(addButtonText);
070            m_addButton.addClickListener(evt -> {
071                Component component = m_group.getNewComponentFactory().get();
072                m_group.addRow(component);
073            });
074        }
075
076        /**
077         * @see org.opencms.ui.components.editablegroup.CmsEditableGroup.I_EmptyHandler#init(org.opencms.ui.components.editablegroup.CmsEditableGroup)
078         */
079        public void init(CmsEditableGroup group) {
080
081            m_group = group;
082        }
083
084        /**
085         * @see org.opencms.ui.components.editablegroup.CmsEditableGroup.I_EmptyHandler#setEmpty(boolean)
086         */
087        public void setEmpty(boolean empty) {
088
089            if (empty) {
090                m_group.getContainer().addComponent(m_addButton);
091            } else {
092                m_group.getContainer().removeComponent(m_addButton);
093            }
094        }
095
096    }
097
098    /**
099     * Default implementation for row builder.
100     */
101    public static class DefaultRowBuilder implements CmsEditableGroup.I_RowBuilder {
102
103        public I_CmsEditableGroupRow buildRow(CmsEditableGroup group, Component component) {
104
105            if (component instanceof Layout.MarginHandler) {
106                // Since the row is a HorizontalLayout with the edit buttons positioned next to the original
107                // widget, a margin on the widget causes it to be vertically offset from the buttons too much
108                Layout.MarginHandler marginHandler = (Layout.MarginHandler)component;
109                marginHandler.setMargin(false);
110            }
111            if (component instanceof AbstractComponent) {
112                component.addListener(group.getErrorListener());
113            }
114            I_CmsEditableGroupRow row = new CmsEditableGroupRow(group, component);
115            if (group.getRowCaption() != null) {
116                row.setCaption(group.getRowCaption());
117            }
118            return row;
119        }
120    }
121
122    /**
123     * Handles state changes when the group becomes empty/not empty.
124     */
125    public interface I_EmptyHandler {
126
127        /**
128         * Needs to be called initially with the group for which this is used.
129         *
130         * @param group the group
131         */
132        public void init(CmsEditableGroup group);
133
134        /**
135         * Called when the group changes from empty to not empty, or vice versa.
136         *
137         * @param empty true if the group is empty
138         */
139        public void setEmpty(boolean empty);
140    }
141
142    /**
143     * Interface for group row components that can have errors.
144     */
145    public interface I_HasError {
146
147        /**
148         * Check if there is an error.
149         *
150         * @return true if there is an error
151         */
152        public boolean hasEditableGroupError();
153    }
154
155    /**
156     * Builds editable group rows by wrapping other components.
157     */
158    public interface I_RowBuilder {
159
160        /**
161         * Builds a row for the given group by wrapping the given component.
162         *
163         * @param group the group
164         * @param component the component
165         * @return the new row
166         */
167        public I_CmsEditableGroupRow buildRow(CmsEditableGroup group, Component component);
168    }
169
170    /** The container in which to render the individual rows of the multivalue widget group. */
171    private AbstractOrderedLayout m_container;
172
173    private I_EmptyHandler m_emptyHandler;
174
175    /** The error label. */
176    private Label m_errorLabel = new Label();
177
178    /** The error listener. */
179    private Listener m_errorListener;
180
181    /** The error message. */
182    private String m_errorMessage;
183
184    /**Should the add option be hidden?*/
185    private boolean m_hideAdd;
186
187    /** Factory for creating new input fields. */
188    private Supplier<Component> m_newComponentFactory;
189
190    /** The builder to use for creating new rows. */
191    private I_RowBuilder m_rowBuilder = new DefaultRowBuilder();
192
193    /** The row caption. */
194    private String m_rowCaption;
195
196    /**
197     * Creates a new instance.<p>
198     *
199     * @param container the container in which to render the individual rows
200     * @param componentFactory the factory used to create new input fields
201     * @param placeholder the placeholder to display when there are no rows
202     */
203    public CmsEditableGroup(
204        AbstractOrderedLayout container,
205        Supplier<Component> componentFactory,
206        I_EmptyHandler emptyHandler) {
207
208        m_hideAdd = false;
209        m_emptyHandler = emptyHandler;
210        m_container = container;
211        m_newComponentFactory = componentFactory;
212        m_emptyHandler = emptyHandler;
213        m_emptyHandler.init(this);
214        m_errorListener = new Listener() {
215
216            private static final long serialVersionUID = 1L;
217
218            @SuppressWarnings("synthetic-access")
219            public void componentEvent(Event event) {
220
221                if (event instanceof ErrorEvent) {
222                    updateGroupValidation();
223                }
224            }
225        };
226        m_errorLabel.setValue(m_errorMessage);
227        m_errorLabel.addStyleName("o-editablegroup-errorlabel");
228        setErrorVisible(false);
229    }
230
231    /**
232     * Creates a new instance.<p>
233     *
234     * @param container the container in which to render the individual rows
235     * @param componentFactory the factory used to create new input fields
236     * @param addButtonCaption the caption for the button which is used to add a new row to an empty list
237     */
238    public CmsEditableGroup(
239        AbstractOrderedLayout container,
240        Supplier<Component> componentFactory,
241        String addButtonCaption) {
242
243        this(container, componentFactory, new AddButtonEmptyHandler(addButtonCaption));
244    }
245
246    /**
247     * Adds a row for the given component at the end of the group.
248     *
249     * @param component the component to wrap in the row to be added
250     */
251    public void addRow(Component component) {
252
253        Component actualComponent = component == null ? m_newComponentFactory.get() : component;
254        I_CmsEditableGroupRow row = m_rowBuilder.buildRow(this, actualComponent);
255        m_container.addComponent(row);
256        updatePlaceholder();
257        updateButtonBars();
258        updateGroupValidation();
259    }
260
261    /**
262     * Adds a new row after the given one.
263     *
264     * @param row the row after which a new one should be added
265     */
266    public void addRowAfter(I_CmsEditableGroupRow row) {
267
268        int index = m_container.getComponentIndex(row);
269        if (index >= 0) {
270            Component component = m_newComponentFactory.get();
271            I_CmsEditableGroupRow newRow = m_rowBuilder.buildRow(this, component);
272            m_container.addComponent(newRow, index + 1);
273        }
274        updatePlaceholder();
275        updateButtonBars();
276        updateGroupValidation();
277    }
278
279    /**
280     * Gets the row container.
281     *
282     * @return the row container
283     */
284    public AbstractOrderedLayout getContainer() {
285
286        return m_container;
287    }
288
289    /**
290     * Gets the error listener.
291     *
292     * @return t
293     */
294    public Listener getErrorListener() {
295
296        return m_errorListener;
297    }
298
299    /**
300     * Gets the factory used for creating new components.
301     *
302     * @return the factory used for creating new components
303     */
304    public Supplier<Component> getNewComponentFactory() {
305
306        return m_newComponentFactory;
307    }
308
309    /**
310     * Returns the row caption.<p>
311     *
312     * @return the row caption
313     */
314    public String getRowCaption() {
315
316        return m_rowCaption;
317    }
318
319    /**
320     * Gets all rows.
321     *
322     * @return the list of all rows
323     */
324    public List<I_CmsEditableGroupRow> getRows() {
325
326        List<I_CmsEditableGroupRow> result = Lists.newArrayList();
327        for (Component component : m_container) {
328            if (component instanceof I_CmsEditableGroupRow) {
329                result.add((I_CmsEditableGroupRow)component);
330            }
331        }
332        return result;
333    }
334
335    /**
336     * Initializes the multivalue group.<p>
337     */
338    public void init() {
339
340        m_container.removeAllComponents();
341        m_container.addComponent(m_errorLabel);
342        updatePlaceholder();
343    }
344
345    /**
346     * Moves the given row down.
347     *
348     * @param row the row to move
349     */
350    public void moveDown(I_CmsEditableGroupRow row) {
351
352        int index = m_container.getComponentIndex(row);
353        if ((index >= 0) && (index < (m_container.getComponentCount() - 1))) {
354            m_container.removeComponent(row);
355            m_container.addComponent(row, index + 1);
356        }
357        updateButtonBars();
358    }
359
360    /**
361     * Moves the given row up.
362     *
363     * @param row the row to move
364     */
365    public void moveUp(I_CmsEditableGroupRow row) {
366
367        int index = m_container.getComponentIndex(row);
368        if (index > 0) {
369            m_container.removeComponent(row);
370            m_container.addComponent(row, index - 1);
371        }
372        updateButtonBars();
373    }
374
375    /**
376     * Removes the given row.
377     *
378     * @param row the row to remove
379     */
380    public void remove(I_CmsEditableGroupRow row) {
381
382        int index = m_container.getComponentIndex(row);
383        if (index >= 0) {
384            m_container.removeComponent(row);
385        }
386        updatePlaceholder();
387        updateButtonBars();
388        updateGroupValidation();
389    }
390
391    /**
392     * @see org.opencms.ui.components.editablegroup.I_CmsEditableGroup#setAddButtonVisible(boolean)
393     */
394    public void setAddButtonVisible(boolean visible) {
395
396        m_hideAdd = !visible;
397
398    }
399
400    /**
401     * Sets the error message.<p>
402     *
403     * @param errorMessage the error message
404     */
405    public void setErrorMessage(String errorMessage) {
406
407        m_errorMessage = errorMessage;
408        m_errorLabel.setValue(errorMessage != null ? errorMessage : "");
409    }
410
411    /**
412     * Sets the row builder.
413     *
414     * @param rowBuilder the row builder
415     */
416    public void setRowBuilder(I_RowBuilder rowBuilder) {
417
418        m_rowBuilder = rowBuilder;
419    }
420
421    /**
422     * Sets the row caption.<p>
423     *
424     * @param rowCaption the row caption to set
425     */
426    public void setRowCaption(String rowCaption) {
427
428        m_rowCaption = rowCaption;
429    }
430
431    /**
432     * Checks if the given group component has an error.<p>
433     *
434     * @param component the component to check
435     * @return true if the component has an error
436     */
437    protected boolean hasError(Component component) {
438
439        if (component instanceof AbstractComponent) {
440            if (((AbstractComponent)component).getComponentError() != null) {
441                return true;
442            }
443        }
444        if (component instanceof I_HasError) {
445            if (((I_HasError)component).hasEditableGroupError()) {
446                return true;
447            }
448
449        }
450        return false;
451    }
452
453    /**
454     * Shows or hides the error label.<p>
455     *
456     * @param hasError true if we have an error
457     */
458    private void setErrorVisible(boolean hasError) {
459
460        m_errorLabel.setVisible(hasError && (m_errorMessage != null));
461    }
462
463    /**
464     * Updates the button bars.<p>
465     */
466    private void updateButtonBars() {
467
468        List<I_CmsEditableGroupRow> rows = getRows();
469        int i = 0;
470        for (I_CmsEditableGroupRow row : rows) {
471            boolean first = i == 0;
472            boolean last = i == (rows.size() - 1);
473            row.getButtonBar().setFirstLast(first, last, m_hideAdd);
474            i += 1;
475        }
476    }
477
478    /**
479     * Updates the visibility of the error label based on errors in the group components.<p>
480     */
481    private void updateGroupValidation() {
482
483        boolean hasError = false;
484        for (I_CmsEditableGroupRow row : getRows()) {
485            if (hasError(row.getComponent())) {
486                hasError = true;
487                break;
488            }
489        }
490        setErrorVisible(hasError);
491    }
492
493    /**
494     * Updates the button visibility.<p>
495     */
496    private void updatePlaceholder() {
497
498        boolean empty = getRows().size() == 0;
499        m_emptyHandler.setEmpty(empty);
500
501    }
502}