View Javadoc

1   /**
2    * This file Copyright (c) 2012 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.admincentral.tree.view;
35  
36  import info.magnolia.ui.admincentral.event.ItemEditedEvent;
37  import info.magnolia.ui.vaadin.grid.MagnoliaTreeTable;
38  
39  import java.util.ArrayList;
40  import java.util.Arrays;
41  import java.util.Collection;
42  import java.util.Iterator;
43  import java.util.List;
44  import java.util.Set;
45  
46  import com.vaadin.data.Container;
47  import com.vaadin.data.Item;
48  import com.vaadin.data.Property;
49  import com.vaadin.data.util.AbstractProperty;
50  import com.vaadin.event.Action;
51  import com.vaadin.event.Action.Handler;
52  import com.vaadin.event.FieldEvents;
53  import com.vaadin.event.FieldEvents.BlurEvent;
54  import com.vaadin.event.FieldEvents.FocusEvent;
55  import com.vaadin.event.ItemClickEvent;
56  import com.vaadin.event.ShortcutAction;
57  import com.vaadin.ui.AbstractTextField;
58  import com.vaadin.ui.Component;
59  import com.vaadin.ui.DefaultFieldFactory;
60  import com.vaadin.ui.Field;
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;
73  
74      private ColumnGenerator bypassedColumnGenerator;
75  
76      private final List<ItemEditedEvent.Handler> listeners = new ArrayList<ItemEditedEvent.Handler>();
77  
78      public InplaceEditingTreeTable() {
79          super();
80          setEditable(true);
81          setTableFieldFactory(new InplaceEditingFieldFactory());
82          addListener(asItemClickListener());
83          getActionManager().addActionHandler(new EditingKeyboardHandler());
84      }
85  
86      // INPLACE EDITING ENTRY POINTS.
87  
88      public void setEditableColumns(Object... editablePropertyIds) {
89          if (editableColumns != null) {
90              this.editableColumns.clear();
91          } else {
92              editableColumns = new ArrayList<Object>();
93          }
94          this.editableColumns.addAll(Arrays.asList(editablePropertyIds));
95      }
96  
97      /**
98       * Sets the item and property for inplace editing.
99       *
100      * @param itemId the item id
101      * @param propertyId the property id
102      */
103     public void setEditing(Object itemId, Object propertyId) {
104         if (itemId != null && propertyId != null) {
105             if ((bypassedColumnGenerator = getColumnGenerator(propertyId)) != null) {
106                 removeGeneratedColumn(propertyId);
107             }
108         } else {
109             if (bypassedColumnGenerator != null) {
110                 addGeneratedColumn(editingPropertyId, bypassedColumnGenerator);
111                 bypassedColumnGenerator = null;
112             }
113             focus();
114         }
115 
116         this.editingItemId = itemId;
117         this.editingPropertyId = propertyId;
118         refreshRowCache();
119         requestRepaint();
120     }
121 
122     // INPLACE EDITING FIELD FACTORY
123 
124     /**
125      * A factory for creating the inplace editing field in the right cell.
126      */
127     private class InplaceEditingFieldFactory extends DefaultFieldFactory {
128 
129         @Override
130         public Field createField(Container container, final Object itemId, final Object propertyId, Component uiContext) {
131 
132             // add TextField only for selected row/column.
133             if (editableColumns.contains(propertyId) && itemId.equals(editingItemId) && propertyId.equals(editingPropertyId)) {
134 
135                 Field field = super.createField(container, itemId, propertyId, uiContext);
136 
137                 // set TextField Focus listeners
138                 if (field instanceof AbstractTextField) {
139                     final AbstractTextField tf = (AbstractTextField) field;
140                     tf.addListener(new FieldEvents.FocusListener() {
141 
142                         @Override
143                         public void focus(FocusEvent event) {
144                             tf.setCursorPosition(tf.toString().length());
145                         }
146                     });
147 
148                     tf.addListener(new FieldEvents.BlurListener() {
149 
150                         @Override
151                         public void blur(BlurEvent event) {
152                             fireItemEditedEvent(getItemFromField(tf));
153                             setEditing(null, null);
154                         }
155                     });
156                     tf.focus();
157                 }
158 
159                 return field;
160             }
161             return null;
162         }
163     }
164 
165     // FIRING ITEM EDITED EVENTS
166 
167     @Override
168     public void addListener(ItemEditedEvent.Handler listener) {
169         listeners.add(listener);
170     }
171 
172     @Override
173     public void removeListener(ItemEditedEvent.Handler listener) {
174         if (listeners.contains(listener)) {
175             listeners.remove(listener);
176         }
177     }
178 
179     private void fireItemEditedEvent(Item item) {
180         if (item != null) {
181             ItemEditedEvent event = new ItemEditedEvent(item);
182             for (ItemEditedEvent.Handler listener : listeners) {
183                 listener.onItemEdited(event);
184             }
185         }
186     }
187 
188     /**
189      * Gets the item whose property is currently being edited in the given field. Since the {{Table}} doesn't keep
190      * references to its items, the only way to get it back is to ask the property datasource for its listeners and see
191      * if the Item is there.
192      *
193      * @param source the vaadin {{Field}} where the editing occured
194      * @return the vaadin {{Item}} if it could be fetched, null otherwise.
195      */
196     private Item getItemFromField(Field source) {
197         if (source != null) {
198             Property property = source.getPropertyDataSource();
199             if (property != null && property instanceof AbstractProperty) {
200                 Collection<?> listeners = ((AbstractProperty) property).getListeners(Property.ValueChangeEvent.class);
201                 Iterator<?> iterator = listeners.iterator();
202                 while (iterator.hasNext()) {
203                     Object listener = iterator.next();
204                     if (listener instanceof Item) {
205                         return (Item) listener;
206                     }
207                 }
208             }
209         }
210         return null;
211     }
212 
213     // DOUBLE CLICK
214 
215     @Override
216     public void itemClick(ItemClickEvent event) {
217         if (event.isDoubleClick()) {
218             setEditing(event.getItemId(), event.getPropertyId());
219         }
220     }
221 
222     private ItemClickEvent.ItemClickListener asItemClickListener() {
223         return this;
224     }
225 
226     // KEYBOARD SHORTCUTS
227 
228     /**
229      * The Class EditingKeyboardHandler for keyboard shortcuts with inplace editing.
230      */
231     private class EditingKeyboardHandler implements Handler {
232 
233         private final ShortcutAction enter = new ShortcutAction("Enter", ShortcutAction.KeyCode.ENTER, null);
234 
235         private final ShortcutAction tabNext = new ShortcutAction("Tab", ShortcutAction.KeyCode.TAB, null);
236 
237         private final ShortcutAction tabPrev = new ShortcutAction("Shift+Tab", ShortcutAction.KeyCode.TAB, new int[]{ShortcutAction.ModifierKey.SHIFT});
238 
239         private final ShortcutAction escape = new ShortcutAction("Esc", ShortcutAction.KeyCode.ESCAPE, null);
240 
241         @Override
242         public Action[] getActions(Object target, Object sender) {
243             return new Action[]{enter, tabNext, tabPrev, escape};
244         }
245 
246         @Override
247         public void handleAction(Action action, Object sender, Object target) {
248             /*
249              * In case of enter the Action needs to be casted back to
250              * ShortcutAction because for some reason the object is not same
251              * as this.enter object. In that case keycode is used in comparison.
252              */
253             if (!(action instanceof ShortcutAction)) {
254                 return;
255             }
256             ShortcutAction shortcut = (ShortcutAction) action;
257 
258             if (target != InplaceEditingTreeTable.this && target instanceof Field) {
259                 Field field = (Field) target;
260 
261                 if (shortcut == enter || shortcut.getKeyCode() == enter.getKeyCode()) {
262                     fireItemEditedEvent(getItemFromField(field));
263                     setEditing(null, null);
264 
265                 } else if (action == tabNext) {
266                     // Saves first
267                     fireItemEditedEvent(getItemFromField(field));
268 
269                     // Then updates current editingItemId, and asks for next candidate
270                     TableCell nextCell = getNextEditableCandidate(editingItemId, editingPropertyId);
271                     setEditing(nextCell.getItemId(), nextCell.getPropertyId());
272 
273                 } else if (action == tabPrev) {
274                     // Saves first
275                     fireItemEditedEvent(getItemFromField(field));
276 
277                     // Then updates current editingItemId, and asks for previous candidate
278                     TableCell previousCell = getPreviousEditableCandidate(editingItemId, editingPropertyId);
279                     setEditing(previousCell.getItemId(), previousCell.getPropertyId());
280 
281                 } else if (action == escape) {
282                     setEditing(null, null);
283                 }
284             } else if (target == InplaceEditingTreeTable.this) {
285                 if (getValue() == null) {
286                     return;
287                 }
288 
289                 if (shortcut == enter || shortcut.getKeyCode() == enter.getKeyCode()) {
290                     // get first selected itemId, handles multiple selection mode
291                     Object firstSelectedId = getValue();
292                     if (firstSelectedId instanceof Set && ((Set<?>) firstSelectedId).size() > 0) {
293                         firstSelectedId = ((Set<?>) firstSelectedId).iterator().next();
294                     }
295                     // Edit selected row at first column
296                     Object propertyId = getVisibleColumns()[0];
297                     if (!editableColumns.contains(propertyId)) {
298                         propertyId = getNextEditableCandidate(firstSelectedId, propertyId).getPropertyId();
299                     }
300                     setEditing(firstSelectedId, propertyId);
301                 }
302             }
303         }
304     }
305 
306     // NEXT/PREVIOUS EDITABLE PROPERTY CANDIDATES
307 
308     private TableCell getNextEditableCandidate(Object itemId, Object propertyId) {
309 
310         List<Object> visibleColumns = Arrays.asList(getVisibleColumns());
311         Object newItemId = itemId;
312         int newColumn = visibleColumns.indexOf(propertyId);
313         do {
314             if (newColumn == visibleColumns.size() - 1) {
315                 newItemId = nextItemId(newItemId);
316             }
317             newColumn = (newColumn + 1) % visibleColumns.size();
318         } while (!editableColumns.contains(visibleColumns.get(newColumn)) && newItemId != null);
319 
320         return new TableCell(newItemId, visibleColumns.get(newColumn));
321     }
322 
323     private TableCell getPreviousEditableCandidate(Object itemId, Object propertyId) {
324 
325         List<Object> visibleColumns = Arrays.asList(getVisibleColumns());
326         Object newItemId = itemId;
327         int newColumn = visibleColumns.indexOf(propertyId);
328         do {
329             if (newColumn == 0) {
330                 newItemId = prevItemId(newItemId);
331             }
332             newColumn = (newColumn + visibleColumns.size() - 1) % visibleColumns.size();
333         } while (!editableColumns.contains(visibleColumns.get(newColumn)) && newItemId != null);
334 
335         return new TableCell(newItemId, visibleColumns.get(newColumn));
336     }
337 
338     /**
339      * The TableCell supporting class.
340      */
341     private class TableCell {
342 
343         private final Object itemId;
344 
345         private final Object propertyId;
346 
347         public TableCell(Object itemId, Object propertyId) {
348             this.itemId = itemId;
349             this.propertyId = propertyId;
350         }
351 
352         public Object getItemId() {
353             return itemId;
354         }
355 
356         public Object getPropertyId() {
357             return propertyId;
358         }
359     }
360 
361 }