Homework 3: Interactors

05-631: Software Architecture for User Interfaces , Fall, 2001

Assigned: October 4, 2001
Due: Thursday, October 25  **NOTE: EXTENDED DEADLINE**

For this assignment, your job is to add interactivity to the graphics system you created in the last assignment. You will write Java classes that create, move, and select graphical objects in response to mouse and keyboard events. You will then combine your graphical objects and interactors to make a rudimentary drawing editor.

Changes to Graphical Objects

In order to implement interactors, we will need a few more methods in GraphicalObject and Group.

GraphicalObject needs one new method, shown in boldface:

public interface GraphicalObject {
     public void draw (Graphics2D graphics);
     public Rectangle getBoundingBox ();
     public void moveTo (int x, int y);
     public Group getGroup ();
     public void setGroup (Group group);
     public boolean contains (int x, int y);
}

contains() does hit-testing. It should return true if (x,y) lies inside the graphical object. (x,y) is interpreted in the same coordinate system as the graphical object's bounding box, i.e. the coordinate system of the object's parent group. For the purpose of this assignment, most of your objects can just test whether the point falls inside the object's bounding box: Rect, FilledRect, Icon, and Text can all behave this way. However, Line must test whether the point actually falls on the line, taking line thickness into account. If the line is diagonal, for example, its bounding box is much bigger than the line itself. Clicking on the empty part of a line's bounding box should not be interpreted as a click on the line. See the sample code in C++ for one way to do hit-testing on lines. Another way is to use Line2D.ptSegDistSq. Another way is to use Graphics2D.hit() (but need a Graphics2D object).

Group needs three new methods in addition to the contains() method it must implement as a GraphicalObject:

public interface Group extends GraphicalObject {
    public void addChild (GraphicalObject child);
    public void removeChild (GraphicalObject child);
    public void resizeChild (GraphicalObject child);
    public void bringChildToFront (GraphicalObject child);
    public void resizeToChildren ();
    public void damage (Rectangle damagedArea);
    public List getChildren ();
    public Point parentToChild (Point pt);
    public Point childToParent (Point pt);
}

getChildren() returns a list of the group's children. List refers to java.util.List, which is an interface implemented by several java.util classes, including Vector, LinkedList, and ArrayList. The children should be listed in display order, so the last child in the list is the frontmost. The caller should treat the return value of getChildren() as immutable.

parentToChild() and childToParent() translate between the group's coordinate system and its parent's coordinate system. parentToChild() takes a point in the parent coordinate system and maps it down to the group's coordinate system. For example, if the group is located at (5,10) in its parent, then parentToChild(Point(5,10)) should return Point(0,0). Similarly, childToParent() maps a point in the group's coordinate system up to the parent coordinate system. Most groups will implement these methods as simple translations, but ScaledGroups should take scaling into account as well. (Note: since Point represents integer coordinates, you'll lose some precision if you put a ScaledGroup inside another ScaledGroup. If accurate scaling were important to our interactors, we'd want to use floating-point coordinates.) Example implementations of parentToChild() and childToParent() can be found in the reference implementation for Homework 2.

Interactors

An interactor is an object that can be attached to a group to make it respond to mouse or keyboard events. You will write three kinds of interactors:

Each interactor can be modelled by a finite state machine with three states:

Transitions between idle and running states are triggered by input events, such as mouse presses or mouse releases.

All interactors should implement the Interactor interface:

public interface Interactor {
     public Group getGroup ();
     public void setGroup (Group group);

     public int getState ();
         public static final int IDLE = 0;
         public static final int RUNNING_INSIDE = 1;
         public static final int RUNNING_OUTSIDE = 2;

     public Event getStartEvent ();
     public void setStartEvent (Event mask);

     public Event getStopEvent ();
     public void setStopEvent (Event mask);

     public boolean getStartAnywhere ();
     public void setStartAnywhere (boolean anywhere);

     public void start (Event event);
     public void running (Event event);
     public void stop (Event event);

}

getGroup() accesses the group that the interactor is attached to, and setGroup() attaches it to a new group.

