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