Wowpedia

We have moved to Warcraft Wiki. Click here for information and the new URL.

READ MORE

Wowpedia
(hasWideEditBox has been replaced with editBoxWidth)
(→‎Basic setup: how to avoid some UI taint)
Line 21: Line 21:
 
whileDead = true,
 
whileDead = true,
 
hideOnEscape = true,
 
hideOnEscape = true,
  +
preferredIndex = 3, -- avoid some UI taint, see http://www.wowace.com/announcements/how-to-avoid-some-ui-taint/
 
}
 
}
   

Revision as of 00:45, 25 March 2012

Static popups are simple one-, two-, or three-button dialog boxes you see when confirming a warlock summon, sending money through the mail, renaming a pet, and so forth. This tutorial describes how you can use the StaticPopup code to display simple dialogs in your own addon.

As you might expect, all of the basic UI elements (positioning, sound effects, layout) are handled for you.


Basic setup

In the "Hello, World" programming tradition, the example on this page will assume that you have a function, called GreetTheWorld, and that you are creating a simple dialog to ask the player whether or not to be socially outgoing.

New dialogs are created by adding an entry to the global StaticPopupDialogs table, and populating the entry with required and optional information. Here is a simple two-button entry:

StaticPopupDialogs["EXAMPLE_HELLOWORLD"] = {
  text = "Do you want to greet the world today?",
  button1 = "Yes",
  button2 = "No",
  OnAccept = function()
      GreetTheWorld()
  end,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
  preferredIndex = 3,  -- avoid some UI taint, see http://www.wowace.com/announcements/how-to-avoid-some-ui-taint/
}

The index into the array is an arbitrary string. It must be unique in the context of the array. (If you are familiar with the SlashCmdList array, this works exactly the same.)

Here you see the basic required information, along with some settings that are strictly optional but should be given by all well-behaved entries:

  • text - This is the text inside the dialog box. For example, "Looting this item will bind it to you."
  • button1 - This is the text on the left-hand "yes" button. Clicking this button will call the OnAccept function in the entry. In a popup with only one button, it is this one. There are global variables includiung ACCEPT, CANCEL, and OKAY, which contain localized strings and are perfectly suited for assigning to the button* fields. Some dialogs (like the "you are not part of this instance's group and are going to get teleported" warning) do not even have a single button.
  • button2 - This is the text on the right-hand "no" button. If the entry has an OnCancel function, clicking this button will call it with "clicked" as the reason (see below).
  • OnAccept - This points to a function; it can be as complicated as you like and is typically a local "anonymous"-style function defined on the spot as shown above. You do not need to explicitly hide the popup frame; it will be hidden for you.
  • timeout - After this many seconds, the dialog will go away. If the entry has an OnCancel function, it will be called with "timeout" as the reason. Dialogs which do not expire should set this to zero.
  • whileDead - Set to true if this dialog can be shown while the player is a ghost. You probably want to do this.
  • hideOnEscape - Set to true if hitting the Escape key should be treated like clicking button2. You probably want to do this.

If your addon has a general OnLoad event handler, that is an excellent place to perform this array insertion. If the contents of the entry do not require any runtime information, you can perform the insertion during loadup by putting it at file scope.

Note that this guide refers to setting boolean options to 'true' or 'false'; earlier editions used '1' and 'nil' for the same purposes. The fact of the matter is that you can set those options to anything which evaluates to true-valued or false-valued results according to Lua: 'nil' and 'false' are false-valued, anything else (including 0 and "") is true-valued. A lot of original Blizzard code was written for an earlier version of Lua, which did not have formal boolean types. Use whatever makes the most sense for your addon.

Displaying the popup

To display the dialog, call StaticPopup_Show with the name of the entry:

StaticPopup_Show ("EXAMPLE_HELLOWORLD")

The dialog will be put together and displayed, and the function returns as soon as the dialog is shown. If all goes well, the table object for that dialog will be returned from the function. If there are any problems, it returns nil.

This function has two more optional arguments, see Dialog Text Parameters below.

Up to 4 static popups may be displayed at once. The UI will handle finding an open popup slot for you.

