Files
Nanami-UI/KeyBindManager.lua
rucky ec9e3c29d6 完成多出修改
修复拾取界面点击无效问题
修复宠物训练界面不显示训练点问题
新增天赋分享到聊天界面
修复飞行界面无法关闭问题
修复术士宠物的显示问题
为天赋界面添加默认数据库支持
框架现在也可自主选择是否启用
玩家框架和目标框架可取消显示3D头像以及透明度修改
背包和银行也添加透明度自定义支持
优化tooltip性能和背包物品显示方式
修复布局模式在ui缩放后不能正常定位的问题
添加硬核模式危险和死亡的工会通报
添加拾取和已拾取的框体
等等
2026-03-23 10:25:25 +08:00

634 lines
21 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

--------------------------------------------------------------------------------
-- Nanami-UI: KeyBind Manager
-- Profile-based keybinding management (save/load/delete/rename/export/import)
-- Covers ALL keybindings, not just action bars.
--------------------------------------------------------------------------------
SFrames.KeyBindManager = {}
local KBM = SFrames.KeyBindManager
--------------------------------------------------------------------------------
-- Data helpers
--------------------------------------------------------------------------------
local function EnsureDB()
if not SFramesGlobalDB then SFramesGlobalDB = {} end
if not SFramesGlobalDB.KeyBindProfiles then SFramesGlobalDB.KeyBindProfiles = {} end
end
local function GetCharName()
return UnitName("player") or "Unknown"
end
local function GetTimestamp()
return time and time() or 0
end
--------------------------------------------------------------------------------
-- Collect / Apply bindings
--------------------------------------------------------------------------------
function KBM:CollectAllBindings()
local result = {}
local n = GetNumBindings()
for i = 1, n do
local command, key1, key2 = GetBinding(i)
if command and (key1 or key2) then
table.insert(result, {
command = command,
key1 = key1,
key2 = key2,
})
end
end
return result
end
function KBM:ClearAllBindings()
local n = GetNumBindings()
for i = 1, n do
local command, key1, key2 = GetBinding(i)
if key2 then SetBinding(key2, nil) end
if key1 then SetBinding(key1, nil) end
end
end
function KBM:ApplyBindings(data)
if not data or type(data) ~= "table" then return false end
self:ClearAllBindings()
local applied = 0
for _, entry in ipairs(data) do
if entry.command then
if entry.key1 then
SetBinding(entry.key1, entry.command)
applied = applied + 1
end
if entry.key2 then
SetBinding(entry.key2, entry.command)
applied = applied + 1
end
end
end
SaveBindings(2)
if SFrames.ActionBars and SFrames.ActionBars.RefreshAllHotkeys then
SFrames.ActionBars:RefreshAllHotkeys()
end
return true, applied
end
--------------------------------------------------------------------------------
-- Base64 encode / decode (pure Lua 5.0 compatible)
--------------------------------------------------------------------------------
local B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
local function Base64Encode(src)
if not src or src == "" then return "" end
local out = {}
local len = string.len(src)
local i = 1
while i <= len do
local a = string.byte(src, i)
local b = (i + 1 <= len) and string.byte(src, i + 1) or 0
local c = (i + 2 <= len) and string.byte(src, i + 2) or 0
local remaining = len - i + 1
local n = a * 65536 + b * 256 + c
local c1 = math.floor(n / 262144)
local c2 = math.floor(math.mod(n, 262144) / 4096)
local c3 = math.floor(math.mod(n, 4096) / 64)
local c4 = math.mod(n, 64)
table.insert(out, string.sub(B64, c1 + 1, c1 + 1))
table.insert(out, string.sub(B64, c2 + 1, c2 + 1))
if remaining >= 2 then
table.insert(out, string.sub(B64, c3 + 1, c3 + 1))
else
table.insert(out, "=")
end
if remaining >= 3 then
table.insert(out, string.sub(B64, c4 + 1, c4 + 1))
else
table.insert(out, "=")
end
i = i + 3
end
return table.concat(out)
end
local B64_INV = {}
for i = 1, 64 do
B64_INV[string.sub(B64, i, i)] = i - 1
end
local function Base64Decode(src)
if not src or src == "" then return "" end
src = string.gsub(src, "%s+", "")
local out = {}
local len = string.len(src)
local i = 1
while i <= len do
local v1 = B64_INV[string.sub(src, i, i)] or 0
local v2 = B64_INV[string.sub(src, i + 1, i + 1)] or 0
local v3 = B64_INV[string.sub(src, i + 2, i + 2)]
local v4 = B64_INV[string.sub(src, i + 3, i + 3)]
local n = v1 * 262144 + v2 * 4096 + (v3 or 0) * 64 + (v4 or 0)
table.insert(out, string.char(math.floor(n / 65536)))
if v3 then
table.insert(out, string.char(math.floor(math.mod(n, 65536) / 256)))
end
if v4 then
table.insert(out, string.char(math.mod(n, 256)))
end
i = i + 4
end
return table.concat(out)
end
--------------------------------------------------------------------------------
-- Serialization (export/import text format, Base64 encoded)
--------------------------------------------------------------------------------
local EXPORT_PREFIX = "!NKB1!"
local SEP = "\t"
local function EncodeRaw(data)
local lines = {}
for _, entry in ipairs(data) do
local line = entry.command
if entry.key1 then
line = line .. SEP .. entry.key1
else
line = line .. SEP
end
if entry.key2 then
line = line .. SEP .. entry.key2
end
table.insert(lines, line)
end
return table.concat(lines, "\n")
end
local function DecodeRaw(raw)
local data = {}
for line in string.gfind(raw .. "\n", "(.-)\n") do
line = string.gsub(line, "\r", "")
if line ~= "" and string.sub(line, 1, 1) ~= "#" then
local parts = {}
for part in string.gfind(line .. SEP, "(.-)" .. SEP) do
table.insert(parts, part)
end
local command = parts[1]
if command and command ~= "" then
local key1 = parts[2]
local key2 = parts[3]
if key1 == "" then key1 = nil end
if key2 == "" then key2 = nil end
if key1 or key2 then
table.insert(data, {
command = command,
key1 = key1,
key2 = key2,
})
end
end
end
end
return data
end
function KBM:SerializeBindings(data)
if not data then data = self:CollectAllBindings() end
local raw = EncodeRaw(data)
return EXPORT_PREFIX .. Base64Encode(raw)
end
function KBM:DeserializeBindings(text)
if not text or text == "" then return nil, "文本为空" end
text = string.gsub(text, "^%s+", "")
text = string.gsub(text, "%s+$", "")
if string.len(text) == 0 then return nil, "文本为空" end
local raw
if string.sub(text, 1, string.len(EXPORT_PREFIX)) == EXPORT_PREFIX then
local encoded = string.sub(text, string.len(EXPORT_PREFIX) + 1)
raw = Base64Decode(encoded)
if not raw or raw == "" then return nil, "解码失败" end
else
raw = text
end
local data = DecodeRaw(raw)
if table.getn(data) == 0 then return nil, "未找到有效的绑定数据" end
return data
end
--------------------------------------------------------------------------------
-- Profile CRUD
--------------------------------------------------------------------------------
function KBM:SaveProfile(name)
if not name or name == "" then return false, "方案名不能为空" end
EnsureDB()
local data = self:CollectAllBindings()
SFramesGlobalDB.KeyBindProfiles[name] = {
timestamp = GetTimestamp(),
charName = GetCharName(),
bindings = data,
}
SFrames:Print("按键绑定方案已保存: |cffffd100" .. name .. "|r (" .. table.getn(data) .. " 条)")
return true
end
function KBM:LoadProfile(name)
if not name or name == "" then return false, "方案名不能为空" end
EnsureDB()
local profile = SFramesGlobalDB.KeyBindProfiles[name]
if not profile then return false, "方案不存在: " .. name end
local ok, count = self:ApplyBindings(profile.bindings)
if ok then
SFrames:Print("按键绑定方案已加载: |cffffd100" .. name .. "|r (" .. count .. " 个按键)")
end
return ok
end
function KBM:DeleteProfile(name)
if not name or name == "" then return false end
EnsureDB()
if not SFramesGlobalDB.KeyBindProfiles[name] then return false end
SFramesGlobalDB.KeyBindProfiles[name] = nil
SFrames:Print("按键绑定方案已删除: |cffffd100" .. name .. "|r")
return true
end
function KBM:RenameProfile(oldName, newName)
if not oldName or oldName == "" or not newName or newName == "" then
return false, "名称不能为空"
end
EnsureDB()
local profiles = SFramesGlobalDB.KeyBindProfiles
if not profiles[oldName] then return false, "方案不存在" end
if profiles[newName] then return false, "目标名称已存在" end
profiles[newName] = profiles[oldName]
profiles[oldName] = nil
SFrames:Print("按键绑定方案已重命名: |cffffd100" .. oldName .. "|r -> |cffffd100" .. newName .. "|r")
return true
end
function KBM:GetProfileList()
EnsureDB()
local list = {}
for name, _ in pairs(SFramesGlobalDB.KeyBindProfiles) do
table.insert(list, name)
end
table.sort(list)
return list
end
function KBM:GetProfileInfo(name)
EnsureDB()
local p = SFramesGlobalDB.KeyBindProfiles[name]
if not p then return nil end
return {
name = name,
charName = p.charName or "?",
timestamp = p.timestamp or 0,
count = p.bindings and table.getn(p.bindings) or 0,
}
end
--------------------------------------------------------------------------------
-- Export / Import Dialog UI
--------------------------------------------------------------------------------
local exportFrame, importFrame
local function CreateDialogFrame(name, titleText, width, height)
local f = CreateFrame("Frame", name, UIParent)
f:SetWidth(width)
f:SetHeight(height)
f:SetPoint("CENTER", UIParent, "CENTER", 0, 80)
f:SetFrameStrata("TOOLTIP")
f:SetFrameLevel(200)
f:SetToplevel(true)
f:EnableMouse(true)
f:SetMovable(true)
f:SetClampedToScreen(true)
f:RegisterForDrag("LeftButton")
f:SetScript("OnDragStart", function() this:StartMoving() end)
f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
if SFrames and SFrames.CreateBackdrop then
SFrames:CreateBackdrop(f)
else
f:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = false, edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 },
})
f:SetBackdropColor(0.08, 0.07, 0.1, 0.96)
f:SetBackdropBorderColor(0.4, 0.4, 0.4, 0.9)
end
local font = (SFrames and SFrames.GetFont) and SFrames:GetFont() or "Fonts\\ARIALN.TTF"
local title = f:CreateFontString(nil, "OVERLAY")
title:SetFont(font, 13, "OUTLINE")
title:SetPoint("TOP", f, "TOP", 0, -10)
title:SetText(titleText)
local _T = SFrames.ActiveTheme
if _T and _T.title then
title:SetTextColor(_T.title[1], _T.title[2], _T.title[3])
else
title:SetTextColor(1, 0.82, 0)
end
f.title = title
local close = CreateFrame("Button", nil, f, "UIPanelCloseButton")
close:SetPoint("TOPRIGHT", f, "TOPRIGHT", -2, -2)
close:SetWidth(20)
close:SetHeight(20)
return f, font
end
local function CreateThemedButton(parent, text, x, y, width, height, onClick)
local name = "SFramesKBM_Btn_" .. string.gsub(text, "%s", "") .. "_" .. tostring(math.random(10000, 99999))
local btn = CreateFrame("Button", name, parent, "UIPanelButtonTemplate")
btn:SetWidth(width)
btn:SetHeight(height)
btn:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
btn:SetText(text)
btn:SetScript("OnClick", onClick)
local font = (SFrames and SFrames.GetFont) and SFrames:GetFont() or "Fonts\\ARIALN.TTF"
local _T = SFrames.ActiveTheme
if not _T then return btn end
local function HideBtnTex(tex)
if not tex then return end
if tex.SetTexture then tex:SetTexture(nil) end
if tex.SetAlpha then tex:SetAlpha(0) end
if tex.Hide then tex:Hide() end
end
HideBtnTex(btn:GetNormalTexture())
HideBtnTex(btn:GetPushedTexture())
HideBtnTex(btn:GetHighlightTexture())
HideBtnTex(btn:GetDisabledTexture())
for _, sfx in ipairs({"Left","Right","Middle"}) do
local t = _G[name .. sfx]
if t then t:SetAlpha(0); t:Hide() end
end
btn:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 14,
insets = { left = 3, right = 3, top = 3, bottom = 3 },
})
btn:SetBackdropColor(_T.btnBg[1], _T.btnBg[2], _T.btnBg[3], _T.btnBg[4])
btn:SetBackdropBorderColor(_T.btnBorder[1], _T.btnBorder[2], _T.btnBorder[3], _T.btnBorder[4])
local fs = btn:GetFontString()
if fs then
fs:SetFont(font, 11, "OUTLINE")
fs:SetTextColor(_T.btnText[1], _T.btnText[2], _T.btnText[3])
end
btn:SetScript("OnEnter", function()
this:SetBackdropColor(_T.btnHoverBg[1], _T.btnHoverBg[2], _T.btnHoverBg[3], _T.btnHoverBg[4])
this:SetBackdropBorderColor(_T.btnHoverBd[1], _T.btnHoverBd[2], _T.btnHoverBd[3], _T.btnHoverBd[4])
local t = this:GetFontString()
if t then t:SetTextColor(_T.btnActiveText[1], _T.btnActiveText[2], _T.btnActiveText[3]) end
end)
btn:SetScript("OnLeave", function()
this:SetBackdropColor(_T.btnBg[1], _T.btnBg[2], _T.btnBg[3], _T.btnBg[4])
this:SetBackdropBorderColor(_T.btnBorder[1], _T.btnBorder[2], _T.btnBorder[3], _T.btnBorder[4])
local t = this:GetFontString()
if t then t:SetTextColor(_T.btnText[1], _T.btnText[2], _T.btnText[3]) end
end)
btn:SetScript("OnMouseDown", function()
this:SetBackdropColor(_T.btnDownBg[1], _T.btnDownBg[2], _T.btnDownBg[3], _T.btnDownBg[4])
end)
btn:SetScript("OnMouseUp", function()
this:SetBackdropColor(_T.btnHoverBg[1], _T.btnHoverBg[2], _T.btnHoverBg[3], _T.btnHoverBg[4])
end)
return btn
end
function KBM:ShowExportDialog()
local text = self:SerializeBindings()
if not exportFrame then
local f, font = CreateDialogFrame("SFramesKBMExport", "|cffffcc00导出按键绑定|r", 480, 340)
local desc = f:CreateFontString(nil, "OVERLAY")
desc:SetFont(font, 10, "OUTLINE")
desc:SetPoint("TOP", f.title, "BOTTOM", 0, -4)
desc:SetText("已编码为字符串Ctrl+A 全选Ctrl+C 复制")
desc:SetTextColor(0.7, 0.7, 0.7)
local sf = CreateFrame("ScrollFrame", "SFramesKBMExportScroll", f, "UIPanelScrollFrameTemplate")
sf:SetPoint("TOPLEFT", f, "TOPLEFT", 12, -48)
sf:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -32, 42)
local edit = CreateFrame("EditBox", "SFramesKBMExportEdit", sf)
edit:SetWidth(420)
edit:SetHeight(200)
edit:SetMultiLine(true)
edit:SetFont(font, 10, "OUTLINE")
edit:SetAutoFocus(false)
edit:SetScript("OnEscapePressed", function() f:Hide() end)
sf:SetScrollChild(edit)
f.edit = edit
CreateThemedButton(f, "关闭", 190, -306, 100, 26, function()
f:Hide()
end)
exportFrame = f
end
exportFrame.edit:SetText(text)
exportFrame:Show()
exportFrame.edit:HighlightText()
exportFrame.edit:SetFocus()
end
function KBM:ShowImportDialog()
if not importFrame then
local f, font = CreateDialogFrame("SFramesKBMImport", "|cffffcc00导入按键绑定|r", 480, 370)
local desc = f:CreateFontString(nil, "OVERLAY")
desc:SetFont(font, 10, "OUTLINE")
desc:SetPoint("TOP", f.title, "BOTTOM", 0, -4)
desc:SetText("将编码后的字符串粘贴到下方 (Ctrl+V),然后点击导入")
desc:SetTextColor(0.7, 0.7, 0.7)
local sf = CreateFrame("ScrollFrame", "SFramesKBMImportScroll", f, "UIPanelScrollFrameTemplate")
sf:SetPoint("TOPLEFT", f, "TOPLEFT", 12, -48)
sf:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -32, 72)
local edit = CreateFrame("EditBox", "SFramesKBMImportEdit", sf)
edit:SetWidth(420)
edit:SetHeight(200)
edit:SetMultiLine(true)
edit:SetFont(font, 10, "OUTLINE")
edit:SetAutoFocus(false)
edit:SetScript("OnEscapePressed", function() f:Hide() end)
sf:SetScrollChild(edit)
f.edit = edit
local statusLabel = f:CreateFontString(nil, "OVERLAY")
statusLabel:SetFont(font, 10, "OUTLINE")
statusLabel:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 14, 46)
statusLabel:SetWidth(300)
statusLabel:SetJustifyH("LEFT")
statusLabel:SetText("")
f.statusLabel = statusLabel
CreateThemedButton(f, "导入并应用", 120, -336, 120, 26, function()
local inputText = f.edit:GetText()
if not inputText or inputText == "" then
f.statusLabel:SetText("|cffff4444请先粘贴绑定数据|r")
return
end
local data, err = KBM:DeserializeBindings(inputText)
if not data then
f.statusLabel:SetText("|cffff4444错误: " .. (err or "未知") .. "|r")
return
end
local ok, count = KBM:ApplyBindings(data)
if ok then
f.statusLabel:SetText("|cff44ff44成功导入 " .. count .. " 个按键绑定|r")
SFrames:Print("已从文本导入 " .. count .. " 个按键绑定")
else
f.statusLabel:SetText("|cffff4444导入失败|r")
end
end)
CreateThemedButton(f, "取消", 250, -336, 100, 26, function()
f:Hide()
end)
importFrame = f
end
importFrame.edit:SetText("")
importFrame.statusLabel:SetText("")
importFrame:Show()
importFrame.edit:SetFocus()
end
--------------------------------------------------------------------------------
-- Input name dialog (for save / rename)
--------------------------------------------------------------------------------
local inputDialog
local function ShowInputDialog(titleText, defaultText, callback)
if not inputDialog then
local f, font = CreateDialogFrame("SFramesKBMInput", "", 360, 120)
local edit = CreateFrame("EditBox", "SFramesKBMInputEdit", f)
edit:SetWidth(320)
edit:SetHeight(24)
edit:SetPoint("TOP", f, "TOP", 0, -40)
edit:SetFont(font, 12, "OUTLINE")
edit:SetAutoFocus(true)
edit:SetMaxLetters(50)
edit:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 10,
insets = { left = 3, right = 3, top = 3, bottom = 3 },
})
edit:SetBackdropColor(0.1, 0.1, 0.1, 0.9)
edit:SetBackdropBorderColor(0.5, 0.5, 0.5, 0.8)
edit:SetTextInsets(6, 6, 0, 0)
f.edit = edit
CreateThemedButton(f, "确定", 90, -78, 80, 24, function()
local val = f.edit:GetText()
if val and val ~= "" and f.callback then
f.callback(val)
end
f:Hide()
end)
CreateThemedButton(f, "取消", 185, -78, 80, 24, function()
f:Hide()
end)
edit:SetScript("OnEscapePressed", function() f:Hide() end)
edit:SetScript("OnEnterPressed", function()
local val = f.edit:GetText()
if val and val ~= "" and f.callback then
f.callback(val)
end
f:Hide()
end)
inputDialog = f
end
inputDialog.title:SetText(titleText)
inputDialog.edit:SetText(defaultText or "")
inputDialog.callback = callback
inputDialog:Show()
inputDialog.edit:SetFocus()
inputDialog.edit:HighlightText()
end
--------------------------------------------------------------------------------
-- Confirm dialog
--------------------------------------------------------------------------------
local confirmDialog
local function ShowConfirmDialog(message, callback)
if not confirmDialog then
local f, font = CreateDialogFrame("SFramesKBMConfirm", "", 360, 110)
local msg = f:CreateFontString(nil, "OVERLAY")
msg:SetFont(font, 11, "OUTLINE")
msg:SetPoint("TOP", f, "TOP", 0, -34)
msg:SetWidth(320)
msg:SetJustifyH("CENTER")
msg:SetTextColor(0.9, 0.9, 0.9)
f.msg = msg
CreateThemedButton(f, "确定", 90, -70, 80, 24, function()
if f.callback then f.callback() end
f:Hide()
end)
CreateThemedButton(f, "取消", 185, -70, 80, 24, function()
f:Hide()
end)
confirmDialog = f
end
confirmDialog.title:SetText("|cffffcc00确认|r")
confirmDialog.msg:SetText(message)
confirmDialog.callback = callback
confirmDialog:Show()
end
KBM.ShowInputDialog = ShowInputDialog
KBM.ShowConfirmDialog = ShowConfirmDialog