/* * DBeaver - Universal Database Manager * Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jkiss.dbeaver.ui.editors.binary.dialogs; import org.eclipse.swt.SWT; import org.eclipse.swt.events.*; import org.eclipse.swt.graphics.FontMetrics; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.*; import org.eclipse.swt.widgets.*; import org.jkiss.dbeaver.core.CoreMessages; import org.jkiss.dbeaver.ui.editors.binary.BinaryTextFinder; import org.jkiss.dbeaver.ui.editors.binary.HexEditControl; import org.jkiss.dbeaver.ui.editors.binary.HexManager; import org.jkiss.dbeaver.utils.GeneralUtils; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Find/Replace dialog with hex/text, forward/backward, and ignore case options. Remembers previous * state, in case it has been closed by the user and reopened again. * * @author Jordi */ public class FindReplaceDialog extends Dialog { private static final Pattern patternHexDigits = Pattern.compile("[0-9a-fA-F]*"); //$NON-NLS-1$ private static final String text1Replacement = CoreMessages.dialog_find_replace_1_replacement; private static final String textBackward = CoreMessages.dialog_find_replace_backward; private static final String textCancel = CoreMessages.dialog_find_replace_cancel; private static final String textClose = CoreMessages.dialog_find_replace_close; private static final String textDirection = CoreMessages.dialog_find_replace_direction; private static final String textError = CoreMessages.dialog_find_replace_error_; private static final String textFind = CoreMessages.dialog_find_replace_find; private static final String textFindLiteral = CoreMessages.dialog_find_replace_find_literal; private static final String textFindReplace = CoreMessages.dialog_find_replace_find_replace; private static final String textForward = CoreMessages.dialog_find_replace_forward; private static final String textFoundLiteral = CoreMessages.dialog_find_replace_found_literal; private static final String textHex = "Hex"; //$NON-NLS-1$ private static final String textIgnoreCase = CoreMessages.dialog_find_replace_ignore_case; private static final String textLiteralNotFound = CoreMessages.dialog_find_replace_literal_not_found; private static final String textNewFind = CoreMessages.dialog_find_replace_new_find; private static final String textReplace = CoreMessages.dialog_find_replace_replace; private static final String textReplaceAll = CoreMessages.dialog_find_replace_replace_all; private static final String textReplaceFind = CoreMessages.dialog_find_replace_replace_find; private static final String textReplaceWith = CoreMessages.dialog_find_replace_replace_with; private static final String textReplacements = CoreMessages.dialog_find_replace_replacements; private static final String textSearching = CoreMessages.dialog_find_replace_searching; private static final String textStop = CoreMessages.dialog_find_replace_stop; private static final String textText = CoreMessages.dialog_find_replace_text; SelectionAdapter defaultSelectionAdapter = new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { if (lastIgnoreCase != checkBox.getSelection() || lastForward != forwardRadioButton.getSelection() || lastFindHexButtonSelected != findGroup.hexRadioButton.getSelection() || lastReplaceHexButtonSelected != replaceGroup.hexRadioButton.getSelection()) { feedbackLabel.setText(""); //$NON-NLS-1$ } lastFocused.textCombo.setFocus(); } }; private static final List<Object[]> findReplaceFindList = new ArrayList<>(); private static final List<Object[]> findReplaceReplaceList = new ArrayList<>(); private HexEditControl editControl = null; private TextHexInputGroup lastFocused = null; private boolean lastForward = true; private boolean lastFindHexButtonSelected = true; private boolean lastReplaceHexButtonSelected = true; private boolean lastIgnoreCase = false; private boolean searching = false; // visual components private Shell sShell = null; private TextHexInputGroup findGroup = null; private TextHexInputGroup replaceGroup = null; private Group directionGroup = null; private Button forwardRadioButton = null; private Button backwardRadioButton = null; private Button checkBox = null; private Composite findReplaceButtonsComposite = null; private Button findButton = null; private Button replaceFindButton = null; private Button replaceButton = null; private Button replaceAllButton = null; private Label feedbackLabel = null; private Composite progressComposite = null; private ProgressBar progressBar = null; private Button progressCancelButton = null; private Button closeButton = null; /** * Group with text/hex selector and text input */ class TextHexInputGroup { private List<Object[]> items = null; // list of tuples {String, Boolean} // visual components private Group group = null; private Composite composite = null; private Button hexRadioButton = null; private Button textRadioButton = null; private Combo textCombo = null; public TextHexInputGroup(List<Object[]> oldItems) { items = oldItems; } private void initialise() { group = new Group(sShell, SWT.NONE); GridLayout gridLayout = new GridLayout(); gridLayout.numColumns = 2; group.setLayout(gridLayout); createComposite(); textCombo = new Combo(group, SWT.BORDER); int columns = 35; GC gc = new GC(textCombo); FontMetrics fm = gc.getFontMetrics(); int width = columns * fm.getAverageCharWidth(); gc.dispose(); textCombo.setLayoutData(new GridData(width, SWT.DEFAULT)); textCombo.addVerifyListener(new VerifyListener() { @Override public void verifyText(org.eclipse.swt.events.VerifyEvent e) { if (e.keyCode == 0) return; // a list selection if (hexRadioButton.getSelection()) { Matcher numberMatcher = patternHexDigits.matcher(e.text); e.doit = numberMatcher.matches(); } } }); textCombo.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { int index = textCombo.getSelectionIndex(); if (index < 0) return; Boolean selection = (Boolean) (items == null ? null : (items.get(index))[1]); if (selection != null) { refreshHexOrText(selection); } } }); textCombo.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent e) { feedbackLabel.setText(""); //$NON-NLS-1$ if (TextHexInputGroup.this == findGroup) { enableDisableControls(); } } }); } /** * This method initializes composite */ private void createComposite() { RowLayout rowLayout1 = new RowLayout(); rowLayout1.marginTop = 2; rowLayout1.marginBottom = 2; rowLayout1.type = SWT.VERTICAL; composite = new Composite(group, SWT.NONE); composite.setLayout(rowLayout1); hexRadioButton = new Button(composite, SWT.RADIO); hexRadioButton.setText(textHex); hexRadioButton.addSelectionListener(defaultSelectionAdapter); hexRadioButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { Matcher numberMatcher = patternHexDigits.matcher(textCombo.getText()); if (!numberMatcher.matches()) textCombo.setText(""); //$NON-NLS-1$ } }); textRadioButton = new Button(composite, SWT.RADIO); textRadioButton.setText(textText); textRadioButton.addSelectionListener(defaultSelectionAdapter); } private void refreshCombo() { if (items == null) return; if (textCombo.getItemCount() > 0) { textCombo.remove(0, textCombo.getItemCount() - 1); } for (Object[] item : items) { String itemString = (String) item[0]; textCombo.add(itemString); } if (!items.isEmpty()) { textCombo.setText((String) items.get(0)[0]); } selectText(); } private void refreshHexOrText(boolean hex) { hexRadioButton.setSelection(hex); textRadioButton.setSelection(!hex); } private void rememberText() { String lastText = textCombo.getText(); if ("".equals(lastText) || items == null) return; //$NON-NLS-1$ for (Iterator<Object[]> iterator = items.iterator(); iterator.hasNext();) { String itemString = (String)iterator.next()[0]; if (lastText.equals(itemString)) { iterator.remove(); } } items.add(0, new Object[]{lastText, hexRadioButton.getSelection()}); refreshCombo(); } private void selectText() { textCombo.setSelection(new Point(0, textCombo.getText().length())); } private void setEnabled(boolean enabled) { group.setEnabled(enabled); hexRadioButton.setEnabled(enabled); textRadioButton.setEnabled(enabled); textCombo.setEnabled(enabled); } } /** * Create find/replace dialog always on top of shell * * @param aShell where it is displayed */ public FindReplaceDialog(Shell aShell) { super(aShell); } private void activateProgressBar() { Display.getCurrent().timerExec(500, new Runnable() { @Override public void run() { if (searching && !progressComposite.isDisposed()) { progressComposite.setVisible(true); } } }); long max = editControl.getContent().length(); long min = editControl.getCaretPos(); if (backwardRadioButton.getSelection()) { max = min; min = 0L; } int factor = 0; while (max > Integer.MAX_VALUE) { max = max >>> 1; min = min >>> 1; ++factor; } progressBar.setMaximum((int) max); progressBar.setMinimum((int) min); progressBar.setSelection(0); final int finalFactor = factor; Display.getCurrent().timerExec(1000, new Runnable() { @Override public void run() { if (!searching || progressBar.isDisposed()) return; int selection = 0; if (editControl.getFinder() != null) { selection = (int) (editControl.getFinder().getSearchPosition() >>> finalFactor); if (backwardRadioButton.getSelection()) { selection = progressBar.getMaximum() - selection; } } progressBar.setSelection(selection); Display.getCurrent().timerExec(1000, this); } }); } /** * Open and display the dialog. */ public void open() { if (sShell == null || sShell.isDisposed()) { createSShell(); } sShell.pack(); HexManager.reduceDistance(getParent(), sShell); findGroup.refreshCombo(); long selectionLength = editControl.getSelection()[1] - editControl.getSelection()[0]; if (selectionLength > 0L && selectionLength <= BinaryTextFinder.MAX_SEQUENCE_SIZE) { findGroup.refreshHexOrText(true); checkBox.setEnabled(false); StringBuilder selectedText = new StringBuilder(); byte[] selection = new byte[(int) selectionLength]; try { editControl.getContent().get(ByteBuffer.wrap(selection), editControl.getSelection()[0]); } catch (IOException e) { throw new IllegalStateException(e); } for (int i = 0; i < selectionLength; ++i) { selectedText.append(GeneralUtils.byteToHex[selection[i] & 0x0ff]); } findGroup.textCombo.setText(selectedText.toString()); findGroup.selectText(); } else { findGroup.refreshHexOrText(lastFindHexButtonSelected); checkBox.setEnabled(!lastFindHexButtonSelected); } replaceGroup.refreshHexOrText(lastReplaceHexButtonSelected); replaceGroup.refreshCombo(); checkBox.setSelection(lastIgnoreCase); if (lastForward) forwardRadioButton.setSelection(true); else backwardRadioButton.setSelection(true); feedbackLabel.setText(textNewFind); lastFocused = findGroup; lastFocused.textCombo.setFocus(); enableDisableControls(); sShell.open(); } /** * This method initializes find/replace buttons composite */ private void createFindReplaceButtonsComposite() { GridLayout gridLayout = new GridLayout(); gridLayout.marginHeight = 5; gridLayout.marginWidth = 0; gridLayout.numColumns = 2; gridLayout.makeColumnsEqualWidth = true; findReplaceButtonsComposite = new Composite(sShell, SWT.NONE); FormData formData = new FormData(); formData.top = new FormAttachment(directionGroup); formData.left = new FormAttachment(0); formData.right = new FormAttachment(100); findReplaceButtonsComposite.setLayoutData(formData); findReplaceButtonsComposite.setLayout(gridLayout); findButton = new Button(findReplaceButtonsComposite, SWT.NONE); findButton.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false)); findButton.setText(textFind); findButton.addSelectionListener(defaultSelectionAdapter); findButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { doFind(); } }); replaceFindButton = new Button(findReplaceButtonsComposite, SWT.NONE); replaceFindButton.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false)); replaceFindButton.setText(textReplaceFind); replaceFindButton.addSelectionListener(defaultSelectionAdapter); replaceFindButton.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() { @Override public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { doReplaceFind(); } }); replaceButton = new Button(findReplaceButtonsComposite, SWT.NONE); replaceButton.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false)); replaceButton.setText(textReplace); replaceButton.addSelectionListener(defaultSelectionAdapter); replaceButton.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() { @Override public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { doReplace(); } }); replaceAllButton = new Button(findReplaceButtonsComposite, SWT.NONE); replaceAllButton.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false)); replaceAllButton.setText(textReplaceAll); replaceAllButton.addSelectionListener(defaultSelectionAdapter); replaceAllButton.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() { @Override public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { doReplaceAll(); } }); sShell.setDefaultButton(findButton); } /** * This method initializes composite3 */ private void createIgnoreCaseComposite() { FillLayout fillLayout = new FillLayout(); fillLayout.marginHeight = 10; fillLayout.marginWidth = 10; Composite ignoreCaseComposite = new Composite(sShell, SWT.NONE); ignoreCaseComposite.setLayout(fillLayout); checkBox = new Button(ignoreCaseComposite, SWT.CHECK); checkBox.setText(textIgnoreCase); checkBox.addSelectionListener(defaultSelectionAdapter); FormData formData = new FormData(); formData.top = new FormAttachment(replaceGroup.group); formData.left = new FormAttachment(directionGroup); ignoreCaseComposite.setLayoutData(formData); } /** * This method initializes group1 */ private void createDirectionGroup() { RowLayout rowLayout = new RowLayout(); rowLayout.fill = true; rowLayout.type = org.eclipse.swt.SWT.VERTICAL; directionGroup = new Group(sShell, SWT.NONE); directionGroup.setText(textDirection); FormData formData = new FormData(); formData.top = new FormAttachment(replaceGroup.group); directionGroup.setLayoutData(formData); directionGroup.setLayout(rowLayout); forwardRadioButton = new Button(directionGroup, SWT.RADIO); forwardRadioButton.setText(textForward); forwardRadioButton.addSelectionListener(defaultSelectionAdapter); backwardRadioButton = new Button(directionGroup, SWT.RADIO); backwardRadioButton.setText(textBackward); backwardRadioButton.addSelectionListener(defaultSelectionAdapter); } /** * This method initializes sShell */ private void createSShell() { sShell = new Shell(getParent(), SWT.MODELESS | SWT.DIALOG_TRIM); sShell.setText(textFindReplace); FormLayout formLayout = new FormLayout(); formLayout.marginHeight = 5; formLayout.marginWidth = 5; formLayout.spacing = 5; sShell.setLayout(formLayout); sShell.addShellListener(new ShellAdapter() { @Override public void shellActivated(ShellEvent e) { enableDisableControls(); } }); if (findGroup == null) { findGroup = new TextHexInputGroup(findReplaceFindList); } findGroup.initialise(); findGroup.group.setText(textFindLiteral); SelectionAdapter hexTextSelectionAdapter = new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { checkBox.setEnabled(e.widget == findGroup.textRadioButton); } }; findGroup.textRadioButton.addSelectionListener(hexTextSelectionAdapter); findGroup.hexRadioButton.addSelectionListener(hexTextSelectionAdapter); if (replaceGroup == null) { replaceGroup = new TextHexInputGroup(findReplaceReplaceList); } replaceGroup.initialise(); replaceGroup.group.setText(textReplaceWith); FormData formData = new FormData(); formData.top = new FormAttachment(findGroup.group); replaceGroup.group.setLayoutData(formData); createDirectionGroup(); createIgnoreCaseComposite(); createFindReplaceButtonsComposite(); Composite feedbackComposite = new Composite(sShell, SWT.NONE); FormData formData2 = new FormData(); formData2.top = new FormAttachment(findReplaceButtonsComposite); formData2.left = new FormAttachment(0); formData2.bottom = new FormAttachment(100); feedbackComposite.setLayoutData(formData2); FormLayout formLayout2 = new FormLayout(); // formLayout2.spacing = 5; feedbackComposite.setLayout(formLayout2); feedbackLabel = new Label(feedbackComposite, SWT.CENTER); feedbackLabel.setText(textNewFind); FormData formData3 = new FormData(); formData3.top = new FormAttachment(0); formData3.left = new FormAttachment(0); formData3.right = new FormAttachment(100); feedbackLabel.setLayoutData(formData3); progressComposite = new Composite(feedbackComposite, SWT.NONE); FormLayout formLayout3 = new FormLayout(); formLayout3.spacing = 5; progressComposite.setLayout(formLayout3); FormData formData4 = new FormData(); formData4.top = new FormAttachment(feedbackLabel); formData4.bottom = new FormAttachment(100); formData4.left = new FormAttachment(0); formData4.right = new FormAttachment(100); progressComposite.setLayoutData(formData4); //progressComposite.setBackground(Display.getCurrent().getSystemColor(SWT.COLOR_CYAN)); progressBar = new ProgressBar(progressComposite, SWT.NONE); FormData formData5 = new FormData(); formData5.bottom = new FormAttachment(100); formData5.left = new FormAttachment(0); formData5.height = progressBar.computeSize(SWT.DEFAULT, SWT.DEFAULT, false).y; progressBar.setLayoutData(formData5); progressCancelButton = new Button(progressComposite, SWT.NONE); progressCancelButton.setText(textCancel); FormData formData6 = new FormData(); formData6.right = new FormAttachment(100); progressCancelButton.setLayoutData(formData6); formData5.right = new FormAttachment(progressCancelButton); progressCancelButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { editControl.stopSearching(); } }); progressComposite.setVisible(false); closeButton = new Button(sShell, SWT.NONE); closeButton.setText(textClose); FormData formData1 = new FormData(); formData1.right = new FormAttachment(100); formData1.bottom = new FormAttachment(100); closeButton.setLayoutData(formData1); closeButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { sShell.close(); } }); formData2.right = new FormAttachment(closeButton); sShell.addListener(SWT.Close, new Listener() { @Override public void handleEvent(Event event) { editControl.stopSearching(); } }); } private void doReplace() { replace(); enableDisableControls(); feedbackLabel.setText(""); //$NON-NLS-1$ } private void doReplaceFind() { replace(); doFind(); } private void doFind() { prepareToRun(); progressCancelButton.setText(textCancel); String message = textLiteralNotFound; String literal = findGroup.textCombo.getText(); if (editControl != null && literal.length() > 0) { try { if (editControl.findAndSelect(literal, findGroup.hexRadioButton.getSelection(), forwardRadioButton.getSelection(), checkBox.getSelection())) message = textFoundLiteral; } catch (IOException e) { message = textError + e; } } endOfRun(message); } private void doReplaceAll() { prepareToRun(); progressCancelButton.setText(textStop); String message = textLiteralNotFound; String literal = findGroup.textCombo.getText(); if (editControl != null && literal.length() > 0) { try { int replacements = editControl.replaceAll(literal, findGroup.hexRadioButton.getSelection(), forwardRadioButton.getSelection(), checkBox.getSelection(), replaceGroup.textCombo.getText(), replaceGroup.hexRadioButton.getSelection()); message = replacements + textReplacements; if (replacements == 1) { message = text1Replacement; } } catch (IOException e) { message = textError + e; } } endOfRun(message); } private void enableDisableControls() { findGroup.setEnabled(!searching); replaceGroup.setEnabled(!searching); directionGroup.setEnabled(!searching); forwardRadioButton.setEnabled(!searching); backwardRadioButton.setEnabled(!searching); checkBox.setEnabled(!searching); findButton.setEnabled(!searching); replaceFindButton.setEnabled(!searching); replaceButton.setEnabled(!searching); replaceAllButton.setEnabled(!searching); closeButton.setEnabled(!searching); // getParent().setEnabled(enableButtons); if (searching) { return; } boolean somethingToFind = findGroup.textCombo.getText().length() > 0; findButton.setEnabled(somethingToFind); replaceAllButton.setEnabled(somethingToFind); long selectionLength = 0L; if (editControl != null) { selectionLength = editControl.getSelection()[1] - editControl.getSelection()[0]; } replaceFindButton.setEnabled(selectionLength > 0L && somethingToFind); replaceButton.setEnabled(selectionLength > 0L); } private void endOfRun(String message) { searching = false; if (progressComposite.isDisposed()) return; progressComposite.setVisible(false); feedbackLabel.setText(message); enableDisableControls(); } private void prepareToRun() { searching = true; lastFindHexButtonSelected = findGroup.hexRadioButton.getSelection(); lastReplaceHexButtonSelected = replaceGroup.hexRadioButton.getSelection(); replaceGroup.rememberText(); findGroup.rememberText(); lastForward = forwardRadioButton.getSelection(); lastIgnoreCase = checkBox.getSelection(); feedbackLabel.setText(textSearching); enableDisableControls(); activateProgressBar(); } private void replace() { editControl.replace(replaceGroup.textCombo.getText(), replaceGroup.hexRadioButton.getSelection()); } /** * Set the target editor to search * * @param aTarget with data to search */ public void setTarget(HexEditControl aTarget) { editControl = aTarget; } }