Advertisement
Guest User

ChatAutocompleteIntegrator.lua

a guest
Jan 8th, 2020
419
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 9.21 KB | None | 0 0
  1. select(2, ...) 'ChatAutocompleteIntegrator'
  2.  
  3. -- Imports
  4. local util = require 'Utility.Functions'
  5.  
  6. -- Consts
  7. local const = util.ReadOnly({ maxItems = 10 })
  8.  
  9. ------------------------------------------
  10. -- Class definition
  11. ------------------------------------------
  12.  
  13. local ChatAutocompleteIntegrator = {}
  14. ChatAutocompleteIntegrator.__index = ChatAutocompleteIntegrator
  15.  
  16. ------------------------------------------
  17. -- Constructor
  18. ------------------------------------------
  19.  
  20. -- Creates a chat autocomplete menu
  21. function ChatAutocompleteIntegrator.New(itemDatabase)
  22.   local self = setmetatable({}, ChatAutocompleteIntegrator)
  23.  
  24.   self.buttonMenu = CreateFrame('Frame', nil, UIParent, 'ItemAutocompleteButtonMenuTemplate')
  25.   self.buttonMenu:Hide()
  26.   self.buttonMenu:SetFrameLevel(10)
  27.   self.editBoxCursorOffsets = {}
  28.   self.itemDatabase = itemDatabase
  29.   self.methods = util.ContextBinder(self)
  30.   self.searchCursorOffsetX = nil
  31.   self:SetItemLinkDelimiters('[', ']')
  32.   self:SetCaseSensitivity(0)
  33.  
  34.   return self
  35. end
  36.  
  37. ------------------------------------------
  38. -- Public methods
  39. ------------------------------------------
  40.  
  41. function ChatAutocompleteIntegrator:Enable()
  42.   -- These are not actual hooks, rather just listeners
  43.   hooksecurefunc('ChatEdit_OnEditFocusLost', self.methods._OnChatFocusLost)
  44.   hooksecurefunc('ChatEdit_OnTextChanged', self.methods._OnChatTextChanged)
  45.  
  46.   self.original = {
  47.     substituteChatMessageBeforeSend = util.Hook(
  48.       'SubstituteChatMessageBeforeSend',
  49.       self.methods._HookChatMessageBeforeSend),
  50.     autoCompleteEditBoxOnEscapePressed = util.Hook(
  51.       'AutoCompleteEditBox_OnEscapePressed',
  52.       self.methods._HookChatEscapePressed),
  53.     chatEditCustomTabPressed = util.Hook(
  54.       'ChatEdit_CustomTabPressed',
  55.       self.methods._HookChatTabPressed),
  56.   }
  57.  
  58.   for i = 1, NUM_CHAT_WINDOWS do
  59.     local editBox = _G['ChatFrame' .. i .. 'EditBox']
  60.     editBox:HookScript('OnArrowPressed', self.methods._OnChatArrowPressed)
  61.     editBox:HookScript('OnCursorChanged', self.methods._OnChatCursorChanged)
  62.   end
  63.  
  64.   self.buttonMenu:HookScript('OnShow', function(menu)
  65.     local parent = menu:GetParent()
  66.     menu.previousArrowKeyMode = parent:GetAltArrowKeyMode()
  67.     parent:SetAltArrowKeyMode(false)
  68.   end)
  69.  
  70.   self.buttonMenu:HookScript('OnHide', function(menu)
  71.     if menu.previousArrowKeyMode then
  72.       menu:GetParent():SetAltArrowKeyMode(menu.previousArrowKeyMode)
  73.       menu.previousArrowKeyMode = nil
  74.     end
  75.   end)
  76. end
  77.  
  78. function ChatAutocompleteIntegrator:Config(options)
  79.   options.itemLinkDelimiters = options.itemLinkDelimiters or '[]'
  80.   self:SetItemLinkDelimiters(
  81.     options.itemLinkDelimiters:byte(1),
  82.     options.itemLinkDelimiters:byte(2))
  83.  
  84.    options.caseSensitivity = options.caseSensitivity or 0
  85.    self:SetCaseSensitivity(options.caseSensitivity)
  86.  
  87.   return {
  88.     caseSensitivity = {
  89.       type = 'select',
  90.       values = {
  91.         [0] = 'Smart case',
  92.         [1] = 'Case-insensitive',
  93.       },
  94.       style = 'dropdown',
  95.       name = 'Case sensitivity',
  96.       desc = 'Specify the case sensitivity of the item search.',
  97.       get = function() return options.caseSensitivity end,
  98.       set = function(_, key)
  99.         options.caseSensitivity = key
  100.         self:SetCaseSensitivity(key)
  101.       end,
  102.     },
  103.     itemLinkDelimiters = {
  104.       type = 'select',
  105.       values = {
  106.         ['<>'] = 'Angle brackets — <>',
  107.         ['{}'] = 'Curly brackets — {}',
  108.         ['[]'] = 'Square brackets — []',
  109.         ['()'] = 'Parentheses — ()',
  110.       },
  111.       style = 'dropdown',
  112.       name = 'Chat item link delimiters',
  113.       desc = 'Specify the item link delimiters used.',
  114.       get = function() return options.itemLinkDelimiters end,
  115.       set = function(_, key)
  116.         options.itemLinkDelimiters = key
  117.         self:SetItemLinkDelimiters(key:byte(1), key:byte(2))
  118.       end,
  119.     },
  120.   }
  121. end
  122.  
  123. function ChatAutocompleteIntegrator:SetItemLinkDelimiters(open, close)
  124.   self.itemLinkDelimiters = {
  125.     type(open) == 'string' and string.byte(open) or open,
  126.     type(close) == 'string' and string.byte(close) or close,
  127.   }
  128. end
  129.  
  130. function ChatAutocompleteIntegrator:SetCaseSensitivity(key)
  131.   self.caseSensitivity = key
  132. end
  133.  
  134. ------------------------------------------
  135. -- Private methods
  136. ------------------------------------------
  137.  
  138. function ChatAutocompleteIntegrator:_OnItemSearchComplete(editBox, items, searchInfo)
  139.   if not editBox:IsShown() then return end
  140.  
  141.   local searchTerm = self:_GetEditBoxSearchTerm(editBox)
  142.  
  143.   -- Since this is received asynchronously, discard the result if it has become irrelevant
  144.   if util.IsNilOrEmpty(searchTerm) or searchTerm:find(searchInfo.searchTerm) ~= 1 then
  145.     return self.buttonMenu:Hide()
  146.   end
  147.  
  148.   self.buttonMenu:ClearAll()
  149.   for item in items do
  150.     self.buttonMenu:AddButton({
  151.       text = item.link,
  152.       value = item,
  153.       onTooltipShow = function(tooltip) tooltip:SetItemByID(item.id) end,
  154.       onClick = function(_) self:_OnItemSelected(editBox, item) end
  155.     })
  156.   end
  157.  
  158.   if not self.buttonMenu:IsEmpty() then
  159.     local offsetX = select(1, editBox:GetTextInsets()) + searchInfo.searchOffsetX
  160.     self.buttonMenu:SetParent(editBox)
  161.     self.buttonMenu:ClearAllPoints()
  162.     self.buttonMenu:SetPoint('BOTTOMLEFT', editBox, 'TOPLEFT', offsetX, editBox.autoCompleteYOffset or -AUTOCOMPLETE_DEFAULT_Y_OFFSET)
  163.     self.buttonMenu:Show()
  164.   else
  165.     self.buttonMenu:Hide()
  166.   end
  167. end
  168.  
  169. function ChatAutocompleteIntegrator:_OnItemSelected(editBox, item)
  170.   local searchTerm, prefixText, suffixText = self:_GetEditBoxSearchTerm(editBox)
  171.  
  172.   if not util.IsNilOrEmpty(searchTerm) then
  173.     editBox:SetText(prefixText .. item.link .. suffixText)
  174.     editBox:SetCursorPosition(#prefixText + #item.link)
  175.   end
  176.  
  177.   self.buttonMenu:Hide()
  178. end
  179.  
  180. function ChatAutocompleteIntegrator:_OnChatTextChanged(editBox, isUserInput)
  181.   if not isUserInput then return end
  182.  
  183.   local searchTerm = self:_GetEditBoxSearchTerm(editBox)
  184.  
  185.   if util.IsNilOrEmpty(searchTerm) then
  186.     self.searchCursorOffsetX = searchTerm == '' and self.editBoxCursorOffsets[editBox] or nil
  187.     self.buttonMenu:Hide()
  188.     return
  189.   end
  190.  
  191.   self.itemDatabase:FindItemsAsync(searchTerm, const.maxItems, function(items)
  192.     self:_OnItemSearchComplete(editBox, items, {
  193.       searchTerm = searchTerm,
  194.       searchOffsetX = self.searchCursorOffsetX or self.editBoxCursorOffsets[editBox],
  195.     })
  196.   end)
  197. end
  198.  
  199. function ChatAutocompleteIntegrator:_OnChatArrowPressed(_, key)
  200.   if self.buttonMenu:IsShown() then
  201.     if key == 'UP' then
  202.       self.buttonMenu:IncrementSelection(true)
  203.     elseif key == 'DOWN' then
  204.       self.buttonMenu:IncrementSelection(false)
  205.     end
  206.   end
  207. end
  208.  
  209. function ChatAutocompleteIntegrator:_OnChatCursorChanged(editBox, x)
  210.   self.editBoxCursorOffsets[editBox] = x
  211. end
  212.  
  213. function ChatAutocompleteIntegrator:_OnChatFocusLost(_)
  214.   self.buttonMenu:Hide()
  215. end
  216.  
  217. function ChatAutocompleteIntegrator:_HookChatMessageBeforeSend(text)
  218.   if self.buttonMenu:IsShown() then
  219.     local editBox = GetCurrentKeyBoardFocus()
  220.  
  221.     -- Whilst hooking the 'enter pressed' event seems to be the most obvious, it
  222.     -- taints the runtime and prevents any secure commands from being executed
  223.     -- in the chat (e.g. /target). To circumvent this, a function run later in
  224.     -- the invocation chain is hooked instead - SubstituteChatMessageBeforeSend.
  225.     -- To actually prevent normal operations, the return value cannot be
  226.     -- utilized due to being unused. Instead an error is thrown whilst a
  227.     -- temporary error handler is set to avoid any interference for users.
  228.     self:_OnItemSelected(editBox, self.buttonMenu:GetSelection())
  229.     util.Abort()
  230.   end
  231.  
  232.   return self.original.substituteChatMessageBeforeSend(text)
  233. end
  234.  
  235. function ChatAutocompleteIntegrator:_HookChatEscapePressed(editBox)
  236.   if self.buttonMenu:IsShown() then
  237.     self.buttonMenu:Hide()
  238.     return true
  239.   end
  240.  
  241.   return self.original.autoCompleteEditBoxOnEscapePressed(editBox)
  242. end
  243.  
  244. function ChatAutocompleteIntegrator:_HookChatTabPressed(editBox)
  245.   if self.buttonMenu:IsShown() then
  246.     self.buttonMenu:IncrementSelection(IsShiftKeyDown())
  247.     return true
  248.   end
  249.  
  250.   return self.original.chatEditCustomTabPressed(editBox)
  251. end
  252.  
  253. function ChatAutocompleteIntegrator:_GetEditBoxSearchTerm(editBox)
  254.   local cursorPosition = editBox:GetCursorPosition()
  255.   local text = editBox:GetText()
  256.   if(self.caseSensitivity == 1) then
  257.     text = string.lower(text)
  258.   end
  259.   local activeText = text:sub(1, cursorPosition)
  260.   local searchTerm, startIndex = self:_ExtractSearchTerm(activeText)
  261.  
  262.   if searchTerm == nil then
  263.     return nil, nil, nil
  264.   end
  265.  
  266.   local prefixText = text:sub(1, startIndex - 1)
  267.   local suffixText = text:sub(cursorPosition + 1)
  268.  
  269.   return searchTerm, prefixText, suffixText
  270. end
  271.  
  272. function ChatAutocompleteIntegrator:_ExtractSearchTerm(text)
  273.   local open, close = unpack(self.itemLinkDelimiters)
  274.  
  275.   for i = #text, 1, -1 do
  276.     if text:byte(i) == close then
  277.       return nil, 0
  278.     end
  279.  
  280.     if text:byte(i) == open then
  281.       return text:sub(i + 1), i
  282.     end
  283.   end
  284.  
  285.   return nil, 0
  286. end
  287.  
  288. ------------------------------------------
  289. -- Exports
  290. ------------------------------------------
  291.  
  292. export.New = function(...) return ChatAutocompleteIntegrator.New(...) end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement