1+ /*******************************************************************************
2+ * Copyright (c) 2024 Patrick Ziegler and others.
3+ *
4+ * This program and the accompanying materials
5+ * are made available under the terms of the Eclipse Public License 2.0
6+ * which accompanies this distribution, and is available at
7+ * https://www.eclipse.org/legal/epl-2.0/
8+ *
9+ * SPDX-License-Identifier: EPL-2.0
10+ *
11+ * Contributors:
12+ * Patrick Ziegler - initial API and implementation
13+ ******************************************************************************/
14+ package org .eclipse .jface .text ;
15+
16+ import org .eclipse .swt .SWT ;
17+ import org .eclipse .swt .events .FocusAdapter ;
18+ import org .eclipse .swt .events .FocusEvent ;
19+ import org .eclipse .swt .events .MouseAdapter ;
20+ import org .eclipse .swt .events .MouseEvent ;
21+ import org .eclipse .swt .layout .GridData ;
22+ import org .eclipse .swt .layout .GridLayout ;
23+ import org .eclipse .swt .widgets .Composite ;
24+ import org .eclipse .swt .widgets .Display ;
25+ import org .eclipse .swt .widgets .Text ;
26+
27+ import org .eclipse .core .runtime .jobs .Job ;
28+
29+ import org .eclipse .jface .viewers .StructuredViewer ;
30+
31+ /**
32+ * The abstract base class for a composite containing both a column viewer and a text widget. The
33+ * text drives the filter of the viewer.
34+ *
35+ * @since 3.27
36+ */
37+ public abstract class AbstractFilteredStructuredViewer extends Composite {
38+
39+ /**
40+ * The filter text widget to be used by this viewer. This value may be {@code null} if there is
41+ * no filter widget, or if the controls have not yet been created.
42+ */
43+ protected Text filterText ;
44+
45+ /**
46+ * The Composite on which the filter controls are created. This is used to set the background
47+ * color of the filter controls to match the surrounding controls.
48+ */
49+ protected Composite filterComposite ;
50+
51+ /**
52+ * The text to initially show in the filter text control.
53+ */
54+ protected String initialText = "" ; //$NON-NLS-1$
55+
56+ /**
57+ * The job used to refresh the viewer.
58+ */
59+ private Job refreshJob ;
60+
61+ /**
62+ * The parent composite of the filtered viewer.
63+ */
64+ protected Composite parent ;
65+
66+ /**
67+ * Whether or not to show the filter controls (text and clear button).
68+ */
69+ protected boolean showFilterControls ;
70+
71+ /**
72+ * Tells whether this filtered viewer is used to make quick selections. In this mode the first
73+ * match in the viewer is automatically selected while filtering and the 'Enter' key is not used
74+ * to move the focus to the viewer.
75+ */
76+ private boolean quickSelectionMode = false ;
77+
78+ /**
79+ * Time for refresh job delay in terms of expansion in long value
80+ */
81+ private final long refreshJobDelayInMillis ;
82+
83+ protected AbstractFilteredStructuredViewer (Composite parent , int style , long refreshJobDelayInMillis ) {
84+ super (parent , style );
85+ this .parent = parent ;
86+ this .refreshJobDelayInMillis = refreshJobDelayInMillis ;
87+ }
88+
89+ /**
90+ * Create the filtered viewer.
91+ *
92+ * @param style the style bits for the {@link StructuredViewer}.
93+ */
94+ protected void init (int style ) {
95+ showFilterControls = isShowFilterControls ();
96+ createControl (parent , style );
97+ createRefreshJob ();
98+ setFont (parent .getFont ());
99+ getViewer ().getControl ().addDisposeListener (e -> refreshJob .cancel ());
100+ }
101+
102+ /**
103+ * Indicates whether the filter controls (text and clear button) should be shown.
104+ *
105+ * @return {@code true}, if the filter controls should be shown, otherwise {@code false}.
106+ */
107+ protected abstract boolean isShowFilterControls ();
108+
109+ /**
110+ * Get the quick selection mode.
111+ *
112+ * @return {@code true}, if enabled otherwise {@code false}.
113+ */
114+ protected boolean isQuickSelectionMode () {
115+ return quickSelectionMode ;
116+ }
117+
118+ /**
119+ * Create the filtered viewer's controls. Subclasses should override.
120+ *
121+ * @param parentComposite the parent
122+ * @param style SWT style bits used to create the viewer
123+ */
124+ protected void createControl (Composite parentComposite , int style ) {
125+ GridLayout layout = new GridLayout ();
126+ layout .marginHeight = 0 ;
127+ layout .marginWidth = 0 ;
128+ setLayout (layout );
129+
130+ if (parentComposite .getLayout () instanceof GridLayout ) {
131+ setLayoutData (new GridData (SWT .FILL , SWT .FILL , true , true ));
132+ }
133+
134+ if (showFilterControls ) {
135+ filterComposite = new Composite (this , SWT .NONE );
136+ GridLayout filterLayout = new GridLayout ();
137+ filterLayout .marginHeight = 0 ;
138+ filterLayout .marginWidth = 0 ;
139+ filterComposite .setLayout (filterLayout );
140+ filterComposite .setFont (parentComposite .getFont ());
141+
142+ createFilterControls (filterComposite );
143+ filterComposite .setLayoutData (new GridData (SWT .FILL , SWT .BEGINNING , true , false ));
144+ }
145+ }
146+
147+ /**
148+ * Create the filter controls. By default, a text and corresponding tool bar button that clears
149+ * the contents of the text is created. Subclasses may override.
150+ *
151+ * @param parentComposite parent {@link Composite} of the filter controls
152+ * @return the {@link Composite} that contains the filter controls
153+ */
154+ protected Composite createFilterControls (Composite parentComposite ) {
155+ createFilterText (parentComposite );
156+ return parentComposite ;
157+ }
158+
159+ /**
160+ * Create the refresh job for the receiver.
161+ */
162+ private void createRefreshJob () {
163+ refreshJob = doCreateRefreshJob ();
164+ refreshJob .setSystem (true );
165+ }
166+
167+ /**
168+ * Creates a workbench job that will refresh the viewer based on the current filter text.
169+ * Subclasses may override.
170+ *
171+ * @return a workbench job that can be scheduled to refresh the viewer
172+ */
173+ protected abstract Job doCreateRefreshJob ();
174+
175+ /**
176+ * Creates the filter text and adds listeners. This method calls
177+ * {@link #doCreateFilterText(Composite)} to create the text control. Subclasses should override
178+ * {@link #doCreateFilterText(Composite)} instead of overriding this method.
179+ *
180+ * @param parentComposite {@link Composite} of the filter text
181+ */
182+ protected void createFilterText (Composite parentComposite ) {
183+ filterText = doCreateFilterText (parentComposite );
184+
185+ filterText .addFocusListener (new FocusAdapter () {
186+ @ Override
187+ public void focusGained (FocusEvent e ) {
188+ /*
189+ * Running in an asyncExec because the selectAll() does not appear to work when
190+ * using mouse to give focus to text.
191+ */
192+ Display display = filterText .getDisplay ();
193+ display .asyncExec (() -> {
194+ if (!filterText .isDisposed ()) {
195+ if (getInitialText ().equals (filterText .getText ().trim ())) {
196+ filterText .selectAll ();
197+ }
198+ }
199+ });
200+ }
201+
202+ @ Override
203+ public void focusLost (FocusEvent e ) {
204+ if (filterText .getText ().equals (initialText )) {
205+ //We cannot call clearText() due to
206+ // https://bugs.eclipse.org/bugs/show_bug.cgi?id=260664
207+ doClearText ();
208+ }
209+ }
210+ });
211+
212+ filterText .addMouseListener (new MouseAdapter () {
213+ @ Override
214+ public void mouseDown (MouseEvent e ) {
215+ if (filterText .getText ().equals (initialText )) {
216+ //We cannot call clearText() due to
217+ // https://bugs.eclipse.org/bugs/show_bug.cgi?id=260664
218+ doClearText ();
219+ }
220+ }
221+ });
222+
223+ filterText .addModifyListener (e -> textChanged ());
224+
225+ GridData gridData = new GridData (SWT .FILL , SWT .CENTER , true , false );
226+ filterText .setLayoutData (gridData );
227+ }
228+
229+ /**
230+ * Creates the text control for entering the filter text. Subclasses may override.
231+ *
232+ * @param parentComposite the parent composite
233+ * @return the text widget
234+ */
235+ protected Text doCreateFilterText (Composite parentComposite ) {
236+ return new Text (parentComposite , SWT .SINGLE | SWT .BORDER | SWT .SEARCH | SWT .ICON_CANCEL );
237+ }
238+
239+ /**
240+ * Update the receiver after the text has changed.
241+ */
242+ protected void textChanged () {
243+ // cancel currently running job first, to prevent unnecessary redraw
244+ refreshJob .cancel ();
245+ refreshJob .schedule (getRefreshJobDelay ());
246+ }
247+
248+ /**
249+ * Return the time delay that should be used when scheduling the filter refresh job. Subclasses
250+ * may override.
251+ *
252+ * @return a time delay in milliseconds before the job should run
253+ */
254+ protected long getRefreshJobDelay () {
255+ return refreshJobDelayInMillis ;
256+ }
257+
258+ /**
259+ * Clears the text in the filter text widget.
260+ */
261+ protected void clearText () {
262+ doClearText ();
263+ }
264+
265+ private void doClearText () {
266+ // https://bugs.eclipse.org/bugs/show_bug.cgi?id=260664
267+ setFilterText ("" ); //$NON-NLS-1$
268+ textChanged ();
269+ }
270+
271+ /**
272+ * Set the text in the filter control.
273+ *
274+ * @param filterText the text to set.
275+ */
276+ protected void setFilterText (String filterText ) {
277+ if (this .filterText != null ) {
278+ this .filterText .setText (filterText );
279+ selectAll ();
280+ }
281+ }
282+
283+ /**
284+ * Get the structured viewer of the receiver.
285+ *
286+ * @return the structured viewer
287+ */
288+ public abstract StructuredViewer getViewer ();
289+
290+ /**
291+ * Get the filter text for the receiver, if it was created. Otherwise return {@code null}.
292+ *
293+ * @return the filter Text, or null if it was not created
294+ */
295+ public Text getFilterControl () {
296+ return filterText ;
297+ }
298+
299+ /**
300+ * Convenience method to return the text of the filter control. If the text widget is not
301+ * created, then null is returned.
302+ *
303+ * @return String in the text, or null if the text does not exist
304+ */
305+ protected String getFilterString () {
306+ return filterText != null ? filterText .getText () : null ;
307+ }
308+
309+ /**
310+ * Set the text that will be shown until the first focus. A default value is provided, so this
311+ * method only need be called if overriding the default initial text is desired.
312+ *
313+ * @param text initial text to appear in text field
314+ */
315+ public void setInitialText (String text ) {
316+ initialText = text ;
317+ if (filterText != null ) {
318+ filterText .setMessage (text );
319+ if (filterText .isFocusControl ()) {
320+ setFilterText (initialText );
321+ textChanged ();
322+ } else {
323+ getDisplay ().asyncExec (() -> {
324+ if (!filterText .isDisposed () && filterText .isFocusControl ()) {
325+ setFilterText (initialText );
326+ textChanged ();
327+ }
328+ });
329+ }
330+ } else {
331+ setFilterText (initialText );
332+ textChanged ();
333+ }
334+ }
335+
336+ /**
337+ * Sets whether this filtered viewer is used to make quick selections. In this mode the first
338+ * match in the viewer is automatically selected while filtering and the 'Enter' key is not used
339+ * to move the focus to the viewer.
340+ * <p>
341+ * By default, this is set to {@code false}.
342+ * </p>
343+ *
344+ * @param enabled {@code true} if this filtered viewer is used to make quick selections,
345+ * {@code false} otherwise
346+ */
347+ public void setQuickSelectionMode (boolean enabled ) {
348+ this .quickSelectionMode = enabled ;
349+ }
350+
351+ /**
352+ * Select all text in the filter text field.
353+ */
354+ protected void selectAll () {
355+ if (filterText != null ) {
356+ filterText .selectAll ();
357+ }
358+ }
359+
360+ /**
361+ * Get the initial text for the receiver.
362+ *
363+ * @return String
364+ */
365+ protected String getInitialText () {
366+ return initialText ;
367+ }
368+ }
0 commit comments