WoW:Creating GUI configuration options

From AddOn Studio
Jump to navigation Jump to search


Making a GUI Config/Options dialog for your addon is always a bit tricky, however this HOWTO will hopefully help you in such an endeavor. Since a config dialog is usually added to an existing addon, this HOWTO will use an existing addon as an example, but we encourage you to use your own addon while following this HOWTO.

Reference Material (not needed for HOWTO)
myClock by Scheid

This HOWTO is not meant to reproduce myClock in any shape or form, and you'll notice that the code has been changed in a number of places. Nevertheless this HOWTO is based on it, and if you filled in the gaps would give you a nice basic myClock like addon.

The TOC file[edit]

Reference Material (optional reading)
The TOC Format

In the TOC file we need to add your config dialog's .xml file. Also to have a config dialog that remembers it's settings between game sessions, you will need to use the SavedVariables directive.

myClock.toc: Before changes

 ## Interface: 1600
 ## Title: myClock
 ## Notes: Clock replacing day-night indicator
 myClockIndicatorFrame.xml

We need to:

  • Add the config frame .xml
  • Add a SavedVariables directive
  • Add myAddOns optional dependency

This HOWTO uses myAddOns to show the config dialog because it is much simpler then creating a slash command. If you wish to use a slash command, it is recommended you read HOWTO: Create a Slash Command as it is not covered in this HOWTO.

myClock.toc: After changes

 ## Interface: 1600
 ## Title: myClock
 ## Notes: Clock replacing day-night indicator
 ## OptionalDeps: myAddOns
 ## SavedVariables: myClockConfig
 myClockIndicatorFrame.xml
 myClockConfigFrame.xml
OptionalDeps
Denotes optional dependencies. myAddOns is not required, but recommended.
SavedVariables
Allow us to use a variable that is saved between game sessions, on a per-account basis.
myClockIndicatorFrame.xml
Main addon frame
myClockConfigFrame.xml
Config addon frame

Load config from SavedVariables[edit]

Reference Material (optional reading) : Saving variables between game sessions

Using the SavedVariable 'myClockConfig' we declared in the .toc file, we can now load and save our addon's configuration between game sessions. Remember that SavedVariable is per-account, not per-realm and not per-character, so we will need to use tables to store individual settings.

Main frame: .xml[edit]

We will be modifying the main addon frame, in this case 'myClockIndicatorFrame.xml' which is used to display the current time.

We need to:

  • Add a OnLoad Script action
  • Add a OnEvent Script action

The OnLoad is needed to start watching the 'VARIABLES_LOADED' event (when myClockConfig will have its saved values set). Next, we use OnEvent to actually do something when the event fires.

myClockIndicatorFrame.xml

 <Ui ...>
 <Frame name="myClockIndicatorFrame" ...>
 ...
 <Scripts>
   <OnLoad> myClockIndicatorFrame_OnLoad(); </OnLoad>
   <OnEvent> myClockIndicatorFrame_OnEvent(); </OnEvent>
   ...
 </Scripts>
 </Frame>
 </Ui>

Main frame: .lua[edit]

Globals[edit]

We need to:

  • Get the Realm and Character names for later
  • Ensure SavedVariable 'myClockConfig' exists
  • Create addon details for myAddOns
  • Create default config settings

myClockIndicatorFrame.lua

 ...
 -- Globals
   -- so we know when our configuration is loaded
 myClock_variablesLoaded = false;
   -- for configuration saving
 myClockRealm = GetCVar("realmName");
 myClockChar = UnitName("player");
   -- details used by myAddOns
 myClock_details = {
	name = "myClock",
	frame = "myClockIndicatorFrame",
	optionsframe = "myClockConfigFrame"
 };
   -- default config settings
 local myClockConfig_defaultOn = true;		-- addon enabled?
 local myClockConfig_defaultTime24 = true; 	-- 24 hour format?
 local myClockConfig_defaultOffset = 0; 	-- time offset?
 ...
