Documentation of IntegraLive GUI

Leighton Hargreaves

17-06-10 (NOTE: THIS DOCUMENT NEEDS UPDATING)


1.Introduction

1.1.Purpose of this document

This document seeks to provide a top down description of the implementation of the Integra Live GUI, including application-wide patterns and concepts, and any important exceptions to these patterns.


It is intended to provide software developers with enough information about how the application has been written to allow them to maintain, fix bugs, or continue development of the software.


1.2.Overview of the system

IntegraLive GUI is written in Flex 3, using a combination of AS3 and MXML. It targets the Adobe Air runtime, which is available for a variety of platforms including Windows, OSX and Linux.

IntegraLive GUI constitutes the 'client' end of a simple client/server architecture; it communicates using local XML RPC with a backend called 'libIntegra'.



IntegraLive GUI consists of a main application, distributable as a .swf file (shockwave flash binary)



This component relationship is illustrated in the following diagram:




1.3.Build Environment

The source code for IntegraLive GUI is in SourceForge at http://sourceforge.net/projects/integralive

To check out a working copy from SVN:

svn co https://integralive.svn.sourceforge.new/svnroot/integralive integralive”

1.3.1.Compile time dependencies

IntegraLive GUI has two compile-time dependencies:

1.3.1.1.XMLRPC for Actionscript 3.0

The original version of this XMLRPC library is available here http://danielmclaren.net/2007/08/03/xmlrpc-for-actionscript-30-free-library



However, I have been forced to make an important bugfix to this library in order for it to be able to handle nested arrays. This fix has been submitted to the author in the hope that they include it in a future release. In the meantime, the fixed version is available here: http://www.integralive.org/incoming/as3_xmlrpc.zip

To add this dependency to IntegraLive GUI's build environment, place its <com> source directory below IntegraLive GUI's <src> directory.

1.3.1.2.FlexUnit 0.9

FlexUnit 0.9 is a unit testing framework for Flex and ActionScript 3 applications. It is only used for its Assert class. It is available from http://opensource.adobe.com/wiki/display/flexunit/Downloads

To add this dependency to IntegraLive GUI's build environment, place the file “FlexUnit.swc” in the directory src/libs/.



1.4.Coding Standards



