Type specific graph explorer filter area
[simantics/platform.git] / bundles / org.simantics.browsing.ui.swt / src / org / simantics / browsing / ui / swt / FilterArea.java
1 /*******************************************************************************
2  * Copyright (c) 2007, 2010 Association for Decentralized Information Management
3  * in Industry THTH ry.
4  * All rights reserved. This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License v1.0
6  * which accompanies this distribution, and is available at
7  * http://www.eclipse.org/legal/epl-v10.html
8  *
9  * Contributors:
10  *     VTT Technical Research Centre of Finland - initial API and implementation
11  *******************************************************************************/
12 package org.simantics.browsing.ui.swt;
13
14 import java.util.HashMap;
15 import java.util.Map;
16 import java.util.concurrent.TimeUnit;
17 import java.util.concurrent.atomic.AtomicInteger;
18
19 import org.eclipse.jface.layout.GridDataFactory;
20 import org.eclipse.jface.layout.GridLayoutFactory;
21 import org.eclipse.jface.resource.FontDescriptor;
22 import org.eclipse.jface.resource.ImageDescriptor;
23 import org.eclipse.jface.resource.JFaceResources;
24 import org.eclipse.jface.resource.LocalResourceManager;
25 import org.eclipse.jface.viewers.IPostSelectionProvider;
26 import org.eclipse.jface.viewers.ISelection;
27 import org.eclipse.jface.viewers.ISelectionChangedListener;
28 import org.eclipse.jface.viewers.SelectionChangedEvent;
29 import org.eclipse.swt.SWT;
30 import org.eclipse.swt.accessibility.ACC;
31 import org.eclipse.swt.accessibility.AccessibleAdapter;
32 import org.eclipse.swt.accessibility.AccessibleControlAdapter;
33 import org.eclipse.swt.accessibility.AccessibleControlEvent;
34 import org.eclipse.swt.accessibility.AccessibleEvent;
35 import org.eclipse.swt.events.ModifyEvent;
36 import org.eclipse.swt.events.ModifyListener;
37 import org.eclipse.swt.events.MouseAdapter;
38 import org.eclipse.swt.events.MouseEvent;
39 import org.eclipse.swt.events.MouseMoveListener;
40 import org.eclipse.swt.events.MouseTrackListener;
41 import org.eclipse.swt.events.SelectionAdapter;
42 import org.eclipse.swt.events.SelectionEvent;
43 import org.eclipse.swt.graphics.Image;
44 import org.eclipse.swt.graphics.Point;
45 import org.eclipse.swt.layout.GridData;
46 import org.eclipse.swt.widgets.Composite;
47 import org.eclipse.swt.widgets.Control;
48 import org.eclipse.swt.widgets.Display;
49 import org.eclipse.swt.widgets.Label;
50 import org.eclipse.swt.widgets.Text;
51 import org.eclipse.ui.ISharedImages;
52 import org.eclipse.ui.internal.WorkbenchImages;
53 import org.eclipse.ui.internal.WorkbenchMessages;
54 import org.simantics.browsing.ui.GraphExplorer;
55 import org.simantics.browsing.ui.NodeContext;
56 import org.simantics.browsing.ui.common.processors.FilterSelectionRequestQueryProcessor;
57 import org.simantics.browsing.ui.common.views.IFilterArea;
58 import org.simantics.browsing.ui.common.views.IFilterAreaProvider;
59 import org.simantics.browsing.ui.common.views.IFocusable;
60 import org.simantics.db.layer0.SelectionHints;
61 import org.simantics.utils.threads.SWTThread;
62 import org.simantics.utils.threads.ThreadUtils;
63 import org.simantics.utils.ui.ISelectionUtils;
64
65 @SuppressWarnings("restriction")
66 public class FilterArea extends Composite implements IFocusable, IFilterArea, IFilterAreaProvider {
67
68     protected static final Integer                 FILTER_DELAY = 500;
69
70     private final LocalResourceManager             resourceManager;
71
72     protected GraphExplorer                        explorer;
73
74     protected FilterSelectionRequestQueryProcessor queryProcessor;
75
76     private Text                                   filterText;
77     private NodeContext                                                    currentContext;
78
79     /**
80      * The control representing the clear button for the filter text entry. This
81      * value may be <code>null</code> if no such button exists, or if the
82      * controls have not yet been created.
83      */
84     protected Control                              clearButtonControl;
85
86     /**
87      * Construct the filter area UI component.
88      * 
89      * @param explorer
90      * @param queryProcessor
91      * @param parent
92      * @param style
93      */
94     public FilterArea(final GraphExplorer explorer, final FilterSelectionRequestQueryProcessor queryProcessor, Composite parent, int style) {
95         super(parent, style | SWT.BORDER);
96
97         this.explorer = explorer;
98         this.queryProcessor = queryProcessor;
99
100         resourceManager = new LocalResourceManager(JFaceResources.getResources(parent.getDisplay()), this);
101
102         GridDataFactory.fillDefaults().grab(true, false).span(2, 1).applyTo(this);
103         GridLayoutFactory.fillDefaults().margins(0, 0).spacing(0, 0).numColumns(2).equalWidth(false).applyTo(this);
104
105         createFilterText(this);
106         createFilterCancelIcon(this);
107
108         this.setBackground(filterText.getBackground());
109
110         addTextModifyListener();
111         addExplorerSelectionListener();
112     }
113
114     private void createFilterText(FilterArea filterArea) {
115         //filterText = new Text(this, SWT.SINGLE | SWT.FLAT | SWT.SEARCH | SWT.ICON_CANCEL);
116         filterText = new Text(this, SWT.SINGLE | SWT.ICON_CANCEL);
117         GridDataFactory.fillDefaults().grab(true, false).applyTo(filterText);
118
119         filterText.setFont(resourceManager.createFont(FontDescriptor.createFrom(filterText.getFont()).increaseHeight(-1)));
120
121         // if we're using a field with built in cancel we need to listen for
122         // default selection changes (which tell us the cancel button has been
123         // pressed)
124         if ((filterText.getStyle() & SWT.ICON_CANCEL) != 0) {
125             filterText.addSelectionListener(new SelectionAdapter() {
126                 @Override
127                 public void widgetDefaultSelected(SelectionEvent e) {
128                     if (e.detail == SWT.ICON_CANCEL)
129                         clearText();
130                 }
131             });
132         }
133
134         GridData gridData = new GridData(SWT.FILL, SWT.CENTER, true, true);
135
136         // if the text widget supported cancel then it will have it's own
137         // integrated button. We can take all of the space.
138         if ((filterText.getStyle() & SWT.ICON_CANCEL) != 0)
139             gridData.horizontalSpan = 2;
140         filterText.setLayoutData(gridData);
141     }
142
143     private void createFilterCancelIcon(Composite parent) {
144         // only create the button if the text widget doesn't support one
145         // natively
146         if ((filterText.getStyle() & SWT.ICON_CANCEL) == 0) {
147             final Image inactiveImage = WorkbenchImages.getImage(ISharedImages.IMG_ETOOL_CLEAR_DISABLED);
148             final Image activeImage = WorkbenchImages.getImage(ISharedImages.IMG_ETOOL_CLEAR);
149             final Image pressedImage = (Image) resourceManager.get( ImageDescriptor.createWithFlags( ImageDescriptor.createFromImage( activeImage ), SWT.IMAGE_GRAY ) );
150
151             final Label clearButton= new Label(parent, SWT.NONE);
152             clearButton.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, false, false));
153             clearButton.setImage(inactiveImage);
154             clearButton.setBackground(parent.getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND));
155             clearButton.setToolTipText(WorkbenchMessages.FilteredTree_ClearToolTip);
156             clearButton.addMouseListener(new MouseAdapter() {
157                 private MouseMoveListener fMoveListener;
158
159                 @Override
160                 public void mouseDown(MouseEvent e) {
161                     clearButton.setImage(pressedImage);
162                     fMoveListener= new MouseMoveListener() {
163                         private boolean fMouseInButton= true;
164
165                         @Override
166                         public void mouseMove(MouseEvent e) {
167                             boolean mouseInButton= isMouseInButton(e);
168                             if (mouseInButton != fMouseInButton) {
169                                 fMouseInButton= mouseInButton;
170                                 clearButton.setImage(mouseInButton ? pressedImage : inactiveImage);
171                             }
172                         }
173                     };
174                     clearButton.addMouseMoveListener(fMoveListener);
175                 }
176
177                 @Override
178                 public void mouseUp(MouseEvent e) {
179                     if (fMoveListener != null) {
180                         clearButton.removeMouseMoveListener(fMoveListener);
181                         fMoveListener= null;
182                         boolean mouseInButton= isMouseInButton(e);
183                         clearButton.setImage(mouseInButton ? activeImage : inactiveImage);
184                         if (mouseInButton) {
185                             clearText();
186                             filterText.setFocus();
187                         }
188                     }
189                 }
190
191                 private boolean isMouseInButton(MouseEvent e) {
192                     Point buttonSize = clearButton.getSize();
193                     return 0 <= e.x && e.x < buttonSize.x && 0 <= e.y && e.y < buttonSize.y;
194                 }
195             });
196             clearButton.addMouseTrackListener(new MouseTrackListener() {
197                 @Override
198                 public void mouseEnter(MouseEvent e) {
199                     clearButton.setImage(activeImage);
200                 }
201
202                 @Override
203                 public void mouseExit(MouseEvent e) {
204                     clearButton.setImage(inactiveImage);
205                 }
206
207                 @Override
208                 public void mouseHover(MouseEvent e) {
209                 }
210             });
211             clearButton.getAccessible().addAccessibleListener(
212                     new AccessibleAdapter() {
213                         @Override
214                         public void getName(AccessibleEvent e) {
215                             e.result= WorkbenchMessages.FilteredTree_AccessibleListenerClearButton;
216                         }
217                     });
218             clearButton.getAccessible().addAccessibleControlListener(
219                     new AccessibleControlAdapter() {
220                         @Override
221                         public void getRole(AccessibleControlEvent e) {
222                             e.detail= ACC.ROLE_PUSHBUTTON;
223                         }
224                     });
225             this.clearButtonControl = clearButton;
226         }
227     }
228
229     /**
230      * Clears the text in the filter text widget.
231      */
232     protected void clearText() {
233         //System.out.println("clearText");
234         filterText.setText(""); //$NON-NLS-1$
235         // TODO: HACK: this can be used to access filter for root context
236         explorer.select(null);
237         //textChanged();
238     }
239     
240     protected void addTextModifyListener() {
241         filterText.addModifyListener(new ModifyListener() {
242
243             
244             @Override
245             public void modifyText(ModifyEvent e) {
246                 final String filter = filterText.getText();
247                 //System.out.println("Scheduling setFilter(" + context + ", " + filter + ")");
248                 setFilter(filter, false);
249             }
250         });
251     }
252     
253     private Map<NodeContext, AtomicInteger> modCount = new HashMap<NodeContext, AtomicInteger>();
254
255     
256     public void setFilter(String filter) {
257         setFilter(filter, true);
258     }
259     
260     protected void setFilter(String filter, boolean updateUI) {
261         final NodeContext context = getFilteredNode();
262         if (context == null)
263             return;
264         
265          AtomicInteger i = modCount.get(context);
266          if (i == null)
267              modCount.put(context, new AtomicInteger());
268          final AtomicInteger counter = modCount.get(context);
269          final int count = counter.incrementAndGet();
270
271          ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
272              @Override
273              public void run() {
274                  int newCount = counter.get();
275                  if (newCount != count)
276                      return;
277                  //System.out.println("schedule setFilter(" + context + ", " + filter + ")");
278                  modCount.remove(context);
279                  if (isDisposed())
280                      return;
281                  ThreadUtils.asyncExec(SWTThread.getThreadAccess(getDisplay()), new Runnable() {
282                      @Override
283                      public void run() {
284                          if (isDisposed())
285                              return;
286                          applyFilter(context, filter, updateUI);
287                      }
288                  });
289              }
290          }, FILTER_DELAY, TimeUnit.MILLISECONDS);
291         
292     }
293     
294     protected void applyFilter(NodeContext context, String filter, boolean updateUI) {
295          if (updateUI) {
296                  String current = filterText.getText();
297                  if (!current.equals(filter))
298                          filterText.setText(filter);
299          }
300          //System.out.println("queryProcessor.setFilter(" + context + ", " + filter + ")");
301          queryProcessor.setFilter(context, filter.isEmpty() ? null : filter);
302     }
303
304     protected void addExplorerSelectionListener() {
305         IPostSelectionProvider selectionProvider = (IPostSelectionProvider)explorer.getAdapter(IPostSelectionProvider.class);
306         selectionProvider.addSelectionChangedListener(new ISelectionChangedListener() {
307             @Override
308             public void selectionChanged(SelectionChangedEvent event) {
309
310                 ISelection selection = event.getSelection();
311                 NodeContext context = ISelectionUtils.getSinglePossibleKey(selection, SelectionHints.KEY_MAIN, NodeContext.class);
312                 if(context == null) {
313                     context = ISelectionUtils.filterSingleSelection(selection, NodeContext.class);
314                 }
315
316                 if (context == null) {
317                     context = explorer.getRoot();
318                 }
319
320                 assert (context != null);
321
322                 String filter = queryProcessor.getFilter(context);
323
324                 if (filter == null)
325                     filter = "";
326
327                 //filterText.setData(null);
328                 //System.out.println("selection changed, new filter: " + filter + " for context " + context + ", currentContext=" + currentContext);
329                 currentContext = context;
330                 filterText.setText(filter);
331                 //filterText.setData("context", context);
332             }
333         });
334     }
335
336     protected NodeContext getFilteredNode() {
337         if(currentContext != null) return currentContext;
338         else return explorer.getRoot();
339 //      return currentContext;
340 //      NodeContext context = (NodeContext)filterText.getData("context");
341 //      if (context == null)
342 //              context = explorer.getRoot();
343 //        return context;
344
345     }
346
347     @Override
348     public void focus() {
349         if (filterText.isDisposed())
350             return;
351         Display d = filterText.getDisplay();
352         if (d.getThread() == Thread.currentThread()) {
353             doSetFocus();
354         } else {
355             d.asyncExec(new Runnable() {
356                 @Override
357                 public void run() {
358                     if (filterText.isDisposed())
359                         return;
360                     doSetFocus();
361                 }
362             });
363         }
364     }
365
366     @Override
367     public void dispose() {
368         
369         if (isDisposed())
370             return;
371         
372         resourceManager.dispose();
373         explorer = null;
374         queryProcessor = null;
375         filterText = null;
376         currentContext = null;
377         clearButtonControl = null;
378         
379         super.dispose();
380         
381     }
382
383     protected void doSetFocus() {
384         filterText.selectAll();
385         filterText.setFocus();
386     }
387
388     @Override
389     public IFilterArea getFilterArea() {
390         return this;
391     }
392
393 }