org.netbeans.spi.debugger.ui.MethodChooser Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.netbeans.spi.debugger.ui;
import java.awt.Color;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JEditorPane;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import javax.swing.text.StyleConstants;
import org.netbeans.editor.BaseDocument;
import org.netbeans.editor.BaseCaret;
import org.netbeans.editor.Utilities;
import org.netbeans.spi.editor.highlighting.HighlightAttributeValue;
import org.netbeans.spi.editor.highlighting.support.OffsetsBag;
import org.netbeans.api.editor.settings.AttributesUtilities;
import org.netbeans.api.editor.settings.EditorStyleConstants;
import org.netbeans.spi.editor.highlighting.HighlightsLayer;
import org.netbeans.spi.editor.highlighting.HighlightsLayerFactory;
import org.netbeans.spi.editor.highlighting.ZOrder;
import org.openide.cookies.EditorCookie;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.URLMapper;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectNotFoundException;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
import org.openide.windows.TopComponent;
/**
* Support for Step Into action implementations. It allows the user to select
* directly in a source file a method call the debugger should step into.
* A simple graphical interface is provided. The user can navigate among available
* method calls. The navigation can be done using a keyboard as well as a mouse.
*
* The method chooser is initialized by an url (pointing to a source file), an array of
* {@link Segment} elements (each of them corresponds typically to a method call name
* in the source file) and an index of the segment element which is displayed
* as the default selection.
*
*
Optionally, two sets of (additional) shortcuts that confirm, resp. cancel the selection
* mode can be specified.
* It is also possible to pass a text, which should be shown at the editor pane's
* status line after the selection mode has been activated. This text serves as a hint
* to the user how to make the method call selection.
*
*
Method chooser does not use any special highlighting for the background of the
* area where the selection takes place. If it is required it can be done by attaching
* instances of {@link org.openide.text.Annotation} to the proper source file's lines. These annotation should
* be added before calling {@link #showUI} and removed after calling {@link #releaseUI}.
*
*
To display the method chooser's ui correctly, it is required to register
* {@link HighlightsLayerFactory} created by {@link #createHighlihgtsLayerFactory}
* in an xml layer. An example follows.
*
*
<folder name="Editors">
<folder name="text">
<folder name="x-java">
<file name="org.netbeans.spi.editor.highlighting.HighlightsLayerFactory.instance">
<attr name="instanceCreate" methodvalue="org.netbeans.spi.debugger.ui.MethodChooser.createHighlihgtsLayerFactory"/>
</file>
</folder>
</folder>
</folder>
* "x-java"
should be replaced by the targeted mime type.
*
* @author Daniel Prusa
* @since 2.22
*/
public class MethodChooser {
private static AttributeSet defaultHyperlinkHighlight;
private String url;
private Segment[] segments;
private int selectedIndex = -1;
private String hintText;
private KeyStroke[] stopEvents;
private KeyStroke[] confirmEvents;
private AttributeSet attribsLeft = null;
private AttributeSet attribsRight = null;
private AttributeSet attribsMiddle = null;
private AttributeSet attribsAll = null;
private AttributeSet attribsLeftUnc = null;
private AttributeSet attribsRightUnc = null;
private AttributeSet attribsMiddleUnc = null;
private AttributeSet attribsAllUnc = null;
private AttributeSet attribsArea = null;
private AttributeSet attribsMethod = null;
private AttributeSet attribsMethodUnc = null;
private AttributeSet attribsHyperlink = null;
private Cursor handCursor;
private Cursor arrowCursor;
private Cursor originalCursor;
private CentralListener mainListener;
private Document doc;
private JEditorPane editorPane;
private List releaseListeners = new ArrayList();
private int startLine;
private int endLine;
private int mousedIndex = -1;
private boolean isInSelectMode = false;
/**
* Creates an instance of {@link MethodChooser}.
*
* @param url Url of the source file.
* @param segments Array of segments where each of the segments represents one method
* call. The user traverses the calls in the order given by the array.
* @param initialIndex Index of a call that should be preselected when the method chooser
* is shown.
*/
public MethodChooser(String url, Segment[] segments, int initialIndex) {
this(url, segments, initialIndex, null, new KeyStroke[0], new KeyStroke[0]);
}
/**
* Creates an instance of {@link MethodChooser}. Supports optional parameters.
*
* @param url Url of the source file.
* @param segments Array of segments where each of the segments represents one method
* call. The user traverses the calls in the order given by the array.
* @param initialIndex Index of a call that should be preselected when the method chooser
* is shown.
* @param hintText Text which is displayed in the editor pane's status line. Serves as a hint
* informing briefly the user how to make a selection.
* @param stopEvents Custom key strokes which should stop the selection mode.
* For example, it is possible to pass a {@link KeyStroke} corresponding to
* the shortcut of Step Over action. Then, whenever the shorcut is pressed, the selection
* mode is cancelled. The generated {@link KeyEvent} is not consumed thus can be
* handled and invokes Step Over action.
* Note that a method chooser can be always cancelled by Esc or by clicking outside the
* visualized area in the source editor.
* @param confirmEvents Custom key strokes which confirm the current selection.
* By default, a selection can be confirmed by Enter or Space Bar. It is possible
* to extend this set of confirmation keys.
*/
public MethodChooser(String url, Segment[] segments, int initialIndex, String hintText,
KeyStroke[] stopEvents, KeyStroke[] confirmEvents) {
this.url = url;
this.segments = segments;
this.selectedIndex = initialIndex;
this.hintText = hintText;
if (stopEvents == null) {
stopEvents = new KeyStroke[0];
}
if (confirmEvents == null) {
confirmEvents = new KeyStroke[0];
}
this.stopEvents = stopEvents;
this.confirmEvents = confirmEvents;
}
/**
* Sets up and displays the method selection mode.
*
* @return true
if a {@link JEditorPane} has been found and the selection mode
* has been properly displayed
*/
public boolean showUI() {
findEditorPane();
if (editorPane == null) {
return false; // cannot do anything without editor
}
requestFocus(editorPane);
doc = editorPane.getDocument();
// compute start line and end line
int minOffs = Integer.MAX_VALUE;
int maxOffs = 0;
for (int x = 0; x < segments.length; x++) {
minOffs = Math.min(segments[x].getStartOffset(), minOffs);
maxOffs = Math.max(segments[x].getEndOffset(), maxOffs);
}
try {
startLine = Utilities.getLineOffset((BaseDocument)doc, minOffs) + 1;
endLine = Utilities.getLineOffset((BaseDocument)doc, maxOffs) + 1;
} catch (BadLocationException e) {
}
if (SwingUtilities.isEventDispatchThread()) {
showUIinEDT();
} else {
try {
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
showUIinEDT();
}
});
} catch (InterruptedException ex) {
Exceptions.printStackTrace(ex);
} catch (InvocationTargetException ex) {
Exceptions.printStackTrace(ex);
}
}
// continue by showing method selection ui
isInSelectMode = true;
return true;
}
private void showUIinEDT() {
assert SwingUtilities.isEventDispatchThread();
mainListener = new CentralListener();
editorPane.putClientProperty(MethodChooser.class, this);
editorPane.addKeyListener(mainListener);
editorPane.addMouseListener(mainListener);
editorPane.addMouseMotionListener(mainListener);
editorPane.addFocusListener(mainListener);
originalCursor = editorPane.getCursor();
handCursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
arrowCursor = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR);
editorPane.setCursor(arrowCursor);
Caret caret = editorPane.getCaret();
if (caret instanceof BaseCaret) {
((BaseCaret)caret).setVisible(false);
}
requestRepaint();
if (hintText != null && hintText.trim().length() > 0) {
Utilities.setStatusText(editorPane, " " + hintText);
}
}
/**
* Ends the method selection mode, clears all used ui elements. Notifies each registered
* {@link ReleaseListener}.
*
* @param performAction true
indicates that the current selection should
* be used to perform an action, false
means that the selection mode
* has beencancelled
*/
public synchronized void releaseUI(boolean performAction) {
if (!isInSelectMode) {
return; // do nothing
}
getHighlightsBag(doc).clear();
editorPane.removeKeyListener(mainListener);
editorPane.removeMouseListener(mainListener);
editorPane.removeMouseMotionListener(mainListener);
editorPane.removeFocusListener(mainListener);
editorPane.putClientProperty(MethodChooser.class, null);
editorPane.setCursor(originalCursor);
Caret caret = editorPane.getCaret();
if (caret instanceof BaseCaret) {
((BaseCaret)caret).setVisible(true);
}
if (hintText != null && hintText.trim().length() > 0) {
Utilities.clearStatusText(editorPane);
}
isInSelectMode = false;
for (ReleaseListener listener : releaseListeners) {
listener.released(performAction);
}
}
/**
* Can be used to check whether the selection mode is activated.
*
* @return true
if the method selection mode is currently displayed
*/
public boolean isUIActive() {
return isInSelectMode;
}
/**
* Returns index of {@link Segment} that is currently selected. If the method
* chooser has been released, it corresponds to the final selection made by the user.
*
* @return index of currently selected method
*/
public int getSelectedIndex() {
return selectedIndex;
}
/**
* Registers {@link ReleaseListener}. The listener is notified when the selection
* mode finishes. This occurs whenever the user comfirms (or cancels) the current
* selection. It also occrus when {@link #releaseUI} is called.
*
* @param listener an instance of {@link ReleaseListener} to be registered
*/
public synchronized void addReleaseListener(ReleaseListener listener) {
releaseListeners.add(listener);
}
/**
* Unregisters {@link ReleaseListener}.
*
* @param listener an instance of {@link ReleaseListener} to be unregistered
*/
public synchronized void removeReleaseListener(ReleaseListener listener) {
releaseListeners.remove(listener);
}
/**
* This method should be referenced in xml layer files. To display the method
* chooser ui correctly, it is required to register an instance of
* {@link HighlightsLayerFactory} using the following pattern.
*
<folder name="Editors">
<folder name="text">
<folder name="x-java">
<file name="org.netbeans.spi.editor.highlighting.HighlightsLayerFactory.instance">
<attr name="instanceCreate" methodvalue="org.netbeans.spi.debugger.ui.MethodChooser.createHighlihgtsLayerFactory"/>
</file>
</folder>
</folder>
</folder>
* "x-java"
should be replaced by the targeted mime type
*
* @return highligts layer factory that handles method chooser ui visualization
*/
public static HighlightsLayerFactory createHighlihgtsLayerFactory() {
return new MethodChooserHighlightsLayerFactory();
}
static OffsetsBag getHighlightsBag(Document doc) {
OffsetsBag bag = (OffsetsBag) doc.getProperty(MethodChooser.class);
if (bag == null) {
doc.putProperty(MethodChooser.class, bag = new OffsetsBag(doc, true));
}
return bag;
}
private void findEditorPane() {
editorPane = null;
FileObject file;
try {
file = URLMapper.findFileObject(new URL(url));
} catch (MalformedURLException e) {
return;
}
if (file == null) {
return;
}
DataObject dobj = null;
try {
dobj = DataObject.find(file);
} catch (DataObjectNotFoundException ex) {
}
if (dobj == null) {
return;
}
final EditorCookie ec = dobj.getLookup().lookup(EditorCookie.class);
if (SwingUtilities.isEventDispatchThread()) {
JEditorPane[] openedPanes = ec.getOpenedPanes();
if (openedPanes != null) {
editorPane = openedPanes[0];
}
} else {
try {
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
JEditorPane[] openedPanes = ec.getOpenedPanes();
if (openedPanes != null) {
editorPane = openedPanes[0];
}
}
});
} catch (InterruptedException ex) {
Exceptions.printStackTrace(ex);
} catch (InvocationTargetException ex) {
Exceptions.printStackTrace(ex);
}
}
}
private void requestRepaint() {
assert SwingUtilities.isEventDispatchThread();
if (attribsLeft == null) {
Color foreground = editorPane.getForeground();
Color foreground2 = Color.GRAY;
attribsLeft = createAttribs(EditorStyleConstants.LeftBorderLineColor, foreground, EditorStyleConstants.TopBorderLineColor, foreground, EditorStyleConstants.BottomBorderLineColor, foreground);
attribsRight = createAttribs(EditorStyleConstants.RightBorderLineColor, foreground, EditorStyleConstants.TopBorderLineColor, foreground, EditorStyleConstants.BottomBorderLineColor, foreground);
attribsMiddle = createAttribs(EditorStyleConstants.TopBorderLineColor, foreground, EditorStyleConstants.BottomBorderLineColor, foreground);
attribsAll = createAttribs(EditorStyleConstants.LeftBorderLineColor, foreground, EditorStyleConstants.RightBorderLineColor, foreground, EditorStyleConstants.TopBorderLineColor, foreground, EditorStyleConstants.BottomBorderLineColor, foreground);
attribsLeftUnc = createAttribs(EditorStyleConstants.LeftBorderLineColor, foreground2, EditorStyleConstants.TopBorderLineColor, foreground2, EditorStyleConstants.BottomBorderLineColor, foreground2);
attribsRightUnc = createAttribs(EditorStyleConstants.RightBorderLineColor, foreground2, EditorStyleConstants.TopBorderLineColor, foreground2, EditorStyleConstants.BottomBorderLineColor, foreground2);
attribsMiddleUnc = createAttribs(EditorStyleConstants.TopBorderLineColor, foreground2, EditorStyleConstants.BottomBorderLineColor, foreground2);
attribsAllUnc = createAttribs(EditorStyleConstants.LeftBorderLineColor, foreground2, EditorStyleConstants.RightBorderLineColor, foreground2, EditorStyleConstants.TopBorderLineColor, foreground2, EditorStyleConstants.BottomBorderLineColor, foreground2);
attribsHyperlink = getHyperlinkHighlight();
attribsMethod = createAttribs(StyleConstants.Foreground, foreground,
StyleConstants.Bold, Boolean.TRUE);
attribsMethodUnc = createAttribs(StyleConstants.Foreground, foreground2,
StyleConstants.Bold, Boolean.TRUE);
attribsArea = createAttribs(
StyleConstants.Foreground, foreground,
StyleConstants.Italic, Boolean.FALSE,
StyleConstants.Bold, Boolean.FALSE);
}
OffsetsBag newBag = new OffsetsBag(doc, true);
int start = segments[0].getStartOffset();
int end = segments[segments.length - 1].getEndOffset();
newBag.addHighlight(start, end, attribsArea);
for (int i = 0; i < segments.length; i++) {
int startOffset = segments[i].getStartOffset();
int endOffset = segments[i].getEndOffset();
boolean isCertain = !segments[i].getClass().getSimpleName().toLowerCase().contains("uncertain"); // [TODO] temporal hack, Segment API extension is required
newBag.addHighlight(startOffset, endOffset, isCertain ? attribsMethod : attribsMethodUnc);
if (selectedIndex == i) {
int size = endOffset - startOffset;
if (size == 1) {
newBag.addHighlight(startOffset, endOffset, isCertain ? attribsAll : attribsAllUnc);
} else if (size > 1) {
newBag.addHighlight(startOffset, startOffset + 1, isCertain ? attribsLeft : attribsLeftUnc);
newBag.addHighlight(endOffset - 1, endOffset, isCertain ? attribsRight : attribsRightUnc);
if (size > 2) {
newBag.addHighlight(startOffset + 1, endOffset - 1, isCertain ? attribsMiddle : attribsMiddleUnc);
}
}
}
if (mousedIndex == i) {
AttributeSet attr = AttributesUtilities.createComposite(
attribsHyperlink,
AttributesUtilities.createImmutable(EditorStyleConstants.Tooltip, new TooltipResolver())
);
newBag.addHighlight(startOffset, endOffset, attr);
}
}
OffsetsBag bag = getHighlightsBag(doc);
bag.setHighlights(newBag);
}
private AttributeSet createAttribs(Object... keyValuePairs) {
List