An Introduction to TabPanes in Quorum

What is a Tab Pane?

In this tutorial, we will learn how to use TabPanes in the Quorum Game Engine. Tab panes are interface tools that allows for different content to share the same space or panel. The user can select which content to view by changing the selected tab. For example, most Internet browsers allow you to open multiple webpages as Tabs in a single window. The TabPane would then be the window where the tabs exist. All the tabs share the pane, but only the selected tab gets drawn while the other tabs are inactive.

What is a Tab?

Tabs are the different panels that exist in the Tab Pane. In our Internet browser example, the Tabs would then be the different webpages. Each Tab in the browser has content that is related to it that is displayed depending on whether it is selected or not. In Quorum, each Tab has a "Related Item" that we can set, and that item is displayed when that Tab is being selected. One important thing to note is that while only one Tab is active at a time, the data stored on inactive Tabs is not lost, and can be displayed simply by switching the active Tab.

For this tutorial, we will create a TabPane with three different Tabs, where their Related Items are different shapes, and we will allow the user to add or remove Tabs using Buttons. To start, create a new Game Application project.

Creating a TabPane

Making our Pane

The main class will need the libraries for TabPane, Tab, Icon, Drawable, Color, and Array, requiring the following use statements:

use Libraries.Game.Game
use Libraries.Interface.Controls.TabPane
use Libraries.Interface.Controls.Tab
use Libraries.Interface.Controls.Icon
use Libraries.Game.Graphics.Drawable
use Libraries.Game.Graphics.Color
use Libraries.Containers.Array

Since we want the TabPane to be usable as soon as the program begins, we will be writing code in the CreateGame action. To start, create a TabPane, three Tabs, and a Color. We will be using the Color for our Drawables later. Our declarations are as follows:

TabPane pane
Tab tab1
Tab tab2
Tab tab3
Color color

Then we will make the TabPane, set its position, and add the TabPane to the game. This is done with the following lines of code:

pane:SetSize(450, 450)
pane:SetPosition(50, 100)
Add(pane)

Additionally, we will add an outline for the display area of our Tabs. This is only forappearances, but is easily done by creating a simple Drawable. This adds the following lines:

Drawable outline
outline:LoadRectangle(450, 425, color:Black())
outline:SetPosition(50, 100)
Add(outline)

Making the Tabs

If we were to run the program now, all that would be on the screen is our outline showing where the pane's display area is. This is because we have not added anything to the tabs or added them to the pane yet. Each Tab will show a different shape: a rectangle, a circle, or a triangle. The shapes are created as follows:

Drawable box
Drawable circle
Drawable triangle

box:LoadFilledRectangle(300, 300, color:Red())
circle:LoadFilledCircle(150, color:Blue())
triangle:LoadFilledTriangle(0, 0, 300, 0, 150, 300, color:Green())

The next thing we will do before we can add our Tabs are the icons and they will appear next to the Tab's name. Our icons are created as follows:

Icon icon1
Icon icon2
Icon icon3

icon1:LoadFilledRectangle(50, 50)
icon2:LoadFilledCircle(25)
icon3:LoadFilledTriangle(0, 0, 50, 0, 25, 50)

Now that our shapes and icons are created, we take our Tabs we created earlier and set their Name, Related Item, and Icon. The name will be the text that is displayed on the tab at the top of our pane along with its icon. The Related Item will be the drawable that is displayed when that tab is selected. This is done as follows:

tab1:SetName("Square")
tab1:SetRelatedItem(box)
tab1:SetIcon(icon1)

tab2:SetName("Circle")
tab2:SetRelatedItem(circle)
tab2:SetIcon(icon2)

tab3:SetName("Triangle")
tab3:SetRelatedItem(triangle)
tab3:SetIcon(icon3)

Next, we need to create an Array with our Tabs and add it to our pane. This is done through the following lines of code:

Array<Tab> tabs
tabs:Add(tab1)
tabs:Add(tab2)
tabs:Add(tab3)
pane:Add(tabs)

Note that these tabs do not need to be in an Array to be added to the pane. Using pane:Add(tab1) for each tab will also work, but we do it this way to show that this method also works.

For accessibility we will be setting the Focus to the TabPane with the following line of code:

SetFocus(pane)

When a Tab Pane receives Focus the user can navigate through the Tabs using Ctrl + Tab and Ctrl + Shift + Tab on Windows or Ctrl + [ and Ctrl + ] on Mac.

Now, when we run the program, we can switch Tabs to see our different shapes, and we can close out of them. But, we can still add more to the pane by adding Behaviors for when you change your current selection or close a Tab.

This image shows the Tab Pane with the Circle Tab open.

Note that we did not set the position of the shapes but they are drawn inside of our outline. This is because the shapes, now Related Items to the Tabs, are drawn relative to the TabPane.

TabPaneSelection

