What 1.12 withholds here is a framework, not a verb. Its world map is a fixed 1002x668 frame showing one zone — twelve tiles, a hit button, POI buttons, two dropdowns — with no canvas, no pan, no zoom, no pins and no notion of where one map sits on another. Every map addon written after 4.0 drives the canvas framework instead, and there is no stock file to migrate from because there is no stock feature. The constraint that shaped every decision: `WorldMapFrame` already has eight 1.12 consumers on this install, all anchoring icons to `WorldMapDetailFrame` and `WorldMapButton`. So nothing is replaced. The canvas is added, and those two frames are reparented INTO it — at rest the rect they had, at scale 1 with no pan, so the stock window is pixel-identical, and the payoff is that the 1.12 art and every icon on it now pan and scale WITH the canvas for free. Clipping comes from a real ScrollFrame: this client clips a scroll child and every descendant of it from one source, in the draw pass and the hit-test, so drawing and hitting agree. The base-art layer a canvas addon hides is a proxy that switches draw layers rather than the frame, because hiding the frame would take the POI buttons and every addon's icons with it. Three regressions, each found by measurement, each pinned: the anchor cycle (container to detail frame to canvas to container — a cycle resolves to nothing, and the symptom was `GetCenter()` nil inside the stock hover pass); the base scale (a minScale below 1 opens the map zoomed out of its own window); and `dataProviders` being a set rather than an array, because callers iterate it with `pairs` and bind the key — an array handed that loop the number 1. `CreateLine` is a Texture carrying two endpoints, drawn through the explicit-corner quad path the cooldown wedge already uses. Nothing stands in for it: a line between two moving points is a rotated quad whose angle and length both change. `C_Texture.GetAtlasInfo` answers nil for every name, and the nil is the answer — but the getter's presence is what makes the common atlas guard take its fallback branch instead of raising. The harness grew a second surface, and the finding is worth keeping: the full screen map hides `UIParent`, so opening it in the ordinary render probe dropped the corpus's draw column from 37/86 to 18/86 — nineteen unrelated addons. As its own pass: no addon lost a quad and nineteen gained, pfQuest 374 -> 911, pfUI 1063 -> 2259. OpenMapClassic now reads 1/1 on all five columns, stitching 74 of the realm's own zones. Corpus roster unchanged at 85 ok / 1 fail; the 12 data-half reds byte-identical to HEAD's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
1655 lines
54 KiB
Lua
1655 lines
54 KiB
Lua
-- MapCanvas.lua — the Era map-canvas framework, bolted onto 1.12's own WorldMapFrame.
|
|
--
|
|
-- WHY THIS FILE EXISTS, AND WHY IT IS NOT A MIGRATION
|
|
--
|
|
-- 1.12's world map is a fixed 1002x668 frame showing one zone at a time: twelve detail tiles, a
|
|
-- hit-testing button over them, POI buttons placed in its coordinates, and two dropdowns. It has
|
|
-- no canvas, no pan, no zoom, no pins and no notion of where one map sits on another. Every map
|
|
-- addon written after 4.0 — a seamless world map, Mapster, TomTom's pin layer, Questie's map —
|
|
-- drives the *canvas* framework instead: a scaled, pannable child frame, pins acquired from pools
|
|
-- at named frame levels, and data providers notified when the map, the view or the scale changes.
|
|
-- There is no stock 1.12 file to migrate from, because 1.12 has no such thing. This is the same
|
|
-- footing as `CompatTemplates.xml`: a FrameXML-era name addons inherit and reach for, which this
|
|
-- client has to provide or the addon simply does not run.
|
|
--
|
|
-- THE ONE CONSTRAINT THAT SHAPED EVERY DECISION HERE
|
|
--
|
|
-- `WorldMapFrame` already has eight consumers on this install (Atlas-CFM, FlightMap,
|
|
-- LevelRange-Turtle, pfQuest, pfUI, pfUI-locplus, SoftSpoken, SoftSpoken_Places), and all eight
|
|
-- are 1.12 addons: they anchor their icons to `WorldMapDetailFrame` and `WorldMapButton` and read
|
|
-- the `(continent, zone)` selection. Replacing the window would break all eight. So nothing here
|
|
-- replaces anything:
|
|
--
|
|
-- * The canvas is ADDED to `WorldMapFrame`, as `ScrollContainer` and `ScrollContainer.Child`.
|
|
-- * `WorldMapDetailFrame` is REPARENTED INTO the canvas and `SetAllPoints`ed to it. At rest the
|
|
-- canvas is exactly the rect the detail frame had, at scale 1 with no pan, so the stock map is
|
|
-- pixel-identical. The payoff is that the 1.12 art and every POI anchored to it now pan and
|
|
-- zoom WITH the canvas for free — a 1.12 quest icon stays on its quest while a canvas addon
|
|
-- moves the view, which no amount of per-addon work could achieve.
|
|
-- * `SetMapID` moves the 1.12 selection too (`BenillaSelectMapID`), so the stock window and the
|
|
-- canvas can never show different places.
|
|
--
|
|
-- WHAT GIVES US CLIPPING
|
|
--
|
|
-- Era's `ScrollContainer` is a plain Frame with `SetClipsChildren(true)`, which this client has
|
|
-- no verb for. Ours is a real `ScrollFrame` with the canvas as its scroll child, because this
|
|
-- client clips a ScrollFrame's scroll child and every descendant of it to the ScrollFrame's rect
|
|
-- — in the draw pass and in the pointer hit-test, from one shared source (`script/clip.rs`), so
|
|
-- drawing and hitting agree. The pan is the scroll offset, which is what a ScrollFrame's offsets
|
|
-- already mean; the zoom is `Child:SetScale`.
|
|
--
|
|
-- DIALECT
|
|
--
|
|
-- 1.12 Lua 5.0: `table.getn` rather than `#`, `arg`/`unpack(arg)` rather than `...` as an
|
|
-- expression, `string.format` reachable as `format`. Written the way the rest of this chain is.
|
|
|
|
-- The world sheet, Era's own id for Azeroth, which `C_Map.GetFallbackWorldMapID` answers.
|
|
local WORLD_MAP_ID = C_Map.GetFallbackWorldMapID()
|
|
|
|
-- Zoom behaviour constants Era publishes and addons read off the container.
|
|
MAP_CANVAS_MOUSE_WHEEL_ZOOM_BEHAVIOR_FULL = 1
|
|
MAP_CANVAS_MOUSE_WHEEL_ZOOM_BEHAVIOR_SMOOTH = 2
|
|
|
|
-- How close to the target the smooth pan/zoom has to be before it snaps and stops. Era's own
|
|
-- values; small enough that the snap is invisible and large enough that the OnUpdate stops.
|
|
local PAN_SNAP = 0.0005
|
|
local SCALE_SNAP = 0.001
|
|
-- How fast the view approaches its target, per second, as a fraction closed.
|
|
local PAN_RATE = 10
|
|
local SCALE_RATE = 10
|
|
-- A drag under this many pixels is still a click, which is what `WouldCursorPositionBeClick`
|
|
-- answers; Era's own threshold.
|
|
local CLICK_SLOP = 5
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- PinFrameLevelsManager
|
|
--
|
|
-- Pins draw in named bands rather than at raw frame levels, so an addon can insert its own band
|
|
-- relative to Blizzard's by name and get a range of levels inside it. Era's structure exactly,
|
|
-- because the names are the contract: `InsertFrameLevelAbove("PIN_FRAME_LEVEL_OPENMAP_STITCH",
|
|
-- "PIN_FRAME_LEVEL_MAP_EXPLORATION", n)` has to mean what it means upstream or the art lands in
|
|
-- the wrong band.
|
|
--------------------------------------------------------------------------------
|
|
|
|
PinFrameLevelsManagerMixin = {}
|
|
|
|
function PinFrameLevelsManagerMixin:Initialize()
|
|
self.frameLevelRanges = {}
|
|
self.ordered = {}
|
|
self.currentFrameLevels = {}
|
|
end
|
|
|
|
-- The base level every band is offset from: above the canvas itself, so a pin never hides behind
|
|
-- the art it sits on.
|
|
local PIN_FRAME_LEVEL_BASE = 100
|
|
|
|
function PinFrameLevelsManagerMixin:Reset()
|
|
self.currentFrameLevels = {}
|
|
for i = 1, table.getn(self.ordered) do
|
|
self.currentFrameLevels[self.ordered[i]] = 0
|
|
end
|
|
end
|
|
|
|
-- Append a band with `count` levels in it; the first call for a name wins, as upstream's does.
|
|
function PinFrameLevelsManagerMixin:InsertFrameLevel(frameLevelType, count)
|
|
if self.frameLevelRanges[frameLevelType] then
|
|
return false
|
|
end
|
|
local base = PIN_FRAME_LEVEL_BASE
|
|
local n = table.getn(self.ordered)
|
|
if n > 0 then
|
|
local last = self.frameLevelRanges[self.ordered[n]]
|
|
base = last.base + last.count
|
|
end
|
|
self.frameLevelRanges[frameLevelType] = { base = base, count = count or 1 }
|
|
self.ordered[n + 1] = frameLevelType
|
|
self.currentFrameLevels[frameLevelType] = 0
|
|
return true
|
|
end
|
|
|
|
-- Insert a band immediately above a named one, shifting every band above it up. False when the
|
|
-- reference band does not exist, which is the answer a caller branches on: OpenMapClassic prints
|
|
-- "Blizzard's map layers aren't the expected ones" and carries on.
|
|
function PinFrameLevelsManagerMixin:InsertFrameLevelAbove(frameLevelType, aboveType, count)
|
|
if self.frameLevelRanges[frameLevelType] or not self.frameLevelRanges[aboveType] then
|
|
return false
|
|
end
|
|
count = count or 1
|
|
local at
|
|
for i = 1, table.getn(self.ordered) do
|
|
if self.ordered[i] == aboveType then
|
|
at = i
|
|
break
|
|
end
|
|
end
|
|
if not at then
|
|
return false
|
|
end
|
|
table.insert(self.ordered, at + 1, frameLevelType)
|
|
-- Re-base every band from the bottom, so the ranges stay contiguous and ordered.
|
|
local base = PIN_FRAME_LEVEL_BASE
|
|
for i = 1, table.getn(self.ordered) do
|
|
local name = self.ordered[i]
|
|
local range = self.frameLevelRanges[name]
|
|
if not range then
|
|
range = { count = count }
|
|
self.frameLevelRanges[name] = range
|
|
self.currentFrameLevels[name] = 0
|
|
end
|
|
range.base = base
|
|
base = base + range.count
|
|
end
|
|
return true
|
|
end
|
|
|
|
function PinFrameLevelsManagerMixin:InsertFrameLevelBelow(frameLevelType, belowType, count)
|
|
if not self.frameLevelRanges[belowType] then
|
|
return false
|
|
end
|
|
-- Below `belowType` is above whatever is under it; with nothing under it, the bottom.
|
|
local at
|
|
for i = 1, table.getn(self.ordered) do
|
|
if self.ordered[i] == belowType then
|
|
at = i
|
|
break
|
|
end
|
|
end
|
|
if not at then
|
|
return false
|
|
end
|
|
if at == 1 then
|
|
if self.frameLevelRanges[frameLevelType] then
|
|
return false
|
|
end
|
|
table.insert(self.ordered, 1, frameLevelType)
|
|
self.frameLevelRanges[frameLevelType] = { base = 0, count = count or 1 }
|
|
self.currentFrameLevels[frameLevelType] = 0
|
|
local base = PIN_FRAME_LEVEL_BASE
|
|
for i = 1, table.getn(self.ordered) do
|
|
local range = self.frameLevelRanges[self.ordered[i]]
|
|
range.base = base
|
|
base = base + range.count
|
|
end
|
|
return true
|
|
end
|
|
return self:InsertFrameLevelAbove(frameLevelType, self.ordered[at - 1], count)
|
|
end
|
|
|
|
-- The next free level inside a band, which is what a pin asks for as it is acquired. A band that
|
|
-- runs out keeps handing back its top level rather than spilling into the next band's.
|
|
function PinFrameLevelsManagerMixin:GetValidFrameLevel(frameLevelType)
|
|
local range = self.frameLevelRanges[frameLevelType]
|
|
if not range then
|
|
return PIN_FRAME_LEVEL_BASE
|
|
end
|
|
local used = self.currentFrameLevels[frameLevelType] or 0
|
|
if used < range.count then
|
|
self.currentFrameLevels[frameLevelType] = used + 1
|
|
else
|
|
used = range.count - 1
|
|
end
|
|
return range.base + used
|
|
end
|
|
|
|
function PinFrameLevelsManagerMixin:GetFrameLevelRange(frameLevelType)
|
|
local range = self.frameLevelRanges[frameLevelType]
|
|
if not range then
|
|
return nil
|
|
end
|
|
return range.base, range.base + range.count - 1
|
|
end
|
|
|
|
local function CreatePinFrameLevelsManager()
|
|
local manager = {}
|
|
for k, v in pairs(PinFrameLevelsManagerMixin) do
|
|
manager[k] = v
|
|
end
|
|
manager:Initialize()
|
|
return manager
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- MapCanvasDataProviderMixin — the base every data provider is built from.
|
|
--
|
|
-- Every member is a no-op, deliberately: upstream's is too, and that is the whole contract. A
|
|
-- provider overrides the two or three hooks it cares about and inherits silence for the rest, so
|
|
-- adding a hook here can never break an existing provider.
|
|
--------------------------------------------------------------------------------
|
|
|
|
MapCanvasDataProviderMixin = {}
|
|
|
|
function MapCanvasDataProviderMixin:OnAdded(mapCanvas)
|
|
self.owningMap = mapCanvas
|
|
end
|
|
|
|
function MapCanvasDataProviderMixin:OnRemoved(mapCanvas)
|
|
self.owningMap = nil
|
|
end
|
|
|
|
function MapCanvasDataProviderMixin:GetMap()
|
|
return self.owningMap
|
|
end
|
|
|
|
function MapCanvasDataProviderMixin:RemoveAllData() end
|
|
function MapCanvasDataProviderMixin:RefreshAllData(fromOnShow) end
|
|
function MapCanvasDataProviderMixin:OnShow() end
|
|
function MapCanvasDataProviderMixin:OnHide() end
|
|
function MapCanvasDataProviderMixin:OnEvent(event, arg1, arg2, arg3) end
|
|
function MapCanvasDataProviderMixin:OnMapChanged() end
|
|
function MapCanvasDataProviderMixin:OnMapInsetSizeChanged(insetIndex, expanded) end
|
|
function MapCanvasDataProviderMixin:OnCanvasScaleChanged() end
|
|
function MapCanvasDataProviderMixin:OnCanvasPanChanged() end
|
|
function MapCanvasDataProviderMixin:OnCanvasSizeChanged() end
|
|
function MapCanvasDataProviderMixin:OnGlobalAlphaChanged() end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- MapCanvasPinMixin — the base every pin is built from.
|
|
--
|
|
-- A pin is a frame on the canvas placed by a normalized position rather than by anchors, so it
|
|
-- stays on its place as the canvas pans and scales. `ApplyCurrentPosition` is the one that does
|
|
-- the work and is called on every pan and scale change.
|
|
--------------------------------------------------------------------------------
|
|
|
|
MapCanvasPinMixin = {}
|
|
|
|
function MapCanvasPinMixin:OnLoad()
|
|
self.numActiveAnimations = 0
|
|
self.scaleFactor = 1
|
|
self.startScale = 1
|
|
self.endScale = 1
|
|
self.normalizedX = 0.5
|
|
self.normalizedY = 0.5
|
|
end
|
|
|
|
function MapCanvasPinMixin:GetMap()
|
|
return self.owningMap
|
|
end
|
|
|
|
function MapCanvasPinMixin:OnAcquired() end
|
|
function MapCanvasPinMixin:OnReleased() end
|
|
function MapCanvasPinMixin:OnClick(button) end
|
|
function MapCanvasPinMixin:OnMouseEnter() end
|
|
function MapCanvasPinMixin:OnMouseLeave() end
|
|
function MapCanvasPinMixin:OnMapInsetSizeChanged(insetIndex, expanded) end
|
|
function MapCanvasPinMixin:OnCanvasScaleChanged()
|
|
self:ApplyCurrentScale()
|
|
end
|
|
function MapCanvasPinMixin:OnCanvasPanChanged() end
|
|
function MapCanvasPinMixin:OnCanvasSizeChanged() end
|
|
|
|
function MapCanvasPinMixin:SetPosition(normalizedX, normalizedY, insetIndex)
|
|
self.normalizedX = normalizedX
|
|
self.normalizedY = normalizedY
|
|
self.insetIndex = insetIndex
|
|
self:ApplyCurrentPosition()
|
|
end
|
|
|
|
function MapCanvasPinMixin:GetPosition()
|
|
return self.normalizedX, self.normalizedY
|
|
end
|
|
|
|
function MapCanvasPinMixin:ApplyCurrentPosition()
|
|
local map = self:GetMap()
|
|
if not map then
|
|
return
|
|
end
|
|
local canvas = map:GetCanvas()
|
|
if not canvas then
|
|
return
|
|
end
|
|
self:ClearAllPoints()
|
|
self:SetPoint("CENTER", canvas, "TOPLEFT",
|
|
(self.normalizedX or 0.5) * canvas:GetWidth(),
|
|
-(self.normalizedY or 0.5) * canvas:GetHeight())
|
|
end
|
|
|
|
-- A pin's own scale, held constant on screen as the canvas scales: a quest icon should stay the
|
|
-- same size however far the map is zoomed out, which is `1 / canvasScale` times whatever the pin
|
|
-- asked for. `scaleFactor` 0 opts out and lets the pin scale with the map, which is what an area
|
|
-- outline (a "blob") wants.
|
|
function MapCanvasPinMixin:SetScalingLimits(scaleFactor, startScale, endScale)
|
|
self.scaleFactor = scaleFactor
|
|
self.startScale = startScale
|
|
self.endScale = endScale
|
|
end
|
|
|
|
function MapCanvasPinMixin:ApplyCurrentScale()
|
|
local map = self:GetMap()
|
|
if not map then
|
|
return
|
|
end
|
|
local factor = self.scaleFactor
|
|
if not factor or factor == 0 then
|
|
self:SetScale(1)
|
|
return
|
|
end
|
|
local canvasScale = map:GetCanvasScale()
|
|
if not canvasScale or canvasScale <= 0 then
|
|
return
|
|
end
|
|
local scale = factor / canvasScale
|
|
local lo = self.startScale
|
|
local hi = self.endScale
|
|
if lo and hi and lo > hi then
|
|
lo, hi = hi, lo
|
|
end
|
|
if lo and scale < lo then
|
|
scale = lo
|
|
end
|
|
if hi and scale > hi then
|
|
scale = hi
|
|
end
|
|
self:SetScale(scale)
|
|
end
|
|
|
|
function MapCanvasPinMixin:UseFrameLevelType(pinFrameLevelType, index)
|
|
self.pinFrameLevelType = pinFrameLevelType
|
|
self.pinFrameLevelIndex = index
|
|
end
|
|
|
|
function MapCanvasPinMixin:GetFrameLevelType()
|
|
return self.pinFrameLevelType
|
|
end
|
|
|
|
function MapCanvasPinMixin:ApplyFrameLevel()
|
|
local map = self:GetMap()
|
|
if not map or not self.pinFrameLevelType then
|
|
return
|
|
end
|
|
local manager = map:GetPinFrameLevelsManager()
|
|
if manager then
|
|
self:SetFrameLevel(manager:GetValidFrameLevel(self.pinFrameLevelType))
|
|
end
|
|
end
|
|
|
|
-- Which mouse buttons fall through to the canvas underneath. Era's pins declare this so a
|
|
-- right-drag over an icon still pans the map; a pin with no declaration swallows everything,
|
|
-- which is what a clickable icon wants.
|
|
function MapCanvasPinMixin:SetPassThroughButtons(button1, button2, button3)
|
|
self.passThroughButtons = { button1, button2, button3 }
|
|
end
|
|
|
|
function MapCanvasPinMixin:CheckMouseButtonPassthrough(button)
|
|
local list = self.passThroughButtons
|
|
if not list then
|
|
return false
|
|
end
|
|
for i = 1, table.getn(list) do
|
|
if list[i] == button then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- MapCanvasScrollControllerMixin — the pan and zoom.
|
|
--
|
|
-- The view is held as a NORMALIZED CENTRE (`currentScrollX`, `currentScrollY`: the canvas point
|
|
-- under the middle of the container, 0..1 across the canvas) plus a scale. That is upstream's own
|
|
-- representation, and it has to be, because a canvas addon reads and writes those fields directly
|
|
-- — OpenMapClassic's `SetView` assigns `currentScrollX`, `targetScrollX`, `currentScale`,
|
|
-- `targetScale` and `zoomTarget` by hand (`Map/WorldMap.lua:184-191`) and then calls
|
|
-- `SetNormalizedHorizontalScroll`. So the field names and their meanings are the contract, not an
|
|
-- implementation detail.
|
|
--
|
|
-- The pan is applied as the ScrollFrame's own scroll offset, which is what a ScrollFrame offset
|
|
-- already means and is the cheapest write this client has (`SetVerticalScroll` re-anchors one
|
|
-- node and skips the layout touch).
|
|
--------------------------------------------------------------------------------
|
|
|
|
MapCanvasScrollControllerMixin = {}
|
|
|
|
function MapCanvasScrollControllerMixin:OnLoad()
|
|
self.currentScrollX = 0.5
|
|
self.currentScrollY = 0.5
|
|
self.targetScrollX = 0.5
|
|
self.targetScrollY = 0.5
|
|
self.currentScale = 1
|
|
self.targetScale = 1
|
|
self.zoomTarget = 1
|
|
self.baseScale = 1
|
|
self.zoomLevels = {}
|
|
self.scrollXExtentsMin = 0.5
|
|
self.scrollXExtentsMax = 0.5
|
|
self.scrollYExtentsMin = 0.5
|
|
self.scrollYExtentsMax = 0.5
|
|
self.mouseButtonInfo = {
|
|
LeftButton = { down = false, x = 0, y = 0 },
|
|
RightButton = { down = false, x = 0, y = 0 },
|
|
MiddleButton = { down = false, x = 0, y = 0 },
|
|
}
|
|
self.scalingMode = "SCALING_MODE_TRANSLATE_FASTER_THAN_SCALE"
|
|
self.mouseWheelZoomMode = MAP_CANVAS_MOUSE_WHEEL_ZOOM_BEHAVIOR_FULL
|
|
self.panning = false
|
|
self.viewRectDirty = true
|
|
self.areaTriggersDirty = true
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:GetCanvasContainer()
|
|
return self
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:GetCanvas()
|
|
return self.Child
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:GetCanvasScale()
|
|
return self.currentScale or 1
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:SetScalingMode(mode)
|
|
self.scalingMode = mode
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:SetMouseWheelZoomMode(mode)
|
|
self.mouseWheelZoomMode = mode
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:MarkViewRectDirty()
|
|
self.viewRectDirty = true
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:MarkAreaTriggersDirty()
|
|
self.areaTriggersDirty = true
|
|
end
|
|
|
|
-- Canvas px to a fraction of the canvas, the two helpers every extents computation goes through.
|
|
function MapCanvasScrollControllerMixin:NormalizeHorizontalSize(size)
|
|
local w = self.Child and self.Child:GetWidth()
|
|
if not w or w <= 0 then
|
|
return 0
|
|
end
|
|
return size / w
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:NormalizeVerticalSize(size)
|
|
local h = self.Child and self.Child:GetHeight()
|
|
if not h or h <= 0 then
|
|
return 0
|
|
end
|
|
return size / h
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:DenormalizeHorizontalSize(size)
|
|
local w = self.Child and self.Child:GetWidth()
|
|
return size * (w or 0)
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:DenormalizeVerticalSize(size)
|
|
local h = self.Child and self.Child:GetHeight()
|
|
return size * (h or 0)
|
|
end
|
|
|
|
-- The normalized centre may range over this, so the view stays inside the canvas. When the canvas
|
|
-- is smaller than the container on an axis the range collapses to its middle, which centres it.
|
|
function MapCanvasScrollControllerMixin:CalculateScrollExtentsAtScale(scale)
|
|
local halfW = self:NormalizeHorizontalSize(self:GetWidth() * 0.5 / scale)
|
|
local halfH = self:NormalizeVerticalSize(self:GetHeight() * 0.5 / scale)
|
|
local minX, maxX = halfW, 1 - halfW
|
|
local minY, maxY = halfH, 1 - halfH
|
|
if minX > maxX then
|
|
minX, maxX = 0.5, 0.5
|
|
end
|
|
if minY > maxY then
|
|
minY, maxY = 0.5, 0.5
|
|
end
|
|
return minX, maxX, minY, maxY
|
|
end
|
|
|
|
-- Through `CalculateScrollExtentsAtScale`, never duplicating its arithmetic: a canvas addon
|
|
-- replaces that one method to widen the pan (OpenMapClassic pans the whole world, not just the
|
|
-- map in view), and a second copy of the law here would ignore the replacement.
|
|
function MapCanvasScrollControllerMixin:CalculateScrollExtents()
|
|
self.scrollXExtentsMin, self.scrollXExtentsMax, self.scrollYExtentsMin, self.scrollYExtentsMax =
|
|
self:CalculateScrollExtentsAtScale(self:GetCanvasScale())
|
|
end
|
|
|
|
local function clamp(v, lo, hi)
|
|
if lo and v < lo then
|
|
return lo
|
|
end
|
|
if hi and v > hi then
|
|
return hi
|
|
end
|
|
return v
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:ClampScrollToExtents(x, y)
|
|
return clamp(x, self.scrollXExtentsMin, self.scrollXExtentsMax),
|
|
clamp(y, self.scrollYExtentsMin, self.scrollYExtentsMax)
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:SetNormalizedHorizontalScroll(x)
|
|
self.currentScrollX = x
|
|
self:ApplyCurrentScroll()
|
|
self:MarkViewRectDirty()
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:SetNormalizedVerticalScroll(y)
|
|
self.currentScrollY = y
|
|
self:ApplyCurrentScroll()
|
|
self:MarkViewRectDirty()
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:GetCurrentScrollX()
|
|
return self.currentScrollX
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:GetCurrentScrollY()
|
|
return self.currentScrollY
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:GetNormalizedHorizontalScroll()
|
|
return self.currentScrollX
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:GetNormalizedVerticalScroll()
|
|
return self.currentScrollY
|
|
end
|
|
|
|
-- The whole view at once, no easing: the canvas scaled, the extents recomputed at that scale, and
|
|
-- the centre clamped into them.
|
|
function MapCanvasScrollControllerMixin:InstantPanAndZoom(scale, x, y)
|
|
self.currentScale = scale
|
|
self.targetScale = scale
|
|
self.zoomTarget = scale
|
|
if self.Child then
|
|
self.Child:SetScale(scale)
|
|
end
|
|
self:CalculateScrollExtents()
|
|
x, y = self:ClampScrollToExtents(x, y)
|
|
self.currentScrollX, self.targetScrollX = x, x
|
|
self.currentScrollY, self.targetScrollY = y, y
|
|
self:ApplyCurrentScroll()
|
|
self:MarkViewRectDirty()
|
|
self:MarkAreaTriggersDirty()
|
|
local map = self:GetMapCanvas()
|
|
if map then
|
|
map:OnCanvasScaleChanged()
|
|
map:OnCanvasPanChanged()
|
|
end
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:GetMapCanvas()
|
|
return self.mapCanvas
|
|
end
|
|
|
|
-- The zoom steps, derived from the map's own art layers: one step per layer, at that layer's
|
|
-- minimum scale, plus a fitted step so the whole map is reachable. A canvas addon replaces this
|
|
-- wholesale (OpenMapClassic builds a geometric ladder from its own zoom option), which is why it
|
|
-- is one method and why `ResetZoom` reads `zoomLevels` rather than recomputing.
|
|
function MapCanvasScrollControllerMixin:CreateZoomLevels()
|
|
local mapID = self.mapID
|
|
local levels = {}
|
|
if mapID then
|
|
local layers = C_Map.GetMapArtLayers(mapID)
|
|
local layer = layers and layers[1]
|
|
if layer then
|
|
-- A geometric ladder from the layer's own bounds. Scale 1 is the whole map in the
|
|
-- container by construction — the canvas is sized to the layer and the container is the
|
|
-- rect that layer used to fill — so a `minScale` below 1 would open the map zoomed
|
|
-- *out* of its own window, which is how this was first wrong.
|
|
local lo = layer.minScale or 1
|
|
local hi = layer.maxScale or 1
|
|
if lo > 1 then
|
|
lo = 1
|
|
end
|
|
if hi < lo then
|
|
hi = lo
|
|
end
|
|
local steps = (layer.additionalZoomSteps or 0) + 1
|
|
if steps < 1 then
|
|
steps = 1
|
|
end
|
|
local ratio = 1
|
|
if steps > 1 and lo > 0 then
|
|
ratio = math.pow(hi / lo, 1 / (steps - 1))
|
|
end
|
|
local scale = lo
|
|
for i = 1, steps do
|
|
levels[i] = { scale = scale, layerIndex = 1 }
|
|
scale = scale * ratio
|
|
end
|
|
end
|
|
end
|
|
if table.getn(levels) == 0 then
|
|
levels[1] = { scale = 1, layerIndex = 1 }
|
|
end
|
|
self.zoomLevels = levels
|
|
self.baseScale = levels[1].scale
|
|
-- The scale itself is only seeded when there is none: `CreateZoomLevels` is called again on
|
|
-- every map change and on every container resize, and a view the player has zoomed must not
|
|
-- snap back because the window was dragged.
|
|
if not self.currentScale then
|
|
self.currentScale = levels[1].scale
|
|
self.targetScale = levels[1].scale
|
|
self.zoomTarget = levels[1].scale
|
|
end
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:GetScaleForMaxZoom()
|
|
local n = table.getn(self.zoomLevels)
|
|
if n == 0 then
|
|
return 1
|
|
end
|
|
return self.zoomLevels[n].scale
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:GetScaleForMinZoom()
|
|
if table.getn(self.zoomLevels) == 0 then
|
|
return 1
|
|
end
|
|
return self.zoomLevels[1].scale
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:IsAtMaxZoom()
|
|
return self:GetCanvasScale() >= self:GetScaleForMaxZoom()
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:IsAtMinZoom()
|
|
return self:GetCanvasScale() <= self:GetScaleForMinZoom()
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:IsZoomingIn()
|
|
return (self.targetScale or 1) > (self.currentScale or 1)
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:IsZoomingOut()
|
|
return (self.targetScale or 1) < (self.currentScale or 1)
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:IsZooming()
|
|
return self:IsZoomingIn() or self:IsZoomingOut()
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:IsGamepadZooming()
|
|
return self.gamepadZoom and self.gamepadZoom ~= 0
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:HasGamepadScrollSpeed()
|
|
return self.gamepadZoomSpeed ~= nil
|
|
end
|
|
|
|
-- The next step up or down the ladder, toward a point if one is given.
|
|
function MapCanvasScrollControllerMixin:SetZoomTarget(scale, x, y)
|
|
self.zoomTarget = scale
|
|
self.targetScale = scale
|
|
if x and y then
|
|
self.targetScrollX, self.targetScrollY = x, y
|
|
end
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:ZoomIn()
|
|
local n = table.getn(self.zoomLevels)
|
|
local current = self:GetCanvasScale()
|
|
for i = 1, n do
|
|
if self.zoomLevels[i].scale > current + 0.0001 then
|
|
self:SetZoomTarget(self.zoomLevels[i].scale)
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:ZoomOut()
|
|
local current = self:GetCanvasScale()
|
|
for i = table.getn(self.zoomLevels), 1, -1 do
|
|
if self.zoomLevels[i].scale < current - 0.0001 then
|
|
self:SetZoomTarget(self.zoomLevels[i].scale)
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:ResetZoom()
|
|
local scale = self:GetScaleForMinZoom()
|
|
self:InstantPanAndZoom(scale, 0.5, 0.5)
|
|
end
|
|
|
|
-- Called when the container or the canvas changes size, and on the first show: the canvas is
|
|
-- sized from the map's own art layer, the zoom ladder rebuilt and the view reset.
|
|
function MapCanvasScrollControllerMixin:OnCanvasSizeChanged()
|
|
local child = self.Child
|
|
if not child then
|
|
return
|
|
end
|
|
local mapID = self.mapID
|
|
local w, h
|
|
if mapID then
|
|
local layers = C_Map.GetMapArtLayers(mapID)
|
|
local layer = layers and layers[1]
|
|
if layer then
|
|
w, h = layer.layerWidth, layer.layerHeight
|
|
end
|
|
end
|
|
-- With no art layer (an instance map, a map the realm has no folder for) the canvas keeps the
|
|
-- container's own size, so pins still place and the view is simply not zoomable.
|
|
if not w or w <= 0 or not h or h <= 0 then
|
|
w, h = self:GetWidth(), self:GetHeight()
|
|
end
|
|
child:SetWidth(w)
|
|
child:SetHeight(h)
|
|
self:CreateZoomLevels()
|
|
self:CalculateScrollExtents()
|
|
self.currentScrollX, self.currentScrollY = self:ClampScrollToExtents(
|
|
self.currentScrollX or 0.5, self.currentScrollY or 0.5)
|
|
self.targetScrollX, self.targetScrollY = self.currentScrollX, self.currentScrollY
|
|
child:SetScale(self:GetCanvasScale())
|
|
self:ApplyCurrentScroll()
|
|
self:MarkViewRectDirty()
|
|
local map = self:GetMapCanvas()
|
|
if map then
|
|
map:OnCanvasSizeChanged()
|
|
end
|
|
end
|
|
|
|
-- The cursor in canvas-normalized coordinates, which is what a click handler is handed.
|
|
function MapCanvasScrollControllerMixin:GetCursorPosition()
|
|
local x, y = GetCursorPosition()
|
|
local effective = self:GetEffectiveScale()
|
|
if not effective or effective == 0 then
|
|
return 0.5, 0.5
|
|
end
|
|
return x / effective, y / effective
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:GetNormalizedCursorPosition()
|
|
local cx, cy = self:GetCursorPosition()
|
|
local left, bottom = self:GetLeft(), self:GetBottom()
|
|
local w, h = self:GetWidth(), self:GetHeight()
|
|
if not left or not w or w <= 0 or h <= 0 then
|
|
return 0.5, 0.5
|
|
end
|
|
-- Inside the container first, then through the view onto the canvas: the container's centre is
|
|
-- the normalized scroll, and one container width spans `containerW / (canvasW * scale)` of it.
|
|
local fx = (cx - left) / w
|
|
local fy = 1 - (cy - bottom) / h
|
|
local spanX = self:NormalizeHorizontalSize(w / self:GetCanvasScale())
|
|
local spanY = self:NormalizeVerticalSize(h / self:GetCanvasScale())
|
|
return (self.currentScrollX or 0.5) + (fx - 0.5) * spanX,
|
|
(self.currentScrollY or 0.5) + (fy - 0.5) * spanY
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:WouldCursorPositionBeClick(button, x, y)
|
|
local info = self.mouseButtonInfo and self.mouseButtonInfo[button]
|
|
if not info or not info.down then
|
|
return false
|
|
end
|
|
local dx = (x or 0) - (info.x or 0)
|
|
local dy = (y or 0) - (info.y or 0)
|
|
return dx * dx + dy * dy <= CLICK_SLOP * CLICK_SLOP
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:OnMouseDown(button)
|
|
local info = self.mouseButtonInfo and self.mouseButtonInfo[button]
|
|
if not info then
|
|
return
|
|
end
|
|
local x, y = self:GetCursorPosition()
|
|
info.down = true
|
|
info.x, info.y = x, y
|
|
if button == "LeftButton" then
|
|
self.panning = true
|
|
self.panStartX, self.panStartY = x, y
|
|
self.panStartScrollX = self.currentScrollX
|
|
self.panStartScrollY = self.currentScrollY
|
|
end
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:OnMouseUp(button)
|
|
local info = self.mouseButtonInfo and self.mouseButtonInfo[button]
|
|
if not info then
|
|
return
|
|
end
|
|
local x, y = self:GetCursorPosition()
|
|
local wasClick = self:WouldCursorPositionBeClick(button, x, y)
|
|
info.down = false
|
|
if button == "LeftButton" then
|
|
self.panning = false
|
|
end
|
|
-- Recorded, not just returned: the stock `WorldMapButton`'s own mouse-up is the 1.12
|
|
-- click-to-zoom, and it must not run when a canvas handler took the click or when the gesture
|
|
-- was a drag. The wrapper in `BuildCanvas` reads this.
|
|
self.lastClickConsumed = true
|
|
local map = self:GetMapCanvas()
|
|
if wasClick and map then
|
|
local x, y = self:GetNormalizedCursorPosition()
|
|
self.lastClickConsumed = map:ProcessCanvasClickHandlers(button, x, y) and true or false
|
|
end
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:OnMouseWheel(delta)
|
|
if delta > 0 then
|
|
self:ZoomIn()
|
|
else
|
|
self:ZoomOut()
|
|
end
|
|
end
|
|
|
|
-- The drag, and the ease toward the pan and zoom targets. One place, so a drag during a zoom
|
|
-- keeps working, and it stops writing once both have arrived.
|
|
function MapCanvasScrollControllerMixin:OnUpdate(elapsed)
|
|
local moved = false
|
|
if self.panning then
|
|
local x, y = self:GetCursorPosition()
|
|
local scale = self:GetCanvasScale()
|
|
if scale > 0 then
|
|
local dx = self:NormalizeHorizontalSize((self.panStartX - x) / scale)
|
|
local dy = self:NormalizeVerticalSize((y - self.panStartY) / scale)
|
|
local nx, ny = self:ClampScrollToExtents(
|
|
(self.panStartScrollX or 0.5) + dx, (self.panStartScrollY or 0.5) + dy)
|
|
if nx ~= self.currentScrollX or ny ~= self.currentScrollY then
|
|
self.currentScrollX, self.currentScrollY = nx, ny
|
|
self.targetScrollX, self.targetScrollY = nx, ny
|
|
moved = true
|
|
end
|
|
end
|
|
else
|
|
local rate = 1 - math.pow(0.5, (elapsed or 0) * PAN_RATE / 7)
|
|
local tx, ty = self.targetScrollX or 0.5, self.targetScrollY or 0.5
|
|
local cx, cy = self.currentScrollX or 0.5, self.currentScrollY or 0.5
|
|
if math.abs(tx - cx) > PAN_SNAP or math.abs(ty - cy) > PAN_SNAP then
|
|
self.currentScrollX = cx + (tx - cx) * rate
|
|
self.currentScrollY = cy + (ty - cy) * rate
|
|
moved = true
|
|
elseif cx ~= tx or cy ~= ty then
|
|
self.currentScrollX, self.currentScrollY = tx, ty
|
|
moved = true
|
|
end
|
|
end
|
|
|
|
local scaled = false
|
|
local ts, cs = self.targetScale or 1, self.currentScale or 1
|
|
if math.abs(ts - cs) > SCALE_SNAP then
|
|
local rate = 1 - math.pow(0.5, (elapsed or 0) * SCALE_RATE / 7)
|
|
self.currentScale = cs + (ts - cs) * rate
|
|
scaled = true
|
|
elseif cs ~= ts then
|
|
self.currentScale = ts
|
|
scaled = true
|
|
end
|
|
|
|
if scaled then
|
|
if self.Child then
|
|
self.Child:SetScale(self.currentScale)
|
|
end
|
|
self:CalculateScrollExtents()
|
|
self.currentScrollX, self.currentScrollY = self:ClampScrollToExtents(
|
|
self.currentScrollX, self.currentScrollY)
|
|
moved = true
|
|
end
|
|
if moved then
|
|
self:ApplyCurrentScroll()
|
|
self:MarkViewRectDirty()
|
|
end
|
|
local map = self:GetMapCanvas()
|
|
if map then
|
|
if scaled then
|
|
map:OnCanvasScaleChanged()
|
|
end
|
|
if moved then
|
|
map:OnCanvasPanChanged()
|
|
end
|
|
end
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- MapCanvasMixin — the map itself.
|
|
--------------------------------------------------------------------------------
|
|
|
|
MapCanvasMixin = {}
|
|
|
|
-- What a global pin mouse-action handler is told happened. The values are opaque to a caller,
|
|
-- which compares against these names (`MapCanvasMixin.MouseAction.Click`), so only the identity
|
|
-- matters.
|
|
MapCanvasMixin.MouseAction = { Click = 1, Up = 2, Down = 3 }
|
|
|
|
function MapCanvasMixin:OnLoad()
|
|
-- A SET, keyed by the provider with `true` as the value, because that is upstream's shape and
|
|
-- callers iterate it: `for provider in pairs(map.dataProviders) do` binds the *key*, so an
|
|
-- array here hands the loop the number 1 and the next line indexes a number. Measured: it
|
|
-- raised at `Map/Extras.lua:281`, inside the addon's own `OnEnable`, which caught it, printed
|
|
-- it and left the whole addon disabled — the same trap `pools.rs` names for `activeObjects`,
|
|
-- where a "tidier" array would break `for icon in pool:EnumerateActive()` silently.
|
|
--
|
|
-- `dataProviderOrder` is ours, beside it: `pairs` order is undefined, and a map whose
|
|
-- providers are notified in a different order each session is a bug nobody can reproduce. So
|
|
-- the set is the shape addons read and the list is the order we walk.
|
|
self.dataProviders = {}
|
|
self.dataProviderOrder = {}
|
|
self.dataProviderEventsCount = {}
|
|
self.pinPools = {}
|
|
self.pinTemplateTypes = {}
|
|
self.activePins = {}
|
|
self.canvasClickHandlers = {}
|
|
self.globalPinMouseActionHandlers = {}
|
|
self.cursorHandlers = {}
|
|
self.pinFrameLevelsManager = CreatePinFrameLevelsManager()
|
|
self.mapID = nil
|
|
self.globalPinScale = 1
|
|
end
|
|
|
|
function MapCanvasMixin:GetMapID()
|
|
return self.mapID
|
|
end
|
|
|
|
function MapCanvasMixin:GetCanvas()
|
|
return self.ScrollContainer and self.ScrollContainer.Child
|
|
end
|
|
|
|
function MapCanvasMixin:GetCanvasContainer()
|
|
return self.ScrollContainer
|
|
end
|
|
|
|
function MapCanvasMixin:GetCanvasScale()
|
|
return self.ScrollContainer and self.ScrollContainer:GetCanvasScale() or 1
|
|
end
|
|
|
|
function MapCanvasMixin:GetGlobalPinScale()
|
|
return self.globalPinScale or 1
|
|
end
|
|
|
|
function MapCanvasMixin:SetGlobalPinScale(scale)
|
|
self.globalPinScale = scale
|
|
self:OnCanvasScaleChanged()
|
|
end
|
|
|
|
function MapCanvasMixin:GetPinFrameLevelsManager()
|
|
return self.pinFrameLevelsManager
|
|
end
|
|
|
|
-- The view in canvas-normalized coordinates: left, right, top, bottom.
|
|
function MapCanvasMixin:GetViewRect()
|
|
local sc = self.ScrollContainer
|
|
if not sc then
|
|
return 0, 1, 0, 1
|
|
end
|
|
local scale = sc:GetCanvasScale()
|
|
if scale <= 0 then
|
|
return 0, 1, 0, 1
|
|
end
|
|
local halfW = sc:NormalizeHorizontalSize(sc:GetWidth() * 0.5 / scale)
|
|
local halfH = sc:NormalizeVerticalSize(sc:GetHeight() * 0.5 / scale)
|
|
local x, y = sc.currentScrollX or 0.5, sc.currentScrollY or 0.5
|
|
return x - halfW, x + halfW, y - halfH, y + halfH
|
|
end
|
|
|
|
function MapCanvasMixin:NormalizeHorizontalSize(size)
|
|
return self.ScrollContainer and self.ScrollContainer:NormalizeHorizontalSize(size) or 0
|
|
end
|
|
|
|
function MapCanvasMixin:NormalizeVerticalSize(size)
|
|
return self.ScrollContainer and self.ScrollContainer:NormalizeVerticalSize(size) or 0
|
|
end
|
|
|
|
function MapCanvasMixin:SetPinNudgingDirty(dirty)
|
|
self.pinNudgingDirty = dirty
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Data providers
|
|
--------------------------------------------------------------------------------
|
|
|
|
function MapCanvasMixin:AddDataProvider(dataProvider)
|
|
if not dataProvider or self.dataProviders[dataProvider] then
|
|
return
|
|
end
|
|
self.dataProviders[dataProvider] = true
|
|
self.dataProviderOrder[table.getn(self.dataProviderOrder) + 1] = dataProvider
|
|
if dataProvider.OnAdded then
|
|
dataProvider:OnAdded(self)
|
|
end
|
|
if self:IsShown() and dataProvider.RefreshAllData then
|
|
dataProvider:RefreshAllData(false)
|
|
end
|
|
end
|
|
|
|
function MapCanvasMixin:RemoveDataProvider(dataProvider)
|
|
if not dataProvider or not self.dataProviders[dataProvider] then
|
|
return
|
|
end
|
|
self.dataProviders[dataProvider] = nil
|
|
for i = 1, table.getn(self.dataProviderOrder) do
|
|
if self.dataProviderOrder[i] == dataProvider then
|
|
table.remove(self.dataProviderOrder, i)
|
|
break
|
|
end
|
|
end
|
|
if dataProvider.RemoveAllData then
|
|
dataProvider:RemoveAllData()
|
|
end
|
|
if dataProvider.OnRemoved then
|
|
dataProvider:OnRemoved(self)
|
|
end
|
|
end
|
|
|
|
function MapCanvasMixin:RemoveAllDataProviders()
|
|
while table.getn(self.dataProviderOrder) > 0 do
|
|
self:RemoveDataProvider(self.dataProviderOrder[1])
|
|
end
|
|
end
|
|
|
|
-- One walk, one guard: a provider that raises must not stop the providers after it, because a
|
|
-- third-party provider is ordinary here and a half-notified map draws a half-built view.
|
|
function MapCanvasMixin:CallDataProviders(method, a, b, c)
|
|
local providers = self.dataProviderOrder
|
|
for i = 1, table.getn(providers) do
|
|
local provider = providers[i]
|
|
local fn = provider[method]
|
|
if fn then
|
|
local ok, err = pcall(fn, provider, a, b, c)
|
|
if not ok then
|
|
geterrorhandler()(err)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function MapCanvasMixin:RefreshAllDataProviders(fromOnShow)
|
|
self:CallDataProviders("RefreshAllData", fromOnShow)
|
|
end
|
|
|
|
function MapCanvasMixin:OnMapChanged()
|
|
self:CallDataProviders("OnMapChanged")
|
|
end
|
|
|
|
function MapCanvasMixin:OnCanvasScaleChanged()
|
|
local pins = self.activePins
|
|
for i = 1, table.getn(pins) do
|
|
local pin = pins[i]
|
|
if pin.OnCanvasScaleChanged then
|
|
pin:OnCanvasScaleChanged()
|
|
end
|
|
end
|
|
self:CallDataProviders("OnCanvasScaleChanged")
|
|
end
|
|
|
|
function MapCanvasMixin:OnCanvasPanChanged()
|
|
local pins = self.activePins
|
|
for i = 1, table.getn(pins) do
|
|
local pin = pins[i]
|
|
if pin.OnCanvasPanChanged then
|
|
pin:OnCanvasPanChanged()
|
|
end
|
|
end
|
|
self:CallDataProviders("OnCanvasPanChanged")
|
|
end
|
|
|
|
function MapCanvasMixin:OnCanvasSizeChanged()
|
|
local pins = self.activePins
|
|
for i = 1, table.getn(pins) do
|
|
local pin = pins[i]
|
|
if pin.ApplyCurrentPosition then
|
|
pin:ApplyCurrentPosition()
|
|
end
|
|
end
|
|
self:CallDataProviders("OnCanvasSizeChanged")
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Pins
|
|
--------------------------------------------------------------------------------
|
|
|
|
function MapCanvasMixin:GetPinPool(pinTemplate)
|
|
local pool = self.pinPools[pinTemplate]
|
|
if not pool then
|
|
local frameType = self.pinTemplateTypes[pinTemplate] or "Frame"
|
|
pool = CreateFramePool(frameType, self:GetCanvas(), pinTemplate, function(_, pin)
|
|
pin:Hide()
|
|
pin:ClearAllPoints()
|
|
if pin.OnReleased then
|
|
pin:OnReleased()
|
|
end
|
|
end)
|
|
self.pinPools[pinTemplate] = pool
|
|
end
|
|
return pool
|
|
end
|
|
|
|
-- Declare the frame type a template needs, for the templates that are not plain Frames. Era's own
|
|
-- `SetPinTemplateType`; without it a Button-shaped pin would be created as a Frame and its
|
|
-- `SetScript("OnClick")` would be nil.
|
|
function MapCanvasMixin:SetPinTemplateType(pinTemplate, pinFrameType)
|
|
self.pinTemplateTypes[pinTemplate] = pinFrameType
|
|
end
|
|
|
|
-- Declared vararg so `arg` is this function's own table: `LUA_COMPAT_VARARG` hands it over to a
|
|
-- vararg function that does not otherwise mention `...` (`script/lua50.rs`), and without the `...`
|
|
-- `arg` would resolve to the *global* one a script handler leaves behind.
|
|
function MapCanvasMixin:AcquirePin(pinTemplate, ...)
|
|
local extra = arg
|
|
local pool = self:GetPinPool(pinTemplate)
|
|
local pin, isNew = pool:Acquire()
|
|
pin.pinTemplate = pinTemplate
|
|
pin.owningMap = self
|
|
if isNew then
|
|
for k, v in pairs(MapCanvasPinMixin) do
|
|
if pin[k] == nil then
|
|
pin[k] = v
|
|
end
|
|
end
|
|
if pin.OnLoad then
|
|
pin:OnLoad()
|
|
end
|
|
end
|
|
self.activePins[table.getn(self.activePins) + 1] = pin
|
|
pin:Show()
|
|
pin:ApplyFrameLevel()
|
|
if pin.OnAcquired then
|
|
if extra and table.getn(extra) > 0 then
|
|
pin:OnAcquired(unpack(extra))
|
|
else
|
|
pin:OnAcquired()
|
|
end
|
|
end
|
|
pin:ApplyCurrentScale()
|
|
return pin
|
|
end
|
|
|
|
function MapCanvasMixin:RemovePin(pin)
|
|
for i = 1, table.getn(self.activePins) do
|
|
if self.activePins[i] == pin then
|
|
table.remove(self.activePins, i)
|
|
break
|
|
end
|
|
end
|
|
local pool = pin.pinTemplate and self.pinPools[pin.pinTemplate]
|
|
if pool then
|
|
pool:Release(pin)
|
|
else
|
|
pin:Hide()
|
|
end
|
|
end
|
|
|
|
function MapCanvasMixin:RemoveAllPinsByTemplate(pinTemplate)
|
|
local i = 1
|
|
while i <= table.getn(self.activePins) do
|
|
local pin = self.activePins[i]
|
|
if pin.pinTemplate == pinTemplate then
|
|
table.remove(self.activePins, i)
|
|
local pool = self.pinPools[pinTemplate]
|
|
if pool then
|
|
pool:Release(pin)
|
|
else
|
|
pin:Hide()
|
|
end
|
|
else
|
|
i = i + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
function MapCanvasMixin:RemoveAllPins()
|
|
while table.getn(self.activePins) > 0 do
|
|
self:RemovePin(self.activePins[1])
|
|
end
|
|
end
|
|
|
|
-- An iterator, because that is what every caller writes: `for pin in
|
|
-- map:EnumeratePinsByTemplate(t) do`. A template with no pins answers an iterator that stops
|
|
-- immediately, never nil, so the loop is always safe.
|
|
function MapCanvasMixin:EnumeratePinsByTemplate(pinTemplate)
|
|
local pins = self.activePins
|
|
local i = 0
|
|
return function()
|
|
while true do
|
|
i = i + 1
|
|
local pin = pins[i]
|
|
if not pin then
|
|
return nil
|
|
end
|
|
if pin.pinTemplate == pinTemplate then
|
|
return pin
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function MapCanvasMixin:EnumerateAllPins()
|
|
local pins = self.activePins
|
|
local i = 0
|
|
return function()
|
|
i = i + 1
|
|
return pins[i]
|
|
end
|
|
end
|
|
|
|
function MapCanvasMixin:GetNumActivePinsByTemplate(pinTemplate)
|
|
local n = 0
|
|
for i = 1, table.getn(self.activePins) do
|
|
if self.activePins[i].pinTemplate == pinTemplate then
|
|
n = n + 1
|
|
end
|
|
end
|
|
return n
|
|
end
|
|
|
|
function MapCanvasMixin:GetHoveredPin()
|
|
for i = 1, table.getn(self.activePins) do
|
|
local pin = self.activePins[i]
|
|
if pin:IsShown() and pin:IsMouseOver() then
|
|
return pin
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Handlers: canvas clicks, global pin mouse actions, the cursor
|
|
--
|
|
-- Each is a priority-sorted list, highest first, and the first handler that returns true consumes
|
|
-- the event. That ordering is the contract — a canvas addon registers at priority 1000 precisely
|
|
-- so it sees a click before Blizzard's own handlers.
|
|
--------------------------------------------------------------------------------
|
|
|
|
local function InsertByPriority(list, handler, priority)
|
|
local entry = { handler = handler, priority = priority or 0 }
|
|
local n = table.getn(list)
|
|
local at = n + 1
|
|
for i = 1, n do
|
|
if (list[i].priority or 0) < entry.priority then
|
|
at = i
|
|
break
|
|
end
|
|
end
|
|
table.insert(list, at, entry)
|
|
end
|
|
|
|
function MapCanvasMixin:AddCanvasClickHandler(handler, priority)
|
|
InsertByPriority(self.canvasClickHandlers, handler, priority)
|
|
end
|
|
|
|
function MapCanvasMixin:AddGlobalPinMouseActionHandler(handler, priority)
|
|
InsertByPriority(self.globalPinMouseActionHandlers, handler, priority)
|
|
end
|
|
|
|
function MapCanvasMixin:AddCursorHandler(handler, priority)
|
|
InsertByPriority(self.cursorHandlers, handler, priority)
|
|
end
|
|
|
|
function MapCanvasMixin:ProcessCanvasClickHandlers(button, cursorX, cursorY)
|
|
local list = self.canvasClickHandlers
|
|
for i = 1, table.getn(list) do
|
|
local ok, consumed = pcall(list[i].handler, self, button, cursorX, cursorY)
|
|
if not ok then
|
|
geterrorhandler()(consumed)
|
|
elseif consumed then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function MapCanvasMixin:ProcessGlobalPinMouseActionHandlers(mouseAction, button)
|
|
local list = self.globalPinMouseActionHandlers
|
|
for i = 1, table.getn(list) do
|
|
local ok, consumed = pcall(list[i].handler, self, mouseAction, button)
|
|
if not ok then
|
|
geterrorhandler()(consumed)
|
|
elseif consumed then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function MapCanvasMixin:ProcessCursorHandlers()
|
|
local list = self.cursorHandlers
|
|
for i = 1, table.getn(list) do
|
|
local ok, cursor = pcall(list[i].handler, self)
|
|
if not ok then
|
|
geterrorhandler()(cursor)
|
|
elseif cursor then
|
|
return cursor
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Navigation
|
|
--------------------------------------------------------------------------------
|
|
|
|
function MapCanvasMixin:NavigateToMap(mapID)
|
|
self:SetMapID(mapID)
|
|
end
|
|
|
|
function MapCanvasMixin:NavigateToParentMap()
|
|
local info = self.mapID and C_Map.GetMapInfo(self.mapID)
|
|
if info and info.parentMapID and info.parentMapID ~= 0 then
|
|
self:SetMapID(info.parentMapID)
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
function MapCanvasMixin:NavigateToCursor()
|
|
local sc = self.ScrollContainer
|
|
if not sc or not self.mapID then
|
|
return false
|
|
end
|
|
local x, y = sc:GetNormalizedCursorPosition()
|
|
local info = C_Map.GetMapInfoAtPosition(self.mapID, x, y)
|
|
if info and info.mapID ~= self.mapID then
|
|
self:SetMapID(info.mapID)
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- The map change
|
|
--------------------------------------------------------------------------------
|
|
|
|
function MapCanvasMixin:SetMapID(mapID)
|
|
if not mapID or self.mapID == mapID then
|
|
return
|
|
end
|
|
self.mapID = mapID
|
|
if self.ScrollContainer then
|
|
self.ScrollContainer.mapID = mapID
|
|
end
|
|
-- The stock window follows, so the 1.12 detail art under this canvas and every 1.12 addon's
|
|
-- icons on it are the same place the canvas shows. The engine's own selection is the authority
|
|
-- for both; see `BenillaSelectMapID`.
|
|
BenillaSelectMapID(mapID)
|
|
self:RefreshDetailLayers()
|
|
if self.ScrollContainer then
|
|
self.ScrollContainer:OnCanvasSizeChanged()
|
|
self.ScrollContainer:ResetZoom()
|
|
end
|
|
self:OnMapChanged()
|
|
self:RefreshAllDataProviders(false)
|
|
end
|
|
|
|
-- Follow the stock window when something else moved it (its dropdowns, `SetMapToCurrentZone`,
|
|
-- another addon's `SetMapZoom`), without calling back into it.
|
|
function MapCanvasMixin:SyncFromSelection()
|
|
local mapID = BenillaMapIDForSelection()
|
|
if not mapID or mapID == self.mapID then
|
|
return
|
|
end
|
|
self.mapID = mapID
|
|
if self.ScrollContainer then
|
|
self.ScrollContainer.mapID = mapID
|
|
end
|
|
self:RefreshDetailLayers()
|
|
if self.ScrollContainer then
|
|
self.ScrollContainer:OnCanvasSizeChanged()
|
|
self.ScrollContainer:ResetZoom()
|
|
end
|
|
self:OnMapChanged()
|
|
self:RefreshAllDataProviders(false)
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- The detail layers
|
|
--
|
|
-- Era's canvas builds its base art from a pool of layer frames. Ours has exactly one layer and it
|
|
-- is 1.12's own `WorldMapDetailFrame`, reparented into the canvas — so the base art is whatever
|
|
-- the stock window draws, from the same selection, and there is no second copy of the tiling law
|
|
-- to disagree with it. The pool exists because a canvas addon enumerates it to hide the base art
|
|
-- while it draws its own (`for layer in map.detailLayerPool:EnumerateActive() do`), and that is
|
|
-- the only thing anyone does with it.
|
|
--------------------------------------------------------------------------------
|
|
|
|
local DetailLayerPoolMixin = {}
|
|
|
|
function DetailLayerPoolMixin:EnumerateActive()
|
|
local layers = self.layers
|
|
local i = 0
|
|
return function()
|
|
i = i + 1
|
|
return layers[i]
|
|
end
|
|
end
|
|
|
|
function DetailLayerPoolMixin:GetNumActive()
|
|
return table.getn(self.layers)
|
|
end
|
|
|
|
function MapCanvasMixin:RefreshDetailLayers()
|
|
-- `WorldMapFrame_Update` is the stock repaint, driven by `WORLD_MAP_UPDATE`; nothing to do
|
|
-- here but let a hook see the call, which is what a canvas addon hooks it for.
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- MapUtil
|
|
--------------------------------------------------------------------------------
|
|
|
|
MapUtil = {}
|
|
|
|
function MapUtil.GetDisplayableMapForPlayer()
|
|
local mapID = C_Map.GetBestMapForUnit("player")
|
|
if mapID then
|
|
return mapID
|
|
end
|
|
return C_Map.GetFallbackWorldMapID()
|
|
end
|
|
|
|
-- The name of the smallest thing under the cursor: a sub-area's if its highlight claims the
|
|
-- point, else the child map's, else nothing. Over `C_Map`, so the name is the one the stock
|
|
-- window's own hover would show for that point.
|
|
function MapUtil.FindBestAreaNameAtMouse(mapID, x, y)
|
|
if not mapID or not x or not y then
|
|
return nil
|
|
end
|
|
local info = C_Map.GetMapInfoAtPosition(mapID, x, y)
|
|
if info and info.mapID ~= mapID then
|
|
return info.name
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function MapUtil.ShouldMapTypeShowQuests()
|
|
return true
|
|
end
|
|
|
|
function MapUtil.IsMapTypeAllowed(mapType)
|
|
return mapType == Enum.UIMapType.Continent or mapType == Enum.UIMapType.Zone
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Bolting the canvas onto 1.12's WorldMapFrame
|
|
--------------------------------------------------------------------------------
|
|
|
|
local function Mix(target, mixin)
|
|
for k, v in pairs(mixin) do
|
|
target[k] = v
|
|
end
|
|
end
|
|
|
|
local function BuildDefaultFrameLevels(manager)
|
|
-- Blizzard's own band names and order, bottom to top. The names are the contract: a canvas
|
|
-- addon inserts its own band relative to one of these by name, so a missing or renamed band is
|
|
-- art landing in the wrong place.
|
|
manager:InsertFrameLevel("PIN_FRAME_LEVEL_TERRAIN", 4)
|
|
manager:InsertFrameLevel("PIN_FRAME_LEVEL_MAP_EXPLORATION", 4)
|
|
manager:InsertFrameLevel("PIN_FRAME_LEVEL_MAP_HIGHLIGHT", 4)
|
|
manager:InsertFrameLevel("PIN_FRAME_LEVEL_AREA_POI", 8)
|
|
manager:InsertFrameLevel("PIN_FRAME_LEVEL_FLIGHT_POINT", 8)
|
|
manager:InsertFrameLevel("PIN_FRAME_LEVEL_WORLD_QUEST", 16)
|
|
manager:InsertFrameLevel("PIN_FRAME_LEVEL_STORY_LINE", 8)
|
|
manager:InsertFrameLevel("PIN_FRAME_LEVEL_GROUP_MEMBER", 8)
|
|
manager:InsertFrameLevel("PIN_FRAME_LEVEL_CORPSE", 4)
|
|
manager:InsertFrameLevel("PIN_FRAME_LEVEL_UNIT", 8)
|
|
manager:InsertFrameLevel("PIN_FRAME_LEVEL_MAP_LINK", 4)
|
|
manager:InsertFrameLevel("PIN_FRAME_LEVEL_DEATH_RELEASE", 4)
|
|
end
|
|
|
|
-- The pan mechanism: the canvas's own TOPLEFT against the container's, which is Blizzard's own
|
|
-- way and the one whose signs this file controls. A ScrollFrame's scroll offsets would do it with
|
|
-- no layout touch, but their sign convention is the engine's rather than this file's, and a pan
|
|
-- already dirties the layout of everything on the canvas.
|
|
function MapCanvasScrollControllerMixin:ApplyCurrentScroll()
|
|
local child = self.Child
|
|
if not child then
|
|
return
|
|
end
|
|
local scale = self:GetCanvasScale()
|
|
local cw = child:GetWidth() * scale
|
|
local ch = child:GetHeight() * scale
|
|
-- The canvas point under the container's middle is `currentScroll`; place the canvas so that
|
|
-- point lands there. Divided by the scale because the offsets are in the canvas's own units,
|
|
-- which the scale multiplies.
|
|
local offsetX = ((self.currentScrollX or 0.5) * cw - self:GetWidth() * 0.5) / scale
|
|
local offsetY = ((self.currentScrollY or 0.5) * ch - self:GetHeight() * 0.5) / scale
|
|
child:ClearAllPoints()
|
|
child:SetPoint("TOPLEFT", self, "TOPLEFT", -offsetX, offsetY)
|
|
end
|
|
|
|
local function BuildCanvas()
|
|
local map = WorldMapFrame
|
|
local detail = WorldMapDetailFrame
|
|
local button = WorldMapButton
|
|
if not map or not detail or not button or map.ScrollContainer then
|
|
return
|
|
end
|
|
|
|
Mix(map, MapCanvasMixin)
|
|
map:OnLoad()
|
|
|
|
-- The container takes the detail frame's own rect, so at rest the canvas is exactly where the
|
|
-- stock art was and the window is pixel-identical. A real ScrollFrame, for the clipping its
|
|
-- scroll child and every descendant of it get (see this file's header).
|
|
local sc = CreateFrame("ScrollFrame", "WorldMapScrollContainer", map)
|
|
sc:SetWidth(detail:GetWidth())
|
|
sc:SetHeight(detail:GetHeight())
|
|
-- The detail frame's **own** XML anchor, copied rather than referenced: once it is reparented
|
|
-- into the canvas below, anchoring the container to it would be a cycle — container to detail
|
|
-- to canvas to container — and a cycle resolves to nothing, which showed up as
|
|
-- `WorldMapButton:GetCenter()` answering nil inside the stock hover pass. The measured offsets
|
|
-- against `WorldMapFrame` are the fallback for a chain whose map window is laid out some other
|
|
-- way.
|
|
local guide = WorldMapPositioningGuide
|
|
if guide then
|
|
sc:SetPoint("TOPLEFT", guide, "TOP", -502, -69)
|
|
else
|
|
local l, t = detail:GetLeft(), detail:GetTop()
|
|
local ml, mt = map:GetLeft(), map:GetTop()
|
|
if l and t and ml and mt then
|
|
sc:SetPoint("TOPLEFT", map, "TOPLEFT", l - ml, t - mt)
|
|
else
|
|
sc:SetPoint("TOPLEFT", map, "TOPLEFT", 0, 0)
|
|
end
|
|
end
|
|
-- No mouse of its own: the map's mouse surface is 1.12's `WorldMapButton`, which covers the
|
|
-- same rect and is above this in the tree, so a mouse-enabled container here would never see
|
|
-- a press anyway. The hooks below drive the controller from that button instead.
|
|
sc:EnableMouse(false)
|
|
|
|
local canvas = CreateFrame("Frame", "WorldMapCanvas", sc)
|
|
canvas:SetWidth(detail:GetWidth())
|
|
canvas:SetHeight(detail:GetHeight())
|
|
sc:SetScrollChild(canvas)
|
|
|
|
Mix(sc, MapCanvasScrollControllerMixin)
|
|
sc.Child = canvas
|
|
sc.mapCanvas = map
|
|
sc:OnLoad()
|
|
|
|
map.ScrollContainer = sc
|
|
map.Canvas = canvas
|
|
|
|
-- 1.12's art and its mouse surface become the canvas's content: the twelve detail tiles, the
|
|
-- exploration overlays, the hover highlight, the POI buttons, the player arrow and the party
|
|
-- and raid pins — and with them every 1.12 addon's icons, which are all anchored to or
|
|
-- parented into these two frames. They now pan, scale and clip with the canvas, which is the
|
|
-- whole reason the canvas is built around them instead of beside them: a 1.12 quest icon stays
|
|
-- on its quest while a canvas addon moves the view.
|
|
detail:SetParent(canvas)
|
|
detail:ClearAllPoints()
|
|
detail:SetAllPoints(canvas)
|
|
button:SetParent(canvas)
|
|
|
|
-- The base-art layer a canvas addon hides while it draws its own. It is a proxy rather than the
|
|
-- detail frame itself, and that distinction is the point: `Hide()` on the frame would take the
|
|
-- POI buttons and every addon's icons down with it, because they are its children and its
|
|
-- siblings' children. Switching off its BACKGROUND and ARTWORK **draw layers** removes exactly
|
|
-- the stock tiles, the exploration overlays and the hover highlight — regions, all three —
|
|
-- and leaves every child frame drawing.
|
|
local baseLayer = {
|
|
SetShown = function(self, shown)
|
|
if shown then
|
|
detail:EnableDrawLayer("BACKGROUND")
|
|
detail:EnableDrawLayer("ARTWORK")
|
|
else
|
|
detail:DisableDrawLayer("BACKGROUND")
|
|
detail:DisableDrawLayer("ARTWORK")
|
|
end
|
|
self.shown = shown and true or false
|
|
end,
|
|
Show = function(self)
|
|
self:SetShown(true)
|
|
end,
|
|
Hide = function(self)
|
|
self:SetShown(false)
|
|
end,
|
|
IsShown = function(self)
|
|
return self.shown ~= false
|
|
end,
|
|
SetAlpha = function() end,
|
|
GetAlpha = function()
|
|
return 1
|
|
end,
|
|
}
|
|
map.detailLayerPool = { layers = { baseLayer } }
|
|
Mix(map.detailLayerPool, DetailLayerPoolMixin)
|
|
|
|
BuildDefaultFrameLevels(map.pinFrameLevelsManager)
|
|
|
|
-- The controller's input, off 1.12's own map button.
|
|
--
|
|
-- Its `<OnMouseUp>` *is* the 1.12 click-to-zoom (`WorldMapButton_OnClick`), so that one is
|
|
-- replaced rather than hooked: a canvas click handler that consumes the click must stop the
|
|
-- stock zoom, and a drag that ended over the map is not a click at all. Both of those are
|
|
-- decisions the controller has already made by the time the wrapper reads
|
|
-- `lastClickConsumed`, and neither can be expressed by a hook, which cannot refuse.
|
|
local stockMouseUp = button:GetScript("OnMouseUp")
|
|
button:SetScript("OnMouseUp", function()
|
|
sc:OnMouseUp(arg1)
|
|
if not sc.lastClickConsumed and stockMouseUp then
|
|
stockMouseUp()
|
|
end
|
|
end)
|
|
button:SetScript("OnMouseDown", function()
|
|
sc:OnMouseDown(arg1)
|
|
end)
|
|
button:SetScript("OnMouseWheel", function()
|
|
sc:OnMouseWheel(arg1)
|
|
end)
|
|
button:HookScript("OnUpdate", function()
|
|
sc:OnUpdate(arg1)
|
|
end)
|
|
button:EnableMouseWheel(true)
|
|
button:RegisterForClicks("LeftButtonUp", "RightButtonUp", "MiddleButtonUp")
|
|
|
|
-- Seat the first map before anything reads `GetMapID()`: the stock window has already resolved
|
|
-- the player's zone by the time this runs, so the canvas starts where the window is.
|
|
map.mapID = BenillaMapIDForSelection() or WORLD_MAP_ID
|
|
sc.mapID = map.mapID
|
|
sc:OnCanvasSizeChanged()
|
|
|
|
map:HookScript("OnShow", function()
|
|
WorldMapFrame:SyncFromSelection()
|
|
WorldMapFrame:RefreshAllDataProviders(true)
|
|
WorldMapFrame:CallDataProviders("OnShow")
|
|
end)
|
|
map:HookScript("OnHide", function()
|
|
WorldMapFrame:CallDataProviders("OnHide")
|
|
end)
|
|
end
|
|
|
|
local loader = CreateFrame("Frame", "BenillaMapCanvasLoader")
|
|
loader:RegisterEvent("PLAYER_LOGIN")
|
|
loader:RegisterEvent("WORLD_MAP_UPDATE")
|
|
loader:SetScript("OnEvent", function()
|
|
if event == "PLAYER_LOGIN" then
|
|
BuildCanvas()
|
|
elseif event == "WORLD_MAP_UPDATE" and WorldMapFrame and WorldMapFrame.SyncFromSelection then
|
|
-- Something moved the stock window: its dropdowns, `SetMapToCurrentZone`, another addon.
|
|
-- The canvas follows without calling back, so the two cannot fight over the selection.
|
|
WorldMapFrame:SyncFromSelection()
|
|
end
|
|
end)
|