|
|
SATIN Tutorial
Table of Contents
Introducing the SATIN Informal Ink Toolkit
|
SATIN is a toolkit for making informal ink-based applications. Informal
ink-based applications use pens and sketching as the primary interaction
technique. Sketching is quick and easy to do with pen and paper, but it's
difficult to create computer-based apps that use sketching today. It's often
the case that pens are just treated as another pointing device, clicking on
buttons and menus.
That's where SATIN comes in. We're trying to make it easier to build informal
ink-based apps, and SATIN is our first take at a toolkit to help you do that.

Above are two examples of informal ink-based apps. Click on the images above for enlarged views.
Other SATIN Documentation
In this tutorial, we'll go over lots of details on how SATIN works. If you
want to understand the big picture, that is what the problems are and why we
built SATIN, then check these out:
- UIST2000 paper on SATIN
This is the conference paper on SATIN, describing the toolkit in 10 pages.
- Presentation on SATIN (ppt)
This is a presentation of the paper above. The HTML version doesn't do
animations, so I'd suggest taking a look at the PowerPoint slides if you can.
If you want to try out a large-scale app built with SATIN, check out DENIM,
a sketch-based web site design tool we're working on. It shows a lot of the
features in SATIN, like sketching, recognition, zooming, and so on.
Also, all of the Javadoc APIs for SATIN are available online. You can skim
through the docs or read them while going thru this tutorial.
If you haven't already, download the latest SATIN
zip file. After you've done this, unzip it some location. You'll also
want to get the zip file of the tutorial source
code. Alternatively, you can just download each individual source file as
we get to it.
If you don't have a zip program, you can use jar to unzip.
The jar app comes with Java Development Kit, so you should already
have it. To unzip, type:
jar -xvf [name of zip file]
If you want to try out some SATIN apps, try running SketchySPICE or
Brainstorm (there are some batch files for doing so). With SketchySPICE,
try drawing AND and OR gates. With Brainstorm, try drawing rectangular
shapes about the size of post-it notes, and draw lines between the notes.
Classpath
After you've downloaded SATIN, you'll need to set your CLASSPATH variable to
point to the satin jar file. With WinNT and Win2K, go to "Control Panel ->
System". There should be some tab that will take you to environment variables.
Be sure to point to the absolute directory location of the satin jar file.
With Unix based systems...well, it's either set, setenv, or something like
that. It really depends on what shell you're using.
Once you've set the CLASSPATH, you should be able to use the SATIN classes from
any directory. One thing to note, though. There's a directory with the SATIN
distribution called data. This directory contains the data files for
gestures. If you want to use gestures, you'll have to have this directory in
the same directory where you run the Java Virtual Machine. We'll remind you of
this once we get to gestures.
Part One - Using SATIN Sheets
|
Immediate gratification can be a good thing, so let's develop our first app, a
simple Scribbling program. We'll augment this app as we go along.
I'll assume you have a basic knowledge of Java Swing. So here's the
code for our first app:
Download code v1
import javax.swing.*;
import edu.berkeley.guir.lib.satin.*;
/**
* An example of how to use the SATIN toolkit.
* This app demonstrates how the Sheet works with Java Swing.
*/
public class Scribbler {
//===========================================================================
//=== SELF-TESTING MAIN =================================================
public static void main(String[] argv) {
JFrame f = new JFrame();
Sheet s = new Sheet();
//// 1. Make the Sheet the content pane for the frame.
f.setContentPane(s);
//// 2. Make the frame visible.
f.setSize(600, 600);
f.setVisible(true);
} // of main
//=== SELF-TESTING MAIN =================================================
//===========================================================================
} // of class
|
Ok, so besides my quirky programming style, you should notice that we just set
the frame's content pane to be the Sheet. The Sheet extends a JPanel, so it's
okay. Try compiling this app and running it. You should be able to just
scribble things using your mouse. Here's my picture. Keep in mind that it's
really hard to draw with a mouse. One of my friends likens drawing with a mouse
to drawing with a potato.
Some Definitions
To make sure we're on the same page, we need to define some terms.
- A stroke is pen (or mouse) input that comes from dragging
- Ink is a stroke that stays and is displayed on the Sheet
So here are some "hidden" debug features in SATIN. Try them out with the
Scribbler app.
- Hit "Ctrl-Alt-D" to turn on bounding boxes
- Hit "Ctrl-Alt-W" to turn on the debugging output window
(this uses the debugging package we wrote up)
- Hit "Ctrl-Z" to undo an action
- Hit "Ctrl-Y" to redo an action
- Use the arrow keys to pan
Another thing to note is that the Sheet automatically pans if you scribble
outside of the window. Click somewhere near an edge, and then move outside of
the window to see for yourself.
Let's go on to the next app.
Part Two - Using Interpreters
|
So scribbling is neat, but we can do more. One feature a lot of these informal
ink-based apps have is gestures, that is a stroke that executes a command.
For example, you could scribble over a word to erase it. As another example,
you could circle all of the objects you wanted to select.

