Quantcast
-- Based off of AceGUI-3.0 EditBox
local AceGUI = LibStub("AceGUI-3.0")
do
	local Type = "Spell_EditBox"
	local Version = 2
	local PREDICTION_ROWS = 10
	local totalSpellsLoaded, spellLoader = 0
	local spells, indexedSpells, visiblePredicters = {}, {}, {}

	-- Defined blew
	local searchSpells

	-- Spells have to gradually be loaded in to prevent the client from lagging, this starts as soon as one widget is shown
	-- as of 3.1 the spellID goes up to ~66,000 which means it'll take around 5 - 10 seconds for it to load them all
	-- Given users have to actually move the mouse, type what they want etc
	-- it should result in them not noticing it does not have all the spell data yet
	local function startLoading()
		if( spellLoader ) then return end

		spellLoader = CreateFrame("Frame")
		spellLoader.timeElapsed = 0
		spellLoader.totalInvalid = 0
		spellLoader.index = 0
		spellLoader:SetScript("OnUpdate", function(self, elapsed)
			self.timeElapsed = self.timeElapsed + elapsed
			if( self.timeElapsed < 0.10 ) then return end
			self.timeElapsed = self.timeElapsed - 0.10

			-- Too many invalid spells found will assume we found all there is that we can
			if( self.totalInvalid >= 5000 ) then
				self:Hide()
				return
			end

			-- Load as many spells in
			local spellsLoaded = totalSpellsLoaded
			for i=spellLoader.index + 1, spellLoader.index + 500 do
				local name, _, icon = GetSpellInfo(i)

				-- The majority of spells that use the engineer gear icon are actually invalid spells that we can easily ignore
				-- since there are ~12000 not counting duplicate names that use this icon it's worthwhile to filter out these spells
				self.totalInvalid = self.totalInvalid + 1
				if( name and icon ~= "Interface\\Icons\\Trade_Engineering" ) then
					name = string.lower(name)

					if( not spells[name] ) then
						spells[string.lower(name)] = i
						table.insert(indexedSpells, name)

						totalSpellsLoaded = totalSpellsLoaded + 1
						self.totalInvalid = 0
					end
				end
			end

			-- Every ~1 second it will update any visible predicters to make up for the fact that the data is delay loaded
			if( spellLoader.index % 5000 == 0 ) then
				for predicter in pairs(visiblePredicters) do
					searchSpells(predicter, predicter.lastQuery)
				end
			end

			-- Increment and do it all over!
			spellLoader.index = spellLoader.index + 500
		end)
	end

	-- Search for spells quickly
	searchSpells = function(self, query)
		for _, button in pairs(self.buttons) do button:Hide() end

		local usedButtons = 0
		for i=1, totalSpellsLoaded do
			local name = indexedSpells[i]
			if( string.match(name, query) ) then
				usedButtons = usedButtons + 1

				local spellName, _, spellIcon = GetSpellInfo(spells[name])
				local button = self.buttons[usedButtons]
				button.spellID = spells[name]
				button:SetFormattedText("|T%s:20:20:2:11|t %s", spellIcon, spellName)
				button:Show()

				if( usedButtons ~= self.selectedButton ) then
					button:UnlockHighlight()

					if( GameTooltip:IsOwned(button) ) then
						GameTooltip:Hide()
					end
				end

				-- Ran out of text to suggest :<
				if( usedButtons >= PREDICTION_ROWS ) then break end
			end
		end

		if( usedButtons > 0 ) then
			self:SetHeight(15 + usedButtons * 17)
			self:Show()
		else
			self:Hide()
		end

		self.lastQuery = query
		self.usedButtons = usedButtons
	end

	local function OnAcquire(self)
		self:SetHeight(26)
		self:SetWidth(200)
		self:SetDisabled(false)
		self:SetLabel()
		self.showbutton = true
	end

	local function OnRelease(self)
		self.frame:ClearAllPoints()
		self.frame:Hide()
		self.predictFrame:Hide()

		self:SetDisabled(false)
	end

	local function Control_OnEnter(this)
		this.obj:Fire("OnEnter")
	end

	local function Control_OnLeave(this)
		this.obj:Fire("OnLeave")
	end

	local function EditBox_OnEscapePressed(this)
		this:ClearFocus()
	end

	local function ShowButton(self)
		if( self.lasttext ~= "" ) then
			self.editbox.predictFrame.selectedButton = nil
			searchSpells(self.editbox.predictFrame, "^" .. string.lower(self.lasttext))
		else
			self.editbox.predictFrame:Hide()
		end

		if( self.showbutton ) then
			self.button:Show()
			self.editbox:SetTextInsets(0,20,3,3)
		end
	end

	local function HideButton(self)
		self.button:Hide()
		self.editbox:SetTextInsets(0,0,3,3)
		self.editbox.predictFrame:Hide()
	end

	local function EditBox_OnEnterPressed(this)
		if( this.predictFrame.selectedButton ) then
			this.predictFrame.buttons[this.predictFrame.selectedButton]:Click()
			this.predictFrame.selectedButton = nil
			return
		end

		local self = this.obj
		local value = this:GetText()
		local cancel = self:Fire("OnEnterPressed", value)
		if( not cancel ) then
			HideButton(self)
		end

		-- Reactivate the cursor, odds are if you're adding auras you're adding multiple auras
		self.editbox:SetFocus()
	end

	local function Button_OnClick(this)
		local editbox = this.obj.editbox

		editbox:ClearFocus()
		EditBox_OnEnterPressed(editbox)
	end

	local function Predicter_OnHide(self)
		-- Allow users to use arrows to go back and forth again without the fix
		self.editbox:SetAltArrowKeyMode(false)

		visiblePredicters[self] = nil

		ClearOverrideBindings(self)
	end

	local function Predicter_OnShow(self)
		-- I'm pretty sure this is completely against what you are supposed to actually do :>
		visiblePredicters[self] = true

		-- User doesn't need arrow keys, and by doing this the override binding for up/down arrows will work properly
		self.editbox:SetAltArrowKeyMode(true)

		SetOverrideBindingClick(self, true, "DOWN", self:GetName(), 1)
		SetOverrideBindingClick(self, true, "UP", self:GetName(), -1)
		SetOverrideBindingClick(self, true, "LEFT", self:GetName(), "LEFT")
		SetOverrideBindingClick(self, true, "RIGHT", self:GetName(), "RIGHT")
	end

	-- When using SetAltArrowKeyMode the ability to move the cursor with left and right is disabled, this reenables that
	-- since the cursor position automatically can't go below 0, this is a quick and easy fix
	local function EditBox_FixCursorPosition(self, direction)
		self:SetCursorPosition(self:GetCursorPosition() + (direction == "RIGHT" and 1 or -1))
	end

	local function EditBox_OnReceiveDrag(this)
		local self = this.obj
		local type, id, info = GetCursorInfo()
		if( type == "spell" ) then
			local name = GetSpellName(id, info)
			self:SetText(name)
			self:Fire("OnEnterPressed" ,name)
			ClearCursor()
		end
		HideButton(self)
		AceGUI:ClearFocus()
	end

	local function EditBox_OnTextChanged(this)
		local self = this.obj
		local value = this:GetText()
		if( value ~= self.lasttext ) then
			self:Fire("OnTextChanged", value)
			self.lasttext = value
			ShowButton(self)
		end
	end

	local function EditBox_OnEditFocusLost(self)
		Predicter_OnHide(self.predictFrame)
	end

	local function EditBox_OnEditFocusGained(self)
		if( self.predictFrame:IsVisible() ) then
			Predicter_OnShow(self.predictFrame)
		end
	end

	local function SetDisabled(self, disabled)
		self.disabled = disabled
		if( disabled ) then
			self.editbox:EnableMouse(false)
			self.editbox:ClearFocus()
			self.editbox:SetTextColor(0.5, 0.5, 0.5)
			self.label:SetTextColor(0.5, 0.5, 0.5)
		else
			self.editbox:EnableMouse(true)
			self.editbox:SetTextColor(1, 1, 1)
			self.label:SetTextColor(1, 0.82, 0)
		end
	end

	local function SetText(self, text, cursor)
		self.lasttext = text or ""
		self.editbox:SetText(self.lasttext)
		self.editbox:SetCursorPosition(cursor or 0)

		HideButton(self)
	end

	local function SetLabel(self, text)
		if( text and text ~= "" ) then
			self.label:SetText(text)
			self.label:Show()
			self.editbox:SetPoint("TOPLEFT", self.frame, "TOPLEFT", 7, -18)
			self:SetHeight(44)
			self.alignoffset = 30
		else
			self.label:SetText("")
			self.label:Hide()
			self.editbox:SetPoint("TOPLEFT", self.frame, "TOPLEFT", 7, 0)
			self:SetHeight(26)
			self.alignoffset = 12
		end
	end

	local function Predicter_OnMouseDown(self, direction)
		if( direction == "LEFT" or direction == "RIGHT" ) then
			EditBox_FixCursorPosition(self.editbox, direction)
			return
		end

		self.selectedButton = (self.selectedButton or 0) + direction
		if( self.selectedButton > self.usedButtons ) then
			self.selectedButton = 1
		elseif( self.selectedButton <= 0 ) then
			self.selectedButton = self.usedButtons
		end

		for i=1, self.usedButtons do
			local button = self.buttons[i]
			if( i == self.selectedButton ) then
				button:LockHighlight()

				GameTooltip:SetOwner(button, "ANCHOR_BOTTOMRIGHT")
				GameTooltip:SetHyperlink("spell:" .. button.spellID)
			else
				button:UnlockHighlight()

				if( GameTooltip:IsOwned(button) ) then
					GameTooltip:Hide()
				end
			end
		end
	end

	local function Spell_OnClick(self)
		local name = GetSpellInfo(self.spellID)

		SetText(self.parent.obj, name, string.len(name))
		self.parent.obj:Fire("OnEnterPressed", name)
	end

	local function Spell_OnEnter(self)
		self.parent.selectedButton = nil
		self:LockHighlight()

		GameTooltip:SetOwner(self, "ANCHOR_BOTTOMRIGHT")
		GameTooltip:SetHyperlink("spell:" .. self.spellID)
	end

	local function Spell_OnLeave(self)
		self:UnlockHighlight()
		GameTooltip:Hide()
	end

	local predicterBackdrop = {
	  bgFile = "Interface\\ChatFrame\\ChatFrameBackground",
	  edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
	  edgeSize = 26,
	  insets = {left = 9, right = 9, top = 9, bottom = 9},
	}

	local function Constructor()
		local num  = AceGUI:GetNextWidgetNum(Type)
		local frame = CreateFrame("Frame", nil, UIParent)
		local editbox = CreateFrame("EditBox", "AceGUI30SpellEditBox" .. num, frame, "InputBoxTemplate")

		-- Don't feel like looking up the specific callbacks for when a widget resizes, so going to be creative with SetPoint instead!
		local predictFrame = CreateFrame("Frame", "AceGUI30SpellEditBox" .. num .. "Predicter", UIParent)
		predictFrame:SetBackdrop(predicterBackdrop)
		predictFrame:SetBackdropColor(0, 0, 0, 0.85)
		predictFrame:SetWidth(1)
		predictFrame:SetHeight(150)
		predictFrame:SetPoint("TOPLEFT", editbox, "BOTTOMLEFT", -6, 0)
		predictFrame:SetPoint("TOPRIGHT", editbox, "BOTTOMRIGHT", 0, 0)
		predictFrame:SetFrameStrata("TOOLTIP")
		predictFrame:Hide()

		predictFrame.buttons = {}

		for i=1, PREDICTION_ROWS do
			local button = CreateFrame("Button", nil, predictFrame)
			button:SetHeight(17)
			button:SetWidth(1)
			button:SetPushedTextOffset(-2, 0)
			button:SetScript("OnClick", Spell_OnClick)
			button:SetScript("OnEnter", Spell_OnEnter)
			button:SetScript("OnLeave", Spell_OnLeave)
			button.parent = predictFrame
			button.editbox = editbox
			button:Hide()

			if( i > 1 ) then
				button:SetPoint("TOPLEFT", predictFrame.buttons[i - 1], "BOTTOMLEFT", 0, 0)
				button:SetPoint("TOPRIGHT", predictFrame.buttons[i - 1], "BOTTOMRIGHT", 0, 0)
			else
				button:SetPoint("TOPLEFT", predictFrame, 8, -8)
				button:SetPoint("TOPRIGHT", predictFrame, -7, 0)
			end

			-- Create the actual text
			local text = button:CreateFontString(nil, "ARTWORK", "GameFontNormal")
			text:SetHeight(1)
			text:SetWidth(1)
			text:SetJustifyH("LEFT")
			text:SetAllPoints(button)
			button:SetFontString(text)

			-- Setup the highlighting
			local texture = button:CreateTexture(nil, "ARTWORK")
			texture:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
			texture:ClearAllPoints()
			texture:SetPoint("TOPLEFT", button, 0, -2)
			texture:SetPoint("BOTTOMRIGHT", button, 5, 2)
			texture:SetAlpha(0.70)

			button:SetHighlightTexture(texture)
			button:SetHighlightFontObject(GameFontHighlight)
			button:SetNormalFontObject(GameFontNormal)

			table.insert(predictFrame.buttons, button)
		end

		local self = {}
		self.type = Type
		self.num = num

		self.OnRelease = OnRelease
		self.OnAcquire = OnAcquire

		self.SetDisabled = SetDisabled
		self.SetText = SetText
		self.SetLabel = SetLabel

		frame.obj = self
		self.frame = frame

		editbox.obj = self
		editbox.predictFrame = predictFrame
		self.editbox = editbox

		self.predictFrame = predictFrame
		predictFrame.editbox = editbox
		predictFrame.obj = self

		self.alignoffset = 30

		frame:SetHeight(44)
		frame:SetWidth(200)

		-- Despite the fact that wowprogramming/wowwiki say EditBoxes have OnKeyUp/OnKeyDown thats not actually true
		-- so doing some trickery with bindings and such to make navigation work
		predictFrame:SetScript("OnMouseDown", Predicter_OnMouseDown)
		predictFrame:SetScript("OnHide", Predicter_OnHide)
		predictFrame:SetScript("OnShow", Predicter_OnShow)

		editbox:SetScript("OnShow", startLoading)
		editbox:SetScript("OnEnter", Control_OnEnter)
		editbox:SetScript("OnLeave", Control_OnLeave)

		editbox:SetAutoFocus(false)
		editbox:SetFontObject(ChatFontNormal)
		editbox:SetScript("OnEscapePressed", EditBox_OnEscapePressed)
		editbox:SetScript("OnEnterPressed", EditBox_OnEnterPressed)
		editbox:SetScript("OnTextChanged", EditBox_OnTextChanged)
		editbox:SetScript("OnReceiveDrag", EditBox_OnReceiveDrag)
		editbox:SetScript("OnMouseDown", EditBox_OnReceiveDrag)
		editbox:SetScript("OnEditFocusGained", EditBox_OnEditFocusGained)
		editbox:SetScript("OnEditFocusLost", EditBox_OnEditFocusLost)

		editbox:SetTextInsets(0, 0, 3, 3)
		editbox:SetMaxLetters(256)

		editbox:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 6, 0)
		editbox:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 0)
		editbox:SetHeight(19)

		local label = frame:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
		label:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, -2)
		label:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, -2)
		label:SetJustifyH("LEFT")
		label:SetHeight(18)
		self.label = label

		local button = CreateFrame("Button",nil,editbox,"UIPanelButtonTemplate")
		button:SetWidth(40)
		button:SetHeight(20)
		button:SetPoint("RIGHT", editbox, "RIGHT", -2, 0)
		button:SetText(OKAY)
		button:SetScript("OnClick", Button_OnClick)
		button:Hide()

		self.button = button
		button.obj = self

		AceGUI:RegisterAsWidget(self)
		return self
	end

	AceGUI:RegisterWidgetType(Type, Constructor, Version)
end