]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.browsing.ui.swt/src/org/simantics/browsing/ui/swt/widgets/TrackedCombo.java
Migrated source code from Simantics SVN
[simantics/platform.git] / bundles / org.simantics.browsing.ui.swt / src / org / simantics / browsing / ui / swt / widgets / TrackedCombo.java
1 /*******************************************************************************\r
2  * Copyright (c) 2007, 2012 Association for Decentralized Information Management\r
3  * in Industry THTH ry.\r
4  * All rights reserved. This program and the accompanying materials\r
5  * are made available under the terms of the Eclipse Public License v1.0\r
6  * which accompanies this distribution, and is available at\r
7  * http://www.eclipse.org/legal/epl-v10.html\r
8  *\r
9  * Contributors:\r
10  *     VTT Technical Research Centre of Finland - initial API and implementation\r
11  *******************************************************************************/\r
12 package org.simantics.browsing.ui.swt.widgets;\r
13 \r
14 import java.util.Map;\r
15 \r
16 import org.eclipse.core.runtime.Assert;\r
17 import org.eclipse.core.runtime.ListenerList;\r
18 import org.eclipse.jface.dialogs.IInputValidator;\r
19 import org.eclipse.jface.resource.ColorDescriptor;\r
20 import org.eclipse.jface.resource.JFaceResources;\r
21 import org.eclipse.jface.resource.LocalResourceManager;\r
22 import org.eclipse.jface.resource.ResourceManager;\r
23 import org.eclipse.swt.SWT;\r
24 import org.eclipse.swt.events.DisposeEvent;\r
25 import org.eclipse.swt.events.DisposeListener;\r
26 import org.eclipse.swt.events.FocusEvent;\r
27 import org.eclipse.swt.events.FocusListener;\r
28 import org.eclipse.swt.events.KeyEvent;\r
29 import org.eclipse.swt.events.KeyListener;\r
30 import org.eclipse.swt.events.ModifyEvent;\r
31 import org.eclipse.swt.events.ModifyListener;\r
32 import org.eclipse.swt.events.MouseEvent;\r
33 import org.eclipse.swt.events.MouseListener;\r
34 import org.eclipse.swt.events.MouseTrackListener;\r
35 import org.eclipse.swt.events.SelectionEvent;\r
36 import org.eclipse.swt.events.SelectionListener;\r
37 import org.eclipse.swt.graphics.Color;\r
38 import org.eclipse.swt.graphics.Font;\r
39 import org.eclipse.swt.graphics.Point;\r
40 import org.eclipse.swt.graphics.RGB;\r
41 import org.eclipse.swt.widgets.Composite;\r
42 import org.eclipse.swt.widgets.Display;\r
43 import org.simantics.browsing.ui.swt.widgets.impl.ITrackedColorProvider;\r
44 import org.simantics.browsing.ui.swt.widgets.impl.ReadFactory;\r
45 import org.simantics.browsing.ui.swt.widgets.impl.TextModifyListener;\r
46 import org.simantics.browsing.ui.swt.widgets.impl.TrackedModifyEvent;\r
47 import org.simantics.browsing.ui.swt.widgets.impl.Widget;\r
48 import org.simantics.browsing.ui.swt.widgets.impl.WidgetSupport;\r
49 import org.simantics.db.management.ISessionContext;\r
50 import org.simantics.db.procedure.Listener;\r
51 import org.simantics.utils.threads.SWTThread;\r
52 \r
53 public class TrackedCombo implements Widget {\r
54 \r
55     private static final int      EDITING                 = 1 << 0;\r
56     private static final int      MODIFIED_DURING_EDITING = 1 << 1;\r
57 \r
58     /**\r
59      * Used to tell whether or not a mouseDown has occurred after a focusGained\r
60      * event to be able to select the whole text field when it is pressed for\r
61      * the first time while the widget holds focus.\r
62      */\r
63     private static final int      MOUSE_DOWN_FIRST_TIME   = 1 << 2;\r
64     private static final int      MOUSE_INSIDE_CONTROL    = 1 << 3;\r
65 \r
66     private int                   state;\r
67 \r
68     private int                   caretPositionBeforeEdit;\r
69 \r
70     private String                textBeforeEdit;\r
71 \r
72     final private org.eclipse.swt.widgets.Combo combo;\r
73 \r
74     private CompositeListener     listener;\r
75 \r
76     private ListenerList          modifyListeners;\r
77 \r
78     private IInputValidator       validator;\r
79 \r
80     private ITrackedColorProvider colorProvider;\r
81 \r
82     private final ResourceManager       resourceManager;\r
83 \r
84     private ReadFactory<?, Map<String, Object>> itemFactory;\r
85     protected ReadFactory<?, String> selectionFactory;\r
86 \r
87 \r
88 \r
89 \r
90     public void setItemFactory(ReadFactory<?, Map<String, Object>> itemFactory) {\r
91         this.itemFactory = itemFactory;\r
92     }\r
93 \r
94     public void setSelectionFactory(ReadFactory<?, String> selectionFactory) {\r
95         this.selectionFactory = selectionFactory;\r
96     }\r
97 \r
98     public void setFont(Font font) {\r
99         combo.setFont(font);\r
100     }\r
101 \r
102     @Override\r
103     public void setInput(ISessionContext context, Object input) {\r
104 \r
105         if (modifyListeners != null) {\r
106             for (Object listener : modifyListeners.getListeners()) {\r
107                 if(listener instanceof Widget) {\r
108                     ((Widget) listener).setInput(context, input);\r
109                 }\r
110             }\r
111         }\r
112 \r
113         if(itemFactory != null) {\r
114             itemFactory.listen(context, input, new Listener<Map<String, Object>>() {\r
115 \r
116                 @Override\r
117                 public void exception(Throwable t) {\r
118                     t.printStackTrace();\r
119                 }\r
120 \r
121                 @Override\r
122                 public void execute(final Map<String, Object> items) {\r
123                     if(isDisposed()) return;\r
124                     Runnable r = new Runnable() {\r
125 \r
126                         @Override\r
127                         public void run() {\r
128                             if(isDisposed()) return;\r
129 //                          System.out.println("Combo received new items: " + items.size());\r
130 //                            if(modifyListeners != null)\r
131 //                                for(Object listener : modifyListeners.getListeners()) combo.removeModifyListener((ModifyListener)listener);\r
132                             if(listener != null)\r
133                                 combo.removeModifyListener(listener);\r
134                             combo.setData(items);\r
135                             combo.clearSelection();\r
136                             try {\r
137                                 combo.removeAll();\r
138                             } catch (Throwable t) {\r
139                                 t.printStackTrace();\r
140                             }\r
141                             int index = 0;\r
142                             for(String key : items.keySet()) {\r
143 //                              System.out.println("-" + key);\r
144                                 combo.add(key);\r
145                                 combo.setData(key, index++);\r
146                             }\r
147                             String selectionKey = (String)combo.getData("_SelectionKey");\r
148                             if(selectionKey != null) {\r
149                                 Integer selectionIndex = (Integer)combo.getData(selectionKey);\r
150                                 if(selectionIndex != null) combo.select(selectionIndex);\r
151                             }\r
152 //                            if(modifyListeners != null)\r
153 //                                for(Object listener : modifyListeners.getListeners()) combo.addModifyListener((ModifyListener)listener);\r
154                             if(listener != null)\r
155                                 combo.addModifyListener(listener);\r
156                             //label.setSize(200, 20);\r
157 //                          label.getParent().layout();\r
158 //                          label.getParent().getParent().layout();\r
159                         }\r
160 \r
161                     };\r
162                     if(SWTThread.getThreadAccess().currentThreadAccess())\r
163                         r.run();\r
164                     else\r
165                         combo.getDisplay().asyncExec(r);        \r
166                 }\r
167 \r
168                 @Override\r
169                 public boolean isDisposed() {\r
170                     return combo.isDisposed();\r
171                 }\r
172 \r
173             });\r
174         }\r
175 \r
176         if(selectionFactory != null) {\r
177             selectionFactory.listen(context, input, new Listener<String>() {\r
178 \r
179                 @Override\r
180                 public void exception(Throwable t) {\r
181                     t.printStackTrace();\r
182                 }\r
183 \r
184                 @Override\r
185                 public void execute(final String selectionKey) {\r
186                     if(isDisposed()) return;\r
187                     combo.getDisplay().asyncExec(new Runnable() {\r
188 \r
189                         @Override\r
190                         public void run() {\r
191                             if(isDisposed()) return;\r
192 //                          System.out.println("Combo received new selection key: " + selectionKey);\r
193 \r
194                             if(selectionKey == null) return;\r
195                             combo.setData("_SelectionKey", selectionKey);\r
196                             Integer selectionIndex = (Integer)combo.getData(selectionKey);\r
197                             if(selectionIndex != null) combo.select(selectionIndex);\r
198 \r
199                         }\r
200 \r
201                     });\r
202                 }\r
203 \r
204                 @Override\r
205                 public boolean isDisposed() {\r
206                     return combo.isDisposed();\r
207                 }\r
208 \r
209             });\r
210         }\r
211 \r
212     }\r
213 \r
214     public void manualSelect(int index) {\r
215         \r
216         String key = combo.getItem(index);\r
217         combo.setData("_SelectionKey", key);\r
218         combo.select(index);\r
219         \r
220     }\r
221     \r
222     private class DefaultColorProvider implements ITrackedColorProvider {\r
223         private final ColorDescriptor highlightColor = ColorDescriptor.createFrom(new RGB(254, 255, 197));\r
224         private final ColorDescriptor inactiveColor = ColorDescriptor.createFrom(new RGB(245, 246, 190));\r
225         private final ColorDescriptor invalidInputColor = ColorDescriptor.createFrom(new RGB(255, 128, 128));\r
226 \r
227         @Override\r
228         public Color getEditingBackground() {\r
229             return null;\r
230         }\r
231 \r
232         @Override\r
233         public Color getHoverBackground() {\r
234             return resourceManager.createColor(highlightColor);\r
235         }\r
236 \r
237         @Override\r
238         public Color getInactiveBackground() {\r
239             return resourceManager.createColor(inactiveColor);\r
240         }\r
241 \r
242         @Override\r
243         public Color getInvalidBackground() {\r
244             return resourceManager.createColor(invalidInputColor);\r
245         }\r
246     };\r
247 \r
248     /**\r
249      * A composite of many UI listeners for creating the functionality of this\r
250      * class.\r
251      */\r
252     private class CompositeListener\r
253     implements ModifyListener, DisposeListener, KeyListener, MouseTrackListener,\r
254     MouseListener, FocusListener, SelectionListener\r
255     {\r
256         // Keyboard/editing events come in the following order:\r
257         //   1. keyPressed\r
258         //   2. verifyText\r
259         //   3. modifyText\r
260         //   4. keyReleased\r
261 \r
262         @Override\r
263         public void modifyText(ModifyEvent e) {\r
264             //System.out.println("modifyText: " + e);\r
265             setModified(true);\r
266 \r
267             String valid = isTextValid();\r
268             if (valid != null) {\r
269                 setBackground(colorProvider.getInvalidBackground());\r
270             } else {\r
271                 if (isEditing())\r
272                     setBackground(colorProvider.getEditingBackground());\r
273                 else\r
274                     setBackground(colorProvider.getInactiveBackground());\r
275             }\r
276         }\r
277 \r
278         @Override\r
279         public void widgetDisposed(DisposeEvent e) {\r
280             getWidget().removeModifyListener(this);\r
281         }\r
282 \r
283         @Override\r
284         public void keyPressed(KeyEvent e) {\r
285             //System.out.println("keyPressed: " + e);\r
286             if (!isEditing()) {\r
287                 // ESC, ENTER & keypad ENTER must not start editing\r
288                 if (e.keyCode == SWT.ESC)\r
289                     return;\r
290 \r
291                 if (e.keyCode == SWT.F2) {\r
292                     startEdit(true);\r
293                 } else if (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) {\r
294                     startEdit(false);\r
295                 } else if (e.keyCode == SWT.TAB) {\r
296                     combo.traverse(((e.stateMask & SWT.SHIFT) != 0) ? SWT.TRAVERSE_TAB_PREVIOUS : SWT.TRAVERSE_TAB_NEXT);\r
297                     e.doit = false;\r
298                 } else if (e.character != '\0') {\r
299                     startEdit(false);\r
300                 }\r
301 \r
302             } else {\r
303                 // ESC reverts any changes made during this edit\r
304                 if (e.keyCode == SWT.ESC) {\r
305                     revertEdit();\r
306                 }\r
307                 if (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) {\r
308                     applyEdit();\r
309                     e.doit = false;\r
310                 }\r
311             }\r
312         }\r
313 \r
314         @Override\r
315         public void keyReleased(KeyEvent e) {\r
316             //System.out.println("keyReleased: " + e);\r
317         }\r
318 \r
319         @Override\r
320         public void mouseEnter(MouseEvent e) {\r
321             //System.out.println("mouseEnter");\r
322             if (!isEditing()) {\r
323                 setBackground(colorProvider.getHoverBackground());\r
324             }\r
325             setMouseInsideControl(true);\r
326         }\r
327 \r
328         @Override\r
329         public void mouseExit(MouseEvent e) {\r
330             //System.out.println("mouseExit");\r
331             if (!isEditing()) {\r
332                 setBackground(colorProvider.getInactiveBackground());\r
333             }\r
334             setMouseInsideControl(false);\r
335         }\r
336 \r
337         @Override\r
338         public void mouseHover(MouseEvent e) {\r
339             //System.out.println("mouseHover");\r
340             setMouseInsideControl(true);\r
341         }\r
342 \r
343         @Override\r
344         public void mouseDoubleClick(MouseEvent e) {\r
345             //System.out.println("mouseDoubleClick: " + e);\r
346             if (e.button == 1) {\r
347                 getWidget().setSelection(new Point(0, combo.getText().length()));\r
348             }\r
349         }\r
350 \r
351         @Override\r
352         public void mouseDown(MouseEvent e) {\r
353             //System.out.println("mouseDown: " + e);\r
354             if (!isEditing()) {\r
355                 // In reality we should never get here, since focusGained\r
356                 // always comes before mouseDown, but let's keep this\r
357                 // fallback just to be safe.\r
358                 if (e.button == 1) {\r
359                     startEdit(true);\r
360                 }\r
361             } else {\r
362                 if (e.button == 1 && (state & MOUSE_DOWN_FIRST_TIME) != 0) {\r
363                     getWidget().setSelection(new Point(0, combo.getText().length()));\r
364                     state &= ~MOUSE_DOWN_FIRST_TIME;\r
365                 }\r
366             }\r
367         }\r
368 \r
369         @Override\r
370         public void mouseUp(MouseEvent e) {\r
371         }\r
372 \r
373         @Override\r
374         public void focusGained(FocusEvent e) {\r
375             //System.out.println("focusGained");\r
376             if (!isEditing()) {\r
377                 startEdit(true);\r
378             }\r
379         }\r
380 \r
381         @Override\r
382         public void focusLost(FocusEvent e) {\r
383             //System.out.println("focusLost");\r
384             if (isEditing()) {\r
385                 applyEdit();\r
386             }\r
387         }\r
388 \r
389         @Override\r
390         public void widgetDefaultSelected(SelectionEvent e) {\r
391             applyEdit();\r
392         }\r
393 \r
394         @Override\r
395         public void widgetSelected(SelectionEvent e) {\r
396             applyEdit();\r
397         }\r
398         \r
399     }\r
400 \r
401     public TrackedCombo(Composite parent, WidgetSupport support, int style) {\r
402         combo = new org.eclipse.swt.widgets.Combo(parent, style);\r
403         combo.setData("org.simantics.browsing.ui.widgets.Combo", this);\r
404         this.resourceManager = new LocalResourceManager(JFaceResources.getResources(), combo);\r
405         this.colorProvider = new DefaultColorProvider();\r
406         support.register(this);\r
407         initialize();\r
408     }\r
409 \r
410     /**\r
411      * Common initialization. Assumes that text is already created.\r
412      */\r
413     private void initialize() {\r
414         Assert.isNotNull(combo);\r
415 \r
416         combo.setBackground(colorProvider.getInactiveBackground());\r
417 //        combo.setDoubleClickEnabled(false);\r
418 \r
419         listener = new CompositeListener();\r
420 \r
421         combo.addModifyListener(listener);\r
422         combo.addDisposeListener(listener);\r
423         combo.addKeyListener(listener);\r
424         combo.addMouseTrackListener(listener);\r
425         combo.addMouseListener(listener);\r
426         combo.addFocusListener(listener);\r
427         combo.addSelectionListener(listener);\r
428     }\r
429 \r
430     private void startEdit(boolean selectAll) {\r
431         if (isEditing()) {\r
432             // Print some debug incase we end are in an invalid state\r
433             System.out.println("TrackedText: BUG: startEdit called when in editing state");\r
434         }\r
435         //System.out.println("start edit: selectall=" + selectAll + ", text=" + text.getText() + ", caretpos=" + caretPositionBeforeEdit);\r
436 \r
437         // Backup text-field data for reverting purposes\r
438         caretPositionBeforeEdit = combo.getSelection().x;\r
439         textBeforeEdit = combo.getText();\r
440 \r
441         // Signal editing state\r
442         setBackground(colorProvider.getEditingBackground());\r
443 \r
444         if (selectAll) {\r
445             combo.setSelection(new Point(0, combo.getText().length()));\r
446         }\r
447         state |= EDITING | MOUSE_DOWN_FIRST_TIME;\r
448     }\r
449 \r
450     private void applyEdit() {\r
451         try {\r
452             if (isTextValid() != null) {\r
453                 combo.setText(textBeforeEdit);\r
454             } else if (isModified() && !combo.getText().equals(textBeforeEdit)) {\r
455                 //System.out.println("apply");\r
456                 if (modifyListeners != null) {\r
457                     TrackedModifyEvent event = new TrackedModifyEvent(combo, combo.getText());\r
458                     for (Object o : modifyListeners.getListeners()) {\r
459                         ((TextModifyListener) o).modifyText(event);\r
460                     }\r
461                 }\r
462             }\r
463         } finally {\r
464             endEdit();\r
465         }\r
466     }\r
467 \r
468     private void endEdit() {\r
469         if (!isEditing()) {\r
470             // Print some debug incase we end are in an invalid state\r
471             //ExceptionUtils.logError(new Exception("BUG: endEdit called when not in editing state"));\r
472             System.out.println();\r
473         }\r
474         setBackground(isMouseInsideControl() ? colorProvider.getHoverBackground() : colorProvider.getInactiveBackground());\r
475         //System.out.println("endEdit: " + text.getText() + ", caret: " + text.getCaretLocation() + ", selection: " + text.getSelection());\r
476         // Always move the caret to the end of the string\r
477         combo.setSelection(new Point(combo.getText().length(), 0));\r
478         state &= ~(EDITING | MOUSE_DOWN_FIRST_TIME);\r
479         setModified(false);\r
480     }\r
481 \r
482     private void revertEdit() {\r
483         if (!isEditing()) {\r
484             // Print some debug incase we end are in an invalid state\r
485             //ExceptionUtils.logError(new Exception("BUG: revertEdit called when not in editing state"));\r
486             System.out.println("BUG: revertEdit called when not in editing state");\r
487         }\r
488         combo.setText(textBeforeEdit);\r
489         combo.setSelection(new Point(caretPositionBeforeEdit, 0));\r
490         setBackground(isMouseInsideControl() ? colorProvider.getHoverBackground() : colorProvider.getInactiveBackground());\r
491         state &= ~(EDITING | MOUSE_DOWN_FIRST_TIME);\r
492         setModified(false);\r
493     }\r
494 \r
495     private boolean isEditing() {\r
496         return (state & EDITING) != 0;\r
497     }\r
498 \r
499     private void setModified(boolean modified) {\r
500         if (modified) {\r
501             state |= MODIFIED_DURING_EDITING;\r
502         } else {\r
503             state &= ~MODIFIED_DURING_EDITING;\r
504         }\r
505     }\r
506 \r
507     private boolean isMouseInsideControl() {\r
508         return (state & MOUSE_INSIDE_CONTROL) != 0;\r
509     }\r
510 \r
511     private boolean isModified() {\r
512         return (state & MODIFIED_DURING_EDITING) != 0;\r
513     }\r
514 \r
515     private void setMouseInsideControl(boolean inside) {\r
516         if (inside)\r
517             state |= MOUSE_INSIDE_CONTROL;\r
518         else\r
519             state &= ~MOUSE_INSIDE_CONTROL;\r
520     }\r
521 \r
522     public void setEditable(boolean editable) {\r
523         if (editable) {\r
524             combo.setEnabled(true);\r
525             setBackground(isMouseInsideControl() ? colorProvider.getHoverBackground() : colorProvider.getInactiveBackground());\r
526         } else {\r
527             combo.setEnabled(false);\r
528             combo.setBackground(null);\r
529         }\r
530     }\r
531 \r
532     public void setText(String text) {\r
533         this.combo.setText(text);\r
534     }\r
535 \r
536     public void setTextWithoutNotify(String text) {\r
537         this.combo.removeModifyListener(listener);\r
538         setText(text);\r
539         this.combo.addModifyListener(listener);\r
540     }\r
541 \r
542     public org.eclipse.swt.widgets.Combo getWidget() {\r
543         return combo;\r
544     }\r
545 \r
546     public synchronized void addModifyListener(TextModifyListener listener) {\r
547         if (modifyListeners == null) {\r
548             modifyListeners = new ListenerList(ListenerList.IDENTITY);\r
549         }\r
550         modifyListeners.add(listener);\r
551     }\r
552 \r
553     public synchronized void removeModifyListener(TextModifyListener listener) {\r
554         if (modifyListeners == null)\r
555             return;\r
556         modifyListeners.remove(listener);\r
557     }\r
558 \r
559     public void setInputValidator(IInputValidator validator) {\r
560         if (validator != this.validator) {\r
561             this.validator = validator;\r
562         }\r
563     }\r
564 \r
565     private String isTextValid() {\r
566         if (validator != null) {\r
567             return validator.isValid(getWidget().getText());\r
568         }\r
569         return null;\r
570     }\r
571 \r
572     public void setColorProvider(ITrackedColorProvider provider) {\r
573         Assert.isNotNull(provider);\r
574         this.colorProvider = provider;\r
575     }\r
576 \r
577     public void setBackground(Color color) {\r
578         if (!combo.getEnabled()) {\r
579             // Do not alter background when the widget is not editable.\r
580             return;\r
581         }\r
582         combo.setBackground(color);\r
583     }\r
584 \r
585     public void setForeground(Color color) {\r
586         combo.setForeground(color);\r
587     }\r
588 \r
589     public boolean isDisposed() {\r
590         return combo.isDisposed();\r
591     }\r
592 \r
593     public Display getDisplay() {\r
594         return combo.getDisplay();\r
595     }\r
596 \r
597 }\r