diff --git a/Core/MultiBotConfig.lua b/Core/MultiBotConfig.lua index e208cd8..840778f 100644 --- a/Core/MultiBotConfig.lua +++ b/Core/MultiBotConfig.lua @@ -17,6 +17,12 @@ local THROTTLE_DEFAULTS = { burst = 8, } +local UI_DEFAULTS = { + mainBar = { + moveLocked = true, + }, +} + local DB_DEFAULTS = { profile = { timers = { @@ -29,6 +35,11 @@ local DB_DEFAULTS = { rate = THROTTLE_DEFAULTS.rate, burst = THROTTLE_DEFAULTS.burst, }, + ui = { + mainBar = { + moveLocked = UI_DEFAULTS.mainBar.moveLocked, + }, + }, }, } @@ -62,6 +73,12 @@ local function migrateLegacyConfigIntoProfile(profile) profile.throttle[key] = defaultValue end end + + profile.ui = profile.ui or {} + profile.ui.mainBar = profile.ui.mainBar or {} + if type(profile.ui.mainBar.moveLocked) ~= "boolean" then + profile.ui.mainBar.moveLocked = UI_DEFAULTS.mainBar.moveLocked + end end local function getConfigStore(createIfMissing) @@ -117,6 +134,12 @@ function MultiBot.Config_Ensure() if type(config.throttle.burst) ~= "number" or config.throttle.burst <= 0 then config.throttle.burst = THROTTLE_DEFAULTS.burst end + + config.ui = config.ui or {} + config.ui.mainBar = config.ui.mainBar or {} + if type(config.ui.mainBar.moveLocked) ~= "boolean" then + config.ui.mainBar.moveLocked = UI_DEFAULTS.mainBar.moveLocked + end end -- Copy saved values into runtime timers. @@ -212,3 +235,21 @@ function MultiBot.SetThrottleBurst(value) MultiBot._ThrottleStats(MultiBot.GetThrottleRate(), config.throttle.burst) end end + +function MultiBot.GetMainBarMoveLocked() + local config = getConfigStore(false) + local value = config and config.ui and config.ui.mainBar and config.ui.mainBar.moveLocked + if type(value) == "boolean" then + return value + end + + return UI_DEFAULTS.mainBar.moveLocked +end + +function MultiBot.SetMainBarMoveLocked(value) + local config = getConfigStore(true) + config.ui = config.ui or {} + config.ui.mainBar = config.ui.mainBar or {} + config.ui.mainBar.moveLocked = value and true or false + return config.ui.mainBar.moveLocked +end \ No newline at end of file diff --git a/Core/MultiBotEngine.lua b/Core/MultiBotEngine.lua index 71a9dd6..7cc8985 100644 --- a/Core/MultiBotEngine.lua +++ b/Core/MultiBotEngine.lua @@ -267,13 +267,18 @@ MultiBot.toTip = function(pClass, pLevel, pName) end MultiBot.toPoint = function(pFrame) - -- Mesurer par rapport au parent global stable et arrondir à l’unité. - local uiRight = (UIParent and UIParent:GetRight()) or GetScreenWidth() - local xRight = pFrame:GetRight() or 0 - local yBottom = pFrame:GetBottom() or 0 - -- Offset vers BOTTOMRIGHT (négatif ou nul) - local offX = xRight - uiRight - local offY = yBottom + if not pFrame then + return 0, 0 + end + -- Mesurer par rapport au parent global stable et arrondir à l’unité. + local uiRight = (UIParent and UIParent:GetRight()) or GetScreenWidth() + local getRight = pFrame.GetRight or pFrame.getRight + local getBottom = pFrame.GetBottom or pFrame.getBottom + local xRight = (type(getRight) == "function" and getRight(pFrame)) or 0 + local yBottom = (type(getBottom) == "function" and getBottom(pFrame)) or 0 + -- Offset vers BOTTOMRIGHT (négatif ou nul) + local offX = xRight - uiRight + local offY = yBottom -- Arrondi au plus proche pour éviter la dérive cumulée return math.floor(offX + 0.5), math.floor(offY + 0.5) end @@ -1307,6 +1312,307 @@ MultiBot.boxButton = function(pParent, pX, pY, pSize, pState) return button; end +-- BUTTON REORDER (SHIFT + RIGHT CLICK) -- + +local function _mbParseButtonLayout(raw) + local parsed = {} + if(type(raw) ~= "string" or raw == "") then + return parsed + end + + for token in string.gmatch(raw, "([^;]+)") do + local name, x, y = string.match(token, "^([^:]+):(-?%d+),(-?%d+)$") + if(name and x and y) then + parsed[name] = { x = tonumber(x), y = tonumber(y) } + end + end + + return parsed +end + +local function _mbSerializeButtonLayout(entries) + local chunks = {} + for _, entry in ipairs(entries) do + local button = entry.button + if(button and type(button.x) == "number" and type(button.y) == "number") then + table.insert(chunks, string.format("%s:%d,%d", entry.id or entry.name, button.x, button.y)) + end + end + return table.concat(chunks, ";") +end + +local function _mbApplyLinkedFrameOffset(entry) + if(not entry or not entry.frameName) then + return + end + + local button = entry.button + local frame = entry.frame + if(not button or not frame or not frame.setPoint) then + return + end + + local offsetX = entry.frameOffsetX or 0 + local offsetY = entry.frameOffsetY or 0 + frame.setPoint(button.x + offsetX, button.y + offsetY) +end + +function MultiBot.BindShiftRightSwapButtons(host, contextKey, entries) + if(not host or not contextKey or type(entries) ~= "table") then + return nil + end + + MultiBot._mbShiftSwapGlobal = MultiBot._mbShiftSwapGlobal or {} + MultiBot._mbRegisteredButtonLayoutKeys = MultiBot._mbRegisteredButtonLayoutKeys or {} + MultiBot._mbRegisteredButtonLayoutKeys["ButtonLayout:" .. contextKey] = true + local state = MultiBot._mbShiftSwapGlobal[contextKey] + if(not state) then + local saveKey = "ButtonLayout:" .. contextKey + local saved = MultiBot.GetSavedLayoutValue and MultiBot.GetSavedLayoutValue(saveKey) or nil + state = { + selected = nil, + saveKey = saveKey, + parsed = _mbParseButtonLayout(saved), + entries = {}, + byName = {}, + } + MultiBot._mbShiftSwapGlobal[contextKey] = state + end + + local function persist() + if(MultiBot.SetSavedLayoutValue) then + MultiBot.SetSavedLayoutValue(state.saveKey, _mbSerializeButtonLayout(state.entries)) + end + end + + local function paintEntry(entryRec, r, g, b, a) + local button = entryRec and entryRec.button + if(not button) then + return + end + + if(button.icon and button.icon.SetVertexColor) then + button.icon:SetVertexColor(r or 1, g or 1, b or 1) + end + if(button.SetAlpha) then + button:SetAlpha(a or 1) + end + end + + local function clearVisualState(entryRec) + paintEntry(entryRec, 1, 1, 1, 1) + end + + local function applySelectionVisuals() + for _, entryRec in ipairs(state.entries or {}) do + clearVisualState(entryRec) + end + + if(state.selected) then + paintEntry(state.selected, 1, 0.85, 0.35, 1) + end + + if(state.hovered and state.hovered ~= state.selected) then + paintEntry(state.hovered, 0.6, 1, 0.6, 1) + if(state.selected) then + paintEntry(state.selected, 1, 0.85, 0.35, 0.9) + end + end + end + + local function clearSelectionState() + state.selected = nil + state.hovered = nil + state.previewTarget = nil + applySelectionVisuals() + end + + local function swapButtons(entryA, entryB) + local buttonA = entryA and entryA.button + local buttonB = entryB and entryB.button + if(not buttonA or not buttonB or not buttonA.setPoint or not buttonB.setPoint) then + return + end + + local absARight, absABottom = buttonA:GetRight(), buttonA:GetBottom() + local absBRight, absBBottom = buttonB:GetRight(), buttonB:GetBottom() + if(not absARight or not absABottom or not absBRight or not absBBottom) then + return + end + + local parentA = buttonA:GetParent() + local parentB = buttonB:GetParent() + if(not parentA or not parentB) then + return + end + + local parentARight, parentABottom = parentA:GetRight(), parentA:GetBottom() + local parentBRight, parentBBottom = parentB:GetRight(), parentB:GetBottom() + if(not parentARight or not parentABottom or not parentBRight or not parentBBottom) then + return + end + + local newAX, newAY = absBRight - parentARight, absBBottom - parentABottom + local newBX, newBY = absARight - parentBRight, absABottom - parentBBottom + buttonA.setPoint(newAX, newAY) + buttonB.setPoint(newBX, newBY) + + _mbApplyLinkedFrameOffset(entryA) + _mbApplyLinkedFrameOffset(entryB) + persist() + end + + local function wrapButton(entryRec) + local button = entryRec and entryRec.button + if(not button) then + return + end + + local originalDoRight = button.doRight + button.doRight = function(btn) + if(IsShiftKeyDown()) then + if(state.selected == nil) then + state.selected = entryRec + state.hovered = nil + state.previewTarget = nil + applySelectionVisuals() + if(UIErrorsFrame) then + UIErrorsFrame:AddMessage(MultiBot.L("ui.swap.source_prefix") .. (entryRec.id or entryRec.name), 1, 0.82, 0, 1) + end + return + end + + if(state.selected == entryRec) then + clearSelectionState() + if(UIErrorsFrame) then + UIErrorsFrame:AddMessage(MultiBot.L("ui.swap.cancelled"), 1, 0.25, 0.25, 1) + end + return + end + + local sourceEntry = state.selected + clearSelectionState() + swapButtons(sourceEntry, entryRec) + if(UIErrorsFrame) then + UIErrorsFrame:AddMessage((sourceEntry.id or sourceEntry.name) .. " <-> " .. (entryRec.id or entryRec.name), 0.25, 1, 0.25, 1) + end + return + end + + if(originalDoRight) then + originalDoRight(btn) + end + end + + button._mbSwapWrapped = true + button:HookScript("OnEnter", function() + if(state.selected and state.selected ~= entryRec) then + state.hovered = entryRec + applySelectionVisuals() + if(UIErrorsFrame and state.previewTarget ~= entryRec) then + state.previewTarget = entryRec + UIErrorsFrame:AddMessage(MultiBot.L("ui.swap.preview_prefix") .. (state.selected.id or state.selected.name) .. " <-> " .. (entryRec.id or entryRec.name), 1, 1, 0.4, 1) + end + end + end) + button:HookScript("OnLeave", function() + if(state.hovered == entryRec) then + state.hovered = nil + applySelectionVisuals() + end + end) + end + + for _, entry in ipairs(entries) do + local id = entry and (entry.id or entry.name) or nil + if(id and not state.byName[id]) then + state.byName[id] = true + + local button = host.buttons and host.buttons[entry.name] + local frame = entry.frameName and host.frames and host.frames[entry.frameName] or nil + local entryRec = { + id = id, + name = entry.name, + frameName = entry.frameName, + button = button, + frame = frame, + defaultX = button and button.x or nil, + defaultY = button and button.y or nil, + } + table.insert(state.entries, entryRec) + + if(button and frame and type(frame.x) == "number" and type(frame.y) == "number") then + entryRec.frameOffsetX = frame.x - button.x + entryRec.frameOffsetY = frame.y - button.y + end + + local savedPoint = state.parsed and state.parsed[id] + if(button and savedPoint and button.setPoint) then + button.setPoint(savedPoint.x, savedPoint.y) + end + _mbApplyLinkedFrameOffset(entryRec) + wrapButton(entryRec) + end + end + + return state +end + +function MultiBot.ResetButtonLayoutContext(contextKey, clearPersistedValue) + if(not contextKey or not MultiBot._mbShiftSwapGlobal) then + return false + end + + local state = MultiBot._mbShiftSwapGlobal[contextKey] + if(not state) then + return false + end + + for _, entryRec in ipairs(state.entries or {}) do + local button = entryRec and entryRec.button + local defaultX = entryRec and entryRec.defaultX + local defaultY = entryRec and entryRec.defaultY + if(button and button.setPoint and type(defaultX) == "number" and type(defaultY) == "number") then + button.setPoint(defaultX, defaultY) + end + _mbApplyLinkedFrameOffset(entryRec) + end + + if(clearPersistedValue and MultiBot.SetSavedLayoutValue) then + MultiBot.SetSavedLayoutValue(state.saveKey, nil) + end + + state.parsed = {} + state.selected = nil + return true +end + +function MultiBot.ApplySavedButtonLayout(contextKey) + if(not contextKey or not MultiBot._mbShiftSwapGlobal) then + return false + end + + local state = MultiBot._mbShiftSwapGlobal[contextKey] + if(not state) then + return false + end + + local raw = MultiBot.GetSavedLayoutValue and MultiBot.GetSavedLayoutValue(state.saveKey) or nil + state.parsed = _mbParseButtonLayout(raw) + + for _, entryRec in ipairs(state.entries or {}) do + local button = entryRec and entryRec.button + local id = entryRec and (entryRec.id or entryRec.name) + local savedPoint = id and state.parsed and state.parsed[id] or nil + if(button and savedPoint and button.setPoint) then + button.setPoint(savedPoint.x, savedPoint.y) + end + _mbApplyLinkedFrameOffset(entryRec) + end + + return true +end + -- BUTTON:CAT -- MultiBot.catButton = function(pParent, pX, pY, pWidth, pHeight) diff --git a/Core/MultiBotHandler.lua b/Core/MultiBotHandler.lua index 63f7c05..1783d27 100644 --- a/Core/MultiBotHandler.lua +++ b/Core/MultiBotHandler.lua @@ -254,6 +254,286 @@ MultiBot.SetSavedLayoutValue = function(key, value) return setSavedLayoutValue(key, value) end +local LAYOUT_EXPORT_VERSION = "MBLAYOUT1" + +local function getPlayerLayoutOwnerKey() + local playerName = UnitName and UnitName("player") or nil + local realmName = GetRealmName and GetRealmName() or nil + if type(playerName) ~= "string" or playerName == "" then + playerName = "UnknownPlayer" + end + if type(realmName) ~= "string" or realmName == "" then + return playerName + end + return playerName .. "-" .. realmName +end + +local function getGlobalLayoutLibrary(createIfMissing) + local globalSave = _G.MultiBotGlobalSave + if type(globalSave) ~= "table" then + if not createIfMissing then + return nil + end + globalSave = {} + _G.MultiBotGlobalSave = globalSave + end + + if createIfMissing then + globalSave.savedLayoutsByPlayer = globalSave.savedLayoutsByPlayer or {} + + local db = MultiBot.db + local legacyStore = db and db.global and db.global.ui and db.global.ui.savedLayoutsByPlayer or nil + if type(legacyStore) == "table" then + for ownerKey, payload in pairs(legacyStore) do + if type(ownerKey) == "string" and type(payload) == "string" and payload ~= "" and globalSave.savedLayoutsByPlayer[ownerKey] == nil then + globalSave.savedLayoutsByPlayer[ownerKey] = payload + end + end + end + end + return globalSave.savedLayoutsByPlayer +end + +local function encodePayloadValue(value) + return (tostring(value):gsub(".", function(ch) + return string.format("%02X", string.byte(ch)) + end)) +end + +local function decodePayloadValue(value) + if type(value) ~= "string" or value == "" or (string.len(value) % 2) ~= 0 then + return nil + end + + local chunks = {} + for i = 1, string.len(value), 2 do + local byteHex = string.sub(value, i, i + 1) + local byte = tonumber(byteHex, 16) + if not byte then + return nil + end + chunks[#chunks + 1] = string.char(byte) + end + return table.concat(chunks) +end + +local function shouldExportLayoutKey(key) + return type(key) == "string" and (key == "MultiBarPoint" or string.find(key, "^ButtonLayout:") ~= nil) +end + +local function collectLayoutExportEntries() + local entries = {} + local profileStore = getLayoutProfileStore() + if profileStore then + migrateLegacyLayoutStateIfNeeded(profileStore) + for key, value in pairs(profileStore) do + if shouldExportLayoutKey(key) and type(value) == "string" and value ~= "" then + entries[key] = value + end + end + end + + local registered = MultiBot._mbRegisteredButtonLayoutKeys or {} + for key in pairs(registered) do + if shouldExportLayoutKey(key) and entries[key] == nil then + local value = getSavedLayoutValue(key) + if type(value) == "string" and value ~= "" then + entries[key] = value + end + end + end + + if entries["MultiBarPoint"] == nil then + local pointValue = getSavedLayoutValue("MultiBarPoint") + if type(pointValue) == "string" and pointValue ~= "" then + entries["MultiBarPoint"] = pointValue + end + end + + return entries +end + +local function sortedKeysOf(map) + local keys = {} + for key in pairs(map or {}) do + keys[#keys + 1] = key + end + table.sort(keys) + return keys +end + +function MultiBot.ExportMainBarLayoutPayload() + local payloadParts = { LAYOUT_EXPORT_VERSION } + local moveLocked = MultiBot.GetMainBarMoveLocked and MultiBot.GetMainBarMoveLocked() and "1" or "0" + payloadParts[#payloadParts + 1] = "mainBarMoveLocked=" .. encodePayloadValue(moveLocked) + + local entries = collectLayoutExportEntries() + for _, key in ipairs(sortedKeysOf(entries)) do + payloadParts[#payloadParts + 1] = encodePayloadValue(key) .. "=" .. encodePayloadValue(entries[key]) + end + + return table.concat(payloadParts, "|") +end + +function MultiBot.SaveMainBarLayoutForCurrentPlayer() + local payload = MultiBot.ExportMainBarLayoutPayload() + local ownerKey = getPlayerLayoutOwnerKey() + local store = getGlobalLayoutLibrary(true) + if not store then + return false, "store_global_indisponible" + end + store[ownerKey] = payload + return true, ownerKey, payload +end + +function MultiBot.GetSavedMainBarLayoutOwners() + local store = getGlobalLayoutLibrary(false) + local owners = {} + for ownerKey, payload in pairs(store or {}) do + if type(ownerKey) == "string" and type(payload) == "string" and payload ~= "" then + owners[#owners + 1] = ownerKey + end + end + table.sort(owners) + return owners +end + +function MultiBot.GetSavedMainBarLayoutPayload(ownerKey) + if type(ownerKey) ~= "string" or ownerKey == "" then + return nil + end + local store = getGlobalLayoutLibrary(false) + local payload = store and store[ownerKey] or nil + if type(payload) ~= "string" or payload == "" then + return nil + end + return payload +end + +function MultiBot.ImportSavedMainBarLayout(ownerKey) + local payload = MultiBot.GetSavedMainBarLayoutPayload(ownerKey) + if not payload then + return false, "layout_introuvable" + end + return MultiBot.ImportMainBarLayoutPayload(payload) +end + +function MultiBot.DeleteSavedMainBarLayout(ownerKey) + if type(ownerKey) ~= "string" or ownerKey == "" then + return false, "owner_invalide" + end + local store = getGlobalLayoutLibrary(true) + if not store or store[ownerKey] == nil then + return false, "layout_introuvable" + end + store[ownerKey] = nil + return true +end + +local function isMainBarLayoutKey(key) + return type(key) == "string" and (key == "MultiBarPoint" or string.find(key, "^ButtonLayout:") ~= nil) +end + +function MultiBot.ResetMainBarLayoutState() + local removed = 0 + local profileStore = getLayoutProfileStore() + if profileStore then + migrateLegacyLayoutStateIfNeeded(profileStore) + for key in pairs(profileStore) do + if isMainBarLayoutKey(key) then + profileStore[key] = nil + removed = removed + 1 + end + end + end + + local legacy = getLegacyStateStore(false) + if type(legacy) == "table" then + for key in pairs(legacy) do + if isMainBarLayoutKey(key) then + legacy[key] = nil + end + end + end + + if MultiBot._mbShiftSwapGlobal and MultiBot.ResetButtonLayoutContext then + for contextKey in pairs(MultiBot._mbShiftSwapGlobal) do + MultiBot.ResetButtonLayoutContext(contextKey, false) + end + end + + local multiBar = MultiBot.frames and MultiBot.frames["MultiBar"] + if multiBar and multiBar.setPoint then + multiBar.setPoint(-262, 144) + end + + return true, removed +end + +local function applyImportedLayoutEntry(key, value) + if key == "mainBarMoveLocked" then + if MultiBot.SetMainBarMoveLocked then + MultiBot.SetMainBarMoveLocked(value == "1") + end + return true + end + + if not shouldExportLayoutKey(key) then + return false + end + + setSavedLayoutValue(key, value) + if key == "MultiBarPoint" then + local multibar = MultiBot.frames and MultiBot.frames["MultiBar"] + if multibar and multibar.setPoint and MultiBot.doSplit then + local split = MultiBot.doSplit(value, ", ") + multibar.setPoint(tonumber(split[1]), tonumber(split[2])) + end + return true + end + + local context = string.match(key, "^ButtonLayout:(.+)$") + if context and MultiBot.ApplySavedButtonLayout then + MultiBot.ApplySavedButtonLayout(context) + end + return true +end + +function MultiBot.ImportMainBarLayoutPayload(payload) + if type(payload) ~= "string" or payload == "" then + return false, "payload_vide" + end + + local tokens = {} + for token in string.gmatch(payload, "([^|]+)") do + tokens[#tokens + 1] = token + end + if tokens[1] ~= LAYOUT_EXPORT_VERSION then + return false, "version_invalide" + end + + local imported = 0 + for index = 2, #tokens do + local token = tokens[index] + local left, right = string.match(token, "^([^=]+)=(.+)$") + if left and right then + local key = left + if key ~= "mainBarMoveLocked" then + key = decodePayloadValue(left) + end + local value = decodePayloadValue(right) + if key and value and applyImportedLayoutEntry(key, value) then + imported = imported + 1 + end + end + end + + if imported == 0 then + return false, "aucune_donnee_importee" + end + return true, imported +end + -- HANDLER -- @@ -1251,7 +1531,7 @@ function MultiBot.HandleMultiBotEvent(event, ...) -- On ne traite que les réponses commençant par "Glyphs:" ou "No glyphs" if not rawMsg:match("^[Gg]lyphs:") and not rawMsg:match("^[Nn]o glyphs") then - DEFAULT_CHAT_FRAME:AddMessage("|cff66ccff[ERROR]|r Ignored non-glyphs msg") + DEFAULT_CHAT_FRAME:AddMessage("|cff66ccff[ERROR]|r " .. MultiBot.L("talent.glyphs.error_ignored_non_glyph")) return end @@ -1719,11 +1999,140 @@ local function ClassTestCommand() end end +local function MainBarLayoutExportCommand() + if not MultiBot.SaveMainBarLayoutForCurrentPlayer then + printToChat("[MB] Export indisponible.") + return + end + + local ok, ownerKeyOrError = MultiBot.SaveMainBarLayoutForCurrentPlayer() + if not ok then + printToChat(("[MB] Export échoué: %s"):format(tostring(ownerKeyOrError))) + return + end + printToChat(("[MB] Layout sauvegardé pour %s"):format(ownerKeyOrError)) +end + +local function MainBarLayoutImportOwnerCommand(msg) + if not MultiBot.ImportSavedMainBarLayout then + printToChat("[MB] Import (owner) indisponible.") + return + end + + local ownerKey = tostring(msg or "") + ownerKey = string.match(ownerKey, "^%s*(.-)%s*$") or "" + if ownerKey == "" then + printToChat("[MB] Usage: /mblio ") + return + end + + local ok, detail = MultiBot.ImportSavedMainBarLayout(ownerKey) + if ok then + printToChat(("[MB] Layout '%s' importé (%s entrées)."):format(ownerKey, tostring(detail))) + return + end + printToChat(("[MB] Import '%s' échoué: %s"):format(ownerKey, tostring(detail))) +end + +local function MainBarLayoutListCommand() + if not MultiBot.GetSavedMainBarLayoutOwners then + printToChat("[MB] Liste layouts indisponible.") + return + end + local owners = MultiBot.GetSavedMainBarLayoutOwners() + if #owners == 0 then + printToChat("[MB] Aucun layout sauvegardé.") + return + end + printToChat("[MB] Layouts sauvegardés:") + for _, owner in ipairs(owners) do + printToChat(" - " .. owner) + end +end + +local function MainBarLayoutImportPayloadCommand(msg) + if not MultiBot.ImportMainBarLayoutPayload then + printToChat("[MB] Import payload indisponible.") + return + end + + local payload = tostring(msg or "") + payload = string.match(payload, "^%s*(.-)%s*$") or "" + local ok, detail = MultiBot.ImportMainBarLayoutPayload(payload) + if ok then + printToChat(("[MB] Payload importé (%s entrées)."):format(tostring(detail))) + return + end + printToChat(("[MB] Import payload échoué: %s"):format(tostring(detail))) +end + +local function MainBarLayoutShowPayloadCommand(msg) + if not MultiBot.GetSavedMainBarLayoutPayload then + printToChat("[MB] Show payload indisponible.") + return + end + + local ownerKey = tostring(msg or "") + ownerKey = string.match(ownerKey, "^%s*(.-)%s*$") or "" + if ownerKey == "" then + ownerKey = getPlayerLayoutOwnerKey() + end + local payload = MultiBot.GetSavedMainBarLayoutPayload(ownerKey) + if not payload then + printToChat(("[MB] Aucun payload pour '%s'."):format(ownerKey)) + return + end + printToChat(("[MB] Payload '%s':"):format(ownerKey)) + printToChat(payload) +end + +local function MainBarLayoutDeleteCommand(msg) + if not MultiBot.DeleteSavedMainBarLayout then + printToChat("[MB] Delete layout indisponible.") + return + end + + local ownerKey = tostring(msg or "") + ownerKey = string.match(ownerKey, "^%s*(.-)%s*$") or "" + if ownerKey == "" then + printToChat("[MB] Usage: /mbldel ") + return + end + + local ok, detail = MultiBot.DeleteSavedMainBarLayout(ownerKey) + if ok then + printToChat(("[MB] Layout supprimé: %s"):format(ownerKey)) + return + end + printToChat(("[MB] Suppression impossible (%s): %s"):format(ownerKey, tostring(detail))) +end + +local function MainBarLayoutResetCommand() + if not MultiBot.ResetMainBarLayoutState then + printToChat("[MB] Reset layout indisponible.") + return + end + + local ok, removed = MultiBot.ResetMainBarLayoutState() + if ok then + printToChat(("[MB] Layout reset effectué (%s clés supprimées)."):format(tostring(removed))) + return + end + printToChat("[MB] Reset layout échoué.") +end + local COMMAND_DEFINITIONS = { { "MULTIBOT", ToggleMultiBotUI, { "multibot", "mbot", "mb" } }, { "MBFAKEGM", FakeGMCommand, { "mbfakegm" } }, { "MBCLASS", ClassCommand, { "mbclass" } }, { "MBCLASSTEST", ClassTestCommand, { "mbclasstest" } }, + { "MBLAYOUTEXPORT", MainBarLayoutExportCommand, { "mblayoutexport", "mblx" } }, + { "MBLAYOUTLIST", MainBarLayoutListCommand, { "mblayoutlist", "mbll" } }, + { "MBLAYOUTIMPORTOWNER", MainBarLayoutImportOwnerCommand, { "mblayoutimportowner", "mblio" } }, + { "MBLAYOUTIMPORTPAYLOAD", MainBarLayoutImportPayloadCommand, { "mblayoutimportpayload", "mbli" } }, + { "MBLAYOUTSHOWPAYLOAD", MainBarLayoutShowPayloadCommand, { "mblayoutshowpayload", "mblp" } }, + { "MBLAYOUTDELETE", MainBarLayoutDeleteCommand, { "mblayoutdelete", "mbldel" } }, + { "MBLAYOUTRESET", MainBarLayoutResetCommand, { "mblayoutreset", "mblreset" } }, } for _, def in ipairs(COMMAND_DEFINITIONS) do diff --git a/Locales/MultiBotAceLocale-deDE.lua b/Locales/MultiBotAceLocale-deDE.lua index 5216c71..71e4de3 100644 --- a/Locales/MultiBotAceLocale-deDE.lua +++ b/Locales/MultiBotAceLocale-deDE.lua @@ -192,6 +192,50 @@ local deDEValues = { ["tips.units.inviteRaid25"] = "25er Raid\n|cffffffffMit dieser Schaltfläche füllt man sein Raid auf.\nDiese Funktion nimmt Einheiten vom aktuellen Roster und ignoriert dabei den Klassen-Filter.\nSie hält am Ende des Rosters oder sobald die Gruppe 25 Mitglieder hat.|r\n\n|cffff0000Linksklicken um Raid-Mitglieder einzuladen|r\n|cff999999(Ausführreihenfolge: System)|r", ["tips.units.inviteRaid40"] = "40er Raid\n|cffffffffMit dieser Schaltfläche füllt man sein Raid auf.\nDiese Funktion nimmt Einheiten vom aktuellen Roster und ignoriert dabei den Klassen-Filter.\nSie hält am Ende des Rosters oder sobald die Gruppe 40 Mitglieder hat.|r\n\n|cffff0000Linksklicken um Raid-Mitglieder einzuladen|r\n|cff999999(Ausführreihenfolge: System)|r", ["tips.units.alliance"] = "Alle PlayerBots ein-/ausloggen\n|cffffffffLoggt alle PlayerBots ein oder aus, auf die du Zugriff hast.\nDiese Funktion kann je nach Gesamtanzahl der PlayerBots einige Zeit benötigen,\num die Buttonleisten für jeden PlayerBot zu laden.\n\n|cffff0000Linksklick, um alle PlayerBots einzuloggen|r\n|cff999999(Ausgeführt von: System)|r\n\n|cffff0000Rechtsklick, um alle PlayerBots auszuloggen|r\n|cff999999(Ausgeführt von: System)|r", + ["options.minimap.explainer"] = "Blendet den MultiBot-Minimap-Button ein oder aus.", + ["options.layout.lock_mainbar"] = "Bewegung der Hauptleiste sperren", + ["options.layout.lock_mainbar_desc"] = "Aktiviert: Strg + Rechtsklick zum Verschieben der Leiste. Deaktiviert: Rechtsklick genügt.", + ["options.layout.owner_import"] = "Spieler-Layout zum Importieren", + ["options.layout.export"] = "Layout exportieren", + ["options.layout.import"] = "Layout importieren", + ["options.layout.delete"] = "Layout löschen", + ["options.layout.refresh"] = "Liste aktualisieren", + ["options.layout.reset"] = "Layout zurücksetzen", + ["options.layout.none"] = "Kein Layout", + ["options.layout.error_no_layout_to_import"] = "Kein gespeichertes Layout zum Importieren.", + ["options.layout.error_no_layout_to_delete"] = "Kein Layout zum Löschen ausgewählt.", + ["options.layout.error_delete_unavailable"] = "Löschen nicht verfügbar.", + ["options.layout.list_refreshed"] = "Layoutliste aktualisiert.", + ["options.layout.error_reset_unavailable"] = "Zurücksetzen des Layouts nicht verfügbar.", + ["options.layout.error_reset_failed"] = "Zurücksetzen des Layouts fehlgeschlagen.", + ["options.layout.saved"] = "Layout gespeichert: %s", + ["options.layout.error_export_failed"] = "Export fehlgeschlagen: %s", + ["options.layout.imported"] = "Layout importiert: %s (%s Einträge)", + ["options.layout.error_import_failed"] = "Import fehlgeschlagen: %s", + ["options.layout.deleted"] = "Layout gelöscht: %s", + ["options.layout.error_delete_failed"] = "Löschen fehlgeschlagen: %s", + ["options.layout.reset_done"] = "Layout zurückgesetzt (%s Schlüssel).", + ["options.tabs.minimap"] = "Minikarte", + ["options.tabs.layout"] = "Layout", + ["options.tabs.strata"] = "Strata", + ["options.tabs.intervals"] = "Intervalle", + ["mainbar.swap.locked"] = "Leiste gesperrt: Halte Strg + Rechtsklick zum Verschieben.", + ["mainbar.swap.source_prefix"] = "Hauptleiste: Quelle = ", + ["mainbar.swap.cancelled"] = "Hauptleiste: Tausch abgebrochen.", + ["mainbar.swap.preview_prefix"] = "Hauptleiste: ", + ["pvp.stats.error_select_bot"] = "Wähle zuerst ein Bot‑Ziel aus.", + ["pvp.stats.error_not_in_group"] = "Du bist nicht in einer Gruppe.", + ["pvp.stats.error_not_in_raid"] = "Du bist nicht in einem Schlachtzug.", + ["ui.swap.source_prefix"] = "Tauschquelle: ", + ["ui.swap.cancelled"] = "Tausch abgebrochen.", + ["ui.swap.preview_prefix"] = "Tauschvorschau: ", + ["talent.glyphs.waiting"] = "Warte auf Glyphen…", + ["talent.glyphs.debug_prefix"] = "Glyphen‑Payload => ", + ["talent.glyphs.error_ignored_non_glyph"] = "Nicht‑Glyphen‑Nachricht ignoriert", + ["inventory.mode.sell"] = "Verkaufen", + ["spellbook.rank"] = "Rang", + ["spellbook.title"] = "Titel", + ["quests.none"] = "Keine Quests", ["tips.sliders.throttleinstalled"] = "MultiBot-Drossel installiert", ["tips.sliders.frametitle"] = "MultiBot — Optionen", ["tips.sliders.actionsinter"] = "Intervalle für automatische Aktionen", diff --git a/Locales/MultiBotAceLocale-enGB.lua b/Locales/MultiBotAceLocale-enGB.lua index 2dc51ad..596bf43 100644 --- a/Locales/MultiBotAceLocale-enGB.lua +++ b/Locales/MultiBotAceLocale-enGB.lua @@ -194,6 +194,50 @@ local enGBValues = { ["tips.units.inviteRaid25"] = "25-Man Raid Group\n|cffffffffFills your Raid Group with up to 25 PlayerBots.\nPlayerBots are added from the selected Roster, ignoring any Class filters.|r\n\n|cffff0000Left-click to activate.|r\n|cff999999(Executed by: System)|r", ["tips.units.inviteRaid40"] = "40-Man Raid Group\n|cffffffffFills your Raid Group with up to 40 PlayerBots.\nPlayerBots are added from the selected Roster, ignoring any Class filters.|r\n\n|cffff0000Left-click to activate.|r\n|cff999999(Executed by: System)|r", ["tips.units.alliance"] = "Log In/Out All PlayerBots\n|cffffffffLog in/Out all PlayerBots you have access to.\nThis function will take time to populate the ButtonBars for each PlayerBot, depending on the number of PlayerBots total.\n\n|cffff0000Left-click to log in all PlayerBots|r\n|cff999999(Executed by: System)|r\n\n|cffff0000Right-click to log out all PlayerBots|r\n|cff999999(Executed by: System)|r", + ["options.minimap.explainer"] = "Hide or show the MultiBot minimap button.", + ["options.layout.lock_mainbar"] = "Lock main bar movement", + ["options.layout.lock_mainbar_desc"] = "Checked: Ctrl + right-click to move the bar. Unchecked: right-click is enough.", + ["options.layout.owner_import"] = "Player layout to import", + ["options.layout.export"] = "Export layout", + ["options.layout.import"] = "Import layout", + ["options.layout.delete"] = "Delete layout", + ["options.layout.refresh"] = "Refresh list", + ["options.layout.reset"] = "Reset layout", + ["options.layout.none"] = "No layout", + ["options.layout.error_no_layout_to_import"] = "No saved layout to import.", + ["options.layout.error_no_layout_to_delete"] = "No layout selected to delete.", + ["options.layout.error_delete_unavailable"] = "Delete unavailable.", + ["options.layout.list_refreshed"] = "Layout list refreshed.", + ["options.layout.error_reset_unavailable"] = "Layout reset unavailable.", + ["options.layout.error_reset_failed"] = "Layout reset failed.", + ["options.layout.saved"] = "Layout saved: %s", + ["options.layout.error_export_failed"] = "Export failed: %s", + ["options.layout.imported"] = "Layout imported: %s (%s entries)", + ["options.layout.error_import_failed"] = "Import failed: %s", + ["options.layout.deleted"] = "Layout deleted: %s", + ["options.layout.error_delete_failed"] = "Delete failed: %s", + ["options.layout.reset_done"] = "Layout reset (%s keys).", + ["options.tabs.minimap"] = "Minimap", + ["options.tabs.layout"] = "Layout", + ["options.tabs.strata"] = "Strata", + ["options.tabs.intervals"] = "Intervals", + ["mainbar.swap.locked"] = "Bar locked: hold Ctrl + right-click to move.", + ["mainbar.swap.source_prefix"] = "MainBar: source = ", + ["mainbar.swap.cancelled"] = "MainBar: swap cancelled.", + ["mainbar.swap.preview_prefix"] = "MainBar: ", + ["pvp.stats.error_select_bot"] = "Select a bot target first.", + ["pvp.stats.error_not_in_group"] = "You are not in a group.", + ["pvp.stats.error_not_in_raid"] = "You are not in a raid.", + ["ui.swap.source_prefix"] = "Swap source: ", + ["ui.swap.cancelled"] = "Swap cancelled.", + ["ui.swap.preview_prefix"] = "Swap preview: ", + ["talent.glyphs.waiting"] = "Waiting for glyphs…", + ["talent.glyphs.debug_prefix"] = "Glyph payload => ", + ["talent.glyphs.error_ignored_non_glyph"] = "Ignored non-glyphs message", + ["inventory.mode.sell"] = "Sell", + ["spellbook.rank"] = "Rank", + ["spellbook.title"] = "Title", + ["quests.none"] = "No quests", ["tips.sliders.throttleinstalled"] = "MultiBot throttle installed", ["tips.sliders.frametitle"] = "MultiBot — Options", ["tips.sliders.actionsinter"] = "Automatic action intervals", diff --git a/Locales/MultiBotAceLocale-enUS.lua b/Locales/MultiBotAceLocale-enUS.lua index 538e103..93d5525 100644 --- a/Locales/MultiBotAceLocale-enUS.lua +++ b/Locales/MultiBotAceLocale-enUS.lua @@ -194,6 +194,50 @@ local enUSValues = { ["tips.units.inviteRaid25"] = "25-Man Raid Group\n|cffffffffFills your Raid Group with up to 25 PlayerBots.\nPlayerBots are added from the selected Roster, ignoring any Class filters.|r\n\n|cffff0000Left-click to activate.|r\n|cff999999(Executed by: System)|r", ["tips.units.inviteRaid40"] = "40-Man Raid Group\n|cffffffffFills your Raid Group with up to 40 PlayerBots.\nPlayerBots are added from the selected Roster, ignoring any Class filters.|r\n\n|cffff0000Left-click to activate.|r\n|cff999999(Executed by: System)|r", ["tips.units.alliance"] = "Log In/Out All PlayerBots\n|cffffffffLog in/Out all PlayerBots you have access to.\nThis function will take time to populate the ButtonBars for each PlayerBot, depending on the number of PlayerBots total.\n\n|cffff0000Left-click to log in all PlayerBots|r\n|cff999999(Executed by: System)|r\n\n|cffff0000Right-click to log out all PlayerBots|r\n|cff999999(Executed by: System)|r", + ["options.minimap.explainer"] = "Hide or show the MultiBot minimap button.", + ["options.layout.lock_mainbar"] = "Lock main bar movement", + ["options.layout.lock_mainbar_desc"] = "Checked: Ctrl + right-click to move the bar. Unchecked: right-click is enough.", + ["options.layout.owner_import"] = "Player layout to import", + ["options.layout.export"] = "Export layout", + ["options.layout.import"] = "Import layout", + ["options.layout.delete"] = "Delete layout", + ["options.layout.refresh"] = "Refresh list", + ["options.layout.reset"] = "Reset layout", + ["options.layout.none"] = "No layout", + ["options.layout.error_no_layout_to_import"] = "No saved layout to import.", + ["options.layout.error_no_layout_to_delete"] = "No layout selected to delete.", + ["options.layout.error_delete_unavailable"] = "Delete unavailable.", + ["options.layout.list_refreshed"] = "Layout list refreshed.", + ["options.layout.error_reset_unavailable"] = "Layout reset unavailable.", + ["options.layout.error_reset_failed"] = "Layout reset failed.", + ["options.layout.saved"] = "Layout saved: %s", + ["options.layout.error_export_failed"] = "Export failed: %s", + ["options.layout.imported"] = "Layout imported: %s (%s entries)", + ["options.layout.error_import_failed"] = "Import failed: %s", + ["options.layout.deleted"] = "Layout deleted: %s", + ["options.layout.error_delete_failed"] = "Delete failed: %s", + ["options.layout.reset_done"] = "Layout reset (%s keys).", + ["options.tabs.minimap"] = "Minimap", + ["options.tabs.layout"] = "Layout", + ["options.tabs.strata"] = "Strata", + ["options.tabs.intervals"] = "Intervals", + ["mainbar.swap.locked"] = "Bar locked: hold Ctrl + right-click to move.", + ["mainbar.swap.source_prefix"] = "MainBar: source = ", + ["mainbar.swap.cancelled"] = "MainBar: swap cancelled.", + ["mainbar.swap.preview_prefix"] = "MainBar: ", + ["pvp.stats.error_select_bot"] = "Select a bot target first.", + ["pvp.stats.error_not_in_group"] = "You are not in a group.", + ["pvp.stats.error_not_in_raid"] = "You are not in a raid.", + ["ui.swap.source_prefix"] = "Swap source: ", + ["ui.swap.cancelled"] = "Swap cancelled.", + ["ui.swap.preview_prefix"] = "Swap preview: ", + ["talent.glyphs.waiting"] = "Waiting for glyphs…", + ["talent.glyphs.debug_prefix"] = "Glyph payload => ", + ["talent.glyphs.error_ignored_non_glyph"] = "Ignored non-glyphs message", + ["inventory.mode.sell"] = "Sell", + ["spellbook.rank"] = "Rank", + ["spellbook.title"] = "Title", + ["quests.none"] = "No quests", ["tips.sliders.throttleinstalled"] = "MultiBot throttle installed", ["tips.sliders.frametitle"] = "MultiBot — Options", ["tips.sliders.actionsinter"] = "Automatic action intervals", diff --git a/Locales/MultiBotAceLocale-esES.lua b/Locales/MultiBotAceLocale-esES.lua index a24810f..c978d71 100644 --- a/Locales/MultiBotAceLocale-esES.lua +++ b/Locales/MultiBotAceLocale-esES.lua @@ -192,6 +192,50 @@ local esESValues = { ["tips.units.inviteRaid25"] = "Raid of Twenty-Five\n|cffffffffCon este botón puedes llenar tu banda de hasta 25 integrantes.\nEsta función toma unidades de la lista seleccionada, ignorando el filtro de clase.\nSe detiene al final de la lista o cuando la banda llega a 25 miembros.|r\n\n|cffff0000Clic izquierdo para invitar miembros de la banda|r\n|cff999999(Orden de ejecución: Sistema)|r", ["tips.units.inviteRaid40"] = "Raid of Forty\n|cffffffffCon este botón puedes llenar tu banda de hasta 40 integrantes.\nEsta función toma unidades de la lista seleccionada, ignorando el filtro de clase.\nSe detiene al final de la lista o cuando la banda llega a 40 miembros.|r\n\n|cffff0000Clic izquierdo para invitar miembros de la banda|r\n|cff999999(Orden de ejecución: Sistema)|r", ["tips.units.alliance"] = "Iniciar/Cerrar sesión de todos los PlayerBots\n|cffffffffInicia o cierra la sesión de todos los PlayerBots a los que tengas acceso.\nEsta función puede tardar en llenar las barras de botones de cada PlayerBot,\ndependiendo del número total de PlayerBots.\n\n|cffff0000Clic izquierdo para iniciar sesión en todos los PlayerBots|r\n|cff999999(Ejecutado por: Sistema)|r\n\n|cffff0000Clic derecho para cerrar sesión en todos los PlayerBots|r\n|cff999999(Ejecutado por: Sistema)|r", + ["options.minimap.explainer"] = "Muestra u oculta el botón del minimapa de MultiBot.", + ["options.layout.lock_mainbar"] = "Bloquear movimiento de la barra principal", + ["options.layout.lock_mainbar_desc"] = "Marcado: Ctrl + clic derecho para mover la barra. Desmarcado: clic derecho suficiente.", + ["options.layout.owner_import"] = "Layout de jugador para importar", + ["options.layout.export"] = "Exportar diseño", + ["options.layout.import"] = "Importar diseño", + ["options.layout.delete"] = "Eliminar diseño", + ["options.layout.refresh"] = "Actualizar lista", + ["options.layout.reset"] = "Restablecer diseño", + ["options.layout.none"] = "Sin diseño", + ["options.layout.error_no_layout_to_import"] = "No hay ningún diseño guardado para importar.", + ["options.layout.error_no_layout_to_delete"] = "No se ha seleccionado ningún diseño para eliminar.", + ["options.layout.error_delete_unavailable"] = "Eliminación no disponible.", + ["options.layout.list_refreshed"] = "Lista de diseños actualizada.", + ["options.layout.error_reset_unavailable"] = "Restablecimiento del diseño no disponible.", + ["options.layout.error_reset_failed"] = "Error al restablecer el diseño.", + ["options.layout.saved"] = "Diseño guardado: %s", + ["options.layout.error_export_failed"] = "Error al exportar: %s", + ["options.layout.imported"] = "Diseño importado: %s (%s entradas)", + ["options.layout.error_import_failed"] = "Error al importar: %s", + ["options.layout.deleted"] = "Diseño eliminado: %s", + ["options.layout.error_delete_failed"] = "Error al eliminar: %s", + ["options.layout.reset_done"] = "Diseño restablecido (%s claves).", + ["options.tabs.minimap"] = "Minimapa", + ["options.tabs.layout"] = "Diseño", + ["options.tabs.strata"] = "Estratos", + ["options.tabs.intervals"] = "Intervalos", + ["mainbar.swap.locked"] = "Barra bloqueada: mantén Ctrl + clic derecho para mover.", + ["mainbar.swap.source_prefix"] = "Barra principal: origen = ", + ["mainbar.swap.cancelled"] = "Barra principal: intercambio cancelado.", + ["mainbar.swap.preview_prefix"] = "Barra principal: ", + ["pvp.stats.error_select_bot"] = "Selecciona primero un bot como objetivo.", + ["pvp.stats.error_not_in_group"] = "No estás en un grupo.", + ["pvp.stats.error_not_in_raid"] = "No estás en una banda.", + ["ui.swap.source_prefix"] = "Origen del intercambio: ", + ["ui.swap.cancelled"] = "Intercambio cancelado.", + ["ui.swap.preview_prefix"] = "Vista previa del intercambio: ", + ["talent.glyphs.waiting"] = "Esperando glifos…", + ["talent.glyphs.debug_prefix"] = "Carga de glifos => ", + ["talent.glyphs.error_ignored_non_glyph"] = "Mensaje no relacionado con glifos ignorado", + ["inventory.mode.sell"] = "Vender", + ["spellbook.rank"] = "Rango", + ["spellbook.title"] = "Título", + ["quests.none"] = "Sin misiones", ["tips.sliders.throttleinstalled"] = "Limitador de MultiBot instalado", ["tips.sliders.frametitle"] = "MultiBot — Opciones", ["tips.sliders.actionsinter"] = "Intervalos de acciones automáticas", diff --git a/Locales/MultiBotAceLocale-frFR.lua b/Locales/MultiBotAceLocale-frFR.lua index b731e0f..7f58e86 100644 --- a/Locales/MultiBotAceLocale-frFR.lua +++ b/Locales/MultiBotAceLocale-frFR.lua @@ -192,6 +192,50 @@ local frFRValues = { ["tips.units.inviteRaid25"] = "Raid de Vingt-Cinq\n|cffffffffAvec ce bouton, vous pouvez remplir votre Raid.\nCette fonctionnalité prend les unités du Rôle sélectionné en ignorant le filtre de classe.\nElle s'arrête à la fin du Rôle ou jusqu'à ce que le groupe atteigne 25 membres.|r\n\n|cffff0000Clic gauche pour inviter des membres du Raid|r\n|cff999999(Ordre d'exécution : Système)|r", ["tips.units.inviteRaid40"] = "Raid de Quarante\n|cffffffffAvec ce bouton, vous pouvez remplir votre Raid.\nCette fonctionnalité prend les unités du Rôle sélectionné en ignorant le filtre de classe.\nElle s'arrête à la fin du Rôle ou jusqu'à ce que le groupe atteigne 40 membres.|r\n\n|cffff0000Clic gauche pour inviter des membres du Raid|r\n|cff999999(Ordre d'exécution : Système)|r", ["tips.units.alliance"] = "Connexion/Déconnexion de tous les PlayerBots\n|cffffffffConnecte ou déconnecte tous les PlayerBots auxquels vous avez accès.\nCette fonction peut prendre du temps pour remplir les barres de boutons de chaque PlayerBot,\nen fonction du nombre total de PlayerBots.\n\n|cffff0000Clic gauche pour connecter tous les PlayerBots|r\n|cff999999(Exécuté par : Système)|r\n\n|cffff0000Clic droit pour déconnecter tous les PlayerBots|r\n|cff999999(Exécuté par : Système)|r", + ["options.minimap.explainer"] = "Affiche ou masque le bouton minimap de MultiBot.", + ["options.layout.lock_mainbar"] = "Verrouiller déplacement barre principale", + ["options.layout.lock_mainbar_desc"] = "Coché : Ctrl + clic droit pour déplacer la barre. Décoché : clic droit suffit.", + ["options.layout.owner_import"] = "Layout joueur à importer", + ["options.layout.export"] = "Exporter le layout", + ["options.layout.import"] = "Importer le layout", + ["options.layout.delete"] = "Supprimer le layout", + ["options.layout.refresh"] = "Rafraîchir la liste", + ["options.layout.reset"] = "Réinitialiser le layout", + ["options.layout.none"] = "Aucun layout", + ["options.layout.error_no_layout_to_import"] = "Aucun layout enregistré à importer.", + ["options.layout.error_no_layout_to_delete"] = "Aucun layout sélectionné pour la suppression.", + ["options.layout.error_delete_unavailable"] = "Suppression indisponible.", + ["options.layout.list_refreshed"] = "Liste des layouts rafraîchie.", + ["options.layout.error_reset_unavailable"] = "Réinitialisation du layout indisponible.", + ["options.layout.error_reset_failed"] = "Échec de la réinitialisation du layout.", + ["options.layout.saved"] = "Layout enregistré : %s", + ["options.layout.error_export_failed"] = "Échec de l’export : %s", + ["options.layout.imported"] = "Layout importé : %s (%s entrées)", + ["options.layout.error_import_failed"] = "Échec de l’import : %s", + ["options.layout.deleted"] = "Layout supprimé : %s", + ["options.layout.error_delete_failed"] = "Échec de la suppression : %s", + ["options.layout.reset_done"] = "Layout réinitialisé (%s clés).", + ["options.tabs.minimap"] = "Minicarte", + ["options.tabs.layout"] = "Layout", + ["options.tabs.strata"] = "Strates", + ["options.tabs.intervals"] = "Intervalles", + ["mainbar.swap.locked"] = "Barre verrouillée : maintenez Ctrl + clic droit pour déplacer.", + ["mainbar.swap.source_prefix"] = "Barre principale : source = ", + ["mainbar.swap.cancelled"] = "Barre principale : échange annulé.", + ["mainbar.swap.preview_prefix"] = "Barre principale : ", + ["pvp.stats.error_select_bot"] = "Sélectionnez d’abord une cible bot.", + ["pvp.stats.error_not_in_group"] = "Vous n’êtes pas dans un groupe.", + ["pvp.stats.error_not_in_raid"] = "Vous n’êtes pas dans un raid.", + ["ui.swap.source_prefix"] = "Source de l’échange : ", + ["ui.swap.cancelled"] = "Échange annulé.", + ["ui.swap.preview_prefix"] = "Aperçu de l’échange : ", + ["talent.glyphs.waiting"] = "En attente des glyphes…", + ["talent.glyphs.debug_prefix"] = "Payload des glyphes => ", + ["talent.glyphs.error_ignored_non_glyph"] = "Message non‑glyphe ignoré", + ["inventory.mode.sell"] = "Vendre", + ["spellbook.rank"] = "Rang", + ["spellbook.title"] = "Titre", + ["quests.none"] = "Aucune quête", ["tips.sliders.throttleinstalled"] = "Limitation MultiBot installée", ["tips.sliders.frametitle"] = "MultiBot — Options", ["tips.sliders.actionsinter"] = "Intervalles des actions automatiques", diff --git a/Locales/MultiBotAceLocale-koKR.lua b/Locales/MultiBotAceLocale-koKR.lua index 5d43c0e..149ae47 100644 --- a/Locales/MultiBotAceLocale-koKR.lua +++ b/Locales/MultiBotAceLocale-koKR.lua @@ -191,6 +191,50 @@ local koKRValues = { ["tips.units.inviteRaid10"] = "10인 팀\n|cffffffff이 버튼을 사용하여 팀을 구성하세요.\n이 기능은 클래스 필터를 무시하고 선택한 팀 목록에서 유닛을 가져옵니다.\n팀 목록이 끝나거나 팀원이 10명이 되면 멈춥니다.|r\n\n|cffff0000팀원을 초대하려면 왼쪽 클릭|r\n|cff999999(명령 실행: 시스템)|r", ["tips.units.inviteRaid25"] = "25명으로 구성된 팀\n|cffffffff이 버튼을 사용하여 팀을 구성하세요.\n이 기능은 클래스 필터를 무시하고 선택한 팀 목록에서 유닛을 가져옵니다.\n팀 목록이 끝나거나 팀 구성원이 25명이 되면 중단됩니다.|r\n\n|cffff0000팀원을 초대하려면 왼쪽 클릭|r\n|cff999999(명령 실행: 시스템)|r", ["tips.units.alliance"] = "얼라이언스 / 호드\n|cffffffff이 버튼을 사용하면 모든 파티원을 온라인 또는 오프라인 상태로 전환할 수 있습니다.\nMultiBot이 충분히 빠르게 반응하지 못해 모든 봇바를 표시하지 못할 수도 있습니다.\n\n|cffff0000왼쪽 클릭: 모든 파티원을 온라인 상태로 전환|r\n|cff999999(실행 순서: 시스템)|r\n\n|cffff0000오른쪽 클릭: 모든 파티원을 오프라인 상태로 전환|r\n|cff999999(실행 순서: 시스템)|r", + ["options.minimap.explainer"] = "MultiBot 미니맵 버튼을 숨기거나 표시합니다.", + ["options.layout.lock_mainbar"] = "기본 바 이동 잠금", + ["options.layout.lock_mainbar_desc"] = "체크: Ctrl + 우클릭으로 바 이동. 해제: 우클릭만으로 이동.", + ["options.layout.owner_import"] = "가져올 플레이어 레이아웃", + ["options.layout.export"] = "레이아웃 내보내기", + ["options.layout.import"] = "레이아웃 가져오기", + ["options.layout.delete"] = "레이아웃 삭제", + ["options.layout.refresh"] = "목록 새로고침", + ["options.layout.reset"] = "레이아웃 초기화", + ["options.layout.none"] = "레이아웃 없음", + ["options.layout.error_no_layout_to_import"] = "가져올 저장된 레이아웃이 없습니다.", + ["options.layout.error_no_layout_to_delete"] = "삭제할 레이아웃이 선택되지 않았습니다.", + ["options.layout.error_delete_unavailable"] = "삭제할 수 없습니다.", + ["options.layout.list_refreshed"] = "레이아웃 목록이 새로고침되었습니다.", + ["options.layout.error_reset_unavailable"] = "레이아웃 초기화를 사용할 수 없습니다.", + ["options.layout.error_reset_failed"] = "레이아웃 초기화 실패.", + ["options.layout.saved"] = "레이아웃 저장됨: %s", + ["options.layout.error_export_failed"] = "내보내기 실패: %s", + ["options.layout.imported"] = "레이아웃 가져옴: %s (%s개 항목)", + ["options.layout.error_import_failed"] = "가져오기 실패: %s", + ["options.layout.deleted"] = "레이아웃 삭제됨: %s", + ["options.layout.error_delete_failed"] = "삭제 실패: %s", + ["options.layout.reset_done"] = "레이아웃 초기화 완료 (%s개 키).", + ["options.tabs.minimap"] = "미니맵", + ["options.tabs.layout"] = "레이아웃", + ["options.tabs.strata"] = "스트라타", + ["options.tabs.intervals"] = "간격", + ["mainbar.swap.locked"] = "바 잠김: 이동하려면 Ctrl + 오른쪽 클릭을 누르세요.", + ["mainbar.swap.source_prefix"] = "메인바: 소스 = ", + ["mainbar.swap.cancelled"] = "메인바: 교체 취소됨.", + ["mainbar.swap.preview_prefix"] = "메인바: ", + ["pvp.stats.error_select_bot"] = "먼저 봇 대상을 선택하세요.", + ["pvp.stats.error_not_in_group"] = "파티에 속해 있지 않습니다.", + ["pvp.stats.error_not_in_raid"] = "공격대에 속해 있지 않습니다.", + ["ui.swap.source_prefix"] = "교체 소스: ", + ["ui.swap.cancelled"] = "교체 취소됨.", + ["ui.swap.preview_prefix"] = "교체 미리보기: ", + ["talent.glyphs.waiting"] = "문양을 기다리는 중…", + ["talent.glyphs.debug_prefix"] = "문양 페이로드 => ", + ["talent.glyphs.error_ignored_non_glyph"] = "문양이 아닌 메시지 무시됨", + ["inventory.mode.sell"] = "판매", + ["spellbook.rank"] = "등급", + ["spellbook.title"] = "제목", + ["quests.none"] = "퀘스트 없음", ["tips.sliders.throttleinstalled"] = "MultiBot 제한이 설치되었습니다", ["tips.sliders.frametitle"] = "MultiBot — 옵션", ["tips.sliders.actionsinter"] = "자동 작업 간격", diff --git a/Locales/MultiBotAceLocale-ruRU.lua b/Locales/MultiBotAceLocale-ruRU.lua index 954d0d7..b635266 100644 --- a/Locales/MultiBotAceLocale-ruRU.lua +++ b/Locales/MultiBotAceLocale-ruRU.lua @@ -192,6 +192,50 @@ local ruRUValues = { ["tips.units.inviteRaid25"] = "Рейд из 25\n|cffffffffЭтой кнопкой можно заполнить рейд.\nИспользует юнитов из выбранного списка, игнорируя фильтр классов.\nОстанавливается в конце списка или при достижении 25 участников.|r\n\n|cffff0000Левый клик - пригласить участников рейда|r\n|cff999999(Порядок выполнения: Система)|r", ["tips.units.inviteRaid40"] = "Рейд из 40\n|cffffffffЭтой кнопкой можно заполнить рейд.\nИспользует юнитов из выбранного списка, игнорируя фильтр классов.\nОстанавливается в конце списка или при достижении 40 участников.|r\n\n|cffff0000Левый клик - пригласить участников рейда|r\n|cff999999(Порядок выполнения: Система)|r", ["tips.units.alliance"] = "Iniciar/Cerrar sesión de todos los PlayerBots\n|cffffffffInicia o cierra la sesión de todos los PlayerBots a los que tengas acceso.\nEsta función puede tardar en llenar las barras de botones de cada PlayerBot,\ndependiendo del número total de PlayerBots.\n\n|cffff0000Clic izquierdo para iniciar sesión en todos los PlayerBots|r\n|cff999999(Ejecutado por: Sistema)|r\n\n|cffff0000Clic derecho para cerrar sesión en todos los PlayerBots|r\n|cff999999(Ejecutado por: Sistema)|r", + ["options.minimap.explainer"] = "Скрывает или показывает кнопку миникарты MultiBot.", + ["options.layout.lock_mainbar"] = "Заблокировать перемещение главной панели", + ["options.layout.lock_mainbar_desc"] = "Включено: Ctrl + ПКМ для перемещения панели. Выключено: достаточно ПКМ.", + ["options.layout.owner_import"] = "Макет игрока для импорта", + ["options.layout.export"] = "Экспорт макета", + ["options.layout.import"] = "Импорт макета", + ["options.layout.delete"] = "Удалить макет", + ["options.layout.refresh"] = "Обновить список", + ["options.layout.reset"] = "Сбросить макет", + ["options.layout.none"] = "Нет макета", + ["options.layout.error_no_layout_to_import"] = "Нет сохранённого макета для импорта.", + ["options.layout.error_no_layout_to_delete"] = "Не выбран макет для удаления.", + ["options.layout.error_delete_unavailable"] = "Удаление недоступно.", + ["options.layout.list_refreshed"] = "Список макетов обновлён.", + ["options.layout.error_reset_unavailable"] = "Сброс макета недоступен.", + ["options.layout.error_reset_failed"] = "Сбой при сбросе макета.", + ["options.layout.saved"] = "Макет сохранён: %s", + ["options.layout.error_export_failed"] = "Ошибка экспорта: %s", + ["options.layout.imported"] = "Макет импортирован: %s (%s записей)", + ["options.layout.error_import_failed"] = "Ошибка импорта: %s", + ["options.layout.deleted"] = "Макет удалён: %s", + ["options.layout.error_delete_failed"] = "Ошибка удаления: %s", + ["options.layout.reset_done"] = "Макет сброшен (%s ключей).", + ["options.tabs.minimap"] = "Миникарта", + ["options.tabs.layout"] = "Макет", + ["options.tabs.strata"] = "Слои", + ["options.tabs.intervals"] = "Интервалы", + ["mainbar.swap.locked"] = "Панель заблокирована: удерживайте Ctrl + ПКМ для перемещения.", + ["mainbar.swap.source_prefix"] = "Главная панель: источник = ", + ["mainbar.swap.cancelled"] = "Главная панель: обмен отменён.", + ["mainbar.swap.preview_prefix"] = "Главная панель: ", + ["pvp.stats.error_select_bot"] = "Сначала выберите цель‑бота.", + ["pvp.stats.error_not_in_group"] = "Вы не в группе.", + ["pvp.stats.error_not_in_raid"] = "Вы не в рейде.", + ["ui.swap.source_prefix"] = "Источник обмена: ", + ["ui.swap.cancelled"] = "Обмен отменён.", + ["ui.swap.preview_prefix"] = "Предпросмотр обмена: ", + ["talent.glyphs.waiting"] = "Ожидание глифов…", + ["talent.glyphs.debug_prefix"] = "Пакет глифов => ", + ["talent.glyphs.error_ignored_non_glyph"] = "Сообщение не‑глифа проигнорировано", + ["inventory.mode.sell"] = "Продать", + ["spellbook.rank"] = "Уровень", + ["spellbook.title"] = "Название", + ["quests.none"] = "Нет заданий", ["tips.sliders.throttleinstalled"] = "Ограничитель MultiBot установлен", ["tips.sliders.frametitle"] = "MultiBot — Опции", ["tips.sliders.actionsinter"] = "Интервалы автоматических действий", diff --git a/Locales/MultiBotAceLocale-zhCN.lua b/Locales/MultiBotAceLocale-zhCN.lua index cf00cbf..9100c27 100644 --- a/Locales/MultiBotAceLocale-zhCN.lua +++ b/Locales/MultiBotAceLocale-zhCN.lua @@ -192,6 +192,50 @@ local zhCNValues = { ["tips.units.inviteRaid25"] = "二十五人团队\n|cffffffff使用此按钮可以填充你的团队。\n此功能会从选定的团队列表中获取单位,忽略职业筛选器。\n它会在团队列表结束处或队伍达到 25 名成员时停止。|r\n\n|cffff0000左键单击邀请团队成员|r\n|cff999999(执行命令: 系统)|r", ["tips.units.inviteRaid40"] = "四十人团队\n|cffffffff使用此按钮可以填充你的团队。\n此功能会从选定的团队列表中获取单位,忽略职业筛选器。\n它会在团队列表结束处或队伍达到 40 名成员时停止。|r\n\n|cffff0000左键单击邀请团队成员|r\n|cff999999(执行命令: 系统)|r", ["tips.units.alliance"] = "登录/登出所有 PlayerBot\n|cffffffff登录或登出你有权限访问的所有 PlayerBot。\n根据 PlayerBot 的总数量,此功能可能需要一些时间来填充每个 PlayerBot 的按钮栏。\n\n|cffff0000左键点击:登录所有 PlayerBot|r\n|cff999999(执行者:系统)|r\n\n|cffff0000右键点击:登出所有 PlayerBot|r\n|cff999999(执行者:系统)|r", + ["options.minimap.explainer"] = "显示或隐藏 MultiBot 小地图按钮。", + ["options.layout.lock_mainbar"] = "锁定主动作条移动", + ["options.layout.lock_mainbar_desc"] = "勾选:Ctrl + 右键拖动动作条。取消勾选:仅右键即可。", + ["options.layout.owner_import"] = "要导入的玩家布局", + ["options.layout.export"] = "导出布局", + ["options.layout.import"] = "导入布局", + ["options.layout.delete"] = "删除布局", + ["options.layout.refresh"] = "刷新列表", + ["options.layout.reset"] = "重置布局", + ["options.layout.none"] = "无布局", + ["options.layout.error_no_layout_to_import"] = "没有可导入的已保存布局。", + ["options.layout.error_no_layout_to_delete"] = "未选择要删除的布局。", + ["options.layout.error_delete_unavailable"] = "无法删除。", + ["options.layout.list_refreshed"] = "布局列表已刷新。", + ["options.layout.error_reset_unavailable"] = "无法重置布局。", + ["options.layout.error_reset_failed"] = "布局重置失败。", + ["options.layout.saved"] = "布局已保存:%s", + ["options.layout.error_export_failed"] = "导出失败:%s", + ["options.layout.imported"] = "布局已导入:%s(%s 条目)", + ["options.layout.error_import_failed"] = "导入失败:%s", + ["options.layout.deleted"] = "布局已删除:%s", + ["options.layout.error_delete_failed"] = "删除失败:%s", + ["options.layout.reset_done"] = "布局已重置(%s 个键)。", + ["options.tabs.minimap"] = "小地图", + ["options.tabs.layout"] = "布局", + ["options.tabs.strata"] = "层级", + ["options.tabs.intervals"] = "间隔", + ["mainbar.swap.locked"] = "栏已锁定:按住 Ctrl + 右键可移动。", + ["mainbar.swap.source_prefix"] = "主栏:来源 = ", + ["mainbar.swap.cancelled"] = "主栏:交换已取消。", + ["mainbar.swap.preview_prefix"] = "主栏:", + ["pvp.stats.error_select_bot"] = "请先选择一个机器人目标。", + ["pvp.stats.error_not_in_group"] = "你不在队伍中。", + ["pvp.stats.error_not_in_raid"] = "你不在团队中。", + ["ui.swap.source_prefix"] = "交换来源:", + ["ui.swap.cancelled"] = "交换已取消。", + ["ui.swap.preview_prefix"] = "交换预览:", + ["talent.glyphs.waiting"] = "正在等待雕文…", + ["talent.glyphs.debug_prefix"] = "雕文载荷 => ", + ["talent.glyphs.error_ignored_non_glyph"] = "忽略了非雕文消息", + ["inventory.mode.sell"] = "出售", + ["spellbook.rank"] = "等级", + ["spellbook.title"] = "标题", + ["quests.none"] = "无任务", ["tips.sliders.throttleinstalled"] = "已安装 MultiBot 限速", ["tips.sliders.frametitle"] = "MultiBot — 选项", ["tips.sliders.actionsinter"] = "自动操作间隔", diff --git a/TODO.md b/TODO.md index 99c9b71..8dc4228 100644 --- a/TODO.md +++ b/TODO.md @@ -13,8 +13,31 @@ TODO * Mettre une option pour choisir la tailles des icones de la main barre et des quickhunter/shaman * Voir si il y'a pas d'autres option que l'on peut ajouter à la frame options de multibot * creer le multilangue pour le tooltip: setTooltip(self, "Show / Hide / Move Quick Shaman") des fichiers quickshaman et quickhunter -* faire de la main barre + droite et gauche une barre de boutons ou l'on peux disposer les bouton changer l'orde etc... * Ajouter des emplacements de sacs à la fenêtre inventaire +* Creator a l'air de planter et apparait même quand il est desactivé. +* Faire les tootips multi dans la fonction: + local originalDoRight = button.doRight + button.doRight = function(btn) + if(IsShiftKeyDown()) then + if(state.selected == nil) then + state.selected = entryRec + if(UIErrorsFrame) then + UIErrorsFrame:AddMessage("Swap source: " .. (entryRec.id or entryRec.name), 1, 0.82, 0, 1) + end + return + end + + if(state.selected == entryRec) then + state.selected = nil + if(UIErrorsFrame) then + UIErrorsFrame:AddMessage("Swap annulé.", 1, 0.25, 0.25, 1) + end + return + end +De MultiboEngine +* Finir les options de déplacement des boutons +* Debuguer le blocage de la barre principale en déplacement ça a l'air de ne pas persister apres une deco reco +* Améliorer le panneau options avec des onglets Ajouter la fonction unequipe à Multibit: diff --git a/UI/MultiBotAttackUI.lua b/UI/MultiBotAttackUI.lua index 8cfae5a..671e93c 100644 --- a/UI/MultiBotAttackUI.lua +++ b/UI/MultiBotAttackUI.lua @@ -61,6 +61,12 @@ function MultiBot.BuildAttackUI(tLeft) addAttackButton(attackFrame, definition, index) end + if MultiBot.BindShiftRightSwapButtons then + MultiBot.BindShiftRightSwapButtons(tLeft, "LeftRoot", { + { name = "Attack", frameName = ATTACK_FRAME_NAME }, + }) + end + return { mainButton = mainButton, frame = attackFrame, diff --git a/UI/MultiBotFleeUI.lua b/UI/MultiBotFleeUI.lua index d0fd7bb..2d72a2b 100644 --- a/UI/MultiBotFleeUI.lua +++ b/UI/MultiBotFleeUI.lua @@ -70,6 +70,12 @@ function MultiBot.BuildFleeUI(tLeft) addFleeButton(fleeFrame, definition, index) end + if MultiBot.BindShiftRightSwapButtons then + MultiBot.BindShiftRightSwapButtons(tLeft, "LeftRoot", { + { name = "Flee", frameName = FLEE_FRAME_NAME }, + }) + end + return { mainButton = mainButton, frame = fleeFrame, diff --git a/UI/MultiBotFormationUI.lua b/UI/MultiBotFormationUI.lua index 2456ad7..34b7028 100644 --- a/UI/MultiBotFormationUI.lua +++ b/UI/MultiBotFormationUI.lua @@ -59,6 +59,12 @@ function MultiBot.BuildFormationUI(tLeft) addFormationButton(formatFrame, definition, 1, index) end + if MultiBot.BindShiftRightSwapButtons then + MultiBot.BindShiftRightSwapButtons(tLeft, "LeftRoot", { + { name = FORMATION_BUTTON_NAME, frameName = FORMATION_FRAME_NAME }, + }) + end + return { rootButton = formatButton, frame = formatFrame, diff --git a/UI/MultiBotGroupActionsUI.lua b/UI/MultiBotGroupActionsUI.lua index 4dd222a..76f8a32 100644 --- a/UI/MultiBotGroupActionsUI.lua +++ b/UI/MultiBotGroupActionsUI.lua @@ -67,6 +67,13 @@ function MultiBot.InitializeGroupActionsUI(tRight) local summonButton = createGroupCommand(tRight, SUMMON_BUTTON) + if MultiBot.BindShiftRightSwapButtons then + MultiBot.BindShiftRightSwapButtons(tRight, "RightRoot", { + { name = "GroupActions", frameName = "GroupActionsMenu" }, + { name = "Summon" }, + }) + end + GroupActionsUI.initialized = true GroupActionsUI.mainButton = mainButton GroupActionsUI.menu = menu diff --git a/UI/MultiBotInventoryFrame.lua b/UI/MultiBotInventoryFrame.lua index 3f575bf..f5bb8ec 100644 --- a/UI/MultiBotInventoryFrame.lua +++ b/UI/MultiBotInventoryFrame.lua @@ -741,7 +741,7 @@ local function createInventoryContent(window) modeValueLabel:SetJustifyH("LEFT") modeValueLabel:SetJustifyV("TOP") modeValueLabel:SetHeight(INVENTORY_WINDOW_DEFAULTS.modeValueHeight) - modeValueLabel:SetText("Sell") + modeValueLabel:SetText(MultiBot.L("inventory.mode.sell")) local helperText = leftPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge") helperText:SetPoint("TOPLEFT", modeValueLabel, "BOTTOMLEFT", 0, -INVENTORY_WINDOW_DEFAULTS.helperTextOffsetY) diff --git a/UI/MultiBotLeftCoreUI.lua b/UI/MultiBotLeftCoreUI.lua index 469cd79..834e40e 100644 --- a/UI/MultiBotLeftCoreUI.lua +++ b/UI/MultiBotLeftCoreUI.lua @@ -93,5 +93,14 @@ function MultiBot.InitializeLeftCoreUI(tLeft) createModeUI(tLeft) createStayFollowUI(tLeft) + if MultiBot.BindShiftRightSwapButtons then + MultiBot.BindShiftRightSwapButtons(tLeft, "LeftRoot", { + { name = "Tanker" }, + { name = "Mode", frameName = "Mode" }, + { name = "Stay" }, + { name = "Follow" }, + }) + end + return tLeft end \ No newline at end of file diff --git a/UI/MultiBotMainUI.lua b/UI/MultiBotMainUI.lua index bbbe605..318916b 100644 --- a/UI/MultiBotMainUI.lua +++ b/UI/MultiBotMainUI.lua @@ -5,6 +5,9 @@ local MAIN_BUTTON_NAME = "Main" local MAIN_BUTTON_ICON = "inv_gizmo_02" local MAIN_FRAME_X = -2 local MAIN_FRAME_Y = 38 +local MULTIBAR_LAYOUT_KEY = "MultiBarPoint" +local MAINBAR_BUTTON_ORDER_LAYOUT_KEY = "MainBarButtonsOrder" +local MAINBAR_BUTTON_STEP_Y = 34 local LEFT_LAYOUT_SHIFT = 34 local LEFT_LAYOUT_NAMES = { @@ -312,6 +315,94 @@ local function createMainActionButton(mainFrame, definition) return button end +local function isMainBarMoveAllowed() + local moveLocked = MultiBot.GetMainBarMoveLocked and MultiBot.GetMainBarMoveLocked() + if moveLocked == nil then + moveLocked = true + end + + if moveLocked then + return IsControlKeyDown() + end + + return true +end + +local function saveMultiBarPosition() + local multiBar = MultiBot.frames and MultiBot.frames["MultiBar"] + if not multiBar or not MultiBot.SetSavedLayoutValue or not MultiBot.toPoint then + return + end + + local offsetX, offsetY = MultiBot.toPoint(multiBar) + MultiBot.SetSavedLayoutValue(MULTIBAR_LAYOUT_KEY, offsetX .. ", " .. offsetY) +end + +local function splitCsv(value) + if type(value) ~= "string" or value == "" then + return {} + end + + local result = {} + for token in string.gmatch(value, "([^,]+)") do + local trimmed = string.gsub(token, "^%s*(.-)%s*$", "%1") + if trimmed ~= "" then + table.insert(result, trimmed) + end + end + return result +end + +local function findOrderIndex(order, name) + for index, value in ipairs(order) do + if value == name then + return index + end + end + return nil +end + +local function buildResolvedOrder(defaultOrder, savedOrder) + local resolved = {} + local seen = {} + + for _, name in ipairs(savedOrder) do + if findOrderIndex(defaultOrder, name) and not seen[name] then + table.insert(resolved, name) + seen[name] = true + end + end + + for _, name in ipairs(defaultOrder) do + if not seen[name] then + table.insert(resolved, name) + end + end + + return resolved +end + +local function applyMainButtonOrder(mainFrame, order) + if not mainFrame or not mainFrame.buttons then + return + end + + for index, name in ipairs(order) do + local button = mainFrame.buttons[name] + if button and button.setPoint then + button.setPoint(0, (index - 1) * MAINBAR_BUTTON_STEP_Y) + end + end +end + +local function saveMainButtonOrder(order) + if not MultiBot.SetSavedLayoutValue then + return + end + + MultiBot.SetSavedLayoutValue(MAINBAR_BUTTON_ORDER_LAYOUT_KEY, table.concat(order, ",")) +end + function MultiBot.InitializeMainUI(tMultiBar) if not tMultiBar or not tMultiBar.addButton or not tMultiBar.addFrame then return nil @@ -320,10 +411,18 @@ function MultiBot.InitializeMainUI(tMultiBar) local mainButton = tMultiBar.addButton(MAIN_BUTTON_NAME, 0, 0, MAIN_BUTTON_ICON, MultiBot.L("tips.main.master")) mainButton:RegisterForDrag("RightButton") mainButton:SetScript("OnDragStart", function() + if not isMainBarMoveAllowed() then + if UIErrorsFrame then + UIErrorsFrame:AddMessage(MultiBot.L("mainbar.swap.locked"), 1, 0.25, 0.25, 1) + end + return + end + MultiBot.frames["MultiBar"]:StartMoving() end) mainButton:SetScript("OnDragStop", function() MultiBot.frames["MultiBar"]:StopMovingOrSizing() + saveMultiBarPosition() end) mainButton.doLeft = function(button) MultiBot.ShowHideSwitch(button.parent.frames[MAIN_FRAME_NAME]) @@ -332,6 +431,76 @@ function MultiBot.InitializeMainUI(tMultiBar) local mainFrame = tMultiBar.addFrame(MAIN_FRAME_NAME, MAIN_FRAME_X, MAIN_FRAME_Y) mainFrame:Hide() + local defaultMainButtonOrder = { + "Coords", + "Masters", + "RTSC", + "Raidus", + "Creator", + "Beast", + "Expand", + "Release", + "Stats", + "Reward", + "Reset", + "Actions", + } + local savedOrderValue = MultiBot.GetSavedLayoutValue and MultiBot.GetSavedLayoutValue(MAINBAR_BUTTON_ORDER_LAYOUT_KEY) or nil + local currentMainButtonOrder = buildResolvedOrder(defaultMainButtonOrder, splitCsv(savedOrderValue)) + local selectedSwapButtonName = nil + + local function swapMainButtons(buttonName) + if not buttonName then + return + end + + if not selectedSwapButtonName then + selectedSwapButtonName = buttonName + UIErrorsFrame:AddMessage(MultiBot.L("mainbar.swap.source_prefix") .. buttonName, 1, 0.82, 0, 1) + return + end + + if selectedSwapButtonName == buttonName then + selectedSwapButtonName = nil + UIErrorsFrame:AddMessage(MultiBot.L("mainbar.swap.cancelled"), 1, 0.25, 0.25, 1) + return + end + + local fromIndex = findOrderIndex(currentMainButtonOrder, selectedSwapButtonName) + local toIndex = findOrderIndex(currentMainButtonOrder, buttonName) + if not fromIndex or not toIndex then + selectedSwapButtonName = nil + return + end + + currentMainButtonOrder[fromIndex], currentMainButtonOrder[toIndex] = + currentMainButtonOrder[toIndex], currentMainButtonOrder[fromIndex] + + applyMainButtonOrder(mainFrame, currentMainButtonOrder) + saveMainButtonOrder(currentMainButtonOrder) + + UIErrorsFrame:AddMessage(MultiBot.L("mainbar.swap.preview_prefix") .. selectedSwapButtonName .. " <-> " .. buttonName, 0.25, 1, 0.25, 1) + selectedSwapButtonName = nil + end + + local function wireShiftRightSwap(button, buttonName) + if not button or not buttonName then + return + end + + local originalDoRight = button.doRight + button.doRight = function(btn) + if IsShiftKeyDown() then + swapMainButtons(buttonName) + return + end + + if originalDoRight then + originalDoRight(btn) + end + end + end + createMainActionButton(mainFrame, { name = "Coords", y = 0, @@ -341,6 +510,7 @@ function MultiBot.InitializeMainUI(tMultiBar) resetDefaultWindowPositions() end, }) + wireShiftRightSwap(mainFrame.buttons["Coords"], "Coords") createMainActionButton(mainFrame, { name = "Masters", @@ -352,6 +522,7 @@ function MultiBot.InitializeMainUI(tMultiBar) toggleMasters(button) end, }) + wireShiftRightSwap(mainFrame.buttons["Masters"], "Masters") createMainActionButton(mainFrame, { name = "RTSC", @@ -363,6 +534,7 @@ function MultiBot.InitializeMainUI(tMultiBar) toggleRTSC(button) end, }) + wireShiftRightSwap(mainFrame.buttons["RTSC"], "RTSC") createMainActionButton(mainFrame, { name = "Raidus", @@ -374,6 +546,7 @@ function MultiBot.InitializeMainUI(tMultiBar) toggleRaidus(button) end, }) + wireShiftRightSwap(mainFrame.buttons["Raidus"], "Raidus") createMainActionButton(mainFrame, { name = "Creator", @@ -385,6 +558,7 @@ function MultiBot.InitializeMainUI(tMultiBar) toggleCreator(button) end, }) + wireShiftRightSwap(mainFrame.buttons["Creator"], "Creator") createMainActionButton(mainFrame, { name = "Beast", @@ -396,6 +570,7 @@ function MultiBot.InitializeMainUI(tMultiBar) toggleBeast(button) end, }) + wireShiftRightSwap(mainFrame.buttons["Beast"], "Beast") createMainActionButton(mainFrame, { name = "Expand", @@ -407,6 +582,7 @@ function MultiBot.InitializeMainUI(tMultiBar) toggleExpand(button) end, }) + wireShiftRightSwap(mainFrame.buttons["Expand"], "Expand") createMainActionButton(mainFrame, { name = "Release", @@ -418,6 +594,7 @@ function MultiBot.InitializeMainUI(tMultiBar) toggleRelease(button) end, }) + wireShiftRightSwap(mainFrame.buttons["Release"], "Release") createMainActionButton(mainFrame, { name = "Stats", @@ -429,8 +606,10 @@ function MultiBot.InitializeMainUI(tMultiBar) toggleStats(button) end, }) + wireShiftRightSwap(mainFrame.buttons["Stats"], "Stats") local rewardButton = createRewardButton(mainFrame) + wireShiftRightSwap(rewardButton, "Reward") refreshLeftLayout() @@ -443,6 +622,7 @@ function MultiBot.InitializeMainUI(tMultiBar) MultiBot.ActionToTargetOrGroup("reset botAI") end, }) + wireShiftRightSwap(mainFrame.buttons["Reset"], "Reset") createMainActionButton(mainFrame, { name = "Actions", @@ -453,6 +633,9 @@ function MultiBot.InitializeMainUI(tMultiBar) MultiBot.ActionToTargetOrGroup("reset") end, }) + wireShiftRightSwap(mainFrame.buttons["Actions"], "Actions") + + applyMainButtonOrder(mainFrame, currentMainButtonOrder) return { mainButton = mainButton, diff --git a/UI/MultiBotOptions.lua b/UI/MultiBotOptions.lua index 95ab4d1..ac191a9 100644 --- a/UI/MultiBotOptions.lua +++ b/UI/MultiBotOptions.lua @@ -31,6 +31,40 @@ local function debugCall(method, ...) end end +local function getSavedLayoutOwners() + if not MultiBot.GetSavedMainBarLayoutOwners then + return {} + end + local owners = MultiBot.GetSavedMainBarLayoutOwners() + local currentPlayer = UnitName and UnitName("player") or nil + local currentRealm = GetRealmName and GetRealmName() or nil + local currentOwner = currentPlayer + if type(currentPlayer) == "string" and currentPlayer ~= "" and type(currentRealm) == "string" and currentRealm ~= "" then + currentOwner = currentPlayer .. "-" .. currentRealm + end + + if type(currentOwner) ~= "string" or currentOwner == "" then + return owners + end + + local ordered = {} + for _, owner in ipairs(owners) do + if owner == currentOwner then + table.insert(ordered, 1, owner) + else + table.insert(ordered, owner) + end + end + return ordered +end + +local function importLayoutOwner(ownerKey) + if not MultiBot.ImportSavedMainBarLayout then + return false, "import_indisponible" + end + return MultiBot.ImportSavedMainBarLayout(ownerKey) +end + local function makeSlider(parent, key, label, minV, maxV, step, y) local name = PANEL_NAME .. "_" .. key .. "_Slider" local s = CreateFrame("Slider", name, parent, "OptionsSliderTemplate") @@ -118,6 +152,7 @@ local function buildLegacyOptionsContent(panel) scrollFrame:SetScrollChild(scrollChild) local minimapConfig = MultiBot.GetMinimapConfig and MultiBot.GetMinimapConfig() or { hide = false } + local mainBarMoveLocked = MultiBot.GetMainBarMoveLocked and MultiBot.GetMainBarMoveLocked() or true local strataDropDown = CreateFrame("Frame", "MultiBotStrataDropDown", scrollChild, "UIDropDownMenuTemplate") @@ -141,15 +176,173 @@ local function buildLegacyOptionsContent(panel) end end) + local chkMainBarMoveLocked = CreateFrame("CheckButton", "MultiBot_MainBarMoveLockedCheck", scrollChild, "InterfaceOptionsCheckButtonTemplate") + chkMainBarMoveLocked:SetPoint("TOPLEFT", chkMinimapHide, "BOTTOMLEFT", 0, -8) + _G[chkMainBarMoveLocked:GetName() .. "Text"]:SetText(optL("options.layout.lock_mainbar")) + chkMainBarMoveLocked.tooltipText = optL("options.layout.lock_mainbar_desc") + chkMainBarMoveLocked:SetChecked(mainBarMoveLocked and true or false) + chkMainBarMoveLocked:SetScript("OnClick", function(btn) + if MultiBot.SetMainBarMoveLocked then + MultiBot.SetMainBarMoveLocked(btn:GetChecked() and true or false) + end + end) + + panel.chkMinimapHide = chkMinimapHide + panel.chkMainBarMoveLocked = chkMainBarMoveLocked + + local selectedOwnerKey = nil + local refreshOwnerDropdown + + local exportBtn = CreateFrame("Button", nil, scrollChild, "UIPanelButtonTemplate") + exportBtn:SetSize(110, 22) + exportBtn:SetPoint("TOPLEFT", chkMainBarMoveLocked, "BOTTOMLEFT", 0, -14) + exportBtn:SetText(optL("options.layout.export")) + exportBtn:SetScript("OnClick", function() + if MultiBot.SaveMainBarLayoutForCurrentPlayer then + local ok, ownerKey, payloadOrError = MultiBot.SaveMainBarLayoutForCurrentPlayer() + if UIErrorsFrame then + if ok then + UIErrorsFrame:AddMessage((optL("options.layout.saved")):format(ownerKey), 0.25, 1, 0.25, 1) + selectedOwnerKey = ownerKey + if refreshOwnerDropdown then + refreshOwnerDropdown() + end + else + UIErrorsFrame:AddMessage((optL("options.layout.error_export_failed")):format(tostring(ownerKey or payloadOrError)), 1, 0.25, 0.25, 1) + end + end + end + end) + + local importBtn = CreateFrame("Button", nil, scrollChild, "UIPanelButtonTemplate") + importBtn:SetSize(110, 22) + importBtn:SetPoint("LEFT", exportBtn, "RIGHT", 8, 0) + importBtn:SetText(optL("options.layout.import")) + importBtn:SetScript("OnClick", function() + if selectedOwnerKey then + local ok, detail = importLayoutOwner(selectedOwnerKey) + if UIErrorsFrame then + if ok then + UIErrorsFrame:AddMessage((optL("options.layout.imported")):format(selectedOwnerKey, tostring(detail)), 0.25, 1, 0.25, 1) + else + UIErrorsFrame:AddMessage((optL("options.layout.error_import_failed")):format(tostring(detail)), 1, 0.25, 0.25, 1) + end + end + return + end + if UIErrorsFrame then + UIErrorsFrame:AddMessage(optL("options.layout.error_no_layout_to_import"), 1, 0.25, 0.25, 1) + end + end) + + local deleteBtn = CreateFrame("Button", nil, scrollChild, "UIPanelButtonTemplate") + deleteBtn:SetSize(110, 22) + deleteBtn:SetPoint("TOPLEFT", importBtn, "BOTTOMLEFT", 0, -6) + deleteBtn:SetText(optL("options.layout.delete")) + deleteBtn:SetScript("OnClick", function() + if not selectedOwnerKey then + if UIErrorsFrame then + UIErrorsFrame:AddMessage(optL("options.layout.error_no_layout_to_delete"), 1, 0.25, 0.25, 1) + end + return + end + + if not MultiBot.DeleteSavedMainBarLayout then + if UIErrorsFrame then + UIErrorsFrame:AddMessage(optL("options.layout.error_delete_unavailable"), 1, 0.25, 0.25, 1) + end + return + end + + local ok, detail = MultiBot.DeleteSavedMainBarLayout(selectedOwnerKey) + if UIErrorsFrame then + if ok then + UIErrorsFrame:AddMessage((optL("options.layout.deleted")):format(selectedOwnerKey), 1, 0.82, 0, 1) + else + UIErrorsFrame:AddMessage((optL("options.layout.error_delete_failed")):format(tostring(detail)), 1, 0.25, 0.25, 1) + end + end + refreshOwnerDropdown() + end) + + local refreshBtn = CreateFrame("Button", nil, scrollChild, "UIPanelButtonTemplate") + refreshBtn:SetSize(110, 22) + refreshBtn:SetPoint("LEFT", deleteBtn, "RIGHT", 8, 0) + refreshBtn:SetText(optL("options.layout.refresh")) + refreshBtn:SetScript("OnClick", function() + refreshOwnerDropdown() + if UIErrorsFrame then + UIErrorsFrame:AddMessage(optL("options.layout.list_refreshed"), 0.25, 1, 0.25, 1) + end + end) + + local resetBtn = CreateFrame("Button", nil, scrollChild, "UIPanelButtonTemplate") + resetBtn:SetSize(110, 22) + resetBtn:SetPoint("TOPLEFT", refreshBtn, "BOTTOMLEFT", 0, -6) + resetBtn:SetText(optL("options.layout.reset")) + resetBtn:SetScript("OnClick", function() + if not MultiBot.ResetMainBarLayoutState then + if UIErrorsFrame then + UIErrorsFrame:AddMessage(optL("options.layout.error_reset_unavailable"), 1, 0.25, 0.25, 1) + end + return + end + local ok, removed = MultiBot.ResetMainBarLayoutState() + if UIErrorsFrame then + if ok then + UIErrorsFrame:AddMessage((optL("options.layout.reset_done")):format(tostring(removed)), 1, 0.82, 0, 1) + else + UIErrorsFrame:AddMessage(optL("options.layout.error_reset_failed"), 1, 0.25, 0.25, 1) + end + end + refreshOwnerDropdown() + end) + + local ownerDropDown = CreateFrame("Frame", "MultiBotLayoutOwnerDropDown", scrollChild, "UIDropDownMenuTemplate") + ownerDropDown:SetPoint("TOPLEFT", exportBtn, "BOTTOMLEFT", -14, -8) + local ownerLabel = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontNormal") + ownerLabel:SetPoint("BOTTOMLEFT", ownerDropDown, "TOPLEFT", 16, 3) + ownerLabel:SetText(optL("options.layout.owner_import")) + + refreshOwnerDropdown = function() + local owners = getSavedLayoutOwners() + UIDropDownMenu_Initialize(ownerDropDown, function(_, level) + for idx, ownerKey in ipairs(owners) do + local info = UIDropDownMenu_CreateInfo() + info.text = ownerKey + info.value = ownerKey + info.func = function(button) + selectedOwnerKey = owners[button:GetID()] + UIDropDownMenu_SetSelectedID(ownerDropDown, button:GetID()) + end + UIDropDownMenu_AddButton(info, level) + end + end) + if #owners > 0 then + selectedOwnerKey = selectedOwnerKey or owners[1] + local selectedIndex = 1 + for idx, ownerKey in ipairs(owners) do + if ownerKey == selectedOwnerKey then + selectedIndex = idx + break + end + end + UIDropDownMenu_SetSelectedID(ownerDropDown, selectedIndex) + UIDropDownMenu_SetText(ownerDropDown, selectedOwnerKey) + else + selectedOwnerKey = nil + UIDropDownMenu_SetText(ownerDropDown, optL("options.layout.none")) + end + end + refreshOwnerDropdown() + strataDropDown:ClearAllPoints() - strataDropDown:SetPoint("TOPLEFT", chkMinimapHide, "BOTTOMLEFT", -14, -18) + strataDropDown:SetPoint("TOPLEFT", resetBtn, "BOTTOMLEFT", -14, -12) local strataLabel = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontNormal") strataLabel:SetPoint("BOTTOMLEFT", strataDropDown, "TOPLEFT", 16, 3) strataLabel:SetText(MultiBot.L("options.frame_strata")) - panel.chkMinimapHide = chkMinimapHide - local current = (MultiBot.GetGlobalStrataLevel and MultiBot.GetGlobalStrataLevel()) or "HIGH" local strataLevels = { "BACKGROUND", "LOW", "MEDIUM", "HIGH", "DIALOG", "TOOLTIP" } @@ -271,134 +464,373 @@ function MultiBot.BuildOptionsPanel() root.frame:SetPoint("BOTTOMRIGHT", -8, 8) self._aceRoot = root - local scroll = AceGUI:Create("ScrollFrame") - scroll:SetLayout("List") - root:AddChild(scroll) - - local minimapConfig = MultiBot.GetMinimapConfig and MultiBot.GetMinimapConfig() or { hide = false } - local chkMinimapHide = AceGUI:Create("CheckBox") - chkMinimapHide:SetLabel(optL("info.buttonoptionshide")) - chkMinimapHide:SetValue(minimapConfig.hide and true or false) - chkMinimapHide:SetFullWidth(true) - chkMinimapHide:SetCallback("OnValueChanged", function(_, _, hide) - if MultiBot.SetMinimapConfig then - MultiBot.SetMinimapConfig("hide", hide and true or false) - end - if MultiBot.Minimap_Refresh then - MultiBot.Minimap_Refresh() - else - local b = _G["MultiBot_MinimapButton"] or MultiBot.MinimapButton - if b then - if hide then b:Hide() else b:Show() end + local selectedOwnerKey = nil + local strataLevels = { "BACKGROUND", "LOW", "MEDIUM", "HIGH", "DIALOG", "TOOLTIP" } + local minimapHelpText = optL("options.minimap.explainer") + local mainBarMoveLockLabel = optL("options.layout.lock_mainbar") + local mainBarMoveLockDesc = optL("options.layout.lock_mainbar_desc") + local layoutOwnerLabel = optL("options.layout.owner_import") + + local function addTabScroll(tabGroup) + local scroll = AceGUI:Create("ScrollFrame") + scroll:SetLayout("List") + tabGroup:AddChild(scroll) + return scroll + end + + local function buildMinimapTab(tabGroup) + local scroll = addTabScroll(tabGroup) + local minimapConfig = MultiBot.GetMinimapConfig and MultiBot.GetMinimapConfig() or { hide = false } + + local explainer = AceGUI:Create("Label") + explainer:SetFullWidth(true) + explainer:SetText(minimapHelpText) + scroll:AddChild(explainer) + + local explainerSpacer = AceGUI:Create("Label") + explainerSpacer:SetFullWidth(true) + explainerSpacer:SetText(" ") + scroll:AddChild(explainerSpacer) + + local chkMinimapHide = AceGUI:Create("CheckBox") + chkMinimapHide:SetLabel(optL("info.buttonoptionshide")) + chkMinimapHide:SetValue(minimapConfig.hide and true or false) + chkMinimapHide:SetFullWidth(true) + chkMinimapHide:SetCallback("OnValueChanged", function(_, _, hide) + if MultiBot.SetMinimapConfig then + MultiBot.SetMinimapConfig("hide", hide and true or false) end - end - end) - scroll:AddChild(chkMinimapHide) - panel.chkMinimapHide = chkMinimapHide + if MultiBot.Minimap_Refresh then + MultiBot.Minimap_Refresh() + else + local b = _G["MultiBot_MinimapButton"] or MultiBot.MinimapButton + if b then + if hide then b:Hide() else b:Show() end + end + end + end) + scroll:AddChild(chkMinimapHide) + panel.chkMinimapHide = chkMinimapHide + end - local strata = AceGUI:Create("Dropdown") - strata:SetLabel(MultiBot.L("options.frame_strata")) - strata:SetWidth(240) - local strataLevels = { "BACKGROUND", "LOW", "MEDIUM", "HIGH", "DIALOG", "TOOLTIP" } - local strataList = {} - for _, v in ipairs(strataLevels) do strataList[v] = v end - strata:SetList(strataList) - strata:SetValue((MultiBot.GetGlobalStrataLevel and MultiBot.GetGlobalStrataLevel()) or "HIGH") - strata:SetCallback("OnValueChanged", function(_, _, value) - if MultiBot.SetGlobalStrataLevel then - MultiBot.SetGlobalStrataLevel(value) - end - if MultiBot.ApplyGlobalStrata then - MultiBot.ApplyGlobalStrata() + local function buildLayoutTab(tabGroup) + local scroll = addTabScroll(tabGroup) + local mainBarMoveLocked = MultiBot.GetMainBarMoveLocked and MultiBot.GetMainBarMoveLocked() or true + + local chkMainBarMoveLocked = AceGUI:Create("CheckBox") + chkMainBarMoveLocked:SetLabel(mainBarMoveLockLabel) + if chkMainBarMoveLocked.SetDescription then + chkMainBarMoveLocked:SetDescription(mainBarMoveLockDesc) end - end) - scroll:AddChild(strata) - - local spacer = AceGUI:Create("Label") - spacer:SetText(" ") - spacer:SetFullWidth(true) - scroll:AddChild(spacer) - - local sub = AceGUI:Create("Label") - sub:SetText(optL("tips.sliders.actionsinter")) - sub:SetFullWidth(true) - scroll:AddChild(sub) - - local sliderRefs = {} - - local function buildTimerSlider(key, label, minV, maxV, step) - local slider = AceGUI:Create("Slider") - slider:SetFullWidth(true) - slider:SetSliderValues(minV, maxV, step) - slider:SetLabel(label) - slider:SetCallback("OnValueChanged", function(widget, _, value) - value = round(value, step) - MultiBot.SetTimer(key, value) - widget:SetLabel(formatSliderLabel(label, secondsLabel(value))) - widget:SetValue(value) + chkMainBarMoveLocked:SetValue(mainBarMoveLocked and true or false) + chkMainBarMoveLocked:SetFullWidth(true) + chkMainBarMoveLocked:SetCallback("OnValueChanged", function(_, _, value) + if MultiBot.SetMainBarMoveLocked then + MultiBot.SetMainBarMoveLocked(value and true or false) + end end) - slider._refresh = function() - local value = MultiBot.GetTimer(key) - slider:SetValue(value) - slider:SetLabel(formatSliderLabel(label, secondsLabel(value))) + scroll:AddChild(chkMainBarMoveLocked) + panel.chkMainBarMoveLocked = chkMainBarMoveLocked + + local ownerTitle = AceGUI:Create("Label") + ownerTitle:SetFullWidth(true) + ownerTitle:SetText(layoutOwnerLabel) + scroll:AddChild(ownerTitle) + + local ownerTopSpacer = AceGUI:Create("Label") + ownerTopSpacer:SetFullWidth(true) + ownerTopSpacer:SetText(" ") + scroll:AddChild(ownerTopSpacer) + + local ownerDropDown = AceGUI:Create("Dropdown") + ownerDropDown:SetLabel(" ") + ownerDropDown:SetWidth(320) + ownerDropDown:SetCallback("OnValueChanged", function(_, _, value) + selectedOwnerKey = value + end) + + local function refreshOwnerList() + local owners = getSavedLayoutOwners() + local options = {} + for _, owner in ipairs(owners) do + options[owner] = owner + end + ownerDropDown:SetList(options) + if #owners == 0 then + selectedOwnerKey = nil + ownerDropDown:SetValue(nil) + return + end + if not selectedOwnerKey or not options[selectedOwnerKey] then + selectedOwnerKey = owners[1] + end + ownerDropDown:SetValue(selectedOwnerKey) end - sliderRefs[#sliderRefs + 1] = slider - scroll:AddChild(slider) - return slider + + refreshOwnerList() + scroll:AddChild(ownerDropDown) + + local ownerBottomSpacer = AceGUI:Create("Label") + ownerBottomSpacer:SetFullWidth(true) + ownerBottomSpacer:SetText(" ") + scroll:AddChild(ownerBottomSpacer) + + local layoutActions = AceGUI:Create("SimpleGroup") + layoutActions:SetLayout("Flow") + layoutActions:SetFullWidth(true) + + local exportBtn = AceGUI:Create("Button") + exportBtn:SetText(optL("options.layout.export")) + exportBtn:SetWidth(150) + exportBtn:SetCallback("OnClick", function() + if MultiBot.SaveMainBarLayoutForCurrentPlayer then + local ok, ownerKey = MultiBot.SaveMainBarLayoutForCurrentPlayer() + if UIErrorsFrame then + if ok then + UIErrorsFrame:AddMessage((optL("options.layout.saved")):format(ownerKey), 0.25, 1, 0.25, 1) + else + UIErrorsFrame:AddMessage((optL("options.layout.error_export_failed")):format(tostring(ownerKey)), 1, 0.25, 0.25, 1) + end + end + end + refreshOwnerList() + end) + layoutActions:AddChild(exportBtn) + + local importBtn = AceGUI:Create("Button") + importBtn:SetText(optL("options.layout.import")) + importBtn:SetWidth(150) + importBtn:SetCallback("OnClick", function() + if selectedOwnerKey then + local ok, detail = importLayoutOwner(selectedOwnerKey) + if UIErrorsFrame then + if ok then + UIErrorsFrame:AddMessage((optL("options.layout.imported")):format(selectedOwnerKey, tostring(detail)), 0.25, 1, 0.25, 1) + else + UIErrorsFrame:AddMessage((optL("options.layout.error_import_failed")):format(tostring(detail)), 1, 0.25, 0.25, 1) + end + end + return + end + if UIErrorsFrame then + UIErrorsFrame:AddMessage(optL("options.layout.error_no_layout_to_import"), 1, 0.25, 0.25, 1) + end + end) + layoutActions:AddChild(importBtn) + + local deleteBtn = AceGUI:Create("Button") + deleteBtn:SetText(optL("options.layout.delete")) + deleteBtn:SetWidth(150) + deleteBtn:SetCallback("OnClick", function() + if not selectedOwnerKey then + if UIErrorsFrame then + UIErrorsFrame:AddMessage(optL("options.layout.error_no_layout_to_delete"), 1, 0.25, 0.25, 1) + end + return + end + if not MultiBot.DeleteSavedMainBarLayout then + if UIErrorsFrame then + UIErrorsFrame:AddMessage(optL("options.layout.error_delete_unavailable"), 1, 0.25, 0.25, 1) + end + return + end + local ok, detail = MultiBot.DeleteSavedMainBarLayout(selectedOwnerKey) + if UIErrorsFrame then + if ok then + UIErrorsFrame:AddMessage((optL("options.layout.deleted")):format(selectedOwnerKey), 1, 0.82, 0, 1) + else + UIErrorsFrame:AddMessage((optL("options.layout.error_delete_failed")):format(tostring(detail)), 1, 0.25, 0.25, 1) + end + end + refreshOwnerList() + end) + layoutActions:AddChild(deleteBtn) + + local refreshBtn = AceGUI:Create("Button") + refreshBtn:SetText(optL("options.layout.refresh")) + refreshBtn:SetWidth(150) + refreshBtn:SetCallback("OnClick", function() + refreshOwnerList() + if UIErrorsFrame then + UIErrorsFrame:AddMessage(optL("options.layout.list_refreshed"), 0.25, 1, 0.25, 1) + end + end) + layoutActions:AddChild(refreshBtn) + + local resetBtn = AceGUI:Create("Button") + resetBtn:SetText(optL("options.layout.reset")) + resetBtn:SetWidth(150) + resetBtn:SetCallback("OnClick", function() + if not MultiBot.ResetMainBarLayoutState then + if UIErrorsFrame then + UIErrorsFrame:AddMessage(optL("options.layout.error_reset_unavailable"), 1, 0.25, 0.25, 1) + end + return + end + local ok, removed = MultiBot.ResetMainBarLayoutState() + if UIErrorsFrame then + if ok then + UIErrorsFrame:AddMessage((optL("options.layout.reset_done")):format(tostring(removed)), 1, 0.82, 0, 1) + else + UIErrorsFrame:AddMessage(optL("options.layout.error_reset_failed"), 1, 0.25, 0.25, 1) + end + end + refreshOwnerList() + end) + layoutActions:AddChild(resetBtn) + scroll:AddChild(layoutActions) end - local function buildThrottleSlider(key, label, minV, maxV, step) - local getValue = (key == "thr_rate") and MultiBot.GetThrottleRate or MultiBot.GetThrottleBurst - local setValue = (key == "thr_rate") and MultiBot.SetThrottleRate or MultiBot.SetThrottleBurst - local slider = AceGUI:Create("Slider") - slider:SetFullWidth(true) - slider:SetSliderValues(minV, maxV, step) - slider:SetLabel(label) - slider:SetCallback("OnValueChanged", function(widget, _, value) - value = round(value, step) - setValue(value) - widget:SetLabel(formatSliderLabel(label, tostring(value))) - widget:SetValue(value) + local function buildStrataTab(tabGroup) + local scroll = addTabScroll(tabGroup) + local strataTitle = AceGUI:Create("Label") + strataTitle:SetFullWidth(true) + strataTitle:SetText(MultiBot.L("options.frame_strata")) + scroll:AddChild(strataTitle) + + local strataSpacer = AceGUI:Create("Label") + strataSpacer:SetFullWidth(true) + strataSpacer:SetText(" ") + scroll:AddChild(strataSpacer) + + local strata = AceGUI:Create("Dropdown") + strata:SetLabel(" ") + strata:SetWidth(240) + local strataList = {} + for _, strataLevel in ipairs(strataLevels) do + strataList[strataLevel] = strataLevel + end + strata:SetList(strataList) + strata:SetValue((MultiBot.GetGlobalStrataLevel and MultiBot.GetGlobalStrataLevel()) or "HIGH") + strata:SetCallback("OnValueChanged", function(_, _, value) + if MultiBot.SetGlobalStrataLevel then + MultiBot.SetGlobalStrataLevel(value) + end + if MultiBot.ApplyGlobalStrata then + MultiBot.ApplyGlobalStrata() + end end) - slider._refresh = function() - local value = getValue() - slider:SetValue(value) - slider:SetLabel(formatSliderLabel(label, tostring(value))) + scroll:AddChild(strata) + end + + local function buildIntervalsTab(tabGroup) + local scroll = addTabScroll(tabGroup) + + local sub = AceGUI:Create("Label") + sub:SetText(optL("tips.sliders.actionsinter")) + sub:SetFullWidth(true) + scroll:AddChild(sub) + + local intervalsTopSpacer = AceGUI:Create("Label") + intervalsTopSpacer:SetFullWidth(true) + intervalsTopSpacer:SetText(" ") + scroll:AddChild(intervalsTopSpacer) + + local sliderRefs = {} + + local function buildTimerSlider(key, label, minV, maxV, step) + local slider = AceGUI:Create("Slider") + slider:SetFullWidth(true) + slider:SetSliderValues(minV, maxV, step) + slider:SetLabel(label) + slider:SetCallback("OnValueChanged", function(widget, _, value) + value = round(value, step) + MultiBot.SetTimer(key, value) + widget:SetLabel(formatSliderLabel(label, secondsLabel(value))) + widget:SetValue(value) + end) + slider._refresh = function() + local value = MultiBot.GetTimer(key) + slider:SetValue(value) + slider:SetLabel(formatSliderLabel(label, secondsLabel(value))) + end + sliderRefs[#sliderRefs + 1] = slider + scroll:AddChild(slider) + return slider + end + + local function buildThrottleSlider(key, label, minV, maxV, step) + local getValue = (key == "thr_rate") and MultiBot.GetThrottleRate or MultiBot.GetThrottleBurst + local setValue = (key == "thr_rate") and MultiBot.SetThrottleRate or MultiBot.SetThrottleBurst + local slider = AceGUI:Create("Slider") + slider:SetFullWidth(true) + slider:SetSliderValues(minV, maxV, step) + slider:SetLabel(label) + slider:SetCallback("OnValueChanged", function(widget, _, value) + value = round(value, step) + setValue(value) + widget:SetLabel(formatSliderLabel(label, tostring(value))) + widget:SetValue(value) + end) + slider._refresh = function() + local value = getValue() + slider:SetValue(value) + slider:SetLabel(formatSliderLabel(label, tostring(value))) + end + sliderRefs[#sliderRefs + 1] = slider + scroll:AddChild(slider) + return slider end - sliderRefs[#sliderRefs + 1] = slider - scroll:AddChild(slider) - return slider + + local s_stats = buildTimerSlider("stats", optL("tips.sliders.statsinter"), 5, 300, 1) + local s_talent = buildTimerSlider("talent", optL("tips.sliders.talentsinter"), 1, 30, 0.5) + local s_invite = buildTimerSlider("invite", optL("tips.sliders.invitsinter"), 1, 60, 1) + local s_sort = buildTimerSlider("sort", optL("tips.sliders.sortinter"), 0.2, 10, 0.2) + local s_thr_rate = buildThrottleSlider("thr_rate", optL("tips.sliders.messpersec"), 1, 20, 1) + local s_thr_burst = buildThrottleSlider("thr_burst", optL("tips.sliders.maxburst"), 1, 50, 1) + + local btn = AceGUI:Create("Button") + btn:SetText(optL("tips.sliders.rstbutn")) + btn:SetWidth(180) + btn:SetCallback("OnClick", function() + MultiBot.SetTimer("stats", 45) + MultiBot.SetTimer("talent", 3) + MultiBot.SetTimer("invite", 5) + MultiBot.SetTimer("sort", 1) + MultiBot.SetThrottleRate(5) + MultiBot.SetThrottleBurst(8) + for _, slider in ipairs(sliderRefs) do + slider._refresh() + end + end) + + local resetTopSpacer = AceGUI:Create("Label") + resetTopSpacer:SetFullWidth(true) + resetTopSpacer:SetText(" ") + scroll:AddChild(resetTopSpacer) + + scroll:AddChild(btn) + + s_stats._refresh() + s_talent._refresh() + s_invite._refresh() + s_sort._refresh() + s_thr_rate._refresh() + s_thr_burst._refresh() end - local s_stats = buildTimerSlider("stats", optL("tips.sliders.statsinter"), 5, 300, 1) - local s_talent = buildTimerSlider("talent", optL("tips.sliders.talentsinter"), 1, 30, 0.5) - local s_invite = buildTimerSlider("invite", optL("tips.sliders.invitsinter"), 1, 60, 1) - local s_sort = buildTimerSlider("sort", optL("tips.sliders.sortinter"), 0.2, 10, 0.2) - local s_thr_rate = buildThrottleSlider("thr_rate", optL("tips.sliders.messpersec"), 1, 20, 1) - local s_thr_burst = buildThrottleSlider("thr_burst", optL("tips.sliders.maxburst"), 1, 50, 1) - - local btn = AceGUI:Create("Button") - btn:SetText(optL("tips.sliders.rstbutn")) - btn:SetWidth(180) - btn:SetCallback("OnClick", function() - MultiBot.SetTimer("stats", 45) - MultiBot.SetTimer("talent", 3) - MultiBot.SetTimer("invite", 5) - MultiBot.SetTimer("sort", 1) - MultiBot.SetThrottleRate(5) - MultiBot.SetThrottleBurst(8) - for _, slider in ipairs(sliderRefs) do - slider._refresh() + local tabGroup = AceGUI:Create("TabGroup") + tabGroup:SetLayout("Fill") + tabGroup:SetTabs({ + { text = optL("options.tabs.minimap"), value = "minimap" }, + { text = optL("options.tabs.layout"), value = "layout" }, + { text = optL("options.tabs.strata"), value = "strata" }, + { text = optL("options.tabs.intervals"), value = "intervals" }, + }) + tabGroup:SetCallback("OnGroupSelected", function(widget, _, group) + widget:ReleaseChildren() + if group == "minimap" then + buildMinimapTab(widget) + elseif group == "layout" then + buildLayoutTab(widget) + elseif group == "strata" then + buildStrataTab(widget) + elseif group == "intervals" then + buildIntervalsTab(widget) end end) - scroll:AddChild(btn) - - s_stats._refresh() - s_talent._refresh() - s_invite._refresh() - s_sort._refresh() - s_thr_rate._refresh() - s_thr_burst._refresh() + root:AddChild(tabGroup) + tabGroup:SelectTab("minimap") end) if type(InterfaceOptions_AddCategory) == "function" then diff --git a/UI/MultiBotQuestAllFrame.lua b/UI/MultiBotQuestAllFrame.lua index d2e10c1..da7631d 100644 --- a/UI/MultiBotQuestAllFrame.lua +++ b/UI/MultiBotQuestAllFrame.lua @@ -75,7 +75,7 @@ end local function createEmptySectionHint(self, text) local hint = self.aceGUI:Create("Label") hint:SetFullWidth(true) - hint:SetText(" " .. (text or MultiBot.L("tips.quests.gobnosearchdata") or "No quests")) + hint:SetText(" " .. (text or MultiBot.L("tips.quests.gobnosearchdata") or MultiBot.L("quests.none"))) self.scroll:AddChild(hint) end diff --git a/UI/MultiBotQuestCompletedFrame.lua b/UI/MultiBotQuestCompletedFrame.lua index 2081f9f..f2eab13 100644 --- a/UI/MultiBotQuestCompletedFrame.lua +++ b/UI/MultiBotQuestCompletedFrame.lua @@ -12,66 +12,14 @@ local function clearList(self) end end -local function createQuestEntryRow(self, entry) - local row = self.aceGUI:Create("SimpleGroup") - row:SetFullWidth(true) - row:SetLayout("Flow") - - local icon = self.aceGUI:Create("Icon") - icon:SetImage(Shared.ICON_BOT_QUEST or "Interface\\Icons\\inv_misc_note_02") - icon:SetImageSize(14, 14) - icon:SetWidth(20) - row:AddChild(icon) - - local label = self.aceGUI:Create("InteractiveLabel") - label:SetWidth(320) - label:SetText(Shared.BuildQuestLink(entry.id, entry.name)) - label:SetCallback("OnEnter", function(widget) - GameTooltip:SetOwner(widget.frame, "ANCHOR_CURSOR") - GameTooltip:SetHyperlink("quest:" .. tostring(entry.id)) - GameTooltip:Show() - end) - label:SetCallback("OnLeave", function() - GameTooltip_Hide() - end) - row:AddChild(label) - - self.scroll:AddChild(row) - - if entry.bots and #entry.bots > 0 then - local botsLabel = self.aceGUI:Create("Label") - botsLabel:SetFullWidth(true) - botsLabel:SetText(" " .. Shared.FormatBotsLabel(entry.bots)) - self.scroll:AddChild(botsLabel) - end -end - -local function renderQuestList(self, entries, summaryText) - clearList(self) - - local questEntries = entries or {} - for _, entry in ipairs(questEntries) do - createQuestEntryRow(self, entry) - end - - if #questEntries == 0 then - local noData = self.aceGUI:Create("Label") - noData:SetFullWidth(true) - noData:SetText(MultiBot.L("tips.quests.gobnosearchdata") or "No quests") - self.scroll:AddChild(noData) - end - - if self.summary then - self.summary:SetText(summaryText or MultiBot.L("tips.quests.complist") or "") - end -end - function MultiBot.BuildBotCompletedList(botName) local frame = MultiBot.InitializeQuestCompletedFrame() local entries = Shared.SortQuestEntries(MultiBot.BotQuestsCompleted[botName] or {}) frame:Show() - renderQuestList(frame, entries, botName and ("|cff80ff80" .. botName .. "|r") or nil) + Shared.RenderQuestEntries(frame, entries, { + summaryText = botName and ("|cff80ff80" .. botName .. "|r") or (MultiBot.L("tips.quests.complist") or ""), + }) end function MultiBot.BuildAggregatedCompletedList() @@ -79,7 +27,9 @@ function MultiBot.BuildAggregatedCompletedList() local entries = Shared.BuildAggregatedQuestEntries(MultiBot.BotQuestsCompleted) frame:Show() - renderQuestList(frame, entries, "") + Shared.RenderQuestEntries(frame, entries, { + summaryText = "", + }) end function QuestCompletedFrame:Show() diff --git a/UI/MultiBotQuestIncompleteFrame.lua b/UI/MultiBotQuestIncompleteFrame.lua index a06ef94..53494a4 100644 --- a/UI/MultiBotQuestIncompleteFrame.lua +++ b/UI/MultiBotQuestIncompleteFrame.lua @@ -12,66 +12,14 @@ local function clearList(self) end end -local function createQuestEntryRow(self, entry) - local row = self.aceGUI:Create("SimpleGroup") - row:SetFullWidth(true) - row:SetLayout("Flow") - - local icon = self.aceGUI:Create("Icon") - icon:SetImage(Shared.ICON_BOT_QUEST or "Interface\\Icons\\inv_misc_note_02") - icon:SetImageSize(14, 14) - icon:SetWidth(20) - row:AddChild(icon) - - local label = self.aceGUI:Create("InteractiveLabel") - label:SetWidth(320) - label:SetText(Shared.BuildQuestLink(entry.id, entry.name)) - label:SetCallback("OnEnter", function(widget) - GameTooltip:SetOwner(widget.frame, "ANCHOR_CURSOR") - GameTooltip:SetHyperlink("quest:" .. tostring(entry.id)) - GameTooltip:Show() - end) - label:SetCallback("OnLeave", function() - GameTooltip_Hide() - end) - row:AddChild(label) - - self.scroll:AddChild(row) - - if entry.bots and #entry.bots > 0 then - local botsLabel = self.aceGUI:Create("Label") - botsLabel:SetFullWidth(true) - botsLabel:SetText(" " .. Shared.FormatBotsLabel(entry.bots)) - self.scroll:AddChild(botsLabel) - end -end - -local function renderQuestList(self, entries, summaryText) - clearList(self) - - local questEntries = entries or {} - for _, entry in ipairs(questEntries) do - createQuestEntryRow(self, entry) - end - - if #questEntries == 0 then - local noData = self.aceGUI:Create("Label") - noData:SetFullWidth(true) - noData:SetText(MultiBot.L("tips.quests.gobnosearchdata") or "No quests") - self.scroll:AddChild(noData) - end - - if self.summary then - self.summary:SetText(summaryText or MultiBot.L("tips.quests.incomplist") or "") - end -end - function MultiBot.BuildBotQuestList(botName) local frame = MultiBot.InitializeQuestIncompleteFrame() local entries = Shared.SortQuestEntries(MultiBot.BotQuestsIncompleted[botName] or {}) frame:Show() - renderQuestList(frame, entries, botName and ("|cff80ff80" .. botName .. "|r") or nil) + Shared.RenderQuestEntries(frame, entries, { + summaryText = botName and ("|cff80ff80" .. botName .. "|r") or (MultiBot.L("tips.quests.incomplist") or ""), + }) end function MultiBot.BuildAggregatedQuestList() @@ -79,7 +27,9 @@ function MultiBot.BuildAggregatedQuestList() local entries = Shared.BuildAggregatedQuestEntries(MultiBot.BotQuestsIncompleted) frame:Show() - renderQuestList(frame, entries, "") + Shared.RenderQuestEntries(frame, entries, { + summaryText = "", + }) end function QuestIncompleteFrame:Show() diff --git a/UI/MultiBotQuestUIShared.lua b/UI/MultiBotQuestUIShared.lua index 2bf93e5..bcd6669 100644 --- a/UI/MultiBotQuestUIShared.lua +++ b/UI/MultiBotQuestUIShared.lua @@ -60,6 +60,80 @@ function Shared.ApplyEditBoxStyle(widget) end end +local function setQuestTooltip(widget, questID) + if not questID then + return + end + + GameTooltip:SetOwner(widget.frame, "ANCHOR_CURSOR") + GameTooltip:SetHyperlink("quest:" .. tostring(questID)) + GameTooltip:Show() +end + +function Shared.CreateQuestEntryRow(self, entry, opts) + if not self or not self.aceGUI or not self.scroll or type(entry) ~= "table" then + return + end + + opts = opts or {} + + local row = self.aceGUI:Create("SimpleGroup") + row:SetFullWidth(true) + row:SetLayout("Flow") + + local icon = self.aceGUI:Create("Icon") + icon:SetImage(opts.iconPath or Shared.ICON_BOT_QUEST or "Interface\\Icons\\inv_misc_note_02") + icon:SetImageSize(opts.iconSize or 14, opts.iconSize or 14) + icon:SetWidth(opts.iconWidth or 20) + row:AddChild(icon) + + local label = self.aceGUI:Create("InteractiveLabel") + label:SetWidth(opts.labelWidth or 320) + label:SetText(Shared.BuildQuestLink(entry.id, entry.name)) + label:SetCallback("OnEnter", function(widget) + setQuestTooltip(widget, entry.id) + end) + label:SetCallback("OnLeave", GameTooltip_Hide) + row:AddChild(label) + + self.scroll:AddChild(row) + + if opts.showBots ~= false and entry.bots and #entry.bots > 0 then + local botsLabel = self.aceGUI:Create("Label") + botsLabel:SetFullWidth(true) + botsLabel:SetText((opts.botsPrefix or " ") .. Shared.FormatBotsLabel(entry.bots)) + self.scroll:AddChild(botsLabel) + end +end + +function Shared.RenderQuestEntries(self, entries, opts) + if not self then + return + end + + if self.scroll then + self.scroll:ReleaseChildren() + end + + opts = opts or {} + local questEntries = entries or {} + + for _, entry in ipairs(questEntries) do + Shared.CreateQuestEntryRow(self, entry, opts.rowOptions) + end + + if #questEntries == 0 and self.aceGUI and self.scroll then + local noData = self.aceGUI:Create("Label") + noData:SetFullWidth(true) + noData:SetText(opts.emptyText or MultiBot.L("tips.quests.gobnosearchdata") or "No quests") + self.scroll:AddChild(noData) + end + + if self.summary then + self.summary:SetText(opts.summaryText or "") + end +end + function Shared.GetLocalizedQuestName(questID, fallback) if MultiBot.GetLocalizedQuestName then return MultiBot.GetLocalizedQuestName(questID) or fallback or tostring(questID) diff --git a/UI/MultiBotQuestsMenu.lua b/UI/MultiBotQuestsMenu.lua index cf22f69..1fd708e 100644 --- a/UI/MultiBotQuestsMenu.lua +++ b/UI/MultiBotQuestsMenu.lua @@ -233,6 +233,12 @@ function MultiBot.InitializeQuestsMenu(tRight) tRight.buttons["BotUseGOBName"] = gobNameButton tRight.buttons["BotUseGOBSearch"] = gobSearchButton + if MultiBot.BindShiftRightSwapButtons then + MultiBot.BindShiftRightSwapButtons(tRight, "RightRoot", { + { name = "Quests Menu", frameName = "QuestMenu" }, + }) + end + QuestsMenu.initialized = true QuestsMenu.button = button QuestsMenu.menu = menu diff --git a/UI/MultiBotSpecUI.lua b/UI/MultiBotSpecUI.lua index e707a2b..95e01fb 100644 --- a/UI/MultiBotSpecUI.lua +++ b/UI/MultiBotSpecUI.lua @@ -526,9 +526,8 @@ local function getAceGUI() end local function debugSpecPath(path) - if MultiBot and MultiBot.Debug and type(MultiBot.Debug.Once) == "function" then - --MultiBot.Debug.Once("spec.dropdown.path", "MultiBot Spec: using " .. tostring(path) .. " path", "33ccff") - end + -- Debug volontairement désactivé : helper conservé pour ne pas toucher les callsites. + return path end local function disableSetTalentsToggle(wrapper) diff --git a/UI/MultiBotSpellBookFrame.lua b/UI/MultiBotSpellBookFrame.lua index 6cb5c0e..0857f40 100644 --- a/UI/MultiBotSpellBookFrame.lua +++ b/UI/MultiBotSpellBookFrame.lua @@ -347,13 +347,13 @@ local function createSpellbookContent(window) local rank = textLayer:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") rank:SetPoint("TOPLEFT", textLayer, "TOPLEFT", tRankX, tRankY) rank:SetDrawLayer("OVERLAY", getSafeTextDrawSubLevel()) - rank:SetText("|cff" .. (getSpellBookUI().RANK_TEXT_COLOR_HEX or "ffcc00") .. "Rank|r") + rank:SetText("|cff" .. (getSpellBookUI().RANK_TEXT_COLOR_HEX or "ffcc00") .. MultiBot.L("spellbook.rank") .. "|r") tOverlay.texts["R" .. tIndex] = rank local titleText = textLayer:CreateFontString(nil, "OVERLAY", "GameFontHighlight") titleText:SetPoint("TOPLEFT", textLayer, "TOPLEFT", tTitleX, tTextY) titleText:SetDrawLayer("OVERLAY", getSafeTextDrawSubLevel()) - titleText:SetText("|cffffcc00Title|r") + titleText:SetText("|cffffcc00" .. MultiBot.L("spellbook.title") .. "|r") titleText:Hide() tOverlay.texts["T" .. tIndex] = titleText diff --git a/UI/MultiBotTalentFrame.lua b/UI/MultiBotTalentFrame.lua index 89a9ce5..a8095de 100644 --- a/UI/MultiBotTalentFrame.lua +++ b/UI/MultiBotTalentFrame.lua @@ -121,7 +121,7 @@ function MultiBot.InitializeTalentFrameModule() return end - if not MultiBot.talent then + if not MultiBot.talent or type(MultiBot.talent.addFrame) ~= "function" then MultiBot.TalentHostContentLayout = MultiBot.TalentHostContentLayout or DEFAULT_TALENT_HOST_CONTENT_LAYOUT local layout = MultiBot.TalentHostContentLayout @@ -996,7 +996,7 @@ function MultiBot.InitializeTalentFrameModule() local rec = MultiBot.receivedGlyphs and MultiBot.receivedGlyphs[botName] if not rec then - DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[MultiBot]|r Waiting for glyphs…") + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[MultiBot]|r " .. MultiBot.L("talent.glyphs.waiting")) return end @@ -1080,7 +1080,7 @@ function MultiBot.InitializeTalentFrameModule() ids[i] = (socket and socket.item) or 0 end local payload = "glyph equip " .. table.concat(ids, " ") - DEFAULT_CHAT_FRAME:AddMessage("|cff66ccff[DBG]|r " .. + DEFAULT_CHAT_FRAME:AddMessage("|cff66ccff[DBG]|r " .. MultiBot.L("talent.glyphs.debug_prefix") .. (MultiBot.talent.name or "?") .. " : " .. payload) SendChatMessage(payload, "WHISPER", nil, MultiBot.talent.name) end diff --git a/UI/MultiBotUnitsRootUI.lua b/UI/MultiBotUnitsRootUI.lua index b4ceedc..b48475c 100644 --- a/UI/MultiBotUnitsRootUI.lua +++ b/UI/MultiBotUnitsRootUI.lua @@ -489,7 +489,7 @@ local function createPvpStatsControls(controlFrame) whisperButton.doLeft = function() local bot = UnitName("target") if not bot or not UnitIsPlayer("target") then - UIErrorsFrame:AddMessage("Sélectionne un bot (cible) d'abord.", 1, 0.2, 0.2, 1) + UIErrorsFrame:AddMessage(MultiBot.L("pvp.stats.error_select_bot"), 1, 0.2, 0.2, 1) return end SendChatMessage("pvp stats", "WHISPER", nil, bot) @@ -498,7 +498,7 @@ local function createPvpStatsControls(controlFrame) partyButton.doLeft = function() if GetNumPartyMembers() == 0 and GetNumRaidMembers() == 0 then - UIErrorsFrame:AddMessage("Tu n'es pas en groupe.", 1, 0.2, 0.2, 1) + UIErrorsFrame:AddMessage(MultiBot.L("pvp.stats.error_not_in_group"), 1, 0.2, 0.2, 1) return end SendChatMessage("pvp stats", "PARTY") @@ -507,7 +507,7 @@ local function createPvpStatsControls(controlFrame) raidButton.doLeft = function() if GetNumRaidMembers() == 0 then - UIErrorsFrame:AddMessage("Tu n'es pas en raid.", 1, 0.2, 0.2, 1) + UIErrorsFrame:AddMessage(MultiBot.L("pvp.stats.error_not_in_raid"), 1, 0.2, 0.2, 1) return end SendChatMessage("pvp stats", "RAID") diff --git a/docs/ace3-mainbar-layout-migration-tracker.md b/docs/ace3-mainbar-layout-migration-tracker.md new file mode 100644 index 0000000..49f3509 --- /dev/null +++ b/docs/ace3-mainbar-layout-migration-tracker.md @@ -0,0 +1,170 @@ +# MultiBot ACE3 — Main Bar Layout Migration Tracker + +## Objectif global +Rendre la barre principale configurable et sûre à manipuler, avec une approche incrémentale en **2 phases**. + +--- + +## Phase 1 — Plan ajusté (simple, rapide, propre) + +### 1) Lock par défaut + déplacement sécurisé +- Par défaut : **tous les boutons sont verrouillés**. +- Déplacement autorisé uniquement si : + - touche **Ctrl** enfoncée ; + - **clic droit** maintenu sur le bouton Main (déplacement de la barre). +- Bénéfice : éviter les déplacements accidentels. + +✅ Implémenté (lock persistant + Ctrl + clic droit). + +#### Pourquoi c’est simple à intégrer +- La base drag existe déjà (barre principale en drag + infra drag générique). + +--- + +### 2) Modèle de sauvegarde unique (pas de variantes) +- Un seul objet : `mainLayout` (positions des boutons principaux). +- Format minimal par bouton : `{ x, y, visible }`. +- Aucune notion PvE/PvP/rôle/spec. + +#### Base existante réutilisable +- Le système de persistance layout est déjà disponible : + - `GetSavedLayoutValue` + - `SetSavedLayoutValue` + +✅ Implémenté pour : +- position de la barre ; +- layouts de swap boutons (par contexte). + +--- + +### 3) Boutons `Save` / `Export` / `Import` / `Reset` +- **Save** : écrit les positions courantes dans la sauvegarde. +- **Export** : sérialise en string compacte (copiable). +- **Import** : colle la string et applique immédiatement. +- **Reset** : revient aux positions par défaut. + +#### Cohérence existante +- Les positions par défaut existent déjà dans `resetDefaultWindowPositions`. + +✅ Implémenté : +- export/import fonctionnels via bibliothèque globale + payload ; +- reset des clés layout (`MultiBarPoint` + `ButtonLayout:*`) avec remise en position par défaut de la barre principale ; +- actions exposées en Options (legacy + Ace3) et via slash (`/mblreset`). + + +--- + +### 4) Réorganisation des boutons de la Main Bar (nouvelle fonctionnalité) +#### Objectif +Permettre de **déplacer/réordonner les boutons de la barre principale** pour adapter l’ergonomie : +- exemple : inverser `Attack` et `Control` (ou tout autre bouton principal). + +#### Interaction utilisateur +- Entrer en mode réorganisation via **Shift + clic droit** sur un bouton de la Main Bar. +- Sélection source puis cible via **Shift + clic droit** pour échanger leurs positions (swap). +- Afficher un feedback visuel minimal : + - slot source/survol; + - aperçu de permutation; + - confirmation à la fin du drop. + +#### Persistance +- Sauvegarder l’ordre des boutons dans la sauvegarde de layout par profil. +- Format actuel : mapping sérialisé `buttonId -> x,y` par contexte (`ButtonLayout:`). +- Compatibilité : fallback automatique sur l’ordre par défaut si une clé manque. + +#### Contraintes +- Ne pas casser les callbacks existants (`doLeft`, `doRight`, états toggle, disable). +- Préserver les tooltips et icônes. +- Garder le comportement de déplacement de la **barre elle-même** séparé (Ctrl + clic droit selon lock). + +✅ Implémenté partiellement : +- swap actif sur les groupes de boutons configurés ; +- état visuel source/survol + message d’aperçu léger avant validation ; +- les frames de menus verticaux liées suivent leur bouton principal ; +- bouton **Main** reste fixe ; +- bouton **Units** laissé fixe (pas de swap) pour stabilité. + +--- + +### 5) Import A -> B (cas d’usage principal) +- Sur perso A : `Export` → copier la string. +- Sur perso B : `Import` → coller la string → `Apply` → `Save`. +- Optionnel : checksum/version pour valider la compatibilité de la string. +- Le payload doit inclure : + - position de la barre; + - ordre personnalisé des boutons; + - visibilité/flags nécessaires au rendu. + +✅ Implémenté : +- export d’un payload versionné (`MBLAYOUT1`) incluant lock déplacement + position barre + layouts `ButtonLayout:*` ; +- import avec application immédiate (barre principale + layouts de swap déjà enregistrés) ; +- sauvegarde **globale** des layouts exportés indexés par `NomJoueur-Royaume` ; +- stockage global dans `MultiBotGlobalSave.savedLayoutsByPlayer` (scope compte, pas par personnage) ; +- import via **liste déroulante** des layouts sauvegardés (Options legacy + Ace3) ; +- actions exposées dans Options (legacy + Ace3) et slash commands (`/mblx`, `/mbll`, `/mblio `, `/mbli `, `/mblp [owner]`, `/mbldel `, `/mblreset`). + +--- + +### 6) UX minimale mais propre +- Message visuel : + - `Locked` par défaut ; + - `Hold Ctrl + Right Click to move bar`. + - `Hold Shift + Right Click to move buttons`. +- Pendant drag : afficher les coordonnées. +- Fin de drag : autosave (ou save manuel, selon choix final). + +✅ Implémenté (messages lock/swap + aperçu + hint explicite si drag refusé + autosave layout). + +--- + +### 7) Checkbox `Verrouiller déplacement barre` +#### Objectif +Ajouter dans le panneau Options une case simple : +- **Cochée** → barre principale verrouillée ; +- **Décochée** → barre déplaçable. + +#### Pourquoi c’est rapide +- Le panneau options a déjà des `CheckBox` (legacy + ACE3). +- Le drag de la barre principale existe déjà (actuellement right-drag). +- Il suffit d’ajouter une condition de lock avant d’autoriser le déplacement. +- Le booléen peut être persisté comme les autres options UI. + +#### Comportement UX proposé +- Valeur par défaut : `verrouillé = true`. +- Tooltip : `Décoche pour autoriser le déplacement de la barre principale`. +- Position : à côté des toggles UI existants dans le panneau Options. + +--- + +## Phase 2 — Slots supplémentaires pour boutons custom + +### Objectif +Ajouter des **emplacements vides** sur la barre principale pour y attacher des boutons custom. + +### Approche propre +- Définir un nombre de slots configurables (`N`). +- Chaque slot = bouton standard MultiBot (même API `newButton`, `setPoint`, etc.). +- Slots stockés/chargés via la persistance layout existante. +- Le binding d’action du slot sera traité dans une phase dédiée : + - menu de choix action, + - macro command, + - etc. + +### Pourquoi plus tard +Cette partie touche : +- UX, +- modèle de données, +- assignation d’actions. + +=> Mieux de la sortir du correctif lock/unlock pour garder la Phase 1 légère et livrable rapidement. + +--- + +## Statut +- [x] Phase 1 — Finalisée + - [x] lock déplacement barre (Ctrl + clic droit) + - [x] checkbox options lock/unlock + - [x] persistance layout de déplacement + - [x] swap boutons Shift + clic droit (avec suivi des menus verticaux liés) + - [x] export/import des layouts entre personnages +- [ ] Phase 2 — Non démarrée \ No newline at end of file