View Javadoc

1   /**
2    * This file Copyright (c) 2012-2013 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.workbench.tree;
35  
36  import info.magnolia.ui.vaadin.grid.MagnoliaTreeTable;
37  import info.magnolia.ui.workbench.event.ItemEditedEvent;
38  
39  import java.util.ArrayList;
40  import java.util.Arrays;
41  import java.util.List;
42  import java.util.Set;
43  
44  import com.vaadin.data.Container;
45  import com.vaadin.data.Item;
46  import com.vaadin.data.Property;
47  import com.vaadin.event.ActionManager;
48  import com.vaadin.event.FieldEvents;
49  import com.vaadin.event.FieldEvents.BlurEvent;
50  import com.vaadin.event.ItemClickEvent;
51  import com.vaadin.ui.AbstractTextField;
52  import com.vaadin.ui.Component;
53  import com.vaadin.ui.DefaultFieldFactory;
54  import com.vaadin.ui.Field;
55  import com.vaadin.ui.TableFieldFactory;
56  import com.vaadin.ui.TextField;
57  import com.vaadin.ui.Tree.CollapseEvent;
58  import com.vaadin.ui.Tree.CollapseListener;
59  import com.vaadin.ui.Tree.ExpandEvent;
60  import com.vaadin.ui.Tree.ExpandListener;
61  
62  /**
63   * The Inplace-editing TreeTable, for editing item properties inplace, on double click or via keyboard shortcuts.
64   * Additionally, editable columns are configurable
65   */
66  public class InplaceEditingTreeTable extends MagnoliaTreeTable implements ItemClickEvent.ItemClickListener, ItemEditedEvent.Notifier {
67  
68      private Object editingItemId;
69  
70      private Object editingPropertyId;
71  
72      private List<Object> editableColumns = new ArrayList<Object>();
73  
74      private InplaceEditingFieldFactory fieldFactory;
75  
76      private ColumnGenerator bypassedColumnGenerator;
77  
78      private final List<ItemEditedEvent.Handler> listeners = new ArrayList<ItemEditedEvent.Handler>();
79  
80      private ActionManager shortcutActionManager;
81  
82      public InplaceEditingTreeTable() {
83          super();
84          fieldFactory = new InplaceEditingFieldFactory();
85          setTableFieldFactory(fieldFactory);
86          addItemClickListener(asItemClickListener());
87  
88          addExpandListener(new ExpandListener() {
89  
90              @Override
91              public void nodeExpand(ExpandEvent event) {
92                  if (editingItemId != null) {
93                      setEditing(null, null);
94                  }
95              }
96          });
97  
98          addCollapseListener(new CollapseListener() {
99  
100             @Override
101             public void nodeCollapse(CollapseEvent event) {
102                 if (editingItemId != null) {
103                     setEditing(null, null);
104                 }
105             }
106         });
107     }
108 
109     // INPLACE EDITING ENTRY POINTS.
110 
111     public void setEditableColumns(Object... editablePropertyIds) {
112         editableColumns.clear();
113         editableColumns.addAll(Arrays.asList(editablePropertyIds));
114     }
115 
116     /**
117      * Sets the item and property for inplace editing.
118      *
119      * @param itemId the item id
120      * @param propertyId the property id
121      */
122     public void setEditing(Object itemId, Object propertyId) {
123         // ensure we don't keep outdated itemIds
124         if (getItem(itemId) == null) {
125             itemId = null;
126             propertyId = null;
127         }
128 
129         if (itemId != null && propertyId != null) {
130             Item item = getItem(itemId);
131             Property<?> property = item.getItemProperty(propertyId);
132 
133             // The previous call can return null, i.e. when clicking on an empty cell of a node row (i.e. /config/server and then the "value" cell)
134             // Do not allow editing for multi-value property.
135             if (property == null || property.getValue() instanceof List) {
136                 return;
137             } else {
138                 if ((bypassedColumnGenerator = getColumnGenerator(propertyId)) != null) {
139                     removeGeneratedColumn(propertyId);
140                 }
141                 fieldFactory.createFieldAndRegister(getContainerDataSource(), itemId, propertyId, this);
142             }
143         } else {
144             if (bypassedColumnGenerator != null) {
145                 addGeneratedColumn(editingPropertyId, bypassedColumnGenerator);
146                 bypassedColumnGenerator = null;
147             }
148             focus();
149         }
150 
151         this.editingItemId = itemId;
152         this.editingPropertyId = propertyId;
153 
154         refreshRowCache();
155     }
156 
157     // PARTIAL UPDATES
158     // MGNLUI-282 partial updates are disabled for inplace-editing to prevent tree from turning unstable.
159 
160     @Override
161     protected int getFirstUpdatedItemIndex() {
162         // if (editingItemId != null) {
163         // return indexOfId(editingItemId);
164         // }
165         return super.getFirstUpdatedItemIndex();
166     }
167 
168     @Override
169     protected int getUpdatedRowCount() {
170         // if (editingItemId != null) {
171         // return 1;
172         // }
173         return super.getUpdatedRowCount();
174     }
175 
176     @Override
177     protected int getFirstAddedItemIndex() {
178         // if (editingItemId != null) {
179         // return indexOfId(editingItemId);
180         // }
181         return super.getFirstAddedItemIndex();
182     }
183 
184     @Override
185     protected int getAddedRowCount() {
186         // if (editingItemId != null) {
187         // return 0;
188         // }
189         return super.getAddedRowCount();
190     }
191 
192     @Override
193     protected boolean shouldHideAddedRows() {
194         // if (editingItemId != null) {
195         // return false;
196         // }
197         return super.shouldHideAddedRows();
198     }
199 
200     @Override
201     protected boolean isPartialRowUpdate() {
202         return
203         // editingItemId != null ||
204         super.isPartialRowUpdate();
205     }
206 
207     // INPLACE EDITING FIELD FACTORY
208 
209     /**
210      * A factory for creating the inplace editing field in the right cell.
211      */
212     private class InplaceEditingFieldFactory implements TableFieldFactory {
213 
214         private Field<?> inplaceEditingField;
215 
216         /**
217          * For partial updates to work, we need to perform a dry run to attach the component to the table beforehand,
218          * i.e. before it is actually requested at paint phase by the table.
219          */
220         public void createFieldAndRegister(Container container, Object itemId, Object propertyId, Component uiContext) {
221 
222             Property<?> containerProperty = container.getContainerProperty(itemId, propertyId);
223             // the previous call can return null, i.e. when clicking on an empty cell of a node row (i.e. /config/server and then the "value" cell)
224             if (containerProperty == null) {
225                 return;
226             }
227             Class<?> type = containerProperty.getType();
228             final Field<?> field = createFieldByPropertyType(type);
229             if (field != null) {
230                 field.setCaption(DefaultFieldFactory.createCaptionByPropertyId(propertyId));
231                 field.setSizeFull();
232             }
233 
234             // set TextField listeners
235             if (field instanceof AbstractTextField) {
236                 final AbstractTextField tf = (AbstractTextField) field;
237                 tf.addBlurListener(new FieldEvents.BlurListener() {
238 
239                     @Override
240                     public void blur(BlurEvent event) {
241                         fireItemEditedEvent(tf.getPropertyDataSource());
242                         setEditing(null, null);
243                     }
244                 });
245                 tf.focus();
246 
247                 tf.addValueChangeListener(new ValueChangeListener() {
248 
249                     @Override
250                     public void valueChange(com.vaadin.data.Property.ValueChangeEvent event) {
251                         final Object text = event.getProperty().getValue();
252                         if (text instanceof String) {
253                             tf.selectAll();
254                         }
255                         tf.removeValueChangeListener(this);
256                     }
257                 });
258             }
259 
260             // register component on the table
261             InplaceEditingTreeTable.this.registerComponent(field);
262 
263             inplaceEditingField = field;
264         }
265 
266         @Override
267         public Field<?> createField(Container container, Object itemId, Object propertyId, Component uiContext) {
268 
269             // add TextField only for selected row/column.
270             if (editableColumns.contains(propertyId) && itemId.equals(editingItemId) && propertyId.equals(editingPropertyId)) {
271                 return inplaceEditingField;
272             }
273 
274             return null;
275         }
276 
277         private Field<?> createFieldByPropertyType(Class<?> type) {
278             if (type == null) {
279                 return null;
280             }
281 
282             return new TextField();
283         }
284 
285     }
286 
287 
288 
289     // FIRING ITEM EDITED EVENTS
290 
291     @Override
292     public void addItemEditedListener(ItemEditedEvent.Handler listener) {
293         listeners.add(listener);
294     }
295 
296     @Override
297     public void removeItemEditedListener(ItemEditedEvent.Handler listener) {
298         if (listeners.contains(listener)) {
299             listeners.remove(listener);
300         }
301     }
302 
303     /**
304      * Fires an {@link ItemEditedEvent} to all listeners. Since the property does not contain a reference to the item
305      * it came from we need to fetch the item from the container and change the property before we send the item in the
306      * event.
307      */
308     public void fireItemEditedEvent(Property property) {
309 
310         Item item = getContainerDataSource().getItem(editingItemId);
311         if (item == null) {
312             return;
313         }
314 
315         Property itemProperty = item.getItemProperty(editingPropertyId);
316         if (itemProperty == null) {
317             return;
318         }
319 
320         itemProperty.setValue(property.getValue());
321 
322         ItemEditedEvent event = new ItemEditedEvent(item);
323         for (ItemEditedEvent.Handler listener : listeners) {
324             listener.onItemEdited(event);
325         }
326     }
327 
328     // DOUBLE CLICK
329 
330     @Override
331     public void itemClick(ItemClickEvent event) {
332         if (event.isDoubleClick() && editableColumns.contains(event.getPropertyId())) {
333             setEditing(event.getItemId(), event.getPropertyId());
334         }
335     }
336 
337     private ItemClickEvent.ItemClickListener asItemClickListener() {
338         return this;
339     }
340 
341 
342     // EDITING API
343 
344     public void editNextCell(Field<?> field) {
345         // First gets a reference to next candidate
346         TableCell nextCell = getNextEditableCandidate(editingItemId, editingPropertyId);
347         // Then saves
348         fireItemEditedEvent(field.getPropertyDataSource());
349 
350         setEditing(nextCell.getItemId(), nextCell.getPropertyId());
351     }
352 
353     public void editPreviousCell(Field<?> field) {
354         // First gets a reference to previous candidate
355         TableCell previousCell = getPreviousEditableCandidate(editingItemId, editingPropertyId);
356         // Then saves
357         fireItemEditedEvent(field.getPropertyDataSource());
358 
359         setEditing(previousCell.getItemId(), previousCell.getPropertyId());
360     }
361 
362     public void editFirstCellofFirstSelectedRow() {
363 
364         // get first selected itemId, handles multiple selection mode
365         Object firstSelectedId = getValue();
366         if (firstSelectedId instanceof Set && ((Set<?>) firstSelectedId).size() > 0) {
367             firstSelectedId = ((Set<?>) firstSelectedId).iterator().next();
368         }
369         // Edit selected row at first column
370         Object propertyId = getVisibleColumns()[0];
371         if (!editableColumns.contains(propertyId)) {
372             propertyId = getNextEditableCandidate(firstSelectedId, propertyId).getPropertyId();
373         }
374         setEditing(firstSelectedId, propertyId);
375     }
376 
377     // NEXT/PREVIOUS EDITABLE PROPERTY CANDIDATES
378 
379     private TableCell getNextEditableCandidate(Object itemId, Object propertyId) {
380 
381         List<Object> visibleColumns = Arrays.asList(getVisibleColumns());
382         Object newItemId = itemId;
383         int newColumn = visibleColumns.indexOf(propertyId);
384         do {
385             if (newColumn == visibleColumns.size() - 1) {
386                 newItemId = nextItemId(newItemId);
387             }
388             newColumn = (newColumn + 1) % visibleColumns.size();
389         } while (!editableColumns.contains(visibleColumns.get(newColumn)) && newItemId != null);
390 
391         return new TableCell(newItemId, visibleColumns.get(newColumn));
392     }
393 
394     private TableCell getPreviousEditableCandidate(Object itemId, Object propertyId) {
395 
396         List<Object> visibleColumns = Arrays.asList(getVisibleColumns());
397         Object newItemId = itemId;
398         int newColumn = visibleColumns.indexOf(propertyId);
399         do {
400             if (newColumn == 0) {
401                 newItemId = prevItemId(newItemId);
402             }
403             newColumn = (newColumn + visibleColumns.size() - 1) % visibleColumns.size();
404         } while (!editableColumns.contains(visibleColumns.get(newColumn)) && newItemId != null);
405 
406         return new TableCell(newItemId, visibleColumns.get(newColumn));
407     }
408 
409     /**
410      * The TableCell supporting class.
411      */
412     private class TableCell {
413 
414         private final Object itemId;
415 
416         private final Object propertyId;
417 
418         public TableCell(Object itemId, Object propertyId) {
419             this.itemId = itemId;
420             this.propertyId = propertyId;
421         }
422 
423         public Object getItemId() {
424             return itemId;
425         }
426 
427         public Object getPropertyId() {
428             return propertyId;
429         }
430     }
431 
432 }