View Javadoc

1   /**
2    * This file Copyright (c) 2011-2014 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.list.ListViewImpl;
38  
39  import java.util.ArrayList;
40  import java.util.Arrays;
41  import java.util.Collection;
42  import java.util.List;
43  import java.util.Set;
44  
45  import com.vaadin.data.Item;
46  import com.vaadin.data.Property;
47  import com.vaadin.event.Action;
48  import com.vaadin.event.Action.Container;
49  import com.vaadin.event.Action.Handler;
50  import com.vaadin.event.FieldEvents.BlurEvent;
51  import com.vaadin.event.FieldEvents.BlurListener;
52  import com.vaadin.event.ItemClickEvent;
53  import com.vaadin.event.ItemClickEvent.ItemClickListener;
54  import com.vaadin.event.ShortcutAction;
55  import com.vaadin.event.dd.DropHandler;
56  import com.vaadin.ui.Field;
57  import com.vaadin.ui.Table;
58  import com.vaadin.ui.Table.ColumnGenerator;
59  import com.vaadin.ui.Table.TableDragMode;
60  import com.vaadin.ui.Tree.CollapseEvent;
61  import com.vaadin.ui.Tree.CollapseListener;
62  import com.vaadin.ui.Tree.ExpandEvent;
63  import com.vaadin.ui.Tree.ExpandListener;
64  import com.vaadin.ui.TreeTable;
65  
66  /**
67   * Default Vaadin implementation of the tree view.
68   */
69  public class TreeViewImpl extends ListViewImpl implements TreeView {
70  
71      private MagnoliaTreeTable tree;
72  
73      private boolean editable;
74      private final List<Object> editableColumns = new ArrayList<Object>();
75      private InplaceEditingFieldFactory fieldFactory;
76      private ExpandListener expandListener;
77      private CollapseListener collapseListener;
78      private Container shortcutActionManager;
79      private EditingKeyboardHandler editingKeyboardHandler;
80      private ColumnGenerator bypassedColumnGenerator;
81  
82      @Override
83      protected MagnoliaTreeTable createTable(com.vaadin.data.Container container) {
84          tree = new MagnoliaTreeTable(container);
85          return tree;
86      }
87  
88      @Override
89      protected void initializeTable(Table table) {
90          super.initializeTable(table);
91          tree.setSortEnabled(false);
92          int size = tree.size();
93          if (size > 0) {
94              tree.setCollapsed(tree.firstItemId(), false);
95          }
96      }
97  
98      @Override
99      public void select(List<Object> itemIds) {
100         Object firstItemId = itemIds == null || itemIds.isEmpty() ? null : itemIds.get(0);
101         if (firstItemId == null || tree.isSelected(firstItemId)) {
102             return;
103         }
104         tree.focus();
105         expandTreeToNode(firstItemId, false);
106 
107         tree.setValue(null);
108         for (Object id : itemIds) {
109             tree.select(id);
110         }
111         tree.setCurrentPageFirstItemId(firstItemId);
112     }
113 
114     @Override
115     public void expand(Object itemId) {
116         expandTreeToNode(itemId, true);
117     }
118 
119     private void expandTreeToNode(Object id, boolean expandNode) {
120         com.vaadin.data.Container.Hierarchical container = tree.getContainerDataSource();
121         Item item = container.getItem(id);
122 
123         if (item == null) {
124             return;
125         }
126 
127         // Determine node to expand.
128         Object node = null;
129         if (!container.areChildrenAllowed(id)) {
130             node = container.getParent(id);
131         } else {
132             if (expandNode) {
133                 node = id;
134             } else {
135                 Object parent = container.getParent(id);
136                 // Check if item is root.
137                 if (parent != null) {
138                     node = parent;
139                 }
140             }
141         }
142 
143         // as long as parent is within the scope of the workbench
144         while (node != null) {
145             tree.setCollapsed(node, false);
146             node = container.getParent(node);
147         }
148 
149     }
150 
151     @Override
152     protected TreeView.Listener getListener() {
153         return (TreeView.Listener) super.getListener();
154     }
155 
156     @Override
157     public TreeTable asVaadinComponent() {
158         return tree;
159     }
160 
161     @Override
162     public void setEditable(boolean editable) {
163         if (editable) {
164             // field factory
165             fieldFactory = new InplaceEditingFieldFactory();
166             fieldFactory.setFieldBlurListener(new BlurListener() {
167 
168                 @Override
169                 public void blur(BlurEvent event) {
170                     Object source = event.getSource();
171                     if (source instanceof Field<?>) {
172                         saveItemProperty(((Field<?>) source).getPropertyDataSource());
173                     }
174                     setEditing(null, null);
175                 }
176             });
177             tree.setTableFieldFactory(fieldFactory);
178 
179             // expanding and collapsing tree must turn off editing
180             expandListener = new ExpandListener() {
181 
182                 @Override
183                 public void nodeExpand(ExpandEvent event) {
184                     setEditing(null, null);
185                 }
186             };
187             collapseListener = new CollapseListener() {
188 
189                 @Override
190                 public void nodeCollapse(CollapseEvent event) {
191                     setEditing(null, null);
192                 }
193             };
194             tree.addExpandListener(expandListener);
195             tree.addCollapseListener(collapseListener);
196 
197             // double-click listener
198             ItemClickListener clickListener = new ItemClickListener() {
199 
200                 @Override
201                 public void itemClick(ItemClickEvent event) {
202                     if (event.isDoubleClick()) {
203                         setEditing(event.getItemId(), event.getPropertyId());
204                     }
205                 }
206             };
207             tree.addItemClickListener(clickListener);
208 
209             // keyboard shortcuts
210             editingKeyboardHandler = new EditingKeyboardHandler(tree);
211             if (shortcutActionManager != null) {
212                 shortcutActionManager.addActionHandler(editingKeyboardHandler);
213             }
214 
215         } else {
216             tree.setTableFieldFactory(null);
217             fieldFactory = null;
218             tree.removeExpandListener(expandListener);
219             tree.removeCollapseListener(collapseListener);
220             expandListener = null;
221             collapseListener = null;
222             if (shortcutActionManager != null) {
223                 shortcutActionManager.removeActionHandler(editingKeyboardHandler);
224             }
225             editingKeyboardHandler = null;
226         }
227 
228         tree.setEditable(editable);
229         this.editable = editable;
230     }
231 
232     @Override
233     public void setEditableColumns(Object... editablePropertyIds) {
234         editableColumns.clear();
235         editableColumns.addAll(Arrays.asList(editablePropertyIds));
236     }
237 
238     private void setEditing(Object itemId, Object propertyId) {
239 
240         // restore generated column if it was disabled for editing
241         if (bypassedColumnGenerator != null) {
242             tree.addGeneratedColumn(fieldFactory.getEditingPropertyId(), bypassedColumnGenerator);
243             bypassedColumnGenerator = null;
244         }
245 
246         if (editable && editableColumns.contains(propertyId)) {
247             if (itemId == null || propertyId == null) {
248                 tree.focus();
249                 fieldFactory.setEditing(null, null);
250             } else {
251                 // disable generated column for editing
252                 if ((bypassedColumnGenerator = tree.getColumnGenerator(propertyId)) != null) {
253                     tree.removeGeneratedColumn(propertyId);
254                 }
255                 fieldFactory.setEditing(itemId, propertyId);
256             }
257         } else {
258             fieldFactory.setEditing(null, null);
259         }
260         tree.refreshRowCache();
261     }
262 
263     private void saveItemProperty(Property<?> propertyDataSource) {
264         getListener().onItemEdited(fieldFactory.getEditingItemId(), fieldFactory.getEditingPropertyId(), propertyDataSource);
265     }
266 
267     @Override
268     public void setDragAndDropHandler(DropHandler dropHandler) {
269         if (dropHandler != null) {
270             tree.setDragMode(TableDragMode.ROW);
271             tree.setDropHandler(dropHandler);
272         } else {
273             tree.setDragMode(TableDragMode.NONE);
274             tree.setDropHandler(null);
275         }
276     }
277 
278     // KEYBOARD SHORTCUTS
279 
280     @Override
281     public void setActionManager(Container shortcutActionManager) {
282         if (editable) {
283             shortcutActionManager.addActionHandler(editingKeyboardHandler);
284         }
285         this.shortcutActionManager = shortcutActionManager;
286     }
287 
288     /**
289      * The Class EditingKeyboardHandler for keyboard shortcuts with inplace editing.
290      */
291     private class EditingKeyboardHandler implements Handler {
292 
293         private final ShortcutAction enter = new ShortcutAction("Enter", ShortcutAction.KeyCode.ENTER, null);
294 
295         private final ShortcutAction tabNext = new ShortcutAction("Tab", ShortcutAction.KeyCode.TAB, null);
296 
297         private final ShortcutAction tabPrev = new ShortcutAction("Shift+Tab", ShortcutAction.KeyCode.TAB, new int[] { ShortcutAction.ModifierKey.SHIFT });
298 
299         private final ShortcutAction escape = new ShortcutAction("Esc", ShortcutAction.KeyCode.ESCAPE, null);
300 
301         private final TreeTable tree;
302 
303         public EditingKeyboardHandler(TreeTable tree) {
304             this.tree = tree;
305         }
306 
307         @Override
308         public Action[] getActions(Object target, Object sender) {
309             // TODO: Find a better solution for handling tab key events: MGNLUI-1384
310             return new Action[] { enter, tabNext, tabPrev, escape };
311         }
312 
313         @Override
314         public void handleAction(Action action, Object sender, Object target) {
315             /*
316              * In case of enter the Action needs to be casted back to
317              * ShortcutAction because for some reason the object is not same
318              * as this.enter object. In that case keycode is used in comparison.
319              */
320             if (!(action instanceof ShortcutAction)) {
321                 return;
322             }
323             ShortcutAction shortcut = (ShortcutAction) action;
324 
325             if (target != tree && target instanceof Field) {
326                 Field<?> field = (Field<?>) target;
327 
328                 if (shortcut == enter || shortcut.getKeyCode() == enter.getKeyCode()) {
329                     saveItemProperty(fieldFactory.getField().getPropertyDataSource());
330                     setEditing(null, null);
331 
332                 } else if (action == tabNext) {
333                     saveItemProperty(fieldFactory.getField().getPropertyDataSource());
334                     editNextCell(fieldFactory.getEditingItemId(), fieldFactory.getEditingPropertyId());
335 
336                 } else if (action == tabPrev) {
337                     saveItemProperty(fieldFactory.getField().getPropertyDataSource());
338                     editPreviousCell(fieldFactory.getEditingItemId(), fieldFactory.getEditingPropertyId());
339 
340                 } else if (action == escape) {
341                     setEditing(null, null);
342                 }
343             } else if (target == tree) {
344                 if (tree.getValue() == null) {
345                     return;
346                 }
347 
348                 if (shortcut == enter || shortcut.getKeyCode() == enter.getKeyCode()) {
349                     editFirstCell();
350 
351                 }
352             }
353         }
354     }
355 
356     // EDITING API
357 
358     private void editNextCell(Object itemId, Object propertyId) {
359 
360         List<Object> visibleColumns = Arrays.asList(tree.getVisibleColumns());
361         Object newItemId = itemId;
362         int newColumn = visibleColumns.indexOf(propertyId);
363         do {
364             if (newColumn == visibleColumns.size() - 1) {
365                 newItemId = tree.nextItemId(newItemId);
366             }
367             newColumn = (newColumn + 1) % visibleColumns.size();
368         } while (!editableColumns.contains(visibleColumns.get(newColumn)) && newItemId != null);
369 
370         setEditing(newItemId, visibleColumns.get(newColumn));
371     }
372 
373     public void editPreviousCell(Object itemId, Object propertyId) {
374 
375         List<Object> visibleColumns = Arrays.asList(tree.getVisibleColumns());
376         Object newItemId = itemId;
377         int newColumn = visibleColumns.indexOf(propertyId);
378         do {
379             if (newColumn == 0) {
380                 newItemId = tree.prevItemId(newItemId);
381             }
382             newColumn = (newColumn + visibleColumns.size() - 1) % visibleColumns.size();
383         } while (!editableColumns.contains(visibleColumns.get(newColumn)) && newItemId != null);
384 
385         setEditing(newItemId, visibleColumns.get(newColumn));
386     }
387 
388     public void editFirstCell() {
389 
390         // get first selected itemId, handles multiple selection mode
391         Object firstSelectedId = tree.getValue();
392         if (firstSelectedId instanceof Collection) {
393             if (((Collection<?>) firstSelectedId).size() > 0) {
394                 firstSelectedId = ((Set<?>) firstSelectedId).iterator().next();
395             } else {
396                 firstSelectedId = null;
397             }
398         }
399 
400         // Edit selected row at first column
401         Object propertyId = tree.getVisibleColumns()[0];
402         if (!editableColumns.contains(propertyId)) {
403             editNextCell(firstSelectedId, propertyId);
404         } else {
405             setEditing(firstSelectedId, propertyId);
406         }
407     }
408 
409 }