Skip to content

Commit eea8f68

Browse files
author
mergetest
committed
feat(filters): export and import a single aura filter
Adds a per-filter share flow so a filter can travel without a whole profile export. Registry: - ResolveFilterContent(ref) factored out of DuplicateFilter, so duplicate and export resolve a ref identically. Presets flatten to their currently-enabled records; the blacklist resolves to nil (it is a per-mode db set, not a registry filter). - contentMatches/defHasContent lifted out of ImportCustomFilters' private canReuse closure, and exposed as FindContentMatch. Behaviour of profile import is unchanged. - ExportFilter/DecodeFilterString/ImportFilterPayload behind a !DFF1! prefix, using the same LibSerialize -> CompressDeflate -> EncodeForPrint chain as the profile and click-casting exports. - BucketIDs re-derives the spells/rawIDs split against the local spell database on import. That split reflects whoever exported it, so trusting it would leave a known id in rawIDs and skip the canonical-id snap, making the filter match differently for the receiver. Options: - Export button in the action strip (works on presets and customs, disabled for the blacklist); strip grows to three rows and the scroll frame's bottom inset follows. - "+ Import Filter" row under "+ New Buff Filter" — it creates rather than acts on the selection, so it belongs with New, not in the strip. - A content-equal local filter is surfaced as a choice (copy vs use existing) instead of being silently reused the way profile import does, where silent reuse would look like the paste did nothing. - Follow-up popups are deferred a frame: the popup frame is a singleton and an alert button runs onClick before f:Hide(), so a popup opened from inside another's callback would be hidden immediately. Imported names are stripped of colour escapes and control characters and clamped to the rename prompt's length; payloads are bounded at 2000 ids.
1 parent fab7983 commit eea8f68

4 files changed

Lines changed: 409 additions & 44 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ DandersFrames has been rebuilt for WoW 12.1 (Midnight), which fundamentally chan
3333
* (Aura Designer) **Sound alerts do more than announce a new buff.** Each indicator's Sound section can now play a separate sound when a tracked buff **drops off** and when it **gains a stack**, alongside the existing on-applied alert (which now has its own on/off toggle). The sound system was also rebuilt on 12.1's new aura-sound engine so alerts keep working on the current build. The buff-dropped and stack-gained triggers need the current game build; the section notes this if your client doesn't have them yet.
3434
* (Profiles) Custom filters and your category tweaks travel with profile exports — including filters linked in the Aura Designer.
3535
* (Profiles) Custom filters picked inside a raid auto layout's settings are handled everywhere the rest of the profile is: deleting a filter now removes it from every layout (including the active one), exports carry filters only a layout references, and imports re-link them instead of pointing at the wrong filter on the receiving account.
36+
* (Aura Filters) **New: share a single filter.** Filters can now be exported and imported one at a time, so you can send someone one filter instead of your whole profile. "+ Import Filter" sits under "+ New Buff Filter" in the list, and Export is in the button strip below it. Built-in presets can be shared too — they arrive as a custom filter holding whatever spells you had enabled. If you import a filter you already have, DandersFrames says so and lets you pick between keeping yours or adding a copy.
3637
* (Frames) The Out of Range "Text Alpha" slider now also governs pet frame text and the test-mode preview — those still followed hidden per-element values no control could change.
3738
* (Interface) In Aura Filters, a spell added by ID now shows its real name and icon (resolved from the game) instead of a bare "#id" and a question mark. It's tagged "not in database" so you can spot spells that aren't part of a built-in preset yet; only a genuinely invalid ID still reads "unknown ID".
3839
* (Interface) Fixed the Aura Filters page's Add, Add from Database, Rename and Delete buttons filling with a solid colour on hover after switching between a built-in preset and a custom filter, and gave their disabled-state tooltips a consistent two-line layout.

FilterRegistry/Options.lua

Lines changed: 167 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,50 @@ local function ConfirmDeleteFilter(displayName, onAccept)
7272
})
7373
end
7474