These Coding Standards are also available in the Integra Wiki (http://www.integralive.org/dokuwiki/doku.php/coding_guidelines)

1.4.1.File structure

1.4.2.Naming conventions



1.4.3.Enumerations

There is no native 'enumeration' type in Actionscript. A common Actionscript convention, also used in the Integra Live GUI, is to define enumerations as classes whose only content is a set of public static const Strings, one for each enumeration value. When enumerations are only needed internally by one class, the set of static const Strings can be defined privately within that class, instead of in a separate class.



1.4.4.Indentation style

IntegraLive GUI uses the Allman style ( http://en.wikipedia.org/wiki/Indent_style )



1.4.5.Code flow style

Minimise indentation. EG instead of

function doSomething
{
        if( condition1 )
        {
                if( condition2 )
                {
                        if( condition3 )
                        {
                                doSomethingElse();
                        }
                }
        }
}

it's better to write

function doSomething
{
        if( !condition1 )
        {
                return;
        }
        
        if( !condition2 )
        {
                return;
        }

        if( !condition3 )
        {
                return;
        }

        doSomethingElse();
}

1.4.6.switch statements

Switch statements should always contain a 'default case'. If the default is not expected to be used (eg when there's a case for each value of an enumeration), the default case should fail an assertion, to alert developers to a possible error.

1.4.7.Defensive programming - assertions and failsafe mechanisms

There is no native 'Assert' functionality in Actionscript. The IntegraLive GUI uses a 3rd party component called 'FlexUnit' (http://opensource.adobe.com/wiki/display/flexunit/FlexUnit), which provides an 'Assert' class. Assertions should be used as liberally as possible. Furthermore, in any situation where an assertion failure is likely to cause a significant software malfunction, and where a simple failsafe mechanism is possible, one should be implemented.

Example of assertion without failsafe mechanism:

Assert.assertNotNull( referenceToSomething )
//subsequent code which relies on referenceToSomething

Example of assertion with failsafe mechanism:

if( referenceToSomething == null )
{
        Assert.assertTrue( false );
        return;
}
//subsequent code which relies on referenceToSomething

2.Structure of IntegraLive GUI

2.1.MVC (Model/View/Controller)

IntegraLive GUI employs a simple MVC pattern (http://en.wikipedia.org/wiki/model-view-controller) in order to impose a topdown structure onto the various components of the application, and maximise encapsulation of functionality.

The key attributes of this pattern as as follows:




2.2.Framework and Utilities

The only parts of the application which are not part of the MVC are as follows:

2.2.1.Top level framework

This manages the main application window, application level menus etc, and is responsible for instantiating the views.

2.2.2.Utilities

These constitute a small set of reusable helper classes – since they are used by more than one other class they are outside of the main tree of MVC elements.



3.Detailed descriptions of these elements

3.1.The Model

Although the primary copy of the currently loaded project is stored in libIntegra, a replica of these data is maintained in the GUI. This 'internal model' stores the state of the project in AS3 classes, and can be queried by views and the controller. All source files relating to the model are located in the source tree under the directory src/components/model/.

The model consists of the following classes:

3.1.1.IntegraModel

IntegraModel is a top-level 'container' for the current state of the model. It is implemented as a singleton with a static 'getter', so is accessible from anywhere in the application.

IntegraModel provides a set of query methods, for example retrieving objects by their Ids, and more complicated query methods eg determining whether a connection can be made without causing a feedback loop.

It also provides some modification methods – eg clearModel, addDataObject, removeDataObject. These modifications methods are never called directly by views – they are only used within the 'controller' (ie by commands).

The private data stored in IntegraModel is as follows:

3.1.2.IntegraDataObject and derived classes

All instantiable data objects are represented in IntegraLive GUI's internal data model as subclasses of IntegraDataObject. However, there isn't quite a one to one mapping between the classes used in IntegraLive GUI and the set of libIntegra classes. There are two reasons for this:

  1. IntegraLive GUI uses the libIntegra class Container in a gui-specific way; it always treats the top-level container as the 'project', treats 2nd level containers as 'tracks' and 3rd level containers as 'blocks'. These concepts 'project', 'track' and 'block' have no meaning for libIntegra – they are merely implementation details of the IntegraLive GUI. But it's useful for the GUI to have separate classes to represent them in it's internal model, since it performs different operations on them.

  2. LibIntegra has two types of class: 'system' classes and 'non-system classes'. System classes are assumed to always exist by the GUI – it needs to make hardcoded assumptions about their existence, attributes and behaviour because they provide the gui's core functionality. LibIntegra's system classes are: Container, Connection, Player, Script, Envelope, ControlPoint. These are all represented explicitly in the GUI by their own class (with three subclasses for Container as described above).

In contrast, non-system classes can be added and removed from libIntegra without any requirement to make changes to the GUI, because they are all represented in the GUI by the single class ModuleInstance.



The following inheritance diagram illustrates the set of classes in IntegraLive GUI which inherit from IntegraDataObject, and shows which libIntegra classes they store data for:




3.1.3.Identification of Data Objects

3.1.3.1.Identification within the XMLRPC API

Within the GUI ↔ libIntegra communication, all objects are identified by dot-connected strings containing their own names, preceded by the chain of names of their ancestors.

For example, an object of type 'Connection' whose parent is an object of type 'Container' is identified as “<container name>.<connection name”.

Each object must have a unique name within its set of siblings, whilst objects with different parents can have the same name. Therefore this concatenated notation is the minimum identification system sufficient to uniquely identity objects.

3.1.3.2.Internal identification in IntegraLive GUI

IntegraLive GUI has its own system to identify data objects using positive integers which are allocated by the method IntegraModel.generateNewID() each time a new object is created. The advantages of this system are twofold:

  1. Objects sometimes refer to each other (the class Connection refers to it's 'source' and 'target' objects, and some fields in user data refer to objects, in order to store information about selection state and live view controls). Objects' names can change at runtime, and objects can be moved from one parent to another – in both cases changing the identification which would be used within the XMLRPC API. These processes are simplified by having a 'fixed' id, which doesn't change during the lifecycle of the object's representation withing the gui.

  2. Objects in the gui can be retrieved efficiently using a map. Integer ids are more efficient map keys due to their small size.

IntegraLive GUI always uses positive numbers for IDs, and by convention uses the value -1 to signify 'not an object' (this convention would is used where functions return an object's ID when the object cannot be found, or as a value to store for an ID where no object is referred to).

The class IntegraModel provides a set of methods to convert between internal IDs and “paths”, as well as methods to look objects up by their ids. It also provides type-safe object retrieval methods (getModuleInstance / getConnection etc) – in practice these are used wherever possible as a more defensive way to retrieve objects.

3.1.4.User Data

IntegraLive GUI stores some extra information which is only relevant to this GUI. The mechanism by which this is persisted consists of a string attribute on every data object, which is stored 'blindly' by libIntegra (that is to say that libIntegra persists this attribute but has no idea what it does). IntegraLive GUI uses this string to store XML, which contains sets of object-specific information.

All the classes relating to UserData are located in the source tree under the directory src/components/model/userData.

There is a bass class called UserData, which stores user data which is relevant to most types of IntegraDataObject: currently this is simply data about which objects are selected.

3.1.4.1.Selection

There are two types of selection in IntegraLive GUI – primary selection (which can only be held by one object in each group of siblings) and multiple selection (which can be held by an arbitrary number of objects). Generally this distinction allows a mix of operations on multiple objects (such as mass deletions or parallel moves), and operations on single objects (such as opening them). Typically multiple selection is indicated with background colour and primary selection is indicated with border weight.

Not all types of objects actually feature both types of selection in the views, but both types are supported for all data objects in the model, through the common UserData class. UserData has a boolean field isSelected to represent whether the object is in a multiple selection set, and a ID field primarySelectedChildID to represent the primary selected object within the object's children.

The actual implementation of these types of selection for each type of object is as follows:

Object

Has Primary Selection

Has Multiple Selection

Project

No

No

Player

No

No

Track

Yes

No

Block

Yes

Yes

Module Instance

Yes

Yes

Script

Yes

No

Connection

No

Yes

Scene

Yes

No

Envelope

Yes

No

Control Point

No

No



3.1.4.2.Specific User Data Classes

There are a set of classes which inherit from UserData, and act as containers for extra data which is only needed for certain types of object. These types of object customise the user data class they use by calling the protected 'setter' defined in IntegraDataObject from their constructor.

3.1.4.2.1.ProjectUserData

Project User Data stores project-level GUI-specific attributes: View Mode, Timeline State, Color Scheme, and the set of Scenes.

3.1.4.2.2.TrackUserData

Track User Data stores the colour of each track, the height it should be displayed in in the Arrange View, and whether it is expanded in the Arrange and Live views.

3.1.4.2.3.BlockUserData

Block User Data stores the layout of modules in the Module Graph, and the set of live view controls to be displayed for the Block.

3.1.4.3.'Leaf' User Data Classes

These are a handful of classes which are referred to by these user data classes:

  1. ColorScheme is a simple enumeration class.

  2. LiveViewControl stores information about each control to display in the Live View.

  3. Scene stores information about each scene.

  4. TimelineState stores the zoom and scroll postion of the timeline

  5. ViewMode represents the currently selected view: Arrange vs Live, and whether block properties are open.





3.1.5.Model Loader

The class ModelLoader is responsible for loading all the contents of the model from libIntegra.

It does this in a set of discrete phases, each of which consists of a set of XMLRPC calls. It waits for each phase to complete before beginning the next phases, by tracking the number of outstanding XMLRPC calls. This is necessary because some parts of the data need to be retrieved before the next sequence of calls can be made (for example it cannot query the attributes of objects until the set of existing objects has been received).

The classes relating to ModelLoader are located in the source tree under the directory src/components/model/modelLoader.

The model is loaded from scratch whenever the user selects the 'new project' or 'open project' menu commands. ModelLoader is also reused by the 'import' commands (ImportTrack, ImportBlock, ImportModule) because unlike most commands, these cannot predict their own result. This detail will be discussed in more detail in the section on controller commands.

3.1.5.1.Extraneous Nodes

The IntegraLive GUI expects libIntegra projects to contain an object tree which is meaningful to the gui (for example it only expects to find a single object of type container at the top level of the object tree, and only expects to find objects of certain types at certain levels of the tree underneath this. When ModelLoader encounters any objects which do not match its expectations, it calls the function ModelLoader.foundExtraneousNode(). This function traces the path of the unexpected node, as a stub for a possible future enhanced handling of this situation.

Note that at present extraneous nodes are not an expected occurrence, since IntegraLive GUI is the only available GUI for libIntegra. However if alternative GUIs are developed is is likely that they could create different types of object tree (for example they might have no concepts such as 'Track' or 'Block'). In this instance it would be desirable to enhance IntegraLive GUI to implement some form of tree rotation such that any conceivable configuration of object tree can be massaged into a shape which is meaningful for this GUI, in order to facilitate cross-application interchangeability.

3.1.5.2.Creation of default new project objects

On completion of loading the model, ModelLoader creates mandatory default 'new project' objects if these are not already present. These are:

  1. The Project. This is a top level container object, which is required for IntegraLive GUI, although not for libIntegra

  2. The Project Player. This is an object of type 'Player' and is created directly under the project.

When ModelLoader detects that these objects are not present, it creates them, and dispatches a message to the controller, telling to create the non-mandatory default 'new project' objects (an initial track containing an initial block, containing initial input and output modules). These non-mandatory objects are not created directly by ModelLoader, because normal command classes exist to create them, so this code can be reused by delegating the task to the Controller.

3.1.5.3.ControlPositionLoader

The class ControlPositionLoader is instantiated and invoked by ModelLoader once for each ModuleDefinition. It looks for an xml file for each non-system libIntegra class, and reads control layout information from any such files it finds. This is explained more fully in the following section “Module Control Layouts”.

3.1.6.Non-instantiable Model Components

As well as the object types discussed above, which store elements within the object tree, IntegraModel represents information which does not change during libIntegra's execution lifecycle. This is information about the different types of class available in LibIntegra for instantiation, as well as GUI-specific derived information about controls.

3.1.6.1.ClassDefinition

The class ClassDefinition stores information about class types, in particular an array of AttributeDefinitions, each describing the properties of each of a class's attributes. ClassDefinition objects are assembled for each type of system class, and IntegraModel stores a map of class names to their ClassDefinition.

Note that (as discussed above) system classes and their nature are assumed as fixed by the GUI – it needs to make hard-coded assumptions about their attributes because they provide the gui's core functionality, so the information in their runtime ClassDefinitions as downloaded from libIntegra is duplicated by hardcoded references to attributes. However the information in the ClassDefinitions does allow their attribute values to be queried by ModelLoader in an abtracted manner, saving repetitive code for this task.

3.1.6.2.ModuleDefinition

The class ModuleDefinition inherits from ClassDefinition, and is used to store class information about non-system classes. It adds to ClassDefinition information about a module's inputs and outputs, as well as an array of Control Definitions for the module.

3.1.6.3.ControlDefinition

The class ControlDefinition describes which controls should be available for a module. There is a one-to-many relationship between controls and attributes – many controls are connected to only one attribute but it is also possible for a single control to control many attributes. The mapping is not directly provided by libIntegra, rather it is referred from the attribute definition by ModelLoader, with default behaviours for modules which do not define any controls to use.

3.1.6.4.Module Control Layouts

ControlDefinition also stores information about how controls should be laid out in the Module Properties view – this information is not currently provided by the server, but is loaded from a set of optional xml files located in <applicationDirectory>/ModuleControlLayouts/ by the class ControlPositionLoader.

These xml files are currently created by a standalone flash utility application called ModuleLayoutEditor, which is located in the source tree under integralive/database/trunk/ModuleLayoutEditor, There is a plan to eventually integrate this editor into the module definition gui in to web backend, at which point control layouts could be included within libIntegra and the XMLRPC API, and loaded along with all the other non-instantiable Model Components.





3.2.The Controller

The 'Controller' part of IntegraLive GUI's MVC consists of a set of 'command' classes, each of which stores data to manage a single operation, and contains code to apply the operation to the internal model and libIntegra via the XMLRPC API.


It also contains:



All source files relating to the controller are located in the source tree under the directory src/components/controller/.

3.2.1.IntegraController

IntegraController is implemented as a singleton with a static 'getter', so is accessible from anywhere in the application. In practice it is never referred to from any part of the model. but is used by views everywhere where they need to apply changes to the model.

IntegraController contains a public method processCommand( <command> ). This is the entry point for all such changes to the model – views instantiate commands and pass them into IntegraController.processCommand.

IntegraController also contains public methods relating to the lifecycle of a project (new, load, save etc), and relating to the undostack (canUndo, canRedo, undo, redo).

3.2.2.Command Classes

All command classes inherit from either of the classes ServerCommand or UserDataCommand, which in turn both inherit from the class Command, as illustrated in the following inheritance diagram:



The each command class has private variables to store any data specific to the operation it represents. These variables are passed into, and initialized by the command's constructor. For example, the command SetTrackColor stores private variables track ID and colour.


The base classes for command classes define virtual functions, which are called by IntegraController.processCommand, and implemented by concrete command classes. This allows IntegraController to handle command classes with no specific knowledge of their details.


Concrete classes do not typically need to override all the available virtual functions, only those that are relevent to them.

The virtual functions defined in Command are as follows:

Function name

Purpose

initialize()

Implement initialize() for two reasons:


1) to initialize any members whose initial values are not always specified by the sender of the command.

2) to return false and reject the command if the parameters are invalid or if the command would do nothing (ie replace a value with the same value.


Note that initialize is only called for commands that are instantiated by views. They are not called when commands are executed from the undo/redo stack. The implication of this is that inverse commands should not rely on initialize to initialize default values – they should all be provided in advance.

generateInverse()

Implement generateInverse() to make successive (typically 1) calls to pushInverseCommand with a set of commands which do the exact opposite of this command. This is how the undo/redo functionality is implemented – each command defines its opposite, and the opposite can be executed to revert the project to its previous state. The original command is also retained to be reexecuted to apply redo if necessary.


For example, a the command AddBlock implements generateInverse by calling pushInverseCommand( new RemoveBlock( _blockID ) )

preChain()

Implement preChain() when a command needs to execute other commands before executing itself (for example removing connections before removing a module). This is a mechanism to construct 'cascading' commands, in order to reuse existing functionality when one operation constitutes a subset of another operation.

execute()

Implement execute() to update the internal model from previous state to new state according to this command. This is the 'meat' of the command, and will generally need to be implemented for every concrete command class.


The execute() methods of command classes are the only place where data in the internal model should be directly written to.

postChain()

Implement postChain() when a command needs to execute other commands after executing itself (for example selecting an object after it was created). Like preChain() this method allows construction of 'cascading' commands in order to reuse existing functionality when one operation constitutes a subset of another operation.

canReplacePreviousCommand()

Implement canReplacePreviousCommand for commands which replace a value, to minimise size of transactions in which a single value changes many times. canReplacePreviousCommand must only return true when the effect of the previous command is entirely replaced by the effect of this command (eg setting the same attribute of the same module instance).


3.2.2.1.ServerCommand

In addition to these methods, commands which inherit from ServerCommand, and thus affect libIntegra (as opposed to merely affecting GUI-specific user data) can override the following functions, defined in ServerCommand:

executeServerCommand()

Implement executeServerCommand to actually send the XMLRPC command to the server.

testServerResponse()

Implement testServerResponse to check whether the libIntegra command has executed as expected. Return false if the command does not appear to have executed successfully.


Note that there isn't a one-to-one mapping between concrete implementations of ServerCommand and commands in the XMLRPC API.


In general, there are more Server Commands in the GUI, and often multiple Server Commands use the same XMLRPC API command internally. For example the Server Commands SetModuleAttribute and SetPlayPosition both use the same XMLRPC API command 'set'.


Additionally, many Server Commands use more than one XMLRPC API command in their implementation. They do this by using the XMLRPC 'multicall' notation, to send an array of commands. For example the Server Command AddBlock sends 'new' commands to libIntegra to instantiate a container, and an envelope next to it (to control the block's 'active' attribute), and a connection to connect the envelope to the block, and control points to the block envelope, and also sends 'set' commands to libIntegra to perform all necessary initialisation on the attributes of these new objects.


Concrete implementations of ServerCommand are all located in the source tree under the directory src/components/controller/serverCommands.


3.2.2.2.UserDataCommand

Commands which inherit from UserDataCommand, and thus only affect the “userData” attribute of data objects can override the following function, defined in UserDataCommand:

getObjectsWhoseUserDataIsAffected()

Override this function to fill a vector with the IDs of all objects whose userData was changed by the command. This allows IntegraController to automate the process of propagating the xml string representation of each object's userData up to libIntegra, whilst efficiently only dealing with objects whose userData might have changed.


Additionally, UserDataCommand provides protected helper functions to implement 'utility-type' functionality that is needed by more than one concrete User Data Command.


Concrete implementations of UserDataCommand are all located in the source tree under the directory src/components/controller/userCommands.


3.2.2.3.Undo/Redo Stack

The Undo/Redo stack consists of the classes UndoManager and Transaction, both of which are located in the source tree under the directory src/components/controller/undostack.


Transactions are vectors of commands, each of which contain vectors of 'inverse commands', and the undo manager stores a vector of transactions for the undo stack, and a separate vector of transactions for the redo stack. These transactions are applied by UndoManager, which iterates through the constituent commands, passing them to an 'inner process method' supplied by IntegraController. This inner process method calls the commands' virtual functions execute, and executeServerCommand, and updates views accordingly, but does not call initialize, preChain or postChain.


The undo manager is only accessed directly by IntegraController, which encapsulates its functionality and presents a simple set of undo-related public methods to the application framework.


The concept of 'transactions' is necessary because when the user clicks 'undo' or 'redo' they expect these actions to relate to what conceptually feels like a single action, as opposed to undoing or redoing individual commands.


Commands can be concatenated into the same transaction for the following reasons:


3.2.2.4.UpdateReceiver

The class UpdateReceiver is in charge of updating IntegraLive GUI when libIntegra's state changes for some external reason. There are various reasons why this could happen:

  1. Modules changing their own attribute (eg Player's play position, or Audio Input's vu meter).

  2. libIntegra's implementation of connections between attributes – one attribute changing because it is the target of a connection from another attribute whose value has changed.

  3. Commands from the scripting engine.

UpdateReceiver works by making repeated calls to the XMLRPC API function 'updates', passing in a state index referring to the last state known by the GUI. This 'updates' function is implemented by libIntegra as a delayed-response function – it doesn't return until at least one command has executed. The GUI also waits for a response to each call to 'updates' before making any subsequent calls.

On receipt of a response from an 'updates' call, UpdateReceiver examines the response (an array of XMLRPC API commands) and attempts to construct an ephemeral ServerCommand-derived class from each XMLRPC API command. These constructed Server Commands are not fed through IntegraController in the usual way, because they are not commands emanating from views, and shouldn't be pushed unto the undo stack or sent back to the server. Rather, their execute methods are invoked (to update the GUI's internal model), and they are dispatched in an IntegraCommandEvent, to signal any views to update.

This approach has the advantage that views' updating code is encapsulated from the reason a command happened, eg if the play position has moved, a view can use the same code to update it's appearance regardless of whether the change came from libIntegra or a different view.



Note: at present, external actions can only change object's attribute values. They cannot create or delete objects. Consequentially, UpdateReceiver only handles XMLRPC API 'set' commands. Also, it doesn't handle all conceivable 'set' commands, only the ones it expects to encounter. The implementation of UpdateReceiver may need to be expanded in future, but hopefully the overall architecture of the system can be retained and built upon.

3.2.2.5.Controller Event Classes

The controller notifies views and the application framework about changes to the state of the Internal Model by dispatching actionscript Event Classes. There are a set of custom event classes, all of which are located in the source tree under the directory src/components/controller/events.

The set of controller event classes are as follows:

AllDataChangedEvent

Dispatched when data has been reloaded from the server in it's entirety. Instructs views to update their state from scratch.

IntegraCommandEvent

Dispatched when a command has executed. Stores a reference to the command as a member variable, and allows views to perform light, context-specific updates by interrogating the command which has occurred and only updating as much as necessary.

ImportEvent

Dispatched on start & end of import processes – intercepted by the application framework to put the application into a modal state whilst the import process is underway, and end the modal state on completion.

CreateDefaultNewProjectObjectsEvent

Dispatched on completion of reloading the data from the server, in cases where non-mandatory new objects need to be created (see section on “Creation of default new project objects” above),

ReloadAllDataEvent

Dispatched when all data needs to be reloaded, for example as a failsafe mechanism when a serious error is detected.

ServerTimeOutEvent

Dispatched when the server has timed out, to instruct the application framework to display a 'server timeout' error, and close.



3.2.2.6.Importing data objects from disk (Tracks / Blocks / Modules )

The commands to import data objects from disk (ImportTrack, ImportBlock and ImportModule) are awkward because they break the normal order in which Server Commands are processed.

Normally when IntegraController processes a Server Command, it dispatches the XMLRPC API call to the server, updates the internal model, and updates views within a single entry-point, and then only waits for the XMLRPC-API response to confirm that the result was as expected.

This approach only works because the effect on libIntegra's state of most XMLRPC-API commands is entirely predictable by the GUI – it essentially duplicates the same change within the Server Command's execute function, and obtains an advance copy of what libIntegra's new state will be once the XMLRPC-API call has completed.

However, the effect of importing data objects is not predictable, because there is no code to load and interpret Integra XML Documents in the GUI. So instead, the GUI dispatches the XMLRPC-API command, to initiate the loading, and then sends a special event to lock the GUI into a modal state (and prevent and subsequent commands from being executed) until libIntegra responds. Then, it reuses functionality in ModelLoader to query libIntegra for the new state of it's object tree, and finally dispatches another special event to unlock the GUI and allow resumption of further commands.





3.3.The Views

The 'Views' part of IntegraLive GUI's MVC consists of a set of 'view' classes, each of which inherits from the base class IntegraView:




Each view displays a component of the GUI, querying IntegraModel to know what to display, passing commands into IntegraController to update the project's state, and responding to updates from IntegraController when the state of the project has changed.

Views are instantiated in a hierarchical way, mimicking their physical positioning within the main application frame.

Some views' lifecycles match the application's lifecycle (ArrangeView/ModuleGraph etc), whilst some views' lifecycles match the lifecycles of individual data objects (ArrangeViewTrack, BlockView etc). These shorter-lived views are created and destroyed by their containing view, and occupy a subset of the same screenspace. For example, instances of ArrangeViewTrack are created and destroyed by ArrangeView, and live 'inside' ArrangeView, as owned child views. In the most extreme example, BlockView manages a set of overlapping EnvelopeView instances, one for each envelope that exists in the block. These Envelope Views occupy the same space as each other – they are overlays.

In order to maintain encapsulation, views which own other views shouldn't tightly couple to them – they should be as ignorant as possible of the nature of the views they are managing, and should rely on the normal workings of the MVC for any actions which affect more than one view.

The following diagram illustrates the tree of view instantiation and ownership, from the top level framework down:





Apart from view classes themselves, the 'views' part of the MVC also contains:



All source files relating to the views are located in the source tree under the directory src/components/views/.

3.3.1.IntegraView

IntegraView contains a set of virtual functions individual view classes can override, and public and protected functions views can call.

3.3.1.1.View updating functions

The following functions control the views' updating logic.

virtual function onAllDataChanged

Implement this function to provide the view's “full update”. This function should assume that the internal model has changed arbitrarily, and the view should update itself completely from scratch. It is called when new projects are created or loaded, when views are created, and when views are revealed after a large number of commands occur when the view is hidden. It is also called when the testing 'update all views' or 'reload from backend and update all views' commands are invoked.

function addUpdateMethod

This function is called repeatedly from views' constructors to register command-specific update functions for all commands which can cause this view to need updating. The implementation of these update functions can assume that the view was up to date before the function was called, so can perform a minimal 'smart update'.



Views do not receive any update calls when they are hidden; instead they are flagged as dirty, and receive an update call when they are revealed.

However, since it is possible for a view to be hidden whilst its titlebar is not hidden (in the case of collapsed views), there is a separate system for identifying commands which can cause the titlebar contents to become invalid. This system consists of a set of functions for registering command classes which can make the various elements of each view's titlebar become invalid:



function addTitleInvalidatingCommand

This function is called repeatedly from views' constructors to register command classes which can cause the view's title to become invalid.

function addTitlebarInvalidatingCommand

This function is called repeatedly from views' constructors to register command classes which can cause the view's titlebar view to become invalid.

function addVuMeterChangingCommand

This function is called repeatedly from views' constructors to register command classes which can cause the view's vuMeter container ID to become invalid.

function addColorChangingCommand

This function is called repeatedly from views' constructors to register command classes which can cause the view's colour to become invalid.

Note that IntegraView itself does not implement a titlebar; this functionality is provided by the class ViewHolder. So these titlebar-related only need to be used by views which are instantiated within a ViewHolder.



3.3.1.2.Titlebar-related functions

The following table describes the role of IntegraView's virtual functions related to views' titlebars:

virtual getter title

Implement to return the view's title – either a constant such as “Arrange View”, or a changeable value such as the name of a selected object.

virtual setter title

Implement to allow views to process a command to rename an object when the user uses a view's titlebar to provide a new object name.

virtual getter isTitleEditable

Implement to allow views to specify whether their title should be readonly or read/write. The virtual setter for title will never be called unless isTitleEditable returns true.

virtual getter titlebarView

Implement to return a separate IntegraView­-derived view instance to be used as a custom view embedded within this view's titlebar. Such titlebar views are used in the titlebar for ArrangeViewProperties, which displays selected block's properties, or selected scene's properties, depending on current selection.

virtual getter vuMeterContainerID

Implement to return the ID of a Block, Track, or Project for which a vu meter should be displayed in the titlebar of this view. This method is used for views such as ModuleGraph, ArrangeViewTrack, LiveViewTrack, ArrangeView and LiveView, which display aggregate vu meters in their titlebars.

virtual getter color

Implement to return the foreground colour for the view's titlebar (this colour will be used to display the collapse/expand button, title label and vuMeter).

Note that IntegraView itself does not implement a titlebar; this functionality is provided by the class ViewHolder. Therefore the titlebar-related functions will have no effect unless used from a view which is instantiated within a ViewHolder.

3.3.1.3.Window size and state-related functions

The following table describes the role of IntegraView's functions related to views' size and state:

virtual function resizeFinished

Implement to allow views to process a command to store a view's size in user data.

virtual setter collapsed

Implement to allow views to process a command to store in user data the fact that the view's expand/collapse state has changed.

virtual getter isSidebarColours

Implement to tell the IntegraView to render it's background with 'sidebar' appearance rather than as a “main content view”.

setter hintLabel

Pass a string into this function to display a “newbie hint” (eg “double-click here to create a block”). Pass null to remove the hint. Typically views only display hints when they are empty – so when the user creates an object the hint disappears.

setter hintLabelVerticalCentre

Pass a number into this function to alter the vertical centering of the hint. This is only needed if the hint label's verical centering needs to be offset.

setter contextMenuDataProvider

Use this function to provide an array of objects specifying the view's context menu contents. Each menu item object can contain a label, keyEquivalent, handler function (executed when the menu item is chosen), and updater function (executed to update the menu item's attributes such as checkstate, enabled etc before the menu is displayed).

virtual function onUpdateContextMenu

Implement to allow views to customize the updating of their context menu. Note that views only need to implement this function if the actual set of menu items can change at runtime (eg LiveViewBlock). If only the attributes of menu items (checkstate, enabled etc) change, views should provide menu item updater functions in the data provider instead.

virtual function free

Implement to allow views to drop reference to any circularly-referenced child objects, prior to the view being no longer used. This is to ensure that actionscript's garbage collection is able to successfully delete the entire tree of unused objects.

Note that IntegraView itself does not contain resizing or expand/collapsing functionality; this functionality is provided by the class ViewHolder. Therefore these functions will only be executed from views which are instantiated within a ViewHolder.

3.3.2.Classes relating to presentation of views

Because there are many commonalities in the way views are presented within the top level application framework, there are some classes which act as containers for views, in order to reuse code wherever there are similarities and patterns. Source files relating to these classes are located in the source tree under the directory src/components/views/viewContainers.

3.3.2.1.ViewHolder

ViewHolder is a container for IntegraView. Since there are many similarities and many differences between the ways in which different views are presented, ViewHolder has a broad range of optional functionality, so that views can mix & match the subset of functionality they use.

ViewHolder provides the following functionality:



ViewHolder has no direct visibility of concrete view classes; it interacts with them via virtual functions defined in IntegraView (described above), and receives notifications from them via IntegraViewEvents dispatched from IntegraView, in a manner which is transparent to concrete view classes.

Not all views are managed by an ViewHolderl; views which have their own title bar are, and views which do not have their own title bar are not.

Views which are contained by ViewHolder: ArrangeView, ArrrangeViewTrack, BlockLibrary, ArrangeViewProperties, ModuleGraph, ModuleLibrary, ModuleProperties, LiveView, KeybindingView, ArrrangeViewTrack, LiveViewTrack.

Views which are not contained by ViewHolder: RibbonBar, MainViewContainer, BlockPropertiesToolbar, ScenePropertiesToolbar, LiveViewBlock, BlockView, EnvelopeView, RoutingView, ScriptingView, RoutingItem, ScriptingItem, TrackColorPicker, TimeLine.

3.3.2.2.ViewTree

ViewTree is a container for ViewHolder-contained views, which presents a scrollable, expand/collapsible list of views. It is used to present collapsible tracks in the arrange view and live view. ViewTree presents a public interface similar to that found in common list classes, with commands to insert, remove, retrieve items. ViewTree implements optional tree reordering functionality (via drag & drop of views' titlebars).

3.3.2.3.IntegraViewEvent

IntegraViewEvent is a custom event class which manages communication from IntegraView to it's containing ViewHolder. It can represent a variety of different event types. Concrete view classes and their owners should not need to use this event class directly; it should be an internal implementation detail between IntegraView and ViewHolder.



3.3.3.GUI widget classes

Many views contain visual subcomponents which are too complex to use existing mxml controls, but do not contain enough functionality to be free-standing 'views' themselves. These have been implemented as dumb leaf 'widgets' – they should not interact with the model or controller, instead they receive imput from their owner via settable properties, and communicate input back to their owner through flash events. Typically these widgets perform specialised roles relating to only one view, so are not reused by other views.

The set of widget classes are:

3.3.4.Skin Classes

Many views and gui widgets use standard MXML controls, but customise their appearance using programmatic skins. These skins are typically reused by multiple views and gui widgets, and their appearance can be styled as necessary. The following table shows the set of skins, the controls they are used for, and the styles they respond to:

Skin

Control skin is used for

Styles skin responds to

AddButtonSkin

Button

color, fillColor, fillAlpha

CloseButtonSkin

Button

color, fillColor, fillAlpha

CollapseButtonSkin

Button

color

ComboBoxSkin

ComboBox

colorScheme

IntegraScrollThumbSkin

Scrollbar Thumb

colorScheme

IntegraScrollTrackSkin

Scrollbar Track

colorScheme

TabButtonSkin

Button

colorScheme

TickButtonSkin

Button

color

3.3.5.Mouse Capture

In any application in which objects can be dragged by a pointing device, it is necessary to 'capture' mouse input during drag operations, so that mouse input messages are relayed to the part of the program responsible for handling the drag operation, regardless of the current position of the mouse, until the drag operation ends. Even if the mouse leaves the application window altogether, and passes over another application, the dragged object should continue to receive mouse input.

Most development environments provide a simple mechanism to achieve this functionality, however in flash it is not simple; it requires a cumbersome process of multiple event listeners.

IntegraLive GUI encapsulates this complexity by providing a simple utility class MouseCapture, which exposes simple static public functions for capturing and receiving mouse input.

As an additional benefit, the MouseCapture utility assigns a numeric ID to each mouse capture operation, so that each drag operation which is tracked by MouseCapture can be uniquely identified by the function MouseCapture.getCaptureID(). This mechanism is used by UndoManager to determine whether commands should be within the same transaction as a previous command (see discussion of undo stack above).



3.4.Top Level Application Framework

The top level of the application is implemented in main.mxml. Conceptually it is outside it MVC – it is responsible for instantiating the views, and also manages the main application window, application level menus etc.

Its main features are as follows:

3.4.1.Instantiation of Views

The application framework instantiates the views RibbonBar and MainViewContainer, thus initiating the cascading tree of view instantiation described above in “The Views” above.

3.4.2.Menus

The application framework defines the main application menus, including 'handler' functions (executed when menu items are chosen, these implement the menu items' functionality) and 'updater' functions (executed when menus are displayed, these update each menu item's check state, enabled state etc).

3.4.3.Persistant State

The application framework contains the entrypoint for functionality to load and store persistent view layout state from an xml file called “IntegraLiveguiapplicationState.xml”, which is stored in the OS-specific application storage directory. The functions loadPersistantState and storePersistantState handle the top level window position and show state (maximised vs normal) and the application-level font size. They then delegate some more view-specific persistent state settings to the class MainViewContainer.

3.4.4.Save Changes Functionality

Whenever the user navigates away from an open project (by starting a new project, opening a different project, or closing the application), the application framework checks whether the project state has changed since it was last saved. If it has changed, it displays a message box asking whether the user would like to save their changes (yes|no|cancel). If they opt to save their changes, it checks whether the project has already been saved, and automatically performs a 'save' or 'save as' operation as appropriate, before continuing with the operation they instigated before the “Save Modified” prompt was displayed.

3.4.5.Modal state

When IntegraLive GUI loads data from the server, using the class ModelLoader as described above, the application becomes temporarily locked. This happens in the following circumstances:

  1. Loading the application

  2. Creating a new project

  3. Loading a project

  4. Importing Tracks, Blocks, or Modules from disk

The reason this is done is to prevent the user from carrying out any additional actions when the internal model could be in an interim state, rather than beingfully synchronised with libIntegra.

The application framework is responsible for putting the application into this locked state, and unlocking it when the loading operation is complete. It does this by displaying an invisible modal dialog (this greying out the rest of the application and throttling input messages), and disabling all menu commands except for 'exit'.

The functions responsible for handling 'modal state' are called enterModalState and finishModalState.

3.4.6.Application-wide Styles

There are a few styles which are set at the application level. Some of these are fixed for the lifecycle of the application, and some can change at runtime.

3.4.6.1.Fixed styles
3.4.6.2.Styles which can change at runtime

3.5.Utility classes

Within the directory src/components/util/ are a set of reusable helper classes. These are conceptually outside the MVC as they tend to be reused by multiple classes. Their purposes are as follows:

3.5.1.ControlContainer

The class ControlContainer is the main GUI application's bridge with the control control widgets. It loads and displays them, sets their values, listens to their responses, and fires Controller commands when module attribute value change as a result of control widget input.

It contains some marshalling code to convert between different attribute types and scales, and supports 'enumeration' type controls by handling the 'allowed value' field of attribute definitions.

3.5.2.AggregateVUContainer

The class AggregateVUContainer is a specialised version of ControlContainer, used to embed vu meter controls into views' titlebars. It is far simpler than ControlContainer, since it only has to connect with one type of control, and only needs to send information in one direction (vu meters are readonly controls).


Unlike ControlContainer, AggregateVUContainer does not directly display the value of module attributes. Instead, it calculates the maximum value of all AudioOut and StereoAudioOut modules' vu attributes for a given container, and sends this maximum value to the vuMeter control. Therefore, AggregateVUContainer instances display the 'aggregate' vu for a container, rather than individual vu attributes.

3.5.3.ControlMeasurer

Controls specify their own default, minimum and maximum dimensions, but the GUI needs to know this information when laying controls out in the module properties view and adding them to the live view.

Since controls are delay-loaded, it's impossible to query them for their default, minimum and maximum sizes until one has been loaded. And since they're plugins, the GUI could find itself loading controls which didn't even exist when the GUI was compiled. So, on startup the GUI preloads one instance of each control, in order to query and store these dimensions, before unloading the control. This is done in the singleton class ControlMeasurer, which then exposes query functions to obtain these control dimension data.


3.5.4.ControlPool

Loading controls can be timeconsuming – it is a memory intensive procedure and in terms of user experience, actions which cause controls to load are the least 'responsive' in IntegraLive GUI. To mitigate this problem, ControlPool maintains a collection of instances of all the controls which have been loaded previously, so that they can be reused instead of new ones being loaded.


When a displayed control instance is no longer needed, instead of unloading the plugin .swf, and dropping all references to it so that the flash garbage collection can free it, it is merely removed from the display list, and a reference to it is passed to ControlPool, which can retrieve it when it is next needed.

3.5.5.CursorSetter

The class CursorSetter defines an enumeration with entries for each of IntegraLive GUI's custom mouse pointers. It also embeds the graphic resources for each of these mouse pointers, and exposes static functions to set and remove custom mouse pointers. These functions are used by all views and GUI widgets which use custom mouse pointers.


Note that custom mouse pointers are referenced by ID. The function CursorSetter.setCursor returns a mouse cursor ID; this ID must be tracked by the caller in order to be passed into CursorSetter.removeCursor when the custom cursor is removed. This allows multiple custom-cursors to be 'stacked up'.


Also note that the implementation of the function CursorSetter.setCursor defines the 'active point' offset for each custom cursor type. If new custom cursors are added, or cursor graphics are changed, these active point coordinates must be updated to reflect the desired active point for each custom cursor.


3.5.6.FontSize

The class FontSize defines an enumeration of font sizes, as well as static methods to iterate up and down through the set of font sizes, and methods to obtain 'row height' and 'button height' relative to fontSize. These helpers are used to scale application components which need to resize when the application-level font size changes.

3.5.7.IntegraAlert

IntegraAlert is a helper class to display the standard flash Alert with an appearance consistent with IntegraLive GUI's graphic designs. This class should be used wherever alerts need to be displayed.


3.5.8.IntegraConnection

IntegraConnection extends the ConnectionImpl class in the XMLRPC library. It adds to ConnectionImpl the following functionality:


  1. The queue is already empty

  2. The previous call completes

    Call queueing eliminates the possibility of XMLRPC calls being received by libIntegra in a different order to that in which they were sent.




3.5.10.LibraryRenderer

The class LibraryRenderer is a custom item renderer used to create the appearance of items in BlockLibrary and ModuleLibrary. It responds to the colorScheme style, and renders itself with a flash bevel filter, and a glow filter which lights up on mouse over.


It also tests for an “isUserItem” attribute on the data to be rendered, and uses a softer shade of background colour when this attribute exists and evaluates to <true>. This allows BlockLibrary to achieve a distinction in appearance between user items and library items.


3.5.11.MessagePopup

The class MessagePopup displays a simple Alert-style popup message. Unlike IntegraAlert, MessagePopup has no buttons. It is displayed when the application enters a modal state, but has been hidden for cosmetic reasons. Therefore, this class is largely redundant.


3.5.12.RepositionType

RepositionType is a simply enumeration, used by any part of the GUI in which different types of reposition operation need to be distinguished from each other.


3.5.13.StartControlRepositionEvent

The class StartControlRepositionEvent is dispatched by ControlContainer when a control is repositioned. Control reposition operations have to be initiated by the ControlContainer, because controls can be repositioned by clicking on any inactive area of the control widget itself. The event is received by any owner of ControlContainer in which controls can be repositioned (currently only LiveViewBlock).


3.5.14.Utilities

The class Utilities is a collection of reusable individual static helper functions.