mirror of
https://github.com/brues-code/pfUI.git
synced 2026-10-30 15:40:51 +00:00
pfUIScrollingEditBoxTemplate (api/ui-widgets.xml) is ScrollingEditBoxTemplate with pfUIScrollBarTemplate beside the text. Its mixin's SetFont gives the box a font object of its own, since the edit box sets its font by object on every SetText and pfUI fonts are paths. ui-widgets.lua now loads before the XML, which needs the mixin. The share box no longer recomputes its scroll child on every text change. Chat copy reads each chat frame's messages with ClassicAPI's GetMessageInfo when it opens, so the AddMessage hook, its 100-line copy of the history and the skip of combat frames are gone. It shows every line the frame keeps, lines from before login included. It puts the caret at the end to scroll to the newest line instead of three OnUpdate passes, and Escape now leaves copy mode on every chat frame.
381 lines
13 KiB
Lua
381 lines
13 KiB
Lua
pfUI:RegisterModule("share", function ()
|
|
-- Profile sharing rides ClassicAPI's C_EncodingUtil:
|
|
-- export: diff vs defaults -> SerializeCBOR -> CompressString (zlib)
|
|
-- -> EncodeBase64, prefixed with the format magic
|
|
-- import: the reverse, into a plain table — no loadstring, so a
|
|
-- pasted profile can't execute code
|
|
-- The Decode/Encode button converts between the transport blob and an
|
|
-- editable JSON view of the same table.
|
|
local MAGIC = "!pf1!"
|
|
|
|
-- Deep-diff `tbl` against `default`, keeping only changed or added
|
|
-- values. `ignored` keys are skipped at the top level only (matches the
|
|
-- old serializer: "position"/"disabled" when Ignore Layout is checked).
|
|
-- Returns nil when nothing differs.
|
|
local function DiffConfig(tbl, default, ignored)
|
|
local diff = nil
|
|
for k, v in pairs(tbl) do
|
|
if not ( ignored and ignored[k] ) then
|
|
local dv = default and default[k]
|
|
if type(v) == "table" then
|
|
local sub = DiffConfig(v, type(dv) == "table" and dv or nil)
|
|
if sub then
|
|
diff = diff or {}
|
|
diff[k] = sub
|
|
end
|
|
elseif v ~= dv and ( type(v) == "string" or type(v) == "number" or type(v) == "boolean" ) then
|
|
diff = diff or {}
|
|
diff[k] = v
|
|
end
|
|
end
|
|
end
|
|
return diff
|
|
end
|
|
|
|
-- EditBoxes don't soft-wrap one giant unbroken line; chunk the blob.
|
|
local function wrap(str, width)
|
|
local out = {}
|
|
for i = 1, strlen(str), width do
|
|
table.insert(out, strsub(str, i, i + width - 1))
|
|
end
|
|
return table.concat(out, "\n")
|
|
end
|
|
|
|
local function Encode(tbl)
|
|
return MAGIC .. wrap(C_EncodingUtil.EncodeBase64(
|
|
C_EncodingUtil.CompressString(
|
|
C_EncodingUtil.SerializeCBOR(tbl))), 92)
|
|
end
|
|
|
|
local function Decode(text)
|
|
if not text then return nil end
|
|
text = gsub(text, "%s", "")
|
|
if strsub(text, 1, strlen(MAGIC)) ~= MAGIC then return nil end
|
|
local ok, result = pcall(function()
|
|
return C_EncodingUtil.DeserializeCBOR(
|
|
C_EncodingUtil.DecompressString(
|
|
C_EncodingUtil.DecodeBase64(strsub(text, strlen(MAGIC) + 1))))
|
|
end)
|
|
if ok and type(result) == "table" then return result end
|
|
return nil
|
|
end
|
|
|
|
local function DecodeJSON(text)
|
|
if not text or gsub(text, "%s", "") == "" then return nil end
|
|
local ok, result = pcall(function()
|
|
return C_EncodingUtil.DeserializeJSON(text)
|
|
end)
|
|
if ok and type(result) == "table" then return result end
|
|
return nil
|
|
end
|
|
|
|
-- Legacy import (pre-!pf1! exports): base64 -> LZW -> Lua source
|
|
-- "pfUI_config = {...}". The base64 layer is standard and handled by
|
|
-- C_EncodingUtil.DecodeBase64; only the custom LZW format needs the old
|
|
-- Lua decoder. Import-only — new exports always use the CBOR pipeline.
|
|
local function decompress(input)
|
|
-- based on Rochet2's lzw compression
|
|
if type(input) ~= "string" or strlen(input) < 1 then
|
|
return nil
|
|
end
|
|
|
|
local control = strsub(input, 1, 1)
|
|
if control == "u" then
|
|
return strsub(input, 2)
|
|
elseif control ~= "c" then
|
|
return nil
|
|
end
|
|
input = strsub(input, 2)
|
|
local len = strlen(input)
|
|
|
|
if len < 2 then
|
|
return nil
|
|
end
|
|
|
|
local dict = {}
|
|
for i = 0, 255 do
|
|
local ic, iic = strchar(i), strchar(i, 0)
|
|
dict[iic] = ic
|
|
end
|
|
|
|
local a, b = 0, 1
|
|
|
|
local result = {}
|
|
local n = 1
|
|
local last = strsub(input, 1, 2)
|
|
result[n] = dict[last]
|
|
n = n+1
|
|
for i = 3, len, 2 do
|
|
local code = strsub(input, i, i+1)
|
|
local lastStr = dict[last]
|
|
if not lastStr then
|
|
return nil
|
|
end
|
|
local toAdd = dict[code]
|
|
if toAdd then
|
|
result[n] = toAdd
|
|
n = n+1
|
|
local str = lastStr..strsub(toAdd, 1, 1)
|
|
if a >= 256 then
|
|
a, b = 0, b+1
|
|
if b >= 256 then
|
|
dict = {}
|
|
b = 1
|
|
end
|
|
end
|
|
dict[strchar(a,b)] = str
|
|
a = a+1
|
|
else
|
|
local str = lastStr..strsub(lastStr, 1, 1)
|
|
result[n] = str
|
|
n = n+1
|
|
if a >= 256 then
|
|
a, b = 0, b+1
|
|
if b >= 256 then
|
|
dict = {}
|
|
b = 1
|
|
end
|
|
end
|
|
dict[strchar(a,b)] = str
|
|
a = a+1
|
|
end
|
|
last = code
|
|
end
|
|
return table.concat(result)
|
|
end
|
|
|
|
local function DecodeLegacy(text)
|
|
if not text or gsub(text, "%s", "") == "" then return nil end
|
|
|
|
-- encoded blob? peel base64 + LZW down to Lua source. Raw (already
|
|
-- decoded) source pastes are accepted as-is.
|
|
local source = text
|
|
local stripped = gsub(text, "%s", "")
|
|
local ok, decoded = pcall(function()
|
|
return C_EncodingUtil.DecodeBase64(stripped)
|
|
end)
|
|
if ok and decoded then
|
|
local decompressed = decompress(decoded)
|
|
if decompressed then source = decompressed end
|
|
end
|
|
|
|
local chunk = loadstring(source)
|
|
if not chunk then return nil end
|
|
|
|
-- Sandbox the chunk: an empty environment means it can assign its
|
|
-- config table but can't reach any global or API function — a legacy
|
|
-- profile string is data, never code.
|
|
local sandbox = {}
|
|
setfenv(chunk, sandbox)
|
|
if not pcall(chunk) then return nil end
|
|
if type(sandbox.pfUI_config) == "table" then return sandbox.pfUI_config end
|
|
return nil
|
|
end
|
|
|
|
do -- Window
|
|
local f = CreateFrame("Frame", "pfShare", UIParent)
|
|
f:Hide()
|
|
f:SetPoint("CENTER", 0, 0)
|
|
f:SetWidth(580)
|
|
f:SetHeight(420)
|
|
f:SetMovable(true)
|
|
f:EnableMouse(true)
|
|
f:RegisterForDrag("LeftButton")
|
|
f:SetScript("OnDragStart", function() f:StartMoving() end)
|
|
f:SetScript("OnDragStop", function() f:StopMovingOrSizing() end)
|
|
f:SetFrameStrata("DIALOG")
|
|
|
|
f:SetScript("OnShow", function()
|
|
if pfUI.gui and pfUI.gui:IsShown() then
|
|
f.hadGUI = true
|
|
pfUI.gui:Hide()
|
|
else
|
|
f.hadGUI = nil
|
|
end
|
|
end)
|
|
|
|
f:SetScript("OnHide", function()
|
|
if f.hadGUI then
|
|
pfUI.gui:Show()
|
|
end
|
|
end)
|
|
|
|
CreateBackdrop(f, nil, true, 0.8)
|
|
CreateBackdropShadow(f)
|
|
table.insert(UISpecialFrames, "pfShare")
|
|
|
|
do -- Edit Box
|
|
f.scroll = CreateFrame("Frame", "pfShareScroll", f, "pfUIScrollingEditBoxTemplate")
|
|
f.scroll:SetPoint("TOPLEFT", f, "TOPLEFT", 10, -30)
|
|
f.scroll:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -10, 50)
|
|
f.scroll:SetFont(pfUI.media["font:RobotoMono.ttf"], 9)
|
|
f.scroll:SetTextInsets(15,15,15,15)
|
|
|
|
-- the ScrollBox clips its children, so the backdrop is a sibling
|
|
f.scroll.backdrop = CreateFrame("Frame", "pfShareScrollBackdrop", f)
|
|
f.scroll.backdrop:SetPoint("TOPLEFT", f.scroll, "TOPLEFT", -5, 5)
|
|
f.scroll.backdrop:SetPoint("BOTTOMRIGHT", f.scroll, "BOTTOMRIGHT", 5, -5)
|
|
pfUI.api.CreateBackdrop(f.scroll.backdrop, nil, true)
|
|
f.scroll.backdrop.tint = f.scroll.backdrop:CreateTexture(nil, "ARTWORK")
|
|
f.scroll.backdrop.tint:SetAllPoints(f.scroll)
|
|
f.scroll.backdrop.tint:SetTexture(1,1,1,.05)
|
|
|
|
f.scroll:RegisterCallback("OnTextChanged", function()
|
|
local text = f.scroll:GetText()
|
|
local blob = Decode(text)
|
|
local json = not blob and DecodeJSON(text) or nil
|
|
local legacy = not blob and not json and DecodeLegacy(text) or nil
|
|
|
|
if blob or json or legacy then
|
|
f.loadButton:Enable()
|
|
f.loadButton.text:SetTextColor(.5,1,.5,1)
|
|
else
|
|
f.loadButton:Disable()
|
|
f.loadButton.text:SetTextColor(1,.5,.5,1)
|
|
end
|
|
|
|
if blob or legacy then
|
|
-- decoding a legacy string yields the JSON view; re-encoding it
|
|
-- from there produces a new-format blob (migration path)
|
|
f.readButton:Enable()
|
|
f.readButton.text:SetText(T["Decode"])
|
|
f.readButton.func = function()
|
|
local current = f.scroll:GetText()
|
|
local config = Decode(current) or DecodeLegacy(current)
|
|
if config then
|
|
f.scroll:SetText(C_EncodingUtil.SerializeJSON(config))
|
|
end
|
|
end
|
|
elseif json then
|
|
f.readButton:Enable()
|
|
f.readButton.text:SetText(T["Encode"])
|
|
f.readButton.func = function()
|
|
local config = DecodeJSON(f.scroll:GetText())
|
|
if config then
|
|
f.scroll:SetText(Encode(config))
|
|
end
|
|
end
|
|
else
|
|
f.readButton:Disable()
|
|
f.readButton.text:SetText(T["N/A"])
|
|
end
|
|
end, f)
|
|
end
|
|
|
|
do -- button: close
|
|
f.closeButton = CreateFrame("Button", "pfShareClose", f)
|
|
f.closeButton:SetPoint("TOPRIGHT", -5, -5)
|
|
f.closeButton:SetHeight(12)
|
|
f.closeButton:SetWidth(12)
|
|
f.closeButton.texture = f.closeButton:CreateTexture("pfQuestionDialogCloseTex")
|
|
f.closeButton.texture:SetTexture(pfUI.media["img:close"])
|
|
f.closeButton.texture:ClearAllPoints()
|
|
f.closeButton.texture:SetAllPoints(f.closeButton)
|
|
f.closeButton.texture:SetVertexColor(1,.25,.25,1)
|
|
pfUI.api.SkinButton(f.closeButton, 1, .5, .5)
|
|
f.closeButton:SetScript("OnClick", function()
|
|
this:GetParent():Hide()
|
|
end)
|
|
end
|
|
|
|
do -- checkbox: ignore positions
|
|
f.ignorePosition = CreateFrame("CheckButton", "pfShareIgnorePosition", f, "UICheckButtonTemplate")
|
|
f.ignorePosition:SetNormalTexture("")
|
|
f.ignorePosition:SetPushedTexture("")
|
|
f.ignorePosition:SetHighlightTexture("")
|
|
CreateBackdrop(f.ignorePosition, nil, true)
|
|
f.ignorePosition:SetWidth(14)
|
|
f.ignorePosition:SetHeight(14)
|
|
f.ignorePosition:SetPoint("BOTTOMLEFT", 10, 10)
|
|
|
|
f.ignorePositionCaption = f.ignorePosition:CreateFontString("Status", "LOW", "GameFontNormal")
|
|
f.ignorePositionCaption:SetFont(pfUI.font_default, C.global.font_size + 2, "OUTLINE")
|
|
f.ignorePositionCaption:SetPoint("LEFT", f.ignorePosition, "RIGHT", 5, 0)
|
|
f.ignorePositionCaption:SetFontObject(GameFontWhite)
|
|
f.ignorePositionCaption:SetJustifyH("LEFT")
|
|
f.ignorePositionCaption:SetText(T["Ignore Layout"])
|
|
end
|
|
|
|
|
|
do -- button: load
|
|
f.loadButton = CreateFrame("Button", "pfShareLoad", f)
|
|
pfUI.api.SkinButton(f.loadButton)
|
|
f.loadButton:SetPoint("BOTTOMRIGHT", -5, 5)
|
|
f.loadButton:SetWidth(75)
|
|
f.loadButton:SetHeight(25)
|
|
f.loadButton.text = f.loadButton:CreateFontString("Caption", "LOW", "GameFontWhite")
|
|
f.loadButton.text:SetAllPoints(f.loadButton)
|
|
f.loadButton.text:SetFont(pfUI.font_default, pfUI_config.global.font_size, "OUTLINE")
|
|
f.loadButton.text:SetText(T["Import"])
|
|
f.loadButton:SetScript("OnClick", function()
|
|
local text = f.scroll:GetText()
|
|
local config = Decode(text) or DecodeJSON(text) or DecodeLegacy(text)
|
|
if not config then return end
|
|
|
|
_G.pfUI_config = config
|
|
C = _G.pfUI_config
|
|
pfUI:LoadConfig()
|
|
|
|
-- Skip firstrun wizard when importing a shared profile
|
|
-- The imported config is a complete setup, no wizard needed
|
|
if pfUI.firstrun and pfUI.firstrun.steps then
|
|
for _, step in pairs(pfUI.firstrun.steps) do
|
|
pfUI_init[step.name] = true
|
|
end
|
|
end
|
|
|
|
CreateQuestionDialog(T["Some settings need to reload the UI to take effect.\nDo you want to reloadUI now?"], ReloadUI)
|
|
end)
|
|
end
|
|
|
|
do -- button: read
|
|
f.readButton = CreateFrame("Button", "pfShareDecode", f)
|
|
pfUI.api.SkinButton(f.readButton)
|
|
f.readButton:SetPoint("RIGHT", f.loadButton, "LEFT", -10, 0)
|
|
f.readButton:SetWidth(75)
|
|
f.readButton:SetHeight(25)
|
|
f.readButton.text = f.readButton:CreateFontString("Caption", "LOW", "GameFontWhite")
|
|
f.readButton.text:SetAllPoints(f.readButton)
|
|
f.readButton.text:SetFont(pfUI.font_default, pfUI_config.global.font_size, "OUTLINE")
|
|
f.readButton.text:SetText(T["N/A"])
|
|
f.readButton:SetScript("OnClick", function()
|
|
this.func()
|
|
end)
|
|
end
|
|
|
|
do -- button: export
|
|
f.exportButton = CreateFrame("Button", "pfShareExport", f)
|
|
pfUI.api.SkinButton(f.exportButton)
|
|
f.exportButton:SetPoint("RIGHT", f.readButton, "LEFT", -10, 0)
|
|
f.exportButton:SetWidth(75)
|
|
f.exportButton:SetHeight(25)
|
|
f.exportButton.text = f.exportButton:CreateFontString("Caption", "LOW", "GameFontWhite")
|
|
f.exportButton.text:SetAllPoints(f.exportButton)
|
|
f.exportButton.text:SetFont(pfUI.font_default, pfUI_config.global.font_size, "OUTLINE")
|
|
f.exportButton.text:SetText(T["Export"])
|
|
f.exportButton:SetScript("OnClick", function()
|
|
-- generate a default config
|
|
local myconfig = CopyTable(pfUI_config)
|
|
_G.pfUI_config = {}
|
|
pfUI:LoadConfig()
|
|
local defconfig = CopyTable(pfUI_config)
|
|
|
|
-- restore config and references
|
|
_G.pfUI_config = CopyTable(myconfig)
|
|
C = _G.pfUI_config
|
|
|
|
local ignored = {}
|
|
ignored["position"] = f.ignorePosition:GetChecked()
|
|
ignored["disabled"] = f.ignorePosition:GetChecked()
|
|
|
|
local encoded = Encode(DiffConfig(myconfig, defconfig, ignored) or {})
|
|
f.scroll:SetText(encoded)
|
|
end)
|
|
end
|
|
|
|
pfUI.api.RegisterSlashCommand("PFEXPORT", { "/export", "/import", "/share" }, function(msg, editbox)
|
|
f:Show()
|
|
end, true)
|
|
end
|
|
end)
|