75+
-- Error keys from R:DecodeFilterString / R:ExportFilter -> user-facing text.
76+
-- A string carrying another DandersFrames prefix is a valid export of the WRONG
77+
-- kind, and is called out as such: "that isn't a filter string" would send
78+
-- someone hunting for corruption when they simply pasted into the wrong box.
79+
local function FilterStringError(errKey)
80+
if errKey == "profile" then
81+
return L["That's a profile string. Import it from the Profiles page instead."]
82+
elseif errKey == "clickcasting" then
83+
return L["That's a click casting string. Import it from the Click Casting page instead."]
84+
elseif errKey == "wizard" then
85+
return L["That's a setup wizard string, not a filter."]
86+
elseif errKey == "newer" then
87+
return L["This filter was exported by a newer version of DandersFrames."]
88+
elseif errKey == "tooLarge" then
89+
return L["That filter string is too large."]
90+
elseif errKey == "corrupt" then
91+
return L["That filter string is corrupt or incomplete."]
92+
end
93+
-- "libs", "encode", "noSelection" and anything unrecognised: nothing the
94+
-- user can act on beyond retrying.
95+
return L["That doesn't look like a filter string."]
96+
end
97+
98+
-- The popup frame is a SINGLETON, and an alert button's handler runs
99+
-- btnConfig.onClick() and THEN f:Hide(). So opening a second popup from inside
100+
-- the first one's callback reconfigures the shared frame and then has it hidden
101+
-- out from under it — the new popup flashes and vanishes. Defer a frame so the
102+
-- first one finishes closing before the next opens.
103+
local function ChainPopup(fn)
104+
C_Timer.After(0, fn)
105+
end
106+
107+
local function ShowFilterStringError(title, errKey)
108+
local message = FilterStringError(errKey)
109+
ChainPopup(function()
110+
DF:ShowPopupAlert({
111+
title = title,
112+
tone = "danger",
113+
message = message,
114+
buttons = { { label = L["OK"] } },
115+
})
116+
end)
117+
end
118+
75119
-- Deleting a custom filter must also unhook it from every profile's
76120
-- per-mode selections (both the buff and defensive rows) AND from every
77121
-- Aura Designer filter group's filterSelection (A5 — the AD preset
@@ -628,12 +672,12 @@ function DF.BuildFilterDesignerPage(guiRef, pageRef, dbRef)
628672

629673
local leftScroll = CreateFrame("ScrollFrame", nil, leftPanel, "ScrollFrameTemplate")
630674
leftScroll:SetPoint("TOPLEFT", 4, -32) -- clears the Buffs/Debuffs tab strip (4 + TAB_H + 4)
631-
-- Clears the action strip at the foot of the panel: two 20px rows, a 4px gutter,
632-
-- a 6px margin under them and the rule + gap above (6 + 20 + 4 + 20 + 6 = 56, +6
633-
-- of air). It briefly went to 6 while those buttons lived on the right-hand
634-
-- header; they came back down here because that header row could not fit them
635-
-- and a variable-width filter name at every window width.
636-
leftScroll:SetPoint("BOTTOMRIGHT", -24, 62)
675+
-- Clears the action strip at the foot of the panel: three 20px rows, 4px gutters,
676+
-- a 6px margin under them and the rule + gap above (6 + 20 + 4 + 20 + 4 + 20 + 6
677+
-- = 80, +6 of air). It briefly went to 6 while those buttons lived on the
678+
-- right-hand header; they came back down here because that header row could not
679+
-- fit them and a variable-width filter name at every window width.
680+
leftScroll:SetPoint("BOTTOMRIGHT", -24, 86)
637681
DF.GUI.StyleScrollBar(leftScroll)
638682

639683
local leftContent = CreateFrame("Frame", nil, leftScroll)
@@ -1243,6 +1287,67 @@ function DF.BuildFilterDesignerPage(guiRef, pageRef, dbRef)
12431287
if DF.HighlightWidget then DF:HighlightWidget(addRow) end
12441288
end
12451289