Gestures are strokes that activate a command.
In the example above, you can scrub over some text and erase that text.
In SATIN, we process strokes through
Interpreters.
SATIN already has one called
CircleSelectInterpreter, which selects things
contained within anything that's sort of circular. We've modified the code
from the last example, changes are in bold.
Download code v2
import javax.swing.*;
import edu.berkeley.guir.lib.satin.*;
import edu.berkeley.guir.lib.satin.interpreter.commands.*;
/**
* An example of how to use the SATIN toolkit.
* This app demonstrates how the Sheet works with Java Swing.
*/
public class Scribbler {
//===========================================================================
//=== SELF-TESTING MAIN =================================================
public static void main(String[] argv) {
JFrame f = new JFrame();
Sheet s = new Sheet();
//// 1. Make the Sheet the content pane for the frame.
f.setContentPane(s);
//// 2. Add a Circle Select Interpreter to the Sheet.
s.setGestureInterpreter(new CircleSelectInterpreter());
//// 3. Make the frame visible.
f.setSize(600, 600);
f.setVisible(true);
} // of main
//=== SELF-TESTING MAIN =================================================
//===========================================================================
} // of class
|
Now compile the program and run it. You should still be able to scribble
normally. To circle select some items, just draw a circular shape around
some objects. Here's an example, going from left to right.
Here are some more features built into the Sheet that you can
do to selected objects. These are keys that are standard to most Windows
applications.
- "Ctrl-X" or "Shift-Del" to cut
- "Ctrl-V" or "Shift-Ins" to paste
- "Del" to delete
Differentiating Between Gestures and Ink
One potential problem is figuring out whether the stroke should be ink or a
gesture. In other words, if you just drew a stroke, how can the computer
reliably know whether you just want to draw something or you want to execute a
command?
Unfortunately, there really aren't good answers to that yet. It's going to take
a lot of good design and lot of advances in recognition technology before it
can be done really well. In the meanwhile, one thing that's often done is to
just use the right button to mean gestures (as in "right" and "left").
Here's how we can make this work. Interpreters can filter out certain things.
All we have to do is to tell it to ignore left and middle buttons. The other
thing we should do is to tell the Sheet to not add right-button input as ink.
Here's the code below, changes marked in bold.
Download code v3
import javax.swing.*;
import edu.berkeley.guir.lib.satin.*;
import edu.berkeley.guir.lib.satin.interpreter.*;
import edu.berkeley.guir.lib.satin.interpreter.commands.*;
/**
* An example of how to use the SATIN toolkit.
* This app demonstrates how the Sheet works with Java Swing.
*/
public class Scribbler {
//===========================================================================
//=== SELF-TESTING MAIN =================================================
public static void main(String[] argv) {
JFrame f = new JFrame();
Sheet s = new Sheet();
//// 1. Make the Sheet the content pane for the frame.
f.setContentPane(s);
//// 2. Add a Circle Select Interpreter to the Sheet.
Interpreter intrp = new CircleSelectInterpreter();
intrp.setAcceptLeftButton(false);
intrp.setAcceptMiddleButton(false);
s.setGestureInterpreter(intrp);
//// 3. Make it so that by default, the Sheet will not add
//// right mouse button strokes as ink.
s.setAddRightButtonStrokes(false);
//// 4. Make the frame visible.
f.setSize(600, 600);
f.setVisible(true);
} // of main
//=== SELF-TESTING MAIN =================================================
//===========================================================================
} // of class
|
Ok, one last topic in this section. You've probably
noticed that the method to add the CircleSelectInterpreter was
setGestureInterpreter(). There's also another parallel method
for ink, setInkInterpreter(). The way it's currently defined
in SATIN, ink interpreters get called after gesture interpreters. So if
the gesture interpreter says its not a gesture, it gets handed off to the
ink interpreter.
We actually don't make a strict distinction between gesture and ink
interpreters in SATIN. There is no class or interface to distinguish between
gesture and ink interpreters. If something is assigned as an ink interpreter,
then it's an ink interpreter.
Below are some examples of gesture and ink interpreters.
Here's some more code, changes in bold. We're adding in an ink interpreter that
straightens out lines for us. Make the changes, compile, and try it out.
Download code v4
import javax.swing.*;
import edu.berkeley.guir.lib.satin.*;
import edu.berkeley.guir.lib.satin.interpreter.*;
import edu.berkeley.guir.lib.satin.interpreter.commands.*;
import edu.berkeley.guir.lib.satin.interpreter.stroke.*;
/**
* An example of how to use the SATIN toolkit.
* This app demonstrates how the Sheet works with Java Swing.
*/
public class Scribbler {
//===========================================================================
//=== SELF-TESTING MAIN =================================================
public static void main(String[] argv) {
JFrame f = new JFrame();
Sheet s = new Sheet();
//// 1. Make the Sheet the content pane for the frame.
f.setContentPane(s);
//// 2. Add a Circle Select Interpreter to the Sheet.
Interpreter intrp = new CircleSelectInterpreter();
intrp.setAcceptLeftButton(false);
intrp.setAcceptMiddleButton(false);
s.setGestureInterpreter(intrp);
//// 3. Add an interpreter that straightens ink to the Sheet.
s.setInkInterpreter(new LinearizeStrokeInterpreter());
//// 4. Make it so that by default, the Sheet will not add
//// right mouse button strokes as ink.
s.setAddRightButtonStrokes(false);
//// 5. Make the frame visible.
f.setSize(600, 600);
f.setVisible(true);
} // of main
//=== SELF-TESTING MAIN =================================================
//===========================================================================
} // of class
|
Here are two pictures showing how it works. The one on the left shows what I
drew, the one on the right shows what it was changed to.
Part Three - Creating Interpreters
|
So let's create our first interpreter. We'll do something simple, an
interpreter that randomly assigns colors to ink. We'll also show how
to use the debugging window too. The code for the interpreter
is below, commentary follows.
Download code v5
import edu.berkeley.guir.lib.debugging.*;
import edu.berkeley.guir.lib.satin.event.*;
import edu.berkeley.guir.lib.satin.objects.*;
import edu.berkeley.guir.lib.satin.interpreter.*;
public class RandomColorInterpreter
extends InterpreterImpl {
//===========================================================================
//=== CONSTANTS =========================================================
private static final Debug debug = new Debug(Debug.ON);
//=== CONSTANTS =========================================================
//===========================================================================
//===========================================================================
//=== INTERPRETER METHODS ===============================================
public void handleSingleStroke(SingleStrokeEvent evt) {
debug.println("method called");
} // of method
//=== INTERPRETER METHODS ===============================================
//===========================================================================
//===========================================================================
//=== CLONE =============================================================
public Object clone() {
//// It actually turns out that in Java, you're not supposed to
//// create new objects in method clone(). See:
//// http://java.sun.com/docs/books/tutorial/java/javaOO/objectclass.html
////
//// However, SATIN has not been updated to reflect this, so...
return (new RandomColorInterpreter());
} // of method
//=== CLONE =============================================================
//===========================================================================
} // of class
|
The CONSTANTS section shows how to get access to the debugging
object. The INTERPRETER METHODS section exposes some of
the event handling in SATIN. The CLONE section is just for
handling cloning, a requirement for interpreters. Cloning is used when
objects are cut and paste.
The interesting part is the method handleSingleStroke().
This method is called for you whenever a person draws a single stroke
(ie, button down, drag, button up). There are ways of handling strokes
as they are drawn, but that's a little more complex, so we won't touch on it
here.
For now, let's just make sure that method handleSingleStroke()
is being called correctly. So we'll just add some code to make a call to
the Debug object, which will display it on the debugging window.
Below are the modifications to the Scribbler class.
Download code v5
import javax.swing.*;
import edu.berkeley.guir.lib.satin.*;
import edu.berkeley.guir.lib.satin.interpreter.*;
import edu.berkeley.guir.lib.satin.interpreter.commands.*;
import edu.berkeley.guir.lib.satin.interpreter.stroke.*;
/**
* An example of how to use the SATIN toolkit.
* This app demonstrates how the Sheet works with Java Swing.
*/
public class Scribbler {
//===========================================================================
//=== SELF-TESTING MAIN =================================================
public static void main(String[] argv) {
JFrame f = new JFrame();
Sheet s = new Sheet();
//// 1. Make the Sheet the content pane for the frame.
f.setContentPane(s);
//// 2. Add a Circle Select Interpreter to the Sheet.
Interpreter intrp = new CircleSelectInterpreter();
intrp.setAcceptLeftButton(false);
intrp.setAcceptMiddleButton(false);
s.setGestureInterpreter(intrp);
//// 3. Add an interpreter that changes ink colors.
s.setInkInterpreter(new RandomColorInterpreter());
//// 4. Make it so that by default, the Sheet will not add
//// right mouse button strokes as ink.
s.setAddRightButtonStrokes(false);
//// 5. Make the frame visible.
f.setSize(600, 600);
f.setVisible(true);
} // of main
//=== SELF-TESTING MAIN =================================================
//===========================================================================
} // of class
|
So compile the classes and run them. Bring up the debugging window
(Ctrl-Alt-W), and then scribble something. You should get something like
the following:
The debugging window tells you which class and which method were called,
plus what was printed out (through the debug.println() method).
Changing Colors
So let's actually do something interesting with this Interpreter.
To set the color for a stroke (actually called a
TimedStroke),
you get its Style object. Styles group together lots of the visual
presentation of objects, like color, line width, and so on.
The other thing changed is the Sheet's color. The reason is that it's hard to
see some colors on a gray background, so let's change it to white.
Below is the code for doing this:
Download code v6
import edu.berkeley.guir.lib.debugging.*;
import edu.berkeley.guir.lib.satin.event.*;
import edu.berkeley.guir.lib.satin.objects.*;
import edu.berkeley.guir.lib.satin.interpreter.*;
import edu.berkeley.guir.lib.satin.stroke.*;
import java.awt.Color;
import java.util.Random;
public class RandomColorInterpreter
extends InterpreterImpl {
//===========================================================================
//=== CONSTANTS =========================================================
private static final Debug debug = new Debug(Debug.ON);
//=== CONSTANTS =========================================================
//===========================================================================
//===========================================================================
//=== NONLOCAL VARIABLES ================================================
Random rand = new Random();
//=== NONLOCAL VARIABLES ================================================
//===========================================================================
//===========================================================================
//=== INTERPRETER METHODS ===============================================
public void handleSingleStroke(SingleStrokeEvent evt) {
//// 1. Get a reference to the stroke.
TimedStroke stk = evt.getStroke();
//// 2. Generate a random color.
Color c = new Color(rand.nextFloat(),
rand.nextFloat(),
rand.nextFloat());
//// 3. Set the stroke's style to use this color.
stk.getStyleRef().setDrawColor(c);
} // of method
//=== INTERPRETER METHODS ===============================================
//===========================================================================
//===========================================================================
//=== CLONE =============================================================
public Object clone() {
//// It actually turns out that in Java, you're not supposed to
//// create new objects in method clone(). See:
//// http://java.sun.com/docs/books/tutorial/java/javaOO/objectclass.html
////
//// However, SATIN has not been updated to reflect this, so...
return (new RandomColorInterpreter());
} // of method
//=== CLONE =============================================================
//===========================================================================
} // of class
|
Download code v6
import javax.swing.*;
import edu.berkeley.guir.lib.satin.*;
import edu.berkeley.guir.lib.satin.interpreter.*;
import edu.berkeley.guir.lib.satin.interpreter.commands.*;
import edu.berkeley.guir.lib.satin.interpreter.stroke.*;
/**
* An example of how to use the SATIN toolkit.
* This app demonstrates how the Sheet works with Java Swing.
*/
public class Scribbler {
//===========================================================================
//=== SELF-TESTING MAIN =================================================
public static void main(String[] argv) {
JFrame f = new JFrame();
Sheet s = new Sheet();
//// 1. Make the Sheet the content pane for the frame.
f.setContentPane(s);
s.setBackground(java.awt.Color.white);
//// 2. Add a Circle Select Interpreter to the Sheet.
Interpreter intrp = new CircleSelectInterpreter();
intrp.setAcceptLeftButton(false);
intrp.setAcceptMiddleButton(false);
s.setGestureInterpreter(intrp);
//// 3. Add an interpreter that changes ink colors.
s.setInkInterpreter(new RandomColorInterpreter());
//// 4. Make it so that by default, the Sheet will not add
//// right mouse button strokes as ink.
s.setAddRightButtonStrokes(false);
//// 5. Make the frame visible.
f.setSize(600, 600);
f.setVisible(true);
} // of main
//=== SELF-TESTING MAIN =================================================
//===========================================================================
} // of class
|
Compile the code and run it. Here's a screenshot of what should appear.
Part Four - Multi-Interpreters
|
Multi-Interpreters are a special kind of Interpreter that lets you combine
existing interpreters together. There are other reasons why we have
Multi-Interpreters, but we won't discuss them here.
So let's say we want to combine three different interpreters:
- Tap Select, which lets you tap on objects to select them
- Circle Select, which lets you select a bunch of objects by
circling them
- Move Selected, which lets you drag selected objects around
Below is the code for combining these Interpreters together. To do this,
we create a MultiInterpreter and the Interpreters to it.
Then, we set the MultiInterpreter as the gesture interpreter.
When a stroke is created, the Sheet first asks its gesture interpreter to
handle the stroke. In this case, the gesture interpreter is the
MultiInterpreter. The MultiInterpreter just asks the Interpreters it contains
to handle the stroke.
We actually use a
DefaultMultiInterpreterImpl
as our specific instance of a
MultiInterpreter. The Default one just asks each of the Interpreters in the
order they were added if it knows how to handle the stroke. If yes, then
we're done. If no, then it goes on and asks the next Interpreter. In other
words, it chains the Interpreters together.
This is probably a little confusing, but there is an animation of how it works
in the
powerpoint slides on SATIN. Skip to the part about MultiInterpreters and Stroke Dispatching,
and put it into Slide Show mode to see the animations.
One thing to note is that this makes ordering of Interpreters very important.
With the DefaultMultiInterpreterImpl, if you want one Interpreter to act
before another, you have to add that one first.
Download code v7
import javax.swing.*;
import edu.berkeley.guir.lib.satin.*;
import edu.berkeley.guir.lib.satin.interpreter.*;
import edu.berkeley.guir.lib.satin.interpreter.commands.*;
import edu.berkeley.guir.lib.satin.interpreter.stroke.*;
/**
* An example of how to use the SATIN toolkit.
* This app demonstrates how the Sheet works with Java Swing.
*/
public class Scribbler {
//===========================================================================
//=== SELF-TESTING MAIN =================================================
public static void main(String[] argv) {
JFrame f = new JFrame();
Sheet s = new Sheet();
//// 1. Make the Sheet the content pane for the frame.
f.setContentPane(s);
s.setBackground(java.awt.Color.white);
//// 2. Set up the gesture interpreters.
MultiInterpreter mintrp;
Interpreter intrp;
mintrp = new DefaultMultiInterpreterImpl();
intrp = new TapSelectInterpreter();
intrp.setAcceptLeftButton(true);
intrp.setAcceptRightButton(true);
mintrp.add(intrp);
intrp = new CircleSelectInterpreter();
intrp.setAcceptLeftButton(false);
intrp.setAcceptRightButton(true);
mintrp.add(intrp);
intrp = new MoveSelectedInterpreter();
intrp.setAcceptLeftButton(true);
intrp.setAcceptRightButton(true);
mintrp.add(intrp);
s.setGestureInterpreter(mintrp);
//// 3. Set up the ink interpreters.
s.setInkInterpreter(new RandomColorInterpreter());
//// 4. Make it so that by default, the Sheet will not add
//// right mouse button strokes as ink.
s.setAddRightButtonStrokes(false);
//// 5. Make the frame visible.
f.setSize(600, 600);
f.setVisible(true);
} // of main
//=== SELF-TESTING MAIN =================================================
//===========================================================================
} // of class
|
Another interesting thing we can do with MultiInterpreters is to "lie" about
whether we handled the stroke event or not. An Interpreter can actually do
something with a stroke event but say it didn't, and let another Interpreter
in the chain handle the event.
For example, the RandomColorInterpreter does this. It changes the color
of the stroke, but doesn't tell the stroke event that it's been handled.
We can actually combine the RandomColorInterpreter with the
LinearizeStrokeInterpreter
and get new effects. (If you did want to tell the
stroke event it's been handled, you would call evt.setConsumed(true) where evt
is the stroke event).
So here's some code for how to combine the RandomColorInterpreter and the
LinearizeStrokeInterpreter.
Download code v8
import javax.swing.*;
import edu.berkeley.guir.lib.satin.*;
import edu.berkeley.guir.lib.satin.interpreter.*;
import edu.berkeley.guir.lib.satin.interpreter.commands.*;
import edu.berkeley.guir.lib.satin.interpreter.stroke.*;
/**
* An example of how to use the SATIN toolkit.
* This app demonstrates how the Sheet works with Java Swing.
*/
public class Scribbler {
//===========================================================================
//=== SELF-TESTING MAIN =================================================
public static void main(String[] argv) {
JFrame f = new JFrame();
Sheet s = new Sheet();
//// 1. Make the Sheet the content pane for the frame.
f.setContentPane(s);
s.setBackground(java.awt.Color.white);
//// 2. Set up the gesture interpreters.
MultiInterpreter mintrp;
Interpreter intrp;
mintrp = new DefaultMultiInterpreterImpl();
intrp = new TapSelectInterpreter();
intrp.setAcceptLeftButton(true);
intrp.setAcceptRightButton(true);
mintrp.add(intrp);
intrp = new CircleSelectInterpreter();
intrp.setAcceptLeftButton(false);
intrp.setAcceptRightButton(true);
mintrp.add(intrp);
intrp = new MoveSelectedInterpreter();
intrp.setAcceptLeftButton(true);
intrp.setAcceptRightButton(true);
mintrp.add(intrp);
s.setGestureInterpreter(mintrp);
//// 3. Set up the ink interpreters.
mintrp = new DefaultMultiInterpreterImpl();
intrp = new RandomColorInterpreter();
mintrp.add(intrp);
intrp = new LinearizeStrokeInterpreter();
mintrp.add(intrp);
s.setInkInterpreter(mintrp);
//// 4. Make it so that by default, the Sheet will not add
//// right mouse button strokes as ink.
s.setAddRightButtonStrokes(false);
//// 5. Make the frame visible.
f.setSize(600, 600);
f.setVisible(true);
} // of main
//=== SELF-TESTING MAIN =================================================
//===========================================================================
} // of class
|
Here's a screenshot of my attempt at sketching a Mondrian painting.
Even though sketching is cool, not everything you do with pens will
be sketching. Sometimes, standard GUI widgets (like buttons and menus)
can be more effective. However, these GUI widgets often aren't easy to use
with pens. For example, it's impossible to double-tap on anything with a pen.
Another thing we've done with SATIN is create widgets specialized for pens.
One example of such are pie menus. It turns out that pie menus are easier to
learn and are faster than standard linear popup menus. And they work better for
pens and mice, too.
Even though pie menus ship with SATIN, we've actually made them independent of
the rest of SATIN. In other words, you can use the pie menus without having to
use the sketching functionality.
Here's how to add pie menus to our current app. We create the pie menu
instance, and then just add labels to it.
Download code v9
import javax.swing.*;
import edu.berkeley.guir.lib.satin.*;
import edu.berkeley.guir.lib.satin.interpreter.*;
import edu.berkeley.guir.lib.satin.interpreter.commands.*;
import edu.berkeley.guir.lib.satin.interpreter.stroke.*;
import edu.berkeley.guir.lib.satin.widgets.*;
/**
* An example of how to use the SATIN toolkit.
* This app demonstrates how the Sheet works with Java Swing.
*/
public class Scribbler {
//===========================================================================
//=== PIE MENU SETUP ====================================================
public static void setupPieMenu(JComponent comp) {
PieMenu pm = new PieMenu();
//// 1. Add the pie menu slices to the pie menu.
//// Order starts at north, goes counter clockwise.
//// Reason for '\n' is because pie menu is small, so
//// words don't fit correctly. Either resize or go
//// to next line.
pm.add("Zoom\nIn");
pm.add("Rotate\nLeft");
pm.add("Zoom\nOut");
pm.add("Rotate\nRight");
//// 2. Add the pie menu to the component.
pm.addPieMenuTo(comp);
} // of method
//=== PIE MENU SETUP ====================================================
//===========================================================================
//===========================================================================
//=== SELF-TESTING MAIN =================================================
public static void main(String[] argv) {
JFrame f = new JFrame();
Sheet s = new Sheet();
//// 1. Make the Sheet the content pane for the frame.
f.setContentPane(s);
s.setBackground(java.awt.Color.white);
//// 2. Set up the gesture interpreters.
MultiInterpreter mintrp;
Interpreter intrp;
mintrp = new DefaultMultiInterpreterImpl();
intrp = new TapSelectInterpreter();
intrp.setAcceptLeftButton(true);
intrp.setAcceptRightButton(true);
mintrp.add(intrp);
intrp = new CircleSelectInterpreter();
intrp.setAcceptLeftButton(false);
intrp.setAcceptRightButton(true);
mintrp.add(intrp);
intrp = new MoveSelectedInterpreter();
intrp.setAcceptLeftButton(true);
intrp.setAcceptRightButton(true);
mintrp.add(intrp);
s.setGestureInterpreter(mintrp);
//// 3. Set up the ink interpreters.
mintrp = new DefaultMultiInterpreterImpl();
intrp = new RandomColorInterpreter();
mintrp.add(intrp);
intrp = new LinearizeStrokeInterpreter();
mintrp.add(intrp);
s.setInkInterpreter(mintrp);
//// 4. Make it so that by default, the Sheet will not add
//// right mouse button strokes as ink.
s.setAddRightButtonStrokes(false);
//// 5. Set up the pie menu.
setupPieMenu(s);
//// 6. Make the frame visible.
f.setSize(600, 600);
f.setVisible(true);
} // of main
//=== SELF-TESTING MAIN =================================================
//===========================================================================
} // of class
|
By default, the pie menu opens when you right click (right-button down then
right-button up). Here's what the app looks like:
Right now, the pie menu doesn't really do anything. The reason is because
we haven't set up the callbacks for it. So we're going to add in some
functionality that SATIN provides but we haven't discussed yet: rotation and
zooming. We won't go into the details of how rotation and zooming works.
Instead, we'll just give the code for you to play around with it.
(You may also be wondering why we built zooming and rotation as part of this
toolkit, especially if it's for pens and sketching. It turns out that zooming
is a generally useful technique for managing information in 2D. Rotation came
in for free, but think about how people rotate sheets of paper while drawing in
the real world.)
There are a lot of changes in this version. There's going to have to be some
hand-waving, because there are many things in here that will take a lot of
explaning. Also, the Scribbler class is getting large enough that we should
split it into ScribblerSheet and Scribbler (and avoid the use of static
class variables). So we'll show the single class version first, and then the
multiple class version next. If you want to continue hacking with SATIN,
then it's best if you work with the multiple class version.
Download code v10
import javax.swing.*;
import edu.berkeley.guir.lib.satin.*;
import edu.berkeley.guir.lib.satin.interpreter.*;
import edu.berkeley.guir.lib.satin.interpreter.commands.*;
import edu.berkeley.guir.lib.satin.interpreter.stroke.*;
import edu.berkeley.guir.lib.satin.widgets.*;
import edu.berkeley.guir.lib.satin.command.*;
import edu.berkeley.guir.lib.satin.objects.*;
import edu.berkeley.guir.lib.awt.geom.*;
import java.awt.geom.*;
import java.awt.event.*;
/**
* An example of how to use the SATIN toolkit.
* This app demonstrates how the Sheet works with Java Swing.
*/
public class Scribbler {
//===========================================================================
//=== CONSTANTS =========================================================
/**
* Number of frames to animate when zooming or rotating.
*/
public static final int NUMFRAMES = 10;
//=== CONSTANTS =========================================================
//===========================================================================
//===========================================================================
//=== PIE MENU INNER CLASS CALLBACKS ====================================
static class RotateLeftListener implements ActionListener {
public void actionPerformed(ActionEvent evt) {
AffineTransform[] txArray = new AffineTransform[NUMFRAMES];
txArray = AffineTransformLib.animateSlowInSlowOut(
AffineTransform.getRotateInstance(-Math.PI/2,
SatinConstants.cmdsubsys.getAbsoluteLastXLocation(),
SatinConstants.cmdsubsys.getAbsoluteLastYLocation()),
NUMFRAMES);
GraphicalObjectLib.animate(s, txArray);
} // of method
} // of inner class
//-----------------------------------------------------------------
static class RotateRightListener implements ActionListener {
public void actionPerformed(ActionEvent evt) {
AffineTransform[] txArray = new AffineTransform[NUMFRAMES];
txArray = AffineTransformLib.animateSlowInSlowOut(
AffineTransform.getRotateInstance(Math.PI/2,
SatinConstants.cmdsubsys.getAbsoluteLastXLocation(),
SatinConstants.cmdsubsys.getAbsoluteLastYLocation()),
NUMFRAMES);
GraphicalObjectLib.animate(s, txArray);
} // of method
} // of inner class
//-----------------------------------------------------------------
static class ZoomInListener implements ActionListener {
public void actionPerformed(ActionEvent evt) {
AffineTransform[] txArray = new AffineTransform[NUMFRAMES];
txArray = AffineTransformLib.animateSlowInSlowOut(
AffineTransformLib.scaleAndCenterAt(2,
SatinConstants.cmdsubsys.getAbsoluteLastXLocation(),
SatinConstants.cmdsubsys.getAbsoluteLastYLocation(),
s.getBounds2D(SatinConstants.COORD_ABS)),
NUMFRAMES);
GraphicalObjectLib.animate(s, txArray);
} // of method
} // of inner class
//-----------------------------------------------------------------
static class ZoomOutListener implements ActionListener {
public void actionPerformed(ActionEvent evt) {
AffineTransform[] txArray = new AffineTransform[NUMFRAMES];
txArray = AffineTransformLib.animateSlowInSlowOut(
AffineTransformLib.scaleAndCenterAt(0.5,
SatinConstants.cmdsubsys.getAbsoluteLastXLocation(),
SatinConstants.cmdsubsys.getAbsoluteLastYLocation(),
s.getBounds2D(SatinConstants.COORD_ABS)),
NUMFRAMES);
GraphicalObjectLib.animate(s, txArray);
} // of method
} // of inner class
//=== PIE MENU INNER CLASS CALLBACKS ====================================
//===========================================================================
//===========================================================================
//=== CLASS VARIABLES ===================================================
static JFrame f;
static Sheet s = new Sheet();
//=== CLASS VARIABLES ===================================================
//===========================================================================
//===========================================================================
//=== PIE MENU SETUP ====================================================
public static void setupPieMenu(JComponent comp) {
PieMenu pm = new PieMenu();
//// 1. Add the pie menu slices to the pie menu.
//// Order starts at north, goes counter clockwise.
//// Reason for '\n' is because pie menu is small, so
//// words don't fit correctly. Either resize or go
//// to next line.
pm.add("Zoom\nIn").addActionListener(new ZoomInListener());
pm.add("Rotate\nLeft").addActionListener(new RotateLeftListener());
pm.add("Zoom\nOut").addActionListener(new ZoomOutListener());
pm.add("Rotate\nRight").addActionListener(new RotateRightListener());
//// 2. Add the pie menu to the component.
pm.addPieMenuTo(comp);
} // of method
//=== PIE MENU SETUP ====================================================
//===========================================================================
//===========================================================================
//=== SELF-TESTING MAIN =================================================
public static void main(String[] argv) {
f = new JFrame();
s = new Sheet();
//// 1. Make the Sheet the content pane for the frame.
f.setContentPane(s);
s.setBackground(java.awt.Color.white);
//// 2. Set up the gesture interpreters.
MultiInterpreter mintrp;
Interpreter intrp;
mintrp = new DefaultMultiInterpreterImpl();
intrp = new TapSelectInterpreter();
intrp.setAcceptLeftButton(true);
intrp.setAcceptRightButton(true);
mintrp.add(intrp);
intrp = new CircleSelectInterpreter();
intrp.setAcceptLeftButton(false);
intrp.setAcceptRightButton(true);
mintrp.add(intrp);
intrp = new MoveSelectedInterpreter();
intrp.setAcceptLeftButton(true);
intrp.setAcceptRightButton(true);
mintrp.add(intrp);
s.setGestureInterpreter(mintrp);
//// 3. Set up the ink interpreters.
mintrp = new DefaultMultiInterpreterImpl();
intrp = new RandomColorInterpreter();
mintrp.add(intrp);
intrp = new LinearizeStrokeInterpreter();
mintrp.add(intrp);
s.setInkInterpreter(mintrp);
//// 4. Make it so that by default, the Sheet will not add
//// right mouse button strokes as ink.
s.setAddRightButtonStrokes(false);
//// 5. Set up the pie menu.
setupPieMenu(s);
//// 6. Make the frame visible.
f.setSize(600, 600);
f.setVisible(true);
} // of main
//=== SELF-TESTING MAIN =================================================
//===========================================================================
} // of class
|
Here's the multi-class version, the one that is engineered better.
Functionally, it's equivalent to the previous version, but it will
be easier to extend.
Download code v11
Download code v11
import javax.swing.*;
import edu.berkeley.guir.lib.satin.*;
import edu.berkeley.guir.lib.satin.interpreter.*;
import edu.berkeley.guir.lib.satin.interpreter.commands.*;
import edu.berkeley.guir.lib.satin.interpreter.stroke.*;
import edu.berkeley.guir.lib.satin.widgets.*;
import edu.berkeley.guir.lib.satin.command.*;
import edu.berkeley.guir.lib.satin.objects.*;
import edu.berkeley.guir.lib.awt.geom.*;
import java.awt.geom.*;
import java.awt.event.*;
/**
* Sheet used in tutorial.
*/
public class ScribblerSheet
extends Sheet {
//===========================================================================
//=== CONSTANTS =========================================================
/**
* Number of frames to animate when zooming or rotating.
*/
public static final int NUMFRAMES = 10;
//=== CONSTANTS =========================================================
//===========================================================================
//===========================================================================
//=== PIE MENU INNER CLASS CALLBACKS ====================================
class RotateLeftListener implements ActionListener {
public void actionPerformed(ActionEvent evt) {
AffineTransform[] txArray = new AffineTransform[NUMFRAMES];
txArray = AffineTransformLib.animateSlowInSlowOut(
AffineTransform.getRotateInstance(-Math.PI/2,
cmdsubsys.getAbsoluteLastXLocation(),
cmdsubsys.getAbsoluteLastYLocation()),
NUMFRAMES);
GraphicalObjectLib.animate(ScribblerSheet.this, txArray);
} // of method
} // of inner class
//-----------------------------------------------------------------
class RotateRightListener implements ActionListener {
public void actionPerformed(ActionEvent evt) {
AffineTransform[] txArray = new AffineTransform[NUMFRAMES];
txArray = AffineTransformLib.animateSlowInSlowOut(
AffineTransform.getRotateInstance(Math.PI/2,
cmdsubsys.getAbsoluteLastXLocation(),
cmdsubsys.getAbsoluteLastYLocation()),
NUMFRAMES);
GraphicalObjectLib.animate(ScribblerSheet.this, txArray);
} // of method
} // of inner class
//-----------------------------------------------------------------
class ZoomInListener implements ActionListener {
public void actionPerformed(ActionEvent evt) {
AffineTransform[] txArray = new AffineTransform[NUMFRAMES];
txArray = AffineTransformLib.animateSlowInSlowOut(
AffineTransformLib.scaleAndCenterAt(2,
cmdsubsys.getAbsoluteLastXLocation(),
cmdsubsys.getAbsoluteLastYLocation(),
ScribblerSheet.this.getBounds2D(COORD_ABS)),
NUMFRAMES);
GraphicalObjectLib.animate(ScribblerSheet.this, txArray);
} // of method
} // of inner class
//-----------------------------------------------------------------
class ZoomOutListener implements ActionListener {
public void actionPerformed(ActionEvent evt) {
AffineTransform[] txArray = new AffineTransform[NUMFRAMES];
txArray = AffineTransformLib.animateSlowInSlowOut(
AffineTransformLib.scaleAndCenterAt(0.5,
cmdsubsys.getAbsoluteLastXLocation(),
cmdsubsys.getAbsoluteLastYLocation(),
ScribblerSheet.this.getBounds2D(COORD_ABS)),
NUMFRAMES);
GraphicalObjectLib.animate(ScribblerSheet.this, txArray);
} // of method
} // of inner class
//=== PIE MENU INNER CLASS CALLBACKS ====================================
//===========================================================================
//===========================================================================
//=== CONSTRUCTORS ======================================================
public ScribblerSheet() {
setupPieMenu(this);
} // of constructor
//=== CONSTRUCTORS ======================================================
//===========================================================================
//===========================================================================
//=== PIE MENU SETUP ====================================================
protected void setupPieMenu(JComponent comp) {
PieMenu pm = new PieMenu();
//// 1. Add the pie menu slices to the pie menu.
//// Order starts at north, goes counter clockwise.
//// Reason for '\n' is because pie menu is small, so
//// words don't fit correctly. Either resize or go
//// to next line.
pm.add("Zoom\nIn").addActionListener(new ZoomInListener());
pm.add("Rotate\nLeft").addActionListener(new RotateLeftListener());
pm.add("Zoom\nOut").addActionListener(new ZoomOutListener());
pm.add("Rotate\nRight").addActionListener(new RotateRightListener());
//// 2. Add the pie menu to the component.
pm.addPieMenuTo(comp);
} // of method
//=== PIE MENU SETUP ====================================================
//===========================================================================
} // of class
|
Part Six - Recognizers and Interpreters
|
So all of this is interesting and all, but how do we create new gestures, and
do cool things like cut / copy / paste, or even clean up sketches into real
shapes?
That's where Recognizers come in. Recognizers are black boxes
that, given a stroke, return an n-best sorted list of classifications.
That's it. Interpreters act on strokes, whereas Recognizers just classify
strokes. See the diagram below for an example.
Interpreters can use recognizers if they want. Look at the diagram below.
Here's what's happening:
- A "cut" stroke is made
- The stroke is eventually dispatched to the Interpreter
- The Interpreter asks the Recognizer to classify the stroke
- The Recognizer returns an n-best list of what it thinks the stroke is
- The Interpreter gets the top-ranked classification. It then executes
the code to do a cut.
SATIN ships with Rubine's Recognizer, one that can be trained by
example. (It's named after Dean Rubine, who designed and developed it as part
of his PhD Dissertation at Carnegie-Mellon). All you have to do is create
a name for the gesture category and then scribble a few examples. We've
created a tool called
Quill to
help you do this (sometimes you'll hear us call it GDT or Gesture Design
Tool. That was Quill's old name).
So let's show you an example of how gestures work. We'll do this first by
adding some gestures to our sample app. There's an Interpreter we've defined
called StandardGestureInterpreter. It does gestures that we consider
"standard", but you can redefine the gestures if you want.
Note that when you load the app, it takes a little longer before anything
happens. This is because the data file is being read in.
If the gestures aren't working, make sure you have the data/ directory
copied over to whatever directory you're working in. SATIN assumes that there
is a directory called data/interpreters/ in the directory the Java Virtual
Machine is started from. This is where the gesture data is stored.
Download code v12
import javax.swing.*;
import edu.berkeley.guir.lib.satin.*;
import edu.berkeley.guir.lib.satin.interpreter.*;
import edu.berkeley.guir.lib.satin.interpreter.commands.*;
import edu.berkeley.guir.lib.satin.interpreter.stroke.*;
import edu.berkeley.guir.lib.satin.widgets.*;
import edu.berkeley.guir.lib.satin.command.*;
import edu.berkeley.guir.lib.satin.objects.*;
import edu.berkeley.guir.lib.awt.geom.*;
import java.awt.geom.*;
import java.awt.event.*;
/**
* An example of how to use the SATIN toolkit.
* This app demonstrates how the Sheet works with Java Swing.
*/
public class Scribbler {
//===========================================================================
//=== CLASS VARIABLES ===================================================
static JFrame f;
static Sheet s = new Sheet();
//=== CLASS VARIABLES ==================================================
//===========================================================================
//===========================================================================
//=== SELF-TESTING MAIN =================================================
public static void main(String[] argv) {
f = new JFrame();
s = new ScribblerSheet();
//// 1. Make the Sheet the content pane for the frame.
f.setContentPane(s);
s.setBackground(java.awt.Color.white);
//// 2. Set up the gesture interpreters.
MultiInterpreter mintrp;
Interpreter intrp;
mintrp = new DefaultMultiInterpreterImpl();
intrp = new TapSelectInterpreter();
intrp.setAcceptLeftButton(true);
intrp.setAcceptRightButton(true);
mintrp.add(intrp);
intrp = new CircleSelectInterpreter();
intrp.setAcceptLeftButton(false);
intrp.setAcceptRightButton(true);
mintrp.add(intrp);
intrp = new MoveSelectedInterpreter();
intrp.setAcceptLeftButton(true);
intrp.setAcceptRightButton(true);
mintrp.add(intrp);
intrp = new StandardGestureInterpreter();
intrp.setAcceptLeftButton(true);
intrp.setAcceptRightButton(true);
mintrp.add(intrp);
s.setGestureInterpreter(mintrp);
//// 3. Set up the ink interpreters.
mintrp = new DefaultMultiInterpreterImpl();
mintrp.setAcceptRightButton(false);
intrp = new RandomColorInterpreter();
mintrp.add(intrp);
intrp = new LinearizeStrokeInterpreter();
mintrp.add(intrp);
s.setInkInterpreter(mintrp);
//// 4. Make it so that by default, the Sheet will not add
//// right mouse button strokes as ink.
s.setAddRightButtonStrokes(false);
//// 5. Make the frame visible.
f.setSize(600, 600);
f.setVisible(true);
} // of main
//=== SELF-TESTING MAIN =================================================
//===========================================================================
} // of class
|
Gestures
With this new addition, we should be able to do gestures like cut / copy /
paste and so on. Here's a chart of our "standard" gestures. Try scribbling a
few things, and try the gestures. If nothing happens, try the gesture again.
Sometimes there are recognition errors. In fact, try bringing up the debugging
window (Ctrl-Alt-W) to see some of the debugging messages.
The dot in the gesture signifies where you start gesturing.
| Pan |
|
| Undo |
 |
| Redo |
 |
| Cut |
 |
| Copy |
 |
Paste
|
 |
Quill and the Gestures File
As we said before, Quill is a tool for designing gestures. Here's how to load
it up. Go to the directory where the satin and gdt jar files are. Then, either
run run-gdt.bat or java -jar gdt.jar. This will bring
up Quill.
Go to the file-open menu, and go to
data/interpreters/standard.gsa. You should have something that
looks like the following:
Double-click on some of the names on the top-left, like "ViewPortLeft" and
"CutA". It should show you what the gestures look like. Ok, now we're going to
show you the code for how gestures are handled in StandardGestureInterpreter.
You don't have to download or compile this code.
The parts to focus on are in bold and italics.
Like other interpreters, the StandardGestureInterpreter has a method called
handleSingleStroke(). This method asks a
RubineInterpreter inner class
to classify the stroke and take action. (Technically, given our definition of
Recognizer and Interpreter, the RubineInterpreter isn't really an Interpreter
since it doesn't take any actions. It's more of a skeleton for an interpreter.)
//// See bottom of source code for software license
package edu.berkeley.guir.lib.satin.interpreter.commands;
import java.awt.*;
import java.awt.geom.*;
import java.util.*;
import edu.berkeley.guir.lib.awt.geom.*;
import edu.berkeley.guir.lib.debugging.*;
import edu.berkeley.guir.lib.satin.*;
import edu.berkeley.guir.lib.satin.objects.*;
import edu.berkeley.guir.lib.satin.command.*;
import edu.berkeley.guir.lib.satin.recognizer.*;
import edu.berkeley.guir.lib.satin.event.*;
import edu.berkeley.guir.lib.satin.interpreter.*;
import edu.berkeley.guir.lib.satin.interpreter.rubine.*;
import edu.berkeley.guir.lib.satin.stroke.*;
/**
* Standard gestures, like cut, copy, paste, delete, and pan.
*
* <P>
* This software is distributed under the
* <A HREF="http://guir.cs.berkeley.edu/projects/COPYRIGHT.txt">
* Berkeley Software License</A>.
*
* <PRE>
* Revisions: - SATIN-v1.0-1.0.0, Aug 02 1999, JH
* Created class
* - SATIN-v2.1-1.0.0, Aug 11 2000, JH
* Touched for SATIN release
* </PRE>
*
* @author <A HREF="http://www.cs.berkeley.edu/~jasonh/">Jason Hong</A> (
* @since JDK 1.2.2
* @version SATIN-v2.1-1.0.0, Aug 11 2000
*/
public class StandardGestureInterpreter
extends GestureCommandInterpreterImpl {
//===========================================================================
//=== CONSTANTS =========================================================
static final long serialVersionUID = 1118908262546911989L;
//=== CONSTANTS =========================================================
//===========================================================================
//===========================================================================
//=== INNER CLASS =======================================================
class StandardRubineInterpreter
extends RubineInterpreter {
public StandardRubineInterpreter(String str) {
super(str);
} // of constructor
protected void
handleSingleStroke(SingleStrokeEvent evt, Classification c) {
String str = (String) c.getFirstKey();
TimedStroke stk = evt.getStroke();
str = str.trim();
if (stk.getLength2D(COORD_ABS) < 20) {
return;
}
if (str.startsWith("ViewPort")) {
handleViewPort(str, stk);
evt.setConsumed();
}
if (str.startsWith("Center")) {
handleCenter(stk);
evt.setConsumed();
}
if (str.startsWith("Copy")) {
handleCopy(stk);
evt.setConsumed();
}
if (str.startsWith("Cut")) {
handleCut(stk);
evt.setConsumed();
}
if (str.startsWith("Paste")) {
handlePaste(stk);
evt.setConsumed();
}
if (str.startsWith("Delete")) {
handleDelete(stk);
evt.setConsumed();
}
if (str.startsWith("Undo")) {
handleUndo();
evt.setConsumed();
}
if (str.startsWith("Redo")) {
handleRedo();
evt.setConsumed();
}
} // of handleSingleStroke
public Object clone() {
return (new StandardRubineInterpreter(this.getFileName()));
} // of method
} // of class
//=== INNER CLASS =======================================================
//===========================================================================
//===========================================================================
//=== CLASS VARIABLES ===================================================
private static final Debug debug = new Debug();
//=== CLASS VARIABLES ===================================================
//===========================================================================
//===========================================================================
//=== NONLOCAL VARIABLES ================================================
protected boolean flagShallow = true;
protected RubineInterpreter intrp;
//=== NONLOCAL VARIABLES ================================================
//===========================================================================
//===========================================================================
//=== CONSTRUCTORS ======================================================
public StandardGestureInterpreter() {
intrp = new StandardRubineInterpreter("standard.gsa");
commonInitializations();
} // of constructor
//-----------------------------------------------------------------
private void commonInitializations() {
setName("Standard Gesture Interpreter");
} // of commonInitializations
//=== CONSTRUCTORS ======================================================
//===========================================================================
//===========================================================================
//=== ACCESSOR / MODIFIER METHODS =======================================
/**
* Set the GraphicalObject retrieval to be shallow (in terms of the data
* structure, ie one level). This affects how cut, copy, paste, and delete
* work.
*/
public void setShallow() {
flagShallow = true;
} // of setShallow
//-----------------------------------------------------------------
/**
* Set the GraphicalObject retrieval to be deep (in terms of the data
* structure, ie one level). This affects how cut, copy, paste, and delete
* work.
*/
public void setDeep() {
flagShallow = false;
} // of setDeep
//=== ACCESSOR / MODIFIER METHODS =======================================
//===========================================================================
//===========================================================================
//=== HANDLE GESTURE METHODS ============================================
public void handleSingleStroke(SingleStrokeEvent evt) {
intrp.handleSingleStroke(evt);
} // of method
//-----------------------------------------------------------------
/**
* Handle an undo.
* Override this method if you want.
*/
protected void handleUndo() {
try {
cmdqueue.undo();
getAttachedGraphicalObject().damage(DAMAGE_LATER);
}
catch (Exception e) {
debug.println(e);
}
} // of handleUndo
//-----------------------------------------------------------------
/**
* Handle a redo.
* Override this method if you want.
*/
protected void handleRedo() {
try {
cmdqueue.redo();
getAttachedGraphicalObject().damage(DAMAGE_LATER);
}
catch (Exception e) {
debug.println(e);
}
} // of handleRedo
//-----------------------------------------------------------------
/**
* Cut whatever intersects the stroke.
* Override this method if you want.
*/
protected void handleCut(TimedStroke stk) {
GraphicalObjectCollection gobcol;
Iterator it;
GraphicalObject attach;
GraphicalObject gob;
CutCommand cmd = new CutCommand();
gobcol = getGraphicalObjectsTouching(stk, 2);
it = gobcol.getForwardIterator();
attach = getAttachedGraphicalObject();
while (it.hasNext()) {
gob = (GraphicalObject) it.next();
if (gob != attach) {
cmd.addGraphicalObject(gob);
}
}
cmdqueue.doCommand(cmd);
} // of method
//-----------------------------------------------------------------
/**
* Copy whatever is in the center of the stroke.
* Override this method if you want.
*/
protected void handleCopy(TimedStroke stk) {
GraphicalObject gob = getGraphicalObjectCenteredAt(stk);
if (gob != null && gob != getAttachedGraphicalObject()) {
cmdqueue.doCommand(new CopyCommand (gob));
}
} // of handleCopy
//-----------------------------------------------------------------
/**
* Paste to the bottom-center of the stroke.
* Override this method if you want.
*/
protected void handlePaste(TimedStroke stk) {
GraphicalObject gob = getAttachedGraphicalObject();
Rectangle2D rect = stk.getBounds2D(COORD_ABS);
Point2D pt = new Point2D.Float(
(float) (rect.getX() + rect.getWidth()/2),
(float) (rect.getY() + rect.getHeight()));
GraphicalObjectLib.absoluteToLocal(gob, pt, pt);
if (gob instanceof GraphicalObjectGroup) {
cmdqueue.doCommand(new PasteCommand((GraphicalObjectGroup) gob,
pt.getX(), pt.getY()));
}
} // of handlePaste
//-----------------------------------------------------------------
/**
* Delete whatever is in the center of the stroke (except ourself of course).
* Override this method if you want.
*/
protected void handleDelete(TimedStroke stk) {
GraphicalObject gob = getGraphicalObjectCenteredAt(stk);
if (gob != null && gob != getAttachedGraphicalObject()) {
cmdqueue.doCommand(new DeleteCommand (gob));
}
} // of handleDelete
//-----------------------------------------------------------------
/**
* Center the screen to be at the mark.
* Override this method if you want.
*/
protected void handleCenter(TimedStroke stk) {
GraphicalObject gob = getAttachedGraphicalObject();
Rectangle2D gobbds = gob.getBounds2D();
Rectangle2D stkbds = stk.getBounds2D();
double x1 = gobbds.getX() + gobbds.getWidth()/2;
double y1 = gobbds.getY() + gobbds.getHeight()/2;
double x2 = stkbds.getX() + stkbds.getWidth()/2;
double y2 = stkbds.getY() + stkbds.getHeight()/2;
AffineTransform[] txArray;
txArray = AffineTransformLib.animateSlowInSlowOut(
AffineTransform.getTranslateInstance(x1 - x2, y1 - y2), 10);
GraphicalObjectLib.animate(gob, txArray);
} // of handleCenter
//-----------------------------------------------------------------
/**
* Handle a move view port command.
* Override this method if you want.
*/
protected void handleViewPort(String str, TimedStroke stk) {
AffineTransform tx = new AffineTransform();
GraphicalObject gob = getAttachedGraphicalObject();
double len = stk.getLength2D(COORD_ABS);
if (str.equalsIgnoreCase("ViewPortLeft")) {
tx.translate(+len, 0);
}
if (str.equalsIgnoreCase("ViewPortRight")) {
tx.translate(-len, 0);
}
if (str.equalsIgnoreCase("ViewPortUp")) {
tx.translate(0, +len);
}
if (str.equalsIgnoreCase("ViewPortDown")) {
tx.translate(0, -len);
}
if (str.equalsIgnoreCase("ViewPortDiagUpRight")) {
tx.translate(-len/2, +len/2);
}
if (str.equalsIgnoreCase("ViewPortDiagUpLeft")) {
tx.translate(+len/2, +len/2);
}
if (str.equalsIgnoreCase("ViewPortDiagDownRight")) {
tx.translate(-len/2, -len/2);
}
if (str.equalsIgnoreCase("ViewPortDiagDownLeft")) {
tx.translate(+len/2, -len/2);
}
AffineTransform[] txArray;
txArray = AffineTransformLib.animateSlowInSlowOut(tx, 8);
GraphicalObjectLib.animate(gob, txArray);
} // of method
//=== HANDLE GESTURE METHODS ============================================
//===========================================================================
//===========================================================================
//=== GET GOB METHODS ===================================================
/**
* Get things that intersect the stroke.
* Override this method to change the behavior of how objects intersecting
* a stroke are selected. For example, in DENIM, this method is modified
* such that DenimPanel objects can be deleted when zoomed out (ie shallow),
* and strokes and phrases can be deleted when zoomed in (ie deep).
*/
protected GraphicalObjectCollection
getGraphicalObjectsTouching(TimedStroke stk, double thresh) {
//// 0. Don't do anything if we're not a group.
if (! (getAttachedGraphicalObject() instanceof GraphicalObjectGroup)) {
return (null);
}
GraphicalObject gob = getAttachedGraphicalObject();
GraphicalObject gobtmp = null;
Shape s = stk.getBoundingPoints2D(COORD_ABS);
GraphicalObjectGroup gobgrp = (GraphicalObjectGroup) gob;
GraphicalObjectCollection gobcol = new GraphicalObjectCollectionImpl();
int depth;
//// 1. Set for shallow / deep search.
if (flagShallow == true) {
depth = SHALLOW;
}
else {
depth = DEEP;
}
//// 2.1. Try searching using the stroke bounds.
gobcol = gobgrp.getGraphicalObjects(COORD_ABS,
s,
ALL,
depth,
INTERSECTS,
thresh,
gobcol);
return (gobcol);
} // of method
//-----------------------------------------------------------------
/**
* Get the GraphicalObject near the center of this TimedStroke.
*/
protected GraphicalObject
getGraphicalObjectCenteredAt(TimedStroke stk) {
return (getGraphicalObjectCenteredAt(stk, DEFAULT_SELECT_THRESHOLD));
} // of method
//-----------------------------------------------------------------
/**
* Get the GraphicalObject near the center of this TimedStroke, with
* the specified threshold.
*/
protected GraphicalObject
getGraphicalObjectCenteredAt(TimedStroke stk, double thresh) {
GraphicalObjectCollection gobcol;
GraphicalObject gobtmp = null;
//// 1. Get the graphical objects touching the stroke.
gobcol = getGraphicalObjectsTouching(stk, thresh);
//// 2. Return the topmost Graphical Object first.
if (gobcol.numElements() > 0) {
gobtmp = GraphicalObjectLib.getTopmostGraphicalObject(gobcol);
}
return (gobtmp);
} // of method
//=== GET GOB METHODS ===================================================
//===========================================================================
//===========================================================================
//=== CLONE =============================================================
public Object clone() {
return (new StandardGestureInterpreter());
} // of clone
//=== CLONE =============================================================
//===========================================================================
} // of class
//==============================================================================
/*
Copyright (c) 2000 Regents of the University of California.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. All advertising materials mentioning features or use of this software
must display the following acknowledgement:
This product includes software developed by the Group for User
Interface Research at the University of California at Berkeley.
4. The name of the University may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
*/
|
Using RubineInterpreter to Handle Strokes
Note how the stroke is handled in the inner class. It has code like:
if (str.startsWith("Copy")) {
handleCopy(stk);
evt.setConsumed();
}
if (str.startsWith("Cut")) {
handleCut(stk);
evt.setConsumed();
}
Compare the names in the standard.gsa file in Quill with the names here.
You'll see that they are very similar. The RubineInterpreter classifies the
stroke and also gives us the gesture category name. Once we have the
name of the gesture category, we just act on it.
Another thing to note is that we have lots of strange names for gestures, like
"CutA" and "CutB". Why are there multiple Cut gesture categories? It turns out
that this is to overcome one problem with Rubine's Recognizer. Rubine's
Recognizer doesn't work well with gestures that are different sizes or rotated.
To overcome this problem, we just created separate categories for these kinds
of gestures, but named them similarly.
Then, in the Interpreter, instead of doing an exact String match (with
.equals()) we did a .startsWith() match.
Problem solved.
If you have created a gesture file with Quill and are using it in SATIN, you
will notice that sometimes you will get "tap" as the recognized gesture even
though "tap" is not defined. This indicates an error condition meaning that
there aren't enough gestures and/or gesture categories defined (or that
they aren't sufficiently different for the recognizer to classify). Adding
a few more of each will likely solve the problem.
Part Seven - Graphical Objects
|
We're going to take a slight detour before coming back to recognizers and
interpreters. So we've seen that we can have ink strokes on the Sheet.
Can we have anything else? The answer is yes!
If you looked at the code for StandardGestureInterpreter, you'll notice
that there are references to something called a
GraphicalObject. A GraphicalObject is an object that can be displayed and has some behaviors. It turns out that the Sheet is also a GraphicalObject.
So let's go through an example of how to add some GraphicalObjects to the
Sheet. We'll give some code first, and then describe what's going on after.
Make the changes, compile, and run. Below is a screenshot of what should appear.
Download code v13
import javax.swing.*;
import edu.berkeley.guir.lib.satin.*;
import edu.berkeley.guir.lib.satin.interpreter.*;
import edu.berkeley.guir.lib.satin.interpreter.commands.*;
import edu.berkeley.guir.lib.satin.interpreter.stroke.*;
import edu.berkeley.guir.lib.satin.widgets.*;
import edu.berkeley.guir.lib.satin.command.*;
import edu.berkeley.guir.lib.satin.objects.*;
import edu.berkeley.guir.lib.awt.geom.*;
import java.awt.geom.*;
import java.awt.event.*;
import java.awt.*;
/**
* An example of how to use the SATIN toolkit.
* This app demonstrates how the Sheet works with Java Swing.
*/
public class Scribbler {
//===========================================================================
//=== CLASS VARIABLES ===================================================
static JFrame f;
static Sheet s = new Sheet();
//=== CLASS VARIABLES ==================================================
//===========================================================================
//===========================================================================
//=== SELF-TESTING MAIN =================================================
public static void main(String[] argv) {
f = new JFrame();
s = new ScribblerSheet();
//// 1. Make the Sheet the content pane for the frame.
f.setContentPane(s);
s.setBackground(java.awt.Color.white);
//// 2. Set up the gesture interpreters.
MultiInterpreter mintrp;
Interpreter intrp;
mintrp = new DefaultMultiInterpreterImpl();
intrp = new TapSelectInterpreter();
intrp.setAcceptLeftButton(true);
intrp.setAcceptRightButton(true);
mintrp.add(intrp);
intrp = new CircleSelectInterpreter();
intrp.setAcceptLeftButton(false);
intrp.setAcceptRightButton(true);
mintrp.add(intrp);
intrp = new MoveSelectedInterpreter();
intrp.setAcceptLeftButton(true);
intrp.setAcceptRightButton(true);
mintrp.add(intrp);
intrp = new StandardGestureInterpreter();
intrp.setAcceptLeftButton(true);
intrp.setAcceptRightButton(true);
mintrp.add(intrp);
s.setGestureInterpreter(mintrp);
//// 3. Set up the ink interpreters.
mintrp = new DefaultMultiInterpreterImpl();
mintrp.setAcceptRightButton(false);
intrp = new RandomColorInterpreter();
mintrp.add(intrp);
intrp = new LinearizeStrokeInterpreter();
mintrp.add(intrp);
s.setInkInterpreter(mintrp);
//// 4. Make it so that by default, the Sheet will not add
//// right mouse button strokes as ink.
s.setAddRightButtonStrokes(false);
//// 5. Add some rectangular objects to the Sheet.
GraphicalObject gob;
gob = new PatchImpl new Rectangle(0, 0, 150, 150));
s.add(gob);
gob = new PatchImpl new Rectangle(200, 100, 150, 150));
s.add(gob);
//// 6. Make the frame visible.
f.setSize(600, 600);
f.setVisible(true);
} // of main
//=== SELF-TESTING MAIN =================================================
//===========================================================================
} // of class
|
Patches
In the code, we added two PatchImpl objects.
PatchImpl implements the
Patch
interface. Think of patches as windows, except that they can be arbitrarily
shaped.
You should be able to select, move, copy, cut, and paste these two Patch
objects the same as any other objects. It turns out that most of the gestures
we have implemented work on GraphicalObjects. Strokes and Patches are just
two subclasses of GraphicalObject.
Patch objects can actually contain other objects. In the screenshot above, you
should be able to see this. In fact, if you move the Patch, the objects within
also move.
However, one interesting thing to note is that the strokes inside of the Patch
aren't straight and are all black! But strokes outside are straightened and
have the random color assigned. The reason for this is that Patches
also have their own gesture and ink interpreters. Again,
check out the SATIN powerpoint
slides for an animation on exactly how stroke dispatching works.
Other things to know about Patches:
- Patches have Style objects that you can change.
- Patches can be shapes other than rectangular. Pass in a Polygon
into it and see what happens. In fact, you can pass in any
java.awt.Shape object into it. (Sorry, it doesn't handle curves very
well yet).
- Patches can have their own gesture and ink interpreters.
Part Eight - Tying Interpreters and Graphical Objects Together
|
In this part, we show one way to integrate Interpreters with
GraphicalObjects, by doing some clean-up of recognized shapes. Below is
an example of clean-up. Given a sketch that sort of looks like a rectangle,
replace it with a Patch that is rectangular.
Below is another example of clean-up. SketchySPICE, one of the sample
applications that ships with SATIN, lets you do very simple recognition
and clean-up of sketched AND and OR gates.

