An Introduction to Menus in Quorum

In this tutorial, we will learn how to use Menus in the Quorum Game Engine. Menus are GUI tools very similar to Trees in that the items have a similar hierarchical structure, but the MenuItems are arranged on a MenuBar differently than TreeItems on a Tree. You will typically see MenuBars run horizontally while trees run vertically. Another key difference is that only one submenu can be open at a time, whereas multiple subtrees can be expanded at a time. Typically, MenuBars are located at the top of an application and common headers found on those MenuBars are File, Edit, View, and so on.

This image shows an example of a Menu in NetBeans. The File menu is chosen, with Import Project chosen under it and expanded. From Zip is the final choice highlighted.

For this tutorial, we will create a simple Menu with several operations for editing a Drawable. Specifically, our Menu will allow us to scale, rotate, color, and reset a triangle. To start, create a new Game Application project.

Creating a Menu

Setup

For our main class, we will need the libraries for MenuBar, MenuItem, FlowLayout, Array, Dawable, and Color, adding the following use statements:

use Libraries.Game.Game
use Libraries.Interface.Controls.MenuBar
use Libraries.Interface.Controls.MenuItem
use Libraries.Interface.Layouts.FlowLayout
use Libraries.Containers.Array
use Libraries.Game.Graphics.Drawable
use Libraries.Game.Graphics.Color

We need the FlowLayout library because the container of the menu needs to have a FlowLayout set for the menu to render properly. For this tutorial, we will not be making a container for the menu, since giving the game window a FlowLayout will work for our example. We will write the remaining code for the main class in the CreateGame action. To start, we will set a FlowLayout, declare our MenuBar and Array that will hold the highest-level headers, and create our triangle Drawable with the following lines of code:

FlowLayout flow
SetLayout(flow)

Array<MenuItem> headers
MenuBar menu

Drawable triangle
triangle:LoadFilledTriangle(0, 0, 150, 80, 200, 25)
triangle:SetPosition(100, 100)
Add(triangle)

In this example, setting the game to have a FlowLayout will work properly, but if you need the game to have a ManualLayout in your own applications, look at the Layout tutorial where we go over how to make a container hold items in a FlowLayout.

Creating Headers with Arrays

To start, we need to create our MenuItems for our first header. But there are two ways of adding the headers to the MenuBar and submenus: we can either call the Add action for each one individually, or we can add them to an Array and then add the Array of MenuItems to the MenuBar. Both methods will achieve the same results, the only thing to be careful of is that the Array must already be populated before you try to add it to a MenuItem or MenuBar. For this tutorial, only the first header's submenus will and the highest-level headers will be in an Array.

We will declare the MenuItems for header1 and create three Arrays of MenuItems, which will be used to create our submenus. Our declarations are as follows:

MenuItem header1        //size
MenuItem header1_1          //increase size
MenuItem header1_1_1            //horizontal
MenuItem header1_1_2            //vertical
MenuItem header1_1_3            //both
MenuItem header1_2          //decrease size
MenuItem header1_2_1            //horizontal
MenuItem header1_2_2            //vertical
MenuItem header1_2_3            //both

Array <MenuItem> head1
Array <MenuItem> head1_1
Array <MenuItem> head1_2

Note that the comments indicate the organization of our Menu, where submenus have an extra indentation level than their parent menu. For example, this means that header1_1_3 is one of the three submenus under header1_1, and header1_1 itself is a submenu under header1.

Before we can Add our MenuItems, we need to set up our Arrays. The Arrays will contain the headers that will form a submenu in our MenuBar. The following code block shows which header goes to which array. Also note the order you add MenuItems because that will affect the order that they will be seen in the MenuBar.

head1:Add(header1_1)
head1:Add(header1_2)
head1_1:Add(header1_1_1)
head1_1:Add(header1_1_2)
head1_1:Add(header1_1_3)
head1_2:Add(header1_2_1)
head1_2:Add(header1_2_2)
head1_2:Add(header1_2_3)

With our Arrays created, we can Add them to the higher level MenuItems. The MenuItems header1, header1_1, and header1_2 will have Array of MenuItems Added to them. And header1 will be added to the headers Array since it is a top level MenuItem.

header1:Add(head1)
header1_1:Add(head1_1)
header1_2:Add(head1_2)
headers:Add(header1)

Now that the MenuItems are added to the MenuBar we can use the SetName action to give a name to each Menu Item which will be the Text that is displayed when viewing the MenuBar. This is done with the following lines:

