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.
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.
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:
MoveInteractor: moves graphical objects around in the group.
ChoiceInteractor: selects one graphical object in a group.
NewInteractor: creates new instances of a graphical object.
Each interactor can be modelled by a finite state machine with three states:
Idle: the interactor is doing nothing.
RunningInside: the interactor is currently moving, selecting, or creating an object.
RunningOutside: the interactor is running, but the user has moved the mouse outside the interactor's area of interest.
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:
left mouse pressed: new Event(Event.MOUSE_DOWN, KeyEvent.BUTTON1_MASK, 0, 0, 0)
left mouse released: new Event(Event.MOUSE_UP, KeyEvent.BUTTON1_MASK, 0, 0, 0)
right mouse pressed: new Event(Event.MOUSE_DOWN, KeyEvent.BUTTON3_MASK, 0, 0, 0)
Control-left mouse pressed: new Event(Event.MOUSE_DOWN, KeyEvent.CTRL_MASK | KeyEvent.BUTTON1_MASK, 0, 0, 0)
'A' pressed: new Event(Event.KEY_DOWN, 0, 'A', 0, 0)
Shift-'A' pressed: new Event(Event.KEY_DOWN, KeyEvent.SHIFT_MASK, 'A', 0, 0)
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.
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:
type can be SINGLE, TOGGLE, or MULTIPLE. SINGLE selects the clicked object and de-selects all other objects in the group (exactly one selection). TOGGLE toggles the selection of the clicked object and de-selects all other objects in the group (zero or one selection). MULTIPLE toggles the selection of the clicked object without changing any other objects (zero or more selections). To be clear:
SINGLE: clicking on the object that is already selected does nothing (leaves it selected). Clicking on an object that is not selected causes that object to be selected, and makes sure no other objects are selected. It is OK if there is nothing selected to start, but once the user selects something, there is no way for the user to make there be no selections again.
TOGGLE: clicking on the object that is already selected causes it to be un-selected, so there is nothing selected. Clicking on an object that is not selected causes that object to be selected, and makes sure no other objects are selected. (One or zero selections)
MULTILE: clicking on an object that is already selected causes it to be un-selected but does not affect any other objects. Clicking on an object that is not selected causes it to be selected, but does not affect any other objects.
if firstOnly is true, then only the object that was initially clicked can be selected or toggled by the running interactor. To select a different object, the user would have to restart the interactor by releasing the mouse and clicking on the other object. This corresponds to how push buttons, radio buttons, and checkboxes work in most systems. If firstOnly is false, then the target object can be changed while the interactor is running by moving the mouse. This corresponds to the behavior of menus in most systems.
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.
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:
NewLineInteractor: creates Line objects
NewRectInteractor: creates Rect objects
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.
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:
creating lines and rectangles of fixed color and thickness
selecting a graphical object, with feedback showing which object is selected
moving graphical objects around
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.
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:
SelectableImage: displays one of four images depending on whether it is selected, interim-selected, both, or neither.
Interactors:
NewImageInteractor: scales an image to fill the rectangle dragged out by the user.
GrowInteractor: resizes a graphical object. Requires adding a resize() method to GraphicalObject.
Grid size parameter for MoveInteractor, which constrains the moving object to grid coordinates.
Drawing editor features:
changing line thickness and color of the NewLineInteractor
changing line thickness and color of selected object
more kinds of graphical objects
Widgets combine graphical objects with interactors in a single package that implements GraphicalObject.
PushButton: a push button. The button label can be a string or an image. The button should display interim-selection feedback when it is being pressed.
RadioButtons: a group of radio buttons created from a set of strings, using a ChoiceInteractor to select exactly one.
Menu: a menu created from a set of strings.
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.
Files:
GraphicalObject.java -- revised GraphicalObject interface
Group.java -- revised Group interface
Interactor.java -- Interactor interface
Event.java -- Event class
Selectable.java -- Selectable interface
WindowGroup.java -- a Group that displays graphical objects in a window.
point_in_line_segment.cpp -- one way to do hit testing for lines
Hints and discussion of Homework 3
The reference implementation for Homework 2 you might want to use
Some of the files can be found in this ZIP archive:
hw3-files.zip