getState() returns the current state of the interactor: IDLE, RUNNING_INSIDE, or RUNNING_OUTSIDE.

getStartEvent() and getStopEvent() access the events that trigger the interactor. Start events make the interactor transition from idle to running. Stop events make it transition from running back to idle.

getStartAnywhere() and setStartAnywhere() control the scope of the interactor. If startAnywhere is true, then the interactor starts whenever its start event occurs, regardless of the mouse position. If startAnywhere is false, then the interactor only starts when the start event occurs inside one of the objects in the interactor's group.

start() is called when the start event occurs. It should do everything needed to start the interactor and put it in a running state. start() may perform additional tests to decide whether or not the interactor should be started.

running() is called whenever the mouse moves while the interactor is running. It should update the state of the interactor and determine whether the mouse is inside or outside the area of interest.

stop() is called when the stop event occurs. It should stop the interactor and return it to an idle state.

To make your interactors work, you'll have to write a class that handles Java mouse and keyboard events and makes the appropriate state changes to the interactors. We will provide a class WindowGroup that implements Group and displays its children in a Java window. You will write a subclass, InteractiveWindowGroup, which listens for Java input events on the window. Your subclass should have at least these methods

public class InteractiveWindowGroup extends WindowGroup {
     public void addInteractor (Interactor inter);
     public void removeInteractor (Interactor inter);
}

Note that the InteractiveWindowGroup represents the window. It is both a JFrame and a Group. There will only be one InteractiveWindowGroup per window.

Whenever your InteractiveWindowGroup gets a Java input event, it should scan its collection of interactors and call start(), running(), and/or stop() as appropriate. The input event should be passed to start(), running() or stop() as an instance of the Event class:

 

public class Event {
     public Event (int id, int modifiers, int key, int x, int y);
     public int getID () { 
     public int getModifiers ();
     public int getKey ();
     public int getX ();
     public int getY ();
     public boolean matches (Event event);

     public final static int KEY_DOWN = 0;
     public final static int KEY_UP = 1;
     public final static int MOUSE_DOWN = 2;
     public final static int MOUSE_UP = 3;
     public final static int MOUSE_MOVE = 4;
}

id is one of the five values KEY_DOWN, KEY_UP, MOUSE_DOWN, MOUSE_UP, or MOUSE_MOVE. MOUSE_MOVE indicates that the mouse has moved but no other event has occurred.

modifiers is a bit mask of CTRL_MASK, SHIFT_MASK, ALT_MASK, BUTTON1_MASK, BUTTON2_MASK, or BUTTON3_MASK, indicating which keyboard keys or mouse buttons are currently pressed. These constants are defined in java.awt.event.InputEvent.

key is the same as java.awt.event.KeyEvent.getKeyCode(). For letters, numbers, and punctuation, it is the character's uppercase ASCII value, like 'A' or '?'. For special keys on the keyboard, like F1 or Page Up, it is one of the VK_ constants defined in java.awt.event.KeyEvent. For a mouse event, key is 0.

x and y are the last known x,y position of the mouse. When an event is passed to an interactor, (x,y) should be in the coordinate system of the group that the interactor is attached to, not the coordinate system of the whole window.

matches() returns true if two events match. This method is used to test whether an input event matches the start event or stop event of an interactor. matches() compares only the id, modifiers, and key fields of the two events -- the mouse position doesn't matter.

Here are some examples of events:

MoveInteractor

A move interactor moves a graphical object around in its group. It has only one required constructor and no required methods:

public class MoveInteractor implements Interactor {
     public MoveInteractor ();
}

A move interactor should start running only if the mouse is over a graphical object in its group. While it is running, it should use moveTo() to make the object follow the mouse. When the mouse goes outside the group, the interactor should stop moving the object, so that it can't be dragged outside the group's clipping area. When the stop event occurs, the interactor should stop moving the object.

ChoiceInteractor

A choice interactor selects one or more graphical objects in a group.

public class ChoiceInteractor implements Interactor {
     public ChoiceInteractor (int type, boolean firstOnly);
     public List getSelection ();

