mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-04 18:29:37 +02:00
Merge remote-tracking branch 'remotes/origin/GT-2973-dragonmacher-snapshot-navigation-arrows'
This commit is contained in:
commit
6d05537b7f
17 changed files with 768 additions and 402 deletions
|
@ -19,6 +19,7 @@ import java.awt.event.InputEvent;
|
|||
import java.awt.event.KeyEvent;
|
||||
import java.util.*;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.ImageIcon;
|
||||
|
||||
import docking.ActionContext;
|
||||
|
@ -195,8 +196,9 @@ public class NextPrevAddressPlugin extends Plugin {
|
|||
}
|
||||
|
||||
Navigatable navigatable = getNavigatable(context);
|
||||
return historyService.hasNext(navigatable) ||
|
||||
historyService.hasPrevious(navigatable);
|
||||
boolean hasNext = historyService.hasNext(navigatable);
|
||||
boolean hasPrevious = historyService.hasPrevious(navigatable);
|
||||
return hasNext || hasPrevious;
|
||||
}
|
||||
};
|
||||
clearAction.setHelpLocation(new HelpLocation(HelpTopics.NAVIGATION, clearAction.getName()));
|
||||
|
@ -289,11 +291,9 @@ public class NextPrevAddressPlugin extends Plugin {
|
|||
return null;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// //
|
||||
// Inner Classes //
|
||||
// //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
//==================================================================================================
|
||||
// Inner Classes
|
||||
//==================================================================================================
|
||||
|
||||
private class NextPreviousAction extends MultiActionDockingAction {
|
||||
|
||||
|
@ -311,7 +311,12 @@ public class NextPrevAddressPlugin extends Plugin {
|
|||
|
||||
@Override
|
||||
public boolean isValidContext(ActionContext context) {
|
||||
return (context instanceof NavigatableActionContext);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidGlobalContext(ActionContext globalContext) {
|
||||
return (globalContext instanceof NavigatableActionContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -374,8 +379,9 @@ public class NextPrevAddressPlugin extends Plugin {
|
|||
this.service = service;
|
||||
this.navigatable = navigatable;
|
||||
|
||||
setMenuBarData(new MenuData(new String[] { buildActionName(location, formatter) },
|
||||
navigatable.getNavigatableIcon()));
|
||||
Icon navIcon = navigatable.getNavigatableIcon();
|
||||
setMenuBarData(
|
||||
new MenuData(new String[] { buildActionName(location, formatter) }, navIcon));
|
||||
setEnabled(true);
|
||||
}
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ public class HorizontalRuleAction extends DockingAction {
|
|||
*
|
||||
* @param owner the action owner
|
||||
* @param topName the name that will appear above the separator bar
|
||||
* @param bottomName the name that will apppear below the separator bar
|
||||
* @param bottomName the name that will appear below the separator bar
|
||||
*/
|
||||
public HorizontalRuleAction(String owner, String topName, String bottomName) {
|
||||
super("HorizontalRuleAction: " + ++idCount, owner, false);
|
||||
|
|
|
@ -66,8 +66,7 @@ import utilities.util.reflection.ReflectionUtilities;
|
|||
* <b><u>Show Provider Action</u></b> - Each provider has an action to show the provider. For
|
||||
* typical, non-transient providers (see {@link #setTransient()}) the action will appear in
|
||||
* the tool's <b>Window</b> menu. You can have your provider also appear in the tool's toolbar
|
||||
* by calling {@link #setIcon(Icon, boolean)}, passing <code>true</code> for
|
||||
* <code>isToolbarAction</code>.
|
||||
* by calling {@link #addToTool()}.
|
||||
* <p>
|
||||
* Historical Note: This class was created so that implementors could add local actions within the constructor
|
||||
* without having to understand that they must first add themselves to the WindowManager.
|
||||
|
@ -805,7 +804,15 @@ public abstract class ComponentProvider implements HelpDescriptor, ActionContext
|
|||
|
||||
@Override
|
||||
public void actionPerformed(ActionContext context) {
|
||||
dockingTool.showComponentProvider(ComponentProvider.this, true);
|
||||
|
||||
DockingWindowManager myDwm = DockingWindowManager.getInstance(getComponent());
|
||||
if (myDwm == null) {
|
||||
// don't think this can happen
|
||||
dockingTool.showComponentProvider(ComponentProvider.this, true);
|
||||
return;
|
||||
}
|
||||
|
||||
myDwm.showComponent(ComponentProvider.this, true, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -42,13 +42,9 @@ public class DockableComponent extends JPanel implements ContainerListener {
|
|||
public static ComponentPlaceholder SOURCE_INFO;
|
||||
public static boolean DROP_CODE_SET;
|
||||
|
||||
enum DropCode {
|
||||
INVALID, STACK, LEFT, RIGHT, TOP, BOTTOM, ROOT, WINDOW
|
||||
}
|
||||
|
||||
private DockableHeader header;
|
||||
private MouseListener popupListener;
|
||||
private ComponentPlaceholder componentInfo;
|
||||
private ComponentPlaceholder placeholder;
|
||||
private JComponent providerComp;
|
||||
private Component focusedComponent;
|
||||
private DockingWindowManager winMgr;
|
||||
|
@ -62,7 +58,7 @@ public class DockableComponent extends JPanel implements ContainerListener {
|
|||
*/
|
||||
DockableComponent(ComponentPlaceholder placeholder, boolean isDocking) {
|
||||
if (placeholder != null) {
|
||||
this.componentInfo = placeholder;
|
||||
this.placeholder = placeholder;
|
||||
|
||||
winMgr = placeholder.getNode().winMgr;
|
||||
actionMgr = winMgr.getActionToGuiMapper();
|
||||
|
@ -108,11 +104,11 @@ public class DockableComponent extends JPanel implements ContainerListener {
|
|||
}
|
||||
}
|
||||
|
||||
private JComponent initializeComponentPlaceholder(ComponentPlaceholder placeholder) {
|
||||
JComponent providerComponent = placeholder.getProviderComponent();
|
||||
private JComponent initializeComponentPlaceholder(ComponentPlaceholder newPlaceholder) {
|
||||
JComponent providerComponent = newPlaceholder.getProviderComponent();
|
||||
|
||||
// Ensure that every provider component has a registered help location
|
||||
ComponentProvider provider = placeholder.getProvider();
|
||||
ComponentProvider provider = newPlaceholder.getProvider();
|
||||
HelpLocation helpLocation = provider.getHelpLocation();
|
||||
HelpLocation location = registerHelpLocation(provider, helpLocation);
|
||||
|
||||
|
@ -167,45 +163,38 @@ public class DockableComponent extends JPanel implements ContainerListener {
|
|||
boolean withinBounds = bounds.contains(point);
|
||||
|
||||
if (e.isPopupTrigger() && withinBounds) {
|
||||
actionMgr.showPopupMenu(componentInfo, e);
|
||||
actionMgr.showPopupMenu(placeholder, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see java.awt.Component#getMinimumSize()
|
||||
*/
|
||||
@Override
|
||||
public Dimension getMinimumSize() {
|
||||
return MIN_DIM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user component that this wraps.
|
||||
*/
|
||||
JComponent getProviderComponent() {
|
||||
return providerComp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the info object associated with this DockableComponent.
|
||||
* Returns the placeholder object associated with this DockableComponent
|
||||
* @return the placeholder object associated with this DockableComponent
|
||||
*/
|
||||
public ComponentPlaceholder getComponentWindowingPlaceholder() {
|
||||
return componentInfo;
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (componentInfo == null) {
|
||||
if (placeholder == null) {
|
||||
return "";
|
||||
}
|
||||
return componentInfo.getFullTitle();
|
||||
return placeholder.getFullTitle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up for drag and drop.
|
||||
*
|
||||
* Translates the given point so that it is relative to the given component
|
||||
*/
|
||||
|
||||
private void translate(Point p, Component c) {
|
||||
Point cLoc = c.getLocationOnScreen();
|
||||
Point myLoc = getLocationOnScreen();
|
||||
|
@ -219,10 +208,6 @@ public class DockableComponent extends JPanel implements ContainerListener {
|
|||
super(comp, null);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @see java.awt.dnd.DropTargetListener#drop(java.awt.dnd.DropTargetDropEvent)
|
||||
*/
|
||||
@Override
|
||||
public synchronized void drop(DropTargetDropEvent dtde) {
|
||||
clearAutoscroll();
|
||||
|
@ -230,7 +215,7 @@ public class DockableComponent extends JPanel implements ContainerListener {
|
|||
Point p = dtde.getLocation();
|
||||
translate(p, ((DropTarget) dtde.getSource()).getComponent());
|
||||
setDropCode(p);
|
||||
TARGET_INFO = componentInfo;
|
||||
TARGET_INFO = placeholder;
|
||||
dtde.acceptDrop(dtde.getDropAction());
|
||||
dtde.dropComplete(true);
|
||||
}
|
||||
|
@ -239,10 +224,6 @@ public class DockableComponent extends JPanel implements ContainerListener {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @see java.awt.dnd.DropTargetListener#dragEnter(java.awt.dnd.DropTargetDragEvent)
|
||||
*/
|
||||
@Override
|
||||
public synchronized void dragEnter(DropTargetDragEvent dtde) {
|
||||
super.dragEnter(dtde);
|
||||
|
@ -258,7 +239,7 @@ public class DockableComponent extends JPanel implements ContainerListener {
|
|||
Point p = dtde.getLocation();
|
||||
translate(p, ((DropTarget) dtde.getSource()).getComponent());
|
||||
setDropCode(p);
|
||||
DRAGGED_OVER_INFO = componentInfo;
|
||||
DRAGGED_OVER_INFO = placeholder;
|
||||
dtde.acceptDrag(dtde.getDropAction());
|
||||
}
|
||||
else {
|
||||
|
@ -266,10 +247,6 @@ public class DockableComponent extends JPanel implements ContainerListener {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @see java.awt.dnd.DropTargetListener#dragOver(java.awt.dnd.DropTargetDragEvent)
|
||||
*/
|
||||
@Override
|
||||
public synchronized void dragOver(DropTargetDragEvent dtde) {
|
||||
super.dragOver(dtde);
|
||||
|
@ -285,7 +262,7 @@ public class DockableComponent extends JPanel implements ContainerListener {
|
|||
Point p = dtde.getLocation();
|
||||
translate(p, ((DropTarget) dtde.getSource()).getComponent());
|
||||
setDropCode(p);
|
||||
DRAGGED_OVER_INFO = componentInfo;
|
||||
DRAGGED_OVER_INFO = placeholder;
|
||||
dtde.acceptDrag(dtde.getDropAction());
|
||||
}
|
||||
else {
|
||||
|
@ -293,10 +270,6 @@ public class DockableComponent extends JPanel implements ContainerListener {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @see java.awt.dnd.DropTargetListener#dragExit(java.awt.dnd.DropTargetEvent)
|
||||
*/
|
||||
@Override
|
||||
public synchronized void dragExit(DropTargetEvent dte) {
|
||||
super.dragExit(dte);
|
||||
|
@ -362,7 +335,7 @@ public class DockableComponent extends JPanel implements ContainerListener {
|
|||
private void setDropCode(Point p) {
|
||||
DROP_CODE_SET = true;
|
||||
|
||||
if (componentInfo == null) {
|
||||
if (placeholder == null) {
|
||||
DROP_CODE = DropCode.ROOT;
|
||||
return;
|
||||
}
|
||||
|
@ -370,11 +343,11 @@ public class DockableComponent extends JPanel implements ContainerListener {
|
|||
DROP_CODE = DropCode.WINDOW;
|
||||
return;
|
||||
}
|
||||
if (SOURCE_INFO.getNode().winMgr != componentInfo.getNode().winMgr) {
|
||||
if (SOURCE_INFO.getNode().winMgr != placeholder.getNode().winMgr) {
|
||||
DROP_CODE = DropCode.WINDOW;
|
||||
return;
|
||||
}
|
||||
if (SOURCE_INFO == componentInfo && !componentInfo.isStacked()) {
|
||||
if (SOURCE_INFO == placeholder && !placeholder.isStacked()) {
|
||||
DROP_CODE = DropCode.INVALID;
|
||||
return;
|
||||
}
|
||||
|
@ -390,7 +363,7 @@ public class DockableComponent extends JPanel implements ContainerListener {
|
|||
else if (p.y > getHeight() - DROP_EDGE_OFFSET) {
|
||||
DROP_CODE = DropCode.BOTTOM;
|
||||
}
|
||||
else if (SOURCE_INFO == componentInfo) {
|
||||
else if (SOURCE_INFO == placeholder) {
|
||||
DROP_CODE = DropCode.INVALID;
|
||||
}
|
||||
else {
|
||||
|
@ -409,10 +382,6 @@ public class DockableComponent extends JPanel implements ContainerListener {
|
|||
header.emphasize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the title displayed in this component's header.
|
||||
* @param title
|
||||
*/
|
||||
void setTitle(String title) {
|
||||
header.setTitle(title);
|
||||
}
|
||||
|
@ -421,13 +390,10 @@ public class DockableComponent extends JPanel implements ContainerListener {
|
|||
header.setIcon(icon);
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases all resources for this object.
|
||||
*/
|
||||
void dispose() {
|
||||
header.dispose();
|
||||
header = null;
|
||||
componentInfo = null;
|
||||
placeholder = null;
|
||||
providerComp = null;
|
||||
actionMgr = null;
|
||||
}
|
||||
|
|
|
@ -19,24 +19,29 @@ import java.awt.*;
|
|||
import java.awt.dnd.*;
|
||||
import java.awt.event.InputEvent;
|
||||
import java.awt.event.MouseListener;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.*;
|
||||
|
||||
import org.jdesktop.animation.timing.Animator;
|
||||
import org.jdesktop.animation.timing.Animator.RepeatBehavior;
|
||||
import org.jdesktop.animation.timing.TimingTargetAdapter;
|
||||
import org.jdesktop.animation.timing.interpolation.PropertySetter;
|
||||
|
||||
import docking.help.Help;
|
||||
import docking.help.HelpService;
|
||||
import docking.util.AnimationUtils;
|
||||
import generic.util.WindowUtilities;
|
||||
import generic.util.image.ImageUtils;
|
||||
import ghidra.framework.OperatingSystem;
|
||||
import ghidra.framework.Platform;
|
||||
import ghidra.util.HelpLocation;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.bean.GGlassPane;
|
||||
import ghidra.util.bean.GGlassPanePainter;
|
||||
import resources.ResourceManager;
|
||||
|
||||
/**
|
||||
* Component for providing component titles and toolbar. Also provides Drag
|
||||
|
@ -46,41 +51,6 @@ public class DockableHeader extends GenericHeader
|
|||
implements DragGestureListener, DragSourceListener {
|
||||
|
||||
private DockableComponent dockComp;
|
||||
private static Cursor leftCursor;
|
||||
private static Cursor rightCursor;
|
||||
private static Cursor topCursor;
|
||||
private static Cursor bottomCursor;
|
||||
private static Cursor stackCursor;
|
||||
private static Cursor newWindowCursor;
|
||||
private static Cursor noDropCursor = DragSource.DefaultMoveNoDrop;
|
||||
|
||||
static {
|
||||
Toolkit tk = Toolkit.getDefaultToolkit();
|
||||
|
||||
BufferedImage image = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
|
||||
drawLeftArrow(image);
|
||||
leftCursor = tk.createCustomCursor(image, new Point(0, 6), "LEFT");
|
||||
|
||||
image = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
|
||||
drawRightArrow(image);
|
||||
rightCursor = tk.createCustomCursor(image, new Point(31, 6), "RIGHT");
|
||||
|
||||
image = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
|
||||
drawTopArrow(image);
|
||||
topCursor = tk.createCustomCursor(image, new Point(6, 0), "TOP");
|
||||
|
||||
image = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
|
||||
drawBottomArrow(image);
|
||||
bottomCursor = tk.createCustomCursor(image, new Point(6, 31), "BOTTOM");
|
||||
|
||||
image = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
|
||||
drawStack(image);
|
||||
stackCursor = tk.createCustomCursor(image, new Point(8, 8), "STACK");
|
||||
|
||||
image = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
|
||||
drawNewWindow(image);
|
||||
newWindowCursor = tk.createCustomCursor(image, new Point(0, 0), "NEW_WINDOW");
|
||||
}
|
||||
|
||||
private DragCursorManager dragCursorManager = createDragCursorManager();
|
||||
private DragSource dragSource = null;
|
||||
|
@ -174,17 +144,61 @@ public class DockableHeader extends GenericHeader
|
|||
protected Animator createEmphasizingAnimator(JFrame parentFrame) {
|
||||
|
||||
double random = Math.random();
|
||||
int choices = 4;
|
||||
int choices = 7;
|
||||
int value = (int) (choices * random);
|
||||
|
||||
switch (value) {
|
||||
case 0:
|
||||
return AnimationUtils.shakeComponent(component);
|
||||
case 1:
|
||||
return AnimationUtils.rotateComponent(component);
|
||||
case 2:
|
||||
return raiseComponent(parentFrame);
|
||||
default:
|
||||
return AnimationUtils.pulseComponent(component);
|
||||
case 3:
|
||||
return AnimationUtils.showTheDragonOverComponent(component);
|
||||
case 4:
|
||||
return AnimationUtils.focusComponent(component);
|
||||
case 5:
|
||||
return emphasizeDockableComponent();
|
||||
default:
|
||||
return raiseComponent(parentFrame);
|
||||
}
|
||||
}
|
||||
|
||||
private Animator emphasizeDockableComponent() {
|
||||
|
||||
ComponentPlaceholder placeholder = dockComp.getComponentWindowingPlaceholder();
|
||||
ComponentNode node = placeholder.getNode();
|
||||
WindowNode windowNode = node.getTopLevelNode();
|
||||
Set<ComponentNode> componentNodes = new HashSet<>();
|
||||
getComponents(windowNode, componentNodes);
|
||||
|
||||
//@formatter:off
|
||||
Set<Component> components = componentNodes.stream()
|
||||
.map(cn -> cn.getComponent())
|
||||
.filter(c -> c != null)
|
||||
.filter(c -> !SwingUtilities.isDescendingFrom(component, c))
|
||||
.collect(Collectors.toSet())
|
||||
;
|
||||
//@formatter:on
|
||||
|
||||
components.remove(component);
|
||||
|
||||
EmphasizeDockableComponentAnimationDriver driver =
|
||||
new EmphasizeDockableComponentAnimationDriver(component, components);
|
||||
return driver.animator;
|
||||
}
|
||||
|
||||
private void getComponents(Node node, Set<ComponentNode> results) {
|
||||
|
||||
List<Node> children = node.getChildren();
|
||||
for (Node child : children) {
|
||||
if (child instanceof ComponentNode) {
|
||||
results.add((ComponentNode) child);
|
||||
}
|
||||
else {
|
||||
getComponents(child, results);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -235,7 +249,7 @@ public class DockableHeader extends GenericHeader
|
|||
(modifiers & InputEvent.BUTTON3_DOWN_MASK) != 0) {
|
||||
return;
|
||||
}
|
||||
DockableComponent.DROP_CODE = DockableComponent.DropCode.WINDOW;
|
||||
DockableComponent.DROP_CODE = DropCode.WINDOW;
|
||||
DockableComponent.DROP_CODE_SET = true;
|
||||
DockableComponent.SOURCE_INFO = dockComp.getComponentWindowingPlaceholder();
|
||||
|
||||
|
@ -252,7 +266,7 @@ public class DockableHeader extends GenericHeader
|
|||
|
||||
ComponentPlaceholder info = dockComp.getComponentWindowingPlaceholder();
|
||||
DockingWindowManager winMgr = info.getNode().winMgr;
|
||||
if (DockableComponent.DROP_CODE == DockableComponent.DropCode.INVALID) {
|
||||
if (DockableComponent.DROP_CODE == DropCode.INVALID) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -262,28 +276,12 @@ public class DockableHeader extends GenericHeader
|
|||
// return;
|
||||
// }
|
||||
// else
|
||||
if (DockableComponent.DROP_CODE == DockableComponent.DropCode.WINDOW) {
|
||||
if (DockableComponent.DROP_CODE == DropCode.WINDOW) {
|
||||
winMgr.movePlaceholder(info, event.getLocation());
|
||||
}
|
||||
else {
|
||||
winMgr.movePlaceholder(info, DockableComponent.TARGET_INFO, getWindowPosition());
|
||||
}
|
||||
}
|
||||
|
||||
private WindowPosition getWindowPosition() {
|
||||
switch (DockableComponent.DROP_CODE) {
|
||||
case BOTTOM:
|
||||
return WindowPosition.BOTTOM;
|
||||
case LEFT:
|
||||
return WindowPosition.LEFT;
|
||||
case RIGHT:
|
||||
return WindowPosition.RIGHT;
|
||||
case STACK:
|
||||
return WindowPosition.STACK;
|
||||
case TOP:
|
||||
return WindowPosition.TOP;
|
||||
default:
|
||||
return WindowPosition.STACK;
|
||||
winMgr.movePlaceholder(info, DockableComponent.TARGET_INFO,
|
||||
DockableComponent.DROP_CODE.getWindowPosition());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -314,34 +312,7 @@ public class DockableHeader extends GenericHeader
|
|||
// }
|
||||
|
||||
DockableComponent.DROP_CODE_SET = false;
|
||||
Cursor c = noDropCursor;
|
||||
switch (DockableComponent.DROP_CODE) {
|
||||
case LEFT:
|
||||
c = leftCursor;
|
||||
break;
|
||||
case RIGHT:
|
||||
c = rightCursor;
|
||||
break;
|
||||
case TOP:
|
||||
c = topCursor;
|
||||
break;
|
||||
case BOTTOM:
|
||||
c = bottomCursor;
|
||||
break;
|
||||
case STACK:
|
||||
c = stackCursor;
|
||||
break;
|
||||
case ROOT:
|
||||
c = stackCursor;
|
||||
break;
|
||||
case WINDOW:
|
||||
c = newWindowCursor;
|
||||
break;
|
||||
case INVALID:
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
Cursor c = DockableComponent.DROP_CODE.getCursor();
|
||||
dragCursorManager.setCursor(event, c);
|
||||
}
|
||||
|
||||
|
@ -467,129 +438,196 @@ public class DockableHeader extends GenericHeader
|
|||
}
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
// Static Methods
|
||||
//==================================================================================================
|
||||
public static class EmphasizeDockableComponentAnimationDriver {
|
||||
|
||||
/**
|
||||
* Draws the left arrow cursor image.
|
||||
*
|
||||
* @param image the image object to draw into.
|
||||
*/
|
||||
private static void drawLeftArrow(BufferedImage image) {
|
||||
int v = 0xff000000;
|
||||
int y = 6;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
for (int j = 0; j < 2 * i + 1; j++) {
|
||||
image.setRGB(i, y - i + j, v);
|
||||
private Animator animator;
|
||||
private GGlassPane glassPane;
|
||||
private EmphasizeDockableComponentPainter rotatePainter;
|
||||
|
||||
EmphasizeDockableComponentAnimationDriver(Component component, Set<Component> others) {
|
||||
|
||||
glassPane = AnimationUtils.getGlassPane(component);
|
||||
rotatePainter = new EmphasizeDockableComponentPainter(component, others);
|
||||
|
||||
double start = 0;
|
||||
double max = 1;
|
||||
int duration = 1000;
|
||||
animator = PropertySetter.createAnimator(duration, this, "percentComplete", start, max);
|
||||
|
||||
animator.setAcceleration(0.2f);
|
||||
animator.setDeceleration(0.8f);
|
||||
|
||||
animator.setRepeatCount(2);
|
||||
animator.setRepeatBehavior(RepeatBehavior.REVERSE);
|
||||
|
||||
animator.addTarget(new TimingTargetAdapter() {
|
||||
@Override
|
||||
public void end() {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
glassPane.addPainter(rotatePainter);
|
||||
|
||||
animator.start();
|
||||
}
|
||||
|
||||
public void setPercentComplete(double percentComplete) {
|
||||
rotatePainter.setPercentComplete(percentComplete);
|
||||
glassPane.repaint();
|
||||
}
|
||||
|
||||
void done() {
|
||||
glassPane.repaint();
|
||||
glassPane.removePainter(rotatePainter);
|
||||
}
|
||||
}
|
||||
|
||||
private static class EmphasizeDockableComponentPainter implements GGlassPanePainter {
|
||||
|
||||
private Set<ComponentPaintInfo> otherComponentInfos = new HashSet<>();
|
||||
private Image image;
|
||||
|
||||
private Component component;
|
||||
private Rectangle cBounds;
|
||||
private double percentComplete = 0.0;
|
||||
|
||||
EmphasizeDockableComponentPainter(Component component, Set<Component> otherComponents) {
|
||||
this.component = component;
|
||||
this.image = ImageUtils.createImage(component);
|
||||
|
||||
for (Component otherComponent : otherComponents) {
|
||||
ComponentPaintInfo info = new ComponentPaintInfo(otherComponent);
|
||||
otherComponentInfos.add(info);
|
||||
}
|
||||
}
|
||||
for (int i = 6; i < 12; i++) {
|
||||
for (int j = 0; j < 3; j++) {
|
||||
image.setRGB(i, y - 1 + j, v);
|
||||
|
||||
private class ComponentPaintInfo {
|
||||
|
||||
private Component myComponent;
|
||||
private Image myImage;
|
||||
|
||||
ComponentPaintInfo(Component component) {
|
||||
this.myComponent = component;
|
||||
this.myImage = ImageUtils.createImage(component);
|
||||
}
|
||||
|
||||
Image getImage() {
|
||||
return myImage;
|
||||
}
|
||||
|
||||
Rectangle getRelativeBounds(Component other) {
|
||||
Rectangle r = myComponent.getBounds();
|
||||
return SwingUtilities.convertRectangle(myComponent.getParent(), r, other);
|
||||
}
|
||||
}
|
||||
|
||||
void setPercentComplete(double percent) {
|
||||
percentComplete = percent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paint(GGlassPane glassPane, Graphics g) {
|
||||
|
||||
Graphics2D g2d = (Graphics2D) g;
|
||||
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
|
||||
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
|
||||
Color background = new Color(218, 232, 250);
|
||||
g.setColor(background);
|
||||
|
||||
Rectangle othersBounds = null;
|
||||
for (ComponentPaintInfo info : otherComponentInfos) {
|
||||
|
||||
Rectangle b = info.getRelativeBounds(glassPane);
|
||||
if (othersBounds == null) {
|
||||
othersBounds = b;
|
||||
}
|
||||
else {
|
||||
othersBounds.add(b);
|
||||
}
|
||||
}
|
||||
|
||||
if (othersBounds == null) {
|
||||
// No other components in this window. In this case, use the bounds of the
|
||||
// active component. This has the effect of showing the image behind the
|
||||
// active component.
|
||||
Rectangle componentBounds = component.getBounds();
|
||||
componentBounds = SwingUtilities.convertRectangle(component.getParent(),
|
||||
componentBounds, glassPane);
|
||||
othersBounds = componentBounds;
|
||||
|
||||
othersBounds = new Rectangle();
|
||||
}
|
||||
|
||||
g2d.fillRect(othersBounds.x, othersBounds.y, othersBounds.width, othersBounds.height);
|
||||
|
||||
ImageIcon ghidra = ResourceManager.loadImage("images/GhidraIcon256.png");
|
||||
Image ghidraImage = ghidra.getImage();
|
||||
|
||||
double scale = percentComplete * 7;
|
||||
int gw = ghidraImage.getWidth(null);
|
||||
int gh = ghidraImage.getHeight(null);
|
||||
int w = (int) (gw * scale);
|
||||
int h = (int) (gh * scale);
|
||||
|
||||
Rectangle gpBounds = glassPane.getBounds();
|
||||
double cx = gpBounds.getCenterX();
|
||||
double cy = gpBounds.getCenterY();
|
||||
int offsetX = (int) (cx - (w >> 1));
|
||||
int offsetY = (int) (cy - (h >> 1));
|
||||
|
||||
Shape originalClip = g2d.getClip();
|
||||
if (!othersBounds.isEmpty()) {
|
||||
// restrict the icon to the 'others' area; otherwise, place it behind the provider
|
||||
g2d.setClip(othersBounds);
|
||||
}
|
||||
g2d.drawImage(ghidraImage, offsetX, offsetY, w, h, null);
|
||||
g2d.setClip(originalClip);
|
||||
|
||||
paintOthers(glassPane, (Graphics2D) g, background);
|
||||
|
||||
Rectangle b = component.getBounds();
|
||||
Point p = new Point(b.getLocation());
|
||||
p = SwingUtilities.convertPoint(component.getParent(), p, glassPane);
|
||||
|
||||
g2d.setRenderingHints(new RenderingHints(null));
|
||||
g2d.drawImage(image, p.x, p.y, b.width, b.height, null);
|
||||
}
|
||||
|
||||
private void paintOthers(GGlassPane glassPane, Graphics2D g2d, Color background) {
|
||||
|
||||
if (cBounds == null) {
|
||||
cBounds = component.getBounds();
|
||||
cBounds =
|
||||
SwingUtilities.convertRectangle(component.getParent(), cBounds, glassPane);
|
||||
}
|
||||
|
||||
double destinationX = cBounds.getCenterX();
|
||||
double destinationY = cBounds.getCenterY();
|
||||
|
||||
g2d.setColor(background);
|
||||
for (ComponentPaintInfo info : otherComponentInfos) {
|
||||
|
||||
Rectangle b = info.getRelativeBounds(glassPane);
|
||||
double scale = 1 - percentComplete;
|
||||
int w = (int) (b.width * scale);
|
||||
int h = (int) (b.height * scale);
|
||||
|
||||
int offsetX = b.x - ((w - b.width) >> 1);
|
||||
int offsetY = b.y - ((h - b.height) >> 1);
|
||||
|
||||
double deltaX = destinationX - offsetX;
|
||||
double deltaY = destinationY - offsetY;
|
||||
|
||||
double moveX = percentComplete * deltaX;
|
||||
double moveY = percentComplete * deltaY;
|
||||
offsetX += moveX;
|
||||
offsetY += moveY;
|
||||
|
||||
g2d.drawImage(info.getImage(), offsetX, offsetY, w, h, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the right arrow cursor image.
|
||||
*
|
||||
* @param image the image object to draw into.
|
||||
*/
|
||||
private static void drawRightArrow(BufferedImage image) {
|
||||
int v = 0xff000000;
|
||||
int y = 6;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
for (int j = 0; j < 2 * i + 1; j++) {
|
||||
image.setRGB(31 - i, y - i + j, v);
|
||||
}
|
||||
}
|
||||
for (int i = 6; i < 12; i++) {
|
||||
for (int j = 0; j < 3; j++) {
|
||||
image.setRGB(31 - i, y - 1 + j, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the up arrow cursor image.
|
||||
*
|
||||
* @param image the image object to draw into.
|
||||
*/
|
||||
private static void drawTopArrow(BufferedImage image) {
|
||||
int v = 0xff000000;
|
||||
int x = 6;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
for (int j = 0; j < 2 * i + 1; j++) {
|
||||
image.setRGB(x - i + j, i, v);
|
||||
}
|
||||
}
|
||||
for (int i = 6; i < 12; i++) {
|
||||
for (int j = 0; j < 3; j++) {
|
||||
image.setRGB(x - 1 + j, i, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the down arrow cursor image.
|
||||
*
|
||||
* @param image the image object to draw into.
|
||||
*/
|
||||
private static void drawBottomArrow(BufferedImage image) {
|
||||
int v = 0xff000000;
|
||||
int x = 6;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
for (int j = 0; j < 2 * i + 1; j++) {
|
||||
image.setRGB(x - i + j, 31 - i, v);
|
||||
}
|
||||
}
|
||||
for (int i = 6; i < 12; i++) {
|
||||
for (int j = 0; j < 3; j++) {
|
||||
image.setRGB(x - 1 + j, 31 - i, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the stack cursor image.
|
||||
*
|
||||
* @param image the image object to draw into.
|
||||
*/
|
||||
private static void drawStack(BufferedImage image) {
|
||||
int v = 0xff000000;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
int x = i * 3;
|
||||
int y = 6 - i * 3;
|
||||
for (int j = 0; j < 10; j++) {
|
||||
image.setRGB(x, y + j, v);
|
||||
image.setRGB(x + 10, y + j, v);
|
||||
image.setRGB(x + j, y, v);
|
||||
image.setRGB(x + j, y + 10, v);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the "new window" cursor image.
|
||||
*
|
||||
* @param image the image object to draw into.
|
||||
*/
|
||||
private static void drawNewWindow(BufferedImage image) {
|
||||
int v = 0xff000000;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
for (int j = 0; j < 14; j++) {
|
||||
image.setRGB(j, i, 0xff0000ff);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < 14; i++) {
|
||||
image.setRGB(i, 0, v);
|
||||
image.setRGB(i, 10, v);
|
||||
}
|
||||
for (int i = 0; i < 10; i++) {
|
||||
image.setRGB(0, i, v);
|
||||
image.setRGB(14, i, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -707,19 +707,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
* @see #addComponent(ComponentProvider, boolean)
|
||||
*/
|
||||
public void showComponent(ComponentProvider provider, boolean visibleState) {
|
||||
ComponentPlaceholder placeholder = getActivePlaceholder(provider);
|
||||
if (placeholder != null) {
|
||||
showComponent(placeholder, visibleState, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (visibleState) {
|
||||
|
||||
// a null placeholder implies the client is trying to show a provider that has not
|
||||
// been added to the tool
|
||||
Msg.warn(this, "Attempting to show an unknown Component Provider '" +
|
||||
provider.getName() + "' - " + "check that the provider has been added to the tool");
|
||||
}
|
||||
showComponent(provider, visibleState, false);
|
||||
}
|
||||
|
||||
public void toFront(ComponentProvider provider) {
|
||||
|
@ -809,6 +797,23 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
root = null;
|
||||
}
|
||||
|
||||
void showComponent(ComponentProvider provider, boolean visibleState, boolean shouldEmphasize) {
|
||||
|
||||
ComponentPlaceholder placeholder = getActivePlaceholder(provider);
|
||||
if (placeholder != null) {
|
||||
showComponent(placeholder, visibleState, true, shouldEmphasize);
|
||||
return;
|
||||
}
|
||||
|
||||
if (visibleState) {
|
||||
|
||||
// a null placeholder implies the client is trying to show a provider that has not
|
||||
// been added to the tool
|
||||
Msg.warn(this, "Attempting to show an unknown Component Provider '" +
|
||||
provider.getName() + "' - " + "check that the provider has been added to the tool");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows or hides the component associated with the given placeholder object.
|
||||
*
|
||||
|
@ -816,15 +821,20 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
* @param visibleState true to show or false to hide.
|
||||
* @param requestFocus True signals that the system should request focus on the component.
|
||||
*/
|
||||
void showComponent(final ComponentPlaceholder placeholder, final boolean visibleState,
|
||||
private void showComponent(ComponentPlaceholder placeholder, final boolean visibleState,
|
||||
boolean requestFocus) {
|
||||
showComponent(placeholder, visibleState, requestFocus, false);
|
||||
}
|
||||
|
||||
void showComponent(ComponentPlaceholder placeholder, final boolean visibleState,
|
||||
boolean requestFocus, boolean shouldEmphasize) {
|
||||
if (root == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (visibleState == placeholder.isShowing()) {
|
||||
if (visibleState) {
|
||||
movePlaceholderToFront(placeholder, true);
|
||||
movePlaceholderToFront(placeholder, shouldEmphasize);
|
||||
setNextFocusPlaceholder(placeholder);
|
||||
scheduleUpdate();
|
||||
}
|
||||
|
@ -2066,17 +2076,19 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
}
|
||||
|
||||
public void contextChanged(ComponentProvider provider) {
|
||||
|
||||
if (provider == null) {
|
||||
actionToGuiMapper.contextChangedAll(); // update all windows;
|
||||
actionToGuiMapper.contextChangedAll(); // this updates the actions for all windows
|
||||
return;
|
||||
}
|
||||
|
||||
ComponentPlaceholder placeHolder = getActivePlaceholder(provider);
|
||||
if (placeHolder == null) {
|
||||
ComponentPlaceholder placeholder = getActivePlaceholder(provider);
|
||||
if (placeholder == null) {
|
||||
return;
|
||||
}
|
||||
placeHolder.contextChanged();
|
||||
actionToGuiMapper.contextChanged(placeHolder);
|
||||
|
||||
placeholder.contextChanged();
|
||||
actionToGuiMapper.contextChanged(placeholder);
|
||||
}
|
||||
|
||||
public void addContextListener(DockingContextListener listener) {
|
||||
|
@ -2143,7 +2155,6 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
private ComponentPlaceholder lastActivatedPlaceholder;
|
||||
|
||||
void activated(ComponentPlaceholder placeholder) {
|
||||
|
||||
if (lastActivatedPlaceholder == placeholder) {
|
||||
// repeat call--see if it was quickly called again (a sign of confusion/frustration)
|
||||
long elapsedTime = System.currentTimeMillis() - lastCalledTimestamp;
|
||||
|
|
74
Ghidra/Framework/Docking/src/main/java/docking/DropCode.java
Normal file
74
Ghidra/Framework/Docking/src/main/java/docking/DropCode.java
Normal file
|
@ -0,0 +1,74 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking;
|
||||
|
||||
import java.awt.Cursor;
|
||||
|
||||
/**
|
||||
* An enum that represents available drag-n-drop options for a docking tool. There are also
|
||||
* convenience methods for translating this drop code into a cursor and window position.
|
||||
*/
|
||||
enum DropCode {
|
||||
INVALID, STACK, LEFT, RIGHT, TOP, BOTTOM, ROOT, WINDOW;
|
||||
|
||||
public Cursor getCursor() {
|
||||
Cursor c = HeaderCursor.NO_DROP;
|
||||
switch (this) {
|
||||
case LEFT:
|
||||
c = HeaderCursor.LEFT;
|
||||
break;
|
||||
case RIGHT:
|
||||
c = HeaderCursor.RIGHT;
|
||||
break;
|
||||
case TOP:
|
||||
c = HeaderCursor.TOP;
|
||||
break;
|
||||
case BOTTOM:
|
||||
c = HeaderCursor.BOTTOM;
|
||||
break;
|
||||
case STACK:
|
||||
c = HeaderCursor.STACK;
|
||||
break;
|
||||
case ROOT:
|
||||
c = HeaderCursor.STACK;
|
||||
break;
|
||||
case WINDOW:
|
||||
c = HeaderCursor.NEW_WINDOW;
|
||||
break;
|
||||
case INVALID:
|
||||
break;
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
public WindowPosition getWindowPosition() {
|
||||
switch (this) {
|
||||
case BOTTOM:
|
||||
return WindowPosition.BOTTOM;
|
||||
case LEFT:
|
||||
return WindowPosition.LEFT;
|
||||
case RIGHT:
|
||||
return WindowPosition.RIGHT;
|
||||
case STACK:
|
||||
return WindowPosition.STACK;
|
||||
case TOP:
|
||||
return WindowPosition.TOP;
|
||||
default:
|
||||
return WindowPosition.STACK;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -164,13 +164,17 @@ public class GlobalMenuAndToolBarManager implements DockingWindowListener {
|
|||
}
|
||||
|
||||
public void contextChanged(ComponentPlaceholder placeHolder) {
|
||||
if (placeHolder == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
WindowNode topLevelNode = placeHolder.getTopLevelNode();
|
||||
if (topLevelNode == null) {
|
||||
return; // no provider in this window has focus - can this happen?
|
||||
return;
|
||||
}
|
||||
|
||||
if (topLevelNode.getLastFocusedProviderInWindow() != placeHolder) {
|
||||
return; // actions in this window are not currently responding to the provider
|
||||
// whose context has changed.
|
||||
return; // actions in this window are not currently responding to this provider
|
||||
}
|
||||
|
||||
WindowActionManager windowActionManager = windowToActionManagerMap.get(topLevelNode);
|
||||
|
|
168
Ghidra/Framework/Docking/src/main/java/docking/HeaderCursor.java
Normal file
168
Ghidra/Framework/Docking/src/main/java/docking/HeaderCursor.java
Normal file
|
@ -0,0 +1,168 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.dnd.DragSource;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
/**
|
||||
* The cursor values used when drag-n-dropping dockable components
|
||||
*/
|
||||
public class HeaderCursor {
|
||||
|
||||
static Cursor LEFT;
|
||||
static Cursor RIGHT;
|
||||
static Cursor TOP;
|
||||
static Cursor BOTTOM;
|
||||
static Cursor STACK;
|
||||
static Cursor NEW_WINDOW;
|
||||
static Cursor NO_DROP = DragSource.DefaultMoveNoDrop;
|
||||
|
||||
static {
|
||||
Toolkit tk = Toolkit.getDefaultToolkit();
|
||||
|
||||
Image image = drawLeftArrow();
|
||||
LEFT = tk.createCustomCursor(image, new Point(0, 6), "LEFT");
|
||||
|
||||
image = drawRightArrow();
|
||||
RIGHT = tk.createCustomCursor(image, new Point(31, 6), "RIGHT");
|
||||
|
||||
image = drawTopArrow();
|
||||
TOP = tk.createCustomCursor(image, new Point(6, 0), "TOP");
|
||||
|
||||
image = drawBottomArrow();
|
||||
BOTTOM = tk.createCustomCursor(image, new Point(6, 31), "BOTTOM");
|
||||
|
||||
image = drawStack();
|
||||
STACK = tk.createCustomCursor(image, new Point(8, 8), "STACK");
|
||||
|
||||
image = drawNewWindow();
|
||||
NEW_WINDOW = tk.createCustomCursor(image, new Point(0, 0), "NEW_WINDOW");
|
||||
}
|
||||
|
||||
private static Image drawLeftArrow() {
|
||||
|
||||
BufferedImage image = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
|
||||
int v = 0xff000000;
|
||||
int y = 6;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
for (int j = 0; j < 2 * i + 1; j++) {
|
||||
image.setRGB(i, y - i + j, v);
|
||||
}
|
||||
}
|
||||
for (int i = 6; i < 12; i++) {
|
||||
for (int j = 0; j < 3; j++) {
|
||||
image.setRGB(i, y - 1 + j, v);
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
private static Image drawRightArrow() {
|
||||
|
||||
BufferedImage image = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
|
||||
int v = 0xff000000;
|
||||
int y = 6;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
for (int j = 0; j < 2 * i + 1; j++) {
|
||||
image.setRGB(31 - i, y - i + j, v);
|
||||
}
|
||||
}
|
||||
for (int i = 6; i < 12; i++) {
|
||||
for (int j = 0; j < 3; j++) {
|
||||
image.setRGB(31 - i, y - 1 + j, v);
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
private static Image drawTopArrow() {
|
||||
|
||||
BufferedImage image = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
|
||||
int v = 0xff000000;
|
||||
int x = 6;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
for (int j = 0; j < 2 * i + 1; j++) {
|
||||
image.setRGB(x - i + j, i, v);
|
||||
}
|
||||
}
|
||||
for (int i = 6; i < 12; i++) {
|
||||
for (int j = 0; j < 3; j++) {
|
||||
image.setRGB(x - 1 + j, i, v);
|
||||
}
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
private static Image drawBottomArrow() {
|
||||
|
||||
BufferedImage image = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
|
||||
int v = 0xff000000;
|
||||
int x = 6;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
for (int j = 0; j < 2 * i + 1; j++) {
|
||||
image.setRGB(x - i + j, 31 - i, v);
|
||||
}
|
||||
}
|
||||
for (int i = 6; i < 12; i++) {
|
||||
for (int j = 0; j < 3; j++) {
|
||||
image.setRGB(x - 1 + j, 31 - i, v);
|
||||
}
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
private static Image drawStack() {
|
||||
|
||||
BufferedImage image = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
|
||||
int v = 0xff000000;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
int x = i * 3;
|
||||
int y = 6 - i * 3;
|
||||
for (int j = 0; j < 10; j++) {
|
||||
image.setRGB(x, y + j, v);
|
||||
image.setRGB(x + 10, y + j, v);
|
||||
image.setRGB(x + j, y, v);
|
||||
image.setRGB(x + j, y + 10, v);
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
private static Image drawNewWindow() {
|
||||
|
||||
BufferedImage image = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
|
||||
int v = 0xff000000;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
for (int j = 0; j < 14; j++) {
|
||||
image.setRGB(j, i, 0xff0000ff);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < 14; i++) {
|
||||
image.setRGB(i, 0, v);
|
||||
image.setRGB(i, 10, v);
|
||||
}
|
||||
for (int i = 0; i < 10; i++) {
|
||||
image.setRGB(0, i, v);
|
||||
image.setRGB(14, i, v);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
}
|
|
@ -42,7 +42,7 @@ public class ShowAllComponentsAction extends ShowComponentAction {
|
|||
public void actionPerformed(ActionContext context) {
|
||||
boolean focusMe = true;
|
||||
for (ComponentPlaceholder info : infoList) {
|
||||
winMgr.showComponent(info, true, focusMe);
|
||||
winMgr.showComponent(info, true, focusMe, true);
|
||||
focusMe = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -121,7 +121,7 @@ class ShowComponentAction extends DockingAction
|
|||
|
||||
@Override
|
||||
public void actionPerformed(ActionContext context) {
|
||||
winMgr.showComponent(info, true, true);
|
||||
winMgr.showComponent(info, true, true, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
* REVIEWED: YES
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -51,18 +50,10 @@ import docking.util.GraphicsUtils;
|
|||
*/
|
||||
public class DockingMenuItemUI extends MenuItemUI {
|
||||
|
||||
static final String MAX_TEXT_WIDTH = "maxTextWidth1";
|
||||
static final String MAX_ACC_WIDTH = "maxAccWidth1";
|
||||
static final String TABULATOR_PROPERTIES = "menuItemTabulator";
|
||||
public static int ARROW_GAP = 4;
|
||||
static final int ICON_WIDTH = 16;
|
||||
static final int ICON_HEIGHT = 16;
|
||||
static final int LEFT_MARGIN = 2;
|
||||
static final int TOP_MARGIN = 2;
|
||||
static final int RIGHT_MARGIN = 4;
|
||||
static final int BOTTOM_MARGIN = 2;
|
||||
static final int ACC_GAP = 5;
|
||||
static final int COLUMN_PADDING = 5;
|
||||
private static final String TABULATOR_PROPERTIES = "menuItemTabulator";
|
||||
|
||||
// make this big enough to differentiate columns
|
||||
private static final int COLUMN_PADDING = 20;
|
||||
|
||||
protected MenuItemUI ui;
|
||||
|
||||
|
@ -127,19 +118,24 @@ public class DockingMenuItemUI extends MenuItemUI {
|
|||
sg2.setDoImage(false);
|
||||
|
||||
Icon origIcon = c.getIcon();
|
||||
int iconWidth = 0;
|
||||
if (origIcon != null) {
|
||||
iconWidth = origIcon.getIconWidth();
|
||||
}
|
||||
String origText = c.getText();
|
||||
KeyStroke origAcc = c.getAccelerator();
|
||||
String[] parts = origText.split("\t");
|
||||
for (int i = 0; i < parts.length; i++) {
|
||||
if (i == 1) {
|
||||
c.setIcon(null);
|
||||
c.setAccelerator(null);
|
||||
}
|
||||
c.setText(parts[i]);
|
||||
|
||||
c.setText(parts[i]);
|
||||
ui.paint(sg2, c);
|
||||
|
||||
sg2.translate(t.columns.get(i) + COLUMN_PADDING, 0);
|
||||
sg2.translate(iconWidth + t.columns.get(i) + COLUMN_PADDING, 0);
|
||||
|
||||
// this is only needed for the first pass
|
||||
iconWidth = 0;
|
||||
c.setIcon(null);
|
||||
c.setAccelerator(null);
|
||||
}
|
||||
|
||||
c.setIcon(origIcon);
|
||||
|
@ -154,10 +150,9 @@ public class DockingMenuItemUI extends MenuItemUI {
|
|||
if (text.indexOf('\t') == -1) {
|
||||
return uiPref;
|
||||
}
|
||||
|
||||
int extra = uiPref.width - textWidth(c, text);
|
||||
|
||||
MenuTabulator tabulator = MenuTabulator.tabulate((JMenuItem) c);
|
||||
|
||||
return new Dimension(tabulator.getWidth() + extra, uiPref.height);
|
||||
}
|
||||
|
||||
|
@ -303,14 +298,16 @@ public class DockingMenuItemUI extends MenuItemUI {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) {
|
||||
public void drawRoundRect(int x, int y, int width, int height, int arcWidth,
|
||||
int arcHeight) {
|
||||
if (doDraw) {
|
||||
g.drawRoundRect(x, y, width, height, arcWidth, arcHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) {
|
||||
public void fillRoundRect(int x, int y, int width, int height, int arcWidth,
|
||||
int arcHeight) {
|
||||
if (doFill) {
|
||||
g.fillRoundRect(x, y, width, height, arcWidth, arcHeight);
|
||||
}
|
||||
|
|
|
@ -24,6 +24,8 @@ import javax.swing.*;
|
|||
import javax.swing.border.Border;
|
||||
import javax.swing.event.*;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import docking.*;
|
||||
import docking.action.*;
|
||||
import docking.widgets.EmptyBorderButton;
|
||||
|
@ -427,7 +429,7 @@ public class MultipleActionDockingToolbarButton extends EmptyBorderButton {
|
|||
renderer.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
renderer.setVisible(true);
|
||||
|
||||
if (name != null && !"".equals(name)) {
|
||||
if (!StringUtils.isBlank(name)) {
|
||||
separatorHeight = TEXT_SEPARATOR_HEIGHT;
|
||||
}
|
||||
|
||||
|
@ -437,14 +439,20 @@ public class MultipleActionDockingToolbarButton extends EmptyBorderButton {
|
|||
|
||||
@Override
|
||||
protected void paintComponent(Graphics g) {
|
||||
// assume horizontal
|
||||
Dimension s = getSize();
|
||||
int center = separatorHeight >> 1;
|
||||
g.setColor(getForeground());
|
||||
g.drawLine(0, center, s.width, center);
|
||||
Dimension d = getSize();
|
||||
|
||||
// some edge padding, for classiness
|
||||
int pad = 10;
|
||||
int center = separatorHeight >> 1;
|
||||
int x = 0 + pad;
|
||||
int y = center;
|
||||
int w = d.width - pad;
|
||||
g.setColor(getForeground());
|
||||
g.drawLine(x, y, w, y);
|
||||
|
||||
// drop-shadow
|
||||
g.setColor(getBackground());
|
||||
g.drawLine(0, (center + 1), s.width, (center + 1));
|
||||
g.drawLine(x, (y + 1), w, (y + 1));
|
||||
|
||||
// now add our custom text
|
||||
renderer.setSize(getSize());
|
||||
|
|
|
@ -116,7 +116,7 @@ public class ToolBarItemManager implements PropertyChangeListener, ActionListene
|
|||
|
||||
private String getToolTipText(DockingActionIf action) {
|
||||
String description = action.getDescription();
|
||||
if (description != null && description.trim().length() > 0) {
|
||||
if (!StringUtils.isEmpty(description)) {
|
||||
return description;
|
||||
}
|
||||
return action.getName();
|
||||
|
|
|
@ -26,10 +26,12 @@ import org.jdesktop.animation.timing.TimingTargetAdapter;
|
|||
import org.jdesktop.animation.timing.interpolation.PropertySetter;
|
||||
|
||||
import generic.util.WindowUtilities;
|
||||
import generic.util.image.ImageUtils;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.bean.GGlassPane;
|
||||
import ghidra.util.bean.GGlassPanePainter;
|
||||
import ghidra.util.exception.AssertException;
|
||||
import resources.ResourceManager;
|
||||
|
||||
public class AnimationUtils {
|
||||
|
||||
|
@ -216,6 +218,21 @@ public class AnimationUtils {
|
|||
return pulser.animator;
|
||||
}
|
||||
|
||||
public static Animator showTheDragonOverComponent(Component component) {
|
||||
if (!animationEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
GGlassPane glassPane = getGlassPane(component);
|
||||
if (glassPane == null) {
|
||||
// could happen if the given component has not yet been realized
|
||||
return null;
|
||||
}
|
||||
|
||||
DragonImageDriver pulser = new DragonImageDriver(component);
|
||||
return pulser.animator;
|
||||
}
|
||||
|
||||
public static Animator executeSwingAnimationCallback(SwingAnimationCallback callback) {
|
||||
// note: instead of checking for 'animationEnabled' here, it will happen in the driver
|
||||
// so that the we can call SwingAnimationCallback.done(), which will let the client
|
||||
|
@ -225,16 +242,18 @@ public class AnimationUtils {
|
|||
return driver.animator;
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
// Private Methods
|
||||
//==================================================================================================
|
||||
|
||||
private static GGlassPane getGlassPane(Component component) {
|
||||
/**
|
||||
* Returns the {@link GGlassPane} for the given component
|
||||
*
|
||||
* @param c the component
|
||||
* @return the glass pane
|
||||
*/
|
||||
public static GGlassPane getGlassPane(Component c) {
|
||||
|
||||
// TODO: validate component has been realized? ...check for window, but that would
|
||||
// then put the onus on the client
|
||||
|
||||
Window window = WindowUtilities.windowForComponent(component);
|
||||
Window window = WindowUtilities.windowForComponent(c);
|
||||
if (window instanceof JFrame) {
|
||||
JFrame frame = (JFrame) window;
|
||||
Component glass = frame.getGlassPane();
|
||||
|
@ -364,7 +383,7 @@ public class AnimationUtils {
|
|||
FocusPainter(Component component, double max) {
|
||||
this.component = component;
|
||||
this.max = max;
|
||||
image = paintImage();
|
||||
image = ImageUtils.createImage(component);
|
||||
}
|
||||
|
||||
void setPercentComplete(double percent) {
|
||||
|
@ -434,16 +453,6 @@ public class AnimationUtils {
|
|||
|
||||
g.drawImage(image, x, y, width, height, null);
|
||||
}
|
||||
|
||||
private Image paintImage() {
|
||||
Rectangle bounds = component.getBounds();
|
||||
BufferedImage bufferedImage =
|
||||
new BufferedImage(bounds.width, bounds.height, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics g = bufferedImage.getGraphics();
|
||||
component.paint(g);
|
||||
g.dispose();
|
||||
return bufferedImage;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PointToComponentDriver {
|
||||
|
@ -491,10 +500,6 @@ public class AnimationUtils {
|
|||
new Point((int) startBounds.getCenterX(), (int) startBounds.getCenterY());
|
||||
return SwingUtilities.convertPoint(component.getParent(), relativeStartCenter,
|
||||
glassPane);
|
||||
|
||||
// TODO do we need this?
|
||||
// Rectangle glassPaneBounds = glassPane.getBounds();
|
||||
// return new Point((int) glassPaneBounds.getCenterX(), (int) glassPaneBounds.getCenterY());
|
||||
}
|
||||
|
||||
// note: must be public--it is a callback from the animator (also, its name must
|
||||
|
@ -522,7 +527,7 @@ public class AnimationUtils {
|
|||
PointToComponentPainter(Point startPoint, Component component) {
|
||||
this.startPoint = startPoint;
|
||||
this.component = component;
|
||||
image = paintImage();
|
||||
image = ImageUtils.createImage(component);
|
||||
}
|
||||
|
||||
void setPercentComplete(double percent) {
|
||||
|
@ -557,16 +562,6 @@ public class AnimationUtils {
|
|||
//
|
||||
g2d.drawImage(image, (int) currentX, (int) currentY, scaledWidth, scaledHeight, null);
|
||||
}
|
||||
|
||||
private Image paintImage() {
|
||||
Rectangle bounds = component.getBounds();
|
||||
BufferedImage bufferedImage =
|
||||
new BufferedImage(bounds.width, bounds.height, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics g = bufferedImage.getGraphics();
|
||||
component.paint(g);
|
||||
g.dispose();
|
||||
return bufferedImage;
|
||||
}
|
||||
}
|
||||
|
||||
// note: must be public due to reflection used by the timing framework
|
||||
|
@ -798,7 +793,7 @@ public class AnimationUtils {
|
|||
|
||||
RotatePainter(Component component) {
|
||||
this.component = component;
|
||||
image = paintImage();
|
||||
image = ImageUtils.createImage(component);
|
||||
}
|
||||
|
||||
void setPercentComplete(double percent) {
|
||||
|
@ -873,16 +868,6 @@ public class AnimationUtils {
|
|||
g.drawRect(offsetX, offsetY, iw, ih);
|
||||
g.drawImage(image, offsetX, offsetY, iw, ih, null);
|
||||
}
|
||||
|
||||
private Image paintImage() {
|
||||
Rectangle bounds = component.getBounds();
|
||||
BufferedImage bufferedImage =
|
||||
new BufferedImage(bounds.width, bounds.height, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics g = bufferedImage.getGraphics();
|
||||
component.paint(g);
|
||||
g.dispose();
|
||||
return bufferedImage;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ShakeDriver {
|
||||
|
@ -993,7 +978,7 @@ public class AnimationUtils {
|
|||
|
||||
PulsePainter(Component component) {
|
||||
this.component = component;
|
||||
image = paintImage();
|
||||
image = ImageUtils.createImage(component);
|
||||
}
|
||||
|
||||
void setEmphasis(double emphasis) {
|
||||
|
@ -1024,16 +1009,6 @@ public class AnimationUtils {
|
|||
|
||||
g.drawImage(image, offsetX, offsetY, width, height, null);
|
||||
}
|
||||
|
||||
private Image paintImage() {
|
||||
Rectangle bounds = component.getBounds();
|
||||
BufferedImage bufferedImage =
|
||||
new BufferedImage(bounds.width, bounds.height, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics g = bufferedImage.getGraphics();
|
||||
component.paint(g);
|
||||
g.dispose();
|
||||
return bufferedImage;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ShakePainter implements GGlassPanePainter {
|
||||
|
@ -1043,7 +1018,7 @@ public class AnimationUtils {
|
|||
|
||||
ShakePainter(Component component) {
|
||||
this.component = component;
|
||||
image = paintImage();
|
||||
image = ImageUtils.createImage(component);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1083,16 +1058,6 @@ public class AnimationUtils {
|
|||
g2d.rotate(lastDirection, emphasizedBounds.getCenterX(), emphasizedBounds.getCenterY());
|
||||
g.drawImage(image, offsetX, offsetY, width, height, null);
|
||||
}
|
||||
|
||||
private Image paintImage() {
|
||||
Rectangle bounds = component.getBounds();
|
||||
BufferedImage bufferedImage =
|
||||
new BufferedImage(bounds.width, bounds.height, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics g = bufferedImage.getGraphics();
|
||||
component.paint(g);
|
||||
g.dispose();
|
||||
return bufferedImage;
|
||||
}
|
||||
}
|
||||
|
||||
private static class PulseAndShakePainter extends PulsePainter {
|
||||
|
@ -1143,4 +1108,104 @@ public class AnimationUtils {
|
|||
}
|
||||
}
|
||||
|
||||
// Draws the system dragon icon over the given component
|
||||
public static class DragonImageDriver {
|
||||
|
||||
private Animator animator;
|
||||
private GGlassPane glassPane;
|
||||
private DragonImagePainter rotatePainter;
|
||||
|
||||
DragonImageDriver(Component component) {
|
||||
|
||||
glassPane = AnimationUtils.getGlassPane(component);
|
||||
rotatePainter = new DragonImagePainter(component);
|
||||
|
||||
double start = 0;
|
||||
double max = 1;
|
||||
int duration = 1500;
|
||||
animator =
|
||||
PropertySetter.createAnimator(duration, this, "percentComplete", start, max, start);
|
||||
|
||||
animator.setAcceleration(0.2f);
|
||||
animator.setDeceleration(0.8f);
|
||||
|
||||
animator.addTarget(new TimingTargetAdapter() {
|
||||
@Override
|
||||
public void end() {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
glassPane.addPainter(rotatePainter);
|
||||
|
||||
animator.start();
|
||||
}
|
||||
|
||||
public void setPercentComplete(double percentComplete) {
|
||||
rotatePainter.setPercentComplete(percentComplete);
|
||||
glassPane.repaint();
|
||||
}
|
||||
|
||||
void done() {
|
||||
glassPane.repaint();
|
||||
glassPane.removePainter(rotatePainter);
|
||||
}
|
||||
}
|
||||
|
||||
private static class DragonImagePainter implements GGlassPanePainter {
|
||||
|
||||
private Component component;
|
||||
private double percentComplete = 0.0;
|
||||
|
||||
DragonImagePainter(Component component) {
|
||||
this.component = component;
|
||||
}
|
||||
|
||||
void setPercentComplete(double percent) {
|
||||
percentComplete = percent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paint(GGlassPane glassPane, Graphics g) {
|
||||
|
||||
Graphics2D g2d = (Graphics2D) g;
|
||||
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
|
||||
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
|
||||
float alpha = (float) percentComplete;
|
||||
alpha = Math.min(alpha, .5f);
|
||||
Composite originaComposite = g2d.getComposite();
|
||||
AlphaComposite alphaComposite =
|
||||
AlphaComposite.getInstance(AlphaComposite.SrcOver.getRule(), alpha);
|
||||
g2d.setComposite(alphaComposite);
|
||||
|
||||
ImageIcon ghidra = ResourceManager.loadImage("images/GhidraIcon256.png");
|
||||
Image ghidraImage = ghidra.getImage();
|
||||
|
||||
Rectangle fullBounds = component.getBounds();
|
||||
fullBounds =
|
||||
SwingUtilities.convertRectangle(component.getParent(), fullBounds, glassPane);
|
||||
|
||||
int gw = ghidraImage.getWidth(null);
|
||||
int gh = ghidraImage.getHeight(null);
|
||||
double smallest =
|
||||
fullBounds.width > fullBounds.height ? fullBounds.height : fullBounds.width;
|
||||
smallest -= 10; // padding
|
||||
|
||||
double scale = smallest / gw;
|
||||
int w = (int) (gw * scale);
|
||||
int h = (int) (gh * scale);
|
||||
|
||||
double cx = fullBounds.getCenterX();
|
||||
double cy = fullBounds.getCenterY();
|
||||
int offsetX = (int) (cx - (w >> 1));
|
||||
int offsetY = (int) (cy - (h >> 1));
|
||||
|
||||
g2d.setClip(fullBounds);
|
||||
g2d.drawImage(ghidraImage, offsetX, offsetY, w, h, null);
|
||||
|
||||
g2d.setComposite(originaComposite);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -41,6 +41,26 @@ public class ImageUtils {
|
|||
// no
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an image of the given component
|
||||
*
|
||||
* @param c the component
|
||||
* @return the image
|
||||
*/
|
||||
public static Image createImage(Component c) {
|
||||
|
||||
// prevent this from being called when the user has made the window too small to work
|
||||
Rectangle bounds = c.getBounds();
|
||||
int w = Math.max(bounds.width, 1);
|
||||
int h = Math.max(bounds.height, 1);
|
||||
|
||||
BufferedImage bufferedImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics g = bufferedImage.getGraphics();
|
||||
c.paint(g);
|
||||
g.dispose();
|
||||
return bufferedImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads the given image with space in the amount given.
|
||||
*
|
||||
|
|
|
@ -15,8 +15,10 @@
|
|||
*/
|
||||
package ghidra.graph;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.hamcrest.Matchers.isOneOf;
|
||||
import static org.hamcrest.CoreMatchers.hasItem;
|
||||
import static org.hamcrest.CoreMatchers.hasItems;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.*;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue