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