From f1b4ea25d3f7d26d54f6e4320d551a1cc2186e37 Mon Sep 17 00:00:00 2001 From: Wishmaster117 <140754794+Wishmaster117@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:51:23 +0100 Subject: [PATCH 1/6] Create ace3-mainbar-layout-migration-tracker.md --- docs/ace3-mainbar-layout-migration-tracker.md | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 docs/ace3-mainbar-layout-migration-tracker.md diff --git a/docs/ace3-mainbar-layout-migration-tracker.md b/docs/ace3-mainbar-layout-migration-tracker.md new file mode 100644 index 0000000..4428931 --- /dev/null +++ b/docs/ace3-mainbar-layout-migration-tracker.md @@ -0,0 +1,106 @@ +# 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 gauche** maintenu sur un bouton. +- Bénéfice : éviter les déplacements accidentels. + +#### 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` + +--- + +### 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`. + +--- + +### 4) 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. + +--- + +### 5) UX minimale mais propre +- Message visuel : + - `Locked` par défaut ; + - `Hold Ctrl + Left Click to move`. +- Pendant drag : afficher les coordonnées. +- Fin de drag : autosave (ou save manuel, selon choix final). + +--- + +### 6) 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 +- [ ] Phase 1 — Non démarrée +- [ ] Phase 2 — Non démarrée \ No newline at end of file From 8a67537a9bcb2546297b39211241e0b9d6b37c1a Mon Sep 17 00:00:00 2001 From: Wishmaster117 <140754794+Wishmaster117@users.noreply.github.com> Date: Mon, 30 Mar 2026 13:01:22 +0100 Subject: [PATCH 2/6] Move duplicated functions to shared one --- UI/MultiBotQuestCompletedFrame.lua | 62 +++--------------------- UI/MultiBotQuestIncompleteFrame.lua | 62 +++--------------------- UI/MultiBotQuestUIShared.lua | 74 +++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 112 deletions(-) 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) From e53baa6bf4e1993f39bf977dea9c8fafa5514b1b Mon Sep 17 00:00:00 2001 From: Wishmaster117 <140754794+Wishmaster117@users.noreply.github.com> Date: Mon, 30 Mar 2026 22:53:27 +0100 Subject: [PATCH 3/6] add possibility to move some buttons --- Core/MultiBotConfig.lua | 41 ++++ Core/MultiBotEngine.lua | 190 ++++++++++++++++++ UI/MultiBotAttackUI.lua | 6 + UI/MultiBotFleeUI.lua | 6 + UI/MultiBotFormationUI.lua | 6 + UI/MultiBotGroupActionsUI.lua | 7 + UI/MultiBotLeftCoreUI.lua | 9 + UI/MultiBotMainUI.lua | 180 +++++++++++++++++ UI/MultiBotOptions.lua | 32 ++- UI/MultiBotQuestsMenu.lua | 6 + docs/ace3-mainbar-layout-migration-tracker.md | 61 +++++- 11 files changed, 539 insertions(+), 5 deletions(-) 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..44fdf02 100644 --- a/Core/MultiBotEngine.lua +++ b/Core/MultiBotEngine.lua @@ -1307,6 +1307,196 @@ 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 {} + 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 findEntry(name) + for _, entry in ipairs(state.entries) do + if(entry.name == name) then + return entry + end + end + return nil + 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 + 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 + + local sourceEntry = state.selected + state.selected = nil + 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 + 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, + } + 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 + -- BUTTON:CAT -- MultiBot.catButton = function(pParent, pX, pY, pWidth, pHeight) 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/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..f25b0f9 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,15 @@ 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 + 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 +428,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("MainBar: source = " .. buttonName, 1, 0.82, 0, 1) + return + end + + if selectedSwapButtonName == buttonName then + selectedSwapButtonName = nil + UIErrorsFrame:AddMessage("MainBar: permutation annulée.", 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("MainBar: " .. 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 +507,7 @@ function MultiBot.InitializeMainUI(tMultiBar) resetDefaultWindowPositions() end, }) + wireShiftRightSwap(mainFrame.buttons["Coords"], "Coords") createMainActionButton(mainFrame, { name = "Masters", @@ -352,6 +519,7 @@ function MultiBot.InitializeMainUI(tMultiBar) toggleMasters(button) end, }) + wireShiftRightSwap(mainFrame.buttons["Masters"], "Masters") createMainActionButton(mainFrame, { name = "RTSC", @@ -363,6 +531,7 @@ function MultiBot.InitializeMainUI(tMultiBar) toggleRTSC(button) end, }) + wireShiftRightSwap(mainFrame.buttons["RTSC"], "RTSC") createMainActionButton(mainFrame, { name = "Raidus", @@ -374,6 +543,7 @@ function MultiBot.InitializeMainUI(tMultiBar) toggleRaidus(button) end, }) + wireShiftRightSwap(mainFrame.buttons["Raidus"], "Raidus") createMainActionButton(mainFrame, { name = "Creator", @@ -385,6 +555,7 @@ function MultiBot.InitializeMainUI(tMultiBar) toggleCreator(button) end, }) + wireShiftRightSwap(mainFrame.buttons["Creator"], "Creator") createMainActionButton(mainFrame, { name = "Beast", @@ -396,6 +567,7 @@ function MultiBot.InitializeMainUI(tMultiBar) toggleBeast(button) end, }) + wireShiftRightSwap(mainFrame.buttons["Beast"], "Beast") createMainActionButton(mainFrame, { name = "Expand", @@ -407,6 +579,7 @@ function MultiBot.InitializeMainUI(tMultiBar) toggleExpand(button) end, }) + wireShiftRightSwap(mainFrame.buttons["Expand"], "Expand") createMainActionButton(mainFrame, { name = "Release", @@ -418,6 +591,7 @@ function MultiBot.InitializeMainUI(tMultiBar) toggleRelease(button) end, }) + wireShiftRightSwap(mainFrame.buttons["Release"], "Release") createMainActionButton(mainFrame, { name = "Stats", @@ -429,8 +603,10 @@ function MultiBot.InitializeMainUI(tMultiBar) toggleStats(button) end, }) + wireShiftRightSwap(mainFrame.buttons["Stats"], "Stats") local rewardButton = createRewardButton(mainFrame) + wireShiftRightSwap(rewardButton, "Reward") refreshLeftLayout() @@ -443,6 +619,7 @@ function MultiBot.InitializeMainUI(tMultiBar) MultiBot.ActionToTargetOrGroup("reset botAI") end, }) + wireShiftRightSwap(mainFrame.buttons["Reset"], "Reset") createMainActionButton(mainFrame, { name = "Actions", @@ -453,6 +630,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..138db65 100644 --- a/UI/MultiBotOptions.lua +++ b/UI/MultiBotOptions.lua @@ -118,6 +118,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,14 +142,27 @@ 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("Verrouiller déplacement barre principale") + chkMainBarMoveLocked.tooltipText = "Cochée: Ctrl + clic droit pour déplacer la barre.\nDécochée: clic droit suffit." + 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) + strataDropDown:ClearAllPoints() - strataDropDown:SetPoint("TOPLEFT", chkMinimapHide, "BOTTOMLEFT", -14, -18) + strataDropDown:SetPoint("TOPLEFT", chkMainBarMoveLocked, "BOTTOMLEFT", -14, -18) local strataLabel = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontNormal") strataLabel:SetPoint("BOTTOMLEFT", strataDropDown, "TOPLEFT", 16, 3) strataLabel:SetText(MultiBot.L("options.frame_strata")) + panel.chkMinimapHide = chkMinimapHide + panel.chkMainBarMoveLocked = chkMainBarMoveLocked local current = (MultiBot.GetGlobalStrataLevel and MultiBot.GetGlobalStrataLevel()) or "HIGH" local strataLevels = { "BACKGROUND", "LOW", "MEDIUM", "HIGH", "DIALOG", "TOOLTIP" } @@ -276,6 +290,7 @@ function MultiBot.BuildOptionsPanel() root:AddChild(scroll) local minimapConfig = MultiBot.GetMinimapConfig and MultiBot.GetMinimapConfig() or { hide = false } + local mainBarMoveLocked = MultiBot.GetMainBarMoveLocked and MultiBot.GetMainBarMoveLocked() or true local chkMinimapHide = AceGUI:Create("CheckBox") chkMinimapHide:SetLabel(optL("info.buttonoptionshide")) chkMinimapHide:SetValue(minimapConfig.hide and true or false) @@ -296,6 +311,21 @@ function MultiBot.BuildOptionsPanel() scroll:AddChild(chkMinimapHide) panel.chkMinimapHide = chkMinimapHide + local chkMainBarMoveLocked = AceGUI:Create("CheckBox") + chkMainBarMoveLocked:SetLabel("Verrouiller déplacement barre principale") + if chkMainBarMoveLocked.SetDescription then + chkMainBarMoveLocked:SetDescription("Cochée: Ctrl + clic droit pour déplacer la barre. Décochée: clic droit suffit.") + end + 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) + scroll:AddChild(chkMainBarMoveLocked) + panel.chkMainBarMoveLocked = chkMainBarMoveLocked + local strata = AceGUI:Create("Dropdown") strata:SetLabel(MultiBot.L("options.frame_strata")) strata:SetWidth(240) 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/docs/ace3-mainbar-layout-migration-tracker.md b/docs/ace3-mainbar-layout-migration-tracker.md index 4428931..cd8eaf7 100644 --- a/docs/ace3-mainbar-layout-migration-tracker.md +++ b/docs/ace3-mainbar-layout-migration-tracker.md @@ -12,8 +12,11 @@ Rendre la barre principale configurable et sûre à manipuler, avec une approche - Déplacement autorisé uniquement si : - touche **Ctrl** enfoncée ; - **clic gauche** maintenu sur un bouton. + - **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). @@ -29,6 +32,10 @@ Rendre la barre principale configurable et sûre à manipuler, avec une approche - `GetSavedLayoutValue` - `SetSavedLayoutValue` +✅ Implémenté pour : +- position de la barre ; +- layouts de swap boutons (par contexte). + --- ### 3) Boutons `Save` / `Export` / `Import` / `Reset` @@ -42,23 +49,64 @@ Rendre la barre principale configurable et sûre à manipuler, avec une approche --- -### 4) Import A -> B (cas d’usage principal) +### 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 ; +- 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. + +⏳ À faire (Export/Import applicatif pas encore branché). --- -### 5) UX minimale mais propre +### 6) UX minimale mais propre - Message visuel : - `Locked` par défaut ; - `Hold Ctrl + Left Click to move`. + - `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). +✅ Partiellement implémenté (messages de swap + autosave layout). + --- -### 6) Checkbox `Verrouiller déplacement barre` +### 7) Checkbox `Verrouiller déplacement barre` #### Objectif Ajouter dans le panneau Options une case simple : - **Cochée** → barre principale verrouillée ; @@ -102,5 +150,10 @@ Cette partie touche : --- ## Statut -- [ ] Phase 1 — Non démarrée +- [~] Phase 1 — En cours + - [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) + - [ ] export/import des layouts entre personnages - [ ] Phase 2 — Non démarrée \ No newline at end of file From 628f3f358b667a21cbf23a09c6775c138f0eedf9 Mon Sep 17 00:00:00 2001 From: Wishmaster117 <140754794+Wishmaster117@users.noreply.github.com> Date: Mon, 30 Mar 2026 23:07:32 +0100 Subject: [PATCH 4/6] oupsieee --- Core/MultiBotEngine.lua | 9 --------- TODO.md | 25 ++++++++++++++++++++++++- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/Core/MultiBotEngine.lua b/Core/MultiBotEngine.lua index 44fdf02..da8cfd1 100644 --- a/Core/MultiBotEngine.lua +++ b/Core/MultiBotEngine.lua @@ -1378,15 +1378,6 @@ function MultiBot.BindShiftRightSwapButtons(host, contextKey, entries) end end - local function findEntry(name) - for _, entry in ipairs(state.entries) do - if(entry.name == name) then - return entry - end - end - return nil - end - local function swapButtons(entryA, entryB) local buttonA = entryA and entryA.button local buttonB = entryB and entryB.button 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: From 43d0de514d89c310fcb36557d1b23d90849cc7c2 Mon Sep 17 00:00:00 2001 From: Wishmaster117 <140754794+Wishmaster117@users.noreply.github.com> Date: Tue, 31 Mar 2026 21:07:06 +0100 Subject: [PATCH 5/6] add more layout functions --- Core/MultiBotEngine.lua | 73 +++- Core/MultiBotHandler.lua | 409 ++++++++++++++++++ UI/MultiBotOptions.lua | 321 +++++++++++++- docs/ace3-mainbar-layout-migration-tracker.md | 15 +- 4 files changed, 802 insertions(+), 16 deletions(-) diff --git a/Core/MultiBotEngine.lua b/Core/MultiBotEngine.lua index da8cfd1..338f05a 100644 --- a/Core/MultiBotEngine.lua +++ b/Core/MultiBotEngine.lua @@ -1358,6 +1358,8 @@ function MultiBot.BindShiftRightSwapButtons(host, contextKey, entries) 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 @@ -1462,13 +1464,15 @@ function MultiBot.BindShiftRightSwapButtons(host, contextKey, entries) 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, - } + 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 @@ -1488,6 +1492,61 @@ function MultiBot.BindShiftRightSwapButtons(host, contextKey, entries) 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..0073d8c 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 -- @@ -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/UI/MultiBotOptions.lua b/UI/MultiBotOptions.lua index 138db65..8c86416 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") @@ -153,17 +187,162 @@ local function buildLegacyOptionsContent(panel) 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("Export layout") + exportBtn:SetScript("OnClick", function() + if MultiBot.SaveMainBarLayoutForCurrentPlayer then + local ok, ownerKey, payloadOrError = MultiBot.SaveMainBarLayoutForCurrentPlayer() + if UIErrorsFrame then + if ok then + UIErrorsFrame:AddMessage(("Layout sauvegardé: %s"):format(ownerKey), 0.25, 1, 0.25, 1) + selectedOwnerKey = ownerKey + if refreshOwnerDropdown then + refreshOwnerDropdown() + end + else + UIErrorsFrame:AddMessage(("Export échoué: %s"):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("Import layout") + importBtn:SetScript("OnClick", function() + if selectedOwnerKey then + local ok, detail = importLayoutOwner(selectedOwnerKey) + if UIErrorsFrame then + if ok then + UIErrorsFrame:AddMessage(("Layout importé: %s (%s entrées)"):format(selectedOwnerKey, tostring(detail)), 0.25, 1, 0.25, 1) + else + UIErrorsFrame:AddMessage(("Import échoué: %s"):format(tostring(detail)), 1, 0.25, 0.25, 1) + end + end + return + end + if UIErrorsFrame then + UIErrorsFrame:AddMessage("Aucun layout enregistré à importer.", 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("Suppr layout") + deleteBtn:SetScript("OnClick", function() + if not selectedOwnerKey then + if UIErrorsFrame then + UIErrorsFrame:AddMessage("Aucun layout sélectionné à supprimer.", 1, 0.25, 0.25, 1) + end + return + end + + if not MultiBot.DeleteSavedMainBarLayout then + if UIErrorsFrame then + UIErrorsFrame:AddMessage("Suppression indisponible.", 1, 0.25, 0.25, 1) + end + return + end + + local ok, detail = MultiBot.DeleteSavedMainBarLayout(selectedOwnerKey) + if UIErrorsFrame then + if ok then + UIErrorsFrame:AddMessage(("Layout supprimé: %s"):format(selectedOwnerKey), 1, 0.82, 0, 1) + else + UIErrorsFrame:AddMessage(("Suppression échouée: %s"):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("Refresh liste") + refreshBtn:SetScript("OnClick", function() + refreshOwnerDropdown() + if UIErrorsFrame then + UIErrorsFrame:AddMessage("Liste layouts rafraîchie.", 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("Reset layout") + resetBtn:SetScript("OnClick", function() + if not MultiBot.ResetMainBarLayoutState then + if UIErrorsFrame then + UIErrorsFrame:AddMessage("Reset layout indisponible.", 1, 0.25, 0.25, 1) + end + return + end + local ok, removed = MultiBot.ResetMainBarLayoutState() + if UIErrorsFrame then + if ok then + UIErrorsFrame:AddMessage(("Layout reset (%s clés)."):format(tostring(removed)), 1, 0.82, 0, 1) + else + UIErrorsFrame:AddMessage("Reset layout échoué.", 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("Layout joueur à importer") + + 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, "Aucun layout") + end + end + refreshOwnerDropdown() + strataDropDown:ClearAllPoints() - strataDropDown:SetPoint("TOPLEFT", chkMainBarMoveLocked, "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 - panel.chkMainBarMoveLocked = chkMainBarMoveLocked - local current = (MultiBot.GetGlobalStrataLevel and MultiBot.GetGlobalStrataLevel()) or "HIGH" local strataLevels = { "BACKGROUND", "LOW", "MEDIUM", "HIGH", "DIALOG", "TOOLTIP" } @@ -326,6 +505,138 @@ function MultiBot.BuildOptionsPanel() scroll:AddChild(chkMainBarMoveLocked) panel.chkMainBarMoveLocked = chkMainBarMoveLocked + local layoutActions = AceGUI:Create("SimpleGroup") + layoutActions:SetLayout("Flow") + layoutActions:SetFullWidth(true) + + local selectedOwnerKey = nil + + local ownerDropDown = AceGUI:Create("Dropdown") + ownerDropDown:SetLabel("Layout joueur à importer") + ownerDropDown:SetWidth(280) + 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 = selectedOwnerKey or owners[1] + ownerDropDown:SetValue(selectedOwnerKey) + else + selectedOwnerKey = nil + ownerDropDown:SetValue(nil) + end + end + ownerDropDown:SetCallback("OnValueChanged", function(_, _, value) + selectedOwnerKey = value + end) + refreshOwnerList() + layoutActions:AddChild(ownerDropDown) + + local exportBtn = AceGUI:Create("Button") + exportBtn:SetText("Export layout") + exportBtn:SetWidth(150) + exportBtn:SetCallback("OnClick", function() + if MultiBot.SaveMainBarLayoutForCurrentPlayer then + local ok, ownerKey = MultiBot.SaveMainBarLayoutForCurrentPlayer() + if UIErrorsFrame then + if ok then + UIErrorsFrame:AddMessage(("Layout sauvegardé: %s"):format(ownerKey), 0.25, 1, 0.25, 1) + else + UIErrorsFrame:AddMessage(("Export échoué: %s"):format(tostring(ownerKey)), 1, 0.25, 0.25, 1) + end + end + end + refreshOwnerList() + end) + layoutActions:AddChild(exportBtn) + + local importBtn = AceGUI:Create("Button") + importBtn:SetText("Import layout") + importBtn:SetWidth(150) + importBtn:SetCallback("OnClick", function() + if selectedOwnerKey then + local ok, detail = importLayoutOwner(selectedOwnerKey) + if UIErrorsFrame then + if ok then + UIErrorsFrame:AddMessage(("Layout importé: %s (%s entrées)"):format(selectedOwnerKey, tostring(detail)), 0.25, 1, 0.25, 1) + else + UIErrorsFrame:AddMessage(("Import échoué: %s"):format(tostring(detail)), 1, 0.25, 0.25, 1) + end + end + return + end + if UIErrorsFrame then + UIErrorsFrame:AddMessage("Aucun layout enregistré à importer.", 1, 0.25, 0.25, 1) + end + end) + layoutActions:AddChild(importBtn) + + local deleteBtn = AceGUI:Create("Button") + deleteBtn:SetText("Suppr layout") + deleteBtn:SetWidth(150) + deleteBtn:SetCallback("OnClick", function() + if not selectedOwnerKey then + if UIErrorsFrame then + UIErrorsFrame:AddMessage("Aucun layout sélectionné à supprimer.", 1, 0.25, 0.25, 1) + end + return + end + if not MultiBot.DeleteSavedMainBarLayout then + if UIErrorsFrame then + UIErrorsFrame:AddMessage("Suppression indisponible.", 1, 0.25, 0.25, 1) + end + return + end + local ok, detail = MultiBot.DeleteSavedMainBarLayout(selectedOwnerKey) + if UIErrorsFrame then + if ok then + UIErrorsFrame:AddMessage(("Layout supprimé: %s"):format(selectedOwnerKey), 1, 0.82, 0, 1) + else + UIErrorsFrame:AddMessage(("Suppression échouée: %s"):format(tostring(detail)), 1, 0.25, 0.25, 1) + end + end + refreshOwnerList() + end) + layoutActions:AddChild(deleteBtn) + + local refreshBtn = AceGUI:Create("Button") + refreshBtn:SetText("Refresh liste") + refreshBtn:SetWidth(150) + refreshBtn:SetCallback("OnClick", function() + refreshOwnerList() + if UIErrorsFrame then + UIErrorsFrame:AddMessage("Liste layouts rafraîchie.", 0.25, 1, 0.25, 1) + end + end) + layoutActions:AddChild(refreshBtn) + + local resetBtn = AceGUI:Create("Button") + resetBtn:SetText("Reset layout") + resetBtn:SetWidth(150) + resetBtn:SetCallback("OnClick", function() + if not MultiBot.ResetMainBarLayoutState then + if UIErrorsFrame then + UIErrorsFrame:AddMessage("Reset layout indisponible.", 1, 0.25, 0.25, 1) + end + return + end + local ok, removed = MultiBot.ResetMainBarLayoutState() + if UIErrorsFrame then + if ok then + UIErrorsFrame:AddMessage(("Layout reset (%s clés)."):format(tostring(removed)), 1, 0.82, 0, 1) + else + UIErrorsFrame:AddMessage("Reset layout échoué.", 1, 0.25, 0.25, 1) + end + end + refreshOwnerList() + end) + layoutActions:AddChild(resetBtn) + + scroll:AddChild(layoutActions) + local strata = AceGUI:Create("Dropdown") strata:SetLabel(MultiBot.L("options.frame_strata")) strata:SetWidth(240) diff --git a/docs/ace3-mainbar-layout-migration-tracker.md b/docs/ace3-mainbar-layout-migration-tracker.md index cd8eaf7..ae887ec 100644 --- a/docs/ace3-mainbar-layout-migration-tracker.md +++ b/docs/ace3-mainbar-layout-migration-tracker.md @@ -11,7 +11,6 @@ Rendre la barre principale configurable et sûre à manipuler, avec une approche - Par défaut : **tous les boutons sont verrouillés**. - Déplacement autorisé uniquement si : - touche **Ctrl** enfoncée ; - - **clic gauche** maintenu sur un bouton. - **clic droit** maintenu sur le bouton Main (déplacement de la barre). - Bénéfice : éviter les déplacements accidentels. @@ -89,14 +88,22 @@ Permettre de **déplacer/réordonner les boutons de la barre principale** pour a - ordre personnalisé des boutons; - visibilité/flags nécessaires au rendu. -⏳ À faire (Export/Import applicatif pas encore branché). +✅ 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`). +- 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`). --- ### 6) UX minimale mais propre - Message visuel : - `Locked` par défaut ; - - `Hold Ctrl + Left Click to move`. - `Hold Ctrl + Right Click to move bar`. - `Hold Shift + Right Click to move buttons`. - Pendant drag : afficher les coordonnées. @@ -155,5 +162,5 @@ Cette partie touche : - [x] checkbox options lock/unlock - [x] persistance layout de déplacement - [x] swap boutons Shift + clic droit (avec suivi des menus verticaux liés) - - [ ] export/import des layouts entre personnages + - [x] export/import des layouts entre personnages - [ ] Phase 2 — Non démarrée \ No newline at end of file From 9e0c0acf3ef943faf6b6c79d1eb1ea6e8f934995 Mon Sep 17 00:00:00 2001 From: Wishmaster117 <140754794+Wishmaster117@users.noreply.github.com> Date: Tue, 31 Mar 2026 21:24:51 +0100 Subject: [PATCH 6/6] Phase final --- Core/MultiBotEngine.lua | 65 ++++++++++++++++++- UI/MultiBotMainUI.lua | 3 + docs/ace3-mainbar-layout-migration-tracker.md | 14 ++-- 3 files changed, 75 insertions(+), 7 deletions(-) diff --git a/Core/MultiBotEngine.lua b/Core/MultiBotEngine.lua index 338f05a..19036ce 100644 --- a/Core/MultiBotEngine.lua +++ b/Core/MultiBotEngine.lua @@ -1380,6 +1380,48 @@ function MultiBot.BindShiftRightSwapButtons(host, contextKey, 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 @@ -1426,6 +1468,9 @@ function MultiBot.BindShiftRightSwapButtons(host, contextKey, entries) if(IsShiftKeyDown()) then if(state.selected == nil) then state.selected = entryRec + state.hovered = nil + state.previewTarget = nil + applySelectionVisuals() if(UIErrorsFrame) then UIErrorsFrame:AddMessage("Swap source: " .. (entryRec.id or entryRec.name), 1, 0.82, 0, 1) end @@ -1433,7 +1478,7 @@ function MultiBot.BindShiftRightSwapButtons(host, contextKey, entries) end if(state.selected == entryRec) then - state.selected = nil + clearSelectionState() if(UIErrorsFrame) then UIErrorsFrame:AddMessage("Swap annulé.", 1, 0.25, 0.25, 1) end @@ -1441,7 +1486,7 @@ function MultiBot.BindShiftRightSwapButtons(host, contextKey, entries) end local sourceEntry = state.selected - state.selected = nil + 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) @@ -1455,6 +1500,22 @@ function MultiBot.BindShiftRightSwapButtons(host, contextKey, entries) 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("Aperçu swap: " .. (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 diff --git a/UI/MultiBotMainUI.lua b/UI/MultiBotMainUI.lua index f25b0f9..526d538 100644 --- a/UI/MultiBotMainUI.lua +++ b/UI/MultiBotMainUI.lua @@ -412,6 +412,9 @@ function MultiBot.InitializeMainUI(tMultiBar) mainButton:RegisterForDrag("RightButton") mainButton:SetScript("OnDragStart", function() if not isMainBarMoveAllowed() then + if UIErrorsFrame then + UIErrorsFrame:AddMessage("Barre verrouillée: maintiens Ctrl + clic droit pour déplacer.", 1, 0.25, 0.25, 1) + end return end diff --git a/docs/ace3-mainbar-layout-migration-tracker.md b/docs/ace3-mainbar-layout-migration-tracker.md index ae887ec..49f3509 100644 --- a/docs/ace3-mainbar-layout-migration-tracker.md +++ b/docs/ace3-mainbar-layout-migration-tracker.md @@ -46,6 +46,12 @@ Rendre la barre principale configurable et sûre à manipuler, avec une approche #### 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é) @@ -73,6 +79,7 @@ Permettre de **déplacer/réordonner les boutons de la barre principale** pour a ✅ 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é. @@ -95,9 +102,6 @@ Permettre de **déplacer/réordonner les boutons de la barre principale** pour a - 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`). -- 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`). --- @@ -109,7 +113,7 @@ Permettre de **déplacer/réordonner les boutons de la barre principale** pour a - Pendant drag : afficher les coordonnées. - Fin de drag : autosave (ou save manuel, selon choix final). -✅ Partiellement implémenté (messages de swap + autosave layout). +✅ Implémenté (messages lock/swap + aperçu + hint explicite si drag refusé + autosave layout). --- @@ -157,7 +161,7 @@ Cette partie touche : --- ## Statut -- [~] Phase 1 — En cours +- [x] Phase 1 — Finalisée - [x] lock déplacement barre (Ctrl + clic droit) - [x] checkbox options lock/unlock - [x] persistance layout de déplacement