Showing posts with label Swing. Show all posts
Showing posts with label Swing. Show all posts

Monday, June 9, 2014

A Side-Scrolling JList

I just spent a less-than-enjoyable chunk of time trying to get a JList to scroll (horizontally) in a Java application with a Swing GUI. Everything I found in an online search either made it seem easier than it actually turned out to be (by omitting the key ingredient in the recipe) or sent me off in unproductive directions. So I'm recording what worked because I will, with probability 1.0, forget it soon enough.

The structure of the program, omitting most details, looked like this:

JList wideList = functionThatSpitsUpWideList();
JScrollPane pane = new JScrollPane(wideList);
JOptionPane.showMessageDialog(parent, pane, title, JOptionPane.PLAIN_MESSAGE);

It produced a dialog with a mile-wide list and no scroll bars. I tried what I thought was the obvious remedy, invoking setMaximumSize() (with modest dimensions) first on wideList and then on pane. Neither helped. The answer turned out to be invoking setPreferredSize() on pane:

JList wideList = functionThatSpitsUpWideList();
JScrollPane pane = new JScrollPane(wideList);
pane.setPreferredSize(new Dimension(400, 200));
JOptionPane.showMessageDialog(parent, pane, title, JOptionPane.PLAIN_MESSAGE);

I (naively?) thought that the maximum size trumps the preferred size. Oops.

Tuesday, May 20, 2014

A Java Slider/Text Combo

A few years back I was coding (in Java, of course) the <shudder>GUI</shudder> for a research program. I needed to provide controls that would let a user specify priorities (0-100) scale for various things. Two possibilities occurred to me, with pretty much diametrically opposed strengths and weaknesses.

Sliders have a few virtues.
  • Grabbing and yanking the handle is usually faster than typing.
  • When you have more than one, they provide a sort of quasi-horizontal bar graph of the inputs -- it's easy to tell from their relative positions which inputs are larger than which others.
  • It's immediately obvious if you are the absolute maximum or minimum value for the input.
When your input range is something like {1, 2, 3, 4, 5}, it's pretty easy to look at a slider and see what the exact value is. When your input range is {0, 1, ..., 100}, that's a lot harder, which led me to consider text fields. Text fields have their own virtues.
  • It's easy to be precise, regardless of the legal range of inputs. (Try hitting exactly 58, rather than 57, on a 0-100 slider.)
  • You can immediately see exactly what value is being set.
On the other hand, the sort of approximate ratio information you get just by looking at two sliders requires a bit of mental arithmetic with two text fields. Also, if the range of legal values is not obvious to the user, it can be hard to know whether a particular input is at (or near) the maximum or minimum legal value.

Hoping for the best of both worlds, I went looking for Java Swing control that combined a slider with an editable text entry field, and did not find one I liked (which may say more about how picky I am and how good my search skills are than about their availability). So I rolled my own.

Fast-forward to this month, where I'm working on a program for an entirely different research project and once again wanted a slider/text combo control. I dusted off the old code and decided to pretty it up a (very) little, add some documentation and release it into the wild. The project name is ComboSlider, and you can find its repository on Bitbucket. I released it under the EPL 1.0 open source license. Source code, a compiled jar file and documentation are all in the repository. The jar file includes a small demonstration program.

Here is a screen shot of the demo program:


The ComboSlider control is in the orange rectangle. Pull the slider and the text field updates. Type a legal value in the text field and the slider automatically repositions.

The top two text fields demonstrate how to redefine the domain of the ComboSlider on the fly. The bottom text label updates automatically when you change the input value, demonstrating how to use a change listener to monitor the ComboSlider's value. (In an ordinary input dialog, though, you would not need the change listener; you would just wait for the dialog to complete and then use ComboSlider.getValue() to find out what the input value is.)

There are two limitations I probably should mention: it only accepts integer inputs (positive or negative); and, while you can set the size of the entire ComboSlider using the usual setters for minimum/maximum/preferred size, the separate dimensions of the slider and text field are hard coded in the constructor.