myClock_variablesLoaded, myClock_realm, myClock_char
These are needed to create a table of settings, per-realm, per-character. By default a SavedVariable is only per-account. This way changing configuration options on one character does not change them for all your characters!
myClock_details
myAddOns will display information about your AddOn, and needs the name of your main and config frames to display the configuration dialog properly.
myClockConfig_default
Each of these variables is a default setting for a configuration option the user will be able to change in our config dialog. They are also used the first time a character uses our addon, as they have no saved settings yet.

Registering Events[edit]

Since we will use the VARIABLES_LOADED event to know when it is safe to use our SavedVariable,

We need to:

  • Register the VARIABLES_LOADED event
  • Call a function when the event fires

myClockIndicatorFrame.lua: anywhere inside

 ...
 -- OnLoad
 function myClockIndicatorFrame_OnLoad()
	-- register events
	this:RegisterEvent("VARIABLES_LOADED"); -- eventually will call OnEvent
       ...
 end
 ...

myClockIndicatorFrame.lua: anywhere inside

 ...
 -- OnEvent
 function myClockIndicatorFrame_OnEvent()
	-- VARIABLES_LOADED event
	if ( event == "VARIABLES_LOADED" ) then
		-- execute event code in this function
		myClockIndicatorFrame_VARIABLES_LOADED();
	end
 end
 ...

Custom Update Functions[edit]

We now have a function that will be called when our variables are loaded.

It will need to:

  • load default SavedVariable values
  • register addon with myAddOns
  • record that configuration was loaded

within myClockIndicatorFrame.lua

 ...
 function myClockIndicatorFrame_VARIABLES_LOADED()
	-- initialize our SavedVariable
	if ( not myClockConfig ) then 
	 	myClockConfig = {}; 
	end
	if ( not myClockConfig[myClockRealm] ) then 
	 	myClockConfig[myClockRealm] = {}; 
	end
	if ( not myClockConfig[myClockRealm][myClockChar] ) then 
	 	myClockConfig[myClockRealm][myClockChar] = {}; 
	end
	-- load each option, set default if not there
	if ( not myClockConfig[myClockRealm][myClockChar].on ) then 
		myClockConfig[myClockRealm][myClockChar].on = myClockConfig_defaultOn; 
	end
	if ( not myClockConfig[myClockRealm][myClockChar].time24 ) then 
		myClockConfig[myClockRealm][myClockChar].time24 = myClockConfig_defaultTime24; 
	end
	if ( not myClockConfig[myClockRealm].offset ) then 
		myClockConfig[myClockRealm].offset = myClockConfig_defaultOffset; 
	end
	-- record that we have been loaded
	myClock_variablesLoaded = true;
	-- we know other addons have been "loaded" now
	   -- optional dependance on myAddOns, leads to our config panel
	if( myAddOnsFrame_Register ) then
		myAddOnsFrame_Register( myClock_details );
	end
	-- configuration might have changed
	myClock_ConfigChange();
 end
 ...

So this function is very simple, it created a new table for storing our configuration on a addon-wide, realm-wide and character basis, then checked for every config option and made sure it was loaded, if not then set it to the default. We added myAddons support, telling myAddons what our mod's name, main frame, and config frame was using a table declared in our globals.

The last line is interesting, we make a call to myClock_ConfigChange(). This is the function that will actually change our mod based on our settings. In the case of a clock most of those options ('time24', 'offset') are actually in OnUpdate, but we would handle the 'on' in this function.

Our config change should:

  • Turn the mod on and off

within myClockIndicatorFrame.lua

 ...
 function myClock_ConfigChange()
	-- make sure that our profile has been loaded before allowing this function to be called
	if ( not myClock_variablesLoaded ) then -- config not loaded
		myClockIndicatorFrame:Hide(); -- turn our mod off
		return;
	end
	-- make sure to use the frame's name here, cannot rely on 'this' to mean the main frame
	if ( myClockConfig[myClockRealm][myClockChar].on ) then
		myClockIndicatorFrame:Show(); -- show our mod frame
	else
		myClockIndicatorFrame:Hide(); -- hide our mod frame
	end
 end
 ...

It can also be useful to have a function to set everything back to defaults... here is one you might use in this case

