local db = select(2, ...) --------------------------------------------------------------- -- Indexed widget pool --------------------------------------------------------------- CPIndexPoolMixin = {}; function CPIndexPoolMixin:OnLoad() self.Registry = {}; end function CPIndexPoolMixin:CreateFramePool(type, template, mixin, resetterFunc, parent) assert(not self.FramePool, 'Frame pool already exists.') self.FramePool = CreateFramePool(type, parent or self, template, resetterFunc) self.FramePoolMixin = mixin; return self.FramePool; end function CPIndexPoolMixin:Acquire(index) local widget, newObj = self.FramePool:Acquire() if newObj then Mixin(widget, self.FramePoolMixin) end self.Registry[index] = widget; return widget, newObj; end function CPIndexPoolMixin:TryAcquireRegistered(index) local widget = self.Registry[index]; if widget then local pool = self.FramePool; local inactiveIndex = tIndexOf(pool.inactiveObjects, widget) if inactiveIndex then pool.activeObjects[tremove(pool.inactiveObjects, inactiveIndex)] = true; pool.numActiveObjects = pool.numActiveObjects + 1; end return widget; end return self:Acquire(index) end function CPIndexPoolMixin:GetObjectByIndex(index) return self.Registry[index]; end function CPIndexPoolMixin:EnumerateActive() return self.FramePool:EnumerateActive() end function CPIndexPoolMixin:GetNumActive() return self.FramePool:GetNumActive() end function CPIndexPoolMixin:ReleaseAll() self.FramePool:ReleaseAll() end --------------------------------------------------------------- -- Trackable widget pool --------------------------------------------------------------- CPFocusPoolMixin = CreateFromMixins(CPIndexPoolMixin); function CPFocusPoolMixin:OnPostHide() self.focusIndex = nil; end function CPFocusPoolMixin:GetFocusIndex() return self.focusIndex end function CPFocusPoolMixin:GetFocusWidget() return self.focusIndex and self.Registry[self.focusIndex] end function CPFocusPoolMixin:SetFocusByIndex(index) local old = self.focusIndex ~= index and self.focusIndex; self.focusIndex = index; local oldObj = old and self.Registry[old]; local newObj = self.Registry[index]; return newObj, oldObj; end function CPFocusPoolMixin:SetFocusByWidget(widget) return self:SetFocusByIndex(tIndexOf(self.Registry, widget)) end --------------------------------------------------------------- -- Gradient mixin --------------------------------------------------------------- CPGradientMixin = {}; function CPGradientMixin:OnLoad() self.VertexColor = CPAPI.GetClassColorObject() self.VertexValid = CreateColor(1, .81, 0, 1) self.VertexOrient = 'VERTICAL'; end function CPGradientMixin:SetGradientDirection(direction) assert(direction == 'VERTICAL' or direction == 'HORIZONTAL', 'Valid: VERTICAL, HORIZONTAL') self.VertexOrient = direction; end function CPGradientMixin:GetClassColor() return self.VertexColor:GetRGB() end function CPGradientMixin:GetValidColor() return self.VertexColor:GetRGB() end function CPGradientMixin:GetMixGradient(...) return CPAPI.GetReverseMixColorGradient(self.VertexOrient, ...) end function CPGradientMixin:GetReverseMixGradient(...) return CPAPI.GetMixColorGradient(self.VertexOrient, ...) end function CPGradientMixin:GetFadeGradient(...) return self.VertexOrient, 1, 1, 1, 0, ...; end --------------------------------------------------------------- -- Frame background --------------------------------------------------------------- CPBackgroundMixin = CreateFromMixins(BackdropTemplateMixin); function CPBackgroundMixin:OnLoad() local r, g, b = CPAPI.GetWebColor(CPAPI.GetClassFile()):GetRGB() self:HookScript('OnSizeChanged', self.OnBackdropSizeChanged) self.Background = self:CreateTexture(nil, 'BACKGROUND', nil, 2) self.Rollover = self:CreateTexture(nil, 'BACKGROUND', nil, 3) self.Rollover:SetAllPoints(self.Background) self.Rollover:SetTexture(CPAPI.GetAsset([[Textures\Frame\Backdrop_Vertex_White]])) self.Rollover:SetGradientAlpha('VERTICAL', r*0.5, g*0.5, b*0.5, 1, r*0.5, g*0.5, b*0.5, 0) self:SetOriginTop(true) self:CreateBackground(2048, 2048, 2048, 2048, CPAPI.GetAsset([[Art\Background\%s]]):format(CPAPI.GetClassFile())) end function CPBackgroundMixin:GetBGOffset(point, size) return ((point / 2) / size) end function CPBackgroundMixin:GetBGFraction(point, size) return (point / size) end function CPBackgroundMixin:SetBackgroundDimensions(w, h, x, y) assert(self.Background, 'Frame is missing background.') self.Background.maxWidth = w; self.Background.maxHeight = h; self.Background.sizeX = x; self.Background.sizeY = y; end function CPBackgroundMixin:SetOriginTop(enabled) self.originTop = enabled; end function CPBackgroundMixin:OnAspectRatioChanged() local maxWidth, maxHeight = self.Background.maxWidth, self.Background.maxHeight; local sizeX, sizeY = self.Background.sizeX, self.Background.sizeY; local width, height = self:GetSize() local maxCoordX, maxCoordY, centerCoordX, centerCoordY = self:GetBGFraction(maxWidth, sizeX), self:GetBGFraction(maxHeight, sizeY), self:GetBGOffset(maxWidth, sizeX), self:GetBGOffset(maxHeight, sizeY); local top, bottom, left, right = 0, 1, 0, 1; if width > height then local newHeight = self:GetBGFraction(height, width) * maxWidth; if self.originTop then top, left, right = 0, 0, maxCoordX; bottom = self:GetBGFraction(newHeight, sizeY) else local offset = self:GetBGOffset(newHeight, sizeY) left, right = 0, maxCoordX; top = centerCoordY - offset; bottom = centerCoordY + offset; end end if height > width or (top < 0 or bottom < 0) then local newWidth = self:GetBGFraction(width, height) * maxHeight; local offset = self:GetBGOffset(newWidth, sizeX) top, bottom = 0, maxCoordY; left = centerCoordX - offset; right = centerCoordX + offset; end self.Background:SetTexCoord(left, right, top, bottom) end function CPBackgroundMixin:SetBackgroundInsets(enabled, value) self.Background:ClearAllPoints() if enabled then local inset = value or 8; self.Background:SetPoint('TOPLEFT', inset, -inset) self.Background:SetPoint('BOTTOMRIGHT', -inset, inset) else self.Background:SetAllPoints() end end function CPBackgroundMixin:CreateBackground(w, h, x, y, texture) self.Background:SetTexture(texture) self:SetBackgroundDimensions(w, h, x, y) self:OnAspectRatioChanged() self:HookScript('OnShow', self.OnAspectRatioChanged) self:HookScript('OnSizeChanged', self.OnAspectRatioChanged) end function CPBackgroundMixin:SetBackgroundVertexColor(...) self.Background:SetVertexColor(...) end --------------------------------------------------------------- -- Ambience mixin --------------------------------------------------------------- CPAmbienceMixin = { soundKitOnShow = SOUNDKIT.UI_ADVENTURES_ADVENTURER_LEVEL_UP; soundVars = { Sound_EnableSFX = false; Sound_EnableMusic = false; Sound_EnableDialog = false; Sound_EnableAmbience = false; ----------------------------- Sound_MasterVolume = false; Sound_AmbienceVolume = false; }; soundKits = { WARRIOR = SOUNDKIT.AMB_GLUESCREEN_BATTLE_FOR_AZEROTH; HUNTER = SOUNDKIT.AMB_GLUESCREEN_NIGHTELF; MAGE = SOUNDKIT.AMB_GLUESCREEN_VOIDELF; ROGUE = SOUNDKIT.AMB_GLUESCREEN_DARKIRONDWARF; PRIEST = SOUNDKIT.AMB_GLUESCREEN_DWARF; WARLOCK = SOUNDKIT.AMB_GLUESCREEN_LEGION; PALADIN = SOUNDKIT.AMB_GLUESCREEN_LIGHTFORGEDDRAENEI; DRUID = SOUNDKIT.AMB_GLUESCREEN_WARLORDS_OF_DRAENOR; SHAMAN = SOUNDKIT.AMB_GLUESCREEN_DRAENEI; MONK = SOUNDKIT.AMB_GLUESCREEN_PANDAREN; DEMONHUNTER = SOUNDKIT.AMB_GLUESCREEN_DEMONHUNTER; DEATHKNIGHT = SOUNDKIT.AMB_GLUESCREEN_DEATHKNIGHT; }; } function CPAmbienceMixin:OnLoad() self:HookScript('OnShow', self.PlayAmbience) self:HookScript('OnHide', self.StopAmbience) end function CPAmbienceMixin:PlayAmbience() PlaySound(self.soundKitOnShow, 'Master', true) if db('disableAmbientFrames') then return end; local playFileID = self.soundKits[CPAPI.GetClassFile()] if playFileID and GetCVarBool('Sound_EnableAmbience') then local willPlay, handle = PlaySound(playFileID, 'Master', true) if willPlay then local volume = GetCVar('Sound_AmbienceVolume') self.isPlayingAmbience = handle; for var in pairs(self.soundVars) do self.soundVars[var] = GetCVar(var) SetCVar(var, 0) end SetCVar('Sound_MasterVolume', volume) end end end function CPAmbienceMixin:StopAmbience() if self.isPlayingAmbience then StopSound(self.isPlayingAmbience, 1000) self.isPlayingAmbience = nil; for var, val in pairs(self.soundVars) do SetCVar(var, val) end end end --------------------------------------------------------------- -- Smooth scroll --------------------------------------------------------------- do local Scroller, Clamp = CreateFrame('Frame'), Clamp; Scroller.Frames = {}; function Scroller:OnUpdate(elapsed) local current, delta, target, range; for frame, data in pairs(self.Frames) do range = frame:GetRange() current = frame:GetScroll() if abs(current - data.targetPos) < 1 then frame:SetScroll(data.targetPos) self:RemoveFrame(frame) else delta = current > data.targetPos and -1 or 1; target = current + (delta * abs(current - data.targetPos) / data.stepSize * 8) frame:SetScroll(Clamp(target, 0, range)); end end end function Scroller:AddFrame(frame, targetPos, stepSize) self.Frames[frame] = { targetPos = targetPos; stepSize = stepSize; }; self:SetScript('OnUpdate', self.OnUpdate) end function Scroller:RemoveFrame(frame) self.Frames[frame] = nil; if not next(self.Frames) then self:SetScript('OnUpdate', nil) end return frame.OnScrollFinished and frame:OnScrollFinished() end ----------------------------------------------------------- -- Mixin ----------------------------------------------------------- CPSmoothScrollMixin = { MouseWheelDelta = 100; Horizontal = { GetRange = 'GetHorizontalScrollRange'; GetScroll = 'GetHorizontalScroll'; SetScroll = 'SetHorizontalScroll'; GetAnchor = 'GetLeft'; }; Vertical = { GetRange = 'GetVerticalScrollRange'; GetScroll = 'GetVerticalScroll'; SetScroll = 'SetVerticalScroll'; GetAnchor = 'GetTop'; }; } function CPSmoothScrollMixin:SetScrollOrientation(orient) assert(self[orient], 'Orientation must be either Horizontal or Vertical.') self.ScrollOrientationSet = true; for alias, metaname in pairs(self[orient]) do self[alias] = self[metaname] end end function CPSmoothScrollMixin:SetDelta(delta) self.MouseWheelDelta = delta; end function CPSmoothScrollMixin:OnScrollMouseWheel(delta) local range = self:GetRange() local current = self.targetPos or self:GetScroll() local new = current - delta * self.MouseWheelDelta; Scroller:AddFrame(self, Clamp(new, 0, range), self.MouseWheelDelta) end function CPSmoothScrollMixin:ScrollTo(frac, steps) local range = self:GetRange() local step = range / steps; local new = frac <= 0 and 0 or frac >= steps and range or step * (frac - 1); local target = Clamp(new, 0, range) Scroller:AddFrame(self, target, step) if range > 0 then return target / range; end end function CPSmoothScrollMixin:GetElementPosition(element) local wrapper = self:GetScrollChild(); return ClampedPercentageBetween(select(2, element:GetCenter()), wrapper:GetTop(), wrapper:GetBottom()) end function CPSmoothScrollMixin:GetElementOffsetFromAnchor(element, padding) local getAnchor = self.GetAnchor; return getAnchor(self) - getAnchor(element) + self:GetScroll() + (padding or 0); end function CPSmoothScrollMixin:ScrollToOffset(offset) Scroller:AddFrame(self, offset * self:GetRange(), self.MouseWheelDelta) end function CPSmoothScrollMixin:ScrollToPosition(position) Scroller:AddFrame(self, position, self.MouseWheelDelta) end function CPSmoothScrollMixin:ScrollToElement(element, padding) Scroller:AddFrame(self, Clamp(self:GetElementOffsetFromAnchor(element, padding), 0, self:GetRange()), self.MouseWheelDelta) end function CPSmoothScrollMixin:OnScrollSizeChanged(...) if not self.ScrollOrientationSet then return end; if (self:GetScroll() > self:GetRange()) then self:SetScroll(self:GetRange()) end end end --------------------------------------------------------------- -- Specific button catcher with callbacks --------------------------------------------------------------- do local ModListen = CreateFrame('Frame'); ModListen.Listeners = {}; function ModListen:OnModifierStateChanged(_, key, down) if (down == 1) then for _, modifier in db:For('Gamepad/Modsims') do if key:match(modifier) then self:TriggerModifier(modifier) end end end end function ModListen:TriggerModifier(modifier) for signature, data in pairs(self.Listeners) do if (data.modifier == modifier) then data.frame:OnGamePadButtonDown(data.emulated) end end end function ModListen:GetSignature(frame, modifier) return tostring(frame) .. tostring(modifier); end function ModListen:TryIdle() if not next(self.Listeners) then self:UnregisterEvent('MODIFIER_STATE_CHANGED') self:SetScript('OnEvent', nil) end end function ModListen:RegisterClosure(frame, modifier) self.Listeners[self:GetSignature(frame, modifier)] = { frame = frame; modifier = modifier; emulated = GetCVar('GamepadEmulate'..modifier); }; self:RegisterEvent('MODIFIER_STATE_CHANGED') self:SetScript('OnEvent', self.OnModifierStateChanged) end function ModListen:RemoveClosure(frame, modifier) self.Listeners[self:GetSignature(frame, modifier)] = nil; self:TryIdle() end function ModListen:RemoveFrame(frame) local partial = tostring(frame); local signature = next(self.Listeners) while signature do if signature:match(partial) then self.Listeners[signature] = nil; signature = nil; end signature = next(self.Listeners, signature) end self:TryIdle() end ----------------------------------------------------------- -- Mixin ----------------------------------------------------------- CPButtonCatcherMixin = {}; function CPButtonCatcherMixin:OnLoad() self.ClosureRegistry = {}; end function CPButtonCatcherMixin:OnGamePadButtonDown(button) if not self.catcherPaused then if self.catchAllCallback and self:IsButtonValid(button) then self.catchAllCallback(button) return self:SetPropagateKeyboardInput(false) elseif self.ClosureRegistry[button] then self.ClosureRegistry[button](button) return self:SetPropagateKeyboardInput(false) end end self:SetPropagateKeyboardInput(true) end function CPButtonCatcherMixin:OnHide() self:ReleaseClosures() end function CPButtonCatcherMixin:CatchAll(callback, ...) self.catchAllCallback = GenerateClosure(callback, ...) for _, modifier in db:For('Gamepad/Modsims') do ModListen:RegisterClosure(self, modifier) end end function CPButtonCatcherMixin:CatchButton(button, callback, ...) local closure = GenerateClosure(callback, ...) self.ClosureRegistry[button] = closure; local modifier = db.Gamepad:GetActiveModifier(button) if modifier then ModListen:RegisterClosure(self, modifier) end self:EnableGamePadButton(true) return closure; -- return the event owner end function CPButtonCatcherMixin:FreeButton(button, ...) if select('#', ...) > 0 then local closure = ...; if closure and (self.ClosureRegistry[button] ~= closure) then return false; -- assert event owner if supplied end end if db.Gamepad:GetActiveModifier(button) then ModListen:RemoveClosure(self, button) end self.ClosureRegistry[button] = nil; if not next(self.ClosureRegistry) then self:EnableGamePadButton(false) end return true; end function CPButtonCatcherMixin:PauseCatcher() self.catcherPaused = true; end function CPButtonCatcherMixin:ResumeCatcher() self.catcherPaused = false; end function CPButtonCatcherMixin:ReleaseClosures() ModListen:RemoveFrame(self) self.catchAllCallback = nil; self:EnableGamePadButton(false) if self.ClosureRegistry then wipe(self.ClosureRegistry) end end function CPButtonCatcherMixin:IsButtonValid(button) return CPAPI.IsButtonValidForBinding(button) end end