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          int size = tree.size();
94          if (size > 0) {
95              tree.setCollapsed(tree.firstItemId(), false);
96          }
97          collapseListener = new CollapsedNodeListener();
98          tree.addCollapseListener(collapseListener);
99      }
100 
101     @Override
102     public void select(List<Object> itemIds) {
103         Object firstItemId = itemIds == null || itemIds.isEmpty() ? null : itemIds.get(0);
104         if (firstItemId == null) {
105             return;
106         }
107 
108         tree.setValue(null);
109         for (Object id : itemIds) {
110             tree.select(id);
111         }
112 
113         rowScroller.bringRowIntoView(firstItemId);
114     }
115 
116     @Override
117     public void expand(Object itemId) {
118         rowScroller.expandTreeToNode(itemId, true);
119     }
120 
121     @Override
122     protected TreeView.Listener getListener() {
123         return (TreeView.Listener) super.getListener();
124     }
125 
126     @Override
127     public TreeTable asVaadinComponent() {
128         return tree;
129     }
130 
131     @Override
132     public void setEditable(boolean editable) {
133         if (editable) {
134             // field factory
135             fieldFactory = new InplaceEditingFieldFactory();
136             fieldFactory.setFieldBlurListener(new BlurListener() {
137 
138                 @Override
139                 public void blur(BlurEvent event) {
140                     Object source = event.getSource();
141                     if (source instanceof Field<?>) {
142                         saveItemProperty(((Field<?>) source).getPropertyDataSource());
143                     }
144                     setEditing(null, null);
145                 }
146             });
147             tree.setTableFieldFactory(fieldFactory);
148 
149             // expanding tree must turn off editing
150             expandListener = new ExpandListener() {
151 
152                 @Override
153                 public void nodeExpand(ExpandEvent event) {
154                     setEditing(null, null);
155                 }
156             };
157 
158             tree.addExpandListener(expandListener);
159 
160             // double-click listener
161             ItemClickListener clickListener = new ItemClickListener() {
162 
163                 @Override
164                 public void itemClick(ItemClickEvent event) {
165                     if (event.isDoubleClick()) {
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(Container shortcutActionManager) {
243         if (editable) {
244             shortcutActionManager.addActionHandler(editingKeyboardHandler);
245         }
246         this.shortcutActionManager = shortcutActionManager;
247     }
248 
249     private final class CollapsedNodeListener implements CollapseListener {
250 
251         @Override
252         public void nodeCollapse(CollapseEvent event) {
253             // collapsing tree must turn off editing
254             if (editable) {
255                 setEditing(null, null);
256             }
257             Object collapsedNodeId = event.getItemId();
258             // collapsed node descendants should be unselected as they're not visible, yet any ops affect them
259             unselectDescendants(collapsedNodeId);
260         }
261     }
262 
263     private void unselectDescendants(final Object parentId) {
264         if (tree.isMultiSelect()) {
265             Set<Object> selectedIds = (Set<Object>) tree.getValue();
266             for (Object id : selectedIds) {
267                 if (tree.isDescendantOf(id, parentId)) {
268                     tree.unselect(id);
269                 }
270             }
271         }
272     }
273 
274     /**
275      * The Class EditingKeyboardHandler for keyboard shortcuts with inplace editing.
276      */
277     private class EditingKeyboardHandler implements Handler {
278 
279         private final ShortcutAction enter = new ShortcutAction("Enter", ShortcutAction.KeyCode.ENTER, null);
280 
281         private final ShortcutAction tabNext = new ShortcutAction("Tab", ShortcutAction.KeyCode.TAB, null);
282 
283         private final ShortcutAction tabPrev = new ShortcutAction("Shift+Tab", ShortcutAction.KeyCode.TAB, new int[] { ShortcutAction.ModifierKey.SHIFT });
284 
285         private final ShortcutAction escape = new ShortcutAction("Esc", ShortcutAction.KeyCode.ESCAPE, null);
286 
287         private final TreeTable tree;
288 
289         public EditingKeyboardHandler(TreeTable tree) {
290             this.tree = tree;
291         }
292 
293         @Override
294         public Action[] getActions(Object target, Object sender) {
295             // TODO: Find a better solution for handling tab key events: MGNLUI-1384
296             return new Action[] { enter, tabNext, tabPrev, escape };
297         }
298 
299         @Override
300         public void handleAction(Action action, Object sender, Object target) {
301             /*
302              * In case of enter the Action needs to be casted back to
303              * ShortcutAction because for some reason the object is not same
304              * as this.enter object. In that case keycode is used in comparison.
305              */
306             if (!(action instanceof ShortcutAction)) {
307                 return;
308             }
309             ShortcutAction shortcut = (ShortcutAction) action;
310 
311             // Because shortcutActionManager is typically the workbench's keyboardEventPanel, this handler might be called from other content views
312             if (tree == null || !tree.isAttached()) {
313                 return;
314             }
315 
316             if (target != tree && target instanceof Field) {
317                 Field<?> field = (Field<?>) target;
318 
319                 if (shortcut == enter || shortcut.getKeyCode() == enter.getKeyCode()) {
320                     saveItemProperty(fieldFactory.getField().getPropertyDataSource());
321                     setEditing(null, null);
322 
323                 } else if (action == tabNext) {
324                     saveItemProperty(fieldFactory.getField().getPropertyDataSource());
325                     editNextCell(fieldFactory.getEditingItemId(), fieldFactory.getEditingPropertyId());
326 
327                 } else if (action == tabPrev) {
328                     saveItemProperty(fieldFactory.getField().getPropertyDataSource());
329                     editPreviousCell(fieldFactory.getEditingItemId(), fieldFactory.getEditingPropertyId());
330 
331                 } else if (action == escape) {
332                     setEditing(null, null);
333                 }
334             } else if (target == tree) {
335                 if (tree.getValue() == null) {
336                     return;
337                 }
338 
339                 if (shortcut == enter || shortcut.getKeyCode() == enter.getKeyCode()) {
340                     editFirstCell();
341 
342                 }
343             }
344         }
345     }
346 
347     // EDITING API
348 
349     private void editNextCell(Object itemId, Object propertyId) {
350 
351         List<Object> visibleColumns = Arrays.asList(tree.getVisibleColumns());
352         Object newItemId = itemId;
353         int newColumn = visibleColumns.indexOf(propertyId);
354         do {
355             if (newColumn == visibleColumns.size() - 1) {
356                 newItemId = tree.nextItemId(newItemId);
357             }
358             newColumn = (newColumn + 1) % visibleColumns.size();
359         } while (!editableColumns.contains(visibleColumns.get(newColumn)) && newItemId != null);
360 
361         setEditing(newItemId, visibleColumns.get(newColumn));
362     }
363 
364     public void editPreviousCell(Object itemId, Object propertyId) {
365 
366         List<Object> visibleColumns = Arrays.asList(tree.getVisibleColumns());
367         Object newItemId = itemId;
368         int newColumn = visibleColumns.indexOf(propertyId);
369         do {
370             if (newColumn == 0) {
371                 newItemId = tree.prevItemId(newItemId);
372             }
373             newColumn = (newColumn + visibleColumns.size() - 1) % visibleColumns.size();
374         } while (!editableColumns.contains(visibleColumns.get(newColumn)) && newItemId != null);
375 
376         setEditing(newItemId, visibleColumns.get(newColumn));
377     }
378 
379     public void editFirstCell() {
380 
381         // get first selected itemId, handles multiple selection mode
382         Object firstSelectedId = tree.getValue();
383         if (firstSelectedId instanceof Collection) {
384             if (((Collection<?>) firstSelectedId).size() > 0) {
385                 firstSelectedId = ((Set<?>) firstSelectedId).iterator().next();
386             } else {
387                 firstSelectedId = null;
388             }
389         }
390 
391         // Edit selected row at first column
392         Object propertyId = tree.getVisibleColumns()[0];
393         if (!editableColumns.contains(propertyId)) {
394             editNextCell(firstSelectedId, propertyId);
395         } else {
396             setEditing(firstSelectedId, propertyId);
397         }
398     }
399 
400 }