490 lines
20 KiB
Text
490 lines
20 KiB
Text
Sodipodi internal architecture
|
|
|
|
1. Overview
|
|
|
|
The Sodipodi display and editing engine is built using the
|
|
"Model-View-Controller" (MVC) paradigm. Unlike "classic" MVC
|
|
programs, we have further split model into two distinct layers,
|
|
'backbone' and 'document'. This has proven to be extremely powerful
|
|
technique, giving us clear and fast implementation, functional
|
|
granularity and easy expandibility.
|
|
|
|
1.1. Agnostic XML backbone
|
|
|
|
The basis of the sodipodi document is its plain XML representation in
|
|
memory. This is a tree-shaped structure, in which each node is
|
|
represented by a lightweight typeless object (SPRepr). These objects
|
|
implement a minimal interface of both control (methods) and mutation
|
|
events (callbacks). We use the term 'agnostic' for describing that
|
|
part of model, to underline the typeless nature of SPRepr. More or
|
|
less, this is just an XML file representation in memory.
|
|
|
|
1.2. Typed SVG document
|
|
|
|
The most actively used part of the sodipodi document model is the SVG
|
|
object tree. This is constructed on top of the XML tree, and reacts to
|
|
all mutation events in the agnostic tree, thus always keeping its
|
|
internal state synchronized with the backbone. The opposite is not
|
|
true - the XML backbone is not aware of the SVG object tree, and thus
|
|
does not react to its modifications. If writeback to the backbone is
|
|
needed, it must be requested explicitly by the controller. The SVG
|
|
tree is constructed of SPObject subclasses - in general there is one
|
|
subclass for each SVG element type, plus abstract base classes.
|
|
|
|
1.3. NRArena view
|
|
|
|
NRarena is an abstract display engine that allows construction of
|
|
'display caches' from NRArenaItem subclasses. These are lightweight,
|
|
having only some basic object types, and used for most of the display
|
|
needs of Sodipodi. Both the editing window, and the bitmap export
|
|
code create special NRArena instances, and ask the SVG document to
|
|
show itself to the given NRArena. There is a ::show virtual method,
|
|
implemented by all visible object classes, that adds an NRArenaItem
|
|
node to the display tree. The completed display cache is used for fast
|
|
screen updates and stripe based bitmap exports. During the NRArena
|
|
lifetime SVG objects keep all of the display cache elements constantly
|
|
updated, thus ensuring the display is always up to date.
|
|
|
|
1.4. Controllers
|
|
|
|
Like the model suggests, controllers can be implemented acting on
|
|
different layers. Which one is best depends on the type of action
|
|
that the given controller performs. Usually very generic and
|
|
single-shot operating controllers act on the SPRepr layer, while those
|
|
providing visual feedback or tied to a certain object type act on the
|
|
SPObject layer.
|
|
|
|
|
|
|
|
2. Detailed view
|
|
|
|
2.1. SPRepr
|
|
|
|
The most basic SVG (XML) document backbone is implemented as an
|
|
in-memory tree of SPRepr objects, each object corresponding to a
|
|
single node in the XML file. Currently there are only two types of
|
|
SPReprs - normal element nodes and text nodes. More types may be
|
|
added in the future, but the structure will probably always remain
|
|
much simpler (and faster) than DOM.
|
|
|
|
SPRepr may have:
|
|
- attributes (keyword/value) pairs
|
|
- content (text)
|
|
- child nodes
|
|
|
|
Attribute values are textual, and no checks are performed in that
|
|
layer to ensure document validity. Also, CSS style strings are
|
|
unparsed in that layer. The SPRepr tree is built during document
|
|
loading or creation. As it is textual and always synchronized with the
|
|
display, unfiltered saving involves just dumping it into a file.
|
|
|
|
The basic API acting on SPRepr level is really spartan.
|
|
|
|
SPRepr *sp_repr_new (const unsigned char *name)
|
|
SPRepr *sp_repr_new_text (const unsigned char *content)
|
|
SPRepr *sp_repr_ref (SPRepr *repr)
|
|
SPRepr *sp_repr_unref (SPRepr *repr)
|
|
SPRepr *sp_repr_duplicate (SPRepr *repr)
|
|
|
|
int sp_repr_set_content (SPRepr *repr, const unsigned char *content)
|
|
int sp_repr_set_attr (SPRepr *repr, const unsigned char *key, const unsigned char *value)
|
|
int sp_repr_add_child (SPRepr *repr, SPRepr *child, SPRepr *ref)
|
|
int sp_repr_remove_child (SPRepr *repr, SPRepr *child)
|
|
int sp_repr_change_order (SPRepr *repr, SPRepr *child, SPRepr *ref)
|
|
|
|
In addition there are some accessor methods and lot of convenience ones.
|
|
|
|
Each SPRepr can have one or many event vectors associated with it.
|
|
Event vector is a block of callback pointers for different kind of
|
|
mutation events.
|
|
|
|
void sp_repr_add_listener (SPRepr *repr, const SPReprEventVector *vector, void *data)
|
|
void sp_repr_remove_listener_by_data (SPRepr *repr, void *data)
|
|
|
|
struct _SPReprEventVector {
|
|
void (* destroy) (SPRepr *repr, gpointer data);
|
|
gboolean (* add_child) (SPRepr *repr, SPRepr *child, SPRepr *ref, gpointer data);
|
|
void (* child_added) (SPRepr *repr, SPRepr *child, SPRepr *ref, gpointer data);
|
|
gboolean (* remove_child) (SPRepr *repr, SPRepr *child, SPRepr *ref, gpointer data);
|
|
void (* child_removed) (SPRepr *repr, SPRepr *child, SPRepr *ref, gpointer data);
|
|
gboolean (* change_attr) (SPRepr *repr, const guchar *key, const guchar *oldval, const guchar *newval, gpointer data);
|
|
void (* attr_changed) (SPRepr *repr, const guchar *key, const guchar *oldval, const guchar *newval, gpointer data);
|
|
gboolean (* change_content) (SPRepr *repr, const guchar *oldcontent, const guchar *newcontent, gpointer data);
|
|
void (* content_changed) (SPRepr *repr, const guchar *oldcontent, const guchar *newcontent, gpointer data);
|
|
gboolean (* change_order) (SPRepr *repr, SPRepr *child, SPRepr *oldref, SPRepr *newref, gpointer data);
|
|
void (* order_changed) (SPRepr *repr, SPRepr *child, SPRepr *oldref, SPRepr *newref, gpointer data);
|
|
}
|
|
|
|
All events, except destroys (which are unconditional), have pre- and
|
|
post- event callbacks. The pre-event callback's return value is used to
|
|
signal whether the given modification is allowed. If it is FALSE the
|
|
operation will be cancelled and the invoking method will also return
|
|
FALSE. Using callbacks in block is much more convenient than adding
|
|
them one-by-one, as the listening code usually wants to install several
|
|
handlers at once, and the same set of handlers for many different
|
|
nodes. NULL pointers are allowed in event vector.
|
|
|
|
Although the most important functionality of the SPRepr tree is to
|
|
serve as a document backbone, it has other functions besides
|
|
that. SPReprs are also used to store preferences, the copy buffer and
|
|
the undo stack.
|
|
|
|
|
|
|
|
2.2. SPObject
|
|
|
|
SPObject is an abstract base class of all of the document nodes at the
|
|
SVG document level. Each SPObject subclass implements a certain SVG
|
|
element node type, or is an abstract base class for different node
|
|
types. The SPObject layer is bound to the SPRepr layer, closely
|
|
following the SPRepr mutations via callbacks. During creation,
|
|
SPObject parses and interprets all textual attributes and CSS style
|
|
strings of the SPRepr, and later updates the internal state whenever
|
|
it receives a signal about a change. The opposite is not true - there
|
|
are methods manipulating SPObjects directly and such changes do not
|
|
propagate to the SPRepr layer. This is important for implementation of
|
|
the undo stack, animations and other features.
|
|
|
|
SPObjects are bound to the higher-level container SPDocument, which
|
|
provides document level functionality such as the undo stack,
|
|
dictionary and so on.
|
|
|
|
SPObjects are implemented using the Gtk object system (GObjects in gtk
|
|
2 version), which provides an extremely powerful and flexible OO
|
|
framework in pure C.
|
|
|
|
SPObject class hierarchy
|
|
|
|
SPObject ABSTRACT
|
|
SPObjectGroup ABSTRACT
|
|
SPNamedView <sodipodi:namedview>
|
|
SPClipPath <clipPath>
|
|
SPGuide <sodipodi:guide>
|
|
SPPaintServer ABSTRACT
|
|
SPGradient ABSTRACT
|
|
SPLinearGradient <linearGradient>
|
|
SPRadialGradient <radialGradient>
|
|
SPPattern <pattern>
|
|
SPDefs <defs>
|
|
SPItem ABSTRACT
|
|
SPGroup <g>
|
|
SPRoot <svg>
|
|
SPAnchor <a>
|
|
SPImage <image>
|
|
SPPath ABSTARCT
|
|
SPShape <path>
|
|
SPLine <line>
|
|
SPPolyLine <polyline>
|
|
SPPolygon <polygon>
|
|
SPStar <sodipodi:star>
|
|
SPRect <rect>
|
|
SPSpiral <sodipodi:spiral>
|
|
SPGenericEllipse ABSTRACT
|
|
SPCircle <circle>
|
|
SPEllipse <ellipse>
|
|
SPArc <sodipodi:arc>
|
|
SPChars ABSTRACT
|
|
SPString TEXT NODE
|
|
SPDefs <defs>
|
|
SPText <text>
|
|
SPTSpan <tspan>
|
|
|
|
SPObject internals
|
|
|
|
struct _SPObject {
|
|
GtkObject object;
|
|
unsigned int hrefcount;
|
|
SPDocument *document;
|
|
SPObject *parent;
|
|
SPObject *next;
|
|
SPRepr *repr;
|
|
unsigned char *id;
|
|
SPStyle *style;
|
|
const unsigned char *title;
|
|
const unsigned char *description;
|
|
};
|
|
|
|
The basic refcounting is handled by the parent class
|
|
(GtkObject). Hrefcount is used for weak references, for example, to
|
|
determine whether any graphical element references a certain gradient
|
|
node. The parent and next fields are used to establish the tree
|
|
structure. Id is copy of the SPRepr 'id' attribute for normal nodes,
|
|
and is used as a unique index of all objects in the given document.
|
|
|
|
Virtual methods
|
|
|
|
/******** Disclaimer *******/
|
|
This will change a lot in the future
|
|
|
|
void ::build (SPObject *object, SPDocument *document, SPRepr *repr)
|
|
|
|
This has to be invoked immediately after creation of an SPObject. The
|
|
frontend method ensures that the new object is properly attached to
|
|
the document and repr; implementation then will parse all of the attributes,
|
|
generate the children objects and so on. Invoking ::build on the SPRoot
|
|
object results in creation of the whole document tree (this is, what
|
|
SPDocument does after the creation of the XML tree).
|
|
|
|
void ::release (SPObject *object)
|
|
|
|
This is the opposite of ::build. It has to be invoked as soon as the
|
|
object is removed from the tree, even if it is still alive according
|
|
to reference count. The frontend unregisters the object from the
|
|
document and releases the SPRepr bindings; implementations should free
|
|
state data and release all child objects. Invoking ::release on
|
|
SPRoot destroys the whole document tree.
|
|
|
|
void ::child_added (SPObject *object, SPRepr *child, SPRepr *ref)
|
|
void ::remove_child (SPObject *object, SPRepr *child)
|
|
void ::order_changed (SPObject *object, SPRepr *repr, SPRepr *oldref, SPRepr *newref)
|
|
|
|
These are invoked whenever the given mutation event happens in the XML
|
|
tree. ::remove_child is invoked BEFORE removal from the XML tree
|
|
happens, so grouping objects can safely release the child data. The
|
|
other two will be invoked AFTER the actual XML tree mutation. Only
|
|
grouping objects have to implement these.
|
|
|
|
void ::read_attr (SPObject *object, const unsigned char *key)
|
|
|
|
Signals object that the XML attribute is changed. The frontend checks
|
|
for 'id' attribute; implementations have to read the actual attribute
|
|
value and update the internal state.
|
|
|
|
void ::read_content (SPObject *object)
|
|
|
|
Signals object that the XML node content has changed. Only meaningful for
|
|
SPString implementing XML TEXT node.
|
|
|
|
void ::modified (SPObject *object, unsigned int flags)
|
|
|
|
Virtual method AND signal implementing asynchronous state change
|
|
notification. Whenever the object internal state changes, it requests
|
|
that ::modified will be scheduled from the idle loop. Flags are given
|
|
as hints as to what exactly changes. Read the relevant section for
|
|
more information.
|
|
|
|
SPRepr ::write (SPObject *object, SPRepr *repr, unsigned int flags)
|
|
|
|
Requests SPObject internal state to be written back to the SPRepr. If
|
|
the SP_OBJECT_WRITE_BUILD flag is set, SPRepr is created, if necessary.
|
|
This is used at various places, most notably to generate a plain SVG
|
|
document, and to complete certain GUI operations.
|
|
|
|
|
|
|
|
2.3. SPItem
|
|
|
|
SPItem is an abstract base class for all graphic (visible) SVG nodes. It
|
|
is a subclass of SPObject, with great deal of specific functionality.
|
|
|
|
SPItem internals
|
|
|
|
struct _SPItem {
|
|
SPObject object;
|
|
unsigned int sensitive : 1;
|
|
unsigned int stop_paint: 1;
|
|
double affine[6];
|
|
SPItemView *display;
|
|
SPClipPath *clip;
|
|
};
|
|
|
|
Affine is a 3x2 matrix describing transformation from the item to the
|
|
parent local coordinate systems. Each display in linked list has a link
|
|
to a single NRArenaItem that implements actual renderable image of
|
|
the item.
|
|
|
|
Virtual methods
|
|
|
|
/******** Disclaimer *******/
|
|
This will change a lot in the future
|
|
Only the most important are listed
|
|
|
|
void ::bbox (SPItem *item, ArtDRect *bbox, const double *transform)
|
|
|
|
Calculates item's logical bounding box. The logical bbox does not
|
|
take into account the stroke width, nor certain other visual
|
|
properties. Transformation is a 3x2 matrix describing coordinate
|
|
transform from the item's local coordinate system to the coordinate
|
|
system of the bounding box.
|
|
|
|
void ::print (SPItem *item, SPPrintContext *ctx)
|
|
|
|
Prints the item's visual representation, using the internal printing
|
|
frontend. In the future this may be turned into a more generic
|
|
exporting method.
|
|
|
|
char ::description (SPItem *item)
|
|
|
|
Gives a short description of the item suitable for use in a statusbar,
|
|
etc.
|
|
|
|
NRArenaItem ::show (SPItem *item, NRArena *arena)
|
|
|
|
Creates an NRArena display cache representation of the item. The
|
|
frontend places the generated item into a hierarchy; implementations
|
|
have to build a correct NRArenaItem and keep it up to date.
|
|
|
|
void (* hide) (SPitem *item, NRArena *arena)
|
|
|
|
The opposite of ::show.
|
|
|
|
void ::write_transform (SPItem *item, SPRepr *repr, double *transform)
|
|
|
|
Tries to remove the extra transformation by modifying other aspects of
|
|
the item representation. For example, by changing the rectangle width
|
|
and height, the scaling component of the transformation can be
|
|
dropped. This is used to make the SVG file more human-readable.
|
|
|
|
void ::menu (SPItem *item, SPDesktop *desktop, GtkMenu *menu)
|
|
|
|
Appends item specific lines into the menu. This is used to generate
|
|
the context menu, and will probably move into a separate module in
|
|
the future.
|
|
|
|
|
|
|
|
2.4 SPDocument
|
|
|
|
SPDocument serves as the container of both model trees (agnostic XML
|
|
and typed object tree), and implements all of the document-level
|
|
functionality used by the program.
|
|
|
|
SPDocument implements undo and redo stacks and an id-based object
|
|
dictionary. Thanks to unique id attributes, the latter can be used to
|
|
map from the XML tree back to the object tree. Documents are
|
|
themselves registered by the main program metaobject 'Sodipodi', which
|
|
does elementary bookkeeping.
|
|
|
|
SPDocument performs the basic operations needed for asynchronous
|
|
update notification (SPObject ::modified virtual method), and implements
|
|
the 'modified' signal, as well.
|
|
|
|
Many document level operations, like load, save, print, export and so on,
|
|
use SPDocument as their basic datatype.
|
|
|
|
2.4.1. Undo and Redo implementation
|
|
|
|
Using the split document model gives sodipodi a very simple and clean
|
|
undo implementation. Whenever mutation occurs in the XML tree,
|
|
SPObject invokes one of the five corresponding handlers of its
|
|
container document. This writes down a generic description of the
|
|
given action, and appends it to the recent action list, kept by the
|
|
document. There will be as many action records as there are mutation
|
|
events, which are all kept and processed together in the undo
|
|
stack. Two methods exist to indicate that the given action is completed:
|
|
|
|
void sp_document_done (SPDocument *document)
|
|
void sp_document_maybe_done (SPDocument *document, const unsigned char *key)
|
|
|
|
Both move the recent action list into the undo stack and clear the
|
|
list afterwards. While the first method does an unconditional push,
|
|
the second one first checks the key of the most recent stack entry. If
|
|
the keys are identical, the current action list is appended to the
|
|
existing stack entry, instead of pushing it onto its own. This
|
|
behaviour can be used to collect multi-step actions (like winding the
|
|
Gtk spinbutton) from the UI into a single undoable step.
|
|
|
|
For controls implemented by Sodipodi itself, implementing undo as a
|
|
single step is usually done in a more efficient way. Most controls have
|
|
the abstract model of grab, drag, release, and change user
|
|
action. During the grab phase, all modifications are done to the
|
|
SPObject directly - i.e. they do not change XML tree, and thus do not
|
|
generate undo actions either. Only at the release phase (normally
|
|
associated with releasing the mousebutton), changes are written back
|
|
to the XML tree, thus generating only a single set of undo actions.
|
|
|
|
|
|
2.5. SPView and SPviewWidget
|
|
|
|
SPView is an abstract base class of all UI document views. This
|
|
includes both the editing window and the SVG preview, but does not
|
|
include the non-UI RGBA buffer-based NRArenas nor the XML editor or
|
|
similar views. The SPView base class has very little functionality of
|
|
its own.
|
|
|
|
SPViewWidget is a GUI widget that contain a single SPView. It is also
|
|
an abstract base class with little functionality of its own.
|
|
|
|
2.6. SPDesktop
|
|
|
|
SPDesktop is a subclass of SPView, implementing an editable document
|
|
canvas. It is extensively used by many UI controls that need certain
|
|
visual representations of their own.
|
|
|
|
SPDesktop provides a certain set of SPCanvasItems, serving as GUI
|
|
layers of different control objects. The one containing the whole
|
|
document is the drawing layer. In addition to it, there are grid,
|
|
guide, sketch and control layers. The sketch layer is used for
|
|
temporary drawing objects, before the real objects in document are
|
|
created. The control layer contains editing knots, rubberband and
|
|
similar non-document UI objects.
|
|
|
|
Each SPDesktop is associated with a SPNamedView node of the document
|
|
tree. Currently, all desktops are created from a single main named
|
|
view, but in the future there may be support for different ones.
|
|
SPNamedView serves as an in-document container for desktop-related
|
|
data, like grid and guideline placement, snapping options and so on.
|
|
|
|
Associated with each SPDesktop are the two most important editing
|
|
related objects - SPSelection and SPEventContext.
|
|
|
|
Sodipodi keeps track of the active desktop and invokes notification
|
|
signals whenever it changes. UI elements can use these to update their
|
|
display to the selection of the currently active editing window.
|
|
|
|
2.7. SPSelection
|
|
|
|
This is a per-desktop object that keeps the list of selected objects
|
|
at the given desktop. Both SPItem and SPRepr lists can be retrieved
|
|
from the selection. Many actions operate on the selection, so it is
|
|
widely used throughout the Sodipodi code.
|
|
|
|
SPSelection also implements its own asynchronous notification signals,
|
|
that UI elements can listen to.
|
|
|
|
2.8. SPEventContext
|
|
|
|
SPEventContext is an abstract base class of all tools. As the name
|
|
indicates, event context implementations process UI events (mouse
|
|
movements and keypresses) and take actions (like creating or modifying
|
|
objects). There is one event context implementation for each tool,
|
|
plus few abstract base classes. Writing a new tool involves
|
|
subclassing SPEventContext.
|
|
|
|
|
|
|
|
3. Some thoughts
|
|
|
|
3.1. Why do we need a two-level model tree?
|
|
|
|
The need for a typed object tree is obvious if we want to utilize OO
|
|
programming - which we certainly want to do. Although implemented in pure C,
|
|
Sodipodi uses the gtk (glib in future versions) type and object system,
|
|
which gives us an extremely powerful set of OO functionality. As SVG is
|
|
designed with inheritance in mind, using object subclassing to represent
|
|
it is perfectly the right thing to do.
|
|
|
|
But there are also areas where typed object structure would make
|
|
things more complex. For example, to implement the copy buffer we had
|
|
to save the full state of copied objects. While this could be done
|
|
with the separate virtual method of SPObject, we can use a much easier
|
|
way and create the duplicate corresponding SPRepr. As our document
|
|
model already has to implement generation of full object
|
|
representation for SPRepr tree of nodes, generation of new objects
|
|
during paste happens automatically when the given SPRepr is inserted
|
|
into XML tree. The agnostic xml tree is also used for undo stack, as
|
|
described earlier.
|
|
|
|
The main benefit comes from the extreme simplicity of the XML tree
|
|
manipulation API. All operations can be done, using only around 10
|
|
methods, which makes code much more robust, and is perfect for
|
|
implementing compatibility sensitive things, like a plugin API.
|
|
|
|
The XML tree also makes implementing two SVG features - cloning and
|
|
animations - much easier by providing an invariant backbone.
|
|
|
|
|
|
|
|
22. Novemebr 2002
|
|
Lauris Kaplinski
|
|
<lauris@kaplinski.com>
|