header1:SetName("Size")
header1_1:SetName("Increase Size")
header1_1_1:SetName("Horizontal Scale")
header1_1_2:SetName("Vertical Scale")
header1_1_3:SetName("Full Scale")
header1_2:SetName("Decrease Size")
header1_2_1:SetName("Horizontal Shrink")
header1_2_2:SetName("Vertical Shrink")
header1_2_3:SetName("Full Shrink")

Creating Headers with Add

In addition to the Arrays method from the last section, we can also use the Add action to create submenus. This allows us to avoid having to set up an Array for different groups of MenuItems. We will be creating our second header and its submenus using this method. Below are our declarations of the MenuItems:

MenuItem header2        //rotation
MenuItem header2_1          //clockwise rotation
MenuItem header2_1_1            //30 degrees
MenuItem header2_1_2            //45 degrees
MenuItem header2_2          //counter-clockwise rotation
MenuItem header2_2_1            //30 degrees
MenuItem header2_2_2            //45 degrees

Next, to add the MenuItems to one another, we will use the Add action as follows:

headers:Add(header2)
header2:Add(header2_1)
header2:Add(header2_2)
header2_1:Add(header2_1_1)
header2_1:Add(header2_1_2)
header2_2:Add(header2_2_1)
header2_2:Add(header2_2_2)

Now we set the names for each header with the following lines of code:

header2:SetName("Rotation")
header2_1:SetName("Clockwise")
header2_1_1:SetName("30 Degrees")
header2_1_2:SetName("45 Degrees")
header2_2:SetName("Counter-Clockwise")
header2_2_1:SetName("30 Degrees")
header2_2_2:SetName("45 Degrees")

Next, let's add the last two headers, one for changing the Color and one to reset the triangle.

We will create MenuItems for the third header, name them, and add them to the menu bar in the same manner as before, giving the following code:

MenuItem header3        //color
MenuItem header3_1          //red
MenuItem header3_2          //green
MenuItem header3_3          //blue

header3:SetName("Color")
header3_1:SetName("Red")
header3_1:SetName("Green")
header3_2:SetName("Blue")

header3:Add(header3_1)
header3:Add(header3_2)
header3:Add(header3_3)
headers:Add(header3)

Now we'll add the final header. We will create MenuItems, name them, then add the header to the MenuBar and, giving us the following lines of code:

MenuItem header4        //reset
MenuItem header4_1          //reset all

header4:SetName("Reset")
header4_1:SetName("Reset All")

header4:Add(header4_1)
headers:Add(header4)

Now we have all of the submenus created and our top-level headers in an Array so all we need to now is add the headers array to the MenuBar and add the MenuBar to the game which can be done with these lines of code:

menu:Add(headers)
Add(menu)

Accessibility

We will now make our Menu accessible and when working with menus it is important to understand how a Menu interacts with Focus. What we will do make a hidden Button that will start with Focus and then you can Tab or Shift + Tab to Focus on the Menu. We will do it this way because when a Menu is closed or a MenuItem is activated, the Menu will return the Focus to whatever the last Item in Focus was. In our case it will be our Button. To do this for our example we will add the following lines of code:

Button focuser
focuser:SetName(" ")
focuser:Hide()
focuser:SetNextFocus(menu)
focuser:SetPreviousFocus(menu)
SetFocus(focuser)
Add(focuser)

Now when we run the program, the menu bar will be at the top of the screen and only the highest-level headers will be visible. You can hover over the headers with your mouse or with the menu in Focus you can navigate the menu with the keyboard. Hovering over or navigating to a MenuItem will its submenu if it has one. However, you might notice that trying to activate a MenuItem will only close the submenus and do nothing else. This is because we have not set any behaviors for the MenuItems to use which we will do in the next section.

Setting Behaviors

Now we simply need to add Behaviors to our lowest-level MenuItems. Although any MenuItem can have a set behavior, typically, if a MenuItem has any submenus under it, it will not have a Behavior, as its purpose is only to open and show the related submenus. For this tutorial we only set behaviors for the lowest-level MenuItems.

We will be using 4 behaviors for our MenuItems and they will be defined in their own files. The next 4 code blocks show the behaviors we will be using along with the name and brief description for each Quorum class.

SizeBehavior will take a Drawable and scale it by a set factor on the X and Y axis. SizeBehavior.quorum:

use Libraries.Interface.Behaviors.Behavior
use Libraries.Interface.Events.BehaviorEvent
use Libraries.Game.Graphics.Drawable
use Libraries.Sound.Speech