Like Trees and TextBoxes, TabPanes have their own specific type of Selection, which keeps track of the index of the current Tab. For this tutorial, we will create a SelectionListener to output and say aloud a newly selected Tab.

First, let's create our SelectionListener. To start, create a new Quorum class, named "TabSelectionListener.quorum." This class will need the libraries for SelectionListener, SelectionEvent, and Speech.

The TabSelectionListener class itself will inherit SelectionListener and override the SelectionChanged action. To read the current Tab's name aloud, we simply need to obtain the name with the SelectionEvent's GetDisplayName action, and then use the Speech class's Say action. This gives us the following complete TabSelectionListener class:

use Libraries.Interface.Events.SelectionListener
use Libraries.Interface.Events.SelectionEvent
use Libraries.Sound.Speech

class TabSelectionListener is SelectionListener
    action SelectionChanged(SelectionEvent selection)
        text currentTab = selection:GetDisplayName()
        Speech speech
        speech:Say(currentTab)
        output currentTab
    end
end

Returning to the main, we need to create a TabSelectionListener variable and add it as a SelectionListener to the game. This is done by adding the following two lines to the CreateGame action:

TabSelectionListener selectionListener
AddSelectionListener(selectionListener)

TabChangeListener

Every time we open or close tabs, a TabChangeEvent occurs. If we want an action to happen when this event occurs, then we need a listener. To do this, create a new Quorum class, "TabPaneListener.quorum," to listen for when a Tab is added or removed from the TabPane. When this listener detects an addition or removal, the program will say and output that a Tab was opened or closed.

This class will use the libraries for TabChangeListener, TabChangeEvent, and Speech. The class itself will inherit the TabChangeListener class, and will override the OpenedTab and ClosedTab actions. In each of those actions, we will create a Speech object, and call its Say action with "Tab opened" or "Tab closed." The resulting class is as follows:

use Libraries.Interface.Events.TabChangeListener
use Libraries.Interface.Events.TabChangeEvent
use Libraries.Sound.Speech

class TabPaneListener is TabChangeListener

    action OpenedTab(TabChangeEvent event)
        Speech speech
        speech:Say("Tab opened", false)
        output "Tab opened"
    end

    action ClosedTab(TabChangeEvent event)
        Speech speech
        speech:Say("Tab closed", false)
        output "Tab closed"
    end
end

Returning to the main, we need to create a TabPaneListener variable and add it to our pane. This is done by adding the following two lines to the CreateGame action:

TabPaneListener listener
pane:AddTabChangeListener(listener)

If both listeners are added to your game, then the program will output and say the Tab's name when selecting Tabs, and it will output and say when a Tab is opened or closed.

For additional information on how these listeners and events work, check out the tutorial on Events.

Adding Tabs

Adding Generic Tab

As the program is now, we can close Tabs, but they will stay closed and we can't open more Tabs. Now, let's give our program the ability to add new Tabs, which we will do with Buttons. To start, we will need to create a Behavior. We will name this new class "AddTabBehavior," and it will use the libraries for Behavior, BehaviorEvent, Tab, TabPane, Icon, and Drawable.

The AddTabBehavior class will inherit the Behavior class, and override the Run action. For simplicity, we will design this Behavior to make the same shape for the new Tab every time. To add our new Tab, we will also need an action to get the TabPane from our main class. This gives us the following template for our class:

use Libraries.Interface.Behaviors.Behavior
use Libraries.Interface.Events.BehaviorEvent
use Libraries.Interface.Controls.Tab
use Libraries.Interface.Controls.TabPane
use Libraries.Interface.Controls.Icon
use Libraries.Game.Graphics.Drawable

class AddTabBehavior is Behavior

    TabPane pane = undefined

    action Run(BehaviorEvent event)
    end

    action SetTabPane(TabPane newPane)
        pane = newPane
    end
end

For the Run action, we need to create a new Tab, making a simple blue rectangle for the icon and Related Item, and add it to the TabPane. We will also protect against program crashes by checking if the TabPane is still undefined. Adding this to our template, the complete AddTabBehavior class is as follows:

use Libraries.Interface.Behaviors.Behavior
use Libraries.Interface.Events.BehaviorEvent
use Libraries.Interface.Controls.Tab
use Libraries.Interface.Controls.TabPane
use Libraries.Interface.Controls.Icon
use Libraries.Game.Graphics.Drawable

class AddTabBehavior is Behavior

    TabPane pane = undefined

    action Run(BehaviorEvent event)
        if pane not= undefined
            Tab newTab
            Drawable newSquare
            Icon newIcon
            Color color

            newSquare:LoadFilledRectangle(300, 300, color:Blue())
            newIcon:LoadFilledRectangle(50, 50, color:Blue())
            newTab:SetName("New Square")
            newTab:SetRelatedItem(newSquare)
            newTab:SetIcon(newIcon)
            pane:Add(newTab)
            pane:Resize()
        end
    end

    action SetTabPane(TabPane newPane)
        pane = newPane
    end
