The intricate stuff
So you made it here. Congratulations!
In this part I'll explain the deeper mechanics of the library. Maybe you're interested in contributing to
the library; or want to create highly customized compontents; or you are just interested in how such a
Framework works. Then you're right here. Or perhaps you just came here by accidents or because
of the
obscure title ;). Nevermind.
Let's get serious.
Basically, the only thing Processing does for this library is drawing the stuff and provide some functions to draw text, rects, lines etc. How everything is arranged, which items come on top of others, when to render what, HOW the stuff should look like is what guiSET deals with. Processing also tells us when the user pressed a key or did something with the mouse. But we need to decide which items are allowed to process the mouse events and how it is determined if the mouse is over a specific element.
Outline
- Inheritance of the component classes
- Graphics and updating: In this part the concept of storing and applying the look of an item as well as the communication between elements about changes is layed out.
- More details about render procedure: Implementation of the stuff that happens around the actual rendering.
- Mouse events: Handling and passing of mouse events. Determining if mouse is over a certain item.
- Events in general: About the way event callbacks are registered, stored and executed. Also something about creating new events.
- PApplet and the static Frame
Short summary about the inheritance of the component classes
Control is the (abstract) base class for all visual components. Container
subclasses Control. As do Slider and the again abstract TextBased
class. It is the base for all components that feature text in some form. These are Button,
Label, Checkbox, MenuItem and ListItem.
Frame, VFlowContainer, HFlowContainer, ScrollArea,
ListView and FlowContainer all extend Container.
VFlowContainer and HScrollContainer subclass the according flow containers and
MultilineTextbox and Textbox are extensions of these two.
Finally we have Knob (inherits from Slider), ContextMenu
(MenuItem), MenuBar (HFlowContainer) and MenuTextbox as
well as MenuSeparator (both from MenuItem).
Graphics and updating
TODO: Default behaviour is now "unbuffered rendering", meaning no temporary PGraphics are
created. This is more memory-efficient. Should be explained properly in this section.
Each instance of a component has a PGraphics object that stores the current look as pixel
image. Its content
is only changed within the render() method. Containers draw the PGraphics of
their
children onto their own
PGraphics and so on. The Frame instance finally draws itself onto the sketch
(the
PApplet).
This way of rendering and storing is chosen to keep the number of drawing processes as low as possible.
If
an element is changed visually, render() will be called and thus the graphics updated. But
any
siblings or
children do not need to be re-rendered as their look is still stored in their own PGraphics
object. However,
this comes at a prize of memory. This is not a problem with modern computers and moderate or even
generous
numbers of elements. If, nevertheless, you aspire to have a list with a few thousand items displayed on
the
screen, this technique can get expensive. I'm thinking about adding functionalty to enable rendering
every
time the graphics are required, without storing the image. This descision could be per-item.
Each time the user of the library sets the state of a property that changes the look of the element, the
setter will call the update() method which sets a dirty flag and also
initiates a
propagation through the
tree structure way up to Frame, setting the dirty flag for every parent,
'grand'-parent etc.
At render time, Frame checks if it needs to redraw itself and if so, proceeds to render all
changed children
in the described way.
A container is always responsible for drawing its content items. For that purpose, in its own
render()
method it calls the function renderItem(Control c, int x, int y) for each item,
specifying with x and y where the item shall be placed on the containers own
PGraphics.
renderItem(Control
c, int x, int y)
takes care of checking if the item is 'dirty' or (semi-) transparent
(but does not check
for visibility). Here is also where i.e. flow or scroll containers give their items a new location.
For example this is what the implementation of render() in Container looks
like.
@Override
protected void render() {
drawDefaultBackground();
for (Control c : content) {
if (c.isVisible()) {
renderItem(c, c.getX(), c.getY());
}
}
drawDefaultDisabled();
}
In VFlowContainer the x and y properties of the item are ignored
and
replaced by other rules:
@Override
protected void render() {
drawDefaultBackground();
int usedSpace = getPaddingTop();
for (Control c : items) {
if (c.isVisible()) {
renderItem(c, c.getMarginLeft() + getPaddingLeft(), usedSpace + c.getMarginTop());
usedSpace += (c.getHeight() + c.getMarginTop() + c.getMarginBottom());
}
}
drawDefaultDisabled();
}
More details about the render procedure
TODO: Explain the different "renderers" (unbuffered/buffered).
The actual rendering takes place like this: the container usually draws its own background and then
calls
renderItem(Control c, int x ,int y). This method sets the
offsetX/offsetY properties of
c according to given
coordinates and sets them outside the container if the opacity is 0. This is not necessary if visibility
is
set to false because mouseEvent(int, int) already checks for visiblity. If the graphics of
c
are "dirty",
preRender() is called which creates the PGraphics instance (only if size has
changed) and calls
pg.beginDraw(). Then the items custom render() method is executed and finally
tint
applied for
opacity.
Mouse Events
guiSET uses relative coordinates (relative to parent) to make mouse event checks.
Generally, mouse events are received by Frame from the sketch (PApplet). As a
container it checks if the
mouse is currently locally within the space it takes up and if so, passes the event on to all children
which
in turn do the same.
In fact, it is sort of the other way round: After checking if mouse coordinates are within the containers bounds rectangle, it first gives the children the chance to process the event as they are always on top of and not below the container. Afterwards the container calls its own event routine.
This way the topmost elements (high z-coordinate) in the application get the event first. They can then
call
stopPropagation() which sets a flag that no more mouse events should be processed. This way
you
can click on
a menu item without firing a button beneath the menu strip. PRESS, RELEASE and DRAG always stop the
propagation.
Still, containers can override the method boolean containerPreItemsMouseEvent(int x, int y)
which is called
before the event is passed on to the children and if returns false, will prevent the children from
receiving
the event. Scroll containers i.e. use this to implement a scroll bar.
As mentioned above, containers use the method renderItem(Control c, int x, int y). This
method
also stores
the given x and y in the elements properties offsetX and offsetY. These are
the
actual coordinates relative
to the parent and they are the ones that are checked when this element receives a mouse event and it
needs
to decide whether the mouse is within itself or not.
When a container receives a mouse event (parent calls mouseEvent(int x, int y)), it passes
x
and y on to the
children but subtracting the own offset coordinates. During setup Frame registers a
mouseEvent(MouseEvent)
method at Processings PApplet. This is called by PApplet before draw() each
time
there is a new mouse event.
Frame then starts the mouse event propagation within guiSET. The
MouseEvent
object is stored in
the static
field Control.currentMouseEvent, is not passed around while the event is recursively
handled
but every
element can access it.
Events in general
The programmer can add certain listeners to his objects. Mouse listeners are available for all classes.
They
can be assigned and given a callback method
with the addMouseListener() method, specifying the type with a string
("press", "release"...). If no target is specified with the overloaded
addMouseListener() method, papplet will be assumed.
How MouseListener works: Frame registers a mouseEvent() method at
PApplet.
Every container (also Frame) calls the mouseEvent for all its items. When an
element gets a mouseEvent, it can decide to stop the propagation by calling
stopPropagation(). This way elements with high z-index, which will be checked
first and can prevent lower elements from getting the mouseEvent. This is
needed for example with menus, spinners, popups etc.
Moreover classes like Frame and Textbox feature key listeners or an
itemchanged-listener (ListView, MenuItem). These listeners are usually
assigned with extra methods like setItemChangeListener() etc.
How to make an event listener in a new class
- In the class, create an EventListener with an appropriate name (don't initialize it)
- Provide adding and removing methods
- Also provide a simple adding method that defaults target to papplet (the sketch)
An example with no arguments.
protected EventListener myListener;
public void setMyListener(String methodName, Object target) {
myListener = createEventListener(methodName, target, null);
}
public void setMyListener(String methodName) {
setMyListener(methodName, getPApplet());
}
public void removeMyListener(){
myListener = null; // setting listener to null is the way to do it
}
protected void someFunctionThatChangesObervedState(){
// do stuff
handleEvent(myListener, null);
}
An example with arguments (in this case a MouseEvent)
protected EventListener myListener;
public void setMyListener(String methodName, Object target) {
myListener = createEventListener(methodName, target, MouseEvent.class);
}
public void setMyListener(String methodName) {
setMyListener(methodName, getPApplet());
}
public void removeMyListener(){
myListener = null; // setting listener to null is the way to do it
}
protected void someFunctionThatChangesObervedState(){
// do stuff
MouseEvent eventThatWeGotFromSomewhere;
handleEvent(myListener, eventThatWeGotFromSomewhere);
}
If arguments shall be passed to the callback function, then pass their
class to the createEventListener() method as third argument. When handling
the event, pass the argument here.
PApplet and the static Frame
No big deal: for creating the PGraphics instances we need to call PApplets
createGraphics(int, int) method. Therefore we always need a reference to the sketch class
which
is solved by passing this to the Frame constructor in the sketch code.
Big deal: for getting things like text-width or text-leading we also need the PApplet or at
least
a functioning PGraphics which can only be achieved properly by having the
PApplet
(at least to my knowledge). Unfortunately, textWidth is needed for autosizing before the graphics
are
created for the first time during rendering. Autosizing may be performed multiple times during setup
when
setting text or padding of an element. In the current solution, a static PFont is created
at
the time that PApplet is know and it can be used by every element. Somehow, though the
values
differs just a little from the results of pg.textWidth(String) although I copied my
implementation from Processings source code. If anyone knows the solution, please tell me.
Now, as we only need one instance of Frame per sketch and as every element needs access to
it,
it is stored in the static Frame.frame0 variable. This one is never null as it is filled
by a dummy (called nullFrame) and replaced when using the Frame(PApplet parent)
constructor.