     public static final int SINGLE = 0;
     public static final int TOGGLE = 1;
     public static final int MULTIPLE = 2;
}

The two parameters to ChoiceInteractor affect what kind of selection it makes:

getSelection() returns a java.util.List containing all the currently-selected objects.

In order to be selectable by a choice interactor, a graphical object must implement the Selectable interface:

public interface Selectable {
     public void setInterimSelected (boolean interimSelected);
     public boolean isInterimSelected ();
     public void setSelected (boolean selected);
     public boolean isSelected ();
}

These methods can be used by the graphical object to change its appearance. "Interim selected" means that a running choice interactor is currently selecting the object. Interim selection is always turned off when the interactor stops. "Selected" means that the object was interim-selected when the choice interactor stopped.

A choice interactor should start running only if the mouse is over a graphical object that implements Selectable. It should update the interim selection as the mouse moves around. Finally, when the stop event occurs, the interactor should clear the interim selection and make the final selection.

To demonstrate selection feedback, you will write a new Group:

public class SelectionHandles implements Group, Selectable {
     public SelectionHandles (Color color);
}

Usually, SelectionHandles will have exactly one child. The SelectionHandles group should keep its bounding box fit tightly around its child (but leaving enough room to draw handles so the size doesn't change when handles are drawn). Whenever SelectionHandles is selected or interim-selected, it should display a small, filled square at each corner of its child object.

NewInteractor

A NewInteractor creates new instances of a class of graphical objects:

public class NewInteractor implements Interactor {
     public NewInteractor (boolean onePoint);
     public abstract GraphicalObject make (int x1, int y1, int x2, int y2);
     public abstract void resize (GraphicalObject gobj, int x1, int y1, int x2, int y2);
}

make() creates a graphical object from point (x1, y1) to point (x2, y2). This method is declared abstract. It should be overridden in a subclass of NewInteractor, which decides which graphical object to create and how to interpret the (x1,y1) and (x2, y2) coordinates.

resize() adjusts a graphical object created by make() with new points. (x1,y1) is the anchor point, which should be the same as was passed to make(). The point (x2,y2) follows the mouse cursor, so it will be different.

If the onePoint parameter to the constructor is true, then the new interactor needs only one point to create the object. It calls make(x, y, x, y) to create the object, then stops immediately after starting, never calling resize(). This is useful for fixed size objects, like icons.

When a NewInteractor starts, it should call its own make() method to create a new instance of a graphical object. The NewInteractor should add the object returned by make() to the group, so it will appear on screen immediately. While the interactor is running, it should resize the new object to follow the mouse (assuming onePoint is false). When it stops, it should leave the object where it is.

You will create two subclasses of NewInteractor:

These classes should have the following constructors:

public NewRectInteractor (Color color, 
                          int lineThickness);

public NewLineInteractor (Color color, 
                          int lineThickness);

Each class should also have a getParam() and setParam() method for each parameter in its constructor.

Note that your NewLineInteractor and NewRectInteractor don't have to create pure Line and Rect objects. You may want to create lines wrapped inside a SelectionHandles group, or perhaps a subclass of Line that implements Selectable and draws selection-handle feedback itself.

 

Drawing Editor

For the last part of the assignment, you will use your GraphicalObjects and Interactors to create a simple drawing editor. The editor should have the following features, at a minimum:

The user interface is up to you. Most drawing editors use a tool palette to switch between creating lines and rectangles. You can do this with Java Swing components if you want, but you can also use keyboard modifiers -- e.g., shift-drag to create lines, plain drag to create rectangles. If it isn't obvious how to use your editor, be sure to display Text objects that document it, preferably outside the drawing area.

Extra Credit

For extra credit, you can implement more selectable objects, more interactors, more features in your drawing editor, or more widgets. Here are some ideas.

Selectable objects:

Interactors:

Drawing editor features:

Widgets combine graphical objects with interactors in a single package that implements GraphicalObject.

Note that your widget must install its interactors when it's added to a group and uninstall them when it's removed. One way to do it is to scan up the group hierarchy until you find a group that's an instance of InteractiveWindowGroup.

Resources

Files:

Some of the files can be found in this ZIP archive:

hw3-files.zip