Understanding the JUCE demo


#1

I'm looking through the Juce demo, gradually figuring out how it holds together.

My respect for the community keeps growing, as I realise that pretty much everyone here must have figured it out by themselves.

I'm also starting to really like the idea of a technology that self-documents through a single demo. It is very zen. Straight away you can run it, find the functionality you want, then dig into the code and see how it works.

However, to someone such as myself with limited C++ familiarity, even getting my mind around the basic structure of this demo-app is proving a challenge.

So every time I figure something out I will add another post to this thread.  If the wiki ever comes back, I will move it across.

Corrections gratefully received, I am on the IRC Channel (#juce on https://webchat.freenode.net/) so if you message me on the channel, this thread will stay clean.

(otherwise someone will write a reply saying "woops you messed that bit up there", and I will fix it, and then the reply won't make any sense)

π


#2


MainWindow.cpp is where the action happens.  main.cpp is almost entirely Juce boilerplate in order to float the main window.

 

Build&Run and you'll see a list of 15 or so demoTypes on the left. Click any one of them and it will spawn an instance of that demoType on the right. Notice the language -- 'demoType, I'm doing that so as to mirror the code.

JuceDemoTypeBase::getDemoTypeList()  encapsulates a static array of all those demoTypes...

// *** MainWindow.cpp ***
:
Array<JuceDemoTypeBase*>& JuceDemoTypeBase::getDemoTypeList()
{
    static Array<JuceDemoTypeBase*> list;
    return list;
}

and each Demos/fooDemo.cpp, right at the end of the file, adds itself to this array with e.g.

// This static object will register this demo type in a global list of demos..
static JuceDemoType<KeyMappingsDemo> demo ("01 Shortcut Keys");

(I use KeyMappingsDemo.cpp as it is the shortest demo, less than one page of code)

Follow this into the JuceDemoType<XXX> constructor and it falls through into the JuceDemoTypeBase constructor, which adds it into the array:

// *** MainWindow.cpp *** 
:
JuceDemoTypeBase::JuceDemoTypeBase (const String& demoName)  : name (demoName)
{
    AlphabeticDemoSorter sorter;
    getDemoTypeList().addSorted (sorter, this);
}

Question: Why can we put the JuceDemoType<XXX> constructor in the .h, but the JuceDemoTypeBase constructor goes in some .cpp?
Answer: actually it doesn't matter, putting implementations in a .cpp keeps the header clean and can reduce compile time (thanks bazrush)

Question: Why not hold that list of demoTypes as a static class variable of JuceDemoTypeBase?
Answer: OOP-ers get upset if you expose data publicly, as something may overwrite it and you wouldn't have any easy way of finding out what.  So OOP convention says "encapsulate data with accessor functions".  So, might as well stick the actual static variable in the getter.

 


So every demoType "registers itself" into this static array. Note that this happens before ...

// *** main.cpp ***
:
//==============================================================================
// This macro generates the main() routine that starts the app.
START_JUCE_APPLICATION(JuceDemoApplication)

runs, as static variables will get initialised before main(){...} executes.

So JuceDemoTypeBase::getDemoTypeList() returns the list of all demo-types.  If you search the workspace for that string, you will get 4 matches in MainWindow.cpp.  The most interesting is:

    void selectedRowsChanged (int lastRowSelected) override
    {
        if (JuceDemoTypeBase* selectedDemoType = JuceDemoTypeBase::getDemoTypeList() [lastRowSelected])
        {
            currentDemo = nullptr;
            addAndMakeVisible (currentDemo = selectedDemoType->createComponent());
            currentDemo->setName (selectedDemoType->name);
            resized();
        }
    }

So whenever a new list item is selected, it creates&shows an instance of that demoType.

This is a great architecture: if you want to add a new demo, you can just write another .cpp file -- you don't need to modify anything anywhere else.

So the next task will be to figure out how the window splits pane and draws this demo-list.

(NOTE: attached PNG showing relevant code)


#3

Not really sure whether I'm helping anyone here, as this is all excellent code that speaks for itself.

But it helps me to write it out, so hopefully it isn't annoying.

I'm going to keep tweaking these posts butuntil I'm happy with them.

π


If you look in main.cpp, in the constructor for JuceDemoApplication ...

class JuceDemoApplication  : public JUCEApplication
{
public:
    JuceDemoApplication() {}

    //==============================================================================
    void initialise (const String& commandLine) override
    {
        :
        // Do your application's initialisation code here..
        mainWindow = new MainAppWindow();
    }

so, ... onto MainAppWindow ...
 

MainAppWindow::MainAppWindow()
    : DocumentWindow (JUCEApplication::getInstance()->getApplicationName(),
                      Colours::lightgrey,
                      DocumentWindow::allButtons)
{
    :
    contentComponent = new ContentComponent();
    :
}

... and now ContentComponent ...

class ContentComponent   : public Component,
                           public ListBoxModel,
                           public ApplicationCommandTarget
{
public:
    ContentComponent()
    {
        :
        addAndMakeVisible (demoList);
        :
        demoList.selectRow (0);
    }

    void resized() override
    {
        Rectangle<int> r (getLocalBounds());

        if (r.getWidth() > 600)
        {
            demoList.setBounds (r.removeFromLeft (210));
            demoList.setRowHeight (20);
        }
        else
        {
            demoList.setBounds (r.removeFromLeft (130));
            demoList.setRowHeight (30);
        }

        if (currentDemo != nullptr)
            currentDemo->setBounds (r);
    }

So, we get a main-window containing a content component.

This content component draws a list of demos on the left and the active demo on the right, and contains the logic for navigating the demo list  -- you can hit a number 0 thru 9, use up&down arrows (both of these methods are classed as keyboard commands), or just click on the demo you want.

You can see that every time it gets resized, it splits its rectangle into DemoList (left) and space for the actual demo (right). If you click through  removeFromLeft you can see that it cuts a strip from the left of the rectangle, reducing the original rectangle in-place and returning the strip.

The paintListBoxItem override speaks for itself. ContentComponent inherits from ListBoxModel, which triggers this function, and it's easy to see how a particular item gets drawn.


Now I'm looking into commands, seeing as that seems to be a large proportion of the code in MainWindow.cpp.

class MainAppWindow   : public DocumentWindow,
                        private AsyncUpdater
{
public:
    :
    // (return the command manager object used to dispatch command events)
    static ApplicationCommandManager& getApplicationCommandManager();

    enum CommandIDs
    {
        showPreviousDemo            = 0x2100,
        showNextDemo                = 0x2101,

        welcome                     = 0x2000
        :
    };

That method is encapsulating a ...

static ScopedPointer<ApplicationCommandManager> applicationCommandManager;

and clicking through ApplicationCommandManager takes you to a complete documentation of commands.  The idea is that you define your commands at the application level, so your app only has one command manager object.  Often there are several different ways to activate a particular command: click, keyboard shortcut, menu, pop-up menu, icon, etc, so when you're designing a component you can choose which command(s) it responds to.

That's why these IDs are getting defined in the main window. Nothing to do with the main window per se, just that an app has only one main window, and we want just one command manager, so it makes some sense to stick them here.

Find all occurrences of "showPreviousDemo" in the workspace (double-click to select, right click -> "find selected text in workspace"), and you can see how it all fits together. You can see that this command gets assigned a hotkey, and when it is detected it calls moveDemoPages (-1); which cycles to the previous demo.


One thing I don't quite get is this:

class ContentComponent   : public Component,
                           public ListBoxModel,
                           public ApplicationCommandTarget
{
private:
    //==============================================================================
    // The following methods implement the ApplicationCommandTarget interface, allowing
    // this window to publish a set of actions it can perform, and which can be mapped
    // onto menus, keypresses, etc.
    :
    void getAllCommands (Array<CommandID>& commands) override
    {
        // this returns the set of all commands that this target can perform..
        const CommandID ids[] = { MainAppWindow::showPreviousDemo,
                                  MainAppWindow::showNextDemo,
                                  MainAppWindow::welcome,

Although the comment says that we let the window publish a set of commands it will listen out for, it seems to be our contentComponent that is feeding the command manager with every command that will exist in this app.

i.e. it is actually the ContentComponent not the MainAppWindow that inherits from ApplicationCommandTarget, and hence gets to choose which commands to listen out for and what to do when one of them triggers.

So I'm not really sure why the CommandIDs exist within MainAppWindow, not ContentComponent.

NOTE: this one is still in progress, I've nearly figured it out now... Going to rewrite later today