--[[ This module handles the behavior of "cards" which includes pet cards, notes and win records. Preferences and possibly team cards will become cards in the future. These cards are hybrid tooltips and dialogs. When the mouse is over a button that displays a card, the card will behave like a tooltip: moving the mouse off the button will hide the card. When the card-displaying button is clicked, a dialog frame appears around the card and it remains locked on the screen. Moving the mouse off the button will not hide the card, so the user can mouseover and interact with the card's contents as if it was a dialog. To use this card manager: - Cards should inherit "RematchCardManagerTemplate". This adds a child frame (hidden by default) with the "locked" border frame to surround the card. - Cards should be movable, clampedToScreen and FULLSCREEN frameStrata - All clickable child frames (like stats on the pet card) should be defined prior to registering the card, when it gathers all clickableElements. - Each card should be registered with a name via RegisterCard: rematch:RegisterCard(cardName,frame,showFunc,title,isFloating) - cardName is a unique string to identify this card ("PetCard", "WinRecord", etc) - frame is the parent that inherited RematchCardManagerTemplate - showFunc a function for each card to ShowCard(self,cardName,button,subject,forceLock) - title is the text to display at the top of the card - isFloating is true when a card is never pinned/anchored to an origin button - rematch:ShowCard(cardName,etc) can be called directly too. - Each button that displays a card should run these in their OnEnter/OnLeave/OnClick: rematch.CardManager.ButtonOnEnter(self,cardName,subject) rematch.CardManager.ButtonOnLeave(self,cardName) rematch.CardManager.ButtonOnClick(self,cardName,subject,highlightFunc) - self is the button being entered/left/clicked - cardName is the name of the card as it was registered - subject is a petID or teamID the showFunc will use to fill the card - (optional) highlightFunc runs when a lock changes func(origin/self,subject) To show a card: - Let the OnEnter/OnLeave/OnClick do its thing. It requires no extra work. - To directly force a card: rematch:ShowCard(cardName,UIParent/button,subject,true) To hide a card: - rematch:HideCard(cardName) to hide a specific card - rematch:HideCard() will hide all cards - Never directly card:Hide()! It will break the card! Each card has these in RematchSettings: XPos: center x coordinate of the card (when not anchored to origin) YPos: center y coordinate of the card (when not anchored to origin) IsPinned: true if the card is presently pinned (not whether pinnable) IsLocked: true if the card is presently locked and can't be moved ]] local _,L = ... local rematch = Rematch rematch.CardManager = {} local manager = rematch.CardManager local settings local cardProperties = {} -- indexed by card name, the properties of the card being managed local cardNamesByFrame = {} -- indexed by frame, the card name registered for the frame local delay = 0.5 -- seconds before card is shown on mouseover with default options function rematch:RegisterCard(cardName,frame,showFunc,title,isFloating) cardProperties[cardName] = {frame=frame,showFunc=showFunc,isFloating=isFloating,clickableElements={}} cardNamesByFrame[frame] = cardName -- setup lockframe frame.LockFrame.CloseButton:SetScript("OnClick",manager.CloseButtonOnClick) frame.LockFrame.CloseButton:SetScript("OnKeyDown",manager.CloseButtonOnKeyDown) frame.LockFrame.TitleText:SetText(title) frame.LockFrame.PinButton.tooltipTitle = isFloating and L["Toggle Lock"] or L["Unpin Card"] frame.LockFrame.PinButton.tooltipBody = isFloating and L["While locked, this card cannot be moved unless Shift is held. It will also remain on screen when ESC is pressed."] or L["While pinned, this card will stay wherever you move it.\n\nClick this to unpin the card and snap it back to the button that spawned it."] rematch:ConvertTitlebarCloseButton(frame.LockFrame.CloseButton) -- if card doesn't have a saved position, then set a default to center of screen if not settings[cardName.."XPos"] then settings[cardName.."XPos"],settings[cardName.."YPos"] = UIParent:GetCenter() end -- discover all clickable elements of a card and save them in its clickableElements manager:AddClickableElements(cardName,frame) end -- can be called from outside this module; cardName has to have been registered -- origin is the frame/button the card is to attach to -- subject is the petID, teamID, etc to show on the card -- forceLock is true if the card should be locked (LockFrame shown) function rematch:ShowCard(cardName,origin,subject,forceLock) local card = cardProperties[cardName] if card then if forceLock then -- any card being forced on the screen is locked card.locked = true end -- save origin/subject to the card card.origin = origin card.subject = subject -- run its showFunc card.showFunc(card.frame,subject) -- handle LockFrame card.frame.LockFrame:SetShown(card.locked) -- update button in topleft corner manager:UpdatePinButton(cardName) -- update whether clickable elements can be clicked through (unlocked cards cannot capture mouse) manager:UpdateClickableElements(cardName,card.locked and true) -- position the card; for pinned cards (or static) anchor card to its saved XPos/YPos if card.isFloating or (settings.FixedPetCard and settings[cardName.."IsPinned"]) then card.frame:ClearAllPoints() card.frame:SetPoint("CENTER",UIParent,"BOTTOMLEFT",settings[cardName.."XPos"],settings[cardName.."YPos"]) else -- otherwise anchor it to origin button rematch:SmartAnchor(card.frame,origin) end -- and finally put it on screen card.frame:Show() end end -- this will update an existing card, likely due to some change function rematch:UpdateCard(cardName) local card = cardProperties[cardName] if card.origin and card.subject then rematch:ShowCard(cardName,card.origin,card.subject) end end -- this should be called instead of card:Hide() to hide a card -- if no cardName is given, all cards will be hidden function rematch:HideCard(cardName) if cardName then rematch:StopTimer(cardName) local card = cardProperties[cardName] if card then card.frame:Hide() if card.origin then card.origin = nil card.subject = nil card.locked = nil end end else -- if a card isn't given, hide them all for cardName in pairs(cardProperties) do rematch:HideCard(cardName) end end end -- adds frame and all children of frame to a card's clickableElements if they can be clicked function manager:AddClickableElements(cardName,frame) if frame:IsMouseEnabled() then tinsert(cardProperties[cardName].clickableElements,frame) end for _,child in pairs({frame:GetChildren()}) do if child:IsMouseEnabled() then manager:AddClickableElements(cardName,child) end end end -- goes through all clickableElements in a card and enables mouse if true, disables if false -- (when an unlocked card is overlapping a button, the card can't capture the mouse or it -- immediately triggers an OnLeave of the button) function manager:UpdateClickableElements(cardName,enable) local card = cardProperties[cardName] if card then local clickableElements = card.clickableElements for i=1,#clickableElements do clickableElements[i]:EnableMouse(enable) end end end --[[ LockFrame ]] -- when a card's LockFrame is shown, make sure the parent card is a higher frameLevel function manager:LockFrameOnShow() local parent = self:GetParent() local parentFrameLevel = parent and parent:GetFrameLevel() local selfFrameLevel = self:GetFrameLevel() if parentFrameLevel and parentFrameLevel<=selfFrameLevel then parent:SetFrameLevel(selfFrameLevel) self:SetFrameLevel(parentFrameLevel) end end -- when LockFrame is dragged it drags its parent function manager:LockFrameOnMouseDown() local cardName = cardNamesByFrame[self:GetParent()] local card = cardProperties[cardName] if card then -- only drag the card if it's not a static card or it's not locked if not card.isFloating or not settings[cardName.."IsLocked"] then self:GetParent():StartMoving() end end end -- when LockFrame is released it stops dragging its parent function manager:LockFrameOnMouseUp() local parent = self:GetParent() parent:StopMovingOrSizing() local cardName = cardNamesByFrame[parent] local card = cardProperties[cardName] -- if card is pinnable and "Allow Cards To Be Pinned" is enabled if card and (card.isFloating or settings.FixedPetCard) then settings[cardName.."IsPinned"] = true settings[cardName.."XPos"],settings[cardName.."YPos"] = parent:GetCenter() manager:UpdatePinButton(cardName) end end -- click of the close button on the LockFrame function manager:CloseButtonOnClick() rematch:HideCard(cardNamesByFrame[self:GetParent():GetParent()]) end -- while lock frame is on screen, the close button will watch for an ESC function manager:CloseButtonOnKeyDown(key) if key==GetBindingKey("TOGGLEGAMEMENU") then local cardName = cardNamesByFrame[self:GetParent():GetParent()] local card = cardProperties[cardName] if card and (not card.isFloating or not settings[cardName.."IsLocked"]) then rematch:HideCard(cardNamesByFrame[self:GetParent():GetParent()]) self:SetPropagateKeyboardInput(false) return end end self:SetPropagateKeyboardInput(true) end --[[ Origin button's OnEnter/Leave/Click ]] -- buttons that show a card should use in their OnEnter -- subject is the petID, teamID, etc of the card to show function manager:ButtonOnEnter(cardName,subject) if settings.ClickPetCard then return end local card = cardProperties[cardName] -- don't do anything if card is already locked on screen (or a dialog/menu just went away) if card.locked or rematch:UIJustChanged() then return end card.origin = self card.subject = subject if settings.FastPetCard then -- if "Faster Pet Card" enabled, show card immediately rematch:ShowCard(cardName,self,subject) else -- otherwise wait a short delay before showing card rematch:StartTimer(cardName,delay,manager.DelayedButtonOnEnter) end end -- called after delay via ButtonOnEnter, actually shows the card function manager:DelayedButtonOnEnter() local cardName = rematch:GetTimerStopped() -- get name of timer that triggered this function local card = cardProperties[cardName] rematch:ShowCard(cardName,card.origin,card.subject) end -- buttons that show a card should use this in their OnLeave function manager:ButtonOnLeave(cardName) if settings.ClickPetCard then return end local card = cardProperties[cardName] if not card.locked then rematch:HideCard(cardName) end end -- this should be called after the button checked if any other clicks (right-click, -- shift+click, etc) needed handling and only if they weren't handled. function manager:ButtonOnClick(cardName,subject,highlightFunc) local card = cardProperties[cardName] if card.frame:IsVisible() then -- if the opened subject is being clicked, toggle its lock state if card.origin==self and card.subject==subject then if settings.ClickPetCard then rematch:HideCard(cardName) elseif card.locked then -- card locked on same origin/subject, unlock it card.locked = nil card.frame.LockFrame:Hide() manager:UpdateClickableElements(cardName,false) else -- card unlocked on same origin/subject, lock it card.locked = true card.frame.LockFrame:Show() manager:UpdateClickableElements(cardName,true) end else -- if a different subject is clicked, open new subject and lock for this click card.locked = true rematch:ShowCard(cardName,self,subject) end else -- card is not visible (clicked before delay over or ClickToOpen) rematch:StopTimer(cardName) card.locked = true rematch:ShowCard(cardName,self,subject) end end -- returns the subject and origin of the shown card function manager:GetCardInfo(cardName) local card = cardProperties[cardName] if card then return card.subject,card.origin end end --[[ Pinning/Locking ]] -- updates the state of the the pin/lock button in the topleft corner of the card function manager:UpdatePinButton(cardName) local card = cardProperties[cardName] if card then local pinButton = card.frame.LockFrame.PinButton if card.isFloating then local isLocked = settings[cardName.."IsLocked"] rematch:SetTitlebarButtonIcon(pinButton,isLocked and "lock" or "unlock") pinButton:Show() elseif settings.FixedPetCard and settings[cardName.."IsPinned"] then rematch:SetTitlebarButtonIcon(pinButton,"pin") pinButton:Show() else pinButton:Hide() end end end -- when pin/lock button is clicked on the lock frame function manager:PinButtonOnClick() local cardName = cardNamesByFrame[self:GetParent():GetParent()] local card = cardProperties[cardName] if card.isFloating then settings[cardName.."IsLocked"] = not settings[cardName.."IsLocked"] manager:UpdatePinButton(cardName) else settings[cardName.."IsPinned"] = nil rematch:UpdateCard(cardName) end end --[[ CardTest ]] local testData = {} local function fillTestCard(self,subject) local petInfo = rematch.petInfo:Fetch(subject) self.Text:SetText(petInfo.name) self.Icon:SetNormalTexture(petInfo.icon) end local function fillTestCardListButton(self,subject) local petInfo = rematch.petInfo:Fetch(subject) self.Pet.Icon:SetTexture(petInfo.icon) self.Text:SetText(petInfo.name) self.petID = subject self:UnlockHighlight() end rematch:InitModule(function() settings = RematchSettings -- rematch:RegisterCard("CardTest",CardTestCard,function(self,subject) self.Text:SetText(subject) self:Show() end,"Card Test","pin") rematch:RegisterCard("CardTest",CardTestCard,fillTestCard,"Card Test",true) local scrollFrame = AutoScrollFrame:Create(CardTest, "CardTestListButtonTemplate", testData, fillTestCardListButton) scrollFrame:SetPoint("TOPLEFT",4,-24) scrollFrame:SetPoint("BOTTOMRIGHT",-6,5) CardTest.scrollFrame = scrollFrame CardTest:SetScript("OnEvent",function(self,event) wipe(testData) for i=1,C_PetJournal.GetNumPets() do local petID = C_PetJournal.GetPetInfoByIndex(i) if petID then tinsert(testData,petID) end end self.scrollFrame:Update() end) CardTest:RegisterEvent("PET_JOURNAL_LIST_UPDATE") end)