1290+
-- Import sits next to New rather than in the action strip below, for the same
1291+
-- reason New does: it CREATES a filter, where every button in that strip acts
1292+
-- on the current selection. Down there it would read as "import into the
1293+
-- selected filter", which is not what it does.
1294+
local importRow = CreateFrame("Button", nil, leftContent, "BackdropTemplate")
1295+
importRow:SetHeight(LEFT_ROW_H - 2)
1296+
GUI:StyleButton(importRow, {
1297+
tinted = true,
1298+
text = L["+ Import Filter"],
1299+
align = "left",
1300+
leftPad = 10,
1301+
font = "DFFontHighlightSmall",
1302+
})
1303+
importRow:SetScript("OnClick", function()
1304+
DF:ShowPopupInput({
1305+
title = L["Import Filter"],
1306+
message = L["Paste a filter string to import:"],
1307+
multiline = true,
1308+
acceptLabel = L["Import"],
1309+
onAccept = function(text)
1310+
if not text or Trim(text) == "" then return end
1311+
local def, err = R:DecodeFilterString(text)
1312+
if not def then
1313+
ShowFilterStringError(L["Import Filter"], err)
1314+
return
1315+
end
1316+
-- A newly imported filter is not in any selection yet
1317+
-- (IsCustomOn defaults false), so nothing on screen changes
1318+
-- until the user ticks it — no DirectFilterChangedProxy here,
1319+
-- matching the New and Duplicate paths.
1320+
local match = R:FindContentMatch(def)
1321+
if not match then
1322+
SelectFilter("custom", R:ImportFilterPayload(def))
1323+
return
1324+
end
1325+
-- Content-equal filter already present. Profile import silently
1326+
-- reuses it, which is right there; here it would mean pasting a
1327+
-- string and watching nothing happen. Ask instead.
1328+
local existing = R:GetCustomFilter(match)
1329+
local message = format(
1330+
L["You already have a filter with these spells: \"%s\". Import a separate copy anyway?"],
1331+
(existing and existing.name) or match)
1332+
ChainPopup(function()
1333+
DF:ShowPopupAlert({
1334+
title = L["Import Filter"],
1335+
message = message,
1336+
buttons = {
1337+
{ label = L["Import as Copy"], onClick = function()
1338+
SelectFilter("custom", R:ImportFilterPayload(def))
1339+
end },
1340+
{ label = L["Use Existing"], onClick = function()
1341+
SelectFilter("custom", match)
1342+
end },
1343+
{ label = L["Cancel"] },
1344+
},
1345+
})
1346+
end)
1347+
end,
1348+
})
1349+
end)
1350+
12461351
local dupBtn = GUI:CreateButton(leftPanel, L["Duplicate"], ACT_BTN_W, 20, function(self)
12471352
if self.dfDisabled or not selKey then return end
12481353
local src = selKey -- capture: selection may move before the prompt closes
@@ -1284,29 +1389,58 @@ function DF.BuildFilterDesignerPage(guiRef, pageRef, dbRef)
12841389
RefreshAll()
12851390
end)
12861391
end)
1287-
-- Anchored only now that all four exist. Each takes its own corner of the panel
1392+
-- Export flattens a preset to its currently-enabled spells (ResolveFilterContent),
1393+
-- so it works on presets as well as customs — unlike Rename/Delete, which need a
1394+
-- store entry. The blacklist is the exception: it is a per-mode db set, not a
1395+
-- registry filter, so there is nothing to resolve.
1396+
local exportBtn = GUI:CreateButton(leftPanel, L["Export"], ACT_BTN_W, 20, function(self)
1397+
if self.dfDisabled or not selKey or selKind == "blacklist" then return end
1398+
local str, err = R:ExportFilter(selKey, CurrentDisplayName())
1399+
if not str then
1400+
ShowFilterStringError(L["Export Failed"], err)
1401+
return
1402+
end
1403+
-- readOnly: the string is there to be selected and copied, not edited. It
1404+
-- opens fully selected, so Ctrl+C alone is enough.
1405+
DF:ShowPopupInput({
1406+
title = L["Export Filter"],
1407+
-- Presets land on the other end as a custom filter carrying a snapshot
1408+
-- of what was enabled at export time. Say so rather than let it surprise.
1409+
message = (selKind == "preset")
1410+
and L["Copy this string to share this filter. It will import as a custom filter."]
1411+
or L["Copy this string to share this filter:"],
1412+
text = str,
1413+
multiline = true,
1414+
readOnly = true,
1415+
cancelLabel = L["Done"],
1416+
})
1417+
end)
1418+
1419+
-- Anchored only now that all five exist. Each takes its own corner of the panel
12881420
-- rather than chaining off a neighbour, so Reset hiding (it only shows for a
1289-
-- modified preset or the blacklist) leaves the other three exactly where they
1290-
-- were:
1421+
-- modified preset or the blacklist) leaves the others exactly where they were:
12911422
--
12921423
-- [ Duplicate ] [ Rename ]
1293-
-- [ Reset ] [ Delete ]
1424+
-- [ Export ] [ Delete ]
1425+
-- [ Reset ]
12941426
--
1295-
-- The two that destroy something share the right-hand column, away from the two
1296-
-- that don't.
1427+
-- The two that destroy something share the right-hand column, away from the ones
1428+
-- that don't. Reset keeps the bottom-left corner it already had, so its show/hide
1429+
-- still moves nothing.
12971430
resetBtn:SetSize(ACT_BTN_W, 20)
1298-
dupBtn:SetPoint("BOTTOMLEFT", leftPanel, "BOTTOMLEFT", 6, 30)
1299-
renameBtn:SetPoint("BOTTOMRIGHT", leftPanel, "BOTTOMRIGHT", -6, 30)
1431+
dupBtn:SetPoint("BOTTOMLEFT", leftPanel, "BOTTOMLEFT", 6, 54)
1432+
renameBtn:SetPoint("BOTTOMRIGHT", leftPanel, "BOTTOMRIGHT", -6, 54)
1433+
exportBtn:SetPoint("BOTTOMLEFT", leftPanel, "BOTTOMLEFT", 6, 30)
1434+
delBtn:SetPoint("BOTTOMRIGHT", leftPanel, "BOTTOMRIGHT", -6, 30)
13001435
resetBtn:SetPoint("BOTTOMLEFT", leftPanel, "BOTTOMLEFT", 6, 6)
1301-
delBtn:SetPoint("BOTTOMRIGHT", leftPanel, "BOTTOMRIGHT", -6, 6)
13021436