Hiding the popup

To make the dialog disappear without having a user click it, call StaticPopup_Hide with the name of the entry:

StaticPopup_Hide ("EXAMPLE_HELLOWORLD")

Calling the StaticPopup_Hide function after the dialog has already been hidden does not appear to have any ill effect. There are no return values from this function.

Advanced setup

  • OnAccept - This function can take up to two arguments, both optional. They are for passing arbitrary data around the callbacks. In general this is used with dialogs that have an editable text field, to pass the entered text back to the function; also for chaining dialog boxes together. For details, see "Passing arguments to local functions" below.
  • OnCancel - This function can take up to two arguments, both optional. The first is used the same way as the first parameter in OnAccept (whatever that may actually be). The second is a string describing the reason the popup was cancelled; the game will pass this as required:
    • "override" - Another popup cancelled this one, or is set to 'exclusive' (see below), or the player is dead or logging out.
    • "timeout" - The popup's entry specified a nonzero timeout field, and the time expired.
    • "clicked" - The player clicked button2, or hit the escape key and hideOnEscape is set.

Dialog text parameters

The text field of an entry can contain formatting placeholders. When the popup is actually displayed, the calling function can pass two additional arguments to StaticPopup_Show. The text field and these two arguments will be passed through the format function before being added to the dialog.

For example, the default guild invitation popup sets text = "%s invites you to join %s", and the display call is to StaticPopup_Show ("GUILD_INVITE", name_of_officer, name_of_guild).

Editable text fields

To add a editbox in the popup set the hasEditBox option field to true. The EditBox gets the name "$parentEditBox", relative to the Popup, and is also available in the popup's .editBox field. (Recall that the popup dialog is the value returned from the StaticPopup_Show() function.) To get and set the value of the EditBox use the following code:

OnShow = function (self, data)
    self.editBox:SetText("Some text goes here")
end,
OnAccept = function (self, data, data2)
    local text = self.editBox:GetText()
    -- do whatever you want with it
end,
hasEditBox = true

More complicated usage might be to use OnShow to call self.button1:Disable() (graying out the Accept button), and then including the following in your options table to allow the Accept button to be clicked as soon as the user types something in the text field:

EditBoxOnTextChanged = function (self, data)   -- careful! 'self' here points to the editbox, not the dialog
    self:GetParent().button1:Enable()          -- self:GetParent() is the dialog
end

Using hasEditBox/.editBox causes a small one-line text field to appear. By default this is 130 pixels wide; you can set the optional editBoxWidth key to change the width. (There used to be a hasWideEditBox boolean key that would create a .wideEditBox field in the dialog; in Cataclysm these were replaced by a editBoxWidth of 260 to 350.) The editbox will respect the maxLetters field if you set it (most editbox popups used by Blizzard set this to 24 or 31).

Optional features

There are some more settings available for use in static popup entries. This list is almost certainly incomplete, I'm just describing the ones which catch my eye. For more information, extract the StaticPopup.xml and .lua files from the default UI and browse.

  • sound - Play this sound when the dialog is displayed. For example, "igPlayerInvite".
  • hasMoneyFrame - This is for things like the money-in-mail confirmation. See the Lua file for more.
  • showAlert - Set this to true if you also want the OH NO SOMETHING BAD icon to be displayed in the popup. For example, deleting a mail message with an item still attached to it.
  • notClosableByLogout - Normally if a player quits the game, your popup will go away and its OnCancel function will be called. Set this field to true to avoid that behavior.
  • cancels - If your popup should make another popup go away, set this field to that popup's name. For example, cancels = "EXAMPLE_HELLOWORLD". If that popup is displayed at the time yours is shown, that popup's OnCancel function will be called and its frame hidden.
  • StartDelay and delayText - These fields are responsible for things like the delay before you can resurrect, before you get kicked out of an ungrouped instance, etc. The first is a function (a pointer to the function itself, not its name) to be called which returns the number of seconds which must pass before button1 can be used. The second is text to be displayed during the delay; it is formatted like the normal text field, with the time as the parameters.
  • exclusive - Set this to true to make your popup go away if any other popup is displayed.
  • enterClicksFirstButton - If your popup is in response to a slash command or some other keyboard event, then setting this allows the player to click Accept without having to move a hand off the keyboard to the mouse. Great for those fussy speed typists!  :-)
  • OnShow and OnHide - These functions will be called when the popup frame is initially shown and finally hidden, respectively. Most popups can ignore these. A very few need to do something special.
  • button3/OnAlt - You can set text for a third button to appear, and its corresponding callback. The button is placed between the first two buttons. (To remember this, think of how mouse buttons are labeled.) As of WoW 4.0, the buttons are ordered by their number ([button1] [button2] [button3]).

