2 * Copyright (C) 2007 EDIT
3 * European Distributed Institute of Taxonomy
4 * http://www.e-taxonomy.eu
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.
10 package eu
.etaxonomy
.taxeditor
.editor
.name
.e4
.container
;
12 import java
.util
.Iterator
;
13 import java
.util
.List
;
16 import org
.apache
.commons
.lang
.StringUtils
;
17 import org
.eclipse
.core
.runtime
.Assert
;
18 import org
.eclipse
.e4
.ui
.di
.UISynchronize
;
19 import org
.eclipse
.jface
.dialogs
.Dialog
;
20 import org
.eclipse
.jface
.text
.IDocument
;
21 import org
.eclipse
.jface
.text
.Position
;
22 import org
.eclipse
.jface
.viewers
.ISelectionChangedListener
;
23 import org
.eclipse
.jface
.window
.DefaultToolTip
;
24 import org
.eclipse
.swt
.custom
.StyledText
;
25 import org
.eclipse
.swt
.dnd
.DND
;
26 import org
.eclipse
.swt
.dnd
.DragSource
;
27 import org
.eclipse
.swt
.dnd
.Transfer
;
28 import org
.eclipse
.swt
.events
.ControlAdapter
;
29 import org
.eclipse
.swt
.events
.ControlEvent
;
30 import org
.eclipse
.swt
.events
.ControlListener
;
31 import org
.eclipse
.swt
.events
.FocusAdapter
;
32 import org
.eclipse
.swt
.events
.FocusEvent
;
33 import org
.eclipse
.swt
.events
.FocusListener
;
34 import org
.eclipse
.swt
.events
.ModifyEvent
;
35 import org
.eclipse
.swt
.events
.ModifyListener
;
36 import org
.eclipse
.swt
.events
.MouseAdapter
;
37 import org
.eclipse
.swt
.events
.MouseEvent
;
38 import org
.eclipse
.swt
.graphics
.Color
;
39 import org
.eclipse
.swt
.graphics
.Font
;
40 import org
.eclipse
.swt
.graphics
.Image
;
41 import org
.eclipse
.swt
.widgets
.Composite
;
42 import org
.eclipse
.swt
.widgets
.Control
;
43 import org
.eclipse
.swt
.widgets
.Display
;
44 import org
.eclipse
.swt
.widgets
.Label
;
45 import org
.eclipse
.ui
.forms
.IFormPart
;
46 import org
.eclipse
.ui
.forms
.IManagedForm
;
47 import org
.eclipse
.ui
.forms
.widgets
.TableWrapData
;
48 import org
.eclipse
.ui
.forms
.widgets
.TableWrapLayout
;
50 import eu
.etaxonomy
.cdm
.hibernate
.HibernateProxyHelper
;
51 import eu
.etaxonomy
.cdm
.model
.common
.CdmBase
;
52 import eu
.etaxonomy
.cdm
.model
.name
.TaxonName
;
53 import eu
.etaxonomy
.cdm
.model
.taxon
.TaxonBase
;
54 import eu
.etaxonomy
.cdm
.strategy
.parser
.ParserProblem
;
55 import eu
.etaxonomy
.taxeditor
.editor
.CdmDataTransfer
;
56 import eu
.etaxonomy
.taxeditor
.editor
.l10n
.Messages
;
57 import eu
.etaxonomy
.taxeditor
.editor
.name
.container
.EditorAnnotation
;
58 import eu
.etaxonomy
.taxeditor
.editor
.name
.container
.EditorAnnotation
.EditorAnnotationType
;
59 import eu
.etaxonomy
.taxeditor
.editor
.name
.container
.IContainerConstants
;
60 import eu
.etaxonomy
.taxeditor
.editor
.name
.container
.LineBreakListener
;
61 import eu
.etaxonomy
.taxeditor
.editor
.name
.container
.LineWrapSupport
;
62 import eu
.etaxonomy
.taxeditor
.editor
.name
.container
.NameViewer
;
63 import eu
.etaxonomy
.taxeditor
.editor
.name
.e4
.TaxonNameEditorE4
;
64 import eu
.etaxonomy
.taxeditor
.editor
.name
.e4
.dnd
.NameEditorDragListenerE4
;
65 import eu
.etaxonomy
.taxeditor
.editor
.name
.e4
.dnd
.NameEditorDragSourceEffect
;
66 import eu
.etaxonomy
.taxeditor
.editor
.name
.operation
.CreateSynonymInNewGroupOperation
;
67 import eu
.etaxonomy
.taxeditor
.model
.AbstractUtility
;
68 import eu
.etaxonomy
.taxeditor
.model
.IElementHasDetails
;
69 import eu
.etaxonomy
.taxeditor
.model
.NameHelper
;
70 import eu
.etaxonomy
.taxeditor
.model
.TextHelper
;
71 import eu
.etaxonomy
.taxeditor
.parser
.ParseHandler
;
72 import eu
.etaxonomy
.taxeditor
.preference
.Resources
;
75 * Formats <code>GroupedComposite</code> with cosmetic and layout properties
76 * specific to the Editor. This should be used to maintain a consistent look and
77 * feel for all Editor freetext area components, such as
78 * DescriptionElementComposite.
80 * Requires an <code>IManagedForm</code>, whose <code>input</code> is set to the
81 * contents of {@link #getData()} when the <code>GroupedComposite</code> gets
82 * focus, i.e. to populate the property sheet with the data.
85 * The <code>IManagedForm</code> is also required to have a <code>Taxon</code>
86 * in its own <code>getData()</code>.
89 * The <code>IManagedForm</code> can also used for drawing borders by calling
90 * the method <code>createBorderSupport()</code>.
97 abstract public class AbstractGroupedContainerE4
<T
extends TaxonBase
> implements
98 IFormPart
, IContainerConstants
, IElementHasDetails
{
100 protected ParseHandler parseHandler
;
102 private FocusListener nameCompositeFocusListener
;
103 private ModifyListener nameCompositeModifyListener
;
105 protected NameViewer nameViewer
;
107 private AbstractGroupE4 group
;
109 private Label nonEditableInfoLabel
;
110 private DefaultToolTip nonEditableInfoHover
;
112 private static AbstractGroupedContainerE4 selection
;
114 private FocusListener focusListener
;
115 private LineBreakListener lineBreakListener
;
117 private int cursorPosition
;
119 protected Composite control
;
121 private Color backgroundColor
;
122 private boolean isDirty
;
124 private ISelectionChangedListener selectionChangedListener
;
126 public AbstractGroupedContainerE4(AbstractGroupE4 group
, T taxonBase
) {
129 parseHandler
= ParseHandler
.NewInstance(taxonBase
.getName());
132 public void createContent() {
136 createLineWrapSupport();
137 createLineBreakListener();
141 setDraggableControl(new Control
[] { getControl(),
142 getNameViewer().getRulerControl() });
144 createEmptyViewerPrompt(EMPTY_NAME_PROMPT
);
146 initializeComposite();
153 protected void createListener() {
154 nameCompositeModifyListener
= new ModifyListener() {
157 public void modifyText(ModifyEvent e
) {
158 // mark the composite dirty
161 String text
= nameViewer
.getTextWidget().getText();
163 TaxonName name
= (TaxonName
)parseHandler
.parse(text
);
164 getTaxonBase().setName(name
);
165 getTaxonBase().setTitleCache((getTaxonBase().generateTitle()));
167 // show errors resulting from parsing
168 calculateAnnotations();
169 // store the position of the cursor
171 // notify selection listener
172 setDelayedSelection();
175 nameCompositeFocusListener
= new FocusAdapter() {
178 public void focusLost(FocusEvent e
) {
188 protected void addListener() {
189 getNameViewer().getTextWidget().addModifyListener(
190 nameCompositeModifyListener
);
191 getNameViewer().getTextWidget().addFocusListener(
192 nameCompositeFocusListener
);
195 protected void removeListener() {
196 getNameViewer().getTextWidget().removeModifyListener(
197 nameCompositeModifyListener
);
198 getNameViewer().getTextWidget().removeFocusListener(
199 nameCompositeFocusListener
);
203 * Initialize the composite specific code
205 protected abstract void initializeComposite();
207 protected String
getEmptyTextPrompt() {
208 return EMPTY_NAME_PROMPT
;
211 protected void initTextViewer() {
217 String text
= NameHelper
.getDisplayNameWithRef(getData());
219 if (text
.length() == 0) {
222 getNameViewer().setText(text
);
225 calculateAnnotations();
228 synchronized protected void calculateAnnotations() {
229 getNameViewer().clearAnnotations();
233 public void showAnnotations() {
235 if (getName() != null && getName().hasProblem()) {
236 showParsingProblems();
239 if (!isNameParsable()) {
242 new EditorAnnotation(EditorAnnotationType
.WARNING
,
244 Messages
.AbstractGroupedContainer_EDIT_IN_DETAILS_VIEW
));
247 if (isNameUsedMultipleTimes()) {
248 getNameViewer().addAnnotation(
249 new EditorAnnotation(EditorAnnotationType
.WARNING
, 0,
250 Messages
.AbstractGroupedContainer_MULTIPLE_USE
));
255 private void showParsingProblems() {
256 TaxonName name
= getName();
261 List
<ParserProblem
> parsingProblems
= name
.getParsingProblems();
263 for (ParserProblem problem
: parsingProblems
) {
264 getNameViewer().addAnnotation(new EditorAnnotation(problem
),
265 getParsingProblemPosition());
269 private Position
getParsingProblemPosition() {
270 String text
= getNameViewer().getTextWidget().getText();
272 if (getName() != null && getName().hasProblem() && text
.length() > 0) {
273 int start
= getName().getProblemStarts();
274 int length
= getName().getProblemEnds() - start
;
276 if (start
== -1 || getName().getProblemEnds() == -1) {
280 // Don't let squigglies try to draw beyond the end of the text
281 if (text
.length() < start
+ length
) {
282 length
= text
.length() - start
;
287 return new Position(start
, length
);
292 protected void handleSplitText(String text
) {
293 // Create a synonym in a new homotypic group using text as name
294 TaxonName synonymName
= TaxonName
.castAndDeproxy(
295 ParseHandler
.parseReferencedName(text
, null));
297 AbstractUtility
.executeOperation(new CreateSynonymInNewGroupOperation(
298 Messages
.AbstractGroupedContainer_NEW_HETERO_SYNONYM
, getEditor().getUndoContext(),
299 getEditor().getTaxon(), synonymName
, getEditor()), group
.getContext().get(UISynchronize
.class));
303 * Refreshes the display with latest data from the model.
305 * Note: Will not parse the text and not calculate errors!
308 public void refresh() {
309 // showNameRelations();
311 String text
= NameHelper
.getDisplayNameWithRef(getTaxonBase());
313 if (getNameViewer().getTextWidget() == null) {
314 // we might get here via dnd. Look slike it can be ignored
318 if (text
.length() == 0) {
320 } else if (!getNameViewer().getTextWidget().getText().equals(text
)) {
322 getNameViewer().getTextWidget().setText(text
);
326 updateNonEditableInfo();
335 protected abstract void updateIcon();
337 protected abstract void updateIndent();
339 protected abstract void updateNonEditableInfo();
341 protected void enableFreeText() {
342 setEnabled(isFreetextEditingAllowed());
347 * Checks whether the freetext should be editable based on specific empty
352 private boolean isFreetextEditingAllowed() {
353 boolean enableFreetext
= true;
355 enableFreetext
|= isNameUsedMultipleTimes();
356 enableFreetext
&= isNameParsable();
358 return enableFreetext
;
362 * Checks whether there are more than one, non-orphaned taxon bases
363 * attached to the taxon name
367 private boolean isNameUsedMultipleTimes() {
369 TaxonName name
= getName();
371 Set
<TaxonBase
> taxonBases
= name
.getTaxonBases();
372 Iterator
<TaxonBase
> tbItr
= taxonBases
.iterator();
373 int nonOrphanedTaxonBaseCount
= taxonBases
.size();
375 while(tbItr
.hasNext()) {
376 TaxonBase
<?
> tb
= tbItr
.next();
377 if(tb
.isOrphaned()) {
378 nonOrphanedTaxonBaseCount
--;
381 if(nonOrphanedTaxonBaseCount
> 1) {
388 private boolean isNameParsable() {
389 TaxonName name
= getName();
394 boolean isParsable
= true;
395 isParsable
&= StringUtils
.isBlank(name
.getAppendedPhrase()); // taxonFieldsEmpty();
397 isParsable
&= !name
.isProtectedAuthorshipCache();
398 isParsable
&= !name
.isProtectedNameCache();
404 * Parse the text and calculate errors
406 public void parseAndCalculateAnnotations() {
408 String unparsedNameString
= getNameViewer().getTextWidget().getText();
409 parseHandler
.parse(unparsedNameString
);
411 calculateAnnotations();
414 public T
getTaxonBase() {
418 public TaxonName
getName() {
419 return CdmBase
.deproxy(getTaxonBase().getName());
422 public void persistName() {
424 getNameViewer().getTextWidget().setEnabled(false);
425 final String unparsedNameString
= getNameViewer().getTextWidget()
427 final TaxonName name
= (TaxonName
)parseHandler
428 .parseAndResolveDuplicates(unparsedNameString
);
429 getTaxonBase().setName(name
);
430 getTaxonBase().setTitleCache((getTaxonBase().generateTitle()));
432 getNameViewer().getTextWidget().setEnabled(true);
436 public AbstractGroupE4
getGroup() {
438 throw new IllegalStateException("Group shall not be null."); //$NON-NLS-1$
443 public void remove() {
444 getGroup().remove(this);
447 protected void createControl() {
448 control
= getEditor().getToolkit().createComposite(
449 getGroup().getControl());
451 control
.setLayoutData(new TableWrapData(TableWrapData
.FILL_GRAB
));
452 TableWrapLayout layout
= new TableWrapLayout();
453 layout
.leftMargin
= 0;
454 layout
.rightMargin
= 0;
455 layout
.topMargin
= 5;
456 layout
.bottomMargin
= 5;
458 layout
.verticalSpacing
= 0;
459 layout
.horizontalSpacing
= 0;
461 control
.setLayout(layout
);
465 protected TaxonNameEditorE4
getEditor() {
466 return getGroup().getEditor();
469 public Composite
getControl() {
473 protected void createLineWrapSupport() {
474 new LineWrapSupport(getNameViewer(), getEditor().getManagedForm());
477 protected void createTextViewer() {
478 nameViewer
= new NameViewer(control
);
480 focusListener
= new FocusAdapter() {
482 public void focusGained(FocusEvent e
) {
487 .getGroupedContainers().isEmpty()){
491 for (AbstractGroupedContainerE4 container
: getEditor()
492 .getGroupedContainers()) {
493 container
.colorSelected(NOT_SELECTED
);
495 getEditor().getManagedForm().setInput(
496 AbstractGroupedContainerE4
.this);
498 colorSelected(SELECTED_FOCUS
);
501 nameViewer
.getTextWidget().addFocusListener(focusListener
);
503 MouseAdapter mouseListener
= new MouseAdapter() {
505 public void mouseDown(MouseEvent e
) {
509 control
.addMouseListener(mouseListener
);
510 nameViewer
.getRulerControl().addMouseListener(mouseListener
);
511 nameViewer
.getTextWidget().addMouseListener(mouseListener
);
514 public void setIcon(Image icon
) {
515 getNameViewer().setIcon(icon
);
518 public void setIndent(int indent
) {
519 if (control
.getLayout() instanceof TableWrapLayout
) {
520 TableWrapLayout layout
= ((TableWrapLayout
) control
.getLayout());
521 layout
.leftMargin
= indent
;
522 layout
.rightMargin
= ACCEPTED_INDENT
;
523 control
.setLayout(layout
);
526 throw new RuntimeException(
527 "Couldn't indent - composite's layout must be TableWrapLayout."); //$NON-NLS-1$
531 public void setSelected() {
532 getNameViewer().getTextWidget().setFocus();
535 public boolean isSelected() {
536 return getEditor().getSelectedContainer() == this;
539 public void colorSelected(int mode
) {
540 if (!control
.isDisposed()) {
541 String colorString
= null;
545 colorString
= Resources
.COLOR_CONTROL_SELECTED_FOCUS
;
547 case SELECTED_NO_FOCUS
:
548 colorString
= Resources
.COLOR_CONTROL_SELECTED
;
551 colorString
= Resources
.COLOR_COMPOSITE_BACKGROUND
;
554 backgroundColor
= AbstractUtility
.getColor(colorString
);
556 setBackground(backgroundColor
);
560 protected void setDelayedSelection() {
561 // TODO this might be done better
562 // this is the quickest solution i could come up with and it improves
564 // please reimplement if you know better.
568 Display display
= Display
.getCurrent();
569 Runnable runnable
= new Runnable() {
573 getEditor().getManagedForm().setInput(selection
);
576 display
.timerExec(1000, runnable
);
580 public void setBackground(Color color
) {
581 control
.setBackground(color
);
583 for (Control child
: control
.getChildren()) {
584 child
.setBackground(color
);
587 getNameViewer().setBackground(color
);
590 public void setFont(Font font
) {
591 getNameViewer().getTextWidget().setFont(font
);
594 public NameViewer
getNameViewer() {
595 if (nameViewer
== null) {
596 throw new RuntimeException(
597 "The Name Viewer is corrupt for Name Container: " //$NON-NLS-1$
598 + getTaxonBase().getName().getTitleCache());
603 public void createEmptyViewerPrompt(final String prompt
) {
605 Assert
.isNotNull(getNameViewer());
607 final StyledText textControl
= getNameViewer().getTextWidget();
608 final IDocument document
= getNameViewer().getDocument();
610 setFocusListener(new FocusListener() {
613 public void focusGained(FocusEvent e
) {
614 if (document
.get().equals(prompt
)) {
615 textControl
.setFont(getViewerFont());
616 document
.set(""); //$NON-NLS-1$
621 public void focusLost(FocusEvent e
) {
622 if (document
.getLength() == 0) {
628 textControl
.addFocusListener(getFocusListener());
630 if (document
.getLength() == 0) {
631 textControl
.setFont(AbstractUtility
632 .getFont(Resources
.FONT_DEFAULT_PROMPT
));
633 document
.set(prompt
);
637 abstract protected Font
getViewerFont();
639 protected void initEmptyText() {
640 Font defaultFont
= AbstractUtility
.getFont(Resources
.FONT_DEFAULT_PROMPT
);
641 getNameViewer().getTextWidget().setFont(defaultFont
);
643 getNameViewer().getDocument().set(getEmptyTextPrompt());
647 protected void setFocusListener(FocusListener focusListener
) {
648 this.focusListener
= focusListener
;
651 private FocusListener
getFocusListener() {
652 return focusListener
;
655 public void setDirty(boolean isDirty
) {
657 getEditor().getManagedForm().dirtyStateChanged();
659 this.isDirty
= isDirty
;
663 public boolean isDirty() {
667 public void setMenu() {
668 getEditor().getMenuService().registerContextMenu(getNameViewer().getTextWidget(), "eu.etaxonomy.taxeditor.editor.popupmenu.nameeditor");
671 private Control
[] draggableControls
;
673 protected void setDraggableControl(Control
[] controls
) {
674 draggableControls
= controls
;
677 public void setIsDraggable(boolean draggable
) {
681 if (draggableControls
== null) {
682 throw new NullPointerException(
683 "Draggable controls must be set to add draggability"); //$NON-NLS-1$
686 Transfer
[] types
= new Transfer
[] { CdmDataTransfer
.getInstance() };
687 int operations
= DND
.DROP_MOVE
;
689 for (Control draggableControl
: draggableControls
) {
690 DragSource dragSource
= new DragSource(draggableControl
,
692 dragSource
.setTransfer(types
);
694 dragSource
.addDragListener(new NameEditorDragListenerE4(this));
695 dragSource
.setDragSourceEffect(new NameEditorDragSourceEffect(
701 private String nonEditableText
;
703 ControlListener nonEditableResizeListener
= new ControlAdapter() {
708 public void controlResized(ControlEvent e
) {
709 if (nonEditableInfoLabel
.getBounds().width
== width
) {
712 width
= nonEditableInfoLabel
.getBounds().width
;
713 if (nonEditableInfoLabel
.getBounds().width
> 0) {
714 nonEditableInfoLabel
.setText(Dialog
.shortenText(
715 nonEditableText
.toUpperCase(), nonEditableInfoLabel
));
720 private String nonEditableHoverText
;
722 private LabelEllipsisListener nonEditableLabelEllipsisListener
;
726 private boolean enabled
;
729 * nonEditableInfo is a label displayed underneath a GroupedComposite's
730 * input field. For instance, NameComposites display things like name
731 * relations, sec. references, etc. here.
734 * the text to display in the label
736 * whether the string should be appended to text that is already
739 public void setNonEditableInfo(String info
, boolean append
) {
740 // TODO non editable info should only be drawn once, when everything
742 info
= info
.toUpperCase();
745 nonEditableText
+= ", " + info
; //$NON-NLS-1$
746 nonEditableHoverText
+= "\n" + info
; //$NON-NLS-1$
748 nonEditableText
= info
;
749 nonEditableHoverText
= info
;
752 if (nonEditableInfoLabel
== null) {
753 nonEditableInfoLabel
= getEditor().getToolkit().createLabel(
754 control
, ""); //$NON-NLS-1$
755 TableWrapData layoutData
= new TableWrapData(
756 TableWrapData
.FILL_GRAB
, TableWrapData
.TOP
);
757 // Set indent to viewer ruler's width
758 if (getNameViewer().getRulerControl() != null) {
759 // TODO right justify
760 layoutData
.indent
= NameViewer
.RULER_WIDTH
;
762 nonEditableInfoLabel
.setLayoutData(layoutData
);
764 nonEditableLabelEllipsisListener
= new LabelEllipsisListener(
765 nonEditableInfoLabel
) {
767 public String
getLabelText() {
768 return nonEditableText
.toUpperCase();
772 .addControlListener(nonEditableLabelEllipsisListener
);
774 nonEditableInfoHover
= new DefaultToolTip(nonEditableInfoLabel
);
775 nonEditableInfoHover
.setRespectDisplayBounds(true);
778 nonEditableInfoHover
.setText(nonEditableHoverText
);
779 nonEditableInfoLabel
.setText(nonEditableText
);
781 calculateAnnotations();
789 public void setData(T data
) {
790 this.data
= HibernateProxyHelper
.deproxy(data
);
794 * If the user hitting carriage return should cause something to happen -
795 * i.e. the creation of a new composite - call this method and override the
796 * method handleSplitText().
798 protected void createLineBreakListener() {
799 lineBreakListener
= new LineBreakListener() {
801 public void handleSplitText(String text
) {
802 AbstractGroupedContainerE4
.this.handleSplitText(text
);
807 getNameViewer().getTextWidget().addVerifyListener(lineBreakListener
);
811 abstract class LabelEllipsisListener
extends ControlAdapter
{
813 private final Label label
;
816 LabelEllipsisListener(Label label
) {
820 abstract public String
getLabelText();
823 public void controlResized(ControlEvent e
) {
824 if (label
.getBounds().width
== width
) {
827 width
= label
.getBounds().width
;
828 if (label
.getBounds().width
> 0) {
829 label
.setText(TextHelper
.shortenText(getLabelText(), label
));
834 public void storeCursor() {
835 this.cursorPosition
= getNameViewer().getCursorPosition();
839 * Puts the cursor to the position it was last seen on or to the end of line
840 * if no former position is known.
842 public void placeCursor() {
843 if (cursorPosition
== 0) {
844 getNameViewer().setCursorToEOL();
846 getNameViewer().setCursorPosition(cursorPosition
);
850 public void restoreColor() {
851 setBackground(backgroundColor
);
855 public void initialize(IManagedForm form
) {
856 // TODO Auto-generated method stub
861 public void dispose() {
862 if (getControl() != null) {
864 getControl().dispose();
869 public void commit(boolean onSave
) {
876 public boolean setFormInput(Object input
) {
881 public void setFocus() {
882 getNameViewer().getControl().setFocus();
886 public boolean isStale() {
890 public void setDisabled(boolean disabled
) {
891 this.enabled
= !disabled
;
895 public void setEnabled(boolean enabled
) {
896 this.enabled
= enabled
;
897 Color color
= enabled ? control
.getForeground() : AbstractUtility
.getColor(Resources
.COLOR_DISABLED_EDITOR
);
899 getNameViewer().getTextWidget().setEditable(enabled
);
900 // getNameViewer().getTextWidget().setEnabled(enabled);
901 getNameViewer().getTextWidget().setForeground(color
);
906 public boolean isEnabled(){