diff --git a/Chattynator.toc b/Chattynator.toc index a9bf8ac..cf2e850 100644 --- a/Chattynator.toc +++ b/Chattynator.toc @@ -28,6 +28,7 @@ Core/Objects.lua Core/Locales.lua Core/Constants.lua Core/Config.lua +Core/EditBoxUndo.lua Core/Utilities.lua Core/HyperlinkHandler.xml Core/Fonts.lua diff --git a/Core/Config.lua b/Core/Config.lua index 7623606..e698819 100644 --- a/Core/Config.lua +++ b/Core/Config.lua @@ -109,6 +109,7 @@ local settings = { CLASS_COLORS = {key = "class_colors", default = true, refresh = {addonTable.Constants.RefreshReason.MessageModifier}}, LINK_URLS = {key = "link_urls", default = true, refresh = {addonTable.Constants.RefreshReason.MessageModifier}}, REDUCE_REDUNDANT_TEXT = {key = "reduce_redundant_text", default = false, refresh = {addonTable.Constants.RefreshReason.MessageModifier}}, + EDIT_BOX_UNDO_HISTORY = {key = "edit_box_undo_history", default = 50}, NEW_WHISPER_NEW_TAB = {key = "new_whisper_new_tab", default = 0}, BUTTON_POSITION = {key = "button_position", default = "outside_left"}, diff --git a/Core/Dialogs.lua b/Core/Dialogs.lua index 276112b..3ffea0c 100644 --- a/Core/Dialogs.lua +++ b/Core/Dialogs.lua @@ -46,6 +46,7 @@ function addonTable.Dialogs.ShowCopy(text) dialog.editBox:SetScript("OnEnterPressed", function() dialog:Hide() end) + addonTable.Core.EditBoxUndo.Attach(dialog.editBox) local okButton = CreateFrame("Button", nil, dialog, "UIPanelDynamicResizeButtonTemplate") okButton:SetText(DONE) @@ -65,6 +66,7 @@ function addonTable.Dialogs.ShowCopy(text) dialog:Hide() dialog:Show() dialog.editBox:SetText(text) + addonTable.Core.EditBoxUndo.Reset(dialog.editBox) dialog.editBox:SetFocus() dialog.editBox:HighlightText() end @@ -79,6 +81,7 @@ function addonTable.Dialogs.ShowEditBox(text, acceptText, cancelText, confirmCal dialog.editBox:SetAutoFocus(false) dialog.editBox:SetSize(200, 30) dialog.editBox:SetPoint("CENTER") + addonTable.Core.EditBoxUndo.Attach(dialog.editBox) dialog.acceptButton = CreateFrame("Button", nil, dialog, "UIPanelDynamicResizeButtonTemplate") dialog.cancelButton = CreateFrame("Button", nil, dialog, "UIPanelDynamicResizeButtonTemplate") @@ -107,6 +110,7 @@ function addonTable.Dialogs.ShowEditBox(text, acceptText, cancelText, confirmCal dialog.acceptButton:SetScript("OnClick", function() confirmCallback(dialog.editBox:GetText()); dialog:Hide() end) dialog.editBox:SetScript("OnEnterPressed", function() confirmCallback(dialog.editBox:GetText()); dialog:Hide() end) dialog.editBox:SetText("") + addonTable.Core.EditBoxUndo.Reset(dialog.editBox) dialog:Show() dialog.editBox:SetFocus() diff --git a/Core/EditBoxUndo.lua b/Core/EditBoxUndo.lua new file mode 100644 index 0000000..9cdbbdc --- /dev/null +++ b/Core/EditBoxUndo.lua @@ -0,0 +1,235 @@ +---@class addonTableChattynator +local addonTable = select(2, ...) + +addonTable.Core.EditBoxUndo = {} + +local trackedEditBoxes = {} +local defaultDebounceSeconds = 0.3 + +local function NormalizeLimit(value) + value = tonumber(value) or 0 + value = math.floor(value) + if value < 1 then + value = 1 + end + return value +end + +local function GetHistoryLimit() + return NormalizeLimit(addonTable.Config.Get(addonTable.Config.Options.EDIT_BOX_UNDO_HISTORY)) +end + +local function TrimHistory(state) + local overflow = #state.history - state.max + if overflow <= 0 then + return + end + for _ = 1, overflow do + table.remove(state.history, 1) + end + state.index = math.max(1, state.index - overflow) +end + +local function ClearPending(state) + if state.pendingTimer then + state.pendingTimer:Cancel() + state.pendingTimer = nil + end + state.pendingText = nil + state.pendingCursor = nil +end + +local function PushHistory(state, text, cursor) + local last = state.history[state.index] + if last and last.text == text then + return + end + for i = #state.history, state.index + 1, -1 do + table.remove(state.history, i) + end + table.insert(state.history, { text = text, cursor = cursor }) + state.index = #state.history + TrimHistory(state) +end + +local function FlushPending(state) + if not state.pendingText then + return + end + local text = state.pendingText + local cursor = state.pendingCursor + ClearPending(state) + PushHistory(state, text, cursor) +end + +local function ScheduleCommit(state, text, cursor) + state.pendingText = text + state.pendingCursor = cursor + if state.pendingTimer then + state.pendingTimer:Cancel() + end + state.pendingTimer = C_Timer.NewTimer(state.debounceSeconds, function() + state.pendingTimer = nil + if state.pendingText then + local pendingText = state.pendingText + local pendingCursor = state.pendingCursor + state.pendingText = nil + state.pendingCursor = nil + PushHistory(state, pendingText, pendingCursor) + end + end) +end + +local function ApplyEntry(editBox, state, entry) + state.ignore = true + editBox:SetText(entry.text) + local cursor = entry.cursor or #entry.text + if cursor > #entry.text then + cursor = #entry.text + end + editBox:SetCursorPosition(cursor) + state.ignore = false +end + +local function Undo(editBox, state) + FlushPending(state) + if state.index <= 1 then + return false + end + state.index = state.index - 1 + ApplyEntry(editBox, state, state.history[state.index]) + return true +end + +local function Redo(editBox, state) + FlushPending(state) + if state.index >= #state.history then + return false + end + state.index = state.index + 1 + ApplyEntry(editBox, state, state.history[state.index]) + return true +end + +local function EnsureState(editBox, options) + if editBox.ChattynatorUndo then + return editBox.ChattynatorUndo + end + local state = { + history = {}, + index = 0, + max = NormalizeLimit((options and options.max) or GetHistoryLimit()), + debounceSeconds = (options and options.debounceSeconds) or defaultDebounceSeconds, + pendingTimer = nil, + pendingText = nil, + pendingCursor = nil, + ignore = false, + hooked = false, + } + local text = editBox:GetText() or "" + local cursor = editBox:GetCursorPosition() or 0 + if cursor > #text then + cursor = #text + end + state.history[1] = { text = text, cursor = cursor } + state.index = 1 + editBox.ChattynatorUndo = state + trackedEditBoxes[editBox] = true + return state +end + +local function CanEditBoxInput(editBox) + if C_ChatInfo and C_ChatInfo.InChatMessagingLockdown and C_ChatInfo.InChatMessagingLockdown() then + return false + end + return editBox:IsVisible() +end + +function addonTable.Core.EditBoxUndo.Attach(editBox, options) + if not editBox then + return + end + local state = EnsureState(editBox, options) + if state.hooked then + return + end + state.hooked = true + + editBox:HookScript("OnTextChanged", function(self) + if state.ignore then + return + end + local text = self:GetText() or "" + local last = state.history[state.index] + if last and last.text == text then + ClearPending(state) + return + end + local cursor = self:GetCursorPosition() or 0 + if cursor > #text then + cursor = #text + end + ScheduleCommit(state, text, cursor) + end) + + editBox:HookScript("OnEditFocusLost", function() + FlushPending(state) + end) + + editBox:HookScript("OnHide", function() + FlushPending(state) + end) + + editBox:HookScript("OnKeyDown", function(self, key) + if not CanEditBoxInput(self) then + return + end + if not IsControlKeyDown() or IsAltKeyDown() then + return + end + if key == "Z" then + if IsShiftKeyDown() then + Redo(self, state) + else + Undo(self, state) + end + elseif key == "Y" then + Redo(self, state) + end + end) +end + +function addonTable.Core.EditBoxUndo.RefreshHistoryLimit() + local max = GetHistoryLimit() + for editBox in pairs(trackedEditBoxes) do + if editBox.ChattynatorUndo then + editBox.ChattynatorUndo.max = max + TrimHistory(editBox.ChattynatorUndo) + end + end +end + +function addonTable.Core.EditBoxUndo.Reset(editBox) + if not editBox or not editBox.ChattynatorUndo then + return + end + local state = editBox.ChattynatorUndo + ClearPending(state) + state.history = {} + state.index = 1 + local text = editBox:GetText() or "" + local cursor = editBox:GetCursorPosition() or 0 + if cursor > #text then + cursor = #text + end + state.history[1] = { text = text, cursor = cursor } +end + +function addonTable.Core.EditBoxUndo.Initialize() + addonTable.CallbackRegistry:RegisterCallback("SettingChanged", function(_, settingName) + if settingName == addonTable.Config.Options.EDIT_BOX_UNDO_HISTORY then + addonTable.Core.EditBoxUndo.RefreshHistoryLimit() + end + end) + addonTable.Core.EditBoxUndo.Attach(ChatFrame1EditBox) +end diff --git a/Core/Initialize.lua b/Core/Initialize.lua index 44eb886..82a498f 100644 --- a/Core/Initialize.lua +++ b/Core/Initialize.lua @@ -199,6 +199,7 @@ function addonTable.Core.Initialize() addonTable.Core.ApplyOverrides() addonTable.Core.InitializeChatCommandLogging() + addonTable.Core.EditBoxUndo.Initialize() addonTable.Modifiers.InitializeShortenChannels() addonTable.Modifiers.InitializeClassColors() addonTable.Modifiers.InitializeURLs() diff --git a/CustomiseDialog/Main.lua b/CustomiseDialog/Main.lua index c3031ba..0104560 100644 --- a/CustomiseDialog/Main.lua +++ b/CustomiseDialog/Main.lua @@ -329,6 +329,14 @@ local function SetupLayout(parent) end table.insert(allFrames, editBoxPositionDropdown) + local editBoxUndoHistory + editBoxUndoHistory = addonTable.CustomiseDialog.Components.GetSlider(container, addonTable.Locales.EDIT_BOX_UNDO_HISTORY, 1, 200, "%s", function() + addonTable.Config.Set(addonTable.Config.Options.EDIT_BOX_UNDO_HISTORY, editBoxUndoHistory:GetValue()) + end) + editBoxUndoHistory.option = addonTable.Config.Options.EDIT_BOX_UNDO_HISTORY + editBoxUndoHistory:SetPoint("TOP", allFrames[#allFrames], "BOTTOM", 0, -30) + table.insert(allFrames, editBoxUndoHistory) + local newWhispersNewTab = addonTable.CustomiseDialog.Components.GetCheckbox(container, addonTable.Locales.NEW_WHISPERS_TO_NEW_TAB, 28, function(state) addonTable.Config.Set(addonTable.Config.Options.NEW_WHISPER_NEW_TAB, state and 1 or 0) end) diff --git a/Locales.lua b/Locales.lua index 2ed8f43..ef449ae 100644 --- a/Locales.lua +++ b/Locales.lua @@ -63,6 +63,7 @@ L["LINK"] = "Link" L["SHOW_COMBAT_LOG"] = "Show Combat Log" L["LOCK_CHAT"] = "Lock chat" L["EDIT_BOX_POSITION"] = "Edit box position" +L["EDIT_BOX_UNDO_HISTORY"] = "Undo history size" L["BOTTOM"] = "Bottom" L["TOP"] = "Top" L["SHOW_TABS"] = "Show tabs"