Java tutorial
package de.codesourcery.jasm16.ide.ui.views; /** * Copyright 2012 Tobias Gierke <tobias.gierke@code-sourcery.de> * * 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. */ import java.awt.Color; import java.awt.Container; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Event; import java.awt.Graphics; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.MouseInfo; import java.awt.Point; import java.awt.PopupMenu; import java.awt.Rectangle; import java.awt.Shape; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.AdjustmentEvent; import java.awt.event.AdjustmentListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionAdapter; import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTextField; import javax.swing.JTextPane; import javax.swing.JToolBar; import javax.swing.JViewport; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import javax.swing.ToolTipManager; import javax.swing.event.CaretEvent; import javax.swing.event.CaretListener; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentEvent.EventType; import javax.swing.event.DocumentListener; import javax.swing.event.UndoableEditEvent; import javax.swing.event.UndoableEditListener; import javax.swing.table.AbstractTableModel; import javax.swing.text.AbstractDocument; import javax.swing.text.AbstractDocument.DefaultDocumentEvent; import javax.swing.text.AttributeSet; import javax.swing.text.BadLocationException; import javax.swing.text.DefaultHighlighter; import javax.swing.text.DefaultStyledDocument.AttributeUndoableEdit; import javax.swing.text.Document; import javax.swing.text.DocumentFilter; import javax.swing.text.Highlighter; import javax.swing.text.JTextComponent; import javax.swing.text.Position; import javax.swing.text.SimpleAttributeSet; import javax.swing.text.StyleConstants; import javax.swing.text.StyledDocument; import javax.swing.text.View; import javax.swing.undo.CannotRedoException; import javax.swing.undo.CannotUndoException; import javax.swing.undo.UndoManager; import javax.swing.undo.UndoableEdit; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import de.codesourcery.jasm16.ast.AST; import de.codesourcery.jasm16.ast.ASTNode; import de.codesourcery.jasm16.ast.CommentNode; import de.codesourcery.jasm16.ast.EndMacroNode; import de.codesourcery.jasm16.ast.IPreprocessorDirective; import de.codesourcery.jasm16.ast.IncludeSourceFileNode; import de.codesourcery.jasm16.ast.InstructionNode; import de.codesourcery.jasm16.ast.InvokeMacroNode; import de.codesourcery.jasm16.ast.LabelNode; import de.codesourcery.jasm16.ast.RegisterReferenceNode; import de.codesourcery.jasm16.ast.StartMacroNode; import de.codesourcery.jasm16.ast.StatementNode; import de.codesourcery.jasm16.ast.SymbolReferenceNode; import de.codesourcery.jasm16.compiler.CompilationListener; import de.codesourcery.jasm16.compiler.ICompilationError; import de.codesourcery.jasm16.compiler.ICompilationUnit; import de.codesourcery.jasm16.compiler.ISymbol; import de.codesourcery.jasm16.compiler.ISymbolTable; import de.codesourcery.jasm16.compiler.Severity; import de.codesourcery.jasm16.compiler.SourceLocation; import de.codesourcery.jasm16.compiler.io.AbstractResourceResolver; import de.codesourcery.jasm16.compiler.io.DefaultResourceMatcher; import de.codesourcery.jasm16.compiler.io.FileResource; import de.codesourcery.jasm16.compiler.io.FileResourceResolver; import de.codesourcery.jasm16.compiler.io.IResource; import de.codesourcery.jasm16.compiler.io.IResource.ResourceType; import de.codesourcery.jasm16.compiler.io.IResourceResolver; import de.codesourcery.jasm16.compiler.phases.ExpandMacrosPhase; import de.codesourcery.jasm16.exceptions.ResourceNotFoundException; import de.codesourcery.jasm16.ide.IAssemblyProject; import de.codesourcery.jasm16.ide.IWorkspace; import de.codesourcery.jasm16.ide.IWorkspaceListener; import de.codesourcery.jasm16.ide.NavigationHistory; import de.codesourcery.jasm16.ide.NavigationHistory.INavigationHistoryListener; import de.codesourcery.jasm16.ide.NavigationHistory.Location; import de.codesourcery.jasm16.ide.WorkspaceListener; import de.codesourcery.jasm16.ide.ui.utils.UIUtils; import de.codesourcery.jasm16.ide.ui.viewcontainers.EditorContainer; import de.codesourcery.jasm16.utils.ITextRegion; import de.codesourcery.jasm16.utils.Line; import de.codesourcery.jasm16.utils.Misc; import de.codesourcery.jasm16.utils.TextRegion; /** * Abstract base-class for views that display source code. * * <p>This class provides common functionality like * syntax-highlighing , navigation history and so forth.</p> * * @author tobias.gierke@code-sourcery.de */ public abstract class SourceCodeView extends AbstractView implements IEditorView { private static final Logger LOG = Logger.getLogger(SourceCodeView.class); // time to wait before recompiling after the user changed the source code private static final int RECOMPILATION_DELAY_MILLIS = 300; // UI widgets private volatile JPanel panel; private Object currentHighlight; private boolean registeredWithTooltipManager = false; private volatile boolean editable; private SearchDialog searchDialog; private Object currentUnderlineHighlight; private final NavigationHistory navigationHistory; private final PopupListener popupListener = new PopupListener(); private final UndoManager undoManager = new UndoManager(); private final UndoableEditListener undoListener = new UndoableEditListener() { public void undoableEditHappened(UndoableEditEvent e) { UndoableEdit edit = e.getEdit(); if (edit instanceof AttributeUndoableEdit) { return; } else if (edit instanceof DefaultDocumentEvent) { if (((DefaultDocumentEvent) edit).getType() == EventType.CHANGE) { return; } } undoManager.addEdit(e.getEdit()); undoAction.updateUndoState(); redoAction.updateRedoState(); } }; protected abstract class UndoRedoAction extends AbstractAction { public void updateUndoState() { if (undoManager.canUndo()) { setEnabled(true); putValue(Action.NAME, undoManager.getUndoPresentationName()); } else { setEnabled(false); putValue(Action.NAME, "Undo"); } } public void updateRedoState() { if (undoManager.canRedo()) { setEnabled(true); putValue(Action.NAME, undoManager.getRedoPresentationName()); } else { setEnabled(false); putValue(Action.NAME, "Redo"); } } } private final UndoRedoAction undoAction = new UndoRedoAction() { @Override public void actionPerformed(ActionEvent e) { try { undoManager.undo(); } catch (CannotUndoException ex) { LOG.error("Unable to undo: " + ex, ex); } updateUndoState(); redoAction.updateRedoState(); } }; private final UndoRedoAction redoAction = new UndoRedoAction() { @Override public void actionPerformed(ActionEvent e) { try { undoManager.redo(); } catch (CannotRedoException ex) { LOG.error("Unable to redo: " + ex, ex); } updateRedoState(); undoAction.updateUndoState(); } }; private final JTextField cursorPosition = new JTextField(); private final JTextPane editorPane = new JTextPane(); private volatile int documentListenerDisableCount = 0; private JScrollPane editorScrollPane; private final SimpleAttributeSet registerStyle; private final SimpleAttributeSet commentStyle; private final SimpleAttributeSet instructionStyle; private final SimpleAttributeSet labelStyle; private final SimpleAttributeSet preProcessorStyle; private final SimpleAttributeSet errorStyle; private final SimpleAttributeSet defaultStyle; private final SimpleAttributeSet macroDefinitionStyle; private final SimpleAttributeSet macroInvocationStyle; // compiler private final IResourceResolver resourceResolver; protected final IWorkspace workspace; private final INavigationHistoryListener navigationHistoryListener = new INavigationHistoryListener() { @Override public void navigationHistoryChanged() { UIUtils.invokeLater(new Runnable() { @Override public void run() { SourceCodeView.this.onNavigationHistoryChange(); } }); } }; private volatile boolean navigationHistoryUpdatesEnabled = true; // controls whether the CaretListener will forward caret position changes to the NavigationHistory private volatile boolean isBuilding = false; private final IWorkspaceListener workspaceListener = new WorkspaceListener() { public void projectDeleted(IAssemblyProject deletedProject) { if (deletedProject.isSame(project)) { dispose(); } } public void buildStarted(IAssemblyProject project) { if (project.isSame(getCurrentProject())) { isBuilding = true; } } public void buildFinished(IAssemblyProject project, boolean success) { if (project.isSame(getCurrentProject())) { isBuilding = false; } }; public void projectClosed(IAssemblyProject closedProject) { if (closedProject.isSame(project)) { dispose(); } } private void dispose() { if (getViewContainer() != null) { getViewContainer().disposeView(SourceCodeView.this); } else { SourceCodeView.this.dispose(); } } public void resourceDeleted(IAssemblyProject project, IResource deletedResource) { if (DefaultResourceMatcher.INSTANCE.isSame(persistentResource, deletedResource)) { dispose(); } } }; private IAssemblyProject project; private String initialHashCode; // hash code used to check whether current editor content differs from the one on disk private IResource persistentResource; // source code on disk private InMemorySourceResource sourceInMemory; // possibly edited source code (in RAM / JEditorPane) private ICompilationUnit compilationUnit; private CompilationThread compilationThread = null; protected static final class UnderlineHighlightPainter extends DefaultHighlighter.DefaultHighlightPainter { private int thickness; public UnderlineHighlightPainter(Color c, int thickness) { super(c); this.thickness = thickness; } @Override public Shape paintLayer(Graphics g, int offs0, int offs1, Shape bounds, JTextComponent c, View view) { Rectangle r; if (offs0 == view.getStartOffset() && offs1 == view.getEndOffset()) { // Contained in view, can just use bounds. if (bounds instanceof Rectangle) { r = (Rectangle) bounds; } else { r = bounds.getBounds(); } } else { // Should only render part of View. try { // --- determine locations --- Shape shape = view.modelToView(offs0, Position.Bias.Forward, offs1, Position.Bias.Backward, bounds); r = (shape instanceof Rectangle) ? (Rectangle) shape : shape.getBounds(); } catch (BadLocationException e) { // can't render r = null; } } if (r != null) { Color color = getColor(); if (color == null) { color = c.getSelectionColor(); } g.setColor(color); // If we are asked to highlight, we should draw something even // if the model-to-view projection is of zero width (6340106). r.width = Math.max(r.width, 1); g.fillRect(r.x, r.y + r.height, r.width, thickness); } return r; } } /* WAIT_FOR_EDIT-------> WAIT_FOR_TIMEOUT ------>( do compilation ) ----+ * ^ ^ | | * | | | | * | +---RESTART_TIMEOUT---+ | * +-----------------------------------------------------------------+ */ private enum WaitState { WAIT_FOR_EDIT, WAIT_FOR_TIMEOUT, RESTART_TIMEOUT; } protected static final class StatusMessage { private final Severity severity; private final ITextRegion location; private final String message; @SuppressWarnings("unused") private final Throwable cause; private final ICompilationError error; public StatusMessage(Severity severity, String message) { this(severity, null, message, null, null); } public StatusMessage(Severity severity, ITextRegion location, String message) { this(severity, location, message, null, null); } public StatusMessage(Severity severity, ICompilationError error) { this(severity, error.getLocation(), error.getMessage(), error, error.getCause()); } public StatusMessage(Severity severity, ITextRegion location, String message, ICompilationError error, Throwable cause) { if (severity == null) { throw new IllegalArgumentException("severity must not be NULL."); } if (StringUtils.isBlank(message)) { throw new IllegalArgumentException("message must not be NULL/blank."); } this.severity = severity; this.location = location; this.message = message; if (cause == null) { this.cause = error != null ? error.getCause() : null; } else { this.cause = cause; } this.error = error; } public StatusMessage(Severity severity, String message, Throwable e) { this(severity, null, message, null, e); } public Severity getSeverity() { return severity; } public ITextRegion getLocation() { return location; } public String getMessage() { return message; } public ICompilationError getError() { return error; } } protected final Highlighter getHighlighter() { return editorPane.getHighlighter(); } protected class StatusModel extends AbstractTableModel { private final List<StatusMessage> messages = new ArrayList<StatusMessage>(); private final int COL_SEVERITY = 0; private final int COL_LOCATION = 1; private final int COL_MESSAGE = 2; public StatusModel() { super(); } @Override public int getRowCount() { return messages.size(); } public StatusMessage getMessage(int row) { return messages.get(row); } public void addMessage(StatusMessage msg) { if (msg == null) { throw new IllegalArgumentException("msg must not be NULL."); } int index = messages.size(); messages.add(msg); fireTableRowsInserted(index, index); } public void setMessage(StatusMessage msg) { if (msg == null) { throw new IllegalArgumentException("msg must not be NULL."); } messages.clear(); messages.add(msg); fireTableDataChanged(); } @Override public int getColumnCount() { return 3; } @Override public String getColumnName(int columnIndex) { switch (columnIndex) { case COL_SEVERITY: return "Severity"; case COL_LOCATION: return "Location"; case COL_MESSAGE: return "Message"; default: return "no column name?"; } } @Override public Class<?> getColumnClass(int columnIndex) { return String.class; } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { return false; } @Override public Object getValueAt(int rowIndex, int columnIndex) { final StatusMessage msg = messages.get(rowIndex); switch (columnIndex) { case COL_SEVERITY: return msg.getSeverity().toString(); case COL_LOCATION: if (msg.getLocation() != null) { SourceLocation location; try { location = getSourceLocation(msg.getLocation()); return "Line " + location.getLineNumber() + " , column " + location.getColumnNumber(); } catch (NoSuchElementException e) { // ok, can't help it } } return "<unknown>"; case COL_MESSAGE: return msg.getMessage(); default: return "no column name?"; } } @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) { throw new UnsupportedOperationException(""); } public void addError(String message, IOException e1) { addMessage(new StatusMessage(Severity.ERROR, message, e1)); } public void addInfo(String message) { addMessage(new StatusMessage(Severity.INFO, message)); } public void clearMessages() { messages.clear(); fireTableDataChanged(); } } protected final void replaceText(ITextRegion region, String newValue) { disableDocumentListener(); try { editorPane.getDocument().remove(region.getStartingOffset(), region.getLength()); editorPane.getDocument().insertString(region.getStartingOffset(), newValue, defaultStyle); } catch (BadLocationException e) { throw new RuntimeException(e); } finally { enableDocumentListener(); } } protected class CompilationThread extends Thread { private final Object LOCK = new Object(); // @GuardedBy( LOCK ) private WaitState currentState = WaitState.WAIT_FOR_EDIT; public CompilationThread() { setDaemon(true); } @Override public void run() { while (true) { try { internalRun(); } catch (Exception e) { e.printStackTrace(); } } } private void internalRun() throws InterruptedException, InvocationTargetException { synchronized (LOCK) { switch (currentState) { case WAIT_FOR_EDIT: LOCK.wait(); return; case RESTART_TIMEOUT: currentState = WaitState.WAIT_FOR_TIMEOUT; // $FALL-THROUGH$ return; case WAIT_FOR_TIMEOUT: LOCK.wait(RECOMPILATION_DELAY_MILLIS); if (currentState != WaitState.WAIT_FOR_TIMEOUT) { return; } } } try { SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { try { validateSourceCode(); } catch (IOException e) { e.printStackTrace(); } finally { } } }); } finally { synchronized (LOCK) { currentState = WaitState.WAIT_FOR_EDIT; } } } public void documentChanged() { synchronized (LOCK) { currentState = WaitState.RESTART_TIMEOUT; LOCK.notifyAll(); } } } protected final void notifyDocumentChanged() { updateTitle(); if (compilationThread == null) { compilationThread = new CompilationThread(); compilationThread.start(); } compilationThread.documentChanged(); } private final DocumentFilter documentFilter = new DocumentFilter() { private Line getLine(int offset) { try { return compilationUnit.getLineForOffset(offset); } catch (NoSuchElementException e) { return null; } } private int getIndentionOfPreviousLine(int currentOffset) { if (compilationUnit == null || compilationUnit.getAST() == null) { return -1; } Line previous = null; try { previous = compilationUnit.getLineForOffset(currentOffset); } catch (NoSuchElementException e) { return -1; } while (previous != null) { StatementNode stmt = compilationUnit.getAST() .getFirstStatementForOffset(previous.getLineStartingOffset()); if (stmt != null && stmt.hasChildren()) { return stmt.child(0).getTextRegion().getStartingOffset() - previous.getLineStartingOffset(); } previous = compilationUnit.getPreviousLine(previous); } return -1; } public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException { if (text.equals("\n") && length == 0) { final int indention = getIndentionOfPreviousLine(offset); if (indention > 0) { super.replace(fb, offset, length, text + StringUtils.repeat(" ", indention), attrs); return; } } super.replace(fb, offset, length, text, attrs); } }; private final DocumentListener recompilationListener = new DocumentListener() { private void textChanged(DocumentEvent e) { notifyDocumentChanged(); } @Override public void removeUpdate(DocumentEvent e) { textChanged(e); } @Override public void insertUpdate(DocumentEvent e) { textChanged(e); } @Override public void changedUpdate(DocumentEvent e) { /* do nothing, style change only */ } }; private final CaretListener listener = new CaretListener() { @Override public void caretUpdate(final CaretEvent e) { // according to JDK docs, caretUpdate() is NOT necessarily called by the EDT, // wrap all code so we can safely update UI components final Runnable r = new Runnable() { public void run() { // gotoLocation() will set updateNavigationHistory == false when it's been // programatically triggered if (navigationHistoryUpdatesEnabled && sourceInMemory != null) { navigationHistory.add(new Location(project, sourceInMemory, e.getDot())); } // do not fire caret updates while building, this // causes at least the SourceEditorView to access the AST that // is still under construction and trigger a NPE in ASTNode#getNodeInRange() if (!isEditable() || isBuilding()) { return; } if (compilationUnit != null && compilationUnit.getAST() != null && compilationUnit.getAST().getTextRegion() != null) { try { final SourceLocation location = getSourceLocation(e.getDot()); cursorPosition.setHorizontalAlignment(JTextField.RIGHT); cursorPosition.setText("Line " + location.getLineNumber() + " , column " + location.getColumnNumber() + " (offset " + e.getDot() + ")"); } catch (NoSuchElementException e2) { // ok, user clicked on unknown location } } onCaretUpdate(e); }; }; UIUtils.invokeLater(r); } }; protected void onCaretUpdate(CaretEvent e) { } public SourceCodeView(IResourceResolver resourceResolver, IWorkspace workspace, NavigationHistory navigationHistory, boolean isEditable) { if (workspace == null) { throw new IllegalArgumentException("workspace must not be null"); } if (resourceResolver == null) { throw new IllegalArgumentException("resourceResolver must not be NULL."); } this.navigationHistory = navigationHistory; this.resourceResolver = resourceResolver; this.editable = isEditable; this.workspace = workspace; defaultStyle = new SimpleAttributeSet(); errorStyle = createStyle(Color.RED); registerStyle = createStyle(Color.ORANGE); commentStyle = createStyle(Color.WHITE); macroDefinitionStyle = createStyle(Color.YELLOW); macroInvocationStyle = createStyle(Color.YELLOW); instructionStyle = createStyle(new Color(50, 186, 223)); labelStyle = createStyle(new Color(237, 237, 81)); preProcessorStyle = createStyle(new Color(200, 200, 200)); workspace.addWorkspaceListener(workspaceListener); navigationHistory.addListener(navigationHistoryListener); } protected final static SimpleAttributeSet createStyle(Color color) { SimpleAttributeSet result = new SimpleAttributeSet(); StyleConstants.setForeground(result, color); return result; } protected final SourceLocation getSourceLocation(ITextRegion range) { return getSourceLocation(range.getStartingOffset()); } protected final SourceLocation getSourceLocation(int offset) { final Line line = compilationUnit.getLineForOffset(offset); return new SourceLocation(compilationUnit, line, new TextRegion(offset, 0)); } protected final GridBagConstraints constraints(int x, int y, int fill) { GridBagConstraints result = new GridBagConstraints(); result.fill = fill; result.weightx = 1.0; result.weighty = 1.0; result.gridheight = 1; result.gridwidth = 1; result.gridx = x; result.gridy = y; result.insets = new Insets(1, 1, 1, 1); return result; } protected void setStatusMessage(String message) { } protected final String getTextFromTextPane() { final int len = editorPane.getDocument().getLength(); if (len == 0) { return ""; } try { return editorPane.getDocument().getText(0, len); } catch (BadLocationException e) { throw new RuntimeException("bad location: ", e); } } @Override public final void openResource(IAssemblyProject project, IResource resource, int caretPosition) throws IOException { if (this.project != project || this.persistentResource != resource) { openResource(project, resource, caretPosition, true); } } protected final void openResource(final IAssemblyProject project, final IResource sourceFile, boolean compileSource) throws IOException { openResource(project, sourceFile, 0, compileSource); } protected final void openResource(final IAssemblyProject project, final IResource sourceFile, int caretPosition, boolean compileSource) throws IOException { if (project == null) { throw new IllegalArgumentException("project must not be NULL"); } if (sourceFile == null) { throw new IllegalArgumentException("sourceFile must not be NULL"); } // read source first so we don't discard internal state // and end up with an IOException later on... final String source = Misc.readSource(sourceFile); this.initialHashCode = Misc.calcHash(source); this.project = project; if (sourceFile instanceof InMemorySourceResource) { this.sourceInMemory = (InMemorySourceResource) sourceFile; this.persistentResource = sourceInMemory.getPersistentResource(); } else { this.sourceInMemory = new InMemorySourceResource(sourceFile, editorPane) { @Override public String toString() { return "SourceCodeView[ " + persistentResource + " ]"; } }; this.persistentResource = sourceFile; } clearHighlight(); try { disableDocumentListener(); try { final Document doc = editorPane.getDocument(); doc.putProperty(Document.StreamDescriptionProperty, null); disableNavigationHistoryUpdates(); System.out.println("Text length: " + (source == null ? 0 : source.length())); try { editorPane.setText(source); } finally { enableNavigationHistoryUpdates(); } try { editorPane.setCaretPosition(caretPosition); } catch (IllegalArgumentException e) { LOG.error("openResource(): Invalid caret position " + caretPosition + " in resource " + sourceFile); } if (panel != null) { ICompilationUnit existing = null; if (!compileSource) { existing = project.getProjectBuilder().getCompilationUnit(sourceFile); } validateSourceCode(existing); } } finally { enableDocumentListener(); } } finally { enableNavigationHistoryUpdates(); } editorPane.requestFocus(); updateTitle(); } protected final boolean isBuilding() { return isBuilding; } protected final void validateSourceCode() throws IOException { validateSourceCode(null); } protected final void validateSourceCode(ICompilationUnit existing) throws IOException { long time = -System.currentTimeMillis(); disableDocumentListener(); try { clearCompilationErrors(); onSourceCodeValidation(); final IResourceResolver delegatingResolver = new AbstractResourceResolver() { private IResourceResolver getChildResourceResolver(IResource parent) { IResource r = parent == null ? getCurrentResource() : parent; if (!(r instanceof FileResource)) { if (r instanceof InMemorySourceResource) { r = ((InMemorySourceResource) r).getPersistentResource(); } } if (!(r instanceof FileResource)) { throw new RuntimeException("Internal error, not a file-resource: " + getCurrentResource()); } final FileResource fr = (FileResource) r; return new FileResourceResolver(fr.getAbsoluteFile().getParentFile()) { @Override protected ResourceType determineResourceType(File file) { // TODO: Maybe implement some more general mechanism of determining resource types ? return project.getConfiguration().isSourceFile(file) ? ResourceType.SOURCE_CODE : ResourceType.UNKNOWN; } }; } @Override public IResource resolve(String identifier) throws ResourceNotFoundException { try { if (resourceResolver != null) { return resourceResolver.resolve(identifier); } } catch (ResourceNotFoundException e) { } return getChildResourceResolver(null).resolve(identifier); } @Override public IResource resolveRelative(String identifier, IResource parent) throws ResourceNotFoundException { try { if (resourceResolver != null) { return resourceResolver.resolveRelative(identifier, parent); } } catch (ResourceNotFoundException e) { } final IResource realParent; if (parent instanceof InMemorySourceResource) { realParent = ((InMemorySourceResource) parent).getPersistentResource(); } else { realParent = parent; } return getChildResourceResolver(parent).resolveRelative(identifier, realParent); } }; try { if (existing == null || existing.getAST() == null) { compilationUnit = project.getProjectBuilder().parse(sourceInMemory, delegatingResolver, new CompilationListener()); } else { compilationUnit = existing; } } catch (Exception e) { LOG.error("validateSourceCode(): ", e); } finally { doHighlighting(compilationUnit, true); } for (ICompilationError error : compilationUnit.getErrors()) { onCompilationError(error); } for (ICompilationError error : compilationUnit.getWarnings()) { onCompilationWarning(error); } } finally { enableDocumentListener(); time += System.currentTimeMillis(); System.out.println("Source code validation: " + time + " ms"); } } protected void onSourceCodeValidation() { } protected final void doHighlighting(ICompilationUnit unit, boolean called) { if (unit == null) { throw new IllegalArgumentException("Internal error,compilation unit must not be NULL."); } if (panel == null) { return; } if (unit.getAST() != null) { onHighlightingStart(); long time = -System.currentTimeMillis(); try { final int markerCount = unit.getMarkers((String[]) null).size(); System.out.println("DEBUG: Starting to highlight " + unit.getResource() + " with " + markerCount + " markers."); doSemanticHighlighting(unit); } finally { time += System.currentTimeMillis(); System.out.println("DEBUG: Highlighting " + unit.getResource() + " took " + time + " ms."); } } if (unit.hasErrors()) { highlightCompilationErrors(compilationUnit); } } protected void onHighlightingStart() { } protected final void doSemanticHighlighting(ICompilationUnit unit) { if (unit.getAST() == null) { return; } // changing character styles triggers // change events that in turn would // again trigger recompilation...we don't want that... disableDocumentListener(); try { final ITextRegion visible = getVisibleTextRegion(); if (visible != null) { final List<ASTNode> nodes = unit.getAST().getNodesInRange(visible); System.out.println("Highlighting " + nodes.size() + " nodes."); for (ASTNode child : nodes) { doSemanticHighlighting(unit, child); } } } finally { enableDocumentListener(); } } protected final void doSemanticHighlighting(ICompilationUnit unit, ASTNode node) { if (highlight(node)) { return; // don't highlight children if parent already was } if (!(node instanceof IncludeSourceFileNode)) { for (ASTNode child : node.getChildren()) { doSemanticHighlighting(unit, child); } } } protected final boolean highlight(ASTNode node) { if (node instanceof StartMacroNode || node instanceof EndMacroNode) { highlight(node, macroDefinitionStyle); return true; } else if (node instanceof InvokeMacroNode) { highlight(node, macroInvocationStyle); return true; } else if (node instanceof InstructionNode) { ITextRegion children = null; for (ASTNode child : node.getChildren()) { if (children == null) { children = child.getTextRegion(); } else { children.merge(child.getTextRegion()); } } ITextRegion whole = new TextRegion(node.getTextRegion()); whole.subtract(children); highlight(whole, instructionStyle); return true; } else if (node instanceof IPreprocessorDirective) { highlight(node, preProcessorStyle); return true; } else if (node instanceof SymbolReferenceNode || node instanceof LabelNode) { highlight(node, labelStyle); return true; } else if (node instanceof CommentNode) { highlight(node, commentStyle); return true; } else if (node instanceof RegisterReferenceNode) { highlight(node, registerStyle); return true; } return false; } protected final void highlight(ASTNode node, AttributeSet attributes) { highlight(node.getTextRegion(), attributes); } protected final void highlight(ITextRegion range, AttributeSet attributes) { editorPane.getStyledDocument().setCharacterAttributes(range.getStartingOffset(), range.getLength(), attributes, true); } public final void moveCursorTo(ITextRegion location, boolean updateNavigationHistory) { moveCursorTo(location.getStartingOffset(), updateNavigationHistory); } public final void moveCursorTo(int offset, boolean updateNavigationHistory) { if (compilationUnit == null || compilationUnit.getAST() == null) { return; } if (!editorPane.hasFocus()) { editorPane.requestFocus(); } try { if (!updateNavigationHistory) { disableNavigationHistoryUpdates(); try { editorPane.setCaretPosition(offset); } finally { enableNavigationHistoryUpdates(); } } else { editorPane.setCaretPosition(offset); } } catch (IllegalArgumentException e) { LOG.error("moveCursorTo(): Failed to offset " + offset + " on project " + project + " , resource " + sourceInMemory, e); } centerCurrentLineInScrollPane(); } public final void centerCurrentLineInScrollPane() { final Runnable r = new Runnable() { @Override public void run() { final Container container = SwingUtilities.getAncestorOfClass(JViewport.class, editorPane); if (container == null) { return; } try { final Rectangle r = editorPane.modelToView(editorPane.getCaretPosition()); if (r == null) { return; } final JViewport viewport = (JViewport) container; final int extentHeight = viewport.getExtentSize().height; final int viewHeight = viewport.getViewSize().height; int y = Math.max(0, r.y - (extentHeight / 2)); y = Math.min(y, viewHeight - extentHeight); viewport.setViewPosition(new Point(0, y)); } catch (BadLocationException ble) { LOG.error("centerCurrentLineInScrollPane(): ", ble); } } }; UIUtils.invokeLater(r); } protected final boolean canNavigationHistoryBack() { return navigationHistory.canGoBack(); } protected final boolean canNavigationHistoryForward() { return navigationHistory.canGoForward(); } protected final void navigationHistoryBack() { gotoLocation(navigationHistory.goBack()); } protected final void navigationHistoryForward() { gotoLocation(navigationHistory.goForward()); } // guaranteed to only be called on EDT protected void onNavigationHistoryChange() { } public final void gotoLocation(final int offset) { gotoLocation(project, sourceInMemory, offset, false); } private final void gotoLocation(Location location) { if (location == null) { return; } IAssemblyProject project = workspace.getProjectForResource(location.getResource()); gotoLocation(project, location.getResource(), location.getOffset(), true); } private final void gotoLocation(IAssemblyProject project, IResource resource, final int offset, final boolean triggeredProgramatically) { if (this.project != project || !isCurrentlyDisplayed(resource)) { if (getViewContainer() instanceof EditorContainer) { try { ((EditorContainer) getViewContainer()).openResource(workspace, project, resource, offset); } catch (IOException e) { LOG.error("gotoLocation(): Failed for project " + project + ", resource " + resource, e); } return; } try { openResource(project, resource, offset, true); } catch (IOException e) { LOG.error("gotoLocation(): Failed to project " + project + ",resource " + resource, e); } return; } final Runnable r = new Runnable() { @Override public void run() { editorPane.requestFocusInWindow(); if (triggeredProgramatically) { disableNavigationHistoryUpdates(); editorPane.setCaretPosition(offset); enableNavigationHistoryUpdates(); } else { editorPane.setCaretPosition(offset); } centerCurrentLineInScrollPane(); } }; UIUtils.invokeLater(r); } protected final boolean isCurrentlyDisplayed(IResource resource) { if (this.sourceInMemory == null) { return false; } return this.sourceInMemory.getIdentifier().equals(resource.getIdentifier()); } protected final void disableNavigationHistoryUpdates() { navigationHistoryUpdatesEnabled = false; } protected final void enableNavigationHistoryUpdates() { navigationHistoryUpdatesEnabled = true; } protected final ITextRegion getVisibleTextRegion() { JViewport viewport = editorScrollPane.getViewport(); Rectangle viewRect = viewport.getViewRect(); Point p1 = viewRect.getLocation(); int startIndex = editorPane.viewToModel(p1); if (startIndex < 0) { return null; } Point p2 = new Point(p1.x + viewRect.width - 10, p1.y + viewRect.height - 10); // -10 is some arbitrary offset to fix an issue with viewToModel() returning a position at the end of the input text int endIndex = editorPane.viewToModel(p2); if (endIndex < 0) { return null; } int len = endIndex - startIndex; if (len < 0) { return null; } System.out.println("getVisibleTextRegion( " + p1 + " , " + p2 + " => " + startIndex + "," + endIndex); return new TextRegion(startIndex, len); } // protected final ITextRegion getVisibleTextRegion() // { // final Point startPoint = editorScrollPane.getViewport().getViewPosition(); // final Dimension size = editorScrollPane.getViewport().getExtentSize(); // // final Point endPoint = new Point(startPoint.x + size.width, startPoint.y + size.height); // try { // final int start = editorPane.viewToModel( startPoint ); // if ( start < 0 ) { // return null; // } // final int end = editorPane.viewToModel( endPoint ); // if ( end < 0 ) { // return null; // } // final int len = end-start; // if ( len < 0 ) { // return null; // } // return new TextRegion( start , len ); // } // catch(NullPointerException e) // { // LOG.error("getVisibleTextRegion(): Caught ",e); // return null; // } // } protected final void clearCompilationErrors() { disableDocumentListener(); try { final StyledDocument doc = editorPane.getStyledDocument(); doc.setCharacterAttributes(0, doc.getLength(), defaultStyle, true); } finally { enableDocumentListener(); } } protected final void disableDocumentListener() { documentListenerDisableCount++; editorPane.getDocument().removeDocumentListener(recompilationListener); if (editorPane.getDocument() instanceof AbstractDocument) { ((AbstractDocument) editorPane.getDocument()).setDocumentFilter(null); } } protected final void enableDocumentListener() { documentListenerDisableCount--; if (documentListenerDisableCount == 0) { editorPane.getDocument().addDocumentListener(recompilationListener); if (editorPane.getDocument() instanceof AbstractDocument) { ((AbstractDocument) editorPane.getDocument()).setDocumentFilter(documentFilter); } } } protected final void highlightCompilationErrors(ICompilationUnit unit) { disableDocumentListener(); try { for (ICompilationError error : unit.getErrors()) { final ITextRegion location; if (error.getLocation() != null) { location = error.getLocation(); } else { if (error.getErrorOffset() != -1) { location = new TextRegion(error.getErrorOffset(), 1); } else { location = null; } } if (location != null) { highlight(location, errorStyle); } } } finally { enableDocumentListener(); } } protected void onCompilationError(ICompilationError error) { } protected void onCompilationWarning(ICompilationError error) { } // ============= view creation =================== @Override public JPanel getPanel() { if (panel == null) { panel = createPanel(); if (this.persistentResource != null) { try { validateSourceCode(); } catch (IOException e) { LOG.error("getPanel(): ", e); } } } return panel; } private final MouseListener mouseListener = new MouseAdapter() { public void mouseClicked(java.awt.event.MouseEvent e) { if (e.getClickCount() == 1 && e.getButton() == MouseEvent.BUTTON1 && (e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) != 0) { // navigate to symbol definition final ASTNode node = getASTNodeForLocation(e.getPoint()); if (node instanceof SymbolReferenceNode) { final SymbolReferenceNode ref = (SymbolReferenceNode) node; gotoToSymbolDefinition(ref); } } } }; protected final void gotoToSymbolDefinition(SymbolReferenceNode ref) { if (getCurrentCompilationUnit() != null) { ISymbolTable table = getCurrentCompilationUnit().getSymbolTable(); ISymbol symbol = ref.resolve(table, true); if (symbol != null) { if (symbol != null) { final ITextRegion location = symbol.getLocation(); final ICompilationUnit newCompilationUnit = symbol.getCompilationUnit(); final IAssemblyProject project = workspace .getProjectForResource(newCompilationUnit.getResource()); gotoLocation(project, newCompilationUnit.getResource(), location.getStartingOffset(), false); } } } } private final JPanel createPanel() { disableDocumentListener(); // necessary because setting colors on editor pane triggers document change listeners (is considered a style change...) try { editorPane.setEditable(editable); editorPane.getDocument().addUndoableEditListener(undoListener); editorPane.setCaretColor(Color.WHITE); setupKeyBindings(editorPane); setColors(editorPane); editorScrollPane = new JScrollPane(editorPane); setColors(editorScrollPane); editorPane.addCaretListener(listener); editorPane.addMouseListener(mouseListener); editorPane.addMouseMotionListener(new MouseMotionAdapter() { @Override public void mouseMoved(MouseEvent e) { if ((e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) != 0) // ctrl pressed { final ASTNode node = getASTNodeForLocation(e.getPoint()); if (node instanceof SymbolReferenceNode) { maybeUnderlineIdentifierAt(e.getPoint()); } else { clearUnderlineHighlight(); } } else if (compilationUnit != null) { String tooltipText = null; if (compilationUnit != null) { final ASTNode node = getASTNodeForLocation(e.getPoint()); if (node instanceof InvokeMacroNode) { tooltipText = ExpandMacrosPhase.expandInvocation((InvokeMacroNode) node, compilationUnit); if (tooltipText != null) { tooltipText = "<html>" + tooltipText.replace("\n", "<br>") + "</html>"; } } } if (!StringUtils.equals(editorPane.getToolTipText(), tooltipText)) { editorPane.setToolTipText(tooltipText); } } } }); editorPane.addMouseListener(popupListener); } finally { enableDocumentListener(); } EditorContainer.addEditorCloseKeyListener(editorPane, this); editorScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); editorScrollPane.setPreferredSize(new Dimension(400, 600)); editorScrollPane.setMinimumSize(new Dimension(100, 100)); final AdjustmentListener adjustmentListener = new AdjustmentListener() { @Override public void adjustmentValueChanged(AdjustmentEvent e) { if (!e.getValueIsAdjusting()) { if (compilationUnit != null) { doHighlighting(compilationUnit, false); } } } }; editorScrollPane.getVerticalScrollBar().addAdjustmentListener(adjustmentListener); editorScrollPane.getHorizontalScrollBar().addAdjustmentListener(adjustmentListener); // button panel final JPanel topPanel = new JPanel(); final JToolBar toolbar = new JToolBar(); setColors(toolbar); cursorPosition.setSize(new Dimension(400, 15)); cursorPosition.setEditable(false); setColors(cursorPosition); /** * TOOLBAR * SOURCE * cursor position * status area */ topPanel.setLayout(new GridBagLayout()); GridBagConstraints cnstrs = constraints(0, 0, GridBagConstraints.HORIZONTAL); cnstrs.gridwidth = GridBagConstraints.REMAINDER; cnstrs.weighty = 0; topPanel.add(toolbar, cnstrs); cnstrs = constraints(0, 1, GridBagConstraints.BOTH); cnstrs.gridwidth = GridBagConstraints.REMAINDER; topPanel.add(editorScrollPane, cnstrs); cnstrs = constraints(0, 2, GridBagConstraints.HORIZONTAL); cnstrs.gridwidth = GridBagConstraints.REMAINDER; cnstrs.weighty = 0; topPanel.add(cursorPosition, cnstrs); cnstrs = constraints(0, 3, GridBagConstraints.HORIZONTAL); cnstrs.gridwidth = GridBagConstraints.REMAINDER; cnstrs.weighty = 0; // setup result panel final JPanel panel = new JPanel(); panel.setLayout(new GridBagLayout()); setColors(panel); cnstrs = constraints(0, 0, true, true, GridBagConstraints.BOTH); panel.add(topPanel, cnstrs); return panel; } /** * Returns the mouse pointer's location relative to the * editorpane or <code>null</code> if the mouse pointer is * outside of the editor. * * @return location or <code>null</code> if mouse ptr is outside of the editor pane */ protected final Point getMouseLocation() { final Point location = MouseInfo.getPointerInfo().getLocation(); final Point locOnScreen = editorPane.getLocationOnScreen(); final Point result = new Point(location.x - locOnScreen.x, location.y - locOnScreen.y); return editorPane.contains(result) ? result : null; } protected final void setupKeyBindings(final JTextPane editor) { // 'Save' action addKeyBinding(editor, KeyStroke.getKeyStroke(KeyEvent.VK_S, Event.CTRL_MASK), new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { saveCurrentFile(); } }); // 'Underline text when pressing CTRL while hovering over an identifier' editorPane.addKeyListener(new KeyAdapter() { private boolean isControlKey(KeyEvent e) { return e.getKeyCode() == KeyEvent.VK_CONTROL; } public void keyPressed(KeyEvent e) { if (isControlKey(e)) { final Point ptr = getMouseLocation(); if (ptr != null) { maybeUnderlineIdentifierAt(ptr); } } } public void keyReleased(KeyEvent e) { if (isControlKey(e)) { clearUnderlineHighlight(); } }; }); // "Undo" action addKeyBinding(editor, KeyStroke.getKeyStroke(KeyEvent.VK_Z, Event.CTRL_MASK), undoAction); addKeyBinding(editor, KeyStroke.getKeyStroke(KeyEvent.VK_Y, Event.CTRL_MASK), redoAction); // 'Search' action addKeyBinding(editor, KeyStroke.getKeyStroke(KeyEvent.VK_F, Event.CTRL_MASK), new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { showSearchDialog(); } }); setupKeyBindingsHook(editor); } protected void setupKeyBindingsHook(JTextPane editor) { } protected final void saveCurrentFile() { if (!hasUnsavedContent()) { return; } final String source = getTextFromTextPane(); try { Misc.writeResource(getCurrentResource(), source); this.initialHashCode = Misc.calcHash(source); updateTitle(); } catch (IOException e1) { LOG.error("save(): Failed to write to " + getCurrentResource()); return; } if (compilationUnit == null || compilationUnit.hasErrors()) { return; } try { getCurrentProject().getProjectBuilder().build(); } catch (IOException e) { LOG.error("save(): Compilation failed", e); } } public final IAssemblyProject getCurrentProject() { return project; } public final IResource getCurrentResource() { return this.persistentResource; } public final IResource getSourceFromMemory() { return this.sourceInMemory; } @Override public final void disposeHook() { try { disposeHook2(); } finally { if (registeredWithTooltipManager) { ToolTipManager.sharedInstance().unregisterComponent(editorPane); } workspace.removeWorkspaceListener(workspaceListener); navigationHistory.removeListener(navigationHistoryListener); } } protected void disposeHook2() { } @Override public final void refreshDisplay() { try { if (project != null) { validateSourceCode(); } } catch (IOException e) { e.printStackTrace(); } refreshDisplayHook(); } protected void refreshDisplayHook() { } @Override public String getTitle() { if (getCurrentResource() == null) { return "source view"; } final String prefix = hasUnsavedContent() ? "*" : ""; final String identifier; if (getCurrentResource() instanceof FileResource) { identifier = ((FileResource) getCurrentResource()).getFile().getName(); } else { identifier = getCurrentResource().getIdentifier(); } return prefix + identifier; } @Override public final boolean hasUnsavedContent() { if (this.persistentResource == null) { return false; } return !initialHashCode.equals(Misc.calcHash(getTextFromTextPane())); } @Override public final boolean mayBeDisposed() { return !hasUnsavedContent(); } protected final void updateTitle() { final String title = (hasUnsavedContent() ? "*" : "") + getCurrentResource().getIdentifier(); getViewContainer().setTitle(SourceCodeView.this, title); } @Override public String getID() { return "source-view"; } public final boolean isEditable() { return editable; } public void setEditable(boolean editable) { this.editable = editable; editorPane.setEditable(editable); } protected final ICompilationUnit getCurrentCompilationUnit() { return compilationUnit; } protected final void addMouseListener(MouseAdapter listener) { editorPane.addMouseListener(listener); editorPane.addMouseMotionListener(listener); } protected final void showTooltip(String s) { if (!registeredWithTooltipManager) { ToolTipManager.sharedInstance().registerComponent(editorPane); registeredWithTooltipManager = true; } editorPane.setToolTipText(s); } protected final void clearTooltip() { editorPane.setToolTipText(null); } protected final void removeMouseListener(MouseAdapter listener) { editorPane.removeMouseListener(listener); editorPane.removeMouseMotionListener(listener); } protected final int getModelOffsetForLocation(Point p) { return editorPane.viewToModel(p); } protected final void addPopupMenu(PopupMenu menu) { editorPane.add(menu); } protected final ASTNode getASTNodeForLocation(Point p) { final AST ast = getCurrentCompilationUnit() != null ? getCurrentCompilationUnit().getAST() : null; if (ast == null) { return null; } int offset = editorPane.viewToModel(p); if (offset != -1) { return ast.getNodeInRange(offset); } return null; } protected class PopupListener extends MouseAdapter { public void mousePressed(MouseEvent e) { maybeShowPopup(e); } public void mouseReleased(MouseEvent e) { maybeShowPopup(e); } private void maybeShowPopup(MouseEvent e) { if (e.isPopupTrigger()) { final ASTNode node = getASTNodeForLocation(e.getPoint()); final JPopupMenu menu = createPopupMenu(node, editorPane.getCaretPosition(), editorPane.getSelectedText()); if (menu != null) { menu.show(e.getComponent(), e.getX(), e.getY()); } } } } protected JPopupMenu createPopupMenu(ASTNode node, int caretPosition, String currentSelection) { return null; } protected final void showSearchDialog() { final int cursorPos = editorPane.getCaretPosition(); final String selection = editorPane.getSelectedText(); if (searchDialog == null) { searchDialog = new SearchDialog(); searchDialog.setVisible(true); searchDialog.activate(selection, cursorPos); } else { searchDialog.activate(selection, cursorPos); } } protected final void highlightLocation(ITextRegion region) { if (region == null) { throw new IllegalArgumentException("region must not be null"); } try { if (currentHighlight == null) { currentHighlight = editorPane.getHighlighter().addHighlight(region.getStartingOffset(), region.getEndOffset(), new DefaultHighlighter.DefaultHighlightPainter(Color.WHITE)); } else { editorPane.getHighlighter().changeHighlight(currentHighlight, region.getStartingOffset(), region.getEndOffset()); } } catch (BadLocationException e) { LOG.error("highlightLocation(): Bad location " + region, e); throw new RuntimeException("Bad text location " + region, e); } } protected final void clearUnderlineHighlight() { final Runnable r = new Runnable() { @Override public void run() { if (currentUnderlineHighlight != null) { editorPane.getHighlighter().removeHighlight(currentUnderlineHighlight); currentUnderlineHighlight = null; editorPane.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); editorPane.repaint(); } } }; UIUtils.invokeLater(r); } protected final void maybeUnderlineIdentifierAt(Point mouseLocation) { final ASTNode node = getASTNodeForLocation(mouseLocation); if (!(node instanceof SymbolReferenceNode)) { return; } final SymbolReferenceNode ref = (SymbolReferenceNode) node; underlineLocation(ref.getTextRegion()); } protected final void underlineLocation(final ITextRegion region) { if (region == null) { throw new IllegalArgumentException("region must not be NULL."); } Runnable r = new Runnable() { @Override public void run() { try { if (currentUnderlineHighlight == null) { currentUnderlineHighlight = editorPane.getHighlighter().addHighlight( region.getStartingOffset(), region.getEndOffset(), new UnderlineHighlightPainter(Color.BLUE, 1)); } else { editorPane.getHighlighter().changeHighlight(currentUnderlineHighlight, region.getStartingOffset(), region.getEndOffset()); } editorPane.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); editorPane.repaint(); } catch (BadLocationException e) { LOG.error("underlineLocation(): Bad location " + region, e); } } }; UIUtils.invokeLater(r); } protected final void clearHighlight() { if (currentHighlight != null) { editorPane.getHighlighter().removeHighlight(currentHighlight); currentHighlight = null; } } protected static enum Direction { FORWARD { @Override public int advance(int index) { return index + 1; } }, BACKWARD { @Override public int advance(int index) { return index - 1; } }; public abstract int advance(int index); } protected final class SearchDialog extends JFrame { private final JTextField searchPattern = new JTextField(); private final JCheckBox wrapSearch = new JCheckBox("Wrap", false); private final JCheckBox caseSensitive = new JCheckBox("Match case", false); private final JButton nextButton = new JButton("Next"); private final JButton previousButton = new JButton("Previous"); private final JButton closeButton = new JButton("Close"); private final JTextField messageArea = new JTextField("", 25); private String lastSearchPattern; private int lastMatch = -1; private int currentIndex = 0; private Direction lastDirection = Direction.FORWARD; public SearchDialog() { super("Search"); messageArea.setBackground(null); messageArea.setEditable(false); messageArea.setBorder(null); messageArea.setFocusable(false); setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); final JPanel panel = new JPanel(); panel.setLayout(new GridBagLayout()); // add search pattern GridBagConstraints cnstrs = constraints(0, 0, true, false, GridBagConstraints.HORIZONTAL); panel.add(searchPattern, cnstrs); searchPattern.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { search(lastDirection); } }); // add wrap checkbox cnstrs = constraints(0, 1, false, false, GridBagConstraints.HORIZONTAL); panel.add(wrapSearch, cnstrs); // 'case-sensitive' checkbox cnstrs = constraints(1, 1, false, false, GridBagConstraints.HORIZONTAL); panel.add(caseSensitive, cnstrs); // add message area cnstrs = constraints(0, 2, true, false, GridBagConstraints.HORIZONTAL); panel.add(messageArea, cnstrs); // create button panel final JPanel buttonPanel = new JPanel(); buttonPanel.setLayout(new GridBagLayout()); cnstrs = constraints(0, 0, false, true, GridBagConstraints.HORIZONTAL); buttonPanel.add(previousButton, cnstrs); previousButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { search(Direction.BACKWARD); } }); cnstrs = constraints(1, 0, false, true, GridBagConstraints.HORIZONTAL); buttonPanel.add(nextButton, cnstrs); nextButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { search(Direction.FORWARD); } }); cnstrs = constraints(2, 0, true, true, GridBagConstraints.HORIZONTAL); buttonPanel.add(closeButton, cnstrs); closeButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { setVisible(false); clearHighlight(); } }); // add button panel // add wrap checkbox cnstrs = constraints(0, 3, true, true, GridBagConstraints.HORIZONTAL); panel.add(buttonPanel, cnstrs); // add everything to content pane getContentPane().add(panel); setAlwaysOnTop(true); pack(); } public final void activate(String selectedText, int cursorPos) { setVisible(true); currentIndex = cursorPos; final String text = StringUtils.isNotBlank(selectedText) ? selectedText : lastSearchPattern; if (text != null) { searchPattern.setText(selectedText); } searchPattern.requestFocus(); } protected void search(Direction direction) { showMessage(null); final String source = getTextFromTextPane(); final String pattern = searchPattern.getText(); if (StringUtils.isBlank(pattern)) { showMessage("Please enter a search pattern"); return; } lastSearchPattern = pattern; lastDirection = direction; if (lastMatch != -1) // advance past last match { if (!advance(source, direction, pattern, lastMatch)) { clearHighlight(); showNotFoundMessage(); return; } } // remember start index so we don't loop forever // if the search pattern doesn't match at all final int searchStartIndex = currentIndex; final boolean matchCaseSensitive = caseSensitive.isSelected(); do { final String currentText = source.substring(currentIndex, currentIndex + pattern.length()); final boolean matches; if (matchCaseSensitive) { matches = currentText.equals(pattern); } else { matches = currentText.equalsIgnoreCase(pattern); } if (matches) { lastMatch = currentIndex; gotoLocation(currentIndex); editorPane.requestFocus(); editorPane.setCaretPosition(currentIndex); highlightLocation(new TextRegion(currentIndex, pattern.length())); // TODO: Maybe show 'match found' message ? return; } if (!advance(source, direction, pattern, currentIndex)) { clearHighlight(); showNotFoundMessage(); break; } } while (currentIndex != searchStartIndex); if (lastMatch != -1) { currentIndex = lastMatch; } clearHighlight(); showNotFoundMessage(); } private void showNotFoundMessage() { showMessage("No (more) matches"); } private boolean advance(String source, Direction direction, String pattern, int index) { int newIndex = direction.advance(index); if (newIndex < 0) { if (wrapSearch.isSelected()) { showSearchWrappedMessage(); newIndex = source.length() - 1 - pattern.length(); } else { return false; } } else if ((newIndex + pattern.length()) >= source.length()) { if (wrapSearch.isSelected()) { showSearchWrappedMessage(); newIndex = 0; } else { return false; } } currentIndex = newIndex; return true; } private void showSearchWrappedMessage() { showMessage("Search wrapped"); } private void showMessage(String message) { messageArea.setText(message); } } protected final int getCaretPosition() { return editorPane.getCaretPosition(); } }