You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
416 lines
13 KiB
416 lines
13 KiB
--- MSA-Tutorials-1.0
|
|
--- Tutorials from Marouan Sabbagh based on CustomTutorials from João Cardoso.
|
|
|
|
--[[
|
|
Copyright 2010-2015 João Cardoso
|
|
CustomTutorials is distributed under the terms of the GNU General Public License (or the Lesser GPL).
|
|
|
|
CustomTutorials is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
CustomTutorials is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with CustomTutorials. If not, see <http://www.gnu.org/licenses/>.
|
|
--]]
|
|
|
|
--[[
|
|
General Arguments
|
|
-----------------
|
|
savedvariable
|
|
icon ........... Default is "?" icon. Image path (tga or blp).
|
|
title .......... Default is "Tutorial".
|
|
width .......... Default is 350. Internal frame width (without borders).
|
|
font ........... Default is game font (empty string).
|
|
|
|
Frame Arguments
|
|
---------------
|
|
title .......... Title relative to frame (replace General value).
|
|
width .......... Width relative to frame (replace General value).
|
|
Note: All other arguments can be used as a general!
|
|
image .......... [optional] Image path (tga or blp).
|
|
imageWidth ..... Default is 256.
|
|
imageHeight .... Default is 128.
|
|
imagePoint ..... Default is "TOP".
|
|
imageX ......... Default is 0.
|
|
imageY ......... Default is 20.
|
|
imageAbsolute .. Default is false. The image is not part of the content flow and no place is created for it.
|
|
imageTexCoords . [optional] Sets the coordinates for cropping or transforming the texture.
|
|
text ........... Text string.
|
|
textHeight ..... Default is 0 (auto height).
|
|
textX .......... Default is 25. Left and Right margin.
|
|
textY .......... Default is 20 (top margin).
|
|
editbox ........ [optional] Table of edit boxes. Edit box is out of content flow.
|
|
One table keys:
|
|
- text ..... [required]
|
|
- width .... Default is 400
|
|
- left ..... Default is 0
|
|
- top ...... Default is 0
|
|
- bottom ... Default is 0
|
|
button ......... [optional] Button text string (directing value). Button is out of content flow.
|
|
buttonWidth .... Default is 100.
|
|
buttonClick .... Function with button's click action.
|
|
buttonLeft, buttonBottom
|
|
shine .......... [optional] The frame to anchor the flashing "look at me!" glow.
|
|
shineTop, shineBottom, shineLeft, shineRight
|
|
point .......... Default is "CENTER".
|
|
anchor ......... Default is "UIParent".
|
|
relPoint ....... Default is "CENTER".
|
|
x, y ........... Default is 0, 0.
|
|
--]]
|
|
|
|
-- Lua API
|
|
local floor = math.floor
|
|
local fmod = math.fmod
|
|
local format = string.format
|
|
local strfind = string.find
|
|
local round = function(n) return floor(n + 0.5) end
|
|
|
|
local Lib = LibStub:NewLibrary('MSA-Tutorials-1.0', 14)
|
|
if Lib then
|
|
Lib.NewFrame, Lib.NewButton, Lib.UpdateFrame = nil
|
|
Lib.numFrames = Lib.numFrames or 1
|
|
Lib.frames = Lib.frames or {}
|
|
else
|
|
return
|
|
end
|
|
|
|
local BUTTON_TEX = 'Interface\\Buttons\\UI-SpellbookIcon-%sPage-%s'
|
|
local Frames = Lib.frames
|
|
local freeEditboxes = {}
|
|
|
|
local default = {
|
|
title = "Tutorial",
|
|
width = 350,
|
|
font = "",
|
|
imageWidth = 256,
|
|
imageHeight = 128,
|
|
imagePoint = "TOP",
|
|
imageX = 0,
|
|
imageY = 20,
|
|
imageFloat = false,
|
|
textHeight = 0,
|
|
textX = 25,
|
|
textY = 20,
|
|
buttonWidth = 100,
|
|
point = "CENTER",
|
|
anchor = UIParent,
|
|
relPoint = "CENTER",
|
|
x = 0,
|
|
y = 0,
|
|
}
|
|
|
|
--[[ Internal API ]]--
|
|
|
|
local function ConvertPixelsToUI(pixels, frameScale)
|
|
local physicalScreenHeight = select(2, GetPhysicalScreenSize());
|
|
return (pixels * 768.0)/(physicalScreenHeight * frameScale);
|
|
end
|
|
|
|
local function NewEditbox(frame, width)
|
|
local numFreeEditboxes = #freeEditboxes
|
|
local editbox
|
|
if numFreeEditboxes > 0 then
|
|
editbox = tremove(freeEditboxes, numFreeEditboxes)
|
|
editbox:SetParent(frame)
|
|
else
|
|
editbox = CreateFrame('EditBox', nil, frame, 'InputBoxTemplate')
|
|
editbox:SetAutoFocus(false)
|
|
end
|
|
editbox:SetWidth(width)
|
|
editbox:SetHeight(20)
|
|
editbox:Show()
|
|
return editbox
|
|
end
|
|
|
|
local function RemoveEditboxes(frame)
|
|
for i = 1, #frame.editboxes do
|
|
tinsert(freeEditboxes, frame.editboxes[i])
|
|
frame.editboxes[i]:Hide()
|
|
frame.editboxes[i] = nil
|
|
end
|
|
end
|
|
|
|
local function UpdateFrame(frame, i)
|
|
local data = frame.data[i]
|
|
if not data then
|
|
return
|
|
end
|
|
|
|
if (not data.image or data.imageAbsolute) and not data.textY then
|
|
data.textY = 0
|
|
end
|
|
for k, v in pairs(default) do
|
|
if not data[k] then
|
|
if not frame.data[k] then
|
|
data[k] = v
|
|
else
|
|
data[k] = frame.data[k]
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Callbacks
|
|
if frame.data.onShow then
|
|
frame.data.onShow(frame.data, i)
|
|
end
|
|
|
|
-- Frame
|
|
frame:ClearAllPoints()
|
|
frame:SetPoint(data.point, data.anchor, data.relPoint, data.x, data.y)
|
|
frame:SetWidth(data.width + 16)
|
|
local titleText = WOW_PROJECT_ID == WOW_PROJECT_MAINLINE and frame.TitleContainer.TitleText or frame.TitleText
|
|
titleText:SetPoint('TOP', 0, -5)
|
|
titleText:SetText(data.title)
|
|
|
|
-- Cache inline texture
|
|
local j, idx = 1, 1
|
|
local lastTex
|
|
while idx do
|
|
local s, e, tex = strfind(data.text, "|T(Interface\\AddOns\\[^:]+)[^|]+|t", idx)
|
|
if tex then
|
|
if tex ~= lastTex then
|
|
if not frame["cache"..j] then
|
|
frame["cache"..j] = frame:CreateTexture()
|
|
end
|
|
frame["cache"..j]:SetTexture(tex)
|
|
lastTex = tex
|
|
j = j + 1
|
|
end
|
|
idx = e
|
|
else
|
|
break
|
|
end
|
|
end
|
|
|
|
-- Image
|
|
for _, image in pairs(frame.images) do
|
|
image:Hide()
|
|
end
|
|
if data.image then
|
|
local img = frame.images[i]
|
|
if not img then
|
|
img = CreateFrame("Frame", nil, frame)
|
|
img:SetFrameLevel(1)
|
|
img.texture = img:CreateTexture()
|
|
img.texture:SetAllPoints()
|
|
end
|
|
img.texture:SetTexture(data.image)
|
|
if data.imageTexCoords then
|
|
img.texture:SetTexCoord(unpack(data.imageTexCoords))
|
|
end
|
|
img:SetSize(data.imageWidth, data.imageHeight)
|
|
img:SetPoint(data.imagePoint, frame, data.imageX - 1, -(25 + data.imageY))
|
|
img:Show()
|
|
frame.images[i] = img
|
|
end
|
|
|
|
-- Text
|
|
frame.text:SetPoint('TOP', frame, 0, -(((data.image and not data.imageAbsolute) and 26 + data.imageY + data.imageHeight or 60) + data.textY))
|
|
frame.text:SetWidth(data.width - (2 * data.textX))
|
|
frame.text:SetText(data.text)
|
|
|
|
local textHeight = round(frame.text:GetHeight())
|
|
if data.textHeight > textHeight then
|
|
textHeight = data.textHeight
|
|
end
|
|
textHeight = textHeight - fmod(textHeight, 2)
|
|
frame:SetHeight(((data.image and not data.imageAbsolute) and 56 + data.imageY + data.imageHeight or 90) + (data.text and data.textY + textHeight or 0) + 18)
|
|
frame.i = i
|
|
frame:Show()
|
|
|
|
-- EditBox
|
|
RemoveEditboxes(frame)
|
|
if data.editbox then
|
|
for i = 1, #data.editbox do
|
|
frame.editboxes[i] = NewEditbox(frame, data.editbox[i].width or 400)
|
|
frame.editboxes[i]:ClearFocus()
|
|
frame.editboxes[i]:ClearAllPoints()
|
|
if data.editbox[i].top then
|
|
frame.editboxes[i]:SetPoint('TOPLEFT', 14 + data.textX + (data.editbox[i].left or 0), -(60 + data.textY + (data.editbox[i].top or 0)))
|
|
elseif data.editbox[i].bottom then
|
|
frame.editboxes[i]:SetPoint('BOTTOMLEFT', 14 + data.textX + (data.editbox[i].left or 0), 28 + 18 + (data.editbox[i].bottom or 0))
|
|
end
|
|
frame.editboxes[i]:SetText(data.editbox[i].text)
|
|
end
|
|
end
|
|
|
|
-- Button
|
|
if data.button then
|
|
frame.button:SetWidth(data.buttonWidth)
|
|
frame.button:SetPoint('BOTTOMLEFT', 8 + data.textX + (data.buttonLeft or 0), 28 + 18 + (data.buttonBottom or 0))
|
|
frame.button:SetText(data.button)
|
|
frame.button:SetScript('OnClick', data.buttonClick)
|
|
frame.button:Show()
|
|
else
|
|
frame.button:Hide()
|
|
end
|
|
|
|
-- Shine
|
|
if data.shine then
|
|
frame.shine:SetParent(data.shine)
|
|
frame.shine:SetPoint('BOTTOMRIGHT', data.shineRight or 0, data.shineBottom or 0)
|
|
frame.shine:SetPoint('TOPLEFT', data.shineLeft or 0, data.shineTop or 0)
|
|
frame.shine:Show()
|
|
frame.flash:Play()
|
|
else
|
|
frame.flash:Stop()
|
|
frame.shine:Hide()
|
|
end
|
|
|
|
-- Buttons
|
|
if i == 1 then
|
|
frame.prev:Disable()
|
|
else
|
|
frame.prev:Enable()
|
|
end
|
|
frame.pageNum:SetText(("%d/%d"):format(i, frame.unlocked))
|
|
if i < (frame.unlocked or 0) then
|
|
frame.next:Enable()
|
|
else
|
|
frame.next:Disable()
|
|
end
|
|
|
|
-- Save
|
|
local sv = frame.data.key or frame.data.savedvariable
|
|
if sv then
|
|
local table = frame.data.key and frame.data.savedvariable or _G
|
|
table[sv] = max(i, table[sv] or 0)
|
|
end
|
|
end
|
|
|
|
local function NewButton(frame, name, direction)
|
|
local button = CreateFrame('Button', nil, frame)
|
|
button:SetHighlightTexture('Interface\\Buttons\\UI-Common-MouseHilight')
|
|
button:SetDisabledTexture(BUTTON_TEX:format(name, 'Disabled'))
|
|
button:SetPushedTexture(BUTTON_TEX:format(name, 'Down'))
|
|
button:SetNormalTexture(BUTTON_TEX:format(name, 'Up'))
|
|
button:SetPoint('BOTTOM'..((direction == -1) and 'LEFT' or 'RIGHT'), -(30 * direction), 1)
|
|
button:SetSize(26, 26)
|
|
button:SetScript('OnClick', function()
|
|
UpdateFrame(frame, frame.i + direction)
|
|
end)
|
|
|
|
local text = button:CreateFontString(nil, nil, 'GameFontHighlightSmall')
|
|
text:SetText(_G[strupper(name)])
|
|
text:SetPoint('LEFT', -(13 + text:GetStringWidth()/2) * direction, 0)
|
|
|
|
return button
|
|
end
|
|
|
|
local function NewFrame(data)
|
|
local frame = CreateFrame('Frame', 'Tutorials'..Lib.numFrames, UIParent, 'ButtonFrameTemplate')
|
|
local portrait = WOW_PROJECT_ID == WOW_PROJECT_MAINLINE and frame:GetPortrait() or frame.portrait
|
|
portrait:SetPoint('TOPLEFT', -3, 5)
|
|
portrait:SetTexture(data.icon or 'Interface\\TutorialFrame\\UI-HELP-PORTRAIT')
|
|
frame.Inset:SetPoint('TOPLEFT', 4, -23)
|
|
frame.Inset.Bg:SetColorTexture(0, 0, 0)
|
|
|
|
frame.images = {}
|
|
frame.text = frame:CreateFontString(nil, nil, 'GameFontHighlight')
|
|
if data.font then
|
|
frame.text:SetFont(data.font, 12)
|
|
end
|
|
frame.text:SetJustifyH('LEFT')
|
|
frame.editboxes = {}
|
|
|
|
frame.prev = NewButton(frame, 'Prev', -1)
|
|
frame.next = NewButton(frame, 'Next', 1)
|
|
|
|
frame.pageNum = frame:CreateFontString(nil, nil, 'GameFontHighlightSmall')
|
|
frame.pageNum:SetPoint('BOTTOM', 0, 9)
|
|
|
|
frame:SetFrameStrata('DIALOG')
|
|
frame:SetClampedToScreen(true)
|
|
frame:EnableMouse(true)
|
|
frame:SetToplevel(true)
|
|
frame:SetScript('OnHide', function()
|
|
frame.flash:Stop()
|
|
frame.shine:Hide()
|
|
if frame.data.onHide then
|
|
frame.data.onHide()
|
|
end
|
|
end)
|
|
frame:SetScript("OnEvent", function(self, event)
|
|
if event == "PLAYER_ENTERING_WORLD" then
|
|
UpdateFrame(self, self.i) -- for update textHeight (UI Scale)
|
|
self:UnregisterEvent(event)
|
|
end
|
|
end)
|
|
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
|
|
frame.button = CreateFrame('Button', nil, frame, 'UIPanelButtonTemplate')
|
|
frame.button:SetSize(100, 22)
|
|
frame.button:SetPoint("CENTER")
|
|
frame.button:Hide()
|
|
|
|
frame.shine = CreateFrame('Frame', nil, frame, BackdropTemplateMixin and 'BackdropTemplate')
|
|
frame.shine:SetBackdrop({edgeFile = 'Interface\\TutorialFrame\\UI-TutorialFrame-CalloutGlow', edgeSize = 16})
|
|
for i = 1, frame.shine:GetNumRegions() do
|
|
select(i, frame.shine:GetRegions()):SetBlendMode('ADD')
|
|
end
|
|
|
|
local flash = frame.shine:CreateAnimationGroup()
|
|
flash:SetLooping('BOUNCE')
|
|
frame.flash = flash
|
|
|
|
local anim = flash:CreateAnimation('Alpha')
|
|
anim:SetDuration(.75)
|
|
anim:SetFromAlpha(.7)
|
|
anim:SetToAlpha(0)
|
|
|
|
frame.data = data
|
|
Lib.numFrames = Lib.numFrames + 1
|
|
return frame
|
|
end
|
|
|
|
|
|
--[[ User API ]]--
|
|
|
|
function Lib:RegisterTutorial(data)
|
|
assert(type(data) == 'table', 'RegisterTutorials: 2nd arg must be a table', 2)
|
|
assert(self, 'RegisterTutorials: 1st arg was not provided', 2)
|
|
|
|
if not Lib.frames[self] then
|
|
Lib.frames[self] = NewFrame(data)
|
|
end
|
|
end
|
|
|
|
function Lib:TriggerTutorial(index, maxAdvance)
|
|
assert(type(index) == 'number', 'TriggerTutorial: 2nd arg must be a number', 2)
|
|
assert(self, 'RegisterTutorials: 1st arg was not provided', 2)
|
|
|
|
local frame = Lib.frames[self]
|
|
if frame then
|
|
local sv = frame.data.key or frame.data.savedvariable
|
|
local table = frame.data.key and frame.data.savedvariable or _G
|
|
local last = sv and table[sv] or 0
|
|
|
|
if index > last then
|
|
frame.unlocked = index
|
|
UpdateFrame(frame, (maxAdvance == true or not sv) and index or last + (maxAdvance or 1))
|
|
end
|
|
end
|
|
end
|
|
|
|
function Lib:ResetTutorial()
|
|
assert(self, 'RegisterTutorials: 1st arg was not provided', 2)
|
|
|
|
local frame = Lib.frames[self]
|
|
if frame then
|
|
local sv = frame.data.key or frame.data.savedvariable
|
|
if sv then
|
|
local table = frame.data.key and frame.data.savedvariable or _G
|
|
table[sv] = false
|
|
end
|
|
end
|
|
end
|
|
|
|
function Lib:GetTutorial()
|
|
return self and Lib.frames[self] and Lib.frames[self].data
|
|
end
|