View Javadoc
1   /**
2    * This file Copyright (c) 2014-2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.ui.vaadin.switcher;
35  
36  import java.util.Collection;
37  import java.util.Iterator;
38  
39  import org.apache.commons.lang3.StringUtils;
40  
41  import com.vaadin.ui.Button;
42  import com.vaadin.ui.Component;
43  import com.vaadin.ui.CssLayout;
44  import com.vaadin.v7.data.Container;
45  import com.vaadin.v7.data.Item;
46  import com.vaadin.v7.data.Property;
47  import com.vaadin.v7.data.util.ContainerOrderedWrapper;
48  import com.vaadin.v7.data.util.IndexedContainer;
49  import com.vaadin.v7.data.util.converter.Converter;
50  import com.vaadin.v7.ui.ComboBox;
51  import com.vaadin.v7.ui.CustomField;
52  import com.vaadin.v7.ui.HorizontalLayout;
53  import com.vaadin.v7.ui.Label;
54  
55  /**
56   * This class is a {@link CustomField} which wraps a {@link ComboBox} and additionally provides 2 arrows to "navigate" -
57   * actually to select the previous or the next item from the available set.
58   * Additionally, the Switcher can display a description of the currently selected item.
59   *
60   * Its Layout looks like<br/>
61   *
62   * <b>&lt;&lt; | combobox v | &gt;&gt;<br/>
63   * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  description </b>
64   */
65  public class Switcher extends CustomField<Object> implements Container.Viewer {
66  
67      /**
68       * The constant for the color-variation "green" (which is default).
69       */
70      public static final String VARIATION_GREEN = "green";
71  
72      /**
73       * The constant for the color-variation "black".
74       */
75      public static final String VARIATION_BLACK = "black";
76  
77      private static final String cssGreenVariationClassname = "greenSwitcher";
78  
79      private static final String cssBlackVariationClassname = "blackSwitcher";
80  
81      private static final String cssSwitcherArrowClassname = "switcherArrow";
82  
83      private static final String cssSwitcherBorderClassname = "switcherBorder";
84  
85      private CssLayout comboLayout = new CssLayout();
86  
87      private HorizontalLayout mainLayout = new HorizontalLayout();
88  
89      private String descriptionPropertyName;
90  
91      private String colorVariation;
92  
93      private Button back, forward;
94  
95      private ComboBox combobox;
96  
97      private Label descriptionLabel;
98  
99      /**
100      * Constructs a Switcher by a given collection. The collection is converted into a container during instantiation.
101      * @param options
102      */
103     public Switcher(Collection<?> options) {
104         this(createContainer(options), VARIATION_GREEN);
105     }
106 
107     /**
108      * Constructs a Switcher by a given {@link Collection} and a given color-variant.
109      * The collection is converted into a container during instantiation.
110      *
111      * @param options
112      * @param colorVariation
113      */
114     public Switcher(Collection<?> options, String colorVariation) {
115         this(createContainer(options), colorVariation);
116     }
117 
118     /**
119      * Constructs a Switcher by a given {@link Container} using the default color-variation (VARIATION_GREEN).
120      * @param container
121      */
122     public Switcher(Container container) {
123         this(container, VARIATION_GREEN);
124     }
125 
126 
127     /**
128      * Constructs a Switcher by a given {@link Container} and colorVariant ("green" or "black", "green" is default).
129      * @param container
130      * @param colorVariation
131      */
132     public Switcher(Container container, String colorVariation) {
133         this.colorVariation = colorVariation;
134 
135         // instantiate the combobox with the container
136         if (!(container instanceof Container.Ordered)) {
137             container = new ContainerOrderedWrapper(container);
138         }
139 
140         combobox = new ComboBox("", container);
141         combobox.setTextInputAllowed(false);
142 
143     }
144 
145     /**
146      * Sets the name of the property which is used as the value of the description of an item.<br/>
147      * If it's not set, a description cannot be showed.<br/>
148      * When set, the passed {@link Container} should have set the property name descriptionPropertyName.
149      *
150      * @param descriptionPropertyName
151      */
152     public void setDescriptionPropertyName(String descriptionPropertyName) {
153         this.descriptionPropertyName = descriptionPropertyName;
154     }
155 
156     /**
157      * Adds the css style which ensures that the switcher comes with border on top and on bottom, if set true.
158      * When set to false, the style gets removed.
159      * @param withBorder
160      */
161     public void withBorder(boolean withBorder){
162         if(withBorder){
163             mainLayout.addStyleName(cssSwitcherBorderClassname);
164         }else{
165             mainLayout.removeStyleName(cssSwitcherBorderClassname);
166         }
167     }
168 
169 
170     @Override
171     protected Component initContent() {
172         construct();
173         addHandlers();
174         mainLayout.setSizeFull();
175 
176         return mainLayout;
177     }
178 
179     /**
180      * Allow or disallow empty selection by the user; it applies #setNullSelectionAllowed on underlying {@link ComboBox}.
181      * @param nullSelectionAllowed
182      */
183     public void setNullSelectionAllowed(boolean nullSelectionAllowed) {
184         combobox.setNullSelectionAllowed(nullSelectionAllowed);
185     }
186 
187 
188     /**
189      * Sets the item id that represents null value of this select;
190      * it applies #setNullSelectionItemId on underlying {@link ComboBox}.
191      * @param item
192      */
193     public void setNullSelectionItemId(Object item) {
194         combobox.setNullSelectionItemId(item);
195     }
196 
197     /**
198      * Sets the item caption property of the underlying {@link ComboBox}.
199      *
200      * @param propertyId the id of the property.
201      */
202     public void setItemCaptionPropertyId(String propertyId) {
203         combobox.setItemCaptionPropertyId(propertyId);
204     }
205 
206     /**
207      * Sets the visible value of the property.
208      * <p>The value of the select is the selected item id.</p>
209      * Sets the value to the underlying {@link ComboBox}.
210      */
211     @Override
212     public void setValue(Object newValue) {
213         if (newValue == null) {
214             return;
215         }
216         combobox.setValue(newValue);
217     }
218 
219     /**
220      * Sets the next value - if the current value is not the last available in the set.
221      */
222     public void goForward() {
223         Object currentItemId = combobox.getValue();
224         if (getContainerDataSource().isLastId(currentItemId)) {
225             return;
226         }
227         setValue(getContainerDataSource().nextItemId(currentItemId));
228     }
229 
230 
231     /**
232      * Sets the previous value - if the current value is not the first available in the set.
233      */
234     public void goBack() {
235         Object currentItemId = combobox.getValue();
236         if (getContainerDataSource().isFirstId(currentItemId)) {
237             return;
238         }
239         setValue(getContainerDataSource().prevItemId(currentItemId));
240     }
241 
242     /**
243      * This method adds a {@link Property.ValueChangeListener} to the underlying ComboBox.<br/>
244      * Such a ValueChangeListener would be notified not only when a user is changing the value directly with the wrapped {@link ComboBox},
245      * but also fired when the arrows (back, forward) are used.
246      */
247     @Override
248     public void addValueChangeListener(Property.ValueChangeListener valueChangeListener) {
249         combobox.addValueChangeListener(valueChangeListener);
250     }
251 
252 
253     /**
254      * Gets the current data source of the field, if any (actually from its underlying {@link ComboBox}).
255      * @return
256      */
257     @Override
258     public Property getPropertyDataSource() {
259         return combobox.getPropertyDataSource();
260     }
261 
262 
263     /**
264      * Sets the specified Property as the data source for the field on the underlying {@link ComboBox}.<br/>
265      * For more details see the javadoc of {@link ComboBox#setPropertyDataSource}.
266      * @param newDataSource
267      */
268     @Override
269     public void setPropertyDataSource(Property newDataSource) {
270         combobox.setPropertyDataSource(newDataSource);
271     }
272 
273     /**
274      * Sets the {@link Converter} used to convert the field value to property data
275      * source type. The converter must have a presentation type that matches the
276      * field type.
277      *
278      * @param converter
279      */
280     @Override
281     public void setConverter(Converter<Object, ?> converter) {
282         combobox.setConverter(converter);
283     }
284 
285 
286     /**
287      * Returns the selected item id.
288      * @return Object
289      */
290     @Override
291     protected Object getInternalValue() {
292         return combobox.getValue();
293     }
294 
295     @Override
296     public Class<?> getType() {
297         return Object.class;
298     }
299 
300     /**
301      * Sets the {@link Container} that serves as the data source of the viewer (on the underlying {@link ComboBox}).
302      * @param newDataSource
303      */
304     @Override
305     public void setContainerDataSource(Container newDataSource) {
306         combobox.setContainerDataSource(newDataSource);
307     }
308 
309 
310     /**
311      * Returns the {@link Container}.
312      * @return
313      */
314     @Override
315     public Container.Ordered getContainerDataSource() {
316         return (Container.Ordered) combobox.getContainerDataSource();
317     }
318 
319     // Creates the options container and adds the given options to it
320     private static Container createContainer(Collection<?> options) {
321         final Container container = new IndexedContainer();
322         if (options != null) {
323             for (final Iterator<?> i = options.iterator(); i.hasNext(); ) {
324                 container.addItem(i.next());
325             }
326         }
327         return container;
328     }
329 
330     // This method is instantiating the sub-components and assembling the widget.
331     private void construct() {
332         mainLayout.setPrimaryStyleName("switcher");
333         mainLayout.addStyleName(VARIATION_GREEN.equals(colorVariation) ? cssGreenVariationClassname : cssBlackVariationClassname);
334 
335         // back-arrow
336         back = new Button("");
337         back.setPrimaryStyleName(cssSwitcherArrowClassname);
338         back.addStyleName("icon-arrow2_w");
339         mainLayout.addComponent(back);
340 
341         // combobox with descriptionLabel below wrapped in verticalLayout
342         //
343         comboLayout.setWidth(100, Unit.PERCENTAGE);
344         combobox.setWidth(100, Unit.PERCENTAGE);
345         comboLayout.addComponent(combobox);
346         // descriptionLabel
347         descriptionLabel = new Label("");
348         descriptionLabel.setPrimaryStyleName("switcherItemDescription");
349         descriptionLabel.addStyleName("descriptionLabel");
350         comboLayout.addComponent(descriptionLabel);
351 
352         mainLayout.addComponent(comboLayout);
353         mainLayout.setExpandRatio(comboLayout, 1.0f);
354 
355         // forward arrow
356         forward = new Button("");
357         forward.setPrimaryStyleName(cssSwitcherArrowClassname);
358         forward.addStyleName("icon-arrow2_e");
359         mainLayout.addComponent(forward);
360 
361         updateButtonState(getValue());
362         updateItemDescription();
363     }
364 
365     // This method just adds or removes styles indicating the the button doesn't work and cannot be used at this state of the switcher
366     private void setButtonEnabled(Button button, boolean isEnabled) {
367         if (isEnabled) {
368             button.removeStyleName("disabled");
369             button.removeStyleName("v-button-disabled");
370             button.removeStyleName("switcher-back-disabled");
371         } else {
372             button.addStyleName("disabled");
373         }
374     }
375 
376     private void addHandlers() {
377         back.addClickListener(new Button.ClickListener() {
378             @Override
379             public void buttonClick(Button.ClickEvent event) {
380                 goBack();
381             }
382         });
383 
384         forward.addClickListener(new Button.ClickListener() {
385             @Override
386             public void buttonClick(Button.ClickEvent event) {
387                 goForward();
388             }
389         });
390 
391         combobox.addValueChangeListener(new Property.ValueChangeListener() {
392             @Override
393             public void valueChange(Property.ValueChangeEvent event) {
394                 updateButtonState(event.getProperty().getValue());
395                 updateItemDescription();
396                 fireValueChange(true);
397             }
398         });
399     }
400 
401     private void updateButtonState(Object newValue) {
402         if (newValue == null) {
403             setButtonEnabled(back, false);
404             setButtonEnabled(forward, false);
405             return;
406         }
407 
408         Object first = getContainerDataSource().firstItemId();
409         Object last = getContainerDataSource().lastItemId();
410         if (!newValue.equals(first) && !newValue.equals(last)) {
411             setButtonEnabled(back, true);
412             setButtonEnabled(forward, true);
413         } else if (newValue.equals(first)) {
414             setButtonEnabled(back, false);
415             setButtonEnabled(forward, true);
416         } else {
417             setButtonEnabled(back, true);
418             setButtonEnabled(forward, false);
419         }
420     }
421 
422     private void updateItemDescription() {
423         String description = null;
424         final Object itemId = getValue();
425         final Item item = getContainerDataSource().getItem(itemId);
426 
427         boolean isDescriptionBlank = true;
428         if (item != null) {
429             Property property = item.getItemProperty(descriptionPropertyName);
430 
431             if (property != null && property.getValue() != null) {
432                 description = property.getValue().toString();
433             }
434             isDescriptionBlank = StringUtils.isBlank(description);
435             descriptionLabel.setValue(isDescriptionBlank ? "" : description);
436         }
437 
438         if (isDescriptionBlank) {
439             mainLayout.removeStyleName("has-description");
440         } else {
441             mainLayout.addStyleName("has-description");
442         }
443 
444         descriptionLabel.setVisible(!isDescriptionBlank);
445     }
446 }