migrating to cdmlib-plugin 2.0.0.20 including new term loading
[taxeditor.git] / eclipseprojects / eu.etaxonomy.taxeditor / src / eu / etaxonomy / taxeditor / navigation / TaxonomicTreeViewer.java
1 /**
2 * Copyright (C) 2007 EDIT
3 * European Distributed Institute of Taxonomy
4 * http://www.e-taxonomy.eu
5 *
6 * The contents of this file are subject to the Mozilla Public License Version 1.1
7 * See LICENSE.TXT at the top of this package for the full license terms.
8 */
9
10 package eu.etaxonomy.taxeditor.navigation;
11
12 import java.util.Set;
13
14 import org.apache.log4j.Logger;
15 import org.eclipse.core.commands.operations.IUndoContext;
16 import org.eclipse.core.commands.operations.IUndoableOperation;
17 import org.eclipse.core.databinding.observable.set.IObservableSet;
18 import org.eclipse.jface.action.Action;
19 import org.eclipse.jface.action.IMenuListener;
20 import org.eclipse.jface.action.IMenuManager;
21 import org.eclipse.jface.action.MenuManager;
22 import org.eclipse.jface.action.Separator;
23 import org.eclipse.jface.dialogs.MessageDialog;
24 import org.eclipse.jface.resource.ImageDescriptor;
25 import org.eclipse.jface.viewers.CellEditor;
26 import org.eclipse.jface.viewers.DoubleClickEvent;
27 import org.eclipse.jface.viewers.ICellEditorListener;
28 import org.eclipse.jface.viewers.ICellModifier;
29 import org.eclipse.jface.viewers.IDoubleClickListener;
30 import org.eclipse.jface.viewers.ITreeContentProvider;
31 import org.eclipse.jface.viewers.StructuredSelection;
32 import org.eclipse.jface.viewers.TextCellEditor;
33 import org.eclipse.jface.viewers.TreePath;
34 import org.eclipse.jface.viewers.TreeSelection;
35 import org.eclipse.jface.viewers.TreeViewer;
36 import org.eclipse.jface.viewers.ViewerComparator;
37 import org.eclipse.swt.SWT;
38 import org.eclipse.swt.dnd.DND;
39 import org.eclipse.swt.dnd.DragSource;
40 import org.eclipse.swt.dnd.DragSourceAdapter;
41 import org.eclipse.swt.dnd.DragSourceEvent;
42 import org.eclipse.swt.dnd.DropTarget;
43 import org.eclipse.swt.dnd.DropTargetAdapter;
44 import org.eclipse.swt.dnd.DropTargetEvent;
45 import org.eclipse.swt.dnd.Transfer;
46 import org.eclipse.swt.graphics.Point;
47 import org.eclipse.swt.graphics.Rectangle;
48 import org.eclipse.swt.layout.FillLayout;
49 import org.eclipse.swt.widgets.Composite;
50 import org.eclipse.swt.widgets.Display;
51 import org.eclipse.swt.widgets.Event;
52 import org.eclipse.swt.widgets.Item;
53 import org.eclipse.swt.widgets.Label;
54 import org.eclipse.swt.widgets.Listener;
55 import org.eclipse.swt.widgets.Menu;
56 import org.eclipse.swt.widgets.Shell;
57 import org.eclipse.swt.widgets.Tree;
58 import org.eclipse.swt.widgets.TreeItem;
59
60 import eu.etaxonomy.cdm.model.name.NonViralName;
61 import eu.etaxonomy.cdm.model.name.TaxonNameBase;
62 import eu.etaxonomy.cdm.model.reference.ReferenceBase;
63 import eu.etaxonomy.cdm.model.taxon.Taxon;
64 import eu.etaxonomy.cdm.model.taxon.TaxonBase;
65 import eu.etaxonomy.taxeditor.ITaxEditorConstants;
66 import eu.etaxonomy.taxeditor.TaxEditorPlugin;
67 import eu.etaxonomy.taxeditor.actions.TaxonTransfer;
68 import eu.etaxonomy.taxeditor.actions.cdm.DeleteTaxonAction;
69 import eu.etaxonomy.taxeditor.actions.ui.OpenNewTaxonEditorAction;
70 import eu.etaxonomy.taxeditor.actions.ui.OpenTaxonEditorAction;
71 import eu.etaxonomy.taxeditor.controller.EditorController;
72 import eu.etaxonomy.taxeditor.controller.GlobalController;
73 import eu.etaxonomy.taxeditor.editor.name.CdmParserController;
74 import eu.etaxonomy.taxeditor.editor.name.IterableSynonymyList;
75 import eu.etaxonomy.taxeditor.model.CdmSessionDataRepository;
76 import eu.etaxonomy.taxeditor.model.CdmUtil;
77 import eu.etaxonomy.taxeditor.model.ICdmTaxonSetListener;
78 import eu.etaxonomy.taxeditor.operations.MoveTaxonOperation;
79 import eu.etaxonomy.taxeditor.preference.PreferencesUtil;
80
81 /**
82 * Taxon tree viewer which responds to events within individual taxa.
83 *
84 * Good overview of TreeViewer:
85 * http://www.eclipse.org/articles/Article-TreeViewer/TreeViewerArticle.htm
86 *
87 * @author p.ciardelli
88 * @created 06.05.2008
89 * @version 1.0
90 */
91 public class TaxonomicTreeViewer extends TreeViewer{
92 private static final Logger logger = Logger
93 .getLogger(TaxonomicTreeViewer.class);
94
95 private Tree tree;
96
97 private IUndoContext undoContext;
98
99 private boolean initialized = false;
100
101 /**
102 * Creates a lazy-loading taxonomic tree that is sorted by TitleCache, and
103 * which listens for name changes and new taxa
104 *
105 * @param parent
106 */
107 public TaxonomicTreeViewer(Composite parent) {
108
109 // SW.VIRTUAL causes nodes to be loaded on-demand, improving performance
110 super(parent, SWT.VIRTUAL);
111
112 undoContext = GlobalController.getWorkbenchUndoContext();
113
114 tree = this.getTree();
115 tree.setLinesVisible(false);
116 tree.setHeaderVisible(false);
117
118 createContent();
119 createToolTips();
120
121 initialized = true;
122 }
123
124 public boolean isInitialized() {
125 return initialized;
126 }
127
128 /**
129 * Tool tips are used to show warnings; there is no tooltip mechanism for
130 * individual tree items out-of-the-box, so tipListener below builds one
131 */
132 private void createToolTips() {
133 Tree tree = this.getTree();
134
135 // Disable native tooltip
136 tree.setToolTipText("");
137
138 tree.addListener(SWT.Dispose, tipListener);
139 tree.addListener(SWT.KeyDown, tipListener);
140 tree.addListener(SWT.MouseMove, tipListener);
141 tree.addListener(SWT.MouseHover, tipListener);
142 }
143
144 void createDragAndDrop() {
145 final Tree tree = this.getTree();
146 Transfer[] types = new Transfer[] { TaxonTransfer.getInstance() };
147 int operations = DND.DROP_MOVE | DND.DROP_COPY | DND.DROP_LINK;
148
149 final DragSource source = new DragSource(tree, operations);
150 source.setTransfer(types);
151 final TreeItem[] dragSourceItem = new TreeItem[1];
152 source.addDragListener(new DragSourceAdapter() {
153 public void dragStart(DragSourceEvent event) {
154 TreeItem[] selection = tree.getSelection();
155 if (selection.length > 0) {
156 event.doit = true;
157 dragSourceItem[0] = selection[0];
158 } else {
159 event.doit = false;
160 }
161 }
162
163 public void dragSetData(DragSourceEvent event) {
164 Taxon taxon = (Taxon) dragSourceItem[0].getData();
165 TaxonTransfer.getInstance().setTaxon(taxon);
166 }
167 });
168
169 DropTarget target = new DropTarget(tree, operations);
170 target.setTransfer(types);
171 target.addDropListener(new DropTargetAdapter() {
172 public void drop(DropTargetEvent event) {
173
174 // Only drop ONTO another TreeItem OR outside the tree area.
175 // Dropping between TreeItems makes no sense because tree is
176 // sorted.
177 Taxon taxon = (Taxon) dragSourceItem[0].getData();
178 Taxon parentTaxon = null;
179
180 // If event.item == null, user has dragged outside of the tree
181 if (event.item == null) {
182 parentTaxon = null;
183 } else {
184 parentTaxon = (Taxon) event.item.getData();
185
186 // Make sure parentTaxon is not a child
187 if (CdmUtil.isTaxonChildOfTaxon(parentTaxon, taxon)) {
188
189 MessageDialog.openError(GlobalController.getShell(), "Can't move taxon.",
190 "'" + CdmUtil.getDisplayName(taxon) + "' sits above " +
191 "'" + CdmUtil.getDisplayName(parentTaxon) + "' " +
192 "in the taxonomic hierarchy.");
193
194 return;
195 }
196
197 // Make sure taxon is not being dropped onto itself
198 if (taxon.equals(parentTaxon)) {
199 return;
200 }
201 }
202
203 String text = "Move taxon"; //$NON-NLS-1$
204
205 IUndoableOperation operation = new MoveTaxonOperation
206 (text, undoContext, taxon, parentTaxon);
207
208 GlobalController.executeOperation(operation);
209 }
210 });
211 }
212
213 /**
214 * Taxa with no NameCache - i.e. added with quick name - can be edited in
215 * place in the tax. tree
216 */
217 public void createQuickAdd() {
218
219 final Tree tree = this.getTree();
220 final TextCellEditor taxTreeNodeEditor = new TextCellEditor(tree);
221
222 // Create cell editor for nodes with the QuickNameTaxon singleton
223 setCellEditors(new CellEditor[] { taxTreeNodeEditor });
224 setColumnProperties(new String[] { "col1" });
225 setCellModifier(new ICellModifier() {
226 public boolean canModify(Object element, String property) {
227 if (element instanceof QuickNameTaxon) {
228 return true;
229 } else {
230 return false;
231 }
232 }
233
234 public Object getValue(Object element, String property) {
235 // If this node is modifiable, name is by definition empty
236 return "";
237 }
238
239 public void modify(Object element, String property, Object value) {
240 }
241 });
242
243 // If the node loses focus and there is a non zero-length string, create a new taxon
244 taxTreeNodeEditor.addListener(new ICellEditorListener() {
245 public void applyEditorValue() {
246 Object value = taxTreeNodeEditor.getValue();
247
248 // Only focus on parent if no taxon is created
249 boolean focusParent = false;
250
251 if (value instanceof String) {
252 String trimmedValue = ((String) value).trim();
253
254 if (trimmedValue.length() > 0) {
255
256 // TODO move this to a create new name operation
257
258 NonViralName name =
259 PreferencesUtil.getInstanceOfPreferredNameClass();
260 CdmParserController.parseFullReference(name, trimmedValue);
261
262 ReferenceBase sec = QuickNameTaxon.getInstance().getSec();
263
264 Taxon taxon = Taxon.NewInstance(name, sec);
265 taxon.setTaxonomicParent(QuickNameTaxon.getInstance().getTaxonomicParent(), null, null);
266
267 CdmSessionDataRepository.getDefault().saveTaxon(taxon);
268 } else {
269 // User has likely hit carriage return
270 focusParent = true;
271 }
272 }
273 removeQuickName(focusParent);
274 }
275
276 public void cancelEditor() {
277 removeQuickName(true);
278 }
279
280 private void removeQuickName(boolean focusParent) {
281 // Set focus to quick name's parent?
282 if (focusParent) {
283 setSelection(new StructuredSelection(
284 QuickNameTaxon.getInstance().getTaxonomicParent()), true);
285 }
286
287 // Clear singleton's parent
288 QuickNameTaxon.getInstance().setParent(null);
289
290 // Remove quick name from tree
291 remove(QuickNameTaxon.getInstance());
292 }
293
294 public void editorValueChanged(boolean oldValidState,
295 boolean newValidState) {
296 }
297 });
298 }
299
300 /**
301 * On double click, open name editor
302 */
303 public void createDoubleClickListener() {
304 this.addDoubleClickListener(new IDoubleClickListener() {
305
306 public void doubleClick(DoubleClickEvent event) {
307 if (event.getSelection() instanceof StructuredSelection) {
308 Object element = ((StructuredSelection) event
309 .getSelection()).getFirstElement();
310 if (element instanceof Taxon) {
311 EditorController.open((Taxon) element);
312 }
313 }
314 }
315 });
316 }
317
318 /**
319 * Set up content providers and viewer input
320 */
321 private void createContent() {
322
323 // Custom content provider
324 ITreeContentProvider viewerContentProviderList = new TaxonomicTreeContentProvider();
325 setContentProvider(viewerContentProviderList);
326
327 // Label provider that listens for changes to name cache
328 IObservableSet observableTaxonSet =
329 CdmSessionDataRepository.getDefault().getObservableTaxa();
330 setLabelProvider(new TaxonomicTreeLabelProvider(
331 observableTaxonSet));
332
333 // TaxonTreeList added to every time a node is opened with its
334 // children, or when a new taxon is added
335 setInput(CdmSessionDataRepository.getDefault().getRootTaxa());
336
337 // Sort according to "getColumnText" above, i.e. by NameCache
338 setComparator(new ViewerComparator());
339
340 // Listen for changes to taxa in session data repository
341 CdmSessionDataRepository.getDefault().
342 addTaxonSetListener(new ICdmTaxonSetListener() {
343
344
345 public void taxaAdded(Set<Taxon> taxa) {
346 Taxon lastTaxon = null;
347 for (Taxon taxon : taxa) {
348 addTaxonToTree(taxon, taxon.getTaxonomicParent());
349 lastTaxon = taxon;
350 }
351
352 // Show last taxon added
353 expandToLevel(lastTaxon, 1);
354 }
355
356
357 public void taxaRemoved(Set<Taxon> taxa) {
358 for (Taxon taxon : taxa) {
359 remove(taxon);
360 }
361 }
362
363
364 public void taxonMoved(Taxon taxon, Taxon newParentTaxon) {
365
366 // Save any selection and expansion info ...
367 // NOTE: may be superfluous
368 // Object[] viewerExpandedElements = null;
369 // ISelection viewerSelection = null;
370 // viewerExpandedElements = getExpandedElements();
371 // viewerSelection = getSelection();
372
373 remove(taxon);
374 addTaxonToTree(taxon, newParentTaxon);
375
376 // ... to restore after taxon has been removed then re-added
377 // setExpandedElements(viewerExpandedElements);
378 // setSelection(viewerSelection, true);
379 }
380 });
381 }
382
383 private void addTaxonToTree(Taxon taxon, Taxon parentTaxon) {
384 if (parentTaxon == null) {
385 add(TreePath.EMPTY, taxon);
386
387 // New root effectively changes the viewer's input, theerefore refresh
388 refresh();
389 } else {
390 add(parentTaxon, taxon);
391 }
392
393 }
394
395 /**
396 * If taxon node in the tree is hidden, open and select it
397 *
398 * @param taxon
399 */
400 public void revealTaxon(Taxon taxon) {
401 if (taxon == null) {
402 return;
403 }
404 this.setSelection(new StructuredSelection(taxon), true);
405 this.reveal(taxon);
406 }
407
408 // Implement a "fake" tooltip
409 final Listener labelListener = new Listener() {
410 public void handleEvent(Event event) {
411 Label label = (Label) event.widget;
412 Shell shell = label.getShell();
413 switch (event.type) {
414 case SWT.MouseDown:
415 Event e = new Event();
416 e.item = (TreeItem) label.getData("_TREEITEM");
417 // Assuming table is single select, set the selection as if
418 // the mouse down event went through to the table
419 // Tree tree = TaxonomicTreeViewer.this.getTree();
420 Tree tree = getTree();
421 tree.setSelection(new TreeItem[] { (TreeItem) e.item });
422 tree.notifyListeners(SWT.Selection, e);
423 shell.dispose();
424 tree.setFocus();
425 break;
426 case SWT.MouseExit:
427 shell.dispose();
428 break;
429 }
430 }
431 };
432
433 Listener tipListener = new Listener() {
434 Shell tip = null;
435 Label label = null;
436
437 public void handleEvent(Event event) {
438 switch (event.type) {
439 case SWT.Dispose:
440 case SWT.KeyDown:
441 case SWT.MouseMove: {
442 if (tip == null)
443 break;
444 tip.dispose();
445 tip = null;
446 label = null;
447 break;
448 }
449 case SWT.MouseHover: {
450
451 // TODO make disappear on ESC
452
453 // Item item = TaxonomicTreeViewer.this.getItemAt(new
454 // Point(event.x, event.y));
455 Item item = getItemAt(new Point(event.x, event.y));
456 if (item != null) {
457 if (tip != null && !tip.isDisposed())
458 tip.dispose();
459 tip = new Shell(Display.getCurrent(), SWT.ON_TOP
460 | SWT.NO_FOCUS | SWT.TOOL);
461 tip.setBackground(Display.getCurrent().getSystemColor(
462 SWT.COLOR_INFO_BACKGROUND));
463 FillLayout layout = new FillLayout();
464 layout.marginWidth = 2;
465 tip.setLayout(layout);
466 label = new Label(tip, SWT.NONE);
467 label.setForeground(Display.getCurrent().getSystemColor(
468 SWT.COLOR_INFO_FOREGROUND));
469 label.setBackground(Display.getCurrent().getSystemColor(
470 SWT.COLOR_INFO_BACKGROUND));
471 label.setData("_TABLEITEM", item);
472 label.setText(item.getText());
473 if (item.getData() instanceof Taxon) {
474 Taxon taxon = (Taxon) item.getData();
475 IterableSynonymyList synonymyList = new IterableSynonymyList(
476 taxon);
477 String synonymyListDisplay = CdmUtil
478 .getDisplayName(taxon);
479
480 for (TaxonBase synonymOrMisName : synonymyList) {
481 TaxonNameBase name = synonymOrMisName.getName();
482 if (name != null) {
483 synonymyListDisplay += "\n "
484 + CdmUtil.getDisplayName(name);
485 }
486 }
487 label.setText(synonymyListDisplay);
488 }
489 // label.setText ("");
490 label.addListener(SWT.MouseExit, labelListener);
491 label.addListener(SWT.MouseDown, labelListener);
492 Point size = tip.computeSize(SWT.DEFAULT, SWT.DEFAULT);
493 Rectangle rect = ((TreeItem) item).getBounds(0);
494 Point pt = getTree().toDisplay(rect.x, rect.y);
495
496 // Move tip 100px to the right so as not to interfere w clicking
497 tip.setBounds(pt.x+100, pt.y, size.x, size.y);
498 tip.setVisible(true);
499 }
500 }
501 }
502 }
503 };
504
505 public void createMenu() {
506 final MenuManager menuManager = new MenuManager();
507 menuManager.setRemoveAllWhenShown(true);
508 final Menu menu = menuManager.createContextMenu(tree);
509 tree.setMenu(menu);
510
511 menuManager.addMenuListener(new IMenuListener() {
512 public void menuAboutToShow(IMenuManager manager) {
513
514 Object selection = ((TreeSelection) getSelection())
515 .getFirstElement();
516 if (!(selection instanceof Taxon)) {
517 return;
518 }
519 final Taxon taxon = (Taxon) selection;
520
521 // Open new taxon editor window
522 Action openEditorAction = new OpenTaxonEditorAction(taxon);
523 manager.add(openEditorAction);
524
525 // Open new editor with new child taxon. The taxon will not appear in the tree until saved
526 Action openNewChildEditorAction = new OpenNewTaxonEditorAction(
527 taxon);
528 manager.add(openNewChildEditorAction);
529
530 // Create a new node in the tree to enter taxon name directly in the tree.
531 manager.add(new QuickNameAction(taxon));
532
533 manager.add(new Separator());
534
535 // Remove taxon from tree
536 Action deleteTaxonAction = new DeleteTaxonAction(taxon);
537 manager.add(deleteTaxonAction);
538 }
539 });
540 }
541
542 /**
543 * Opens a child node for the parent taxon using the quick name singleton
544 */
545 class QuickNameAction extends Action {
546
547 private static final String text = "Add child taxon with quick name";
548 private final ImageDescriptor image = TaxEditorPlugin.getDefault()
549 .getImageDescriptor(ITaxEditorConstants.QUICK_ADD_ICON);
550
551 private Taxon parentTaxon;
552
553 QuickNameAction(Taxon parentTaxon) {
554 super(text);
555 setImageDescriptor(image);
556
557 this.parentTaxon = parentTaxon;
558 }
559
560 public void run() {
561
562 QuickNameTaxon quickNameTaxon = QuickNameTaxon.getInstance();
563 quickNameTaxon.setParent(parentTaxon);
564
565 add(parentTaxon, quickNameTaxon);
566 reveal(quickNameTaxon);
567 editElement(quickNameTaxon, 0);
568 }
569 }
570 }