end

Returning the main class, we can now make a AddTabBehavior object, set the TabPane, and create a Button to add new Tabs, which adds the following lines to the CreateGame action:

AddTabBehavior tabBehavior
tabBehavior:SetTabPane(pane)

Button addTab
addTab:SetName("Add New Tab")
addTab:SetPosition(600, 400)
addTab:SetBehavior(tabBehavior)
Add(addTab)

Adding Tabs Based on Selection

Now, we will create another new Quorum class for the new Button's Behavior. We will call this class "AddCurrentBehavior.quorum." The template for this class is almost identical to the AddTabBehavior class, but it will also include the TabPaneSelection library.

The Run action will first check if the pane variable was defined by the SetTabPane action. If so, it will create a TabPaneSelection variable and set it using the TabPane's GetSelection action. Once this selection has been obtained, we can create a Tab object and set it to the current Tab using the TabPaneSelection's GetTab action.

Still within this conditional if-statement, we now need to check if the Tab variable "current" is undefined before calling any actions from it. If it is defined, we can obtain the Tab's name and the contents of the Tab using the Tab class's GetName and GetRelatedItem actions, respectively. Note that the GetRelatedItem action returns an Item2D, not a Drawable, so we will have to cast it before assigning it to a Drawable variable. This gives us the following code so far:

if pane not= undefined
    TabPaneSelection selection = pane:GetSelection()
    Tab current = selection:GetTab()

    if current not= undefined
        text name = current:GetName()
        Drawable shape = cast(Drawable, current:GetRelatedItem())
    end
end

We now have the Tab's name and shape, but are still missing the icon. While there is not an action to get the Tab's icon, we can simply use the obtained name to determine which shape to make. Using conditionals, we can accomplish this with the following lines of code, which are still within the previous conditionals:

Icon icon

if name = "Square"
    icon:LoadFilledRectangle(50, 50)
elseif name = "Circle"
    icon:LoadFilledCircle(25)
elseif name = "Triangle"
    icon:LoadFilledTriangle(0, 0, 50, 0, 25, 50)
end

With our name, shape, and icon variables obtained from the current Tab, we can now create a new Tab and Add it to the pane.

Tab newTab
newTab:SetName(name)
newTab:SetRelatedItem(shape)
newTab:SetIcon(icon)
pane:Add(newTab)

This gives the completed AddCurrentBehavior class as follows:

use Libraries.Interface.Behaviors.Behavior
use Libraries.Interface.Events.BehaviorEvent
use Libraries.Interface.Controls.TabPane
use Libraries.Interface.Selections.TabPaneSelection
use Libraries.Interface.Controls.Tab
use Libraries.Interface.Controls.Icon
use Libraries.Game.Graphics.Drawable

class AddCurrentBehavior is Behavior

    TabPane pane = undefined

    action Run(BehaviorEvent behavior)
        if pane not= undefined
            TabPaneSelection selection = pane:GetSelection()
            Tab current = selection:GetTab()
            Icon icon

            if current not= undefined
                Tab newTab
                text name = current:GetName()
                Drawable shape = cast(Drawable, current:GetRelatedItem())

                if name = "Circle"
                    icon:LoadFilledCircle(25)
                elseif name = "Triangle"
                    icon:LoadFilledTriangle(0, 0, 50, 0, 25, 50)
                else
                    icon:LoadFilledRectangle(50, 50)
                end

                newTab:SetName(name)
                newTab:SetRelatedItem(shape)
                newTab:SetIcon(icon)
                pane:Add(newTab)
            end
        end
    end

    action SetTabPane(TabPane newPane)
        pane = newPane
    end
end

Returning to the main class, we need to make another Button for the new Behavior. This gives us the following code added to the end of the CreateGame action:

AddCurrentBehavior currentBehavior
currentBehavior:SetTabPane(pane)

Button addCurrent
addCurrent:SetName("Add Current")
addCurrent:SetPosition(600, 350)
addCurrent:SetBehavior(currentBehavior)
Add(addCurrent)

Now that we have all of the Buttons added we will add our focus cycle so that our application is accessible.

pane:SetNextFocus(addTab)
pane:SetPreviousFocus(addCurrent)
addTab:SetNextFocus(addCurrent)
addTab:SetPreviousFocus(pane)
addCurrent:SetNextFocus(pane)
addCurrent:SetPreviousFocus(addTab)

Now when we run the program, we can add new Tabs or duplicate the current Tab by clicking the appropriate Button. Then we can select between which Tab to display, reading aloud the Tab title, and can actively open or close more Tabs, the program saying aloud either "Tab opened" or "Tab closed" when we do so.

This image shows the final expected game window with the Square Tab open.

Next Tutorial

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