/* * 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; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.StyleRange; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.custom.VerifyKeyListener; import org.eclipse.swt.events.*; import org.eclipse.swt.graphics.*; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.*; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.core.DBeaverUI; import org.jkiss.dbeaver.ui.UIUtils; import org.jkiss.dbeaver.utils.GeneralUtils; import org.jkiss.utils.CommonUtils; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CodingErrorAction; import java.util.ArrayList; import java.util.List; /** * A binary file editor, composed of two synchronized displays: an hexadecimal and a basic ascii char * display. The file size has no effect on the memory footprint of the editor. It has binary, ascii * and unicode find functionality. Use addListener(SWT.Modify, Listener) to listen to changes of the * 'dirty', 'overwrite/insert', 'selection' and 'canUndo/canRedo' status. * * @author Jordi */ public class HexEditControl extends Composite { private static final Log log = Log.getLog(HexEditControl.class); public static final String DEFAULT_FONT_NAME = "Courier New"; //$NON-NLS-1$" public static final FontData DEFAULT_FONT_DATA = new FontData(DEFAULT_FONT_NAME, 10, SWT.NORMAL); static final Color COLOR_BLUE = Display.getCurrent().getSystemColor(SWT.COLOR_BLUE); static final Color COLOR_LIGHT_SHADOW = Display.getCurrent().getSystemColor(SWT.COLOR_WIDGET_LIGHT_SHADOW); static final Color COLOR_NORMAL_SHADOW = Display.getCurrent().getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW); /** * Map of displayed chars. Chars that cannot be displayed correctly are changed for a '.' char. * There are differences on which chars can correctly be displayed in each operating system, * charset encoding, or font system. */ static String headerRow = null; static final byte[] hexToNibble = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15}; static final int maxScreenResolution = 1920; static final int minCharSize = 5; static final int SET_TEXT = 0; static final int SHIFT_FORWARD = 1; // frame static final int SHIFT_BACKWARD = 2; static { // Compose header row StringBuilder rowChars = new StringBuilder(); for (int i = 0; i < maxScreenResolution / minCharSize / 3; ++i) rowChars.append(GeneralUtils.byteToHex[i & 0x0ff]).append(' '); headerRow = rowChars.toString().toUpperCase(); } public final char[] byteToChar = new char[256]; private boolean readOnly; private int charsForFileSizeAddress = 0; private String charset = null; private boolean delayedInQueue = false; private Runnable delayedWaiting = null; private boolean dragging = false; private int fontCharWidth = -1; private List<Integer> highlightRangesInScreen = null; private List<Long> mergeChangeRanges = null; private List<Integer> mergeHighlightRanges = null; private int mergeIndexChange = -2; private int mergeIndexHighlight = -2; private boolean mergeRangesIsBlue = false; private boolean mergeRangesIsHighlight = false; private int mergeRangesPosition = -1; private final int charsForAddress; // Files up to 16 Ters: 11 binary digits + ':' private int bytesPerLine; private boolean caretStickToStart = false; // stick to end private BinaryClipboard myClipboard = null; private BinaryContent content = null; private long endPosition = 0L; private BinaryTextFinder finder = null; private boolean isInserting = true; private KeyListener keyAdapter = new ControlKeyAdapter(); private int lastFocusedTextArea = -1; // 1 or 2; private long lastLocationPosition = -1L; private List<SelectionListener> longSelectionListeners = null; private long previousFindEnd = -1; private boolean previousFindIgnoredCase = false; private String previousFindString = null; private boolean previousFindStringWasHex = false; private int previousLine = -1; private long previousRedrawStart = -1; private long startPosition = 0L; private long textAreasStart = -1L; private int upANibble = 0; // always 0 or 1 private int numberOfLines = 16; private int numberOfLines_1 = numberOfLines - 1; private boolean stopSearching = false; private byte[] tmpRawBuffer = new byte[maxScreenResolution / minCharSize / 3 * maxScreenResolution / minCharSize]; private int verticalBarFactor = 0; // visual components private Color colorCaretLine = null; private Color colorHighlight = null; private Font fontCurrent = null; // disposed externally private Font fontDefault = null; // disposed internally private GridData textGridData = null; private GridData previewGridData = null; private GC styledText1GC = null; private GC styledText2GC = null; private Text linesTextSeparator = null; private StyledText linesText = null; private StyledText hexHeaderText = null; private StyledText hexText = null; private Text previewTextSeparator = null; private StyledText previewText = null; /** * Get long selection start and end points. Helper method for long selection listeners. * The start point is formed by event.width as the most significant int and event.x as the least * significant int. The end point is similarly formed by event.height and event.y */ public static long[] getLongSelection(SelectionEvent event) { return new long[]{((long) event.width) << 32 | (event.x & 0x0ffffffffL), ((long) event.height) << 32 | (event.y & 0x0ffffffffL)}; } /** * Converts a hex String to byte[]. Will convert full bytes only, odd number of hex characters will * have a leading '0' added. Big endian. * * @param hexString an hex string (ie. "0fdA1"). * @return the byte[] value of the hex string */ public static byte[] hexStringToByte(String hexString) { if ((hexString.length() & 1) == 1) // nibbles promote to a full byte hexString = '0' + hexString; byte[] tmp = new byte[hexString.length() / 2]; for (int i = 0; i < tmp.length; ++i) { String hexByte = hexString.substring(i * 2, i * 2 + 2); tmp[i] = (byte) Integer.parseInt(hexByte, 16); } return tmp; } private class ControlKeyAdapter extends KeyAdapter { @Override public void keyPressed(KeyEvent e) { switch (e.keyCode) { case SWT.ARROW_UP: case SWT.ARROW_DOWN: case SWT.ARROW_LEFT: case SWT.ARROW_RIGHT: case SWT.END: case SWT.HOME: case SWT.PAGE_UP: case SWT.PAGE_DOWN: boolean selection = startPosition != endPosition; boolean ctrlKey = (e.stateMask & SWT.CONTROL) != 0; if ((e.stateMask & SWT.SHIFT) != 0) { // shift mod2 long newPos = doNavigateKeyPressed(ctrlKey, e.keyCode, getCaretPos(), false); shiftStartAndEnd(newPos); } else { // if no modifier or control or alt endPosition = startPosition = doNavigateKeyPressed( ctrlKey, e.keyCode, getCaretPos(), e.widget == hexText && !isInserting); caretStickToStart = false; } ensureCaretIsVisible(); Runnable delayed = new Runnable() { @Override public void run() { redrawTextAreas(false); runnableEnd(); } }; runnableAdd(delayed); notifyLongSelectionListeners(); // if (selection != (startPosition != endPosition)) // notifyListeners(SWT.Modify, null); e.doit = false; break; case SWT.INSERT: if ((e.stateMask & SWT.MODIFIER_MASK) == 0) { redrawCaret(true); } else if (!readOnly && e.stateMask == SWT.SHIFT) { paste(); } else if (e.stateMask == SWT.CONTROL) { copy(); } break; case 'a': if (e.stateMask == SWT.CONTROL) // control mod1 selectAll(); break; case 'c': if (e.stateMask == SWT.CONTROL) // control mod1 copy(); break; case 'v': if (!readOnly && e.stateMask == SWT.CONTROL) // control mod1 paste(); break; case 'x': if (!readOnly && e.stateMask == SWT.CONTROL) // control mod1 cut(); break; case 'y': if (!readOnly && e.stateMask == SWT.CONTROL) // control mod1 redo(); break; case 'z': if (!readOnly && e.stateMask == SWT.CONTROL) // control mod1 undo(); break; default: break; } } } private class ControlMouseAdapter extends MouseAdapter { int charLen; public ControlMouseAdapter(boolean hexContent) { charLen = 1; if (hexContent) charLen = 3; } @Override public void mouseDown(MouseEvent e) { if (e.button == 1) dragging = true; int textOffset; try { textOffset = ((StyledText) e.widget).getOffsetAtLocation(new Point(e.x, e.y)); } catch (IllegalArgumentException ex) { textOffset = ((StyledText) e.widget).getCharCount(); } int byteOffset = textOffset / charLen; ((StyledText) e.widget).setTopIndex(0); if (e.button == 1 && (e.stateMask & SWT.MODIFIER_MASK & ~SWT.SHIFT) == 0) {// no modif or shift if ((e.stateMask & SWT.MODIFIER_MASK) == 0) { caretStickToStart = false; startPosition = endPosition = textAreasStart + byteOffset; } else { // shift shiftStartAndEnd(textAreasStart + byteOffset); } refreshCaretsPosition(); setFocus(); refreshSelections(); //notifyListeners(SWT.Modify, null); notifyLongSelectionListeners(); } } @Override public void mouseUp(MouseEvent e) { if (e.button == 1) dragging = false; } } private class ControlPaintAdapter implements PaintListener { boolean hexContent = false; ControlPaintAdapter(boolean isHexText) { hexContent = isHexText; } @Override public void paintControl(PaintEvent event) { event.gc.setForeground(COLOR_LIGHT_SHADOW); int lineWidth = 1; int charLen = 1; int rightHalfWidth = 0; // is 1, but better to tread on leftmost char pixel than rightmost one if (hexContent) { lineWidth = fontCharWidth; charLen = 3; rightHalfWidth = (lineWidth + 1) / 2; // line spans to both sides of its position } event.gc.setLineWidth(lineWidth); for (int block = 8; block <= bytesPerLine; block += 8) { int xPos = (charLen * block) * fontCharWidth - rightHalfWidth; event.gc.drawLine(xPos, event.y, xPos, event.y + event.height); } } } private class ControlSelectionAdapter extends SelectionAdapter implements SelectionListener { int charLen; public ControlSelectionAdapter(boolean hexContent) { charLen = 1; if (hexContent) charLen = 3; } @Override public void widgetSelected(SelectionEvent e) { if (!dragging) return; boolean selection = startPosition != endPosition; int lower = e.x / charLen; int higher = e.y / charLen; int caretPos = ((StyledText) e.widget).getCaretOffset() / charLen; caretStickToStart = caretPos < higher || caretPos < lower; if (lower > higher) { lower = higher; higher = e.x / charLen; } select(textAreasStart + lower, textAreasStart + higher); // if (selection != (startPosition != endPosition)) // notifyListeners(SWT.Modify, null); redrawTextAreas(false); } } private class ControlTraverseAdapter implements TraverseListener { @Override public void keyTraversed(TraverseEvent e) { if (e.detail == SWT.TRAVERSE_TAB_NEXT) e.doit = true; } } private class ControlVerifyKeyAdapter implements VerifyKeyListener { @Override public void verifyKey(VerifyEvent e) { if (readOnly) { return; } if ((e.character == SWT.DEL || e.character == SWT.BS) && isInserting) { if (!deleteSelected()) { if (e.character == SWT.BS) { startPosition += upANibble; if (startPosition > 0L) { content.delete(startPosition - 1L, 1L); endPosition = --startPosition; } } else { // e.character == SWT.DEL content.delete(startPosition, 1L); } ensureWholeScreenIsVisible(); ensureCaretIsVisible(); Runnable delayed = new Runnable() { @Override public void run() { redrawTextAreas(true); runnableEnd(); } }; runnableAdd(delayed); updateScrollBar(); notifyListeners(SWT.Modify, null); notifyLongSelectionListeners(); } upANibble = 0; } else { doModifyKeyPressed(e); } e.doit = false; } } public HexEditControl(final Composite parent, int style) { this(parent, style, 12, 16); } /** * Create a binary text editor * * @param parent parent in the widget hierarchy * @param style not used for the moment */ public HexEditControl(final Composite parent, int style, int charsForAddress, int bytesPerLine) { super(parent, style | SWT.V_SCROLL); this.readOnly = (style & SWT.READ_ONLY) != 0; this.charsForAddress = charsForAddress; this.bytesPerLine = bytesPerLine; this.colorCaretLine = new Color(Display.getCurrent(), 232, 242, 254); // very light blue this.colorHighlight = new Color(Display.getCurrent(), 255, 248, 147); // mellow yellow this.highlightRangesInScreen = new ArrayList<>(); this.myClipboard = new BinaryClipboard(parent.getDisplay()); this.longSelectionListeners = new ArrayList<>(); addDisposeListener(new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { colorCaretLine.dispose(); colorHighlight.dispose(); if (fontDefault != null && !fontDefault.isDisposed()) fontDefault.dispose(); try { myClipboard.dispose(); } catch (IOException ex) { log.warn("Can't cleanup clipboard temporary data"); } } }); initialize(); this.lastFocusedTextArea = 1; this.previousLine = -1; } public BinaryTextFinder getFinder() { return finder; } /** * compose byte-to-char map */ private void composeByteToCharMap() { if (charset == null || previewText == null) return; CharsetDecoder decoder = Charset.forName(charset).newDecoder(). onMalformedInput(CodingErrorAction.REPLACE). onUnmappableCharacter(CodingErrorAction.REPLACE). replaceWith("."); ByteBuffer bb = ByteBuffer.allocate(1); CharBuffer cb = CharBuffer.allocate(1); for (int i = 0; i < 256; ++i) { if (i < 0x20 || i == 0x7f) { byteToChar[i] = (char) (160 + i); } else { bb.clear(); bb.put((byte) i); bb.rewind(); cb.clear(); decoder.reset(); decoder.decode(bb, cb, true); decoder.flush(cb); cb.rewind(); char decoded = cb.get(); // neither font metrics nor graphic context work for charset 8859-1 chars between 128 and // 159 // It works too slow. Dumn with it. byteToChar[i] = decoded; } } } public void setCharset(String name) { if (CommonUtils.isEmpty(name)) { name = GeneralUtils.getDefaultFileEncoding(); } charset = name; composeByteToCharMap(); } public StyledText getHexText() { return hexText; } public StyledText getPreviewText() { return previewText; } /** * redraw the caret with respect of Inserting/Overwriting mode */ public void redrawCaret(boolean focus) { drawUnfocusedCaret(false); setCaretsSize(focus ? (!isInserting) : isInserting); if (isInserting && upANibble != 0) { upANibble = 0; refreshCaretsPosition(); if (focus) setFocus(); } else { drawUnfocusedCaret(true); } // if (focus) notifyListeners(SWT.Modify, null); } /** * Adds a long selection listener. Events sent to the listener have long start and end points. * The start point is formed by event.width as the most significant int and event.x as the least * significant int. The end point is similarly formed by event.height and event.y * A listener can obtain the long selection with this code: getLongSelection(SelectionEvent) * long start = ((long)event.width) << 32 | (event.x & 0x0ffffffffL) * Similarly for the end point: * long end = ((long)event.height) << 32 | (event.y & 0x0ffffffffL) * * @param listener the listener * @see StyledText#addSelectionListener(org.eclipse.swt.events.SelectionListener) */ public void addLongSelectionListener(SelectionListener listener) { if (listener == null) throw new IllegalArgumentException(); if (!longSelectionListeners.contains(listener)) longSelectionListeners.add(listener); } /** * This method initializes composite */ private void initialize() { GridLayout gridLayout1 = new GridLayout(); gridLayout1.numColumns = 3; gridLayout1.marginHeight = 0; gridLayout1.verticalSpacing = 0; gridLayout1.horizontalSpacing = 0; gridLayout1.marginWidth = 0; setLayout(gridLayout1); FocusListener myFocusAdapter = new FocusAdapter() { @Override public void focusGained(FocusEvent e) { drawUnfocusedCaret(false); lastFocusedTextArea = 1; if (e.widget == previewText) lastFocusedTextArea = 2; DBeaverUI.asyncExec(new Runnable() { @Override public void run() { drawUnfocusedCaret(true); } }); } }; Caret defaultCaret; Caret nonDefaultCaret; { // Lines Composite linesColumn = new Composite(this, SWT.NONE); GridLayout columnLayout = new GridLayout(); columnLayout.marginHeight = 0; columnLayout.verticalSpacing = 1; columnLayout.horizontalSpacing = 0; columnLayout.marginWidth = 0; linesColumn.setLayout(columnLayout); //linesColumn.setBackground(COLOR_LIGHT_SHADOW); GridData gridDataColumn = new GridData(SWT.BEGINNING, SWT.FILL, false, true); linesColumn.setLayoutData(gridDataColumn); GridData gridDataTextSeparator = new GridData(SWT.FILL, SWT.BEGINNING, true, false); gridDataTextSeparator.widthHint = 10; linesTextSeparator = new Text(linesColumn, SWT.SEPARATOR); linesTextSeparator.setEnabled(false); linesTextSeparator.setBackground(COLOR_LIGHT_SHADOW); linesTextSeparator.setLayoutData(gridDataTextSeparator); linesText = new StyledText(linesColumn, SWT.MULTI | SWT.READ_ONLY); linesText.setEditable(false); linesText.setEnabled(false); //linesText.setBackground(COLOR_LIGHT_SHADOW); //linesText.setForeground(Display.getCurrent().getSystemColor(SWT.COLOR_BLACK)); fontDefault = new Font(Display.getCurrent(), DEFAULT_FONT_DATA); fontCurrent = fontDefault; linesText.setFont(fontCurrent); GC styledTextGC = new GC(linesText); fontCharWidth = styledTextGC.getFontMetrics().getAverageCharWidth(); styledTextGC.dispose(); GridData gridDataAddresses = new GridData(SWT.BEGINNING, SWT.FILL, false, true); gridDataAddresses.heightHint = numberOfLines * linesText.getLineHeight(); linesText.setLayoutData(gridDataAddresses); setAddressesGridDataWidthHint(); linesText.setContent(new DisplayedContent(charsForAddress, numberOfLines)); } { // Hex Composite hexColumn = new Composite(this, SWT.NONE); GridLayout column1Layout = new GridLayout(); column1Layout.marginHeight = 0; column1Layout.verticalSpacing = 1; column1Layout.horizontalSpacing = 0; column1Layout.marginWidth = 0; hexColumn.setLayout(column1Layout); //hexColumn.setBackground(Display.getCurrent().getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW)); GridData gridDataColumn1 = new GridData(SWT.BEGINNING, SWT.FILL, false, true); hexColumn.setLayoutData(gridDataColumn1); Composite hexHeaderGroup = new Composite(hexColumn, SWT.NONE); //hexHeaderGroup.setBackground(COLOR_LIGHT_SHADOW); GridLayout column1HeaderLayout = new GridLayout(); column1HeaderLayout.marginHeight = 0; column1HeaderLayout.marginWidth = 0; hexHeaderGroup.setLayout(column1HeaderLayout); GridData gridDataColumn1Header = new GridData(SWT.BEGINNING, SWT.BEGINNING, false, false); hexHeaderGroup.setLayoutData(gridDataColumn1Header); GridData gridData = new GridData(); gridData.horizontalIndent = 1; hexHeaderText = new StyledText(hexHeaderGroup, SWT.SINGLE | SWT.READ_ONLY); hexHeaderText.setEditable(false); hexHeaderText.setEnabled(false); hexHeaderText.setBackground(COLOR_LIGHT_SHADOW); //hexHeaderText.setForeground(Display.getCurrent().getSystemColor(SWT.COLOR_BLACK)); hexHeaderText.setLayoutData(gridData); hexHeaderText.setFont(fontCurrent); refreshHeader(); hexText = new StyledText(hexColumn, SWT.MULTI); hexText.setFont(fontCurrent); if (readOnly) { hexText.setEditable(false); } styledText1GC = new GC(hexText); int width = bytesPerLine * 3 * fontCharWidth; textGridData = new GridData(); textGridData.horizontalIndent = 1; textGridData.verticalAlignment = SWT.FILL; textGridData.widthHint = hexText.computeTrim(0, 0, width, 0).width; textGridData.grabExcessVerticalSpace = true; hexText.setLayoutData(textGridData); hexText.addKeyListener(keyAdapter); hexText.addFocusListener(myFocusAdapter); hexText.addMouseListener(new ControlMouseAdapter(true)); hexText.addPaintListener(new ControlPaintAdapter(true)); hexText.addTraverseListener(new ControlTraverseAdapter()); hexText.addVerifyKeyListener(new ControlVerifyKeyAdapter()); hexText.setContent(new DisplayedContent(bytesPerLine * 3, numberOfLines)); hexText.setDoubleClickEnabled(false); hexText.addSelectionListener(new ControlSelectionAdapter(true)); // StyledText.setCaretOffset() version 3.448 bug resets the caret size if using the default one, // so we use not the default one. defaultCaret = hexText.getCaret(); nonDefaultCaret = new Caret(defaultCaret.getParent(), defaultCaret.getStyle()); nonDefaultCaret.setBounds(defaultCaret.getBounds()); hexText.setCaret(nonDefaultCaret); } { // Preview Composite previewColumn = new Composite(this, SWT.NONE); GridLayout column2Layout = new GridLayout(); column2Layout.marginHeight = 0; column2Layout.verticalSpacing = 1; column2Layout.horizontalSpacing = 0; column2Layout.marginWidth = 0; previewColumn.setLayout(column2Layout); //previewColumn.setBackground(hexText.getBackground()); GridData gridDataColumn2 = new GridData(SWT.FILL, SWT.FILL, true, true); previewColumn.setLayoutData(gridDataColumn2); GridData gridDataTextSeparator2 = new GridData(); gridDataTextSeparator2.horizontalAlignment = SWT.FILL; gridDataTextSeparator2.verticalAlignment = SWT.FILL; gridDataTextSeparator2.grabExcessHorizontalSpace = true; previewTextSeparator = new Text(previewColumn, SWT.SEPARATOR); previewTextSeparator.setEnabled(false); previewTextSeparator.setBackground(COLOR_LIGHT_SHADOW); previewTextSeparator.setLayoutData(gridDataTextSeparator2); makeFirstRowSameHeight(); previewText = new StyledText(previewColumn, SWT.MULTI); previewText.setFont(fontCurrent); if (readOnly) { previewText.setEditable(false); } int width = bytesPerLine * fontCharWidth + 1; // one pixel for caret in last linesColumn previewGridData = new GridData(); previewGridData.verticalAlignment = SWT.FILL; previewGridData.widthHint = previewText.computeTrim(0, 0, width, 0).width; previewGridData.grabExcessVerticalSpace = true; previewText.setLayoutData(previewGridData); previewText.addKeyListener(keyAdapter); previewText.addFocusListener(myFocusAdapter); previewText.addMouseListener(new ControlMouseAdapter(false)); previewText.addPaintListener(new ControlPaintAdapter(false)); previewText.addTraverseListener(new ControlTraverseAdapter()); previewText.addVerifyKeyListener(new ControlVerifyKeyAdapter()); previewText.setContent(new DisplayedContent(bytesPerLine, numberOfLines)); previewText.setDoubleClickEnabled(false); previewText.addSelectionListener(new ControlSelectionAdapter(false)); // StyledText.setCaretOffset() version 3.448 bug resets the caret size if using the default one, // so we use not the default one. defaultCaret = previewText.getCaret(); nonDefaultCaret = new Caret(defaultCaret.getParent(), defaultCaret.getStyle()); nonDefaultCaret.setBounds(defaultCaret.getBounds()); previewText.setCaret(nonDefaultCaret); styledText2GC = new GC(previewText); setCharset(null); } super.setFont(fontCurrent); ScrollBar vertical = getVerticalBar(); vertical.setSelection(0); vertical.setMinimum(0); vertical.setIncrement(1); vertical.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { e.doit = false; long previousStart = textAreasStart; textAreasStart = (((long) getVerticalBar().getSelection()) << verticalBarFactor) * (long) bytesPerLine; if (previousStart == textAreasStart) return; Runnable delayed = new Runnable() { @Override public void run() { redrawTextAreas(false); setFocus(); runnableEnd(); } }; runnableAdd(delayed); } }); updateScrollBar(); addMouseListener(new org.eclipse.swt.events.MouseAdapter() { @Override public void mouseDown(org.eclipse.swt.events.MouseEvent e) { setFocus(); } }); addControlListener(new org.eclipse.swt.events.ControlAdapter() { @Override public void controlResized(org.eclipse.swt.events.ControlEvent e) { updateTextsMetrics(); } }); addDisposeListener(new org.eclipse.swt.events.DisposeListener() { @Override public void widgetDisposed(org.eclipse.swt.events.DisposeEvent e) { if (content != null) content.dispose(); } }); } /** * Tells whether the last action can be redone * * @return true: an action ca be redone */ public boolean canRedo() { return content != null && content.canRedo(); } /** * Tells whether the last action can be undone * * @return true: an action ca be undone */ public boolean canUndo() { return content != null && content.canUndo(); } /** * Copies the selection into the clipboard. If nothing is selected leaves the clipboard with its * current contents. The clipboard will hold text data (for pasting into a text editor) and binary * data (internal for HexText). Text data is limited to 4Mbytes, binary data is limited by disk space. */ public void copy() { if (startPosition >= endPosition) return; myClipboard.setContents(content, startPosition, endPosition - startPosition); } StringBuilder cookAddresses(long address, int limit) { StringBuilder theText = new StringBuilder(); for (int i = 0; i < limit; i += bytesPerLine, address += bytesPerLine) { boolean indenting = true; for (int j = (charsForAddress - 2) * 4; j > 0; j -= 4) { int nibble = ((int) (address >>> j)) & 0x0f; if (nibble != 0) indenting = false; if (indenting) { if (j >= (charsForFileSizeAddress * 4)) theText.append(' '); else theText.append('0'); } else { theText.append(GeneralUtils.nibbleToHex[nibble]); } } theText.append(GeneralUtils.nibbleToHex[((int) address) & 0x0f]).append(':'); } return theText; } StringBuilder cookTexts(boolean isHexOutput, int length) { if (length > tmpRawBuffer.length) length = tmpRawBuffer.length; StringBuilder result; if (isHexOutput) { result = new StringBuilder(length * 3); for (int i = 0; i < length; ++i) { result.append(GeneralUtils.byteToHex[tmpRawBuffer[i] & 0x0ff]).append(' '); } } else { result = new StringBuilder(length); for (int i = 0; i < length; ++i) { result.append(byteToChar[tmpRawBuffer[i] & 0x0ff]); } } return result; } /** * Calls copy();deleteSelected(); */ public void cut() { copy(); deleteSelected(); } /** * While in insert mode, deletes the selection * * @return did delete something */ public boolean deleteSelected() { if (!handleSelectedPreModify()) { return false; } upANibble = 0; ensureWholeScreenIsVisible(); restoreStateAfterModify(); return true; } void doModifyKeyPressed(KeyEvent event) { char aChar = event.character; if (aChar == '\0' || aChar == '\b' || aChar == '\u007f' || event.stateMask == SWT.CTRL || event.widget == hexText && ((event.stateMask & SWT.MODIFIER_MASK) != 0 || aChar < '0' || aChar > '9' && aChar < 'A' || aChar > 'F' && aChar < 'a' || aChar > 'f')) { return; } boolean origInserting = isInserting; if (getCaretPos() == content.length() && !isInserting) { // ensureCaretIsVisible(); // redrawTextAreas(false); // return; isInserting = true; } try { handleSelectedPreModify(); try { if (isInserting) { if (event.widget == previewText) { content.insert((byte) aChar, getCaretPos()); } else if (upANibble == 0) { content.insert((byte) (hexToNibble[aChar - '0'] << 4), getCaretPos()); } else { content.overwrite(hexToNibble[aChar - '0'], 4, 4, getCaretPos()); } } else { if (event.widget == previewText) { content.overwrite((byte) aChar, getCaretPos()); } else { content.overwrite(hexToNibble[aChar - '0'], upANibble * 4, 4, getCaretPos()); } content.get(ByteBuffer.wrap(tmpRawBuffer, 0, 1), null, getCaretPos()); int offset = (int) (getCaretPos() - textAreasStart); hexText.replaceTextRange(offset * 3, 2, GeneralUtils.byteToHex[tmpRawBuffer[0] & 0x0ff]); hexText.setStyleRange(new StyleRange(offset * 3, 2, COLOR_BLUE, null)); previewText.replaceTextRange( offset, 1, Character.toString(byteToChar[tmpRawBuffer[0] & 0x0ff])); previewText.setStyleRange(new StyleRange(offset, 1, COLOR_BLUE, null)); } } catch (IOException e) { log.warn(e); } startPosition = endPosition = incrementPosWithinLimits(getCaretPos(), event.widget == hexText); Runnable delayed = new Runnable() { @Override public void run() { ensureCaretIsVisible(); redrawTextAreas(false); if (isInserting) { updateScrollBar(); redrawTextAreas(true); } refreshSelections(); runnableEnd(); } }; runnableAdd(delayed); notifyListeners(SWT.Modify, null); notifyLongSelectionListeners(); } finally { isInserting = origInserting; } } private long doNavigateKeyPressed(boolean ctrlKey, int keyCode, long oldPos, boolean countNibbles) { if (!countNibbles) upANibble = 0; switch (keyCode) { case SWT.ARROW_UP: if (oldPos >= bytesPerLine) oldPos -= bytesPerLine; break; case SWT.ARROW_DOWN: if (oldPos <= content.length() - bytesPerLine) oldPos += bytesPerLine; if (countNibbles && oldPos == content.length()) upANibble = 0; break; case SWT.ARROW_LEFT: if (countNibbles && (oldPos > 0 || oldPos == 0 && upANibble > 0)) { if (upANibble == 0) --oldPos; upANibble ^= 1; // 1->0, 0->1 } if (!countNibbles && oldPos > 0) --oldPos; break; case SWT.ARROW_RIGHT: oldPos = incrementPosWithinLimits(oldPos, countNibbles); break; case SWT.END: if (ctrlKey) { oldPos = content.length(); } else { oldPos = oldPos - oldPos % bytesPerLine + bytesPerLine - 1L; if (oldPos >= content.length()) oldPos = content.length(); } upANibble = 0; if (countNibbles && oldPos < content.length()) upANibble = 1; break; case SWT.HOME: if (ctrlKey) { oldPos = 0; } else { oldPos = oldPos - oldPos % bytesPerLine; } upANibble = 0; break; case SWT.PAGE_UP: if (oldPos >= bytesPerLine) { oldPos = oldPos - bytesPerLine * numberOfLines_1; if (oldPos < 0L) oldPos = (oldPos + bytesPerLine * numberOfLines_1) % bytesPerLine; } break; case SWT.PAGE_DOWN: if (oldPos <= content.length() - bytesPerLine) { oldPos = oldPos + bytesPerLine * numberOfLines_1; if (oldPos > content.length()) oldPos = oldPos - ((oldPos - 1 - content.length()) / bytesPerLine + 1) * bytesPerLine; } if (countNibbles && oldPos == content.length()) upANibble = 0; break; } return oldPos; } void drawUnfocusedCaret(boolean visible) { if (hexText.isDisposed()) return; GC unfocusedGC; Caret unfocusedCaret; int chars = 0; int shift = 0; if (lastFocusedTextArea == 1) { unfocusedCaret = previewText.getCaret(); unfocusedGC = styledText2GC; } else { unfocusedCaret = hexText.getCaret(); unfocusedGC = styledText1GC; chars = 1; if (hexText.getCaretOffset() % 3 == 1) shift = -1; } if (unfocusedCaret.getVisible()) { Rectangle unfocused = unfocusedCaret.getBounds(); unfocusedGC.setForeground(visible ? COLOR_NORMAL_SHADOW : colorCaretLine); unfocusedGC.drawRectangle(unfocused.x + shift * unfocused.width, unfocused.y, unfocused.width << chars, unfocused.height - 1); } } void ensureCaretIsVisible() { long caretPos = getCaretPos(); long posInLine = caretPos % bytesPerLine; if (textAreasStart > caretPos) { textAreasStart = caretPos - posInLine; } else if (textAreasStart + bytesPerLine * numberOfLines < caretPos || textAreasStart + bytesPerLine * numberOfLines == caretPos && caretPos != content.length()) { textAreasStart = caretPos - posInLine - bytesPerLine * numberOfLines_1; if (caretPos == content.length() && posInLine == 0) textAreasStart = caretPos - bytesPerLine * numberOfLines; if (textAreasStart < 0L) textAreasStart = 0L; } else { return; } getVerticalBar().setSelection((int) ((textAreasStart / bytesPerLine) >>> verticalBarFactor)); } private void ensureWholeScreenIsVisible() { if (textAreasStart + bytesPerLine * numberOfLines > content.length()) textAreasStart = content.length() - (content.length() - 1L) % bytesPerLine - 1L - bytesPerLine * numberOfLines_1; if (textAreasStart < 0L) textAreasStart = 0L; } /** * Performs a find on the text and sets the selection accordingly. * The find starts at the current caret position. * * @param findString the literal to find * @param isHexString consider the literal as an hex string (ie. "0fdA1"). Used for binary finds. * Will search full bytes only, odd number of hex characters will have a leading '0' added. * @param searchForward look for matches after current position * @param ignoreCase match upper case with lower case characters * @return whether a match was found */ public boolean findAndSelect(String findString, boolean isHexString, boolean searchForward, boolean ignoreCase) throws IOException { return findAndSelectInternal(findString, isHexString, searchForward, ignoreCase, true); } private boolean findAndSelectInternal(String findString, boolean isHexString, boolean searchForward, boolean ignoreCase, boolean updateGui) throws IOException { if (findString == null) return true; initFinder(findString, isHexString, searchForward, ignoreCase); final Object[] result = new Object[2]; HexManager.blockUntilFinished(new Runnable() { @Override public void run() { try { result[0] = finder.getNextMatch(); } catch (IOException e) { result[1] = e; } } }); if (result[1] != null) { throw (IOException) result[1]; } Object[] vector = (Object[]) result[0]; if (vector != null && vector.length > 1 && vector[0] != null && vector[1] != null) { startPosition = (Long) vector[0]; caretStickToStart = false; if (updateGui) { setSelection(startPosition, startPosition + (Integer) vector[1]); } else { select(startPosition, startPosition + (Integer) vector[1]); } previousFindEnd = getCaretPos(); return true; } return false; } /** * Get caret position in file, which can be out of view * * @return the current caret position */ public long getCaretPos() { if (caretStickToStart) return startPosition; else return endPosition; } public byte getActualValue() { return getValue(getCaretPos()); } public byte getValue(long pos) { try { content.get(ByteBuffer.wrap(tmpRawBuffer, 0, 1), null, pos); } catch (IOException e) { log.warn(e); } return tmpRawBuffer[0]; } /** * Get the binary content * * @return the content being edited */ public BinaryContent getContent() { return content; } private void getHighlightRangesInScreen(long start, int length) { highlightRangesInScreen.clear(); if (lastLocationPosition >= start && lastLocationPosition < start + length) { highlightRangesInScreen.add((int) (lastLocationPosition - textAreasStart)); highlightRangesInScreen.add(1); } } /** * Gets the selection start and end points as long values * * @return 2 elements long array, first one the start point (inclusive), second one the end point * (exclusive) */ public long[] getSelection() { return new long[]{startPosition, endPosition}; } public boolean isSelected() { return (startPosition != endPosition); } boolean handleSelectedPreModify() { if (startPosition == endPosition || !isInserting) return false; content.delete(startPosition, endPosition - startPosition); endPosition = startPosition; return true; } long incrementPosWithinLimits(long oldPos, boolean countNibbles) { if (oldPos < content.length()) if (countNibbles) { if (upANibble > 0) ++oldPos; upANibble ^= 1; // 1->0, 0->1 } else { ++oldPos; } return oldPos; } private void initFinder(String findString, boolean isHexString, boolean searchForward, boolean ignoreCase) { if (!searchForward) caretStickToStart = true; if (finder == null || !findString.equals(previousFindString) || isHexString != previousFindStringWasHex || ignoreCase != previousFindIgnoredCase) { previousFindString = findString; previousFindStringWasHex = isHexString; previousFindIgnoredCase = ignoreCase; if (isHexString) { finder = new BinaryTextFinder(hexStringToByte(findString), content); } else { finder = new BinaryTextFinder(findString, content); if (ignoreCase) finder.setCaseSensitive(false); } finder.setNewStart(getCaretPos()); } if (previousFindEnd != getCaretPos()) { finder.setNewStart(getCaretPos()); } finder.setDirectionForward(searchForward); } /** * Tells whether the input is in overwrite or insert mode * * @return true: overwriting, false: inserting */ public boolean isOverwriteMode() { return !isInserting; } void makeFirstRowSameHeight() { ((GridData) linesTextSeparator.getLayoutData()).heightHint = hexHeaderText.computeSize(SWT.DEFAULT, SWT.DEFAULT).y; ((GridData) previewTextSeparator.getLayoutData()).heightHint = hexHeaderText.computeSize(SWT.DEFAULT, SWT.DEFAULT).y; } /** * Merge ranges of changes in file with ranges of highlighted elements. * Finds lowest range border, finds next lowest range border. That's the first result. Keeps going * until last range border. * * @return list of StyleRanges, each with a style of type 'changed', 'highlighted', or both. */ List<StyleRange> mergeRanges(List<Long> changeRanges, List<Integer> highlightRanges) { if (!mergerInit(changeRanges, highlightRanges)) { return null; } List<StyleRange> result = new ArrayList<>(); mergerNext(); int start = mergeRangesPosition; boolean blue = mergeRangesIsBlue; boolean highlight = mergeRangesIsHighlight; while (mergerNext()) { if (blue || highlight) { result.add(new StyleRange(start, mergeRangesPosition - start, blue ? COLOR_BLUE : null, highlight ? colorHighlight : null)); } start = mergeRangesPosition; blue = mergeRangesIsBlue; highlight = mergeRangesIsHighlight; } return result; } boolean mergerCatchUps() { boolean withinRange = false; if (mergeChangeRanges != null && mergeChangeRanges.size() > mergeIndexChange) { withinRange = true; if (mergerPosition(true) < mergeRangesPosition) { ++mergeIndexChange; } } if (mergeHighlightRanges != null && mergeHighlightRanges.size() > mergeIndexHighlight) { withinRange = true; if (mergerPosition(false) < mergeRangesPosition) { ++mergeIndexHighlight; } } return withinRange; } /** * Initialise merger variables * * @return whether the parameters hold any data */ boolean mergerInit(List<Long> changeRanges, List<Integer> highlightRanges) { if ((changeRanges == null || changeRanges.size() < 2) && (highlightRanges == null || highlightRanges.size() < 2)) { return false; } this.mergeChangeRanges = changeRanges; this.mergeHighlightRanges = highlightRanges; mergeRangesIsBlue = false; mergeRangesIsHighlight = false; mergeRangesPosition = -1; mergeIndexChange = 0; mergeIndexHighlight = 0; return true; } int mergerMinimumInChangesHighlights() { int change = Integer.MAX_VALUE; if (mergeChangeRanges != null && mergeChangeRanges.size() > mergeIndexChange) { change = mergerPosition(true); } int highlight = Integer.MAX_VALUE; if (mergeHighlightRanges != null && mergeHighlightRanges.size() > mergeIndexHighlight) { highlight = mergerPosition(false); } int result = Math.min(change, highlight); if (change == result) { mergeRangesIsBlue = (mergeIndexChange & 1) == 0; } if (highlight == result) { mergeRangesIsHighlight = (mergeIndexHighlight & 1) == 0; } return result; } boolean mergerNext() { ++mergeRangesPosition; if (!mergerCatchUps()) { return false; } mergeRangesPosition = mergerMinimumInChangesHighlights(); return true; } int mergerPosition(boolean changesNotHighlights) { int result; if (changesNotHighlights) { result = (int) (mergeChangeRanges.get(mergeIndexChange & 0xfffffffe) - textAreasStart); if ((mergeIndexChange & 1) == 1) { result = (int) Math.min(bytesPerLine * numberOfLines, result + mergeChangeRanges.get(mergeIndexChange)); } } else { result = mergeHighlightRanges.get(mergeIndexHighlight & 0xfffffffe); if ((mergeIndexHighlight & 1) == 1) { result += mergeHighlightRanges.get(mergeIndexHighlight); } } return result; } void notifyLongSelectionListeners() { if (longSelectionListeners.isEmpty()) return; Event basicEvent = new Event(); basicEvent.widget = this; SelectionEvent anEvent = new SelectionEvent(basicEvent); anEvent.width = (int) (startPosition >>> 32); anEvent.x = (int) startPosition; anEvent.height = (int) (endPosition >>> 32); anEvent.y = (int) endPosition; for (SelectionListener aListener : longSelectionListeners) { aListener.widgetSelected(anEvent); } } /** * Pastes the clipboard content. The result depends on which insertion mode is currently active: * Insert mode replaces the selection with the DND.CLIPBOARD clipboard contents or, if there is no * selection, inserts at the current caret offset. * Overwrite mode replaces contents at the current caret offset, unless pasting would overflow the * content length, in which case does nothing. */ public void paste() { if (!myClipboard.hasContents()) return; handleSelectedPreModify(); long caretPos = getCaretPos(); long total = myClipboard.getContents(content, caretPos, isInserting); startPosition = caretPos; endPosition = caretPos + total; caretStickToStart = false; redrawTextAreas(true); restoreStateAfterModify(); } /** * Redoes the last undone action */ public void redo() { undo(false); } void redrawTextAreas(int mode, StringBuilder newText, StringBuilder resultHex, StringBuilder resultChar, List<StyleRange> viewRanges) { hexText.getCaret().setVisible(false); previewText.getCaret().setVisible(false); if (mode == SET_TEXT) { linesText.getContent().setText(newText.toString()); hexText.getContent().setText(resultHex.toString()); previewText.getContent().setText(resultChar.toString()); previousLine = -1; } else { boolean forward = mode == SHIFT_FORWARD; linesText.setRedraw(false); hexText.setRedraw(false); previewText.setRedraw(false); ((DisplayedContent) linesText.getContent()).shiftLines(newText.toString(), forward); ((DisplayedContent) hexText.getContent()).shiftLines(resultHex.toString(), forward); ((DisplayedContent) previewText.getContent()).shiftLines(resultChar.toString(), forward); linesText.setRedraw(true); hexText.setRedraw(true); previewText.setRedraw(true); if (previousLine >= 0 && previousLine < numberOfLines) previousLine += newText.length() / charsForAddress * (forward ? 1 : -1); if (previousLine < -1 || previousLine >= numberOfLines) previousLine = -1; } if (viewRanges != null) { for (StyleRange styleRange : viewRanges) { previewText.setStyleRange(styleRange); styleRange = (StyleRange) styleRange.clone(); styleRange.start *= 3; styleRange.length *= 3; hexText.setStyleRange(styleRange); } } } void redrawTextAreas(boolean fromScratch) { if (content == null || hexText.isDisposed()) return; long newLinesStart = textAreasStart; int linesShifted = numberOfLines; int mode = SET_TEXT; if (!fromScratch && previousRedrawStart >= 0L) { long lines = (textAreasStart - previousRedrawStart) / bytesPerLine; if (Math.abs(lines) < numberOfLines) { mode = lines > 0L ? SHIFT_BACKWARD : SHIFT_FORWARD; linesShifted = Math.abs((int) lines); if (linesShifted < 1) { refreshSelections(); refreshCaretsPosition(); return; } if (mode == SHIFT_BACKWARD) newLinesStart = textAreasStart + (numberOfLines - (int) lines) * bytesPerLine; } } previousRedrawStart = textAreasStart; StringBuilder newText = cookAddresses(newLinesStart, linesShifted * bytesPerLine); List<Long> changeRanges = new ArrayList<>(); int actuallyRead; try { actuallyRead = content.get(ByteBuffer.wrap(tmpRawBuffer, 0, linesShifted * bytesPerLine), changeRanges, newLinesStart); } catch (IOException e) { actuallyRead = 0; } StringBuilder resultHex = cookTexts(true, actuallyRead); StringBuilder resultChar = cookTexts(false, actuallyRead); getHighlightRangesInScreen(newLinesStart, linesShifted * bytesPerLine); List<StyleRange> viewRanges = mergeRanges(changeRanges, highlightRangesInScreen); redrawTextAreas(mode, newText, resultHex, resultChar, viewRanges); refreshSelections(); refreshCaretsPosition(); } private void refreshCaretsPosition() { drawUnfocusedCaret(false); long caretLocation = getCaretPos() - textAreasStart; if (caretLocation >= 0L && caretLocation < bytesPerLine * numberOfLines || getCaretPos() == content.length() && caretLocation == bytesPerLine * numberOfLines) { int tmp = (int) caretLocation; if (tmp == bytesPerLine * numberOfLines) { hexText.setCaretOffset(tmp * 3 - 1); previewText.setCaretOffset(tmp); } else { hexText.setCaretOffset(tmp * 3 + upANibble); previewText.setCaretOffset(tmp); } int line = hexText.getLineAtOffset(hexText.getCaretOffset()); if (line != previousLine) { if (previousLine >= 0 && previousLine < numberOfLines) { hexText.setLineBackground(previousLine, 1, null); previewText.setLineBackground(previousLine, 1, null); } hexText.setLineBackground(line, 1, colorCaretLine); previewText.setLineBackground(line, 1, colorCaretLine); previousLine = line; } hexText.getCaret().setVisible(true); previewText.getCaret().setVisible(true); DBeaverUI.asyncExec(new Runnable() { @Override public void run() { drawUnfocusedCaret(true); } }); } else { hexText.getCaret().setVisible(false); previewText.getCaret().setVisible(false); } } void refreshHeader() { hexHeaderText.setText(headerRow.substring(0, Math.min(bytesPerLine * 3, headerRow.length()))); } void refreshSelections() { if (startPosition >= endPosition || startPosition > textAreasStart + bytesPerLine * numberOfLines || endPosition <= textAreasStart) return; long startLocation = startPosition - textAreasStart; if (startLocation < 0L) startLocation = 0L; int intStart = (int) startLocation; long endLocation = endPosition - textAreasStart; if (endLocation > bytesPerLine * numberOfLines) endLocation = bytesPerLine * numberOfLines; int intEnd = (int) endLocation; if (caretStickToStart) { int tmp = intStart; intStart = intEnd; intEnd = tmp; } hexText.setSelection(intStart * 3, intEnd * 3); hexText.setTopIndex(0); previewText.setSelection(intStart, intEnd); previewText.setTopIndex(0); } /** * Replaces the selection. The result depends on which insertion mode is currently active: * Insert mode replaces the selection with the replaceString or, if there is no selection, inserts * at the current caret offset. * Overwrite mode replaces contents at the current selection start. * * @param replaceString the new string * @param isHexString consider the literal as an hex string (ie. "0fdA1"). Used for binary finds. * Will replace full bytes only, odd number of hex characters will have a leading '0' added. */ public void replace(String replaceString, boolean isHexString) { handleSelectedPreModify(); byte[] replaceData = replaceString.getBytes(Charset.defaultCharset()); if (isHexString) { replaceData = hexStringToByte(replaceString); } ByteBuffer newSelection = ByteBuffer.wrap(replaceData); if (isInserting) { content.insert(newSelection, startPosition); } else { newSelection.limit((int) Math.min(newSelection.limit(), content.length() - startPosition)); content.overwrite(newSelection, startPosition); } endPosition = startPosition + newSelection.limit() - newSelection.position(); caretStickToStart = false; redrawTextAreas(true); restoreStateAfterModify(); } /** * Replaces all occurrences of findString with replaceString. * The find starts at the current caret position. * * @param findString the literal to find * @param isFindHexString consider the literal as an hex string (ie. "0fdA1"). Used for binary finds. * Will search full bytes only, odd number of hex characters will have a leading '0' added. * @param searchForward look for matches after current position * @param ignoreCase match upper case with lower case characters * @param replaceString the new string * @param isReplaceHexString consider the literal as an hex string (ie. "0fdA1"). Used for binary * finds. Will replace full bytes only, odd number of hex characters will have a leading '0' added. * @return number of replacements */ public int replaceAll(String findString, boolean isFindHexString, boolean searchForward, boolean ignoreCase, String replaceString, boolean isReplaceHexString) throws IOException { int result = 0; stopSearching = false; while (!stopSearching && findAndSelectInternal(findString, isFindHexString, searchForward, ignoreCase, false)) { ++result; replace(replaceString, isReplaceHexString); } if (result > 0) { setSelection(getSelection()[0], getSelection()[1]); } return result; } void restoreStateAfterModify() { ensureCaretIsVisible(); redrawTextAreas(true); updateScrollBar(); notifyListeners(SWT.Modify, null); notifyLongSelectionListeners(); } void runnableAdd(Runnable delayed) { if (delayedInQueue) { delayedWaiting = delayed; } else { delayedInQueue = true; DBeaverUI.asyncExec(delayed); } } void runnableEnd() { if (delayedWaiting != null) { DBeaverUI.asyncExec(delayedWaiting); delayedWaiting = null; } else { delayedInQueue = false; } } /** * Sets the selection to the entire text. Caret remains either at the selection start or end */ public void selectAll() { select(0L, content.length()); refreshSelections(); } /** * Sets the selection from start to end. */ public void selectBlock(long start, long end) { select(start, end); refreshSelections(); showMark(start); } void select(long start, long end) { upANibble = 0; boolean selection = startPosition != endPosition; startPosition = 0L; if (start > 0L) { startPosition = start; if (startPosition > content.length()) startPosition = content.length(); } endPosition = startPosition; if (end > startPosition) { endPosition = end; if (endPosition > content.length()) endPosition = content.length(); } notifyLongSelectionListeners(); } void setAddressesGridDataWidthHint() { ((GridData) linesText.getLayoutData()).widthHint = charsForAddress * fontCharWidth; } void setCaretsSize(boolean insert) { isInserting = insert; int width = 0; int height = hexText.getCaret().getSize().y; if (!isInserting) width = fontCharWidth; hexText.getCaret().setSize(width, height); previewText.getCaret().setSize(width, height); } /** * Sets the content to be displayed. Replacing an existing content keeps the display area in the * same position, but only if it falls within the new content's limits. * * @param aContent the content to be displayed */ public void setContentProvider(BinaryContent aContent) { if (isDisposed()) { return; } boolean firstContent = content == null; if (!firstContent) { content.dispose(); } content = aContent; finder = null; if (content != null) { content.setActionsHistory(); if (firstContent || endPosition > content.length() || textAreasStart >= content.length()) { textAreasStart = startPosition = endPosition = 0L; caretStickToStart = false; } charsForFileSizeAddress = Long.toHexString(content.length()).length(); } updateScrollBar(); redrawTextAreas(true); notifyLongSelectionListeners(); notifyListeners(SWT.Modify, null); } public void setContent(byte[] data, String charset) { BinaryContent binaryContent = new BinaryContent(); if (charset != null) { setCharset(charset); } ByteBuffer byteBuffer = ByteBuffer.wrap(data); binaryContent.insert(byteBuffer, 0); setContentProvider(binaryContent); } /** * Causes the receiver to have the keyboard focus. Within Eclipse, never call setFocus() before * the workbench has called EditorActionBarContributor.setActiveEditor() * * @see Composite#setFocus() */ @Override public boolean setFocus() { redrawCaret(false); if (lastFocusedTextArea == 1) return hexText.setFocus(); else return previewText.setFocus(); } /** * @throws IllegalArgumentException if font height is 1 or 2 * @see Control#setFont(org.eclipse.swt.graphics.Font) * Font height must not be 1 or 2. */ @Override public void setFont(Font font) { // bugfix: HexText's raw array overflows when font is very small and window very big // very small sizes would compromise responsiveness in large windows, and they are too small // to see anyway if (font != null) { int newSize = UIUtils.getFontHeight(font); if (newSize == 1 || newSize == 2) throw new IllegalArgumentException("Font size is " + newSize + ", too small"); } fontCurrent = font; if (fontCurrent == null) { fontCurrent = fontDefault; } super.setFont(fontCurrent); hexHeaderText.setFont(fontCurrent); hexHeaderText.pack(true); GC gc = new GC(hexHeaderText); fontCharWidth = gc.getFontMetrics().getAverageCharWidth(); gc.dispose(); makeFirstRowSameHeight(); linesText.setFont(fontCurrent); setAddressesGridDataWidthHint(); linesText.pack(true); hexText.setFont(fontCurrent); hexText.pack(true); previewText.setFont(fontCurrent); previewText.pack(true); updateTextsMetrics(); layout(); setCaretsSize(isInserting); } /** * Sets the selection. The caret may change position but stays at the same selection point (if it * was at the start of the selection it will move to the new start, otherwise to the new end point). * The new selection is made visible * * @param start inclusive start selection char * @param end exclusive end selection char */ public void setSelection(long start, long end) { select(start, end); ensureCaretIsVisible(); redrawTextAreas(false); } void shiftStartAndEnd(long newPos) { if (caretStickToStart) { startPosition = Math.min(newPos, endPosition); endPosition = Math.max(newPos, endPosition); } else { endPosition = Math.max(newPos, startPosition); startPosition = Math.min(newPos, startPosition); } caretStickToStart = endPosition != newPos; } /** * Shows the position on screen. * * @param position where relocation should go */ public void showMark(long position) { lastLocationPosition = position; if (position < 0) return; position = position - position % bytesPerLine; textAreasStart = position; if (numberOfLines > 2) textAreasStart = position - (numberOfLines / 2) * bytesPerLine; ensureWholeScreenIsVisible(); redrawTextAreas(true); // setFocus(); updateScrollBar(); } /** * Stop findAndSelect() or replaceAll() calls. Long running searches can be stopped from another * thread. */ public void stopSearching() { stopSearching = true; if (finder != null) { finder.stopSearching(); } } long totalNumberOfLines() { long result = 1L; if (content != null) { if (bytesPerLine > 0) { result = (content.length() - 1L) / bytesPerLine + 1L; } } return result; } /** * Undoes the last action */ public void undo() { undo(true); } void undo(boolean previousAction) { long[] selection = previousAction ? content.undo() : content.redo(); if (selection == null) return; upANibble = 0; startPosition = selection[0]; endPosition = selection[1]; caretStickToStart = false; ensureWholeScreenIsVisible(); restoreStateAfterModify(); } void updateNumberOfLines() { int height = getClientArea().height - hexHeaderText.computeSize(SWT.DEFAULT, SWT.DEFAULT, false).y; numberOfLines = height / linesText.getLineHeight(); if (numberOfLines < 1) numberOfLines = 1; numberOfLines_1 = numberOfLines - 1; ((DisplayedContent) linesText.getContent()).setDimensions(charsForAddress, numberOfLines); ((DisplayedContent) hexText.getContent()).setDimensions(bytesPerLine * 3, numberOfLines); ((DisplayedContent) previewText.getContent()).setDimensions(bytesPerLine, numberOfLines); } private void updateScrollBar() { ScrollBar vertical = getVerticalBar(); long max = totalNumberOfLines(); verticalBarFactor = 0; while (max > Integer.MAX_VALUE) { max >>>= 1; ++verticalBarFactor; } vertical.setMaximum((int) max); vertical.setSelection((int) ((textAreasStart / bytesPerLine) >>> verticalBarFactor)); vertical.setPageIncrement(numberOfLines_1); vertical.setThumb(numberOfLines); } void updateTextsMetrics() { int width = getClientArea().width - linesText.computeSize(SWT.DEFAULT, SWT.DEFAULT).x; int displayedNumberWidth = fontCharWidth * 4; // hexText and previewText bytesPerLine = (width / displayedNumberWidth) & 0xfffffff8; // 0, 8, 16, 24, etc. if (bytesPerLine <= 8) { bytesPerLine = 8; } // if (bytesPerLine < 16) // bytesPerLine = 16; textGridData.widthHint = hexText.computeTrim(0, 0, bytesPerLine * 3 * fontCharWidth, 100).width; previewGridData.widthHint = previewText.computeTrim(0, 0, bytesPerLine * fontCharWidth, 100).width; updateNumberOfLines(); changed(new Control[]{hexHeaderText, linesText, hexText, previewText}); updateScrollBar(); refreshHeader(); textAreasStart = (((long) getVerticalBar().getSelection()) * bytesPerLine) << verticalBarFactor; redrawTextAreas(true); } }