0e8ddd63d2b86c86c31459d8d38810bb45afd484
[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             Map<NodeContext, AtomicInteger> modCount = new HashMap<NodeContext, AtomicInteger>();
244
245             @Override
246             public void modifyText(ModifyEvent e) {
247
248                 final NodeContext context = getFilteredNode();
249                 if (context == null)
250                     return;
251
252                 final String filter = filterText.getText();
253                 //System.out.println("Scheduling setFilter(" + context + ", " + filter + ")");
254
255                 AtomicInteger i = modCount.get(context);
256                 if (i == null)
257                     modCount.put(context, new AtomicInteger());
258                 final AtomicInteger counter = modCount.get(context);
259                 final int count = counter.incrementAndGet();
260
261                 ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
262                     @Override
263                     public void run() {
264                         int newCount = counter.get();
265                         if (newCount != count)
266                             return;
267                         //System.out.println("schedule setFilter(" + context + ", " + filter + ")");
268                         modCount.remove(context);
269                         if (isDisposed())
270                             return;
271                         ThreadUtils.asyncExec(SWTThread.getThreadAccess(getDisplay()), new Runnable() {
272                             @Override
273                             public void run() {
274                                 if (isDisposed())
275                                     return;
276                                 //System.out.println("queryProcessor.setFilter(" + context + ", " + filter + ")");
277                                 queryProcessor.setFilter(context, filter.isEmpty() ? null : filter);
278                             }
279                         });
280                     }
281                 }, FILTER_DELAY, TimeUnit.MILLISECONDS);
282             }
283         });
284     }
285
286     protected void addExplorerSelectionListener() {
287         IPostSelectionProvider selectionProvider = (IPostSelectionProvider)explorer.getAdapter(IPostSelectionProvider.class);
288         selectionProvider.addSelectionChangedListener(new ISelectionChangedListener() {
289             @Override
290             public void selectionChanged(SelectionChangedEvent event) {
291
292                 ISelection selection = event.getSelection();
293                 NodeContext context = ISelectionUtils.getSinglePossibleKey(selection, SelectionHints.KEY_MAIN, NodeContext.class);
294                 if(context == null) {
295                     context = ISelectionUtils.filterSingleSelection(selection, NodeContext.class);
296                 }
297
298                 if (context == null) {
299                     context = explorer.getRoot();
300                 }
301
302                 assert (context != null);
303
304                 String filter = queryProcessor.getFilter(context);
305
306                 if (filter == null)
307                     filter = "";
308
309                 //filterText.setData(null);
310                 //System.out.println("selection changed, new filter: " + filter + " for context " + context + ", currentContext=" + currentContext);
311                 currentContext = context;
312                 filterText.setText(filter);
313                 //filterText.setData("context", context);
314             }
315         });
316     }
317
318     protected NodeContext getFilteredNode() {
319         if(currentContext != null) return currentContext;
320         else return explorer.getRoot();
321 //      return currentContext;
322 //      NodeContext context = (NodeContext)filterText.getData("context");
323 //      if (context == null)
324 //              context = explorer.getRoot();
325 //        return context;
326
327     }
328
329     @Override
330     public void focus() {
331         if (filterText.isDisposed())
332             return;
333         Display d = filterText.getDisplay();
334         if (d.getThread() == Thread.currentThread()) {
335             doSetFocus();
336         } else {
337             d.asyncExec(new Runnable() {
338                 @Override
339                 public void run() {
340                     if (filterText.isDisposed())
341                         return;
342                     doSetFocus();
343                 }
344             });
345         }
346     }
347
348     @Override
349     public void dispose() {
350         
351         if (isDisposed())
352             return;
353         
354         resourceManager.dispose();
355         explorer = null;
356         queryProcessor = null;
357         filterText = null;
358         currentContext = null;
359         clearButtonControl = null;
360         
361         super.dispose();
362         
363     }
364
365     protected void doSetFocus() {
366         filterText.selectAll();
367         filterText.setFocus();
368     }
369
370     @Override
371     public IFilterArea getFilterArea() {
372         return this;
373     }
374
375 }