Saturday, April 13, 2013

Reusing Context Menus in Swing (Java)

I just spent hours (hours I can ill afford at my age!) resolving a very frustrating and in my opinion obscure problem with a graphical user interface I'm building using Swing (Java 7). My interface has a bunch of lists scattered around, using a customized class (extending Swing's JList) that includes tool tips for individual items in the list, pop up context menus (JPopupMenu), and tool tips for the menu items in the context menus. In one such list, everything worked: item tool tips displayed, a right click popped up the context menu, and hovering on a context menu displayed its tool tip. Everywhere else, item tool tips displayed and context menus popped up (and their actions correctly fired if I clicked them), but the bleeping menu tool tips were nowhere to be seen.

I'll skip the long and painful history of blind alleys I went down. (Somewhere, some Google employee is desperately trying to cool down a server I overheated during my search.) I ultimately realized that the meaningful distinction between the one list that worked and the many that did not had nothing to do with the lists themselves, nor with their parent containers. The one list that worked properly had a context menu that was only used in that one place. All the other lists popped up one of a few menus that could be invoked from more than one place.

My understanding is that you cannot add the same Swing component to more than one container, but that's not the issue with instances of JPopupMenu. You don't actually add the context menu to more than one component; you just invoke it (set it visible and position it) from more than one place. The fact that the same menu correctly displayed in more than one place, and the menu item actions correctly fired from each place, seems to support that. So I'm baffled why menu item tool tips appear if there is only one location that listens for the pop-up action -- right-click on most (but not all?) systems -- but fail to appear if more than one location listens for the pop-up action. Is this an "undocumented feature" of Swing?

The solution was to change my customized JList class so that, rather than using the original JPopupMenu, it uses a deep copy of the menu. The only thing multiple copies of the menu share is the action method. In case it will help anyone, here is my code for making the clone menu.

  /**
   * Clone a popup menu (deep copy).
   * @param m the menu to clone
   * @return the clone
   */
  private static JPopupMenu cloneMenu(JPopupMenu m) {
    if (m == null) {
      return null;
    }
    JPopupMenu menu = new JPopupMenu();
    for (Component i : m.getComponents()) {
      if (i instanceof JMenuItem) {
        JMenuItem item = new JMenuItem();
        JMenuItem old = (JMenuItem) i;
        item.setText(old.getText());
        item.setToolTipText(old.getToolTipText());
        item.setMnemonic(old.getMnemonic());
        for (ActionListener a : old.getActionListeners()) {
          item.addActionListener(a);
        }
        menu.add(item);
      }
    }
    return menu;
  }

I should note that I only copied the bits I use (text, tool tip, mnemonic and action listener). If you use other bits (accelerators, for instance), you'll need to copy those as well.

There's one other little piece of the puzzle. Since clones of a given menu all use the same action listeners, you need to give the action listeners a way of knowing which clone invoked them. Here's my tweak to a prototype action listener:

private void someMenuItemActionPerformed(java.awt.event.ActionEvent evt) {                                                          
  JPopupMenu m = (JPopupMenu) ((Component) evt.getSource()).getParent();
  // m.getInvoker() is the component on which the context menu was invoked
  // TODO add your handling code here:
} 

In my case, m.getInvoker() will be an instance of my modified JList class.

Saturday, April 6, 2013

Watching Swing Text Fields for Changes

As I mentioned earlier, I'm currently beating my head against a wall (or several walls) writing a graphical user interface (GUI) for a Java program, using Swing. There are certain dialogs in which I want the user to fill in text fields. In some cases the field content should be a string, with no domain restriction. In other cases the content needs to be a positive integer between specified limits. Either way, I want to listen for changes as they happen.