1303-
-- Rule above the strip: without it the buttons read as two more rows of the list
1437+
-- Rule above the strip: without it the buttons read as more rows of the list
13041438
-- they sit under, rather than as a toolbar acting on that list's selection.
13051439
local actRule = leftPanel:CreateTexture(nil, "ARTWORK")
13061440
actRule:SetHeight(1)
13071441
actRule:SetColorTexture(0.22, 0.22, 0.22, 1)
1308-
actRule:SetPoint("BOTTOMLEFT", leftPanel, "BOTTOMLEFT", 6, 56)
1309-
actRule:SetPoint("BOTTOMRIGHT", leftPanel, "BOTTOMRIGHT", -6, 56)
1442+
actRule:SetPoint("BOTTOMLEFT", leftPanel, "BOTTOMLEFT", 6, 80)
1443+
actRule:SetPoint("BOTTOMRIGHT", leftPanel, "BOTTOMRIGHT", -6, 80)
13101444

13111445
-- ========== DATABASE FRESHNESS NOTE ==========
13121446
-- Static by design: the stamp and the client build can't change
@@ -1395,6 +1529,9 @@ function DF.BuildFilterDesignerPage(guiRef, pageRef, dbRef)
13951529
dupBtn:SetDisabled(selKey == nil or isBlacklist)
13961530
renameBtn:SetDisabled(not isCustom)
13971531
delBtn:SetDisabled(not isCustom)
1532+
-- Same gate as Duplicate: both resolve a ref to content, and both can do
1533+
-- that for a preset but not for the blacklist.
1534+
exportBtn:SetDisabled(selKey == nil or isBlacklist)
13981535
addBox:SetEnabled(isCustom)
13991536
addBtn:SetDisabled(not isCustom)
14001537
dbBtn:SetDisabled(not isCustom)
@@ -1933,6 +2070,13 @@ function DF.BuildFilterDesignerPage(guiRef, pageRef, dbRef)
19332070
addRowY = y
19342071
y = y + LEFT_ROW_H
19352072

2073+
-- Import directly under New: the two create-a-filter actions read as a
2074+
-- pair, and both stay above the list they create into.
2075+
importRow:ClearAllPoints()
2076+
importRow:SetPoint("TOPLEFT", 0, -y)
2077+
importRow:SetPoint("TOPRIGHT", 0, -y)
2078+
y = y + LEFT_ROW_H
2079+
19362080
for _, cfId in ipairs(SortedCustomIDs()) do
19372081
local f = R:GetCustomFilter(cfId)
19382082
used = used + 1
@@ -2017,11 +2161,12 @@ function DF.BuildFilterDesignerPage(guiRef, pageRef, dbRef)
20172161

20182162
end -- leftTab
20192163

2020-
-- The custom-filter add row belongs to the Buffs tab only. Pooled ROWS get
2021-
-- hidden by the sweep below, but this one is a standing frame -- it would
2022-
-- otherwise float over the debuff list at whatever y the last buff refresh
2023-
-- left it at.
2164+
-- The custom-filter add and import rows belong to the Buffs tab only. Pooled
2165+
-- ROWS get hidden by the sweep below, but these are standing frames -- they
2166+
-- would otherwise float over the debuff list at whatever y the last buff
2167+
-- refresh left them at.
20242168
addRow:SetShown(leftTab == "buffs")
2169+
importRow:SetShown(leftTab == "buffs")
20252170

20262171
-- Hide pooled rows beyond this refresh's needs
20272172
for j = used + 1, #leftRows do

0 commit comments

Comments
 (0)