Hands on Testing Java: Lab #G22

The Mouse

Introduction

In today's exercise, you will look at how a GUI program can react to mouse events. In Lab #11b, you provided computation classes for computing different mathematical functions. One thing that this program lacked was an easy way to select a new region to plot. Sure, you could enter new values into the x0, x1, y0, and y1 text fields, but how fun is that?

Instead, it would be nicer if you could use the mouse to select an area of an image that you want to zoom into.

It turns out that this is perhaps surpringly easy to add. Granted, you'll implement this on top of the GUI already provided to you, but all of that code implements many different features. In this lab you need to add just one very useful feature, and it really doesn't take that much code.

The Code

Do this...

Also, a word of warning: I have not bothered to list where the various library classes come from in this lab exercise. If you have not become friends with Source -> Organize Imports in Eclipse, now's the time.

Widgets and Listeners

Mouse events are detected by our Java code with listeners, just the same as with textboxes and buttons and other widgets. The only question with mouse events is who should do the listening? The answer, in this case, is the panel that draws the image.

So consider what we want the behavior of the mouse listener to be:

When the mouse button is pressed in the panel containing the image, the point where the mouse was clicked specifies one corner of the "selection rectangle". As the mouse is dragged, the current location of the mouse determines the other corner of the "selection rectangle".

So there are two events you need to worry about: a pressed mouse button and a dragged mouse. Normally, the Swing library would separate those into two separate methods; in this case, its two separate methods in two different interfaces!

Both Problems

Pressing mouse button is considered just a "normal" mouse event. Dragging the mouse which is considered a "mouse motion" event. So you need to implement a normal mouse-event listener for the pressed mouse button and a mouse-motion listener for the dragged mouse.

You want to listen to the panel containing the image, and that panel is created in the PointComputationFrame class. This class has access to the text fields that you want to update based on the location of the mouse click.

The panel itself is a PointComputationPanel which is created in PointComputationFrame#addImagePanel(). Listeners can be added to myImagePanel right after its been declared. Here's the new algorithm:

Algorithm of PointComputationFrame#addImagePanel()
  1. Let plotter be an image plotter...
  2. Let image be an image from the plotter...
  3. Set myImagePanel to be a plot-computation panel using image.
  4. Let selectCornerListener be a new select-corner listener constructed with myself.
  5. Add selectCornerListener to myImagePanel as a mouse listener.
  6. Let selectOtherCornerListener be a new select-other-corner listener constructed with myself.
  7. Add selectOtherCornerListener to myImagePanel as a mouse-motion listener.
  8. Add myImagePanel to the frame.

This algorithm is mostly implemented already except for the emphasized steps which you now need to implement. But first the objects and operations for those two steps need some explaining:

Objects of two steps of PointComputationFrame#addImagePanel()
Description Type Kind Movement Name
the listener to select the first corner of the selection rectangle SelectCornerListener variable local selectCornerListener
the listener to select the second corner of the selection rectangle SelectOtherCornerListener variable local selectOtherCornerListener
myself PointComputationFrame keyword instance this

Neither of the listeners have been written yet.

Operations of two steps of PointComputationFrame#addImagePanel()
Description Predefined? Name Library
create a new SelectCornerListener with a PointComputationFrame no SelectCornerListener(PointComputationFrame) SelectCornerListener
add a mouse listener to a point-computation panel yes addMouseListener(MouseListener) JPanel
create a new SelectOtherCornerListener with a PointComputationFrame no SelectOtherCornerListener(PointComputationFrame) SelectOtherCornerListener
add a mouse-motion listener to a point-computation panel yes addMouseMotionListener(MouseMotionListener) JPanel

Check out the inheritance: PointComputationPanel is a JPanel, so there's no problem invoking the two add-mouse-listener methods on myImagePanel. Implicit in these lists is that SelectCornerListener will have to implement the MouseListener interface.

A Pressed Mouse Button

Do this...
Implement Steps 4 and 5 of the algorithm for PointComputationFrame#addImagePanel(). You'll get compilation errors for the missing class.

Before you implement SelectCornerListener, though, you should be prepared for how much work this will take. If you look at the MouseListener interface, there are five methods you need to implement. Only one method is used for listening for a pressed mouse button. But since it's an interface, you're required to define them all in your listener subclass, even if you leave the body of the method empty.

Fortunately, the Java people knew this was going to be common, so they also created a MouseAdapter class which implements the MouseListener interface, each method with an empty body. So if you extend MouseAdapter, you indirectly will implement the MouseListener interface, and you'll be given "do nothing" definition for each method in the interface. You won't have to do anything else for the events you're not interested in.

Do this...
Create a class named SelectCornerListener which extends the MouseAdapter class.

The compiler will still complain about a missing SelectCornerListener constructor. It's perhaps not clear at this point, but the listener will need access to the image panel and to the control panel, and it can get those accesses through the frame.

