tmf : Add search dialog to timegraph views
[deliverable/tracecompass.git] / tmf / org.eclipse.tracecompass.tmf.ui / src / org / eclipse / tracecompass / tmf / ui / views / timegraph / TimeGraphFindDialog.java
1 /*****************************************************************************
2 * Copyright (c) 2016 Ericsson
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the Eclipse Public License v1.0
5 * which accompanies this distribution, and is available at
6 * http://www.eclipse.org/legal/epl-v10.html
7 *****************************************************************************/
8 package org.eclipse.tracecompass.tmf.ui.views.timegraph;
9
10 import java.util.ArrayList;
11 import java.util.HashMap;
12 import java.util.HashSet;
13 import java.util.List;
14 import java.util.Map;
15 import java.util.Set;
16 import java.util.regex.Pattern;
17 import java.util.regex.PatternSyntaxException;
18
19 import org.eclipse.jdt.annotation.NonNull;
20 import org.eclipse.jdt.annotation.Nullable;
21 import org.eclipse.jface.action.LegacyActionTools;
22 import org.eclipse.jface.dialogs.Dialog;
23 import org.eclipse.jface.dialogs.IDialogSettings;
24 import org.eclipse.jface.fieldassist.ComboContentAdapter;
25 import org.eclipse.jface.text.FindReplaceDocumentAdapterContentProposalProvider;
26 import org.eclipse.jface.util.Util;
27 import org.eclipse.swt.SWT;
28 import org.eclipse.swt.events.ModifyEvent;
29 import org.eclipse.swt.events.ModifyListener;
30 import org.eclipse.swt.events.SelectionAdapter;
31 import org.eclipse.swt.events.SelectionEvent;
32 import org.eclipse.swt.events.SelectionListener;
33 import org.eclipse.swt.events.ShellAdapter;
34 import org.eclipse.swt.events.ShellEvent;
35 import org.eclipse.swt.events.TraverseEvent;
36 import org.eclipse.swt.events.TraverseListener;
37 import org.eclipse.swt.graphics.Point;
38 import org.eclipse.swt.graphics.Rectangle;
39 import org.eclipse.swt.layout.GridData;
40 import org.eclipse.swt.layout.GridLayout;
41 import org.eclipse.swt.widgets.Button;
42 import org.eclipse.swt.widgets.Combo;
43 import org.eclipse.swt.widgets.Composite;
44 import org.eclipse.swt.widgets.Control;
45 import org.eclipse.swt.widgets.Event;
46 import org.eclipse.swt.widgets.Group;
47 import org.eclipse.swt.widgets.Label;
48 import org.eclipse.swt.widgets.Shell;
49 import org.eclipse.tracecompass.common.core.NonNullUtils;
50 import org.eclipse.tracecompass.internal.tmf.ui.Activator;
51 import org.eclipse.tracecompass.internal.tmf.ui.Messages;
52 import org.eclipse.tracecompass.tmf.ui.views.timegraph.AbstractTimeGraphView.FindTarget;
53 import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ITimeGraphEntry;
54 import org.eclipse.ui.fieldassist.ContentAssistCommandAdapter;
55 import org.eclipse.ui.texteditor.ITextEditorActionDefinitionIds;
56
57 import com.google.common.collect.BiMap;
58 import com.google.common.collect.HashBiMap;
59
60 /**
61 * Find dialog to search entries into a time graph. This implementation is based
62 * on the org.eclipse.ui.texteditor.FindReplaceDialog of Eclipse.
63 *
64 * @author Jean-Christian Kouame
65 */
66 class TimeGraphFindDialog extends Dialog {
67
68 /**
69 * Updates the find dialog on activation changes.
70 */
71 class ActivationListener extends ShellAdapter {
72 /*
73 * @see ShellListener#shellActivated(ShellEvent)
74 */
75 @Override
76 public void shellActivated(ShellEvent e) {
77 fActiveShell = (Shell) e.widget;
78 updateButtonState();
79
80 if (fGiveFocusToFindField && getShell() == fActiveShell && okToUse(fFindField)) {
81 fFindField.setFocus();
82 }
83
84 }
85
86 /*
87 * @see ShellListener#shellDeactivated(ShellEvent)
88 */
89 @Override
90 public void shellDeactivated(ShellEvent e) {
91 fGiveFocusToFindField = false;
92
93 storeSettings();
94
95 fActiveShell = null;
96 updateButtonState();
97 }
98 }
99
100 /**
101 * Modify listener to update the search result in case of incremental
102 * search.
103 */
104 private class FindModifyListener implements ModifyListener {
105
106 /*
107 * @see ModifyListener#modifyText(ModifyEvent)
108 */
109 @Override
110 public void modifyText(ModifyEvent e) {
111 updateButtonState();
112 }
113 }
114
115 /** The size of the dialogs search history. */
116 private static final int HISTORY_SIZE = 5;
117
118 private final String WRAP = "wrap"; //$NON-NLS-1$
119 private final String CASE_SENSITIVE = "casesensitive"; //$NON-NLS-1$
120 private final String WHOLE_WORD = "wholeword"; //$NON-NLS-1$
121 private final String IS_REGEX_EXPRESSION = "isRegEx"; //$NON-NLS-1$
122 private final String FIND_HISTORY = "findhistory"; //$NON-NLS-1$
123
124 private boolean fWrapInit;
125 private boolean fCaseInit;
126 private boolean fWholeWordInit;
127 private boolean fForwardInit;
128 private boolean fIsRegExInit;
129
130 private @NonNull List<String> fFindHistory;
131
132 private static Shell fParentShell;
133 private Shell fActiveShell;
134
135 private final ActivationListener fActivationListener = new ActivationListener();
136 private final FindModifyListener fFindModifyListener = new FindModifyListener();
137
138 private Label fStatusLabel;
139 private Button fForwardRadioButton;
140 private Button fCaseCheckBox;
141 private Button fWrapCheckBox;
142 private Button fWholeWordCheckBox;
143 private Button fIsRegExCheckBox;
144
145 private Button fFindNextButton;
146 private Combo fFindField;
147
148 /**
149 * Find command adapters.
150 */
151 private ContentAssistCommandAdapter fContentAssistFindField;
152
153 private Rectangle fDialogPositionInit;
154
155 private IDialogSettings fDialogSettings;
156 /**
157 * <code>true</code> if the find field should receive focus the next time
158 * the dialog is activated, <code>false</code> otherwise.
159 */
160 private boolean fGiveFocusToFindField = true;
161
162 /**
163 * Holds the mnemonic/button pairs for all buttons.
164 */
165 private HashMap<Character, Button> fMnemonicButtonMap = new HashMap<>();
166
167 private @Nullable FindTarget fFindTarget;
168
169 /**
170 * Creates a new dialog with the given shell as parent.
171 *
172 * @param parentShell
173 * the parent shell
174 */
175 public TimeGraphFindDialog(Shell parentShell) {
176 super(parentShell);
177
178 fParentShell = null;
179 fFindTarget = null;
180
181 fDialogPositionInit = null;
182 fFindHistory = new ArrayList<>(HISTORY_SIZE - 1);
183
184 fWrapInit = true;
185 fCaseInit = false;
186 fIsRegExInit = false;
187 fWholeWordInit = false;
188 fForwardInit = true;
189
190 readConfiguration();
191 setShellStyle(getShellStyle() & ~SWT.APPLICATION_MODAL);
192 setBlockOnOpen(false);
193 }
194
195 @Override
196 protected boolean isResizable() {
197 return true;
198 }
199
200 /**
201 * Returns <code>true</code> if control can be used.
202 *
203 * @param control
204 * the control to be checked
205 * @return <code>true</code> if control can be used
206 */
207 private static boolean okToUse(Control control) {
208 return control != null && !control.isDisposed();
209 }
210
211 @Override
212 public void create() {
213 super.create();
214
215 Shell shell = getShell();
216 shell.addShellListener(fActivationListener);
217
218 // fill in combo contents
219 fFindField.removeModifyListener(fFindModifyListener);
220 updateCombo(fFindField, fFindHistory);
221 fFindField.addModifyListener(fFindModifyListener);
222
223 // get find string
224 initFindStringFromSelection();
225
226 // set dialog position
227 if (fDialogPositionInit != null) {
228 shell.setBounds(fDialogPositionInit);
229 }
230
231 shell.setText(Messages.TimeGraphFindDialog_FindTitle);
232 }
233
234 /**
235 * Creates the options configuration section of the find dialog.
236 *
237 * @param parent
238 * the parent composite
239 * @return the options configuration section
240 */
241 private Composite createConfigPanel(Composite parent) {
242
243 Composite panel = new Composite(parent, SWT.NONE);
244 GridLayout layout = new GridLayout();
245 panel.setLayout(layout);
246
247 Composite directionGroup = createDirectionGroup(panel);
248 setGridData(directionGroup, SWT.FILL, true, SWT.FILL, false);
249
250 Composite optionsGroup = createOptionsGroup(panel);
251 setGridData(optionsGroup, SWT.FILL, true, SWT.FILL, true);
252 ((GridData) optionsGroup.getLayoutData()).horizontalSpan = 2;
253
254 return panel;
255 }
256
257 @Override
258 protected Control createContents(Composite parent) {
259
260 Composite panel = new Composite(parent, SWT.NULL);
261 GridLayout layout = new GridLayout();
262 layout.numColumns = 1;
263 layout.makeColumnsEqualWidth = true;
264 panel.setLayout(layout);
265 panel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
266
267 Composite inputPanel = createInputPanel(panel);
268 setGridData(inputPanel, SWT.FILL, true, SWT.TOP, false);
269
270 Composite configPanel = createConfigPanel(panel);
271 setGridData(configPanel, SWT.FILL, true, SWT.TOP, true);
272
273 Composite statusBar = createStatusAndCloseButton(panel);
274 setGridData(statusBar, SWT.FILL, true, SWT.BOTTOM, false);
275
276 panel.addTraverseListener(new TraverseListener() {
277 @Override
278 public void keyTraversed(TraverseEvent e) {
279 if (e.detail == SWT.TRAVERSE_RETURN) {
280 if (!Util.isMac()) {
281 Control controlWithFocus = getShell().getDisplay().getFocusControl();
282 if (controlWithFocus != null && (controlWithFocus.getStyle() & SWT.PUSH) == SWT.PUSH) {
283 return;
284 }
285 }
286 Event event = new Event();
287 event.type = SWT.Selection;
288 event.stateMask = e.stateMask;
289 fFindNextButton.notifyListeners(SWT.Selection, event);
290 e.doit = false;
291 } else if (e.detail == SWT.TRAVERSE_MNEMONIC) {
292 Character mnemonic = new Character(Character.toLowerCase(e.character));
293 Button button = fMnemonicButtonMap.get(mnemonic);
294 if (button != null) {
295 if ((fFindField.isFocusControl() || (button.getStyle() & SWT.PUSH) != 0)
296 && button.isEnabled()) {
297 Event event = new Event();
298 event.type = SWT.Selection;
299 event.stateMask = e.stateMask;
300 if ((button.getStyle() & SWT.RADIO) != 0) {
301 Composite buttonParent = button.getParent();
302 if (buttonParent != null) {
303 Control[] children = buttonParent.getChildren();
304 for (int i = 0; i < children.length; i++) {
305 ((Button) children[i]).setSelection(false);
306 }
307 }
308 button.setSelection(true);
309 } else {
310 button.setSelection(!button.getSelection());
311 }
312 button.notifyListeners(SWT.Selection, event);
313 e.detail = SWT.TRAVERSE_NONE;
314 e.doit = true;
315 }
316 }
317 }
318 }
319 });
320
321 updateButtonState();
322
323 applyDialogFont(panel);
324
325 return panel;
326 }
327
328 private void setContentAssistsEnablement(boolean enable) {
329 fContentAssistFindField.setEnabled(enable);
330 }
331
332 /**
333 * Creates the direction defining part of the options defining section of
334 * the find dialog.
335 *
336 * @param parent
337 * the parent composite
338 * @return the direction defining part
339 */
340 private Composite createDirectionGroup(Composite parent) {
341
342 Composite panel = new Composite(parent, SWT.NONE);
343 GridLayout layout = new GridLayout();
344 layout.marginWidth = 0;
345 layout.marginHeight = 0;
346 panel.setLayout(layout);
347
348 Group group = new Group(panel, SWT.SHADOW_ETCHED_IN);
349 group.setText(Messages.TimeGraphFindDialog_Direction);
350 GridLayout groupLayout = new GridLayout();
351 group.setLayout(groupLayout);
352 group.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
353
354 fForwardRadioButton = new Button(group, SWT.RADIO | SWT.LEFT);
355 fForwardRadioButton.setText(Messages.TimeGraphFindDialog_ForwardRadioButtonLabel);
356 setGridData(fForwardRadioButton, SWT.LEFT, false, SWT.CENTER, false);
357 storeButtonWithMnemonicInMap(fForwardRadioButton);
358
359 Button backwardRadioButton = new Button(group, SWT.RADIO | SWT.LEFT);
360 backwardRadioButton.setText(Messages.TimeGraphFindDialog_BackwardRadioButtonLabel);
361 setGridData(backwardRadioButton, SWT.LEFT, false, SWT.CENTER, false);
362 storeButtonWithMnemonicInMap(backwardRadioButton);
363
364 backwardRadioButton.setSelection(!fForwardInit);
365 fForwardRadioButton.setSelection(fForwardInit);
366
367 return panel;
368 }
369
370 /**
371 * Creates the panel where the user specifies the text to search for
372 *
373 * @param parent
374 * the parent composite
375 * @return the input panel
376 */
377 private Composite createInputPanel(Composite parent) {
378 Composite panel = new Composite(parent, SWT.NULL);
379 GridLayout layout = new GridLayout();
380 layout.numColumns = 2;
381 panel.setLayout(layout);
382
383 Label findLabel = new Label(panel, SWT.LEFT);
384 findLabel.setText(Messages.TimeGraphFindDialog_FindLabel);
385 setGridData(findLabel, SWT.LEFT, false, SWT.CENTER, false);
386
387 // Create the find content assist field
388 ComboContentAdapter contentAdapter = new ComboContentAdapter();
389 FindReplaceDocumentAdapterContentProposalProvider findProposer = new FindReplaceDocumentAdapterContentProposalProvider(true);
390 fFindField = new Combo(panel, SWT.DROP_DOWN | SWT.BORDER);
391 fContentAssistFindField = new ContentAssistCommandAdapter(
392 fFindField,
393 contentAdapter,
394 findProposer,
395 ITextEditorActionDefinitionIds.CONTENT_ASSIST_PROPOSALS,
396 new char[0],
397 true);
398 setGridData(fFindField, SWT.FILL, true, SWT.CENTER, false);
399 fFindField.addModifyListener(fFindModifyListener);
400
401 return panel;
402 }
403
404 /**
405 * Creates the functional options part of the options defining section of
406 * the find dialog.
407 *
408 * @param parent
409 * the parent composite
410 * @return the options group
411 */
412 private Composite createOptionsGroup(Composite parent) {
413
414 Composite panel = new Composite(parent, SWT.NONE);
415 GridLayout layout = new GridLayout();
416 layout.marginWidth = 0;
417 layout.marginHeight = 0;
418 panel.setLayout(layout);
419
420 Group group = new Group(panel, SWT.SHADOW_NONE);
421 group.setText(Messages.TimeGraphFindDialog_Options);
422 GridLayout groupLayout = new GridLayout();
423 groupLayout.numColumns = 1;
424 groupLayout.makeColumnsEqualWidth = true;
425 group.setLayout(groupLayout);
426 group.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
427
428 SelectionListener selectionListener = new SelectionListener() {
429 @Override
430 public void widgetSelected(SelectionEvent e) {
431 storeSettings();
432 }
433
434 @Override
435 public void widgetDefaultSelected(SelectionEvent e) {
436 }
437 };
438
439 fCaseCheckBox = new Button(group, SWT.CHECK | SWT.LEFT);
440 fCaseCheckBox.setText(Messages.TimeGraphFindDialog_CaseCheckBoxLabel);
441 setGridData(fCaseCheckBox, SWT.LEFT, false, SWT.CENTER, false);
442 fCaseCheckBox.setSelection(fCaseInit);
443 fCaseCheckBox.addSelectionListener(selectionListener);
444 storeButtonWithMnemonicInMap(fCaseCheckBox);
445
446 fWrapCheckBox = new Button(group, SWT.CHECK | SWT.LEFT);
447 fWrapCheckBox.setText(Messages.TimeGraphFindDialog_WrapCheckBoxLabel);
448 setGridData(fWrapCheckBox, SWT.LEFT, false, SWT.CENTER, false);
449 fWrapCheckBox.setSelection(fWrapInit);
450 fWrapCheckBox.addSelectionListener(selectionListener);
451 storeButtonWithMnemonicInMap(fWrapCheckBox);
452
453 fWholeWordCheckBox = new Button(group, SWT.CHECK | SWT.LEFT);
454 fWholeWordCheckBox.setText(Messages.TimeGraphFindDialog_WholeWordCheckBoxLabel);
455 setGridData(fWholeWordCheckBox, SWT.LEFT, false, SWT.CENTER, false);
456 fWholeWordCheckBox.setSelection(fWholeWordInit);
457 fWholeWordCheckBox.addSelectionListener(selectionListener);
458 storeButtonWithMnemonicInMap(fWholeWordCheckBox);
459
460 fIsRegExCheckBox = new Button(group, SWT.CHECK | SWT.LEFT);
461 fIsRegExCheckBox.setText(Messages.TimeGraphFindDialog_REgExCheckBoxLabel);
462 setGridData(fIsRegExCheckBox, SWT.LEFT, false, SWT.CENTER, false);
463 ((GridData) fIsRegExCheckBox.getLayoutData()).horizontalSpan = 2;
464 fIsRegExCheckBox.setSelection(fIsRegExInit);
465 fIsRegExCheckBox.addSelectionListener(new SelectionAdapter() {
466 @Override
467 public void widgetSelected(SelectionEvent e) {
468 boolean newState = fIsRegExCheckBox.getSelection();
469 updateButtonState();
470 storeSettings();
471 setContentAssistsEnablement(newState);
472 }
473 });
474 storeButtonWithMnemonicInMap(fIsRegExCheckBox);
475 fWholeWordCheckBox.setEnabled(!isRegExSearch());
476 fWholeWordCheckBox.addSelectionListener(new SelectionAdapter() {
477 @Override
478 public void widgetSelected(SelectionEvent e) {
479 updateButtonState();
480 }
481 });
482 return panel;
483 }
484
485 /**
486 * Creates the status and close section of the dialog.
487 *
488 * @param parent
489 * the parent composite
490 * @return the status and close button
491 */
492 private Composite createStatusAndCloseButton(Composite parent) {
493
494 Composite panel = new Composite(parent, SWT.NULL);
495 GridLayout layout = new GridLayout();
496 layout.numColumns = 2;
497 layout.marginWidth = 0;
498 layout.marginHeight = 0;
499 panel.setLayout(layout);
500
501 fStatusLabel = new Label(panel, SWT.LEFT);
502 setGridData(fStatusLabel, SWT.FILL, true, SWT.CENTER, false);
503
504 Composite buttonSection = new Composite(panel, SWT.NULL);
505 GridLayout buttonLayout = new GridLayout();
506 buttonLayout.numColumns = 2;
507 buttonSection.setLayout(buttonLayout);
508
509 String label = Messages.TimeGraphFindDialog_CloseButtonLabel;
510 Button closeButton = createButton(buttonSection, 101, label, false);
511 setGridData(closeButton, SWT.RIGHT, false, SWT.BOTTOM, false);
512
513 fFindNextButton = makeButton(buttonSection, Messages.TimeGraphFindDialog_FindNextButtonLabel, 102, true, new SelectionAdapter() {
514 @Override
515 public void widgetSelected(SelectionEvent e) {
516 performSearch(((e.stateMask & SWT.SHIFT) != 0) ^ isForwardSearch());
517 updateFindHistory();
518 }
519 });
520 setGridData(fFindNextButton, SWT.FILL, true, SWT.FILL, false);
521
522 return panel;
523 }
524
525 @Override
526 protected void buttonPressed(int buttonID) {
527 if (buttonID == 101) {
528 close();
529 }
530 }
531
532 /**
533 * Update the dialog data (parentShell, listener, input, ...)
534 *
535 * @param findTarget
536 * the new find target
537 */
538 public void update(@NonNull FindTarget findTarget) {
539 updateTarget(findTarget, true);
540 }
541
542 // ------- action invocation ---------------------------------------
543
544 /**
545 * Returns the index of the entry that match the specified search string, or
546 * <code>-1</code> if the string can not be found when searching using the
547 * given options.
548 *
549 * @param findString
550 * the string to search for
551 * @param startIndex
552 * the index at which to start the search
553 * @param items
554 * The map of items in the time graph view
555 * @param options
556 * The options use for the search
557 * @return the index of the find entry following the options or
558 * <code>-1</code> if nothing found
559 */
560 private int findNext(String findString, int startIndex, BiMap<ITimeGraphEntry, Integer> items, SearchOptions options) {
561 int index;
562 if (options.forwardSearch) {
563 index = startIndex == items.size() - 1 ? -1 : findNext(startIndex + 1, findString, items, options);
564 } else {
565 index = startIndex == 0 ? -1 : findNext(startIndex - 1, findString, items, options);
566 }
567
568 if (index == -1) {
569 if (okToUse(getShell())) {
570 getShell().getDisplay().beep();
571 }
572 if (options.wrapSearch) {
573 statusMessage(Messages.TimeGraphFindDialog_StatusWrappedLabel);
574 index = findNext(-1, findString, items, options);
575 }
576 }
577 return index;
578 }
579
580 private int findNext(int startIndex, String findString, BiMap<ITimeGraphEntry, Integer> items, SearchOptions options) {
581 if (fFindTarget != null) {
582 if (findString == null || findString.length() == 0) {
583 return -1;
584 }
585
586 final @NonNull Pattern pattern = getPattern(findString, options);
587 int index = adjustIndex(startIndex, items.size(), options.forwardSearch);
588 BiMap<Integer, ITimeGraphEntry> entries = items.inverse();
589 while (index >= 0 && index < entries.size()) {
590 final @Nullable ITimeGraphEntry entry = entries.get(index);
591 if (entry != null && entry.matches(pattern)) {
592 return index;
593 }
594 index = options.forwardSearch ? ++index : --index;
595 }
596 }
597 return -1;
598 }
599
600 /**
601 * Returns whether the specified search string can be found using the given
602 * options.
603 *
604 * @param findString
605 * the string to search for
606 * @param options
607 * The search options
608 * @return <code>true</code> if the search string can be found using the
609 * given options
610 *
611 */
612 private boolean findAndSelect(String findString, SearchOptions options) {
613 FindTarget findTarget = fFindTarget;
614 if (findTarget == null) {
615 return false;
616 }
617 ITimeGraphEntry[] topInput = findTarget.getEntries();
618 BiMap<@NonNull ITimeGraphEntry, @NonNull Integer> items = HashBiMap.create();
619 for (ITimeGraphEntry entry : topInput) {
620 listEntries(items, entry);
621 }
622 int startPosition = findTarget.getSelection() == null ? 0 : NonNullUtils.checkNotNull(items.get(findTarget.getSelection()));
623
624 int index = findNext(findString, startPosition, items, options);
625
626 if (index == -1) {
627 statusMessage(Messages.TimeGraphFindDialog_StatusNoMatchLabel);
628 return false;
629 }
630
631 if (options.forwardSearch && index >= startPosition || !options.forwardSearch && index <= startPosition) {
632 statusMessage(""); //$NON-NLS-1$
633 }
634
635 // Send the entry found to target
636 findTarget.selectAndReveal(NonNullUtils.checkNotNull(items.inverse().get(index)));
637 return true;
638 }
639
640 private void listEntries(Map<ITimeGraphEntry, Integer> items, ITimeGraphEntry root) {
641 items.put(root, items.size());
642 for (ITimeGraphEntry child : root.getChildren()) {
643 listEntries(items, child);
644 }
645 }
646
647 // ------- accessors ---------------------------------------
648
649 /**
650 * Retrieves the string to search for from the appropriate text input field
651 * and returns it.
652 *
653 * @return the search string
654 */
655 private String getFindString() {
656 if (okToUse(fFindField)) {
657 return fFindField.getText();
658 }
659 return ""; //$NON-NLS-1$
660 }
661
662 /**
663 * Returns the dialog's boundaries.
664 *
665 * @return the dialog's boundaries
666 */
667 private Rectangle getDialogBoundaries() {
668 if (okToUse(getShell())) {
669 return getShell().getBounds();
670 }
671 return fDialogPositionInit;
672 }
673
674 // ------- init / close ---------------------------------------
675 @Override
676 public boolean close() {
677 handleDialogClose();
678 return super.close();
679 }
680
681 /**
682 * Removes focus changed listener from browser and stores settings for
683 * re-open.
684 */
685 private void handleDialogClose() {
686
687 // remove listeners
688 if (okToUse(fFindField)) {
689 fFindField.removeModifyListener(fFindModifyListener);
690 }
691 updateTarget(null, false);
692
693 if (fParentShell != null) {
694 fParentShell.removeShellListener(fActivationListener);
695 fParentShell = null;
696 }
697
698 if (getShell() != null && !getShell().isDisposed()) {
699 getShell().removeShellListener(fActivationListener);
700 }
701
702 // store current settings in case of re-open
703 storeSettings();
704
705 // prevent leaks
706 fActiveShell = null;
707
708 }
709
710 /**
711 * Writes the current selection to the dialog settings.
712 */
713 private void writeSelection() {
714 final FindTarget input = fFindTarget;
715 if (input == null) {
716 return;
717 }
718
719 IDialogSettings s = getDialogSettings();
720 final ITimeGraphEntry selection = input.getSelection();
721 if (selection != null) {
722 s.put("selection", selection.getName()); //$NON-NLS-1$
723 }
724 }
725
726 /**
727 * Stores the current state in the dialog settings.
728 */
729 private void storeSettings() {
730 fDialogPositionInit = getDialogBoundaries();
731 fWrapInit = isWrapSearch();
732 fWholeWordInit = isWholeWordSetting();
733 fCaseInit = isCaseSensitiveSearch();
734 fIsRegExInit = isRegExSearch();
735 fForwardInit = isForwardSearch();
736
737 writeConfiguration();
738 }
739
740 /**
741 * Initializes the string to search for and the appropriate text in the Find
742 * field based on the selection found in the timegraph view.
743 */
744 private void initFindStringFromSelection() {
745 FindTarget findTarget = fFindTarget;
746 if (findTarget != null && okToUse(fFindField)) {
747 final ITimeGraphEntry selection = findTarget.getSelection();
748 if (selection != null) {
749 String fullSelection = selection.getName();
750 fFindField.removeModifyListener(fFindModifyListener);
751 if (fullSelection.length() > 0) {
752 fFindField.setText(fullSelection);
753 }
754 } else {
755 if ("".equals(fFindField.getText())) { //$NON-NLS-1$
756 if (fFindHistory.size() > 0) {
757 fFindField.setText(fFindHistory.get(0));
758 } else {
759 fFindField.setText(""); //$NON-NLS-1$
760 }
761 }
762 }
763 fFindField.setSelection(new Point(0, fFindField.getText().length()));
764 fFindField.addModifyListener(fFindModifyListener);
765 }
766 }
767
768 // ------- Options ---------------------------------------
769
770 /**
771 * Retrieves and returns the option case sensitivity from the appropriate
772 * check box.
773 *
774 * @return <code>true</code> if case sensitive
775 */
776 private boolean isCaseSensitiveSearch() {
777 if (okToUse(fCaseCheckBox)) {
778 return fCaseCheckBox.getSelection();
779 }
780 return fCaseInit;
781 }
782
783 /**
784 * Retrieves and returns the regEx option from the appropriate check box.
785 *
786 * @return <code>true</code> if case sensitive
787 */
788 private boolean isRegExSearch() {
789 if (okToUse(fIsRegExCheckBox)) {
790 return fIsRegExCheckBox.getSelection();
791 }
792 return fIsRegExInit;
793 }
794
795 /**
796 * Retrieves and returns the option search direction from the appropriate
797 * check box.
798 *
799 * @return <code>true</code> if searching forward
800 */
801 private boolean isForwardSearch() {
802 if (okToUse(fForwardRadioButton)) {
803 return fForwardRadioButton.getSelection();
804 }
805 return fForwardInit;
806 }
807
808 /**
809 * Retrieves and returns the option search whole words from the appropriate
810 * check box.
811 *
812 * @return <code>true</code> if searching for whole words
813 */
814 private boolean isWholeWordSetting() {
815 if (okToUse(fWholeWordCheckBox)) {
816 return fWholeWordCheckBox.getSelection();
817 }
818 return fWholeWordInit;
819 }
820
821 /**
822 * Returns <code>true</code> if searching should be restricted to entire
823 * words, <code>false</code> if not. This is the case if the respective
824 * checkbox is turned on, regex is off, and the checkbox is enabled, i.e.
825 * the current find string is an entire word.
826 *
827 * @return <code>true</code> if the search is restricted to whole words
828 */
829 private boolean isWholeWordSearch() {
830 return isWholeWordSetting() && !isRegExSearch() && (okToUse(fWholeWordCheckBox) ? fWholeWordCheckBox.isEnabled() : true);
831 }
832
833 /**
834 * Retrieves and returns the option wrap search from the appropriate check
835 * box.
836 *
837 * @return <code>true</code> if wrapping while searching
838 */
839 private boolean isWrapSearch() {
840 if (okToUse(fWrapCheckBox)) {
841 return fWrapCheckBox.getSelection();
842 }
843 return fWrapInit;
844 }
845
846 /**
847 * Creates a button.
848 *
849 * @param parent
850 * the parent control
851 * @param label
852 * the button label
853 * @param id
854 * the button id
855 * @param dfltButton
856 * is this button the default button
857 * @param listener
858 * a button pressed listener
859 * @return the new button
860 */
861 private Button makeButton(Composite parent, String label, int id, boolean dfltButton, SelectionListener listener) {
862 Button button = createButton(parent, id, label, dfltButton);
863 button.addSelectionListener(listener);
864 storeButtonWithMnemonicInMap(button);
865 return button;
866 }
867
868 /**
869 * Stores the button and its mnemonic in {@link #fMnemonicButtonMap}.
870 *
871 * @param button
872 * button whose mnemonic has to be stored
873 */
874 private void storeButtonWithMnemonicInMap(Button button) {
875 char mnemonic = LegacyActionTools.extractMnemonic(button.getText());
876 if (mnemonic != LegacyActionTools.MNEMONIC_NONE) {
877 fMnemonicButtonMap.put(new Character(Character.toLowerCase(mnemonic)), button);
878 }
879 }
880
881 /**
882 * Sets the given status message in the status line.
883 *
884 * @param message
885 * the message
886 */
887 private void statusMessage(String message) {
888 fStatusLabel.setText(message);
889 getShell().getDisplay().beep();
890 }
891
892 /**
893 * Locates the user's findString in the entries information of the time
894 * graph view.
895 *
896 * @param forwardSearch
897 * the search direction
898 */
899 private void performSearch(boolean forwardSearch) {
900
901 String findString = getFindString();
902 if (findString != null && findString.length() > 0) {
903 findAndSelect(findString, getSearchOptions(forwardSearch));
904 }
905 writeSelection();
906 updateButtonState();
907 }
908
909 private SearchOptions getSearchOptions(boolean forwardSearch) {
910 SearchOptions options = new SearchOptions();
911 options.forwardSearch = forwardSearch;
912 options.caseSensitive = isCaseSensitiveSearch();
913 options.wrapSearch = isWrapSearch();
914 options.wholeWord = isWholeWordSearch();
915 options.regExSearch = isRegExSearch();
916 return options;
917 }
918
919 // ------- UI creation ---------------------------------------
920
921 /**
922 * Attaches the given layout specification to the <code>component</code>.
923 *
924 * @param component
925 * the component
926 * @param horizontalAlignment
927 * horizontal alignment
928 * @param grabExcessHorizontalSpace
929 * grab excess horizontal space
930 * @param verticalAlignment
931 * vertical alignment
932 * @param grabExcessVerticalSpace
933 * grab excess vertical space
934 */
935 private static void setGridData(Control component, int horizontalAlignment, boolean grabExcessHorizontalSpace, int verticalAlignment, boolean grabExcessVerticalSpace) {
936 GridData gd;
937 if (component instanceof Button && (((Button) component).getStyle() & SWT.PUSH) != 0) {
938 gd = (GridData) component.getLayoutData();
939 gd.horizontalAlignment = GridData.FILL;
940 gd.widthHint = 100;
941 } else {
942 gd = new GridData();
943 component.setLayoutData(gd);
944 gd.horizontalAlignment = horizontalAlignment;
945 gd.grabExcessHorizontalSpace = grabExcessHorizontalSpace;
946 }
947 gd.verticalAlignment = verticalAlignment;
948 gd.grabExcessVerticalSpace = grabExcessVerticalSpace;
949 }
950
951 /**
952 * Updates the enabled state of the buttons.
953 */
954 private void updateButtonState() {
955 if (okToUse(getShell()) && okToUse(fFindNextButton)) {
956
957 boolean enable = fFindTarget != null && (fActiveShell == fParentShell || fActiveShell == getShell());
958 String str = getFindString();
959 boolean findString = str != null && str.length() > 0;
960
961 fWholeWordCheckBox.setEnabled(isWord(str) && !isRegExSearch());
962
963 fFindNextButton.setEnabled(enable && findString);
964 }
965 }
966
967 /**
968 * Tests whether each character in the given string is a letter.
969 *
970 * @param str
971 * the string to check
972 * @return <code>true</code> if the given string is a word
973 */
974 private static boolean isWord(String str) {
975 if (str == null || str.length() == 0) {
976 return false;
977 }
978
979 for (int i = 0; i < str.length(); i++) {
980 if (!Character.isJavaIdentifierPart(str.charAt(i))) {
981 return false;
982 }
983 }
984 return true;
985 }
986
987 /**
988 * Updates the given combo with the given content.
989 *
990 * @param combo
991 * combo to be updated
992 * @param content
993 * to be put into the combo
994 */
995 private static void updateCombo(Combo combo, List<String> content) {
996 combo.removeAll();
997 for (int i = 0; i < content.size(); i++) {
998 combo.add(content.get(i).toString());
999 }
1000 }
1001
1002 // ------- open / reopen ---------------------------------------
1003
1004 /**
1005 * Called after executed find action to update the history.
1006 */
1007 private void updateFindHistory() {
1008 if (okToUse(fFindField)) {
1009 fFindField.removeModifyListener(fFindModifyListener);
1010
1011 updateHistory(fFindField, fFindHistory);
1012 fFindField.addModifyListener(fFindModifyListener);
1013 }
1014 }
1015
1016 /**
1017 * Updates the combo with the history.
1018 *
1019 * @param combo
1020 * to be updated
1021 * @param history
1022 * to be put into the combo
1023 */
1024 private static void updateHistory(Combo combo, List<String> history) {
1025 String findString = combo.getText();
1026 int index = history.indexOf(findString);
1027 if (index != 0) {
1028 if (index != -1) {
1029 history.remove(index);
1030 }
1031 history.add(0, findString);
1032 Point selection = combo.getSelection();
1033 updateCombo(combo, history);
1034 combo.setText(findString);
1035 combo.setSelection(selection);
1036 }
1037 }
1038
1039 /**
1040 * Sets the parent shell of this dialog to be the given shell.
1041 *
1042 * @param shell
1043 * the new parent shell
1044 */
1045 @Override
1046 public void setParentShell(Shell shell) {
1047 if (shell != fParentShell) {
1048
1049 if (fParentShell != null) {
1050 fParentShell.removeShellListener(fActivationListener);
1051 }
1052
1053 fParentShell = shell;
1054 fParentShell.addShellListener(fActivationListener);
1055 }
1056
1057 fActiveShell = shell;
1058 }
1059
1060 // --------------- configuration handling --------------
1061
1062 /**
1063 * Returns the dialog settings object used to share state of the find
1064 * dialog.
1065 *
1066 * @return the dialog settings to be used
1067 */
1068 private IDialogSettings getDialogSettings() {
1069 IDialogSettings settings = Activator.getDefault().getDialogSettings();
1070 fDialogSettings = settings.getSection(getClass().getName());
1071 if (fDialogSettings == null) {
1072 fDialogSettings = settings.addNewSection(getClass().getName());
1073 }
1074 return fDialogSettings;
1075 }
1076
1077 /**
1078 * Initializes itself from the dialog settings with the same state as at the
1079 * previous invocation.
1080 */
1081 private void readConfiguration() {
1082 IDialogSettings s = getDialogSettings();
1083
1084 fWrapInit = s.get(WRAP) == null || s.getBoolean(WRAP);
1085 fCaseInit = s.getBoolean(CASE_SENSITIVE);
1086 fWholeWordInit = s.getBoolean(WHOLE_WORD);
1087 fIsRegExInit = s.getBoolean(IS_REGEX_EXPRESSION);
1088
1089 String[] findHistory = s.getArray(FIND_HISTORY);
1090 if (findHistory != null) {
1091 fFindHistory.clear();
1092 for (int i = 0; i < findHistory.length; i++) {
1093 fFindHistory.add(findHistory[i]);
1094 }
1095 }
1096 }
1097
1098 /**
1099 * Stores its current configuration in the dialog store.
1100 */
1101 private void writeConfiguration() {
1102 IDialogSettings s = getDialogSettings();
1103
1104 s.put(WRAP, fWrapInit);
1105 s.put(CASE_SENSITIVE, fCaseInit);
1106 s.put(WHOLE_WORD, fWholeWordInit);
1107 s.put(CASE_SENSITIVE, fIsRegExInit);
1108
1109 String findString = getFindString();
1110 if (findString.length() > 0) {
1111 fFindHistory.add(0, findString);
1112 }
1113 writeHistory(fFindHistory, s, FIND_HISTORY);
1114 }
1115
1116 /**
1117 * Writes the given history into the given dialog store.
1118 *
1119 * @param history
1120 * the history
1121 * @param settings
1122 * the dialog settings
1123 * @param sectionName
1124 * the section name
1125 */
1126 private static void writeHistory(List<String> history, IDialogSettings settings, String sectionName) {
1127 int itemCount = history.size();
1128 Set<String> distinctItems = new HashSet<>(itemCount);
1129 for (int i = 0; i < itemCount; i++) {
1130 String item = history.get(i);
1131 if (distinctItems.contains(item)) {
1132 history.remove(i--);
1133 itemCount--;
1134 } else {
1135 distinctItems.add(item);
1136 }
1137 }
1138
1139 while (history.size() > 8) {
1140 history.remove(8);
1141 }
1142
1143 String[] names = new String[history.size()];
1144 history.toArray(names);
1145 settings.put(sectionName, names);
1146
1147 }
1148
1149 /**
1150 * Update the time graph viewer this dialog search in
1151 *
1152 * @param findTarget
1153 * The find target. Can be <code>null</code>.
1154 * @param initFindString
1155 * This value should be true if the find text field needs to be
1156 * populated with selected entry in the find viewer, false
1157 * otherwise
1158 */
1159 public void updateTarget(@Nullable FindTarget findTarget, boolean initFindString) {
1160 fFindTarget = findTarget;
1161 if (initFindString) {
1162 initFindStringFromSelection();
1163 }
1164 updateButtonState();
1165 }
1166
1167 /**
1168 * Test if the shell to test is the parent shell of the dialog
1169 *
1170 * @param shell
1171 * The shell to test
1172 * @return <code>true</code> if the shell to test is the parent shell,
1173 * <code>false</code> otherwise
1174 */
1175 public boolean isDialogParentShell(Shell shell) {
1176 return shell == getParentShell();
1177 }
1178
1179 /**
1180 * Adjust the index where to start the search
1181 *
1182 * @param previousIndex
1183 * The previous index
1184 * @param count
1185 * The number of items to search into
1186 * @param forwardSearch
1187 * The search direction
1188 * @return The adjusted index
1189 */
1190 private static int adjustIndex(int previousIndex, int count, boolean forwardSearch) {
1191 int index = previousIndex;
1192 if (forwardSearch) {
1193 index = index >= count || index < 0 ? 0 : index;
1194 } else {
1195 index = index >= count || index < 0 ? count - 1 : index;
1196 }
1197 return index;
1198 }
1199
1200 /**
1201 * Create a pattern from the string to find and with options provided
1202 *
1203 * This implementation is drawn from the jface implementation of
1204 * org.eclipse.jface.text.FindReplaceDocumentAdapter
1205 *
1206 * @param findString
1207 * The string to find
1208 * @param caseSensitive
1209 * Tells if the pattern will activate the case sensitive flag
1210 * @param wholeWord
1211 * Tells if the pattern will activate the whole word flag
1212 * @param regExSearch
1213 * Tells if the pattern will activate the regEx flag
1214 * @return The created pattern
1215 */
1216 private static @NonNull Pattern getPattern(String findString, SearchOptions options) {
1217 String toFind = findString;
1218
1219 int patternFlags = 0;
1220
1221 if (options.regExSearch) {
1222 toFind = substituteLinebreak(toFind);
1223 }
1224
1225 if (!options.caseSensitive) {
1226 patternFlags |= Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE;
1227 }
1228
1229 if (!options.regExSearch) {
1230 toFind = asRegPattern(toFind);
1231 }
1232
1233 if (options.wholeWord) {
1234 toFind = "\\b" + toFind + "\\b"; //$NON-NLS-1$ //$NON-NLS-2$
1235 }
1236 Pattern pattern = Pattern.compile(toFind, patternFlags);
1237 return pattern;
1238 }
1239
1240 /**
1241 * Substitutes \R in a regex find pattern with (?>\r\n?|\n)
1242 *
1243 * This implementation is drawn from the jface implementation of
1244 * org.eclipse.jface.text.FindReplaceDocumentAdapter
1245 *
1246 * @param findString
1247 * the original find pattern
1248 * @return the transformed find pattern
1249 */
1250 private static String substituteLinebreak(String findString) {
1251 int length = findString.length();
1252 StringBuffer buf = new StringBuffer(length);
1253
1254 int inCharGroup = 0;
1255 int inBraces = 0;
1256 boolean inQuote = false;
1257 for (int i = 0; i < length; i++) {
1258 char ch = findString.charAt(i);
1259 switch (ch) {
1260 case '[':
1261 buf.append(ch);
1262 if (!inQuote) {
1263 inCharGroup++;
1264 }
1265 break;
1266
1267 case ']':
1268 buf.append(ch);
1269 if (!inQuote) {
1270 inCharGroup--;
1271 }
1272 break;
1273
1274 case '{':
1275 buf.append(ch);
1276 if (!inQuote && inCharGroup == 0) {
1277 inBraces++;
1278 }
1279 break;
1280
1281 case '}':
1282 buf.append(ch);
1283 if (!inQuote && inCharGroup == 0) {
1284 inBraces--;
1285 }
1286 break;
1287
1288 case '\\':
1289 if (i + 1 < length) {
1290 char ch1 = findString.charAt(i + 1);
1291 if (inQuote) {
1292 if (ch1 == 'E') {
1293 inQuote = false;
1294 }
1295 buf.append(ch).append(ch1);
1296 i++;
1297
1298 } else if (ch1 == 'R') {
1299 if (inCharGroup > 0 || inBraces > 0) {
1300 String msg = "TimeGrahViewer.illegalLinebreak"; //$NON-NLS-1$
1301 throw new PatternSyntaxException(msg, findString, i);
1302 }
1303 buf.append("(?>\\r\\n?|\\n)"); //$NON-NLS-1$
1304 i++;
1305
1306 } else {
1307 if (ch1 == 'Q') {
1308 inQuote = true;
1309 }
1310 buf.append(ch).append(ch1);
1311 i++;
1312 }
1313 } else {
1314 buf.append(ch);
1315 }
1316 break;
1317
1318 default:
1319 buf.append(ch);
1320 break;
1321 }
1322
1323 }
1324 return buf.toString();
1325 }
1326
1327 /**
1328 * Converts a non-regex string to a pattern that can be used with the regex
1329 * search engine.
1330 *
1331 * This implementation is drawn from the jface implementation of
1332 * org.eclipse.jface.text.FindReplaceDocumentAdapter
1333 *
1334 * @param string
1335 * the non-regex pattern
1336 * @return the string converted to a regex pattern
1337 */
1338 private static String asRegPattern(String string) {
1339 StringBuffer out = new StringBuffer(string.length());
1340 boolean quoting = false;
1341
1342 for (int i = 0, length = string.length(); i < length; i++) {
1343 char ch = string.charAt(i);
1344 if (ch == '\\') {
1345 if (quoting) {
1346 out.append("\\E"); //$NON-NLS-1$
1347 quoting = false;
1348 }
1349 out.append("\\\\"); //$NON-NLS-1$
1350 continue;
1351 }
1352 if (!quoting) {
1353 out.append("\\Q"); //$NON-NLS-1$
1354 quoting = true;
1355 }
1356 out.append(ch);
1357 }
1358 if (quoting) {
1359 out.append("\\E"); //$NON-NLS-1$
1360 }
1361
1362 return out.toString();
1363 }
1364
1365 private class SearchOptions {
1366 boolean forwardSearch;
1367 boolean caseSensitive;
1368 boolean wrapSearch;
1369 boolean wholeWord;
1370 boolean regExSearch;
1371 }
1372 }
This page took 0.080721 seconds and 5 git commands to generate.