All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.netbeans.spi.debugger.ui.MethodChooser Maven / Gradle / Ivy

The newest version!
/*
 * 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 list = new ArrayList(); for (int i = keyValuePairs.length / 2 - 1; i >= 0; i--) { Object attrKey = keyValuePairs[2 * i]; Object attrValue = keyValuePairs[2 * i + 1]; if (attrKey != null && attrValue != null) { list.add(attrKey); list.add(attrValue); } } return AttributesUtilities.createImmutable(list.toArray()); } private AttributeSet getHyperlinkHighlight() { synchronized(this) { if (defaultHyperlinkHighlight == null) { defaultHyperlinkHighlight = AttributesUtilities.createImmutable( StyleConstants.Foreground, Color.BLUE, StyleConstants.Underline, Color.BLUE); } } return defaultHyperlinkHighlight; } private static void requestFocus(final JEditorPane editorPane) { if (!SwingUtilities.isEventDispatchThread()) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { requestFocus(editorPane); } }); return ; } Container p = editorPane; while ((p = p.getParent()) != null) { if (p instanceof TopComponent) { ((TopComponent) p).requestActive(); break; } } editorPane.requestFocusInWindow(); } // ************************************************************************** // public inner classes // ************************************************************************** /** * Represents an interval of offsets in a document. Used to pass entry points * of method calls among which the user selects the desired one to step into. */ public static class Segment { int startOffset; int endOffset; /** * Creates a new instance of {@link Segment}. * * @param startOffset segment start offset (inclusive) * @param endOffset segment end offset (exclusive) */ public Segment(int startOffset, int endOffset) { this.startOffset = startOffset; this.endOffset = endOffset; } /** * Returns the start offset. * * @return the start offset */ public int getStartOffset() { return startOffset; } /** * Returns the endt offset. * * @return the end offset */ public int getEndOffset() { return endOffset; } } /** * An instance of {@link ReleaseListener} can be registered using {@link MethodChooser#addReleaseListener}. * It is notified when the selection mode finishes (e.g. if the user confirms the current selection). * The selection mode finishes whenever {@link #releaseUI} is called. */ public interface ReleaseListener { /** * Called on the method selection mode finish. * * @param performAction true means that the current selection has been confirmed * and the proper action (Step Into) is expected to be performed, false * means that the method chooser has been cancelled */ public void released(boolean performAction); } // ************************************************************************** // private inner classes // ************************************************************************** private class CentralListener implements KeyListener, MouseListener, MouseMotionListener, FocusListener { // ************************************************************************** // KeyListener implementation // ************************************************************************** @Override public void keyTyped(KeyEvent e) { e.consume(); } @Override public void keyPressed(KeyEvent e) { int code = e.getKeyCode(); boolean consumeEvent = true; switch (code) { case KeyEvent.VK_ENTER: case KeyEvent.VK_SPACE: // selection confirmed releaseUI(true); break; case KeyEvent.VK_ESCAPE: // action canceled releaseUI(false); break; case KeyEvent.VK_RIGHT: case KeyEvent.VK_DOWN: case KeyEvent.VK_TAB: selectedIndex++; if (selectedIndex == segments.length) { selectedIndex = 0; } requestRepaint(); break; case KeyEvent.VK_LEFT: case KeyEvent.VK_UP: selectedIndex--; if (selectedIndex < 0) { selectedIndex = segments.length - 1; } requestRepaint(); break; case KeyEvent.VK_HOME: selectedIndex = 0; requestRepaint(); break; case KeyEvent.VK_END: selectedIndex = segments.length - 1; requestRepaint(); break; default: int mods = e.getModifiersEx(); for (int x = 0; x < stopEvents.length; x++) { if (stopEvents[x].getKeyCode() == code && (stopEvents[x].getModifiers() & mods) == stopEvents[x].getModifiers()) { releaseUI(false); consumeEvent = false; break; } } for (int x = 0; x < confirmEvents.length; x++) { if (confirmEvents[x].getKeyCode() == code && (confirmEvents[x].getModifiers() & mods) == confirmEvents[x].getModifiers()) { releaseUI(true); break; } } } if (consumeEvent) { e.consume(); } } @Override public void keyReleased(KeyEvent e) { e.consume(); } // ************************************************************************** // MouseListener and MouseMotionListener implementation // ************************************************************************** @Override public void mouseClicked(MouseEvent e) { if (e.isPopupTrigger()) { return; } e.consume(); int position = editorPane.viewToModel(e.getPoint()); if (e.getClickCount() == 1 && e.getButton() == MouseEvent.BUTTON1) { if (position < 0) { return; } if (mousedIndex != -1) { selectedIndex = mousedIndex; releaseUI(true); return; } } try { int line = Utilities.getLineOffset((BaseDocument) doc, position) + 1; if (line < startLine || line > endLine) { releaseUI(false); return; } } catch (BadLocationException ex) { } } @Override public void mouseMoved(MouseEvent e) { e.consume(); int position = editorPane.viewToModel(e.getPoint()); int newIndex = -1; if (position >= 0) { for (int x = 0; x < segments.length; x++) { int start = segments[x].getStartOffset(); int end = segments[x].getEndOffset(); if (position >= start && position <= end) { newIndex = x; break; } } // for } // if if (newIndex != mousedIndex) { if (newIndex == -1) { editorPane.setCursor(arrowCursor); } else { editorPane.setCursor(handCursor); } mousedIndex = newIndex; requestRepaint(); } } @Override public void mouseReleased(MouseEvent e) { e.consume(); } @Override public void mousePressed(MouseEvent e) { e.consume(); } @Override public void mouseExited(MouseEvent e) { e.consume(); } @Override public void mouseEntered(MouseEvent e) { e.consume(); } @Override public void mouseDragged(MouseEvent e) { e.consume(); } // ************************************************************************** // FocusListener implementation // ************************************************************************** @Override public void focusGained(FocusEvent e) { editorPane.getCaret().setVisible(false); } @Override public void focusLost(FocusEvent e) { } } static class MethodChooserHighlightsLayerFactory implements HighlightsLayerFactory { @Override public HighlightsLayer[] createLayers(Context context) { return new HighlightsLayer[] { HighlightsLayer.create(MethodChooser.class.getName(), ZOrder.SYNTAX_RACK.forPosition(10000), false, MethodChooser.getHighlightsBag(context.getDocument())) }; } } private static final class TooltipResolver implements HighlightAttributeValue { public TooltipResolver() { } @Override public String getValue(JTextComponent component, Document document, Object attributeKey, int startOffset, int endOffset) { return NbBundle.getMessage(MethodChooser.class, "MSG_Step_Into_Method"); } } }