Building User Interface Dialogs

In this section we'll discuss user interface construction and the s3d.ui.Dialog class in particular.

There are two supported methods for interacting with the user in Lua via dialogs. The simplest approach is to call the s3d.ui.Alert function with a message string, at which time the user is presented with your message and an "Okay" button. This works well for those times when all you want to do is let the user know something.

The most flexible approach is to use the s3d.ui.Dialog class to build a dialog window and present it to the user. You can even build a more flexible alert facility using this class, and that's the purpose of our first example.

A Better Alert

The following Lua code will create a function that poses an alert including an arbitrary number of buttons whose labels are given as arguments. The first two arguments to this function after the window title and message arguments will result in re-labeling the standard okay and cancel buttons, while subsequent arguments will create additional buttons.

The distinction between the okay and cancel buttons is important because default keystrokes are associated with these buttons (return/enter and escape/cmd-'.') regardless of what their label might be.

The relative index of the hit button numbered from 1 in the Lua style is returned from this function, with the okay button having index #1 and the cancel button having index #2.

Get the source code.

function BetterAlert (title, message, ...)

-- Allocate a dialog window with the requested title or a default title.

local d = s3d.ui.Dialog( title or "Alert" )

local buttons = {}

-- Add the message string to the dialog.

d:AddStaticText( message or "Something really important happened." )

-- Hide the default cancel button. We'll show it again if a label is provided.

d:SetShown( s3d.ui.Dialog.kCancel, false )

if arg ~= nil then

-- If we have button name arguments then process them now.

for i, button in ipairs(arg) do

local id

if i == 1 then

id = s3d.ui.Dialog.kOkay

d:SetLabel( id, button )

elseif i == 2 then

id = s3d.ui.Dialog.kCancel

d:SetShown( id, true )

d:SetLabel( id, button )

else

-- Add a new button with a minimum width of 64 pixels.

id = d:AddButton( button, 64 )

end

buttons[ id ] = i

end

end

-- Interact with the user.

local actualHit = 0

local function HandleButtons (hit)

-- The dialog will only return on the okay and cancel buttons.

-- We'll fake up an okay button hit for all the other buttons

-- but remember the actual hit.

actualHit = hit

return (buttons[ hit ] > 2 and s3d.ui.Dialog.kOkay) or hit

end

-- We'll ignore the reported hit because we saved what we wanted in "actualHit"

local reportedHit = d:Present( HandleButtons )

return buttons[ actualHit ]

end

-- First pose the standard alert for comparison purposes.

s3d.ui.Alert("You need a squirrel.")

-- Test the function.

didHit = BetterAlert("Alert", "This functionality requires two squirrels and a hamster.",

"Okay", "Skip It", "Make It Three Squirrels")

print("Hit button index #", didHit)

The function supplied to the Present method is called after any control has been clicked on with the identifier for that control. We use this as a bottleneck for handling and recording the extra buttons we add to this dialog.

If you enter the above code into the scripting console of Strata Design 3D CX and then select all of the text and hit enter or click the evaluate button you'll see the standard alert:

followed by the new alert with three custom labeled buttons:

After dismissing the custom alert the console will report which button was hit. Of course we could have used an alert to do that as well.

It's important to note that all code executed in the console runs in a protected custom environment, so nothing declared as a global item will actually be put in the global table. This is done to avoid accidental damage to the entire Lua runtime which depends on the global table remaining consistent.

If you really want to modify the global table it's easy enough to do. Just prefix any name in the console with _G. while assigning to it or while declaring a function. This will bypass the local environment and directly access the global table.

Columns and Clusters

Dialogs are built up from columns of controls, and those columns are gathered into clusters. The width of a column and the height of a cluster are always large enough to fit the controls inside of them, dynamically resizing larger if necessary. By nesting these columns and clusters you can build up fairly complex dialog layouts.

Buttons are never added to a column. They are automatically arranged along the bottom of the dialog based on the order in which they are added. The default okay and cancel buttons get special treatment and are arranged in a platform dependent order: cancel and then okay on the Mac, okay and then cancel on Windows.

Radio buttons are mutually exclusive within a column. Hitting one radio button will deselect the others in the same column. If you want radio buttons arranged horizontally in multiple columns then you need to do some extra work to keep them mutually exclusive from each other with only one selected at a time.

The root dialog window is a cluster, and as such you can add an arbitrary number of columns to it by using the Dialog:AddColumn method. The next example will illustrate how to add columns.

Get the source code.

-- Allocate a dialog window with the requested title or a default title.

local d = s3d.ui.Dialog( "Radio Buttons" )

local selected

local feedback

local radio1

local radio2

local radio3

local radios = {}

d:BeginCluster( "Select a radio button" )

d:AddStaticText( "Choose one and only one..." )

radio1 = d:AddRadio( "First Choice" )

d:AddColumn()

radio2 = d:AddRadio( "Second Choice" )

d:AddColumn()

radio3 = d:AddRadio( "Third Choice" )

d:EndCluster()

feedback = d:AddStaticText( "" )

-- Start with the first radio button selected.

selected = radio1

d:SetSelected( selected, true )

radios[ radio1 ] = { "You chose the first choice.", { radio2, radio3 } }

radios[ radio2 ] = { "You chose the second choice.", { radio1, radio3 } }

radios[ radio3 ] = { "You chose the third choice.", { radio1, radio2 } }

local function HandleRadioButtons (hit)

local handled = radios[hit]

if handled ~= nil then

local message, disable = unpack( handled )

if message ~= nil then

d:SetText( feedback, message )

end

if disable ~= nil then

for _, id in ipairs( disable ) do

d:SetSelected( id, false )

end

end

selected = hit

end

return hit

end

-- Interact with the user.

if d:Present( HandleRadioButtons ) == s3d.ui.Dialog.kOkay then

print( "Selected radio button with id", selected )

end

As for the example in the previous section, if you enter the above code into the scripting console of Strata Design 3D CX and then select all of the text and hit enter or click the evaluate button you'll see the dialog on your screen.

We're using a callback function on the dialog Present method again, this time to handle the radio buttons that aren't in the same column but which we want to behave as a group. Clicking the okay button gives a message in the console with the label of the chosen radio button.