Repository navigation
Expand file tree
/
Copy pathFrame.lua
More file actions
400 lines (386 loc) · 19 KB
/
Copy pathFrame.lua
File metadata and controls
400 lines (386 loc) · 19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
local _, Maps = ...
Maps.frameWidgetAPI=1
-- Alpha chrome surrounds the existing map. Its canvas and data providers stay
-- owned by Blizzard and the addons which created them.
function Maps:UseCustomFrame()
return PatchTools and PatchTools.isClassicEra and self.db.customFrame
and not (self.map.IsMaximized and self.map:IsMaximized())
end
function Maps:GetCustomFrameInsets(width)
local bottom=24
-- Reserve another row only when an addon actually registered fixed info.
for _,entry in ipairs(self.frameWidgets or {}) do
if entry.slot~="buttons" and entry.frame:IsShown() then
bottom=math.max(bottom,28+math.min(32,entry.frame:GetHeight()))
end
end
return 4,-36-self:GetToolbarLayout(width).extraHeight,-4,bottom+(self.novaRowHeight or 0)
end
function Maps:GetCustomFrameHeight(width)
local left,top,right,bottom=self:GetCustomFrameInsets(width)
local layers=C_Map.GetMapArtLayers(self.map:GetMapID())
local art=layers and layers[1]
local ratio=(self.originalHeight-50)/(self.originalWidth-30)
if art and type(art.layerWidth)=="number" and art.layerWidth>0
and type(art.layerHeight)=="number" and art.layerHeight>0 then
ratio=art.layerHeight/art.layerWidth
end
-- Keep the viewport in the map art's aspect ratio. A fixed outer-window
-- ratio plus fixed toolbar heights creates a growing letterbox at fit zoom.
return (width-left+right)*ratio-top+bottom
end
local function Allowed(frame)
return (type(frame)=="table" or type(frame)=="userdata") and type(frame.GetNumPoints)=="function"
and not (type(frame.IsProtected)=="function" and frame:IsProtected())
end
function Maps:GetToolbarLayout(width)
width=width or self.map:GetWidth()
local available=math.max(24,width-18)
local layout={items={},total=0,extraHeight=0,rows=1}
for _,entry in ipairs(self.frameWidgets or {}) do
local widget=entry.frame
if entry.slot=="buttons" and Allowed(widget) and widget:IsShown() then
local scale=math.min(1,24/math.max(1,widget:GetHeight()),available/math.max(1,widget:GetWidth()))
local item={scale=scale,width=widget:GetWidth()*scale}
layout.items[entry]=item
layout.total=layout.total+item.width+3
end
end
layout.total=math.max(0,layout.total-3)
layout.stacked=layout.total>math.max(0,width-247)
local x,row=0,0
for _,entry in ipairs(self.frameWidgets or {}) do
local item=layout.items[entry]
if item then
if layout.stacked and x>0 and x+item.width>available then x=0; row=row+1 end
item.x,item.y=x,row*27
x=x+item.width+3
end
end
layout.rows=row+1
layout.width=math.max(1,math.min(layout.total,available))
if layout.stacked then layout.extraHeight=layout.rows*27 end
return layout
end
local function Snapshot(frame)
local entry={frame=frame,parent=frame:GetParent(),points={},scale=frame:GetScale()}
for i=1,frame:GetNumPoints() do entry.points[i]={frame:GetPoint(i)} end
return entry
end
local function Restore(entry)
local frame=entry.frame
frame:SetParent(entry.parent)
frame:SetScale(entry.scale)
frame:ClearAllPoints()
for _,point in ipairs(entry.points) do frame:SetPoint(unpack(point)) end
end
local function RestoreUnprotected(entry)
local widget=entry.frame
-- Combat repair may only restore existing, unprotected children in place.
-- Never reparent a widget or resize/reposition the protected map here.
if not Allowed(widget) or widget:GetParent()~=entry.parent then return end
local changed=widget:GetNumPoints()~=#entry.points
if not changed then
for i,point in ipairs(entry.points) do
local current={widget:GetPoint(i)}
for j=1,5 do if current[j]~=point[j] then changed=true; break end end
if changed then break end
end
end
if widget:GetScale()~=entry.scale then widget:SetScale(entry.scale) end
if not changed then return end
widget:ClearAllPoints()
for _,point in ipairs(entry.points) do widget:SetPoint(unpack(point)) end
end
function Maps:RestoreCombatFrame()
if not self.customControlsPlaced or not self.customFrame then return end
for _,entry in ipairs(self.frameWidgets or {}) do
if entry.docked then RestoreUnprotected(entry.docked) end
end
if self.combatCanvas then RestoreUnprotected(self.combatCanvas) end
end
-- Addons may register an existing unprotected widget. Never clone its click
-- handlers, change its visibility preference, or move map pins into the dock.
-- Slots: buttons, info-left, info-right.
function Maps:PositionHeaderTooltip(widget)
if not self.map or not self:UseCustomFrame() or not GameTooltip
or not GameTooltip:IsOwned(widget) then return end
GameTooltip:ClearAllPoints()
GameTooltip:SetPoint("BOTTOM",widget,"TOP",0,6)
GameTooltip:SetClampedToScreen(true)
end
function Maps:RegisterFrameWidget(id,frame,slot)
if type(id)~="string" or id=="" or not Allowed(frame) then return false end
if slot~="buttons" and slot~="info-left" and slot~="info-right" then return false end
self.frameWidgets=self.frameWidgets or {}
for _,entry in ipairs(self.frameWidgets) do
if entry.frame==frame then return entry.id==id end
if entry.id==id then return false end
end
self.frameWidgets[#self.frameWidgets+1]={id=id,frame=frame,slot=slot}
return true
end
function Maps:DiscoverFrameWidgets()
self:DiscoverDeathlogControl()
if not LibStub then return end
local buttons=LibStub("Krowi_WorldMapButtons-1.4",true)
if buttons then
for _,button in ipairs(type(buttons.Buttons)=="table" and buttons.Buttons or {}) do
-- The library sometimes includes Blizzard's overlay buttons.
-- Adopt only its own named buttons and ATT's standalone fallback.
local name=button:GetName()
if type(name)=="string" and name:match("^Krowi_WorldMapButtons%d+$") then
self:RegisterFrameWidget(name,button,"buttons")
end
end
-- LibStub upgrades reuse the table but replace its functions. Track
-- those functions, too, so a later library copy cannot drop our hook.
if type(buttons.SetPoints)=="function" and self.buttonLibrarySetPoints~=buttons.SetPoints then
hooksecurefunc(buttons,"SetPoints",function()
if not (InCombatLockdown and InCombatLockdown()) then self:DiscoverFrameWidgets() end
self:LayoutCustomFrame()
end)
self.buttonLibrarySetPoints=buttons.SetPoints
end
if type(buttons.Add)=="function" and self.buttonLibraryAdd~=buttons.Add then
hooksecurefunc(buttons,"Add",function()
-- Questie and ATT create their buttons after ADDON_LOADED.
-- Wait one frame for the caller to finish setting size/art.
C_Timer.After(0,function()
if not (InCombatLockdown and InCombatLockdown()) then
self:DiscoverFrameWidgets(); self:LayoutCustomFrame()
end
end)
end)
self.buttonLibraryAdd=buttons.Add
end
end
if _G["AllTheThings-WorldMap"] then
self:RegisterFrameWidget("ATT",_G["AllTheThings-WorldMap"],"buttons")
end
local ace=LibStub("AceAddon-3.0",true)
local silver=ace and ace:GetAddon("SilverDragon",true)
local ldb=silver and silver:GetModule("LDB",true)
if ldb and ldb.worldmap then self:RegisterFrameWidget("SilverDragon",ldb.worldmap,"buttons") end
end
function Maps:DiscoverDeathlogControl()
if not self.map or (InCombatLockdown and InCombatLockdown()) then return end
local checkbox=_G.DeathlogHeatmapCheckbox
local container=checkbox and type(checkbox.GetParent)=="function" and checkbox:GetParent()
-- Deathlog uses the same global name in its own statistics window. Adopt
-- only the world-map container, never that other checkbox or any heatmap pin.
if not self.deathlogControl and Allowed(container) and Allowed(checkbox)
and container:GetParent()==self.map:GetCanvas() and container.heatmap_checkbox==checkbox then
local control={container=container,checkbox=checkbox}
local broker=LibStub and LibStub("LibDataBroker-1.1",true)
local data=broker and broker:GetDataObjectByName("Deathlog")
local registered,button=self:RegisterMapAction("Deathlog.heatmap",{
style="minimap",
icon=data and data.icon or "Interface\\TARGETINGFRAME\\UI-TargetingFrame-Skull",
tooltip=function()
return self:T("DEATHLOG_HEATMAP",self:T(checkbox:GetChecked() and "STATE_ON" or "STATE_OFF"))
end,
isShown=function() return container:IsShown() and checkbox:IsShown() end,
isActive=function() return checkbox:GetChecked()==true end,
onClick=function(_,mouseButton)
if mouseButton~="LeftButton" or (InCombatLockdown and InCombatLockdown()) then return end
-- Run Deathlog's current handler; it owns the setting and refresh.
local click=checkbox:GetScript("OnClick")
if click then click(checkbox,"LeftButton") end
end,
})
if registered then control.button=button; self.deathlogControl=control end
end
self:LayoutDeathlogControl(self:UseCustomFrame())
self:UpdateMapActions()
end
function Maps:LayoutDeathlogControl(enabled)
local control=self.deathlogControl
if not control then return end
local container,checkbox=control.container,control.checkbox
if enabled then
if not control.original then
control.original={alpha=container:GetAlpha(),mouse=checkbox:IsMouseEnabled()}
end
-- Keep Deathlog's native anchors, parent and Show/Hide state untouched.
container:SetAlpha(0); checkbox:EnableMouse(false)
elseif control.original then
container:SetAlpha(control.original.alpha); checkbox:EnableMouse(control.original.mouse)
control.original=nil
end
end
function Maps:InstallCustomFrame(map)
if not (PatchTools and PatchTools.isClassicEra) then return end
local frame=CreateFrame("Frame",nil,map,"BackdropTemplate")
self.customFrame=frame
frame:Hide()
frame:SetAllPoints(map)
frame:SetFrameLevel(map:GetFrameLevel()+1)
frame:EnableMouse(false)
-- Use Blizzard's XML close handler directly. A secure action child would
-- implicitly protect the entire map and prevent combat layout restoration.
-- Parent this ordinary native close button to the map, not the chrome.
local close=CreateFrame("Button","PatchToolsMapsModernCloseButton",map,"UIPanelCloseButton")
self.customCloseButton=close
close:SetSize(32,32); close:SetScale(1)
close:SetPoint("TOPRIGHT",map,"TOPRIGHT",-4,-3)
close:SetFrameLevel(map:GetFrameLevel()+30)
close:SetFrameStrata("TOOLTIP")
close:RegisterForClicks("LeftButtonUp")
local listed=false
for _,name in ipairs(UISpecialFrames) do if name=="WorldMapFrame" then listed=true; break end end
if not listed then tinsert(UISpecialFrames,"WorldMapFrame") end
if WorldMapFrameCloseButton then
self.nativeCloseAppearance={alpha=WorldMapFrameCloseButton:GetAlpha(),mouse=WorldMapFrameCloseButton:IsMouseEnabled()}
end
frame:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8X8",
edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=10,
insets={left=2,right=2,top=2,bottom=2}})
frame:SetBackdropColor(.055,.045,.035,1)
frame:SetBackdropBorderColor(.48,.37,.23,1)
local header=CreateFrame("Button",nil,frame)
header:SetPoint("TOPLEFT",9,-4)
header:SetHeight(26); header:RegisterForDrag("LeftButton")
self.customHeader=header
header:RegisterForClicks("RightButtonUp")
header:SetScript("OnClick",function(_,button)
if button=="RightButton" then self:ToggleOpacityPopup(header) end
end)
header:SetScript("OnEnter",function()
GameTooltip:SetOwner(header,"ANCHOR_TOP"); GameTooltip:SetText(self:T("OPACITY_SHORTCUT")); GameTooltip:Show()
self:PositionHeaderTooltip(header)
end)
header:SetScript("OnLeave",function() GameTooltip:Hide() end)
header:HookScript("OnDragStart",function() if self.opacityPopup then self.opacityPopup:Hide() end end)
frame.title=header:CreateFontString(nil,"OVERLAY","GameFontNormal")
frame.title:SetPoint("LEFT",0,0); frame.title:SetPoint("RIGHT",-4,0)
frame.title:SetJustifyH("LEFT"); frame.title:SetText("PatchToolsMaps")
local dock=CreateFrame("Frame",nil,frame)
dock:SetPoint("TOPRIGHT",map,"TOPRIGHT",-70,-4); dock:SetSize(1,26)
dock:SetFrameLevel(map:GetFrameLevel()+20)
frame.buttons=dock
header:SetPoint("TOPRIGHT",dock,"TOPLEFT",-8,0)
local footer=CreateFrame("Frame",nil,frame)
footer:SetPoint("BOTTOMLEFT",9,5); footer:SetPoint("BOTTOMRIGHT",-24,5); footer:SetHeight(14)
footer:SetFrameLevel(map:GetFrameLevel()+20)
frame.footer=footer
frame.coordinates=footer:CreateFontString(nil,"OVERLAY","GameFontHighlightSmall")
frame.coordinates:SetPoint("TOPLEFT",0,0); frame.coordinates:SetPoint("TOPRIGHT",0,0)
frame.coordinates:SetJustifyH("LEFT")
self.frameControls={}
for _,control in ipairs({WorldMapFrameCloseButton,map.MaximizeMinimizeFrame}) do
if Allowed(control) then self.frameControls[#self.frameControls+1]=Snapshot(control) end
end
self.frameArt={}
for _,region in pairs({MiniBorderLeft,MiniBorderRight,MiniWorldMapTitle}) do
if region and type(region.GetAlpha)=="function" then
self.frameArt[#self.frameArt+1]={region=region,alpha=region:GetAlpha()}
end
end
-- Read-only coordinate updates; never change another addon's options.
local elapsedTotal=0
frame:SetScript("OnUpdate",function(_,elapsed)
elapsedTotal=elapsedTotal+elapsed
if elapsedTotal<.15 then return end
elapsedTotal=0
self:UpdateFrameCoordinates()
if InCombatLockdown and InCombatLockdown() then self:RestoreCombatFrame() end
local actionsChanged=self:UpdateMapActions()
if self:UpdateNovaTimers() or actionsChanged then self:Refresh() end
end)
end
function Maps:UpdateFrameCoordinates()
local frame=self.customFrame
if not frame or not self:UseCustomFrame() then return end
local mapID=self.map:GetMapID()
local info=mapID and C_Map.GetMapInfo and C_Map.GetMapInfo(mapID)
frame.title:SetText(info and info.name or WORLD_MAP or "PatchToolsMaps")
local player,cursor="—","—"
local playerMap=C_Map.GetBestMapForUnit and C_Map.GetBestMapForUnit("player")
local position=playerMap and C_Map.GetPlayerMapPosition and C_Map.GetPlayerMapPosition(playerMap,"player")
if position then
local x,y=position:GetXY()
if x and y then
local zone=C_Map.GetMapInfo(playerMap)
player=string.format("%.1f, %.1f",x*100,y*100)
if playerMap~=self.map:GetMapID() and zone then player=player.." ("..zone.name..")" end
end
end
local scroll=self.map.ScrollContainer
if scroll and scroll:IsMouseOver() and type(scroll.GetNormalizedCursorPosition)=="function" then
local x,y=scroll:GetNormalizedCursorPosition()
if x and y and x>=0 and x<=1 and y>=0 and y<=1 then cursor=string.format("%.1f, %.1f",x*100,y*100) end
end
frame.coordinates:SetText(self:T("FRAME_COORDS",player,cursor))
end
function Maps:LayoutCustomFrame()
if not self.customFrame or self.layingOutFrame then return end
if not self:CanUseMapInCombat() then self:RestoreCombatFrame(); return end
self.layingOutFrame=true
local enabled=self:UseCustomFrame()
if not enabled and self.opacityPopup then self.opacityPopup:Hide() end
self:LayoutDeathlogControl(enabled)
self:UpdateMapActions()
self:UpdateNovaTimers()
self.customFrame:SetShown(enabled)
self.customCloseButton:SetShown(enabled)
if WorldMapFrameCloseButton and self.nativeCloseAppearance then
WorldMapFrameCloseButton:SetAlpha(enabled and 0 or self.nativeCloseAppearance.alpha)
WorldMapFrameCloseButton:EnableMouse(not enabled and self.nativeCloseAppearance.mouse)
end
for _,entry in ipairs(self.frameArt) do entry.region:SetAlpha(enabled and 0 or entry.alpha) end
if enabled then
for _,entry in ipairs(self.frameControls) do
entry.frame:SetScale(1); entry.frame:ClearAllPoints()
entry.frame:SetPoint("TOPRIGHT",self.map,"TOPRIGHT",entry.frame==WorldMapFrameCloseButton and -4 or -35,-3)
end
self.customControlsPlaced=true
elseif self.customControlsPlaced then
for _,entry in ipairs(self.frameControls) do
Restore(entry)
local maximized=self.map.IsMaximized and self.map:IsMaximized()
entry.frame:SetScale(entry.scale*(maximized and 1 or self.windowDB.scale))
end
self.customControlsPlaced=nil
end
local toolbar=self:GetToolbarLayout()
if enabled then
local _,_,_,bottom=self:GetCustomFrameInsets()
self.customFrame.footer:SetHeight(bottom-10)
local dock=self.customFrame.buttons
dock:ClearAllPoints()
dock:SetPoint("TOPRIGHT",self.map,"TOPRIGHT",toolbar.stacked and -9 or -70,toolbar.stacked and -30 or -4)
dock:SetSize(toolbar.width,toolbar.rows*27)
self.customHeader:ClearAllPoints()
self.customHeader:SetPoint("TOPLEFT",self.customFrame,"TOPLEFT",9,-4)
if toolbar.stacked then self.customHeader:SetPoint("TOPRIGHT",self.map,"TOPRIGHT",-70,-4)
else self.customHeader:SetPoint("TOPRIGHT",dock,"TOPLEFT",-8,0) end
end
local left,right=0,0
for _,entry in ipairs(self.frameWidgets or {}) do
if Allowed(entry.frame) then
if enabled then
entry.original=entry.original or Snapshot(entry.frame)
local widget=entry.frame
local item=toolbar.items[entry]
local scale=item and item.scale or 1
widget:SetScale(scale)
widget:SetParent(entry.slot=="buttons" and self.customFrame.buttons or self.customFrame.footer)
widget:ClearAllPoints()
if entry.slot=="buttons" then
widget:SetPoint("TOPLEFT",self.customFrame.buttons,"TOPLEFT",item and item.x/scale or 0,item and -item.y/scale or 0)
elseif entry.slot=="info-left" then
widget:SetPoint("BOTTOMLEFT",self.customFrame.footer,"BOTTOMLEFT",left,self.novaRowHeight or 0)
if widget:IsShown() then left=left+widget:GetWidth()+8 end
else
widget:SetPoint("BOTTOMRIGHT",self.customFrame.footer,"BOTTOMRIGHT",-right,self.novaRowHeight or 0)
if widget:IsShown() then right=right+widget:GetWidth()+8 end
end
entry.docked=Snapshot(widget)
elseif entry.original then Restore(entry.original); entry.original=nil; entry.docked=nil end
end
end
self.combatCanvas=enabled and Snapshot(self.map.ScrollContainer) or nil
if enabled then self:UpdateFrameCoordinates() end
self.layingOutFrame=nil
end