local _, env = ...; local db, L = env.db, env.L; local Serialize, Deserialize = env.Serialize, env.Deserialize; local Tabular, Carpenter = LibStub('Tabular'), LibStub('Carpenter') local CreateDataContainer; --------------------------------------------------------------- -- Consts --------------------------------------------------------------- local BROWSER_HEIGHT = 500; local BROWSER_CONTENT_WIDTH = 500; local BROWSER_FRAME_WIDTH = 560; local BROWSER_FRAME_PADDING = (BROWSER_FRAME_WIDTH - BROWSER_CONTENT_WIDTH) / 2; local KEYS_MARK = BLUE_FONT_COLOR:WrapTextInColorCode(CTRL_KEY_TEXT ..'+A') local KEYS_COPY = BLUE_FONT_COLOR:WrapTextInColorCode(CTRL_KEY_TEXT ..'+C') local KEYS_PASTE = BLUE_FONT_COLOR:WrapTextInColorCode(CTRL_KEY_TEXT ..'+V') local KEYS_COPY_STRING = ('%s + %s'):format(KEYS_MARK, KEYS_COPY) local EXPORT_DATA_TEXT = L([[ |cFFFFFF00Export|r Select which data you want to export. A string will be generated below, which you can then paste into another client, or share with others. Use %s to copy the string. ]], KEYS_COPY_STRING) local IMPORT_DATA_TEXT = L([[ |cFFFFFF00Import|r Paste an exported string below, then load and select the data you want to import. Imported data will overwrite your current data when applicable. Use %s to copy the string from the source, and %s to paste the string below. ]], KEYS_COPY_STRING, KEYS_PASTE) local IMPORT_FAILED_TEXT = L([[ |cFFFFFF00Import|r Import failed:]]) local PFX = '^ConsolePort'; -- alias path pattern prefix --------------------------------------------------------------- -- Map of aliases to replace display keys and values --------------------------------------------------------------- local AliasMap = { key = { ['^$'] = function() return ('|cFF757575%s|r'):format(NONE) end; ['^(%u?[^PAD]*)(PAD[%d%u]+)$'] = function(mod, button) local buttonSlug = ConsolePort:GetFormattedButtonCombination(button, mod) if buttonSlug:match('ALL_MISSING') then return ('|cFF757575%s%s|r'):format(mod, button); end return buttonSlug; end; }; path = { [PFX..'Bindings$'] = KEY_BINDINGS_MAC; [PFX..'Settings$'] = INTERFACE_OPTIONS; [PFX..'Utility$'] = L'Rings'; [PFX..'Configs$'] = L'Device Mappings'; [PFX..'Cvars$'] = L'Device Settings'; [PFX..'Devices$'] = L'Device Profiles'; [PFX..'_BarSetup$'] = L'Action Bar Setup'; [PFX..'_BarLoadout$'] = L'Action Bar Loadout'; [PFX..'_Talents$'] = TALENTS; [PFX..'Bindings/(%u+%d?)/(.*)$'] = function(button, mod) return ConsolePort:GetFormattedButtonCombination(button, mod) end; [PFX..'Configs/%d+/(%l%a+)$'] = function(variable) return variable:gsub('(%u+)', ' %1'):gsub('^%l', string.upper) end; [PFX..'Cvars/(%a+)/([%a_]+)$'] = function(section, cvar) local section = db.Console[section]; if section then for i, data in ipairs(section) do if (data.cvar == cvar) then return data.name; end end end return cvar; end; [PFX..'Settings/(%a+)$'] = function(variable) local var = db('Variables/'..variable); if type(var) == 'table' then return ('|cFF757575%s|r\n%s'):format(var.head or NOT_APPLICABLE, var.name) end return variable; end; [PFX..'Utility/([^/]+)$'] = function(setID) if ( tonumber(setID) == 1 ) then return BLUE_FONT_COLOR:WrapTextInColorCode(DEFAULT); end return db.Utility:GetBindingDisplayNameForSet(setID) end; [PFX..'Utility/%w+/(%d+)$'] = function(buttonID) return L('Button |cFF00FFFF%s|r', buttonID) end; }; value = { ['^""$'] = function() return ('|cFF757575%s|r'):format(NONE) end; ['^"none"$'] = function() return ('|cFF757575%s|r'):format(NONE) end; ['^"([%u%d]+)"$'] = function(binding) env.BindingInfo:RefreshDictionary() return env.BindingInfo:GetBindingName(binding) or ('%q'):format(binding) end; ['^"(CLICK %w+:%w+)"$'] = function(binding) local _, _, name = db.Bindings:GetDescriptionForBinding(binding) return name or ('%q'):format(binding) end; ['^"(%u?[^PAD]*)(PAD[%d%u]+)"$'] = function(mod, button) local buttonSlug = ConsolePort:GetFormattedButtonCombination(button, mod) if buttonSlug:match('ALL_MISSING') then return ('|cFF757575%s%s|r'):format(mod, button); end return buttonSlug; end; }; }; local AliasMapExport = db.table.merge({ path = { [PFX..'_BarLoadout/(%d+)/?(%d*)$'] = function(pageID, buttonID) local actionID = tonumber(buttonID) and (tonumber(pageID) - 1) * NUM_ACTIONBAR_BUTTONS + tonumber(buttonID); if actionID then return ('|cFF00FFFF%s|r: %s'):format(buttonID, env.BindingInfo:GetActionInfo(actionID) or ('|cFF757575%s|r'):format(NONE)) end return L('Page |cFF00FFFF%s|r', pageID) end; } }, AliasMap) --------------------------------------------------------------- -- Pickup handlers --------------------------------------------------------------- local ActionPickupHandlers = { spell = function(id) return PickupSpell(id) end; item = function(id) return PickupItem(id) end; summonpet = C_PetJournal and C_PetJournal.PickupPet; equipmentset = function(id) local setID = C_EquipmentSet.GetEquipmentSetID(id) if setID then return C_EquipmentSet.PickupEquipmentSet(setID) end end; summonmount = function(id) local mountInfo = {C_MountJournal.GetMountInfoByID(id)} local spellID, isCollected = mountInfo[2], mountInfo[11]; if not isCollected then return end -- HACK: Have to do all this because you can't pickup mount by mount ID. C_MountJournal.SetDefaultFilters() for i=1, C_MountJournal.GetNumDisplayedMounts() do if select(2, C_MountJournal.GetDisplayedMountInfo(i)) == spellID then return C_MountJournal.Pickup(i) end end end; flyout = function(id) local spellBookIndex; for i = 1, GetNumSpellTabs() do local _, _, offset, numSlots = GetSpellTabInfo(i) for j = offset+1, offset+numSlots do local spellType, spellID = GetSpellBookItemInfo(j, BOOKTYPE_SPELL) if (spellType == 'FLYOUT' and spellID == id) then return PickupSpellBookItem(j, BOOKTYPE_SPELL) end end end end; macro = function(id, name, icon, body) local function IsEquivalentMacro(mName, mIcon, mBody) return ( mName == name and icon == icon and body == body ) end -- Try to find equivalent macro if IsEquivalentMacro(GetMacroInfo(id)) then return PickupMacro(id) end local global, perChar = GetNumMacros() for i=1, global do if IsEquivalentMacro(GetMacroInfo(i)) then return PickupMacro(id) end end for i=1, perChar do if IsEquivalentMacro(GetMacroInfo(120 + i)) then return PickupMacro(id) end end -- Create new macro local createOK, macroID = pcall(CreateMacro, name, icon, body, id > 120) if createOK and macroID then return PickupMacro(macroID) end end; }; --------------------------------------------------------------- -- Generate content for the browser --------------------------------------------------------------- local Aggregators = { ConsolePortUtility = function() return db.Utility.Data end; ConsolePortBindings = function() return db.Gamepad:GetBindings(true) end; ConsolePortDevices = function() return db.Gamepad.Devices end; ConsolePortSettings = function() local settings = {}; for varID in db:For('Variables') do settings[varID] = db(varID) end return settings; end; ConsolePortConfigs = function() local configs; for i, config in ipairs(C_GamePad.GetAllConfigIDs()) do configs = configs or {}; configs[i] = C_GamePad.GetConfig(config) end return configs; end; ConsolePortCvars = function() local vars, var = {}; for header, set in pairs(db.Console) do vars[header] = vars[header] or {}; for i, data in ipairs(set) do var = GetCVar(data.cvar) vars[header][data.cvar] = tonumber(var) or var; end end return vars; end; ConsolePort_BarSetup = function() return ConsolePort_BarSetup end; ConsolePort_BarLoadout = function() local actions = {}; for page = 1, 10 do actions[page] = {}; for slot = 1, NUM_ACTIONBAR_BUTTONS do local actionInfo = {GetActionInfo(((page - 1) * NUM_ACTIONBAR_BUTTONS) + slot)} actions[page][slot] = actionInfo; if (actionInfo[1] == 'macro') then tAppendAll(actionInfo, {GetMacroInfo(actionInfo[2])}) end end end return actions; end; ConsolePort_Talents = function() if CPAPI.IsClassicVersion then return end; local talents = {}; for tier = 1, MAX_TALENT_TIERS do local available, column = GetTalentTierInfo(tier, 1) if available and column and column > 0 then local id, name = GetTalentInfo(tier, column, 1) talents[name] = id; end end return talents; end; } local Evaluators = { {'ConsolePort_Talents', function(talents) if CPAPI.IsClassicVersion then return L'Cannot import talents in Classic.'; end for name, id in pairs(talents) do if not LearnTalent(id) then return L('Failed to activate talent %s.', name) end end end}; {'ConsolePortSettings', function(settings) for varID, value in pairs(settings) do if ( db(varID) ~= value ) then db('Settings/'..varID, value) end end end}; {'ConsolePortCvars', function(vars) for header, set in pairs(vars) do for cvar, value in pairs(set) do SetCVar(cvar, value) end end end}; {'ConsolePortBindings', function(bindings) local bindingSetID = GetCurrentBindingSet() for btn, set in pairs(bindings) do for mod, binding in pairs(set) do SetBinding(mod..btn, binding) end end SaveBindings(bindingSetID) end}; {'ConsolePortUtility', function(data) for setID, set in pairs(data) do local ring = {}; for _, buttonData in db.table.spairs(set) do tinsert(ring, buttonData) end db('Utility/Data/'..setID, ring) end return true; end}; {'ConsolePortDevices', function(devices) ConsolePortDevices = devices; return true; end}; {'ConsolePortConfigs', function(configs) for i, config in pairs(configs) do C_GamePad.SetConfig(config) end end}; {'ConsolePort_BarSetup', function(setup) ConsolePort_BarSetup = setup; return true; end}; {'ConsolePort_BarLoadout', function(actions) for page, set in pairs(actions) do for slot, actionInfo in pairs(set) do local actionID = ((page - 1) * NUM_ACTIONBAR_BUTTONS) + slot; local pickup = ActionPickupHandlers[actionInfo[1]]; if pickup then pickup(unpack(actionInfo, 2)) if GetCursorInfo() then PlaceAction(actionID) end ClearCursor() else PickupAction(actionID) ClearCursor() end end end end}; } --------------------------------------------------------------- local Browser = CreateFromMixins(env.FlexibleMixin) function Browser:OnLoad() self.Child:SetHeight(self:GetHeight()) self.Child:SetMeasurementOrigin(self.Child, self.Child, BROWSER_CONTENT_WIDTH, 0) end function Browser:OnShow() self:SetVerticalScroll(0) self.LoadingSpinner:Show() end function Browser:OnEnter() self:Raise() end function Browser:OnHide() if self.release then self.release() self.compile, self.release = nil, nil; end end function Browser:SetData(args, data) self:OnHide() args.parent = self.Child; args.width = BROWSER_CONTENT_WIDTH; args.state = false; self.compile, self.release = Tabular(args, data) self.Child:SetHeight(self:GetHeight()) self.LoadingSpinner:Hide() return self.compile, self.release; end function Browser:Compile() if self.compile then return self.compile() end end --------------------------------------------------------------- local function AdjustBrowserSize(popup, container, browser) C_Timer.After(0, function() local offset = -popup:GetBottom() if offset < 0 then container:SetHeight(BROWSER_HEIGHT) return browser:SetHeight(BROWSER_HEIGHT) end container:SetHeight(BROWSER_HEIGHT - offset) browser:SetHeight(BROWSER_HEIGHT - offset) popup:SetHeight(popup:GetHeight() - offset) end) end local function GenerateExportData() local data = {}; for key, aggregator in pairs(Aggregators) do data[key] = aggregator(); end return data; end local function ValidateImportData(data) if type(data) ~= 'table' then return end; for k in pairs(data) do if not Aggregators[k] then return end end return data; end local function EvaluateImportData(data) if not data then return end local reloadWhenDone = false; for i, evaluatorMeta in ipairs(Evaluators) do local key, evaluator = unpack(evaluatorMeta) local set = data[key]; local result = set and evaluator(set) if (type(result) == 'string') then return CPAPI.Popup('ConsolePort_Import_Failed', { button1 = OKAY; showAlert = 1; text = IMPORT_FAILED_TEXT..'\n%s'; }, result) elseif result then reloadWhenDone = true; end end if reloadWhenDone then ReloadUI(); end end local function CreateAsyncCallback(callback) local mutex; return function() if not mutex then mutex = true; C_Timer.After(0, function() callback() mutex = nil; end) end end end local ImportButton, ExportButton = {}, {}; function ImportButton:OnClick() if self.popup then return self:Check() end; if self:GetParent().Export.popup then return self:Uncheck() end; local dataBin = CreateDataContainer() local alias = AliasMap; local onLoadData = function(popup, editBox, text) local data = ValidateImportData(env.Deserialize(text)) if data then dataBin.Browser:SetData({alias = alias}, data) editBox:ClearFocus() end editBox:SetText('') end; self.popup = CPAPI.Popup('ConsolePort_Import_Data', { text = IMPORT_DATA_TEXT; hasEditBox = 1; maxLetters = 0; button1 = APPLY; button2 = CANCEL; button3 = L'Load'; noCloseOnAlt = true; EditBoxOnEscapePressed = function(self) self:GetParent():Hide() end; EditBoxOnTextChanged = function(self) self:GetParent().button3:SetEnabled(self:GetText():len() > 0) end; EditBoxOnEnterPressed = function(self) onLoadData(self:GetParent(), self, self:GetText()) end; OnHide = function(popup) self.popup = nil; self:Uncheck() end; OnAlt = function(self) onLoadData(self, self.editBox, self.editBox:GetText()) end; OnShow = function(self) self.button3:SetEnabled(false) AdjustBrowserSize(self, dataBin, dataBin.Browser) end; OnAccept = function(popup) db:RunSafe(EvaluateImportData, dataBin.Browser:Compile()) end; }, nil, nil, nil, dataBin) end function ExportButton:OnClick() if self.popup then return self:Check() end; if self:GetParent().Import.popup then return self:Uncheck() end; local dataBin, mutex = CreateDataContainer() local alias = AliasMapExport; local data = GenerateExportData() local callback = CreateAsyncCallback(function() self.output = env.Serialize(dataBin.Browser:Compile()) self.popup.editBox:SetText(self.output) end) self.popup = CPAPI.Popup('ConsolePort_Export_Data', { text = EXPORT_DATA_TEXT; hasEditBox = 1; maxLetters = 0; button1 = DONE; EditBoxOnEscapePressed = function(self) self:GetParent():Hide() end; EditBoxOnEnterPressed = function(self) self:GetParent():Hide() end; EditBoxOnTextChanged = function(editBox) local output = self.output or ''; if ( editBox:GetText() ~= output ) then editBox:SetText(output) end end; OnHide = function(popup) self.popup, self.output = nil, nil; self:Uncheck() end; OnShow = function(self, data) local browser = dataBin.Browser; AdjustBrowserSize(self, dataBin, browser) browser:SetData({callback = callback, alias = alias}, data) end; }, nil, nil, data, dataBin) end function CreateDataContainer() if not env.DataContainer then env.DataContainer = Carpenter('Frame', nil, nil, nil, { _Level = 100; _Size = {BROWSER_FRAME_WIDTH, BROWSER_HEIGHT}; { Browser = { _Mixin = Browser; _Type = 'ScrollFrame'; _Setup = {'CPSmoothScrollTemplate'}; _Points = { {'TOPLEFT', BROWSER_FRAME_PADDING, -BROWSER_FRAME_PADDING}; {'BOTTOMRIGHT', -BROWSER_FRAME_PADDING, BROWSER_FRAME_PADDING}; }; { Child = { _Width = BROWSER_CONTENT_WIDTH; _Mixin = env.ScaleToContentMixin; }; LoadingSpinner = { _Type = 'Texture'; _Size = {146, 146}; _Point = {'CENTER', 0, 0}; _Atlas = {'auctionhouse-ui-loadingspinner'}; }; LoadingAnimation = { _Type = 'AnimationGroup'; _OnLoad = function(self) self:Play() self:SetLooping('REPEAT') end; { SpinnerAnimation = { _Type = 'Animation'; _Setup = 'ROTATION'; _OnLoad = function(self) self:SetChildKey('LoadingSpinner') self:SetOrder(1) self:SetDuration(1) self:SetDegrees(-360); end; }; }; }; }; }; }; }) end return env.DataContainer; end Carpenter:BuildFrame(env.Config, { Header = { { Controls = { _Width = 110; { Export = { _Type = 'IndexButton'; _Size = {26, 26}; _Mixin = ExportButton; _Point = {'RIGHT', '$parent.Close', 'LEFT', -4, 0}; _SetNormalTexture = CPAPI.GetAsset([[Textures\Frame\Export]]); _SetHighlightTexture = CPAPI.GetAsset([[Textures\Frame\Export]]); _SetThumbPosition = {'BOTTOM', 0.5}; }; Import = { _Type = 'IndexButton'; _Size = {26, 26}; _Mixin = ImportButton; _Point = {'RIGHT', '$parent.Export', 'LEFT', -4, 0}; _SetNormalTexture = CPAPI.GetAsset([[Textures\Frame\Import]]); _SetHighlightTexture = CPAPI.GetAsset([[Textures\Frame\Import]]); _SetThumbPosition = {'BOTTOM', 0.5}; }; }; }; }; }; })