Do this...

Hint: see how ReplotListener does these same things.

Do this...
Compile the code, and run the driver. It should run just as before.

The terminology used by the two mouse listeners is important. A mouse click is defined as a mouse press and a mouse release. A dragged mouse motion is defined as a mouse press and a moved mouse. Your first goal is to select one corner when the mouse is pressed (not clicked or released).

Do this...

Everytime you press the mouse in the display area, your program will print "Hey!" to the console screen. This indicates that the listener is doing its job. Notice that it does not print anything if you click somewhere outside the image panel.

Helpful hint: Watch the terminology closely here! Unfortunately, a MouseEvent talks about x- and y-coordinates instead of column and row coordinates. For our purposes, x- and y-coordinates are for the real numbers being plotted. (That's the x and y in your Computation subclasses.) But when dealing with a MouseEvent, x and y will refer to column and row, respectively.

Now you should make it do something useful! The useful thing it should do is select new x- and y-coordinates for the myX0 and myY0 text fields. The MouseEvent passed into mousePressed(Event) has the column and row screen coordinates of the place where the mouse was pressed. The screen coordinates need to be translated into real-number coordinates for the user to see in the text fields.

Consider this algorithm:

Algorithm of SelectCornerListener#mousePressed(MouseEvent)
  1. Receive event.
  2. Let plotter be the real-number plotter from myFrame.
  3. Let column be the x value gotten from event.
  4. Let x be the x-equivalent of column (using columnToX(int) of plotter).
  5. Get the coordinate control from my frame, and set X0 of this coordinate control to be x.
  6. Let row be the y value gotten from event.
  7. Let y be the y-equivalent of row (using rowToY(int) of plotter).
  8. Get the coordinate control from my frame, and set Y0 of this coordinate control to be y.

Do this...
Implement this algorithm, replacing the println statement. Your code should compile and run without problems; as you click in the image area, the X0 and Y0 textfields should change appropriately. Make sure that the numeric conversion is correct.

A Dragged Click

It turns out handling a dragged click is similar to handling a pressed mouse-button. The main difference is that you need to react to a different event, a "dragging" event. For whatever reason, a dragging event is handled with a different listener, but what you do with that listener and how you implement it is identical to what you did before.

Do this...
Implement Steps 6 and 7 of PointComputationFrame#addImagePanel().

The new class for a mouse-motion listener doesn't exist, so you need to create it. This time, it extends a different adapter which helps you with a different listener interface.

Do this...

And now you need to do something when the mouse is dragged:

Do this...
Override SelectOtherCornerListener#mouseDragged(MouseEvent). The algorithm is identical to the one you used for SelectCornerListener#mousePressed(MouseEvent) except that you should replace mentions of x0 with x1 and mentions of y0 with y1.

Do this...
It should compile and run just fine. Remember to hit the Replot button after selecting a new area.

Other Things To Do

If you feel inspired, you might want to try improving this new feature.

Displayed Selection Box

One thing you might find disconcerting is that there's no visual indication of the selection box as you press and drag the mouse. It's up to you to provide this, if you want it!

It actually isn't too difficult. PointComputationPanel would need four more instance variables (and mutators) for column0, row0, column1, and row1. As either mouse listener changes the text fields, it should also set these new instance variables. Then PointComputationPanel#paintComponent(Graphics) should draw a box based on those coordinates.

You may also want some way to toggle the drawing of the box. Draw the box when the mouse is pressed and while it's dragged; don't draw the box when the mouse is released.

Aspect Ratios

It's easy to select a rectangle whose width-to-height ratio does not match the width-to-height ratio of the panel itself. For example, select and plot a very narrow and very tall rectangle. The picture will be distorted because this skinny rectangle is zoomed to a square.

There are several ways to deal with this. The easiest possibility is to ignore the issue; if a user really needs square selections, the user will have to be careful when making a selection. Another possibility would be to force the selection rectangle to be square; you could enforce this by tweaking the row and column (or x and y) values in the SelectOtherCornerListener. Alternately, you could compute a new aspect ratio, and then change the size of the image panel (and the GUI) to match this new ratio.

Resizable Image

The image in this GUI does not change size, even if you resize the GUI itself. To allow the user to resize the GUI leads to the same aspect-ratio issues described in the previous section.

More Precision

If you zoom in enough times, you will reach a point where you can't zoom in any further. One option is to change the precision implied by ControlPanel#COORDINATE_FORMATTER, but this may make the numbers hard to read at other times (because of too much precision). Alternatively, you could increase or decrease the precision based on the difference between the coordinates. This is non-trivial.

Whatever way you use to get this to work, eventually you will run into the fundamental limits of floating point numbers. You can perhaps increase your precision options by switching to BigDecimals, but that would require changing a lot of code and it would undoubtedly slow things down significantly.