| /***************************************************************************** |
| * Copyright (c) 2016 Ericsson |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| *****************************************************************************/ |
| package org.eclipse.tracecompass.tmf.ui.views.timegraph; |
| |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.regex.Pattern; |
| import java.util.regex.PatternSyntaxException; |
| |
| import org.eclipse.jdt.annotation.NonNull; |
| import org.eclipse.jdt.annotation.Nullable; |
| import org.eclipse.jface.action.LegacyActionTools; |
| import org.eclipse.jface.dialogs.Dialog; |
| import org.eclipse.jface.dialogs.IDialogSettings; |
| import org.eclipse.jface.fieldassist.ComboContentAdapter; |
| import org.eclipse.jface.text.FindReplaceDocumentAdapterContentProposalProvider; |
| import org.eclipse.jface.util.Util; |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.events.ModifyEvent; |
| import org.eclipse.swt.events.ModifyListener; |
| import org.eclipse.swt.events.SelectionAdapter; |
| import org.eclipse.swt.events.SelectionEvent; |
| import org.eclipse.swt.events.SelectionListener; |
| import org.eclipse.swt.events.ShellAdapter; |
| import org.eclipse.swt.events.ShellEvent; |
| import org.eclipse.swt.graphics.Point; |
| import org.eclipse.swt.graphics.Rectangle; |
| import org.eclipse.swt.layout.GridData; |
| import org.eclipse.swt.layout.GridLayout; |
| import org.eclipse.swt.widgets.Button; |
| import org.eclipse.swt.widgets.Combo; |
| import org.eclipse.swt.widgets.Composite; |
| import org.eclipse.swt.widgets.Control; |
| import org.eclipse.swt.widgets.Event; |
| import org.eclipse.swt.widgets.Group; |
| import org.eclipse.swt.widgets.Label; |
| import org.eclipse.swt.widgets.Shell; |
| import org.eclipse.tracecompass.common.core.NonNullUtils; |
| import org.eclipse.tracecompass.internal.tmf.ui.Activator; |
| import org.eclipse.tracecompass.internal.tmf.ui.Messages; |
| import org.eclipse.tracecompass.tmf.ui.views.timegraph.AbstractTimeGraphView.FindTarget; |
| import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ITimeGraphEntry; |
| import org.eclipse.ui.fieldassist.ContentAssistCommandAdapter; |
| import org.eclipse.ui.texteditor.ITextEditorActionDefinitionIds; |
| |
| import com.google.common.collect.BiMap; |
| import com.google.common.collect.HashBiMap; |
| |
| /** |
| * Find dialog to search entries into a time graph. This implementation is based |
| * on the org.eclipse.ui.texteditor.FindReplaceDialog of Eclipse. |
| * |
| * @author Jean-Christian Kouame |
| */ |
| class TimeGraphFindDialog extends Dialog { |
| |
| /** |
| * Updates the find dialog on activation changes. |
| */ |
| class ActivationListener extends ShellAdapter { |
| /* |
| * @see ShellListener#shellActivated(ShellEvent) |
| */ |
| @Override |
| public void shellActivated(ShellEvent e) { |
| fActiveShell = (Shell) e.widget; |
| updateButtonState(); |
| |
| if (fGiveFocusToFindField && getShell() == fActiveShell && okToUse(fFindField)) { |
| fFindField.setFocus(); |
| } |
| |
| } |
| |
| /* |
| * @see ShellListener#shellDeactivated(ShellEvent) |
| */ |
| @Override |
| public void shellDeactivated(ShellEvent e) { |
| fGiveFocusToFindField = false; |
| |
| storeSettings(); |
| |
| fActiveShell = null; |
| updateButtonState(); |
| } |
| } |
| |
| /** |
| * Modify listener to update the search result in case of incremental |
| * search. |
| */ |
| private class FindModifyListener implements ModifyListener { |
| |
| /* |
| * @see ModifyListener#modifyText(ModifyEvent) |
| */ |
| @Override |
| public void modifyText(ModifyEvent e) { |
| updateButtonState(); |
| } |
| } |
| |
| /** The size of the dialogs search history. */ |
| private static final int HISTORY_SIZE = 5; |
| |
| private final String WRAP = "wrap"; //$NON-NLS-1$ |
| private final String CASE_SENSITIVE = "casesensitive"; //$NON-NLS-1$ |
| private final String WHOLE_WORD = "wholeword"; //$NON-NLS-1$ |
| private final String IS_REGEX_EXPRESSION = "isRegEx"; //$NON-NLS-1$ |
| private final String FIND_HISTORY = "findhistory"; //$NON-NLS-1$ |
| |
| private boolean fWrapInit; |
| private boolean fCaseInit; |
| private boolean fWholeWordInit; |
| private boolean fForwardInit; |
| private boolean fIsRegExInit; |
| |
| private @NonNull List<String> fFindHistory; |
| |
| private static Shell fParentShell; |
| private Shell fActiveShell; |
| |
| private final ActivationListener fActivationListener = new ActivationListener(); |
| private final FindModifyListener fFindModifyListener = new FindModifyListener(); |
| |
| private Label fStatusLabel; |
| private Button fForwardRadioButton; |
| private Button fCaseCheckBox; |
| private Button fWrapCheckBox; |
| private Button fWholeWordCheckBox; |
| private Button fIsRegExCheckBox; |
| |
| private Button fFindNextButton; |
| private Combo fFindField; |
| |
| /** |
| * Find command adapters. |
| */ |
| private ContentAssistCommandAdapter fContentAssistFindField; |
| |
| private Rectangle fDialogPositionInit; |
| |
| private IDialogSettings fDialogSettings; |
| /** |
| * <code>true</code> if the find field should receive focus the next time |
| * the dialog is activated, <code>false</code> otherwise. |
| */ |
| private boolean fGiveFocusToFindField = true; |
| |
| /** |
| * Holds the mnemonic/button pairs for all buttons. |
| */ |
| private HashMap<Character, Button> fMnemonicButtonMap = new HashMap<>(); |
| |
| private @Nullable FindTarget fFindTarget; |
| |
| /** |
| * Creates a new dialog with the given shell as parent. |
| * |
| * @param parentShell |
| * the parent shell |
| */ |
| public TimeGraphFindDialog(Shell parentShell) { |
| super(parentShell); |
| |
| fParentShell = null; |
| fFindTarget = null; |
| |
| fDialogPositionInit = null; |
| fFindHistory = new ArrayList<>(HISTORY_SIZE - 1); |
| |
| fWrapInit = true; |
| fCaseInit = false; |
| fIsRegExInit = false; |
| fWholeWordInit = false; |
| fForwardInit = true; |
| |
| readConfiguration(); |
| setShellStyle(getShellStyle() & ~SWT.APPLICATION_MODAL); |
| setBlockOnOpen(false); |
| } |
| |
| @Override |
| protected boolean isResizable() { |
| return true; |
| } |
| |
| /** |
| * Returns <code>true</code> if control can be used. |
| * |
| * @param control |
| * the control to be checked |
| * @return <code>true</code> if control can be used |
| */ |
| private static boolean okToUse(Control control) { |
| return control != null && !control.isDisposed(); |
| } |
| |
| @Override |
| public void create() { |
| super.create(); |
| |
| Shell shell = getShell(); |
| shell.addShellListener(fActivationListener); |
| |
| // fill in combo contents |
| fFindField.removeModifyListener(fFindModifyListener); |
| updateCombo(fFindField, fFindHistory); |
| fFindField.addModifyListener(fFindModifyListener); |
| |
| // get find string |
| initFindStringFromSelection(); |
| |
| shell.setMinimumSize(shell.getSize()); |
| |
| // set dialog position |
| if (fDialogPositionInit != null) { |
| shell.setBounds(fDialogPositionInit); |
| } |
| |
| shell.setText(Messages.TimeGraphFindDialog_FindTitle); |
| } |
| |
| /** |
| * Creates the options configuration section of the find dialog. |
| * |
| * @param parent |
| * the parent composite |
| * @return the options configuration section |
| */ |
| private Composite createConfigPanel(Composite parent) { |
| |
| Composite panel = new Composite(parent, SWT.NONE); |
| GridLayout layout = new GridLayout(); |
| panel.setLayout(layout); |
| |
| Composite directionGroup = createDirectionGroup(panel); |
| setGridData(directionGroup, SWT.FILL, true, SWT.FILL, false); |
| |
| Composite optionsGroup = createOptionsGroup(panel); |
| setGridData(optionsGroup, SWT.FILL, true, SWT.FILL, true); |
| ((GridData) optionsGroup.getLayoutData()).horizontalSpan = 2; |
| |
| return panel; |
| } |
| |
| @Override |
| protected Control createContents(Composite parent) { |
| |
| Composite panel = new Composite(parent, SWT.NULL); |
| GridLayout layout = new GridLayout(); |
| layout.numColumns = 1; |
| layout.makeColumnsEqualWidth = true; |
| panel.setLayout(layout); |
| panel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); |
| |
| Composite inputPanel = createInputPanel(panel); |
| setGridData(inputPanel, SWT.FILL, true, SWT.TOP, false); |
| |
| Composite configPanel = createConfigPanel(panel); |
| setGridData(configPanel, SWT.FILL, true, SWT.TOP, true); |
| |
| Composite statusBar = createStatusAndCloseButton(panel); |
| setGridData(statusBar, SWT.FILL, true, SWT.BOTTOM, false); |
| |
| panel.addTraverseListener(e -> { |
| if (e.detail == SWT.TRAVERSE_RETURN) { |
| if (!Util.isMac()) { |
| Control controlWithFocus = getShell().getDisplay().getFocusControl(); |
| if (controlWithFocus != null && (controlWithFocus.getStyle() & SWT.PUSH) == SWT.PUSH) { |
| return; |
| } |
| } |
| Event event1 = new Event(); |
| event1.type = SWT.Selection; |
| event1.stateMask = e.stateMask; |
| fFindNextButton.notifyListeners(SWT.Selection, event1); |
| e.doit = false; |
| } else if (e.detail == SWT.TRAVERSE_MNEMONIC) { |
| Character mnemonic = new Character(Character.toLowerCase(e.character)); |
| Button button = fMnemonicButtonMap.get(mnemonic); |
| if (button != null) { |
| if ((fFindField.isFocusControl() || (button.getStyle() & SWT.PUSH) != 0) |
| && button.isEnabled()) { |
| Event event2 = new Event(); |
| event2.type = SWT.Selection; |
| event2.stateMask = e.stateMask; |
| if ((button.getStyle() & SWT.RADIO) != 0) { |
| Composite buttonParent = button.getParent(); |
| if (buttonParent != null) { |
| Control[] children = buttonParent.getChildren(); |
| for (int i = 0; i < children.length; i++) { |
| ((Button) children[i]).setSelection(false); |
| } |
| } |
| button.setSelection(true); |
| } else { |
| button.setSelection(!button.getSelection()); |
| } |
| button.notifyListeners(SWT.Selection, event2); |
| e.detail = SWT.TRAVERSE_NONE; |
| e.doit = true; |
| } |
| } |
| } |
| }); |
| |
| updateButtonState(); |
| |
| applyDialogFont(panel); |
| |
| return panel; |
| } |
| |
| private void setContentAssistsEnablement(boolean enable) { |
| fContentAssistFindField.setEnabled(enable); |
| } |
| |
| /** |
| * Creates the direction defining part of the options defining section of |
| * the find dialog. |
| * |
| * @param parent |
| * the parent composite |
| * @return the direction defining part |
| */ |
| private Composite createDirectionGroup(Composite parent) { |
| |
| Composite panel = new Composite(parent, SWT.NONE); |
| GridLayout layout = new GridLayout(); |
| layout.marginWidth = 0; |
| layout.marginHeight = 0; |
| panel.setLayout(layout); |
| |
| Group group = new Group(panel, SWT.SHADOW_ETCHED_IN); |
| group.setText(Messages.TimeGraphFindDialog_Direction); |
| GridLayout groupLayout = new GridLayout(); |
| group.setLayout(groupLayout); |
| group.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); |
| |
| fForwardRadioButton = new Button(group, SWT.RADIO | SWT.LEFT); |
| fForwardRadioButton.setText(Messages.TimeGraphFindDialog_ForwardRadioButtonLabel); |
| setGridData(fForwardRadioButton, SWT.LEFT, false, SWT.CENTER, false); |
| storeButtonWithMnemonicInMap(fForwardRadioButton); |
| |
| Button backwardRadioButton = new Button(group, SWT.RADIO | SWT.LEFT); |
| backwardRadioButton.setText(Messages.TimeGraphFindDialog_BackwardRadioButtonLabel); |
| setGridData(backwardRadioButton, SWT.LEFT, false, SWT.CENTER, false); |
| storeButtonWithMnemonicInMap(backwardRadioButton); |
| |
| backwardRadioButton.setSelection(!fForwardInit); |
| fForwardRadioButton.setSelection(fForwardInit); |
| |
| return panel; |
| } |
| |
| /** |
| * Creates the panel where the user specifies the text to search for |
| * |
| * @param parent |
| * the parent composite |
| * @return the input panel |
| */ |
| private Composite createInputPanel(Composite parent) { |
| Composite panel = new Composite(parent, SWT.NULL); |
| GridLayout layout = new GridLayout(); |
| layout.numColumns = 2; |
| panel.setLayout(layout); |
| |
| Label findLabel = new Label(panel, SWT.LEFT); |
| findLabel.setText(Messages.TimeGraphFindDialog_FindLabel); |
| setGridData(findLabel, SWT.LEFT, false, SWT.CENTER, false); |
| |
| // Create the find content assist field |
| ComboContentAdapter contentAdapter = new ComboContentAdapter(); |
| FindReplaceDocumentAdapterContentProposalProvider findProposer = new FindReplaceDocumentAdapterContentProposalProvider(true); |
| fFindField = new Combo(panel, SWT.DROP_DOWN | SWT.BORDER); |
| fContentAssistFindField = new ContentAssistCommandAdapter( |
| fFindField, |
| contentAdapter, |
| findProposer, |
| ITextEditorActionDefinitionIds.CONTENT_ASSIST_PROPOSALS, |
| new char[0], |
| true); |
| setGridData(fFindField, SWT.FILL, true, SWT.CENTER, false); |
| fFindField.addModifyListener(fFindModifyListener); |
| |
| return panel; |
| } |
| |
| /** |
| * Creates the functional options part of the options defining section of |
| * the find dialog. |
| * |
| * @param parent |
| * the parent composite |
| * @return the options group |
| */ |
| private Composite createOptionsGroup(Composite parent) { |
| |
| Composite panel = new Composite(parent, SWT.NONE); |
| GridLayout layout = new GridLayout(); |
| layout.marginWidth = 0; |
| layout.marginHeight = 0; |
| panel.setLayout(layout); |
| |
| Group group = new Group(panel, SWT.SHADOW_NONE); |
| group.setText(Messages.TimeGraphFindDialog_Options); |
| GridLayout groupLayout = new GridLayout(); |
| groupLayout.numColumns = 1; |
| groupLayout.makeColumnsEqualWidth = true; |
| group.setLayout(groupLayout); |
| group.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); |
| |
| SelectionListener selectionListener = new SelectionListener() { |
| @Override |
| public void widgetSelected(SelectionEvent e) { |
| storeSettings(); |
| } |
| |
| @Override |
| public void widgetDefaultSelected(SelectionEvent e) { |
| // Do nothing |
| } |
| }; |
| |
| fCaseCheckBox = new Button(group, SWT.CHECK | SWT.LEFT); |
| fCaseCheckBox.setText(Messages.TimeGraphFindDialog_CaseCheckBoxLabel); |
| setGridData(fCaseCheckBox, SWT.LEFT, false, SWT.CENTER, false); |
| fCaseCheckBox.setSelection(fCaseInit); |
| fCaseCheckBox.addSelectionListener(selectionListener); |
| storeButtonWithMnemonicInMap(fCaseCheckBox); |
| |
| fWrapCheckBox = new Button(group, SWT.CHECK | SWT.LEFT); |
| fWrapCheckBox.setText(Messages.TimeGraphFindDialog_WrapCheckBoxLabel); |
| setGridData(fWrapCheckBox, SWT.LEFT, false, SWT.CENTER, false); |
| fWrapCheckBox.setSelection(fWrapInit); |
| fWrapCheckBox.addSelectionListener(selectionListener); |
| storeButtonWithMnemonicInMap(fWrapCheckBox); |
| |
| fWholeWordCheckBox = new Button(group, SWT.CHECK | SWT.LEFT); |
| fWholeWordCheckBox.setText(Messages.TimeGraphFindDialog_WholeWordCheckBoxLabel); |
| setGridData(fWholeWordCheckBox, SWT.LEFT, false, SWT.CENTER, false); |
| fWholeWordCheckBox.setSelection(fWholeWordInit); |
| fWholeWordCheckBox.addSelectionListener(selectionListener); |
| storeButtonWithMnemonicInMap(fWholeWordCheckBox); |
| |
| fIsRegExCheckBox = new Button(group, SWT.CHECK | SWT.LEFT); |
| fIsRegExCheckBox.setText(Messages.TimeGraphFindDialog_REgExCheckBoxLabel); |
| setGridData(fIsRegExCheckBox, SWT.LEFT, false, SWT.CENTER, false); |
| ((GridData) fIsRegExCheckBox.getLayoutData()).horizontalSpan = 2; |
| fIsRegExCheckBox.setSelection(fIsRegExInit); |
| fIsRegExCheckBox.addSelectionListener(new SelectionAdapter() { |
| @Override |
| public void widgetSelected(SelectionEvent e) { |
| boolean newState = fIsRegExCheckBox.getSelection(); |
| updateButtonState(); |
| storeSettings(); |
| setContentAssistsEnablement(newState); |
| } |
| }); |
| storeButtonWithMnemonicInMap(fIsRegExCheckBox); |
| fWholeWordCheckBox.setEnabled(!isRegExSearch()); |
| fWholeWordCheckBox.addSelectionListener(new SelectionAdapter() { |
| @Override |
| public void widgetSelected(SelectionEvent e) { |
| updateButtonState(); |
| } |
| }); |
| return panel; |
| } |
| |
| /** |
| * Creates the status and close section of the dialog. |
| * |
| * @param parent |
| * the parent composite |
| * @return the status and close button |
| */ |
| private Composite createStatusAndCloseButton(Composite parent) { |
| |
| Composite panel = new Composite(parent, SWT.NULL); |
| GridLayout layout = new GridLayout(); |
| layout.numColumns = 2; |
| layout.marginWidth = 0; |
| layout.marginHeight = 0; |
| panel.setLayout(layout); |
| |
| fStatusLabel = new Label(panel, SWT.LEFT); |
| fStatusLabel.setText(Messages.TimeGraphFindDialog_StatusWrappedLabel); |
| setGridData(fStatusLabel, SWT.FILL, true, SWT.CENTER, false); |
| GridData gd = (GridData) fStatusLabel.getLayoutData(); |
| gd.widthHint = fStatusLabel.computeSize(SWT.DEFAULT, SWT.DEFAULT).x; |
| fStatusLabel.setText(""); //$NON-NLS-1$ |
| |
| Composite buttonSection = new Composite(panel, SWT.NULL); |
| GridLayout buttonLayout = new GridLayout(); |
| buttonLayout.numColumns = 2; |
| buttonSection.setLayout(buttonLayout); |
| |
| String label = Messages.TimeGraphFindDialog_CloseButtonLabel; |
| Button closeButton = createButton(buttonSection, 101, label, false); |
| setGridData(closeButton, SWT.RIGHT, false, SWT.BOTTOM, false); |
| |
| fFindNextButton = makeButton(buttonSection, Messages.TimeGraphFindDialog_FindNextButtonLabel, 102, true, new SelectionAdapter() { |
| @Override |
| public void widgetSelected(SelectionEvent e) { |
| performSearch(((e.stateMask & SWT.SHIFT) != 0) ^ isForwardSearch()); |
| updateFindHistory(); |
| } |
| }); |
| setGridData(fFindNextButton, SWT.FILL, true, SWT.FILL, false); |
| |
| return panel; |
| } |
| |
| @Override |
| protected void buttonPressed(int buttonID) { |
| if (buttonID == 101) { |
| close(); |
| } |
| } |
| |
| /** |
| * Update the dialog data (parentShell, listener, input, ...) |
| * |
| * @param findTarget |
| * the new find target |
| */ |
| public void update(@NonNull FindTarget findTarget) { |
| updateTarget(findTarget, true); |
| } |
| |
| // ------- action invocation --------------------------------------- |
| |
| /** |
| * Returns the index of the entry that match the specified search string, or |
| * <code>-1</code> if the string can not be found when searching using the |
| * given options. |
| * |
| * @param findString |
| * the string to search for |
| * @param startIndex |
| * the index at which to start the search |
| * @param items |
| * The map of items in the time graph view |
| * @param options |
| * The options use for the search |
| * @return the index of the find entry following the options or |
| * <code>-1</code> if nothing found |
| */ |
| private int findNext(String findString, int startIndex, BiMap<ITimeGraphEntry, Integer> items, SearchOptions options) { |
| int index; |
| if (options.forwardSearch) { |
| index = startIndex == items.size() - 1 ? -1 : findNext(startIndex + 1, findString, items, options); |
| } else { |
| index = startIndex == 0 ? -1 : findNext(startIndex - 1, findString, items, options); |
| } |
| |
| if (index == -1) { |
| if (okToUse(getShell())) { |
| getShell().getDisplay().beep(); |
| } |
| if (options.wrapSearch) { |
| statusMessage(Messages.TimeGraphFindDialog_StatusWrappedLabel); |
| index = findNext(-1, findString, items, options); |
| } |
| } |
| return index; |
| } |
| |
| private int findNext(int startIndex, String findString, BiMap<ITimeGraphEntry, Integer> items, SearchOptions options) { |
| FindTarget findTarget = fFindTarget; |
| if (findTarget != null) { |
| if (findString == null || findString.length() == 0) { |
| return -1; |
| } |
| |
| final @NonNull Pattern pattern = getPattern(findString, options); |
| int index = adjustIndex(startIndex, items.size(), options.forwardSearch); |
| BiMap<Integer, ITimeGraphEntry> entries = items.inverse(); |
| while (index >= 0 && index < entries.size()) { |
| final @Nullable ITimeGraphEntry entry = entries.get(index); |
| if (entry != null) { |
| String[] columnTexts = findTarget.getColumnTexts(entry); |
| for (int i = 0; i < columnTexts.length; i++) { |
| String columnText = columnTexts[i]; |
| if (columnText != null && !columnText.isEmpty() && pattern.matcher(columnTexts[i]).find()) { |
| return index; |
| } |
| } |
| } |
| index = options.forwardSearch ? ++index : --index; |
| } |
| } |
| return -1; |
| } |
| |
| /** |
| * Returns whether the specified search string can be found using the given |
| * options. |
| * |
| * @param findString |
| * the string to search for |
| * @param options |
| * The search options |
| * @return <code>true</code> if the search string can be found using the |
| * given options |
| * |
| */ |
| private boolean findAndSelect(String findString, SearchOptions options) { |
| FindTarget findTarget = fFindTarget; |
| if (findTarget == null) { |
| return false; |
| } |
| ITimeGraphEntry[] topInput = findTarget.getEntries(); |
| BiMap<@NonNull ITimeGraphEntry, @NonNull Integer> items = HashBiMap.create(); |
| for (ITimeGraphEntry entry : topInput) { |
| listEntries(items, entry); |
| } |
| int startPosition = findTarget.getSelection() == null ? 0 : NonNullUtils.checkNotNull(items.get(findTarget.getSelection())); |
| |
| int index = findNext(findString, startPosition, items, options); |
| |
| if (index == -1) { |
| statusMessage(Messages.TimeGraphFindDialog_StatusNoMatchLabel); |
| return false; |
| } |
| |
| if (options.forwardSearch && index >= startPosition || !options.forwardSearch && index <= startPosition) { |
| statusMessage(""); //$NON-NLS-1$ |
| } |
| |
| // Send the entry found to target |
| findTarget.selectAndReveal(NonNullUtils.checkNotNull(items.inverse().get(index))); |
| return true; |
| } |
| |
| private void listEntries(Map<ITimeGraphEntry, Integer> items, ITimeGraphEntry root) { |
| items.put(root, items.size()); |
| for (ITimeGraphEntry child : root.getChildren()) { |
| listEntries(items, child); |
| } |
| } |
| |
| // ------- accessors --------------------------------------- |
| |
| /** |
| * Retrieves the string to search for from the appropriate text input field |
| * and returns it. |
| * |
| * @return the search string |
| */ |
| private String getFindString() { |
| if (okToUse(fFindField)) { |
| return fFindField.getText(); |
| } |
| return ""; //$NON-NLS-1$ |
| } |
| |
| /** |
| * Returns the dialog's boundaries. |
| * |
| * @return the dialog's boundaries |
| */ |
| private Rectangle getDialogBoundaries() { |
| if (okToUse(getShell())) { |
| return getShell().getBounds(); |
| } |
| return fDialogPositionInit; |
| } |
| |
| // ------- init / close --------------------------------------- |
| @Override |
| public boolean close() { |
| handleDialogClose(); |
| return super.close(); |
| } |
| |
| /** |
| * Removes focus changed listener from browser and stores settings for |
| * re-open. |
| */ |
| private void handleDialogClose() { |
| |
| // remove listeners |
| if (okToUse(fFindField)) { |
| fFindField.removeModifyListener(fFindModifyListener); |
| } |
| updateTarget(null, false); |
| |
| if (fParentShell != null) { |
| fParentShell.removeShellListener(fActivationListener); |
| fParentShell = null; |
| } |
| |
| if (getShell() != null && !getShell().isDisposed()) { |
| getShell().removeShellListener(fActivationListener); |
| } |
| |
| // store current settings in case of re-open |
| storeSettings(); |
| |
| // prevent leaks |
| fActiveShell = null; |
| |
| } |
| |
| /** |
| * Writes the current selection to the dialog settings. |
| */ |
| private void writeSelection() { |
| final FindTarget input = fFindTarget; |
| if (input == null) { |
| return; |
| } |
| |
| IDialogSettings s = getDialogSettings(); |
| final ITimeGraphEntry selection = input.getSelection(); |
| if (selection != null) { |
| s.put("selection", selection.getName()); //$NON-NLS-1$ |
| } |
| } |
| |
| /** |
| * Stores the current state in the dialog settings. |
| */ |
| private void storeSettings() { |
| fDialogPositionInit = getDialogBoundaries(); |
| fWrapInit = isWrapSearch(); |
| fWholeWordInit = isWholeWordSetting(); |
| fCaseInit = isCaseSensitiveSearch(); |
| fIsRegExInit = isRegExSearch(); |
| fForwardInit = isForwardSearch(); |
| |
| writeConfiguration(); |
| } |
| |
| /** |
| * Initializes the string to search for and the appropriate text in the Find |
| * field based on the selection found in the timegraph view. |
| */ |
| private void initFindStringFromSelection() { |
| FindTarget findTarget = fFindTarget; |
| if (findTarget != null && okToUse(fFindField)) { |
| final ITimeGraphEntry selection = findTarget.getSelection(); |
| if (selection != null) { |
| String fullSelection = selection.getName(); |
| fFindField.removeModifyListener(fFindModifyListener); |
| if (fullSelection.length() > 0) { |
| fFindField.setText(fullSelection); |
| } |
| } else { |
| if ("".equals(fFindField.getText())) { //$NON-NLS-1$ |
| if (!fFindHistory.isEmpty()) { |
| fFindField.setText(fFindHistory.get(0)); |
| } else { |
| fFindField.setText(""); //$NON-NLS-1$ |
| } |
| } |
| } |
| fFindField.setSelection(new Point(0, fFindField.getText().length())); |
| fFindField.addModifyListener(fFindModifyListener); |
| } |
| } |
| |
| // ------- Options --------------------------------------- |
| |
| /** |
| * Retrieves and returns the option case sensitivity from the appropriate |
| * check box. |
| * |
| * @return <code>true</code> if case sensitive |
| */ |
| private boolean isCaseSensitiveSearch() { |
| if (okToUse(fCaseCheckBox)) { |
| return fCaseCheckBox.getSelection(); |
| } |
| return fCaseInit; |
| } |
| |
| /** |
| * Retrieves and returns the regEx option from the appropriate check box. |
| * |
| * @return <code>true</code> if case sensitive |
| */ |
| private boolean isRegExSearch() { |
| if (okToUse(fIsRegExCheckBox)) { |
| return fIsRegExCheckBox.getSelection(); |
| } |
| return fIsRegExInit; |
| } |
| |
| /** |
| * Retrieves and returns the option search direction from the appropriate |
| * check box. |
| * |
| * @return <code>true</code> if searching forward |
| */ |
| private boolean isForwardSearch() { |
| if (okToUse(fForwardRadioButton)) { |
| return fForwardRadioButton.getSelection(); |
| } |
| return fForwardInit; |
| } |
| |
| /** |
| * Retrieves and returns the option search whole words from the appropriate |
| * check box. |
| * |
| * @return <code>true</code> if searching for whole words |
| */ |
| private boolean isWholeWordSetting() { |
| if (okToUse(fWholeWordCheckBox)) { |
| return fWholeWordCheckBox.getSelection(); |
| } |
| return fWholeWordInit; |
| } |
| |
| /** |
| * Returns <code>true</code> if searching should be restricted to entire |
| * words, <code>false</code> if not. This is the case if the respective |
| * checkbox is turned on, regex is off, and the checkbox is enabled, i.e. |
| * the current find string is an entire word. |
| * |
| * @return <code>true</code> if the search is restricted to whole words |
| */ |
| private boolean isWholeWordSearch() { |
| return isWholeWordSetting() && !isRegExSearch() && (okToUse(fWholeWordCheckBox) ? fWholeWordCheckBox.isEnabled() : true); |
| } |
| |
| /** |
| * Retrieves and returns the option wrap search from the appropriate check |
| * box. |
| * |
| * @return <code>true</code> if wrapping while searching |
| */ |
| private boolean isWrapSearch() { |
| if (okToUse(fWrapCheckBox)) { |
| return fWrapCheckBox.getSelection(); |
| } |
| return fWrapInit; |
| } |
| |
| /** |
| * Creates a button. |
| * |
| * @param parent |
| * the parent control |
| * @param label |
| * the button label |
| * @param id |
| * the button id |
| * @param dfltButton |
| * is this button the default button |
| * @param listener |
| * a button pressed listener |
| * @return the new button |
| */ |
| private Button makeButton(Composite parent, String label, int id, boolean dfltButton, SelectionListener listener) { |
| Button button = createButton(parent, id, label, dfltButton); |
| button.addSelectionListener(listener); |
| storeButtonWithMnemonicInMap(button); |
| return button; |
| } |
| |
| /** |
| * Stores the button and its mnemonic in {@link #fMnemonicButtonMap}. |
| * |
| * @param button |
| * button whose mnemonic has to be stored |
| */ |
| private void storeButtonWithMnemonicInMap(Button button) { |
| char mnemonic = LegacyActionTools.extractMnemonic(button.getText()); |
| if (mnemonic != LegacyActionTools.MNEMONIC_NONE) { |
| fMnemonicButtonMap.put(new Character(Character.toLowerCase(mnemonic)), button); |
| } |
| } |
| |
| /** |
| * Sets the given status message in the status line. |
| * |
| * @param message |
| * the message |
| */ |
| private void statusMessage(String message) { |
| fStatusLabel.setText(message); |
| getShell().getDisplay().beep(); |
| } |
| |
| /** |
| * Locates the user's findString in the entries information of the time |
| * graph view. |
| * |
| * @param forwardSearch |
| * the search direction |
| */ |
| private void performSearch(boolean forwardSearch) { |
| |
| String findString = getFindString(); |
| if (findString != null && findString.length() > 0) { |
| findAndSelect(findString, getSearchOptions(forwardSearch)); |
| } |
| writeSelection(); |
| updateButtonState(); |
| } |
| |
| private SearchOptions getSearchOptions(boolean forwardSearch) { |
| SearchOptions options = new SearchOptions(); |
| options.forwardSearch = forwardSearch; |
| options.caseSensitive = isCaseSensitiveSearch(); |
| options.wrapSearch = isWrapSearch(); |
| options.wholeWord = isWholeWordSearch(); |
| options.regExSearch = isRegExSearch(); |
| return options; |
| } |
| |
| // ------- UI creation --------------------------------------- |
| |
| /** |
| * Attaches the given layout specification to the <code>component</code>. |
| * |
| * @param component |
| * the component |
| * @param horizontalAlignment |
| * horizontal alignment |
| * @param grabExcessHorizontalSpace |
| * grab excess horizontal space |
| * @param verticalAlignment |
| * vertical alignment |
| * @param grabExcessVerticalSpace |
| * grab excess vertical space |
| */ |
| private static void setGridData(Control component, int horizontalAlignment, boolean grabExcessHorizontalSpace, int verticalAlignment, boolean grabExcessVerticalSpace) { |
| GridData gd; |
| if (component instanceof Button && (((Button) component).getStyle() & SWT.PUSH) != 0) { |
| gd = (GridData) component.getLayoutData(); |
| gd.horizontalAlignment = GridData.FILL; |
| gd.widthHint = 100; |
| } else { |
| gd = new GridData(); |
| component.setLayoutData(gd); |
| gd.horizontalAlignment = horizontalAlignment; |
| gd.grabExcessHorizontalSpace = grabExcessHorizontalSpace; |
| } |
| gd.verticalAlignment = verticalAlignment; |
| gd.grabExcessVerticalSpace = grabExcessVerticalSpace; |
| } |
| |
| /** |
| * Updates the enabled state of the buttons. |
| */ |
| private void updateButtonState() { |
| if (okToUse(getShell()) && okToUse(fFindNextButton)) { |
| |
| boolean enable = fFindTarget != null && (fActiveShell == fParentShell || fActiveShell == getShell()); |
| String str = getFindString(); |
| boolean findString = str != null && str.length() > 0; |
| |
| fWholeWordCheckBox.setEnabled(isWord(str) && !isRegExSearch()); |
| |
| fFindNextButton.setEnabled(enable && findString); |
| } |
| } |
| |
| /** |
| * Tests whether each character in the given string is a letter. |
| * |
| * @param str |
| * the string to check |
| * @return <code>true</code> if the given string is a word |
| */ |
| private static boolean isWord(String str) { |
| if (str == null || str.length() == 0) { |
| return false; |
| } |
| |
| for (int i = 0; i < str.length(); i++) { |
| if (!Character.isJavaIdentifierPart(str.charAt(i))) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Updates the given combo with the given content. |
| * |
| * @param combo |
| * combo to be updated |
| * @param content |
| * to be put into the combo |
| */ |
| private static void updateCombo(Combo combo, List<String> content) { |
| combo.removeAll(); |
| for (int i = 0; i < content.size(); i++) { |
| combo.add(content.get(i).toString()); |
| } |
| } |
| |
| // ------- open / reopen --------------------------------------- |
| |
| /** |
| * Called after executed find action to update the history. |
| */ |
| private void updateFindHistory() { |
| if (okToUse(fFindField)) { |
| fFindField.removeModifyListener(fFindModifyListener); |
| |
| updateHistory(fFindField, fFindHistory); |
| fFindField.addModifyListener(fFindModifyListener); |
| } |
| } |
| |
| /** |
| * Updates the combo with the history. |
| * |
| * @param combo |
| * to be updated |
| * @param history |
| * to be put into the combo |
| */ |
| private static void updateHistory(Combo combo, List<String> history) { |
| String findString = combo.getText(); |
| int index = history.indexOf(findString); |
| if (index != 0) { |
| if (index != -1) { |
| history.remove(index); |
| } |
| history.add(0, findString); |
| Point selection = combo.getSelection(); |
| updateCombo(combo, history); |
| combo.setText(findString); |
| combo.setSelection(selection); |
| } |
| } |
| |
| /** |
| * Sets the parent shell of this dialog to be the given shell. |
| * |
| * @param shell |
| * the new parent shell |
| */ |
| @Override |
| public void setParentShell(Shell shell) { |
| if (shell != fParentShell) { |
| |
| if (fParentShell != null) { |
| fParentShell.removeShellListener(fActivationListener); |
| } |
| |
| fParentShell = shell; |
| fParentShell.addShellListener(fActivationListener); |
| } |
| |
| fActiveShell = shell; |
| } |
| |
| // --------------- configuration handling -------------- |
| |
| /** |
| * Returns the dialog settings object used to share state of the find |
| * dialog. |
| * |
| * @return the dialog settings to be used |
| */ |
| private IDialogSettings getDialogSettings() { |
| IDialogSettings settings = Activator.getDefault().getDialogSettings(); |
| fDialogSettings = settings.getSection(getClass().getName()); |
| if (fDialogSettings == null) { |
| fDialogSettings = settings.addNewSection(getClass().getName()); |
| } |
| return fDialogSettings; |
| } |
| |
| /** |
| * Initializes itself from the dialog settings with the same state as at the |
| * previous invocation. |
| */ |
| private void readConfiguration() { |
| IDialogSettings s = getDialogSettings(); |
| |
| fWrapInit = s.get(WRAP) == null || s.getBoolean(WRAP); |
| fCaseInit = s.getBoolean(CASE_SENSITIVE); |
| fWholeWordInit = s.getBoolean(WHOLE_WORD); |
| fIsRegExInit = s.getBoolean(IS_REGEX_EXPRESSION); |
| |
| String[] findHistory = s.getArray(FIND_HISTORY); |
| if (findHistory != null) { |
| fFindHistory.clear(); |
| for (int i = 0; i < findHistory.length; i++) { |
| fFindHistory.add(findHistory[i]); |
| } |
| } |
| } |
| |
| /** |
| * Stores its current configuration in the dialog store. |
| */ |
| private void writeConfiguration() { |
| IDialogSettings s = getDialogSettings(); |
| |
| s.put(WRAP, fWrapInit); |
| s.put(CASE_SENSITIVE, fCaseInit); |
| s.put(WHOLE_WORD, fWholeWordInit); |
| s.put(CASE_SENSITIVE, fIsRegExInit); |
| |
| String findString = getFindString(); |
| if (findString.length() > 0) { |
| fFindHistory.add(0, findString); |
| } |
| writeHistory(fFindHistory, s, FIND_HISTORY); |
| } |
| |
| /** |
| * Writes the given history into the given dialog store. |
| * |
| * @param history |
| * the history |
| * @param settings |
| * the dialog settings |
| * @param sectionName |
| * the section name |
| */ |
| private static void writeHistory(List<String> history, IDialogSettings settings, String sectionName) { |
| int itemCount = history.size(); |
| Set<String> distinctItems = new HashSet<>(itemCount); |
| for (int i = 0; i < itemCount; i++) { |
| String item = history.get(i); |
| if (distinctItems.contains(item)) { |
| history.remove(i--); |
| itemCount--; |
| } else { |
| distinctItems.add(item); |
| } |
| } |
| |
| while (history.size() > 8) { |
| history.remove(8); |
| } |
| |
| String[] names = new String[history.size()]; |
| history.toArray(names); |
| settings.put(sectionName, names); |
| |
| } |
| |
| /** |
| * Update the time graph viewer this dialog search in |
| * |
| * @param findTarget |
| * The find target. Can be <code>null</code>. |
| * @param initFindString |
| * This value should be true if the find text field needs to be |
| * populated with selected entry in the find viewer, false |
| * otherwise |
| */ |
| public void updateTarget(@Nullable FindTarget findTarget, boolean initFindString) { |
| fFindTarget = findTarget; |
| if (initFindString) { |
| initFindStringFromSelection(); |
| } |
| updateButtonState(); |
| } |
| |
| /** |
| * Test if the shell to test is the parent shell of the dialog |
| * |
| * @param shell |
| * The shell to test |
| * @return <code>true</code> if the shell to test is the parent shell, |
| * <code>false</code> otherwise |
| */ |
| public boolean isDialogParentShell(Shell shell) { |
| return shell == getParentShell(); |
| } |
| |
| /** |
| * Adjust the index where to start the search |
| * |
| * @param previousIndex |
| * The previous index |
| * @param count |
| * The number of items to search into |
| * @param forwardSearch |
| * The search direction |
| * @return The adjusted index |
| */ |
| private static int adjustIndex(int previousIndex, int count, boolean forwardSearch) { |
| int index = previousIndex; |
| if (forwardSearch) { |
| index = index >= count || index < 0 ? 0 : index; |
| } else { |
| index = index >= count || index < 0 ? count - 1 : index; |
| } |
| return index; |
| } |
| |
| /** |
| * Create a pattern from the string to find and with options provided |
| * |
| * This implementation is drawn from the jface implementation of |
| * org.eclipse.jface.text.FindReplaceDocumentAdapter |
| * |
| * @param findString |
| * The string to find |
| * @param caseSensitive |
| * Tells if the pattern will activate the case sensitive flag |
| * @param wholeWord |
| * Tells if the pattern will activate the whole word flag |
| * @param regExSearch |
| * Tells if the pattern will activate the regEx flag |
| * @return The created pattern |
| */ |
| private static @NonNull Pattern getPattern(String findString, SearchOptions options) { |
| String toFind = findString; |
| |
| int patternFlags = 0; |
| |
| if (options.regExSearch) { |
| toFind = substituteLinebreak(toFind); |
| } |
| |
| if (!options.caseSensitive) { |
| patternFlags |= Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE; |
| } |
| |
| if (!options.regExSearch) { |
| toFind = asRegPattern(toFind); |
| } |
| |
| if (options.wholeWord) { |
| toFind = "\\b" + toFind + "\\b"; //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| Pattern pattern = Pattern.compile(toFind, patternFlags); |
| return pattern; |
| } |
| |
| /** |
| * Substitutes \R in a regex find pattern with (?>\r\n?|\n) |
| * |
| * This implementation is drawn from the jface implementation of |
| * org.eclipse.jface.text.FindReplaceDocumentAdapter |
| * |
| * @param findString |
| * the original find pattern |
| * @return the transformed find pattern |
| */ |
| private static String substituteLinebreak(String findString) { |
| int length = findString.length(); |
| StringBuffer buf = new StringBuffer(length); |
| |
| int inCharGroup = 0; |
| int inBraces = 0; |
| boolean inQuote = false; |
| for (int i = 0; i < length; i++) { |
| char ch = findString.charAt(i); |
| switch (ch) { |
| case '[': |
| buf.append(ch); |
| if (!inQuote) { |
| inCharGroup++; |
| } |
| break; |
| |
| case ']': |
| buf.append(ch); |
| if (!inQuote) { |
| inCharGroup--; |
| } |
| break; |
| |
| case '{': |
| buf.append(ch); |
| if (!inQuote && inCharGroup == 0) { |
| inBraces++; |
| } |
| break; |
| |
| case '}': |
| buf.append(ch); |
| if (!inQuote && inCharGroup == 0) { |
| inBraces--; |
| } |
| break; |
| |
| case '\\': |
| if (i + 1 < length) { |
| char ch1 = findString.charAt(i + 1); |
| if (inQuote) { |
| if (ch1 == 'E') { |
| inQuote = false; |
| } |
| buf.append(ch).append(ch1); |
| i++; |
| |
| } else if (ch1 == 'R') { |
| if (inCharGroup > 0 || inBraces > 0) { |
| String msg = "TimeGrahViewer.illegalLinebreak"; //$NON-NLS-1$ |
| throw new PatternSyntaxException(msg, findString, i); |
| } |
| buf.append("(?>\\r\\n?|\\n)"); //$NON-NLS-1$ |
| i++; |
| |
| } else { |
| if (ch1 == 'Q') { |
| inQuote = true; |
| } |
| buf.append(ch).append(ch1); |
| i++; |
| } |
| } else { |
| buf.append(ch); |
| } |
| break; |
| |
| default: |
| buf.append(ch); |
| break; |
| } |
| |
| } |
| return buf.toString(); |
| } |
| |
| /** |
| * Converts a non-regex string to a pattern that can be used with the regex |
| * search engine. |
| * |
| * This implementation is drawn from the jface implementation of |
| * org.eclipse.jface.text.FindReplaceDocumentAdapter |
| * |
| * @param string |
| * the non-regex pattern |
| * @return the string converted to a regex pattern |
| */ |
| private static String asRegPattern(String string) { |
| StringBuffer out = new StringBuffer(string.length()); |
| boolean quoting = false; |
| |
| for (int i = 0, length = string.length(); i < length; i++) { |
| char ch = string.charAt(i); |
| if (ch == '\\') { |
| if (quoting) { |
| out.append("\\E"); //$NON-NLS-1$ |
| quoting = false; |
| } |
| out.append("\\\\"); //$NON-NLS-1$ |
| continue; |
| } |
| if (!quoting) { |
| out.append("\\Q"); //$NON-NLS-1$ |
| quoting = true; |
| } |
| out.append(ch); |
| } |
| if (quoting) { |
| out.append("\\E"); //$NON-NLS-1$ |
| } |
| |
| return out.toString(); |
| } |
| |
| private class SearchOptions { |
| boolean forwardSearch; |
| boolean caseSensitive; |
| boolean wrapSearch; |
| boolean wholeWord; |
| boolean regExSearch; |
| } |
| } |