class SizeBehavior is Behavior

    Drawable triangle = undefined
    number scaleX = 1.0
    number scaleY = 1.0

    action Run(BehaviorEvent behavior)
        if triangle not= undefined
            triangle:SetScale(scaleX, scaleY)
            Speech speech
            speech:Say("Scaled by a factor of " + scaleX + " horizontally, and a factor of " + scaleY + "vertically.")
        end
    end

    action SetDrawable(Drawable newTriangle)
        triangle = newTriangle
    end

    action SetScale(number newScaleX, number newScaleY)
        scaleX = newScaleX
        scaleY = newScaleY
    end
end

RotationBehavior will take a Drawable and rotate it by a specified number of degrees. RotationBehavior.quorum:

use Libraries.Interface.Behaviors.Behavior
use Libraries.Interface.Events.BehaviorEvent
use Libraries.Game.Graphics.Drawable
use Libraries.Sound.Speech

class RotationBehavior is Behavior

    Drawable triangle = undefined
    number degrees = 0.0

    action Run(BehaviorEvent behavior)
        if triangle not= undefined
            triangle:Rotate(degrees)
            Speech speech
            speech:Say("Rotated " + degrees + " degrees.")
        end
    end

    action SetDrawable(Drawable newTriangle)
        triangle = newTriangle
    end

    action SetDegrees(number newDegrees)
        degrees = newDegrees
    end
end

ColorBehavior takes a Drawable and changes its color. ColorBehavior.quorum:

use Libraries.Interface.Behaviors.Behavior
use Libraries.Interface.Events.BehaviorEvent
use Libraries.Game.Graphics.Drawable
use Libraries.Game.Graphics.Color
use Libraries.Sound.Speech

class ColorBehavior is Behavior

    Drawable triangle = undefined
    Color color = undefined
    text colorName = undefined

    action Run(BehaviorEvent behavior)
        if triangle not= undefined
            triangle:SetColor(color)
            Speech speech
            speech:Say("Color changed to " + colorName + ".")
        end
    end

    action SetDrawable(Drawable newTriangle)
        triangle = newTriangle
    end

    action SetColor(Color newColor, text newName)
        color = newColor
        colorName = newName
    end
end

ResetBehavior will reset the triangle Drawable to its defaults. ResetBehavior.quorum:

use Libraries.Interface.Behaviors.Behavior
use Libraries.Interface.Events.BehaviorEvent
use Libraries.Game.Graphics.Drawable
use Libraries.Sound.Speech

class ResetBehavior is Behavior

    Drawable triangle = undefined

    action Run(BehaviorEvent behavior)
        if triangle not= undefined
            triangle:SetRotation(0)
            triangle:SetScale(1)
            triangle:LoadFilledTriangle(0, 0, 150, 80, 200, 25)
            Speech speech
            speech:Say("Reset")
        end
    end

    action SetDrawable(Drawable newTriangle)
        triangle = newTriangle
    end
end

Now we go back to our main class and make our Behavior objects and call their Set actions so can edit the triangle properly. With the behaviors made we then simply call SetBehavior for each MenuItem that we will use to alter the triangle. This is done whit the following lines of code:

//header1 behaviors
SizeBehavior horizontalScale
SizeBehavior verticalScale
SizeBehavior fullScale
SizeBehavior horizontalShrink
SizeBehavior verticalShrink
SizeBehavior fullShrink

horizontalScale:SetDrawable(triangle)
verticalScale:SetDrawable(triangle)
fullScale:SetDrawable(triangle)
horizontalShrink:SetDrawable(triangle)
verticalShrink:SetDrawable(triangle)
fullShrink:SetDrawable(triangle)

horizontalScale:SetScale(1.25, 1.00)
verticalScale:SetScale(1.00, 1.25)
fullScale:SetScale(1.25, 1.25)
horizontalShrink:SetScale(0.75, 1.00)
verticalShrink:SetScale(1.00, 0.75)
fullShrink:SetScale(0.75, 0.75)

header1_1_1:SetBehavior(horizontalScale)
header1_1_2:SetBehavior(verticalScale)
header1_1_3:SetBehavior(fullScale)
header1_2_1:SetBehavior(horizontalShrink)
header1_2_2:SetBehavior(verticalShrink)
header1_2_3:SetBehavior(fullShrink)

//header2 behaviors
RotationBehavior clockwise30
RotationBehavior clockwise45
RotationBehavior counter30
RotationBehavior counter45

clockwise30:SetDrawable(triangle)
clockwise45:SetDrawable(triangle)
counter30:SetDrawable(triangle)
counter45:SetDrawable(triangle)

