Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
cb2c995a82 | |||
a29f6486fe | |||
ecd6e5c340 | |||
cc26683328 | |||
b572203dd2 | |||
db3db16594 | |||
32a692279a |
4
.docmeta
Normal file
4
.docmeta
Normal file
@@ -0,0 +1,4 @@
|
||||
-
|
||||
type: markdown
|
||||
input-file: README.md
|
||||
output-page: "Main"
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -37,7 +37,7 @@ out/
|
||||
obj/
|
||||
|
||||
# Misc
|
||||
twitch/
|
||||
.bin/
|
||||
*.log
|
||||
*.graphml
|
||||
coverage.db*
|
||||
|
25
.pkgmeta
Normal file
25
.pkgmeta
Normal file
@@ -0,0 +1,25 @@
|
||||
package-as: Grichelde
|
||||
|
||||
enable-nolib-creation: no
|
||||
|
||||
externals:
|
||||
libs/LibStub: https://repos.wowace.com/wow/libstub/tags/1.0
|
||||
libs/CallbackHandler-1.0: https://repos.wowace.com/wow/callbackhandler/trunk/CallbackHandler-1.0
|
||||
libs/AceAddon-3.0: https://repos.wowace.com/wow/ace3/trunk/AceAddon-3.0
|
||||
libs/AceConfig-3.0: https://repos.wowace.com/wow/ace3/trunk/AceConfig-3.0
|
||||
libs/AceConsole-3.0: https://repos.wowace.com/wow/ace3/trunk/AceConsole-3.0
|
||||
libs/AceDB-3.0: https://repos.wowace.com/wow/ace3/trunk/AceDB-3.0
|
||||
libs/AceDBOptions-3.0: https://repos.wowace.com/wow/ace3/trunk/AceDBOptions-3.0
|
||||
libs/AceEvent-3.0: https://repos.wowace.com/wow/ace3/trunk/AceEvent-3.0
|
||||
libs/AceGUI-3.0: https://repos.wowace.com/wow/ace3/trunk/AceGUI-3.0
|
||||
libs/AceHook-3.0: https://repos.wowace.com/wow/ace3/trunk/AceHook-3.0
|
||||
libs/AceLocale-3.0: https://repos.wowace.com/wow/ace3/trunk/AceLocale-3.0
|
||||
|
||||
ignore:
|
||||
- twitch
|
||||
|
||||
manual-changelog:
|
||||
filename: CHANGELOG.md
|
||||
markup-type: markdown
|
||||
|
||||
license-output: LICENSE
|
60
CHANGELOG.md
60
CHANGELOG.md
@@ -3,13 +3,57 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased] Version 1.0 - 2020-05-27
|
||||
First version to be released
|
||||
## [Upcoming] Version 0.7.1-beta - 2020-06-08
|
||||
### Added
|
||||
- slash commands
|
||||
- Options GUI
|
||||
- store settings globally or per character
|
||||
- added translations
|
||||
- handle replacement via slash command
|
||||
|
||||
## Version 0.7.0-beta - 2020-06-07
|
||||
### Added
|
||||
- order buttons
|
||||
### Changed
|
||||
- use numeric LogLevel over booleans
|
||||
- exact case option reversed (again)
|
||||
- smart case handling if replacement is longer than match
|
||||
### Fixed
|
||||
- Deletion of all mappings
|
||||
|
||||
## Version 0.6.0 (unreleased) - 2020-06-05
|
||||
### Added
|
||||
- honour capital/mixed cases for ignore case
|
||||
- consolidate consecutive matches
|
||||
- database upgrade capability
|
||||
### Changed
|
||||
- case sensitivity option reversed
|
||||
- removed obsolete WIM handling
|
||||
### Fixed
|
||||
- skip empty mapping
|
||||
- Deletion of mapping
|
||||
|
||||
## Version 0.5.0 - 2020-06-01
|
||||
### Added
|
||||
- add replacements via options UI
|
||||
|
||||
## Version 0.4.0 (unreleased) - 2020-05-30
|
||||
### Added
|
||||
- restructured files
|
||||
- extract functions and color codes
|
||||
- filter channels
|
||||
|
||||
## Version 0.3.0 - 2020-05-27
|
||||
### Fixed
|
||||
- fixed DB storange and debug printing
|
||||
|
||||
## Version 0.2.2 (unreleased) - 2020-05-26
|
||||
### Added
|
||||
- added Options UI under Interface Options
|
||||
- store settings in profiles
|
||||
- added more translations
|
||||
|
||||
## Version 0.2.1 (unreleased) - 2020-05-25
|
||||
### Added
|
||||
- support automatic packaging for curseforge via .pkgmeta
|
||||
- include project logo
|
||||
- auto-update the project description via .docmeta
|
||||
|
||||
## Version 0.2 - 2020-05-25
|
||||
### Added
|
||||
@@ -18,6 +62,6 @@ First version to be released
|
||||
- handle SendChatMessage ordering if addon Misspelled is also installed
|
||||
- break long texts in chunks of 255 length
|
||||
|
||||
## Version 0.1 - 2020-05-24
|
||||
## Version 0.1 (unreleased) - 2020-05-24
|
||||
### Added
|
||||
- bootstrap addon with Ace3 from [Misspelled](https://www.curseforge.com/wow/addons/misspelled)
|
||||
- bootstrap addon with Ace3 based on [Misspelled](https://www.curseforge.com/wow/addons/misspelled)
|
374
Grichelde.lua
374
Grichelde.lua
@@ -1,365 +1,71 @@
|
||||
--[[---------------------------------------------------------------------------
|
||||
Grichelde
|
||||
|
||||
Grichelde - Text Replacer
|
||||
Copyright 2020 Teilzeit-Jedi <tj@teilzeit-jedi.de>
|
||||
|
||||
based on Misspelled developed by Nathan Pieper - nrpieper (@) gmail (dot) com
|
||||
This addon is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
This code freely distributed for your use in any GPL compliant project.
|
||||
See conditions in the LICENSE file attached.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with the addon. If not, see <http://www.gnu.org/licenses/gpl-3.0.txt>.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
--------------------------------------------------------------------------]] --
|
||||
-----------------------------------------------------------------------------]]
|
||||
|
||||
-- read namespace from global env
|
||||
local AddonName, AddonTable = ...
|
||||
local _G = _G
|
||||
|
||||
local Grichelde = LibStub("AceAddon-3.0"):NewAddon("Grichelde", "AceEvent-3.0", "AceHook-3.0")
|
||||
-- initialize addon
|
||||
local Grichelde = LibStub("AceAddon-3.0"):NewAddon(AddonTable, AddonName, "AceConsole-3.0", "AceEvent-3.0", "AceHook-3.0")
|
||||
Grichelde.L = LibStub("AceLocale-3.0"):GetLocale("Grichelde", true)
|
||||
Grichelde.version = GetAddOnMetadata(AddonName, "Version")
|
||||
Grichelde.build = GetAddOnMetadata(AddonName, "X-Build") or "UNKNOWN"
|
||||
Grichelde.build = GetAddOnMetadata(AddonName, "X-Build") or "Experimental"
|
||||
Grichelde.classic = _G.WOW_PROJECT_ID == _G.WOW_PROJECT_CLASSIC
|
||||
Grichelde.logLevel = 0 -- cannot reference Grichelde.LOG_LEVELs here as they are loaded afterwards
|
||||
|
||||
local L = LibStub("AceLocale-3.0"):GetLocale("Grichelde", true)
|
||||
|
||||
local Grichelde_Debug = false
|
||||
|
||||
-- faster function lookups by mapping to local refs
|
||||
local string_find = string.find
|
||||
local string_gsub = string.gsub
|
||||
local string_len = string.len
|
||||
local string_rep = string.rep
|
||||
local string_sub = string.sub
|
||||
local strtrim = strtrim
|
||||
local strmatch = strmatch
|
||||
local tostring = tostring
|
||||
local tInsert = table.insert
|
||||
local tContains = tContains
|
||||
local pairs = pairs
|
||||
local ipairs = ipairs
|
||||
|
||||
local Grichelde_Hooks = {}
|
||||
|
||||
--local Grichelde_ChatTypes = { "SAY", "EMOTE", "YELL", "PARTY", "GUILD", "OFFICER", "RAID", "RAID_WARNING", "INSTANCE_CHAT", "BATTLEGROUND", "WHISPER" }
|
||||
local Grichelde_ChatTypes = { "SAY", "EMOTE", "YELL", "PARTY", "GUILD" }
|
||||
local Grichelde_ChatCommands = { "/s", "/e", "/me", "/y", "/p", "/pl", "/g", "/o", "/raid", "/rl", "/rw", "/i", "bg", "/w", "/r", "/tt" }
|
||||
|
||||
-- do not replace these patterns
|
||||
local Grichelde_IgnorePatterns = {
|
||||
"|[Cc]%x%x%x%x%x%x%x%x.-|r", -- colored items (or links)
|
||||
"|H.-|h", -- item links (http://www.wowwiki.com/ItemLink)
|
||||
"|T.-|t", -- textures
|
||||
"|n", -- newline
|
||||
"{rt[1-8]}", -- rumbered raid target icons
|
||||
"{Star}", -- named raid target icon 1
|
||||
"{Circle}", -- named raid target icon 2
|
||||
"{Coin}", -- named raid target icon 2
|
||||
"{Diamond}", -- named raid target icon 3
|
||||
"{Triangle}", -- named raid target icon 4
|
||||
"{Moon}", -- named raid target icon 5
|
||||
"{Square}", -- named raid target icon 6
|
||||
"{Cross}", -- named raid target icon 7
|
||||
"{X}", -- named raid target icon 7
|
||||
"{Skull}" -- named raid target icon 8
|
||||
}
|
||||
-- publish to global env
|
||||
_G.Grichelde = Grichelde
|
||||
|
||||
-- Ace3 callbacks
|
||||
function Grichelde:OnInitialize()
|
||||
-- Build Interface Options window
|
||||
--self:CreateInterfaceOptions()
|
||||
self.db = self:LoadDatabase()
|
||||
self:UpgradeDatabase()
|
||||
|
||||
-- Watch for WIM and Prat to Load, then integrate
|
||||
self:RegisterEvent("ADDON_LOADED", "HookIntoForOtherChatAddons")
|
||||
self.options, self.dialog = self:SetupOptions()
|
||||
|
||||
-- populate UI from database
|
||||
self:RefreshOptions("OnProfileChanged")
|
||||
|
||||
self:SetupSlashCommands()
|
||||
end
|
||||
|
||||
function Grichelde:OnEnable()
|
||||
-- Hook in before message is sent to replace all character occurrences where replacements have been defined in the options
|
||||
self:RawHook("SendChatMessage", true)
|
||||
|
||||
if (Misspelled) then
|
||||
print("Misspelled detected, Grichelde will have any messsage being cleansed")
|
||||
end
|
||||
|
||||
-- tell the world we are listening
|
||||
print(L.AddonLoaded)
|
||||
self:Print(self.L.AddonLoaded, self.COLOR_CODES.PREFIX .. self.L.AddonName .. " " .. self.L.VersionAbbr .. self.version .. self.COLOR_CODES.CLOSE)
|
||||
end
|
||||
|
||||
function Grichelde:OnDisable()
|
||||
self:Unhook("SendChatMessage")
|
||||
end
|
||||
|
||||
--- @param event string
|
||||
--- @param addonName string
|
||||
function Grichelde:HookIntoForOtherChatAddons(event, addonName)
|
||||
if event == "ADDON_LOADED" then
|
||||
if addonName == "WIM" then
|
||||
WIM.RegisterWidgetTrigger("msg_box", "whisper,chat,w2w", "OnEnterPressed", Grichelde.EditBox_OnEnterPressed)
|
||||
|
||||
-- If available use the WIM API
|
||||
if (WIM.RegisterPreSendFilterText) then -- avoid error if WIM not up to date.
|
||||
WIM.RegisterPreSendFilterText(function(text)
|
||||
return Grichelde:CheckAndReplace(text)
|
||||
end)
|
||||
else
|
||||
-- WIM sends its chat messages via the API ChatThrottleLib, which itself hooks the default SendChatMessage api
|
||||
-- many times before Grichelde will. ChatThrottleLib might potentially load before Grichelde, so we just hook
|
||||
-- into ChatThrottleLib to be on the safe side.
|
||||
|
||||
if (ChatThrottleLib) then
|
||||
Grichelde_Hooks["ChatThrottleLib"] = ChatThrottleLib.SendChatMessage
|
||||
|
||||
function ChatThrottleLib:SendChatMessage(prio, prefix, text, ...)
|
||||
Grichelde:DebugPrint("ChatThrottleLib:SendChatMessage : Hook called")
|
||||
local replacedText = Grichelde:CheckAndReplace(text)
|
||||
return Grichelde_Hooks["ChatThrottleLib"](ChatThrottleLib, prio, prefix, replacedText, ...)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Before af chat message is sent, check if replacement is required and replace the text accordingly.
|
||||
--- @param message string
|
||||
--- @param type string
|
||||
--- @param language string
|
||||
--- @param channel string
|
||||
function Grichelde:SendChatMessage(message, type, language, channel, ...)
|
||||
local replacedText = self:CheckAndReplace(message, type)
|
||||
|
||||
self:DebugPrint("SendChatMessage : replacedText: " .. replacedText)
|
||||
|
||||
-- Send text in chunks if length exceeds 255 bytes after replacement
|
||||
local chunks = self:SplitText(replacedText)
|
||||
self:DebugPrint("SendChatMessage : #chunks: " .. #chunks)
|
||||
|
||||
for _, chunk in ipairs(chunks) do
|
||||
self.hooks["SendChatMessage"](chunk, type, language, channel, ...);
|
||||
end
|
||||
end
|
||||
|
||||
function Grichelde:CheckAndReplace(message, type)
|
||||
local text = message
|
||||
if (Misspelled) then
|
||||
self:DebugPrint("Misspelled detected: cleansing message")
|
||||
text = Misspelled:RemoveHighlighting(text)
|
||||
end
|
||||
text = strtrim(text)
|
||||
|
||||
if (self:CheckReplacement(text, type)) then
|
||||
text = self:ReplaceText(text)
|
||||
end
|
||||
return text
|
||||
end
|
||||
|
||||
function Grichelde:CheckReplacement(text, type)
|
||||
-- todo: globally disabled?
|
||||
|
||||
-- check type
|
||||
if (not tContains(Grichelde_ChatTypes, type)) then
|
||||
self:DebugPrint("CheckReplacement : skip channel type")
|
||||
return false
|
||||
end
|
||||
|
||||
-- don't replace slash commands except chat related commands
|
||||
if string_sub(text, 1, 1) == "/" then
|
||||
local firstWord, _ = self:SplitOnFirstMatch(text)
|
||||
if (firstWord == nil or not tContains(Grichelde_ChatCommands, firstWord)) then
|
||||
self:DebugPrint("CheckReplacement : ignore slash command")
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- in any other case
|
||||
return true
|
||||
end
|
||||
|
||||
--- Checks if the text starts with a preversable ignore pattern, such as itemLinks, textures or raid target icons
|
||||
--- and returns the end location of the match, or 0 if no pattern was found
|
||||
--- @param text string
|
||||
--- @return number
|
||||
function Grichelde:CheckForPreversableText(text)
|
||||
self:DebugPrint("CheckForPreversableText : text is " .. text)
|
||||
-- Calling find on ever pattern might be inefficient but its way less code.
|
||||
for _, pattern in ipairs(Grichelde_IgnorePatterns) do
|
||||
local pos1, pos2 = string_find(text, pattern)
|
||||
if pos1 == 1 and pos2 ~= nil then
|
||||
self:DebugPrint("CheckForPreversableText : Found ignore pattern " .. pattern .. " at (" .. pos1 .. "," .. pos2 .. ")")
|
||||
return pos2
|
||||
end
|
||||
end
|
||||
self:DebugPrint("CheckForPreversableText : no ignore pattern found")
|
||||
return 0
|
||||
end
|
||||
|
||||
--- Replaces all character occurrences for which replacements have been defined in the options,
|
||||
--- while preserving any itemLinks or textures. (http://www.wowwiki.com/ItemLink)
|
||||
--- @param text string
|
||||
--- @return string
|
||||
function Grichelde:ReplaceText(text)
|
||||
local finalText = ""
|
||||
local newText = text
|
||||
|
||||
-- don't replace non-chat related slash commands
|
||||
local firstWord, line = self:SplitOnFirstMatch(text)
|
||||
if (firstWord ~= nil and tContains(Grichelde_ChatCommands, firstWord)) then
|
||||
self:DebugPrint("ReplaceText : Found slash command " .. (firstWord + "") )
|
||||
-- skip chat slash command
|
||||
finalText = finalText .. firstWord .. ' '
|
||||
newText = line
|
||||
end
|
||||
|
||||
local current = 1
|
||||
local lastStart = 1
|
||||
|
||||
while current <= string_len(newText) do
|
||||
local currentChar = string_sub(newText, current, current)
|
||||
self:DebugPrint("current/char : " .. current .. "," .. currentChar)
|
||||
|
||||
if currentChar ~= '|' and currentChar ~= '{' then
|
||||
current = current + 1
|
||||
--- Register slash commands 'gri' and 'grichelde'
|
||||
function Grichelde:SetupSlashCommands()
|
||||
local function HandleSlashCommand(input)
|
||||
-- Show the GUI if no input is supplied, otherwise handle the chat input.
|
||||
if self.functions.nilOrEmpty(input) then
|
||||
LibStub("AceConfigDialog-3.0"):Open(self.name)
|
||||
else
|
||||
|
||||
-- lookahead-check for itemLinks, textures and raid target icons
|
||||
local textAhead = string_sub(newText, current)
|
||||
local posEnd = self:CheckForPreversableText(textAhead)
|
||||
if posEnd > 0 then
|
||||
self:DebugPrint("ReplaceText : Found an ignore pattern")
|
||||
|
||||
local textBehind = string_sub(newText, lastStart, current - 1)
|
||||
local replacement = self:ReplaceCharacters(textBehind)
|
||||
local preservedText = string_sub(textAhead, 1, posEnd)
|
||||
|
||||
finalText = finalText .. replacement .. preservedText
|
||||
current = current + posEnd
|
||||
lastStart = current
|
||||
self:DebugPrint("ReplaceText : restarting at " .. lastStart)
|
||||
else
|
||||
-- no corresponding end was found to start pattern, continue loop with next char
|
||||
current = current + 1
|
||||
end
|
||||
-- handle slash ourselves
|
||||
self:Print("Handle slash command: " .. input)
|
||||
end
|
||||
end
|
||||
|
||||
-- cleanup remaining text to the end
|
||||
local remainingText = string_sub(newText, lastStart)
|
||||
local replacement = self:ReplaceCharacters(remainingText)
|
||||
finalText = finalText .. replacement
|
||||
|
||||
self:DebugPrint("ReplaceText : replaced \"" .. text .. "\"")
|
||||
self:DebugPrint("ReplaceText : with \"" .. finalText .. "\"")
|
||||
return finalText
|
||||
end
|
||||
|
||||
--- Replaces all character occurrences for which replacements have been defined in the options
|
||||
--- @param text string
|
||||
--- @return string
|
||||
function Grichelde:ReplaceCharacters(text)
|
||||
-- todo: read from options
|
||||
-- todo: case (in)sensitivity
|
||||
local replacement = text
|
||||
replacement = string_gsub(replacement, "s", "ch")
|
||||
replacement = string_gsub(replacement, "S", "Ch")
|
||||
replacement = string_gsub(replacement, "t", "k")
|
||||
replacement = string_gsub(replacement, "T", "K")
|
||||
self:DebugPrint("ReplaceCharacters : replaced \"" .. text .. "\" with \"" .. replacement .. "\"")
|
||||
return replacement
|
||||
end
|
||||
|
||||
--- Splits a long text in longest possible chunks of <= 255 length, split at last available space
|
||||
--- @param text string
|
||||
--- @return table
|
||||
function Grichelde:SplitText(text)
|
||||
local chunks = {}
|
||||
local splitText = text
|
||||
local textSize = string_len(splitText)
|
||||
|
||||
while textSize > 255 do
|
||||
local chunk = string_sub(splitText, 1, 255)
|
||||
local remaining = ""
|
||||
|
||||
-- special case: if space is the start of the next chunk, don't split this chunk
|
||||
if string_sub(splitText, 256, 256) ~= ' ' then
|
||||
-- split at last space, don't assign directly as nil might be returned
|
||||
local left, right = self:SplitOnLastMatch(chunk)
|
||||
if left ~= nil then
|
||||
chunk = left
|
||||
end
|
||||
if right ~= nil then
|
||||
remaining = right
|
||||
end
|
||||
end
|
||||
|
||||
self:DebugPrint("SplitText : chunk: " .. chunk )
|
||||
|
||||
tInsert(chunks, chunk)
|
||||
splitText = remaining .. string_sub(splitText, 256)
|
||||
textSize = string_len(splitText)
|
||||
end
|
||||
|
||||
-- pickup remaining text < 255
|
||||
self:DebugPrint("SplitText : last chunk: " .. splitText)
|
||||
tInsert(chunks, splitText)
|
||||
|
||||
return chunks
|
||||
end
|
||||
|
||||
-- split first word of a text line
|
||||
function Grichelde:SplitOnFirstMatch(text, start)
|
||||
self:DebugPrint("SplitOnFirstMatch : text: " .. text .. ", start: " .. self:EmptyIfNil(start))
|
||||
local pos = 1
|
||||
if start ~= nil then pos = start end
|
||||
local left, right = strmatch(text, "^.- .+", pos)
|
||||
self:DebugPrint("SplitOnFirstMatch : left: " .. self:EmptyIfNil(left) .. ", right: " .. self:EmptyIfNil(right))
|
||||
return left, right
|
||||
end
|
||||
|
||||
function Grichelde:SplitOnLastMatch(text, start)
|
||||
self:DebugPrint("SplitOnLastMatch : text: " .. text .. ", start: " .. self:EmptyIfNil(start))
|
||||
local pos = 1
|
||||
if start ~= nil then pos = start end
|
||||
local left, right = strmatch(text, ".+ .-$", pos)
|
||||
self:DebugPrint("SplitOnLastMatch : left: " .. self:EmptyIfNil(left) .. ", right: " .. self:EmptyIfNil(right))
|
||||
return left, right
|
||||
end
|
||||
|
||||
function Grichelde:DebugPrint(message)
|
||||
if (Grichelde_Debug) then
|
||||
print(GRAY_FONT_COLOR_CODE .. "Grichelde:" .. FONT_COLOR_CODE_CLOSE .. " " .. message)
|
||||
end
|
||||
end
|
||||
|
||||
function Grichelde:EmptyIfNil(value)
|
||||
if value == nil then return "" end
|
||||
return tostring(value)
|
||||
end
|
||||
|
||||
function Grichelde:tprint(t, indent, done)
|
||||
-- in case we run it standalone
|
||||
local Note = Note or print
|
||||
-- local Tell = Tell or io.write
|
||||
|
||||
-- show strings differently to distinguish them from numbers
|
||||
local function show(val)
|
||||
if type(val) == "string" then
|
||||
return '"' .. val .. '"'
|
||||
else
|
||||
return tostring(val)
|
||||
end
|
||||
end
|
||||
|
||||
-- entry point here
|
||||
done = done or {}
|
||||
indent = indent or 0
|
||||
for key, value in pairs(t) do
|
||||
print(string_rep(" ", indent)) -- indent it
|
||||
if type(value) == "table" and not done[value] then
|
||||
done[value] = true
|
||||
Note(show(key), ":");
|
||||
self:tprint(value, indent + 2, done)
|
||||
else
|
||||
print(show(key), "=")
|
||||
print(show(value))
|
||||
end
|
||||
end
|
||||
end
|
||||
self:RegisterChatCommand("grichelde", HandleSlashCommand)
|
||||
self:RegisterChatCommand("gri", HandleSlashCommand)
|
||||
end
|
BIN
Grichelde.tga
Normal file
BIN
Grichelde.tga
Normal file
Binary file not shown.
@@ -1,23 +1,29 @@
|
||||
## Interface: 11304
|
||||
|
||||
## Title: Grichelde
|
||||
## Notes: Replaces characters you type in the chat box
|
||||
## Notes: Replaces characters from the chat box
|
||||
## Notes-de: Ersetzt eingegebene Zeichen in der Chat-Zeile
|
||||
## Version: 1.0
|
||||
## Version: 0.7.0-beta
|
||||
## Author: Teilzeit-Jedi
|
||||
## eMail: tj@teilzeit-jedi.de
|
||||
|
||||
## X-Build: Classic
|
||||
## X-Curse-Project-ID: 385480
|
||||
## X-License: GPLv3
|
||||
## X-Category: Chat/Communication
|
||||
## X-Credits: Teilzeit-Jedi, Nathan Pieper
|
||||
## X-Embeds: Ace3
|
||||
|
||||
## OptionalDeps: Ace3
|
||||
## SavedVariables: GrichseldeOptions
|
||||
## SavedVariablesPerCharacter: GrichseldeCharOptions
|
||||
## SavedVariables: GricheldeDB
|
||||
|
||||
libs.xml
|
||||
localisation.xml
|
||||
|
||||
Grichelde.lua
|
||||
Grichelde.lua
|
||||
GricheldeConstants.lua
|
||||
GricheldeUtils.lua
|
||||
GricheldeDatabase.lua
|
||||
GricheldeUpgrade.lua
|
||||
GricheldeOptions.lua
|
||||
GricheldeChat.lua
|
||||
|
417
GricheldeChat.lua
Normal file
417
GricheldeChat.lua
Normal file
@@ -0,0 +1,417 @@
|
||||
-- import addon read namespace from global env
|
||||
local _G = _G
|
||||
local Grichelde = _G.Grichelde
|
||||
|
||||
local nilOrEmpty, ipairs, spairs, tContains, tFilter, tInsert, tConcat, find, sub, isUpper, isLower, toUpper, toLower, trim, length
|
||||
= Grichelde.functions.nilOrEmpty, Grichelde.functions.ipairs, Grichelde.functions.spairs, Grichelde.functions.tContains, Grichelde.functions.tFilter, Grichelde.functions.tInsert, Grichelde.functions.tConcat,
|
||||
Grichelde.functions.find, Grichelde.functions.sub, Grichelde.functions.isUpper, Grichelde.functions.isLower, Grichelde.functions.toUpper, Grichelde.functions.toLower, Grichelde.functions.trim, Grichelde.functions.length
|
||||
|
||||
--- Before a chat message is sent, check if replacement is required and replace the text accordingly.
|
||||
-- @param message string
|
||||
-- @param type string
|
||||
-- @param language string
|
||||
-- @param channel string
|
||||
function Grichelde:SendChatMessage(message, type, language, channel, ...)
|
||||
local replacedText = self:CheckAndReplace(message, type)
|
||||
|
||||
|
||||
-- Send text in chunks if length exceeds 255 bytes after replacement
|
||||
local chunks = self:SplitText(replacedText)
|
||||
self:DebugPrint("SendChatMessage : #chunks:", #chunks)
|
||||
|
||||
for _, chunk in ipairs(chunks) do
|
||||
self.hooks["SendChatMessage"](chunk, type, language, channel, ...);
|
||||
end
|
||||
end
|
||||
|
||||
function Grichelde:CheckAndReplace(message, type)
|
||||
local text = message
|
||||
if (self:CheckReplacement(text, type)) then
|
||||
if (_G.Misspelled) then
|
||||
self:DebugPrint("Misspelled detected: cleansing message")
|
||||
text = _G.Misspelled:RemoveHighlighting(text)
|
||||
end
|
||||
text = self:ReplaceText(trim(text))
|
||||
end
|
||||
return text
|
||||
end
|
||||
|
||||
function Grichelde:CheckReplacement(text, channel)
|
||||
-- skip if not enabled
|
||||
if (not self.db.profile.enabled) then
|
||||
self:DebugPrint("CheckReplacement : disabled")
|
||||
return false
|
||||
end
|
||||
|
||||
-- skip if no text
|
||||
if nilOrEmpty(text) then
|
||||
return false
|
||||
end
|
||||
|
||||
-- skip if wrong channel
|
||||
local allowedChannels = tFilter(self.db.profile.channels,
|
||||
function(_,v) return v == true end,
|
||||
function(k,_) return k end
|
||||
)
|
||||
self:DebugPrint("CheckReplacement : allowed channels:", tConcat(allowedChannels, ", "))
|
||||
local type = self:ConvertBlizChannelToType(channel)
|
||||
if (type == nil or not tContains(allowedChannels, type)) then
|
||||
self:DebugPrint("CheckReplacement : skip channel type:", type)
|
||||
return false
|
||||
end
|
||||
|
||||
-- don't replace slash commands except chat related commands
|
||||
if sub(text, 1, 1) == "/" then
|
||||
local firstWord, _ = self:SplitOnFirstMatch(text)
|
||||
-- todo: adapt allowed slash commands
|
||||
if (firstWord == nil or not tContains(self.slashCommands, firstWord)) then
|
||||
self:DebugPrint("CheckReplacement : ignore slash command")
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- in any other case
|
||||
return true
|
||||
end
|
||||
|
||||
function Grichelde:ConvertBlizChannelToType(channel)
|
||||
local type = toLower(channel)
|
||||
self:DebugPrint("ConvertBlizChannelToType : convert %s to %s", channel, type)
|
||||
return type
|
||||
end
|
||||
|
||||
--- Replaces all character occurrences for which replacements have been defined in the options,
|
||||
--- while preserving any itemLinks or textures. (http://www.wowwiki.com/ItemLink)
|
||||
-- @param text string
|
||||
-- @return string
|
||||
function Grichelde:ReplaceText(text)
|
||||
local lookAheads = {'|', '{', '%'}
|
||||
local finalText = ""
|
||||
local newText = text
|
||||
|
||||
-- don't replace non-chat related slash commands
|
||||
local firstWord, line = self:SplitOnFirstMatch(text)
|
||||
if (firstWord ~= nil and tContains(self.slashCommands, firstWord)) then
|
||||
self:DebugPrint("ReplaceText : Found slash command:", firstWord )
|
||||
-- skip chat slash command
|
||||
finalText = finalText .. firstWord .. ' '
|
||||
newText = line
|
||||
end
|
||||
|
||||
local current = 1
|
||||
local lastStart = 1
|
||||
|
||||
while current <= length(newText) do
|
||||
local currentChar = sub(newText, current, current)
|
||||
self:TracePrint("current/char : %s,%s", current, currentChar)
|
||||
|
||||
if ( not tContains(lookAheads, currentChar)) then
|
||||
current = current + 1
|
||||
else
|
||||
|
||||
-- lookahead-check for itemLinks, textures and raid target icons
|
||||
local textAhead = sub(newText, current)
|
||||
local posEnd = self:CheckForPreversableText(textAhead)
|
||||
if posEnd > 0 then
|
||||
self:DebugPrint("ReplaceText : Found an ignore pattern")
|
||||
|
||||
local textBehind = sub(newText, lastStart, current - 1)
|
||||
local replacement = self:ReplaceCharacters(textBehind)
|
||||
local preservedText = sub(textAhead, 1, posEnd)
|
||||
|
||||
finalText = finalText .. replacement .. preservedText
|
||||
current = current + posEnd
|
||||
lastStart = current
|
||||
self:DebugPrint("ReplaceText : restarting at", lastStart)
|
||||
else
|
||||
-- no corresponding end was found to start pattern, continue loop with next char
|
||||
current = current + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- cleanup remaining text to the end
|
||||
local remainingText = sub(newText, lastStart)
|
||||
local replacement = self:ReplaceCharacters(remainingText)
|
||||
finalText = finalText .. replacement
|
||||
|
||||
self:DebugPrint("ReplaceText : replaced \"%s\"", text)
|
||||
self:DebugPrint("ReplaceText : with \"%s\"", finalText)
|
||||
return finalText
|
||||
end
|
||||
|
||||
|
||||
--- Checks if the text starts with a preversable ignore pattern, such as itemLinks, textures, raid target icons or
|
||||
--- %-substitutons and returns the end location of the match, or 0 if no pattern was found
|
||||
-- @param text string
|
||||
-- @return number
|
||||
function Grichelde:CheckForPreversableText(text)
|
||||
self:DebugPrint("CheckForPreversableText : text:", text)
|
||||
|
||||
-- do not replace these patterns
|
||||
local ignorePatterns = {
|
||||
"|[Cc]%x%x%x%x%x%x%x%x.-|r", -- colored items (or links)
|
||||
"|H.-|h", -- item links (http://www.wowwiki.com/ItemLink)
|
||||
"|T.-|t", -- textures
|
||||
"|n", -- newline
|
||||
|
||||
"{rt[1-8]}", -- rumbered raid target icons
|
||||
"{Star}", -- named raid target icon 1
|
||||
"{Circle}", -- named raid target icon 2
|
||||
"{Coin}", -- named raid target icon 2
|
||||
"{Diamond}", -- named raid target icon 3
|
||||
"{Triangle}", -- named raid target icon 4
|
||||
"{Moon}", -- named raid target icon 5
|
||||
"{Square}", -- named raid target icon 6
|
||||
"{Cross}", -- named raid target icon 7
|
||||
"{X}", -- named raid target icon 7
|
||||
"{Skull}", -- named raid target icon 8
|
||||
|
||||
"%%n", -- player's name
|
||||
"%%z", -- player's currnt zone
|
||||
"%%sz", -- player's current sub-zone
|
||||
"%%loc", -- player's map coordinates
|
||||
"%%t", -- name of target
|
||||
"%%f", -- name of focus target
|
||||
"%%m", -- name of mouseover unit
|
||||
"%%p", -- name of player pet
|
||||
"%%tt" -- name of player's target's target
|
||||
}
|
||||
|
||||
-- Calling find on ever pattern might be inefficient but its way less code.
|
||||
for _, pattern in ipairs(ignorePatterns) do
|
||||
local pos1, pos2 = find(text, pattern)
|
||||
if pos1 == 1 and pos2 ~= nil then
|
||||
self:DebugPrint("CheckForPreversableText : Found ignore pattern \"%s\" at (%d, %d)", pattern, pos1, pos2)
|
||||
return pos2
|
||||
end
|
||||
end
|
||||
self:DebugPrint("CheckForPreversableText : no ignore pattern found")
|
||||
return 0
|
||||
end
|
||||
|
||||
--- Replaces all character occurrences for which replacements have been defined in the options
|
||||
-- @param text string
|
||||
-- @return string
|
||||
function Grichelde:ReplaceCharacters(text)
|
||||
local replacements = self.db.profile.replacements or {}
|
||||
self:DebugPrint("ReplaceCharacters : replacements")
|
||||
self:DebugPrint(replacements)
|
||||
|
||||
local result = text
|
||||
local consolidate = {}
|
||||
|
||||
-- replacements are done first
|
||||
for replName, replTable in spairs(replacements) do
|
||||
local before = result
|
||||
local search = replTable.searchText
|
||||
|
||||
if not nilOrEmpty(search) then
|
||||
local replace = replTable.replaceText
|
||||
consolidate[replName] = {}
|
||||
|
||||
if replTable.exactCase then
|
||||
-- exact case
|
||||
self:DebugPrint("ReplaceCharacters : \"%s => %s\" (exact case)", search, replace)
|
||||
local pos, offset = 1, 0
|
||||
local oldResult = result
|
||||
|
||||
local pos1, pos2 = find(oldResult, search, pos)
|
||||
while (pos1 and pos2) do
|
||||
self:TracePrint("pos1: %d, pos2: %d", pos1, pos2)
|
||||
local pre = sub(result, 1, pos1 - 1 + offset)
|
||||
local post = sub(result, pos2 + 1 + offset)
|
||||
self:TracePrint("pre: %s, post: %s", pre, post)
|
||||
|
||||
-- actual replacement
|
||||
result = pre .. replace .. post
|
||||
self:DebugPrint("result: %s", result)
|
||||
|
||||
-- remember positions for consolidate
|
||||
if replTable.consolidate then
|
||||
tInsert(consolidate[replName], pos1 + offset)
|
||||
end
|
||||
|
||||
-- replacement text can lengthen or shorten the resulting text
|
||||
-- after replacement result and lowerResult can have different sizes
|
||||
offset = offset + length(replace) - length(search)
|
||||
-- update values for next iteration
|
||||
pos = pos2 + 1
|
||||
pos1, pos2 = find(oldResult, search, pos)
|
||||
end
|
||||
else
|
||||
self:DebugPrint("ReplaceCharacters : \"%s => %s\" (ignoreCase)", search, replace)
|
||||
local pos, offset = 1, 0
|
||||
local lowerResult = toLower(result)
|
||||
local lowerSearch = toLower(search)
|
||||
|
||||
local pos1, pos2 = find(lowerResult, lowerSearch, pos)
|
||||
while (pos1 and pos2) do
|
||||
self:TracePrint("pos1: %d, pos2: %d", pos1, pos2)
|
||||
local pre = sub(result, 1, pos1 - 1 + offset)
|
||||
local match = sub(result, pos1 + offset, pos2 + offset)
|
||||
local post = sub(result, pos2 + 1 + offset)
|
||||
self:TracePrint("pre: %s, match: %s, post: %s", pre, match, post)
|
||||
|
||||
-- keep cases
|
||||
local repl = ""
|
||||
local lastCase = nil
|
||||
for p = pos1, pos2 do
|
||||
self:TracePrint("p: %d", p)
|
||||
local c = sub(match, p - pos1 + 1, p - pos1 + 1)
|
||||
local r = sub(replace, p - pos1 + 1, p - pos1 + 1) or ""
|
||||
|
||||
if (isUpper(c)) then -- all UPPER-CASE letter
|
||||
lastCase = true
|
||||
repl = repl .. toUpper(r)
|
||||
elseif (isLower(match)) then -- all lower_case letter
|
||||
lastCase = false
|
||||
repl = repl .. toLower(r)
|
||||
else -- no letter
|
||||
lastCase = nil
|
||||
repl = repl .. r
|
||||
end
|
||||
self:TracePrint("character: %s, %s", c, r)
|
||||
end
|
||||
|
||||
self:TracePrint("length %d > %d", length(replace), pos2 - pos1 + 1)
|
||||
if (length(replace) > pos2 - pos1 + 1) then
|
||||
local remainingReplace = sub(replace, pos2 - pos1 + 2)
|
||||
local nextLetter = sub(post, 1, 1)
|
||||
|
||||
self:TracePrint("rest: %s, n: %s, lastCase: %s", remainingReplace, nextLetter, lastCase)
|
||||
|
||||
if (isUpper(nextLetter)) then
|
||||
if lastCase == nil or lastCase == false then
|
||||
repl = repl .. remainingReplace
|
||||
else
|
||||
repl = repl .. toUpper(remainingReplace)
|
||||
end
|
||||
elseif (isLower(nextLetter)) then
|
||||
if lastCase == nil or lastCase == true then
|
||||
repl = repl .. remainingReplace
|
||||
else
|
||||
repl = repl .. toLower(remainingReplace)
|
||||
end
|
||||
else
|
||||
-- no letter
|
||||
if lastCase == nil then
|
||||
repl = repl .. remainingReplace
|
||||
elseif lastCase == false then
|
||||
repl = repl .. toLower(remainingReplace)
|
||||
else
|
||||
repl = repl .. toUpper(remainingReplace)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- actual replacement
|
||||
result = pre .. repl .. post
|
||||
self:DebugPrint("result: %s", result)
|
||||
|
||||
-- remember positions for consolidate
|
||||
if replTable.consolidate then
|
||||
tInsert(consolidate[replName], pos1 + offset)
|
||||
end
|
||||
|
||||
-- replacement text can be longer or shorter the resulting text
|
||||
-- after replacement result and lowerResult can have different sizes
|
||||
offset = offset + length(repl) - length(lowerSearch)
|
||||
-- update values for next iteration
|
||||
pos = pos2 + 1
|
||||
pos1, pos2 = find(lowerResult, lowerSearch, pos)
|
||||
end
|
||||
end
|
||||
|
||||
if before ~= result then
|
||||
self:DebugPrint("ReplaceCharacters : replaced \"%s\" with \"%s\"", before, result)
|
||||
end
|
||||
else
|
||||
self:DebugPrint("ReplaceCharacters : Skip replacement for empty mapping")
|
||||
end
|
||||
end
|
||||
|
||||
-- consolidation is done last
|
||||
for replName, replTable in spairs(replacements) do
|
||||
local before = result
|
||||
--local search = replTable.searchText
|
||||
local search = replTable.searchText
|
||||
|
||||
if not nilOrEmpty(search) then
|
||||
local replace = replTable.replaceText
|
||||
local lowerResult = toLower(result)
|
||||
local offset = 0
|
||||
|
||||
if replTable.consolidate then
|
||||
self:DebugPrint("ReplaceCharacters : consolidating \"%s => %s\"", search, replace)
|
||||
self:DebugPrint("consolidate[" .. replName .. "] is:")
|
||||
self:DebugPrint(consolidate[replName])
|
||||
|
||||
for _, pos1 in spairs(consolidate[replName]) do
|
||||
local pos2 = pos1 + length(replace) - 1
|
||||
self:TracePrint("pos1: %d, pos2: %d", pos1, pos2)
|
||||
local match = sub(lowerResult, pos1, pos2)
|
||||
local next = sub(lowerResult, pos2 + 1, pos2 + 1 + pos2 - pos1)
|
||||
self:TracePrint("match: %s, next: %s", match, next)
|
||||
|
||||
local _, p2 = find(next, "^" .. match)
|
||||
self:TracePrint("p2: %d", p2)
|
||||
if (p2) then
|
||||
result = sub(result, 1, pos2 + offset) .. sub(result, pos2 + 1 + p2 + offset)
|
||||
|
||||
-- consolidation will shorten the resulting text
|
||||
offset = offset + length(result) - length(lowerResult)
|
||||
end
|
||||
self:DebugPrint("result: %s", result)
|
||||
end
|
||||
end
|
||||
|
||||
if before ~= result then
|
||||
self:DebugPrint("ReplaceCharacters : consolidate \"%s\" with \"%s\"", before, result)
|
||||
end
|
||||
else
|
||||
self:DebugPrint("ReplaceCharacters : Skip consolidation for empty mapping")
|
||||
end
|
||||
end
|
||||
|
||||
self:DebugPrint("ReplaceCharacters : final text:", result)
|
||||
return result
|
||||
end
|
||||
|
||||
--- Splits a long text in longest possible chunks of <= 255 length, split at last available space
|
||||
-- @param text string
|
||||
-- @return table
|
||||
function Grichelde:SplitText(text)
|
||||
local chunks = {}
|
||||
local splitText = text
|
||||
local textSize = length(splitText or "")
|
||||
|
||||
while textSize > 255 do
|
||||
local chunk = sub(splitText, 1, 255)
|
||||
local remaining = ""
|
||||
|
||||
-- special case: if space is the start of the next chunk, don't split this chunk
|
||||
if sub(splitText, 256, 256) ~= ' ' then
|
||||
-- split at last space, don't assign directly as nil might be returned
|
||||
local left, right = self:SplitOnLastMatch(chunk)
|
||||
if left ~= nil then
|
||||
chunk = left
|
||||
end
|
||||
if right ~= nil then
|
||||
remaining = right
|
||||
end
|
||||
end
|
||||
|
||||
self:DebugPrint("SplitText : chunk:", chunk)
|
||||
|
||||
tInsert(chunks, chunk)
|
||||
splitText = remaining .. sub(splitText, 256)
|
||||
textSize = length(splitText)
|
||||
end
|
||||
|
||||
-- pickup remaining text < 255
|
||||
self:DebugPrint("SplitText : last chunk:", splitText)
|
||||
tInsert(chunks, splitText)
|
||||
|
||||
return chunks
|
||||
end
|
161
GricheldeConstants.lua
Normal file
161
GricheldeConstants.lua
Normal file
@@ -0,0 +1,161 @@
|
||||
-- read namespace from global env
|
||||
local _G = _G
|
||||
local Grichelde = _G.Grichelde
|
||||
|
||||
-- constants and upvalues
|
||||
Grichelde.LOG_LEVEL = {}
|
||||
Grichelde.LOG_LEVEL.DEBUG = 1
|
||||
Grichelde.LOG_LEVEL.TRACE = 2
|
||||
|
||||
Grichelde.MAPPING_OFFSET = 10
|
||||
|
||||
-- colors:
|
||||
Grichelde.COLOR_CODES = {}
|
||||
Grichelde.COLOR_CODES.PREFIX = "|c00FFAA00"
|
||||
-- https://github.com/stoneharry/Misc-WoW-Stuff/blob/master/EoC%20Interface/FrameXML/Constants.lua
|
||||
Grichelde.COLOR_CODES.NORMAL = _G.NORMAL_FONT_COLOR_CODE or "|cffffd200"
|
||||
Grichelde.COLOR_CODES.HIGHLIGHT = _G.HIGHLIGHT_FONT_COLOR_CODE or "|cffffffff"
|
||||
Grichelde.COLOR_CODES.RED = _G.RED_FONT_COLOR_CODE or "|cffff2020"
|
||||
Grichelde.COLOR_CODES.GREEN = _G.GREEN_FONT_COLOR_CODE or "|cff20ff20"
|
||||
Grichelde.COLOR_CODES.LIGHTGRAY = "|cffC0C0C0"
|
||||
Grichelde.COLOR_CODES.GRAY = _G.GRAY_FONT_COLOR_CODE or "|cff808080"
|
||||
Grichelde.COLOR_CODES.DARKGRAY = "|cff404040"
|
||||
Grichelde.COLOR_CODES.YELLOW = _G.YELLOW_FONT_COLOR_CODE or "|cffffff00"
|
||||
Grichelde.COLOR_CODES.LIGHTYELLOW = _G.LIGHTYELLOW_FONT_COLOR_CODE or "|cffffff9a"
|
||||
Grichelde.COLOR_CODES.ORANGE = _G.ORANGE_FONT_COLOR_CODE or "|cffff7f3f"
|
||||
Grichelde.COLOR_CODES.CLOSE = _G.FONT_COLOR_CODE_CLOSE or "|r"
|
||||
|
||||
Grichelde.slashCommands = { "/s", "/say", "/e", "/em", "/me", "/emote", "/y", "/yell", "/sh", "/shout", "/p", "/party", "/pl", "/partyleader", "/g", "/gc", "/guild", "/o", "/osay", "/officer", "/raid", "/rsay", "/rl", "/raidleader", "/rw", "/raidwarning", "/i", "/instance", "/bg", "/battleground", "/w", "/whisper", "/t", "/tell", "/send", "/r", "/reply" }
|
||||
|
||||
local function nilOrEmpty(s)
|
||||
return s == nil or s:trim() == ""
|
||||
end
|
||||
|
||||
local function spairs(t, orderFunc)
|
||||
-- collect the keys
|
||||
local sortedKeys = {}
|
||||
-- for every non-nil value
|
||||
for key, _ in Grichelde.functions.pairs(t) do
|
||||
Grichelde.functions.tInsert(sortedKeys, key)
|
||||
end
|
||||
|
||||
if orderFunc then
|
||||
Grichelde.functions.tSort(sortedKeys, function(a, b) return orderFunc(sortedKeys, a, b) end)
|
||||
else
|
||||
-- lexicographical order
|
||||
Grichelde.functions.tSort(sortedKeys)
|
||||
end
|
||||
|
||||
-- return the iterator function
|
||||
local it = 0
|
||||
return function()
|
||||
it = it + 1
|
||||
if sortedKeys[it] then
|
||||
return sortedKeys[it], t[sortedKeys[it]]
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function tFilter(t, cond, extr)
|
||||
local filtered = {}
|
||||
for key, value in Grichelde.functions.pairs(t) do
|
||||
if cond(key, value) then
|
||||
local val = extr(key, value)
|
||||
Grichelde.functions.tInsert(filtered, #filtered + 1, val)
|
||||
end
|
||||
end
|
||||
return filtered
|
||||
end
|
||||
|
||||
local function tSize(t)
|
||||
local size = 0
|
||||
if (t) then
|
||||
-- for every non-nil value
|
||||
for _, _ in Grichelde.functions.pairs(t) do
|
||||
size = size + 1
|
||||
end
|
||||
end
|
||||
return size
|
||||
end
|
||||
|
||||
local function tClone(orig)
|
||||
local orig_type = Grichelde.functions.type(orig)
|
||||
local copy
|
||||
if orig_type == 'table' then
|
||||
copy = {}
|
||||
-- for every non-nil value
|
||||
for orig_key, orig_value in Grichelde.functions.tNext, orig, nil do
|
||||
copy[tClone(orig_key)] = tClone(orig_value)
|
||||
end
|
||||
Grichelde.functions.setmetatable(copy, tClone(Grichelde.functions.getmetatable(orig)))
|
||||
else -- number, string, boolean, etc
|
||||
copy = orig
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
local function isChar(word)
|
||||
return Grichelde.functions.find(word, "%a+")
|
||||
end
|
||||
|
||||
local function isNumber(digit)
|
||||
return Grichelde.functions.find(digit, "%d+")
|
||||
end
|
||||
|
||||
local function isUpper(word)
|
||||
return Grichelde.functions.isChar(word) and word == Grichelde.functions.toUpper(word)
|
||||
end
|
||||
|
||||
local function isLower(word)
|
||||
return Grichelde.functions.isChar(word) and word == Grichelde.functions.toLower(word)
|
||||
end
|
||||
|
||||
local function isCapital(word)
|
||||
return Grichelde.functions.legnth(word) > 1
|
||||
and Grichelde.functions.isUpper(Grichelde.functions.sub(word,1,1))
|
||||
and Grichelde.functions.isLower(Grichelde.functions.sub(word,2))
|
||||
end
|
||||
|
||||
-- faster function lookups by mapping to local refs
|
||||
Grichelde.functions = {}
|
||||
Grichelde.functions.type = _G.type
|
||||
Grichelde.functions.print = _G.print
|
||||
Grichelde.functions.nilOrEmpty = nilOrEmpty
|
||||
Grichelde.functions.pairs = _G.pairs
|
||||
Grichelde.functions.ipairs = _G.ipairs
|
||||
Grichelde.functions.spairs = spairs
|
||||
Grichelde.functions.tContains = _G.tContains
|
||||
Grichelde.functions.tFilter = tFilter
|
||||
Grichelde.functions.tInsert = _G.table.insert
|
||||
Grichelde.functions.tConcat = _G.table.concat
|
||||
Grichelde.functions.tSize = tSize
|
||||
Grichelde.functions.tSort = _G.table.sort
|
||||
Grichelde.functions.tClone = tClone
|
||||
Grichelde.functions.tNext = _G.next
|
||||
Grichelde.functions.setmetatable = _G.setmetatable
|
||||
Grichelde.functions.getmetatable = _G.getmetatable
|
||||
Grichelde.functions.select = _G.select
|
||||
Grichelde.functions.unpack = _G.unpack
|
||||
Grichelde.functions.find = _G.string.find
|
||||
Grichelde.functions.sub = _G.string.sub
|
||||
Grichelde.functions.gsub = _G.string.gsub
|
||||
Grichelde.functions.match = _G.strmatch
|
||||
Grichelde.functions.join = _G.strjoin
|
||||
Grichelde.functions.split = _G.strsplit
|
||||
Grichelde.functions.toUpper = _G.strupper
|
||||
Grichelde.functions.toLower = _G.strlower
|
||||
Grichelde.functions.isChar = isChar
|
||||
Grichelde.functions.isNumber = isNumber
|
||||
Grichelde.functions.isUpper = isUpper
|
||||
Grichelde.functions.isLower = isLower
|
||||
Grichelde.functions.isCapital = isCapital
|
||||
Grichelde.functions.format = _G.string.format
|
||||
Grichelde.functions.rep = _G.string.rep
|
||||
Grichelde.functions.trim = _G.strtrim
|
||||
Grichelde.functions.length = _G.string.len
|
||||
Grichelde.functions.toString = _G.tostring
|
||||
Grichelde.functions.toNumber = _G.tonumber
|
||||
Grichelde.functions.max = _G.math.max
|
||||
Grichelde.functions.min = _G.math.min
|
156
GricheldeDatabase.lua
Normal file
156
GricheldeDatabase.lua
Normal file
@@ -0,0 +1,156 @@
|
||||
-- read namespace from global env
|
||||
local _G = _G
|
||||
local Grichelde = _G.Grichelde
|
||||
|
||||
local pairs, tInsert, tClone, unpack, join, toString
|
||||
= Grichelde.functions.pairs, Grichelde.functions.tInsert, Grichelde.functions.tClone, Grichelde.functions.unpack, Grichelde.functions.join, Grichelde.functions.toString
|
||||
|
||||
function Grichelde:GetDefaultConfig()
|
||||
return {
|
||||
global = {},
|
||||
profile = {
|
||||
enabled = true,
|
||||
channels = {
|
||||
["*"] = false,
|
||||
say = true,
|
||||
emote = false,
|
||||
yell = true,
|
||||
party = true,
|
||||
guild = true,
|
||||
officer = true,
|
||||
},
|
||||
replacements = {
|
||||
["**"] = {
|
||||
order = 9999,
|
||||
searchText = "",
|
||||
replaceText = "",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
},
|
||||
replacement_10 = {
|
||||
order = 10,
|
||||
searchText = "s",
|
||||
replaceText = "ch",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
},
|
||||
replacement_11 = {
|
||||
order = 11,
|
||||
searchText = "t",
|
||||
replaceText = "ck",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
},
|
||||
replacement_12 = {
|
||||
order = 12,
|
||||
searchText = "Zark",
|
||||
replaceText = "toter Schamane",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function Grichelde:LoadDatabase()
|
||||
local db = LibStub("AceDB-3.0"):New(self.name .."DB", self:GetDefaultConfig(), true)
|
||||
|
||||
db.RegisterCallback(self, "OnNewProfile", "RefreshOptions")
|
||||
db.RegisterCallback(self, "OnProfileChanged", "RefreshOptions")
|
||||
db.RegisterCallback(self, "OnProfileDeleted", "RefreshOptions")
|
||||
db.RegisterCallback(self, "OnProfileCopied", "RefreshOptions")
|
||||
db.RegisterCallback(self, "OnProfileReset", "RefreshOptions")
|
||||
|
||||
return db
|
||||
end
|
||||
|
||||
function Grichelde:SyncToDatabase(info, val)
|
||||
self:TracePrint("SyncToDatabase : info")
|
||||
for i = 0, #info do
|
||||
self:TracePrint("%d = %s", i, info[i])
|
||||
end
|
||||
|
||||
local option = self.db.profile
|
||||
local path = 1
|
||||
while (path < #info) do
|
||||
if info[path] ~= "mappings" then
|
||||
option = option[info[path]] -- or nil
|
||||
end
|
||||
path = path + 1
|
||||
end
|
||||
local optionPath = join(".", unpack(info, 1, #info))
|
||||
self:DebugPrint("change option \"%s\" from %s to %s", optionPath, toString(option[info[path]]), toString(val))
|
||||
option[info[path]] = val
|
||||
end
|
||||
|
||||
function Grichelde:ReadFromDatabase(info)
|
||||
self:TracePrint("ReadFromDatabase : info")
|
||||
for i = 0, #info do
|
||||
self:TracePrint("%d = %s", i, info[i])
|
||||
end
|
||||
|
||||
local option = self.db.profile
|
||||
local path = 1
|
||||
while (path <= #info) do
|
||||
if info[path] ~= "mappings" then
|
||||
option = option[info[path]] -- or nil
|
||||
end
|
||||
path = path + 1
|
||||
end
|
||||
local optionPath = join(".", unpack(info, 1, #info))
|
||||
self:DebugPrint("read option \"%s\": %s", optionPath, toString(option))
|
||||
return option
|
||||
end
|
||||
|
||||
--- Sorts a replacements table by order sub-field and rename.
|
||||
--- Do NOT reassign self.db.profile.replacements here or with its output as it will break defaults
|
||||
function Grichelde:ReorderReplacements()
|
||||
local replacements = self.db.profile.replacements or {}
|
||||
|
||||
self:TracePrint("ReorderReplacements : unsorted table")
|
||||
self:TracePrint(replacements)
|
||||
|
||||
local orderToName = {}
|
||||
local size = 0
|
||||
for replName, replTable in pairs(replacements) do
|
||||
size = size + 1
|
||||
tInsert(orderToName, replTable.order, replName)
|
||||
end
|
||||
|
||||
self:TracePrint("ReorderReplacements : size: %d, orderToName", size)
|
||||
self:TracePrint(orderToName)
|
||||
|
||||
local sorted = {}
|
||||
local index, count = 0, 0
|
||||
|
||||
while count < size do
|
||||
local replName = orderToName[index]
|
||||
if replName and replacements[replName] then
|
||||
self:TracePrint("ReorderReplacements : replName: %s, replTable", replName)
|
||||
self:TracePrint(replacements[replName])
|
||||
local order = Grichelde.MAPPING_OFFSET + count
|
||||
sorted["replacement_" .. order] = tClone(replacements[replName])
|
||||
sorted["replacement_" .. order].order = order
|
||||
count = count + 1
|
||||
end
|
||||
index = index + 1
|
||||
if ( index > 10000) then break end
|
||||
end
|
||||
|
||||
-- self:TracePrint("ReorderReplacements : sorted")
|
||||
-- self:TracePrint(sorted)
|
||||
|
||||
-- do NOT set self.db.profile.replacements = {} it will break defaults
|
||||
for replName, _ in pairs(replacements) do
|
||||
replacements[replName] = nil
|
||||
end
|
||||
|
||||
-- copy over sorted replacements
|
||||
for replName, replTable in pairs(sorted) do
|
||||
replacements[replName] = replTable
|
||||
end
|
||||
|
||||
self:DebugPrint("ReorderReplacements : sorted table")
|
||||
self:DebugPrint(self.db.profile.replacements)
|
||||
end
|
430
GricheldeOptions.lua
Normal file
430
GricheldeOptions.lua
Normal file
@@ -0,0 +1,430 @@
|
||||
-- read namespace from global env
|
||||
local _G = _G
|
||||
local Grichelde = _G.Grichelde
|
||||
|
||||
local nilOrEmpty, pairs, find, match, toString, toNumber
|
||||
= Grichelde.functions.nilOrEmpty, Grichelde.functions.pairs, Grichelde.functions.find, Grichelde.functions.match, Grichelde.functions.toString, Grichelde.functions.toNumber
|
||||
|
||||
function Grichelde:CreateOptionsUI()
|
||||
return {
|
||||
name = self:Format(self.L.Options_Title, self.L.AddonName),
|
||||
type = "group",
|
||||
childGroups = "tab",
|
||||
set = function(info, val) self:SyncToDatabase(info, val) end,
|
||||
get = function(info) return self:ReadFromDatabase(info) end,
|
||||
hidden = false,
|
||||
disabled = function(info) return self:IsDisabled(info) end,
|
||||
args = {
|
||||
enabled = {
|
||||
order = 0,
|
||||
type = "toggle",
|
||||
name = self.L.Options_Enabled_Name,
|
||||
desc = self:Format(self.L.Options_Enabled_Desc, self.L.AddonName),
|
||||
disabled = false,
|
||||
},
|
||||
|
||||
channels = {
|
||||
order = 2,
|
||||
type = "group",
|
||||
name = self.L.Options_Channels_Group_Name,
|
||||
desc = self:Format(self.L.Options_Channels_Group_Desc, self.L.AddonName),
|
||||
args = {
|
||||
header = {
|
||||
order = 1,
|
||||
type = "description",
|
||||
name = self.L.Options_Channels_Header
|
||||
},
|
||||
spacer = {
|
||||
order = 2,
|
||||
type = "header",
|
||||
name = ""
|
||||
},
|
||||
say = {
|
||||
order = 10,
|
||||
type = "toggle",
|
||||
name = self.L.Options_Channel_Say_Name,
|
||||
desc = self:Format(self.L.Options_Channel_Say_Desc, self.L.AddonName),
|
||||
},
|
||||
emote = {
|
||||
order = 11,
|
||||
type = "toggle",
|
||||
name = self.L.Options_Channel_Emote_Name,
|
||||
desc = self:Format(self.L.Options_Channel_Emote_Desc, self.L.AddonName),
|
||||
},
|
||||
yell = {
|
||||
order = 12,
|
||||
type = "toggle",
|
||||
name = self.L.Options_Channel_Yell_Name,
|
||||
desc = self:Format(self.L.Options_Channel_Yell_Desc, self.L.AddonName),
|
||||
},
|
||||
party = {
|
||||
order = 13,
|
||||
type = "toggle",
|
||||
name = self.L.Options_Channel_Party_Name,
|
||||
desc = self:Format(self.L.Options_Channel_Party_Desc, self.L.AddonName),
|
||||
},
|
||||
guild = {
|
||||
order = 14,
|
||||
type = "toggle",
|
||||
name = self.L.Options_Channel_Guild_Name,
|
||||
desc = self:Format(self.L.Options_Channel_Guild_Desc, self.L.AddonName),
|
||||
},
|
||||
officer = {
|
||||
order = 15,
|
||||
type = "toggle",
|
||||
name = self.L.Options_Channel_Officer_Name,
|
||||
desc = self:Format(self.L.Options_Channel_Officer_Desc, self.L.AddonName),
|
||||
},
|
||||
raid = {
|
||||
order = 16,
|
||||
type = "toggle",
|
||||
name = self.L.Options_Channel_Raid_Name,
|
||||
desc = self:Format(self.L.Options_Channel_Raid_Desc, self.L.AddonName),
|
||||
},
|
||||
raidWarning = {
|
||||
order = 17,
|
||||
type = "toggle",
|
||||
name = self.L.Options_Channel_RaidWarning_Name,
|
||||
desc = self:Format(self.L.Options_Channel_RaidWarning_Desc, self.L.AddonName),
|
||||
},
|
||||
instance = {
|
||||
order = 18,
|
||||
type = "toggle",
|
||||
name = self.L.Options_Channel_Instance_Name,
|
||||
desc = self:Format(self.L.Options_Channel_Instance_Desc, self.L.AddonName),
|
||||
},
|
||||
battleground = {
|
||||
order = 19,
|
||||
type = "toggle",
|
||||
name = self.L.Options_Channel_Battleground_Name,
|
||||
desc = self:Format(self.L.Options_Channel_Battleground_Desc, self.L.AddonName),
|
||||
},
|
||||
whisper = {
|
||||
order = 20,
|
||||
type = "toggle",
|
||||
name = self.L.Options_Channel_Whisper_Name,
|
||||
desc = self:Format(self.L.Options_Channel_Whisper_Desc, self.L.AddonName),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
replacements = {
|
||||
order = 3,
|
||||
type = "group",
|
||||
name = self.L.Options_Replacements_Group_Name,
|
||||
desc = self.L.Options_Replacements_Group_Desc,
|
||||
args = {
|
||||
add = {
|
||||
order = 0,
|
||||
type = "execute",
|
||||
confirm = false,
|
||||
name = self.L.Options_Replacements_Add_Name,
|
||||
desc = self.L.Options_Replacements_Add_Desc,
|
||||
func = function(info) self:AddEmptyMapping(info) end
|
||||
},
|
||||
deleteAll = {
|
||||
order = 1,
|
||||
type = "execute",
|
||||
confirm = true,
|
||||
confirmText = self.L.Options_Replacements_DeleteAll_ConfirmText,
|
||||
name = self.L.Options_Replacements_DeleteAll_Name,
|
||||
desc = self.L.Options_Replacements_DeleteAll_Desc,
|
||||
func = function(info) self:DeleteAllMappings(info) end
|
||||
},
|
||||
header = {
|
||||
order = 3,
|
||||
type = "description",
|
||||
name = self.L.Options_Replacements_Header
|
||||
},
|
||||
spacer = {
|
||||
order = 4,
|
||||
type = "header",
|
||||
name = ""
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function Grichelde:CreateMapping(offset)
|
||||
return {
|
||||
order = offset or 9999,
|
||||
type = "group",
|
||||
name = function(info) return self:MappingName(info) end,
|
||||
desc = self.L.Options_Mapping_Group_Desc,
|
||||
childGroups = "tree",
|
||||
args = {
|
||||
searchText = {
|
||||
order = 0,
|
||||
type = "input",
|
||||
name = self.L.Options_Mapping_SearchText_Name,
|
||||
desc = self.L.Options_Mapping_SearchText_Desc,
|
||||
},
|
||||
replaceText = {
|
||||
order = 1,
|
||||
type = "input",
|
||||
name = self.L.Options_Mapping_ReplaceText_Name,
|
||||
desc = self.L.Options_Mapping_ReplaceText_Desc,
|
||||
},
|
||||
exactCase = {
|
||||
order = 2,
|
||||
type = "toggle",
|
||||
name = self.L.Options_Mapping_ExactCase_Name,
|
||||
desc = self.L.Options_Mapping_ExactCase_Desc,
|
||||
width = "full",
|
||||
},
|
||||
consolidate = {
|
||||
order = 3,
|
||||
type = "toggle",
|
||||
name = self.L.Options_Mapping_Consolidate_Name,
|
||||
desc = self.L.Options_Mapping_Consolidate_Desc,
|
||||
width = "full"
|
||||
},
|
||||
moveUp = {
|
||||
order = 10,
|
||||
type = "execute",
|
||||
name = self.L.Options_Mapping_MoveUp_Name,
|
||||
desc = self.L.Options_Mapping_MoveUp_Desc,
|
||||
width = 0.25,
|
||||
func = function(info) self:MoveUp(info) end
|
||||
},
|
||||
moveDown = {
|
||||
order = 11,
|
||||
type = "execute",
|
||||
name = self.L.Options_Mapping_MoveDown_Name,
|
||||
desc = self.L.Options_Mapping_MoveDown_Desc,
|
||||
width = 0.25,
|
||||
func = function(info) self:MoveDown(info) end
|
||||
},
|
||||
spacer = {
|
||||
order = 18,
|
||||
type = "description",
|
||||
name = "",
|
||||
width = 1,
|
||||
},
|
||||
delete = {
|
||||
order = 19,
|
||||
type = "execute",
|
||||
confirm = true,
|
||||
confirmText = self.L.Options_Mapping_Delete_ConfirmText,
|
||||
name = self.L.Options_Mapping_Delete_Name,
|
||||
desc = self.L.Options_Mapping_Delete_Desc,
|
||||
width = 0.5,
|
||||
func = function(info) self:DeleteMapping(info) end
|
||||
},
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function Grichelde:SetupOptions()
|
||||
-- add DB-backed profiles to UI options
|
||||
local options = self:CreateOptionsUI()
|
||||
options.args.profiles = LibStub("AceDBOptions-3.0"):GetOptionsTable(self.db)
|
||||
options.args.profiles.disabled = false
|
||||
|
||||
-- Adding options to blizzard frame
|
||||
LibStub("AceConfig-3.0"):RegisterOptionsTable(self.name, options)
|
||||
local dialog = LibStub("AceConfigDialog-3.0")
|
||||
dialog:AddToBlizOptions(self.name, self.L.AddonName)
|
||||
|
||||
return options, dialog
|
||||
end
|
||||
|
||||
function Grichelde:IsDisabled(info)
|
||||
if info.option.type == "group" then
|
||||
return false
|
||||
end
|
||||
return not self.db.profile.enabled
|
||||
end
|
||||
|
||||
function Grichelde:MappingName(info)
|
||||
-- self:TracePrint("MappingName : info")
|
||||
-- self:TracePrint(info)
|
||||
local option = self.db.profile.replacements[info[2]]
|
||||
|
||||
if nilOrEmpty(option.searchText) and nilOrEmpty(option.replaceText) then
|
||||
return self.L.Options_Mapping_EmptyMapping
|
||||
else
|
||||
return self:Format(self.L.Options_Mapping_Group_Name, option.searchText or "", option.replaceText or "")
|
||||
end
|
||||
end
|
||||
|
||||
function Grichelde:RefreshOptions(event)
|
||||
self:DebugPrint("RefreshOptions : event:", event)
|
||||
if event == "OnNewProfile" then
|
||||
self:PrefixedPrint(self.L.Profiles_Created, self.COLOR_CODES.GREEN .. self.db:GetCurrentProfile() .. self.COLOR_CODES.CLOSE)
|
||||
elseif event == "OnProfileChanged" then
|
||||
self:PrefixedPrint(self.L.Profiles_Loaded, self.COLOR_CODES.GREEN .. self.db:GetCurrentProfile() .. self.COLOR_CODES.CLOSE)
|
||||
elseif event == "OnProfileDeleted" then
|
||||
self:PrefixedPrint(self.L.Profiles_Deleted, self.COLOR_CODES.GREEN .. self.db:GetCurrentProfile() .. self.COLOR_CODES.CLOSE)
|
||||
elseif event == "OnProfileCopied" then
|
||||
self:PrefixedPrint(self.L.Profiles_Copied, self.COLOR_CODES.GREEN .. self.db:GetCurrentProfile() .. self.COLOR_CODES.CLOSE)
|
||||
elseif event == "OnProfileReset" then
|
||||
self:PrefixedPrint(self.L.Profiles_Reset, self.COLOR_CODES.GREEN .. self.db:GetCurrentProfile() .. self.COLOR_CODES.CLOSE)
|
||||
else
|
||||
self:DebugPrint("Refreshing Profile %s on options change: %s", self.db:GetCurrentProfile(), event)
|
||||
end
|
||||
|
||||
self:ReorderReplacements()
|
||||
self:RefreshReplacements(self.db.profile.replacements)
|
||||
end
|
||||
|
||||
--- Create UI options for rhe given replacement table (from DB).
|
||||
--- Usually called with with self.db.profile.replacements
|
||||
-- @param replacementsTable
|
||||
function Grichelde:RefreshReplacements(replacementsTable)
|
||||
self:TracePrint("RefreshReplacements : DB table:")
|
||||
self:TracePrint(replacementsTable)
|
||||
|
||||
-- remove all previous replacements from options (not DB), except header and buttons
|
||||
local replacements = self.options.args.replacements.args or {}
|
||||
for k, _ in pairs(replacements) do
|
||||
if k and find(k, "^replacement_") then
|
||||
replacements[k] = nil
|
||||
end
|
||||
end
|
||||
|
||||
for replName, _ in pairs(replacementsTable or {}) do
|
||||
local _, replNumber = self:SplitOnFirstMatch(replName, "_")
|
||||
replacements[replName] = self:CreateMapping(toNumber(replNumber))
|
||||
end
|
||||
|
||||
-- self:TracePrint("RefreshReplacements : UI options:")
|
||||
-- self:TracePrint(replacements)
|
||||
|
||||
self.dialog:ConfigTableChanged(nil, self.name)
|
||||
end
|
||||
|
||||
function Grichelde:AddEmptyMapping()
|
||||
local replacements = self.db.profile.replacements or {}
|
||||
|
||||
self:DebugPrint("AddEmptyMapping : old DB entries:")
|
||||
self:DebugPrint(replacements)
|
||||
|
||||
local maxRepl = Grichelde.MAPPING_OFFSET
|
||||
for replName, _ in pairs(replacements) do
|
||||
local num = match(replName, "^replacement_(%d+)")
|
||||
if num and maxRepl < toNumber(num) then
|
||||
maxRepl = toNumber(num)
|
||||
end
|
||||
end
|
||||
|
||||
local newMapping = "replacement_" .. toString(maxRepl + 1)
|
||||
self:DebugPrint("AddEmptyMapping : new mapping key:", newMapping)
|
||||
|
||||
-- do NOT set self.db.profile.replacements = {} it will break defaults
|
||||
replacements[newMapping].order = toString(maxRepl + 1) -- will be reordered anyway
|
||||
self:DebugPrint("AddEmptyMapping : new DB entries:")
|
||||
self:DebugPrint(replacements)
|
||||
|
||||
self:RefreshOptions("AddEmptyMapping " .. newMapping)
|
||||
self.dialog:SelectGroup(self.name, "replacements", newMapping)
|
||||
end
|
||||
|
||||
function Grichelde:MoveUp(info)
|
||||
self:TracePrint("MoveUp : info")
|
||||
for i = 0, #info do
|
||||
self:TracePrint("%d = %s", i, info[i])
|
||||
end
|
||||
|
||||
local replacements = self.db.profile.replacements or {}
|
||||
local currentName = info[2]
|
||||
|
||||
self:DebugPrint("MoveUp : \"%s\"", currentName)
|
||||
self:DebugPrint(replacements[currentName])
|
||||
|
||||
local _, replNumber = self:SplitOnFirstMatch(currentName, "_")
|
||||
local currentOrder = toNumber(replNumber)
|
||||
|
||||
-- if not on top
|
||||
if currentOrder ~= Grichelde.MAPPING_OFFSET then
|
||||
local swapName = "replacement_" .. toString(currentOrder - 1)
|
||||
|
||||
-- swap ordering
|
||||
self:DebugPrint("swap with option %s", swapName)
|
||||
|
||||
replacements[swapName].order = currentOrder
|
||||
replacements[currentName].order = currentOrder - 1
|
||||
|
||||
self:RefreshOptions("MoveUp " .. currentName)
|
||||
|
||||
self:DebugPrint("MoveUp : refresh focus on %s", swapName)
|
||||
self.dialog:SelectGroup(self.name, "replacements", swapName)
|
||||
else
|
||||
self:DebugPrint("MoveUp : already on top")
|
||||
end
|
||||
end
|
||||
|
||||
function Grichelde:MoveDown(info)
|
||||
self:TracePrint("MoveDown : info")
|
||||
for i = 0, #info do
|
||||
self:TracePrint("%d = %s", i, info[i])
|
||||
end
|
||||
|
||||
local replacements = self.db.profile.replacements or {}
|
||||
local currentName = info[2]
|
||||
|
||||
self:DebugPrint("MoveDown : \"%s\"", currentName)
|
||||
self:DebugPrint(replacements[currentName])
|
||||
|
||||
local _, replNumber = self:SplitOnFirstMatch(currentName, "_")
|
||||
local currentOrder = toNumber(replNumber)
|
||||
|
||||
local maxRepl = Grichelde.MAPPING_OFFSET
|
||||
for replName, _ in pairs(replacements) do
|
||||
local num = match(replName, "^replacement_(%d+)")
|
||||
if num and maxRepl < toNumber(num) then
|
||||
maxRepl = toNumber(num)
|
||||
end
|
||||
end
|
||||
|
||||
-- if not last element
|
||||
self:DebugPrint("MoveDown : maxRepl: %d", maxRepl)
|
||||
if currentOrder < maxRepl then
|
||||
local swapName = "replacement_" .. toString(currentOrder + 1)
|
||||
|
||||
-- swap ordering
|
||||
self:DebugPrint("swap with option %s", swapName)
|
||||
|
||||
replacements[swapName].order = currentOrder
|
||||
replacements[currentName].order = currentOrder + 1
|
||||
|
||||
self:RefreshOptions("MoveDown " .. currentName)
|
||||
|
||||
self:DebugPrint("MoveDown : refresh focus on %s", swapName)
|
||||
self.dialog:SelectGroup(self.name, "replacements", swapName)
|
||||
else
|
||||
self:DebugPrint("MoveDown : already at bottom")
|
||||
end
|
||||
end
|
||||
|
||||
function Grichelde:DeleteMapping(info)
|
||||
self:TracePrint("DeleteMapping : info")
|
||||
for i = 0, #info do
|
||||
self:TracePrint("%d = %s", i, info[i])
|
||||
end
|
||||
|
||||
local currentName = info[2]
|
||||
|
||||
self:DebugPrint("delete option: %s", currentName)
|
||||
self.db.profile.replacements[currentName] = nil
|
||||
|
||||
self:RefreshOptions("DeleteMapping " .. currentName)
|
||||
|
||||
local _, replNumber = self:SplitOnFirstMatch(currentName, "_")
|
||||
local newMapping = "replacement_" .. toNumber(replNumber - 1)
|
||||
self.dialog:SelectGroup(self.name, "replacements", newMapping)
|
||||
end
|
||||
|
||||
function Grichelde:DeleteAllMappings()
|
||||
self:DebugPrint("DeleteAllMappings")
|
||||
|
||||
-- do NOT set self.db.profile.replacements = {} it will break defaults
|
||||
for replName, _ in pairs(self.db.profile.replacements or {}) do
|
||||
self.db.profile.replacements[replName] = nil
|
||||
end
|
||||
self:AddEmptyMapping()
|
||||
|
||||
self:RefreshOptions("DeleteAllMappings")
|
||||
end
|
84
GricheldeUpgrade.lua
Normal file
84
GricheldeUpgrade.lua
Normal file
@@ -0,0 +1,84 @@
|
||||
-- read namespace from global env
|
||||
local _G = _G
|
||||
local Grichelde = _G.Grichelde
|
||||
|
||||
local pairs, find, toNumber = Grichelde.functions.pairs, Grichelde.functions.find, Grichelde.functions.toNumber
|
||||
|
||||
function Grichelde:Upgrade_To_v060()
|
||||
self:PrefixedPrint(self.L.Upgrade_ToVersion, Grichelde.COLOR_CODES.ORANGE .. "0.6.0" .. Grichelde.COLOR_CODES.CLOSE)
|
||||
|
||||
local replacements = self.db.profile.replacements or {}
|
||||
self:DebugPrint("Upgrade_To_060 : old database")
|
||||
self:DebugPrint(replacements)
|
||||
|
||||
for _, replTable in pairs(replacements) do
|
||||
replTable["ignoreCase"] = not replTable["caseSensitive"]
|
||||
replTable["caseSensitive"] = nil
|
||||
end
|
||||
|
||||
self:DebugPrint("Upgrade_To_060 : new database")
|
||||
self:DebugPrint(replacements)
|
||||
return 0, 6, 0
|
||||
end
|
||||
|
||||
function Grichelde:Upgrade_To_v070()
|
||||
self:PrefixedPrint(self.L.Upgrade_ToVersion, Grichelde.COLOR_CODES.ORANGE .. "0.7.0" .. Grichelde.COLOR_CODES.CLOSE)
|
||||
|
||||
local replacements = self.db.profile.replacements or {}
|
||||
self:DebugPrint("Upgrade_To_070 : old database")
|
||||
self:DebugPrint(replacements)
|
||||
|
||||
for _, replTable in pairs(replacements) do
|
||||
replTable["exactCase"] = not replTable["ignoreCase"]
|
||||
replTable["ignoreCase"] = nil
|
||||
end
|
||||
|
||||
self:DebugPrint("Upgrade_To_070 : new database")
|
||||
self:DebugPrint(replacements)
|
||||
return 0, 7, 0
|
||||
end
|
||||
|
||||
--[[
|
||||
function Grichelde:Upgrade_To_v071()
|
||||
return 0, 7, 1
|
||||
end
|
||||
]]
|
||||
|
||||
function Grichelde:UpgradeDatabase()
|
||||
local dbVersion = self.db.global.version or "0.0.0"
|
||||
self:DebugPrint("Database version:", dbVersion)
|
||||
|
||||
local _, _, maj, min, pat = find(dbVersion, "(%d+)%.(%d+)%.(%d+).*")
|
||||
local major, minor, patch = toNumber(maj) or 0, toNumber(min) or 0, toNumber(pat) or 0
|
||||
|
||||
local upgrade = 0
|
||||
local error = false
|
||||
|
||||
if major == 0 then
|
||||
if minor < 6 then
|
||||
upgrade = upgrade + 1
|
||||
major, minor, patch = self:Upgrade_To_v060(dbVersion)
|
||||
end
|
||||
if minor < 7 then
|
||||
upgrade = upgrade + 1
|
||||
major, minor, patch = self:Upgrade_To_v070(dbVersion)
|
||||
end
|
||||
--[[
|
||||
if minor == 7 then
|
||||
if patch < 1 then
|
||||
upgrade = upgrade + 1
|
||||
major, minor, patch = self:Upgrade_To_v71(dbVersion)
|
||||
end
|
||||
end
|
||||
]]
|
||||
end
|
||||
|
||||
if upgrade == 0 then
|
||||
self:DebugPrint("Database up-to-date")
|
||||
else
|
||||
if not error then
|
||||
self.db.global.version = self.version
|
||||
self:PrefixedPrint(Grichelde.COLOR_CODES.GREEN .. self.L.Upgrade_Successful .. Grichelde.COLOR_CODES.CLOSE)
|
||||
end
|
||||
end
|
||||
end
|
173
GricheldeUtils.lua
Normal file
173
GricheldeUtils.lua
Normal file
@@ -0,0 +1,173 @@
|
||||
-- import addon read namespace from global env
|
||||
local _G = _G
|
||||
local Grichelde = _G.Grichelde
|
||||
|
||||
local type, print, pairs, tSize, select, unpack, find, format, rep, toString
|
||||
= Grichelde.functions.type, Grichelde.functions.print, Grichelde.functions.pairs, Grichelde.functions.tSize, Grichelde.functions.select, Grichelde.functions.unpack, Grichelde.functions.find, Grichelde.functions.format, Grichelde.functions.rep, Grichelde.functions.toString
|
||||
|
||||
-- show strings differently to distinguish them from numbers
|
||||
local function plainValue(val)
|
||||
if val == nil then
|
||||
return "<nil>"
|
||||
elseif type(val) == "string" then
|
||||
return '"' .. val .. '"'
|
||||
elseif type(val) == "table" then
|
||||
if tSize(val) > 0 then
|
||||
return toString(val)
|
||||
else
|
||||
return "{}"
|
||||
end
|
||||
else
|
||||
return toString(val)
|
||||
end
|
||||
end
|
||||
|
||||
--- Prints any value to default channel, do NOT return a string.
|
||||
local function tPrint(val, indent, known, printFunc)
|
||||
local printF = printFunc or print
|
||||
indent = indent or 0
|
||||
known = known or {}
|
||||
|
||||
if val == nil then
|
||||
printF(rep(" ", indent) .. "<nil>")
|
||||
elseif type(val) == "string" then
|
||||
printF(rep(" ", indent) .. "\"" .. val .. "\"")
|
||||
elseif type(val) == "table" then
|
||||
if tSize(val) > 0 then
|
||||
for key, value in pairs(val) do
|
||||
if value == nil then
|
||||
printF(rep(" ", indent) .. plainValue(key) .. " = <nil>")
|
||||
elseif type(value) == "table" then
|
||||
printF(rep(" ", indent) .. plainValue(key) .. " = {")
|
||||
if tSize(value) > 0 then
|
||||
if not known[value] then
|
||||
tPrint(value, indent + 4, known, printF)
|
||||
known[value] = true
|
||||
else
|
||||
printF("<known table> " .. plainValue(value))
|
||||
end
|
||||
end
|
||||
printF(rep(" ", indent) .. "}")
|
||||
else
|
||||
printF(rep(" ", indent) .. plainValue(key) .. " = " .. plainValue(value))
|
||||
end
|
||||
end
|
||||
else
|
||||
printF(rep(" ", indent) .. "{}")
|
||||
end
|
||||
else
|
||||
printF(rep(" ", indent) .. toString(val))
|
||||
end
|
||||
end
|
||||
|
||||
-- split at first word of a text line
|
||||
function Grichelde:SplitOnFirstMatch(text, delimPattern, start)
|
||||
if text == nil then return nil end
|
||||
local pattern = "^(.-)" .. (delimPattern or " " ) .."(.*)"
|
||||
local pos = start or 1
|
||||
self:TracePrint("SplitOnFirstMatch : text: %s, pattern: %s, start: %d", text, pattern, start)
|
||||
local _, _, left, right = find(text, pattern, pos)
|
||||
self:TracePrint("SplitOnFirstMatch : left: %s, right: %s", left, right)
|
||||
return left or text, right
|
||||
end
|
||||
|
||||
-- split at last word of a text line
|
||||
function Grichelde:SplitOnLastMatch(text, delimPattern, start)
|
||||
local pattern = "(.*)" .. (delimPattern or " ") .. "(.-)$"
|
||||
local pos = start or 1
|
||||
self:TracePrint("SplitOnLastMatch : text: %s, pattern: %s, start: %d", text, pattern, start)
|
||||
local _, _, left, right = find(text, pattern, pos)
|
||||
self:TracePrint("SplitOnLastMatch : left: %s, right: %s", left, right)
|
||||
return left, right or text
|
||||
end
|
||||
|
||||
-- split at last word of a text line
|
||||
function Grichelde:TestMatch(text, pattern)
|
||||
local _, _, left, right = find(text, pattern, 1)
|
||||
self:DebugPrint("TestMatch : left: %s, right: %s", left, right)
|
||||
end
|
||||
|
||||
function Grichelde:Format(message, ...)
|
||||
if ( not message ) then
|
||||
return "<nil>"
|
||||
elseif type(message) == "string" then
|
||||
if ( not find(message, "%%")) then
|
||||
return message, ...
|
||||
else
|
||||
local l = select("#", ...)
|
||||
if l > 0 then
|
||||
-- sanitize nil values in vararg
|
||||
local packed = { ... }
|
||||
for i = 1, l do
|
||||
packed[i] = toString(packed[i]) or "nil"
|
||||
end
|
||||
-- print("packed = ", packed)
|
||||
-- self:tPrint(packed)
|
||||
-- cannot assign unpacked to a vararg variable and print it for debug
|
||||
-- Manually set count as unpack() stops on nil (bug with #table)
|
||||
return format(message, unpack(packed, 1, l))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- deprecated
|
||||
function Grichelde:Print(...)
|
||||
print(self:Format(...))
|
||||
end
|
||||
|
||||
function Grichelde:PrefixedPrint(...)
|
||||
print(self.COLOR_CODES.PREFIX .. self.L.AddonName .. self.COLOR_CODES.CLOSE .. ":", self:Format(...))
|
||||
end
|
||||
|
||||
function Grichelde:DebugPrint(obj, ...)
|
||||
self:LogPrint(Grichelde.LOG_LEVEL.DEBUG, function(...)
|
||||
print(self.COLOR_CODES.GRAY .. self.L.AddonName .. self.COLOR_CODES.CLOSE .. ":", self:Format(...))
|
||||
end, obj, ...)
|
||||
end
|
||||
|
||||
function Grichelde:TracePrint(obj, ...)
|
||||
self:LogPrint(Grichelde.LOG_LEVEL.TRACE, function(...)
|
||||
print(self.COLOR_CODES.DARKGRAY .. self.L.AddonName .. self.COLOR_CODES.CLOSE .. ":", self:Format(...))
|
||||
end, obj, ...)
|
||||
end
|
||||
|
||||
function Grichelde:LogPrint(logLevel, printFunc, obj, ...)
|
||||
if (self.logLevel >= logLevel) then
|
||||
local printF = printFunc or print
|
||||
if obj == nil then
|
||||
printF("<nil>")
|
||||
else
|
||||
if type(obj) == "string" then
|
||||
local l = select("#", ...)
|
||||
if ( l == 0 or not find(obj, "%%")) then
|
||||
printF(obj, ...)
|
||||
else
|
||||
-- sanitize nil values in vararg
|
||||
local packed = { ... }
|
||||
for i = 1, l do
|
||||
packed[i] = toString(packed[i]) or "nil"
|
||||
end
|
||||
|
||||
-- print("packed = ", packed)
|
||||
-- self:tPrint(packed)
|
||||
-- cannot assign unpacked to a vararg variable and print it for debug
|
||||
local fmtMsg = format(obj, unpack(packed, 1, l)) -- manually set count as unpack() stops on nil (bug with #table)
|
||||
printF(fmtMsg)
|
||||
end
|
||||
elseif type(obj) == "table" then
|
||||
tPrint(obj, 0, {}, printF)
|
||||
else
|
||||
printF(plainValue(obj))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Grichelde:PrintOptions()
|
||||
self:DebugPrint(self.options.args.replacements.args)
|
||||
end
|
||||
|
||||
function Grichelde:PrintMappings()
|
||||
self:DebugPrint(self.db.profile.replacements)
|
||||
end
|
58
Libs/AceConfig-3.0/AceConfig-3.0.lua
Normal file
58
Libs/AceConfig-3.0/AceConfig-3.0.lua
Normal file
@@ -0,0 +1,58 @@
|
||||
--- AceConfig-3.0 wrapper library.
|
||||
-- Provides an API to register an options table with the config registry,
|
||||
-- as well as associate it with a slash command.
|
||||
-- @class file
|
||||
-- @name AceConfig-3.0
|
||||
-- @release $Id: AceConfig-3.0.lua 1161 2017-08-12 14:30:16Z funkydude $
|
||||
|
||||
--[[
|
||||
AceConfig-3.0
|
||||
|
||||
Very light wrapper library that combines all the AceConfig subcomponents into one more easily used whole.
|
||||
|
||||
]]
|
||||
|
||||
local cfgreg = LibStub("AceConfigRegistry-3.0")
|
||||
local cfgcmd = LibStub("AceConfigCmd-3.0")
|
||||
|
||||
local MAJOR, MINOR = "AceConfig-3.0", 3
|
||||
local AceConfig = LibStub:NewLibrary(MAJOR, MINOR)
|
||||
|
||||
if not AceConfig then return end
|
||||
|
||||
--TODO: local cfgdlg = LibStub("AceConfigDialog-3.0", true)
|
||||
--TODO: local cfgdrp = LibStub("AceConfigDropdown-3.0", true)
|
||||
|
||||
-- Lua APIs
|
||||
local pcall, error, type, pairs = pcall, error, type, pairs
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
-- :RegisterOptionsTable(appName, options, slashcmd, persist)
|
||||
--
|
||||
-- - appName - (string) application name
|
||||
-- - options - table or function ref, see AceConfigRegistry
|
||||
-- - slashcmd - slash command (string) or table with commands, or nil to NOT create a slash command
|
||||
|
||||
--- Register a option table with the AceConfig registry.
|
||||
-- You can supply a slash command (or a table of slash commands) to register with AceConfigCmd directly.
|
||||
-- @paramsig appName, options [, slashcmd]
|
||||
-- @param appName The application name for the config table.
|
||||
-- @param options The option table (or a function to generate one on demand). http://www.wowace.com/addons/ace3/pages/ace-config-3-0-options-tables/
|
||||
-- @param slashcmd A slash command to register for the option table, or a table of slash commands.
|
||||
-- @usage
|
||||
-- local AceConfig = LibStub("AceConfig-3.0")
|
||||
-- AceConfig:RegisterOptionsTable("MyAddon", myOptions, {"/myslash", "/my"})
|
||||
function AceConfig:RegisterOptionsTable(appName, options, slashcmd)
|
||||
local ok,msg = pcall(cfgreg.RegisterOptionsTable, self, appName, options)
|
||||
if not ok then error(msg, 2) end
|
||||
|
||||
if slashcmd then
|
||||
if type(slashcmd) == "table" then
|
||||
for _,cmd in pairs(slashcmd) do
|
||||
cfgcmd:CreateChatCommand(cmd, appName)
|
||||
end
|
||||
else
|
||||
cfgcmd:CreateChatCommand(slashcmd, appName)
|
||||
end
|
||||
end
|
||||
end
|
8
Libs/AceConfig-3.0/AceConfig-3.0.xml
Normal file
8
Libs/AceConfig-3.0/AceConfig-3.0.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Include file="AceConfigRegistry-3.0\AceConfigRegistry-3.0.xml"/>
|
||||
<Include file="AceConfigCmd-3.0\AceConfigCmd-3.0.xml"/>
|
||||
<Include file="AceConfigDialog-3.0\AceConfigDialog-3.0.xml"/>
|
||||
<!--<Include file="AceConfigDropdown-3.0\AceConfigDropdown-3.0.xml"/>-->
|
||||
<Script file="AceConfig-3.0.lua"/>
|
||||
</Ui>
|
794
Libs/AceConfig-3.0/AceConfigCmd-3.0/AceConfigCmd-3.0.lua
Normal file
794
Libs/AceConfig-3.0/AceConfigCmd-3.0/AceConfigCmd-3.0.lua
Normal file
@@ -0,0 +1,794 @@
|
||||
--- AceConfigCmd-3.0 handles access to an options table through the "command line" interface via the ChatFrames.
|
||||
-- @class file
|
||||
-- @name AceConfigCmd-3.0
|
||||
-- @release $Id: AceConfigCmd-3.0.lua 1161 2017-08-12 14:30:16Z funkydude $
|
||||
|
||||
--[[
|
||||
AceConfigCmd-3.0
|
||||
|
||||
Handles commandline optionstable access
|
||||
|
||||
REQUIRES: AceConsole-3.0 for command registration (loaded on demand)
|
||||
|
||||
]]
|
||||
|
||||
-- TODO: plugin args
|
||||
|
||||
local cfgreg = LibStub("AceConfigRegistry-3.0")
|
||||
|
||||
local MAJOR, MINOR = "AceConfigCmd-3.0", 14
|
||||
local AceConfigCmd = LibStub:NewLibrary(MAJOR, MINOR)
|
||||
|
||||
if not AceConfigCmd then return end
|
||||
|
||||
AceConfigCmd.commands = AceConfigCmd.commands or {}
|
||||
local commands = AceConfigCmd.commands
|
||||
|
||||
local AceConsole -- LoD
|
||||
local AceConsoleName = "AceConsole-3.0"
|
||||
|
||||
-- Lua APIs
|
||||
local strsub, strsplit, strlower, strmatch, strtrim = string.sub, string.split, string.lower, string.match, string.trim
|
||||
local format, tonumber, tostring = string.format, tonumber, tostring
|
||||
local tsort, tinsert = table.sort, table.insert
|
||||
local select, pairs, next, type = select, pairs, next, type
|
||||
local error, assert = error, assert
|
||||
|
||||
-- WoW APIs
|
||||
local _G = _G
|
||||
|
||||
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
|
||||
-- List them here for Mikk's FindGlobals script
|
||||
-- GLOBALS: LibStub, SELECTED_CHAT_FRAME, DEFAULT_CHAT_FRAME
|
||||
|
||||
|
||||
local L = setmetatable({}, { -- TODO: replace with proper locale
|
||||
__index = function(self,k) return k end
|
||||
})
|
||||
|
||||
|
||||
|
||||
local function print(msg)
|
||||
(SELECTED_CHAT_FRAME or DEFAULT_CHAT_FRAME):AddMessage(msg)
|
||||
end
|
||||
|
||||
-- constants used by getparam() calls below
|
||||
|
||||
local handlertypes = {["table"]=true}
|
||||
local handlermsg = "expected a table"
|
||||
|
||||
local functypes = {["function"]=true, ["string"]=true}
|
||||
local funcmsg = "expected function or member name"
|
||||
|
||||
|
||||
-- pickfirstset() - picks the first non-nil value and returns it
|
||||
|
||||
local function pickfirstset(...)
|
||||
for i=1,select("#",...) do
|
||||
if select(i,...)~=nil then
|
||||
return select(i,...)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- err() - produce real error() regarding malformed options tables etc
|
||||
|
||||
local function err(info,inputpos,msg )
|
||||
local cmdstr=" "..strsub(info.input, 1, inputpos-1)
|
||||
error(MAJOR..": /" ..info[0] ..cmdstr ..": "..(msg or "malformed options table"), 2)
|
||||
end
|
||||
|
||||
|
||||
-- usererr() - produce chatframe message regarding bad slash syntax etc
|
||||
|
||||
local function usererr(info,inputpos,msg )
|
||||
local cmdstr=strsub(info.input, 1, inputpos-1);
|
||||
print("/" ..info[0] .. " "..cmdstr ..": "..(msg or "malformed options table"))
|
||||
end
|
||||
|
||||
|
||||
-- callmethod() - call a given named method (e.g. "get", "set") with given arguments
|
||||
|
||||
local function callmethod(info, inputpos, tab, methodtype, ...)
|
||||
local method = info[methodtype]
|
||||
if not method then
|
||||
err(info, inputpos, "'"..methodtype.."': not set")
|
||||
end
|
||||
|
||||
info.arg = tab.arg
|
||||
info.option = tab
|
||||
info.type = tab.type
|
||||
|
||||
if type(method)=="function" then
|
||||
return method(info, ...)
|
||||
elseif type(method)=="string" then
|
||||
if type(info.handler[method])~="function" then
|
||||
err(info, inputpos, "'"..methodtype.."': '"..method.."' is not a member function of "..tostring(info.handler))
|
||||
end
|
||||
return info.handler[method](info.handler, info, ...)
|
||||
else
|
||||
assert(false) -- type should have already been checked on read
|
||||
end
|
||||
end
|
||||
|
||||
-- callfunction() - call a given named function (e.g. "name", "desc") with given arguments
|
||||
|
||||
local function callfunction(info, tab, methodtype, ...)
|
||||
local method = tab[methodtype]
|
||||
|
||||
info.arg = tab.arg
|
||||
info.option = tab
|
||||
info.type = tab.type
|
||||
|
||||
if type(method)=="function" then
|
||||
return method(info, ...)
|
||||
else
|
||||
assert(false) -- type should have already been checked on read
|
||||
end
|
||||
end
|
||||
|
||||
-- do_final() - do the final step (set/execute) along with validation and confirmation
|
||||
|
||||
local function do_final(info, inputpos, tab, methodtype, ...)
|
||||
if info.validate then
|
||||
local res = callmethod(info,inputpos,tab,"validate",...)
|
||||
if type(res)=="string" then
|
||||
usererr(info, inputpos, "'"..strsub(info.input, inputpos).."' - "..res)
|
||||
return
|
||||
end
|
||||
end
|
||||
-- console ignores .confirm
|
||||
|
||||
callmethod(info,inputpos,tab,methodtype, ...)
|
||||
end
|
||||
|
||||
|
||||
-- getparam() - used by handle() to retreive and store "handler", "get", "set", etc
|
||||
|
||||
local function getparam(info, inputpos, tab, depth, paramname, types, errormsg)
|
||||
local old,oldat = info[paramname], info[paramname.."_at"]
|
||||
local val=tab[paramname]
|
||||
if val~=nil then
|
||||
if val==false then
|
||||
val=nil
|
||||
elseif not types[type(val)] then
|
||||
err(info, inputpos, "'" .. paramname.. "' - "..errormsg)
|
||||
end
|
||||
info[paramname] = val
|
||||
info[paramname.."_at"] = depth
|
||||
end
|
||||
return old,oldat
|
||||
end
|
||||
|
||||
|
||||
-- iterateargs(tab) - custom iterator that iterates both t.args and t.plugins.*
|
||||
local dummytable={}
|
||||
|
||||
local function iterateargs(tab)
|
||||
if not tab.plugins then
|
||||
return pairs(tab.args)
|
||||
end
|
||||
|
||||
local argtabkey,argtab=next(tab.plugins)
|
||||
local v
|
||||
|
||||
return function(_, k)
|
||||
while argtab do
|
||||
k,v = next(argtab, k)
|
||||
if k then return k,v end
|
||||
if argtab==tab.args then
|
||||
argtab=nil
|
||||
else
|
||||
argtabkey,argtab = next(tab.plugins, argtabkey)
|
||||
if not argtabkey then
|
||||
argtab=tab.args
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function checkhidden(info, inputpos, tab)
|
||||
if tab.cmdHidden~=nil then
|
||||
return tab.cmdHidden
|
||||
end
|
||||
local hidden = tab.hidden
|
||||
if type(hidden) == "function" or type(hidden) == "string" then
|
||||
info.hidden = hidden
|
||||
hidden = callmethod(info, inputpos, tab, 'hidden')
|
||||
info.hidden = nil
|
||||
end
|
||||
return hidden
|
||||
end
|
||||
|
||||
local function showhelp(info, inputpos, tab, depth, noHead)
|
||||
if not noHead then
|
||||
print("|cff33ff99"..info.appName.."|r: Arguments to |cffffff78/"..info[0].."|r "..strsub(info.input,1,inputpos-1)..":")
|
||||
end
|
||||
|
||||
local sortTbl = {} -- [1..n]=name
|
||||
local refTbl = {} -- [name]=tableref
|
||||
|
||||
for k,v in iterateargs(tab) do
|
||||
if not refTbl[k] then -- a plugin overriding something in .args
|
||||
tinsert(sortTbl, k)
|
||||
refTbl[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
tsort(sortTbl, function(one, two)
|
||||
local o1 = refTbl[one].order or 100
|
||||
local o2 = refTbl[two].order or 100
|
||||
if type(o1) == "function" or type(o1) == "string" then
|
||||
info.order = o1
|
||||
info[#info+1] = one
|
||||
o1 = callmethod(info, inputpos, refTbl[one], "order")
|
||||
info[#info] = nil
|
||||
info.order = nil
|
||||
end
|
||||
if type(o2) == "function" or type(o1) == "string" then
|
||||
info.order = o2
|
||||
info[#info+1] = two
|
||||
o2 = callmethod(info, inputpos, refTbl[two], "order")
|
||||
info[#info] = nil
|
||||
info.order = nil
|
||||
end
|
||||
if o1<0 and o2<0 then return o1<o2 end
|
||||
if o2<0 then return true end
|
||||
if o1<0 then return false end
|
||||
if o1==o2 then return tostring(one)<tostring(two) end -- compare names
|
||||
return o1<o2
|
||||
end)
|
||||
|
||||
for i = 1, #sortTbl do
|
||||
local k = sortTbl[i]
|
||||
local v = refTbl[k]
|
||||
if not checkhidden(info, inputpos, v) then
|
||||
if v.type ~= "description" and v.type ~= "header" then
|
||||
-- recursively show all inline groups
|
||||
local name, desc = v.name, v.desc
|
||||
if type(name) == "function" then
|
||||
name = callfunction(info, v, 'name')
|
||||
end
|
||||
if type(desc) == "function" then
|
||||
desc = callfunction(info, v, 'desc')
|
||||
end
|
||||
if v.type == "group" and pickfirstset(v.cmdInline, v.inline, false) then
|
||||
print(" "..(desc or name)..":")
|
||||
local oldhandler,oldhandler_at = getparam(info, inputpos, v, depth, "handler", handlertypes, handlermsg)
|
||||
showhelp(info, inputpos, v, depth, true)
|
||||
info.handler,info.handler_at = oldhandler,oldhandler_at
|
||||
else
|
||||
local key = k:gsub(" ", "_")
|
||||
print(" |cffffff78"..key.."|r - "..(desc or name or ""))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function keybindingValidateFunc(text)
|
||||
if text == nil or text == "NONE" then
|
||||
return nil
|
||||
end
|
||||
text = text:upper()
|
||||
local shift, ctrl, alt
|
||||
local modifier
|
||||
while true do
|
||||
if text == "-" then
|
||||
break
|
||||
end
|
||||
modifier, text = strsplit('-', text, 2)
|
||||
if text then
|
||||
if modifier ~= "SHIFT" and modifier ~= "CTRL" and modifier ~= "ALT" then
|
||||
return false
|
||||
end
|
||||
if modifier == "SHIFT" then
|
||||
if shift then
|
||||
return false
|
||||
end
|
||||
shift = true
|
||||
end
|
||||
if modifier == "CTRL" then
|
||||
if ctrl then
|
||||
return false
|
||||
end
|
||||
ctrl = true
|
||||
end
|
||||
if modifier == "ALT" then
|
||||
if alt then
|
||||
return false
|
||||
end
|
||||
alt = true
|
||||
end
|
||||
else
|
||||
text = modifier
|
||||
break
|
||||
end
|
||||
end
|
||||
if text == "" then
|
||||
return false
|
||||
end
|
||||
if not text:find("^F%d+$") and text ~= "CAPSLOCK" and text:len() ~= 1 and (text:byte() < 128 or text:len() > 4) and not _G["KEY_" .. text] then
|
||||
return false
|
||||
end
|
||||
local s = text
|
||||
if shift then
|
||||
s = "SHIFT-" .. s
|
||||
end
|
||||
if ctrl then
|
||||
s = "CTRL-" .. s
|
||||
end
|
||||
if alt then
|
||||
s = "ALT-" .. s
|
||||
end
|
||||
return s
|
||||
end
|
||||
|
||||
-- handle() - selfrecursing function that processes input->optiontable
|
||||
-- - depth - starts at 0
|
||||
-- - retfalse - return false rather than produce error if a match is not found (used by inlined groups)
|
||||
|
||||
local function handle(info, inputpos, tab, depth, retfalse)
|
||||
|
||||
if not(type(tab)=="table" and type(tab.type)=="string") then err(info,inputpos) end
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- Grab hold of handler,set,get,func,etc if set (and remember old ones)
|
||||
-- Note that we do NOT validate if method names are correct at this stage,
|
||||
-- the handler may change before they're actually used!
|
||||
|
||||
local oldhandler,oldhandler_at = getparam(info,inputpos,tab,depth,"handler",handlertypes,handlermsg)
|
||||
local oldset,oldset_at = getparam(info,inputpos,tab,depth,"set",functypes,funcmsg)
|
||||
local oldget,oldget_at = getparam(info,inputpos,tab,depth,"get",functypes,funcmsg)
|
||||
local oldfunc,oldfunc_at = getparam(info,inputpos,tab,depth,"func",functypes,funcmsg)
|
||||
local oldvalidate,oldvalidate_at = getparam(info,inputpos,tab,depth,"validate",functypes,funcmsg)
|
||||
--local oldconfirm,oldconfirm_at = getparam(info,inputpos,tab,depth,"confirm",functypes,funcmsg)
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- Act according to .type of this table
|
||||
|
||||
if tab.type=="group" then
|
||||
------------ group --------------------------------------------
|
||||
|
||||
if type(tab.args)~="table" then err(info, inputpos) end
|
||||
if tab.plugins and type(tab.plugins)~="table" then err(info,inputpos) end
|
||||
|
||||
-- grab next arg from input
|
||||
local _,nextpos,arg = (info.input):find(" *([^ ]+) *", inputpos)
|
||||
if not arg then
|
||||
showhelp(info, inputpos, tab, depth)
|
||||
return
|
||||
end
|
||||
nextpos=nextpos+1
|
||||
|
||||
-- loop .args and try to find a key with a matching name
|
||||
for k,v in iterateargs(tab) do
|
||||
if not(type(k)=="string" and type(v)=="table" and type(v.type)=="string") then err(info,inputpos, "options table child '"..tostring(k).."' is malformed") end
|
||||
|
||||
-- is this child an inline group? if so, traverse into it
|
||||
if v.type=="group" and pickfirstset(v.cmdInline, v.inline, false) then
|
||||
info[depth+1] = k
|
||||
if handle(info, inputpos, v, depth+1, true)==false then
|
||||
info[depth+1] = nil
|
||||
-- wasn't found in there, but that's ok, we just keep looking down here
|
||||
else
|
||||
return -- done, name was found in inline group
|
||||
end
|
||||
-- matching name and not a inline group
|
||||
elseif strlower(arg)==strlower(k:gsub(" ", "_")) then
|
||||
info[depth+1] = k
|
||||
return handle(info,nextpos,v,depth+1)
|
||||
end
|
||||
end
|
||||
|
||||
-- no match
|
||||
if retfalse then
|
||||
-- restore old infotable members and return false to indicate failure
|
||||
info.handler,info.handler_at = oldhandler,oldhandler_at
|
||||
info.set,info.set_at = oldset,oldset_at
|
||||
info.get,info.get_at = oldget,oldget_at
|
||||
info.func,info.func_at = oldfunc,oldfunc_at
|
||||
info.validate,info.validate_at = oldvalidate,oldvalidate_at
|
||||
--info.confirm,info.confirm_at = oldconfirm,oldconfirm_at
|
||||
return false
|
||||
end
|
||||
|
||||
-- couldn't find the command, display error
|
||||
usererr(info, inputpos, "'"..arg.."' - " .. L["unknown argument"])
|
||||
return
|
||||
end
|
||||
|
||||
local str = strsub(info.input,inputpos);
|
||||
|
||||
if tab.type=="execute" then
|
||||
------------ execute --------------------------------------------
|
||||
do_final(info, inputpos, tab, "func")
|
||||
|
||||
|
||||
|
||||
elseif tab.type=="input" then
|
||||
------------ input --------------------------------------------
|
||||
|
||||
local res = true
|
||||
if tab.pattern then
|
||||
if not(type(tab.pattern)=="string") then err(info, inputpos, "'pattern' - expected a string") end
|
||||
if not strmatch(str, tab.pattern) then
|
||||
usererr(info, inputpos, "'"..str.."' - " .. L["invalid input"])
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
do_final(info, inputpos, tab, "set", str)
|
||||
|
||||
|
||||
|
||||
elseif tab.type=="toggle" then
|
||||
------------ toggle --------------------------------------------
|
||||
local b
|
||||
local str = strtrim(strlower(str))
|
||||
if str=="" then
|
||||
b = callmethod(info, inputpos, tab, "get")
|
||||
|
||||
if tab.tristate then
|
||||
--cycle in true, nil, false order
|
||||
if b then
|
||||
b = nil
|
||||
elseif b == nil then
|
||||
b = false
|
||||
else
|
||||
b = true
|
||||
end
|
||||
else
|
||||
b = not b
|
||||
end
|
||||
|
||||
elseif str==L["on"] then
|
||||
b = true
|
||||
elseif str==L["off"] then
|
||||
b = false
|
||||
elseif tab.tristate and str==L["default"] then
|
||||
b = nil
|
||||
else
|
||||
if tab.tristate then
|
||||
usererr(info, inputpos, format(L["'%s' - expected 'on', 'off' or 'default', or no argument to toggle."], str))
|
||||
else
|
||||
usererr(info, inputpos, format(L["'%s' - expected 'on' or 'off', or no argument to toggle."], str))
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
do_final(info, inputpos, tab, "set", b)
|
||||
|
||||
|
||||
elseif tab.type=="range" then
|
||||
------------ range --------------------------------------------
|
||||
local val = tonumber(str)
|
||||
if not val then
|
||||
usererr(info, inputpos, "'"..str.."' - "..L["expected number"])
|
||||
return
|
||||
end
|
||||
if type(info.step)=="number" then
|
||||
val = val- (val % info.step)
|
||||
end
|
||||
if type(info.min)=="number" and val<info.min then
|
||||
usererr(info, inputpos, val.." - "..format(L["must be equal to or higher than %s"], tostring(info.min)) )
|
||||
return
|
||||
end
|
||||
if type(info.max)=="number" and val>info.max then
|
||||
usererr(info, inputpos, val.." - "..format(L["must be equal to or lower than %s"], tostring(info.max)) )
|
||||
return
|
||||
end
|
||||
|
||||
do_final(info, inputpos, tab, "set", val)
|
||||
|
||||
|
||||
elseif tab.type=="select" then
|
||||
------------ select ------------------------------------
|
||||
local str = strtrim(strlower(str))
|
||||
|
||||
local values = tab.values
|
||||
if type(values) == "function" or type(values) == "string" then
|
||||
info.values = values
|
||||
values = callmethod(info, inputpos, tab, "values")
|
||||
info.values = nil
|
||||
end
|
||||
|
||||
if str == "" then
|
||||
local b = callmethod(info, inputpos, tab, "get")
|
||||
local fmt = "|cffffff78- [%s]|r %s"
|
||||
local fmt_sel = "|cffffff78- [%s]|r %s |cffff0000*|r"
|
||||
print(L["Options for |cffffff78"..info[#info].."|r:"])
|
||||
for k, v in pairs(values) do
|
||||
if b == k then
|
||||
print(fmt_sel:format(k, v))
|
||||
else
|
||||
print(fmt:format(k, v))
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local ok
|
||||
for k,v in pairs(values) do
|
||||
if strlower(k)==str then
|
||||
str = k -- overwrite with key (in case of case mismatches)
|
||||
ok = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not ok then
|
||||
usererr(info, inputpos, "'"..str.."' - "..L["unknown selection"])
|
||||
return
|
||||
end
|
||||
|
||||
do_final(info, inputpos, tab, "set", str)
|
||||
|
||||
elseif tab.type=="multiselect" then
|
||||
------------ multiselect -------------------------------------------
|
||||
local str = strtrim(strlower(str))
|
||||
|
||||
local values = tab.values
|
||||
if type(values) == "function" or type(values) == "string" then
|
||||
info.values = values
|
||||
values = callmethod(info, inputpos, tab, "values")
|
||||
info.values = nil
|
||||
end
|
||||
|
||||
if str == "" then
|
||||
local fmt = "|cffffff78- [%s]|r %s"
|
||||
local fmt_sel = "|cffffff78- [%s]|r %s |cffff0000*|r"
|
||||
print(L["Options for |cffffff78"..info[#info].."|r (multiple possible):"])
|
||||
for k, v in pairs(values) do
|
||||
if callmethod(info, inputpos, tab, "get", k) then
|
||||
print(fmt_sel:format(k, v))
|
||||
else
|
||||
print(fmt:format(k, v))
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
--build a table of the selections, checking that they exist
|
||||
--parse for =on =off =default in the process
|
||||
--table will be key = true for options that should toggle, key = [on|off|default] for options to be set
|
||||
local sels = {}
|
||||
for v in str:gmatch("[^ ]+") do
|
||||
--parse option=on etc
|
||||
local opt, val = v:match('(.+)=(.+)')
|
||||
--get option if toggling
|
||||
if not opt then
|
||||
opt = v
|
||||
end
|
||||
|
||||
--check that the opt is valid
|
||||
local ok
|
||||
for k,v in pairs(values) do
|
||||
if strlower(k)==opt then
|
||||
opt = k -- overwrite with key (in case of case mismatches)
|
||||
ok = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not ok then
|
||||
usererr(info, inputpos, "'"..opt.."' - "..L["unknown selection"])
|
||||
return
|
||||
end
|
||||
|
||||
--check that if val was supplied it is valid
|
||||
if val then
|
||||
if val == L["on"] or val == L["off"] or (tab.tristate and val == L["default"]) then
|
||||
--val is valid insert it
|
||||
sels[opt] = val
|
||||
else
|
||||
if tab.tristate then
|
||||
usererr(info, inputpos, format(L["'%s' '%s' - expected 'on', 'off' or 'default', or no argument to toggle."], v, val))
|
||||
else
|
||||
usererr(info, inputpos, format(L["'%s' '%s' - expected 'on' or 'off', or no argument to toggle."], v, val))
|
||||
end
|
||||
return
|
||||
end
|
||||
else
|
||||
-- no val supplied, toggle
|
||||
sels[opt] = true
|
||||
end
|
||||
end
|
||||
|
||||
for opt, val in pairs(sels) do
|
||||
local newval
|
||||
|
||||
if (val == true) then
|
||||
--toggle the option
|
||||
local b = callmethod(info, inputpos, tab, "get", opt)
|
||||
|
||||
if tab.tristate then
|
||||
--cycle in true, nil, false order
|
||||
if b then
|
||||
b = nil
|
||||
elseif b == nil then
|
||||
b = false
|
||||
else
|
||||
b = true
|
||||
end
|
||||
else
|
||||
b = not b
|
||||
end
|
||||
newval = b
|
||||
else
|
||||
--set the option as specified
|
||||
if val==L["on"] then
|
||||
newval = true
|
||||
elseif val==L["off"] then
|
||||
newval = false
|
||||
elseif val==L["default"] then
|
||||
newval = nil
|
||||
end
|
||||
end
|
||||
|
||||
do_final(info, inputpos, tab, "set", opt, newval)
|
||||
end
|
||||
|
||||
|
||||
elseif tab.type=="color" then
|
||||
------------ color --------------------------------------------
|
||||
local str = strtrim(strlower(str))
|
||||
if str == "" then
|
||||
--TODO: Show current value
|
||||
return
|
||||
end
|
||||
|
||||
local r, g, b, a
|
||||
|
||||
local hasAlpha = tab.hasAlpha
|
||||
if type(hasAlpha) == "function" or type(hasAlpha) == "string" then
|
||||
info.hasAlpha = hasAlpha
|
||||
hasAlpha = callmethod(info, inputpos, tab, 'hasAlpha')
|
||||
info.hasAlpha = nil
|
||||
end
|
||||
|
||||
if hasAlpha then
|
||||
if str:len() == 8 and str:find("^%x*$") then
|
||||
--parse a hex string
|
||||
r,g,b,a = tonumber(str:sub(1, 2), 16) / 255, tonumber(str:sub(3, 4), 16) / 255, tonumber(str:sub(5, 6), 16) / 255, tonumber(str:sub(7, 8), 16) / 255
|
||||
else
|
||||
--parse seperate values
|
||||
r,g,b,a = str:match("^([%d%.]+) ([%d%.]+) ([%d%.]+) ([%d%.]+)$")
|
||||
r,g,b,a = tonumber(r), tonumber(g), tonumber(b), tonumber(a)
|
||||
end
|
||||
if not (r and g and b and a) then
|
||||
usererr(info, inputpos, format(L["'%s' - expected 'RRGGBBAA' or 'r g b a'."], str))
|
||||
return
|
||||
end
|
||||
|
||||
if r >= 0.0 and r <= 1.0 and g >= 0.0 and g <= 1.0 and b >= 0.0 and b <= 1.0 and a >= 0.0 and a <= 1.0 then
|
||||
--values are valid
|
||||
elseif r >= 0 and r <= 255 and g >= 0 and g <= 255 and b >= 0 and b <= 255 and a >= 0 and a <= 255 then
|
||||
--values are valid 0..255, convert to 0..1
|
||||
r = r / 255
|
||||
g = g / 255
|
||||
b = b / 255
|
||||
a = a / 255
|
||||
else
|
||||
--values are invalid
|
||||
usererr(info, inputpos, format(L["'%s' - values must all be either in the range 0..1 or 0..255."], str))
|
||||
end
|
||||
else
|
||||
a = 1.0
|
||||
if str:len() == 6 and str:find("^%x*$") then
|
||||
--parse a hex string
|
||||
r,g,b = tonumber(str:sub(1, 2), 16) / 255, tonumber(str:sub(3, 4), 16) / 255, tonumber(str:sub(5, 6), 16) / 255
|
||||
else
|
||||
--parse seperate values
|
||||
r,g,b = str:match("^([%d%.]+) ([%d%.]+) ([%d%.]+)$")
|
||||
r,g,b = tonumber(r), tonumber(g), tonumber(b)
|
||||
end
|
||||
if not (r and g and b) then
|
||||
usererr(info, inputpos, format(L["'%s' - expected 'RRGGBB' or 'r g b'."], str))
|
||||
return
|
||||
end
|
||||
if r >= 0.0 and r <= 1.0 and g >= 0.0 and g <= 1.0 and b >= 0.0 and b <= 1.0 then
|
||||
--values are valid
|
||||
elseif r >= 0 and r <= 255 and g >= 0 and g <= 255 and b >= 0 and b <= 255 then
|
||||
--values are valid 0..255, convert to 0..1
|
||||
r = r / 255
|
||||
g = g / 255
|
||||
b = b / 255
|
||||
else
|
||||
--values are invalid
|
||||
usererr(info, inputpos, format(L["'%s' - values must all be either in the range 0-1 or 0-255."], str))
|
||||
end
|
||||
end
|
||||
|
||||
do_final(info, inputpos, tab, "set", r,g,b,a)
|
||||
|
||||
elseif tab.type=="keybinding" then
|
||||
------------ keybinding --------------------------------------------
|
||||
local str = strtrim(strlower(str))
|
||||
if str == "" then
|
||||
--TODO: Show current value
|
||||
return
|
||||
end
|
||||
local value = keybindingValidateFunc(str:upper())
|
||||
if value == false then
|
||||
usererr(info, inputpos, format(L["'%s' - Invalid Keybinding."], str))
|
||||
return
|
||||
end
|
||||
|
||||
do_final(info, inputpos, tab, "set", value)
|
||||
|
||||
elseif tab.type=="description" then
|
||||
------------ description --------------------
|
||||
-- ignore description, GUI config only
|
||||
else
|
||||
err(info, inputpos, "unknown options table item type '"..tostring(tab.type).."'")
|
||||
end
|
||||
end
|
||||
|
||||
--- Handle the chat command.
|
||||
-- This is usually called from a chat command handler to parse the command input as operations on an aceoptions table.\\
|
||||
-- AceConfigCmd uses this function internally when a slash command is registered with `:CreateChatCommand`
|
||||
-- @param slashcmd The slash command WITHOUT leading slash (only used for error output)
|
||||
-- @param appName The application name as given to `:RegisterOptionsTable()`
|
||||
-- @param input The commandline input (as given by the WoW handler, i.e. without the command itself)
|
||||
-- @usage
|
||||
-- MyAddon = LibStub("AceAddon-3.0"):NewAddon("MyAddon", "AceConsole-3.0")
|
||||
-- -- Use AceConsole-3.0 to register a Chat Command
|
||||
-- MyAddon:RegisterChatCommand("mychat", "ChatCommand")
|
||||
--
|
||||
-- -- Show the GUI if no input is supplied, otherwise handle the chat input.
|
||||
-- function MyAddon:ChatCommand(input)
|
||||
-- -- Assuming "MyOptions" is the appName of a valid options table
|
||||
-- if not input or input:trim() == "" then
|
||||
-- LibStub("AceConfigDialog-3.0"):Open("MyOptions")
|
||||
-- else
|
||||
-- LibStub("AceConfigCmd-3.0").HandleCommand(MyAddon, "mychat", "MyOptions", input)
|
||||
-- end
|
||||
-- end
|
||||
function AceConfigCmd:HandleCommand(slashcmd, appName, input)
|
||||
|
||||
local optgetter = cfgreg:GetOptionsTable(appName)
|
||||
if not optgetter then
|
||||
error([[Usage: HandleCommand("slashcmd", "appName", "input"): 'appName' - no options table "]]..tostring(appName)..[[" has been registered]], 2)
|
||||
end
|
||||
local options = assert( optgetter("cmd", MAJOR) )
|
||||
|
||||
local info = { -- Don't try to recycle this, it gets handed off to callbacks and whatnot
|
||||
[0] = slashcmd,
|
||||
appName = appName,
|
||||
options = options,
|
||||
input = input,
|
||||
self = self,
|
||||
handler = self,
|
||||
uiType = "cmd",
|
||||
uiName = MAJOR,
|
||||
}
|
||||
|
||||
handle(info, 1, options, 0) -- (info, inputpos, table, depth)
|
||||
end
|
||||
|
||||
--- Utility function to create a slash command handler.
|
||||
-- Also registers tab completion with AceTab
|
||||
-- @param slashcmd The slash command WITHOUT leading slash (only used for error output)
|
||||
-- @param appName The application name as given to `:RegisterOptionsTable()`
|
||||
function AceConfigCmd:CreateChatCommand(slashcmd, appName)
|
||||
if not AceConsole then
|
||||
AceConsole = LibStub(AceConsoleName)
|
||||
end
|
||||
if AceConsole.RegisterChatCommand(self, slashcmd, function(input)
|
||||
AceConfigCmd.HandleCommand(self, slashcmd, appName, input) -- upgradable
|
||||
end,
|
||||
true) then -- succesfully registered so lets get the command -> app table in
|
||||
commands[slashcmd] = appName
|
||||
end
|
||||
end
|
||||
|
||||
--- Utility function that returns the options table that belongs to a slashcommand.
|
||||
-- Designed to be used for the AceTab interface.
|
||||
-- @param slashcmd The slash command WITHOUT leading slash (only used for error output)
|
||||
-- @return The options table associated with the slash command (or nil if the slash command was not registered)
|
||||
function AceConfigCmd:GetChatCommandOptions(slashcmd)
|
||||
return commands[slashcmd]
|
||||
end
|
4
Libs/AceConfig-3.0/AceConfigCmd-3.0/AceConfigCmd-3.0.xml
Normal file
4
Libs/AceConfig-3.0/AceConfigCmd-3.0/AceConfigCmd-3.0.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="AceConfigCmd-3.0.lua"/>
|
||||
</Ui>
|
1983
Libs/AceConfig-3.0/AceConfigDialog-3.0/AceConfigDialog-3.0.lua
Normal file
1983
Libs/AceConfig-3.0/AceConfigDialog-3.0/AceConfigDialog-3.0.lua
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="AceConfigDialog-3.0.lua"/>
|
||||
</Ui>
|
@@ -0,0 +1,350 @@
|
||||
--- AceConfigRegistry-3.0 handles central registration of options tables in use by addons and modules.\\
|
||||
-- Options tables can be registered as raw tables, OR as function refs that return a table.\\
|
||||
-- Such functions receive three arguments: "uiType", "uiName", "appName". \\
|
||||
-- * Valid **uiTypes**: "cmd", "dropdown", "dialog". This is verified by the library at call time. \\
|
||||
-- * The **uiName** field is expected to contain the full name of the calling addon, including version, e.g. "FooBar-1.0". This is verified by the library at call time.\\
|
||||
-- * The **appName** field is the options table name as given at registration time \\
|
||||
--
|
||||
-- :IterateOptionsTables() (and :GetOptionsTable() if only given one argument) return a function reference that the requesting config handling addon must call with valid "uiType", "uiName".
|
||||
-- @class file
|
||||
-- @name AceConfigRegistry-3.0
|
||||
-- @release $Id: AceConfigRegistry-3.0.lua 1169 2018-02-27 16:18:28Z nevcairiel $
|
||||
local CallbackHandler = LibStub("CallbackHandler-1.0")
|
||||
|
||||
local MAJOR, MINOR = "AceConfigRegistry-3.0", 18
|
||||
local AceConfigRegistry = LibStub:NewLibrary(MAJOR, MINOR)
|
||||
|
||||
if not AceConfigRegistry then return end
|
||||
|
||||
AceConfigRegistry.tables = AceConfigRegistry.tables or {}
|
||||
|
||||
if not AceConfigRegistry.callbacks then
|
||||
AceConfigRegistry.callbacks = CallbackHandler:New(AceConfigRegistry)
|
||||
end
|
||||
|
||||
-- Lua APIs
|
||||
local tinsert, tconcat = table.insert, table.concat
|
||||
local strfind, strmatch = string.find, string.match
|
||||
local type, tostring, select, pairs = type, tostring, select, pairs
|
||||
local error, assert = error, assert
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- Validating options table consistency:
|
||||
|
||||
|
||||
AceConfigRegistry.validated = {
|
||||
-- list of options table names ran through :ValidateOptionsTable automatically.
|
||||
-- CLEARED ON PURPOSE, since newer versions may have newer validators
|
||||
cmd = {},
|
||||
dropdown = {},
|
||||
dialog = {},
|
||||
}
|
||||
|
||||
|
||||
|
||||
local function err(msg, errlvl, ...)
|
||||
local t = {}
|
||||
for i=select("#",...),1,-1 do
|
||||
tinsert(t, (select(i, ...)))
|
||||
end
|
||||
error(MAJOR..":ValidateOptionsTable(): "..tconcat(t,".")..msg, errlvl+2)
|
||||
end
|
||||
|
||||
|
||||
local isstring={["string"]=true, _="string"}
|
||||
local isstringfunc={["string"]=true,["function"]=true, _="string or funcref"}
|
||||
local istable={["table"]=true, _="table"}
|
||||
local ismethodtable={["table"]=true,["string"]=true,["function"]=true, _="methodname, funcref or table"}
|
||||
local optstring={["nil"]=true,["string"]=true, _="string"}
|
||||
local optstringfunc={["nil"]=true,["string"]=true,["function"]=true, _="string or funcref"}
|
||||
local optstringnumberfunc={["nil"]=true,["string"]=true,["number"]=true,["function"]=true, _="string, number or funcref"}
|
||||
local optnumber={["nil"]=true,["number"]=true, _="number"}
|
||||
local optmethod={["nil"]=true,["string"]=true,["function"]=true, _="methodname or funcref"}
|
||||
local optmethodfalse={["nil"]=true,["string"]=true,["function"]=true,["boolean"]={[false]=true}, _="methodname, funcref or false"}
|
||||
local optmethodnumber={["nil"]=true,["string"]=true,["function"]=true,["number"]=true, _="methodname, funcref or number"}
|
||||
local optmethodtable={["nil"]=true,["string"]=true,["function"]=true,["table"]=true, _="methodname, funcref or table"}
|
||||
local optmethodbool={["nil"]=true,["string"]=true,["function"]=true,["boolean"]=true, _="methodname, funcref or boolean"}
|
||||
local opttable={["nil"]=true,["table"]=true, _="table"}
|
||||
local optbool={["nil"]=true,["boolean"]=true, _="boolean"}
|
||||
local optboolnumber={["nil"]=true,["boolean"]=true,["number"]=true, _="boolean or number"}
|
||||
local optstringnumber={["nil"]=true,["string"]=true,["number"]=true, _="string or number"}
|
||||
|
||||
local basekeys={
|
||||
type=isstring,
|
||||
name=isstringfunc,
|
||||
desc=optstringfunc,
|
||||
descStyle=optstring,
|
||||
order=optmethodnumber,
|
||||
validate=optmethodfalse,
|
||||
confirm=optmethodbool,
|
||||
confirmText=optstring,
|
||||
disabled=optmethodbool,
|
||||
hidden=optmethodbool,
|
||||
guiHidden=optmethodbool,
|
||||
dialogHidden=optmethodbool,
|
||||
dropdownHidden=optmethodbool,
|
||||
cmdHidden=optmethodbool,
|
||||
icon=optstringnumberfunc,
|
||||
iconCoords=optmethodtable,
|
||||
handler=opttable,
|
||||
get=optmethodfalse,
|
||||
set=optmethodfalse,
|
||||
func=optmethodfalse,
|
||||
arg={["*"]=true},
|
||||
width=optstringnumber,
|
||||
}
|
||||
|
||||
local typedkeys={
|
||||
header={},
|
||||
description={
|
||||
image=optstringnumberfunc,
|
||||
imageCoords=optmethodtable,
|
||||
imageHeight=optnumber,
|
||||
imageWidth=optnumber,
|
||||
fontSize=optstringfunc,
|
||||
},
|
||||
group={
|
||||
args=istable,
|
||||
plugins=opttable,
|
||||
inline=optbool,
|
||||
cmdInline=optbool,
|
||||
guiInline=optbool,
|
||||
dropdownInline=optbool,
|
||||
dialogInline=optbool,
|
||||
childGroups=optstring,
|
||||
},
|
||||
execute={
|
||||
image=optstringnumberfunc,
|
||||
imageCoords=optmethodtable,
|
||||
imageHeight=optnumber,
|
||||
imageWidth=optnumber,
|
||||
},
|
||||
input={
|
||||
pattern=optstring,
|
||||
usage=optstring,
|
||||
control=optstring,
|
||||
dialogControl=optstring,
|
||||
dropdownControl=optstring,
|
||||
multiline=optboolnumber,
|
||||
},
|
||||
toggle={
|
||||
tristate=optbool,
|
||||
image=optstringnumberfunc,
|
||||
imageCoords=optmethodtable,
|
||||
},
|
||||
tristate={
|
||||
},
|
||||
range={
|
||||
min=optnumber,
|
||||
softMin=optnumber,
|
||||
max=optnumber,
|
||||
softMax=optnumber,
|
||||
step=optnumber,
|
||||
bigStep=optnumber,
|
||||
isPercent=optbool,
|
||||
},
|
||||
select={
|
||||
values=ismethodtable,
|
||||
style={
|
||||
["nil"]=true,
|
||||
["string"]={dropdown=true,radio=true},
|
||||
_="string: 'dropdown' or 'radio'"
|
||||
},
|
||||
control=optstring,
|
||||
dialogControl=optstring,
|
||||
dropdownControl=optstring,
|
||||
itemControl=optstring,
|
||||
},
|
||||
multiselect={
|
||||
values=ismethodtable,
|
||||
style=optstring,
|
||||
tristate=optbool,
|
||||
control=optstring,
|
||||
dialogControl=optstring,
|
||||
dropdownControl=optstring,
|
||||
},
|
||||
color={
|
||||
hasAlpha=optmethodbool,
|
||||
},
|
||||
keybinding={
|
||||
-- TODO
|
||||
},
|
||||
}
|
||||
|
||||
local function validateKey(k,errlvl,...)
|
||||
errlvl=(errlvl or 0)+1
|
||||
if type(k)~="string" then
|
||||
err("["..tostring(k).."] - key is not a string", errlvl,...)
|
||||
end
|
||||
if strfind(k, "[%c\127]") then
|
||||
err("["..tostring(k).."] - key name contained control characters", errlvl,...)
|
||||
end
|
||||
end
|
||||
|
||||
local function validateVal(v, oktypes, errlvl,...)
|
||||
errlvl=(errlvl or 0)+1
|
||||
local isok=oktypes[type(v)] or oktypes["*"]
|
||||
|
||||
if not isok then
|
||||
err(": expected a "..oktypes._..", got '"..tostring(v).."'", errlvl,...)
|
||||
end
|
||||
if type(isok)=="table" then -- isok was a table containing specific values to be tested for!
|
||||
if not isok[v] then
|
||||
err(": did not expect "..type(v).." value '"..tostring(v).."'", errlvl,...)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function validate(options,errlvl,...)
|
||||
errlvl=(errlvl or 0)+1
|
||||
-- basic consistency
|
||||
if type(options)~="table" then
|
||||
err(": expected a table, got a "..type(options), errlvl,...)
|
||||
end
|
||||
if type(options.type)~="string" then
|
||||
err(".type: expected a string, got a "..type(options.type), errlvl,...)
|
||||
end
|
||||
|
||||
-- get type and 'typedkeys' member
|
||||
local tk = typedkeys[options.type]
|
||||
if not tk then
|
||||
err(".type: unknown type '"..options.type.."'", errlvl,...)
|
||||
end
|
||||
|
||||
-- make sure that all options[] are known parameters
|
||||
for k,v in pairs(options) do
|
||||
if not (tk[k] or basekeys[k]) then
|
||||
err(": unknown parameter", errlvl,tostring(k),...)
|
||||
end
|
||||
end
|
||||
|
||||
-- verify that required params are there, and that everything is the right type
|
||||
for k,oktypes in pairs(basekeys) do
|
||||
validateVal(options[k], oktypes, errlvl,k,...)
|
||||
end
|
||||
for k,oktypes in pairs(tk) do
|
||||
validateVal(options[k], oktypes, errlvl,k,...)
|
||||
end
|
||||
|
||||
-- extra logic for groups
|
||||
if options.type=="group" then
|
||||
for k,v in pairs(options.args) do
|
||||
validateKey(k,errlvl,"args",...)
|
||||
validate(v, errlvl,k,"args",...)
|
||||
end
|
||||
if options.plugins then
|
||||
for plugname,plugin in pairs(options.plugins) do
|
||||
if type(plugin)~="table" then
|
||||
err(": expected a table, got '"..tostring(plugin).."'", errlvl,tostring(plugname),"plugins",...)
|
||||
end
|
||||
for k,v in pairs(plugin) do
|
||||
validateKey(k,errlvl,tostring(plugname),"plugins",...)
|
||||
validate(v, errlvl,k,tostring(plugname),"plugins",...)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--- Validates basic structure and integrity of an options table \\
|
||||
-- Does NOT verify that get/set etc actually exist, since they can be defined at any depth
|
||||
-- @param options The table to be validated
|
||||
-- @param name The name of the table to be validated (shown in any error message)
|
||||
-- @param errlvl (optional number) error level offset, default 0 (=errors point to the function calling :ValidateOptionsTable)
|
||||
function AceConfigRegistry:ValidateOptionsTable(options,name,errlvl)
|
||||
errlvl=(errlvl or 0)+1
|
||||
name = name or "Optionstable"
|
||||
if not options.name then
|
||||
options.name=name -- bit of a hack, the root level doesn't really need a .name :-/
|
||||
end
|
||||
validate(options,errlvl,name)
|
||||
end
|
||||
|
||||
--- Fires a "ConfigTableChange" callback for those listening in on it, allowing config GUIs to refresh.
|
||||
-- You should call this function if your options table changed from any outside event, like a game event
|
||||
-- or a timer.
|
||||
-- @param appName The application name as given to `:RegisterOptionsTable()`
|
||||
function AceConfigRegistry:NotifyChange(appName)
|
||||
if not AceConfigRegistry.tables[appName] then return end
|
||||
AceConfigRegistry.callbacks:Fire("ConfigTableChange", appName)
|
||||
end
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
-- Registering and retreiving options tables:
|
||||
|
||||
|
||||
-- validateGetterArgs: helper function for :GetOptionsTable (or, rather, the getter functions returned by it)
|
||||
|
||||
local function validateGetterArgs(uiType, uiName, errlvl)
|
||||
errlvl=(errlvl or 0)+2
|
||||
if uiType~="cmd" and uiType~="dropdown" and uiType~="dialog" then
|
||||
error(MAJOR..": Requesting options table: 'uiType' - invalid configuration UI type, expected 'cmd', 'dropdown' or 'dialog'", errlvl)
|
||||
end
|
||||
if not strmatch(uiName, "[A-Za-z]%-[0-9]") then -- Expecting e.g. "MyLib-1.2"
|
||||
error(MAJOR..": Requesting options table: 'uiName' - badly formatted or missing version number. Expected e.g. 'MyLib-1.2'", errlvl)
|
||||
end
|
||||
end
|
||||
|
||||
--- Register an options table with the config registry.
|
||||
-- @param appName The application name as given to `:RegisterOptionsTable()`
|
||||
-- @param options The options table, OR a function reference that generates it on demand. \\
|
||||
-- See the top of the page for info on arguments passed to such functions.
|
||||
-- @param skipValidation Skip options table validation (primarily useful for extremely huge options, with a noticeable slowdown)
|
||||
function AceConfigRegistry:RegisterOptionsTable(appName, options, skipValidation)
|
||||
if type(options)=="table" then
|
||||
if options.type~="group" then -- quick sanity checker
|
||||
error(MAJOR..": RegisterOptionsTable(appName, options): 'options' - missing type='group' member in root group", 2)
|
||||
end
|
||||
AceConfigRegistry.tables[appName] = function(uiType, uiName, errlvl)
|
||||
errlvl=(errlvl or 0)+1
|
||||
validateGetterArgs(uiType, uiName, errlvl)
|
||||
if not AceConfigRegistry.validated[uiType][appName] and not skipValidation then
|
||||
AceConfigRegistry:ValidateOptionsTable(options, appName, errlvl) -- upgradable
|
||||
AceConfigRegistry.validated[uiType][appName] = true
|
||||
end
|
||||
return options
|
||||
end
|
||||
elseif type(options)=="function" then
|
||||
AceConfigRegistry.tables[appName] = function(uiType, uiName, errlvl)
|
||||
errlvl=(errlvl or 0)+1
|
||||
validateGetterArgs(uiType, uiName, errlvl)
|
||||
local tab = assert(options(uiType, uiName, appName))
|
||||
if not AceConfigRegistry.validated[uiType][appName] and not skipValidation then
|
||||
AceConfigRegistry:ValidateOptionsTable(tab, appName, errlvl) -- upgradable
|
||||
AceConfigRegistry.validated[uiType][appName] = true
|
||||
end
|
||||
return tab
|
||||
end
|
||||
else
|
||||
error(MAJOR..": RegisterOptionsTable(appName, options): 'options' - expected table or function reference", 2)
|
||||
end
|
||||
end
|
||||
|
||||
--- Returns an iterator of ["appName"]=funcref pairs
|
||||
function AceConfigRegistry:IterateOptionsTables()
|
||||
return pairs(AceConfigRegistry.tables)
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
--- Query the registry for a specific options table.
|
||||
-- If only appName is given, a function is returned which you
|
||||
-- can call with (uiType,uiName) to get the table.\\
|
||||
-- If uiType&uiName are given, the table is returned.
|
||||
-- @param appName The application name as given to `:RegisterOptionsTable()`
|
||||
-- @param uiType The type of UI to get the table for, one of "cmd", "dropdown", "dialog"
|
||||
-- @param uiName The name of the library/addon querying for the table, e.g. "MyLib-1.0"
|
||||
function AceConfigRegistry:GetOptionsTable(appName, uiType, uiName)
|
||||
local f = AceConfigRegistry.tables[appName]
|
||||
if not f then
|
||||
return nil
|
||||
end
|
||||
|
||||
if uiType then
|
||||
return f(uiType,uiName,1) -- get the table for us
|
||||
else
|
||||
return f -- return the function
|
||||
end
|
||||
end
|
@@ -0,0 +1,4 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="AceConfigRegistry-3.0.lua"/>
|
||||
</Ui>
|
746
Libs/AceDB-3.0/AceDB-3.0.lua
Normal file
746
Libs/AceDB-3.0/AceDB-3.0.lua
Normal file
@@ -0,0 +1,746 @@
|
||||
--- **AceDB-3.0** manages the SavedVariables of your addon.
|
||||
-- It offers profile management, smart defaults and namespaces for modules.\\
|
||||
-- Data can be saved in different data-types, depending on its intended usage.
|
||||
-- The most common data-type is the `profile` type, which allows the user to choose
|
||||
-- the active profile, and manage the profiles of all of his characters.\\
|
||||
-- The following data types are available:
|
||||
-- * **char** Character-specific data. Every character has its own database.
|
||||
-- * **realm** Realm-specific data. All of the players characters on the same realm share this database.
|
||||
-- * **class** Class-specific data. All of the players characters of the same class share this database.
|
||||
-- * **race** Race-specific data. All of the players characters of the same race share this database.
|
||||
-- * **faction** Faction-specific data. All of the players characters of the same faction share this database.
|
||||
-- * **factionrealm** Faction and realm specific data. All of the players characters on the same realm and of the same faction share this database.
|
||||
-- * **locale** Locale specific data, based on the locale of the players game client.
|
||||
-- * **global** Global Data. All characters on the same account share this database.
|
||||
-- * **profile** Profile-specific data. All characters using the same profile share this database. The user can control which profile should be used.
|
||||
--
|
||||
-- Creating a new Database using the `:New` function will return a new DBObject. A database will inherit all functions
|
||||
-- of the DBObjectLib listed here. \\
|
||||
-- If you create a new namespaced child-database (`:RegisterNamespace`), you'll get a DBObject as well, but note
|
||||
-- that the child-databases cannot individually change their profile, and are linked to their parents profile - and because of that,
|
||||
-- the profile related APIs are not available. Only `:RegisterDefaults` and `:ResetProfile` are available on child-databases.
|
||||
--
|
||||
-- For more details on how to use AceDB-3.0, see the [[AceDB-3.0 Tutorial]].
|
||||
--
|
||||
-- You may also be interested in [[libdualspec-1-0|LibDualSpec-1.0]] to do profile switching automatically when switching specs.
|
||||
--
|
||||
-- @usage
|
||||
-- MyAddon = LibStub("AceAddon-3.0"):NewAddon("DBExample")
|
||||
--
|
||||
-- -- declare defaults to be used in the DB
|
||||
-- local defaults = {
|
||||
-- profile = {
|
||||
-- setting = true,
|
||||
-- }
|
||||
-- }
|
||||
--
|
||||
-- function MyAddon:OnInitialize()
|
||||
-- -- Assuming the .toc says ## SavedVariables: MyAddonDB
|
||||
-- self.db = LibStub("AceDB-3.0"):New("MyAddonDB", defaults, true)
|
||||
-- end
|
||||
-- @class file
|
||||
-- @name AceDB-3.0.lua
|
||||
-- @release $Id: AceDB-3.0.lua 1142 2016-07-11 08:36:19Z nevcairiel $
|
||||
local ACEDB_MAJOR, ACEDB_MINOR = "AceDB-3.0", 26
|
||||
local AceDB, oldminor = LibStub:NewLibrary(ACEDB_MAJOR, ACEDB_MINOR)
|
||||
|
||||
if not AceDB then return end -- No upgrade needed
|
||||
|
||||
-- Lua APIs
|
||||
local type, pairs, next, error = type, pairs, next, error
|
||||
local setmetatable, getmetatable, rawset, rawget = setmetatable, getmetatable, rawset, rawget
|
||||
|
||||
-- WoW APIs
|
||||
local _G = _G
|
||||
|
||||
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
|
||||
-- List them here for Mikk's FindGlobals script
|
||||
-- GLOBALS: LibStub
|
||||
|
||||
AceDB.db_registry = AceDB.db_registry or {}
|
||||
AceDB.frame = AceDB.frame or CreateFrame("Frame")
|
||||
|
||||
local CallbackHandler
|
||||
local CallbackDummy = { Fire = function() end }
|
||||
|
||||
local DBObjectLib = {}
|
||||
|
||||
--[[-------------------------------------------------------------------------
|
||||
AceDB Utility Functions
|
||||
---------------------------------------------------------------------------]]
|
||||
|
||||
-- Simple shallow copy for copying defaults
|
||||
local function copyTable(src, dest)
|
||||
if type(dest) ~= "table" then dest = {} end
|
||||
if type(src) == "table" then
|
||||
for k,v in pairs(src) do
|
||||
if type(v) == "table" then
|
||||
-- try to index the key first so that the metatable creates the defaults, if set, and use that table
|
||||
v = copyTable(v, dest[k])
|
||||
end
|
||||
dest[k] = v
|
||||
end
|
||||
end
|
||||
return dest
|
||||
end
|
||||
|
||||
-- Called to add defaults to a section of the database
|
||||
--
|
||||
-- When a ["*"] default section is indexed with a new key, a table is returned
|
||||
-- and set in the host table. These tables must be cleaned up by removeDefaults
|
||||
-- in order to ensure we don't write empty default tables.
|
||||
local function copyDefaults(dest, src)
|
||||
-- this happens if some value in the SV overwrites our default value with a non-table
|
||||
--if type(dest) ~= "table" then return end
|
||||
for k, v in pairs(src) do
|
||||
if k == "*" or k == "**" then
|
||||
if type(v) == "table" then
|
||||
-- This is a metatable used for table defaults
|
||||
local mt = {
|
||||
-- This handles the lookup and creation of new subtables
|
||||
__index = function(t,k)
|
||||
if k == nil then return nil end
|
||||
local tbl = {}
|
||||
copyDefaults(tbl, v)
|
||||
rawset(t, k, tbl)
|
||||
return tbl
|
||||
end,
|
||||
}
|
||||
setmetatable(dest, mt)
|
||||
-- handle already existing tables in the SV
|
||||
for dk, dv in pairs(dest) do
|
||||
if not rawget(src, dk) and type(dv) == "table" then
|
||||
copyDefaults(dv, v)
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Values are not tables, so this is just a simple return
|
||||
local mt = {__index = function(t,k) return k~=nil and v or nil end}
|
||||
setmetatable(dest, mt)
|
||||
end
|
||||
elseif type(v) == "table" then
|
||||
if not rawget(dest, k) then rawset(dest, k, {}) end
|
||||
if type(dest[k]) == "table" then
|
||||
copyDefaults(dest[k], v)
|
||||
if src['**'] then
|
||||
copyDefaults(dest[k], src['**'])
|
||||
end
|
||||
end
|
||||
else
|
||||
if rawget(dest, k) == nil then
|
||||
rawset(dest, k, v)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Called to remove all defaults in the default table from the database
|
||||
local function removeDefaults(db, defaults, blocker)
|
||||
-- remove all metatables from the db, so we don't accidentally create new sub-tables through them
|
||||
setmetatable(db, nil)
|
||||
-- loop through the defaults and remove their content
|
||||
for k,v in pairs(defaults) do
|
||||
if k == "*" or k == "**" then
|
||||
if type(v) == "table" then
|
||||
-- Loop through all the actual k,v pairs and remove
|
||||
for key, value in pairs(db) do
|
||||
if type(value) == "table" then
|
||||
-- if the key was not explicitly specified in the defaults table, just strip everything from * and ** tables
|
||||
if defaults[key] == nil and (not blocker or blocker[key] == nil) then
|
||||
removeDefaults(value, v)
|
||||
-- if the table is empty afterwards, remove it
|
||||
if next(value) == nil then
|
||||
db[key] = nil
|
||||
end
|
||||
-- if it was specified, only strip ** content, but block values which were set in the key table
|
||||
elseif k == "**" then
|
||||
removeDefaults(value, v, defaults[key])
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif k == "*" then
|
||||
-- check for non-table default
|
||||
for key, value in pairs(db) do
|
||||
if defaults[key] == nil and v == value then
|
||||
db[key] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif type(v) == "table" and type(db[k]) == "table" then
|
||||
-- if a blocker was set, dive into it, to allow multi-level defaults
|
||||
removeDefaults(db[k], v, blocker and blocker[k])
|
||||
if next(db[k]) == nil then
|
||||
db[k] = nil
|
||||
end
|
||||
else
|
||||
-- check if the current value matches the default, and that its not blocked by another defaults table
|
||||
if db[k] == defaults[k] and (not blocker or blocker[k] == nil) then
|
||||
db[k] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- This is called when a table section is first accessed, to set up the defaults
|
||||
local function initSection(db, section, svstore, key, defaults)
|
||||
local sv = rawget(db, "sv")
|
||||
|
||||
local tableCreated
|
||||
if not sv[svstore] then sv[svstore] = {} end
|
||||
if not sv[svstore][key] then
|
||||
sv[svstore][key] = {}
|
||||
tableCreated = true
|
||||
end
|
||||
|
||||
local tbl = sv[svstore][key]
|
||||
|
||||
if defaults then
|
||||
copyDefaults(tbl, defaults)
|
||||
end
|
||||
rawset(db, section, tbl)
|
||||
|
||||
return tableCreated, tbl
|
||||
end
|
||||
|
||||
-- Metatable to handle the dynamic creation of sections and copying of sections.
|
||||
local dbmt = {
|
||||
__index = function(t, section)
|
||||
local keys = rawget(t, "keys")
|
||||
local key = keys[section]
|
||||
if key then
|
||||
local defaultTbl = rawget(t, "defaults")
|
||||
local defaults = defaultTbl and defaultTbl[section]
|
||||
|
||||
if section == "profile" then
|
||||
local new = initSection(t, section, "profiles", key, defaults)
|
||||
if new then
|
||||
-- Callback: OnNewProfile, database, newProfileKey
|
||||
t.callbacks:Fire("OnNewProfile", t, key)
|
||||
end
|
||||
elseif section == "profiles" then
|
||||
local sv = rawget(t, "sv")
|
||||
if not sv.profiles then sv.profiles = {} end
|
||||
rawset(t, "profiles", sv.profiles)
|
||||
elseif section == "global" then
|
||||
local sv = rawget(t, "sv")
|
||||
if not sv.global then sv.global = {} end
|
||||
if defaults then
|
||||
copyDefaults(sv.global, defaults)
|
||||
end
|
||||
rawset(t, section, sv.global)
|
||||
else
|
||||
initSection(t, section, section, key, defaults)
|
||||
end
|
||||
end
|
||||
|
||||
return rawget(t, section)
|
||||
end
|
||||
}
|
||||
|
||||
local function validateDefaults(defaults, keyTbl, offset)
|
||||
if not defaults then return end
|
||||
offset = offset or 0
|
||||
for k in pairs(defaults) do
|
||||
if not keyTbl[k] or k == "profiles" then
|
||||
error(("Usage: AceDBObject:RegisterDefaults(defaults): '%s' is not a valid datatype."):format(k), 3 + offset)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local preserve_keys = {
|
||||
["callbacks"] = true,
|
||||
["RegisterCallback"] = true,
|
||||
["UnregisterCallback"] = true,
|
||||
["UnregisterAllCallbacks"] = true,
|
||||
["children"] = true,
|
||||
}
|
||||
|
||||
local realmKey = GetRealmName()
|
||||
local charKey = UnitName("player") .. " - " .. realmKey
|
||||
local _, classKey = UnitClass("player")
|
||||
local _, raceKey = UnitRace("player")
|
||||
local factionKey = UnitFactionGroup("player")
|
||||
local factionrealmKey = factionKey .. " - " .. realmKey
|
||||
local localeKey = GetLocale():lower()
|
||||
|
||||
local regionTable = { "US", "KR", "EU", "TW", "CN" }
|
||||
local regionKey = regionTable[GetCurrentRegion()]
|
||||
local factionrealmregionKey = factionrealmKey .. " - " .. regionKey
|
||||
|
||||
-- Actual database initialization function
|
||||
local function initdb(sv, defaults, defaultProfile, olddb, parent)
|
||||
-- Generate the database keys for each section
|
||||
|
||||
-- map "true" to our "Default" profile
|
||||
if defaultProfile == true then defaultProfile = "Default" end
|
||||
|
||||
local profileKey
|
||||
if not parent then
|
||||
-- Make a container for profile keys
|
||||
if not sv.profileKeys then sv.profileKeys = {} end
|
||||
|
||||
-- Try to get the profile selected from the char db
|
||||
profileKey = sv.profileKeys[charKey] or defaultProfile or charKey
|
||||
|
||||
-- save the selected profile for later
|
||||
sv.profileKeys[charKey] = profileKey
|
||||
else
|
||||
-- Use the profile of the parents DB
|
||||
profileKey = parent.keys.profile or defaultProfile or charKey
|
||||
|
||||
-- clear the profileKeys in the DB, namespaces don't need to store them
|
||||
sv.profileKeys = nil
|
||||
end
|
||||
|
||||
-- This table contains keys that enable the dynamic creation
|
||||
-- of each section of the table. The 'global' and 'profiles'
|
||||
-- have a key of true, since they are handled in a special case
|
||||
local keyTbl= {
|
||||
["char"] = charKey,
|
||||
["realm"] = realmKey,
|
||||
["class"] = classKey,
|
||||
["race"] = raceKey,
|
||||
["faction"] = factionKey,
|
||||
["factionrealm"] = factionrealmKey,
|
||||
["factionrealmregion"] = factionrealmregionKey,
|
||||
["profile"] = profileKey,
|
||||
["locale"] = localeKey,
|
||||
["global"] = true,
|
||||
["profiles"] = true,
|
||||
}
|
||||
|
||||
validateDefaults(defaults, keyTbl, 1)
|
||||
|
||||
-- This allows us to use this function to reset an entire database
|
||||
-- Clear out the old database
|
||||
if olddb then
|
||||
for k,v in pairs(olddb) do if not preserve_keys[k] then olddb[k] = nil end end
|
||||
end
|
||||
|
||||
-- Give this database the metatable so it initializes dynamically
|
||||
local db = setmetatable(olddb or {}, dbmt)
|
||||
|
||||
if not rawget(db, "callbacks") then
|
||||
-- try to load CallbackHandler-1.0 if it loaded after our library
|
||||
if not CallbackHandler then CallbackHandler = LibStub:GetLibrary("CallbackHandler-1.0", true) end
|
||||
db.callbacks = CallbackHandler and CallbackHandler:New(db) or CallbackDummy
|
||||
end
|
||||
|
||||
-- Copy methods locally into the database object, to avoid hitting
|
||||
-- the metatable when calling methods
|
||||
|
||||
if not parent then
|
||||
for name, func in pairs(DBObjectLib) do
|
||||
db[name] = func
|
||||
end
|
||||
else
|
||||
-- hack this one in
|
||||
db.RegisterDefaults = DBObjectLib.RegisterDefaults
|
||||
db.ResetProfile = DBObjectLib.ResetProfile
|
||||
end
|
||||
|
||||
-- Set some properties in the database object
|
||||
db.profiles = sv.profiles
|
||||
db.keys = keyTbl
|
||||
db.sv = sv
|
||||
--db.sv_name = name
|
||||
db.defaults = defaults
|
||||
db.parent = parent
|
||||
|
||||
-- store the DB in the registry
|
||||
AceDB.db_registry[db] = true
|
||||
|
||||
return db
|
||||
end
|
||||
|
||||
-- handle PLAYER_LOGOUT
|
||||
-- strip all defaults from all databases
|
||||
-- and cleans up empty sections
|
||||
local function logoutHandler(frame, event)
|
||||
if event == "PLAYER_LOGOUT" then
|
||||
for db in pairs(AceDB.db_registry) do
|
||||
db.callbacks:Fire("OnDatabaseShutdown", db)
|
||||
db:RegisterDefaults(nil)
|
||||
|
||||
-- cleanup sections that are empty without defaults
|
||||
local sv = rawget(db, "sv")
|
||||
for section in pairs(db.keys) do
|
||||
if rawget(sv, section) then
|
||||
-- global is special, all other sections have sub-entrys
|
||||
-- also don't delete empty profiles on main dbs, only on namespaces
|
||||
if section ~= "global" and (section ~= "profiles" or rawget(db, "parent")) then
|
||||
for key in pairs(sv[section]) do
|
||||
if not next(sv[section][key]) then
|
||||
sv[section][key] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
if not next(sv[section]) then
|
||||
sv[section] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
AceDB.frame:RegisterEvent("PLAYER_LOGOUT")
|
||||
AceDB.frame:SetScript("OnEvent", logoutHandler)
|
||||
|
||||
|
||||
--[[-------------------------------------------------------------------------
|
||||
AceDB Object Method Definitions
|
||||
---------------------------------------------------------------------------]]
|
||||
|
||||
--- Sets the defaults table for the given database object by clearing any
|
||||
-- that are currently set, and then setting the new defaults.
|
||||
-- @param defaults A table of defaults for this database
|
||||
function DBObjectLib:RegisterDefaults(defaults)
|
||||
if defaults and type(defaults) ~= "table" then
|
||||
error("Usage: AceDBObject:RegisterDefaults(defaults): 'defaults' - table or nil expected.", 2)
|
||||
end
|
||||
|
||||
validateDefaults(defaults, self.keys)
|
||||
|
||||
-- Remove any currently set defaults
|
||||
if self.defaults then
|
||||
for section,key in pairs(self.keys) do
|
||||
if self.defaults[section] and rawget(self, section) then
|
||||
removeDefaults(self[section], self.defaults[section])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Set the DBObject.defaults table
|
||||
self.defaults = defaults
|
||||
|
||||
-- Copy in any defaults, only touching those sections already created
|
||||
if defaults then
|
||||
for section,key in pairs(self.keys) do
|
||||
if defaults[section] and rawget(self, section) then
|
||||
copyDefaults(self[section], defaults[section])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Changes the profile of the database and all of it's namespaces to the
|
||||
-- supplied named profile
|
||||
-- @param name The name of the profile to set as the current profile
|
||||
function DBObjectLib:SetProfile(name)
|
||||
if type(name) ~= "string" then
|
||||
error("Usage: AceDBObject:SetProfile(name): 'name' - string expected.", 2)
|
||||
end
|
||||
|
||||
-- changing to the same profile, dont do anything
|
||||
if name == self.keys.profile then return end
|
||||
|
||||
local oldProfile = self.profile
|
||||
local defaults = self.defaults and self.defaults.profile
|
||||
|
||||
-- Callback: OnProfileShutdown, database
|
||||
self.callbacks:Fire("OnProfileShutdown", self)
|
||||
|
||||
if oldProfile and defaults then
|
||||
-- Remove the defaults from the old profile
|
||||
removeDefaults(oldProfile, defaults)
|
||||
end
|
||||
|
||||
self.profile = nil
|
||||
self.keys["profile"] = name
|
||||
|
||||
-- if the storage exists, save the new profile
|
||||
-- this won't exist on namespaces.
|
||||
if self.sv.profileKeys then
|
||||
self.sv.profileKeys[charKey] = name
|
||||
end
|
||||
|
||||
-- populate to child namespaces
|
||||
if self.children then
|
||||
for _, db in pairs(self.children) do
|
||||
DBObjectLib.SetProfile(db, name)
|
||||
end
|
||||
end
|
||||
|
||||
-- Callback: OnProfileChanged, database, newProfileKey
|
||||
self.callbacks:Fire("OnProfileChanged", self, name)
|
||||
end
|
||||
|
||||
--- Returns a table with the names of the existing profiles in the database.
|
||||
-- You can optionally supply a table to re-use for this purpose.
|
||||
-- @param tbl A table to store the profile names in (optional)
|
||||
function DBObjectLib:GetProfiles(tbl)
|
||||
if tbl and type(tbl) ~= "table" then
|
||||
error("Usage: AceDBObject:GetProfiles(tbl): 'tbl' - table or nil expected.", 2)
|
||||
end
|
||||
|
||||
-- Clear the container table
|
||||
if tbl then
|
||||
for k,v in pairs(tbl) do tbl[k] = nil end
|
||||
else
|
||||
tbl = {}
|
||||
end
|
||||
|
||||
local curProfile = self.keys.profile
|
||||
|
||||
local i = 0
|
||||
for profileKey in pairs(self.profiles) do
|
||||
i = i + 1
|
||||
tbl[i] = profileKey
|
||||
if curProfile and profileKey == curProfile then curProfile = nil end
|
||||
end
|
||||
|
||||
-- Add the current profile, if it hasn't been created yet
|
||||
if curProfile then
|
||||
i = i + 1
|
||||
tbl[i] = curProfile
|
||||
end
|
||||
|
||||
return tbl, i
|
||||
end
|
||||
|
||||
--- Returns the current profile name used by the database
|
||||
function DBObjectLib:GetCurrentProfile()
|
||||
return self.keys.profile
|
||||
end
|
||||
|
||||
--- Deletes a named profile. This profile must not be the active profile.
|
||||
-- @param name The name of the profile to be deleted
|
||||
-- @param silent If true, do not raise an error when the profile does not exist
|
||||
function DBObjectLib:DeleteProfile(name, silent)
|
||||
if type(name) ~= "string" then
|
||||
error("Usage: AceDBObject:DeleteProfile(name): 'name' - string expected.", 2)
|
||||
end
|
||||
|
||||
if self.keys.profile == name then
|
||||
error("Cannot delete the active profile in an AceDBObject.", 2)
|
||||
end
|
||||
|
||||
if not rawget(self.profiles, name) and not silent then
|
||||
error("Cannot delete profile '" .. name .. "'. It does not exist.", 2)
|
||||
end
|
||||
|
||||
self.profiles[name] = nil
|
||||
|
||||
-- populate to child namespaces
|
||||
if self.children then
|
||||
for _, db in pairs(self.children) do
|
||||
DBObjectLib.DeleteProfile(db, name, true)
|
||||
end
|
||||
end
|
||||
|
||||
-- switch all characters that use this profile back to the default
|
||||
if self.sv.profileKeys then
|
||||
for key, profile in pairs(self.sv.profileKeys) do
|
||||
if profile == name then
|
||||
self.sv.profileKeys[key] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Callback: OnProfileDeleted, database, profileKey
|
||||
self.callbacks:Fire("OnProfileDeleted", self, name)
|
||||
end
|
||||
|
||||
--- Copies a named profile into the current profile, overwriting any conflicting
|
||||
-- settings.
|
||||
-- @param name The name of the profile to be copied into the current profile
|
||||
-- @param silent If true, do not raise an error when the profile does not exist
|
||||
function DBObjectLib:CopyProfile(name, silent)
|
||||
if type(name) ~= "string" then
|
||||
error("Usage: AceDBObject:CopyProfile(name): 'name' - string expected.", 2)
|
||||
end
|
||||
|
||||
if name == self.keys.profile then
|
||||
error("Cannot have the same source and destination profiles.", 2)
|
||||
end
|
||||
|
||||
if not rawget(self.profiles, name) and not silent then
|
||||
error("Cannot copy profile '" .. name .. "'. It does not exist.", 2)
|
||||
end
|
||||
|
||||
-- Reset the profile before copying
|
||||
DBObjectLib.ResetProfile(self, nil, true)
|
||||
|
||||
local profile = self.profile
|
||||
local source = self.profiles[name]
|
||||
|
||||
copyTable(source, profile)
|
||||
|
||||
-- populate to child namespaces
|
||||
if self.children then
|
||||
for _, db in pairs(self.children) do
|
||||
DBObjectLib.CopyProfile(db, name, true)
|
||||
end
|
||||
end
|
||||
|
||||
-- Callback: OnProfileCopied, database, sourceProfileKey
|
||||
self.callbacks:Fire("OnProfileCopied", self, name)
|
||||
end
|
||||
|
||||
--- Resets the current profile to the default values (if specified).
|
||||
-- @param noChildren if set to true, the reset will not be populated to the child namespaces of this DB object
|
||||
-- @param noCallbacks if set to true, won't fire the OnProfileReset callback
|
||||
function DBObjectLib:ResetProfile(noChildren, noCallbacks)
|
||||
local profile = self.profile
|
||||
|
||||
for k,v in pairs(profile) do
|
||||
profile[k] = nil
|
||||
end
|
||||
|
||||
local defaults = self.defaults and self.defaults.profile
|
||||
if defaults then
|
||||
copyDefaults(profile, defaults)
|
||||
end
|
||||
|
||||
-- populate to child namespaces
|
||||
if self.children and not noChildren then
|
||||
for _, db in pairs(self.children) do
|
||||
DBObjectLib.ResetProfile(db, nil, noCallbacks)
|
||||
end
|
||||
end
|
||||
|
||||
-- Callback: OnProfileReset, database
|
||||
if not noCallbacks then
|
||||
self.callbacks:Fire("OnProfileReset", self)
|
||||
end
|
||||
end
|
||||
|
||||
--- Resets the entire database, using the string defaultProfile as the new default
|
||||
-- profile.
|
||||
-- @param defaultProfile The profile name to use as the default
|
||||
function DBObjectLib:ResetDB(defaultProfile)
|
||||
if defaultProfile and type(defaultProfile) ~= "string" then
|
||||
error("Usage: AceDBObject:ResetDB(defaultProfile): 'defaultProfile' - string or nil expected.", 2)
|
||||
end
|
||||
|
||||
local sv = self.sv
|
||||
for k,v in pairs(sv) do
|
||||
sv[k] = nil
|
||||
end
|
||||
|
||||
local parent = self.parent
|
||||
|
||||
initdb(sv, self.defaults, defaultProfile, self)
|
||||
|
||||
-- fix the child namespaces
|
||||
if self.children then
|
||||
if not sv.namespaces then sv.namespaces = {} end
|
||||
for name, db in pairs(self.children) do
|
||||
if not sv.namespaces[name] then sv.namespaces[name] = {} end
|
||||
initdb(sv.namespaces[name], db.defaults, self.keys.profile, db, self)
|
||||
end
|
||||
end
|
||||
|
||||
-- Callback: OnDatabaseReset, database
|
||||
self.callbacks:Fire("OnDatabaseReset", self)
|
||||
-- Callback: OnProfileChanged, database, profileKey
|
||||
self.callbacks:Fire("OnProfileChanged", self, self.keys["profile"])
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Creates a new database namespace, directly tied to the database. This
|
||||
-- is a full scale database in it's own rights other than the fact that
|
||||
-- it cannot control its profile individually
|
||||
-- @param name The name of the new namespace
|
||||
-- @param defaults A table of values to use as defaults
|
||||
function DBObjectLib:RegisterNamespace(name, defaults)
|
||||
if type(name) ~= "string" then
|
||||
error("Usage: AceDBObject:RegisterNamespace(name, defaults): 'name' - string expected.", 2)
|
||||
end
|
||||
if defaults and type(defaults) ~= "table" then
|
||||
error("Usage: AceDBObject:RegisterNamespace(name, defaults): 'defaults' - table or nil expected.", 2)
|
||||
end
|
||||
if self.children and self.children[name] then
|
||||
error ("Usage: AceDBObject:RegisterNamespace(name, defaults): 'name' - a namespace with that name already exists.", 2)
|
||||
end
|
||||
|
||||
local sv = self.sv
|
||||
if not sv.namespaces then sv.namespaces = {} end
|
||||
if not sv.namespaces[name] then
|
||||
sv.namespaces[name] = {}
|
||||
end
|
||||
|
||||
local newDB = initdb(sv.namespaces[name], defaults, self.keys.profile, nil, self)
|
||||
|
||||
if not self.children then self.children = {} end
|
||||
self.children[name] = newDB
|
||||
return newDB
|
||||
end
|
||||
|
||||
--- Returns an already existing namespace from the database object.
|
||||
-- @param name The name of the new namespace
|
||||
-- @param silent if true, the addon is optional, silently return nil if its not found
|
||||
-- @usage
|
||||
-- local namespace = self.db:GetNamespace('namespace')
|
||||
-- @return the namespace object if found
|
||||
function DBObjectLib:GetNamespace(name, silent)
|
||||
if type(name) ~= "string" then
|
||||
error("Usage: AceDBObject:GetNamespace(name): 'name' - string expected.", 2)
|
||||
end
|
||||
if not silent and not (self.children and self.children[name]) then
|
||||
error ("Usage: AceDBObject:GetNamespace(name): 'name' - namespace does not exist.", 2)
|
||||
end
|
||||
if not self.children then self.children = {} end
|
||||
return self.children[name]
|
||||
end
|
||||
|
||||
--[[-------------------------------------------------------------------------
|
||||
AceDB Exposed Methods
|
||||
---------------------------------------------------------------------------]]
|
||||
|
||||
--- Creates a new database object that can be used to handle database settings and profiles.
|
||||
-- By default, an empty DB is created, using a character specific profile.
|
||||
--
|
||||
-- You can override the default profile used by passing any profile name as the third argument,
|
||||
-- or by passing //true// as the third argument to use a globally shared profile called "Default".
|
||||
--
|
||||
-- Note that there is no token replacement in the default profile name, passing a defaultProfile as "char"
|
||||
-- will use a profile named "char", and not a character-specific profile.
|
||||
-- @param tbl The name of variable, or table to use for the database
|
||||
-- @param defaults A table of database defaults
|
||||
-- @param defaultProfile The name of the default profile. If not set, a character specific profile will be used as the default.
|
||||
-- You can also pass //true// to use a shared global profile called "Default".
|
||||
-- @usage
|
||||
-- -- Create an empty DB using a character-specific default profile.
|
||||
-- self.db = LibStub("AceDB-3.0"):New("MyAddonDB")
|
||||
-- @usage
|
||||
-- -- Create a DB using defaults and using a shared default profile
|
||||
-- self.db = LibStub("AceDB-3.0"):New("MyAddonDB", defaults, true)
|
||||
function AceDB:New(tbl, defaults, defaultProfile)
|
||||
if type(tbl) == "string" then
|
||||
local name = tbl
|
||||
tbl = _G[name]
|
||||
if not tbl then
|
||||
tbl = {}
|
||||
_G[name] = tbl
|
||||
end
|
||||
end
|
||||
|
||||
if type(tbl) ~= "table" then
|
||||
error("Usage: AceDB:New(tbl, defaults, defaultProfile): 'tbl' - table expected.", 2)
|
||||
end
|
||||
|
||||
if defaults and type(defaults) ~= "table" then
|
||||
error("Usage: AceDB:New(tbl, defaults, defaultProfile): 'defaults' - table expected.", 2)
|
||||
end
|
||||
|
||||
if defaultProfile and type(defaultProfile) ~= "string" and defaultProfile ~= true then
|
||||
error("Usage: AceDB:New(tbl, defaults, defaultProfile): 'defaultProfile' - string or true expected.", 2)
|
||||
end
|
||||
|
||||
return initdb(tbl, defaults, defaultProfile)
|
||||
end
|
||||
|
||||
-- upgrade existing databases
|
||||
for db in pairs(AceDB.db_registry) do
|
||||
if not db.parent then
|
||||
for name,func in pairs(DBObjectLib) do
|
||||
db[name] = func
|
||||
end
|
||||
else
|
||||
db.RegisterDefaults = DBObjectLib.RegisterDefaults
|
||||
db.ResetProfile = DBObjectLib.ResetProfile
|
||||
end
|
||||
end
|
4
Libs/AceDB-3.0/AceDB-3.0.xml
Normal file
4
Libs/AceDB-3.0/AceDB-3.0.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="AceDB-3.0.lua"/>
|
||||
</Ui>
|
460
Libs/AceDBOptions-3.0/AceDBOptions-3.0.lua
Normal file
460
Libs/AceDBOptions-3.0/AceDBOptions-3.0.lua
Normal file
@@ -0,0 +1,460 @@
|
||||
--- AceDBOptions-3.0 provides a universal AceConfig options screen for managing AceDB-3.0 profiles.
|
||||
-- @class file
|
||||
-- @name AceDBOptions-3.0
|
||||
-- @release $Id: AceDBOptions-3.0.lua 1140 2016-07-03 07:53:29Z nevcairiel $
|
||||
local ACEDBO_MAJOR, ACEDBO_MINOR = "AceDBOptions-3.0", 15
|
||||
local AceDBOptions, oldminor = LibStub:NewLibrary(ACEDBO_MAJOR, ACEDBO_MINOR)
|
||||
|
||||
if not AceDBOptions then return end -- No upgrade needed
|
||||
|
||||
-- Lua APIs
|
||||
local pairs, next = pairs, next
|
||||
|
||||
-- WoW APIs
|
||||
local UnitClass = UnitClass
|
||||
|
||||
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
|
||||
-- List them here for Mikk's FindGlobals script
|
||||
-- GLOBALS: NORMAL_FONT_COLOR_CODE, FONT_COLOR_CODE_CLOSE
|
||||
|
||||
AceDBOptions.optionTables = AceDBOptions.optionTables or {}
|
||||
AceDBOptions.handlers = AceDBOptions.handlers or {}
|
||||
|
||||
--[[
|
||||
Localization of AceDBOptions-3.0
|
||||
]]
|
||||
|
||||
local L = {
|
||||
choose = "Existing Profiles",
|
||||
choose_desc = "You can either create a new profile by entering a name in the editbox, or choose one of the already existing profiles.",
|
||||
choose_sub = "Select one of your currently available profiles.",
|
||||
copy = "Copy From",
|
||||
copy_desc = "Copy the settings from one existing profile into the currently active profile.",
|
||||
current = "Current Profile:",
|
||||
default = "Default",
|
||||
delete = "Delete a Profile",
|
||||
delete_confirm = "Are you sure you want to delete the selected profile?",
|
||||
delete_desc = "Delete existing and unused profiles from the database to save space, and cleanup the SavedVariables file.",
|
||||
delete_sub = "Deletes a profile from the database.",
|
||||
intro = "You can change the active database profile, so you can have different settings for every character.",
|
||||
new = "New",
|
||||
new_sub = "Create a new empty profile.",
|
||||
profiles = "Profiles",
|
||||
profiles_sub = "Manage Profiles",
|
||||
reset = "Reset Profile",
|
||||
reset_desc = "Reset the current profile back to its default values, in case your configuration is broken, or you simply want to start over.",
|
||||
reset_sub = "Reset the current profile to the default",
|
||||
}
|
||||
|
||||
local LOCALE = GetLocale()
|
||||
if LOCALE == "deDE" then
|
||||
L["choose"] = "Vorhandene Profile"
|
||||
L["choose_desc"] = "Du kannst ein neues Profil erstellen, indem du einen neuen Namen in der Eingabebox 'Neu' eingibst, oder wähle eines der vorhandenen Profile aus."
|
||||
L["choose_sub"] = "Wählt ein bereits vorhandenes Profil aus."
|
||||
L["copy"] = "Kopieren von..."
|
||||
L["copy_desc"] = "Kopiere die Einstellungen von einem vorhandenen Profil in das aktive Profil."
|
||||
L["current"] = "Aktuelles Profil:"
|
||||
L["default"] = "Standard"
|
||||
L["delete"] = "Profil löschen"
|
||||
L["delete_confirm"] = "Willst du das ausgewählte Profil wirklich löschen?"
|
||||
L["delete_desc"] = "Lösche vorhandene oder unbenutzte Profile aus der Datenbank, um Platz zu sparen und die SavedVariables-Datei 'sauber' zu halten."
|
||||
L["delete_sub"] = "Löscht ein Profil aus der Datenbank."
|
||||
L["intro"] = "Hier kannst du das aktive Datenbankprofil ändern, damit du verschiedene Einstellungen für jeden Charakter erstellen kannst, wodurch eine sehr flexible Konfiguration möglich wird."
|
||||
L["new"] = "Neu"
|
||||
L["new_sub"] = "Ein neues Profil erstellen."
|
||||
L["profiles"] = "Profile"
|
||||
L["profiles_sub"] = "Profile verwalten"
|
||||
L["reset"] = "Profil zurücksetzen"
|
||||
L["reset_desc"] = "Setzt das momentane Profil auf Standardwerte zurück, für den Fall, dass mit der Konfiguration etwas schief lief oder weil du einfach neu starten willst."
|
||||
L["reset_sub"] = "Das aktuelle Profil auf Standard zurücksetzen."
|
||||
elseif LOCALE == "frFR" then
|
||||
L["choose"] = "Profils existants"
|
||||
L["choose_desc"] = "Vous pouvez créer un nouveau profil en entrant un nouveau nom dans la boîte de saisie, ou en choississant un des profils déjà existants."
|
||||
L["choose_sub"] = "Permet de choisir un des profils déjà disponibles."
|
||||
L["copy"] = "Copier à partir de"
|
||||
L["copy_desc"] = "Copie les paramètres d'un profil déjà existant dans le profil actuellement actif."
|
||||
L["current"] = "Profil actuel :"
|
||||
L["default"] = "Défaut"
|
||||
L["delete"] = "Supprimer un profil"
|
||||
L["delete_confirm"] = "Etes-vous sûr de vouloir supprimer le profil sélectionné ?"
|
||||
L["delete_desc"] = "Supprime les profils existants inutilisés de la base de données afin de gagner de la place et de nettoyer le fichier SavedVariables."
|
||||
L["delete_sub"] = "Supprime un profil de la base de données."
|
||||
L["intro"] = "Vous pouvez changer le profil actuel afin d'avoir des paramètres différents pour chaque personnage, permettant ainsi d'avoir une configuration très flexible."
|
||||
L["new"] = "Nouveau"
|
||||
L["new_sub"] = "Créée un nouveau profil vierge."
|
||||
L["profiles"] = "Profils"
|
||||
L["profiles_sub"] = "Gestion des profils"
|
||||
L["reset"] = "Réinitialiser le profil"
|
||||
L["reset_desc"] = "Réinitialise le profil actuel au cas où votre configuration est corrompue ou si vous voulez tout simplement faire table rase."
|
||||
L["reset_sub"] = "Réinitialise le profil actuel avec les paramètres par défaut."
|
||||
elseif LOCALE == "koKR" then
|
||||
L["choose"] = "저장 중인 프로필"
|
||||
L["choose_desc"] = "입력창에 새로운 이름을 입력하거나 저장 중인 프로필 중 하나를 선택하여 새로운 프로필을 만들 수 있습니다."
|
||||
L["choose_sub"] = "현재 이용할 수 있는 프로필 중 하나를 선택합니다."
|
||||
L["copy"] = "복사해오기"
|
||||
L["copy_desc"] = "현재 사용 중인 프로필에 선택한 프로필의 설정을 복사합니다."
|
||||
L["current"] = "현재 프로필:"
|
||||
L["default"] = "기본값"
|
||||
L["delete"] = "프로필 삭제"
|
||||
L["delete_confirm"] = "정말로 선택한 프로필을 삭제할까요?"
|
||||
L["delete_desc"] = "저장 공간 절약과 SavedVariables 파일의 정리를 위해 데이터베이스에서 사용하지 않는 프로필을 삭제하세요."
|
||||
L["delete_sub"] = "데이터베이스의 프로필을 삭제합니다."
|
||||
L["intro"] = "활성 데이터베이스 프로필을 변경할 수 있고, 각 캐릭터 별로 다른 설정을 할 수 있습니다."
|
||||
L["new"] = "새로운 프로필"
|
||||
L["new_sub"] = "새로운 프로필을 만듭니다."
|
||||
L["profiles"] = "프로필"
|
||||
L["profiles_sub"] = "프로필 관리"
|
||||
L["reset"] = "프로필 초기화"
|
||||
L["reset_desc"] = "설정이 깨졌거나 처음부터 다시 설정을 원하는 경우, 현재 프로필을 기본값으로 초기화하세요."
|
||||
L["reset_sub"] = "현재 프로필을 기본값으로 초기화합니다"
|
||||
elseif LOCALE == "esES" or LOCALE == "esMX" then
|
||||
L["choose"] = "Perfiles existentes"
|
||||
L["choose_desc"] = "Puedes crear un nuevo perfil introduciendo un nombre en el recuadro o puedes seleccionar un perfil de los ya existentes."
|
||||
L["choose_sub"] = "Selecciona uno de los perfiles disponibles."
|
||||
L["copy"] = "Copiar de"
|
||||
L["copy_desc"] = "Copia los ajustes de un perfil existente al perfil actual."
|
||||
L["current"] = "Perfil actual:"
|
||||
L["default"] = "Por defecto"
|
||||
L["delete"] = "Borrar un Perfil"
|
||||
L["delete_confirm"] = "¿Estas seguro que quieres borrar el perfil seleccionado?"
|
||||
L["delete_desc"] = "Borra los perfiles existentes y sin uso de la base de datos para ganar espacio y limpiar el archivo SavedVariables."
|
||||
L["delete_sub"] = "Borra un perfil de la base de datos."
|
||||
L["intro"] = "Puedes cambiar el perfil activo de tal manera que cada personaje tenga diferentes configuraciones."
|
||||
L["new"] = "Nuevo"
|
||||
L["new_sub"] = "Crear un nuevo perfil vacio."
|
||||
L["profiles"] = "Perfiles"
|
||||
L["profiles_sub"] = "Manejar Perfiles"
|
||||
L["reset"] = "Reiniciar Perfil"
|
||||
L["reset_desc"] = "Reinicia el perfil actual a los valores por defectos, en caso de que se haya estropeado la configuración o quieras volver a empezar de nuevo."
|
||||
L["reset_sub"] = "Reinicar el perfil actual al de por defecto"
|
||||
elseif LOCALE == "zhTW" then
|
||||
L["choose"] = "現有的設定檔"
|
||||
L["choose_desc"] = "您可以在文字方塊內輸入名字以建立新的設定檔,或是選擇一個現有的設定檔使用。"
|
||||
L["choose_sub"] = "從當前可用的設定檔裡面選擇一個。"
|
||||
L["copy"] = "複製自"
|
||||
L["copy_desc"] = "從一個現有的設定檔,將設定複製到現在使用中的設定檔。"
|
||||
L["current"] = "目前設定檔:"
|
||||
L["default"] = "預設"
|
||||
L["delete"] = "刪除一個設定檔"
|
||||
L["delete_confirm"] = "確定要刪除所選擇的設定檔嗎?"
|
||||
L["delete_desc"] = "從資料庫裡刪除不再使用的設定檔,以節省空間,並且清理 SavedVariables 檔案。"
|
||||
L["delete_sub"] = "從資料庫裡刪除一個設定檔。"
|
||||
L["intro"] = "您可以從資料庫中選擇一個設定檔來使用,如此就可以讓每個角色使用不同的設定。"
|
||||
L["new"] = "新建"
|
||||
L["new_sub"] = "新建一個空的設定檔。"
|
||||
L["profiles"] = "設定檔"
|
||||
L["profiles_sub"] = "管理設定檔"
|
||||
L["reset"] = "重置設定檔"
|
||||
L["reset_desc"] = "將現用的設定檔重置為預設值;用於設定檔損壞,或者單純想要重來的情況。"
|
||||
L["reset_sub"] = "將目前的設定檔重置為預設值"
|
||||
elseif LOCALE == "zhCN" then
|
||||
L["choose"] = "现有的配置文件"
|
||||
L["choose_desc"] = "你可以通过在文本框内输入一个名字创立一个新的配置文件,也可以选择一个已经存在的配置文件。"
|
||||
L["choose_sub"] = "从当前可用的配置文件里面选择一个。"
|
||||
L["copy"] = "复制自"
|
||||
L["copy_desc"] = "从当前某个已保存的配置文件复制到当前正使用的配置文件。"
|
||||
L["current"] = "当前配置文件:"
|
||||
L["default"] = "默认"
|
||||
L["delete"] = "删除一个配置文件"
|
||||
L["delete_confirm"] = "你确定要删除所选择的配置文件么?"
|
||||
L["delete_desc"] = "从数据库里删除不再使用的配置文件,以节省空间,并且清理SavedVariables文件。"
|
||||
L["delete_sub"] = "从数据库里删除一个配置文件。"
|
||||
L["intro"] = "你可以选择一个活动的数据配置文件,这样你的每个角色就可以拥有不同的设置值,可以给你的插件配置带来极大的灵活性。"
|
||||
L["new"] = "新建"
|
||||
L["new_sub"] = "新建一个空的配置文件。"
|
||||
L["profiles"] = "配置文件"
|
||||
L["profiles_sub"] = "管理配置文件"
|
||||
L["reset"] = "重置配置文件"
|
||||
L["reset_desc"] = "将当前的配置文件恢复到它的默认值,用于你的配置文件损坏,或者你只是想重来的情况。"
|
||||
L["reset_sub"] = "将当前的配置文件恢复为默认值"
|
||||
elseif LOCALE == "ruRU" then
|
||||
L["choose"] = "Существующие профили"
|
||||
L["choose_desc"] = "Вы можете создать новый профиль, введя название в поле ввода, или выбрать один из уже существующих профилей."
|
||||
L["choose_sub"] = "Выбор одиного из уже доступных профилей"
|
||||
L["copy"] = "Скопировать из"
|
||||
L["copy_desc"] = "Скопировать настройки из выбранного профиля в активный."
|
||||
L["current"] = "Текущий профиль:"
|
||||
L["default"] = "По умолчанию"
|
||||
L["delete"] = "Удалить профиль"
|
||||
L["delete_confirm"] = "Вы уверены, что вы хотите удалить выбранный профиль?"
|
||||
L["delete_desc"] = "Удалить существующий и неиспользуемый профиль из БД для сохранения места, и очистить SavedVariables файл."
|
||||
L["delete_sub"] = "Удаление профиля из БД"
|
||||
L["intro"] = "Изменяя активный профиль, вы можете задать различные настройки модификаций для каждого персонажа."
|
||||
L["new"] = "Новый"
|
||||
L["new_sub"] = "Создать новый чистый профиль"
|
||||
L["profiles"] = "Профили"
|
||||
L["profiles_sub"] = "Управление профилями"
|
||||
L["reset"] = "Сброс профиля"
|
||||
L["reset_desc"] = "Сбросить текущий профиль к стандартным настройкам, если ваша конфигурация испорчена или вы хотите настроить всё заново."
|
||||
L["reset_sub"] = "Сброс текущего профиля на стандартный"
|
||||
elseif LOCALE == "itIT" then
|
||||
L["choose"] = "Profili Esistenti"
|
||||
L["choose_desc"] = "Puoi creare un nuovo profilo digitando il nome della casella di testo, oppure scegliendone uno tra i profili già esistenti."
|
||||
L["choose_sub"] = "Seleziona uno dei profili attualmente disponibili."
|
||||
L["copy"] = "Copia Da"
|
||||
L["copy_desc"] = "Copia le impostazioni da un profilo esistente, nel profilo attivo in questo momento."
|
||||
L["current"] = "Profilo Attivo:"
|
||||
L["default"] = "Standard"
|
||||
L["delete"] = "Cancella un Profilo"
|
||||
L["delete_confirm"] = "Sei sicuro di voler cancellare il profilo selezionato?"
|
||||
L["delete_desc"] = "Cancella i profili non utilizzati dal database per risparmiare spazio e mantenere puliti i file di configurazione SavedVariables."
|
||||
L["delete_sub"] = "Cancella un profilo dal Database."
|
||||
L["intro"] = "Puoi cambiare il profilo attivo, in modo da usare impostazioni diverse per ogni personaggio."
|
||||
L["new"] = "Nuovo"
|
||||
L["new_sub"] = "Crea un nuovo profilo vuoto."
|
||||
L["profiles"] = "Profili"
|
||||
L["profiles_sub"] = "Gestisci Profili"
|
||||
L["reset"] = "Reimposta Profilo"
|
||||
L["reset_desc"] = "Riporta il tuo profilo attivo alle sue impostazioni predefinite, nel caso in cui la tua configurazione si sia corrotta, o semplicemente tu voglia re-inizializzarla."
|
||||
L["reset_sub"] = "Reimposta il profilo ai suoi valori predefiniti."
|
||||
elseif LOCALE == "ptBR" then
|
||||
L["choose"] = "Perfis Existentes"
|
||||
L["choose_desc"] = "Você pode tanto criar um perfil novo tanto digitando um nome na caixa de texto, quanto escolher um dos perfis já existentes."
|
||||
L["choose_sub"] = "Selecione um de seus perfis atualmente disponíveis."
|
||||
L["copy"] = "Copiar De"
|
||||
L["copy_desc"] = "Copia as definições de um perfil existente no perfil atualmente ativo."
|
||||
L["current"] = "Perfil Autal:"
|
||||
L["default"] = "Padrão"
|
||||
L["delete"] = "Remover um Perfil"
|
||||
L["delete_confirm"] = "Tem certeza que deseja remover o perfil selecionado?"
|
||||
L["delete_desc"] = "Remove perfis existentes e inutilizados do banco de dados para economizar espaço, e limpar o arquivo SavedVariables."
|
||||
L["delete_sub"] = "Remove um perfil do banco de dados."
|
||||
L["intro"] = "Você pode alterar o perfil do banco de dados ativo, para que possa ter definições diferentes para cada personagem."
|
||||
L["new"] = "Novo"
|
||||
L["new_sub"] = "Cria um novo perfil vazio."
|
||||
L["profiles"] = "Perfis"
|
||||
L["profiles_sub"] = "Gerenciar Perfis"
|
||||
L["reset"] = "Resetar Perfil"
|
||||
L["reset_desc"] = "Reseta o perfil atual para os valores padrões, no caso de sua configuração estar quebrada, ou simplesmente se deseja começar novamente."
|
||||
L["reset_sub"] = "Resetar o perfil atual ao padrão"
|
||||
end
|
||||
|
||||
local defaultProfiles
|
||||
local tmpprofiles = {}
|
||||
|
||||
-- Get a list of available profiles for the specified database.
|
||||
-- You can specify which profiles to include/exclude in the list using the two boolean parameters listed below.
|
||||
-- @param db The db object to retrieve the profiles from
|
||||
-- @param common If true, getProfileList will add the default profiles to the return list, even if they have not been created yet
|
||||
-- @param nocurrent If true, then getProfileList will not display the current profile in the list
|
||||
-- @return Hashtable of all profiles with the internal name as keys and the display name as value.
|
||||
local function getProfileList(db, common, nocurrent)
|
||||
local profiles = {}
|
||||
|
||||
-- copy existing profiles into the table
|
||||
local currentProfile = db:GetCurrentProfile()
|
||||
for i,v in pairs(db:GetProfiles(tmpprofiles)) do
|
||||
if not (nocurrent and v == currentProfile) then
|
||||
profiles[v] = v
|
||||
end
|
||||
end
|
||||
|
||||
-- add our default profiles to choose from ( or rename existing profiles)
|
||||
for k,v in pairs(defaultProfiles) do
|
||||
if (common or profiles[k]) and not (nocurrent and k == currentProfile) then
|
||||
profiles[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
return profiles
|
||||
end
|
||||
|
||||
--[[
|
||||
OptionsHandlerPrototype
|
||||
prototype class for handling the options in a sane way
|
||||
]]
|
||||
local OptionsHandlerPrototype = {}
|
||||
|
||||
--[[ Reset the profile ]]
|
||||
function OptionsHandlerPrototype:Reset()
|
||||
self.db:ResetProfile()
|
||||
end
|
||||
|
||||
--[[ Set the profile to value ]]
|
||||
function OptionsHandlerPrototype:SetProfile(info, value)
|
||||
self.db:SetProfile(value)
|
||||
end
|
||||
|
||||
--[[ returns the currently active profile ]]
|
||||
function OptionsHandlerPrototype:GetCurrentProfile()
|
||||
return self.db:GetCurrentProfile()
|
||||
end
|
||||
|
||||
--[[
|
||||
List all active profiles
|
||||
you can control the output with the .arg variable
|
||||
currently four modes are supported
|
||||
|
||||
(empty) - return all available profiles
|
||||
"nocurrent" - returns all available profiles except the currently active profile
|
||||
"common" - returns all avaialble profiles + some commonly used profiles ("char - realm", "realm", "class", "Default")
|
||||
"both" - common except the active profile
|
||||
]]
|
||||
function OptionsHandlerPrototype:ListProfiles(info)
|
||||
local arg = info.arg
|
||||
local profiles
|
||||
if arg == "common" and not self.noDefaultProfiles then
|
||||
profiles = getProfileList(self.db, true, nil)
|
||||
elseif arg == "nocurrent" then
|
||||
profiles = getProfileList(self.db, nil, true)
|
||||
elseif arg == "both" then -- currently not used
|
||||
profiles = getProfileList(self.db, (not self.noDefaultProfiles) and true, true)
|
||||
else
|
||||
profiles = getProfileList(self.db)
|
||||
end
|
||||
|
||||
return profiles
|
||||
end
|
||||
|
||||
function OptionsHandlerPrototype:HasNoProfiles(info)
|
||||
local profiles = self:ListProfiles(info)
|
||||
return ((not next(profiles)) and true or false)
|
||||
end
|
||||
|
||||
--[[ Copy a profile ]]
|
||||
function OptionsHandlerPrototype:CopyProfile(info, value)
|
||||
self.db:CopyProfile(value)
|
||||
end
|
||||
|
||||
--[[ Delete a profile from the db ]]
|
||||
function OptionsHandlerPrototype:DeleteProfile(info, value)
|
||||
self.db:DeleteProfile(value)
|
||||
end
|
||||
|
||||
--[[ fill defaultProfiles with some generic values ]]
|
||||
local function generateDefaultProfiles(db)
|
||||
defaultProfiles = {
|
||||
["Default"] = L["default"],
|
||||
[db.keys.char] = db.keys.char,
|
||||
[db.keys.realm] = db.keys.realm,
|
||||
[db.keys.class] = UnitClass("player")
|
||||
}
|
||||
end
|
||||
|
||||
--[[ create and return a handler object for the db, or upgrade it if it already existed ]]
|
||||
local function getOptionsHandler(db, noDefaultProfiles)
|
||||
if not defaultProfiles then
|
||||
generateDefaultProfiles(db)
|
||||
end
|
||||
|
||||
local handler = AceDBOptions.handlers[db] or { db = db, noDefaultProfiles = noDefaultProfiles }
|
||||
|
||||
for k,v in pairs(OptionsHandlerPrototype) do
|
||||
handler[k] = v
|
||||
end
|
||||
|
||||
AceDBOptions.handlers[db] = handler
|
||||
return handler
|
||||
end
|
||||
|
||||
--[[
|
||||
the real options table
|
||||
]]
|
||||
local optionsTable = {
|
||||
desc = {
|
||||
order = 1,
|
||||
type = "description",
|
||||
name = L["intro"] .. "\n",
|
||||
},
|
||||
descreset = {
|
||||
order = 9,
|
||||
type = "description",
|
||||
name = L["reset_desc"],
|
||||
},
|
||||
reset = {
|
||||
order = 10,
|
||||
type = "execute",
|
||||
name = L["reset"],
|
||||
desc = L["reset_sub"],
|
||||
func = "Reset",
|
||||
},
|
||||
current = {
|
||||
order = 11,
|
||||
type = "description",
|
||||
name = function(info) return L["current"] .. " " .. NORMAL_FONT_COLOR_CODE .. info.handler:GetCurrentProfile() .. FONT_COLOR_CODE_CLOSE end,
|
||||
width = "default",
|
||||
},
|
||||
choosedesc = {
|
||||
order = 20,
|
||||
type = "description",
|
||||
name = "\n" .. L["choose_desc"],
|
||||
},
|
||||
new = {
|
||||
name = L["new"],
|
||||
desc = L["new_sub"],
|
||||
type = "input",
|
||||
order = 30,
|
||||
get = false,
|
||||
set = "SetProfile",
|
||||
},
|
||||
choose = {
|
||||
name = L["choose"],
|
||||
desc = L["choose_sub"],
|
||||
type = "select",
|
||||
order = 40,
|
||||
get = "GetCurrentProfile",
|
||||
set = "SetProfile",
|
||||
values = "ListProfiles",
|
||||
arg = "common",
|
||||
},
|
||||
copydesc = {
|
||||
order = 50,
|
||||
type = "description",
|
||||
name = "\n" .. L["copy_desc"],
|
||||
},
|
||||
copyfrom = {
|
||||
order = 60,
|
||||
type = "select",
|
||||
name = L["copy"],
|
||||
desc = L["copy_desc"],
|
||||
get = false,
|
||||
set = "CopyProfile",
|
||||
values = "ListProfiles",
|
||||
disabled = "HasNoProfiles",
|
||||
arg = "nocurrent",
|
||||
},
|
||||
deldesc = {
|
||||
order = 70,
|
||||
type = "description",
|
||||
name = "\n" .. L["delete_desc"],
|
||||
},
|
||||
delete = {
|
||||
order = 80,
|
||||
type = "select",
|
||||
name = L["delete"],
|
||||
desc = L["delete_sub"],
|
||||
get = false,
|
||||
set = "DeleteProfile",
|
||||
values = "ListProfiles",
|
||||
disabled = "HasNoProfiles",
|
||||
arg = "nocurrent",
|
||||
confirm = true,
|
||||
confirmText = L["delete_confirm"],
|
||||
},
|
||||
}
|
||||
|
||||
--- Get/Create a option table that you can use in your addon to control the profiles of AceDB-3.0.
|
||||
-- @param db The database object to create the options table for.
|
||||
-- @return The options table to be used in AceConfig-3.0
|
||||
-- @usage
|
||||
-- -- Assuming `options` is your top-level options table and `self.db` is your database:
|
||||
-- options.args.profiles = LibStub("AceDBOptions-3.0"):GetOptionsTable(self.db)
|
||||
function AceDBOptions:GetOptionsTable(db, noDefaultProfiles)
|
||||
local tbl = AceDBOptions.optionTables[db] or {
|
||||
type = "group",
|
||||
name = L["profiles"],
|
||||
desc = L["profiles_sub"],
|
||||
}
|
||||
|
||||
tbl.handler = getOptionsHandler(db, noDefaultProfiles)
|
||||
tbl.args = optionsTable
|
||||
|
||||
AceDBOptions.optionTables[db] = tbl
|
||||
return tbl
|
||||
end
|
||||
|
||||
-- upgrade existing tables
|
||||
for db,tbl in pairs(AceDBOptions.optionTables) do
|
||||
tbl.handler = getOptionsHandler(db)
|
||||
tbl.args = optionsTable
|
||||
end
|
4
Libs/AceDBOptions-3.0/AceDBOptions-3.0.xml
Normal file
4
Libs/AceDBOptions-3.0/AceDBOptions-3.0.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="AceDBOptions-3.0.lua"/>
|
||||
</Ui>
|
@@ -1,6 +1,6 @@
|
||||
# Grichelde - Text replacer
|
||||
|
||||
Grichelde replaces characters you entered in a chatbox with any replacment text you specified in the addon options.
|
||||
Grichelde is a WoW Classic Addon that replaces characters you typed in a chatbox with any replacement text you specified in the addon options.
|
||||
|
||||
Intentionally started as a helper addon for a roleplaying friend, Grichelde can be used for much more, like
|
||||
* fixing your common spelling errors :)
|
||||
|
19
libs.xml
19
libs.xml
@@ -1,12 +1,15 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
|
||||
<Script file="libs\LibStub\LibStub.lua"/>
|
||||
<Include file="libs\CallbackHandler-1.0\CallbackHandler-1.0.xml"/>
|
||||
<Include file="libs\AceAddon-3.0\AceAddon-3.0.xml"/>
|
||||
<Include file="libs\AceConsole-3.0\AceConsole-3.0.xml"/>
|
||||
<Include file="libs\AceGUI-3.0\AceGUI-3.0.xml"/>
|
||||
<Include file="libs\AceEvent-3.0\AceEvent-3.0.xml" />
|
||||
<Include file="libs\AceLocale-3.0\AceLocale-3.0.xml" />
|
||||
<Include file="libs\AceHook-3.0\AceHook-3.0.xml"/>
|
||||
<Script file="Libs\LibStub\LibStub.lua"/>
|
||||
<Include file="Libs\CallbackHandler-1.0\CallbackHandler-1.0.xml"/>
|
||||
<Include file="Libs\AceAddon-3.0\AceAddon-3.0.xml"/>
|
||||
<Include file="Libs\AceConfig-3.0\AceConfig-3.0.xml"/>
|
||||
<Include file="Libs\AceConsole-3.0\AceConsole-3.0.xml"/>
|
||||
<Include file="Libs\AceDB-3.0\AceDB-3.0.xml"/>
|
||||
<Include file="Libs\AceDBOptions-3.0\AceDBOptions-3.0.xml"/>
|
||||
<Include file="Libs\AceGUI-3.0\AceGUI-3.0.xml"/>
|
||||
<Include file="Libs\AceEvent-3.0\AceEvent-3.0.xml" />
|
||||
<Include file="Libs\AceLocale-3.0\AceLocale-3.0.xml" />
|
||||
<Include file="Libs\AceHook-3.0\AceHook-3.0.xml"/>
|
||||
</Ui>
|
||||
|
@@ -2,13 +2,85 @@ local L = LibStub('AceLocale-3.0'):NewLocale('Grichelde', 'deDE')
|
||||
if not L then return end
|
||||
|
||||
-- system messages
|
||||
L.AddonLoaded = 'Grichelde hilft Euch jetzt bei euren Sprachschwierigkeiten.'
|
||||
L.AddonName = "Grichelde"
|
||||
L.VersionAbbr = "v"
|
||||
L.AddonLoaded = "%s hilft Euch jetzt bei euren Sprachschwierigkeiten."
|
||||
L.Upgrade_ToVersion = "Hebe Databank auf Version %s an."
|
||||
L.Upgrade_Successful = "Upgrade erfolgreich."
|
||||
|
||||
-- profiles
|
||||
--L.ProfileCreated = 'Neues Profil "%s" erstellt'
|
||||
--L.ProfileLoaded = 'Profil auf "%s" festgelegt'
|
||||
--L.ProfileDeleted = 'Profil "%s" gel\195\182scht'
|
||||
--L.ProfileCopied = 'Einstellungen von "%s" kopiert'
|
||||
--L.ProfileReset = 'Profil "%s" zur\195\188ckgesetzt'
|
||||
--L.CantDeleteCurrentProfile = 'Das aktuelle Profil kann nicht gel\195\182scht werden'
|
||||
--L.InvalidProfile = 'Ung\195\188ltiges Profil "%s"'
|
||||
L.Profiles_Available = "Verf\195\188gbare Profile:"
|
||||
L.Profiles_Created = "Neues Profil %s angelegt."
|
||||
L.Profiles_Loaded = "Profil %s geladen."
|
||||
L.Profiles_Refreshed = "Profil %s aktualisiert."
|
||||
L.Profiles_Deleted = "Profil %s gel\195\182scht."
|
||||
L.Profiles_Copied = "Einstellungen von Profil %s \195\188bernommen."
|
||||
L.Profiles_Reset = "Profil %s zur\195\188ckgesetzt."
|
||||
L.Profiles_Invalid = "Ung\195\188ltiges Profil %s!"
|
||||
L.Profiles_DeleteError = "Das aktive Profil kann nicht gel\195\182scht werden!"
|
||||
|
||||
-- options
|
||||
L.Options_Title = "%s Einstellungen"
|
||||
L.Options_Enabled_Name = "Aktiv"
|
||||
L.Options_Enabled_Desc = "Aktiviert %s"
|
||||
|
||||
L.Options_Channels_Group_Name = "Kan\195\164le"
|
||||
L.Options_Channels_Group_Desc = "%s ist in folgenden Kan\195\164len aktiv."
|
||||
L.Options_Channels_Header = "Eine Ersetzung wird nur in den unten markierten Kan\195\164len durchgef\195\188hrt:"
|
||||
|
||||
L.Options_Channel_Say_Name = "Sagen"
|
||||
L.Options_Channel_Say_Desc = "Aktiviert %s im Kanal \"Sagen\"."
|
||||
L.Options_Channel_Emote_Name = "Emote"
|
||||
L.Options_Channel_Emote_Desc = "Aktiviert %s im Kanal \"Emote\"."
|
||||
L.Options_Channel_Yell_Name = "Schreien"
|
||||
L.Options_Channel_Yell_Desc = "Aktiviert %s im Kanal \"Schreien\"."
|
||||
L.Options_Channel_Party_Name = "Gruppe"
|
||||
L.Options_Channel_Party_Desc = "Aktiviert %s im Kanal \"Gruppe\"."
|
||||
L.Options_Channel_PartyLeader_Name = "Gruppenanf\195\188hrer"
|
||||
L.Options_Channel_PartyLeader_Desc = "Aktiviert %s im Kanal \"Gruppenanf\195\188hrer\"."
|
||||
L.Options_Channel_Guild_Name = "Gilde"
|
||||
L.Options_Channel_Guild_Desc = "Aktiviert %s im Kanal \"Gilde\"."
|
||||
L.Options_Channel_Officer_Name = "Offiziere"
|
||||
L.Options_Channel_Officer_Desc = "Aktiviert %s im Kanal \"Offiziere\"."
|
||||
L.Options_Channel_Raid_Name = "Schlachtzug"
|
||||
L.Options_Channel_Raid_Desc = "Aktiviert %s im Kanal \"Schlachtzug\"."
|
||||
L.Options_Channel_RaidLeader_Name = "Schlachtzugsanf\195\188hrer"
|
||||
L.Options_Channel_RaidLeader_Desc = "Aktiviert %s im Kanal \"Schlachtzugsanf\195\188hrer\"."
|
||||
L.Options_Channel_RaidWarning_Name = "Schlachtzugswarnung"
|
||||
L.Options_Channel_RaidWarning_Desc = "Aktiviert %s im Kanal \"Schlachtzugswarnung."
|
||||
L.Options_Channel_Instance_Name = "Instanz"
|
||||
L.Options_Channel_Instance_Desc = "Aktiviert %s im Kanal \"Instanz\"."
|
||||
L.Options_Channel_Battleground_Name = "Schlachtfeld"
|
||||
L.Options_Channel_Battleground_Desc = "Aktiviert %s im Kanal \"Schlachtfeld\"."
|
||||
L.Options_Channel_Whisper_Name = "Fl\195\188stern"
|
||||
L.Options_Channel_Whisper_Desc = "Aktiviert %s im Kanal \"Fl\195\188stern\"."
|
||||
|
||||
L.Options_Replacements_Group_Name = "Ersetzungen"
|
||||
L.Options_Replacements_Group_Desc = "Diese Vorkommen werden in den aktivierten Kan\195\164len ersetzt."
|
||||
L.Options_Replacements_Add_Name = "Hinzu"
|
||||
L.Options_Replacements_Add_Desc = "F\195\188gt eine neue Zuordnung hinzu."
|
||||
L.Options_Replacements_DeleteAll_Name = "Alle L\195\182schen"
|
||||
L.Options_Replacements_DeleteAll_Desc = "L\195\182scht alle Zuweisungen."
|
||||
L.Options_Replacements_DeleteAll_ConfirmText="Wirklich ALLE Zuweisungen l\195\182schen?"
|
||||
L.Options_Replacements_Header = "Die Vorkommen links vom Pfeil ( => ) werden in den aktivierten Kan\195\164len gesucht und durch den Text rechts vom Pfeil ersetzt."
|
||||
.."|nWird die Groß\195\159- und Kleinschreibung ignoriert, wird die Groß\195\159schreibung jedes Zeichens wird bei der Ersetzung \195\188bernommen."
|
||||
.."|nDas Zusammenfassen aufeinanderfolgender Treffer vermeidet unsch\195\182ne Wiederholungen, die durch die Ersetzung entstehen k\195\182nnen."
|
||||
.."|nMit den beiden Standard-Ersetzung wird so aus \"Tasse\" => \"Ckache\"."
|
||||
L.Options_Mapping_Group_Name = "%s => %s"
|
||||
L.Options_Mapping_Group_Desc = "Dieses Vorkommen wird in den aktivierten Kan\195\164len ersetzt."
|
||||
L.Options_Mapping_EmptyMapping = "(keine)"
|
||||
L.Options_Mapping_SearchText_Name = "Suchtext:"
|
||||
L.Options_Mapping_SearchText_Desc = "Dieser Text wird in der Chateingabe gesucht."
|
||||
L.Options_Mapping_ReplaceText_Name = "Ersetzung:"
|
||||
L.Options_Mapping_ReplaceText_Desc = "Jeder Suchtreffer wird mit diesem Text ersetzt."
|
||||
L.Options_Mapping_ExactCase_Name = "exakte Gro\195\159- und Kleinschreibung"
|
||||
L.Options_Mapping_ExactCase_Desc = "Wenn gesetzt, muss die Groß\195\159- und Kleinschreibung des Suchtextes exakt \195\188berein stimmen. Anderfalls wird die Groß\195\159schreibung jedes Zeichens bei der Ersetzung \195\188bernommen."
|
||||
L.Options_Mapping_Consolidate_Name = "Fa\195\159e aufeinanderfolgende Treffer zusammen"
|
||||
L.Options_Mapping_Consolidate_Desc = "Wenn durch die Ersetzung die Zeichenfolge mehrfach hintereinander steht,|nfasse sie zu einem Vorkommen zusammen."
|
||||
L.Options_Mapping_MoveUp_Name = "^"
|
||||
L.Options_Mapping_MoveUp_Desc = "nach oben verschieben"
|
||||
L.Options_Mapping_MoveDown_Name = "v"
|
||||
L.Options_Mapping_MoveDown_Desc = "nach unten verschieben"
|
||||
L.Options_Mapping_Delete_Name = "L\195\182schen"
|
||||
L.Options_Mapping_Delete_Desc = "L\195\182scht diese Zuweisung."
|
||||
L.Options_Mapping_Delete_ConfirmText="Diese Zuweisung l\195\182schen?"
|
@@ -2,4 +2,85 @@ local L = LibStub('AceLocale-3.0'):NewLocale('Grichelde', 'enUS', true)
|
||||
if not L then return end
|
||||
|
||||
-- system messages
|
||||
L.AddonLoaded = 'Grichelde now helps you with your spelling disabilities.'
|
||||
L.AddonName = "Grichelde"
|
||||
L.VersionAbbr = "v"
|
||||
L.AddonLoaded = "%s now helps you with your spelling disabilities."
|
||||
L.Upgrade_ToVersion = "Upgrade database to version %s."
|
||||
L.Upgrade_Successful = "Upgrade successful."
|
||||
|
||||
-- profiles
|
||||
L.Profiles_Available = "Available profiles:"
|
||||
L.Profiles_Created = "New profile %s created."
|
||||
L.Profiles_Loaded = "Profile %s is loaded."
|
||||
L.Profiles_Refreshed = "Profil %s refreshed."
|
||||
L.Profiles_Deleted = "Profile %s deleted."
|
||||
L.Profiles_Copied = "Settings applied from profile %s."
|
||||
L.Profiles_Reset = "Profil %s reset."
|
||||
L.Profiles_Invalid = "Invalid profile %s!"
|
||||
L.Profiles_DeleteError = "The active profile cannot be deleted!"
|
||||
|
||||
-- options
|
||||
L.Options_Title = "%s Options"
|
||||
L.Options_Enabled_Name = "Enabled"
|
||||
L.Options_Enabled_Desc = "Enables %s"
|
||||
|
||||
L.Options_Channels_Group_Name = "Channels"
|
||||
L.Options_Channels_Group_Desc = "%s is active in the following channels."
|
||||
L.Options_Channels_Header = "Text replacement will only be done for marked channels below:"
|
||||
|
||||
L.Options_Channel_Say_Name = "Say"
|
||||
L.Options_Channel_Say_Desc = "Activates %s in channel \"Say\"."
|
||||
L.Options_Channel_Emote_Name = "Emote"
|
||||
L.Options_Channel_Emote_Desc = "Activates %s in channel \"Emote\"."
|
||||
L.Options_Channel_Yell_Name = "Yell"
|
||||
L.Options_Channel_Yell_Desc = "Activates %s in channel \"Yell\"."
|
||||
L.Options_Channel_Party_Name = "Party"
|
||||
L.Options_Channel_Party_Desc = "Activates %s in channel \"Party\"."
|
||||
L.Options_Channel_PartyLeader_Name = "Party Leader"
|
||||
L.Options_Channel_PartyLeader_Desc = "Activates %s in channel \"Party Leader\"."
|
||||
L.Options_Channel_Guild_Name = "Guild"
|
||||
L.Options_Channel_Guild_Desc = "Activates %s in channel \"Guild\"."
|
||||
L.Options_Channel_Officer_Name = "Officers"
|
||||
L.Options_Channel_Officer_Desc = "Activates %s in channel \"Officers\"."
|
||||
L.Options_Channel_Raid_Name = "Raid"
|
||||
L.Options_Channel_Raid_Desc = "Activates %s in channel \"Raid\"."
|
||||
L.Options_Channel_RaidLeader_Name = "Raid Leader"
|
||||
L.Options_Channel_RaidLeader_Desc = "Activates %s in channel \"Raid Leader\"."
|
||||
L.Options_Channel_RaidWarning_Name = "Raid Warning"
|
||||
L.Options_Channel_RaidWarning_Desc = "Activates %s in channel \"Raid Warning\"."
|
||||
L.Options_Channel_Instance_Name = "Instance"
|
||||
L.Options_Channel_Instance_Desc = "Activates %s in channel \"Instance\"."
|
||||
L.Options_Channel_Battleground_Name = "Battleground"
|
||||
L.Options_Channel_Battleground_Desc = "Activates %s in channel \"Battleground\"."
|
||||
L.Options_Channel_Whisper_Name = "Whisper"
|
||||
L.Options_Channel_Whisper_Desc = "Activates %s in channel \"Whisper\"."
|
||||
|
||||
L.Options_Replacements_Group_Name = "Replacements"
|
||||
L.Options_Replacements_Group_Desc = "These lookups will be replaced in activated channels."
|
||||
L.Options_Replacements_Add_Name = "Add"
|
||||
L.Options_Replacements_Add_Desc = "Adds a new replacement mapping."
|
||||
L.Options_Replacements_DeleteAll_Name = "Delete All"
|
||||
L.Options_Replacements_DeleteAll_Desc = "Deletes all replacement mappings."
|
||||
L.Options_Replacements_DeleteAll_ConfirmText = "Do you really want to delete ALL replacement mappings?"
|
||||
L.Options_Replacements_Header = "All matches on the lefthand side of the arrow ( => ) will be replaced in activated channels by the text on the righthand side."
|
||||
.. "|nIf case sensivity is ignored, the case for each letter of the matching text is taken over when replaced."
|
||||
.. "|nConsolidation of consecutive matches prevent unaesthetic repetitions of letters introduced by replacements."
|
||||
.. "|nWith both default mappings active, the mapping would be \"Tossing\" => \"Ckoching\"."
|
||||
L.Options_Mapping_Group_Name = "%s => %s"
|
||||
L.Options_Mapping_Group_Desc = "This lookup will be replaced in activated channels."
|
||||
L.Options_Mapping_EmptyMapping = "(none)"
|
||||
L.Options_Mapping_SearchText_Name = "Search for:"
|
||||
L.Options_Mapping_SearchText_Desc = "This text is looked up in your chat input box."
|
||||
L.Options_Mapping_ReplaceText_Name = "Replacement:"
|
||||
L.Options_Mapping_ReplaceText_Desc = "Any match will be replaced with this text."
|
||||
L.Options_Mapping_ExactCase_Name = "exact case"
|
||||
L.Options_Mapping_ExactCase_Desc = "When set, matches must be case-sensitive. Otherwise the case for each letter of the matching text is taken over when replaced."
|
||||
L.Options_Mapping_Consolidate_Name = "consolidate consecutive matches"
|
||||
L.Options_Mapping_Consolidate_Desc = "If after the replacement a text sequence is repeated|ndirectly after another, treat them as one occurrence."
|
||||
L.Options_Mapping_MoveUp_Name = "^"
|
||||
L.Options_Mapping_MoveUp_Desc = "move up"
|
||||
L.Options_Mapping_MoveDown_Name = "v"
|
||||
L.Options_Mapping_MoveDown_Desc = "move down"
|
||||
L.Options_Mapping_Delete_Name = "Delete"
|
||||
L.Options_Mapping_Delete_Desc = "Deletes this replacement mapping."
|
||||
L.Options_Mapping_Delete_ConfirmText = "Delete this replacement mapping?"
|
BIN
twitch/grichelde.png
Normal file
BIN
twitch/grichelde.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
BIN
twitch/logo.gif
Normal file
BIN
twitch/logo.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
twitch/logo.pptx
Normal file
BIN
twitch/logo.pptx
Normal file
Binary file not shown.
Reference in New Issue
Block a user