Listening for changes seems to be a fairly common goal, for a variety of reasons. My motivation is that the inputs are optional (the user is specifying properties of a filter), and each text input is matched with a check box indicating whether or not that particular property should be included in the filter. Although my program is a desktop application, I've filled out a number of similar forms on the web, many of which had what I consider to be a desirable feature: as soon as you start typing something valid in the text field, the check box is automatically selected. I want to do that in my dialog.

I spent considerable time searching for solutions, finding more questions than answers, but I did eventually come up with something that works, which I'll share here. Let's start with listening for changes, which proved to be the stickier bit. The advice that I found for JTextField  consistent involved listening for changes to the value property of the field. Unfortunately, I got a bunch of events where value supposedly changed even though nothing had been typed into the field. The field has oldValue and newValue properties, and it seemed intuitive to me to check for newValue != oldValue, but most of the events I saw returned newValue == null. The problem has to do with when changes are "committed". Hitting the Enter key after typing in the field commits the change, but typing itself does not, nor does typing and then changing focus by tabbing or clicking elsewhere.

The first key is to use JFormattedTextField rather than JTextField. That also buys you the ability to validate inputs and force the user to type approved characters. Just switching to JFormattedTextField is not enough, though. The field requires a formatter factory, and the default factories apparently do not automatically commit changes as soon as they are validated. So I ended up creating my own factory methods for generating formatter factories that immediately commit valid changes. Here's my code:

import java.text.NumberFormat;
import javax.swing.text.DefaultFormatter;
import javax.swing.text.DefaultFormatterFactory;
import org.jdesktop.swingx.text.NumberFormatExt;
import org.jdesktop.swingx.text.StrictNumberFormatter;

/**
 * FieldFormatter provides a factory method to provide formatter factories for
 * formatted text fields with input limits. The fields allow blank/null entries,
 * and commit immediately upon valid changes.
 * @author Paul A. Rubin <rubin@msu.edu>
 */
public class FieldFormatter {
  
  /**
   * Factory method to generate a formatter factory for integer inputs.
   * @param digits the maximum number of digits to allow (minimum is 0)
   * @param min the minimum legal value
   * @param max the maximum legal value
   * @return a formatter factory
   */
  public static DefaultFormatterFactory integerFormatter(int digits,
                                                         int min, int max) {
    NumberFormatExt f = new NumberFormatExt(NumberFormat.getIntegerInstance());
    f.setParseIntegerOnly(true);
    f.setMaximumIntegerDigits(3);
    f.setMinimumIntegerDigits(0);
    StrictNumberFormatter fmt = new StrictNumberFormatter(f);
    fmt.setAllowsInvalid(true);
    fmt.setCommitsOnValidEdit(true);
    fmt.setMinimum(min);
    fmt.setMaximum(max);
    return new DefaultFormatterFactory(fmt);
  }
  
  /**
   * Factory method to generate a formatter factory for arbitrary string inputs.
   * @return a formatter factory
   */
  public static DefaultFormatterFactory stringFormatter() {
    DefaultFormatter fmt = new DefaultFormatter();
    fmt.setCommitsOnValidEdit(true);
    fmt.setAllowsInvalid(true);
    return new DefaultFormatterFactory(fmt);
  } 
}

