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