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

1514 lines
41 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.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.Timer;
import org.apache.commons.lang3.StringUtils;
import org.jdesktop.animation.timing.Animator;
import org.jdesktop.animation.timing.TimingTargetAdapter;
import docking.action.*;
import docking.action.builder.ActionBuilder;
import docking.actions.SharedActionRegistry;
import docking.actions.ToolActions;
import docking.event.mouse.GMouseListenerAdapter;
import docking.menu.DialogToolbarButton;
import docking.util.AnimationUtils;
import docking.widgets.label.GDHtmlLabel;
import generic.theme.GColor;
import generic.theme.GThemeDefaults.Colors.Messages;
import ghidra.util.*;
import ghidra.util.exception.AssertException;
import ghidra.util.task.*;
import help.HelpService;
import utility.function.Callback;
/**
* Base class used for creating dialogs in Ghidra. Subclass this to create a dialog provider that has
* all the gui elements to appear in the dialog, then use tool.showDialog() to display your dialog.
*/
public class DialogComponentProvider
implements ActionContextProvider, StatusListener, TaskListener {
private static final String CLOSE_ACTION_NAME = "Close Dialog";
private static final Color FG_COLOR_ALERT = new GColor("color.fg.dialog.status.alert");
private static final Color FG_COLOR_ERROR = new GColor("color.fg.dialog.status.error");
private static final Color FG_COLOR_WARNING = new GColor("color.fg.dialog.status.warning");
private static final Color FG_COLOR_NORMAL = new GColor("color.fg.dialog.status.normal");
private final static int DEFAULT_DELAY = 750;
private static final String PROGRESS = "Progress";
private static final String DEFAULT = "No Progress";
private static int idCounter;
private int id = ++idCounter;
private boolean modal;
private String title;
protected JPanel rootPanel;
private JPanel mainPanel;
private JComponent workPanel;
protected JPanel buttonPanel;
private JPanel statusPanel;
protected JButton okButton;
protected JButton applyButton;
protected JButton cancelButton;
protected JButton dismissButton;
private boolean isAlerting;
private GDHtmlLabel statusLabel;
private JPanel statusProgPanel; // contains status panel and progress panel
private Timer showTimer;
private TaskScheduler taskScheduler;
private TaskMonitorComponent taskMonitorComponent;
private static final KeyStroke ESC_KEYSTROKE = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
private CardLayout progressCardLayout;
private JButton defaultButton;
DockingDialog dialog;
private Component focusComponent;
private JPanel toolbar;
private Map<DockingActionIf, DialogToolbarButton> toolbarButtonsByAction = new HashMap<>();
private DialogComponentProviderPopupActionManager popupManager =
new DialogComponentProviderPopupActionManager(this);
private PopupHandler popupHandler = new PopupHandler();
private Set<DockingActionIf> dialogActions = new HashSet<>();
// we track these separately so that we can remove them on dispose
private Set<DialogActionProxy> keyBindingProxyActions = new HashSet<>();
private Point initialLocation;
private boolean resizeable = true;
private boolean rememberLocation = true;
private boolean rememberSize = true;
private boolean useSharedLocation = false;
private boolean isTransient = false;
private Dimension defaultSize;
private String accessibleDescription;
private Tool tool;
/**
* Constructor for a DialogComponentProvider that will be modal and will include a status line and
* a button panel. Its title will be the same as its name.
* @param title the dialog title.
*/
protected DialogComponentProvider(String title) {
this(title, true, true, true, false);
}
/**
* Constructor for a DialogComponentProvider that will include a status line and a button panel.
* @param title the title for this dialog.
* @param modal true if this dialog should be modal.
*/
protected DialogComponentProvider(String title, boolean modal) {
this(title, modal, true, true, false);
}
/**
* Constructs a new DialogComponentProvider.
* @param title the title for this dialog.
* @param modal true if this dialog should be modal.
* @param includeStatus true if this dialog should include a status line.
* @param includeButtons true if this dialog will have a button panel at
* the bottom.
* @param canRunTasks true means this dialog can execute tasks
* ({@link #executeProgressTask(Task, int)} and it will show a progress monitor when
* doing so.
*/
protected DialogComponentProvider(String title, boolean modal, boolean includeStatus,
boolean includeButtons, boolean canRunTasks) {
this.modal = modal;
this.title = title;
rootPanel = new JPanel(new BorderLayout()) {
@Override
public Dimension getPreferredSize() {
Dimension minSize = getMinimumSize();
Dimension preferredSize = super.getPreferredSize();
preferredSize.width = Math.max(minSize.width, preferredSize.width);
preferredSize.height = Math.max(minSize.height, preferredSize.height);
return preferredSize;
}
};
mainPanel = new JPanel(new BorderLayout());
mainPanel.setBorder(BorderFactory.createEtchedBorder());
rootPanel.add(mainPanel, BorderLayout.CENTER);
taskScheduler = new TaskScheduler(this);
buttonPanel = new JPanel(new GridLayout(1, 0, 6, 0));
buttonPanel.setBorder(BorderFactory.createEmptyBorder(3, 0, 0, 0));
statusPanel = buildStatusPanel();
if (canRunTasks) {
progressCardLayout = new CardLayout();
statusProgPanel = new JPanel(progressCardLayout);
taskMonitorComponent = new TaskMonitorComponent();
statusProgPanel.add(statusPanel, DEFAULT);
statusProgPanel.add(taskMonitorComponent, PROGRESS);
progressCardLayout.show(statusProgPanel, DEFAULT);
mainPanel.add(statusProgPanel, BorderLayout.SOUTH);
}
else if (includeStatus) {
mainPanel.add(statusPanel, BorderLayout.SOUTH);
}
if (includeButtons) {
JPanel panel = new JPanel(new FlowLayout());
panel.add(buttonPanel);
rootPanel.add(panel, BorderLayout.SOUTH);
}
doInitialize();
}
/**
* Called by the framework during startup to register actions that are shared throughout the
* tool. See {@link SharedActionRegistry}.
* @param tool the tool
* @param toolActions the class to which the actions should be added
* @param owner the shared action owner
*/
public static void createSharedActions(Tool tool, ToolActions toolActions, String owner) {
DockingAction closeAction = new ActionBuilder(CLOSE_ACTION_NAME, owner)
.sharedKeyBinding()
.keyBinding(ESC_KEYSTROKE)
.withContext(DialogActionContext.class)
.enabledWhen(c -> c.getDialogComponentProvider() != null)
.onAction(c -> {
DialogComponentProvider dcp = c.getDialogComponentProvider();
dcp.escapeCallback();
})
.build();
toolActions.addGlobalAction(closeAction);
}
/** a callback mechanism for children to do work */
protected void doInitialize() {
// may be overridden by subclasses
}
/**
* Returns true if the given action is one that has been registered by this dialog.
* @param action the action
* @return true if the given action is one that has been registered by this dialog
*/
public boolean isDialogKeyBindingAction(DockingActionIf action) {
if (action instanceof DockingActionProxy proxy) {
return keyBindingProxyActions.contains(proxy);
}
String name = action.getName();
if (name.equals(CLOSE_ACTION_NAME)) {
return true;
}
return false;
}
public int getId() {
return id;
}
public JComponent getComponent() {
return rootPanel;
}
protected void repack() {
if (dialog != null) {
dialog.pack();
}
}
protected void setDialogSize(Dimension d) {
if (dialog != null) {
dialog.setSize(d);
}
}
protected Dimension getDialogSize() {
if (dialog != null) {
return dialog.getSize();
}
return null;
}
/**
* Sets the background on this component.
* @param color The color to set.
*/
public void setBackground(Color color) {
rootPanel.setBackground(color);
}
/**
* Gets the background color of this component.
* @return The background color of this component.
*/
public Color getBackground() {
return rootPanel.getBackground();
}
/**
* Sets the preferred size of the dialog. Note that if you set the preferred size, the
* dialog will ignore any natural preferred size of your components.
* @param width the preferred width
* @param height the preferred height;
*/
public void setPreferredSize(int width, int height) {
this.defaultSize = new Dimension(width, height);
}
public void setDefaultSize(int width, int height) {
this.defaultSize = new Dimension(width, height);
}
public Dimension getDefaultSize() {
return defaultSize;
}
public void setMinimumSize(int width, int height) {
rootPanel.setMinimumSize(new Dimension(width, height));
}
/**
* Sets the minimum size of the dialog
* @param minSize the min size of the dialog
*/
public void setMinimumSize(Dimension minSize) {
setMinimumSize(minSize.width, minSize.height);
}
/**
* Gets the bound of this dialog component. This is relative the parent of this provider.
* @return the bound of this dialog component.
* @see Component#getBounds()
*/
protected Rectangle getBounds() {
return rootPanel.getBounds();
}
/**
* Gets the location of this provider on the screen. Calling {@link #getBounds()} provides
* a relative location. This method provides a location absolute on the screen.
* @return the location of this provider on the screen.
* @see Component#getLocationOnScreen()
*/
protected Point getLocationOnScreen() {
return rootPanel.getLocationOnScreen();
}
/**
* Returns the preferred size of this component.
* @return the preferred size of this component.
*/
public Dimension getPreferredSize() {
return rootPanel.getPreferredSize();
}
/**
* Sets the cursor on the root panel for the dialog component.
* @param cursor the cursor to use.
*/
public void setCursor(Cursor cursor) {
rootPanel.setCursor(cursor);
}
/**
* Used by derived classes to add dialog specific gui elements
* @param comp the Component containing the derived class's components.
*/
protected void addWorkPanel(JComponent comp) {
workPanel = comp;
mainPanel.add(workPanel, BorderLayout.CENTER);
installMouseListener(workPanel);
mainPanel.validate();
}
protected void removeWorkPanel() {
if (workPanel != null) {
mainPanel.remove(workPanel);
uninstallMouseListener(workPanel);
mainPanel.validate();
}
}
private void installMouseListener(Component component) {
if (component instanceof CellRendererPane) {
return;
}
if (component instanceof Container) {
Container c = (Container) component;
c.addContainerListener(popupHandler);
Component comps[] = c.getComponents();
for (Component comp : comps) {
installMouseListener(comp);
}
}
if (component.isFocusable()) {
component.addMouseListener(popupHandler);
}
}
private void uninstallMouseListener(Component comp) {
if (comp instanceof CellRendererPane) {
return;
}
if (comp instanceof Container) {
Container c = (Container) comp;
c.removeContainerListener(popupHandler);
Component comps[] = c.getComponents();
for (Component comp2 : comps) {
uninstallMouseListener(comp2);
}
}
comp.removeMouseListener(popupHandler);
}
/**
* Adds a button to the button panel at the bottom of the dialog.
* Buttons will be added from left to right.
* <p>
* Implementation Note: Calling this method will set the given button as the default button
* on this dialog when:
* <ul>
* <li>
* No button has yet been added, and
* </li>
* <li>
* No default button has been assigned
* </li>
* </ul>
* To change this behavior, call {@link #setDefaultButton(JButton)} with the desired
* default button.
*
* @param button the button
*/
protected void addButton(JButton button) {
if (defaultButton == null && buttonPanel.getComponentCount() == 0) {
// The first button we add will be a suitable default 'default button'.
setDefaultButton(button);
}
buttonPanel.add(button);
}
/**
* Remove the given button from the dialog
* @param button the button
*/
protected void removeButton(JButton button) {
buttonPanel.remove(button);
rootPanel.validate();
}
/**
* Execute a non-modal task that has progress and can be cancelled.
*
* @param task task to execute; a progress bar is displayed next to the status field
* in this dialog if the task has progress; for indeterminate tasks, a
* "spinning globe" is displayed to indicate that something is happening.
* @param delay number of milliseconds to delay until progress bar is displayed; a
* value less than or equal to 0 means to show the progress bar immediately
* @throws IllegalArgumentException if the given task is modal
*/
protected void executeProgressTask(Task task, int delay) {
if (taskMonitorComponent == null) {
throw new AssertException("Cannot execute tasks in a " +
"DialogComponentProvider that has not been created to run taks.");
}
if (task.isModal()) {
throw new IllegalArgumentException("Task cannot be modal");
}
task.addTaskListener(this);
taskScheduler.set(task, delay);
}
protected void clearScheduledTask() {
taskScheduler.clearScheduledTask();
}
/**
* Cancel the task that is running.
*
*/
protected void cancelCurrentTask() {
if (taskMonitorComponent != null) {
taskMonitorComponent.cancel();
}
}
/**
* Blocks the calling thread until the current task has completed; used
* by JUnit tests.
*
*/
public void waitForCurrentTask() {
taskScheduler.waitForCurrentTask();
}
/**
* Returns true if this dialog is running a task.
* @return true if this dialog is running a task.
*/
public boolean isRunningTask() {
return taskScheduler.isBusy();
}
/**
* Adds an "OK" button to the button panel. The protected method
* okCallback() will be invoked whenever the "OK" button is pressed.
*/
protected void addOKButton() {
okButton = new JButton("OK");
okButton.setMnemonic('K');
okButton.setName("OK");
okButton.getAccessibleContext().setAccessibleName("OK");
okButton.addActionListener(e -> okCallback());
addButton(okButton);
}
/**
* Adds a "Cancel" button to the button panel. The protected method
* CancelCallback() will be invoked whenever the "Cancel" button is pressed.
*/
protected void addCancelButton() {
cancelButton = new JButton("Cancel");
cancelButton.setMnemonic('C');
cancelButton.setName("Cancel");
cancelButton.getAccessibleContext().setAccessibleName("Cancel");
cancelButton.addActionListener(e -> cancelCallback());
addButton(cancelButton);
}
/**
* Adds a "Dismiss" button to the button panel. The protected method
* dismissCallback() will be invoked whenever the "Dismiss" button is pressed.
*/
protected void addDismissButton() {
dismissButton = new JButton("Dismiss");
dismissButton.setMnemonic('D');
dismissButton.setName("Dismiss");
dismissButton.getAccessibleContext().setAccessibleName("Dismiss");
dismissButton.addActionListener(e -> dismissCallback());
addButton(dismissButton);
}
/**
* Adds an "Apply" button to the button panel. The protected method
* applyCallback() will be invoked whenever the "Apply" button is pressed.
*/
protected void addApplyButton() {
applyButton = new JButton("Apply");
applyButton.setMnemonic('A');
applyButton.setName("Apply");
applyButton.getAccessibleContext().setAccessibleName("Apply");
applyButton.addActionListener(e -> applyCallback());
addButton(applyButton);
}
/**
* Sets the Tooltip for the Apply button
* @param tooltip the tooltip
*/
protected void setApplyToolTip(String tooltip) {
if (applyButton != null) {
applyButton.setToolTipText(tooltip);
}
}
protected void setOkButtonText(String text) {
if (okButton != null) {
okButton.setText(text);
}
}
/**
* Sets the Tooltip for the OK button
* @param tooltip the tooltip
*/
protected void setOkToolTip(String tooltip) {
if (okButton != null) {
okButton.setToolTipText(tooltip);
}
}
/**
* Sets the Tooltip for the Cancel button
* @param tooltip the tooltip
*/
protected void setCancelToolTip(String tooltip) {
if (cancelButton != null) {
cancelButton.setToolTipText(tooltip);
}
}
protected void setCancelButtonText(String text) {
if (cancelButton != null) {
cancelButton.setText(text);
}
}
/**
* Sets the Tooltip for the Dismiss button
* @param tooltip the tooltip
*/
protected void setDismissToolTip(String tooltip) {
if (dismissButton != null) {
dismissButton.setToolTipText(tooltip);
}
}
/**
* Sets the enablement state of the "OK" button.
* @param state true to enable the button, false to disable the button.
*/
protected void setOkEnabled(boolean state) {
if (okButton != null) {
okButton.setEnabled(state);
}
}
/**
* Sets the enablement state of the "CANCEL" button.
* @param state true to enable the button, false to disable the button.
*/
protected void setCancelEnabled(boolean state) {
if (cancelButton != null) {
cancelButton.setEnabled(state);
}
}
/**
* Sets the enablement state of the "Apply" button.
* @param state true to enable the button, false to disable the button.
*/
protected void setApplyEnabled(boolean state) {
if (applyButton != null) {
applyButton.setEnabled(state);
}
}
/**
* Returns true if the cancel button is enabled
* @return true if the cancel button is enabled
*/
protected boolean isCancelEnabled() {
if (cancelButton != null) {
return cancelButton.isEnabled();
}
return false;
}
/**
* Returns true if the OK button is enabled
* @return true if the OK button is enabled
*/
protected boolean isOKEnabled() {
if (okButton != null) {
return okButton.isEnabled();
}
return false;
}
/**
* Returns true if the apply button is enabled
* @return true if the apply button is enabled
*/
protected boolean isApplyEnabled() {
if (applyButton != null) {
return applyButton.isEnabled();
}
return false;
}
/**
* Sets the text in the dialog's status line using the default color
*
* @param text the text to display in the status line
*/
@Override
public void setStatusText(String text) {
setStatusText(text, MessageType.INFO);
}
/**
* Sets the text in the dialog's status line using the specified message type to control
* the color.
*
* @param message the message
* @param type the message type
*/
@Override
public void setStatusText(String message, MessageType type) {
setStatusText(message, type, false);
}
@Override
public void setStatusText(String message, MessageType type, boolean alert) {
String text = StringUtils.isBlank(message) ? " " : message;
Swing.runIfSwingOrRunLater(() -> doSetStatusText(text, type, alert));
}
/**
* Sets a description of the dialog that will be read by screen readers when the dialog
* is made visible.
* @param description a description of the dialog
*/
public void setAccessibleDescription(String description) {
this.accessibleDescription = description;
}
private void doSetStatusText(String text, MessageType type, boolean alert) {
SystemUtilities
.assertThisIsTheSwingThread("Setting text must be performed on the Swing thread");
statusLabel.setText(text);
statusLabel.setForeground(getStatusColor(type));
updateStatusToolTip();
if (alert) {
alertMessage();
}
}
/**
* Signals for this dialog to visually draw the user's attention to the status text
*/
protected void alertMessage() {
alertMessage(Callback.dummy());
}
/**
* Signals for this dialog to visually draw the user's attention to the status text
* @param alertFinishedCallback this will be called when the alert is finished. This allows
* clients to perform work, like re-enabling buttons that were disabled before
* calling this method
*/
protected void alertMessage(Callback alertFinishedCallback) {
Swing.runIfSwingOrRunLater(() -> {
doAlertMessage(alertFinishedCallback);
});
}
private void doAlertMessage(Callback alertFinishedCallback) {
// must be on Swing; this allows us to synchronize the 'alerting' flag
SystemUtilities
.assertThisIsTheSwingThread("Alerting must be performed on the Swing thread");
if (isAlerting) {
return;
}
Callback animatorFinishedCallback = () -> {
statusLabel.setVisible(true);
alertFinishedCallback.call();
isAlerting = false;
};
isAlerting = true;
// Note: manually call validate() so the 'statusLabel' updates its bounds after
// the text has been setStatusText() (validation is buffered which means the
// normal Swing mechanism may not have yet happened).
mainPanel.validate();
statusLabel.setVisible(false); // disable painting in this dialog so we don't see double
Animator animator = AnimationUtils.pulseComponent(statusLabel, 1);
if (animator == null) {
animatorFinishedCallback.call();
}
else {
animator.addTarget(new TimingTargetAdapter() {
@Override
public void end() {
animatorFinishedCallback.call();
}
});
}
}
protected Color getStatusColor(MessageType type) {
switch (type) {
case ALERT:
return FG_COLOR_ALERT;
case WARNING:
return FG_COLOR_WARNING;
case ERROR:
return FG_COLOR_ERROR;
default:
return FG_COLOR_NORMAL;
}
}
/**
* Stop the timer if one was started to delay showing the progress
* bar.
*
*/
protected void stopProgressTimer() {
if (showTimer != null) {
showTimer.stop();
showTimer = null;
}
}
/**
* Will hide the progress panel if it was showing.
*
* @see #showTaskMonitorComponent(String, boolean, boolean)
*/
public void hideTaskMonitorComponent() {
clearProgress();
}
protected void showProgressBar(String localTitle, boolean hasProgress, boolean canCancel,
int delay) {
taskMonitorComponent.reset();
Runnable r = () -> {
if (delay <= 0) {
showProgressBar(localTitle, hasProgress, canCancel);
}
else {
showTimer = new Timer(delay, ev -> {
if (taskScheduler.isBusy()) {
showProgressBar(localTitle, hasProgress, canCancel);
showTimer = null;
}
});
showTimer.setInitialDelay(delay);
showTimer.setRepeats(false);
showTimer.start();
}
};
SystemUtilities.runSwingNow(r);
}
TaskMonitor showProgress(Task task, int delay) {
showProgressBar(task.getTaskTitle(), task.hasProgress(), task.canCancel(), delay);
return taskMonitorComponent;
}
private void showProgressBar(String localTitle, boolean hasProgress, boolean canCancel) {
if (!isVisible()) {
// It doesn't make any sense to show the task monitor when the dialog is not
// visible, so show the dialog
DockingWindowManager.showDialog(getParent(), this);
}
taskMonitorComponent.setTaskName(localTitle);
taskMonitorComponent.showProgress(hasProgress);
taskMonitorComponent.setCancelButtonVisibility(canCancel);
progressCardLayout.show(statusProgPanel, PROGRESS);
rootPanel.validate();
}
private void clearProgress() {
if (taskMonitorComponent != null) {
progressCardLayout.show(statusProgPanel, DEFAULT);
}
}
/**
* If the status text doesn't fit in the dialog, set a tool tip
* for the status label so the user can see what it says.
* If the status message fits then there is no tool tip.
*/
private void updateStatusToolTip() {
Dimension preferredSize = statusLabel.getPreferredSize();
Dimension size = statusLabel.getSize();
if (preferredSize.width > size.width || preferredSize.height > size.height) {
statusLabel.setToolTipText(statusLabel.getOriginalText());
}
else {
statusLabel.setToolTipText(null);
}
}
/**
* Clears the text from the dialog's status line.
*/
@Override
public void clearStatusText() {
Swing.runIfSwingOrRunLater(() -> {
statusLabel.setText(" ");
updateStatusToolTip();
});
}
/**
* Returns the current status in the dialogs status line
*
* @return the status text
*/
public String getStatusText() {
return statusLabel.getText();
}
protected JLabel getStatusLabel() {
return statusLabel;
}
/**
* Get the task scheduler for the dialog
* @return the task scheduler
*
*/
protected TaskScheduler getTaskScheduler() {
return taskScheduler;
}
protected TaskMonitorComponent getTaskMonitorComponent() {
return taskMonitorComponent;
}
/**
* Shows the progress bar for this dialog.
*
* @param localTitle the name of the task
* @param hasProgress true if the progress bar should show progress; false to be indeterminate
* @param canCancel true if the task can be cancelled
* @return the {@link TaskMonitor} used by to communicate progress
* @see #hideTaskMonitorComponent()
*/
public TaskMonitor showTaskMonitorComponent(String localTitle, boolean hasProgress,
boolean canCancel) {
showProgressBar(localTitle, hasProgress, canCancel, DEFAULT_DELAY);
return taskMonitorComponent;
}
/**
* The callback method for when the "Apply" button is pressed.
*/
protected void applyCallback() {
Msg.debug(this, "Apply button pressed");
}
/**
* The callback method for when the "OK" button is pressed.
*/
protected void okCallback() {
Msg.debug(this, "Ok button pressed");
}
/**
* The callback method for when the "Cancel" button is pressed. The
* default behavior is to call setVisible(false) and dispose() on the
* dialog.
*/
protected void cancelCallback() {
close();
}
public void close() {
closeDialog();
dispose();
}
protected void closeDialog() {
if (isShowing()) {
cancelCurrentTask();
dialog.close();
}
}
/**
* Disposes this dialog. Only call this when the dialog is no longer used. Calling this method
* will close the dialog if it is open.
*/
public void dispose() {
closeDialog();
if (tool != null) {
keyBindingProxyActions.forEach(a -> tool.removeAction(a));
}
keyBindingProxyActions.clear();
popupManager.dispose();
dialogActions.forEach(DockingActionIf::dispose);
toolbarButtonsByAction.clear();
dialogActions.clear();
}
/**
* The callback method for when the "Dismiss" button is pressed.
* The default behavior is to call the cancel Callback.
*/
protected void dismissCallback() {
cancelCallback();
}
/**
* The callback method for when the escape key is pressed. The default
* behavior is the call setVisible(false) on the dialog.
*/
protected void escapeCallback() {
dismissCallback();
}
private JPanel buildStatusPanel() {
JPanel panel = new JPanel(new BorderLayout());
statusLabel = new GDHtmlLabel(" ");
statusLabel.setName("statusLabel");
statusLabel.setHorizontalAlignment(SwingConstants.CENTER);
statusLabel.setForeground(Messages.NORMAL);
statusLabel.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5));
statusLabel.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
updateStatusToolTip();
}
});
// use a strut panel so the size of the message area does not change if we make
// the message label not visible
int height = statusLabel.getPreferredSize().height;
panel.add(Box.createVerticalStrut(height), BorderLayout.WEST);
panel.add(statusLabel, BorderLayout.CENTER);
return panel;
}
/**
* Returns true if this component should be displayed in a modal dialog
* @return true if this component should be displayed in a modal dialog
*/
public boolean isModal() {
return modal;
}
/**
* Sets the horizontal position of the status label.
* @param justification One of the following constants
* defined in <code>SwingConstants</code>:
* <code>LEFT</code>,
* <code>CENTER</code> (the default for image-only labels),
* <code>RIGHT</code>,
*/
public void setStatusJustification(int justification) {
statusLabel.setHorizontalAlignment(justification);
}
/**
* Notification that the task was canceled; the progress panel is
* removed.
* @param task task that was canceled
*/
@Override
public void taskCancelled(Task task) {
clearProgress();
setStatusText("operation canceled");
taskScheduler.clearScheduledTask();
}
/**
* Notification that the given task completed so that the progress
* panel can be removed.
* @param task task that completed
*/
@Override
public void taskCompleted(Task task) {
clearProgress();
}
/**
* Sets the component that should be given focus when the dialog is activated.
* <p>
* Implementation Note: If the given component is a JButton, then that component will be
* made the default button.
*
* @param focusComponent the component that should receive default focus.
* @see #setFocusComponent(Component)
*/
public void setFocusComponent(Component focusComponent) {
this.focusComponent = focusComponent;
if (focusComponent instanceof JButton && defaultButton == null) {
setDefaultButton((JButton) focusComponent);
}
}
/**
* Returns the component that will receive focus when the dialog is shown
* @return the component
*/
public Component getFocusComponent() {
return focusComponent;
}
/**
* Set the help Location for this dialog.
* @param helpLocation the helpLocation for this dialog.
*/
public void setHelpLocation(HelpLocation helpLocation) {
DockingWindowManager.setHelpLocation(rootPanel, helpLocation);
}
/**
* Returns the help location for this dialog
* @return the help location
*/
public HelpLocation getHelpLocation() {
HelpService helpService = DockingWindowManager.getHelpService();
return helpService.getHelpLocation(rootPanel);
}
/**
* Sets the button to make "Default" when the dialog is shown. If no default button is
* desired, then pass <code>null</code> as the <code>button</code> value.
* @param button the button to make default enabled.
*/
public void setDefaultButton(JButton button) {
defaultButton = button;
if (isShowing()) {
// update the button while we are showing
dialog.getRootPane().setDefaultButton(button);
}
}
/**
* Returns the default button for the dialog.
* @return the button
*/
public JButton getDefaultButton() {
return defaultButton;
}
/**
* Sets the title to be displayed in the dialogs title bar
* @param title the title
*/
public void setTitle(String title) {
this.title = title;
if (dialog != null) {
dialog.setTitle(title);
}
}
protected Component getGlassPane() {
return dialog.getRootPane().getGlassPane();
}
protected void setGlassPane(Component component) {
if (dialog == null) {
throw new IllegalStateException(
"Attempted to set the glass pane before the dialog is shown");
}
dialog.getRootPane().setGlassPane(component);
dialog.validate();
}
/**
* Returns the title for this component
* @return the title
*/
public String getTitle() {
return title;
}
/**
* Moves the dialog associated with this provider to the front.
*/
public void toFront() {
if (dialog != null) {
dialog.toFront();
}
}
void setDialog(DockingDialog dialog) {
this.dialog = dialog;
if (dialog != null) {
dialog.getAccessibleContext().setAccessibleDescription(accessibleDescription);
}
}
DockingDialog getDialog() {
return dialog;
}
protected Component getParent() {
if (dialog == null) {
return null;
}
return dialog.getParent();
}
public boolean isVisible() {
return ((dialog != null) && dialog.isVisible());
}
public boolean isShowing() {
return ((dialog != null) && dialog.isShowing());
}
/**
* Called each time the dialog is show. The given tool is the parent tool of the dialog.
* @param t the tool
*/
void dialogShown(Tool t) {
setTool(t);
dialogShown();
}
private void setTool(Tool tool) {
if (this.tool != null) {
return;
}
// initialize the first time we are shown
this.tool = tool;
if (tool == null) {
// The tool can be null for dialogs shown before the framework is initialized, like
// dialogs shown over the splash screen. Without a tool, we cannot add key binding
// actions.
return;
}
// Any actions in this list already were added before we had a tool. Add them now. Any
// future calls to addKeyBindingAction() will get added to the tool at that time.
for (DockingActionIf proxy : keyBindingProxyActions) {
tool.addAction(proxy);
}
}
/**
* Override this method if you want to do something when the dialog is made visible
*/
protected void dialogShown() {
// may be overridden by subclasses
}
/**
* Override this method if you want to do something when the dialog is made invisible
*/
protected void dialogClosed() {
// may be overridden by subclasses
}
/**
* Sets the initial location for the dialog
* @param x the x coordinate
* @param y the y coordinate
*/
public void setInitialLocation(int x, int y) {
initialLocation = new Point(x, y);
}
/**
* Returns the initial location for the dialog or null if none was set
* @return the point
*/
public Point getInitialLocation() {
return initialLocation;
}
/**
* Sets the resizable property for the corresponding dialog.
* @param resizeable if false the user will not be able to resize the dialog.
*/
public void setResizable(boolean resizeable) {
this.resizeable = resizeable;
}
public boolean isResizeable() {
return resizeable;
}
/**
* An optional extension point for subclasses to provider action context for the actions used by
* this provider.
*
* @param event The mouse event used (may be null) to generate a popup menu
*/
@Override
public ActionContext getActionContext(MouseEvent event) {
Component c = getComponent();
KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager();
Component focusedComponent = kfm.getFocusOwner();
if (focusedComponent != null && SwingUtilities.isDescendingFrom(focusedComponent, c)) {
c = focusedComponent;
}
if (event == null) {
return new DialogActionContext(this, c);
}
Component sourceComponent = event.getComponent();
if (sourceComponent != null) {
c = sourceComponent;
}
return new DialogActionContext(this, c).setSourceObject(event.getSource());
}
/**
* Signals to this provider that it needs to updated the enabled state of its managed
* actions.
*/
protected void notifyContextChanged() {
ActionContext context = getActionContext(null);
if (context == null) {
context = new DefaultActionContext();
}
Set<DockingActionIf> keySet = toolbarButtonsByAction.keySet();
for (DockingActionIf action : keySet) {
action.setEnabled(action.isEnabledForContext(context));
}
}
public Set<DockingActionIf> getActions() {
return new HashSet<>(dialogActions);
}
private void addToolbarAction(final DockingActionIf action) {
if (action.getToolBarData() == null) {
return;
}
if (toolbar == null) {
toolbar = new JPanel(new FlowLayout(FlowLayout.RIGHT, 2, 0));
toolbar.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 10));
mainPanel.add(toolbar, BorderLayout.NORTH);
}
DialogToolbarButton button = new DialogToolbarButton(action, this);
toolbar.add(button);
toolbarButtonsByAction.put(action, button);
}
/**
* Add an action to this dialog. Only actions with icons are added to the toolbar.
* Note, if you add an action to this dialog, do not also add the action to
* the tool, as this dialog will do that for you.
* @param action the action
*/
public void addAction(DockingActionIf action) {
dialogActions.add(action);
addToolbarAction(action);
popupManager.addAction(action);
addKeyBindingAction(action);
}
private void addKeyBindingAction(DockingActionIf action) {
DialogActionProxy proxy = new DialogActionProxy(this, action);
keyBindingProxyActions.add(proxy);
// The tool will be null when clients add actions to this dialog before it has been shown.
// This is different than ComponentProviders, which require a Tool at construction. The
// DialogComponentProvider did not originally require a Tool at construction. Rather than
// refactor the dialog to require a Tool, it was easier to simply delay adding actions until
// the Tool is set when the dialog is first shown.
if (tool != null) {
tool.addAction(proxy);
}
}
public void removeAction(DockingActionIf action) {
dialogActions.remove(action);
JButton button = toolbarButtonsByAction.remove(action);
if (button != null && toolbar != null) {
toolbar.remove(button);
}
popupManager.removeAction(action);
removeKeyBindingAction(action);
}
private void removeKeyBindingAction(DockingActionIf action) {
DialogActionProxy proxy = null;
for (DialogActionProxy actionProxy : keyBindingProxyActions) {
if (action == actionProxy.getAction()) {
proxy = actionProxy;
break;
}
}
if (proxy == null) {
return;
}
keyBindingProxyActions.remove(proxy);
if (tool != null) {
tool.removeAction(proxy);
}
}
/**
* Sets this dialog to remember its location from one invocation to the next. The default is to
* remember location.
* @param rememberLocation true to remember, false otherwise.
*/
public void setRememberLocation(boolean rememberLocation) {
this.rememberLocation = rememberLocation;
}
/**
* Returns true if this dialog remembers its location from one invocation to the next.
* @return true if this dialog remembers its location from one invocation to the next.
*/
public boolean getRememberLocation() {
return rememberLocation;
}
/**
* Sets this dialog to remember its size from one invocation to the next. The default is to
* remember size.
* @param rememberSize true to remember, false otherwise.
*/
public void setRememberSize(boolean rememberSize) {
this.rememberSize = rememberSize;
}
/**
* Returns true if this dialog remembers its size from one invocation to the next.
* @return true if this dialog remembers its size from one invocation to the next.
*/
public boolean getRememberSize() {
return rememberSize;
}
/**
* Returns true if this dialog uses shared location and size information.
* @return true if this dialog uses shared location and size information.
* @see #setUseSharedLocation(boolean)
*/
public boolean getUseSharedLocation() {
return useSharedLocation;
}
/**
* Specifies whether or not this dialog component should use the same remembered location (and
* size) no matter which window this dialog is launched from. The default is not to use
* shared location and size, which means that there is a remembered location and size for this
* dialog for each window that has launched it (i.e. the window is the parent of the dialog).
*
* @param useSharedLocation true to share locations
*/
public void setUseSharedLocation(boolean useSharedLocation) {
this.useSharedLocation = useSharedLocation;
}
/**
* Returns true if this dialog is intended to be shown and hidden relatively quickly. This
* is used to determine if this dialog should be allowed to parent other components. The
* default is false.
*
* @return true if this dialog is transient
*/
public boolean isTransient() {
return isTransient;
}
/**
* Sets this dialog to be transient (see {@link #isTransient()}
*
* @param isTransient true for transient; false is the default
*/
public void setTransient(boolean isTransient) {
this.isTransient = isTransient;
}
@Override
public String toString() {
return getTitle();
}
//==================================================================================================
// Inner Classes
//==================================================================================================
private class PopupHandler extends GMouseListenerAdapter implements ContainerListener {
@Override
public void popupTriggered(MouseEvent e) {
ActionContext actionContext = getActionContext(e);
popupManager.popupMenu(actionContext, e);
}
@Override
public void componentAdded(ContainerEvent e) {
installMouseListener(e.getChild());
}
@Override
public void componentRemoved(ContainerEvent e) {
uninstallMouseListener(e.getChild());
}
}
/**
* A placeholder action that we register with the tool in order to get key event management
*/
private class DialogActionProxy extends DockingActionProxy {
private DialogComponentProvider provider;
public DialogActionProxy(DialogComponentProvider provider, DockingActionIf dockingAction) {
super(dockingAction);
this.provider = provider;
}
@Override
public boolean isAddToPopup(ActionContext context) {
return false;
}
@Override
public ToolBarData getToolBarData() {
return null;
}
@Override
public boolean isEnabledForContext(ActionContext context) {
if (context instanceof DialogActionContext dialogContext) {
DialogComponentProvider contextProvider =
dialogContext.getDialogComponentProvider();
if (provider != contextProvider) {
return false;
}
return dockingAction.isEnabledForContext(context);
}
return false;
}
}
}