Files
OEVG 896f46f0d1
enUS Localization Update Announcement / localization-update-announcement (push) Canceled after 0s
Bagshui 1.6.4
2026-10-30 15:55:43 +00:00

400 lines
16 KiB
Lua

-- Bagshui Dialog Box
-- Exposes: BsDialog (and Bagshui.prototypes.Dialog)
--
-- Special dialog boxes for Bagshui functionality.
Bagshui:AddComponent(function()
local Ui = Bagshui.prototypes.Ui
-- Dialog properties and instances should be stored on the prototype so they're global.
Ui.dialogProperties = {}
Ui.dialogInstances = {}
--- Set up a new dialog. This is similar to Blizzard's `StaticPopupDialogs[]`.
--- It's built on Bagshui's ScrollableTextWindow class, so the dialog objects
--- are ScrollableTextWindow instances.
---
--- Properties can be anything accepted by `ScrollableTextWindow:New()`, plus:
--- ```
--- {
--- ---@type string? Text to at the top of the dialog. Will be overridden by `ShowDialog()` parameter if provided.
--- prompt,
--- ---@type string? Text to display on button1 (the left button). Will be overridden by `ShowDialog()` parameter if provided. An empty string will hide the button.
--- button1,
--- ---@type string? Text to display on button2 (the right button). Will be overridden by `ShowDialog()` parameter if provided. An empty string will hide the button.
--- button2,
--- ---@type boolean? Disable button1 unless there is text in the EditBox..
--- button1DisableOnEmptyText,
--- ---@type boolean? Disable button2 unless there is text in the EditBox..
--- button2DisableOnEmptyText,
--- ---@type string? Text to display on button2 (the right button). Will be overridden by `ShowDialog()` parameter if provided. An empty string will hide the button.
--- button2,
--- ---@type function? Called when button1 is clicked (see notes below).
--- OnAccept = function(dialog) -> nil,
--- ---@type function? Called when button2 is clicked (see notes below).
--- OnCancel = function(dialog) -> nil,
--- ---@type function? Perform any dialog customization at the end of the dialog's `InitUi()`.
--- dialogCustomizationFunc = function(dialog) -> nil,
--- }
--- ```
---
--- ### OnAccept/OnCancel
--- These functions will be passed the dialog instance as their only parameter.
--- From there, all `ScrollableTextWindow` properties are available.
--- The most important things are getting the EditBox contents and passing data
--- through from the calling function.
---
--- If OnAccept is defined as:
--- ```
--- OnAccept = function(dialog)
--- Bagshui:PrintDebug(dialog:GetText())
--- Bagshui:PrintDebug(dialog.data.someProperty)
--- end
--- ```
---
--- Then the calling function can do something like:
--- ```
--- local dialog = ui:ShowDialog("DIALOG_ID", "Enter some text:")
--- dialog.data.someProperty = "This will pass through to the callback."
--- ```
---@param id any Unique identifier.
---@param dialogProperties table List of properties.
function Ui:AddMultilineDialog(id, dialogProperties)
dialogProperties.width = dialogProperties.width or 400
dialogProperties.height = dialogProperties.height or 400
self.dialogProperties[id] = dialogProperties
end
--- Obtain a dialog instance as configured in `AddDialog()`.
---@param id any Unique identifier as set up in `AddDialog()`.
---@return table dialog Dialog-modified ScrollableTextWindow instance.
function Ui:GetDialog(id)
assert(self.dialogProperties[id], tostring(id) .. " is not a known dialog! Initialize with `AddDialog()` first.")
if not self.dialogInstances[id] then
self.dialogInstances[id] = {}
end
local dialogProperties = self.dialogProperties[id]
local dialog
-- Find an existing dialog to reuse.
for _, existingDialog in ipairs(self.dialogInstances[id]) do
if
existingDialog.recycle == true
or not existingDialog.uiFrame
or not existingDialog.uiFrame.IsVisible
or not existingDialog.uiFrame:IsVisible()
then
dialog = existingDialog
break
end
end
-- Nothing found; create a new one.
if not dialog then
dialogProperties.name = tostring(id) .. table.getn(self.dialogInstances[id])
dialog = self.ScrollableTextWindow:New(dialogProperties)
dialog.isDialog = true
dialog.ui = self
dialog.id = id
dialog.data = {}
dialog.prompt = nil
dialog.button1 = _G.OKAY
dialog.button2 = _G.CANCEL
dialog.dialogProperties = dialogProperties
--- Initialize the UI.
function dialog:InitUi()
assert(
self.prompt,
"Dialog prompt text is required, either via ShowDialog() parameter or in AddDialog() property list."
)
self._super.InitUi(self)
-- Needed for `Bagshui:CloseAllWindows()`.
self.uiFrame.isDialog = true
-- Keep it on top of everything else.
self.uiFrame:SetFrameStrata("DIALOG")
-- This is called every time ShowDialog() is called, so we need to avoid
-- customizing more than once.
if not self.uiFrame.bagshuiData.prompt then
-- Special stuff for dialog behaviors on top of the base ScrollableTextWindow scripts.
local oldOnShow = self.uiFrame:GetScript("OnShow")
self.uiFrame:SetScript("OnShow", function()
if self.suppressShowHideEvents then
return
end
if oldOnShow then
oldOnShow()
end
self:SetParentHasModalDialog(true)
self.recycle = false
end)
local oldOnHide = self.uiFrame:GetScript("OnHide")
self.uiFrame:SetScript("OnHide", function()
if self.suppressShowHideEvents then
return
end
if oldOnHide then
oldOnHide()
end
if self.modalParent then
self.ui:SetWindowShade(self.modalParent, false)
Bagshui:QueueClassCallback(self, self.SetParentHasModalDialog, 0.1, nil, false)
end
self.editBox:SetText("")
self.recycle = true
end)
-- EditBox changes.
local oldOnChanged = self.editBox:GetScript("OnTextChanged")
self.editBox:SetScript("OnTextChanged", function()
oldOnChanged()
if self.dialogProperties.button1DisableOnEmptyText or self.dialogProperties.button1DisableOnEmptyText then
local state = string.len(BsUtil.Trim(_G.this:GetText() or "")) > 0 and "Enable" or "Disable"
if self.dialogProperties.button1DisableOnEmptyText then
self.uiFrame.bagshuiData.button1[state](self.uiFrame.bagshuiData.button1)
end
if self.dialogProperties.button2DisableOnEmptyText then
self.uiFrame.bagshuiData.button2[state](self.uiFrame.bagshuiData.button2)
end
end
end)
-- Prompt text.
self.uiFrame.bagshuiData.prompt = self.uiFrame:CreateFontString(nil, nil, "GameFontHighlight")
self.uiFrame.bagshuiData.prompt:SetText(self.prompt)
self.uiFrame.bagshuiData.prompt:SetJustifyH("LEFT")
self.uiFrame.bagshuiData.prompt:SetPoint(
"TOPLEFT",
self.uiFrame,
"TOPLEFT",
BsSkin.windowPadding,
-BsSkin.windowPadding
)
self.uiFrame.bagshuiData.prompt:SetWidth(self.dialogProperties.width - (BsSkin.windowPadding * 2))
-- Buttons.
local buttonWidth = math.min(self.dialogProperties.width / 2 - 10, 150)
-- Directly passing the default text parameter here to ensure button heights are set correctly.
self.uiFrame.bagshuiData.button1 = self.ui:CreateButton("Button1", self.uiFrame, _G.TEXT(_G.OKAY), function()
if self.dialogProperties.OnAccept then
self.dialogProperties.OnAccept(dialog)
end
self.uiFrame:Hide()
end)
self.uiFrame.bagshuiData.button1:SetPoint(
"BOTTOMLEFT",
self.uiFrame,
"BOTTOMLEFT",
BsSkin.windowPadding,
BsSkin.windowPadding
)
self.uiFrame.bagshuiData.button1:SetWidth(buttonWidth)
self.uiFrame.bagshuiData.button2 = self.ui:CreateButton(
"Button1",
self.uiFrame,
_G.TEXT(_G.CANCEL),
function()
if self.dialogProperties.OnCancel then
self.dialogProperties.OnCancel(dialog)
end
self.uiFrame:Hide()
end
)
self.uiFrame.bagshuiData.button2:SetPoint(
"BOTTOMRIGHT",
self.uiFrame,
"BOTTOMRIGHT",
-BsSkin.windowPadding,
BsSkin.windowPadding
)
self.uiFrame.bagshuiData.button2:SetWidth(buttonWidth)
-- Re-anchor ScrollFrame to prompt text and buttons.
self.ui:SetPoint(
self.uiFrame.bagshuiData.scrollFrame,
"TOPLEFT",
self.uiFrame.bagshuiData.prompt,
"BOTTOMLEFT",
0,
-4
)
self.ui:SetPoint(
self.uiFrame.bagshuiData.scrollFrame,
"BOTTOMRIGHT",
self.uiFrame.bagshuiData.button2,
"TOPRIGHT",
0,
4
)
if type(self.dialogProperties.dialogCustomizationFunc) == "function" then
self.dialogProperties.dialogCustomizationFunc(self)
end
end -- ScrollableTextWindow customization complete.
-- Update everything to reflect current parameters.
self.uiFrame.bagshuiData.prompt:SetText(self.prompt)
if string.len(tostring(self.button1)) == 0 then
self.uiFrame.bagshuiData.button1:Hide()
else
self.uiFrame.bagshuiData.button1:SetText(self.button1)
self.uiFrame.bagshuiData.button1:Show()
end
if string.len(tostring(self.button2)) == 0 then
self.uiFrame.bagshuiData.button2:Hide()
else
self.uiFrame.bagshuiData.button2:SetText(self.button2)
self.uiFrame.bagshuiData.button2:Show()
end
self.uiFrame:SetHeight(self.dialogProperties.height + self.uiFrame.bagshuiData.prompt:GetHeight())
end
--- Focusing the EditBox too quickly causes the cursor to visually "stick",
--- where it doesn't flash and doesn't disappear when escape is pressed.
--- A short delay on the SetFocus() call works around it. *shrug*
function dialog:FocusEditBox()
self.editBox:SetFocus()
end
--- Clearing the `hasModalDialog` property too quickly causes the check in
--- `Bagshui:CloseAllWindows()` to see it as false before it should be and
--- the parent window ends up closing when it shouldn't.
function dialog:SetParentHasModalDialog(hasModalDialog)
if type(self.modalParent) == "table" and self.modalParent.GetName and self.modalParent.bagshuiData then
self.modalParent.bagshuiData.hasModalDialog = hasModalDialog
end
end
--- Display the window.
---@param text string Text to place in the EditBox.
---@return boolean? willOpen false if the window won't open due to missing parameters.
function dialog:Open(text)
if not self._super.Open(self, text) then
return false
end
if self.modalParent then
-- Modal dialog behavior -- anchor and center it on the parent and
-- place a shade over the parent frame.
self.uiFrame:SetMovable(false)
----------
-- Can't use SetParent() because it messes up the ScrollFrame overflow. :(
-- Instead, we're putting the frame on the DIALOG strata during its' InitUi().
-- self.suppressShowHideEvents = true -- Changing parentage will trigger OnHide/OnShow and we don't want them to fire again.
-- self.uiFrame:SetParent(self.modalParent)
-- self.suppressShowHideEvents = false
----------
self.uiFrame:ClearAllPoints()
self.uiFrame:SetPoint("CENTER", self.modalParent, "CENTER", 0, 0)
self.ui:SetWindowShade(self.modalParent, true, self.uiFrame)
self.modalParent.bagshuiData.hasModalDialog = true
end
-- Focus the EditBox after a short delay (see dialog:FocusEditBox() comments for reasoning).
Bagshui:QueueClassCallback(self, self.FocusEditBox, 0.1)
return true
end
table.insert(self.dialogInstances[id], dialog)
end
return dialog
end
--- Display a multiline input dialog.
--- To get the input, use the OnAccept/OnCancel callbacks (see notes for `AddDialog()`).
---@param idOrDialog any|table Unique identifier set up in `AddDialog()` *or* return value from `GetDialog()`.
---@param prompt string? Text to display above the EditBox. If not provided, mut have been given to `AddDialog()`.
---@param button1Text string? Text to display on the left-hand button. If not provided, will fall back to dialog properties and then default to localized "Okay". An empty string will hide the button.
---@param button2Text string? Text to display on the right-hand button. If not provided, will fall back to dialog properties and then default to localized "Cancel". An empty string will hide the button.
---@param defaultText string? Initial text placed in the EditBox.
---@param modalParent table? Frame to attach this dialog to.
---@return table dialog The dialog box object that will be passed through to callbacks.
function Ui:ShowDialog(idOrDialog, prompt, button1Text, button2Text, defaultText, modalParent)
local dialog = idOrDialog
if type(idOrDialog) == "string" then
dialog = self:GetDialog(idOrDialog)
end
assert(dialog and dialog.isDialog, "Ui:ShowDialog() - " .. tostring(idOrDialog) .. " is not a valid dialog.")
-- Update properties that will be consumed by InitUi().
dialog.properties = self.dialogProperties[dialog.id] -- Refresh pointer to properties in case they've been updated.
dialog.prompt = prompt or self.dialogProperties[dialog.id].prompt or nil
dialog.button1 = button1Text or self.dialogProperties[dialog.id].button1 or _G.OKAY
dialog.button2 = button2Text or self.dialogProperties[dialog.id].button2 or _G.CANCEL
dialog.modalParent = modalParent
-- Create/update UI.
dialog:InitUi()
-- Display the dialog and wait for callback.
-- ScrollableTextWindow:Open() expects a non-nil string so pass an empty one if no default text was given.
dialog:Open(type(defaultText) == "string" and defaultText or "")
return dialog
end
--- Display a copyable URL.
---@param url string
function Ui:ShowUrl(url)
local dialogName = "BAGSHUI_COPY_URL"
if not _G.StaticPopupDialogs[dialogName] then
_G.StaticPopupDialogs[dialogName] = {
text = L.HowToUrl,
button1 = _G.OKAY,
hasEditBox = true,
hasWideEditBox = true,
timeout = 0,
whileDead = true,
hideOnEscape = true,
-- Extra property to be used in OnShow to fill the edit box.
-- Blizzard doesn't provide an easy way to do this in Vanilla as the
-- data property isn't accessible in OnShow, and all the FrameXML code uses
-- global variables and functions.
_bagshuiUrl = "",
-- Set the default value for the edit box
OnShow = function()
-- For whatever reason, Blizzard has hardcoded which dialogs are wide instead of
-- resizing based on hasWideEditBox, so the only way to get the dialog wide enough
-- is to set it manually.
_G.this:SetWidth(420)
_G[_G.this:GetName() .. "WideEditBox"]:SetText(_G.StaticPopupDialogs[dialogName]._bagshuiUrl)
_G[_G.this:GetName() .. "WideEditBox"]:SetFocus()
_G[_G.this:GetName() .. "WideEditBox"]:HighlightText()
-- We also need to reposition Button1 because apparently you're not supposed to
-- have a dialog with an EditBox and one button.
_G[_G.this:GetName() .. "Button1"]:ClearAllPoints()
_G[_G.this:GetName() .. "Button1"]:SetPoint("TOP", _G[_G.this:GetName() .. "EditBox"], "BOTTOM", 0, -8)
end,
-- Clear text on hide so it doesn't bleed over to other dialogs.
OnHide = BsUtil.StaticPopupDialogs_ClearTextOnHide,
}
end
_G.StaticPopupDialogs[dialogName]._bagshuiUrl = url
_G.StaticPopup_Show(dialogName)
end
end)