within myClockIndicatorFrame.lua

 ...
 -- reset to defaults
 function grokClock_ConfigToDefault()
	-- make sure that our profile has been loaded before allowing this function to be called
	if ( not myClock_variablesLoaded  ) then -- config not loaded
		myClockIndicatorFrame:Hide(); -- turn our mod off
		return;
	end
	-- set our profile to defaults
	myClockConfig[myClockRealm][myClockChar].on = myClockConfig_defaultOn;
	myClockConfig[myClockRealm][myClockChar].time24 = myClockConfig_defaultTime24;
	myClockConfig[myClockRealm][myClockChar].offset = myClockConfig_defaultOffset;
	-- frame name used since called from various places
	-- set the location of the frame... in case they dragged it away
	myClockIndicatorFrame:SetPoint("TOPLEFT", "MinimapCluster", "TOPLEFT", 122, -28);
	  -- wow automatically saves the location of frames on the screen!
	-- make sure the defaults are loaded onto our mod frame
	myClock_ConfigChange();
 end
 ...

Making your config frame[edit]

Now that your mod is using your SavedVariable, and is both loading it's settings from it (or using the defaults), you need a configuration frame to allow users to modify their settings! There are many popular ways of doing this, you can create slash commands (learn more at HOWTO: Create a Slash Command), a right click menu, but for now we will stay simple, and just have a frame popup when you use myAddons to display the options. From inside the game, you can hit the 'ESCAPE' key and chose the 'Addons' item to have myAddons open. From there you can click on the name for your addon, and then click the 'Options' button on the bottom right. But it won't open your options right now, because you haven't made the frame yet!

config frame .xml[edit]

Basic Frame with Backdrop[edit]

Let's flesh out a basic config frame:

myClockConfigFrame.xml

 <Ui ...>
 <Script file="myClockConfigFrame.lua"/>
 <Frame name="myClockConfigFrame" 
    toplevel="true" parent="UIParent" frameStrata="DIALOG" 
    hidden="true" enableMouse="true">
	<Size><AbsDimension x="260" y="280"/></Size>
	<Anchors><Anchor point="CENTER"/></Anchors>
	<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background" 
		edgeFile="Interface\DialogFrame\UI-DialogBox-Border" tile="true">
		<BackgroundInsets>
			<AbsInset left="11" right="12" top="12" bottom="11"/>
		</BackgroundInsets>
		<TileSize><AbsValue val="32"/></TileSize>
		<EdgeSize><AbsValue val="32"/></EdgeSize>
	</Backdrop>
 </Frame>
 </Ui>

We now have a config frame, and if you go in-game you can see it in all it's splendor. Feel free to make it any size, and the ANCHOR point="CENTER" will make sure it is dead center on the screen no matter the resolution.

You'll probably want some kind of title on it to say what mod's options it is, so here's some Layers info to do that:

within myClockConfigFrame.xml

 ...
 <Frame ...>
	...
	<Backdrop ...>
	 	...
	</Backdrop>
	<Layers>
		<Layer level="ARTWORK">
			<Texture file="Interface\DialogFrame\UI-DialogBox-Header">
				<Size><AbsDimension x="256" y="64"/></Size>
				<Anchors>
					<Anchor point="TOP">
					<Offset><AbsDimension x="0" y="12"/></Offset>
					</Anchor>
				</Anchors>
			</Texture>
		</Layer>
		<Layer level="OVERLAY">
			<FontString inherits="GameFontNormal" text="Clock Config">
				<Anchors>
					<Anchor point="TOP" relativeTo="$parent"></Anchor>
				</Anchors>
			</FontString>
		</Layer>
	</Layers>
 </Frame>
 ...

Adding Close and Default Buttons[edit]

That should make a small title box appear, and you'll notice the FontString's text attribute is set to "Clock Config" -- you can change this to anything you want. If you're doing localization (ui), maybe even a constant! You might notice that once you display your config frame, there's no way to get rid of it! Let's fix that right away by adding a `Close' button:

