Slide Notes Out from the Taskbar





Slide Notes Out from the Taskbar

Pop up a note above the taskbar when your application wants attention.

On Windows, long-running applications sometimes will slide in a window above the taskbar to call attention to themselves when an interesting event occurs, such as a finished download or an IM buddy's appearance.

If you want to do this in Java, you need to deal with a pretty significant problem: neither AWTnor Swing has any concept of the taskbar (where it is, how big it is, whether it's auto-hiding, or anything else). As a result, you don't know where to draw the window, and just taking a guess or hardcoding something is hazardoustoo high and the window floats inexplicably on the desktop, too low and it gets buried under the taskbar.

Furthermore, how is this going to work on other operating systems? On the Mac, the proper way to get attention is to bounce your application's dock icon. Since there's no API exposing that functionality, can you at least use a Windows-like slide-in window above the dock? Sure…if you can figure out how tall the dock is (it's user configurable), or whether the dock is even on the bottom of the screen (it might be on the right or left, too).

Fortunately, it is possible to figure out what unobstructed space is available to you on the main display. After that, it's just a matter of offscreen imaging and animation.

Figure Out Where You Are

The key to figuring out your available space is to get the local GraphicsEnvironment, which describes the display, and then call getMaximumWindowBounds( ). This method, introduced in Java 1.4, returns a Rectangle representing the largest centered Window that could fit on the display, accounting for objects that intrude on the display's usable space, like the Windows taskbar or the Mac's monolithic menu bar.

This means that on Windows, the Rectangle will have an upper-left corner at 0,0; on the Mac, it will be at 0,22, which leaves space for the Mac's menu bar. Meanwhile, the height of the Rectangle won't be the height of your display unless you have your taskbar set to auto-hide, or you have moved it to the right or left.

So, now you have the beginnings of the slide-in above-taskbar window. By subtracting the height of the window from the y-coordinate of the last usable row, you can place the window directly above the taskbar or dock. To do the slide-in, you'll need to do an animation loop in which progressively larger portions of the complete window are blitted into a smaller onscreen version. This is very similar to the animated sheet [Hack #45] you've already seen. It's so similar, in fact, that you can reuse the inner class from that hack to do the progressive redrawing.

A simple implementation, SlideInNotification, is shown in Figure.

Sliding in a window immediately above the taskbar or dock
	import javax.swing.*; 
	import java.awt.*; 
	import java.awt.event.*; 
	import java.awt.image.*;

	public class SlideInNotification extends Object {

		protected static final int ANIMATION_TIME = 500;
		protected static final float ANIMATION_TIME_F =
			(float) ANIMATION_TIME;
		protected static final int ANIMATION_DELAY = 50;

		JWindow window;
		JComponent contents;
		AnimatingSheet animatingSheet;
		Rectangle desktopBounds;
		Dimension tempWindowSize;
		Timer animationTimer;
		int showX, startY;
		long animationStart;

		public SlideInNotification ( ) {
			initDesktopBounds( );
		}

		public SlideInNotification (JComponent contents) {
			this( );
			setContents (contents);
		}

        protected void initDesktopBounds( ) {
            GraphicsEnvironment env =
                GraphicsEnvironment.getLocalGraphicsEnvironment( );
               desktopBounds = env.getMaximumWindowBounds( ); 
            System.out.println ("max window bounds = " + desktopBounds);
        }

        public void setContents (JComponent contents) {
            this.contents = contents;
            JWindow tempWindow = new JWindow( );
            tempWindow.getContentPane( ).add (contents);
            tempWindow.pack( );
            tempWindowSize = tempWindow.getSize( );
            tempWindow.getContentPane( ).removeAll( );
            window = new JWindow( );
            animatingSheet = new AnimatingSheet ( );
            animatingSheet.setSource (contents);
            window.getContentPane( ).add (animatingSheet);
        }

        public void showAt (int x) {
            // create a window with an animating sheet
            // copy over its contents from the temp window
            // animate it
            // when done, remove animating sheet and add real contents

            showX = x;
            startY = desktopBounds.y + desktopBounds.height;

            ActionListener animationLogic = new ActionListener( ) { 
                    public void actionPerformed(ActionEvent e) { 
                        long elapsed =                        
               System.currentTimeMillis( ) - animationStart;
                        if (elapsed > ANIMATION_TIME) { 
                            // put real contents in window and show
                              window.getContentPane( ).removeAll( );
           window.getContentPane( ).add (contents);
          window.pack( ); window.setLocation (showX,
               startY - window.getSize( ).height); 
                            window.setVisible(true);
window.repaint( );
animationTimer.stop( ); animationTimer = null;
                       } else { 
            // calculate % done 
             float progress =
             (float) elapsed / ANIMATION_TIME_F; 
              // get height to show int animatingHeight =
           nt) (progress * tempWindowSize.getHeight( ));

           
      animatingHeight = Math.max (animatingHeight, 1;
)
      animatingSheet.setAnimatingHeight (animatingHeight);
                            window.pack( );
                            window.setLocation (showX,
            startY - window.getHeight( ));
                            window.setVisible(true);
                            window.repaint( );
                        }
                    }
            };
     animationTimer =
         new Timer (ANIMATION_DELAY, animationLogic);
     animationStart = System.currentTimeMillis( );
     animationTimer.start( );
  }

  // AnimatingSheet inner class listed below

}

After setting the constants for the speed of the animation and the frame rate (i.e., how frequently to call for repaints), the constructors are used to determine the usable screen space (given the GraphicsEnvironment strategy just detailed) and optionally to set the contents of the slide-in window.

setContents( ) is a little tricky because you need to figure out the size of the contents (a JComponent) before you can start the animation loop that draws fragments of them. You can do this by putting them into a temporary window and packing it, which forces all of its contents to be validated and made displayable. Next, you create the real Window to show on screen, but instead of adding the contents, you add an AnimatingSheetthe inner class that shows progressively larger parts of the contents as the animation runs. When the animation is finished, the AnimatingSheet will be removed and the real contents added.

To slide in the window, a caller invokes the showAt( ) method, passing in an arbitrary x-coordinate. Nothing in Java tells you what is showing on the taskbar or dock, so there's no way, short of going native to get your slide-in window to appear above a specific taskbar/dock icon. The showAt( ) method is where you need to figure out the y-coordinate where the animation will begin, namely the last usable row. Given the Rectangle that represented the largest possible onscreen window, you add its y-coordinate to its height. The y-coordinate accounts for top-of-screen obstructions like the Mac menu bar, and the height counts all the space from there to the taskbar or dock, if any.

showAt( ) contains a large, anonymous inner class ActionListener that performs the animation logic that will be called back by a javax.swing.Timer.

As with most animations, the first thing you do is to figure out how much time has elapsed in the animation. If the animation is finished, you take out the AnimatingSheet, insert the real contents, pack the Window and reset its location to its final visible location, show it, and shut down the Timer.

If the animation time has not fully elapsed, you calculate how far into the animation you are, as a percentage (a float between 0.0 and 1.0), and from that you get how many vertical pixels you want to show on this pass. Send this value to AnimatingSheet's setAnimatingHeight( ) method, pack( ) the window (which picks up the preferred height you just set), set the location to the starting y-coordinate minus the window's new height, and repaint.

The AnimatingSheet inner class was already described in the previous hack, but to recap, it represents some vertically cropped fragment of a source component. When you set the source, it creates an offscreen Image of the component's pixels. Then, when you call setAnimatingHeight( ) from the animation loop, it resets its preferred, minimum, and maximum size to use that height (this is why it can be packed by the Window during the animation). Then, when paint( ) is called, it uses BufferedImage.getSubimage( ) to get a portion of the offscreen image that it can blit into the Graphics with a typical double-buffer-like drawImage( ) call.

This version, shown in Figure, has two differences from the earlier version of AnimatingSheet:

  • Because this window scrolls in the opposite direction of the drop-down sheets, the getSubimage( ) call gets the top-most n pixels instead of the bottom-most pixels.

  • On Windows, I found the offscreen buffer was black unless explicitly cleared out first. This wasn't a problem on Mac OS X.

The AnimatingSheet inner class is used in creating notifications that slide in and out
      class AnimatingSheet extends JPanel {
          Dimension animatingSize = new Dimension (0, 1);
          JComponent source;
          BufferedImage offscreenImage;
          public AnimatingSheet ( ) {
              super( );
              setOpaque(true);
          }
          public void setSource (JComponent source) {
              this.source = source;
              animatingSize.width = source.getWidth( );
              makeOffscreenImage(source);
          }
   
          public void setAnimatingHeight (int height) {
              animatingSize.height = height;
              setSize (animatingSize);
          }
          private void makeOffscreenImage(JComponent source) {
              GraphicsEnvironment ge =
                  GraphicsEnvironment.getLocalGraphicsEnvironment( );
              GraphicsConfiguration gfxConfig =
                  ge.getDefaultScreenDevice( ).getDefaultConfiguration( );
              offscreenImage =
                  gfxConfig.createCompatibleImage(source.getWidth( ),
                                                  source.getHeight( ));
              Graphics2D offscreenGraphics =
                  (Graphics2D) offscreenImage.getGraphics( );
              // windows workaround
              offscreenGraphics.setColor (source.getBackground( ));
              offscreenGraphics.fillRect (0, 0,
                                          source.getWidth( ), source.getHeight( )); 
              // paint from source to offscreen buffer 
              source.paint (offscreenGraphics);
          }
          public Dimension getPreferredSize( ) { return animatingSize; }
          public Dimension getMinimumSize( ) { return animatingSize; }
          public Dimension getMaximumSize( ) { return animatingSize; }
          public void update (Graphics g) {
              // override to eliminate flicker from
              // unnecessary clear
              paint (g);
          }
          public void paint (Graphics g) {
               // get the top-most n pixels of source and
               // paint them into g, where n is height
               // (different from sheet example, which used bottom-most)
               BufferedImage fragment =
                   offscreenImage.getSubimage (0, 
                                               0,                                                                   source.getWidth( ), 
                                               animatingSize.height);
               g.drawImage (fragment, 0, 0, this); 
          } 
}

Running the Hack

The SlideInNotification will take any JComponent as its contents. To make things a little interesting, the TestSlideInNotification class, shown in Figure, grabs an icon from the JOptionPane class and makes a JLabel of that and a little nonsense text.

Testing the slide-in notification
import javax.swing.*;

public class TestSlideInNotification {

     public static void main (String[] args) {
         Icon errorIcon = UIManager.getIcon ("OptionPane.errorIcon");
         JLabel label = new JLabel ("Your application asplode",
                                    errorIcon,
                                    SwingConstants.LEFT);
         SlideInNotification slider = new SlideInNotification (label);
         slider.showAt (450);
     } 
}

When you run this application, one thing to make note of is the standard output because the SlideInNotification class has one System.out.println( ) left in to show the discovered dimensions. Here's what Windows reports with a taskbar showing:

       max window bounds = java.awt.Rectangle[x=0,y=0,width=800,height=570]

and what it reports with the taskbar set to auto-hide:

       max window bounds = java.awt.Rectangle[x=0,y=0,width=800,height=600]

Meanwhile, on the Mac, the bounds with a dock on the bottom of the screen look like this:

       max window bounds = java.awt.Rectangle[x=0,y=22,width=1280,height=707]

and with the dock over on the right, they look like this:

       max window bounds = java.awt.Rectangle[x=0,y=22,width=1244,height=746]

Notice that in each case, the first usable y-coordinate is 22, accounting for the unusable space under the Mac's monolithic menu bar. Notice also in the second case that I've lost usable horizontal space because the dock is on the right. By the way, if you're doing the math and can't figure out why nothing adds up to 1024 x 768, it's because I have a wide-screen monitor and my screen size is 1280 x 768.

Of course, don't stare too long at the console output, or you'll miss the appearance of the slide-in window. Figure shows the window in midanimation on Windows, with and without a visible taskbar.

Slide-in window on Windows with taskbar showing (left) and set to autohide (right)


Figure shows the slide-in window on Mac OS X. It's less appropriate on the Mac, and it will be obscured if the user has dock magnification turned on, but it's not really bad either.

Slide-in window on Mac OS X with dock on bottom of screen (left) and not on bottom (right)


Hacking the Hack

To expand this hack, the first thing you'd probably want to do is add some kind of MouseListener so that if the user clicks to acknowledge the appearance of the slide-in window, you could react to it by removing the slide-in window, bringing your application's main window to the front, etc. Then again, you can put live components in here, so there's no reason you couldn't just generate a JOptionPane, make a JDialog from it, grab the content pane of that JDialog, and show it in the slide-in window. That would give you real, active Swing buttons and handy JOptionPane return values. After all, that's what the sheet example did.


     Python   SQL   Java   php   Perl 
     game development   web development   internet   *nix   graphics   hardware 
     telecommunications   C++ 
     Flash   Active Directory   Windows