Each logical piece of user interface is implemented as a separate NetBeans module - for example, the layer display component is implemented in the Layers UI module; the tool palette is implemented in the Tools UI module. The important thing is that there are no interdependencies between most components of the application. Here is the list of modules. The names of those which have a public API are underlined:
Tool in Utilities.actionsGlobalContext().
The Paint API module provides a set of image editor APIs. The various pieces of the application communicate with each other using this API.
In particular, the Paint API module replaces the standard selection model in NetBeans with one based on the active editor instead of whatever component has focus (you wouldn't want the tool customizer to be emptied when you give it focus because it's tracking the active editor and that has disappeared from the global selection context).
The Paint API defines three classes in particular that are the core things tools and such will want to interact with:
Picture - represents an image the user is editing,
which is composed of one or more Layer objects.Layer - One editable layer in an picture. A picture
is essentially a stack of independently editable layers which are
composited together. Different types of Layer can be
plugged in, with their own tool sets and abilities. The most basic
form stores its data as Java BufferedImages and allows
the user to paint into them via Tools. A Layer has
attributes such as location, name, opacity. A Layer can
provide an instance of Selection in its Lookup,
which can be used to manipulate the selection.
Surface - A Layer has a Surface
object which is the the actual drawing surface (a thing you can get
a java.awt.Graphics2D from to draw into).
PictureImplementation, LayerImplementation
and SurfaceImplementation that can be implemented to provide
your own implementation of these things.
The Paint UI module, which supplies the editor, provides an implementation
of Picture; the Raster Layers module and the
Vector Layers module both provide implementations of Layer
and Surface.
At any time, if an editor is open, there is usually an active layer,
which is the layer that the user is currently editing. The current
instance of Picture and the active layer can be found from
the global selection context:
Picture picture = Utilities.actionsGlobalContext().lookup(Picture.class);
assert picture.getActiveLayer() == Utilities.actionsGlobalContext().lookup(Layer.class);
Tool interface
itself is mostly methods for getting an icon and display name for the
tool. The activate(Layer) method is called when the user
selects this tool.
The secret is what other interfaces your tool implements. If a Tool
implements MouseListener, it will receive mouse events from the editor canvas,
and it can call layer.getSurface().getGraphics() to draw
into the layer. If it implements KeyListener, it will receive key
events. Note that all mouse events are properly translated and scaled,
so if the editor is zoomed in, the Tool does not have to do anything
special - the coordinate space of incoming mouse events is scaled
appropriately.
Other interfaces a Tool can implement are
PaintParticipant - meaning that this tool wants to
draw on top of the canvas, or set the cursor on it, or similar,
in response to mouse motion events - but do so without
necessarily drawing anything into the image.
CustomizerProvider - indicates that this tool wants
to provide a customizer component for customizing its settings,
etc.
Customizer<Integer> = Customizers.getCustomizer (Integer.class,
Constants.STROKE, 0, 100);
and the result will have a consistent appearance with other customizers
that let you configure stroke width, and it will be pre-set
to the last user-set value, either from a previous session or the last
time it was used.
Most tools have more than one parameter to configure, so it is easy to create aggregate customizers as follows:
return new AggregateCustomizer (null, customizer1, customizer2, ...)
Note: remember that if you use components from Customizers, you
are sharing these components with other tools. So each call to
getCustomizer() should ensure that the components are really parented
to the component you will return - in other words, if you create a
panel and cache it as the customizer for something, an integer customizer
such as the one we fetched above will be removed from your panel when
some other tool tries to use it in its own customizer.
Effect is basically just a wrapped
java.awt.Composite which manipulates raster data (an API
which allows Effects to be based on BufferedImageOP is coming). An
Effect has an Applicator that allows the
effect's parameters to be configured and can then generate an
appropriate instance of a Composite.
Selection, which is
found by calling
Selection sel = activeLayer.getLookup().lookup(Selection.class);
A selection is a collection of objects of some sort - the exact type
is determined by the layer that is implementing selection, but both the
default layer types use a collection of java.awt.Shape objects. All
Selection objects must support fetching the selection as a shape.
The selection can be listened on for changes.
LayerImplementation should have an instance of
Selection in its Lookup.
By placing your LayerFactory in the default Lookup
(by creating a flat file in META-INF/services in your JAR file),
your new layer type will be available in the new layer popup
menu.
org.openide.awt.UndoRedo.Manager is
made available by the editor in the global selection context, whenever
an editor is open. It can be used to add undoable edits as
editing occurs.
For tools, you can simply call Surface.beginUndoableOperation(localizedName)
draw what you want, then call Surface.endUndoableOperation(),
and before/after snapshots will automatically be saved.
If you are implementing your own undo/redo support (say, in a
custom layer type), bear in mind that keeping lots of bitmap data
on the Java heap will run you out of memory very quickly.
If you need to keep bitmaps, it is best to use the
NIO Images module's ByteNIOBufferedImage, which
creates BufferedImages whose data is stored in a
memory-mapped file off the heap. These images are perfectly usable,
but they bypass all hardware graphics acceleration, so they are not
quick at all to paint; best to use these for caches and convert back
to a standard, accelerated BufferedImage when they are actually
needed. These are quite handy for undo snapshots that may never
be reused.