within myClockConfigFrame.xml

 ...
 <Frame ...>
	...
	<Frames>
		...
		<Button name="$parentButtonClose" inherits="OptionsButtonTemplate" text="Close">
			<Anchors>
				<Anchor point="BOTTOMRIGHT">
				<Offset><AbsDimension x="-12" y="16"/></Offset>
				</Anchor>
			</Anchors>
			<Scripts>
				<OnClick> myClockConfigFrame:Hide(); </OnClick>
			</Scripts>
		</Button>
		...
	</Frames>
 	...
 </Frame ...>
 ...

This button, which you'll notice is anchored at the bottom right of the config frame, simply tells our configuration frame to hide.

While we're at it, let's add a defaults button in the bottom left of the frame.

within myClockConfigFrame.xml

 ...
 <Frame ...>
	...
	<Frames>
		...
		<Button name="$parentButtonToDefault" inherits="OptionsButtonTemplate" text="Defaults">
			<Anchors>
				<Anchor point="BOTTOMLEFT">
				<Offset><AbsDimension x="13" y="16"/></Offset>
				</Anchor>
			</Anchors>
			<Scripts>
				<OnClick> grokClock_ConfigToDefault(); </OnClick>
			</Scripts>
		</Button>
 		...
 	</Frames>
 	...
 </Frame ...>
 ...

You'll notice all we really had to do here was call our ConfigToDefault function we made in 'myClockIndicatorFrame.lua' -- it will take care of updating our addon for us! Now we should be able to undo any damage should we mess around with our mod's config settings.

Checkboxes and Sliders[edit]

With most of the graphical frame stuff out of the way, it's time to add our checkboxes, and a slider for the offset (Dropdown menus are a lot more complicated, so this HOWTO won't be covering that).

Your basic checkbox config will look like:

within myClockConfigFrame.xml

 ...
 <Frame ...>
	...
	<Frames>
		...
		<CheckButton name="$parentCheckButtonOn" inherits="OptionsCheckButtonTemplate">
			<Anchors>
				<Anchor point="TOPLEFT" relativeTo="$parent">
				<Offset><AbsDimension x="20" y="-30"/></Offset>
				</Anchor>
			</Anchors>
			<Scripts>
				<OnLoad> getglobal(this:GetName().."Text"):SetText("On"); </OnLoad>
				<OnClick> myClockConfigFrameOption_OnClick(); </OnClick>
			</Scripts>
		</CheckButton>
		...
	</Frames>
 	...
 </Frame ...>
 ...

You'll notice I'm using the '$parent' and 'this' variables here to make this code generic, able to be used anywhere. If this was the 2nd checkbox, my Anchor relativeTo would be '$parentCheckButtonOn', and so forth, so that each item in the config dialog was just a bit bellow the others. Of course you could use absolute positioning by just getting rid of the 'relativeTo' altogether.

You should also see that the CheckButton has a OnLoad to set its text, since this will be an automatically created FontString. Again I use 'this:GetName().."Text"' to make this code generic. I've set the text to "On" in this case, but you can change this to whatever you want, even a localized (ui) variable!

The OnClick simply loads a lua function, but it is important to note that we will use this function for ALL our CheckButtons. That's right, we can use the same function for all our CheckButton's!

In our case we'll need to repeat this CheckButton xml code twice, once for 'on' and again for 'time24'. So go ahead and try to create another checkbox. Watch out for the offset! Look at the slider example bellow for how the offset should look after the 1rst item.

Here's what our slider code might look like:

within myClockConfigFrame.xml

 ...
 <Frame ...>
	...
	<Frames>
		...
		<Slider name="$parentSliderOffset" inherits="OptionsSliderTemplate">
			<Size>
				<AbsDimension x="220" y="16"/>
			</Size>
			<Anchors>
				<Anchor point="TOPLEFT" relativeTo="$parentCheckButtonTime24">
			<Offset><AbsDimension x="0" y="-40"/></Offset>
			</Anchor>
			</Anchors>
			<Scripts>
				<OnLoad>
					getglobal(this:GetName().."Text"):SetText("Offset");
					getglobal(this:GetName().."High"):SetText("12");
					getglobal(this:GetName().."Low"):SetText("-12");
					this:SetMinMaxValues(-12,12);
					this:SetValueStep(1);
				</OnLoad>
				<OnValueChanged> grokClockConfigFrameOption_OnClick();  </OnValueChanged>
			</Scripts>
		</Slider>
 		...
	</Frames>
 	...
 </Frame>
 ...

