-- ------------------------------------------------------------------------------ -- -- TradeSkillMaster -- -- https://tradeskillmaster.com -- -- All Rights Reserved - Detailed license information included with addon. -- -- ------------------------------------------------------------------------------ -- --- Base Input UI Element Class. -- The base input class is an abstract class which provides shared functionality between the @{Input} and -- @{MultiLineInput} classes. It is a subclass of the @{Element} class. -- @classmod BaseInput local _, TSM = ... local L = TSM.Include("Locale").GetTable() local NineSlice = TSM.Include("Util.NineSlice") local Log = TSM.Include("Util.Log") local Color = TSM.Include("Util.Color") local Theme = TSM.Include("Util.Theme") local Delay = TSM.Include("Util.Delay") local ScriptWrapper = TSM.Include("Util.ScriptWrapper") local CustomPrice = TSM.Include("Service.CustomPrice") local BaseInput = TSM.Include("LibTSMClass").DefineClass("BaseInput", TSM.UI.Element, "ABSTRACT") local UIElements = TSM.Include("UI.UIElements") UIElements.Register(BaseInput) TSM.UI.BaseInput = BaseInput local private = {} local BORDER_THICKNESS = 1 -- ============================================================================ -- Public Class Methods -- ============================================================================ function BaseInput.__init(self, frame) self.__super:__init(frame) self._borderNineSlice = NineSlice.New(frame) self._borderNineSlice:Hide() self._backgroundNineSlice = NineSlice.New(frame, 1) self._backgroundNineSlice:Hide() self._editBox:SetShadowColor(0, 0, 0, 0) self._editBox:SetAutoFocus(false) ScriptWrapper.Set(self._editBox, "OnEscapePressed", private.OnEscapePressed, self) ScriptWrapper.Set(self._editBox, "OnTabPressed", private.OnTabPressed, self) ScriptWrapper.Set(self._editBox, "OnEditFocusGained", private.OnEditFocusGained, self) ScriptWrapper.Set(self._editBox, "OnEditFocusLost", private.OnEditFocusLost, self) ScriptWrapper.Set(self._editBox, "OnChar", self._OnChar, self) self._lostFocusDelayName = "INPUT_LOST_FOCUS_"..tostring(frame) self._backgroundColor = "ACTIVE_BG_ALT" self._borderColor = nil self._value = "" self._escValue = nil self._justifyH = "LEFT" self._justifyV = "MIDDLE" self._font = "BODY_BODY2" self._pasteMode = nil self._validateFunc = nil self._validateContext = nil self._settingTable = nil self._settingKey = nil self._disabled = false self._isValid = true self._tabPrevPath = nil self._tabNextPath = nil self._onValueChangedHandler = nil self._onEnterPressedHandler = nil self._onValidationChangedHandler = nil self._onFocusLostHandler = nil self._pasteChars = {} end function BaseInput.Acquire(self) self.__super:Acquire() ScriptWrapper.Set(self._editBox, "OnEnterPressed", private.OnEnterPressed, self) ScriptWrapper.Set(self._editBox, "OnTextChanged", private.OnTextChanged, self) end function BaseInput.Release(self) Delay.Cancel(self._lostFocusDelayName) ScriptWrapper.Clear(self._editBox, "OnEnterPressed") ScriptWrapper.Clear(self._editBox, "OnTextChanged") self._editBox:SetText("") self._editBox:ClearFocus() self._editBox:Enable() self._editBox:EnableMouse(true) self._editBox:EnableKeyboard(true) self._editBox:HighlightText(0, 0) self._editBox:SetHitRectInsets(0, 0, 0, 0) self._editBox:SetMaxLetters(2147483647) self._editBox:SetMaxBytes(2147483647) self._backgroundColor = "ACTIVE_BG_ALT" self._borderColor = nil self._value = "" self._escValue = nil self._justifyH = "LEFT" self._justifyV = "MIDDLE" self._font = "BODY_BODY2" self._pasteMode = nil self._validateFunc = nil self._validateContext = nil self._settingTable = nil self._settingKey = nil self._disabled = false self._isValid = true self._tabPrevPath = nil self._tabNextPath = nil self._onValueChangedHandler = nil self._onEnterPressedHandler = nil self._onValidationChangedHandler = nil self._onFocusLostHandler = nil wipe(self._pasteChars) self.__super:Release() end --- Sets the background of the input. -- @tparam BaseInput self The input object -- @tparam ?string|nil color The background color as a theme color key or nil -- @treturn BaseInput The input object function BaseInput.SetBackgroundColor(self, color) assert(color == nil or Theme.GetColor(color)) self._backgroundColor = color return self end --- Sets the border of the input. -- @tparam BaseInput self The input object -- @tparam ?string|nil color The border color as a theme color key or nil -- @treturn BaseInput The input object function BaseInput.SetBorderColor(self, color) assert(color == nil or Theme.GetColor(color)) self._borderColor = color return self end --- Sets the horizontal justification of the input. -- @tparam BaseInput self The input object -- @tparam string justifyH The horizontal justification (either "LEFT", "CENTER" or "RIGHT") -- @treturn BaseInput The input object function BaseInput.SetJustifyH(self, justifyH) assert(justifyH == "LEFT" or justifyH == "CENTER" or justifyH == "RIGHT") self._justifyH = justifyH return self end --- Sets the vertical justification of the input. -- @tparam BaseInput self The input object -- @tparam string justifyV The vertical justification (either "TOP", "MIDDLE" or "BOTTOM") -- @treturn BaseInput The input object function BaseInput.SetJustifyV(self, justifyV) assert(justifyV == "TOP" or justifyV == "MIDDLE" or justifyV == "BOTTOM") self._justifyV = justifyV return self end --- Sets the font. -- @tparam BaseInput self The input object -- @tparam string font The font key -- @treturn BaseInput The input object function BaseInput.SetFont(self, font) assert(Theme.GetFont(font)) self._font = font return self end --- Sets the path of the inputs to jump to when tab (or shift-tab to go backwards) is pressed. -- @tparam BaseInput self The input object -- @tparam string prevPath The path to the previous input (for shift-tab) -- @tparam string nextPath The path to the next input (for tab) -- @treturn BaseInput The input object function BaseInput.SetTabPaths(self, prevPath, nextPath) self._tabPrevPath = prevPath self._tabNextPath = nextPath return self end --- Set the highlight to all or some of the input's text. -- @tparam BaseInput self The input object -- @tparam number starting The position at which to start the highlight -- @tparam number ending The position at which to stop the highlight -- @treturn BaseInput The input object function BaseInput.HighlightText(self, starting, ending) if starting and ending then self._editBox:HighlightText(starting, ending) else self._editBox:HighlightText() end return self end --- Sets the current value. -- @tparam BaseInput self The input object -- @tparam string value The value -- @treturn BaseInput The input object function BaseInput.SetValue(self, value) if type(value) == "number" then value = tostring(value) end assert(type(value) == "string") if self:_SetValueHelper(value, true) then self._escValue = self._value else self._escValue = nil end return self end --- Sets whether or not the input is disabled. -- @tparam BaseInput self The input object -- @tparam boolean disabled Whether or not the input is disabled -- @treturn BaseInput The input object function BaseInput.SetDisabled(self, disabled) self._disabled = disabled if disabled then self._editBox:Disable() else self._editBox:Enable() end return self end --- Sets the function to use to validate the input text. -- @tparam BaseInput self The input object -- @tparam ?string|function validateFunc A function which returns true if the passed text is valid -- or false and an error message if not, or one of the following strings for built in validate -- functions: "CUSTOM_PRICE" -- @param[opt=nil] context Extra context to pass to the validate function. For the built-in -- "CUSTOM_PRICE" function, this is optionally a list of bad sources. For the built-in "NUMBER" -- function, this must be a string such as "0:1000" to specify the min and max values. -- @treturn BaseInput The input object function BaseInput.SetValidateFunc(self, validateFunc, context) if type(validateFunc) == "function" then self._validateFunc = validateFunc self._validateContext = context elseif validateFunc == "CUSTOM_PRICE" then assert(context == nil or type(context) == "table") self._validateFunc = private.CustomPriceValidateFunc self._validateContext = context elseif validateFunc == "NUMBER" then local minVal, maxVal, extra = strsplit(":", context) assert(tonumber(minVal) <= tonumber(maxVal) and not extra) self._validateFunc = private.NumberValidateFunc self._validateContext = context else error("Invalid validateFunc: "..tostring(validateFunc)) end return self end --- Returns the input's focus state. -- @tparam BaseInput self The input object function BaseInput.HasFocus(self) return self._editBox:HasFocus() end --- Sets whether or not this input is focused. -- @tparam BaseInput self The input object -- @tparam boolean focused Whether or not this input is focused -- @treturn BaseInput The input object function BaseInput.SetFocused(self, focused) if focused then self._editBox:SetFocus() else self._editBox:ClearFocus() end return self end --- Clears the highlight. -- @tparam BaseInput self The input object -- @treturn BaseInput The input object function BaseInput.ClearHighlight(self) self._editBox:HighlightText(0, 0) return self end --- Set the maximum number of letters for the input's entered text. -- @tparam BaseInput self The input object -- @tparam number number The number of letters for entered text -- @treturn BaseInput The input object function BaseInput.SetMaxLetters(self, number) self._editBox:SetMaxLetters(number) return self end --- Gets the input value. -- @tparam BaseInput self The input object -- @treturn string The input value function BaseInput.GetValue(self) return self._ignoreEnter and self._value or strtrim(self._value) end --- Registers a script handler. -- @tparam BaseInput self The input object -- @tparam string script The script to register for -- @tparam[opt=nil] function handler The script handler which should be called -- @treturn BaseInput The element object function BaseInput.SetScript(self, script, handler) if script == "OnValueChanged" then self._onValueChangedHandler = handler elseif script == "OnEnterPressed" then self._onEnterPressedHandler = handler elseif script == "OnValidationChanged" then self._onValidationChangedHandler = handler elseif script == "OnFocusLost" then self._onFocusLostHandler = handler else error("Invalid base input script: "..tostring(script)) end return self end --- Sets the setting info. -- This method is used to have the value of the input automatically correspond with the value of a field in a table. -- This is useful for inputs which are tied directly to settings. -- @tparam BaseInput self The input object -- @tparam table tbl The table which the field to set belongs to -- @tparam string key The key into the table to be set based on the input state -- @treturn BaseInput The input object function BaseInput.SetSettingInfo(self, tbl, key) assert(self._value == "") self._settingTable = tbl self._settingKey = key self:SetValue(tbl[key]) return self end --- Get the current validation state. -- @tparam BaseInput self The input object -- @treturn boolean The current valiation state function BaseInput.IsValid(self) return self._isValid end --- Sets the input into paste mode for supporting the pasting of large strings. -- @tparam BaseInput self The input object -- @treturn BaseInput The input object function BaseInput.SetPasteMode(self) self._pasteMode = true ScriptWrapper.Clear(self._editBox, "OnTextChanged") self._editBox:SetMaxBytes(1) return self end function BaseInput.Draw(self) self.__super:Draw() self:_DrawBackgroundAndBorder() -- set the font self._editBox:SetFont(Theme.GetFont(self._font):GetWowFont()) -- set the justification self._editBox:SetJustifyH(self._justifyH) self._editBox:SetJustifyV(self._justifyV) -- set the text color self._editBox:SetTextColor(self:_GetTextColor():GetFractionalRGBA()) -- set the highlight color self._editBox:SetHighlightColor(Theme.GetColor("TEXT%HIGHLIGHT"):GetFractionalRGBA()) if not self._editBox:HasFocus() then -- set the text self._editBox:SetText(self._value) end end -- ============================================================================ -- Private Class Methods -- ============================================================================ function BaseInput._GetTextColor(self, tint) local color = Theme.GetColor(self._disabled and "PRIMARY_BG_ALT" or self._backgroundColor) -- the text color should have maximum contrast with the input color, so set it to white/black based on the input color if color:IsLight() then -- the input is light, so set the text to black return Color.GetFullBlack():GetTint(self._disabled and "-DISABLED" or tint or 0) else -- the input is dark, so set the text to white return Color.GetFullWhite():GetTint(self._disabled and "+DISABLED" or tint or 0) end end function BaseInput._SetValueHelper(self, value, noCallback) if not self._validateFunc or self:_validateFunc(strtrim(value), self._validateContext) then self._value = value if self._settingTable then if type(self._settingTable[self._settingKey]) == "number" then value = tonumber(value) assert(value) end self._settingTable[self._settingKey] = value end if not noCallback and self._onValueChangedHandler then self:_onValueChangedHandler() end if not self._isValid then self._isValid = true self:_DrawBackgroundAndBorder() if self._onValidationChangedHandler then self:_onValidationChangedHandler() end end return true else if self._isValid then self._isValid = false self:_DrawBackgroundAndBorder() if self._onValidationChangedHandler then self:_onValidationChangedHandler() end end return false end end function BaseInput._DrawBackgroundAndBorder(self) assert(self._backgroundColor) self._backgroundNineSlice:SetStyle("rounded", (self._borderColor or not self._isValid) and BORDER_THICKNESS or nil) self._backgroundNineSlice:SetVertexColor(Theme.GetColor(self._disabled and "PRIMARY_BG_ALT" or self._backgroundColor):GetFractionalRGBA()) if self._borderColor or not self._isValid then self._borderNineSlice:SetStyle("rounded") self._borderNineSlice:SetVertexColor((not self._isValid and Theme.GetFeedbackColor("RED") or Theme.GetColor(self._borderColor)):GetFractionalRGBA()) else self._borderNineSlice:Hide() end end function BaseInput._OnChar(self, c) -- can be overridden if not self._pasteMode then return end tinsert(self._pasteChars, c) ScriptWrapper.Set(self._editBox, "OnUpdate", private.OnUpdate, self) end function BaseInput._OnTextChanged(self, value) -- can be overridden end function BaseInput._ShouldKeepFocus(self) -- can be overridden return false end -- ============================================================================ -- Local Script Handlers -- ============================================================================ function private.OnEscapePressed(self) if self._escValue then self._value = self._escValue assert(self:_SetValueHelper(self._escValue)) end self:SetFocused(false) self:HighlightText(0, 0) self:Draw() end function private.OnTabPressed(self) local isValid, err = true, nil if self._validateFunc then local value = strtrim(self._editBox:GetText()) isValid, err = self:_validateFunc(value, self._validateContext) end if not isValid and err then -- TODO: better way to show the error message? Log.PrintUser(err) end self:SetFocused(false) self:HighlightText(0, 0) if self._tabPrevPath and IsShiftKeyDown() then self:GetElement(self._tabPrevPath):SetFocused(true) elseif self._tabNextPath and not IsShiftKeyDown() then self:GetElement(self._tabNextPath):SetFocused(true) end end function private.OnEnterPressed(self) local isValid, err = true, nil if self._validateFunc then local value = strtrim(self._editBox:GetText()) isValid, err = self:_validateFunc(value, self._validateContext) end if not isValid and err then -- TODO: better way to show the error message? Log.PrintUser(err) end if isValid then self:SetFocused(false) self:HighlightText(0, 0) if self._onEnterPressedHandler then self:_onEnterPressedHandler() end end end function private.OnEditFocusGained(self) Delay.Cancel(self._lostFocusDelayName) self:Draw() self:HighlightText() end function private.OnEditFocusLost(self) if self:_ShouldKeepFocus() then self:SetFocused(true) return end if self._isValid then self._escValue = self._value end self:HighlightText(0, 0) self:Draw() if not self._isValid then self._isValid = true self:_DrawBackgroundAndBorder() if self._onValidationChangedHandler then self:_onValidationChangedHandler() end end -- wait until the next frame before calling the handler Delay.AfterFrame(self._lostFocusDelayName, 0, private.OnFocusLost, nil, self) end function private.OnFocusLost(self) if self:HasFocus() then return end if self._onFocusLostHandler then self:_onFocusLostHandler() end end function private.OnTextChanged(self, isUserInput) if not isUserInput then return end local value = self._editBox:GetText() self:_SetValueHelper(value) self:_OnTextChanged(value) end function private.OnUpdate(self) ScriptWrapper.Clear(self._editBox, "OnUpdate") local value = table.concat(self._pasteChars) wipe(self._pasteChars) self:_SetValueHelper(value) self:_OnTextChanged(value) end -- ============================================================================ -- Built In Validate Functions -- ============================================================================ function private.CustomPriceValidateFunc(_, value, badSources) local isValid, err = CustomPrice.Validate(value, badSources) if not isValid then return false, L["Invalid custom price."].." "..err end return true end function private.NumberValidateFunc(input, value, range) local minValue, maxValue = strsplit(":", range) minValue = tonumber(minValue) maxValue = tonumber(maxValue) value = tonumber(value) if not value then return false, L["Invalid numeric value."] elseif value < minValue or value > maxValue then return false, format(L["Value must be between %d and %d."], minValue, maxValue) end return true end