ghidra/Ghidra/Framework/Docking/src/main/java/docking/DockableHeader.java
2019-07-18 18:57:08 -04:00

633 lines
18 KiB
Java

/* ###
* 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.*;
import java.awt.event.InputEvent;
import java.awt.event.MouseListener;
import java.util.*;
import java.util.List;
import java.util.stream.Collectors;
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
* source functionality.
*/
public class DockableHeader extends GenericHeader
implements DragGestureListener, DragSourceListener {
private DockableComponent dockComp;
private DragCursorManager dragCursorManager = createDragCursorManager();
private DragSource dragSource = null;
private boolean isDocking;
private Animator focusAnimator;
/**
* Constructs a new DockableHeader for the given dockableComponent.
*
* @param dockableComp the dockableComponent that this header is for.
* @param isDocking true means this widget can be dragged and docked by the user
*/
DockableHeader(DockableComponent dockableComp, boolean isDocking) {
this.dockComp = dockableComp;
this.isDocking = isDocking;
setComponent(dockableComp);
dragSource = new DragSource();
ComponentPlaceholder info = dockableComp.getComponentWindowingPlaceholder();
setTitle(info.getFullTitle());
setIcon(info.getIcon());
toolBarMgr = new DockableToolBarManager(dockableComp, this);
dragSource.createDefaultDragGestureRecognizer(titlePanel.getDragComponent(),
DnDConstants.ACTION_MOVE, DockableHeader.this);
resetComponents();
}
@Override
// overridden to use our DockableComponent
public void requestFocus() {
validateFocusability();
dockComp.requestFocus();
}
@Override
public void setSelected(boolean hasFocus) {
if (!hasFocus) {
if (focusAnimator != null) {
focusAnimator.stop();
focusAnimator = null;
}
}
super.setSelected(hasFocus);
}
void installRenameAction(MouseListener listener) {
titlePanel.installRenameAction(listener);
}
void setHelp(HelpLocation location) {
HelpService service = Help.getHelpService();
if (location == null) {
service.clearHelp(titlePanel);
}
else {
service.registerHelp(titlePanel, location);
}
}
/**
* Signals to use the GUI to make this header (and its provider) stand out from the rest.
*/
void emphasize() {
DockingWindowManager manager = DockingWindowManager.getInstance(this);
if (manager == null) {
return;
}
JFrame toolFrame = manager.getRootFrame();
Component glassPane = toolFrame.getGlassPane();
if (!(glassPane instanceof GGlassPane)) {
return;
}
if (focusAnimator != null && focusAnimator.isRunning()) {
// prevent multiple animation
return;
}
focusAnimator = createEmphasizingAnimator(toolFrame);
}
protected Animator createEmphasizingAnimator(JFrame parentFrame) {
double random = Math.random();
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 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);
}
}
}
private Animator raiseComponent(JFrame parent) {
if (isOnlyComponentInParent(parent)) {
return super.createEmphasizingAnimator();
}
return AnimationUtils.focusComponent(component);
}
private boolean isOnlyComponentInParent(JFrame parentFrame) {
return !isInSplitPanel(); // no split parent means a single component in the window
}
private boolean isInSplitPanel() {
Container parent = component.getParent();
while (parent != null && !(parent instanceof SplitPanel)) {
parent = parent.getParent();
}
return (parent instanceof SplitPanel);
}
private void validateFocusability() {
Container focusCycleRootAncestor = dockComp.getFocusCycleRootAncestor();
FocusTraversalPolicy policy = focusCycleRootAncestor.getFocusTraversalPolicy();
Component firstComponent = policy.getFirstComponent(dockComp);
if (firstComponent == null) {
ComponentPlaceholder info = dockComp.getComponentWindowingPlaceholder();
Msg.debug(this,
"Found a ComponentProvider that does not contain a " + "focusable component: " +
info.getTitle() + ". ComponentProviders are " +
"required to have at least one focusable component!");
setSelected(false); // can't select it can't take focus
}
}
@Override
public void dragGestureRecognized(DragGestureEvent event) {
if (!isDocking) {
return;
}
// if any button other than MB1 is pressed, don't attempt to process the drag and drop event
InputEvent ie = event.getTriggerEvent();
int modifiers = ie.getModifiersEx();
if ((modifiers & InputEvent.BUTTON2_DOWN_MASK) != 0 ||
(modifiers & InputEvent.BUTTON3_DOWN_MASK) != 0) {
return;
}
DockableComponent.DROP_CODE = DropCode.WINDOW;
DockableComponent.DROP_CODE_SET = true;
DockableComponent.SOURCE_INFO = dockComp.getComponentWindowingPlaceholder();
dragCursorManager.dragStarted();
dragSource.startDrag(event, DragSource.DefaultMoveNoDrop,
new ComponentTransferable(new ComponentTransferableData(dockComp)), this);
}
@Override
public void dragDropEnd(DragSourceDropEvent event) {
dragCursorManager.restoreCursorOnPreviousDraggedOverComponent();
dragCursorManager.dragEnded();
ComponentPlaceholder info = dockComp.getComponentWindowingPlaceholder();
DockingWindowManager winMgr = info.getNode().winMgr;
if (DockableComponent.DROP_CODE == DropCode.INVALID) {
return;
}
// TODO - Mac doesn't get the drop success correct when undocking a component (dragging out of
// the Java app
// else if ( !event.getDropSuccess() ) {
// return;
// }
// else
if (DockableComponent.DROP_CODE == DropCode.WINDOW) {
winMgr.movePlaceholder(info, event.getLocation());
}
else {
winMgr.movePlaceholder(info, DockableComponent.TARGET_INFO,
DockableComponent.DROP_CODE.getWindowPosition());
}
}
@Override
public void dragEnter(DragSourceDragEvent event) {
setCursor(event);
}
@Override
public void dragExit(DragSourceEvent event) {
setCursor(event);
}
@Override
public void dragOver(DragSourceDragEvent event) {
setCursor(event);
}
/**
* Sets the drag/drop cursor based on the current drop code.
*
* @param event the event containing the drag source context on which to set the cursor.
*/
private void setCursor(DragSourceEvent event) {
// TODO not sure why we needed this mechanic. It is wrong on the Mac. What is the use case?
// if (!DockableComponent.DROP_CODE_SET) {
// return;
// }
DockableComponent.DROP_CODE_SET = false;
Cursor c = DockableComponent.DROP_CODE.getCursor();
dragCursorManager.setCursor(event, c);
}
@Override
public void dropActionChanged(DragSourceDragEvent event) {
// don't care
}
private DragCursorManager createDragCursorManager() {
if (Platform.CURRENT_PLATFORM.getOperatingSystem() == OperatingSystem.MAC_OS_X) {
return new MacDragCursorManager();
}
return new DragCursorManager();
}
//==================================================================================================
// Inner Classes
//==================================================================================================
private class DragCursorManager {
void setCursor(DragSourceEvent event, Cursor dragCursor) {
DragSourceContext context = event.getDragSourceContext();
context.setCursor(dragCursor); // use Java's normal mechanism
}
void dragStarted() {
// no-op; dummy implementation
}
void dragEnded() {
// no-op; dummy implementation
}
void restoreCursorOnPreviousDraggedOverComponent() {
// no-op; dummy implementation
}
}
private class MacDragCursorManager extends DragCursorManager {
private Map<Component, Cursor> defaultCursors = new HashMap<>();
private Component componentUnderDrag;
@Override
void setCursor(DragSourceEvent event, Cursor dragCursor) {
ComponentPlaceholder placeholder = DockableComponent.DRAGGED_OVER_INFO;
Component mousedComponent = null;
if (placeholder == null) {
// Must not be over any component; This can happen when over a split pane
// resize area or outside of the window
super.setCursor(event, dragCursor);
return;
}
int x = event.getX();
int y = event.getY();
Point screenPoint = new Point(x, y);
DockableComponent draggedOverComponent = placeholder.getComponent();
SwingUtilities.convertPointFromScreen(screenPoint, draggedOverComponent);
Component hoveredComponent = SwingUtilities.getDeepestComponentAt(draggedOverComponent,
screenPoint.x, screenPoint.y);
mousedComponent = hoveredComponent;
if (mousedComponent == null) {
mousedComponent = placeholder.getComponent();
}
if (componentUnderDrag != mousedComponent) {
restoreCursorOnPreviousDraggedOverComponent();
}
componentUnderDrag = mousedComponent;
mousedComponent.setCursor(dragCursor);
// also do the preferred thing, in case they ever fix this on the Mac
super.setCursor(event, dragCursor);
}
@Override
void restoreCursorOnPreviousDraggedOverComponent() {
if (componentUnderDrag == null) {
return;
}
Cursor restoreCursor = defaultCursors.get(componentUnderDrag);
Window window = WindowUtilities.windowForComponent(componentUnderDrag);
if (window != null) { // We've seen this happen when docking/undocking windows
Cursor windowCursor = defaultCursors.get(window);
window.setCursor(windowCursor);
}
componentUnderDrag.setCursor(restoreCursor);
componentUnderDrag = null;
}
@Override
void dragStarted() {
defaultCursors.clear();
Window[] windows = Window.getWindows();
for (Window window : windows) {
storeCursors(window);
}
}
private void storeCursors(Container c) {
defaultCursors.put(c, c.getCursor());
Component[] children = c.getComponents();
for (Component child : children) {
if (child instanceof Container) {
storeCursors((Container) child);
}
}
}
@Override
void dragEnded() {
defaultCursors.clear();
}
}
public static class EmphasizeDockableComponentAnimationDriver {
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);
}
}
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);
}
}
}
}