You'll again notice that we're using the Anchor relativeTo on the last checkbox, which will be $parentCheckButtonTime24. We're again dynamically getting the FontStrings that WoW will create for us, like the 'Text' one set to "Offset". As well, because this is a slider, we have "High" and "Low" FontStrings, have to set the minimum and maximum values the slider has, and how many values are added or subtracted when you move the slider. What will most surprise you is that.. yes... we can still use that option function for our slider too! One function for all the basic config options!

Frame Scripts[edit]

It's also important that all our current settings are loaded into our config frame, and since they might change when we use a slash command or something, the best way would be to use a OnShow event.

within myClockConfigFrame.xml

 ...
 <Frame ...>
	...
	<Scripts>
	 	<OnShow> myClockConfigFrame_OnShow(); </OnShow>
	</Scripts>
	...
 </Frame>
 ...


config frame .lua[edit]

Our lua file for the config frame then breaks down into just 2 functions, one for when the frame is shown, the other for when an option in the config frame (like a checkbox, or a slider is moved) changes.

within myClockConfigFrame.lua

 -- OnShow
 function myClockConfigFrame_OnShow()
	-- make sure our profile has been loaded
	if ( not myClock_variablesLoaded ) then -- config not loaded
		this:Hide(); -- hide our config pane
		return;
	end
	-- read settings from profile, and change our checkbuttons and slider to represent them
	getglobal(this:GetName().."CheckButtonOn"):SetChecked( myClockConfig[myClockRealm][myClockChar].on );
	getglobal(this:GetName().."CheckButtonTime24"):SetChecked( myClockConfig[myClockRealm][myClockChar].time24 );		
	getglobal(this:GetName().."SliderOffset"):SetValue( myClockConfig[myClockRealm].offset );
 end

You'll notice again that we have to make sure that the profile is there (or variables loaded). The code also uses the getglobal() function to construct the names of the controls, so it doesn't matter what we called our frame. After that it's a simple matter of using SetChecked() or SetValue() with our profile settings to make our config dialog look right.

The last part is making sure that when they change something in the config dialog, it changes the profile. Here's what that would look like:

within myClockConfigFrame.lua

 -- OnClick
 function grokClockConfigFrameOption_OnClick()
	-- make sure our profile has been loaded
	if ( not myClock_variablesLoaded ) then -- config not loaded
		this:GetParent():Hide(); -- hide our config pane (this is now a checkbox)
		return;
	end
	-- read setting out of checkbox (or slider) and put into profile
       -- use this:GetName() to know which checkbox was hit.
	if ( this:GetName() == (this:GetParent():GetName().."CheckButtonOn" ) ) then
		myClockConfig[myClockRealm][myClockChar].on = this:GetChecked(); -- set profile
	elseif ( this:GetName() == (this:GetParent():GetName().."CheckButtonTime24" ) ) then
		myClockConfig[myClockRealm][myClockChar].time24 = this:GetChecked();
	elseif ( this:GetName() == (this:GetParent():GetName().."SliderOffset" ) ) then
		myClockConfig[myClockRealm].offset = this:GetValue();
	end
	-- configuration was changed, make sure our addon changes too!
	-- notice our addon is changed right away, not when we hit 'done'.
	myClock_ConfigChange();
 end

Again we have a check to make sure our profile is loaded, and then a mock switch/case statement to determine which CheckButton was pressed. Once we know, it's a simple issue of using this:GetChecked() or this:GetValue() to set our profile config! At the end we make sure our main addon knows that something was changed.

Alternatively you could leave out the 'myClock_ConfigChange();' of this function, and add it to the Done button OnClick event to make a Save/Cancel dialog. Generally speaking for a config people want to see the changes happen live so they can tweak them.

You're done! Hopefully this provided some concrete examples on how to make a config dialog, and much thanks to Scheid for being the ginipig! You could easily go from here to make a more complex configuration pane, using tabs, multiple pages, or even using Cosmos' Khaos (another howto someday).