summaryrefslogtreecommitdiffstats
path: root/doc/architecture.txt
blob: 961d9605dcec37ea834d60256667034adf48a54e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
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>