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
) {
484 .getGroupedContainers().isEmpty()){
488 for (AbstractGroupedContainerE4 container
: getEditor()
489 .getGroupedContainers()) {
490 container
.colorSelected(NOT_SELECTED
);
492 getEditor().getManagedForm().setInput(
493 AbstractGroupedContainerE4
.this);
495 colorSelected(SELECTED_FOCUS
);
498 nameViewer
.getTextWidget().addFocusListener(focusListener
);
500 MouseAdapter mouseListener
= new MouseAdapter() {
502 public void mouseDown(MouseEvent e
) {
506 control
.addMouseListener(mouseListener
);
507 nameViewer
.getRulerControl().addMouseListener(mouseListener
);
508 nameViewer
.getTextWidget().addMouseListener(mouseListener
);
511 public void setIcon(Image icon
) {
512 getNameViewer().setIcon(icon
);
515 public void setIndent(int indent
) {
516 if (control
.getLayout() instanceof TableWrapLayout
) {
517 TableWrapLayout layout
= ((TableWrapLayout
) control
.getLayout());
518 layout
.leftMargin
= indent
;
519 layout
.rightMargin
= ACCEPTED_INDENT
;
520 control
.setLayout(layout
);
523 throw new RuntimeException(
524 "Couldn't indent - composite's layout must be TableWrapLayout."); //$NON-NLS-1$
528 public void setSelected() {
529 getNameViewer().getTextWidget().setFocus();
532 public boolean isSelected() {
533 return getEditor().getSelectedContainer() == this;
536 public void colorSelected(int mode
) {
537 if (!control
.isDisposed()) {
538 String colorString
= null;
542 colorString
= Resources
.COLOR_CONTROL_SELECTED_FOCUS
;
544 case SELECTED_NO_FOCUS
:
545 colorString
= Resources
.COLOR_CONTROL_SELECTED
;
548 colorString
= Resources
.COLOR_COMPOSITE_BACKGROUND
;
551 backgroundColor
= AbstractUtility
.getColor(colorString
);
553 setBackground(backgroundColor
);
557 private Delay delay
= new Delay(Display
.getCurrent());
559 private class Delay
extends Thread
{
561 private volatile boolean stop
= false;
562 private Display display
;
564 public Delay(Display display
) {
565 this.display
= display
;
575 display
.syncExec(new Runnable() {
578 getEditor().getManagedForm().setInput(selection
);
582 } catch (InterruptedException ex
) {
587 public void requestStop() {
592 protected void setDelayedSelection() {
595 Display display
= Display
.getCurrent();
598 delay
= new Delay(display
);
603 public void setBackground(Color color
) {
604 control
.setBackground(color
);
606 for (Control child
: control
.getChildren()) {
607 child
.setBackground(color
);
610 getNameViewer().setBackground(color
);
613 public void setFont(Font font
) {
614 getNameViewer().getTextWidget().setFont(font
);
617 public NameViewer
getNameViewer() {
618 if (nameViewer
== null) {
619 throw new RuntimeException(
620 "The Name Viewer is corrupt for Name Container: " //$NON-NLS-1$
621 + getTaxonBase().getName().getTitleCache());
626 public void createEmptyViewerPrompt(final String prompt
) {
628 Assert
.isNotNull(getNameViewer());
630 final StyledText textControl
= getNameViewer().getTextWidget();
631 final IDocument document
= getNameViewer().getDocument();
633 setFocusListener(new FocusListener() {
636 public void focusGained(FocusEvent e
) {
637 if (document
.get().equals(prompt
)) {
638 textControl
.setFont(getViewerFont());
639 document
.set(""); //$NON-NLS-1$
644 public void focusLost(FocusEvent e
) {
645 if (document
.getLength() == 0) {
651 textControl
.addFocusListener(getFocusListener());
653 if (document
.getLength() == 0) {
654 textControl
.setFont(AbstractUtility
655 .getFont(Resources
.FONT_DEFAULT_PROMPT
));
656 document
.set(prompt
);
660 abstract protected Font
getViewerFont();
662 protected void initEmptyText() {
663 Font defaultFont
= AbstractUtility
.getFont(Resources
.FONT_DEFAULT_PROMPT
);
664 getNameViewer().getTextWidget().setFont(defaultFont
);
666 getNameViewer().getDocument().set(getEmptyTextPrompt());
670 protected void setFocusListener(FocusListener focusListener
) {
671 this.focusListener
= focusListener
;
674 private FocusListener
getFocusListener() {
675 return focusListener
;
678 public void setDirty(boolean isDirty
) {
680 getEditor().getManagedForm().dirtyStateChanged();
682 this.isDirty
= isDirty
;
686 public boolean isDirty() {
690 public void setMenu() {
691 getEditor().getMenuService().registerContextMenu(getNameViewer().getTextWidget(), "eu.etaxonomy.taxeditor.editor.popupmenu.nameeditor");
694 private Control
[] draggableControls
;
696 protected void setDraggableControl(Control
[] controls
) {
697 draggableControls
= controls
;
700 public void setIsDraggable(boolean draggable
) {
704 if (draggableControls
== null) {
705 throw new NullPointerException(
706 "Draggable controls must be set to add draggability"); //$NON-NLS-1$
709 Transfer
[] types
= new Transfer
[] { CdmDataTransfer
.getInstance() };
710 int operations
= DND
.DROP_MOVE
;
712 for (Control draggableControl
: draggableControls
) {
713 DragSource dragSource
= new DragSource(draggableControl
,
715 dragSource
.setTransfer(types
);
717 dragSource
.addDragListener(new NameEditorDragListenerE4(this));
718 dragSource
.setDragSourceEffect(new NameEditorDragSourceEffect(
724 private String nonEditableText
;
726 ControlListener nonEditableResizeListener
= new ControlAdapter() {
731 public void controlResized(ControlEvent e
) {
732 if (nonEditableInfoLabel
.getBounds().width
== width
) {
735 width
= nonEditableInfoLabel
.getBounds().width
;
736 if (nonEditableInfoLabel
.getBounds().width
> 0) {
737 nonEditableInfoLabel
.setText(Dialog
.shortenText(
738 nonEditableText
.toUpperCase(), nonEditableInfoLabel
));
743 private String nonEditableHoverText
;
745 private LabelEllipsisListener nonEditableLabelEllipsisListener
;
750 * nonEditableInfo is a label displayed underneath a GroupedComposite's
751 * input field. For instance, NameComposites display things like name
752 * relations, sec. references, etc. here.
755 * the text to display in the label
757 * whether the string should be appended to text that is already
760 public void setNonEditableInfo(String info
, boolean append
) {
761 // TODO non editable info should only be drawn once, when everything
763 info
= info
.toUpperCase();
766 nonEditableText
+= ", " + info
; //$NON-NLS-1$
767 nonEditableHoverText
+= "\n" + info
; //$NON-NLS-1$
769 nonEditableText
= info
;
770 nonEditableHoverText
= info
;
773 if (nonEditableInfoLabel
== null) {
774 nonEditableInfoLabel
= getEditor().getToolkit().createLabel(
775 control
, ""); //$NON-NLS-1$
776 TableWrapData layoutData
= new TableWrapData(
777 TableWrapData
.FILL_GRAB
, TableWrapData
.TOP
);
778 // Set indent to viewer ruler's width
779 if (getNameViewer().getRulerControl() != null) {
780 // TODO right justify
781 layoutData
.indent
= NameViewer
.RULER_WIDTH
;
783 nonEditableInfoLabel
.setLayoutData(layoutData
);
785 nonEditableLabelEllipsisListener
= new LabelEllipsisListener(
786 nonEditableInfoLabel
) {
788 public String
getLabelText() {
789 return nonEditableText
.toUpperCase();
793 .addControlListener(nonEditableLabelEllipsisListener
);
795 nonEditableInfoHover
= new DefaultToolTip(nonEditableInfoLabel
);
796 nonEditableInfoHover
.setRespectDisplayBounds(true);
799 nonEditableInfoHover
.setText(nonEditableHoverText
);
800 nonEditableInfoLabel
.setText(nonEditableText
);
802 calculateAnnotations();
810 public void setData(T data
) {
811 this.data
= HibernateProxyHelper
.deproxy(data
);
815 * If the user hitting carriage return should cause something to happen -
816 * i.e. the creation of a new composite - call this method and override the
817 * method handleSplitText().
819 protected void createLineBreakListener() {
820 lineBreakListener
= new LineBreakListener() {
822 public void handleSplitText(String text
) {
823 AbstractGroupedContainerE4
.this.handleSplitText(text
);
828 getNameViewer().getTextWidget().addVerifyListener(lineBreakListener
);
832 abstract class LabelEllipsisListener
extends ControlAdapter
{
834 private final Label label
;
837 LabelEllipsisListener(Label label
) {
841 abstract public String
getLabelText();
844 public void controlResized(ControlEvent e
) {
845 if (label
.getBounds().width
== width
) {
848 width
= label
.getBounds().width
;
849 if (label
.getBounds().width
> 0) {
850 label
.setText(TextHelper
.shortenText(getLabelText(), label
));
855 public void storeCursor() {
856 this.cursorPosition
= getNameViewer().getCursorPosition();
860 * Puts the cursor to the position it was last seen on or to the end of line
861 * if no former position is known.
863 public void placeCursor() {
864 if (cursorPosition
== 0) {
865 getNameViewer().setCursorToEOL();
867 getNameViewer().setCursorPosition(cursorPosition
);
871 public void restoreColor() {
872 setBackground(backgroundColor
);
876 public void initialize(IManagedForm form
) {
877 // TODO Auto-generated method stub
882 public void dispose() {
883 if (getControl() != null) {
885 getControl().dispose();
890 public void commit(boolean onSave
) {
897 public boolean setFormInput(Object input
) {
902 public void setFocus() {
903 getNameViewer().getControl().setFocus();
907 public boolean isStale() {
911 public void setDisabled(boolean disabled
) {
912 setEnabled(!disabled
);
915 public void setEnabled(boolean enabled
) {
916 Color color
= enabled ? control
.getForeground() : AbstractUtility
.getColor(Resources
.COLOR_DISABLED_EDITOR
);
918 getNameViewer().getTextWidget().setEditable(enabled
);
919 getNameViewer().getTextWidget().setForeground(color
);