-
Notifications
You must be signed in to change notification settings - Fork 3
For Developers
This section intends to give people, who wants to contribute to the symbolist project, informations and advices about how to develop, compile or write documentation for the project.
- Pre-requisites
- Project's architecture
- Setting up the work environment
- Development guidelines
- Writing and generating documentation
- Warning! For now, symbolist have been compiled, tested and executed only under the macOS platform (starting with version 10.10 Yosemite). Thus, all instructions and informations published here are worth only for this platform.
- Bravura font. symbolist uses the Bravura font to integrate traditional music notation symbols in its palette. Therefore, the Bravura font must be installed in your computer if you want to use these symbols. You can download the Bravura font at https://www.smufl.org/fonts/. To complete the installation of the Bravura font, copy the bravura_metadata.json file (you'll find it in the Bravura font zip file) into your /Library/Fonts/bravura folder (create this folder if it doesn't exist).
Here is described the architecture of the symbolist project.
- Builds: contains three subfolders, one for each OS: Linux, Windows and macOS. The symbolist's XCode project is to be found under the MacOSX subfolder.
- Source: symbolist source code.
- SymbolistTests: contains all files for unit tests and integration tests in symbolist.
- JuceLibraryCode: contains all headers generated by the Projucer app to include JUCE library's modules into the symbolist source code.
- Documentation: contains the Doxygen directory to generate the doc in html format, and the Images directory where are stored the different images populating the Wiki.
- OM: contains all files related to the integration of symbolist in the OpenMusic language.
- max: contains all files related to the integration of symbolist in the Max language.
- symbolist.jucer: the symbolist Projucer file, to manage and generate projects for specific IDE (XCode, Visual Studio…).
- symbolist-deploy: bash script to automate the deployment of symbolist as a Max and an OpenMusic object.
For now the development, and deployment of the symbolist software have been tested on the macOS platform only. Therefore, the following instructions about how to set up the working environment for symbolist are described only for macOS. However, in the future, instructions for the setting of symbolist in other platforms will be added.
The following configuration have been tested in macOS Sierra (v.10.12.6). Please, do follow each one of the steps described below in order to set the working environment for symbolist. The location of each folder, containing libraries and source code, is specified and must be respected, in order to match the relative pathes of the XCode project.
- Get JUCE. symbolist is developed using the JUCE C++ Library. In order to compile and run symbolist, you need to download the JUCE Library (https://shop.juce.com/get-juce/download) and place the downloaded JUCE folder at the same level than your symbolist repository. Currently, version 5.2 of the JUCE library is used. Trouble can be experienced when trying to compile symbolist with an earlier version of JUCE (5.3, for example).
- libo. The libo library (CNMAT, Berkeley) is used to handle the underlying OSC (Open Sound Control) structure of symbols in a symbolist score. In order to compile the symbolist project, download the libo git repository from its GitHub page (https://github.com/CNMAT/libo), and place it at the same level than your symbolist repository. Then, generate the libo.a static library file by following the instructions written in the GitHub page. Let libo.a in its original folder (libo.a in libo).
- Max SDK and libomax. In order to compile symbolist as a Max object, download and extract the Max SDK (https://cycling74.com/downloads/sdk), and clone the libomax git repository (https://github.com/CNMAT/libomax) both in the same location as your symbolist repository. Then, compile the libomax library as described in the libomax GitHub page, and let the produced libomax.a file in the libomax folder.
If the previous steps are correctly followed, the directory containing your symbolist repository should look like that:
.
+-- symbolist // your symbolist repo.
+-- JUCE // JUCE Library source code (version 5.2).
+-- libo // libo repo containing libo.a after compilation.
| +-- libo.a
| ...
+-- libomax // libomax repo containing libomax.a after compilation.
| +-- libomax.a
| ...
+-- max-sdk // containing the Max SDK source code.
This section exposes guidelines to develop the symbolist application following the MVC way of programming.
The symbolist's software architecture follows the Model-View-Controller (MVC) pattern. As the JUCE framework doesn't provide generic classes to implement the MVC in its applications, an MVC API of our own "brew" is integrated in symbolist. The figure 1 describes the UML class diagram representing our MVC API.
Figure 1. An MVC API for symbolist.
The Observer and Observable classes provide an implementation of the Observer design pattern. This pattern is used in a MVC architecture to enable the communication between the model (here represented by the SymbolistModel class) and its related views and controllers. Each time a information is updated in the model, the views and controllers of the application are informed, leveraging the notification system (the method notify is executed by the model, asking all of its observers to perform their update method). Views and controllers of the application are therefore subclasses of the Observer class, meaning that they are observing the model, which inherits from the Observable class.
Two abstract superclasses describing the concept of View and Controller are then inheriting from the Observer class. The View and Controller classes are abstract, considering that the update method received from the Observer class is still undefined at this level. Concrete views and controllers will determine their own update behavior. The SymbolistModel class, representing the model of the symbolist application, is a concrete class. The model concept is not represented by an abstract Model class, as it is too broad a concept and can practically represent anything.
Figure 2 shows how views and controllers composing the symbolist application inherits from the View and Controller abstract classes, and how the application's main view is divided in multiple subviews, structure which is replicated in the controller part.
Figure 2. Applying the MVC pattern in symbolist.
The symbolist's graphic interface is composed of a main view (instance of the SymbolistMainComponent class), subdivided in subviews: the score, the palette, the inspector… Therefore, the composition of the symbolist's GUI is hierarchical; the main view references the score, palette and inspector views as its childs. In their turn, each subview can embed its own subviews, etc. The controllers hierarchy mirrors the views hierarchy; each view is accompanied by its own controller, when necessary. Sometimes views invoke the controller associated to their parent view to interact with the model and, therefore, don't need a specific controller. Indeed, when a new view is added to symbolist, the creation of a new controller is necessary if the new view and the model are interacting a lot. Therefore, a new view-controller couple is set. A view-controller couple is responsible for handling a specific set of interactions with the user. For instance, the InspectorComponent-InspectorController couple is responsible for handling all interactions regarding the symbol inspector, which permits to update the properties of a symbol's inner OSC bundle.
To explain how to develop new functionalities in symbolist in a way that observes the MVC architecture, the following example is proposed. Let us consider the addition of a new code editor window in symbolist, to define new drawing tools by scripting and adding these tools to the palette. The figure 3 presents a mock-up of our new code editor, and describes its behavior regarding the palette component.
Figure 3. Mock-up for a new code editor.
First, we need a graphic component representing the code editor. This component can be a CodeBoxComponent (defined in symbolist), or can be a specialization of the CodeBoxComponent class that we will call the DrawingToolsComponent class.
Code guideline: Classes representing graphic components are suffixed by the word Component in symbolist.
Then, we need to determine where this new graphic component should be placed in the views hierarchy. We have two choices: either the DrawingToolsComponent is a child view of the PaletteComponent, or it is a child view of the SymbolistMainComponent. What is important is to choose the parent view of our DrawingToolsComponent according to what makes more sense regarding the application structure. Here, we can imagine that our code editor is toggled by a button located in the PaletteComponent. So, setting DrawingToolsComponent as a child view of the PaletteComponent will save us some code writing. Indeed, it is easier for the PaletteComponent to toggle the code editor, than to ask the SymbolistMainComponent to toggle the code editor when the button is clicked.
Now that the DrawingToolsComponent is set as a child view of the PaletteComponent, we are free to associate it to a new controller, the DrawingToolsController, or to estimate that the DrawingToolsComponent doesn't need a new controller as it can use the controller associated with its parent view (the PaletteController). For the example to be exhaustive, we will demonstrate how to proceed to associate a new DrawingToolsController to our DrawingToolsComponent. To do so, the DrawingToolsComponent (respectively DrawingToolsController) class must inherit from the View (respectively Controller) class, which is a templated class. The listing 1 shows the source code for the definition of the DrawingToolsComponent and the DrawingToolsController classes.
/**
* DrawingToolsComponent class.
*/
class DrawingToolsComponent : public Component,
public View<SymbolistModel, DrawingToolsController>
{
public:
/**
* Calls the drawing tools controller when the "add to palette" button
* is clicked.
*/
inline void buttonClicked(Button& button)
{
if (button == &add_to_palette_button)
{
getController()->addToolFromSourceCode(code_script);
}
}
/**
* Inherited from the Observer class.
*/
void update() override;
// ...
private:
TextButton add_to_palette_button;
string code_script;
}
/**
* DrawingToolsController class.
*/
class DrawingToolsController : public Controller<SymbolistModel, DrawingToolsComponent>
{
public:
/**
* Asks the model to modify itself.
*/
inline void addToolFromSourceCode(string sourceCode)
{
getModel()->addToolFromSourceCode(sourceCode);
}
/**
* Inherited from the Observer class.
*/
void update() override;
// ...
}
Listing 1. Definition of the DrawingToolsComponent and DrawingToolsController classes.
The View (resp. Controller) class is a templated class receiving two template arguments. The first argument corresponds to the class of the model associated with the view (resp. the controller), and the second argument is the type of the controller (resp. view) associated with the view (resp. controller). In the listing 1, the first template argument is SymbolistModel for both controller and view as this is the class representing the model in the symbolist application. Declaring the type of the associated controller, or view, as a template argument, enables type checking when a view and a controller are linked together. The listing 2 exemplifies the linking phase between DrawingToolsController and DrawingToolsComponent, which is done by their respective parent classes, PaletteController and PaletteComponent.
/**
* PaletteController class.
*/
class PaletteController : public Controller<SymbolistModel, PaletteComponent>
{
public:
/**
* PaletteController generates its child controller, DrawingToolsController.
*/
inline PaletteController()
{
drawing_tools_controller = unique_ptr<DrawingToolsController >(new DrawingToolsController());
/*
* Links the drawing tools controller to its parent controller.
*/
drawing_tools_controller->setParentController(this);
}
/**
* Don't forget to detach the child controller from
* the model on destruction.
*/
inline ~PaletteController()
{
getModel()->detach(drawing_tools_controller);
}
/*
* Sets the model for this instance of PaletteController
* and for its child controller (the drawing tools controller).
*/
void setModel(SymbolistModel* model) override
{
Controller::setModel(model);
drawing_tools_controller->setModel(model);
// Attach drawing tools controller as an observer of the model.
model->attach(drawing_tools_controller);
}
/*
* Sets the view for this instance of PaletteController
* and for its child controller (the drawing tools controller).
*/
void setView(PaletteComponent* paletteView) override
{
Controller::setView(paletteView);
drawing_tools_controller->setView(paletteView->getDrawingToolsView());
}
/**
* A getter for the drawing tools controller.
*/
inline DrawingToolsController* getDrawingToolsController() { return drawing_tools_controller.get(); }
private:
unique_ptr<DrawingToolsController > drawing_tools_controller;
}
/**
* PaletteComponent class.
*/
class PaletteComponent : public Component, public View<SymbolistModel, PaletteController>
{
public:
/**
* PaletteComponent generates its child view, DrawingToolsComponent.
*/
inline PaletteComponent()
{
Component::Component();
drawing_tools_view = unique_ptr<DrawingToolsComponent >(new DrawingToolsComponent());
// Adds drawing tools view as a child view of palette component.
// However, drawing tools view is not visible (visible only when toggle).
addChildComponent(drawing_tools_view);
}
/**
* Don't forget to detach the child view from
* the model on destruction.
*/
inline ~PaletteComponent()
{
Component::~Component();
getModel()->detach(drawing_tools_view);
}
/*
* Sets the model for this instance of PaletteComponent
* and for its child view (the drawing tools view).
*/
void setModel(SymbolistModel* model) override
{
drawing_tools_view->setModel(model); // Inherited from the View class.
// Attaches drawing tools view as an observer of the model.
model->attach(drawing_tools_view);
}
/*
* Sets the controller for this instance of PaletteComponent
* and for its child view (the drawing tools view).
*/
void setController(PaletteController* paletteController) override
{
View::setController(paletteController);
drawing_tools_view->setController(paletteController->getDrawingToolsController());
}
/**
* A getter for the drawing tools view.
*/
inline DrawingToolsComponent* getDrawingToolsComponent() { return &drawing_tools_view; }
private:
DrawingToolsComponent drawing_tools_view;
}
Listing 2. Linking of the DrawingToolsComponent and DrawingToolsController classes.
As shown in the listing 2, the linking between DrawingToolsController and DrawingToolsView is done in the setView and setController methods of the PaletteController and PaletteComponent classes.
The figure 4 summarizes the chain of interactions between the different symbolist's components involved in the new drawing tools functionality.
Figure 4. A workflow for the new drawing tools functionality.
In order to keep a coherent syntax all over the project's source code, the few guidelines, drawn up here, are to be respected:
-
Class declaration must, as much as possible, follow the "one file-one class" rule. Many classes declaration must not be hidden in one file, in order to give an explicit view of the project structure when looking at the directory architecture.
-
Instance variables, and all private class attributes, must be placed at the end of the class declaration.
-
Instance variables are declared following the snake case syntax (underscore is the separator for each word composing the variable identifiers). For example:
int my_counter, my_second_counter
. -
Except for the instance variables, all the other identifiers (local variables, functions and class names...) are declared following the camel case syntax (the first letter of the identifier is in lower case, then the first letter of each word composing the identifier are in upper case). For example:
int myCounter
,void myFunction(int myCounter)
, orint myVeryVeryLongNamedVariable
. -
An object responsible for the creation of another object in the heap (therefore calling the new operator to get the object reference) must encapsulate the object reference in a smart pointer (unique_ptr or shared_ptr, depending on the case). For example, the object A owns a reference to an object B as an instance variable. A is responsible for the creation of B. Therefore, the A constructor is as follow:
inline A() {
// my_b is of type unique_ptr<B >
my_b = unique_ptr<B >(new B()); // Note that instance variable are snake case.
}
The use of smart pointers prevents from memory handling errors as memory leaks or cycling references...
- C-style casting must be avoided. Calls to the
dynamic_cast
andstatic_cast
(see the cpp reference pages about it: dynamic_cast, static_cast) must be preferred. Note thatdynamic_cast
andstatic_cast
return NULL when the cast fails, therefore you must always test the result of these functions before going on with the code.
SYMBOLIST uses the Doxygen tool to write and generate the code documentation. The next section presents how to install Doxygen and how to use it, and in addition, how to write block comments so they could be integrated in the docs.
The Doxygen program enables the automatic generation of documentation following the instructions contained in the configuration file (the Doxyfile). To generate the doc, download the Doxygen binary corresponding to your OS at http://www.stack.nl/~dimitri/doxygen/download.html. When the download is completed, add the binaries to your PATH variable :
export PATH=$PATH:/Applications/Doxygen.app/Resources
Go to the Doxygen directory under your local symbolist repository, then launch the doxygen command :
cd /path_to_your_local_repo/Doxygen
doxygen Doxyfile
This will create an html directory and with it generate all documentation files.
Doxygen supports the javadoc style for documentation's block comments. All block comments beginning strictly with /** will be considered as documentation comments. These blocks can precede all elements of a class definition : class name, fields, constructors, methods... Comments for documentation must be written in the header files. Here is an example of block comment for the documentation of a class method presented in the Oracle's website, on the page How to Write Doc Comments for the Javadoc Tool:
/**
* Draws as much of the specified image as is currently available
* with its northwest corner at the specified coordinate (x, y).
* This method will return immediately in all cases, even if the
* entire image has not yet been scaled, dithered and converted
* for the current output device.
*
* @param img the image to be drawn
* @param x the x-coordinate of the northwest corner
* of the destination rectangle in pixels
* @param y the y-coordinate of the northwest corner
* of the destination rectangle in pixels
* @param observer the image observer to be notified as more
* of the image is converted. May be
* <code>null</code>
* @return <code>true</code> if the image is completely
* loaded and was painted successfully;
* <code>false</code> otherwise.
* @see Image
* @see ImageObserver
* @since 1.0
*/
public abstract boolean drawImage(Image img, int x, int y,
ImageObserver observer);
SYMBOLIST Wiki