-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathHouseEditorEnhancer.lua
More file actions
3908 lines (3564 loc) · 188 KB
/
Copy pathHouseEditorEnhancer.lua
File metadata and controls
3908 lines (3564 loc) · 188 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
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
local _, addon = ...
local L = LibStub("AceLocale-3.0"):GetLocale("LudiusPlus")
-- The Enhanced House Editor module bundles several independent features,
-- each with its own Setup/Teardown pair. SetupOrTeardownHouseEditorEnhancer
-- is a thin dispatcher that toggles each feature based on its config flag.
-- No hook, event, or frame is installed until its feature is first
-- enabled - a cold /reload with all flags off leaves zero footprint.
--
-- 1. ICON SIZE SLIDER and CTRL+WHEEL ZOOM (toggles:
-- houseEditorEnhancer_iconResizerSlider,
-- houseEditorEnhancer_iconResizerCtrlWheel)
-- Two independent ways to drive the same scaling state. The slider
-- flag controls a widget under the SearchBox; the CTRL+wheel flag
-- enables zoom by scrolling over the catalog tiles. Both share the
-- same stored size (houseEditorEnhancer_iconResizerSize) and either
-- one (or both) activating turns on our "parallel catalog" (a complete
-- rebuild of Blizzard's catalog scroll box to prevent taint) + scaling
-- hooks. The slider widget itself only appears when its specific flag
-- is on - CTRL+wheel-only mode keeps the SearchBox in its default
-- position.
-- Scaling is applied via ScrollTarget:SetScale on Blizzard's tiles
-- (Featured) and via per-tile SetSize on our parallel grid (all other
-- categories). The Featured catalog forces scale to 1.0 because its
-- wide bundle cards have fixed non-standard sizes that don't scale
-- meaningfully.
--
-- 2. CTRL+CLICK PREVIEW (toggle: houseEditorEnhancer_preview)
-- Ctrl+LeftClick on a tile opens a model preview anchored to the
-- right of the editor. Blizzard only provides such a preview in the
-- HousingDashboardFrame's catalog; the HouseEditor has none. We
-- reuse Blizzard's HousingModelPreviewTemplate so it looks identical.
-- To keep Ctrl+LeftClick from also starting decor placement, we wrap
-- each catalog entry frame's OnInteract method as it's acquired by
-- the ScrollBox and skip the original when our modifier conditions
-- match. Wrapping the shared mixin (HousingCatalogEntryMixin) would
-- not work: XML-level Mixin copies methods onto each frame instance
-- at creation time, so frames already own their own function
-- reference and bypass any later mixin-table edits.
-- The per-instance OnInteract replacement is a taint-write on a
-- Blizzard frame, but it's scoped to DECOR-type catalog entries
-- and is only reachable from a user click event - DECOR frames never
-- enter the GetProductInfo path (that's BUNDLE/SMALL_PRODUCT in
-- the "Featured" category of the catalog tab), so the taint has no
-- path to a protected C function in practice.
--
-- 3. CHAIN PLACEMENT (toggle: houseEditorEnhancer_chainPlacement)
-- When the user commits a decor placement (click or drag-release)
-- while holding SHIFT, and at least one copy of the same decor is
-- still in storage, immediately start a new placement of the same
-- item. Avoids round-tripping through the catalog UI for repeated
-- placements of the same decor. A small icon at the cursor shows the
-- remaining storage count while SHIFT is held. Implemented by hooking
-- C_HousingBasicMode.StartPlacingNewDecor (to remember the entry)
-- and FinishPlacingNewDecor (to fire the chain on commit).
--
-- 4. RECENT CATEGORY (toggle: houseEditorEnhancer_recent)
-- Adds a custom "Recent" button to the Storage tab's category pane; while
-- it's selected, the catalog lists the decor the player most recently
-- placed or modified (latest first) rendered through the parallel-catalog
-- grid. History is recorded by hooking placements (HOUSING_DECOR_PLACE_SUCCESS,
-- this includes movement, rotation and resize) and dye commits
-- (CommitDyesForSelectedDecor), and persisted per house x indoor/outdoor in
-- the LP_houseEditorRecent SavedVariable. It needs the parallel catalog to
-- render, so enabling it sets that up too - but the grid only goes
-- ACTIVE while the Recent view is open, so with the icon resizer
-- off, normal browsing keeps Blizzard's native catalog. Teardown purges
-- the SavedVariable so a disabled feature leaves no footprint.
--
-- Hooks installed via hooksecurefunc can't be removed; where we use
-- them, Setup installs once per session and the hook body runtime-gates
-- on its config flag so in-session disable is instant. Teardown removes
-- whatever it can (callbacks, events, frames) and leaves the idempotent
-- residue inert.
--
-- NOTE: the "parallel catalog" that features 1 and 4 stand on is not just a
-- scaling/scrolling shell - it is a hand-rebuilt 1:1 copy of Blizzard's catalog
-- tiles and category pane, so it ALSO reproduces decor/room placement, the tile
-- tooltip, the shift-click chat link, and the refund/destroy right-click menu.
-- That copied surface has to be re-checked against Blizzard's source every patch:
-- see the BLIZZARD SOURCE PROVENANCE section below for the map and the routine.
--
--
-- ============================================================================
-- TAINT LESSONS LEARNED (file these in your head before touching this code)
-- ============================================================================
--
-- This module's scroll box is HouseEditorFrame.StoragePanel.OptionsContainer.
-- ScrollBox. It uses ScrollBoxListMixin and a ScrollBoxListSequenceView.
-- The Featured catalog category populates the scroll box with frames whose
-- initializer (HousingMarketProductDisplayMixin:Init) calls the PROTECTED
-- C function C_CatalogShop.GetProductInfo. If anything we do leaves the
-- scroll box's internal state in a tainted condition before
-- RestoreFocusState -> SetFocus -> SetDataProvider -> FullUpdate -> Update
-- -> InvokeInitializers runs, that protected call fires
-- ADDON_ACTION_FORBIDDEN attributed to LudiusPlus. The original repro
-- (pre-parallel-catalog) was: /reload -> open editor -> resize the panel
-- -> click Market tab (which defaults to Featured). The parallel catalog
-- replaces Blizzard's grid on non-Featured tabs, sidestepping the trap;
-- the repro is kept here as the canonical example to keep the rules
-- below grounded in a concrete failure mode.
--
-- 1. HOOK MECHANISMS - WHAT'S SAFE
-- ----------------------------------------------------------------------------
-- * hooksecurefunc(target, "method", fn) - taint-safe by engine design.
-- The wrapper uses securecall semantics so the original runs cleanly
-- even when invoked from tainted code. Use this for everything you can.
-- * frame:HookScript("OnX", fn) - same engine-level isolation, for frame
-- scripts. Safe.
-- * Empirically verified: hooksecurefunc(scrollBox, "Update", emptyFn) on
-- our exact scroll box, with the hook body doing nothing, does NOT
-- cause FORBIDDEN. Hooks themselves do not induce taint.
--
-- 2. HOOK MECHANISMS - WHAT'S NOT SAFE
-- ----------------------------------------------------------------------------
-- * RegisterCallback on a Blizzard CallbackRegistry is a plain Lua table
-- write into target.callbacks[event][owner]. Our addon-defined function
-- becomes a tainted entry; when TriggerEvent iterates the table, the
-- tainted read propagates. For OnUpdate specifically this poisons Update's
-- own state (SetUpdateLocked at ScrollBox.lua:793 writes the tainted
-- state into self.isUpdateLocked) and contaminates the next Update.
-- OnAcquiredFrame's downstream happens not to hit a protected function,
-- so the preview feature's RegisterCallback is OK in practice - but it's
-- a latent risk.
--
-- 3. THE REAL TRAP - BLIZZARD FRAME METHODS WITH HIDDEN WRITES
-- ----------------------------------------------------------------------------
-- Some Blizzard frame methods that LOOK like pure reads actually trigger
-- internal writes (lazy layout calculations / cache fills). Invoked from
-- addon-tainted code on a scroll-box-managed frame, those internal writes
-- get tainted and the scroll box reads them during its next Update.
--
-- TAINTED when called on scroll-box-managed frames from addon code:
-- f:GetSize()
-- f:GetWidth() (assume GetHeight too)
-- ...assume any size/geometry accessor that needs a layout pass
--
-- SAFE on the same frames from the same context:
-- f:GetName()
-- f:GetNumPoints()
-- f:GetPoint(i)
-- tostring(f)
-- iteration via scrollBox:EnumerateFrames()
--
-- Bisected by stepping a hooksecurefunc'd diagnostic timer through each
-- line one at a time. The pattern is consistent: methods that compute
-- geometry trigger taint; methods that read static or pre-computed fields
-- don't. Writes (SetPoint, SetSize, etc.) are inherently more dangerous
-- and have not been verified - assume tainted until proven otherwise.
--
-- 4. INSTANCE vs MIXIN HOOK TARGETS
-- ----------------------------------------------------------------------------
-- WoW's XML mixin="X" attribute COPIES the mixin's methods to the frame at
-- creation time. By the time our addon's ADDON_LOADED runs, frames already
-- have their own copies. hooksecurefunc on the MIXIN table (e.g.
-- ScrollBoxListMixin) will not fire for those existing instances - the
-- read of self.Method finds the instance's copy, not the mixin's wrapper.
-- ALWAYS hook the INSTANCE (e.g. container.ScrollBox), not the mixin,
-- unless you can guarantee you ran before frame creation.
--
-- Exception: methods that are stored elsewhere by Blizzard at view-setup
-- time (e.g. scrollBox.OnViewAcquiredFrame, which the view captures as a
-- callback reference) can't be hooked at the instance level after the
-- fact either - the view still calls the captured original reference.
-- For those, RegisterCallback is unfortunately the only option.
--
--
-- ============================================================================
-- BLIZZARD SOURCE PROVENANCE - AND HOW TO RE-CHECK IT EACH PATCH (READ THIS!)
-- ============================================================================
--
-- LAST CHECKED AGAINST: wow-ui-source 12.0.7 (build 68453). When you run the
-- routine below, diff from this build to the new one and update this line.
--
-- Large parts of this module are a hand-rebuilt 1:1 copy of Blizzard's house
-- editor catalog (our "parallel catalog" replaces their ScrollBox to dodge the
-- taint trap above; our tiles, tooltips, context menu, room placement, and
-- category pane reproduce theirs). That means Blizzard can ADD or CHANGE catalog
-- behavior in a patch and our copy will silently fall behind - exactly how we
-- first shipped without room placement and without the shop right-click menu,
-- because we hadn't noticed Blizzard had them.
--
-- ROUTINE FOR EVERY MAJOR PATCH (do this before adjusting anything else):
-- 1. Update the local wow-ui-source checkout to the new build.
-- 2. `git diff` (or your viewer of choice) the files in the PROVENANCE MAP
-- below between the old and new build. Those are the only Blizzard files
-- we mirror; a diff with no changes there means our copy is still current.
-- 3. For each Blizzard change, find the matching mirror in THIS file. Every
-- mirror is tagged with a comment of the form
-- "Mirrors <Mixin>:<Method> (<File>.lua:<lines>)"
-- so you can grep this file for the Blizzard symbol that changed (e.g.
-- grep "UpdateTypeSpecificVisuals") and land on the code to update.
-- 4. Decide per change: ADOPT it (port into our mirror), or DELIBERATELY SKIP
-- it (and say why in the mirror's comment, so the next check doesn't keep
-- re-flagging it). Line numbers in the tags drift between builds - trust
-- the Mixin:Method NAME, not the number, and refresh the number when you
-- touch a mirror.
-- 5. Watch especially for NEW protected (C_CatalogShop / store) calls on any
-- path our tiles or category clicks can reach - those are the ones that
-- turn into ADDON_ACTION_FORBIDDEN. See the TAINT LESSONS above.
--
-- PROVENANCE MAP - Blizzard files we mirror, and what we took from each:
-- * Blizzard_HousingTemplates/Blizzard_HousingCatalogEntry.lua (+ .xml)
-- The per-tile mixins. Source for our CreateTile / UpdateTileVisuals /
-- OnEnter tooltip / OnClick+OnDrag placement / HandleTileRightClick:
-- - HousingCatalogEntryMixin (UpdateEntryData, UpdateVisuals, OnInteract,
-- HasValidData, UpdateBackground, OnMouseDown/Up)
-- - HousingCatalogDecorEntryMixin (TypeSpecificOnInteract, ShowContextMenu,
-- AddTooltipTitle/AddTooltipLines, UpdateTypeSpecificVisuals)
-- - HousingCatalogRoomEntryMixin (TypeSpecificOnInteract, AddTooltip*)
-- - BaseHousingCatalogEntryTemplate / DecorEntryTemplate /
-- HousingCatalogRoomEntryTemplate (frame layout, atlases, hover bg)
-- * Blizzard_HousingTemplates/Blizzard_HousingCatalogUtil.lua
-- Stateless helpers we call directly (NOT reimplemented): GetEntryQuantity,
-- GetEntryNumStored, GetEntryTotalOwned, FormatPrice, FormatRefundTime.
-- If a signature changes, our call sites change with it.
-- * Blizzard_HousingTemplates/Blizzard_HousingCatalogCategories.lua (+ .xml)
-- The category pane. Source for our Recent button styling, the room-category
-- and "unselected" fakes, and the SetFocus choke-point hook. Key symbols:
-- SetFocus, GetFocusedCategoryString, IsFeaturedCategoryFocused,
-- IsAllCategoryFocused, BaseHousingCatalogCategoryMixin, SelectedBackground.
-- * Blizzard_HousingTemplates/Blizzard_HousingCatalogTemplates.lua (+ .xml)
-- The scroll container/box: SetScrollBoxTopOffset (header offset we mirror),
-- widthSnapMultiplier (resize snap we neutralize), the MinimalScrollBar we
-- clone for our parallel scroll bar, and the template chooser (Decor/Room).
-- * Blizzard_HouseEditor/Blizzard_HouseEditorStorageFrame.lua
-- The storage/market panel that owns it all: OnTabChanged,
-- OnEntryResultsUpdated, OnCatalogEntryUpdated, OnCategoryFocusChanged,
-- UpdateCategoryText/Total, UpdateLoadingSpinner, RefreshMarketData,
-- Check{Start,Close}MarketInteraction, and the OnResizeStopped snap. We hook
-- several of these; we also work around two of its bugs (Featured->All label,
-- and the unhandled CATALOG_SHOP_FETCH_FAILURE - see MinimalWorkingExample).
-- * Blizzard_SharedXML/Shared/Scroll/* (ScrollBox.lua, ScrollBar.lua,
-- ScrollBoxViewUtil.lua, MinimalScrollBar.*)
-- Read-only reference for how the real scroll box/bar behave (edge fade math,
-- ScrollBarMixin:Update show/hide rules, dataIndex range calc). We don't
-- mirror these, but our parallel scroll math has to stay compatible.
-- * Blizzard_HousingModelPreview (LoadOnDemand)
-- Reused AS-IS for the CTRL+click preview (HousingModelPreviewTemplate); we
-- only load + drive it, so changes there rarely affect us.
--
-- IMPROVEMENTS OVER BLIZZARD (intentional divergences - keep them on re-check):
-- * Layout mode fake-selects the lone "Rooms" category and labels the header
-- "Rooms" (Blizzard leaves it unselected showing "All"). See UpdateRoomCategoryFake.
-- * Catalog "Featured"->"All" repaints the header to "All" (Blizzard leaves it
-- stuck on "Featured"). See FixFeaturedToAllCategoryText.
local math_log = _G.math.log
-- ===== Icon resizer: constants =====
local MIN_SCALE = 0.5
local MAX_SCALE = 3.55
local STEP = 0.005
-- Default SearchBox anchor y-offset in Blizzard's XML is -20. We shift
-- it upwards to make vertical room for the slider row underneath.
local SEARCH_BOX_DEFAULT_Y = -20
local SEARCH_BOX_SHIFTED_Y = -13
local DEFAULT_SCALE = 1.0
-- Reset button texture matches DynamicCam's reset buttons.
local RESET_TEXTURE = "Interface\\Transmogrify\\Transmogrify"
local RESET_TEXCOORDS = {0.58203125, 0.64453125, 0.30078125, 0.36328125}
-- ===== Preview: constants =====
local MIN_PREVIEW_WIDTH = 300
local MAX_PREVIEW_WIDTH = 1200
local DEFAULT_PREVIEW_WIDTH = 540
-- Gap between StoragePanel's right edge and preview's left edge. Used
-- both when anchoring and when computing how wide the preview can grow
-- before it'd hit the screen's right edge.
local PANEL_GAP = 8
-- ===== Module state =====
-- Slider:
local slider = nil
local sliderLabel = nil
local resetButton = nil
local categoriesFocusHooked = false
local updateControlStates = nil -- assigned on first slider creation
local refreshParallelOnSliderChange = nil -- forward-declared; assigned by parallel catalog setup
-- Preview:
local previewFrame = nil
local scrollBoxHooked = false -- per-frame OnInteract + OnEnter/OnLeave wrap
local previewButtonsHooked = false -- CollapseButton + StorageButton OnClick
local hoveredFrame = nil
local inspectCursorActive = false
-- True while UpdateTileVisuals re-fires a hovered tile's OnEnter to refresh its
-- tooltip in place. Suppresses the hover sound, since a re-render (e.g. on scroll)
-- is not a fresh hover.
local refreshingTooltipInPlace = false
-- Our OWN GameTooltip frame for the Market right-click "go buy it in Featured"
-- hint. Separate from the global GameTooltip so the tile's item tooltip stays
-- visible alongside it; hidden by the tile's HideHover on leave.
local featuredHintTooltip = nil
local lastPreviewEntryInfo = nil
local previewWasShownBeforeCollapse = false
-- Parallel Catalog:
local parallelFrame = nil
local parallelChild = nil
local parallelScrollBar = nil
local parallelTabHooked = false
local parallelResultsHooked = false
local parallelCategoryHooked = false
-- Re-renders room tiles on layout changes (budget/door) for live grey-out.
local parallelLayoutEventFrame = nil
local parallelLayoutEventsHooked = false
-- Captured once on first ShowParallelCatalog call; restored on deactivate.
local scrollBoxOriginalAnchors = nil
local scrollBarOriginalAnchors = nil
-- Blizzard's storage-panel resize column-snap multiplier (px), captured before we
-- neutralize it while our grid is active, and restored on deactivate (see
-- ShowParallelCatalog). nil until first captured.
local origWidthSnapMultiplier = nil
-- Forward-declared: WireParallelScroll is defined later (it needs RefreshTileGrid
-- in scope) but called from ShowParallelCatalog, which is defined earlier.
local WireParallelScroll
-- Recent:
-- Cap on the tracked history length (per list).
local RECENT_MAX = 200
-- POINTER to the current house+area saved list (LP_houseEditorRecent[key]);
-- re-pointed by SyncRecentList. Empty default until first sync.
local recentEntries = {}
-- True while the Recent view is showing.
local recentMode = false
-- True while a deferred Recent exit is waiting for the new category's results (see BeginRecentExit).
local recentExitPending = false
-- Recent was showing when Layout mode began; restore it on leaving Layout (see the UpdateEditorMode hook in SetupRecent).
local recentWasActiveBeforeLayout = false
-- Our side-pane toggle button (addon-owned frame).
local recentButton = nil
-- Click-through overlay making the unselected Rooms category button look
-- selected in Layout mode - a taint-free stand-in for the SetFocus we mustn't call.
local roomsCategoryFake = nil
-- Click-through overlay hiding the selected look of whichever pane button is
-- currently active (top-level category, subcategory, or "All subcategories"
-- standin) while the Recent view is showing. Adapts per-button by drawing that
-- button's own GetDefaultTexture(). Inverse counterpart of roomsCategoryFake.
local unselectedCategoryFake = nil
-- Guards preventing double-installation of hooks.
local recentRecordHooked = false
-- OnCategoryClicked / OnSearchTextUpdated "exit Recent" hooks installed.
local recentExitHooked = false
-- Listens for HOUSING_DECOR_PLACE_SUCCESS.
local recentPlaceEventFrame = nil
-- Exact entryVariantID of the in-progress fresh placement (stashed by StartPlacingNewDecor hook).
local pendingPlaceVariant = nil
-- These are defined down in the "Recent" section but called from earlier
-- sections (the parallel-catalog renderer + tile code), so they're forward-
-- declared here, with the rest of the Recent state:
-- Re-point recentEntries to the current house+area list (RefreshParallelCatalog).
local SyncRecentList
-- Exit Recent + restore Blizzard's header (RefreshParallelCatalog / exit hooks).
local LeaveRecentMode
-- Show/hide the side-pane toggle (RefreshParallelCatalog).
local UpdateRecentButtonVisibility
-- Drop one entry from history (called from a tile's hover delete button, in CreateTile).
local RemoveRecentEntry
-- Per-variant storage count (UpdateTileVisuals).
local VariantStoredCount
-- Per-RefreshTileGrid-pass memo for VariantStoredCount (wiped at start of each pass).
local variantStoredCache = {}
-- Tile grid:
-- Hoisted here (rather than near RefreshTileGrid) so they're in scope for
-- EnsureParallelFrame and CreateTile, which are defined earlier in the file.
local TILE_WIDTH = 97
local TILE_HEIGHT = 97
local TILE_SPACING = 5
local TOP_PADDING = 0
local OVERSCAN_ROWS = 1
local EDGE_FADE_LENGTH = 75 -- matches Blizzard housing catalog edge fade
-- Tile pool (reusable Button frames) and current active list.
-- AcquireTile pops from the pool or creates fresh; ReleaseAllTiles hides
-- the current actives and pushes them back so the next render reuses them.
local tilePool = {}
local activeTiles = {}
-- ===== Feature-state predicates =====
-- True if any of the icon-resizing features is enabled. The slider UI and
-- the CTRL+wheel handler are two independent ways to drive the same
-- resize state, so the parallel catalog and the scaling hooks must
-- activate when EITHER is on.
local function IsAnyIconResizingActive()
return LP_config.houseEditorEnhancer_iconResizerSlider
or LP_config.houseEditorEnhancer_iconResizerCtrlWheel
end
local function IsRecentEnabled()
return LP_config.houseEditorEnhancer_recent
end
-- The parallel catalog (our custom tile grid) is the renderer for BOTH the
-- icon resizer and the Recent view, so it must be SET UP whenever either is
-- enabled. (Being set up only installs hooks; it stays dormant - Blizzard's
-- native catalog shows - until RefreshParallelCatalog makes it active.)
local function IsParallelCatalogNeeded()
return IsAnyIconResizingActive() or IsRecentEnabled()
end
-- ===== Icon resizer & shared scroll-box helpers =====
-- The Featured catalog category uses wide bundle cards with fixed,
-- non-standard sizes. Scaling them via ScrollTarget:SetScale just makes
-- them overflow the panel. We skip our scaling in Featured (both the
-- slider AND the CTRL+wheel zoom). Everywhere else (storage tab and all
-- non-Featured market categories) scaling applies normally.
--
-- Pure read of Blizzard-set state via the Categories mixin method; no
-- Lua field write, no taint.
local function IsFeaturedCategoryFocused()
local storagePanel = HouseEditorFrame and HouseEditorFrame.StoragePanel
local categories = storagePanel and storagePanel.Categories
if not categories or not categories.IsFeaturedCategoryFocused then return false end
return categories:IsFeaturedCategoryFocused()
end
local function GetScale()
if not IsAnyIconResizingActive() then
return 1.0
end
if IsFeaturedCategoryFocused() then
return 1.0
end
return LP_config.houseEditorEnhancer_iconResizerSize or 1.0
end
local function GetContainer()
return HouseEditorFrame
and HouseEditorFrame.StoragePanel
and HouseEditorFrame.StoragePanel.OptionsContainer
end
-- Apply the current slider scale to the ScrollTarget so tiles appear
-- visually smaller or larger without touching elementSizeCalculator or
-- calling Rebuild (both would taint scroll-view Lua fields that Blizzard's
-- Update reads, causing ADDON_ACTION_FORBIDDEN via GetProductInfo).
-- Column count is unaffected. Blizzard's OnResizeStopped snaps the panel width to
-- whole default-icon columns (widthSnapMultiplier = 102px), which fights our custom
-- icon sizes; while our grid is active we set that multiplier to 1 so the snap is a
-- no-op and resizing stays seamless (see the snap note in ShowParallelCatalog).
local function RefreshCatalog()
local container = GetContainer()
if not container or not container.ScrollBox then return end
local scrollBox = container.ScrollBox
if scrollBox.ScrollTarget then
scrollBox.ScrollTarget:SetScale(GetScale())
end
end
-- ===== Preview: helpers =====
local function HidePreview()
if not previewFrame then return end
previewFrame:Hide()
previewFrame:ClearPreviewData()
end
-- Predicate: is hoveredFrame a tile our CTRL+click preview can act on?
-- Two flavors:
-- - HousingCatalogEntryMixin-based tiles (Storage, non-Featured Market,
-- and bundle-item decor tiles in Featured) have HasValidData.
-- - HousingMarketProductDisplayMixin-based tiles (Featured Small Product
-- cards) don't; we use the same elementData check as the StartPreview
-- hook so the inspect cursor and the click both light up on the same
-- set of frames (single-decor Small Products, not Bundle wide cards).
local function IsHoveredFramePreviewable()
local f = hoveredFrame
if not f then return false end
-- Rooms (Layout mode) aren't previewable - the preview frame is decor-only,
-- so CTRL must NOT flip to the inspect cursor over them. Both our tiles and
-- Blizzard's native room tiles expose entryVariantID and report
-- HasValidData()==true, so without this they'd wrongly read as previewable.
if f.entryVariantID and f.entryVariantID.entryType == Enum.HousingCatalogEntryType.Room then
return false
end
if type(f.HasValidData) == "function" then
return f:HasValidData()
end
if f.elementData and f.elementData.canPreview
and f.elementData.entryVariantID
and f.elementData.entryVariantID.entryType == Enum.HousingCatalogEntryType.Decor then
return true
end
return false
end
-- Show the inspect cursor while CTRL is held over a previewable tile.
-- We track inspectCursorActive so ResetCursor only runs if *we* set it -
-- otherwise we'd clobber cursors set by other systems (drag-and-drop,
-- spell targeting, etc.).
local function UpdateInspectCursor()
local shouldShow = LP_config.houseEditorEnhancer_preview
and IsControlKeyDown()
and IsHoveredFramePreviewable()
if shouldShow then
if not inspectCursorActive then
ShowInspectCursor()
inspectCursorActive = true
end
elseif inspectCursorActive then
ResetCursor()
inspectCursorActive = false
end
end
-- Hover tracker: set hoveredFrame on enter, clear on leave, and refresh
-- the inspect cursor in both cases. Shared by HookCatalogEntry's two
-- branches (HousingCatalogEntryMixin tiles and HousingMarketProductDisplayMixin
-- tiles) since their tracking logic is identical. The parallel
-- catalog's own tiles use the same pattern inline because they also need
-- to drive their hover-bg texture and tooltip in the same script.
local function WireInspectCursorOnHover(frame)
frame:HookScript("OnEnter", function(self)
hoveredFrame = self
UpdateInspectCursor()
end)
frame:HookScript("OnLeave", function(self)
if hoveredFrame == self then
hoveredFrame = nil
end
UpdateInspectCursor()
end)
end
-- Forward declaration: EnsurePreviewFrame's OnSizeChanged hook captures
-- this; the actual function body is defined further down so it can also
-- be called from ShowPreviewForEntry without an extra forward ref.
local UpdatePreviewLayout
local function EnsurePreviewFrame()
if not HouseEditorFrame or not HouseEditorFrame.StoragePanel then return end
if previewFrame then return previewFrame end
if not C_AddOns.IsAddOnLoaded("Blizzard_HousingModelPreview") then
C_AddOns.LoadAddOn("Blizzard_HousingModelPreview")
end
previewFrame = CreateFrame("Frame", "LudiusPlusHouseEditorPreviewFrame", HouseEditorFrame.StoragePanel, "HousingModelPreviewTemplate")
local savedWidth = LP_config.houseEditorEnhancer_previewWidth or DEFAULT_PREVIEW_WIDTH
previewFrame:SetWidth(Clamp(savedWidth, MIN_PREVIEW_WIDTH, MAX_PREVIEW_WIDTH))
previewFrame:SetPoint("BOTTOMLEFT", HouseEditorFrame.StoragePanel, "BOTTOMRIGHT", PANEL_GAP, 0)
-- Ensure the preview frame is behind the StoragePanel's CollapseButton.
previewFrame:SetFrameLevel(HouseEditorFrame.StoragePanel.CollapseButton:GetFrameLevel() - 1)
-- Deliberately NOT SetClampedToScreen: it would snap our anchor inward
-- when the preview's right edge hits the screen, visually detaching us
-- from StoragePanel and overlapping it. UpdatePreviewLayout shrinks the
-- width instead, keeping the anchor relationship intact.
previewFrame:Hide()
-- Hide only on our instance (each template instantiation gets its own
-- texture children), so the HousingDashboardFrame's preview is untouched.
if previewFrame.PreviewCornerLeft then previewFrame.PreviewCornerLeft:Hide() end
if previewFrame.PreviewCornerRight then previewFrame.PreviewCornerRight:Hide() end
-- Border-only variant of the tooltip backdrop (backdropColorAlpha=0):
-- TooltipBorderedFrameTemplate would draw an 0.8-alpha dark Center over
-- the model scene and visibly dim it.
local border = CreateFrame("Frame", nil, previewFrame, "TooltipBorderBackdropTemplate")
-- Extend the border beyond the preview frame a few pixels so the
-- backdrop doesn't show through around the edge.
border:SetPoint("TOPLEFT", previewFrame, "TOPLEFT", -2, 2)
border:SetPoint("BOTTOMRIGHT", previewFrame, "BOTTOMRIGHT", 2, -2)
-- Invisible frame behind previewFrame to catch clicks and prevent click-through.
-- (Setting previewFrame:EnableMouse(true) prevented the mouse from interacting with the 3d model.)
-- Parented to StoragePanel (not previewFrame) so its frame level can sit
-- one below previewFrame's - a child would always render above its
-- parent. Visibility is therefore independent, so we sync it with
-- previewFrame via OnShow/OnHide.
local clickBlocker = CreateFrame("Frame", nil, HouseEditorFrame.StoragePanel)
clickBlocker:SetPoint("TOPLEFT", previewFrame, "TOPLEFT")
clickBlocker:SetPoint("BOTTOMRIGHT", previewFrame, "BOTTOMRIGHT")
clickBlocker:EnableMouse(true)
clickBlocker:SetFrameLevel(previewFrame:GetFrameLevel() - 1)
clickBlocker:Hide()
previewFrame:HookScript("OnShow", function() clickBlocker:Show() end)
previewFrame:HookScript("OnHide", function() clickBlocker:Hide() end)
local credit = previewFrame:CreateFontString(nil, "OVERLAY", "GameFontDisableTiny")
credit:SetText(L["by Ludius Plus"])
credit:SetPoint("BOTTOMLEFT", previewFrame, "BOTTOMLEFT", 6, 4)
local close = CreateFrame("Button", nil, previewFrame, "UIPanelCloseButton")
close:SetPoint("TOPRIGHT", previewFrame, "TOPRIGHT", 2, 2)
close:SetScript("OnClick", function()
PlaySound(SOUNDKIT.IG_CHARACTER_INFO_CLOSE)
HidePreview()
previewWasShownBeforeCollapse = false
end)
-- Width-only resize handle. Uses PanelResizeButtonTemplate for the
-- chat-style grabber visuals to match Blizzard's StoragePanel resize
-- button, but overrides every script: PanelResizeButtonMixin would
-- StartSizing on a BOTTOMRIGHT corner (resizing both width AND height),
-- and our height is driven by UpdatePreviewLayout, not by the user.
local resize = CreateFrame("Button", nil, previewFrame, "PanelResizeButtonTemplate")
resize:SetPoint("BOTTOMRIGHT", previewFrame, "BOTTOMRIGHT", -2, 2)
local dragStartCursorX, dragStartWidth
resize:SetScript("OnMouseDown", function(self)
dragStartCursorX = (GetCursorPosition()) / UIParent:GetEffectiveScale()
dragStartWidth = previewFrame:GetWidth()
self:SetScript("OnUpdate", function()
local cursorX = (GetCursorPosition()) / UIParent:GetEffectiveScale()
-- Cap by available space to the screen's right edge so the preview
-- can't be dragged off-screen. Compute desired-left from the panel
-- (not previewFrame:GetLeft()), to match UpdatePreviewLayout's logic.
local panel = HouseEditorFrame and HouseEditorFrame.StoragePanel
local panelRight = panel and panel:GetRight()
local maxByScreen = panelRight and (UIParent:GetRight() - (panelRight + PANEL_GAP)) or MAX_PREVIEW_WIDTH
local maxWidth = math.min(MAX_PREVIEW_WIDTH, maxByScreen)
local newWidth = Clamp(dragStartWidth + (cursorX - dragStartCursorX), MIN_PREVIEW_WIDTH, maxWidth)
previewFrame:SetWidth(newWidth)
end)
end)
resize:SetScript("OnMouseUp", function(self)
self:SetScript("OnUpdate", nil)
LP_config.houseEditorEnhancer_previewWidth = previewFrame:GetWidth()
end)
resize:SetScript("OnEnter", function() SetCursor("UI_RESIZE_CURSOR") end)
resize:SetScript("OnLeave", function() SetCursor(nil) end)
-- Sounds are intentionally not wired to OnShow/OnHide. The preview is
-- parented to the editor, so OnShow/OnHide also fire on cascaded
-- effective-visibility changes (e.g. StoragePanel hides during decor
-- placement) - we'd play extra sounds in those cases. Instead, the
-- open/select/close sounds are played inline at the actual user-driven
-- actions: click to preview (ShowPreviewForEntry) and the close button.
HouseEditorFrame:HookScript("OnHide", HidePreview)
-- Re-fit on StoragePanel resize: panel resizing changes both the
-- available vertical room (panel bottom moves) and our left edge
-- (panel right moves), so width may need to shrink to stay on-screen.
HouseEditorFrame.StoragePanel:HookScript("OnSizeChanged", UpdatePreviewLayout)
return previewFrame
end
-- Recompute the preview's height and width.
--
-- Height: fill the vertical gap between StoragePanel's bottom and
-- HouseEditorButton's bottom, so the preview never covers the button.
-- WoW's anchor system can't express this with two anchors (left X comes
-- from StoragePanel, top Y from HouseEditorButton at a different X), so
-- we compute height explicitly.
--
-- Width: clamp to the available space between our left edge (which
-- equals panel:GetRight() + PANEL_GAP via our anchor) and the screen's
-- right edge so a wider StoragePanel can't push the preview off-screen.
-- We restore from LP_config rather than just shrinking the current
-- width, so a later StoragePanel-shrink expands the preview back to the
-- user's saved preference.
--
-- Important: we read panel:GetRight() rather than previewFrame:GetLeft()
-- because anchors-with-clamping can lie - if SetClampedToScreen were on,
-- GetLeft would return the post-clamp position, hiding the overflow.
function UpdatePreviewLayout()
local panel = HouseEditorFrame and HouseEditorFrame.StoragePanel
local btn = HousingControlsFrame
and HousingControlsFrame.OwnerControlFrame
and HousingControlsFrame.OwnerControlFrame.HouseEditorButton
if not previewFrame or not panel or not btn then return end
local btnBottom = btn:GetBottom()
local panelBottom = panel:GetBottom()
if btnBottom and panelBottom then
local height = btnBottom - panelBottom
if height > 0 then
previewFrame:SetHeight(height)
end
end
local panelRight = panel:GetRight()
if panelRight then
local maxByScreen = UIParent:GetRight() - (panelRight + PANEL_GAP)
local saved = LP_config.houseEditorEnhancer_previewWidth or DEFAULT_PREVIEW_WIDTH
local target = Clamp(saved, MIN_PREVIEW_WIDTH, math.min(MAX_PREVIEW_WIDTH, maxByScreen))
previewFrame:SetWidth(target)
end
end
local function ShowPreviewForEntry(entry)
local pf = EnsurePreviewFrame()
if not pf then return end
-- First open plays the open sound; subsequent entry swaps play the
-- dashboard's select sound. Only one or the other, never both.
if pf:IsShown() then
PlaySound(SOUNDKIT.HOUSING_CATALOG_ENTRY_SELECT)
else
PlaySound(SOUNDKIT.IG_CHARACTER_INFO_OPEN)
end
pf:PreviewCatalogEntryInfo(entry.entryInfo)
pf.lastEntryInfo = entry.entryInfo
previewWasShownBeforeCollapse = true
UpdatePreviewLayout()
pf:Show()
end
local function HookCatalogEntry(frame)
if not frame or frame._lpHooked then return end
if type(frame.OnInteract) == "function" then
-- HousingCatalogEntryMixin path: Storage tab + non-Featured Market
-- entries + bundle-item decor tiles in Featured (CATALOG_ENTRY_DECOR
-- template). OnInteract funnels both OnClick and OnDragStart, so
-- wrapping it once covers both.
local original = frame.OnInteract
frame.OnInteract = function(self, button, isDrag)
if not isDrag and button == "LeftButton" and IsControlKeyDown()
and LP_config.houseEditorEnhancer_preview and self:HasValidData() then
ShowPreviewForEntry(self)
return
end
return original(self, button, isDrag)
end
-- Track hover so MODIFIER_STATE_CHANGED can flip the cursor while the
-- mouse stays still. OnEnter/OnLeave alone wouldn't catch the case of
-- the user pressing CTRL after hovering.
WireInspectCursorOnHover(frame)
elseif type(frame.StartPreview) == "function" then
-- HousingMarketProductDisplayMixin path: Featured tab Small Product
-- tiles and Bundle wide cards. We can't wrap OnClick on these - the
-- XML uses <OnClick method="OnClick"/> which captures the method
-- reference at frame creation (same trap as hooksecurefunc on a
-- mixin), so reassigning frame.OnClick is silently a no-op against
-- the script handler that's already bound. Instead wrap StartPreview,
-- which Blizzard's OnClick calls via self:StartPreview() - a
-- dynamic method lookup that DOES see our instance-level override.
--
-- We only intercept SINGLE decor items: the canPreview +
-- entryVariantID.entryType == Decor guard skips bundle wide cards
-- (their elementData has decorEntries, no single entryVariantID)
-- and any non-decor previewables. Without CTRL held, we fall through
-- to Blizzard's StartPreview (the in-game placement preview).
local originalStartPreview = frame.StartPreview
frame.StartPreview = function(self)
-- IsMouseButtonDown distinguishes click (button already released by
-- the time OnClick fires, since registerForClicks is "...ButtonUp")
-- from drag (button still held when OnDragStart fires). We only
-- want to intercept clicks; CTRL+drag should fall through to the
-- normal drag-place behavior.
if IsControlKeyDown() and not IsMouseButtonDown("LeftButton")
and LP_config.houseEditorEnhancer_preview
and self.elementData and self.elementData.canPreview
and self.elementData.entryVariantID
and self.elementData.entryVariantID.entryType == Enum.HousingCatalogEntryType.Decor then
local entryInfo = C_HousingCatalog.GetCatalogEntryInfo(self.elementData.entryVariantID)
if entryInfo then
ShowPreviewForEntry({ entryInfo = entryInfo })
return
end
end
return originalStartPreview(self)
end
-- Hover tracking for the inspect cursor. UpdateInspectCursor's
-- previewable check accepts both HasValidData-style frames and
-- elementData/canPreview-style market product frames, so the
-- magnifying-glass cursor appears on hovered single-decor small
-- products too. Bundle wide cards (no entryVariantID on elementData)
-- correctly fall through and don't trigger the cursor change.
WireInspectCursorOnHover(frame)
end
frame._lpHooked = true
end
-- ===== Events =====
-- Single eventFrame used both as the ADDON_LOADED trigger and as the
-- carrier for MODIFIER_STATE_CHANGED (registered only while preview is
-- active). OnEvent is installed unconditionally because ADDON_LOADED has
-- to land somewhere; the per-event bodies are self-gated.
local eventFrame = CreateFrame("Frame")
-- ===== Icon resizer: setup / teardown =====
-- Forward declaration: SetupIconResizer's CTRL+wheel-only branch needs to
-- call HideIconResizerSlider, but that helper is defined after this block
-- so TeardownIconResizer (which sits next to it) can share it.
local HideIconResizerSlider
local function SetupIconResizer()
local container = GetContainer()
if not container then return end
local storagePanel = HouseEditorFrame and HouseEditorFrame.StoragePanel
if not storagePanel or not storagePanel.SearchBox then return end
-- Re-scale whenever the user switches category or tab. We hook
-- Categories:SetFocus rather than the storage panel's
-- OnCategoryFocusChanged because Categories:Initialize captures the
-- callback via GenerateClosure - hooking OnCategoryFocusChanged on the
-- panel would be bypassed. SetFocus is the universal entry point for
-- both category changes and tab changes (called from OnTabChanged).
if storagePanel.Categories and not categoriesFocusHooked then
hooksecurefunc(storagePanel.Categories, "SetFocus", function()
if not IsAnyIconResizingActive() then return end
-- Featured category forces ScrollTarget to 1.0 (slider is ignored
-- there because bundle wide cards have fixed non-standard sizes);
-- everywhere else re-applies the slider's scale.
if IsFeaturedCategoryFocused() then
local scrollBox = container.ScrollBox
if scrollBox and scrollBox.ScrollTarget then
scrollBox.ScrollTarget:SetScale(1.0)
end
else
RefreshCatalog()
end
if updateControlStates then updateControlStates() end
end)
categoriesFocusHooked = true
end
-- Slider UI only shows when the slider flag is specifically on. The
-- CTRL+wheel-only mode (iconResizer off, iconResizerCtrlWheel on) still
-- needs the scaling hook above, but no slider widget, no SearchBox
-- shift. Hand off to the slider-UI teardown helper in that case and
-- skip the slider-creation block entirely.
if not LP_config.houseEditorEnhancer_iconResizerSlider then
HideIconResizerSlider()
RefreshCatalog()
return
end
-- Shift the SearchBox (and the Filters frame anchored to it) up to make
-- room for our slider row beneath it.
local searchBox = storagePanel.SearchBox
searchBox:ClearAllPoints()
searchBox:SetPoint("TOPLEFT", storagePanel, "TOPLEFT", 20, SEARCH_BOX_SHIFTED_Y)
searchBox:SetPoint("TOPRIGHT", storagePanel, "TOPRIGHT", -160, SEARCH_BOX_SHIFTED_Y)
if not slider then
-- Parent the controls to the StoragePanel so they sit below the SearchBox.
sliderLabel = storagePanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
sliderLabel:SetText(L["Decor Icon Size:"])
sliderLabel:SetJustifyH("LEFT")
slider = CreateFrame("Slider", "LudiusPlusHouseEditorSizeSlider", storagePanel, "MinimalSliderTemplate")
slider:SetMinMaxValues(MIN_SCALE, MAX_SCALE)
slider:SetValueStep(STEP)
slider:SetObeyStepOnDrag(true)
resetButton = CreateFrame("Button", "LudiusPlusHouseEditorResetButton", storagePanel)
resetButton:SetSize(18, 18)
local tex = resetButton:CreateTexture(nil, "ARTWORK")
tex:SetTexture(RESET_TEXTURE)
tex:SetTexCoord(unpack(RESET_TEXCOORDS))
tex:SetAllPoints()
resetButton.texture = tex
-- The Featured category ignores our scale (see GetScale), so the
-- slider/reset controls are greyed out and non-interactive while it
-- is focused - otherwise the UI implies the controls affect tiles
-- that they don't.
updateControlStates = function()
local featured = IsFeaturedCategoryFocused()
if featured then
slider:Disable()
else
slider:Enable()
end
local value = slider:GetValue()
local atDefault = math.abs(value - DEFAULT_SCALE) < STEP / 2
local disabled = featured or atDefault
resetButton:SetEnabled(not disabled)
if disabled then
tex:SetDesaturated(true)
tex:SetVertexColor(0.5, 0.5, 0.5)
else
tex:SetDesaturated(false)
tex:SetVertexColor(1, 1, 1)
end
end
slider:SetScript("OnValueChanged", function(_, value)
value = math.floor(value / STEP + 0.5) * STEP
LP_config.houseEditorEnhancer_iconResizerSize = value
updateControlStates()
RefreshCatalog()
if refreshParallelOnSliderChange then refreshParallelOnSliderChange() end
end)
slider:SetScript("OnEnter", function(self)
GameTooltip:SetOwner(self, "ANCHOR_TOPLEFT")
GameTooltip:SetText(L["Resize decor item icons"], NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b, 1, true)
GameTooltip:AddLine(L["by Ludius Plus"], DISABLED_FONT_COLOR.r, DISABLED_FONT_COLOR.g, DISABLED_FONT_COLOR.b, 1, true)
if IsFeaturedCategoryFocused() then
GameTooltip_AddErrorLine(GameTooltip, L["Not working in the \"%1$s\" category. The \"Thoughtfully Augmented Editor\" (TAE) wants no part of %2$s!"]:format(C_HousingCatalog.GetCatalogCategoryInfo(Constants.HousingCatalogConsts.HOUSING_CATALOG_FEATURED_CATEGORY_ID).name, HOUSING_MARKET_HEARTHSTEEL_TOOLTIP))
end
GameTooltip:Show()
end)
slider:SetScript("OnLeave", function() GameTooltip:Hide() end)
resetButton:SetScript("OnClick", function()
slider:SetValue(DEFAULT_SCALE)
end)
resetButton:SetScript("OnEnter", function(self)
GameTooltip:SetOwner(self, "ANCHOR_TOPLEFT")
GameTooltip:SetText(L["Reset to default size"], NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b, 1, true)
GameTooltip:Show()
end)
resetButton:SetScript("OnLeave", function() GameTooltip:Hide() end)
end
-- Anchor layout: label at left under SearchBox, reset button pinned to
-- SearchBox's right edge, slider stretches between them so it respects
-- the label's locale-dependent width dynamically.
sliderLabel:ClearAllPoints()
sliderLabel:SetPoint("TOPLEFT", storagePanel.SearchBox, "BOTTOMLEFT", -3, -2)
resetButton:ClearAllPoints()
resetButton:SetPoint("RIGHT", storagePanel.SearchBox, "RIGHT", -4, 0)
resetButton:SetPoint("TOP", sliderLabel, "TOP", 0, 2)
slider:ClearAllPoints()
slider:SetPoint("LEFT", sliderLabel, "RIGHT", 8, 0)
slider:SetPoint("RIGHT", resetButton, "LEFT", -4, 0)
slider:SetPoint("TOP", sliderLabel, "TOP", 0, 2)
slider:SetFrameLevel(storagePanel:GetFrameLevel() + 20)
sliderLabel:Show()
slider:Show()
resetButton:Show()
slider:SetValue(LP_config.houseEditorEnhancer_iconResizerSize or DEFAULT_SCALE)
if updateControlStates then updateControlStates() end
RefreshCatalog()
end
-- Shared by TeardownIconResizer and the CTRL+wheel-only path of
-- SetupIconResizer: hide the slider/label/reset and restore the SearchBox
-- to its original (un-shifted) position. Assigned to the forward-declared
-- local at top of section.
HideIconResizerSlider = function()
if not slider then return end -- never created this session
slider:Hide()
if sliderLabel then sliderLabel:Hide() end
if resetButton then resetButton:Hide() end
local storagePanel = HouseEditorFrame and HouseEditorFrame.StoragePanel
if storagePanel and storagePanel.SearchBox then
local searchBox = storagePanel.SearchBox
searchBox:ClearAllPoints()
searchBox:SetPoint("TOPLEFT", storagePanel, "TOPLEFT", 20, SEARCH_BOX_DEFAULT_Y)
searchBox:SetPoint("TOPRIGHT", storagePanel, "TOPRIGHT", -160, SEARCH_BOX_DEFAULT_Y)
end
end
local function TeardownIconResizer()
-- If the slider has never been set up in this session there is nothing
-- to revert: GetScale returns 1.0 and the SearchBox has its original anchor.
if not slider then return end
HideIconResizerSlider()
-- GetScale() returns 1.0 when disabled, so RefreshCatalog resets tile
-- sizes back to the Blizzard defaults.
RefreshCatalog()
end