wudsn-ide/com.wudsn.ide.hex/src/com/wudsn/ide/hex/HexEditor.java
2022-08-29 12:15:35 +02:00

630 lines
20 KiB
Java

/**
* Copyright (C) 2009 - 2021 <a href="https://www.wudsn.com" target="_top">Peter Dell</a>
*
* This file is part of WUDSN IDE.
*
* WUDSN IDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* WUDSN IDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with WUDSN IDE. If not, see <http://www.gnu.org/licenses/>.
*/
package com.wudsn.ide.hex;
import java.io.File;
import java.util.Iterator;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorSite;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IPathEditorInput;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.part.EditorPart;
import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
import com.wudsn.ide.base.BasePlugin;
import com.wudsn.ide.base.common.ByteArrayUtility;
import com.wudsn.ide.base.common.FileUtility;
import com.wudsn.ide.base.common.HexUtility;
import com.wudsn.ide.base.common.MessageQueue;
import com.wudsn.ide.base.common.NumberUtility;
import com.wudsn.ide.base.common.Profiler;
import com.wudsn.ide.base.common.TextUtility;
import com.wudsn.ide.base.gui.Action;
import com.wudsn.ide.base.gui.Application;
import com.wudsn.ide.base.gui.EnumField;
import com.wudsn.ide.base.gui.IntegerField;
import com.wudsn.ide.base.gui.MessageManager;
import com.wudsn.ide.base.gui.SWTFactory;
import com.wudsn.ide.base.gui.TextField;
import com.wudsn.ide.base.hardware.HardwareCharacterSet;
/**
* The Hex Editor. This editor offset and outline view for the block of the file
* and a context menu for copying the current selection clip board in different
* formats.
*
* TODO Complete copy & paste, complete documentation
*
* @author Peter Dell
*/
public final class HexEditor extends EditorPart implements ISelectionProvider, Application {
private static final String LABEL_SUFFIX = ": ";
public final class MessageIds {
/**
* Creation is private.
*/
private MessageIds() {
}
public static final int FILE_CONTENT_MODE = 1;
public static final int CHARACTER_SET = 2;
public static final int BYTES_PER_LINE = 3;
}
public final class Actions {
/**
* Creation is private.
*/
private Actions() {
}
public static final int FILE_CONTENT_MODE_CHANGED = 1000;
public static final int CHARACTER_SET_TYPE_CHANGED = 1001;
public static final int BYTES_PER_ROW_CHANGED = 1002;
}
public static final String ID = "com.wudsn.ide.hex.HexEditor";
private static final String CONTEXT_MENU_ID = "#HexEditorContext";
private static final long MAX_FILE_SIZE = 8 * ByteArrayUtility.MB;
private MessageQueue messageQueue;
private MessageManager messageManager;
private HexEditorParserComponent parserComponent;
// Editor content outline page.
private HexEditorContentOutlinePage contentOutlinePage;
// Editor header area.
private TextField fileContentSizeField;
private EnumField<HexEditorFileContentMode> fileContentModeField;
private EnumField<HardwareCharacterSet> characterSetField;
private IntegerField bytesPerRowField;
private StyledText textField;
private HexEditorSelection lastEditorSelection;
// File source.
private IFile iFile;
private File ioFile;
/**
* This main method is for testing the speed of the source file parser component
* only.
*
* @param args Not used, not <code>null</code>.
* @throws Exception If anything goes terribly wrong.
*/
public static void main(String[] args) throws Exception {
System.out.print("Testing HexEditorParserComponent: Parsing took ");
// Initialize for stand alone usage.
new BasePlugin().start(null);
HexEditorParserComponent parser = new HexEditorParserComponent(
new MessageManager(new MessageQueue(), new HexEditor()));
parser.setFileContent(new byte[100000]);
parser.determinePossibleFileContentModes();
long startTimeMillis = System.currentTimeMillis();
parser.setFileContentMode(HexEditorFileContentMode.BINARY);
parser.parseFileContent();
long duration = System.currentTimeMillis() - startTimeMillis;
System.out.println(duration + " ms");
System.exit(0);
}
/**
* Creation is public. Called by extension point "org.eclipse.ui.popupMenus".
*/
public HexEditor() {
super();
messageQueue = new MessageQueue();
messageManager = new MessageManager(messageQueue, this);
parserComponent = new HexEditorParserComponent(messageManager);
}
/**
* @see org.eclipse.ui.IEditorPart#init(IEditorSite, IEditorInput)
*/
@Override
public void init(IEditorSite site, IEditorInput input) throws PartInitException {
setSite(site);
setInput(input);
try {
load();
} catch (CoreException ex) {
BasePlugin.getInstance().showError(site.getShell(), ex.getMessage(), ex);
}
}
/**
* @see org.eclipse.ui.IWorkbenchPart#createPartControl(Composite)
*/
@Override
public void createPartControl(Composite parent) {
getSite().setSelectionProvider(this);
GridLayout gridLayout = new GridLayout(1, true);
gridLayout.marginWidth = 0;
parent.setLayout(gridLayout);
GridData gd = new GridData(GridData.VERTICAL_ALIGN_BEGINNING);
gd.horizontalSpan = 1;
parent.setLayoutData(gd);
Composite header = SWTFactory.createComposite(parent, 8, 1, GridData.FILL_HORIZONTAL);
FillLayout fillLayout = new FillLayout(SWT.HORIZONTAL);
fillLayout.marginWidth = 10;
header.setLayout(fillLayout);
fileContentSizeField = new TextField(header, Texts.HEX_EDITOR_FILE_CONTENT_SIZE_FIELD_LABEL + LABEL_SUFFIX,
SWT.READ_ONLY);
fileContentSizeField.getLabel().setAlignment(SWT.RIGHT);
fileContentModeField = new EnumField<HexEditorFileContentMode>(header,
Texts.HEX_EDITOR_FILE_CONTENT_MODE_FIELD_LABEL + LABEL_SUFFIX, HexEditorFileContentMode.class, null);
fileContentModeField.getLabel().setAlignment(SWT.RIGHT);
messageManager.registerField(fileContentModeField, MessageIds.FILE_CONTENT_MODE);
fileContentModeField.addSelectionAction(new Action(Actions.FILE_CONTENT_MODE_CHANGED, this));
characterSetField = new EnumField<HardwareCharacterSet>(header,
Texts.HEX_EDITOR_CHARACTER_SET_TYPE_FIELD_LABEL + LABEL_SUFFIX, HardwareCharacterSet.class, null);
characterSetField.getLabel().setAlignment(SWT.RIGHT);
messageManager.registerField(characterSetField, MessageIds.CHARACTER_SET);
characterSetField.addSelectionAction(new Action(Actions.CHARACTER_SET_TYPE_CHANGED, this));
bytesPerRowField = new IntegerField(header, Texts.HEX_EDITOR_BYTES_PER_ROW_FIELD_LABEL + LABEL_SUFFIX, null,
false, 1, SWT.NONE);
bytesPerRowField.getLabel().setAlignment(SWT.RIGHT);
messageManager.registerField(characterSetField, MessageIds.BYTES_PER_LINE);
bytesPerRowField.getControl().addKeyListener(new KeyListener() {
@Override
public void keyReleased(KeyEvent e) {
if (e.keyCode == '\r') {
performAction(new Action(Actions.BYTES_PER_ROW_CHANGED, HexEditor.this));
}
}
@Override
public void keyPressed(KeyEvent e) {
}
});
bytesPerRowField.getControl().addFocusListener(new FocusListener() {
@Override
public void focusLost(FocusEvent e) {
performAction(new Action(Actions.BYTES_PER_ROW_CHANGED, HexEditor.this));
}
@Override
public void focusGained(FocusEvent e) {
}
});
// SWT.WRAP is very slow, so it's not used.
textField = new StyledText(parent, SWT.SCROLL_LINE | SWT.V_SCROLL | SWT.H_SCROLL | SWT.READ_ONLY);
gd = new GridData(GridData.FILL_VERTICAL | GridData.FILL_HORIZONTAL);
gd.horizontalIndent = 0;
textField.setLayoutData(gd);
textField.setIndent(10);
textField.setLineSpacing(0);
// Create a menu manager for the context menu.
MenuManager manager = new MenuManager(CONTEXT_MENU_ID, CONTEXT_MENU_ID);
manager.setRemoveAllWhenShown(true);
// Create menu and link to the field.
Menu textContextMenu = manager.createContextMenu(textField);
textField.setMenu(textContextMenu);
getEditorSite().registerContextMenu(CONTEXT_MENU_ID, manager, this, false);
messageManager.clearMessages();
dataToUi();
}
@SuppressWarnings("unchecked")
@Override
public <T> T getAdapter(Class<T> adapter) {
if (adapter != null && IContentOutlinePage.class.equals(adapter)) {
if (contentOutlinePage == null) {
contentOutlinePage = new HexEditorContentOutlinePage(this);
contentOutlinePage.setInput(parserComponent.getOutlineBlocks());
}
return (T) contentOutlinePage;
}
return super.getAdapter(adapter);
}
/**
* @see org.eclipse.ui.IWorkbenchPart#setFocus()
*/
@Override
public void setFocus() {
textField.setFocus();
}
private void load() throws CoreException {
// Clear fields.
ioFile = null;
iFile = null;
String fileName = "";
IEditorInput input = getEditorInput();
if (input instanceof IFileEditorInput) {
// Input file found in Eclipse Workspace.
iFile = ((IFileEditorInput) input).getFile();
ioFile = iFile.getRawLocation().toFile();
fileName = iFile.getName();
} else if (input instanceof IPathEditorInput) {
// Input file is outside the Eclipse Workspace
IPathEditorInput pathEditorInput = (IPathEditorInput) input;
IPath path = pathEditorInput.getPath();
ioFile = path.toFile();
fileName = ioFile.getName();
} else {
// Not supported.
}
byte[] fileContent;
Profiler profiler = new Profiler(this);
profiler.begin("readBytes", fileName);
if (ioFile != null) {
fileContent = FileUtility.readBytes(ioFile, MAX_FILE_SIZE, false);
} else if (iFile != null) {
fileContent = FileUtility.readBytes(iFile, MAX_FILE_SIZE, false);
} else {
fileContent = new byte[0];
}
profiler.end("readBytes");
// Set the content, determine the default file content mode and
// character set.
parserComponent.setFileContent(fileContent);
HexEditorFileContentMode defaultFileContentMode = parserComponent.determinePossibleFileContentModes();
HardwareCharacterSet defaultCharacterSet = defaultFileContentMode.getDefaultCharacterSet();
parserComponent.setFileContentMode(defaultFileContentMode);
parserComponent.setCharacterSet(defaultCharacterSet);
setPartName(fileName);
}
public int getBytesPerRow() {
return parserComponent.getBytesPerRow();
}
@Override
public void addSelectionChangedListener(ISelectionChangedListener listener) {
// Nothing.
}
@Override
public void removeSelectionChangedListener(ISelectionChangedListener listener) {
// Nothing.
}
@Override
public HexEditorSelection getSelection() {
if (textField == null) {
return null;
}
Point textFieldSelection = textField.getSelection();
String selectedText = textField.getText().substring(textFieldSelection.x, textFieldSelection.y);
if (textFieldSelection.x == textFieldSelection.y) {
return null;
}
BasePlugin.getInstance().log("HexEditor selectedText=\"{0}\", selection.x={1}, selection.y={2}", new Object[] {
selectedText, String.valueOf(textFieldSelection.x), String.valueOf(textFieldSelection.y) });
lastEditorSelection = parserComponent.getSelection(textFieldSelection.x, textFieldSelection.y);
return lastEditorSelection;
}
@Override
public void setSelection(ISelection selection) {
// Single range selection?
if (selection instanceof HexEditorSelection) {
HexEditorSelection hexEditorSelection = (HexEditorSelection) selection;
long textStartOffset = parserComponent.getByteTextOffset(hexEditorSelection.getStartOffset());
long textEndOffset = parserComponent.getByteTextOffset(hexEditorSelection.getEndOffset());
setSelectionOffsets(textStartOffset, textEndOffset);
// Range of outline tree objects?
} else if (selection instanceof IStructuredSelection) {
IStructuredSelection structuredSelection = (IStructuredSelection) selection;
if (structuredSelection.getFirstElement() instanceof HexEditorContentOutlineTreeObject) {
Iterator<?> i = ((IStructuredSelection) selection).iterator();
long textStartOffset = Long.MAX_VALUE;
long textEndOffset = Long.MIN_VALUE;
while (i.hasNext()) {
HexEditorContentOutlineTreeObject treeObject = (HexEditorContentOutlineTreeObject) i.next();
textStartOffset = Math.min(treeObject.getTextStartOffset(), textStartOffset);
textEndOffset = Math.max(treeObject.getTextEndOffset(), textEndOffset);
}
setSelectionOffsets(textStartOffset, textEndOffset);
}
}
setFocus();
}
private void setSelectionOffsets(long textStartOffset, long textEndOffset) {
if (textStartOffset < 0 || textEndOffset > Integer.MAX_VALUE) {
throw new IllegalArgumentException("Parameter textStartOffset=" + textStartOffset + " is out of range");
}
if (textEndOffset < 0 || textEndOffset > Integer.MAX_VALUE) {
throw new IllegalArgumentException("Parameter textStartOffset=" + textStartOffset + " is out of range");
}
try {
// Mark complete selection area. This also scrolls to
// the end of the area.
textField.setSelection(new Point((int) textStartOffset, (int) textEndOffset));
lastEditorSelection = null;
//
// // But we want to see start of the selection are, so
// // position explicitly.
textField.setTopIndex(textField.getContent().getLineAtOffset((int) textStartOffset));
} catch (IllegalArgumentException x) {
// Ignore
}
}
/**
* Gets the file path for saving the current selection. Called by
* {@link HexEditorSaveSelectionAsCommandHandler }.
*
* @return The file path, not <code>null</code>.
*/
public String getSelectionSaveFilePath() {
String result = "Selection";
String extension = ".bin";
if (parserComponent.getFileContentMode().equals(HexEditorFileContentMode.ATARI_DISK_IMAGE_K_FILE)) {
extension = ".xex";
}
if (ioFile != null) {
result = ioFile.getAbsolutePath();
int index = result.lastIndexOf('.');
if (index >= 0) {
result = result.substring(0, index);
}
}
result += extension;
return result;
}
@Override
public boolean isDirty() {
return false;
}
@Override
public void doSave(IProgressMonitor monitor) {
// Nothing.
}
@Override
public boolean isSaveAsAllowed() {
return false;
}
@Override
public void doSaveAs() {
// Nothing.
}
/**
* {@inheritDoc}
*/
@Override
public MessageManager getMessageManager() {
return messageManager;
}
private void dataFromUi() {
messageManager.clearMessages();
parserComponent.setFileContentMode(fileContentModeField.getValue());
parserComponent.setCharacterSet(characterSetField.getValue());
int bytesPerRow = bytesPerRowField.getValue();
if (bytesPerRow < 1) {
bytesPerRow = 16;
} else if (bytesPerRow > 256) {
bytesPerRow = 256;
}
parserComponent.setBytesPerRow(bytesPerRow);
}
private void dataToUi() {
// File content size.
FileContent fileContent = parserComponent.getFileContent();
long length = fileContent.getLength();
String text = TextUtility.format(Texts.HEX_EDITOR_FILE_CONTENT_SIZE_FIELD_TEXT,
HexUtility.getLongValueHexString(length), NumberUtility.getLongValueDecimalString(length));
fileContentSizeField.setValue(text);
// File content mode.
fileContentModeField.setValue(parserComponent.getFileContentMode());
// Character set.
HardwareCharacterSet characterSet = parserComponent.getCharacterSet();
characterSetField.setValue(characterSet);
if (!textField.getFont().equals(characterSet.getFont())) {
textField.setFont(characterSet.getFont());
}
// Bytes per Row
bytesPerRowField.setValue(parserComponent.getBytesPerRow());
if (parserComponent.isParsingFileContentRequired()) {
StyledString styledString = parserComponent.parseFileContent();
textField.setText(styledString.getString());
lastEditorSelection = null;
textField.setStyleRanges(styledString.getStyleRanges());
if (contentOutlinePage != null) {
contentOutlinePage.setInput(parserComponent.getOutlineBlocks());
}
}
messageManager.displayMessages();
}
/**
* {@inheritDoc}
*/
@Override
public void performAction(Action action) {
try {
ISelection oldSelection = null;
dataFromUi();
switch (action.getId()) {
case Actions.FILE_CONTENT_MODE_CHANGED:
case Actions.CHARACTER_SET_TYPE_CHANGED:
case Actions.BYTES_PER_ROW_CHANGED:
oldSelection = getSelection();
break;
}
dataToUi();
if (oldSelection != null) {
setSelection(oldSelection);
}
} catch (Exception ex) {
BasePlugin.getInstance().showError(getSite().getShell(), "Error in update()", ex);
}
}
/**
* Called by {@link HexEditorClipboardCommandHandler}.
*
* @param bytes The byte array to be pasted, may be empty, not
* <code>null</code>.
*
* TODO Hex paste is not working yet
*/
final void pasteFromClipboard(byte[] bytes) {
if (bytes == null) {
throw new IllegalArgumentException("Parameter 'bytes' must not be null.");
}
HexEditorSelection selection = getSelection();
// byte[] newFileContent;
//
// // If there is no end offset, we insert the new bytes.
// if (selection.getEndOffset() !=
// HexEditorParserComponent.UNDEFINED_OFFSET) {
// int selectionStartOffset = (int) selection.getStartOffset();
// int selectionEndOffset = (int) selection.getEndOffset();
//
// int selectionLength = selectionEndOffset - selectionStartOffset + 1;
// int newFileContentLength = parserComponent.getFileContent().length -
// selectionLength + bytes.length;
// newFileContent = new byte[newFileContentLength];
// System.arraycopy(parserComponent.getFileContent(), 0, newFileContent,
// 0, selectionStartOffset);
// System.arraycopy(bytes, 0, newFileContent, selectionStartOffset,
// bytes.length);
// int length = parserComponent.getFileContent().length -
// selectionEndOffset - 1;
// if (length > 0) {
// // TODO Hex paste is not working yet
// System.arraycopy(parserComponent.getFileContent(),
// selectionEndOffset, newFileContent,
// selectionStartOffset + bytes.length, length);
// }
// messageManager.sendMessage(0, IStatus.OK,
// "${0} ({1}) bytes pasted from clipboard to replace ${2} ({3}) bytes
// ",
// HexUtility.getLongValueHexString(bytes.length),
// NumberUtility.getLongValueDecimalString(bytes.length),
// HexUtility.getLongValueHexString(selectionLength),
// NumberUtility.getLongValueDecimalString(selectionLength));
// } else {
// // If there is an end offset, we replace the selection with the new
// // bytes.
// newFileContent = parserComponent.getFileContent();
// messageManager.sendMessage(0, IStatus.OK, "${0} ({1}) bytes inserted
// from clipboard",
// HexUtility.getLongValueHexString(bytes.length),
// NumberUtility.getLongValueDecimalString(bytes.length));
// }
//
// parserComponent.setFileContent(newFileContent);
dataToUi();
}
}