As an example, consider the Ready Check from CT_RaidAssist, which pops up a two-button dialog box with a 30-second timeout. The CT authors implemented everything from scratch: 107 lines of XML specifying window, field, and button sizes, plus a couple lines of Lua here and there. It could be replaced with something like the following:

StaticPopupDialogs["EXAMPLE_CTRA_READY"] = {
  text = "%s has performed a ready check.  Are you ready?",
  button1 = "Yes",
  button2 = "No",
  OnAccept = function()
      CT_RA_SendReady()
  end,
  OnCancel = function (_,reason)
      if reason == "timeout" or reason == "clicked" then
          CT_RA_SendNotReady()
      else
          -- "override" ...?
      end;
  end,
  sound = "levelup2",
  timeout = 30,
  whileDead = true,
  hideOnEscape = true,
}

and called via

StaticPopup_Show ("EXAMPLE_CTRA_READY", CT_RA_CheckReady_Person)

(Note: the above was written before the 1.11 game patch added a builtin ready check. While ready checks no longer need to be implemented by addon authors, the example is simple enough to remain here.)

Passing arguments to local functions

It is possible, though not immediately obvious how, to pass arbitrary user data to the local functions (e.g. OnAccept, OnCancel). Write your local function like this:

 OnAccept = function (self, data, data2)
     DoSomethingWith(data)
 end

And make the call like this:

 varName  = "Some value"                           -- This is the data you want to use in OnAccept
 varName2 = "Some other value"                     -- This is the data you want to use in OnAccept
 local dialog = StaticPopup_Show("YOUR_POPUP")     -- dialog contains the frame object
 if (dialog) then
     dialog.data  = varName                        -- set the frame's data field to the value you want
     dialog.data2 = varName2
 end

The dialog's fields "data" and "data2" are passed as additional arguments to OnAccept. The "data" field is passed to OnCancel and OnAlt (the function called if button3 is set), but not "data2". They are also accessible through the dialog itself, as self.data and self.data2. In this example they are strings, but they can be of any type including tables.

Notice that you are actually setting the frame's data after you have called StaticPopup_Show. This works because popping up a dialog box does not halt script execution. The dialog box isn't even visible to the player until after the current script execution cycle completes. By the time the player clicks a button to run OnAccept/OnCancel/etc, all of this code will have long since finished.

If you need to manipulate any of the dialog's visual elements, most of them are directly accessible as table fields of the dialog, so you do not need to use an expensive sequence of getglobal(self:GetName().."Something"). For example, dialog.button1 points to the first button. See the StaticPopup.xml file's various OnLoad sections for the full list.

Notes and observations

  • Creating a static popup with an editbox and only one button will cause the button and the editbox to overlap. Having more than one button will get the desired behavior. (Tested on 2.4.1)
  • While creating your popup entries, you will probably be doing a lot of UI reloading. Extract the FrameXML/DebugUI.xml file from the default UI, copy it into your addon's folder, and add it to your .toc file. This has two effects: it starts verbose logging into FrameXML.log, and it adds a "Reload UI" button to your screen. Very handy timesaver.  :-)
  • Added by Layrajha: The OnHide function will always be called after OnAccept or OnCancel have finished their execution. Therefore, it is safe to assume that your changes done in OnAccept or OnCancel will be done when OnHide is called. Also note that you can prevent the popup from hiding after clicking the "Accept" button: just make the OnAccept function return "true" (or anything different from "nil" and "false", it will work just as well).