ghidra/Ghidra/Framework/Docking/src/main/java/docking/ComponentProvider.java

824 lines
25 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.Component;
import java.awt.KeyboardFocusManager;
import java.awt.event.MouseEvent;
import java.util.*;
import javax.swing.*;
import docking.action.*;
import docking.help.HelpDescriptor;
import docking.help.HelpService;
import ghidra.util.*;
import ghidra.util.exception.AssertException;
import utilities.util.reflection.ReflectionUtilities;
/**
* Abstract base class for creating dockable GUI components within a tool.
* <p>
* The one method that must be implemented is {@link #getComponent()} which is where the top level
* Swing JComponent is returned to be docked into the tool. Typically, the GUI components are
* created in the constructor along with any local actions for the provider. The getComponent()
* method then simply returns the top level component previously created by this provider.
* <p>
* There are many other methods for configuring how to dock the component, set title information,
* configure grouping, set the help, add actions, and receive show/hide notifications, some
* of which are highlighted below. Typically, implementers will use these methods to configure
* how the GUI component behaves within the tool, and then add the business logic that uses and reacts
* to the GUI components created in this provider.
* <p>
* To effectively use this class you merely need to create your component, add your actions to
* this class ({@link #addLocalAction(DockingActionIf)}) and then add this provider to the tool
* ({@link #addToTool()}).
* <p>
* This also provides several useful convenience methods:
* <ul>
* <li>{@link #addLocalAction(DockingActionIf)}
* <li>{@link #addToTool()}
* <li>{@link #setVisible(boolean)}
* <li>{@link #setTitle(String)}
* <li>{@link #setIcon(Icon)}
* </ul>
* <p>
* There are a handful of stub methods that can be overridden as desired:
* <ul>
* <li>{@link #componentActivated()} and {@link #componentDeactived()}
* <li>{@link #componentHidden()} and {@link #componentShown()}
* </ul>
*
* <p>
* <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 #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.
*/
public abstract class ComponentProvider implements HelpDescriptor, ActionContextProvider {
private static final String TRANSIENT_PROVIDER_TOOLBAR_WARNING_MESSAGE =
"Transient providers are not added to the toolbar";
private static final String TRANSIENT_PROVIDER_KEY_BINDING_WARNING_MESSAGE =
"Transient providers cannot have key bindings";
public static final String DEFAULT_WINDOW_GROUP = "Default";
private static final String TOOLBAR_GROUP = "View";
// maps for mapping old provider names and owner to new names and/or owner
private static Map<String, String> oldOwnerMap = new HashMap<>();
private static Map<String, String> oldNameMap = new HashMap<>();
protected DockingTool dockingTool;
private String name;
private final String owner;
private String title;
private String subTitle;
private String tabText;
private Set<DockingActionIf> actionSet = new LinkedHashSet<>();
/** True if this provider's action should appear in the toolbar */
private boolean addToolbarAction;
private boolean isTransient;
private KeyBindingData defaultKeyBindingData;
private Icon icon;
private String windowMenuGroup;
private String group = DEFAULT_WINDOW_GROUP;
private WindowPosition defaultWindowPosition = WindowPosition.WINDOW;
private WindowPosition defaultIntraGroupPosition = WindowPosition.STACK;
private DockingAction showProviderAction;
private HelpLocation helpLocation;
private final Class<?> contextType;
private long instanceID = UniversalIdGenerator.nextID().getValue();
private boolean instanceIDHasBeenInitialized;
private String inceptionInformation;
/**
* Creates a new component provider with a default location of {@link WindowPosition#WINDOW}.
* @param tool The tool will manage and show this provider
* @param name The providers name. This is used to group similar providers into a tab within
* the same window.
* @param owner The owner of this provider, usually a plugin name.
*/
public ComponentProvider(DockingTool tool, String name, String owner) {
this(tool, name, owner, null);
}
/**
* Creates a new component provider with a default location of {@link WindowPosition#WINDOW}.
* @param tool The tool that will manage and show this provider.
* @param name The providers name. This is used to group similar providers into a tab within
* the same window.
* @param owner The owner of this provider, usually a plugin name.
* @param contextType the type of context supported by this provider; may be null (see
* {@link #getContextType()}
*/
public ComponentProvider(DockingTool tool, String name, String owner, Class<?> contextType) {
this.dockingTool = tool;
this.name = name;
this.owner = owner;
this.title = name;
this.contextType = contextType;
recordInception();
}
/**
* Returns the action used to show this provider
* @return the action
*/
DockingActionIf getShowProviderAction() {
createShowProviderAction();
return showProviderAction;
}
private void createShowProviderAction() {
if (showProviderAction != null) {
return;
}
if (addToolbarAction) {
Objects.requireNonNull(icon,
"The provider's icon cannot be null when requesting the provider's action " +
"appear in the toolbar");
}
boolean supportsKeyBindings = !isTransient;
showProviderAction = new ShowProviderAction(supportsKeyBindings);
}
/**
* Returns the component to be displayed
* @return the component to be displayed
*/
public abstract JComponent getComponent();
/**
* A method that allows children to set the <tt>instanceID</tt> to a desired value (useful for
* restoring saved IDs).
* <p>
* Note: this can be called only once during the lifetime of the calling instance; otherwise, an
* {@link AssertException} will be thrown.
* @param newID the new ID of this provider
*/
protected void initializeInstanceID(long newID) {
if (instanceIDHasBeenInitialized) {
if (newID != instanceID) {
throw new AssertException("Cannot initialize the instanceID more than once");
}
}
instanceIDHasBeenInitialized = true;
instanceID = newID;
}
/**
* A unique ID for this provider
* @return unique ID for this provider
*/
public final long getInstanceID() {
return instanceID;
}
// Default implementation
public void requestFocus() {
KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager();
Component focusOwner = kfm.getFocusOwner();
if (focusOwner != null && SwingUtilities.isDescendingFrom(focusOwner, getComponent())) {
return;
}
getComponent().requestFocus();
}
/**
* Returns true if this provider has focus
*
* @return true if this provider has focus
*/
public boolean isFocusedProvider() {
DockingWindowManager dwm = DockingWindowManager.getInstance(getComponent());
if (dwm == null) {
return false; // can happen in testing
}
ComponentPlaceholder placeholder = dwm.getFocusedComponent();
return placeholder != null && placeholder.getProvider() == this;
}
/**
* Adds this provider to the tool in a new window that is not initially visible. The provider
* will then show up in the "Windows" menu of the tool
*/
public void addToTool() {
if (isInTool()) {
throw new IllegalStateException("Component already added: " + name);
}
dockingTool.addComponentProvider(this, false);
for (DockingActionIf action : actionSet) {
dockingTool.addLocalAction(this, action);
}
if (subTitle != null) {
setSubTitle(subTitle);
}
}
/**
* Removes this provider from the tool.
*/
public void removeFromTool() {
dockingTool.removeComponentProvider(this);
}
/**
* Adds the given action to the system and associates it with this provider.
* @param action The action to add.
*/
protected void addLocalAction(DockingActionIf action) {
if (actionSet.contains(action)) {
return;
}
actionSet.add(action);
if (isInTool()) {
dockingTool.addLocalAction(this, action);
}
}
/**
* Removes the given action from the system.
* @param action The action to remove.
*/
protected void removeLocalAction(DockingAction action) {
actionSet.remove(action);
if (isInTool()) {
dockingTool.removeLocalAction(this, action);
}
}
/**
* Convenience method to show or hide this provider.
* @param visible True shows the provider; false hides the provider
*/
public void setVisible(boolean visible) {
if (visible && !isInTool()) {
addToTool();
}
dockingTool.showComponentProvider(this, visible);
}
public void toFront() {
dockingTool.toFront(this);
}
public boolean isInTool() {
if (dockingTool == null) {
return false;
}
DockingWindowManager manager = dockingTool.getWindowManager();
if (manager == null) {
return false;
}
return manager.containsProvider(this);
}
/**
* A signal used when installing actions. Some actions are only added to a given window
* if there is a provider in that window that can work with that action. Providers can return
* a context class from this method to control whether dependent actions get added. Most
* providers return null for this method, which means they will not have any dependent
* actions added to windows other than the primary application window.
*
* @return a class representing the desired context type or null;
*/
public Class<?> getContextType() {
return contextType;
}
/**
* Convenience method to indicate if this provider is showing.
* @return true if this provider is showing.
*/
public boolean isVisible() {
return dockingTool.isVisible(this);
}
/**
* Convenience method to indicate if this provider is the active provider (has focus)
* @return true if this provider is active.
*/
public boolean isActive() {
return dockingTool.isActive(this);
}
/**
* This is the callback that will happen when the user presses the 'X' button of a provider.
* Transient providers will be removed from the tool completely. Non-transient providers
* will merely be hidden.
*
* <P>Subclasses may override this method to prevent a provider from being closed; for
* example, if an editor has unsaved changes, then this method could prevent the close from
* happening.
*/
public void closeComponent() {
if (isTransient) {
removeFromTool();
}
else {
setVisible(false);
}
}
/**
* Notifies the component provider that it is now the active provider
*/
public void componentActivated() {
// subclasses implement as needed
}
/**
* Notifies the component provider that it is no longer the active provider
*/
public void componentDeactived() {
// subclasses implement as needed
}
/**
* Notifies the provider that the component is being hidden. This happens when the
* provider is being closed.
*/
public void componentHidden() {
// subclasses implement as needed
}
/**
* Notifies the provider that the component is being shown.
*/
public void componentShown() {
// subclasses implement as needed
}
/**
* Returns the context object which corresponds to the
* area of focus within this provider's component. Null
* is returned when there is no context.
* @param event popup event which corresponds to this request.
* May be null for key-stroke or other non-mouse event.
*/
@Override
public ActionContext getActionContext(MouseEvent event) {
return new ActionContext(this, getComponent());
}
/**
* Kicks the tool to let it know the context for this provider has changed.
*/
public void contextChanged() {
dockingTool.contextChanged(this);
}
/**
* Returns the general HelpLocation for this provider. Should return null only if no
* help documentation exists.
*
* @return the help location
*/
public HelpLocation getHelpLocation() {
return helpLocation;
}
public void setHelpLocation(HelpLocation helpLocation) {
this.helpLocation = helpLocation;
HelpService helpService = DockingWindowManager.getHelpService();
helpService.registerHelp(this, helpLocation);
if (showProviderAction != null) {
showProviderAction.setHelpLocation(helpLocation);
}
}
/**
* Returns the Icon associated with the component view
* @return the Icon associated with the component view
*/
public Icon getIcon() {
return icon;
}
/**
* Returns the name of this provider
* @return the name of this provider
*/
public String getName() {
return name;
}
/**
* Returns the owner of this provider (usually a plugin)
* @return the owner of this provider
*/
public String getOwner() {
return owner;
}
/**
* Sets the provider's title.
* @param title the title string to use.
*/
public void setTitle(String title) {
this.title = title;
if (isInTool()) {
dockingTool.updateTitle(this);
}
}
/**
* Sets the provider's sub-title (Sub-titles don't show up
* in the window menu).
* @param subTitle the sub-title string to use.
*/
public void setSubTitle(String subTitle) {
this.subTitle = subTitle;
if (isInTool()) {
dockingTool.updateTitle(this);
}
}
/**
* Sets the text to be displayed on tabs when provider is stacked with other providers.
* @param tabText the tab text.
*/
public void setTabText(String tabText) {
this.tabText = tabText;
}
/**
* Returns the provider's current title.
* @return the provider's current title.
*/
public String getTitle() {
return title;
}
/**
* Returns the provider's current sub-title (Sub-titles don't show up
* in the window menu).
* @return the provider's current sub-title.
*/
public String getSubTitle() {
return subTitle;
}
/**
* Returns the optionally set text to display in the tab for a component provider. The
* text returned from {@link #getTitle()} will be used by default.
*
* @return the optionally set text to display in the tab for a component provider.
* @set {@link #setTabText(String)}
*/
public String getTabText() {
return tabText;
}
/**
* Sets the default key binding that will show this provider when pressed. This value can
* be changed by the user and saved as part of the Tool options.
*
* @param kbData the key binding
*/
protected void setKeyBinding(KeyBindingData kbData) {
if (isInTool()) {
throw new IllegalStateException(
"Cannot set the default key binding after the provider is added to the tool");
}
this.defaultKeyBindingData = kbData;
if (isTransient && kbData != null) {
Msg.error(this, TRANSIENT_PROVIDER_KEY_BINDING_WARNING_MESSAGE,
ReflectionUtilities.createJavaFilteredThrowable());
this.defaultKeyBindingData = null;
}
}
/**
* Convenience method for setting the provider's icon
* @param icon the icon to use for this provider
*/
protected void setIcon(Icon icon) {
this.icon = icon;
if (!isInTool()) {
return;
}
if (addToolbarAction && showProviderAction != null) {
Objects.requireNonNull(icon, "Icon cannot be set to null when using a toolbar action");
showProviderAction.setToolBarData(new ToolBarData(icon));
}
dockingTool.getWindowManager().setIcon(this, icon);
}
/**
* Signals that this provider's action for showing the provider should appear in the main
* toolbar
*/
protected void addToToolbar() {
this.addToolbarAction = true;
if (isTransient) {
Msg.error(this, TRANSIENT_PROVIDER_TOOLBAR_WARNING_MESSAGE,
ReflectionUtilities.createJavaFilteredThrowable());
addToolbarAction = false;
}
}
/**
* Returns the name of a cascading sub-menu name to use when when showing this provider in the
* "Window" menu. If the group name is null, the item will appear in the top-level menu.
* @return the menu group for this provider or null if this provider should appear in the
* top-level menu.
*/
public String getWindowSubMenuName() {
return windowMenuGroup;
}
/**
* Returns true if this component goes away during a user session (most providers remain in
* the tool all session long, visible or not)
* @return true if transient
*/
public boolean isTransient() {
return isTransient || isSnapshot();
}
/**
* A special marker that indicates this provider is a snapshot of a primary provider,
* somewhat like a picture of the primary provider.
*
* @return true if a snapshot
*/
public boolean isSnapshot() {
return false;
}
/**
* Sets this class to be transient. Setting this provider to be transient will cause
* this provider to be removed from the tool when the corresponding window is closed.
*/
protected void setTransient() {
isTransient = true;
if (isInTool()) {
throw new IllegalStateException(
"A component provider cannot be marked as 'transient' " +
"after it is added to the tool");
}
// avoid visually disturbing the user by adding/removing toolbar actions for temp providers
if (addToolbarAction) {
addToolbarAction = false;
Msg.error(this, TRANSIENT_PROVIDER_TOOLBAR_WARNING_MESSAGE,
ReflectionUtilities.createJavaFilteredThrowable());
}
if (defaultKeyBindingData != null) {
defaultKeyBindingData = null;
Msg.error(this, TRANSIENT_PROVIDER_KEY_BINDING_WARNING_MESSAGE,
ReflectionUtilities.createJavaFilteredThrowable());
}
}
/**
* Sets the window menu group. If the window menu group is null, the corresponding window menu
* item will appear in the root menu, otherwise it will appear in a
* sub-menu named <code>group</code>.
*
* @param group the name of the window's sub-menu for this provider
*/
protected void setWindowMenuGroup(String group) {
this.windowMenuGroup = group;
}
/**
* The initial {@link WindowPosition} of this provider. If a {@link #getWindowGroup() window
* group} is provided, then this position is relative to that provider. Otherwise, this
* position is relative to the tool window.
*
* @return The initial {@link WindowPosition} of this provider.
*/
public WindowPosition getDefaultWindowPosition() {
return defaultWindowPosition;
}
/**
* Sets the default position of this provider when being shown for the first time. If the
* providers position in the tool has been saved before, then this value is ignored.
*
* @param windowPosition the position
* @see #getDefaultWindowPosition()
*/
protected void setDefaultWindowPosition(WindowPosition windowPosition) {
defaultWindowPosition = windowPosition;
}
/**
* The position of this provider when being placed with other members of the same group. As
* an example, assume this provider is being shown for the first time while there is another
* member of its {@link #getWindowGroup() window group} already visible. Further, assume
* that this method will return {@link WindowPosition#STACK}. This provider will then be
* stacked upon the already showing provider.
* <p>
* To determine where this provider should be initially shown,
* see {@link #getDefaultWindowPosition()}.
*
* @return The position of this provider when being placed with other members of the same group.
*/
public WindowPosition getIntraGroupPosition() {
return defaultIntraGroupPosition;
}
/**
* See {@link #getIntraGroupPosition()}.
*
* @param position the new position
*/
public void setIntraGroupPosition(WindowPosition position) {
this.defaultIntraGroupPosition = position;
}
/**
* Returns an optional group designator that, if non-null, the docking window manager uses to
* determine the initial location of the new component relative to any existing instances
* of this component Provider.
* <p>
* The docking window manager will use {@link #getIntraGroupPosition() Intra-group Position}
* to decide where to place this provider inside of the already open instances of the
* same group. The default position is 'stack', which results in the new instance being
* stacked with other instances of this provider that have the same group unless that instance is
* the active provider or is currently stacked with the active provider. (This is to prevent
* new windows from covering the active window).
*
* @return the window group
*/
public String getWindowGroup() {
return group;
}
/**
* Sets the window group. See {@link #getWindowGroup()}.
*
* @param group the group for this provider.
*/
protected void setWindowGroup(String group) {
this.group = group;
}
@Override
public String getHelpInfo() {
return " PROVIDER: " + getName() + "\n";
}
@Override
public Object getHelpObject() {
return this;
}
public DockingTool getTool() {
return dockingTool;
}
@Override
public String toString() {
return name + " - " + getTitle() + " - " + getSubTitle();
}
private void recordInception() {
if (!SystemUtilities.isInDevelopmentMode()) {
inceptionInformation = "";
return;
}
inceptionInformation = getInceptionFromTheFirstClassThatIsNotUs();
}
private String getInceptionFromTheFirstClassThatIsNotUs() {
Throwable t = ReflectionUtilities.createThrowableWithStackOlderThan(getClass());
StackTraceElement[] trace = t.getStackTrace();
String classInfo = trace[0].toString();
return classInfo;
}
/**
* Returns any registered new provider name for the oldName/oldOwner pair.
* @param oldOwner the old owner name
* @param oldName the old provider name
* @return the new provider name for that oldOwner/oldName
*/
public static String getMappedOwner(String oldOwner, String oldName) {
String key = getKey(oldOwner, oldName);
return oldOwnerMap.get(key);
}
/**
* Returns any registered new provider owner for the oldName/oldOwner pair.
* @param oldOwner the old owner name
* @param oldName the old provider name
* @return the new provider owner for that oldOwner/oldName
*/
public static String getMappedName(String oldOwner, String oldName) {
String key = getKey(oldOwner, oldName);
return oldNameMap.get(key);
}
/**
* Register a name and/or owner change to a provider so that old tools can restore those
* provider windows to their old position and size. Note you must supply all four
* arguments. If the name or owner did not change, use the name or owner that did not change
* for both the old and new values.
*
* <p>Note: when you make use of this method, please signal when it is safe to remove
* its usage.
*
* @param oldName the old name of the provider.
* @param oldOwner the old owner of the provider.
* @param newName the new name of the provider. If the name did not change, use the old name here.
* @param newOwner the new owner of the provider. If the owner did not change, use the old owner here.
*/
public static void registerProviderNameOwnerChange(String oldName, String oldOwner,
String newName, String newOwner) {
String key = getKey(oldOwner, oldName);
oldOwnerMap.put(key, newOwner);
oldNameMap.put(key, newName);
}
private static String getKey(String oldOwner, String oldName) {
return "owner=" + oldOwner + "name=" + oldName;
}
private class ShowProviderAction extends DockingAction {
ShowProviderAction(boolean supportsKeyBindings) {
super(name, owner,
supportsKeyBindings ? KeyBindingType.SHARED : KeyBindingType.UNSUPPORTED);
if (addToolbarAction) {
setToolBarData(new ToolBarData(icon, TOOLBAR_GROUP));
}
if (supportsKeyBindings && defaultKeyBindingData != null) {
// this action itself is not 'key binding managed', but the system *will* use
// any key binding value we set when connecting 'shared' actions
setKeyBindingData(defaultKeyBindingData);
}
setDescription("Display " + name);
HelpLocation providerHelp = ComponentProvider.this.getHelpLocation();
if (providerHelp != null) {
setHelpLocation(providerHelp);
}
}
@Override
public void actionPerformed(ActionContext context) {
DockingWindowManager myDwm = DockingWindowManager.getInstance(getComponent());
if (myDwm == null) {
// this can happen when the tool loses focus
dockingTool.showComponentProvider(ComponentProvider.this, true);
return;
}
myDwm.showComponent(ComponentProvider.this, true, true);
}
@Override
protected String getInceptionFromTheFirstClassThatIsNotUs() {
// overridden to show who created the provider, as that is what this action represents
return inceptionInformation;
}
}
}