clockwise30:SetDegrees(30)
clockwise45:SetDegrees(45)
counter30:SetDegrees(-30)
counter45:SetDegrees(-45)

header2_1_1:SetBehavior(clockwise30)
header2_1_2:SetBehavior(clockwise45)
header2_2_1:SetBehavior(counter30)
header2_2_2:SetBehavior(counter45)

//header3 behaviors
Color color
ColorBehavior redBehavior
ColorBehavior greenBehavior
ColorBehavior blueBehavior

redBehavior:SetDrawable(triangle)
greenBehavior:SetDrawable(triangle)
blueBehavior:SetDrawable(triangle)

redBehavior:SetColor(color:Red(), "Red")
greenBehavior:SetColor(color:Green(), "Green")
blueBehavior:SetColor(color:Blue(), "Blue")

header3_1:SetBehavior(redBehavior)
header3_2:SetBehavior(greenBehavior)
header3_3:SetBehavior(blueBehavior)

//header4 behaviors
ResetBehavior resetBehavior
resetBehavior:SetDrawable(triangle)
header4_1:SetBehavior(resetBehavior)

Now when we run our program, you can still navigate the menu, but when you activate a one of the lowest-level MenuItems the triangle Drawable will change depending on the behavior you activated.

This image shows the final expected Game window with the Decrease Size submenu open

MenuSelection and MenuPath

In Quorum, Menus use their own selection class, MenuSelection, to keep track of information regarding the currently selected MenuItem. In particular, the MenuSelection keeps track of the unique and specific path to the current selection, causing another similarity between Trees and Menus. This path is stored in the form of an Array of MenuItems and Menus allow for us to easily get this Array with the GetMenuPath action. To demonstrate this, we will add a small amount of code to each of our Behavior classes we created in the tutorial thus far.

To start, we will need the libraries for MenuItem and Array, adding the following use statements:

use Libraries.Interface.Controls.MenuItem
use Libraries.Containers.Array

Next, we will add to the end of the Run action, outside of the conditional if statement. First, we need to obtain the currently selected MenuItem. This can be done with BehaviorEvent's GetItem action, but this returns an Item, not a MenuItem, so we will need to cast it. This is done with the following line:

MenuItem tempItem = cast(MenuItem, behavior:GetItem())

Now that we have the MenuItem that called the behavior we can use its GetMenuPath action to get the Array of MenuItems. This gives the following line:

Array<MenuItem> items = tempItem:GetMenuPath()

Now that we have the path to the currently selected MenuItem, we can traverse the Array from the beginning to the end to obtain the full path. We will simply output this as text, so we will use each MenuItem's GetName action as we go through the Array in a repeat while loop. This is gives the following code:

integer counter = 0
integer size = items:GetSize()
text path = ""
repeat while counter < size
    path = path + items:Get(counter):GetName() + "\"
    counter = counter + 1
end

output path

As an example, the updated SizeBehavior class is as follows:

use Libraries.Interface.Behaviors.Behavior
use Libraries.Interface.Events.BehaviorEvent
use Libraries.Interface.Controls.MenuItem
use Libraries.Containers.Arrayuse Libraries.Game.Graphics.Drawable
use Libraries.Sound.Speech

class SizeBehavior is Behavior

    Drawable triangle = undefined
    number scaleX = 1.0
    number scaleY = 1.0

    action Run(BehaviorEvent behavior)
        if triangle not= undefined
            triangle:SetScale(scaleX, scaleY)
            Speech speech
            speech:Say("Scaled by a factor of " + scaleX + " horizontally, and a factor of " + scaleY + "vertically.")
        end

        MenuItem tempItem = cast(MenuItem, behavior:GetItem())
        Array<MenuItem> items = tempItem:GetMenuPath()

        integer counter = 0
        integer size = items:GetSize()
        text path = ""
        repeat while counter < size
            path = path + items:Get(counter):GetName() + "\"
            counter = counter + 1
        end
        output path
    end

    action SetDrawable(Drawable newTriangle)
        triangle = newTriangle
    end

    action SetScale(number newScaleX, number newScaleY)
        scaleX = newScaleX
        scaleY = newScaleY
    end
end

Now, when the SizeBehavior is run, it will output the path to the currently selected MenuItem. This change is done the same way for all five Behavior classes we made, so the use statements and addition to the Run action can simply be copied into the other classes.

Next Tutorial

In the next tutorial, we will discuss ScrollPanes, which describes how to use ScrollPanes.