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