|
|
||
Laird Nelson's BlogSwing ArchivesNimbus and OpacityPosted by ljnelson on July 31, 2008 at 10:25 AM | Permalink | Comments (5)This one tripped me up, and I thought I'd post it here. I have a case where I need to pour text into a JTextField, but have that JTextField look like a JLabel. So as I type in one field, I need this "gray" second field to update, live, before the user's eyes. I also need the ability for the user to cut-and-paste values out of it. This sort of thing is reasonably common in properties panels and the like. Now, normally all you have to do is something like this: ...and you have what looks like a JLabel but what behaves like a JTextField.final JTextComponent previewComponent = new JTextField(); previewComponent.setBorder(null); previewComponent.setEditable(false); previewComponent.setOpaque(false); Well, except under Nimbus, where the setOpaque(false) seems to have no effect. Turns out you need to also set the background color to a transparent color as well. Fortunately, this combination of behaviors seems to work under all the other look and feels (looks and feels?): previewComponent.setBorder(null); previewComponent.setEditable(false); previewComponent.setOpaque(false); previewComponent.setBackground(new Color(0, 0, 0, 0)); I hope this helps other Swing folks out. Useful Swing Thing #1Posted by ljnelson on December 17, 2007 at 12:35 PM | Permalink | Comments (5)In Microsoft Outlook, along with probably dozens of other desktop applications, pressing ESC just about anywhere will cause the current window you're in to be cancelled. In the case of dialogs, it is as though the Cancel button were pressed; in the case of root windows, it is as though you tried to exit the application (and you usually get a warning dialog that asks you if you're sure). For one of my applications, I wanted to get this very useful UI gesture installed once, in the right way, preferably without messing with the current look and feel, without requiring tons of custom code, and without subclassing. Here's a narrative of my journey, which also serves as a tutorial of sorts to the UIManager and UIDefaults classes. For the impatient, here's the source code discussed in the article below: https://bricks.dev.java.net/source/browse/*checkout*/bricks/swing/jrootpanes/src/main/java/bricks/swing/jrootpanes/JRootPanes.java. I started by looking into the UIDefaults class, since I hadn't really done much with the UIManager class and its related classes in general. To sum up, perhaps incorrectly, because I'm still not all that comfortable with it, you can think of the UIDefaults class as a hashtable containing all of the various UI attributes and properties that you can think of in the Java platform. (This is wrong because really those values come from the look and feel classes, and they're the ones who installed them, but for the moment forget about where these values came from and focus on how you get access to them.) UIDefaults instances are maintained and shepherded around by the UIManager class, which has lots of useful static methods to work with those UIDefaults instances. So, for example, if you want to see what the font is, by default, for the current look and feel's rendering of a JButton's text, you would say: final Font font = (Font)UIManager.get("Button.font");
This is really a cover for:
final Font font = UIManager.getDefaults().get("Button.font");
...but is usually the idiom you'll see (and it's a whole lot more convenient).
(How do you know what keys and values there are? Well, obviously you could iterate over the results returned by UIManager.getDefaults().keys(), but failing that the only way to see what's in there is to examine the source of, for example, the BasicLookAndFeel class. That's probably a useful thing to look at anyhow.) Picking up where we left off: if you're lucky and your JDK is normal you'll get back a non-null Font object that some LookAndFeel put there for you. That's why when you create a new JButton its font is...well, whatever that object is (usually it's the Dialog font). That is, you didn't have to set the font on the button yourself; the value was defaulted for you by the current look and feel. The UIManager exposes three levels of these defaults: the user defaults, the look and feel defaults, and the system defaults. This happens transparently for callers: if you call UIManager.getDefaults() you get a UIDefaults object back that handles these three layers for you (in much the same way, for example, as java.util.Properties allows there to be a default Properties that is consulted in the case of a missed lookup). If you never monkey with a UIDefaults object, then effectively you, the user, have never put in any user defaults, so anything you get out of that UIDefaults object is going to be either a look and feel default (i.e. the Metal or Ocean guys decided for you that teal or silver was an awesome color) or a system default (i.e. Microsoft's take on what color title bars should be). On the other hand, if you do put something into those defaults, then your value is the one that is returned, no matter what the current look and feel or system thinks the value should be. Why is all this important? Because if you want to do something UI-ish by default—as in, "affecting every future instance of a UI class of some kind"—then you want to start here, because this is where those default values are managed. Now, in this case I wasn't interested in colors or fonts or text or anything like that. I was interested in key bindings: InputMaps and ActionMaps. ActionMaps store Actions indexed by, conventionally, Strings; InputMaps store, conventionally, Strings indexed by KeyStrokes. Well, it turns out that InputMaps are available—like fonts and colors—in the UIDefaults as well, for pretty much every component. This would mean that if I could get my hands on, for example, the default ActionMap and the right kind of InputMap installed on JRootPane instances by default I might have a prayer of setting up a key binding for the ESCAPE key that would attempt to close the current window. For the ActionMap—i.e. the place that stores an Action under a String key—it turns out that all JRootPane instances that have UI delegates that extend from BasicRootPaneUI (which includes pretty much all the JDK-supplied UIs as well as a good number of third-party ones) will look for an ActionMap stored in the UIManager under the key—surprise, surprise—"RootPane.actionMap". That means if you do this: final ActionMap actionMap = (ActionMap)UIManager.get("RootPane.actionMap");
...you'll get back a non-null ActionMap containing all the Actions that JRootPanes will have if and when any is constructed.
InputMaps are a little more complicated. There are three of them for any given JComponent:
Well, we happen to be lucky in that this little sequence of code will give us the proper InputMap to work with: final InputMap inputMap = UIManager.get("RootPane.ancestorInputMap");
(I'll have more to say in a moment about why this isn't quite right—hint: what if there isn't a component in the window that has the focus?—but it's pretty good for now.) So with these tools, we now have a place to put an Action that will close the parent Window: public static final void installCloseAction() {
assert EventQueue.isDispatchThread();
ActionMap actionMap = (ActionMap)UIManager.get("RootPane.actionMap");
if (actionMap == null) {
actionMap = new ActionMap();
UIManager.put("RootPane.actionMap", actionMap);
}
assert actionMap != null;
if (actionMap.get("close") == null) {
actionMap.put("close", new AbstractAction("Close") {
@Override public final void actionPerformed(final ActionEvent event) {
assert event != null;
final JRootPane rootPane = (JRootPane)event.getSource();
assert rootPane != null;
final Container container = SwingUtilities.getAncestorOfClass(RootPaneContainer.class, rootPane);
if (container instanceof JInternalFrame) {
final JInternalFrame internalFrame = (JInternalFrame)container;
if (internalFrame.isClosable()) {
internalFrame.doDefaultCloseAction();
}
} else {
final Window window;
if (container instanceof Window) {
window = (Window)container;
} else {
window = SwingUtilities.getWindowAncestor(rootPane);
}
if (window != null) {
window.dispatchEvent(new WindowEvent(window, WindowEvent.WINDOW_CLOSING));
}
}
}
});
}
}
Assuming this method is present in a class called JRootPanes, we can test this by using the following snippet of test code (this uses some JUnit assertions to help us out, and for brevity I've omitted all the cruft to get this running on the event dispatch thread): JRootPanes.installCloseAction();
final JRootPane rootPane = new JRootPane();
final ActionMap actionMap = rootPane.getActionMap();
assertNotNull(actionMap);
final Action closeAction = actionMap.get("close");
assertNotNull(closeAction);
Neat! OK, so we have managed to affect the ActionMap prototype for all future JRootPane instances, regardless of look and feel. How are we going to trigger our "close" Action? One way to attempt this might be to do something like the following: final KeyStroke escape = KeyStroke.getKeyStroke("ESCAPE");
assert escape != null;
final InputMap inputMap = (InputMap)UIManager.get("RootPane.ancestorInputMap");
if (inputMap != null && inputMap.get(escape) == null) {
inputMap.put(escape, "close");
}
That would non-intrusively affect the bindings stored in all JRootPane instances' InputMaps that are consulted when the JRootPane in question is the ancestor of a focused component. But what about when the JRootPane is the ancestor of a bunch of components, none of which has the focus?
Here's where things get tricky and annoying. It turns out that there is no InputMap entry in the UIDefaults for JRootPanes for the WHEN_IN_FOCUSED_WINDOW case. This makes a certain amount of sense: InputMaps installed in such cases actually have to be associated with their component (they must be instances of ComponentInputMap, actually) and so at "defaults time" you don't have a component, so it follows that you can't have a ComponentInputMap, and so it follows that you can't install one as the WHEN_IN_FOCUSED_WINDOW InputMap. But wait a minute. Doesn't ESCAPE close the window when you pop up a JOptionPane? Why, yes, it sure does. Hmmm. I took a spin further down in the mammoth BasicLookAndFeel class and discovered a weird little entry for JOptionPane defaults: // lines 1192-3 of BasicLookAndFeel.java follow:
"OptionPane.windowBindings", new Object[] {
"ESCAPE", "close" }, Without (further) boring you to tears I discovered that there is an accepted idiom for building up the raw materials for WHEN_IN_FOCUSED_WINDOW InputMaps (for BasicLookAndFeel-descending look and feel implementations). Sure enough, if you trace the code back, you'll discover that the BasicOptionPaneUI UI delegate (the superclass of most JOptionPane UI delegates out there) grabs the key binding raw materials in here ("ESCAPE", "close") and converts them into a WHEN_IN_FOCUSED_WINDOW InputMap. And—oops, BasicRootPaneUI doesn't do this, which means that BasicRootPaneUI doesn't ever consult anything in the UIManager/UIDefaults machinery to make this happen or to provide an injection point.
(If BasicRootPaneUI were changed, it would do something like this in its implementation of createInputMap(int, JComponent):
Being the stubborn sort, I turned to the other giant cudgel thoughtfully given to us by the AWT guys—Toolkit.addAWTListener(). The only place to "inject" an InputMap of the proper variety—since without subclassing we can't really do it at JRootPane construction time—is when a JRootPane is made displayable. The code below monitors HierarchyEvents for those containing JRootPanes that have just been made displayable, and, when it finds such a JRootPane it tries to non-intrusively modify or install the WHEN_IN_FOCUSED_WINDOW InputMap: private static final KeyStroke escape = KeyStroke.getKeyStroke("ESCAPE");
private static final AWTEventListener awtListener = new AWTEventListener() {
@Override public final void eventDispatched(final AWTEvent event) {
assert EventQueue.isDispatchThread();
assert event != null;
if (event instanceof HierarchyEvent && event.getID() == HierarchyEvent.HIERARCHY_CHANGED) {
// This event represents a change in the containment hierarcy.
// Now let's figure out what kind.
final HierarchyEvent hevent = (HierarchyEvent)event;
final Component changed = hevent.getChanged();
if (changed instanceof JRootPane && ((hevent.getChangeFlags() & HierarchyEvent.DISPLAYABILITY_CHANGED) != 0) && changed.isDisplayable()) {
// Aha! A JRootPane has just been made displayable!
final InputMap inputMap = ((JRootPane)changed).getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
if (inputMap != null && inputMap.get(escape) == null) {
inputMap.put(escape, "close");
}
}
}
}
};
public static final void makeEscapeCloseAllWindows() {
assert EventQueue.isDispatchThread();
final Toolkit toolkit = Toolkit.getDefaultToolkit();
assert toolkit != null;
toolkit.removeAWTEventListener(awtListener);
installCloseAction(); // from before; see earlier in this article
toolkit.addAWTEventListener(awtListener, AWTEvent.HIERARCHY_EVENT_MASK);
}
Sure enough, if you invoke this code before you create any JDialogs or JFrames then silently all such windows will be closeable with the ESCAPE key. Thanks for reading. Objects and Strings and the Wrangling Thereof (Part 2)Posted by ljnelson on August 31, 2007 at 08:07 AM | Permalink | Comments (0)In the prior entry, we learned that Java ships with several tools to standardize the conversion between Strings and Objects. We covered the text conversion methods of java.beans.PropertyEditor. We will most definitely come back to PropertyEditors, because they're about a heck of a lot more than just converting text, but let's take a detour into the dank recesses of the java.text package. Do you smell that? That's all the dust left over from when this package was deposited by IBM and Taligent into the official Java runtime platform 'round about 1997. java.text was put in, as I understand it, primarily for internationalization (I18N) purposes, and one of the bits that landed there was the java.text.Format class, which, although described as a way to format Locale-specific information, has nothing whatsoever to do with Locales or internationalization at all. Go figure. It does, however, have lots to do with converting Objects into Strings and vice versa. FormatThe Format class is responsible for formatting Objects into Strings, and taking Strings and parsing them back into Objects. Unlike PropertyEditor, whose API seems to have more plumbing and rebar and wiring than my house, a Format is a relatively simple thing. Want to parse a String into an Object? Call the stateless parseObject(String) method. Want to do the reverse? Call the format(Object) method. Easy and simple. And stateless. java.text.Format also supports lots of substring parsing and formatting, via the ParsePosition and FieldPosition classes. In general, I've found the use cases for these to be pretty thin, except where Dates are concerned, and for that someone has already written the SimpleDateFormat class, so we'll pretty much deal with them only where necessary. Let's pick up on our previous example and write a URIFormat class: This method is relatively simple to implement properly and fully defensively. Here are the details (once again, numbers below correspond to numbers in the code comments above):
The parseObject method is basically just the reverse, but I'll have a little more to say about it in comparison to the setAsText() method of PropertyEditor. It also has some of the weirdest error handling conventions I've ever seen. Basically, if there is an error, you return null, and use the incoming ParsePosition object to store where the error occurred during parsing. And in the success case, you are obligated to update the supplied parsePosition's regular index property to just past where you finished parsing. So what if you have to return null, so you can, for example, capture and parse user input that should be converted to the null reference? It would appear, although the contract is not at all specific, that you return null, but make sure that the ParsePosition's errorIndex property is set to -1 to indicate that there is no error. Additionally, there's some intent we can read from the source code. Diving into the Format source code, we see that the convenience method, parseObject(String), actually evaluates the return value of this method by more or less ignoring the contract entirely! It checks to see if this method has modified the parsePosition's index property. If it hasn't, then, it would appear, an error has occurred (since hey, parsing couldn't even take place), and, it turns out, regardless of this method's return value, an exception is thrown. So much for contracts, at least where Taligent OK, then; here's how we do it:
But. They can be quite useful for bridging gaps between PropertyEditors and JFormattedTextFields, as we'll see in the next entry. The gist is that we'll create a Format that delegates to a PropertyEditor implementation. From there it's a drop-in to get your PropertyEditor to be used by JFormattedTextFields. Thanks for reading. Powered by ScribeFire. Objects and Strings and the Wrangling ThereofPosted by ljnelson on August 27, 2007 at 10:49 AM | Permalink | Comments (2)See if this little scenario sounds familiar. You're rolling along on some application somewhere, and you've decided to put some information in a Properties file somewhere. You realize that you're beginning to encode a lot of information in a property setting, so much so that you realize that really what you're doing is building up a rather complicated Object. You feel like you've done this before.Or this one: You're working on a Swing application, and you need to validate the input from the user. Great, you say, I'll use a JFormattedTextField, and then I'll...I'll...I'll...read the...documentation...which features lots of...hmm...factories...and AbstractFormatters...and still more formats and navigation thingees and...I think I'll go get some coffee.Or this one: You're halfway through developing a complicated and enterprisey validation framework and you stop abruptly, realizing that there has to be a better way!Starting with this blog entry I'd like to cover the many, many different ways to edit, format and build up different kinds of Objects that are provided by the Java platform. Why should you think about turning Objects into Strings? Or Strings into Objects? Or all the other ways that a user might send input to you? For me, the answer is that whether you're developing on the desktop or on the Web, you are constantly accepting free-form user input in the form of text. In some cases, you have control over this text, and in other cases you do not, but in all cases you often need to turn that text into things like dates, colors, fonts, java.net.URIs, custom domain objects and the like. Wouldn't it be nice to come up with a standard set of tools that would manage this kind of conversion for you in a pluggable manner? Wouldn't it be even better if most of that heavy lifting were done for you by the base platform? Well, it is. In this entry, I'll cover java.beans.PropertyEditor—at least I'll cover some of its features—and then will move onto other tools in subsequent entries. PropertyEditorsThe java.beans.PropertyEditor class is quite a powerful beast. Despite the fact that it's heavily used by Spring, Geronimo, JBoss and doubtless a whole host of other reasonably popular open source products, it seems to get lost in the shuffle, and developers often don't know it exists.A PropertyEditor exists to edit Java bean properties. Or at least that's what the documentation will tell you. But explaining it this way often leads to questions about what a Java bean is, what a property is, oh, doesn't that mean getters and setters and whatnot—all of which is largely irrelevant for understanding what a PropertyEditor actually does. So, then: a PropertyEditor provides a consistent way for accepting user-supplied data and turning it into an object of a particular class. For now, we'll look at just its text conversion utilities and leave its boatload of other features aside. Before we dive into the API, we should think about what we're trying to do. So for this example, let's say that we're going to make a PropertyEditor that converts java.net.URI instances into Strings and vice versa. That is a trivial enough example that it should be easy to step through, and a useful enough one that you may find yourself using the resulting PropertyEditor in an actual project. To start developing a new PropertyEditor, it's most helpful to simply subclass java.beans.PropertyEditorSupport, so that's what we'll do here. For reasons that will become clear later (hint: see the API documentation for java.beans.PropertyEditorManager), you should suffix your property editors with the word Editor. Finally, the API documentation also says that all PropertyEditors must have a public, no-arg constructor: Next, let's focus on the text conversion methods:Actually, before we get there, an overriding thing that is worth remembering is that a PropertyEditor is a stateful object. Granted, if all is going as it should, the state doesn't stick around very long, and technically speaking doesn't even get released back into the wild, but it's there nonetheless. That means the typical flow of using a PropertyEditor for conversion purposes is:public String getAsText();
What's going on here? Why is this so complicated? Let's walk the interesting bits of the code. The numbers in the list below should correspond to the numbers in the code above.
And here are the details:
CallersFor reasons that I'll cover in the next installment, callers will expect to call this PropertyEditor in a way that looks something like this:final PropertyEditor pe = // Get an appropriate property editor Observations and SummarySo, then, for Object-to-String conversion using PropertyEditors, keep the following things in mind:
In the next article, I'll cover java.text.Format, and will put togther a Format implementation that delegates to an underlying PropertyEditor. See you then. Powered by ScribeFire. Of Detachable Root Panes and Desktop HoppingPosted by ljnelson on June 20, 2005 at 04:06 PM | Permalink | Comments (5)Long, long, long time no post. A job change and a two-year-old will do that to you. On today's menu: how to make a JRootPane subclass that can pop itself in and out of JInternalFrames and JFrames. Let's dive right in. BackgroundDifferent people have different ideas on how an application should work when it can open many different documents of many different types at the same time, and, most importantly, when its value derives from being able to see those documents side-by-side (ruling out, for example, a JTabbedPane-based interface). Some like a true, old-school MDI feel; others like there to be multiple external windows, à la Microsoft Word. My current project's customers are evenly divided on the subject. To make everyone happy, I decided to start with a JDesktopPane-based MDI core. Then I added the ability to detach the JInternalFrames from the desktop pane and open them up as external windows—i.e. to make the window hop off the application desktop and onto the user desktop (and, potentially, back again). This comparatively small insight has led to a surprisingly intuitive way of managing the application's information. When you just want to look at one thing and have it be your central focus, it is easier to have it be in an external window. On the other hand, when you want to look at two things side by side, it seems easier to grasp their relationship to each other and to their containing application if they're presented inside an MDI. The CodeThe final code is available here under the MIT license; my ramblings below detail how I designed it. The Gory DetailsSince both a JInternalFrame and a JFrame are RootPaneContainers, I focused on the JRootPane class. If there were some way to pop instances of this class out of one kind of window and into another kind—preferably via the usual ActionMap machinery—I'd be in business. If you look at the APIs for JFrame and JInternalFrame, you can see that there is not a publicly accessible way to do this. In general, this is a good thing—how many times (other than this one!) do you ever even think about the root pane let alone want to remove it or set a new one? While there is a public method for retrieving the root pane on both the JFrame and JInternalFrame classes, there is no such public setter method. So I knew that overriding JRootPane would be in order. I called my JRootPane subclass DetachableRootPane. Implementing the detach() methodNext, I focused on what would become this subclass' detach() method. What would the state of this root pane be at the moment of detaching? What aspects of that state would be worth preserving, and what aspects would be worth throwing away? This turned out to be a bit of a thorny problem. If a JInternalFrame is maximized, and then detached, should the resulting JFrame be maximized? Or should it be the same size on screen as the maximized JInternalFrame it "came from"? If the JInternalFrame were not resizable, then should the resulting JFrame also not be resizable? What about minimized JInternalFrames? Should they be detachable at all? Anyhow, at this stage in design I didn't focus too much on the answers to these questions, but more on the fact that moving a DetachableRootPane from a JInternalFrame to a JFrame and back again would require certain elements of state to be carried through the detaching process. The easiest thing to do, I figured, was to store this state using the normal client property mechanics. Pass 1: Get the Basics WorkingSo, the detach() method began taking shape. Here's more or less what it looked like at this stage in the design, where I was focusing on the going-from-a-JInternalFrame-to-a-JFrame detaching process. Note that the code below is intended to represent where I was in the design process at the time; as such, it may not be correct or complete: public void detach() {
final Component parent = this.getParent();
if (parent instanceof JInternalFrame) {
// Leaving JInternalFrame; going into JFrame
final JInternalFrame internalFrame = (JInternalFrame)parent;
// Grab the JDesktopPane and stash it away because if we "go back" we need
// to tell it to add() whatever JInternalFrame we "go back" to. Note that if
// abused, this could be the source of a memory leak. There's probably
// a way to make this more robust. In short, don't abuse it.
final JDesktopPane pane = internalFrame.getDesktopPane();
if (pane != null) {
pane.remove(internalFrame);
this.putClientProperty("desktop", pane);
}
// Store some state about the JInternalFrame we're "leaving" so that if we
// "come back" we can set up the new JInternalFrame just like the old one.
// Resizability is tricky and isn't yet handled; more on this later.
this.putClientProperty(INTERNAL_FRAME_CLOSABLE, Boolean.valueOf(internalFrame.isClosable()));
this.putClientProperty(INTERNAL_FRAME_MAXIMIZABLE, Boolean.valueOf(internalFrame.isMaximizable()));
this.putClientProperty(INTERNAL_FRAME_ICONIFIABLE, Boolean.valueOf(internalFrame.isIconifiable()));
internalFrame.setVisible(false);
// Create the new JFrame we're going to detach "into".
final JFrame frame = new JFrame(internalFrame.getTitle()) {
protected final void frameInit() {
super.frameInit();
this.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
this.addWindowListener(new WindowAdapter() {
public final void windowClosing(final WindowEvent event) {
// Make it so that closing the external window causes us to "go
// back into" a JInternalFrame.
detach();
}
});
}
protected final JRootPane createRootPane() {
return DetachableRootPane.this;
}
};
// More on this later; we actually want to set bounds explicitly, but at
// this stage in the design we'll punt.
frame.pack();
frame.setVisible(true);
} else if (parent instanceof JFrame) {
// Leaving JFrame; going into JInternalFrame
}
}
At this stage, I had a DetachableRootPane that was capable of popping out of a JInternalFrame into a JFrame if its detach() method were ever called. The details to be worked out were:
Pass 2: Make Detaching Work Both WaysThe first thing I did was to make it so that the DetachableRootPane could go the other way. Here is the section of code from the detach() method that dealt with this issue. Again this is supposed to illustrate the design process more than it is supposed to be compilable, correct code: } else if (parent instanceof JFrame) {
// Leaving JFrame; going into JInternalFrame
final JFrame frame = (JFrame)parent;
// See if someone (usually us) has put a JDesktopPane into our client
// property map. If so, then we have the required parent component to add a
// new JInternalFrame to. If not, well, then we're up a creek, but handle
// that gracefully too.
final JDesktopPane pane = (JDesktopPane)this.getClientProperty("desktop");
final String title = frame.getTitle();
frame.dispose();
if (pane != null) {
// There is indeed a JDesktopPane to add a new JInternalFrame to, so do
// it.
final JInternalFrame internalFrame =
new JInternalFrame(title,
true, // punt on resizability for now
((Boolean)this.getClientProperty(INTERNAL_FRAME_CLOSABLE)).booleanValue(),
((Boolean)this.getClientProperty(INTERNAL_FRAME_MAXIMIZABLE)).booleanValue(),
((Boolean)this.getClientProperty(INTERNAL_FRAME_ICONIFIABLE)).booleanValue()) {
protected final JRootPane createRootPane() {
return DetachableRootPane.this;
}
};
pane.add(internalFrame);
// TODO: we'll worry about bounds in our next pass
internalFrame.pack();
internalFrame.setVisible(true);
} // end of if block
} // end of method
Brief Interlude: Pull In the ActionMapAt this stage in the process, I had a DetachableRootPane that could quite comfortably spawn JFrames and JInternalFrames and hop in and out of them at will, provided that the detach() method could actually be invoked. I also had a test rig that was simply calling the detach() method on a timer. This was proving to be really annoying now that the basic functionality was there. So instead of diving into cleaning up the details, I decided to work on the ActionMap- and InputMap-related code. So, in the constructor for DetachableRootPane I did this: public DetachableRootPane() {
super();
final ActionMap actionMap = this.getActionMap();
assert actionMap != null;
actionMap.put("detach", new AbstractAction("Detach") {
public final void actionPerformed(final ActionEvent event) {
detach();
}
});
final InputMap inputMap = this.getInputMap();
assert inputMap != null;
inputMap.put(KeyStroke.getKeyStroke("F2"), "detach");
}
Now when I pressed F2 on either a JInternalFrame or a JFrame (provided, of course, its root pane was an instance of DetachableRootPane), the frame would detach appropriately and move from one desktop to the other. Pass 3: Get the Bounds WorkingFinally it was time to go back and worry about the bounds. Going from a JInternalFrame in a JDesktopPane to the screen was easy enough. All I had to do was call the getLocationOnScreen() method on the "outgoing" JInternalFrame and set the bounds of the new JFrame accordingly. That part of the detach method now looked (in part) like this:
// internalFrame is the JInternalFrame that this DetachableRootPane is
// "leaving". Grab its *screen* location and set the bounds of the new
// JFrame to those bounds. The effect is of a seamless detaching from the
// JDesktopPane.
final Point point = internalFrame.getLocationOnScreen();
assert point != null;
bounds.x = point.x;
bounds.y = point.y;
// ...other code; see above in this weblog post...
frame.setBounds(bounds);
Going the other way was more complicated and required me to wrap my head around some of the more arcane-looking methods in SwingUtilities. What I wanted to have happen was if someone closed the JFrame I wanted its new JInternalFrame to appear in the JDesktopPane at exactly the location "underneath" the JFrame that had just been closed. After mucking around, I uncovered the convertPointFromScreen() method. The general approach, then, was to grab the screen location of the "outgoing" JFrame and convert it to the JDesktopPane's coordinate space. That part of the detach() method looked like this:
// ...other code...
final Rectangle bounds = frame.getBounds();
frame.dispose();
if (pane != null) {
final JInternalFrame internalFrame =
new JInternalFrame(title,
true, // punt on resizability for now
((Boolean)this.getClientProperty(INTERNAL_FRAME_CLOSABLE)).booleanValue(),
((Boolean)this.getClientProperty(INTERNAL_FRAME_MAXIMIZABLE)).booleanValue(),
((Boolean)this.getClientProperty(INTERNAL_FRAME_ICONIFIABLE)).booleanValue()) {
protected final JRootPane createRootPane() {
return DetachableRootPane.this;
}
};
pane.add(internalFrame);
final Point point = new Point(bounds.x, bounds.y);
SwingUtilities.convertPointFromScreen(point, pane);
bounds.x = point.x;
bounds.y = point.y;
internalFrame.setBounds(bounds);
internalFrame.setVisible(true);
Pass 4: Cleaning Up and Putting It All TogetherThis looked pretty good. F2 on a JInternalFrame caused the frame to pop off onto the user's desktop as a JFrame at exactly its previous location on screen. F2 again caused that same JFrame to sink down onto the JDesktopPane wherever it happened to be. The last bit of work to do involved the resizability of the original JInternalFrame. It turns out that when a JInternalFrame is maximized, it effectively sets its resizable property to false. This is a bug, because of course there are two semantic properties at work here: the current resizability of the JInternalFrame, and the structural resizability of the JInternalFrame. It is true that when a frame of any kind is maximized, typically you can't resize it, but that says nothing about whether it's resizable once it has returned to its normal bounds. To put it in terms of this project, I wanted to monitor the structural resizability of the JInternalFrame and monitor it for any change; that way I could store that property's value accurately in my DetachableRootPane for use later—e.g. if the DetachableRootPane detaches into a JFrame and back, when it comes back we want to make sure that the JInternalFrame it comes back into is indistinguishable from the JInternalFrame it left. This, fortunately, sounds like a simple job for a PropertyChangeListener. The extra wrinkle is that we want to manage the PropertyChangeListener in the DetachableRootPane, but install it on the DetachableRootPane's parent. Because the parent has to be present for this to work, we can't do the installation work in the DetachableRootPane constructor; we have to do it in the addNotify() method instead. This means we have to be careful to also remove it in the removeNotify() method:
private boolean parentIsResizable;
private PropertyChangeListener pcl;
public void addNotify() {
super.addNotify();
final Container parent = this.getParent();
boolean addPropertyChangeListener = true;
if (parent instanceof JInternalFrame) {
final JInternalFrame parentFrame = (JInternalFrame)parent;
this.parentIsResizable = parentFrame.isResizable();
} else if (parent instanceof JFrame) {
final JFrame parentFrame = (JFrame)parent;
this.parentIsResizable = parentFrame.isResizable();
} else {
addPropertyChangeListener = false;
}
if (addPropertyChangeListener) {
this.pcl = new PropertyChangeListener() {
public final void propertyChange(final PropertyChangeEvent event) {
if (event != null && "resizable".equals(event.getPropertyName())) {
final Boolean value = (Boolean)event.getNewValue();
parentIsResizable = value != null && value.booleanValue();
}
}
};
parent.addPropertyChangeListener("resizable", this.pcl);
}
}
public void removeNotify() {
if (this.pcl != null) {
final Container parent = this.getParent();
assert parent != null;
parent.removePropertyChangeListener("resizable", this.pcl);
}
super.removeNotify();
}
Then I went back and amended the parts of my detach() method that "punted" on resizability to take into account the real value of the property:
if (parent instanceof JInternalFrame) {
// ...other code...
this.putClientProperty(INTERNAL_FRAME_RESIZABLE, new Boolean(this.parentIsResizable));
// ...other code...
} else if (parent instanceof JFrame) {
// ...other code...
final JInternalFrame internalFrame =
new JInternalFrame(title,
((Boolean)this.getClientProperty(INTERNAL_FRAME_RESIZABLE)).booleanValue(),
//...
}
Finally, as my last bit of cleanup, I added a few static methods for creating both JFrames and JInternalFrames with their root panes set to an instance of DetachableRootPane. Here's one example:
public static JInternalFrame createJInternalFrame(final String title, final boolean resizable, final boolean closable, final boolean maximizable, final boolean iconifiable) {
return new JInternalFrame(title, resizable, closable, maximizable, iconifiable) {
protected final JRootPane createRootPane() {
return new DetachableRootPane();
}
};
}
The WrapupThis was a fun exercise, largely because it was well-bounded, solved a real UI problem, and is really quite reusable in the large. My respect for the Swing architects who carefully allowed me to get this much rope has just gone up another notch. Thanks for reading, and I'll see you at JavaOne. | ||
|
|