ghidra/Ghidra/Framework/Docking/src/main/java/docking/DockableComponent.java
2025-05-09 13:40:52 -04:00

511 lines
13 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.*;
import javax.swing.*;
import docking.action.DockingActionIf;
import ghidra.util.*;
import help.HelpService;
/**
* Wrapper class for user components. Adds the title, local toolbar and provides the drag target
* functionality.
*/
public class DockableComponent extends JPanel implements ContainerListener {
private static final int DROP_EDGE_OFFSET = 20;
private static final Dimension MIN_DIM = new Dimension(100, 50);
public static DropCode DROP_CODE;
public static ComponentPlaceholder TARGET_INFO;
public static ComponentPlaceholder DRAGGED_OVER_INFO;
public static ComponentPlaceholder SOURCE_INFO;
public static boolean DROP_CODE_SET;
private DockableHeader header;
private MouseListener popupListener;
private ComponentPlaceholder placeholder;
private JComponent providerComp;
private Component lastFocusedComponent;
private DockingWindowManager winMgr;
private ActionToGuiMapper actionMgr;
private DropTarget dockableDropTarget;
/**
* Constructs a new DockableComponent for the given info object.
* @param placeholder the info object that has the component to be shown.
* @param isDocking if true allows components to be dragged and docked.
*/
DockableComponent(ComponentPlaceholder placeholder, boolean isDocking) {
if (placeholder != null) {
this.placeholder = placeholder;
winMgr = placeholder.getNode().winMgr;
actionMgr = winMgr.getActionToGuiMapper();
popupListener = new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
componentSelected((Component) e.getSource());
showContextMenu(e);
}
@Override
public void mouseReleased(MouseEvent e) {
showContextMenu(e);
}
@Override
public void mouseClicked(MouseEvent e) {
showContextMenu(e);
}
};
dockableDropTarget = new DockableComponentDropTarget(this);
initializeComponents(this);
setLayout(new BorderLayout());
header = new DockableHeader(this, isDocking);
if (placeholder.isHeaderShowing()) {
add(header, BorderLayout.NORTH);
}
providerComp = initializeComponentPlaceholder(placeholder);
JPanel contentPanel = new JPanel(new BorderLayout());
setFocusable(false); // this should never be focusable
setFocusCycleRoot(false);
contentPanel.add(providerComp, BorderLayout.CENTER);
add(contentPanel, BorderLayout.CENTER);
}
else {
dockableDropTarget = new DockableComponentDropTarget(this);
}
}
private JComponent initializeComponentPlaceholder(ComponentPlaceholder newPlaceholder) {
JComponent providerComponent = newPlaceholder.getProviderComponent();
// Ensure that every provider component has a registered help location
ComponentProvider provider = newPlaceholder.getProvider();
HelpLocation helpLocation = provider.getHelpLocation();
HelpLocation location = registerHelpLocation(provider, helpLocation);
header.setHelp(location);
return providerComponent;
}
public DockableHeader getHeader() {
return header;
}
private HelpLocation registerHelpLocation(ComponentProvider provider,
HelpLocation helpLocation) {
HelpService helpService = DockingWindowManager.getHelpService();
if (helpService.isExcludedFromHelp(provider)) {
return null;
}
HelpLocation registeredHelpLocation = helpService.getHelpLocation(provider);
if (registeredHelpLocation != null) {
return registeredHelpLocation; // nothing to do; location already registered
}
if (helpLocation == null) {
// this shouldn't happen, but just in case
helpLocation = new HelpLocation(provider.getOwner(), provider.getName());
}
helpService.registerHelp(provider, helpLocation);
return helpLocation;
}
void showContextMenu(PopupMenuContext popupContext) {
actionMgr.showPopupMenu(placeholder, popupContext);
}
private void showContextMenu(MouseEvent e) {
if (e.isConsumed()) {
return;
}
if (!e.isPopupTrigger()) {
return;
}
Component component = e.getComponent();
if (component == null) {
return; // not sure this can happen
}
// get the bounds to see if the clicked point is over the component
Rectangle bounds = component.getBounds();
if (component instanceof JComponent) {
((JComponent) component).computeVisibleRect(bounds);
}
Point point = e.getPoint();
if (!bounds.contains(point)) {
return;
}
//
// Consume the event so that Java UI listeners do not process it. This fixes issues with
// UI classes (e.g., listeners change table selection). We want to run this code later to
// allow trailing application mouse listeners to have a chance to update the context. If
// the delayed nature causes any timing issues, then we will need a more robust way of
// registering mouse listeners to work around this issue.
//
e.consume();
Swing.runLater(() -> {
PopupMenuContext popupContext = new PopupMenuContext(e);
actionMgr.showPopupMenu(placeholder, popupContext);
});
}
@Override
public Dimension getMinimumSize() {
return MIN_DIM;
}
JComponent getProviderComponent() {
return providerComp;
}
/**
* Returns the placeholder object associated with this DockableComponent
* @return the placeholder object associated with this DockableComponent
*/
public ComponentPlaceholder getComponentWindowingPlaceholder() {
return placeholder;
}
/**
* Returns the component provider attached to this dockable component; null if this object
* has been disposed
*
* @return the provider
*/
public ComponentProvider getComponentProvider() {
if (placeholder == null) {
return null;
}
return placeholder.getProvider();
}
/**
* Returns the docking window manager that owns this component
* @return the manager
*/
public DockingWindowManager getDockingWindowManager() {
if (placeholder == null) {
return null;
}
return placeholder.getNode().getDockingWindowManager();
}
@Override
public String toString() {
if (placeholder == null) {
return "";
}
return placeholder.getFullTitle();
}
/**
* 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();
p.x = p.x + cLoc.x - myLoc.x;
p.y = p.y + cLoc.y - myLoc.y;
}
private class DockableComponentDropTarget extends DropTarget {
DockableComponentDropTarget(Component comp) {
super(comp, null);
}
@Override
public synchronized void drop(DropTargetDropEvent dtde) {
clearAutoscroll();
if (dtde.isDataFlavorSupported(ComponentTransferable.localComponentProviderFlavor)) {
Point p = dtde.getLocation();
translate(p, ((DropTarget) dtde.getSource()).getComponent());
setDropCode(p);
TARGET_INFO = placeholder;
dtde.acceptDrop(dtde.getDropAction());
dtde.dropComplete(true);
}
else {
dtde.rejectDrop();
}
}
@Override
public synchronized void dragEnter(DropTargetDragEvent dtde) {
super.dragEnter(dtde);
// On Mac, sometimes this component is not showing,
// which causes exception in the translate method.
if (!isShowing()) {
dtde.rejectDrag();
return;
}
if (dtde.isDataFlavorSupported(ComponentTransferable.localComponentProviderFlavor)) {
Point p = dtde.getLocation();
translate(p, ((DropTarget) dtde.getSource()).getComponent());
setDropCode(p);
DRAGGED_OVER_INFO = placeholder;
dtde.acceptDrag(dtde.getDropAction());
}
else {
dtde.rejectDrag();
}
}
@Override
public synchronized void dragOver(DropTargetDragEvent dtde) {
super.dragOver(dtde);
// On Mac, sometimes this component is not showing,
// which causes exception in the translate method.
if (!isShowing()) {
dtde.rejectDrag();
return;
}
if (dtde.isDataFlavorSupported(ComponentTransferable.localComponentProviderFlavor)) {
Point p = dtde.getLocation();
translate(p, ((DropTarget) dtde.getSource()).getComponent());
setDropCode(p);
DRAGGED_OVER_INFO = placeholder;
dtde.acceptDrag(dtde.getDropAction());
}
else {
dtde.rejectDrag();
}
}
@Override
public synchronized void dragExit(DropTargetEvent dte) {
super.dragExit(dte);
DROP_CODE = DropCode.WINDOW;
DROP_CODE_SET = true;
DRAGGED_OVER_INFO = null;
}
}
public void installDragDropTarget(Component component) {
new DockableComponentDropTarget(component);
}
private void initializeComponents(Component comp) {
if (comp instanceof CellRendererPane) {
return;
}
if (comp instanceof Container) {
Container c = (Container) comp;
c.addContainerListener(this);
Component comps[] = c.getComponents();
for (Component comp2 : comps) {
initializeComponents(comp2);
}
}
DropTarget dt = comp.getDropTarget();
if (dt != null) {
new CascadedDropTarget(comp, dockableDropTarget, dt);
}
if (comp.isFocusable()) {
installPopupListenerFirst(comp);
}
}
/**
* Remove and re-add all mouse listeners so our popup listener can go first. This allows our
* popup listener to consume the event, preventing Java UI listeners from changing the table
* selection when the user is performing a Ctrl-Mouse click on the Mac.
*
* @param comp the component
*/
private void installPopupListenerFirst(Component comp) {
comp.removeMouseListener(popupListener);
MouseListener[] listeners = comp.getMouseListeners();
for (MouseListener l : listeners) {
comp.removeMouseListener(l);
}
comp.addMouseListener(popupListener);
for (MouseListener l : listeners) {
comp.addMouseListener(l);
}
}
private void deinitializeComponents(Component comp) {
if (comp instanceof CellRendererPane) {
return;
}
if (comp instanceof Container) {
Container c = (Container) comp;
c.removeContainerListener(this);
Component comps[] = c.getComponents();
for (Component comp2 : comps) {
deinitializeComponents(comp2);
}
}
DropTarget dt = comp.getDropTarget();
if (dt instanceof CascadedDropTarget) {
CascadedDropTarget cascadedDropTarget = (CascadedDropTarget) dt;
DropTarget newDropTarget = cascadedDropTarget.removeDropTarget(dockableDropTarget);
comp.setDropTarget(newDropTarget);
}
comp.removeMouseListener(popupListener);
}
/**
* Sets the drop code base on the cursor location.
* @param p the cursor location.
*/
private void setDropCode(Point p) {
DROP_CODE_SET = true;
if (placeholder == null) {
DROP_CODE = DropCode.ROOT;
return;
}
if (SOURCE_INFO == null) {
DROP_CODE = DropCode.WINDOW;
return;
}
if (SOURCE_INFO.getNode().winMgr != placeholder.getNode().winMgr) {
DROP_CODE = DropCode.WINDOW;
return;
}
if (SOURCE_INFO == placeholder && !placeholder.isStacked()) {
DROP_CODE = DropCode.INVALID;
return;
}
else if (p.x < DROP_EDGE_OFFSET) {
DROP_CODE = DropCode.LEFT;
}
else if (p.x > getWidth() - DROP_EDGE_OFFSET) {
DROP_CODE = DropCode.RIGHT;
}
else if (p.y < DROP_EDGE_OFFSET) {
DROP_CODE = DropCode.TOP;
}
else if (p.y > getHeight() - DROP_EDGE_OFFSET) {
DROP_CODE = DropCode.BOTTOM;
}
else if (SOURCE_INFO == placeholder) {
DROP_CODE = DropCode.INVALID;
}
else {
DROP_CODE = DropCode.STACK;
}
}
void setSelected(boolean selected) {
header.setSelected(selected);
}
/**
* Signals to use the GUI to make this component stand out from the rest.
*/
void emphasize() {
header.emphasize();
}
void setTitle(String title) {
header.setTitle(title);
}
void setIcon(Icon icon) {
header.setIcon(icon);
}
void dispose() {
header.dispose();
header = null;
placeholder = null;
providerComp = null;
actionMgr = null;
}
/**
* Notifies the header that an action was added.
* @param action the action that was added.
*/
void actionAdded(DockingActionIf action) {
header.actionAdded(action);
}
/**
* Notifies the header that an action was removed.
* @param action the action that was removed.
*/
void actionRemoved(DockingActionIf action) {
header.actionRemoved(action);
}
@Override
public void requestFocus() {
if (lastFocusedComponent != null && lastFocusedComponent.isShowing()) {
lastFocusedComponent.requestFocus();
return;
}
if (placeholder == null) {
return; // this implies we have been disposed
}
placeholder.getProvider().requestFocus();
}
void setFocusedComponent(Component newFocusedComponet) {
// remember it so we can restore it later when necessary
lastFocusedComponent = newFocusedComponet;
}
private void componentSelected(Component component) {
if (!component.isFocusable()) {
// In this case, Java will not change focus for us, so we need to tell the DWM to
// change the active DockableComponent
requestFocus();
}
}
@Override
public void componentAdded(ContainerEvent e) {
initializeComponents(e.getChild());
}
@Override
public void componentRemoved(ContainerEvent e) {
deinitializeComponents(e.getChild());
}
}