/* * Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of Business Objects nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /* * ValueEditorHierarchyManager.java * Creation date: April 3rd 03 * By: Ken Wong */ package org.openquark.gems.client.valueentry; import java.awt.AWTEvent; import java.awt.Component; import java.awt.Container; import java.awt.Dialog; import java.awt.Point; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.Window; import java.awt.event.AWTEventListener; import java.awt.event.HierarchyEvent; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.Stack; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JLayeredPane; import javax.swing.JPopupMenu; import javax.swing.JRootPane; import javax.swing.SwingUtilities; /** * This class is responsible for maintaining the hierarchy of ValueEditors. * The existence of this class allows the possibility of having multiple hierarchies for a single ValueEditorManager. * So in a single apps, if there are multiple deployment of ValueEntry for similar purposes, they can share the same * ValueEditorManager (which allows the creation and management of individual ValueEntryPanels) while maintaining fine grain * control each individual hierarchy. * @author Ken Wong */ public class ValueEditorHierarchyManager { /** The ValueEditorManager that will be providing the resources for this HierarchyManager to manage its hierarchy */ private final ValueEditorManager valueEditorManager; /** * The Container which holds all of the ValueEditors. * If not set, value editors will be placed in the JLayeredPane. */ private final JComponent valueEditorContainer; /** The list of the top level editor panels. */ private final List<ValueEditor> topEditorPanels = new ArrayList<ValueEditor>(); /** * This, and the currentEditor gives the ValueEditor Hierarchy. * The Value Editors in this stack trace a path from the first ValueEditor, thru its children, to the currentEditor. * Note: The currentEditor value is not stored in this stack. It will be stored * in this stack if it launches a new ValueEditor, and so its child will be the new currentEditor. */ private final Stack<ValueEditor> currentEditorStack = new Stack<ValueEditor>(); /** There can be only one editor open (with the focus) across all ValueEditors managed by a hierarchy manager. */ private ValueEditor currentEditor = null; /** This listener listens for mouse clicks and dismisses value editors accordingly. * This listener will be installed when the first top-level value editor is added to the hierarchy, * and will be removed if the last top-level value editor is removed from the hierarchy. */ private final AWTEventListener hierarchyEventListener; /** The commit cancel handler for this hierarchy, if any. */ private HierarchyCommitCancelHandler hierarchyCommitCancelHandler = null; private final boolean useFocusListeners; /** * This interface allows specialization the behaviour of the hierarchy in the event of a top-level "hard" commit * or cancel event from a top-level value editor. * For example, some applications may be waiting on data entered from this hierarchy, in which case a "hard" commit or cancel * will indicate that the user has finished entering data. * @author Edward Lam */ public static interface HierarchyCommitCancelHandler { /** * Handle "hard" commits or cancels from a top-level value editor in this hierarchy. * This occurs when a top-level editor handles a commit or cancel gesture (such as the <Enter> key) or * editors are commited by mouse click which doesn't hit any editors. * @param commit whether a commit or a cancel was performed. * True = commit, false = cancel. */ public abstract void handleHierarchyCommitCancel(boolean commit); } /** * An AWTEventListener installed by the hierarchy manager. It listens to mouse pressed * and hierarchy changed events to update the value editor hierarchy if stuff happens. * @author Frank Worsley */ private class HierarchyEventListener implements AWTEventListener { /** * @see java.awt.event.AWTEventListener#eventDispatched(java.awt.AWTEvent) */ public void eventDispatched(AWTEvent e) { if (e.getID() == MouseEvent.MOUSE_PRESSED) { processMousePressedEvent((MouseEvent) e); } else if (e.getID() == HierarchyEvent.HIERARCHY_CHANGED) { processHierarchyChangedEvent((HierarchyEvent) e); } } /** * Processes a HierarchyEvent of type DISPLAYABILITY_CHANGED & SHOWING_CHANGED. * This event is dispatched when a component is added to another component that is visible * and parented and as a result the visibility of the added component changes. * If this happens to a value editor we want to refresh its display, since that means the * editor has been parented and made visible for the first time. * @param evt the hierarchy event */ private void processHierarchyChangedEvent(HierarchyEvent evt) { Component changedComponent = evt.getChanged(); if ((evt.getChangeFlags() & HierarchyEvent.DISPLAYABILITY_CHANGED) != 0 && (evt.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0 && changedComponent instanceof ValueEditor && changedComponent.isShowing() && isManagingEditor((ValueEditor) changedComponent)) { final ValueEditor changedEditor = (ValueEditor) changedComponent; SwingUtilities.invokeLater(new Runnable() { public void run() { changedEditor.refreshDisplay(); } }); } } /** * Processes a MouseEvent of type MOUSE_PRESSED. * @param evt the mouse event */ private void processMousePressedEvent(MouseEvent evt) { // Do nothing if there are no editors in the current hierarchy. if (currentEditor == null) { return; } // Figure out the "real" container on which to perform hit testing. Container hitTestContainer = valueEditorContainer; JRootPane rootPane = SwingUtilities.getRootPane(valueEditorContainer); if (rootPane != null) { if (valueEditorContainer == rootPane || valueEditorContainer == rootPane.getContentPane() || valueEditorContainer == rootPane.getGlassPane() || valueEditorContainer == rootPane.getLayeredPane()) { hitTestContainer = rootPane.getParent(); } } // Ignore if the component hit isn't in the value editor container (if any). Component componentHit = evt.getComponent(); if (hitTestContainer != null && !SwingUtilities.isDescendingFrom(componentHit, hitTestContainer)) { return; } // Ignore if the component descends from a popup menu (eg. from a combo box drop-down). if (descendsFromPopupMenu(componentHit)) { return; } // Ignore if there's a modal dialog showing, and it's not the window in which the valueEditorContainer (if any) exists. // This fixes the problem with bringing up a JColorChooser from the FormatPaletteValueEditor. Window componentWindow = componentHit instanceof Window ? (Window) componentHit : SwingUtilities.getWindowAncestor(componentHit); Window containerWindow = valueEditorContainer == null ? null : SwingUtilities.getWindowAncestor(valueEditorContainer); if (componentWindow != containerWindow && componentWindow instanceof Dialog && ((Dialog)componentWindow).isModal()) { return; } // Ignore if not managing this editor. ValueEditor editorHit = getValueEditorForComponent(componentHit); if (editorHit != null && !isManagingEditor(editorHit)) { return; } if (editorHit != currentEditor) { activateEditor(editorHit); } // If no editor was clicked, this is considered a "hard" commit. if (editorHit == null) { notifyCommitCancelHandler(true); } } } /** * Default ValueEditorHierarchyManager constructor. * Value editors will be placed in the JLayeredPane. * @param valueEditorManager */ public ValueEditorHierarchyManager(ValueEditorManager valueEditorManager) { this(valueEditorManager, null); } /** * ValueEditorHierarchyManager constructor. * @param valueEditorManager * @param container the container in which value editors exist. * If null, value editors will be placed in the JLayeredPane of the parent frame of the top level value editor. */ public ValueEditorHierarchyManager(ValueEditorManager valueEditorManager, JComponent container) { this(valueEditorManager, container, true); } /** * ValueEditorHierarchyManager constructor. * @param valueEditorManager * @param container the container in which value editors exist. * @param useFocusListeners whether the hierarchy manager should use focus listeners to collapse * the top level value editors. * If null, value editors will be placed in the JLayeredPane of the parent frame of the top level value editor. */ public ValueEditorHierarchyManager(ValueEditorManager valueEditorManager, JComponent container, boolean useFocusListeners) { this.valueEditorManager = valueEditorManager; this.valueEditorContainer = container; this.useFocusListeners = useFocusListeners; // Only create the hierarchy listener if we want to use it to listen to focus events if (useFocusListeners) { this.hierarchyEventListener = new HierarchyEventListener(); } else { this.hierarchyEventListener = null; } } /** * Set the hierarchy commit/cancel handler for this hierarchy. * @param newHandler the new commit/cancel handler. */ public void setHierarchyCommitCancelHandler(HierarchyCommitCancelHandler newHandler) { this.hierarchyCommitCancelHandler = newHandler; } /** * Determine whether a given component has a JPopupMenu as an ancestor in its component hierarchy. * @param component the component in question * @return whether the component descends from a JPopupMenu */ private boolean descendsFromPopupMenu(Component component) { for (Component testComponent = component; testComponent != null; testComponent = testComponent.getParent()) { if (testComponent instanceof JPopupMenu) { return true; } } return false; } /** * Return whether the given editor exists in the current editor hierarchy. * @param editor the editor to check * @return boolean whether the given editor exists in the current editor hierarchy. */ boolean existsInHierarchy(ValueEditor editor) { return editor == currentEditor || currentEditorStack.contains(editor); } /** * Get the child editor (if any) of a value editor in the current hierarchy. * @param parentValueEditor the editor whose current child will be returned. * @return ValueEditor the child editor. Returns null if the parent is null, * has no child, or does not exist in the hierarchy. */ ValueEditor getChildEditor(ValueEditor parentValueEditor) { if (parentValueEditor == null) { return null; } // Get the index of the parent in the hierarchy. int parentIndex = currentEditorStack.indexOf(parentValueEditor); if (parentIndex < 0) { return null; } // Get the child in the hierarchy. if (parentIndex == currentEditorStack.size() - 1) { return currentEditor; } return currentEditorStack.get(parentIndex + 1); } /** * Returns a list that contains all instances of ValueEditors that are being displayed * @return List */ public List<ValueEditor> getTopValueEditors() { return new ArrayList<ValueEditor>(topEditorPanels); } /** * Adds a new top level ValueEditor to the ValueEditorHierarchy. * If the Hierarchy only contains this newly added ValueEditor, then this ValueEditor * will be the currentEditor and given focus. Note that the new editor must be parented * to an existing component. This is because setInitialValue will be called on the new * editor which may require a valid Graphics object. * Note: the current hierarchy will be collapsed. * @param newValueEditor ValueEditor */ public void addTopValueEditor(ValueEditor newValueEditor) { // Safety check. The newEditor should not be in a closing/closed state if (newValueEditor.isEditorClosing()) { throw new IllegalArgumentException("Cannot launch an editor that is closing"); } topEditorPanels.add(newValueEditor); collapseHierarchy(null, true); // Ensure that the editor has been initialized (could change preferred size) // TODO: This should be called by the constructor / setValueNode() and setInitialValue() // should be private so that clients of the value editor system don't need to worry about it. newValueEditor.setInitialValue(); if (topEditorPanels.size() == 1) { // install the dismiss event listener if (useFocusListeners) { Toolkit.getDefaultToolkit().addAWTEventListener(hierarchyEventListener, AWTEvent.MOUSE_EVENT_MASK | AWTEvent.HIERARCHY_EVENT_MASK); } // newValueEditor is our first top level ValueEditor. activateEditor(newValueEditor); } } /** * Removes a top level ValueEditor from the ValueEditorHierarchy. * If the removed top level ValueEditor has descendent ValueEditors open, then these * descendent ValueEditors will be closed, and focus will be given to another * top level ValueEditor (if there is one). * @param removeValueEditor */ private void removeTopValueEditor(ValueEditor removeValueEditor) { // Checking to see if the to be removed ValueEditor has descendent ValueEditors. ValueEditor topLevelEditor = (currentEditorStack.isEmpty()) ? currentEditor : currentEditorStack.get(0); // Check if we're removing the editor at the top of the current editor stack. if (topLevelEditor == removeValueEditor) { // Clear away any editor stack, committing data values. collapseHierarchy(null, true); // Remove the removeValueEditor from the top level panels. // Note: We did not remove the removeValueEditor earlier because the above // commit may still need removeValueEditor to be in topEditorPanels. topEditorPanels.remove(removeValueEditor); // Activate another top level ValueEditor (if there is one) if (!topEditorPanels.isEmpty()) { activateEditor(topEditorPanels.get(0)); } } else { // Remove the removeValueEditor from the top level panels. topEditorPanels.remove(removeValueEditor); } if (topEditorPanels.isEmpty()) { // remove the dismiss event listener if (useFocusListeners) { Toolkit.getDefaultToolkit().removeAWTEventListener(hierarchyEventListener); } // update the current editor. setCurrentEditor(null); } } /** * Close a given value editor and remove it from this manager. * @param valueEditor the value editor to close and remove from this manager. * @param commit if true editor is committed before it is removed, otherwise it is cancelled */ public void removeValueEditor(ValueEditor valueEditor, boolean commit) { removeValueEditors(Collections.singletonList(valueEditor), commit); } /** * Close a number of value editors and remove them from this manager. * @param valueEditorList the value editors to remove. * @param commit if true editor is committed before it is removed, otherwise it is cancelled */ public void removeValueEditors(List<? extends ValueEditor> valueEditorList, boolean commit) { Set<ValueEditor> topValueEditorSet = new HashSet<ValueEditor>(topEditorPanels); for (int i = 0, numPanels = valueEditorList.size(); i < numPanels; i++) { // Remove this panel and its descendants from the display ValueEditor editor = valueEditorList.get(i); if (!isManagingEditor(editor)) { throw new IllegalArgumentException("Attempt to remove a value editor which is not managed by this hierarchy manager."); } closeEditor(editor, commit); if (topValueEditorSet.contains(editor)) { removeTopValueEditor(editor); topValueEditorSet.remove(editor); } } } /** * If there are value editors in the value editor hierarchy, then the current editor will receive focus. */ public void activateCurrentEditor() { activateEditor(currentEditor); } /** * Make a given editor the current editor, and give it focus. * @param editorToActivate the editor to activate, or null to clear the current editor. */ public void activateEditor(ValueEditor editorToActivate) { if (editorToActivate != currentEditor) { makeCurrentEditor(editorToActivate); } // If editorToActivate is null and there are top-level editors, then makeCurrentEditor // will make one of the top-level editors the new current editor. In that case we don't // want to activate that editor. Therefore make sure the current editor is the one to activate. if (currentEditor != null && editorToActivate != null) { currentEditor.editorActivated(); // Get the default focus component to request focus. Component focusComponent = currentEditor.getDefaultFocusComponent(); if (focusComponent != null) { focusComponent.requestFocusInWindow(); } } } /** * Sets the currentEditor. * @param currentEditor The currentEditor to set, or null to clear. */ private void setCurrentEditor(ValueEditor currentEditor) { this.currentEditor = currentEditor; } /** * Morph the editor hierarchy so that the specified editor will be the current editor. * Any editors which are closed will have their values commited to the parent. * @param editor the editor to make into the current editor, or null to collapse the current hierarchy. * The hierarchy will be collapsed as appropriate, and if necessary control will be switched among sibling editors * of a StructuredValueEditor. * Note: the specified editor should be managed by this hierarchy manager. */ private void makeCurrentEditor(ValueEditor editor) { ValueEditor previousCurrentEditor = currentEditor; if (editor != null) { ValueEditor childEditor = getChildEditor(editor); if (editor instanceof StructuredValueEditor && childEditor != null) { // Collapse to the child editor if it's a structured value editor. collapseHierarchy(childEditor, true); } else if (!existsInHierarchy(editor) && existsInHierarchy(editor.getParentValueEditor())) { // If the editor to activate is not in the hierarchy, but its parent is in the // hierarchy, then we are switching among the component editors of the parent. collapseHierarchy(getChildEditor(editor.getParentValueEditor()), true); setCurrentEditor(editor); } else if (editor != null && topEditorPanels.contains(editor)) { // If switching to a top-level editor (possibly not in this hierarchy), collapse all then switch. collapseHierarchy(null, true); setCurrentEditor(editor); } else { // collapse to the editor which was specified (if it's in this hierarchy). collapseHierarchy(editor, true); setCurrentEditor(editor); } } else { // dismiss all open editors collapseHierarchy(null, true); } // This is required to commit the following - // 1) Values for top-level VEPs when clicking off their text fields. // Non top-level editors commit when they close.. but top-level vep's often don't close. // 2) child value entry panels which don't close (eg. in an AlmostSumAbstractValueEditor) when clicking off them. if (previousCurrentEditor != null && previousCurrentEditor != editor && !previousCurrentEditor.isEditorClosing()) { previousCurrentEditor.commitValue(); } // Note that the hierarchy also updates the current editor on closeEditor() as necessary. } /** * Determine whether a given editor is managed by this hierarchy manager. * @param valueEditor the value editor in question * @return whether the editor is managed by this hierarchy manager. */ private boolean isManagingEditor(ValueEditor valueEditor) { // Just find out whether the top-most editor is a member of the top editor panel list. ValueEditor topMostEditor = valueEditor; while (topMostEditor.getParentValueEditor() != null) { topMostEditor = topMostEditor.getParentValueEditor(); } return topEditorPanels.contains(topMostEditor); } /** * Add a value editor to the current value editor hierarchy. * This is normally used when a value editor itself uses value editors without launching them. * Use launchEditor() instead, to launch an editor. * @param newCurrentEditor the new current value editor * @param parentEditor the parent of the newCurrentEditor. * This will be added to the hierarchy as well, if it hasn't already been added. */ public void addEditorToHierarchy(ValueEditor newCurrentEditor, ValueEditor parentEditor) { prepareEditor(newCurrentEditor, parentEditor); activateCurrentEditor(); } /** * Launch a new value editor. * @param newEditor the editor to launch. * @param location The location to launch the newEditor, in the parent's coordinate system. * @param parentEditor The parent editor of the editor being launched. * @return boolean whether an editor was launched. This may return false if, for instance, * the manager is unable to obtain a top level ancestor for the parent. */ public boolean launchEditor(ValueEditor newEditor, Point location, ValueEditor parentEditor) { // Safety check. The newEditor should not be in a closing/closed state if (newEditor.isEditorClosing()) { throw new IllegalArgumentException("Cannot launch an editor that is closing"); } // This is a work around to prevent the null pointer exception that can sometimes arise when // a VEP is being used in a list or tuple editor and the "..." button has been pressed // to open a child editor and then the Switch Type icon is pressed without closing the child // editor first. The problem can also occur when the switch type editor is opened first and // then the "..." button is pressed. What happens is that the TableCellEditor (a subclass of // the ValueEditor) is constantly added and removed to the table while the editors are // opening and closing. It seems that when the buttons/icons are pressed as described above // the timing is such that the cell editor has been removed from the table and as such has no // parent (and therefore no top level parent). When we try to get the layered pane belonging // to the parent, we get a null pointer exception and the new editor is not opened. This // little fix stops trying to display the new editor if it sees that there is no top level parent. Container parentContainer = parentEditor.getParent(); while (parentContainer != null && !(parentContainer instanceof JFrame)) { parentContainer = parentContainer.getParent(); } if (parentContainer == null) { // Reset the flag so that the switch type icon can be clicked again return false; } // Collapse the hierarchy to the parent - this will close any existing child editors. collapseHierarchy(parentEditor, true); // Tell the 'youngest' parent StructuredValueEditor that this element is launching a ValueEditor. // Note: Quite possible that there are no such 'youngest' parent. for (Container ancestor = parentEditor.getParent(); ancestor != null; ancestor = ancestor.getParent()) { if (ancestor instanceof StructuredValueEditor) { StructuredValueEditor sve = (StructuredValueEditor) ancestor; sve.handleElementLaunchingEditor(); break; } } // Prepare the editor to be added to the hierarchy. prepareEditor(newEditor, parentEditor); // Place the value editor on the screen displayValueEditor(newEditor, parentEditor, location); // Activate the editor. activateCurrentEditor(); return true; } /** * Displays the value editor on the screen. The new editor provided should be initialized and needs * to be added to a visible container so that the user can see it. * @param newEditor A valid, fully initialized value editor * @param parentEditor The parent value editor if needed. In this implementation the frame containing * the parent editor will be used to place the new editor. * @param mouseLocation The absolute coordinates of the desired launch location. This could come * from the mouse position or some similar value. */ protected void displayValueEditor(ValueEditor newEditor, ValueEditor parentEditor, Point mouseLocation) { // Get the parent container Container parentContainer = parentEditor.getParent(); while (parentContainer != null && !(parentContainer instanceof JFrame)) { parentContainer = parentContainer.getParent(); } // Add the editor to the value editor container. // If no editor container has been provided, use the layered pane of the parent of the top most editor. JComponent editorContainer = valueEditorContainer != null ? valueEditorContainer : parentContainer != null ? ((JFrame)parentContainer).getLayeredPane() : null; Point finalLocation = getNewEditorLocation(mouseLocation, newEditor, parentEditor, editorContainer); newEditor.setLocation(finalLocation); if (editorContainer instanceof JLayeredPane) { JLayeredPane jlp = (JLayeredPane) editorContainer; jlp.setLayer(newEditor, JLayeredPane.PALETTE_LAYER.intValue(), 1); jlp.add(newEditor); jlp.moveToFront(newEditor); jlp.revalidate(); } else { editorContainer.add(newEditor, 0); editorContainer.revalidate(); } } /** * Prepares a new editor to be added to the current hierarchy and displayed on screen. * @param newEditor the new editor to be added * @param parentEditor the parent of the new editor */ private void prepareEditor(ValueEditor newEditor, ValueEditor parentEditor) { // Set the new editor characteristics. newEditor.setParentValueEditor(parentEditor); newEditor.setEditable(parentEditor.isEditable()); // Update the editor hierarchy. // Do this before setting the initial value since the hierarchy might need // to be up to date for things that happen while setting the initial value. if (currentEditorStack.empty() || currentEditorStack.peek() != parentEditor) { if (currentEditorStack.contains(parentEditor)) { throw new IllegalStateException("parent editor of new editor is not at top of editor stack"); } currentEditorStack.push(parentEditor); } // Make the new editor the current editor. setCurrentEditor(newEditor); // Make sure the editors starting value is initialized. newEditor.setInitialValue(); } /** * Calculates the location on screen where a new editor should appear. * If the editor is too large to appear at the desired location it will * calculate a more appropriate location. */ private Point getNewEditorLocation (Point desiredLocation, ValueEditor newEditor, ValueEditor parentEditor, JComponent editorContainer) { Point location = SwingUtilities.convertPoint(parentEditor, desiredLocation, editorContainer); boolean editorTooWide = false; // Use the visible rectangle instead of the container size, in case the // container is embedded in a scrollpane. Rectangle containerRect = editorContainer.getVisibleRect(); // Check if the editor is too wide. If it is too wide move // it over so it fits inside its container. if (location.x + newEditor.getWidth() > containerRect.x + containerRect.width) { editorTooWide = true; int relativeOffset = parentEditor.getWidth() - desiredLocation.x; int newX = location.x - newEditor.getWidth() + relativeOffset; location.x = newX > 0 ? newX : 0; location.y += newEditor.getInsets().top; // If we move the editor to the left also move it // up so it doesn't cover its parent editor location.y += parentEditor.getHeight(); } // Now check if the editor is too long. If it is too long // move it up so that it fits inside its container. if (location.y + newEditor.getHeight() > containerRect.y + containerRect.height) { int newY = location.y - newEditor.getHeight() + newEditor.getInsets().top + parentEditor.getHeight(); // If the editor is too wide make sure it stays // moved up above the parent editor so it doesn't // cover it's parent. if (editorTooWide) { newY -= 2 * parentEditor.getHeight(); newY -= newEditor.getInsets().top; } location.y = newY > 0 ? newY : 0; } return location; } /** * Get the value editor which contains a given component * @param component the component to check. * @return ValueEditor the closest value editor ancestor to the given component, * If the component is a value editor, returns the component. * Returns null if there is no value editor ancestor or if component is null. */ private static ValueEditor getValueEditorForComponent(Component component) { for (Component parent = component; parent != null; parent = parent.getParent()) { if (parent instanceof ValueEditor) { return (ValueEditor)parent; } } return null; } /** * Collapses the hierarchy to the active top-level editor and makes sure that all editors, * including the top-level editor, have their values committed. This is different from collapseHierarchy, * since that method only collapses to the given editor, but does not commit it. * @param commit whether to commit the editors */ public void commitHierarchy(boolean commit) { collapseHierarchy(null, commit); // Make sure the top-level editor is committed too. if (currentEditor != null) { if (commit) { currentEditor.handleCommitGesture(); } else { currentEditor.handleCancelGesture(); } } } /** * Collapse the current editor stack to a given editor. This calls closeEditor() in turn on * the editors to be closed. Focus will not be directly modified by this method. * Note: clients will have to refreshDisplay() if stale data is currently displayed. * @param targetEditor collapsing will end at this editor. * If this is null, the current hierarchy will be completely collapsed to its top-level editor. * If the target editor is not on the stack, no collapsing will take place. * @param commit Whether to commit values on collapse. */ public void collapseHierarchy(ValueEditor targetEditor, boolean commit) { if (currentEditorStack.empty()) { return; } // Ignore if the target editor is not in the editor stack. if (targetEditor != null && !(currentEditorStack.contains(targetEditor))) { return; } if (targetEditor == null) { targetEditor = currentEditorStack.get(0); } // Collapse to the child editor if it's a structured value editor. ValueEditor childEditor = getChildEditor(targetEditor); if (targetEditor instanceof StructuredValueEditor && childEditor != null) { collapseHierarchy(childEditor, commit); return; } while (!currentEditorStack.empty()) { // As we close editors the current editor will be updated. if (currentEditor == targetEditor) { break; } if (currentEditor.isEditorClosing()) { // HACK: fixes a bug where you instantiate the first element of a pair, and commit -> calls this twice.. return; // throw new IllegalStateException("Attempt to close an editor that is already closing"); } closeEditor(currentEditor, commit); } } /** * Removes a ValueEditor from display. Any children will be closed as well. * Note: this will not update the top value editors. removeValueEditor(s) () should be called instead if this is desired. * @param editorToClose the editor to close. * @param commit whether to commit the currently entered value. */ private void closeEditor(ValueEditor editorToClose, boolean commit) { if (currentEditorStack.contains(editorToClose)) { // Close child editors first. collapseHierarchy(editorToClose.getParentValueEditor(), commit); // Collapsing to parent will would call close on this editor, unless parent is null. if (editorToClose.getParentValueEditor() != null) { return; } // If this is a structured value editor, the child editor will not have been closed on collapse. // Call close on the child editor, then go on to close this editor. ValueEditor childEditor = getChildEditor(editorToClose); if (childEditor != null) { closeEditor(childEditor, commit); } } // check for editor closing: fixes a bug where changing the type of a parametric value in the scope causes an infinite loop if (editorToClose.isEditorClosing()) { return; } // Indicate that this editor is closing editorToClose.setEditorIsClosing(true); // Commit or cancel the edit. if (commit && editorToClose.isEditable()) { editorToClose.commitValue(); } else { editorToClose.cancelValue(); } // Remove this value editor. Container parentContainer = editorToClose.getParent(); if (parentContainer != null) { parentContainer.remove(editorToClose); } if (parentContainer instanceof JComponent) { ((JComponent)parentContainer).repaint(editorToClose.getBounds()); } else if (parentContainer != null) { parentContainer.validate(); } // handle the editor being closed.. handleEditorClosed(editorToClose); } /** * Update the active editor, current editor, and editor stack when an editor is closed. * @param closedEditor the editor that closed. */ private void handleEditorClosed(ValueEditor closedEditor) { if (closedEditor == currentEditor) { // Update the hierarchy - the current editor and the editor stack. setCurrentEditor(currentEditorStack.isEmpty() ? null : currentEditorStack.pop()); if (currentEditor != null) { currentEditor.refreshDisplay(); } } else if (currentEditorStack.contains(closedEditor)) { throw new IllegalStateException("Child editors must close before their parents."); } else { // An editor was closed that is not in the editor hierarchy. // Do nothing. } } /** * Notify the hierarchy manager that the editor in question has received a "commit" request. * Examples include pressing the <ENTER> key or pressing an "OK" button. * This method will handle closing the editor and committing the value as necessary. * @param editor the editor which received the request. */ void handleCommitGesture(ValueEditor editor) { ValueEditor parentEditor = editor.getParentValueEditor(); ValueEditor editorToCommit = parentEditor instanceof StructuredValueEditor ? parentEditor : editor; if (editorToCommit.getParentValueEditor() != null) { closeEditor(editorToCommit, true); } else { editorToCommit.commitValue(); // top-level editor - don't close notifyCommitCancelHandler(true); } activateCurrentEditor(); } /** * Notify the hierarchy manager that the editor in question has received a "cancel" request. * Examples include pressing the <ESC> key or pressing an "Cancel" button. * This method will handle closing the editor and canceling the value as necessary. * @param editor the editor which received the request. */ void handleCancelGesture(ValueEditor editor) { ValueEditor parentEditor = editor.getParentValueEditor(); ValueEditor editorToCancel = parentEditor instanceof StructuredValueEditor ? parentEditor : editor; if (editorToCancel.getParentValueEditor() != null) { closeEditor(editorToCancel, false); } else { editorToCancel.cancelValue(); // top-level editor - don't close notifyCommitCancelHandler(false); } activateCurrentEditor(); } /** * Notify the hierarchy commit/cancel handler that a hard commit/cancel event has taken place. * @param commit whether commit or cancel happened. true = commit, false = cancel. */ private void notifyCommitCancelHandler(boolean commit) { if (hierarchyCommitCancelHandler != null) { hierarchyCommitCancelHandler.handleHierarchyCommitCancel(commit); } } /** * Returns the ValueEditorManager that this Hierarchy Manager is using * @return ValueEditorManager */ public ValueEditorManager getValueEditorManager() { return valueEditorManager; } }