Below, we show one way of doing clean-up. The general idea is:
- Run the stroke through an interpreter that recognizes strokes
- See what the top-ranked recognized object is
- Replace the stroke with a GraphicalObject that represents the recognized
object
[This part in progress.]
[Add in how to use Quilt output file]
By default, all Quill output files (*.gsa) are read in from the
data/interpreters directory (where "data" is off of the directory where
you start your Java program). These files can then be read in from a sublcass
of RubineInterpreter. For example, suppose you had a gesture file named
"alphabet.gsa". Here's a code fragment to read it in:
...
public class AlphabetGestureInterpreter
extends RubineInterpreter {
// constructor
public AlphabetGestureInterpreter() {
super("alphabet.gsa");
} // of constructor
} // of class
There are other constructors for doing more sophisticated operations.
See the RubineInterpreter javadoc for more information.
[Add in how to use and override RubineInterpreter]
[Add in how to create a ReplaceShapeInterpreter]
Part Nine - Dynamic Views
|
This section discusses how dynamic views work in SATIN. You can make
applications with SATIN without having to mess with views, but it's here in
case you want to do more sophisticated operations.
Views let you change how objects are displayed on screen. A GraphicalObject
can have multiple views, which can change depending on context. For example,
you could change the view to show more information when zoomed in. This is
known as semantic zooming.
[This part in progress.]
[Describe reasons for multiple views]
[Show how semantic zoom views work]
|