A few notes about the code:
  • For the integer fields, I used the NumberFormatExt and StrictNumberFormatter classes from SwingX in order to implement domain restrictions (integer only, maximum and minimum number of digits, upper and lower domain limits). Since the string fields had no domain restrictions, I did not need any SwingX classes for them.
  • The setCommitsOnValidEdit method is the key to getting notifications as soon as the user types something valid in the field.
  • I want to allow the user to delete an entry in a field and leave it empty. That requires setAllowsInvalid(true); otherwise, if the user selects and deletes the field content and then exits the field, the deleted content is automatically restored, at least for the integer fields. (I'm not sure I need it for the string fields, but better safe than sorry.)
Now all you have to do is attach a property change listener to the JFormattedTextField that looks something like the following:
private void listen(ProperChangeEvent evt) {
  if (evt.getPropertyName().equals("value")) {
    // do something
  }
}

Sunday, March 31, 2013

Auto-collapsing Tree in Java

I'm writing a program in Java using Swing to build the user interface. Programming is not exactly my strong suit, and building graphical user interfaces is pretty much my Kryptonite. So it's no big shock that progress is slow.

One of the controls in my interface is a tree (instance of the JTree class). To keep the interface clean, I want the act of expanding any node to automatically collapse any currently open sibling node (and any open descendants of that sibling). In other words, only one path from the root node to a leaf node should be expanded at any given time.

This seems both simple and something that would be commonplace, so I naively assumed there would be some property setting in JTree to enforce this. Not only did I not find such a property (or method), but I pretty much wore out one of Google's servers looking in vain for any discussion or sample code relating to this. I did not even find any unanswered questions about it online. So either it is not as common a requirement as I thought or my search technique is atrophying.

I eventually found a way that I think works, although it may be a bit inefficient (a hallmark of my coding). Here's a snippet that demonstrates it, applied to an instance mainTree of the JTree class that is defined elsewhere.


    // listen for tree expansion and collapse the previously open path
    mainTree.addTreeWillExpandListener(new TreeWillExpandListener() {
      @Override
      public void treeWillExpand(TreeExpansionEvent event)
                  throws ExpandVetoException {
        TreePath target = event.getPath();  // the path that will expand
        TreePath parent = target.getParentPath();  // parent of the target
        // get the currently expanded descendants of the parent note
        Enumeration<TreePath> expanded = mainTree.getExpandedDescendants(parent);
        // copy the enumeration to a nonvolatile list (collapsing things
        // will alter the enumeration on the fly)
        ArrayList<TreePath> open = new ArrayList<>();
        while (expanded != null && expanded.hasMoreElements()) {
          open.add(expanded.nextElement());
        }
        // no reason to collapse the parent; it will just reexpand when
        // the target expands
        open.remove(parent);        
        // sort the list so that longer paths (nodes deeper in the tree) are 
        // closed first -- this prevents closed nodes from reopening as their
        // descendants are closed
        Collections.sort(open, new Comparator<TreePath>() {
          @Override
          public int compare(TreePath o1, TreePath o2) {
            return -Integer.compare(o1.getPathCount(), o2.getPathCount());
          }
        });
        // now collapse open paths, starting at their lowest levels and
        // working back up the tree toward the common parent
        for (TreePath p : open) {
          mainTree.collapsePath(p);
        }
      };

      @Override
      public void treeWillCollapse(TreeExpansionEvent event) 
                  throws ExpandVetoException {
      }
    });

I'll point out a few key features:
  • I'm attaching a TreeWillExpandListener, which is called after the click that tells Swing a node needs to be expanded but before the expansion actually takes place.
  • The getExpandedDescendants method returns an enumeration of expanded nodes (in the form of TreePath instances). From the Java 7 documentation for this method:
If you expand/collapse nodes while iterating over the returned Enumeration this may not return all the expanded paths, or may return paths that are no longer expanded.
I speak from experience: they're not kidding. In order to collapse everything in the enumeration, I first convert it into a list (ArrayList).
  •  I remove the parent node from the results of the enumeration. It's harmless to collapse the parent, but also pointless: the parent will re-expand when the target child expands.
  •  If you need to collapse a path that descends more than one level from the common parent, you need to do it in reverse order (from the lowest expanded node back toward the parent). Otherwise, as you collapse descendants of some node, Java will re-expand the ancestor. In genealogical terms, if your sister and nephew are currently expanded, and its your turn to expand, the order of collapse has to be nephew first, then sister. Otherwise, if your sister is collapsed first, the act of collapsing the nephew appears to cause Swing to expand the sister. So I sort the list of TreePaths to collapse using an anonymous comparator class that sorts in reverse length order (longest path first -- don't miss the minus sign).
  • The listener listens for both will-expand and will-collapse signals. I left the will-collapse part empty because it's irrelevant to my application.