Moving editor sources back into trunk
[taxeditor.git] / taxeditor-editor / src / main / java / eu / etaxonomy / taxeditor / editor / GroupedComposite.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.editor;
11
12 import org.apache.log4j.Logger;
13 import org.eclipse.core.runtime.Assert;
14 import org.eclipse.jface.dialogs.Dialog;
15 import org.eclipse.jface.text.IDocument;
16 import org.eclipse.jface.text.TextViewer;
17 import org.eclipse.jface.window.DefaultToolTip;
18 import org.eclipse.swt.SWT;
19 import org.eclipse.swt.custom.StyledText;
20 import org.eclipse.swt.dnd.DND;
21 import org.eclipse.swt.dnd.DragSource;
22 import org.eclipse.swt.dnd.DragSourceAdapter;
23 import org.eclipse.swt.dnd.DragSourceEvent;
24 import org.eclipse.swt.dnd.DragSourceListener;
25 import org.eclipse.swt.dnd.Transfer;
26 import org.eclipse.swt.events.ControlAdapter;
27 import org.eclipse.swt.events.ControlEvent;
28 import org.eclipse.swt.events.ControlListener;
29 import org.eclipse.swt.events.FocusAdapter;
30 import org.eclipse.swt.events.FocusEvent;
31 import org.eclipse.swt.events.FocusListener;
32 import org.eclipse.swt.events.MouseAdapter;
33 import org.eclipse.swt.events.MouseEvent;
34 import org.eclipse.swt.graphics.Color;
35 import org.eclipse.swt.graphics.Font;
36 import org.eclipse.swt.graphics.GC;
37 import org.eclipse.swt.graphics.Image;
38 import org.eclipse.swt.widgets.Composite;
39 import org.eclipse.swt.widgets.Control;
40 import org.eclipse.swt.widgets.Label;
41 import org.eclipse.swt.widgets.Menu;
42 import org.eclipse.ui.forms.IManagedForm;
43 import org.eclipse.ui.forms.widgets.TableWrapData;
44 import org.eclipse.ui.forms.widgets.TableWrapLayout;
45
46 import eu.etaxonomy.cdm.model.taxon.Taxon;
47 import eu.etaxonomy.taxeditor.editor.name.NameViewer;
48 import eu.etaxonomy.taxeditor.store.model.Resources;
49
50 /**
51 * Formats <code>GroupedComposite</code> with cosmetic and layout properties specific to the
52 * Editor. This should be used to maintain a consistent look and feel for all Editor
53 * freetext area components, such as DescriptionElementComposite.
54 * <p>
55 * Requires an <code>IManagedForm</code>, whose <code>input</code> is set to the contents
56 * of {@link #getData()} when the <code>GroupedComposite</code> gets focus, i.e. to
57 * populate the property sheet with the data.
58 * </p>
59 * <p>
60 * The <code>IManagedForm</code> is also required to have a <code>Taxon</code> in its
61 * own <code>getData()</code>.
62 * </p>
63 * <p>
64 * The <code>IManagedForm</code> can also used for drawing borders by calling the method
65 * <code>createBorderSupport()</code>.
66 * </p>
67 * @author p.ciardelli
68 * @created 02.06.2008
69 * @version 1.0
70 */
71 abstract public class GroupedComposite extends Composite implements IHasPropertySource {
72 private static final Logger logger = Logger.getLogger(GroupedComposite.class);
73
74 protected AbstractTaxonEditor editor;
75
76 protected NameViewer textViewer;
77 protected IManagedForm managedForm;
78 private Label nonEditableInfoLabel;
79 private DefaultToolTip nonEditableInfoHover;
80
81 private Color defaultGroupBackgroundColor = Resources.getColor(Resources.COLOR_COMPOSITE_BACKGROUND);
82
83 protected Taxon taxon;
84
85 private CompositeBorderDecorator borderDecorator;
86 private FocusListener focusListener;
87 private LineBreakListener lineBreakListener;
88 private ParseListener parseListener;
89
90 private String emptyViewerPrompt;
91
92 public GroupedComposite(AbstractTaxonEditor editor, Composite parent){
93 super(parent, SWT.NONE);
94
95 this.editor = editor;
96 this.managedForm = editor.getManagedForm();
97
98 Object formData = managedForm.getForm().getBody().getData();
99 Assert.isTrue(formData instanceof Taxon,
100 "Managed form must have a Taxon in its data field.");
101 taxon = (Taxon) formData;
102
103 createControl();
104 }
105
106 protected void createControl() {
107 setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB));
108 TableWrapLayout layout = new TableWrapLayout();
109 layout.leftMargin = 0;
110 layout.topMargin = 0;
111 layout.bottomMargin = 0;
112 layout.verticalSpacing = 0;
113 setLayout(layout);
114
115 setBackground(defaultGroupBackgroundColor);
116 }
117
118 protected void createLineWrapSupport() {
119 if (textViewer instanceof NameViewer) {
120 new LineWrapSupport(textViewer, managedForm);
121 } else {
122 logger.warn("Can't create line wrap support because textViewer has not been initialized.");
123 }
124 }
125
126 public Taxon getTaxon() {
127 return taxon;
128 }
129
130 protected void createTextViewer() {
131 textViewer = new NameViewer(this);
132
133 focusListener = new FocusAdapter() {
134 public void focusGained(FocusEvent e) {
135 setFocus();
136 }
137 };
138
139 textViewer.getTextWidget().addFocusListener(focusListener);
140
141 MouseAdapter mouseListener = new MouseAdapter() {
142 public void mouseDown(MouseEvent e) {
143 setFocus();
144 }
145 };
146 this.addMouseListener(mouseListener);
147 textViewer.getRulerControl().addMouseListener(mouseListener);
148 }
149
150 /**
151 * @param icon
152 */
153 public void setIcon(Image icon) {
154 if (textViewer instanceof NameViewer) {
155 textViewer.setIcon(icon);
156 } else {
157 logger.warn("Can't set icon because textViewer has not been initialized.");
158 }
159 }
160
161 /**
162 * @param indent
163 */
164 public void setIndent(int indent) {
165 if (getLayout() instanceof TableWrapLayout) {
166 TableWrapLayout layout = ((TableWrapLayout) getLayout());
167 layout.leftMargin = indent;
168 this.setLayout(layout);
169 } else {
170 logger.warn("Couldn't indent - composite's layout must be TableWrapLayout.");
171 }
172 }
173
174 /* (non-Javadoc)
175 * @see org.eclipse.swt.widgets.Composite#setFocus()
176 */
177 public boolean setFocus() {
178 setSelection();
179 return true;
180 }
181
182 /**
183 * Set background on selection and the cursor to EOL
184 * I wanted to move this to setFocus() but that leads to an infinite loop
185 */
186 public void setSelected(){
187 setBackground(Resources.getColor(Resources.COLOR_COMPOSITE_SELECTED));
188 if(textViewer != null){
189 textViewer.setCursorToEOL();
190 }
191 }
192
193 /**
194 *
195 */
196 protected void setSelection() {
197 managedForm.setInput(this);
198 }
199
200 /**
201 *
202 */
203 @Override
204 public void setBackground(Color color) {
205 super.setBackground(color);
206 if(textViewer != null){
207 textViewer.setBackground(color);
208 }
209 }
210
211 /* (non-Javadoc)
212 * @see org.eclipse.swt.widgets.Control#setFont(org.eclipse.swt.graphics.Font)
213 */
214 public void setFont(Font font) {
215 if (textViewer != null) {
216 textViewer.getTextWidget().setFont(font);
217 } else {
218 logger.warn("Can't set font because textViewer has not been initalized.");
219 }
220 }
221
222 public TextViewer getTextViewer() {
223 return textViewer;
224 }
225
226 /**
227 * If <code>textViewer</code> has already been set, it will show a
228 * <code>prompt</code> along the lines of "Click here to start entering data"
229 * when empty.
230 *
231 * @param prompt
232 */
233 public void createEmptyViewerPrompt(final String prompt) {
234
235 emptyViewerPrompt = prompt;
236
237 Assert.isNotNull(textViewer);
238 // new EmptyTextViewerPrompt(getTextViewer(), prompt);
239
240 final StyledText textControl = textViewer.getTextWidget();
241 final IDocument document = textViewer.getDocument();
242 final Font promptFont = Resources
243 .getFont(Resources.FONT_DEFAULT_PROMPT);
244 setFocusListener(new FocusListener() {
245
246
247 public void focusGained(FocusEvent e) {
248 if (document.get().equals(prompt)) {
249 textControl.setFont(getViewerFont());
250 document.set("");
251 }
252 }
253
254
255 public void focusLost(FocusEvent e) {
256 if (document.getLength() == 0) {
257 initEmptyText();
258 }
259 }
260
261 });
262 textControl.addFocusListener(getFocusListener());
263
264 if (document.getLength() == 0) {
265 textControl.setFont(promptFont);
266 document.set(prompt);
267 }
268 }
269
270 abstract protected Font getViewerFont();
271
272 protected void initEmptyText() {
273 textViewer.getTextWidget().setFont(
274 Resources
275 .getFont(Resources.FONT_DEFAULT_PROMPT));
276
277 textViewer.getDocument().set(getEmptyTextPrompt());
278 textViewer.setCursorToEOL();
279 }
280
281 protected String getEmptyTextPrompt() {
282 if (emptyViewerPrompt == null) {
283 emptyViewerPrompt = "Click to edit";
284 }
285 return emptyViewerPrompt;
286 }
287
288 private void setFocusListener(FocusListener focusListener) {
289 this.focusListener = focusListener;
290 }
291
292 private FocusListener getFocusListener() {
293 return focusListener;
294 }
295
296 /**
297 *
298 */
299 public void createBorderSupport() {
300
301 if (textViewer == null) {
302 logger.warn("Could not create border support - getTextViewer() returned null.");
303 } else {
304 borderDecorator = new CompositeBorderDecorator(
305 textViewer.getTextWidget(), managedForm);
306 borderDecorator.setLoseFocus(false);
307 textViewer.getTextWidget().addFocusListener(borderDecorator);
308 }
309 }
310
311 protected void setBorderDecorator(CompositeBorderDecorator borderDecorator) {
312 this.borderDecorator = borderDecorator;
313 }
314
315 protected CompositeBorderDecorator getBorderDecorator() {
316 return borderDecorator;
317 }
318
319 public void drawBorder() {
320 if (borderDecorator != null) {
321 borderDecorator.paintBorder();
322 }
323 }
324
325 /**
326 * @param isDirty
327 */
328 protected void setDirty(boolean isDirty) {
329 managedForm.dirtyStateChanged();
330 }
331
332 protected ContextMenu createContextMenu() {
333 if (textViewer != null) {
334 ContextMenu contextMenu = new ContextMenu(textViewer.getRulerControl());
335 textViewer.getTextWidget().setMenu(contextMenu.getMenu());
336 return contextMenu;
337 } else {
338 logger.warn("Can't create menu because textViewer has not been initalized.");
339 return null;
340 }
341 }
342
343 public void setMenu (Menu menu) {
344 super.setMenu(menu);
345
346 if (textViewer != null) {
347 textViewer.getRulerControl().setMenu(menu);
348 textViewer.getTextWidget().setMenu(menu);
349 }
350 }
351
352 public void unpaintBorder() {
353 if (borderDecorator != null) {
354 borderDecorator.unpaintBorder();
355 }
356 }
357
358 private Control draggableControl;
359 private DragSource dragSource;
360
361 protected void setDraggableControl(Control control) {
362 draggableControl = control;
363 }
364
365 public DragSource getDragSource() {
366 return dragSource;
367 }
368
369 public void setIsDraggable(boolean draggable) {
370
371 if (draggable) {
372
373 if (dragSource != null) {
374 // Already initialized
375 return;
376 }
377
378 if (draggableControl == null) {
379 throw new NullPointerException(
380 "Draggable control must be set to add draggability");
381 }
382
383 Transfer[] types = new Transfer[] { WidgetTransfer.getInstance() };
384 int operations = DND.DROP_MOVE | DND.DROP_COPY | DND.DROP_LINK;
385
386 dragSource = new DragSource(draggableControl, operations);
387 dragSource.setTransfer(types);
388 dragSource.addDragListener(dragSourceListener);
389
390 } else {
391 dragSource = null;
392 }
393 }
394
395 /**
396 * Drag listener which passes the Composite as the data in a drag event.
397 */
398 DragSourceListener dragSourceListener = new DragSourceAdapter() {
399
400 public void dragStart(DragSourceEvent event) {
401 if (textViewer != null) {
402 textViewer.getTextWidget().setFocus();
403 }
404 event.doit = true;
405 }
406
407 public void dragSetData(DragSourceEvent event) {
408 WidgetTransfer.getInstance().setWidget(GroupedComposite.this);
409 }
410 };
411
412 private String nonEditableText;
413 ControlListener nonEditableResizeListener = new ControlAdapter() {
414
415 int width = 0;
416
417 public void controlResized(ControlEvent e) {
418 if (nonEditableInfoLabel.getBounds().width == width) {
419 return;
420 }
421 width = nonEditableInfoLabel.getBounds().width;
422 if (nonEditableInfoLabel.getBounds().width > 0) {
423 nonEditableInfoLabel.setText(
424 Dialog.shortenText(nonEditableText.toUpperCase(),
425 nonEditableInfoLabel));
426 }
427 }
428 };
429
430 private String nonEditableHoverText;
431
432 private LabelEllipsisListener nonEditableLabelEllipsisListener;
433
434 /**
435 * nonEditableInfo is a label displayed underneath a GroupedComposite's
436 * input field. For instance, NameComposites display things like name relations,
437 * sec. references, etc. here.
438 *
439 * @param info
440 */
441 public void setNonEditableInfo(String info) {
442 // TODO non editable info should only be drawn once, when everything else is drawn
443 info = info.toUpperCase();
444 if (nonEditableInfoLabel == null) {
445 nonEditableText = info;
446 nonEditableHoverText = info;
447
448 nonEditableInfoLabel = new Label(this, SWT.NONE);
449 TableWrapData layoutData = new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.TOP);
450 // Set indent to viewer ruler's width
451 if (textViewer != null && textViewer.getRulerControl() != null) {
452 // TODO right justify
453 layoutData.indent = NameViewer.RULER_WIDTH;
454 }
455 nonEditableInfoLabel.setLayoutData(layoutData);
456
457
458
459 nonEditableLabelEllipsisListener = new LabelEllipsisListener(nonEditableInfoLabel) {
460 @Override
461 public String getLabelText() {
462 return nonEditableText.toUpperCase();
463 }
464 };
465 nonEditableInfoLabel.addControlListener(nonEditableLabelEllipsisListener);
466
467 nonEditableInfoHover = new DefaultToolTip(nonEditableInfoLabel);
468 nonEditableInfoHover.setRespectDisplayBounds(true);
469
470 } else {
471 nonEditableText += ", " + info;
472 nonEditableHoverText += "\n" + info;
473
474 }
475 nonEditableInfoHover.setText(nonEditableHoverText);
476 }
477
478 /**
479 * If the user entering text requires parsing, call this method and override
480 * the method parse().
481 */
482 protected void createParser() {
483 if (textViewer != null) {
484 parseListener = new ParseListener() {
485 @Override
486 public void parse(String text) {
487 GroupedComposite.this.parse(text);
488 }
489 };
490 textViewer.getTextWidget().addModifyListener(parseListener);
491 } else {
492 logger.warn("Can't create parser because textViewer has not been initalized.");
493 }
494 }
495
496 /**
497 * Supposed to be overridden in implementing classes
498 *
499 * @param text
500 */
501 protected void parse(String text) {
502 logger.warn("No parse method defined for this composite.");
503 }
504
505 /**
506 * If the user hitting carriage return should cause something to happen -
507 * i.e. the creation of a new composite - call this method and override
508 * the method handleSplitText().
509 */
510 protected void createLineBreakListener() {
511 if (textViewer != null) {
512 lineBreakListener = new LineBreakListener() {
513 @Override
514 public void handleSplitText(String text) {
515 GroupedComposite.this.handleSplitText(text);
516 }
517 };
518
519 textViewer.getTextWidget().addVerifyListener(lineBreakListener);
520 textViewer.getTextWidget().addKeyListener(lineBreakListener);
521 } else {
522 logger.warn("Can't create line break listener because textViewer has not been initalized.");
523 }
524 }
525
526 /**
527 * Supposed to be overridden in implementing classes
528 *
529 * @param text
530 */
531 protected void handleSplitText(String text) {
532 logger.warn("No handleSplitText method defined for this composite.");
533 }
534
535 /* (non-Javadoc)
536 * @see eu.etaxonomy.taxeditor.editor.name.GroupedComposite#dispose()
537 */
538 public void dispose () {
539
540 if (dragSource != null) {
541 dragSource.removeDragListener(dragSourceListener);
542 }
543
544 if (textViewer != null) {
545 StyledText textWidget = textViewer.getTextWidget();
546
547 if (focusListener != null) {
548 textWidget.removeFocusListener(focusListener);
549 }
550
551 if (parseListener != null) {
552 textWidget.removeModifyListener(parseListener);
553 }
554
555 if (lineBreakListener != null) {
556 textWidget.removeVerifyListener(lineBreakListener);
557 textWidget.removeKeyListener(lineBreakListener);
558 }
559
560 if (borderDecorator != null) {
561 textWidget.removeFocusListener(borderDecorator);
562 }
563 }
564
565 if (nonEditableInfoLabel != null && nonEditableLabelEllipsisListener != null) {
566 nonEditableInfoLabel.removeControlListener(nonEditableLabelEllipsisListener);
567 }
568
569 super.dispose();
570 }
571
572 public static final String ELLIPSIS = "...";
573
574 /**
575 * @param textValue
576 * @param control
577 * @return
578 * @see org.eclipse.jface.dialogs.Dialog#shortenText(String, Control)
579 */
580 public static String shortenText(String textValue, Control control) {
581 if (textValue == null) {
582 return null;
583 }
584 GC gc = new GC(control);
585 int maxWidth = control.getBounds().width;
586 int maxExtent = gc.textExtent(textValue).x;
587 if (maxExtent < maxWidth) {
588 gc.dispose();
589 return textValue;
590 }
591 int length = textValue.length();
592 int charsToClip = Math.round(0.95f*length * (1 - ((float)maxWidth/maxExtent)));
593
594 int end = length - charsToClip;
595 while (end > 0) {
596 String s1 = textValue.substring(0, end);
597 String s = s1 + ELLIPSIS;
598 int l = gc.textExtent(s).x;
599 if (l < maxWidth) {
600 gc.dispose();
601 return s;
602 }
603 end--;
604 }
605 gc.dispose();
606 return textValue;
607
608 }
609
610 abstract class LabelEllipsisListener extends ControlAdapter {
611
612 private Label label;
613 int width = 0;
614
615 LabelEllipsisListener(Label label) {
616 this.label = label;
617 }
618
619 abstract public String getLabelText();
620
621 public void controlResized(ControlEvent e) {
622 if (label.getBounds().width == width) {
623 return;
624 }
625 width = label.getBounds().width;
626 if (label.getBounds().width > 0) {
627 label.setText(
628 // Dialog.shortenText(getLabelText(), label));
629 shortenText(getLabelText(), label));
630 }
631 }
632 }
633 }