Piccolo2D Patterns
Below, we explain the common programming Paradigms associated with Piccolo2D. This work is based heavily on the work of R. Johnson in his seminal 1992 paper Documenting Frameworks using Patterns and the work of Douglas Kirk in his Patterns for JHotDraw paper. This work merely attempts to adapt the concepts presented there into a format suitable for our Piccolo2D framework.Intro
Pattern 1: Zoomable User Interfaces (ZUI's)
Piccolo2D is a framework for building zoomable user interfaces. The elements of these interfaces can be scaled or animated, and react to user input. The user viewpoint may also be scaled and panned. Interfaces built with Piccolo2D can define your entire application interface, or they can be a small part of a large system.
Figure 1 shows a sequence of frames from a simple zooming interface created using Piccolo2D. This example shows the fundamental aspects of a Piccolo2D application. In this sequence the user is zooming in on a "Hello world!" interface object.
The objects on a piccolo2d canvas are nodes (instances of PNode
), in this case a PText
node. Nodes are used to represent the discrete components of an interface. The framework comes with some commonly required nodes (shapes, images, and text) and the developer has the opportunity to define new ones for their own interfaces.
Figure 1's zooming interaction was created with the help of an event listener (instances of PInputEventListener
). Event listeners define how the interface reacts to user input. In this zooming interaction the event listener reacted to mouse input by manipulating the camera node's view transform to create the zooming effect. Piccolo2D comes with ready-made event listeners for zooming and panning camera nodes, and dragging nodes on the canvas.
All Piccolo2D interfaces need to be placed in a PCanvas
so that they may be viewed and interacted with by the user. PCanvas
extends the javax.swing.JComponent
class in Piccolo2D.Java and the equivalent System.Windows.Forms.Control
class in Piccolo2D.NET. As a result, piccolo2d can easily be integrated with both Java Swing and Windows applications. In addition to hosting Piccolo2D in the application interface, the canvas also maintains a camera and layer node. Scaling and translating the camera's view transform over time is how zooming and panning is accomplished. New nodes are normally added to the canvas's layer node.
The interface depicted in figure 1 can be created with the following code:
import edu.umd.cs.piccolo.PCanvas; import edu.umd.cs.piccolo.nodes.PText; import edu.umd.cs.piccolox.PFrame; // The PFrame class is a utility class that creates a new java window // and adds a PCanvas to it. You can override the initialize method and // start building your interface there. public class Piccolo2DExample extends PFrame { public void initialize() { PNode aNode = new PText("Hello World!"); // Add the node to the canvas layer so that it // will be displayed on the screen. getCanvas().getLayer().addChild(aNode); } public static void main(String[] args) { new Piccolo2DExample(); } }
using UMD.HCIL.Piccolo2D; using UMD.HCIL.Piccolo2D.Nodes; using UMD.HCIL.Piccolo2DX; // The PForm class is a utility class that creates a new window // and adds a PCanvas to it. You can override the Initialize method and // start building your interface there. public class Piccolo2DExample : PForm { public override void Initialize() { PText text = new PText("Hello World"); // Add the node to the canvas layer so that it // will be displayed on the screen. Canvas.Layer.AddChild(text); } [STAThread] static void Main() { Application.Run(new Piccolo2DExample()); } }
Note: For simplicity, the C# example does not include the initialization code inserted by Visual Studio.
This creates the default zooming interface. A new text node is added to the surface, and event handlers for zooming and panning are automatically installed by the PCanvas
.
- To quickly get started using Piccolo2D, see Usage Patterns for Piccolo2D
- To learn more about the Piccolo2D implementation, see Implementation Patterns for Piccolo2D
- To learn more about common ZUI design patterns, see Design Patterns for ZUI Interfaces
Usage Patterns for Piccolo2D
The Piccolo2D usage patterns are designed to get you up and running with Piccolo2D as quickly as possible. They provide quick "cookbook" explanations of the major things that can be done with Piccolo2D. To gain a better understanding of the Piccolo2D implementation and of its runtime behavior is see Implementation Patterns for Piccolo2D.
- To create nodes see, Creating Nodes
- To create user interactions, see Adding User Interaction
- To create animations, see Activities
Pattern 2: Building and Using Piccolo2D
To use Piccolo2D.Java, you will need to add the appropriate .jar files to your classpath, and to do that you need to know what each .jar file contains, and how to build the framework.
Piccolo2D.Java comes with 4 .jar files:
- piccolo.jar
- This .jar contains the Piccolo2D 2D zooming graphics framework.
- piccolox.jar
- This .jar contains the Piccolo2D extras, and it depends on piccolo.jar
- examples.jar
- This .jar contains simple examples of Piccolo2D, it depends on piccolo.jar and piccolox.jar.
- tests.jar
- This .jar contains unit tests for classes in the Piccolo2D framework, it depends on piccolo.jar and /lib/junit.jar
If you downloaded the compiled distribution of Piccolo2D.Java these .jar files are located for you in "piccolo2d/build/". If you didn't then you will need to build them yourself. This is done by typing 'build all' from within the piccolo2d/ folder.
Piccolo2D.Java also comes with project files for the Eclipse IDE (www.eclipse.org), our favorite development environment for Piccolo2D.Java, and Java in general.
To use Piccolo2D.NET, you will need to add references to the appropriate .dll files in your Visual Studio Project.
Piccolo2D.NET comes with 2 .dll files and 2 .xml files:
- UMD.HCIL.Piccolo2D.dll
- This .dll contains the Piccolo2D.NET 2D zooming graphics framework
- UMD.HCIL.Piccolo2DX.dll
- This .dll contains the Piccolo2D extras, and it depends on UMD.HCIL.Piccolo2D.dll.
- UMD.HCIL.Piccolo2D.xml
- This xml file is necessary to provide intellisense support for UMD.HCIL.Piccolo2D.dll, within the Visual Studio.NET IDE
- UMD.HCIL.Piccolo2DX.xml
- This xml file is necessary to provide intellisense support for UMD.HCIL.Piccolo2DX.dll, within the Visual Studio.NET IDE
You do not need to build either distribution of Piccolo2D.NET. The DLL distribution contains all four of these files at the top level. And, the source distribution contains these files in "Piccolo2D.NET/Bin/".
However, if you modify the source code, then you will need to rebuild the framework. To do this, open "Piccolo2D.NET/Source/Piccolo2D.Net.sln" in Visual Studio.NET. Then, from the "Build" menu select "Rebuild Solution." The new .dll files will be located in "Piccolo2D.NET/Source/Piccolo2D/bin/Debug" and "Piccolo2D.NET/Source/Piccolo2DX/bin/Debug".
Note: For more detailed instructions on setting up Piccolo2D, see the README file that comes with each distribution and the Getting Started tutorial.
Pattern 3: Creating Nodes
There are an infinite variety of nodes that can be included in a zoomable user interface. And, there are several ways to make new nodes for each application.
- Use existing classes
- Piccolo2D is supplied with the default implementations for three kinds of visual nodes:
PText
,PPath
, andPImage
. These may be used ‘as is' in a new application, which saves developers the time and effort required to create such elements themselves, but reduces the knowledge they have about how that figure is implemented. - Subclass existing classes
- Often the default implementations of
PNodes
are ‘almost but not quite' what is required for an interface. In such circumstances, it makes sense to customize the existing node to fit the needs of the interface. This saves time compared with starting from scratch and provides insight into the organization of that node but subclassing will increase the number of classes in the system and carries the responsibility of ensuring that the original intent of that inheritance hierarchy is maintained. - Composition
- An alternative to subclassing, composition can be used in situations where the new functionality required can be created by arranging several preexisting nodes together. A text label could be defined as a rectangle (to define the border) with a text object inside. To facilitate this kind of creation any node may have other child nodes added to them. Adding a text node as a child of the rectangle node could create the text label mentioned above. Composition frees the developer from class details, allows reuse of existing tools and allows dynamic configuration (new arrangements of figures can be constructed while the program runs). But, composition doesn't provide as much freedom as subclassing (it can only compose what is available). And, because of its dynamic nature, it can be difficult to debug problems.
- Custom Node
- The ultimate amount of creative freedom comes from subclassing
PNode
directly.PNode
already provides default behavior for all operations (it is a concrete class). So, it is fairly easy to subclass. Many subclasses will want to override a few key methods:setBounds()
,intersects()
, andpaint()
in Piccolo2D.Java andSetBounds()
,Intersects()
andPaint()
in Piccolo2D.NET.
Each visual interface element in Piccolo2D is a subclass of PNode
. Nodes can be used directly or customized by sub classing or composition.
The following code defines a new simple (without a border) ellipse node.
public class SimpleEllipseNode extends PNode { private Ellipse2D ellipse; // This nodes uses an internal Ellipse2D to define its shape. public Ellipse2D getEllipse() { if (ellipse == null) ellipse = new Ellipse2D.Double(); return ellipse; } // This method is important to override so that the geometry of // the ellipse stays consistent with the bounds geometry. public boolean setBounds(double x, double y, double width, double height) { if(super.setBounds(x, y, width, height)) { ellipse.setFrame(x, y, width, height); return true; } return false; } // Non rectangular subclasses need to override this method so // that they will be picked correctly and will receive the // correct mouse events. public boolean intersects(Rectangle2D aBounds) { return getEllipse().intersects(aBounds); } // Nodes that override the visual representation of their super // class need to override a paint method. public void paint(PPaintContext aPaintContext) { Graphics2D g2 = aPaintContext.getGraphics(); g2.setPaint(getPaint()); g2.fill(getEllipse()); } }
public class SimpleEllipseNode : PNode { private GraphicsPath ellipse; // This node uses an internal GraphicsPath to define its shape. public GraphicsPath Ellipse { get { if (ellipse == null) ellipse = new GraphicsPath(); return ellipse; } } // This method is important to override so that the geometry of // the ellipse stays consistent with the bounds geometry. public override bool SetBounds(float x, float y, float width, float height) { if(base.SetBounds (x, y, width, height)) { Ellipse.Reset(); Ellipse.AddEllipse(x, y, width, height); return true; } return false; } // Non rectangular subclasses need to override this method so // that they will be picked correctly and will receive the // correct mouse events. public override bool Intersects(RectangleF bounds) { Region ellipseRegion = new Region(Ellipse); return ellipseRegion.IsVisible(bounds); } // Nodes that override the visual representation of their super // class need to override a paint method. protected override void Paint(PPaintContext paintContext) { Graphics g = paintContext.Graphics; if (Brush != null) { g.FillPath(Brush, Ellipse); } } }
- To let the user change the attributes of a node, see Adding user Interaction
- To constrain the layout of a nodes children, see Layout Constraints
Pattern 4: Adding User Interaction
Event listeners represent the modes of interaction between the user and the interface. Piccolo2D comes with event listeners that let the user zoom and pan their viewpoint, and drag nodes in the interface. An important part of designing an interface using Piccolo2D is to design the set of event listeners that will define the user experience.
Once created an event listener must be registered with a node so that it can receive events. Many event handlers register with the camera node so that they get all events that come from the canvas associated with that camera.
This example class creates an event listener that will create rectangle nodes on the canvas's layer when the user presses, drags, and then releases the mouse.
// This event listener works by keeping track of the mouse press // location and the current mouse drag location. It then sizes the new // rectangle around those points. Note: The implementation of this event // handler could be simplified by sub classing PDragSequenceEventHandler. public class RectangleCreationEventHandler extends PBasicInputEventHandler { // The rectangle that is currently getting created. protected PPath rectangle; // The mouse press location for the current pressed, drag, release // sequence. protected Point2D pressPoint; // The current drag location. protected Point2D dragPoint; public void mousePressed(PInputEvent e) { super.mousePressed(e); PLayer layer = ((PCanvas)(e.getComponent())).getLayer(); // Initialize the locations. pressPoint = e.getPosition(); dragPoint = pressPoint; // create a new rectangle and add it to the canvas layer so // that we can see it. rectangle = new PPath(); rectangle.setStroke(new BasicStroke( (float)(1/ e.getCamera().getViewScale()))); layer.addChild(rectangle); // update the rectangle shape. updateRectangle(); } public void mouseDragged(PInputEvent e) { super.mouseDragged(e); // update the drag point location. dragPoint = e.getPosition(); // update the rectangle shape. updateRectangle(); } public void mouseReleased(PInputEvent e) { super.mouseReleased(e); // update the rectangle shape. updateRectangle(); rectangle = null; } public void updateRectangle() { // create a new bounds that contains both the press and // current drag point. PBounds b = new PBounds(); b.add(pressPoint); b.add(dragPoint); // Set the rectangles bounds. rectangle.setPathTo(b); } }
// This event listener works by keeping track of the mouse press // location and the current mouse drag location. It then sizes the new // rectangle around those points. Note: The implementation of this event // handler could be simplified by sub classing PDragSequenceEventHandler. public class RectangleCreationEventHandler : PBasicInputEventHandler { // The rectangle that is currently getting created. protected PPath rectangle; // The mouse press location for the current pressed, drag, release // sequence. protected PointF pressPoint; // The current drag location. protected PointF dragPoint; public override void OnMouseDown(object sender, PInputEventArgs e) { base.OnMouseDown (sender, e); PLayer layer = e.Canvas.Layer; // Initialize the locations. pressPoint = e.Position; dragPoint = pressPoint; // Create a new rectangle and add it to the canvas layer so // that we can see it. rectangle = new PPath(); rectangle.Pen = new Pen(Brushes.Black, (float)(1/ e.Camera.ViewScale)); layer.AddChild(rectangle); // update the rectangle shape. UpdateRectangle(); } public override void OnMouseDrag(object sender, PInputEventArgs e) { base.OnMouseDrag (sender, e); // Update the drag point location. dragPoint = e.Position; // Update the rectangle shape. UpdateRectangle(); } public override void OnMouseUp(object sender, PInputEventArgs e) { base.OnMouseUp (sender, e); // Update the rectangle shape. UpdateRectangle(); rectangle = null; } public void UpdateRectangle() { // Create a new bounds that contains both the press and // current drag point. RectangleF r = RectangleF.Empty; r = PUtil.AddPointToRect(r, pressPoint); r = PUtil.AddPointToRect(r, dragPoint); // Set the rectangles bounds. rectangle.PathReference.Reset(); rectangle.AddRectangle(r.X, r.Y, r.Width, r.Height); } }
The code to register this event listener with the canvas would look like this:
getCanvas().addInputEventListener(new RectangleCreationEventHandler());
Canvas.AddInputEventListener(new RectangleCreationEventHandler());
Note that this event handler reacts to the same events that the zoom and pan event handlers react to. If they are all active on the canvas at the same time undesired behavior would occur. Often you will want to remove the default pan and zoom event handlers associated with the PCanvas
, this can be done as follows:
getCanvas().removeInputEventListener(getCanvas().getZoomEventHandler()); getCanvas().removeInputEventListener(getCanvas().getPanEventHandler());
Canvas.RemoveInputEventListener(Canvas.ZoomEventHandler); Canvas.RemoveInputEventListener(Canvas.PanEventHandler);
In Piccolo2D.NET you can also use the following shortcut:
C#
Canvas.ZoomEventHandler = null; Canvas.PanEventHandler = null;
A general problem when defining global event handlers is making sure that they do not conflict. The Java PInputEventFilter
class and the C# PInputEventListener.DoesAcceptEvent()
method can help in this regard.
In Piccolo2D.NET there is another way to handle basic input events. You can connect an event handler method directly to a node in the same way that .NET event handlers can be connected to a control. This allows you to define your event handlers in style consistent with .NET events. For example, the following code prints a message every time the user clicks the green node:
C#
public override void Initialize() { PNode aNode = new PNode(); aNode.SetBounds(0,0, 100, 100); aNode.Brush = Brushes.Green; Canvas.Layer.AddChild(aNode); aNode.MouseDown += new PInputEventHandler(aNode_MouseDown); } protected void aNode_MouseDown(object sender, PInputEventArgs e) { System.Console.WriteLine("Clicked aNode!"); }
For more information about the tradeoffs of each of these approaches see the Defining User Interaction tutorial on the Getting Started page.
- To learn more about coordinate systems, see Coordinate Systems
- To learn more about event dispatch, see Dispatching Events
Pattern 5: Layout Constraints
Often an interface has constraints that must be maintained between a node and its children. For example a node may want to always make its children line up in a row or a node may wish to expand its base size to always fully contain the bounds of its children.
The PNode
class does no automatic layout of its own, but it provides methods that subclasses can override to perform layout in the appropriate place during the layout process.. The following is a simple layout node that overrides and lays its children out in a horizontal row.
PNode layoutNode extends PNode { public void layoutChildren() { double xOffset = 0; double yOffset = 0; Iterator i = getChildrenIterator(); while (i.hasNext()) { PNode each = (PNode) i.next(); each.setOffset(xOffset - each.getX(), yOffset); xOffset += each.getFullBoundsReference().getWidth(); } } }
public class LayoutNode : PNode { public override void LayoutChildren() { float xOffset = 0; float yOffset = 0; PNodeList children = this.ChildrenReference; foreach (PNode each in children) { each.Offset = new PointF(xOffset - each.X, yOffset); xOffset += each.FullBounds.Width; } } }
- For more information on the layout process, see Validating Bounds
- For more information on the "full" term used in fullBounds, see Full Terminology
Pattern 6: Activities
Event handlers let an interface react to a user. Activities are used to give the interface a life of its own through the use of animation and other "scheduled" behaviors.
Activities control some time-dependent aspect of the Piccolo2D system, usually some part of a node. This behavior may be of fixed duration or may continue until some termination condition is met (or perhaps forever). Activities of fixed duration may be defined to consume a fixed amount of time, independent of the frame rate.
This method sets up a flash activity that flashes the given node's color from red to green for 5 seconds.
public void flashNode(final PNode aNode) { PActivity flash = new PActivity(5000) { boolean fRed = true; protected void step(long time) { super.step(time); if (fRed) { aNode.setPaint(Color.red); } else { aNode.setPaint(Color.green); } fRed = !fRed; } }; // Must schedule the activity with the root for it to run. aNode.getRoot().addActivity(flash); }
public void flashNode(PNode aNode) { PActivity flash = new PActivity(5000); flash.ActivityStepped = new ActivitySteppedDelegate(ActivityStepped); // Must schedule the activity with the root for it to run. Canvas.Root.AddActivity(flash); } protected void ActivityStepped(PActivity activity) { if (fRed) { aNode.Brush = Brushes.Red; } else { aNode.Brush = Brushes.Green; } fRed = !fRed; }
Activities are scheduled by the PRoot
until they have completed. Note that for animation activities you can also use the convenience methods in PNode
:
public PTransformActivity animateToPositionScaleRotation(double x, double y, double scale, double theta, long duration); public PTransformActivity animateToTransform(AffineTransform aDestination, long duration);
public virtual PTransformActivity AnimateToPositionScaleRotation(float x, float y, float scale, float theta, long duration); public virtual PTransformActivity AnimateToMatrix(PMatrix destMatrix, long duration);
Each activity has a start time and a duration, that together determine when an activity starts stepping and how long it continues to step. The Java PActivity.startAfter()
and the C# PActivity.StartAfter()
methods may be used to sequence an activity so that it starts right after another has stopped.
- To learn more about activities see, Processing Activities
Pattern 7: Standard Widget Toolkit (SWT) connection
SWT is a new Java toolkit (similar to and replacing Swing) that comes out of the Eclipse IDE project. Piccolo2D.Java was originally designed to work with swing, but now has preliminary SWT support as well. You will most likely use SWT if you are writing Piccolo2D extensions to the Eclipse IDE. See www.eclipse.org for more info on the Eclipse project.
Again SWT support is not complete. We are interested in your feedback and hope it is of use, but we can not promise future updates. SWT support is provided in the extras package in edu.umd.cs.piccolox.swt
, and you will find SWT examples in the examples package. Here is the code for SWT hello world:
Java
public class SWTHelloWorld { public SWTHelloWorld() { super(); } public static void main(String[] args) { Display display = new Display (); Shell shell = open(display); while (!shell.isDisposed()) { if (!display.readAndDispatch()) display.sleep(); } display.dispose(); } public static Shell open(Display display) { final Shell shell = new Shell(display); shell.setLayout(new FillLayout()); PSWTCanvas canvas = new PSWTCanvas(shell,0); PSWTText text = new PSWTText("Hello World"); canvas.getLayer().addChild(text); shell.open(); return shell; } }
Implementation Patterns for Piccolo2D
The Piccolo2D implementation patterns are designed to give a general understanding of how Piccolo2D is implemented and what its runtime behaviors are. This is opposed to the patterns in Usage Patterns For Piccolo2D that are designed to get you using the framework as quickly as possible.
Pattern 8: Piccolo2D Framework Design
You should be familiar with the basic concepts of the Piccolo2D framework design and how they relate to each other to effectively use Piccolo2D.
Piccolo2D is a direct-manipulation graphics framework that supports constructing zoomable interfaces. The framework's design borrows heavily from the designs of both the Jazz and Morphic interface frameworks.
Piccolo2D Class Hierarchy
There are four main classes that define the framework's core:
-
PNode
(anything that is visible and gets events) -
PCamera
(a node that looks at other layer nodes, and applies a view transform) -
PLayer
(a node that can be looked at by a camera) -
PRoot
(the root of the Piccolo2D display tree) -
PCanvas
(the host component that letsPNodes
exist in a Java Swing or .NET Windows application. EachPCanvas
is associated with aPCamera
. But all cameras are not necessarily directly associated with aPCanvas
, internal cameras for example are not.)
Piccolo2D Runtime Structure
At runtime these classes form a tree-like structure with the PRoot
situated at the top. Each PCamera
is normally linked with at least one PLayer
that it looks at through it's view transform. If a camera is associated with a canvas then that camera's view is displayed on the canvas, and input events from the canvas enter the Piccolo2D scene graph at that camera's point in the hierarchy.
PNode
Nodes are the central design concept in Piccolo2D. Any object that wants to paint itself on the screen should inherit from the node class. In addition to painting on the screen all nodes may have other "child" nodes added to them. Visual structures are build up by grouping and sub grouping collections of nodes.
Each node also has its own affine transform that is applied before the node is drawn to the screen. This transform can be modified to scale and translate the node. The transform exists directly above the node, but below the node's parent. Thus, translating it will translate the node (and all its descendents) but will not translate the node's parent.
PCamera
Cameras are nodes that have an additional view transform and a collection of layers in addition to the collection of children that they inherit from PNode
. The view transform is applied before drawing or picking the layers, but not when drawing or picking the camera's children. Cameras may (an internal camera might not) also reference a PCanvas
, and forward repaint events to that canvas. The canvas would then later ask the camera to draw the damaged region on its surface.
PLayer
Layer nodes are nodes that can be viewed by one or more cameras. They maintain a list of the cameras that are viewing them, and notify these cameras when they are repainted.
PRoot
The PRoot
serves as the topmost node in the Piccolo2D runtime structure; all other nodes are its direct children or descendents of its children. The PCanvas
communicates with the root node to manage screen updates and to dispatch events to its children.
PCanvas
The PCanvas
is a JComponent
in Piccolo2D.Java and a Control
in Piccolo2D.NET. Thus, it is used to view a Piccolo2D scene graph in Java Swing and .NET Windows applications respectively. The PCanvas
views the scene graph through a PCamera
. It forwards input events to that camera, and uses that camera to draw itself. Translating and scaling that camera's view transform is how panning and zooming are accomplished.
Pattern 9: Full Terminology
Many of PNode's methods work on the composite structure (the node plus all its descendents) of the node. It is helpful to be able to easily distinguish these methods from ones that only work directly on the node and not on its children.
Piccolo2D uses the term "full" to mean a node and its descendants with the node's transform applied. This helps distinguish between methods that work on a single node and those that work on a node together with all of its descendants with the node's transform applied. When traversing the node scene graph to paint or calculate bounds parent nodes generally call the "full" methods of their direct child nodes. For example:
// Returns the bounding box of the given node without the node's // transform applied. aNode.getBounds(); // Returns the bounds of the given node combined with the bounds of // all of the descendants of that node after applying the node's // transform. Descendants need not be contained in the bounds of // their parent, so this full bounds value may be larger than the // bounds, but it will never be smaller. aNode.getFullBounds(); // This will paint just "aNode" and not any of its children. Normally this // method is automatically called from the fullPaint method. aNode.paint(aPaintContext); // This will paint "aNode" together will all of that node's descendents. aNode.fullPaint(aPaintContext);
// Returns the bounding box of the given node without the node's // transform applied. aNode.Bounds; // Returns the bounds of the given node combined with the bounds of // all of the descendants of that node after applying the node's // transform. Descendants need not be contained in the bounds of // their parent, so this full bounds value may be larger than the // bounds, but it will never be smaller. aNode.FullBounds; // This will paint just "aNode" and not any of its children. Normally this // method is automatically called from the FullPaint method. aNode.Paint(aPaintContext); // This will paint "aNode" together will all of that node's descendents. aNode.FullPaint(aPaintContext);
Subclasses should generally not override the "full" methods, since they are implemented in terms of other methods that can be overridden.
Pattern 10: Coordinate Systems
Each node in Piccolo2D has its own transform that it uses to define its own coordinate system. It is essential that you understand coordinate systems, and how to convert from one coordinate system to another when designing a zoomable user interface.
There may be thousands of different coordinate systems in a Piccolo2D interface (a different one for each node), but they can all be organized into three categories:
- Local
- The local coordinate system is specific to a single node. This coordinate system exists directly below that node's transform. The base bounds of a node are kept in this local coordinate system, while the fullBounds of a node are stored in the local coordinate system of the nodes parent. (This means that the full bounds of a node have been transformed up through the node's transform, but are still below the node's parent's transform).
- Global
- The global coordinate system exists above the root node's transform, but below the canvas's view transform. The global coordinate system serves as a common coordinate system that can be used as an intermediate when converting from the local coordinates of one node to that of another node.
- Canvas
- The canvas coordinate system is equal to the coordinate system of the
PCanvas
.
Piccolo2D provides methods that let you easily convert between different coordinate systems. The PNode
class defines the methods:
- localToGlobal() - Transform up from a node's local coordinate system into the global coordinates..
- globalToLocal() - Transform down from global coordinate system into a node's local coordinates.
- localToParent() - Transform up from a node's local coordinate system into the node's parent's local coordinates.
- parentToLocal() - Transform down from a node's parent's local coordinate system into the node's local coordinates.
- LocalToGlobal() - Transform up from a node's local coordinate system into the global coordinates..
- GlobalToLocal() - Transform down from global coordinate system into a node's local coordinates.
- LocalToParent() - Transform up from a nodes local coordinate system into the node's parent's local coordinates.
- ParentToLocal() - Transform down from a node's parent's local coordinate system into the node's local coordinates.
Here is a typical example of how coordinate systems are used:
// This method connects the centers of two // rectangle nodes with a line node. If you know that two nodes // exist in the same coordinate system then you don't need to make // all these conversions. This example assumes the most general case where // they all exist in different coordinate systems. public void connectRectsWithLine(PPath rect1, PPath rect2, PPath line) { // First get the center of each rectangle in the // local coordinate system of each rectangle. Point2D r1c = rect1.getBounds().getCenter2D(); Point2D r2c = rect2.getBounds().getCenter2D(); // Next convert that center point for each rectangle // into global coordinate system. rect1.localToGlobal(r1c); rect2.localToGlobal(r2c); // Now that the centers are in global coordinates they // can be converted into the local coordinate system // of the line node. line.globalToLocal(r1c); line.globalToLocal(r2c); // Finish by setting the endpoints of the line to // the center points of the rectangles, now that those // center points are in the local coordinate system of the line. line.setPathTo(new Line2D.Double(r1c, r2c)); }
// This method connects the centers of two // rectangle nodes with a line node. If you know that two nodes // exist in the same coordinate system then you don't need to make // all these conversions. This example assumes the most general case where // they all exist in different coordinate systems. public void ConnectRectsWithLine(PPath rect1, PPath rect2, PPath line) { // First get the center of each rectangle in the // local coordinate system of each rectangle. PointF r1c = PUtil.CenterOfRectangle(rect1.Bounds); PointF r2c = PUtil.CenterOfRectangle(rect2.Bounds); // Next convert that center point for each rectangle // into global coordinate system. r1c = rect1.LocalToGlobal(r1c); r2c = rect2.LocalToGlobal(r2c); // Now that the centers are in global coordinates they // can be converted into the local coordinate system // of the line node. r1c = line.GlobalToLocal(r1c); r2c = line.GlobalToLocal(r2c); // Finish by setting the endpoints of the line to // the center points of the rectangles, now that those // center points are in the local coordinate system of the line. line.Reset(); line.AddLine(r1c.X, r1c.Y, r2c.X, r2c.Y); }
Pattern 11: UI Cycle
The root node's UI Cycle is at the center of Piccolo2D's runtime behavior where it drives the processes for processing user input, activities, bounds management, and scheduling repaints.
The PRoot
class is responsible for running the UI Cycle. During each cycle it performs four actions:
- Process Inputs
- Incoming input events from Swing or .NET are passed to the dispatch manager which will then send them to the appropriate
PInputEventListeners
. - Process Activities
- All activities that are ready to run are given a chance to do so.
- Validate Bounds
- All invalidated bounds are updated, and bounds caches filled.
- Update Display
- Damage is collected for all areas of the screen that need to be repainted and is then sent to the Swing or .NET repaint manager.
The Piccolo2D UI loop is always driven from the event dispatch thread. A UI cycle is done for each new input event received by the canvas, and for each time the activity timer fires (to support animation).
- To learn more about Piccolo2D and threads, see Threads
- To learn more about dispatching events, see Dispatching Events
- To learn more about processing activities, see Processing Activities
- To learn more about the bounds validation, see Validating Bounds
- To learn more about updating the display, see Updating the Display
Pattern 12: Threads
Piccolo2D is not thread safe and should only be used by a single thread at a time, and that thread will almost always be the event dispatch thread.
If you need to run a computation in another thread you will need to call special methods to connect the results of your computation back up to the event dispatch thread. In Piccolo2D.Java, use SwingUtilities.invokeLater()
or SwingUtilities.invokeAndWait()
. In Piccolo2D.NET, use Control.BeginInvoke()
and Control.Invoke()
.
Pattern 13: Dispatching Events
Event dispatch is the process through which Piccolo2D directs new events coming from the user to event handlers in the interface.
All event dispatch is managed by the input manager (PInputManager
). Events get to the dispatch manager by first coming off the Java or .NET event queue, next they are sent to the canvas (PCanvas
) and the canvas forwards them to the input manager.
The input manager then converts the incoming Java or .NET event to a Piccolo2D event. In Piccolo2D.Java, a java.awt.InputEvent
is converted into a PInputEvent
. In Piccolo2D.NET, a System.EventArgs
is converted into a PInputEventArgs
. Next the input manager sends the Piccolo2D event to the event listeners of the appropriate nodes. The dispatch manager maintains the following focus nodes:
- Keyboard Focus
- The node that gets key events sent to it (Usually defined when the mouse is pressed)
- Mouse Over Focus
- The node that the mouse cursor is over. When a node first becomes the current mouse over node it is sent a mouse entered event, and when the mouse leaves it is sent a mouse exited event.
- Mouse Focus
- The mouse focus node is sent mouse pressed, dragged, and released events. When the mouse is pressed the dispatch manager gets the mouse over focus, and makes it the mouse focus node as well. This mouse focus node is sent all events from that mouse pressed, dragged, released sequence. After the release event arrives the dispatch manager sets the mouse focus back to null.
Events are dispatched up the PPickPath
associated with a given focus node, so they percolate up the pick path until they are consumed or they reach the originating camera node.
TIP: You can always get a reference to the dispatch manager from a PInputEvent
, and ask it for the current focus nodes.
Pattern 14: Processing Activities
All scheduled activities are given a chance to run during the UI Cycle.
Activities are used to control some time dependent aspect of the Piccolo2D framework, for example they can be used to animate a node across the screen, or animate the camera's view transform to perform a zoom.
Pattern 15: Validating Bounds
When the geometry of a node changes its bounds caches need to be recomputed and the geometry of other related nodes may also be effected.
Maintaining bounds caches and updating layouts can become very expensive when manipulating a large number of nodes. Because of this Piccolo2D uses a two stage incremental approach to layout management. The two stages consist of a damage stage where damage is recorded in bit flags for each damaged node and an incremental repair stage where the damage is repaired as is needed.
The damage stage begins when some node geometry changes. When this happens the type of damage that occurred is recorded for each node. The are three kinds of damage that can occur:
- bounds invalidated
- This damage is recorded whenever the bounds of a node change.
- full bounds invalidated
- This damage is recorded anytime the fullBoundsCache becomes invalid.
- child bounds invalidated
- This damage is recorded anytime the full bounds of any descendant node is invalidated.
Damage is repaired by PNode's
validateFullBounds method at the end of the UI cycle. That method does the following things
- If the node's bounds have been invalidated, then validate them.
- If the node's child bounds invalidated flag is set then all of the nodes children are validated.
- Next layout the children. (by default, the layout method of
PNode
does nothing, but layout manager nodes would override it). - If the node's full bounds invalidated flag is set then:
- Record the node's old full bounds
- Compute the node's new full bounds
- If the node's full bounds have change make sure to invalidate the full bounds of the parent node.
- The layout is now up to date, so clear the layout flags.
Pattern 16: Updating the Display
Piccolo2D should paint the screen only when needed, and it should be smart about only painting the portions of the screen that need to be painted.
Display update in Piccolo2D is driven from the UI Cycle, and uses the same damage/repair design that Piccolo2D uses to validate bounds. When a node changes such that it needs to be repainted it invalidates its paint, and invalidates the child paint of all its ancestors. Later (at the end of the UI Cycle) screen damage is collected for all nodes with invalid paint.
Design Patterns for ZUI Interfaces
This section contains a few basic patterns that occur frequently in ZUIs, and describes how they can be implemented with Piccolo2D. To learn more about using Piccolo2D see Usage Patterns For Piccolo2D, and to learn more about the Piccolo2D implementation see Implementation Patterns for Piccolo2D.
Pattern 17: Semantic Zooming
It is useful for an object to change its visual representation based on the scale that it is being viewed at. For example when a document is viewed from far away (at a small scale) in a ZUI it might be best to just show that documents title, but when the view is zoomed in all the documents content should become visible.
To do semantic zooming in Piccolo2D you should override the appropriate paint method, and then choose how the node renders itself based on the scale stored in the paint context parameter. This example creates a new node that will paint its based bounds filled with a blue color when viewed at a scale that is less than one, and with an orange color when the scale is greater than one.
public class SemanticNode extends PNode { public void paint(PPaintContext aPaintContext) { double s = aPaintContext.getScale(); Graphics2D g2 = aPaintContext.getGraphics(); if (s < 1) { g2.setPaint(Color.blue); } else { g2.setPaint(Color.orange); } g2.fill(getBoundsReference()); } }
public class SemanticNode : PNode { protected override void Paint(PPaintContext paintContext) { float s = paintContext.Scale; Graphics g = paintContext.Graphics; if (s < 1) { g.FillRectangle(Brushes.Blue, this.Bounds); } else { g.FillRectangle(Brushes.Orange, this.Bounds); } } }
Pattern 18: Sticky Objects
It is useful for an object to "stick" to a camera, so that its position does not change even when the camera is zoomed and panned.
To do this in Piccolo2D you should add the "sticky node" as a child of the camera. The camera's view transform is only applied to the layer nodes that it is viewing, not to its children. The children are drawn after (on top of) the layer nodes.
This example creates a yellow rectangle with bounds handles and adds it to the camera as a sticky node. A standard rectangle is then added to the main layer. Zooming in and out will change the scale of the standard rectangle but not the sticky one.
public void initialize() { PPath sticky = PPath.createRectangle(0, 0, 50, 50);; sticky.setPaint(Color.YELLOW); sticky.setStroke(null); PBoundsHandle.addBoundsHandlesTo(sticky); getCanvas().getLayer().addChild(PPath.createRectangle(0, 0, 100, 80)); getCanvas().getCamera().addChild(sticky); }
public override void Initialize() { PPath sticky = PPath.CreateRectangle(0, 0, 50, 50);; sticky.Brush = Brushes.Yellow; sticky.Pen = null; PBoundsHandle.AddBoundsHandlesTo(sticky); Canvas.Layer.AddChild(PPath.CreateRectangle(0, 0, 100, 80)); Canvas.Camera.AddChild(sticky); }