From 77ee87198c360825772a20b79fa26de1b191d45e Mon Sep 17 00:00:00 2001 From: Florian Berthold Date: Fri, 22 May 2026 22:11:46 +0200 Subject: [PATCH] Initial commit: Chatter v1.2.11 (Curse package) --- Chatter.lua | 260 +++++++++ Chatter.toc | 32 ++ Libs/AceTab-3.0/AceConfigTab-3.0.lua | 105 ++++ Libs/AceTab-3.0/AceTab-3.0.lua | 443 +++++++++++++++ Libs/AceTab-3.0/AceTab-3.0.xml | 5 + Libs/LibSink-2.0/LibSink-2.0.lua | 782 +++++++++++++++++++++++++++ Libs/LibSink-2.0/lib.xml | 7 + Localization/deDE.lua | 268 +++++++++ Localization/enUS.lua | 341 ++++++++++++ Localization/esES.lua | 268 +++++++++ Localization/frFR.lua | 268 +++++++++ Localization/koKR.lua | 268 +++++++++ Localization/ruRU.lua | 269 +++++++++ Localization/zhCN.lua | 268 +++++++++ Localization/zhTW.lua | 268 +++++++++ Modules/AllResize.lua | 135 +++++ Modules/AltNames.lua | 352 ++++++++++++ Modules/AutoLogChat.lua | 16 + Modules/AutoPopup.lua | 84 +++ Modules/BNet.lua | 131 +++++ Modules/Buttons.lua | 234 ++++++++ Modules/ChannelColors.lua | 114 ++++ Modules/ChannelNames.lua | 199 +++++++ Modules/ChatFading.lua | 38 ++ Modules/ChatFont.lua | 182 +++++++ Modules/ChatFrameBorders.lua | 277 ++++++++++ Modules/ChatFrameBorders.xml | 39 ++ Modules/ChatLink.Lua | 65 +++ Modules/ChatScroll.lua | 160 ++++++ Modules/ChatTabs.lua | 304 +++++++++++ Modules/ClickInvite.lua | 134 +++++ Modules/CopyChat.lua | 228 ++++++++ Modules/DelayGMOTD.lua | 42 ++ Modules/EditBox.lua | 615 +++++++++++++++++++++ Modules/EditBoxHistory.lua | 41 ++ Modules/GroupSay.lua | 61 +++ Modules/Highlight.lua | 253 +++++++++ Modules/Justify.lua | 56 ++ Modules/LinkHover.lua | 53 ++ Modules/PlayerNames.lua | 711 ++++++++++++++++++++++++ Modules/Scrollback.lua | 90 +++ Modules/ServerStorage.lua | 74 +++ Modules/SplitText.lua | 98 ++++ Modules/StickyChannels.lua | 59 ++ Modules/Telltarget.lua | 55 ++ Modules/Timestamps.lua | 153 ++++++ Modules/TinyChat.lua | 42 ++ Modules/UrlCopy.lua | 535 ++++++++++++++++++ modules.xml | 37 ++ 49 files changed, 9519 insertions(+) create mode 100644 Chatter.lua create mode 100644 Chatter.toc create mode 100644 Libs/AceTab-3.0/AceConfigTab-3.0.lua create mode 100644 Libs/AceTab-3.0/AceTab-3.0.lua create mode 100644 Libs/AceTab-3.0/AceTab-3.0.xml create mode 100644 Libs/LibSink-2.0/LibSink-2.0.lua create mode 100644 Libs/LibSink-2.0/lib.xml create mode 100644 Localization/deDE.lua create mode 100644 Localization/enUS.lua create mode 100644 Localization/esES.lua create mode 100644 Localization/frFR.lua create mode 100644 Localization/koKR.lua create mode 100644 Localization/ruRU.lua create mode 100644 Localization/zhCN.lua create mode 100644 Localization/zhTW.lua create mode 100644 Modules/AllResize.lua create mode 100644 Modules/AltNames.lua create mode 100644 Modules/AutoLogChat.lua create mode 100644 Modules/AutoPopup.lua create mode 100644 Modules/BNet.lua create mode 100644 Modules/Buttons.lua create mode 100644 Modules/ChannelColors.lua create mode 100644 Modules/ChannelNames.lua create mode 100644 Modules/ChatFading.lua create mode 100644 Modules/ChatFont.lua create mode 100644 Modules/ChatFrameBorders.lua create mode 100644 Modules/ChatFrameBorders.xml create mode 100644 Modules/ChatLink.Lua create mode 100644 Modules/ChatScroll.lua create mode 100644 Modules/ChatTabs.lua create mode 100644 Modules/ClickInvite.lua create mode 100644 Modules/CopyChat.lua create mode 100644 Modules/DelayGMOTD.lua create mode 100644 Modules/EditBox.lua create mode 100644 Modules/EditBoxHistory.lua create mode 100644 Modules/GroupSay.lua create mode 100644 Modules/Highlight.lua create mode 100644 Modules/Justify.lua create mode 100644 Modules/LinkHover.lua create mode 100644 Modules/PlayerNames.lua create mode 100644 Modules/Scrollback.lua create mode 100644 Modules/ServerStorage.lua create mode 100644 Modules/SplitText.lua create mode 100644 Modules/StickyChannels.lua create mode 100644 Modules/Telltarget.lua create mode 100644 Modules/Timestamps.lua create mode 100644 Modules/TinyChat.lua create mode 100644 Modules/UrlCopy.lua create mode 100644 modules.xml diff --git a/Chatter.lua b/Chatter.lua new file mode 100644 index 0000000..c1ef21e --- /dev/null +++ b/Chatter.lua @@ -0,0 +1,260 @@ +Chatter = LibStub("AceAddon-3.0"):NewAddon("Chatter", "AceConsole-3.0", "AceHook-3.0") --, "AceHook-3.0", "AceTimer-3.0", "AceConsole-3.0", "AceEvent-3.0", "LibSink-2.0") +local L = LibStub("AceLocale-3.0"):GetLocale("Chatter") +local AceConfig = LibStub("AceConfig-3.0") +local AceConfigDialog = LibStub("AceConfigDialog-3.0") +local CreateFrame = _G.CreateFrame +local UIParent = _G.UIParent + +local optFrame + +local options = { + type = "group", + args = { + defaultArgs = { + type = "group", + name = L["Chatter"], + args = { + aceConfig = { + type = "execute", + name = L["Standalone Config"], + desc = L["Open a standalone config window. You might consider installing |cffffff00BetterBlizzOptions|r to make the Blizzard UI options panel resizable."], + func = function() + InterfaceOptionsFrame:Hide() + AceConfigDialog:SetDefaultSize("Chatter", 500, 550) + AceConfigDialog:Open("Chatter") + end + } + } + }, + config = { + type = "execute", + guiHidden = true, + name = L["Configure"], + desc = L["Configure"], + func = Chatter.OpenConfig + }, + modules = { + type = "group", + name = L["Modules"], + desc = L["Modules"], + args = {} + } + } +} + +local defaults = { + profile = { + modules = { + ["Disable Fading"] = false, + ["Chat Autolog"] = false, + ["Automatic Whisper Windows"] = false, + ["Server Positioning"] = false, + } + } +} +--[[ + Creating a prototype for a Decorate/UnDecorate function + Adding these in so after everything is loaded we can post decorate/undecorate the popup frames +--]] +local proto = { + Decorate = function(self,chatframe) end, + Popout = function(self,chatframe,srcChatFrame) end, + TempChatFrames = {}, + AddTempChat = function(self,name) table.insert(self.TempChatFrames,name) end, + AlwaysDecorate = function(self,chatframe) end, +} + +Chatter:SetDefaultModulePrototype(proto) +Chatter:SetDefaultModuleState(false) + +local optionFrames = {} +local ACD3 = LibStub("AceConfigDialog-3.0") + +function Chatter:OnInitialize() + self.db = LibStub("AceDB-3.0"):New("ChatterDB", defaults, "Default") + + LibStub("AceConfigRegistry-3.0"):RegisterOptionsTable("Chatter", options) + LibStub("AceConfigRegistry-3.0"):RegisterOptionsTable("ChatterModules", options.args.modules) + optFrame = ACD3:AddToBlizOptions("Chatter", nil, nil, "defaultArgs") + + for k, v in self:IterateModules() do + options.args.modules.args[k:gsub(" ", "_")] = { + type = "group", + name = (v.modName or k), + args = nil + } + local t + if v.GetOptions then + t = v:GetOptions() + t.settingsHeader = { + type = "header", + name = L["Settings"], + order = 12 + } + end + t = t or {} + t.toggle = { + type = "toggle", + name = v.toggleLabel or (L["Enable "] .. (v.modName or k)), + width = "double", + desc = v.Info and v:Info() or (L["Enable "] .. (v.modName or k)), + order = 11, + get = function() + return Chatter.db.profile.modules[k] ~= false or false + end, + set = function(info, v) + Chatter.db.profile.modules[k] = v + if v then + Chatter:EnableModule(k) + -- L["Module"] + Chatter:Print(L["Enabled"], k, L["Module"]) + else + Chatter:DisableModule(k) + Chatter:Print(L["Disabled"], k, L["Module"]) + end + end + } + t.header = { + type = "header", + name = v.modName or k, + order = 9 + } + if v.Info then + t.description = { + type = "description", + name = v:Info() .. "\n\n", + order = 10 + } + end + options.args.modules.args[k:gsub(" ", "_")].args = t + end + + local moduleList = {} + local moduleNames = {} + for k, v in pairs(options.args.modules.args) do + moduleList[v.name] = k + tinsert(moduleNames, v.name) + end + table.sort(moduleNames) + for _, name in ipairs(moduleNames) do + ACD3:AddToBlizOptions("ChatterModules", name, "Chatter", moduleList[name]) + end + + self:RegisterChatCommand("chatter", "OpenConfig") + + self.db.RegisterCallback(self, "OnProfileChanged", "SetUpdateConfig") + self.db.RegisterCallback(self, "OnProfileCopied", "SetUpdateConfig") + self.db.RegisterCallback(self, "OnProfileReset", "SetUpdateConfig") + + self:AddMenuHook(self, { + text = L["Chatter Settings"], + func = Chatter.OpenConfig, + notCheckable = 1 + }) + self:RawHook("FCF_Tab_OnClick", true) + self:RawHook("FCF_OpenTemporaryWindow",true) +end + +do + local info = {} + local menuHooks = {} + function Chatter:AddMenuHook(module, hook) + menuHooks[module] = hook + end + + function Chatter:RemoveMenuHook(module) + menuHooks[module] = nil + end + + function Chatter:FCF_Tab_OnClick(...) + self.hooks.FCF_Tab_OnClick(...) + for module, v in pairs(menuHooks) do + local menu + if type(v) == "table" then + menu = v + else + menu = module[v](module, ...) + end + UIDropDownMenu_AddButton(menu) + end + end +end + +function Chatter:FCF_OpenTemporaryWindow(chatType, chatTarget, sourceChatFrame, selectWindow) + local frame = self.hooks.FCF_OpenTemporaryWindow(chatType, chatTarget, sourceChatFrame, selectWindow) + if frame then + for k, v in self:IterateModules() do + if not frame.isDecorated then + v:AddTempChat(frame:GetName()) + end + if v:IsEnabled() and not frame.isDecorated then + v:Decorate(frame) + end + if v:IsEnabled() then + v:Popout(frame,sourceChatFrame or DEFAULT_CHAT_FRAME) + end + v:AlwaysDecorate(frame) + end + frame.isDecorated = true + end + FCFDock_ForceReanchoring(GENERAL_CHAT_DOCK) + return frame +end + +function Chatter:OpenConfig(input) + if input == "config" or not InterfaceOptionsFrame:IsResizable() then + options.args.defaultArgs.guiHidden = true + InterfaceOptionsFrame:Hide() + AceConfigDialog:SetDefaultSize("Chatter", 500, 550) + AceConfigDialog:Open("Chatter") + else + InterfaceOptionsFrame_OpenToCategory(Chatter.lastConfig) + options.args.defaultArgs.guiHidden = false + InterfaceOptionsFrame_OpenToCategory(optFrame) + end +end + +do + local timer, t = nil, 0 + local function update() + t = t + arg1 + if t > 0.5 then + timer:SetScript("OnUpdate", nil) + Chatter:UpdateConfig() + end + end + function Chatter:SetUpdateConfig() + t = 0 + timer = timer or CreateFrame("Frame", nil, UIParent) + timer:SetScript("OnUpdate", update) + end +end + +function Chatter:UpdateConfig() + for k, v in self:IterateModules() do + if v:IsEnabled() then + v:Disable() + v:Enable() + end + end +end + +function Chatter:OnEnable() + if not self.db.profile.welcomeMessaged then + self:Print(L["Welcome to Chatter! Type /chatter to configure."]) + self.db.profile.welcomeMessaged = true + end + for k, v in self:IterateModules() do + if self.db.profile.modules[k] ~= false then + v:Enable() + end + end + + if not options.args.Profiles then + options.args.Profiles = LibStub("AceDBOptions-3.0"):GetOptionsTable(self.db) + self.lastConfig = ACD3:AddToBlizOptions("Chatter", L["Profiles"], "Chatter", "Profiles") + end +end + +function Chatter:OnDisable() +end diff --git a/Chatter.toc b/Chatter.toc new file mode 100644 index 0000000..88392eb --- /dev/null +++ b/Chatter.toc @@ -0,0 +1,32 @@ +## Interface: 30300 +## Title: Chatter +## Notes: Lightweight Chat Improvements +## Notes-zhTW: 一個輕量級的聊天增強插件 +## Notes-zhCN: 一个轻量级的聊天增强插件 +## Author: Antiarc +## OptionalDeps: Ace3, LibSharedMedia-3.0, LibSink-2.0 +## X-Category: Chat/Communication +## Version: 1.1 +## SavedVariables: ChatterDB +## X-Curse-Packaged-Version: v1.2.11 +## X-Curse-Project-Name: Chatter +## X-Curse-Project-ID: chatter +## X-Curse-Repository-ID: wow/chatter/mainline + +#@no-lib-strip@ +Libs\AceTab-3.0\AceTab-3.0.xml +Libs\LibSink-2.0\lib.xml +#@end-no-lib-strip@ + + +Localization\enUS.lua +Localization\ruRU.lua +Localization\deDE.lua +Localization\frFR.lua +Localization\esES.lua +Localization\zhCN.lua +Localization\zhTW.lua +Localization\koKR.lua + +Chatter.lua +modules.xml diff --git a/Libs/AceTab-3.0/AceConfigTab-3.0.lua b/Libs/AceTab-3.0/AceConfigTab-3.0.lua new file mode 100644 index 0000000..9a150ee --- /dev/null +++ b/Libs/AceTab-3.0/AceConfigTab-3.0.lua @@ -0,0 +1,105 @@ +--- AceConfigTab-3.0 provides support for tab-completion to AceConfig tables. +-- Note: This library is not yet finalized. +-- @class file +-- @name AceConfigTab-3.0 +-- @release $Id: AceConfigTab-3.0.lua 769 2009-04-04 11:05:08Z nevcairiel $ + +local MAJOR, MINOR = "AceConfigTab-3.0", 1 +local lib = LibStub:NewLibrary(MAJOR, MINOR) + +if not lib then return end + +local ac = LibStub("AceConsole-3.0") + +local function printf(...) + DEFAULT_CHAT_FRAME:AddMessage(string.format(...)) +end + +-- getChildren(opt, ...) +-- +-- Retrieve the next valid group args in an AceConfig table. +-- +-- opt - AceConfig options table +-- ... - args following the slash command +-- +-- opt will need to be determined by the slash-command +-- The args will be obtained using AceConsole:GetArgs() or something similar on the remainder of the line. +-- +-- Returns arg1, arg2, ... +local function getLevel(opt, ...) + -- Walk down the options tree to the last arg in the commandline, or return if it does not follow the tree. + local path = "" + local lastChild + for i = 1, select('#', ...) do + local arg = select(i, ...) + if not arg or type(arg) == 'number' then break end + if opt.plugins then + for k in pairs(opt.plugins) do + if string.lower(k) == string.lower(arg) then + opt = opt.plugins[k] + path = path..arg.." " + lastChild = arg + break + end + end + elseif opt.args then + for k in pairs(opt.args) do + if string.lower(k) == string.lower(arg) then + opt = opt.args[k] + path = path..arg.." " + lastChild = arg + break + end + end + else + break + end + end + return opt, path +end + +local function getChildren(opt, ...) + local lastChild, path + opt, path, lastChild = getLevel(opt, ...) + local args = {} + for _, field in ipairs({"args", "plugins"}) do + if type(opt[field]) == 'table' then + for k in pairs(opt[field]) do + if opt[field].type ~= 'header' then + table.insert(args, k) + end + end + end + end + return args, path +end + +--LibStub("AceConfig-3.0"):RegisterOptionsTable("ag_UnitFrames", aUF.Options.table) +local function createWordlist(t, cmdline, pos) + local cmd = string.match(cmdline, "(/[^ \t\n]+)") + local argslist = string.sub(cmdline, pos, this:GetCursorPosition()) + local opt -- TODO: figure out options table using cmd + opt = LibStub("AceConfigRegistry-3.0"):GetOptionsTable("ag_UnitFrames", "cmd", "AceTab-3.0") -- hardcoded temporarily for testing + if not opt then return end + local args, path = getChildren(opt, ac:GetArgs(argslist, #argslist/2)) -- largest # of args representable by a string of length #argslist, since they must be separated by spaces + for _, v in ipairs(args) do + table.insert(t, path..v) + end +end + +local function usage(t, matches, _, cmdline) + local cmd = string.match(cmdline, "(/[^ \t\n]+)") + local argslist = string.sub(cmdline, #cmd, this:GetCursorPosition()) + local opt -- TODO: figure out options table using cmd + opt = LibStub("AceConfigRegistry-3.0"):GetOptionsTable("ag_UnitFrames")("cmd", "AceTab-3.0") -- hardcoded temporarily for testing + if not opt then return end + local level = getLevel(opt, ac:GetArgs(argslist, #argslist/2)) -- largest # of args representable by a string of length #argslist, since they must be separated by spaces + local option + for _, m in pairs(matches) do + local tail = string.match(m, "([^ \t\n]+)$") + option = level.plugins and level.plugins[tail] or level.args and level.args[tail] + printf("%s - %s", tail, option.desc) + end +end + +LibStub("AceTab-3.0"):RegisterTabCompletion("aguftest", "%/%w+ ", createWordlist, usage) diff --git a/Libs/AceTab-3.0/AceTab-3.0.lua b/Libs/AceTab-3.0/AceTab-3.0.lua new file mode 100644 index 0000000..dc2851f --- /dev/null +++ b/Libs/AceTab-3.0/AceTab-3.0.lua @@ -0,0 +1,443 @@ +--- AceTab-3.0 provides support for tab-completion. +-- Note: This library is not yet finalized. +-- @class file +-- @name AceTab-3.0 +-- @release $Id: AceTab-3.0.lua 947 2010-06-29 16:44:48Z nevcairiel $ + +local ACETAB_MAJOR, ACETAB_MINOR = 'AceTab-3.0', 8 +local AceTab, oldminor = LibStub:NewLibrary(ACETAB_MAJOR, ACETAB_MINOR) + +if not AceTab then return end -- No upgrade needed + +local is335 = GetBuildInfo() >= "3.3.5" + +AceTab.registry = AceTab.registry or {} + +-- local upvalues +local _G = _G +local pairs = pairs +local ipairs = ipairs +local type = type +local registry = AceTab.registry + +local strfind = string.find +local strsub = string.sub +local strlower = string.lower +local strformat = string.format +local strmatch = string.match + +local function printf(...) + DEFAULT_CHAT_FRAME:AddMessage(strformat(...)) +end + +local function getTextBeforeCursor(this, start) + return strsub(this:GetText(), start or 1, this:GetCursorPosition()) +end + +-- Hook OnTabPressed and OnTextChanged for the frame, give it an empty matches table, and set its curMatch to 0, if we haven't done so already. +local function hookFrame(f) + if f.hookedByAceTab3 then return end + f.hookedByAceTab3 = true + if f == (is335 and ChatEdit_GetActiveWindow() or ChatFrameEditBox) then + local origCTP = ChatEdit_CustomTabPressed + function ChatEdit_CustomTabPressed(...) + if AceTab:OnTabPressed(f) then + return origCTP(...) + else + return true + end + end + else + local origOTP = f:GetScript('OnTabPressed') + if type(origOTP) ~= 'function' then + origOTP = function() end + end + f:SetScript('OnTabPressed', function(...) + if AceTab:OnTabPressed(f) then + return origOTP(...) + end + end) + end + f.at3curMatch = 0 + f.at3matches = {} +end + +local firstPMLength + +local fallbacks, notfallbacks = {}, {} -- classifies completions into those which have preconditions and those which do not. Those without preconditions are only considered if no other completions have matches. +local pmolengths = {} -- holds the number of characters to overwrite according to pmoverwrite and the current prematch +-- ------------------------------------------------------------------------------ +-- RegisterTabCompletion( descriptor, prematches, wordlist, usagefunc, listenframes, postfunc, pmoverwrite ) +-- See http://www.wowace.com/wiki/AceTab-2.0 for detailed API documentation +-- +-- descriptor string Unique identifier for this tab completion set +-- +-- prematches string|table|nil String match(es) AFTER which this tab completion will apply. +-- AceTab will ignore tabs NOT preceded by the string(s). +-- If no value is passed, will check all tabs pressed in the specified editframe(s) UNLESS a more-specific tab complete applies. +-- +-- wordlist function|table Function that will be passed a table into which it will insert strings corresponding to all possible completions, or an equivalent table. +-- The text in the editbox, the position of the start of the word to be completed, and the uncompleted partial word +-- are passed as second, third, and fourth arguments, to facilitate pre-filtering or conditional formatting, if desired. +-- +-- usagefunc function|boolean|nil Usage statement function. Defaults to the wordlist, one per line. A boolean true squelches usage output. +-- +-- listenframes string|table|nil EditFrames to monitor. Defaults to ChatFrameEditBox. +-- +-- postfunc function|nil Post-processing function. If supplied, matches will be passed through this function after they've been identified as a match. +-- +-- pmoverwrite boolean|number|nil Offset the beginning of the completion string in the editbox when making a completion. Passing a boolean true indicates that we want to overwrite +-- the entire prematch string, and passing a number will overwrite that many characters prior to the cursor. +-- This is useful when you want to use the prematch as an indicator character, but ultimately do not want it as part of the text, itself. +-- +-- no return +-- ------------------------------------------------------------------------------ +function AceTab:RegisterTabCompletion(descriptor, prematches, wordlist, usagefunc, listenframes, postfunc, pmoverwrite) + -- Arg checks + if type(descriptor) ~= 'string' then error("Usage: RegisterTabCompletion(descriptor, prematches, wordlist, usagefunc, listenframes, postfunc, pmoverwrite): 'descriptor' - string expected.", 3) end + if prematches and type(prematches) ~= 'string' and type(prematches) ~= 'table' then error("Usage: RegisterTabCompletion(descriptor, prematches, wordlist, usagefunc, listenframes, postfunc, pmoverwrite): 'prematches' - string, table, or nil expected.", 3) end + if type(wordlist) ~= 'function' and type(wordlist) ~= 'table' then error("Usage: RegisterTabCompletion(descriptor, prematches, wordlist, usagefunc, listenframes, postfunc, pmoverwrite): 'wordlist' - function or table expected.", 3) end + if usagefunc and type(usagefunc) ~= 'function' and type(usagefunc) ~= 'boolean' then error("Usage: RegisterTabCompletion(descriptor, prematches, wordlist, usagefunc, listenframes, postfunc, pmoverwrite): 'usagefunc' - function or boolean expected.", 3) end + if listenframes and type(listenframes) ~= 'string' and type(listenframes) ~= 'table' then error("Usage: RegisterTabCompletion(descriptor, prematches, wordlist, usagefunc, listenframes, postfunc, pmoverwrite): 'listenframes' - string or table expected.", 3) end + if postfunc and type(postfunc) ~= 'function' then error("Usage: RegisterTabCompletion(descriptor, prematches, wordlist, usagefunc, listenframes, postfunc, pmoverwrite): 'postfunc' - function expected.", 3) end + if pmoverwrite and type(pmoverwrite) ~= 'boolean' and type(pmoverwrite) ~= 'number' then error("Usage: RegisterTabCompletion(descriptor, prematches, wordlist, usagefunc, listenframes, postfunc, pmoverwrite): 'pmoverwrite' - boolean or number expected.", 3) end + + local pmtable = type(prematches) == 'table' and prematches or {} + -- Mark this group as a fallback group if no value was passed. + if not prematches then + pmtable[1] = "" + fallbacks[descriptor] = true + -- Make prematches into a one-element table if it was passed as a string. + elseif type(prematches) == 'string' then + pmtable[1] = prematches + if prematches == "" then + fallbacks[descriptor] = true + else + notfallbacks[descriptor] = true + end + end + + -- Make listenframes into a one-element table if it was not passed a table of frames. + if not listenframes then -- default + if is335 then + listenframes = {} + for i = 1, NUM_CHAT_WINDOWS do + listenframes[i] = _G["ChatFrame"..i.."EditBox"] + end + else + listenframes = { ChatFrameEditBox } + end + elseif type(listenframes) ~= 'table' or type(listenframes[0]) == 'userdata' and type(listenframes.IsObjectType) == 'function' then -- single frame or framename + listenframes = { listenframes } + end + + -- Hook each registered listenframe and give it a matches table. + for _, f in pairs(listenframes) do + if type(f) == 'string' then + f = _G[f] + end + if type(f) ~= 'table' or type(f[0]) ~= 'userdata' or type(f.IsObjectType) ~= 'function' then + error(format(ACETAB_MAJOR..": Cannot register frame %q; it does not exist", f:GetName())) + end + if f then + if f:GetObjectType() ~= 'EditBox' then + error(format(ACETAB_MAJOR..": Cannot register frame %q; it is not an EditBox", f:GetName())) + else + hookFrame(f) + end + end + end + + -- Everything checks out; register this completion. + if not registry[descriptor] then + registry[descriptor] = { prematches = pmtable, wordlist = wordlist, usagefunc = usagefunc, listenframes = listenframes, postfunc = postfunc, pmoverwrite = pmoverwrite } + end +end + +function AceTab:IsTabCompletionRegistered(descriptor) + return registry and registry[descriptor] +end + +function AceTab:UnregisterTabCompletion(descriptor) + registry[descriptor] = nil + pmolengths[descriptor] = nil + fallbacks[descriptor] = nil + notfallbacks[descriptor] = nil +end + +-- ------------------------------------------------------------------------------ +-- gcbs( s1, s2 ) +-- +-- s1 string First string to be compared +-- +-- s2 string Second string to be compared +-- +-- returns the greatest common substring beginning s1 and s2 +-- ------------------------------------------------------------------------------ +local function gcbs(s1, s2) + if not s1 and not s2 then return end + if not s1 then s1 = s2 end + if not s2 then s2 = s1 end + if #s2 < #s1 then + s1, s2 = s2, s1 + end + if strfind(strlower(s2), "^"..strlower(s1)) then + return s1 + else + return gcbs(strsub(s1, 1, -2), s2) + end +end + +local cursor -- Holds cursor position. Set in :OnTabPressed(). +-- ------------------------------------------------------------------------------ +-- cycleTab() +-- For when a tab press has multiple possible completions, we need to allow the user to press tab repeatedly to cycle through them. +-- If we have multiple possible completions, all tab presses after the first will call this function to cycle through and insert the different possible matches. +-- This function will stop being called after OnTextChanged() is triggered by something other than AceTab (i.e. the user inputs a character). +-- ------------------------------------------------------------------------------ +local previousLength, cMatch, matched, postmatch +local function cycleTab(this) + cMatch = 0 -- Counter across all sets. The pseudo-index relevant to this value and corresponding to the current match is held in this.at3curMatch + matched = false + + -- Check each completion group registered to this frame. + for desc, compgrp in pairs(this.at3matches) do + + -- Loop through the valid completions for this set. + for m, pm in pairs(compgrp) do + cMatch = cMatch + 1 + if cMatch == this.at3curMatch then -- we're back to where we left off last time through the combined list + this.at3lastMatch = m + this.at3lastWord = pm + this.at3curMatch = cMatch + 1 -- save the new cMatch index + matched = true + break + end + end + if matched then break end + end + + -- If our index is beyond the end of the list, reset the original uncompleted substring and let the cycle start over next time tab is pressed. + if not matched then + this.at3lastMatch = this.at3origMatch + this.at3lastWord = this.at3origWord + this.at3curMatch = 1 + end + + -- Insert the completion. + this:HighlightText(this.at3matchStart-1, cursor) + this:Insert(this.at3lastWord or '') + this.at3_last_precursor = getTextBeforeCursor(this) or '' +end + +local IsSecureCmd = IsSecureCmd + +local cands, candUsage = {}, {} +local numMatches = 0 +local firstMatch, hasNonFallback, allGCBS, setGCBS, usage +local text_precursor, text_all, text_pmendToCursor +local matches, usagefunc -- convenience locals + +-- Fill the this.at3matches[descriptor] tables with matching completion pairs for each entry, based on +-- the partial string preceding the cursor position and using the corresponding registered wordlist. +-- +-- The entries of the matches tables are of the format raw_match = formatted_match, where raw_match is the plaintext completion and +-- formatted_match is the match after being formatted/altered/processed by the registered postfunc. +-- If no postfunc exists, then the formatted and raw matches are the same. +local pms, pme, pmt, prematchStart, prematchEnd, text_prematch, entry +local function fillMatches(this, desc, fallback) + entry = registry[desc] + -- See what frames are registered for this completion group. If the frame in which we pressed tab is one of them, then we start building matches. + for _, f in ipairs(entry.listenframes) do + if f == this then + + -- Try each precondition string registered for this completion group. + for _, prematch in ipairs(entry.prematches) do + + -- Test if our prematch string is satisfied. + -- If it is, then we find its last occurence prior to the cursor, calculate and store its pmoverwrite value (if applicable), and start considering completions. + if fallback then prematch = "%s" end + + -- Find the last occurence of the prematch before the cursor. + pms, pme, pmt = nil, 1, '' + text_prematch, prematchEnd, prematchStart = nil, nil, nil + while true do + pms, pme, pmt = strfind(text_precursor, "("..prematch..")", pme) + if pms then + prematchStart, prematchEnd, text_prematch = pms, pme, pmt + pme = pme + 1 + else + break + end + end + + if not prematchStart and fallback then + prematchStart, prematchEnd, text_prematch = 0, 0, '' + end + if prematchStart then + -- text_pmendToCursor should be the sub-word/phrase to be completed. + text_pmendToCursor = strsub(text_precursor, prematchEnd + 1) + + -- How many characters should we eliminate before the completion before writing it in. + pmolengths[desc] = entry.pmoverwrite == true and #text_prematch or entry.pmoverwrite or 0 + + -- This is where we will insert completions, taking the prematch overwrite into account. + this.at3matchStart = prematchEnd + 1 - (pmolengths[desc] or 0) + + -- We're either a non-fallback set or all completions thus far have been fallback sets, and the precondition matches. + -- Create cands from the registered wordlist, filling it with all potential (unfiltered) completion strings. + local wordlist = entry.wordlist + local cands = type(wordlist) == 'table' and wordlist or {} + if type(wordlist) == 'function' then + wordlist(cands, text_all, prematchEnd + 1, text_pmendToCursor) + end + if cands ~= false then + matches = this.at3matches[desc] or {} + for i in pairs(matches) do matches[i] = nil end + + -- Check each of the entries in cands to see if it completes the word before the cursor. + -- Finally, increment our match count and set firstMatch, if appropriate. + for _, m in ipairs(cands) do + if strfind(strlower(m), strlower(text_pmendToCursor), 1, 1) == 1 then -- we have a matching completion! + hasNonFallback = not fallback + matches[m] = entry.postfunc and entry.postfunc(m, prematchEnd + 1, text_all) or m + numMatches = numMatches + 1 + if numMatches == 1 then + firstMatch = matches[m] + firstPMLength = pmolengths[desc] or 0 + end + end + end + this.at3matches[desc] = numMatches > 0 and matches or nil + end + end + end + end + end +end + +function AceTab:OnTabPressed(this) + if this:GetText() == '' then return true end + + -- allow Blizzard to handle slash commands, themselves + if this == (is335 and ChatEdit_GetActiveWindow() or ChatFrameEditBox) then + local command = this:GetText() + if strfind(command, "^/[%a%d_]+$") then + return true + end + local cmd = strmatch(command, "^/[%a%d_]+") + if cmd and IsSecureCmd(cmd) then + return true + end + end + + cursor = this:GetCursorPosition() + + text_all = this:GetText() + text_precursor = getTextBeforeCursor(this) or '' + + -- If we've already found some matches and haven't done anything since the last tab press, then (continue) cycling matches. + -- Otherwise, reset this frame's matches and proceed to creating our list of possible completions. + this.at3lastMatch = this.at3curMatch > 0 and (this.at3lastMatch or this.at3origWord) + -- Detects if we've made any edits since the last tab press. If not, continue cycling completions. + if text_precursor == this.at3_last_precursor then + return cycleTab(this) + else + for i in pairs(this.at3matches) do this.at3matches[i] = nil end + this.at3curMatch = 0 + this.at3origWord = nil + this.at3origMatch = nil + this.at3lastWord = nil + this.at3lastMatch = nil + this.at3_last_precursor = text_precursor + end + + numMatches = 0 + firstMatch = nil + firstPMLength = 0 + hasNonFallback = false + for i in pairs(pmolengths) do pmolengths[i] = nil end + + for desc in pairs(notfallbacks) do + fillMatches(this, desc) + end + if not hasNonFallback then + for desc in pairs(fallbacks) do + fillMatches(this, desc, true) + end + end + + if not firstMatch then + this.at3_last_precursor = "\0" + return true + end + + -- We want to replace the entire word with our completion, so highlight it up to the cursor. + -- If only one match exists, then stick it in there and append a space. + if numMatches == 1 then + -- HighlightText takes the value AFTER which the highlighting starts, so we have to subtract 1 to have it start before the first character. + this:HighlightText(this.at3matchStart-1, cursor) + + this:Insert(firstMatch) + this:Insert(" ") + else + -- Otherwise, we want to begin cycling through the valid completions. + -- Beginning a cycle also causes the usage statement to be printed, if one exists. + + -- Print usage statements for each possible completion (and gather up the GCBS of all matches while we're walking the tables). + allGCBS = nil + for desc, matches in pairs(this.at3matches) do + -- Don't print usage statements for fallback completion groups if we have 'real' completion groups with matches. + if hasNonFallback and fallbacks[desc] then break end + + -- Use the group's description as a heading for its usage statements. + DEFAULT_CHAT_FRAME:AddMessage(desc..":") + + usagefunc = registry[desc].usagefunc + if not usagefunc then + -- No special usage processing; just print a list of the (formatted) matches. + for m, fm in pairs(matches) do + DEFAULT_CHAT_FRAME:AddMessage(fm) + allGCBS = gcbs(allGCBS, m) + end + else + -- Print a usage statement based on the corresponding registered usagefunc. + -- candUsage is the table passed to usagefunc to be filled with candidate = usage_statement pairs. + if type(usagefunc) == 'function' then + for i in pairs(candUsage) do candUsage[i] = nil end + + -- usagefunc takes the greatest common substring of valid matches as one of its args, so let's find that now. + -- TODO: Make the GCBS function accept a vararg or table, after which we can just pass in the list of matches. + setGCBS = nil + for m in pairs(matches) do + setGCBS = gcbs(setGCBS, m) + end + allGCBS = gcbs(allGCBS, setGCBS) + usage = usagefunc(candUsage, matches, setGCBS, strsub(text_precursor, 1, prematchEnd)) + + -- If the usagefunc returns a string, then the entire usage statement has been taken care of by usagefunc, and we need only to print it... + if type(usage) == 'string' then + DEFAULT_CHAT_FRAME:AddMessage(usage) + + -- ...otherwise, it should have filled candUsage with candidate-usage statement pairs, and we need to print the matching ones. + elseif next(candUsage) and numMatches > 0 then + for m, fm in pairs(matches) do + if candUsage[m] then DEFAULT_CHAT_FRAME:AddMessage(strformat("%s - %s", fm, candUsage[m])) end + end + end + end + end + + -- Replace the original string with the greatest common substring of all valid completions. + this.at3curMatch = 1 + this.at3origWord = strsub(text_precursor, this.at3matchStart, this.at3matchStart + pmolengths[desc] - 1) .. allGCBS or "" + this.at3origMatch = allGCBS or "" + this.at3lastWord = this.at3origWord + this.at3lastMatch = this.at3origMatch + + this:HighlightText(this.at3matchStart-1, cursor) + this:Insert(this.at3origWord) + this.at3_last_precursor = getTextBeforeCursor(this) or '' + end + end +end diff --git a/Libs/AceTab-3.0/AceTab-3.0.xml b/Libs/AceTab-3.0/AceTab-3.0.xml new file mode 100644 index 0000000..3121436 --- /dev/null +++ b/Libs/AceTab-3.0/AceTab-3.0.xml @@ -0,0 +1,5 @@ + +