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

2514 lines
78 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.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.*;
import java.util.List;
import java.util.Map.Entry;
import javax.swing.*;
import org.apache.commons.collections4.map.LazyMap;
import org.jdom.Element;
import docking.action.ActionContextProvider;
import docking.action.DockingActionIf;
import docking.actions.*;
import docking.widgets.PasswordDialog;
import generic.util.WindowUtilities;
import ghidra.framework.OperatingSystem;
import ghidra.framework.Platform;
import ghidra.framework.options.PreferenceState;
import ghidra.util.*;
import ghidra.util.datastruct.*;
import ghidra.util.exception.AssertException;
import ghidra.util.task.SwingUpdateManager;
import gui.event.MouseBinding;
import help.Help;
import help.HelpService;
import util.CollectionUtils;
/**
* Manages the "Docking" arrangement of a set of components and actions. The components can be
* "docked" together or exist in their own window. Actions can be associated with components so they
* "move" with the component as it moved from one location to another.
* <P>
* Components are added via ComponentProviders. A ComponentProvider is an interface for getting a
* component and its related information. The docking window manager will get the component from the
* provider as needed. It is up to the provider if it wants to reuse the component or recreate a new
* one when the component is requested. When the user hides a component (by using the x button on
* the component header), the docking window manager removes all knowledge of the component and will
* request it again from the provider if the component is again shown. The provider is also notified
* whenever a component is hidden and shown.
*/
public class DockingWindowManager implements PropertyChangeListener, PlaceholderInstaller {
final static String COMPONENT_MENU_NAME = "Window";
private static DockingActionIf actionUnderMouse;
private static Object objectUnderMouse;
/**
* The owner name for docking windows actions.
* <p>
* Warning: Any action with this owner will get removed every time the 'Window' menu is rebuilt,
* with the exception if reserved key bindings.
*/
public static final String DOCKING_WINDOWS_OWNER = "DockingWindows";
public static final String TOOL_PREFERENCES_XML_NAME = "PREFERENCES";
// we use a list to maintain order
private static List<DockingWindowManager> instances = new ArrayList<>();
private Tool tool;
private RootNode root;
private PlaceholderManager placeholderManager;
private LRUSet<ComponentPlaceholder> lastFocusedPlaceholders = new LRUSet<>(20);
private ComponentPlaceholder focusedPlaceholder;
private ComponentPlaceholder nextFocusedPlaceholder;
private ComponentProvider defaultProvider;
private static Component pendingRequestFocusComponent;
private Map<String, ComponentProvider> providerNameCache = new HashMap<>();
private Map<String, PreferenceState> preferenceStateMap = new HashMap<>();
private ActionToGuiMapper actionToGuiMapper;
private WeakSet<PopupActionProvider> popupActionProviders =
WeakDataStructureFactory.createSingleThreadAccessWeakSet();
private WeakSet<DockingContextListener> contextListeners =
WeakDataStructureFactory.createSingleThreadAccessWeakSet();
// The update should happen fairly quickly, as it is what rebuilds the window layout
private SwingUpdateManager rebuildUpdater = new SwingUpdateManager(100, 750, this::doUpdate);
private boolean isVisible;
private boolean isDocking;
private boolean hasStatusBar;
private boolean windowsOnTop;
private Window lastActiveWindow;
private Map<Class<? extends ActionContext>, ActionContextProvider> defaultContextProviderMap =
new HashMap<>();
/**
* Constructs a new DockingWindowManager
*
* @param tool the tool
* @param images the images to use for windows in this window manager
*/
public DockingWindowManager(Tool tool, List<Image> images) {
this(tool, images, false, true, true, null);
}
/**
* Constructs a new DockingWindowManager
*
* @param tool the tool
* @param images the list of icons to set on the window
* @param modal if true then the root window will be a modal dialog instead of a frame
* @param isDocking true for normal operation, false to suppress docking support(removes
* component headers and window menu)
* @param hasStatusBar if true a status bar will be created for the main window
* @param factory the drop target factory
*/
public DockingWindowManager(Tool tool, List<Image> images, boolean modal, boolean isDocking,
boolean hasStatusBar, DropTargetFactory factory) {
KeyBindingOverrideKeyEventDispatcher.install();
this.tool = tool;
this.isDocking = isDocking;
this.hasStatusBar = hasStatusBar;
if (images == null) {
images = new ArrayList<>();
}
root = new RootNode(this, tool.getName(), images, modal, factory);
KeyboardFocusManager km = KeyboardFocusManager.getCurrentKeyboardFocusManager();
km.addPropertyChangeListener("permanentFocusOwner", this);
addInstance(this);
placeholderManager = new PlaceholderManager(this);
actionToGuiMapper = new ActionToGuiMapper(this);
}
@Override
public String toString() {
return "DockingWindowManager: " + root.getTitle();
}
/**
* Returns the global help service.
*
* @return the global help service.
*/
public static HelpService getHelpService() {
return Help.getHelpService();
}
private static synchronized void addInstance(DockingWindowManager winMgr) {
instances.add(winMgr);
}
private static synchronized void removeInstance(DockingWindowManager winMgr) {
instances.remove(winMgr);
}
/**
* Get the docking window manager instance which corresponds to the specified window.
*
* @param win the window for which to find its parent docking window manager.
* @return docking window manager or null if unknown.
*/
private static DockingWindowManager getInstanceForWindow(Window win) {
if (win == null) {
return null;
}
for (DockingWindowManager winMgr : instances) {
if (winMgr.root.getFrame() == win) {
return winMgr;
}
List<DetachedWindowNode> detachedWindows = winMgr.root.getDetachedWindows();
List<DetachedWindowNode> safeAccessCopy = new LinkedList<>(detachedWindows);
for (DetachedWindowNode dw : safeAccessCopy) {
if (dw.getWindow() == win) {
return winMgr;
}
}
}
return null;
}
/**
* A convenience method for getting the window for <code>component</code> and then calling
* {@link #getInstanceForWindow(Window)}.
*
* @param component The component for which to get the associated {@link DockingWindowManager}
* instance.
* @return The {@link DockingWindowManager} instance associated with <code>component</code>
*/
public static synchronized DockingWindowManager getInstance(Component component) {
Window window = WindowUtilities.windowForComponent(component);
while (window != null) {
DockingWindowManager windowManager = getInstanceForWindow(window);
if (windowManager != null) {
return windowManager;
}
window = window.getOwner();
}
return null;
}
/**
* Returns the last active docking window manager which is visible.
*
* @return the last active docking window manager which is visible.
*/
public static synchronized DockingWindowManager getActiveInstance() {
//
// Assumption: the managers are put into the list in the order they are created. The
// most recently created manager is the last shown manager, making it the
// most active. Any time we change the active manager, it will be placed
// in the back of the list.
//
for (int i = instances.size() - 1; i >= 0; i--) {
DockingWindowManager mgr = instances.get(i);
if (mgr.root.isVisible()) {
return mgr;
}
}
return null;
}
/**
* Returns a new list of all DockingWindowManager instances known to exist, ordered from least
* to most-recently active.
*
* @return a new list of all DockingWindowManager instances know to exist.
*/
public static synchronized List<DockingWindowManager> getAllDockingWindowManagers() {
return new ArrayList<>(instances);
}
/**
* The specified docking window manager has just become active
*
* @param mgr the window manager that became active.
*/
static synchronized void setActiveManager(DockingWindowManager mgr) {
if (instances.remove(mgr)) {
instances.add(mgr);
}
}
/**
* Register a specific Help content URL for a component. The DocWinListener will be notified
* with the helpURL if the specified component 'c' has focus and the help key is pressed.
*
* @param c component on which to set help.
* @param helpLocation help content location
*/
public static void setHelpLocation(JComponent c, HelpLocation helpLocation) {
ActionToGuiMapper.setHelpLocation(c, helpLocation);
}
/**
* Set the tool name which is displayed as the title for all windows.
*
* @param toolName tool name / title
*/
public void setToolName(String toolName) {
root.setToolName(toolName);
}
/**
* Set the Icon for all windows.
*
* @param icon image icon
*/
public void setIcon(ImageIcon icon) {
root.setIcon(icon);
}
/**
* Returns true if this manager contains the given provider.
*
* @param provider the provider for which to check
* @return true if this manager contains the given provider.
*/
public boolean containsProvider(ComponentProvider provider) {
return placeholderManager.containsProvider(provider);
}
PlaceholderManager getPlaceholderManager() {
return placeholderManager;
}
ActionToGuiMapper getActionToGuiMapper() {
return actionToGuiMapper;
}
RootNode getRootNode() {
return root;
}
/**
* Returns the tool that owns this manager
*
* @return the tool
*/
public Tool getTool() {
return tool;
}
/**
* Returns the root window frame.
*
* @return the root window frame.
*/
public JFrame getRootFrame() {
if (root == null) {
return null; // must have been disposed
}
return root.getFrame();
}
/**
* Sets the provider that should get the default focus when no component has focus.
*
* @param provider the provider that should get the default focus when no component has focus.
*/
public void setDefaultComponent(ComponentProvider provider) {
defaultProvider = provider;
}
/**
* Registers an action context provider as the default provider for a specific action
* context type. Note that this registers a default provider for exactly
* that type and not a subclass of that type. If the provider want to support a hierarchy of
* types, then it must register separately for each type. See {@link ActionContext} for details
* on how the action context system works.
* @param type the ActionContext class to register a default provider for
* @param provider the ActionContextProvider that provides default tool context for actions
* that consume the given ActionContext type
*/
public void registerDefaultContextProvider(Class<? extends ActionContext> type,
ActionContextProvider provider) {
defaultContextProviderMap.put(type, provider);
}
/**
* Removes the default provider for the given ActionContext type.
* @param type the subclass of ActionContext to remove a provider for
* @param provider the ActionContextProvider to remove for the given ActionContext type
*/
public void unregisterDefaultContextProvider(Class<? extends ActionContext> type,
ActionContextProvider provider) {
ActionContextProvider currentProvider = defaultContextProviderMap.get(type);
if (Objects.equals(provider, currentProvider)) {
defaultContextProviderMap.remove(type);
}
}
/**
* Get the window that contains the specified Provider's component
*
* @param provider component provider
* @return window or null if component is not visible or not found
*/
public Window getProviderWindow(ComponentProvider provider) {
ComponentPlaceholder placeholder = getActivePlaceholder(provider);
if (placeholder != null) {
return root.getWindow(placeholder);
}
return null;
}
/**
* Get the provider that contains the specified component
*
* @param c the component
* @return the provider; null if now containing provider is found
*/
public ComponentProvider getProvider(Component c) {
if (c != null) {
DockableComponent dc = getDockableComponent(c);
if (dc != null) {
ComponentPlaceholder placeholder = dc.getComponentWindowingPlaceholder();
if (placeholder != null) {
return placeholder.getProvider();
}
}
}
return null;
}
ComponentPlaceholder getActivePlaceholder(ComponentProvider provider) {
return placeholderManager.getActivePlaceholder(provider);
}
/**
* Returns the active window (or the root window if nobody has yet been made active).
*
* @return the active window.
*/
public Window getActiveWindow() {
if (lastActiveWindow != null) {
return lastActiveWindow;
}
return root.getFrame();
}
/**
* Returns the current active component.
*
* @return the current active component.
*/
public Component getActiveComponent() {
if (focusedPlaceholder != null) {
return focusedPlaceholder.getComponent();
}
return null;
}
/**
* Returns the component which has focus
*
* @return the placeholder
*/
public ComponentPlaceholder getFocusedComponent() {
return focusedPlaceholder;
}
private ComponentPlaceholder getDefaultFocusComponent() {
return placeholderManager.getActivePlaceholder(defaultProvider);
}
public boolean isActiveProvider(ComponentProvider provider) {
boolean isActiveWindowManager = (this == getActiveInstance());
boolean isFocusedProvider =
(focusedPlaceholder != null) && (focusedPlaceholder.getProvider() == provider);
return isActiveWindowManager && isFocusedProvider;
}
/**
* Returns true if the given provider is in a non-main window (a {@link DetachedWindowNode})
* and is the last component provider in that window.
* @param provider the provider
* @return true if the last provider in a non-main window
*/
public boolean isLastProviderInDetachedWindow(ComponentProvider provider) {
Window providerWindow = getProviderWindow(provider);
WindowNode providerNode = root.getNodeForWindow(providerWindow);
if (!(providerNode instanceof DetachedWindowNode windowNode)) {
return false;
}
return windowNode.getComponentCount() == 1;
}
/**
* Sets the visible state of the set of docking windows.
*
* @param state if true the main window and all sub-windows are set to be visible. If state is
* false, then all windows are set to be invisible.
*/
public synchronized void setVisible(boolean state) {
if (state != isVisible) {
isVisible = state;
if (state) {
scheduleUpdate();
}
root.setVisible(state);
}
}
/**
* Returns true if the set of windows associated with this window manager are visible.
*
* @return true if the set of windows associated with this window manager are visible.
*/
public boolean isVisible() {
return isVisible;
}
/**
* Returns true if the specified provider's component is visible
*
* @param provider component provider
* @return true if the specified provider's component is visible
*/
public boolean isVisible(ComponentProvider provider) {
ComponentPlaceholder placeholder = getActivePlaceholder(provider);
if (placeholder != null) {
return placeholder.isShowing();
}
return false;
}
/**
* Adds a new component (via the provider) to be managed by this docking window manager. The
* component is initially hidden.
*
* @param provider the component provider
*/
public void addComponent(ComponentProvider provider) {
addComponent(provider, true);
}
/**
* Adds a new component (vial the provider) to be managed by this docking window manager. The
* component will be initially shown or hidden based on the the "show" parameter.
*
* @param provider the component provider.
* @param show indicates whether or not the component should be initially shown.
*/
public void addComponent(ComponentProvider provider, boolean show) {
checkIfAlreadyAdded(provider);
HelpLocation helpLoc = provider.getHelpLocation();
registerHelpLocation(provider, helpLoc);
ComponentPlaceholder placeholder = placeholderManager.createOrRecyclePlaceholder(provider);
showComponent(placeholder, show, true);
scheduleUpdate();
}
private void registerHelpLocation(ComponentProvider provider, HelpLocation helpLocation) {
HelpService helpService = Help.getHelpService();
HelpLocation registeredHelpLocation = helpService.getHelpLocation(provider);
if (registeredHelpLocation != null) {
return; // nothing to do; location already registered
}
if (helpLocation == null) {
helpLocation = new HelpLocation(provider.getOwner(), provider.getName());
}
helpService.registerHelp(provider, helpLocation);
}
private void checkIfAlreadyAdded(ComponentProvider provider) {
if (containsProvider(provider)) {
throw new AssertException(
"ComponentProvider " + provider.getName() + " was already added.");
}
}
/**
* Returns the ComponentProvider with the given name. If more than one provider exists with the
* name, one will be returned, but it could be any one of them.
*
* @param name the name of the provider to return.
* @return a provider with the given name, or null if no providers with that name exist.
*/
public ComponentProvider getComponentProvider(String name) {
ComponentProvider cachedProvider = providerNameCache.get(name);
if (cachedProvider != null) {
if (containsProvider(cachedProvider)) {
return cachedProvider;
}
providerNameCache.remove(name);
}
Set<ComponentProvider> providers = placeholderManager.getActiveProviders();
for (ComponentProvider provider : providers) {
if (name.equals(provider.getName())) {
providerNameCache.put(name, provider);
return provider;
}
}
return null;
}
/**
* The <b>first</b> provider instance with a class equal to that of the given class
*
* @param clazz the class of the desired provider
* @return the <b>first</b> provider instance with a class equal to that of the given class.
* @see #getComponentProviders(Class)
*/
public <T extends ComponentProvider> T getComponentProvider(Class<T> clazz) {
List<T> allProviders = getComponentProviders(clazz);
return CollectionUtils.any(allProviders);
}
/**
* Gets all components providers with a matching class. Some component providers will have
* multiple instances in the tool
*
* @param clazz The class of the provider
* @return all found provider instances
*/
public <T extends ComponentProvider> List<T> getComponentProviders(Class<T> clazz) {
List<T> list = new ArrayList<>();
Set<ComponentProvider> providers = placeholderManager.getActiveProviders();
for (ComponentProvider provider : providers) {
if (clazz.isAssignableFrom(provider.getClass())) {
list.add(clazz.cast(provider));
}
}
return list;
}
/**
* Returns the component provider that is the conceptual parent of the given component. More
* precisely, this will return the component provider whose
* {@link ComponentProvider#getComponent() component} is the parent of the given component.
*
* @param component the component for which to find a provider
* @return the provider; null if the component is not the child of a provider
*/
public ComponentProvider getComponentProvider(Component component) {
Set<ComponentProvider> providers = placeholderManager.getActiveProviders();
for (ComponentProvider provider : providers) {
JComponent providerComponent = provider.getComponent();
if (SwingUtilities.isDescendingFrom(component, providerComponent)) {
return provider;
}
}
return null;
}
DockableComponent getDockableComponent(ComponentProvider provider) {
ComponentPlaceholder placeholder = placeholderManager.getPlaceholder(provider);
return placeholder.getComponent();
}
ComponentPlaceholder getPlaceholder(ComponentProvider provider) {
return placeholderManager.getPlaceholder(provider);
}
/**
* Set whether a component's header should be shown; the header is the component that is dragged
* in order to move the component within the tool, or out of the tool into a separate window.
*
* @param provider provider of the visible component in the tool
* @param b true means to show the header
*/
public void showComponentHeader(ComponentProvider provider, boolean b) {
ComponentPlaceholder placeholder = getActivePlaceholder(provider);
if (placeholder != null) {
placeholder.showHeader(b);
scheduleUpdate();
}
}
public void setIcon(ComponentProvider provider, Icon icon) {
ComponentPlaceholder placeholder = getActivePlaceholder(provider);
placeholder.setIcon(icon);
scheduleUpdate();
}
public void updateTitle(ComponentProvider provider) {
String title = provider.getTitle();
if (title == null) {
title = "";
}
ComponentPlaceholder placeholder = getActivePlaceholder(provider);
if (placeholder == null) {
return; // shouldn't happen
}
placeholder.update();
scheduleUpdate();
DetachedWindowNode wNode = placeholder.getDetachedWindowNode();
if (wNode != null) {
wNode.updateTitle();
}
}
/**
* Returns the current subtitle for the component for the given provider.
*
* @param provider the component provider of the component for which to get its subtitle.
* @return the current subtitle for the component for the given provider.
*/
public String getSubTitle(ComponentProvider provider) {
ComponentPlaceholder placeholder = getActivePlaceholder(provider);
if (placeholder != null) {
return placeholder.getSubTitle();
}
return "";
}
/**
* Removes the ComponentProvider (component) from the docking windows manager. The location of
* the window will be remember and reused if the provider is added back in later.
*
* @param provider the provider to be removed.
*/
public void removeComponent(ComponentProvider provider) {
if (provider == defaultProvider) {
defaultProvider = null;
}
placeholderManager.removeComponent(provider);
}
/**
* Get the local actions installed on the given provider
*
* @param provider the provider
* @return an iterator over the actions
*/
public Iterator<DockingActionIf> getComponentActions(ComponentProvider provider) {
ComponentPlaceholder placeholder = getActivePlaceholder(provider);
if (placeholder != null) {
return placeholder.getActions();
}
List<DockingActionIf> emptyList = Collections.emptyList();
return emptyList.iterator();
}
//==================================================================================================
// Package-level Action Methods
//==================================================================================================
void removeProviderAction(ComponentProvider provider, DockingActionIf action) {
ComponentPlaceholder placeholder = getActivePlaceholder(provider);
if (placeholder != null) {
placeholder.removeAction(action);
}
}
void addLocalAction(ComponentProvider provider, DockingActionIf action) {
ComponentPlaceholder placeholder = getActivePlaceholder(provider);
if (placeholder == null) {
throw new IllegalArgumentException("Unknown component provider: " + provider);
}
placeholder.addAction(action);
}
void addToolAction(DockingActionIf action) {
actionToGuiMapper.addToolAction(action);
scheduleUpdate();
}
void removeToolAction(DockingActionIf action) {
actionToGuiMapper.removeToolAction(action);
scheduleUpdate();
}
/**
* Returns any action that is bound to the given keystroke for the tool associated with this
* DockingWindowManager instance.
*
* @param keyStroke The keystroke to check for a bound action.
* @return The action that is bound to the keystroke, or null of there is no binding for the
* given keystroke.
*/
Action getActionForKeyStroke(KeyStroke keyStroke) {
DockingToolActions toolActions = tool.getToolActions();
if (toolActions instanceof ToolActions) {
// Using a cast here; it didn't make sense to include this 'getAction' on the
// DockingToolActions
return ((ToolActions) toolActions).getAction(keyStroke);
}
return null;
}
/**
* Returns any action that is bound to the given mouse binding for the tool associated with this
* DockingWindowManager instance.
*
* @param mouseBinding The mouse binding to check for a bound action.
* @return The action associated with the mouse binding , or null of there is no binding for the
* given keystroke.
*/
Action getActionForMouseBinding(MouseBinding mouseBinding) {
DockingToolActions toolActions = tool.getToolActions();
if (toolActions instanceof ToolActions) {
// Using a cast here; it didn't make sense to include this 'getAction' on the
// DockingToolActions
return ((ToolActions) toolActions).getAction(mouseBinding);
}
return null;
}
//==================================================================================================
// End Package-level Methods
//==================================================================================================
public void ownerRemoved(String owner) {
placeholderManager.removeAll(owner);
scheduleUpdate();
}
/**
* Hides or shows the component associated with the given provider.
* <p>
* <br>
* <b>Note: </b> This method will not show the given provider if it has not previously been
* added via <code>addComponent(...)</code>.
*
* @param provider the provider of the component to be hidden or shown.
* @param visibleState true to show the component, false to hide it.
* @see #addComponent(ComponentProvider)
* @see #addComponent(ComponentProvider, boolean)
*/
public void showComponent(ComponentProvider provider, boolean visibleState) {
showComponent(provider, visibleState, false);
}
public void toFront(ComponentProvider provider) {
ComponentPlaceholder placeholder = getActivePlaceholder(provider);
if (placeholder == null) {
return;
}
if (!placeholder.isShowing()) {
showComponent(placeholder, true, false);
}
movePlaceholderToFront(placeholder, false);
}
public void toFront(final Window window) {
if (window == null) {
return;
}
if (window == getMainWindow()) {
// we don't want special handling for the tool frame, as it triggers flashing
window.toFront();
return;
}
OperatingSystem operatingSystem = Platform.CURRENT_PLATFORM.getOperatingSystem();
if (operatingSystem == OperatingSystem.WINDOWS) {
//
// Handle the window being minimized (Windows doesn't always raise the window when
// calling setVisible()
//
if (window instanceof Frame) {
Frame frame = (Frame) window;
int state = frame.getState();
if ((state & Frame.ICONIFIED) == Frame.ICONIFIED) {
frame.setState(Frame.NORMAL);
return; // this is enough to bring the window to the front on Windows
}
}
window.setVisible(false);
window.setVisible(true);
}
else if (operatingSystem == OperatingSystem.LINUX) {
//
// Handle the window being minimized (Linux doesn't always raise the window when
// calling setVisible()
//
if (window instanceof Frame) {
Frame frame = (Frame) window;
int state = frame.getState();
if ((state & Frame.ICONIFIED) == Frame.ICONIFIED) {
frame.setState(Frame.NORMAL);
}
}
window.toFront();
}
else {
// mac - it actually works on the mac
window.toFront();
}
}
/**
* Releases all resources used by this docking window manager. Once the dispose method is
* called, no other calls to this object should be made.
*/
public synchronized void dispose() {
if (root == null) {
return;
}
rebuildUpdater.dispose();
KeyboardFocusManager mgr = KeyboardFocusManager.getCurrentKeyboardFocusManager();
mgr.removePropertyChangeListener("permanentFocusOwner", this);
actionToGuiMapper.dispose();
root.dispose();
placeholderManager.disposePlaceholders();
setNextFocusPlaceholder(null);
removeInstance(this);
root = null;
lastActiveWindow = null;
}
void showComponent(ComponentProvider provider, boolean visibleState, boolean shouldEmphasize) {
ComponentPlaceholder placeholder = getActivePlaceholder(provider);
if (placeholder != null) {
showComponent(placeholder, visibleState, true, shouldEmphasize);
return;
}
if (visibleState) {
// a null placeholder implies the client is trying to show a provider that has not
// been added to the tool
Msg.warn(this, "Attempting to show an unknown Component Provider '" +
provider.getName() + "' - " + "check that the provider has been added to the tool");
}
}
/**
* Shows or hides the component associated with the given placeholder object.
*
* @param placeholder the component placeholder object for the component to be shown or hidden.
* @param visibleState true to show or false to hide.
* @param requestFocus True signals that the system should request focus on the component.
*/
private void showComponent(ComponentPlaceholder placeholder, final boolean visibleState,
boolean requestFocus) {
showComponent(placeholder, visibleState, requestFocus, false);
}
void showComponent(ComponentPlaceholder placeholder, final boolean visibleState,
boolean requestFocus, boolean shouldEmphasize) {
if (root == null) {
return;
}
if (visibleState == placeholder.isShowing()) {
if (visibleState) {
movePlaceholderToFront(placeholder, shouldEmphasize);
setNextFocusPlaceholder(placeholder);
scheduleUpdate();
}
return;
}
placeholder.show(visibleState);
if (visibleState) {
movePlaceholderToFront(placeholder, shouldEmphasize);
if (placeholder.getNode() == null) {
root.addToNewWindow(placeholder);
}
if (requestFocus) {
setNextFocusPlaceholder(placeholder);
}
}
else {
if (focusedPlaceholder == placeholder) {
clearFocusedComponent();
}
}
scheduleUpdate();
}
private void movePlaceholderToFront(ComponentPlaceholder placeholder, boolean emphasisze) {
placeholder.toFront();
toFront(root.getWindow(placeholder));
if (emphasisze) {
placeholder.emphasize();
}
}
/**
* Generates a JDOM element object for saving the window managers state to XML.
*
* @param rootXMLElement The root element to which to save XML data.
*/
public void saveToXML(Element rootXMLElement) {
Element rootNodeElement = saveWindowingDataToXml();
if (focusedPlaceholder != null) {
rootNodeElement.setAttribute("FOCUSED_OWNER", focusedPlaceholder.getOwner());
rootNodeElement.setAttribute("FOCUSED_NAME", focusedPlaceholder.getName());
rootNodeElement.setAttribute("FOCUSED_TITLE", focusedPlaceholder.getTitle());
}
rootXMLElement.removeChild(rootNodeElement.getName());
rootXMLElement.addContent(rootNodeElement);
Element preferencesElement = savePreferencesToXML();
rootXMLElement.removeChild(preferencesElement.getName());
rootXMLElement.addContent(preferencesElement);
}
/**
* Save this docking window manager's window layout and positioning information as XML.
*
* @return An XML element with the above information.
*/
public Element saveWindowingDataToXml() {
return root.saveToXML();
}
/**
* Restores the docking window managers state from the XML information.
*
* @param rootXMLElement JDOM element from which to extract the state information.
*/
public void restoreFromXML(Element rootXMLElement) {
restoreWindowDataFromXml(rootXMLElement);
restorePreferencesFromXML(rootXMLElement);
}
/**
* Restore to the docking window manager the layout and positioning information from XML.
*
* @param rootXMLElement JDOM element from which to extract the state information.
*/
public void restoreWindowDataFromXml(Element rootXMLElement) {
Element windowData = rootXMLElement.getChild(RootNode.ROOT_NODE_ELEMENT_NAME);
if (windowData == null) {
return;
}
//
// Clear our focus history, as we are changing placeholders' providers, so the old focus
// is no longer relevant.
//
clearFocusedComponent();
lastFocusedPlaceholders.clear();
//
// Save off the active providers. They will be re-assigned to new placeholders.
//
Map<ComponentProvider, ComponentPlaceholder> activeProviders =
placeholderManager.getActiveProvidersToPlaceholders();
//
// Load the placeholders
//
List<ComponentPlaceholder> restoredPlaceholders = root.restoreFromXML(windowData);
placeholderManager = new PlaceholderManager(this, restoredPlaceholders);
String focusedOwner = windowData.getAttributeValue("FOCUSED_OWNER");
String focusedName = windowData.getAttributeValue("FOCUSED_NAME");
String focusedTitle = windowData.getAttributeValue("FOCUSED_TITLE");
ComponentPlaceholder lastFoundFocusReplacement = null;
List<Entry<ComponentProvider, ComponentPlaceholder>> sortedProviders =
sortActiveProviders(activeProviders);
for (Entry<ComponentProvider, ComponentPlaceholder> entry : sortedProviders) {
ComponentProvider provider = entry.getKey();
ComponentPlaceholder oldPlaceholder = entry.getValue();
ComponentPlaceholder newPlaceholder =
placeholderManager.replacePlaceholder(provider, oldPlaceholder);
// Odd case: the replacement placeholder is reused and it's title is different
// than the outgoing placeholder. In that case, use the new placeholder
// as a reasonable default component to focus.
if (SystemUtilities.isEqual(focusedTitle, oldPlaceholder.getTitle())) {
lastFoundFocusReplacement = newPlaceholder;
}
}
restoreSavedFocusedPlaceholder(focusedOwner, focusedName, focusedTitle,
lastFoundFocusReplacement);
placeholderManager.resetPlaceholdersWithoutProviders();
scheduleUpdate();
}
private void restoreSavedFocusedPlaceholder(String focusOwner, String focusName,
String focusTitle, ComponentPlaceholder bestFocusReplacementPlaceholder) {
if (bestFocusReplacementPlaceholder != null) {
// we've found already a preferred replacement
setNextFocusPlaceholder(bestFocusReplacementPlaceholder);
return;
}
restoreFocusOwner(focusOwner, focusName);
}
/**
* Sorts the active providers by window group. This ensures that the dependent window groups are
* loaded after their dependencies have been.
*/
private List<Entry<ComponentProvider, ComponentPlaceholder>> sortActiveProviders(
Map<ComponentProvider, ComponentPlaceholder> activeProviders) {
Set<Entry<ComponentProvider, ComponentPlaceholder>> entrySet = activeProviders.entrySet();
List<Entry<ComponentProvider, ComponentPlaceholder>> list = new ArrayList<>(entrySet);
Collections.sort(list, (e1, e2) -> {
ComponentProvider p1 = e1.getKey();
ComponentProvider p2 = e2.getKey();
String g1 = p1.getWindowGroup();
String g2 = p2.getWindowGroup();
return g1.compareToIgnoreCase(g2);
});
return list;
}
@Override
public void installPlaceholder(ComponentPlaceholder placeholder, WindowPosition position) {
root.add(placeholder, position);
}
@Override
public void uninstallPlaceholder(ComponentPlaceholder placeholder, boolean keepAround) {
disposePlaceholder(placeholder, keepAround);
clearCurrentOrPendingFocusForRemovedPlaceholder(placeholder);
}
private void disposePlaceholder(ComponentPlaceholder placeholder, boolean keepAround) {
placeholder.removeAllActions();
ComponentNode node = placeholder.getNode();
if (node == null) {
return;
}
node.remove(placeholder, keepAround);
}
synchronized void clearCurrentOrPendingFocusForRemovedPlaceholder(
ComponentPlaceholder placeholder) {
if (focusedPlaceholder == placeholder) {
clearFocusedComponent();
}
else if (nextFocusedPlaceholder == placeholder) {
clearFocusedComponent();
}
}
/**
* Moves the component associated with the given source placeholder object from its current
* docked location to its own window that will be anchored at the given point.
*
* @param source the component placeholder containing the component to be moved.
* @param p the location at which to create a new window for the component.
*/
void movePlaceholder(ComponentPlaceholder source, Point p) {
ComponentNode sourceNode = source.getNode();
sourceNode.remove(source);
root.addToNewWindow(source, p);
scheduleUpdate();
}
/**
* Moves the component associated with the given source placeholder object to a new docked
* location relative to the given destination placeholder object
*
* @param source the component placeholder for the component being moved
* @param destination the component placeholder object used to base to move
* @param windowPosition a code specifying the docking relationship between two placeholders
*/
void movePlaceholder(ComponentPlaceholder source, ComponentPlaceholder destination,
WindowPosition windowPosition) {
ComponentNode sourceNode = source.getNode();
if (destination != null) {
ComponentNode destinationNode = destination.getNode();
sourceNode.remove(source);
if (windowPosition == WindowPosition.STACK) {
destinationNode.add(source);
}
else {
destinationNode.split(source, windowPosition);
}
}
else {
sourceNode.remove(source);
root.add(source, WindowPosition.RIGHT);
}
setNextFocusPlaceholder(source);
scheduleUpdate();
}
/**
* Notifies the docking windows listener that the close button has been pressed on the main
* window frame.
*/
void close() {
tool.close();
}
boolean isDocking() {
return isDocking;
}
/**
* Builds the window menu containing a menu item for each component.
*/
private void buildComponentMenu() {
if (!isDocking || !isVisible) {
return;
}
if (isWindowMenuShowing()) {
// Stop menu items from being disposed; this causes exceptions when they are pressed
scheduleUpdate();
return;
}
tool.getToolActions().removeActions(DOCKING_WINDOWS_OWNER);
Map<String, List<ComponentPlaceholder>> permanentMap =
LazyMap.lazyMap(new HashMap<>(), menuName -> new ArrayList<>());
Map<String, List<ComponentPlaceholder>> transientMap =
LazyMap.lazyMap(new HashMap<>(), menuName -> new ArrayList<>());
Map<ComponentProvider, ComponentPlaceholder> map =
placeholderManager.getActiveProvidersToPlaceholders();
Set<Entry<ComponentProvider, ComponentPlaceholder>> entrySet = map.entrySet();
for (Entry<ComponentProvider, ComponentPlaceholder> entry : entrySet) {
ComponentProvider provider = entry.getKey();
ComponentPlaceholder placeholder = entry.getValue();
String subMenuName = provider.getWindowSubMenuName();
if (provider.isTransient() && !provider.isSnapshot()) {
transientMap.get(subMenuName).add(placeholder);
}
else {
permanentMap.get(subMenuName).add(placeholder);
}
}
promoteSingleMenuGroups(permanentMap);
promoteSingleMenuGroups(transientMap);
createActions(transientMap);
createActions(permanentMap);
createWindowActions();
actionToGuiMapper.update();
}
private boolean isWindowMenuShowing() {
MenuElement[] selectedPath = MenuSelectionManager.defaultManager().getSelectedPath();
if (selectedPath == null || selectedPath.length == 0) {
return false;
}
JMenu menu = getMenuForSelection(selectedPath);
if (menu == null) {
return false;
}
String text = menu.getText();
return text.equals(COMPONENT_MENU_NAME);
}
private JMenu getMenuForSelection(MenuElement[] selectedPath) {
for (MenuElement element : selectedPath) {
if (element instanceof JMenu) {
return (JMenu) element;
}
}
return null;
}
private void createActions(Map<String, List<ComponentPlaceholder>> map) {
List<ShowComponentAction> actionList = new ArrayList<>();
for (String subMenuName : map.keySet()) {
List<ComponentPlaceholder> placeholders = map.get(subMenuName);
for (ComponentPlaceholder placeholder : placeholders) {
ComponentProvider provider = placeholder.getProvider();
boolean isTransient = provider.isTransient();
actionList
.add(new ShowComponentAction(this, placeholder, subMenuName, isTransient));
}
if (subMenuName != null) {
// add an 'add all' action for the sub-menu
actionList.add(new ShowAllComponentsAction(this, placeholders, subMenuName));
}
}
DockingToolActions toolActions = tool.getToolActions();
Collections.sort(actionList);
for (ShowComponentAction action : actionList) {
toolActions.addGlobalAction(action);
}
}
private void promoteSingleMenuGroups(Map<String, List<ComponentPlaceholder>> lazyMap) {
List<String> lists = new ArrayList<>(lazyMap.keySet());
for (String key : lists) {
if (key == null) {
continue;
}
List<ComponentPlaceholder> list = lazyMap.get(key);
if (list.size() == 1) {
lazyMap.get(null /*submenu name*/).add(list.get(0));
lazyMap.remove(key);
}
}
}
private void createWindowActions() {
List<DetachedWindowNode> windows = root.getDetachedWindows();
List<ShowWindowAction> actions = new ArrayList<>();
for (DetachedWindowNode node : windows) {
Window window = node.getWindow();
if (window != null) {
actions.add(new ShowWindowAction(node));
}
}
DockingToolActions toolActions = tool.getToolActions();
Collections.sort(actions);
for (ShowWindowAction action : actions) {
toolActions.addGlobalAction(action);
}
}
/*
* Notifies the window manager that an update is needed
*/
void scheduleUpdate() {
if (rebuildUpdater.isBusy()) {
return;
}
rebuildUpdater.updateLater();
}
private boolean updatePending() {
return rebuildUpdater.isBusy();
}
/*
* Updates the component tree as needed
*/
private synchronized void doUpdate() {
if (!isVisible) {
return;
}
root.update(); // do this before rebuilding the menu, as new windows may be opened
buildComponentMenu();
Swing.runLater(() -> updateFocus());
}
private void updateFocus() {
if (updatePending()) {
// we will get called again
return;
}
if (root == null) {
// This method is called from invokeLater(); we may have been disposed since then
return;
}
// still loading components, so come back later when we can request focus
if (!getMainWindow().isShowing()) {
scheduleUpdate();
return;
}
updateFocus(maybeGetPlaceholderToFocus());
}
private synchronized void setNextFocusPlaceholder(ComponentPlaceholder placeholder) {
nextFocusedPlaceholder = placeholder;
}
private synchronized ComponentPlaceholder maybeGetPlaceholderToFocus() {
if (nextFocusedPlaceholder != null) {
ComponentPlaceholder temp = nextFocusedPlaceholder;
setNextFocusPlaceholder(null);
return temp;
}
KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager();
Component permanentFocusOwner = kfm.getPermanentFocusOwner();
Component focusOwner = kfm.getFocusOwner();
// A null focus owner and a null permanent focus owner imply that Java did not know who
// should get focus. Make sure one of our widgets gets focus.
if (focusOwner == null && permanentFocusOwner == null) {
return findNextFocusedComponent();
}
return null;
}
private void updateFocus(ComponentPlaceholder placeholder) {
if (placeholder != null) {
placeholder.requestFocusWhenReady();
}
}
void restoreFocusOwner(String focusOwner, String focusName) {
if (focusOwner == null) {
// nothing to restore
setNextFocusPlaceholder(getDefaultFocusComponent());
return;
}
ComponentPlaceholder focusReplacement = getDefaultFocusComponent();
Map<ComponentProvider, ComponentPlaceholder> map =
placeholderManager.getActiveProvidersToPlaceholders();
Set<Entry<ComponentProvider, ComponentPlaceholder>> entrySet = map.entrySet();
for (Entry<ComponentProvider, ComponentPlaceholder> entry : entrySet) {
ComponentProvider provider = entry.getKey();
ComponentPlaceholder placeholder = entry.getValue();
if (provider.getOwner().equals(focusOwner) && provider.getName().equals(focusName)) {
focusReplacement = placeholder;
break; // found one!
}
}
setNextFocusPlaceholder(focusReplacement);
}
private void setFocusedComponent(ComponentPlaceholder placeholder) {
RootNode rootNode = root;
if (rootNode == null) {
return; // we have been disposed
}
if (focusedPlaceholder != null) {
if (focusedPlaceholder == placeholder) {
return; // ignore if we are already focused
}
focusedPlaceholder.setSelected(false);
}
focusedPlaceholder = placeholder;
// put the last focused placeholder at the front of the list for restoring focus work later
lastFocusedPlaceholders.add(focusedPlaceholder);
focusedPlaceholder.setSelected(true);
WindowNode topLevelNode = focusedPlaceholder.getTopLevelNode();
if (topLevelNode == null) {
return;
}
topLevelNode.setLastFocusedProviderInWindow(focusedPlaceholder);
rootNode.notifyWindowFocusChanged(topLevelNode);
}
private ComponentPlaceholder findNextFocusedComponent() {
Iterator<ComponentPlaceholder> iterator = lastFocusedPlaceholders.iterator();
while (iterator.hasNext()) {
ComponentPlaceholder placeholder = iterator.next();
if (placeholder.isShowing()) {
return placeholder;
}
iterator.remove();
}
return getActivePlaceholder(defaultProvider);
}
/**
* Clears the docking window manager's notion of which component placeholder is focused. This
* is used when a component is removed or component placeholders are rebuilt.
*/
private void clearFocusedComponent() {
if (focusedPlaceholder != null) {
lastFocusedPlaceholders.remove(focusedPlaceholder);
focusedPlaceholder.setSelected(false);
WindowNode topLevelNode = focusedPlaceholder.getTopLevelNode();
if (topLevelNode != null) {
topLevelNode.setLastFocusedProviderInWindow(null);
root.notifyWindowFocusChanged(topLevelNode);
}
}
focusedPlaceholder = null;
setNextFocusPlaceholder(null);
}
/**
* Invoked by associated docking windows when they become active or inactive
*
* @param window the active window
* @param active true signals that this DockingWindowManager has become active
*/
void setActive(Window window, boolean active) {
if (root == null) {
return;
}
actionToGuiMapper.setActive(active);
if (active) {
setActiveManager(this);
if (focusedPlaceholder != null && root.getWindow(focusedPlaceholder) == window) {
focusedPlaceholder.setSelected(true);
}
}
else if (focusedPlaceholder != null) {
focusedPlaceholder.setSelected(false);
}
}
static void requestFocus(Component component) {
if (component.hasFocus()) {
return;
}
if (pendingRequestFocusComponent != null) {
pendingRequestFocusComponent = null; // only do it once so that we don't get stuck in this state
return;
}
pendingRequestFocusComponent = component;
pendingRequestFocusComponent.requestFocus();
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
Window win = KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow();
if (!isMyWindow(win)) {
return;
}
lastActiveWindow = win;
// adjust the focus if no component within the window has focus
Component newFocusComponent = (Component) evt.getNewValue();
if (newFocusComponent == null) {
return; // we'll get called again with the correct value
}
DockableComponent dockableComponent =
getDockableComponentForFocusOwner(win, newFocusComponent);
if (dockableComponent == null) {
return;
}
if (SwingUtilities.isDescendingFrom(newFocusComponent, dockableComponent)) {
updateDockingWindowStateForNewFocusOwner(newFocusComponent, dockableComponent);
return;
}
// The new Java focus owner is not part of our DockableComponent hierarchy. See if we need
// to change the focus to a component that is.
ensureAllowedFocusOwner(newFocusComponent, dockableComponent);
}
private void updateDockingWindowStateForNewFocusOwner(Component newFocusComponent,
DockableComponent dockableComponent) {
ComponentPlaceholder placeholder = dockableComponent.getComponentWindowingPlaceholder();
if (placeholder == null) {
return; // it's been disposed
}
pendingRequestFocusComponent = null;
dockableComponent.setFocusedComponent(newFocusComponent); // for posterity
// Note: do this later, since, during this callback, component providers can do
// things that break focus (e.g., launch a modal dialog). By doing this later,
// it gives the java focus engine a chance to get in the correct state.
Swing.runLater(() -> setFocusedComponent(placeholder));
}
private void ensureAllowedFocusOwner(Component newFocusComponent,
DockableComponent dockableComponent) {
if (nextFocusedPlaceholder != null) {
// We have a new pending focus request for a DockableComponent, so nothing to do.
return;
}
// We allow JTabbedPanes, as that is the component we use to stack components and users need
// to be able to select and activate tabs when using the keyboard focus traversal.
if (newFocusComponent instanceof JTabbedPane) {
if (focusedPlaceholder != null) {
focusedPlaceholder.setSelected(false); // update the header to not be focused
focusedPlaceholder = null;
}
return;
}
// Transfer focus to one of our component providers when a component gets focus that is
// not contained in a dockable component provider. This keeps unexpected components
// from getting focus as the user navigates the application from the keyboard.
dockableComponent.requestFocus();
}
private DockableComponent getDockableComponentForFocusOwner(Window window,
Component focusedComp) {
DockableComponent dockableComponent = getDockableComponent(focusedComp);
if (dockableComponent != null) {
return dockableComponent;
}
// else use last focus component in window
WindowNode node = root.getNodeForWindow(window);
if (node == null) {
return null;
}
// NOTE: We only allow focus within a window on a component that belongs to within a
// DockableComponent hierarchy. If we get here, then we have some component trying
// to take focus outside of such a hierarchy. In this case, we will take focus from
// the currently focused component and give it to one of our DockableComponents.
ComponentPlaceholder placeHolder = node.getLastFocusedProviderInWindow();
if (placeHolder != null) {
return placeHolder.getComponent();
}
return null;
}
private DockableComponent getDockableComponent(Component comp) {
while (comp != null) {
if (comp instanceof DockableComponent) {
return (DockableComponent) comp;
}
comp = comp.getParent();
}
return null;
}
private Element savePreferencesToXML() {
Element toolPreferencesElement = new Element(TOOL_PREFERENCES_XML_NAME);
Set<Entry<String, PreferenceState>> entrySet = preferenceStateMap.entrySet();
for (Entry<String, PreferenceState> entry : entrySet) {
String key = entry.getKey();
PreferenceState state = entry.getValue();
Element preferenceElement = state.saveToXml();
preferenceElement.setAttribute("NAME", key);
toolPreferencesElement.addContent(preferenceElement);
}
return toolPreferencesElement;
}
public void restorePreferencesFromXML(Element rootElement) {
Element toolPreferencesElement = rootElement.getChild(TOOL_PREFERENCES_XML_NAME);
if (toolPreferencesElement == null) {
return;
}
List<?> children =
toolPreferencesElement.getChildren(PreferenceState.PREFERENCE_STATE_NAME);
for (Object name : children) {
Element preferencesElement = (Element) name;
preferenceStateMap.put(preferencesElement.getAttribute("NAME").getValue(),
new PreferenceState(preferencesElement));
}
}
/**
* Adds a PreferenceState object to this window manager instance that is bound to the given key.
* When the state of the tool using this window manager is saved, then the mapped preferences
* will also be saved.
*
* @param key The key with which to store the preferences.
* @param state The state object to store.
* @see #getPreferenceState(String)
*/
public void putPreferenceState(String key, PreferenceState state) {
if (key == null) {
throw new IllegalArgumentException("Key is null!");
}
preferenceStateMap.put(key, state);
}
/**
* Gets a preferences state object stored with the given key. The state objects are loaded from
* persistent storage when the tool using this window manager has its state loaded.
*
* @param key The key with which to store the preferences.
* @return the PrefrenceState object stored by the given key, or null if one does not exist
* @see #putPreferenceState(String, PreferenceState)
*/
public PreferenceState getPreferenceState(String key) {
return preferenceStateMap.get(key);
}
/**
* Removes the Preferences state for the given key.
*
* @param key the key to the preference state to be removed
*/
public void removePreferenceState(String key) {
preferenceStateMap.remove(key);
}
private boolean isMyWindow(Window win) {
if (root == null || win == null) {
return false;
}
Window rootFrame = root.getMainWindow();
if (rootFrame == win) {
return true;
}
WindowNode node = root.getNodeForWindow(win);
if (node != null) {
return true;
}
// see if the given window is a child of the root node's frame
if (SwingUtilities.isDescendingFrom(win, rootFrame)) {
return true;
}
return false;
}
/**
* Shows the dialog using the tool's currently active window as a parent
*
* @param dialogComponent the DialogComponentProvider object to be shown in a dialog
*/
public static void showDialog(DialogComponentProvider dialogComponent) {
doShowDialog(dialogComponent, null);
}
/**
* Shows the dialog using the window containing the given componentProvider as its parent window
*
* @param dialogComponent the DialogComponentProvider object to be shown in a dialog.
* @param centeredOnProvider the component provider that is used to find a parent window for
* this dialog. The dialog is centered on this component provider's component.
*/
public void showDialog(DialogComponentProvider dialogComponent,
ComponentProvider centeredOnProvider) {
ComponentPlaceholder placeholder = getActivePlaceholder(centeredOnProvider);
Component c = null;
if (placeholder == null && centeredOnProvider != null) {
c = centeredOnProvider.getComponent();
}
doShowDialog(dialogComponent, c);
}
/**
* Shows the dialog using the given parent component to find a parent window and to position the
* dialog. If a Window can be found containing the given component, it will be used as the
* parent window for the dialog. If the component is null or not contained in a window, the
* current active window manager will be used to parent the dialog. If there are no active
* tools, then a frame will be created to parent the dialog.
*
* @param parent the component whose window over which the given dialog will be shown; null
* signals to use the active window
* @param dialogComponent the DialogComponentProvider object to be shown in a dialog.
* @see #getParentWindow(Component) for parenting notes
*/
public static void showDialog(Component parent, DialogComponentProvider dialogComponent) {
doShowDialog(dialogComponent, parent);
}
/**
* Shows the dialog using the given parent window using the optional component for positioning.
*
* <p>
* Warning: this method allows user to explicitly pass a parent window and component over which
* to be centered. There is no reason to use this method in the standard workflow. This method
* exists strictly to handle future unforeseen use cases. Use at your own risk of incorrectly
* parenting dialogs.
*
* @param parent the component whose window over which the given dialog will be shown; cannot be
* null
* @param dialogComponent the DialogComponentProvider object to be shown in a dialog
* @param centeredOnComponent the component over which the dialog will be centered; cannot be
* null
*/
public static void showDialog(Window parent, DialogComponentProvider dialogComponent,
Component centeredOnComponent) {
Objects.requireNonNull(parent);
Objects.requireNonNull(centeredOnComponent);
doShowDialog(dialogComponent, parent, centeredOnComponent);
}
private static void doShowDialog(DialogComponentProvider provider,
Component centeredOnComponent) {
Runnable r = () -> {
if (provider.isVisible()) {
provider.toFront();
return;
}
Component bestCenter = getJavaActiveWindow();
Window bestParent = getBestParent(provider, bestCenter);
//
// Note: prefer the active window; allow user's choice of center component when it is
// in the active window
//
if (centeredOnComponent != null &&
SwingUtilities.isDescendingFrom(centeredOnComponent, bestParent)) {
bestCenter = centeredOnComponent;
}
DockingDialog dialog =
DockingDialog.createDialog(bestParent, provider, bestCenter);
dialog.setVisible(true);
};
if (provider.isModal()) {
Swing.runNow(r);
}
else {
Swing.runIfSwingOrRunLater(r);
}
}
private static Window getBestParent(DialogComponentProvider provider, Component component) {
Window bestParent = getParentWindow(component);
if (!provider.isModal()) {
bestParent = getBestNonModalParent(provider, bestParent);
}
if (bestParent == null) {
// Special case: allow transient password dialogs to parent to the active window. This
// allows the password dialog to stay open over the application splash screen. (This
// is described in getParentWindow(), which is called above.)
if (provider instanceof PasswordDialog) {
bestParent = getJavaActiveWindow();
}
}
if (bestParent != null && !bestParent.isShowing()) {
bestParent = null; // don't let non-showing windows be parents
}
return bestParent;
}
private static Window getBestNonModalParent(DialogComponentProvider providerToShow,
Window bestParent) {
Window activeWindow = getJavaActiveWindow();
if (!(activeWindow instanceof DockingDialog)) {
return bestParent;
}
DockingDialog dialog = (DockingDialog) activeWindow;
if (!dialog.isModal()) {
// Note: See issue 'E' described in getParentWindow()
// If we parent to a non-modal progress dialog (which is odd), when it goes away, so
// to do we. Assume that non-modal dialogs in general are long lived. We shall only
// enforce parenting to modal dialogs as defined below in order to prevent blocking of
// the non-modal dialog.
return bestParent; // not modal; assume no issues
}
DialogComponentProvider activeProvider = dialog.getComponent();
if (activeProvider == null) {
return bestParent;
}
int activeId = activeProvider.getId();
int providerId = providerToShow.getId();
if (providerId < activeId) {
// The provider being shown is older than the active window--do not parent the provider
// to that window. The older age suggests that the new provider was shown on a delay
// and should really be considered to live behind the active modal dialog.
return bestParent;
}
if (activeProvider.isTransient()) {
// This prevents transient modal dialogs from being parents to non-modal dialogs. This
// can cause the non-modal dialog to be closed when the transient modal dialog goes
// away. There is the possibility of the non-modal dialog being blocked if not parented
// the this modal dialog. If we find a use case that exposes this pattern, then we
// will have to revisit how this method chooses to parent.
return bestParent;
}
//
// The active window is modal. We must make it the non-modal dialog's parent to
// prevent blocking the non-modal.
//
return dialog;
}
private static void doShowDialog(DialogComponentProvider provider, Window parent,
Component centeredOnComponent) {
Runnable r = () -> {
if (provider.isVisible()) {
provider.toFront();
return;
}
DockingDialog dialog =
DockingDialog.createDialog(parent, provider, centeredOnComponent);
dialog.setVisible(true);
};
if (provider.isModal()) {
Swing.runNow(r);
}
else {
Swing.runIfSwingOrRunLater(r);
}
}
private static Window getParentWindow(Component parent) {
/*
Note: Which window should be the parent of the dialog when the user does not specify?
Some use cases; a dialog is shown from:
1) A toolbar action
2) A component provider's code
3) A dialog provider's code
4) A background thread
5) The help window
6) A modal password dialog appears over the splash screen
It seems like the parent should be the active window for 1-2.
Case 3 should probably use the window of the dialog provider.
Case 4 should probably use the main tool frame, since the user may be
moving between windows while the thread is working. So, rather than using the
active window, we can default to the tool's frame.
Case 5 should use the help window.
Case 6 should use the splash screen as the parent.
We have not yet solidified how we should parent. This documentation is meant to
move us towards clarity as we find Use Cases that don't make sense. (Once we
finalize our understanding, we should update the javadoc to list exactly where
the given Dialog Component will be shown.)
Use Case
A -The user presses an action on a toolbar from a window on screen 1, while the
main tool frame is on screen 2. We want the popup window to appear on screen
1, not 2.
B -The user presses an action on the toolbar of a Dialog Component. The popup
window should appear above the dialog's window and not the main tool frame.
C -The user is working in a modal dialog and presses a button to launch another
dialog that is:
-modal - Java handles this correctly, allowing the new dialog to be used
-non-modal - Java prevents the non-modal from being editing if not parented
correctly
D -The user runs a script that shows an input dialog before the non-modal script
dialog is shown. If the non-modal dialog is parented to the modal input dialog,
then the script progress dialog appears on top (which we do not want) and the
progress dialog goes away when the input dialog is closed.
E -A long-running API shows a non-modal progress dialog. This API then shows a
results dialog which is also non-modal. We do not want to parent the new dialog
to the original dialog, since it is a progress dialog that will go away.
For now, the easiest mental model to use is to always prefer the active non-transient
window so that a dialog will appear in the user's view. If we find a case where this is
not desired, then document it here.
*/
DockingWindowManager dwm = getActiveInstance();
Window defaultWindow = dwm != null ? dwm.getRootFrame() : null;
if (parent == null) {
Window w = getActiveNonTransientWindow();
return w == null ? defaultWindow : w;
}
Component c = parent;
while (c != null) {
// Note: using a Frame here means that we will not find and use a dialog that is a
// parent of 'c' if it itself is parented to a Frame. The issue is that
// Use Case 'C' above may not work correctly. If we find that to be the case,
// then we can try changing 'Frame' to 'Window' here.
if (c instanceof Window && isNonTransientWindow(c)) {
return (Window) c;
}
c = c.getParent();
}
return defaultWindow;
}
private static boolean isNonTransientWindow(Component c) {
if (c == null || !c.isShowing()) {
return false;
}
if (c instanceof DockingFrame) {
DockingFrame frame = (DockingFrame) c;
if (frame.isTransient()) {
return false;
}
// consider any window transient if all of its contained providers cannot be parents
return hasAnyParentableProvider(frame);
}
if (c instanceof DockingDialog) {
DockingDialog d = (DockingDialog) c;
DialogComponentProvider provider = d.getComponent();
if (provider == null) {
return false; // we have seen this in testing
}
return !provider.isModal() && !provider.isTransient();
}
if (c instanceof Dialog) {
return !((Dialog) c).isModal();
}
return (c instanceof Window);
}
private static boolean hasAnyParentableProvider(Window window) {
boolean hasAnyParentableProvider = false;
DockingWindowManager dwm = getInstanceForWindow(window);
if (dwm == null) {
return true; // the window is not affiliated with a window manager
}
WindowNode node = dwm.root.getNodeForWindow(window);
List<ComponentPlaceholder> placeholders = node.getActiveComponents();
for (ComponentPlaceholder placeholder : placeholders) {
DockableComponent dc = placeholder.getComponent();
ComponentProvider provider = dc.getComponentProvider();
hasAnyParentableProvider |= provider.canBeParent();
}
return hasAnyParentableProvider;
}
private static Window getJavaActiveWindow() {
KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager();
Window activeWindow = kfm.getActiveWindow();
if (activeWindow == null) {
return null;
}
if (!activeWindow.isShowing()) {
return null; // don't let non-showing windows be considered active
}
return activeWindow;
}
private static Window getActiveNonTransientWindow() {
Window bestWindow = getJavaActiveWindow();
if (bestWindow instanceof DockingDialog) {
// We do not want Task Dialogs becoming parents, as they will get closed when the
// task is finished, closing any other child dialogs, which means that dialogs such
// as message dialogs will too be closed
DockingDialog d = (DockingDialog) bestWindow;
if (isNonTransientWindow(d)) {
return d;
}
// The active window is not a suitable parent; try its parent
bestWindow = SwingUtilities.getWindowAncestor(d);
}
if (isNonTransientWindow(bestWindow)) {
return bestWindow;
}
return null;
}
public ComponentProvider getActiveComponentProvider() {
if (focusedPlaceholder != null) {
return focusedPlaceholder.getProvider();
}
return null;
}
/**
* Sets the icon for this window's 'home button'. This button, when pressed, will show the
* tool's main application window.
*
* @param icon the button's icon
* @param callback the callback to execute when the button is pressed by the user
*/
public void setHomeButton(Icon icon, Runnable callback) {
root.setHomeButton(icon, callback);
}
/**
* Returns true if a status bar is present.
*
* @return true if a status bar is present.
*/
public boolean hasStatusBar() {
return hasStatusBar;
}
/**
* Add a new status item component to the status area. The preferred height and border for the
* component will be altered. The components preferred width will be preserved.
*
* @param c the status item component to add
* @param addBorder True signals to add a border to the status area
* @param rightSide component will be added to the right-side of the status area if true, else
* it will be added immediately after the status text area if false.
*/
public void addStatusItem(JComponent c, boolean addBorder, boolean rightSide) {
root.addStatusItem(c, addBorder, rightSide);
}
/**
* Remove the specified status item.
*
* @param c status component previously added.
*/
public void removeStatusItem(JComponent c) {
root.removeStatusItem(c);
}
/**
* Set the status text in the active component window
*
* @param text status text
*/
public void setStatusText(String text) {
if (root != null) {
root.setStatusText(text);
}
}
/**
* Set the status text in the active component window
*
* @param text string to be displayed in the Status display area
* @param beep whether to beep or not
*/
public void setStatusText(String text, boolean beep) {
if (root == null) {
return;
}
setStatusText(text);
if (beep) {
Toolkit.getDefaultToolkit().beep();
}
}
/**
* Get the status text in the active component window
*
* @return string currently displayed in the Status display area
*/
public String getStatusText() {
return root.getStatusText();
}
/**
* A convenience method to make an attention-grabbing noise to the user
*/
public static void beep() {
Toolkit.getDefaultToolkit().beep();
}
/*
* A version of setMenuGroup() that does *not* trigger an update. When clients call the
* public API, an update is needed. This method is used during the rebuilding process
* when we know that an update is not need, as we are in the middle of an update.
*/
void doSetMenuGroup(String[] menuPath, String group) {
actionToGuiMapper.setMenuGroup(menuPath, group, null);
}
/**
* Set the menu group associated with a cascaded submenu. This allows a cascading menu item to
* be grouped with a specific set of actions.
* <p>
* The default group for a cascaded submenu is the name of the submenu.
* <p>
*
* @param menuPath menu name path where the last element corresponds to the specified group
* name.
* @param group group name
* @param menuSubGroup the name used to sort the cascaded menu within other menu items at its
* level
*/
public void setMenuGroup(String[] menuPath, String group, String menuSubGroup) {
actionToGuiMapper.setMenuGroup(menuPath, group, menuSubGroup);
scheduleUpdate();
}
/**
* Tests if the given component is one of a known list of component classes that we don't ever
* want to get keyboard focus. Currently excluded is JScrollPane
*
* @param c the component to test for exclusion
* @return true if the component should not be allowed to have keyboard focus.
*/
static boolean excludeFocus(Component c) {
return (c instanceof JScrollPane) || (c instanceof JScrollBar) ||
(c instanceof JTabbedPane);
}
/**
* Sets the mode such that all satellite docking windows always appear on top of the root window
*
* @param windowsOnTop true to set mode to on top, false to disable on top mode.
*/
public void setWindowsOnTop(boolean windowsOnTop) {
this.windowsOnTop = windowsOnTop;
root.updateDialogs();
}
/**
* Returns true if the window mode is "satellite windows always on top of root window".
*
* @return true if the window mode is "satellite windows always on top of root window".
*/
public boolean isWindowsOnTop() {
return windowsOnTop;
}
/**
* Returns a list with all the windows in the windowStack. Used for testing.
*
* @param includeMain if true, include the main root window.
* @return a list with all the windows in the windowStack. Used for testing.
*/
public List<Window> getWindows(boolean includeMain) {
ArrayList<Window> winList = new ArrayList<>();
if (includeMain) {
winList.add(root.getMainWindow());
}
for (DetachedWindowNode node : root.getDetachedWindows()) {
Window win = node.getWindow();
if (win != null) {
winList.add(win);
}
}
return winList;
}
void iconify() {
List<Window> winList = getWindows(false);
for (Window w : winList) {
if (w instanceof Frame) {
w.setVisible(false);
}
}
}
void deIconify() {
List<Window> winList = getWindows(false);
for (Window w : winList) {
if (w instanceof Frame) {
w.setVisible(true);
}
}
}
/**
* Returns the root window.
*
* @return the root window.
*/
public Window getMainWindow() {
return root.getMainWindow();
}
public static DockingActionIf getMouseOverAction() {
return actionUnderMouse;
}
public static void setMouseOverAction(DockingActionIf action) {
actionUnderMouse = action;
}
public static Object getMouseOverObject() {
return objectUnderMouse;
}
public static void setMouseOverObject(Object object) {
objectUnderMouse = object;
}
public static void clearMouseOverHelp() {
actionUnderMouse = null;
objectUnderMouse = null;
}
/**
* Shows a popup menu over the given component. If this given component is not part of the
* docking windows hierarchy, then no action is taken.
*
* @param component the component
*/
public static void showContextMenu(Component component) {
DockingWindowManager dwm = getInstance(component);
if (dwm == null) {
return;
}
DockableComponent dockableComponent = dwm.getDockableComponent(component);
if (dockableComponent == null) {
return;
}
Rectangle bounds = dockableComponent.getBounds();
bounds.x = 0;
bounds.y = 0;
int x = (int) bounds.getCenterX();
int y = (int) bounds.getCenterY();
PopupMenuContext popupContext = new PopupMenuContext(dockableComponent, new Point(x, y));
dockableComponent.showContextMenu(popupContext);
}
public void contextChanged(ComponentProvider provider) {
// if provider is specified, update its local menu and tool bar actions
if (provider != null) {
ComponentPlaceholder placeholder = getActivePlaceholder(provider);
if (placeholder != null) {
placeholder.contextChanged();
}
}
// always update the global tool menu and tool bar actions
actionToGuiMapper.contextChanged();
}
/**
* Adds the given popup action provider to this tool. This provider will be called each time the
* popup menu is about to be shown.
*
* @param provider the provider
*/
public void addPopupActionProvider(PopupActionProvider provider) {
popupActionProviders.add(provider);
}
/**
* Removes the given popup action provider
*
* @param provider the provider
*/
public void removePopupActionProvider(PopupActionProvider provider) {
popupActionProviders.remove(provider);
}
/**
* Returns a list of temporary popup actions to be returned. Only those actions which have a
* suitable popup menu path will be considered. This mechanism allows clients to add transient
* actions to be added to the tool without the accompanying management overhead.
*
* @param context the ActionContext
* @return list of temporary actions
* @see #addPopupActionProvider(PopupActionProvider)
*/
List<DockingActionIf> getTemporaryPopupActions(ActionContext context) {
List<DockingActionIf> actionList = new ArrayList<>();
for (PopupActionProvider pl : popupActionProviders) {
List<DockingActionIf> actions = pl.getPopupActions(tool, context);
if (actions != null) {
actionList.addAll(actions);
}
}
return actionList;
}
public void addContextListener(DockingContextListener listener) {
contextListeners.add(listener);
}
public void removeContextListener(DockingContextListener listener) {
contextListeners.remove(listener);
}
/**
* Returns the default {@link ActionContext} for the given context type
* @param contextType the class of the ActionContext to get a default value for
* @return the default {@link ActionContext} for the given context type
*/
public ActionContext getDefaultActionContext(Class<? extends ActionContext> contextType) {
ActionContextProvider actionContextProvider = defaultContextProviderMap.get(contextType);
if (actionContextProvider != null) {
return actionContextProvider.getActionContext(null);
}
return null;
}
/**
* Returns a map containing a default {@link ActionContext} for each registered type.
* @return a map containing a default {@link ActionContext} for each registered type
*/
public Map<Class<? extends ActionContext>, ActionContext> getDefaultActionContextMap() {
Map<Class<? extends ActionContext>, ActionContext> contextMap = new HashMap<>();
Set<Entry<Class<? extends ActionContext>, ActionContextProvider>> entrySet =
defaultContextProviderMap.entrySet();
for (Entry<Class<? extends ActionContext>, ActionContextProvider> entry : entrySet) {
contextMap.put(entry.getKey(), entry.getValue().getActionContext(null));
}
return contextMap;
}
/**
* Creates the {@link ActionContext} appropriate for the given action. This will normally be the
* context from the currently focused {@link ComponentProvider}. If that context is not valid
* for the given action and the action supports using the default tool context, then the default
* tool context will be returned. Otherwise, returns a generic ActionContext.
*
* @param action the action for which to get an {@link ActionContext}
* @return the {@link ActionContext} appropriate for the given action or null
*/
public ActionContext createActionContext(DockingActionIf action) {
ComponentProvider provider = getActiveComponentProvider();
ActionContext context = provider == null ? null : provider.getActionContext(null);
if (context != null && action.isValidContext(context)) {
return context;
}
// Some actions work on a non-active, default component provider. See if this action
// supports that.
if (action.supportsDefaultContext()) {
context = getDefaultContext(action.getContextClass());
if (context != null) {
return context;
}
}
return new DefaultActionContext(provider, null);
}
/**
* Returns the set of global tool actions
* @return the set of global tool actions
*/
public Set<DockingActionIf> getGlobalActions() {
return new HashSet<>(actionToGuiMapper.getGlobalActions());
}
private ActionContext getDefaultContext(Class<? extends ActionContext> contextType) {
ActionContextProvider contextProvider = defaultContextProviderMap.get(contextType);
if (contextProvider != null) {
return contextProvider.getActionContext(null);
}
return null;
}
/**
* This call will notify any context listeners that the context has changed.
*
* <p>
* Our {@link #contextChanged(ComponentProvider)} method will eventually call back into this
* method after any buffering has taken place.
*
* @param context the context
*/
void doContextChanged(ActionContext context) {
for (DockingContextListener listener : contextListeners) {
listener.contextChanged(context);
}
}
/**
* Registers a callback to be notified when the given component has been parented to a docking
* window manager
*
* @param component the component that will be parented in a docking window system
* @param listener the listener to be notified the component was parented
*/
public static void registerComponentLoadedListener(Component component,
ComponentLoadedListener listener) {
component.addHierarchyListener(new HierarchyListener() {
@Override
public void hierarchyChanged(HierarchyEvent e) {
long changeFlags = e.getChangeFlags();
if (HierarchyEvent.DISPLAYABILITY_CHANGED != (changeFlags &
HierarchyEvent.DISPLAYABILITY_CHANGED)) {
return;
}
// check for the first time we are put together
boolean isDisplayable = component.isDisplayable();
if (!isDisplayable) {
return;
}
component.removeHierarchyListener(this);
DockingWindowManager dwm = getInstance(component);
ComponentProvider provider = null;
if (dwm != null) {
provider = dwm.getComponentProvider(component);
}
listener.componentLoaded(dwm, provider);
}
});
}
}