Default Window Sizes in JFace

The problem with pixels

Default window sizes are a tricky issue. For a long time, developers liked to assume that all people had similar resolutions and screen sizes and simply hardcoded fixed pixel dimensions for all their dialogs. Some of these missteps are still around, like the Windows environment variable dialog that’s been stuck with a fixed (way too small and not resizable) size forever, or the fact that Windows still relies on virtual DPI settings instead of using the measurement provided by the screen. Not to mention the ongoing conflict between fixed-size image design elements and browser settings based font sizes for HTML designs.

Still, I believe it’s the responsibility of any GUI designer (be it for an application, a web site or something else) to make resolution independence a primary feature. To me, this means mostly two things:

  • Never make a dialog a fixed size unless there’s a really, really good reason for not allowing users to resize it.
  • Don’t use pixel measurements for default a minimum sizes if you can avoid it in any way.

Dynamic layouts

SWT and JFace already provide some support for dynamic sizing. Making a window resizable is a simple matter of setting a single shell flag. A shell can have a minimum size and an initial size, which can be persisted to remember the changes a user made to a dialog’s size and reusing these dimensions the next time the same dialog is opened. Layouts like GridLayout and FormLayout make creating resolution independent GUIs comparatively simple, and also allows some basic calculations of the required dimensions for their controls.

In some cases, a call to getShell().pack() can in fact be all that is required to set the size of a simple dialog. Things get complicated when the dialog contains controls with scroll bars, or text input fields with variable inputs. pack() has two different problems here, depending on when it is called:

  • Undersized windows if pack() is called before filling in the data. Text fields and scrollable elements, when empty, have very low minimum sizes, just large enough to show their borders. If these sizes are used to calculate the window size, it will likely be so small that data in these controls will be barely visible.
  • Inconsistently sized or oversized windows if pack() is called after filling in the data. Text fields calculate their preferred size depending on the text they contain. A dialog for entering the name of an element would still be too small if no name has been entered yet, but if the text field is initialized with a 100 character text, the same window’s initial size would be extremely wide. Tables and list boxes have similar issues and can easily case a rather small dialog to be blown up to the full screen height just to display as much of their content as possible.

For reasonably complex dialogs, the best solution seems to me to let the developer choose a reasonable initial size, and perhaps a minimum size as well. But how to make sure that the window is reasonably sized on systems with very low resolutions and a font size of 7pt as well as ultra high resolution displays with corresponding larger fonts (that is, fonts that physically have the same height, but are displayed at a higher resolution and therefore have a greater pixel height)?

Simple: Use character dimensions as the window size units instead of pixels. SWT can provide the height, in pixels, of a font’s glyphs, as well as the average character width. These can be used as factors for the default window size.

The best location to set the default size of a JFace window is the getInitialSize() method. It returns a Point instance containing the desired width and height, in pixels. There’s only one problem: Window#getInitialSize() has a simple default implementation that calculates the size via the current layout and can easily be overridden to provide a custom size. But Dialog‘s implementation of the same method overrides this behaviour so that it first calls the Window implementation to get a default size and then accesses the persisted dialog settings to replace the defaults with the user’s own dimensions, if available. In true Eclipse fashion, this is thrown into a single method, with no option for derived classes to step in and provide a default size without also skipping the dialog settings code.

I’ve therefore implemented a simple dialog class that extends JFace’s Dialog and duplicates its getInitialSize() implementation, extending it with a call to a separate method that derived classes can use to specify a default size. I also had to duplicate a few constants from the Dialog class that refer to keys used in the dialog settings, but are private in Dialog and therefore not available to derived classes.

Implementation

Here’s the final result, placed under the Eclipse license (like the code it duplicates). I’m skipping the package and import stuff as well as the auto generated constructor to save space:

public class DefaultSizeDialog extends Dialog
{
  /** These are copied from Dialog class, where they are private. */
  public static final String DIALOG_FONT_DATA = "DIALOG_FONT_NAME"; //$NON-NLS-1$
  public static final String DIALOG_WIDTH = "DIALOG_WIDTH"; //$NON-NLS-1$
  public static final String DIALOG_HEIGHT = "DIALOG_HEIGHT"; //$NON-NLS-1$

  /**
    * Mostly a copy of the same method in Dialog, but with a call to a separate
    * method for providing a default size that is used if no persisted dialog
    * settings are available.
    * 
    * @see org.eclipse.jface.dialogs.Dialog#getInitialSize()
    */
  @Override
  protected Point getInitialSize()
  {
    Point result = getDefaultSize();

    // Check the dialog settings for a stored size.
    if((getDialogBoundsStrategy() & DIALOG_PERSISTSIZE) != 0)
    {
      IDialogSettings settings = getDialogBoundsSettings();

      if(settings != null)
      {
        // Check that the dialog font matches the font used
        // when the bounds was stored. If the font has changed,
        // we do not honor the stored settings.
        // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=132821
        boolean useStoredBounds = true;
        String previousDialogFontData = settings.get(DIALOG_FONT_DATA);

        // There is a previously stored font, so we will check it.
        // Note that if we haven't stored the font before, then we will
        // use the stored bounds. This allows restoring of dialog bounds
        // that were stored before we started storing the fontdata.
        if(previousDialogFontData != null && previousDialogFontData.length() > 0)
        {
          FontData[] fontDatas = JFaceResources.getDialogFont().getFontData();

          if(fontDatas.length > 0)
          {
            String currentDialogFontData = fontDatas[0].toString();
            useStoredBounds = currentDialogFontData.equalsIgnoreCase(previousDialogFontData);
          }
        }

        if(useStoredBounds)
        {
          try
          {
            // Get the stored width and height.
            int width = settings.getInt(DIALOG_WIDTH);

            if(width != DIALOG_DEFAULT_BOUNDS)
            {
              result.x = width;
            }

            int height = settings.getInt(DIALOG_HEIGHT);

            if(height != DIALOG_DEFAULT_BOUNDS)
            {
              result.y = height;
            }
          }
          catch(NumberFormatException e)
          {
          }
        }
      }
    }

    // No attempt is made to constrain the bounds. The default
    // constraining behavior in Window will be used.
    return result;
  }

  /**
    * Provides the dialog's default size. Duplicates the behaviour of JFace's
    * standard dialog. Subclasses may override.
    * 
    * @return Default size.
    */
  protected Point getDefaultSize()
  {
    return getShell().computeSize(SWT.DEFAULT, SWT.DEFAULT, true);
  }
}
This entry was posted in Eclipse and tagged , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *