LibAceGUIWidgets-v0.10.0
What's new
Changelog
[v0.10.0] -- a secure button that sits over a window and stays clickable
Compatibility -- MINOR 47 -> 48
New: W:CreateSecureOverlayButton(panel, opts); feature-detect with
if W.CreateSecureOverlayButton then or the MINOR (>= 48). Nothing for an existing consumer to do.
New
- A secure button drawn over a panel inside a pooled AceGUI window (TOGProfessionMaster inbox
48ef6be4). The library now owns what TOGPM hand-rolled in
GUI/CraftingTab.luaand broke three times: aUIParentholder (never parented or anchored to the panel or window), the strata one above the panel's set on holder and button at a fixed level (so the Toplevel window's Raise cannot cover it and nothing chases the window's level), a per-frame watcher that follows the panel's rect, effective scale, strata and visibility,isWanted(), a[combat] hide; showstate driver, and a re-sync onPLAYER_REGEN_ENABLED. A release of the AceGUI window around the panel detaches it, and its bookkeeping lives in a library-side weak table, so a pooled panel carries only an inert hook. Differs from the request in two places: in combat the factory returns nil and callsopts.onCreate(button)once it has built it after combat (there is no button to return before then), andopts.namekeys the button -- a second call by name re-attaches the same one, since a secure frame cannot be destroyed. Specs inTests/secureoverlay_spec.lua. Not verified in the client. - Peer Review of the above (thread 38e05fb4), all four taken. The release hook now goes on the
INNERMOST AceGUI widget around the panel, not the outermost: a TabGroup tab switch releases the inner
widget alone while the window stays open, and the button stayed attached to a frame back in AceGUI's
pool, ready to show over the next addon's widget. The
x/yoffsets now follow the library scale like the button's size, and a scale change re-places it. Two in-combat calls with one unbuilt name now queue one build and runonCreateonce, with the later call's panel and opts; and a build queued in combat is dropped, with noonCreate, if the panel's widget is released before combat ends (Release leavesframe.objin place, so the regen build would otherwise land on a pooled frame). The strata-above choice is kept, and its cost (it draws through another window dragged over yours, and sharesTOOLTIPwith tooltips) is now stated in the API comment and the README.
Tests
- Two fallbacks that had no spec are covered: the character counter's byte count on a client with
neither
GetNumLettersnorstrlenutf8, andlib._OwnFloorwith nothing recorded.
[v0.9.0] -- a long list that pages a screen at a time
Compatibility -- MINOR 46 -> 47
New: rl:RowPlan(entry), rl:GetVisibleRowCount(), rl:ScrollPage(direction) and
opts.pageWithShift on a RowList; feature-detect with if rl.RowPlan then / if rl.ScrollPage then
or the MINOR (>= 47). Nothing for an existing consumer to do: a list without pageWithShift scrolls
exactly as before.
New
- A list's span and collapse plan for one row is now public API (TOGProfessionMaster inbox
e8e490b8).
rl:RowPlan(entry)returnsspan, collapse:span[ci] = stopIdxfor each spanning text column (0= runs to the last column's edge),collapse[ci] = truefor each collapsed column -- the same answer the private_rowPlangave, which a consumer's spec was reading and which had already been renamed once (_spanPlan). Differs from_rowPlanin two places: both tables are always returned (empty, never nil), and the internal re-layout key is not. A heading row answers two empty tables, matching that the layout plans none for it._rowPlanstays private. Specs inTests/rowlist_span_spec.lua. - A long list can be paged one screen at a time (Ultimate Guild Roster inbox 8a3feb36, a
14,506-entry guild log). All three things asked for:
rl:GetVisibleRowCount()answers the rows on screen (the privatevisibleRowCountthey were reading);rl:ScrollPage(direction)moves one page,direction > 0down and< 0up, clamped likeSetScrollOffsetand returning the offset that stood; andopts.pageWithShift = truemakes Shift + wheel and Shift + scroll-arrow click page instead of moving one row. A page is the rows on screen less one, so the last row of one page is the first of the next, and never under one row. Only the sign ofdirectioncounts; 0 or a non-number moves nothing. Opt-in because Shift + wheel may already mean something on the consumer's frame; a client withoutIsShiftKeyDownscrolls by rows. The README's MINOR table also gains its missing 44-47 rows. Specs inTests/rowlist_page_spec.lua. Not verified in the client.
Tests and docs
- The v0.2.5 section moved whole to
CHANGELOG_ARCHIVE.mdto keep this file under the release-body ceiling. The README said the three original widget types register at Version 33; they have been at 34 since MINOR 45, and it says so now.
[v0.8.0] -- names in a list that use the room beside them
Compatibility -- MINOR 45 -> 46
New: col.span on a RowList text column and col.collapse on a right-chain column; feature-detect
with the library's MINOR (>= 46). Nothing for an existing consumer to do: a column without either
lays out exactly as before.
New
- A list cell can run across the empty columns after it (TOGProfessionMaster inbox c4a92587). A
text column may carry
span = trueorspan = function(entry) -> boolean. On an entry where it answers true, the cell's text runs on across every following column that draws nothing for that entry -- empty text (colour codes alone count as empty, a texture escape does not), a button whoseshowanswers false or whose label is empty, an icon with no texture, an expander with no value -- and stops one gap before the first column that does draw, or at the last column's own edge. A checkbox always draws. Decided per populate, so it follows the entry through pooling, scrolling and sorting, and re-lays only a row whose plan changed.col.formatis handed the widened width, the hover overlay follows the wider cell, and a function that raises answers "no span". The header, the sort, the filter and the search are unchanged. Differs from the request in one place: the span ends at the last column's edge, one gap short of the action-icon strip, not at the row's very edge, so it never runs under an action icon. Specs inTests/rowlist_span_spec.lua. Not verified in the client. - A list column that shows nothing on a row can give its width back (TOGProfessionMaster inbox
99c4a054, the follow-up to
span). A fixed column after the auto column may carrycollapse = true. On a row where it draws nothing -- the same testspanuses -- it is laid zero wide, the columns further from the right edge close up over it, and the auto column gains the room (a spanning name included). Their case: name, [Bank], [AH], count, where a row showing [Bank] but not [AH] used to leave an empty [AH] slot between [Bank] and the count and cut the name short. The header is unchanged,rl:CellWidthstill reports the uncollapsed width, andcol.formaton the auto column is handed the row's own, wider width. Ignored on a column before or at the auto column. Specs inTests/rowlist_span_spec.lua. Not verified in the client.
[v0.7.0] -- icon tabs down a window's edge, and an addon's own error no longer breaks a list or a window
Compatibility -- MINOR 44 -> 45
New: W:DressSideTabs / W:UndressSideTabs (new satellite LibAceGUIWidgets-SideTabs.lua, listed in
the TOC); feature-detect with if W.DressSideTabs then. Nothing for an existing consumer to do.
Widget versions: ClearFrame, GroupFrame and TLabel move from 33 to 34 (Peer Review NEW-1, thread
3028f298). MINOR 45 changed the ClearFrame settings button and TLabel's tooltip, and AceGUI keeps the
first registration on an equal version, so without the bump a v0.6.0 copy loading first kept its old
constructors for every addon in the session. Tests/widgetversion_spec.lua now fails if any of the
three is not above the last released 33; seen failing at 33.
New
- Icon tabs down a window's right edge (TOGProfessionMaster inbox c73065b3).
W:DressSideTabs(window, specs)hangs 32 x 32 icon tabs off the outside right edge of an AceGUI Frame, ClearFrame or raw frame, stacked from the top-right at Blizzard's spacing (0, -36, then 20 apart:CommunitiesFrame.xml:376-426, Classic Era). Each spec takesicon,texCoord,tipTitle/tipBody(read on hover),onClick,checkedandshownpredicates and akey; the result is the tabs in order plus:Refresh(), which re-reads the predicates and restacks the shown tabs with no gap. Every tab's checked state is re-read after any click. Pool-safe likeDressBottomRow: cached on the frame, idempotent, andW:UndressSideTabshides them all. Drawn, not built onCommunitiesFrameTabTemplateas the request suggested: its mixin's OnClick callsGetParent():SetDisplayMode(CommunitiesTabs.lua:7-12), which only the guild window has, and the template only exists once Blizzard_Communities has loaded (whether that is at login or on demand is not verified). The drawn tab uses the template's own textures and numbers, so it matches the Classic-flavour look; Forever's template inheritsRightSideTabTemplateinstead, and this draws the Classic look there too. The core now exposeslib._tipPartfor satellites. Specs inTests/sidetabs_spec.lua. Not verified in the client.
Fixed
From Peer Review, threads f6a85948 and 3b9ff3b8.
A tooltip function that errored escaped the hover unreported.
tipPartcalled atipTitle/tipBodyfunction bare, thoughsafeCallis the library's one dispatch for consumer callbacks, so the error escaped OnEnter, the rest of the handler (the other half of the tooltip, andShow) was skipped, and no tooltip drew. The client's error handler still saw that error with its stack; the cost was the skipped half and the missing tooltip, not a lost report. It now goes throughsafeCall: the error is reported and that half of the tooltip is left out while the other half draws. CoversDressBottomRowandDressSideTabs.DressBottomRowandAttachHeaderButtonranonClickbare for the same reason; both now go throughsafeCall.A non-string spec
keycould overwrite a slot in the returned table.key = 2replaced the second icon or tab, and inDressSideTabskey = "_lagwFrame"broke:Refresh(). Both now take only string keys, andDressSideTabsalso skips"Refresh"and anything starting_lagw.An older copy's satellite files could overwrite a newer copy's methods. Each satellite (Resize, RowList, DatePicker, ClickBinding, ButtonStrip, SideTabs) checked only that the library existed, so whichever copy's satellite loaded last won, even after a newer core had loaded. When the core file bails because a newer copy is registered, it now sets
lib._staleCopy, and every satellite returns early while that is set. An equal copy's satellites still run, as before. Spec inTests/satellites_spec.lua, seen red with the guard removed from SideTabs. KNOWN COST: copies older than MINOR 45 neither set nor read the flag, so one of those loading last still overwrites, until consumers ship 45 or later.More consumer callbacks ran bare (Peer Review, threads f6a85948 and 3b9ff3b8).
CreateCheckbox'sget/set, a form dialog item row'sonEnter/onLeave/onClick, a form row button'sonClickand the ClearFrame settings button's handler now go throughsafeCall. A setter that errors is reported and the box still showsget(), so it reads as refused; a getter that errors is reported and the box keeps its state; a failed item-row callback still replaces the built-in hover and click. Specs inTests/checkbox_spec.lua,Tests/formdialog_rows_spec.luaandTests/formdialog_spec.lua, seen failing before the fix; the settings button's spec inTests/older_paths_spec.luawas seen failing with the handler's call made bare again ("the error escaped the click: ... gear boom"). The four "error escaped" assertions now print the error that escaped, so a future failure names its cause. RowList's callbacks are covered by the RowList entry below, and the rest (BindCooldownButton, ShowDialog and more) by "The last bare consumer callbacks".RowList's consumer callbacks ran bare (Peer Review, thread 2efae4f2). Sorting (
onSortChanged,sortValue,tiebreak), cell text (format,classDisplay, a button column'sshow/text, theiconfunction,refontHook,strike), group headers, search and the filter menu (isHeader,searchText,filterGroup), toggles (onToggle), button cells and cell overlays (onCellClick,onClick,onCellEnter/onCellLeave,tip/cellTip), rows (onRowClick,onRowEnter,onRowLeave), action icons (onClick,show,texture) and the list hooks (onHeightChanged,onEmptyRender,onScroll,onReorder) now go throughsafeCall. Each error is reported and the rest of the draw or click still runs: a failedsortValuesorts by the raw value, a failedformatshows the raw value, a failedonCellClickfalls through toonClick/onRowClick, a failedonCellLeavestill hides the tooltip, and atiebreakthat raises is reported once and the ties keep theirSetDataorder, sotable.sortnever sees an inconsistent comparison. Specs inTests/rowlist_safecall_spec.lua, written to fail first.The last bare consumer callbacks (Peer Review, threads 2efae4f2 and c48ddae3; found by reading every call site, not by a pattern sweep). 25 sites now go through
safeCall, and each error is reported while the rest of the handler still runs. Two of them were real ordering defects: a dialog whoseonHideraised on OK never ran itsonAccept(OK hides first), and aBindCooldownButtonwhoseonClickraised never started its cooldown, so the rate limit was lost. Also covered: the menu'scheckedfunctions (a raise left the menu half-drawn), searchitemsFor, submenuchildren, a dropdown box'sitems, the Stepper'smaxandonValueChanged, a flash'sonDone,ShowDialog'sonAcceptandonShow(a raise lost the returned token),CreateSearchBox'sonChanged, a form check row'sonChanged, a form dropdown row'sitems, a form dialog'sonAccept, the cooldown'ssecondsandformat(binding a button mid-cooldown raised out ofBindCooldownButton), an expandable list'sonClickandtip(both of its return values kept), the ClearFrame info tooltip, the character counter'sonUpdate(a raise on detach left the release hook in place), ButtonStrip's reason functions (that button keeps its state, the rest still refresh), and DatePicker'stodayfunction, which now falls back to local time. Specs inTests/callback_safecall_spec.lua, seen failing first (25 of 28 red before the fix; the other three pin multi-value returns and a dropdownonChangedthe menu row's ownxpcallalready caught).A tooltip-placement or floor function that errored escaped unreported (Peer Review, threads 64514880 and c48ddae3). The session-wide
tooltipOwner, aShowTooltipcall'sopts.owner, RowList's owntooltipOwner(link and icon hovers), TLabel's tooltip, and aPersistWindow/SetWindowProfileminWidth/minHeightfunction were all called bare. Each now goes throughsafeCall: a placement function that errors is reported and the library's built-in placement (above-right, flipping below-left near the top of the screen; new privatelib:_DefaultAnchor) is used instead -- never the session-wide owner when it was a per-list or per-call one that failed. A floor function that errors is reported and reads as no floor: that axis gets the window's own minimum from beforePersistWindow(the resize handle's own minimum on a window with one), scaled like any other kept bound, and the function is tried again on every later scale change. Specs inTests/widgets_spec.lua,Tests/scale_spec.lua,Tests/rowlist_minor34_spec.lua,Tests/older_paths_spec.luaandTests/windowprofile_spec.lua, each seen failing with its call made bare again. A row action icon whose tooltip placement errors still shows its tooltip (spec inTests/rowlist_minor34_spec.lua).A size profile with no floor kept the previous profile's floor (Peer Review NEW-5, thread 3028f298). Switching from a profile with
minWidth = 600to a resizable one with no floor left 600 on the window, so the player could not drag it narrower, and the next UI scale change grew it to 600 x scale. Each axis a profile gives no floor for now gets the window's own minimum from beforePersistWindow. Spec inTests/windowprofile_spec.lua, seen failing first.Moving the mouse off a TLabel hid a tooltip another window had taken over since (Peer Review NEW-4, thread 3028f298). TLabel now records that it holds the tooltip and hides it on leave only while it still does. Spec in
Tests/older_paths_spec.lua.A floor function that errored was never tried again when it was the only floor (Peer Review NEW-6, thread dfd1e1a5).
PersistWindowandSetWindowProfileregistered their scale listener only when a floor resolved, and an erroring function resolves to none, so a floor that failed once (say, consumer state not ready yet) stayed off for the session. Both now register whenever a floor was asked for, through one shared check (lib._FloorAsked). Specs inTests/scale_spec.lua(with and without a resize handle) andTests/windowprofile_spec.lua, seen failing first (one report instead of two).On a window with no resize handle, a size profile with no floor kept the minimum size of the scale it was chosen at (Peer Review R3, thread dfd1e1a5), while a profile with one floor had its other axis follow the scale. On that path every resizable profile now follows the scale. Spec in
Tests/windowprofile_spec.luaon AceGUI's own Frame, seen failing first (400 x 200 at scale 2).A reused TLabel showed the previous owner's tooltip (Peer Review R4, thread dfd1e1a5). AceGUI pools labels, and acquiring one reset everything but the tooltip. It now clears it. Spec in
Tests/older_paths_spec.lua, seen failing first. TLabel is already at 34 for this release.A RowList whose row height scaled to 0 pixels built rows until the client ran out of memory (Peer Review G2, thread c48ddae3, found by reading every loop in the file).
Newalready refused a height of 0 or less, but a positive one still rounds to 0 at the current scale (0.4 at scale 1.0, anything under 0.625 at 0.8), and the visible-row count then divided the host's height by 0. Every row is now at least 1 pixel tall; a height that already drew at least one pixel is unchanged. Spec inTests/rowlist_groups_spec.lua, seen failing first (it never finished, and the runner's time limit ended it).A floor function that worked once and errored later left its last value on the window (Peer Review F1, thread 322e50a0). On a window with a resize handle the handle kept the floor's last answer, so the player could not drag below it and the next scale change grew the window to it. It now drops back to the handle's own minimum. While a saved window follows the UI scale the library also runs the handle's own scale handler itself, after the floor is updated, so the order the two ran in can no longer leave the old floor in place. KNOWN COST: calling
MakeResizableon a window that is already saved re-registers the handle's own handler, and that ordering race can come back for one scale change. Spec inTests/scale_spec.lua, seen failing first.A second
PersistWindowon the same window whose floor errored kept the first call's floor (Peer Review F2, thread 322e50a0). On a window with no resize handle the fallback was read from the window's current bounds, which the first call had already raised. It now uses the window's own minimum from before the first call. Spec inTests/scale_spec.lua, seen failing first.A huge
rowCountor a stray high key inactionscould hang a RowList (Peer Review G2, thread c48ddae3).rowCount = 1e9built a row per step, and anactionstable with a key like1e9was walked one index at a time up to it.rowCountis the starting pool only (the list grows it to fit anyway), so it is now capped at 200, and one that is not a number or is negative takes the default 30. The actions are now collected by their keys, so the cost follows how many there are. Specs inTests/rowlist_spec.lua, seen failing first.A column that mixed numbers and text sorted in an order that depended on the order the rows came in, and the filter menu listed its values the same way. Numbers compared by value but a number against text compared as text, so "10" sorted before "5x", "5x" before "9" and 9 before 10: a circle (a 400-row mixed column came out wrong but never raised). Numbers now come before text, in the sort and the filter menu alike, through one shared rule. Columns of only numbers or only text sort as before. Specs in
Tests/rowlist_spec.lua, seen failing first.A RowList on a very tall host built a row frame for every row of height, however little data it held, and one whose host reported an infinite height never finished building (Peer Review, thread f4bdb4d0). A host 100,000 units tall built 6,250 rows for a 10-entry list and kept them. The pool now grows only to the rows the view can draw: the window in rows or the entries in view, whichever is smaller. It grows again when the data grows, at the next draw. Room under the header that is not a number, or is 1,000,000 units or more either way (infinite included), now counts as none, which with data present is the empty-render warning. The 1,000,000 is a judgement, not a measurement: no real screen is near it, and the 100,000-unit test hosts fit under it.
rowCountis still the starting pool, and the pool still never shrinks. Specs inTests/rowlist_poolcap_spec.lua. Against the old code the two 100,000-unit examples failed with 6,250 rows and the infinite-height one ran into the runner's 60-second limit. The NaN and short-host examples also pass on the old code, so they guard against a regression and do not prove the fix.Tests/rowlist_groups_spec.luanow expects 3 rows (the starting pool) for a one-entry list where it expected 64.The empty-render warning crashed on an infinite host height (Peer Review, thread af1af9ed). It formatted the height as a whole number: Lua raised "integer overflow attempting to store inf" for an infinite height and printed -2147483648 pixels for NaN, and a height of -3e9 raised the same overflow (Peer Review, thread eeecd1f8, N1). It now says the height "is not a usable height", using the same check that decides the list has no room. The two apply it to different numbers (the row count tests the height minus the header, the warning the raw height), so within one header height of the bound they can differ; either way the list has no room and nothing overflows. The diagnostic entry's
parentHeightis 0 for such a height, so code reading it never meets inf or NaN, and the raw value is in a newparentHeightRawfield (eeecd1f8, F2), now listed in the README. Four examples inTests/rowlist_poolcap_spec.lua(infinite, NaN, negative infinite, -3e9), with noonEmptyRenderhook, were seen failing first; they also pinparentHeightRawto the height the host reported.The scroll bar of a RowList whose host reported a height of negative infinity or -3e9 was placed an infinite or 3-billion-unit distance away (self-audit 22d2a3d9, finding 1). The scroll bar read the host's height without the usable-height check, so its top offset became -inf or -3000000000, and a negative height such as -50 put the bar's top above the host. An unusable or negative height now lays out as on a zero-height host: top and bottom offsets 0, arrows hidden. Five examples in
Tests/rowlist_poolcap_spec.lua(infinite, NaN, negative infinite, -3e9, -50) pin those exact offsets, both cached and as passed toSetPoint(Peer Review 957daeed); all five failed against the old code (top 24, 24, -inf, -3000000000, -50).A
tiebreakthat gave contradictory answers madeSetDataraise and drew no rows. One that answers "before" both ways round can maketable.sortraise, and that error escaped. It is now reported once and the list sorts without the tiebreak, so tied rows keep theirSetDataorder. Example inTests/rowlist_safecall_spec.lua, seen failing first ("attempt to index local 'x' (a nil value)").
Specs for the first three fixes are in Tests/sidetabs_spec.lua, Tests/bottomrow_spec.lua and
Tests/headerbutton_spec.lua. All were seen failing before the fix except the DressBottomRow
numeric-key spec, which was written after the fix and has not been seen red.
Changed
safeCall(andlib._safeCall, which satellites use) now returnsok, firstReturnrather than nothing useful, so a getter, a predicate or a tooltip-text function goes through the one helper instead of a hand-written wrapper per site. The tooltip text,Checkbox_Refreshand the SideTabs predicates use it. Every existing caller ignores the return or already readok, so none changes behaviour. A separatesafeGetlocal was tried first and pushed the core file past Lua 5.1's limit of 200 locals in one function, so the file stopped compiling for anyone loading this working copy (TOGProfessionMaster thread f23cf24c); it was removed the same day.- The core file had no room left for another file-scope local, and now has 18. Measured, it sat at
exactly Lua 5.1's 200. The seven
HEADER_*layout constants are now one localHEADERtable and the thirteenFORM_*constants one localFORMtable; nothing outside the file sees either. NewTests/localheadroom_spec.luaguards all four of Lua 5.1's per-function compile limits for every Lua file the TOC ships: free file-scope locals (by padding the file until it stops compiling), and, read from the bytecode of every function including anonymous closures, the most upvalues one closure holds, the most locals an inner function holds at once, and the most registers any function needs. The limits themselves (200 locals, 60 upvalues, 249 registers) are measured from the interpreter, not quoted. Each example's name prints its number; each fails below a margin of 10. The bytecode reader is checked against generated functions, including one at each limit, which must read as a breach; the check that it counts only locals live at once was seen red with the count deliberately broken. The padding check was seen red against the core before the fold (0 free). At this entry: core 18 free locals, busiest closure 21 of 60 upvalues, busiest inner function 30 of 200 locals, and its main chunk uses 187 of 249 registers. Tests/satellites_spec.luano longer leaks into later spec files. It re-runs the satellite files to test the stale-copy guard, which replaces the library's methods; it now saves every field first and puts them all back afterwards.- A TLabel with no consumer
tooltipOwnernow places its tooltip like every other tooltip in the library (above-right, flipping below-left near the top of the screen) instead of directly above the label, because it now goes throughAnchorTooltip. Tests/scale_spec.lua's raising-listener example no longer leaks its listeners into later examples. It registered them under string owners, which the weak listener table never drops, so a later example's error count picked up an extra "consumer bug" report. It now removes both, and the floor example counts only its own reports.- Spec files that reload the library now empty AceGUI's widget registry first (Peer Review R1,
thread dfd1e1a5). Without it a widget was built by an earlier file's copy of the library, so its
scale listener sat in that copy's table and the reloaded copy's
W:SetScalenever reached it:Tests/windowprofile_spec.luasaw 450 wide instead of 600 when it ran afterTests/scale_spec.lua.windowprofile_specand 22 more files now callace.fresh("AceGUI-3.0")before reloading;Tests/forgetwindow_spec.luaneeded two in-file fixes afterwards. Tests/rowlist_spec.luaruns in about 3 seconds instead of about 40 (Peer Review G2). Two examples compared frames withassert.is_not.equal, and the harness builds the positive assertion's failure message even when the negated one passes, which walks every frame built so far. On a busy machine that pushed the file past the runner's 60-second per-file limit. Both now useassert.is_true(a ~= b, ...). The same change inTests/dialog_spec.luaandTests/dock_spec.lua(seven asserts) took those two files from about 4.9 seconds to about 1.4, andTests/scale_spec.luahas it too. The harness side is reported to WoWAPITesting.- The
Tests/wowapiharness pin moves tof1797a7, where a passing negated assertion no longer builds the positive one's message andrepr()is bounded. Nothing needed adopting; the comment inTests/scale_spec.luathat described the old cost is gone, and theassert.is_true(a ~= b, ...)lines stay. The whole suite passed in normal and reverse order on the new pin. Tests/scale_spec.luaputs its error handler back even when a cleanup step errors. Four examples forgot or released their windows before restoring the handler, so an error there would have left the test's capturing handler in place for every later spec file (Peer Review, thread 55e2f530).Tests/rowlist_spec.lua's instruction budget puts the runner's hook back (Peer Review, thread f733d29e).budgeted()ended with a baredebug.sethook(), which removed the runner's per-file deadline (and coverage's line hook) for the rest of the run, so a later runaway loop would have hung instead of timing out. It now saves the hook it found and restores it. Its example was seen failing first.
[v0.6.0] -- a secure button that acts once per click, a list that draws its last row, and a list that refuses a zero row height
Compatibility -- MINOR 43 -> 44
New: W:CreateSecureActionButton takes opts.clicks = "up"; an older copy ignores it, so detect
with select(2, LibStub("LibAceGUIWidgets-1.0")) >= 44. Changed: RowList counts a row that fits to
within half a unit. Nothing for an existing consumer to do.
New
- A secure button that runs PreClick/PostClick once per click (Ultimate Guild Roster inbox
d761a47e). By default the button registers both halves of a click, because the secure handler acts
on the down or the up depending on
useOnKeyDown/ the ActionButtonUseKeyDown CVar (SecureTemplates.lua:807-808, Classic Era) -- so a consumer'spreClickandpostClickran twice and were not told which half acted. UGR's macro tool re-armed its next batch in the down'spostClick, and the up then ran that batch.opts.clicks = "up"registers"AnyUp"alone and sets theuseOnKeyDownattribute to false, so the action runs on the up under either CVar setting and the consumer sees exactly one pair. Keybindings follow the same rule (act on release) -- chosen so a binding cannot reintroduce the double pair. Default unchanged. Specs inTests/securebutton_spec.lua. Not verified in the client.
Fixed
- A fitContent RowList could draw one row short and show a scroll bar (TOGProfessionMaster inbox
62eb5c52, the Reagent Tracker at a non-100% scale).
_recomputeVisibleRowsfloored the parent's height over the row height with a 1e-6 epsilon, which covered the division's rounding but not a client that hands back a height a hair under the one set: 0.01 short turned 3 rows into 2. The count now allows half a unit, so at most it adds a row that overhangs the host by under half a unit. Spec inTests/rowlist_groups_spec.lua. That the client returns a short height is TOGPM's report, not verified in game here. - A RowList given a row height of 0 or less built rows without end (Peer Review, thread
f069a63d). The visible-row count divided the host's height by that row height, so the pool grew
until the client ran out of memory.
RowList:Newnow takes such a height, or a non-number, as the default. Spec inTests/rowlist_groups_spec.lua.
[v0.5.0] -- a button strip with a status line, a live character counter, toggle chips on an options page, a form label you can rename, and a scrollbar that fits a short list
Compatibility -- MINOR 42 -> 43
New: W:CreateButtonStrip (new satellite LibAceGUIWidgets-ButtonStrip.lua, listed in the TOC);
feature-detect with if W.CreateButtonStrip then. New: W:AttachCharCounter / W:DetachCharCounter;
feature-detect with if W.AttachCharCounter then. New: a CreateFormDialog frame has
f:SetLabel(i, text) and f.labels[i]; feature-detect with if f.SetLabel then. New: W:OffCooldownTick,
W:UnbindCooldownButton and W:RemoveEscapeChild; feature-detect
each with if W.<name> then. Changed: a ShowDialog prompt inside an Escape-layered window joins
that layer for the show (detect with if W.RemoveEscapeChild then). Changed: LAGW-Chip
widget Version 1 -> 2 (the AceConfigDialog toggle surface) and LAGW-SearchBox Version 1 -> 2
(SetShowCounter); feature-detect with AceGUI:GetWidgetVersion(type) >= 2. Changed: a RowList host
too short for both scroll arrows hides them. Nothing for an existing consumer to do; every addition
is opt-in.
New
- A row of explained buttons with a status line (Ultimate Guild Roster inbox 31133c51, item 1,
so UGR can delete the strip it and FastGuildInvite hand-rolled).
W:CreateButtonStrip(parent, specs, opts)laysW:CreateExplainedButtonbuttons left to right with a right-justified status line on the same row. Each spec is{ text, tip, onClick, width, reason, key }; areason(string, or a function asked at build, on every hover and onstrip:Refresh()) greys the button and is the red last tooltip line. Returns the strip frame itself, carrying.frame,.buttons,.statusandSetStatus/Refresh/GetButton(n | key | text), because UGR anchors other frames to its strip. Every default size is UGR's own (GUI/UI.lua:18-57) and followsW:SetScale. The status line is bounded on the left by the last button so a long status is clipped instead of running over the buttons (UGR's was right-anchored only). Specs inTests/buttonstrip_spec.lua. The harness loads the new file since WoWAPITesting pin23d0039(inbox c94d8892). Not verified in the client, including the clipping itself. - A live "n / max" character counter on an edit box (UGR inbox 31133c51, item 2).
W:AttachCharCounter(target[, opts])works on a raw EditBox (single- or multi-line) or an AceGUI widget with.editboxor.editBox; it followsSetMaxLetters(hooked) and every text change, hides while the max is 0, turns red at the limit, and detaches on Release. It counts with the box's ownGetNumLetters()againstGetMaxLetters(), the pair Blizzard's own counters use (SecureUIPanelTemplates.lua:142, Classic Era tree).LAGW-SearchBox:SetShowCounter(bool)uses it and narrows the text box by the width of "max / max" so it does not jitter. Specs inTests/charcounter_spec.lua. Not verified in the client. LAGW-Chipcan be an AceConfig toggle'sdialogControl(UGR inbox 20d63ff4, so an options page draws its switches as toggle tokens). Version 2 addsSetLabel(=SetText),SetTriState(accepted; a nil value draws off),SetDescription(shown as the chip's hover tooltip throughShowTooltip, cleared on acquire and release),SetImage/SetImageSize(ignored). Inside AceConfigDialog a chip with nowidthsizes to its label: AceConfigDialog's own 170px default is read as "no width" onceSetTriStatehas marked the chip as a toggle, and every other width is honoured (width = 1and"normal"reach the chip as the same 170, so they draw label-sized). An explicit width now survivesSetLabel/SetText; before, every relabel shrank the chip back to its label, which a language switch would have hit (Peer Review on audit 509928c3). Specs inTests/chip_aceconfig_spec.luadrive the real AceConfigRegistry and AceConfigDialog. Not verified in the client.- A form dialog's row label can be renamed (UGR inbox 1a2fe53f, for a language switch without a
/reload).f:SetLabel(i, text)sets rowi's label (the text right of the box on acheckrow); nil or""clears it; an index with no row does nothing, asSetNotedoes.f.labels[i]is the FontString. Spec inTests/formdialog_setlabel_spec.lua. - A cooldown listener can be dropped (UGR inbox 58585085).
W:OffCooldownTick(name, fn)removes every registration of that function for that name and returns whether there was one; it never touches the clock. Removing a listener from inside a tick neither calls it again nor skips the next one: the slot is blanked during the fan-out and the list compacted after it. Specs inTests/cooldown_spec.lua. - A cooldown button can be unbound (UGR follow-up on inbox 58585085). UGR's language switch
throws its Sync tab away and builds a new button, so the rebind fix below never sees the old one.
W:UnbindCooldownButton(button)drops the painterBindCooldownButtonregistered for that button and returns true, or false for a button that is not bound; the clock and the button's click handler are left alone. Specs inTests/cooldown_spec.lua.
Fixed
One Escape in combat closed a prompt AND the window it sat in (Peer Review inbox e2a2b784, found reviewing UGR's audit ace8973e). The shared dialog is its own
UISpecialFramesentry; inside a window with an Escape layer, the client's walk hid the dialog and the layer's proxy on the same press, and the proxy, finding no child up, closed the window. Out of combat the dialog's own key handler or focused box usually took Escape first, so this hit a prompt shown in combat or an edit prompt whose box had lost focus.ShowDialognow walks up from its parent to the first window with a layer and joins the dialog to it for that show only; finishing the show takes it out again. The dialog's name goes back onUISpecialFramesat the nextShowDialogthat finds no layer, not when the show ends: an Escape-closed prompt ends inside the client'spairswalk of that list, and Lua 5.1 leaves the walk undefined if the list gains an entry (Peer Review, same thread). So addon A's window closing later cannot cancel addon B's prompt, and a consumer that added the dialog to its layer itself keeps it. NewW:RemoveEscapeChild(window, frame)takes one child out of a layer and re-lists its name; it,EscapeLayerandDropEscapeLayerare documented as not to be called from an OnHide that Escape reaches, for the same reason. A closing window now snapshots its shown children before hiding them, since a child's hide can now take it out of the list being walked. Specs inTests/dialog_escapelayer_spec.lua; with the join disabled, 7 of its 11 fail. Not verified in the client.A prompt over a window's floating panel or form still lost the panel to one Escape (Peer Review inbox ace03ea9, UGR's "Clear a range" form and its confirmation). Those panels are parented to UIParent and join the window's layer as children, so the walk up from the prompt's parent never reached the window. A layer child now carries the windows whose layers it was added to, and
ShowDialog's walk joins the most recently added of those that is shown and still holds it: in combat the first Escape closes the prompt, the second the panel, the third the window. A list, not one owner, because a frame can be a child of several layers (the shared frameRemoveEscapeChildis documented for), and dropping one layer must not cost the other its order; a window that adds the child again moves to the end. A hidden owner is never joined, because its proxy takes no press; with no shown owner the prompt is listed onUISpecialFramesas before (Peer Review on audit 4e6d3a5b). Specs inTests/dialog_escapelayer_spec.lua; four of the five new ones were seen red with their part of the fix disabled; the fifth (a child that left its layer) holds without the lookup too. Not verified in the client.Rebinding a cooldown button stacked another listener (UGR inbox 58585085, found by Peer Review on thread 8ae2f42a).
W:BindCooldownButtonregistered a painter on every call and nothing removed one, so UGR's Sync tab, rebuilt on each language switch, left the old button's painter running every tick until/reload. A button bound again now drops its earlier painter first, whether the new bind uses the same name or another. Specs inTests/cooldown_spec.lua. Not verified in the client.One cooldown listener that errored silenced the rest. The error escaped the tick, so every later listener missed it. Each listener is now called protected and the error goes to the client's error handler. Spec in
Tests/cooldown_spec.lua.A consumer callback's error report now carries the callback's own stack (Peer Review N1 on audit 3ffffa8a). The library called consumer callbacks two ways:
pcall, whose report has already lost the stack of the code that raised, and, for menu clicks only,xpcallwith adebugstackhandler. One file-scopesafeCallnow does the second for every dispatch site:OnMenuClosedcallbacks, scale listeners, the pool-safe release functions, cooldown listeners and every callback the Resize satellite fires (throughlib._safeCall). Menu clicks share itswithStackhandler. Spec inTests/cooldown_spec.lua.A RowList in a short host drew its scrollbar upside down (TOGProfessionMaster inbox 782ce874, their one-row shopping list). The lane was inset one arrow height at each end, so a host shorter than the header plus both arrows put its top below its bottom. Below that height the arrows now hide and the lane spans what is under the header, clamped so it is zero tall at worst; a host that grows back gets its arrows back, re-evaluated on every refresh. Hosts that drew correctly before are laid out exactly as before. Spec in
Tests/rowlist_shorthost_spec.lua, confirmed red without the fix. Not verified in the client, including how the 24px thumb looks in a very short lane.W:AttachWidgetTooltipon an AceGUI MultiLineEditBox said nothing over its text area. AceGUI names that boxeditBox, and onlyeditboxwas looked for. Spec inTests/poolsafe_spec.lua.
Tests
Tests/escapelayer_spec.luadrives Escape through the harness'sframes.pressEscapeinstead of its own copy of the walk (Peer Review inbox 721966e5). Harness pin moved to05cb0c8.
[v0.4.0] -- a menu row's tooltip draws above the menu, an expandable list's rows fill its width, and a prompt can look like a window
Compatibility -- MINOR 41 -> 42
New: CreateExpandableList takes opts.valueColumn = { offset, width, justify }; feature-detect
with LibStub.minors["LibAceGUIWidgets-1.0"] >= 42. A list created without it draws as before.
New: ShowDialog and ShowCopyBox take style = "window" and title; feature-detect with
if W.DIALOG_STYLES and W.DIALOG_STYLES.window then. A prompt without style draws as before, on
the same LibAceGUIWidgets_Dialog frame.
Changed: while a tooltip drawn by this library is anchored to a frame in TOOLTIP strata
(an OpenMenu level, the DatePicker, anything built with W:CreateFloatingFrame), GameTooltip's
frame level is raised 50 above the highest TOOLTIP-strata level among the frame and its parents,
and put back when the tooltip hides or the library shows its next one. Changed: CreateExpandableList
anchors its content's width to the scroll box instead of setting it, and re-lays out from the
scroll box's OnSizeChanged (hooked) instead of list.frame's (which it no longer sets). Nothing for
a consumer to do.
New
- An expandable list can line its values up in one column (TOGProfessionMaster inbox a79b0269,
so the Guild tab's counts sit under a "Characters" header instead of at the window's right edge).
opts.valueColumn = { offset, width, justify }anchors every row's value, header and child style at every depth, LEFT at the row's left plusoffset,widthwide, justifiedjustify(defaultRIGHT). Rows themselves are never indented (only their glyph and label are), so depth does not move the column. Both numbers are scale-1.0 and are re-applied each time a row is acquired, so they followW:SetScale. The label's right edge stays 6px before the value, so a long name truncates before the column; the space to its right stays empty and the row highlight still spans the row. Specs inTests/expandable_depth_spec.lua: a depth-0 and a depth-2 value share the offset, width, justify and left edge; the column follows the scale; a list without the option stays right-anchored. Not verified in the client. - A prompt can look like a window (ClassicCalendar inbox 82cae97e, so their delete confirm
matches their World Buff and Auto-Moderator windows).
ShowDialog(parent, { style = "window", title = ... })draws Blizzard'sBasicFrameTemplateWithInset(title bar, close X, inset;Blizzard_UIPanelTemplates/Classic/UIPanelTemplates.xml:676,:714,:751, Classic Era tree) instead of theUI-DialogBoxbackdrop, 22px taller with the body 22px lower to clear the bar. The window look is a second shared frame,LibAceGUIWidgets_DialogWindowonW._windowDialog, because a frame cannot change template once built and restyling the one plain frame per show would leak into the next addon's prompt. The two frames share every rule: showing either closes a prompt open in the other first (itsonHidebefore the newonShow),CloseDialog(token)looks in both, and both are inUISpecialFrames. The X is wired to the same close as Cancel rather than the template'sUIPanelCloseButtonscript, soonHideruns once.ShowCopyBoxpassesstyleandtitlethrough.W.DIALOG_STYLES = { plain = true, window = true }is the feature check. Specs inTests/dialog_spec.lua: the style is advertised; the window frame carries the title, X and inset and the plain one does not; the X runsonHideonce and never accepts; a replaced prompt'sonHideruns across styles andCloseDialogfinds either; the window frame is one Escape-list entry;ShowCopyBoxcarries the style. Not verified in the client.
Fixed
- A menu row's tooltip drew behind the menu (ClassicCalendar inbox d8a239c7, from a player
report on the Templates menu's "Loot system" row).
CreateFloatingFrameputs every menu level at strataTOOLTIP, level 100 + 10 per depth.GameTooltipis alsoTOOLTIPstrata, declared with no frame level (Blizzard_GameTooltip/Classic/GameTooltip.xml:13, Classic Era tree), andshowTooltipnever raised it, so in the same strata the menu's higher level won and the tooltip was covered wherever it overlapped. Every tooltip the library anchors now lifts the level and restores it through the sameOnHidehook that already restores the minimum width. The lift is inW:AnchorTooltipitself (so form-dialog item rows, RowList icons and hyperlinks, and the ClearFrame info button get it), plusShowTooltip'sopts.ownerbranch, the TLabel tooltip, and RowList's owntooltipOwnerbranch. Specs inTests/menu_spec.lua: a root row's and a submenu row's tooltip sit above their menu and the level comes back on hide;AnchorTooltipalone raises over a floating panel; a frame outsideTOOLTIPstrata leaves the level alone. Not verified in the client. - An expandable list's rows were drawn 40px wide, every label as ".." (TOGProfessionMaster
inbox 0f350cea, their Guild tab).
CreateExpandableListset the content's width from a one-timescroll:GetWidth()read, floored at 40, and re-read it only from the outer frame'sOnSizeChanged. A list placed in an AceGUI group before the group was laid out read a width of about 0, and nothing corrected it once the scroll box was sized. The content's width now comes from its two top corners anchored to the scroll box, less the bar and 2px, exactly the rule theCreateScrollFramenote already states, so it is right in any sizing order. The re-layout (for the scroll range) now hangs off the scroll box's own size change; the outer frame's handler is gone, because the scroll box fills that frame and hooking both laid out twice per resize. A consumer that hookedlist.box.scroll'sOnSizeChangedto calllist._relayout()can drop that hook. Spec inTests/expandable_depth_spec.lua: a list filled in an unsized parent, which is then sized and resized, has rows exactly as wide as the scroll box less the bar. Confirmed red with the width pinned at 40. Not verified in the client.
[v0.3.1] -- a window that re-lays-out while you drag its edge, and a docked ClearFrame that hears its title bar
Compatibility -- MINOR 40 -> 41
No new API. Changed: a ClearFrame now carries its own onResize on its resize handle. At most once
per frame draw during a drag, it calls the widget's DoLayout, but only when AceGUI's own content
hook has not already laid out at the current size. onResizeStop (the size and position saved to
the status table) is unchanged. A consumer that set its own onResize
through W:GetResizeHandle(win).onResize = fn replaces this one, exactly as before; through
W:MakeResizable(win, { onResize = fn }) the same. ClearFrame, GroupFrame and TLabel move
32 -> 33 together, so an older ClearFrame without the live re-layout cannot win a tie. Changed:
W:DockWindow also hears a drop on a ClearFrame's title bar, and a docked AceGUI window gives up its
dock on Release; a consumer that hooked the title bar itself can drop that hook.
Changed
- ClearFrame makes sure its contents are laid out during a resize drag (Dibs contract
DIBSREQ-LAGW-003, the half still open after MINOR 30 madeMakeResizablemerge callbacks). Before, the constructor passedonResizeStoponly. AceGUI'sRegisterAsContaineralready hooks the content frame'sOnSizeChanged(ContentResize), which callsDoLayoutand records the size incontent.width/content.height. In the harness model that hook already re-lays-out the contents during a drag, twice per cursor move (once per dimension). So the newonResizere-lays-out only a size that hook did not record, and the draw adds nothing on top of it. The first cut of this change calledDoLayoutunconditionally, which doubled the work; the README (MakeResizable) already warned against that, and the guard came out of reading it. The callback readsf.objat call time, because AceGUI pools the frame. Not verified in the client: whether the content hook fires during a real drag as it does in the model. If it does, Dibs'C.LiveRelayoutis a secondDoLayoutper draw and can go. Specs inTests/scale_spec.lua: a drag lays out at least once and the draw adds none, and a size the hook missed is laid out once on the draw and not again.
Fixed
- A docked ClearFrame now undocks and re-docks when you move it by its title bar (PersonalShopper
inbox bd091818).
W:DockWindowheard a drop only through the frame'sOnDragStop, but aClearFrameis moved by its title bar, which callsStopMovingOrSizingfrom its ownOnMouseUp, soOnDragStopnever fired andh:Moved()was never called. Dragging a docked ClearFrame away left it docked in the saved preference, and dropping one at the edge never docked it.DockWindownow also hooks the title bar'sOnMouseUpon any widget whosetitleis a child of the docked frame. PersonalShopper's own hook on the title bar can go; if it stays,Moved()simply runs twice. - Releasing a docked AceGUI window lets go of its dock. The window's frame went back to AceGUI's
pool still owned by
DockWindow, so the next addon'sAceGUI:Creategot a frame whoseOnShowdocked it to our target, and thatPersistWindowrefused as ours. Found by the new spec above, whose released ClearFrame made a later spec file'sPersistWindowfail. On release the handle now undocks and drops its record on the frame, and its script hooks (which cannot be removed) do nothing once the frame is no longer theirs. Specs inTests/dock_spec.lua, against a real ClearFrame. Not verified in a game client.
[v0.3.0] -- toggle chips, a tab strip that scrolls, a top-right window button, a strip row without an empty caption band, and Whitemane in the TOC
Compatibility -- MINOR 39 -> 40
New: the LAGW-Chip AceGUI widget type (Version 1) and W:AddChips, the LAGW-TabStrip widget
type (Version 1), and W:AttachHeaderButton / W:DetachHeaderButton with ClearFrame:SetHeaderButton.
Feature-detect with AceGUI:GetWidgetVersion("LAGW-Chip") or if W.AddChips then,
AceGUI:GetWidgetVersion("LAGW-TabStrip"), and if W.AttachHeaderButton then /
if win.SetHeaderButton then. Changed: a LAGW-Strip row with no
caption on any of its controls no longer reserves the caption line, so a strip whose second row is
captionless is shorter by one caption line; a consumer that sized a window floor from
W:StripRowWidth is unaffected (only the height moved). ClearFrame, GroupFrame and TLabel
move 31 -> 32 together, because a ClearFrame without SetHeaderButton must not win a tie.
New
LAGW-Chip, an on/off pill button (Grouper contractc98a1eab; their operator: "ask widgets to add a toggle chip, i think it could be good").SetText,SetValue/GetValue,SetDisabled, andOnValueChanged(value)on a click, as AceGUI's CheckBox does;SetValueis silent. On is the library accent (tinted fill, accent border, white text), off is the dark fill and grey border of the dropdown box; hover adds the button highlight; disabled is dimmed and ignores clicks but keeps its tooltip. Sized to its label, following the UI scale. The frame is a Button, soW:AttachWidgetTooltipanswers on it, and it drops intoW:StripAddlike any control.W:AddChips(container, order, labels, values, onChanged)builds one chip per key inorder, lit fromvalues[key]. A click writesvalues[key]and then callsonChanged(key, value, chip). Returns the chips keyed by key.LAGW-TabStrip, tabs added at runtime that scroll with<and>(Dibs contractcb965300; their operator: "all you have to do is have < and > arrows at either side of the tabs so you can scroll back and forth between tabs").AddTab(value, text)(a value already there is re-labelled),SetTabText,RemoveTab,ClearTabs,SelectTab(value)(silent, and scrolls the tab into view),GetSelected,GetTabs,SetMaxTabWidth,GetVisibleRange, andOnTabSelected(value)on a player's click of another tab. A tab is its label plus 10 a side, capped at 140 (scale-1.0); a longer label is cut short and shows in full on hover. The arrows appear only when the tabs overflow, each scrolls one tab, and each is disabled at its end. The strip only: the consumer draws what a tab shows. Looks like the chips: the selected tab in the accent, the rest grey.- A text button in a window's top-right corner (Grouper contract
12c62f8b; their operator: "can you add a button in the top right to switch the user back to the legacy UI ... we'll need the legacy ui to have the same button to switch to the new UI").W:AttachHeaderButton(widget, text, onClick, tipTitle, tipBody)works on a stock AceGUIFrameand on aClearFrame, which also has it aswin:SetHeaderButton(...); both go through the one builder, so the two windows cannot drift apart. Inside the border, 14 in from the right, its bottom level with the content's top (27 down), sized to its label (10 a side, at least 40) and lifted above the resize strips. On a ClearFrame it sits a further 4 in and 6 down (Grouper inboxe62d2977): with the stock numbers the operator saw it straddle the ClearFrame's top border ("it kinda overlaps the border in the new UI") while the stock Frame's was "perfect". I have not found the cause in the code; the offset is set from what was seen, and the operator rechecked it in game after the move: "perfect". Calling again re-labels the same button;text = nilhides it. It follows the UI scale on a ClearFrame and stays at 1.0 on a stock Frame, whose chrome does not scale. Release hides it and drops its handler, tooltip and scale listener, so AceGUI's pool cannot hand it to another addon's window;W:DetachHeaderButtondoes that early.
Changed
- A strip row with no captions has no caption band (raised with the chip request). I reserved the caption line on every row, so a captionless second row sat under an empty band.
Fixed
- The TOC now lists 38000 (Whitemane) beside 38002 (Titan Reforged) (Peer Review
2bd6dff2). I left it out, so on Whitemane the library read as out of date while Grouper, which depends on it there, was current. 38002 stays.
Tests and docs
- Tests. New
Tests/chip_spec.lua,Tests/tabstrip_spec.luaandTests/headerbutton_spec.lua(placement against a real AceGUI Frame and ClearFrame, the scale, and what a released window keeps);Tests/strip_spec.luacovers a captionless row, andTests/dock_spec.luacovers two windows sharing one saved table. The suite runs 1049 checks, 0 failing, and every shipped.luafile is at 100% line coverage (6380/6380). - Docs. README's filter-strip entry now says a captionless row reserves no caption line. The CurseForge page lists the chips and the tab strip, carries a MINOR 40 example and the two new widget type names, and keeps the last five versions under Recent Updates.
- Docs made to agree with the code and with each other (a full README-against-CurseForge read).
The CurseForge page said a window given both PersistWindow and DockWindow is kept in the
diagnostics record; it is only refused and printed (
otherOwnercalls_Say, not_Report). It said a dialog parented into a window "closes with that window"; since MINOR 33 it goes off screen with it and comes back still open,onHideunrun. It said/lagw debugis remembered, which holds only when the library is installed as its own addon. README still said the three legacy widget types register at 31, and never mentioned the filter menu's Select all / Clear all, which act on every value drawn or not. The CurseForge page now also names the smaller helpers the README documents (scale helpers, named cooldowns,TriggerVersionCheck, the click-binding text helpers,ToggleMenu,SetAllExpanded, RowList column widths, and the read-back partners).
[v0.2.9] -- library chat lines only in debug, a /lagw switch, your own item-row hover, and a menu that closes on a bad pick
Compatibility -- MINOR 38 -> 39
New: the debug field on Configure (true, or a function returning a boolean), W:IsDebug(),
W:SetDebug(true|false|nil), the /lagw debug [on|off|reset] slash command, the LibAceGUIWidgetsDB
SavedVariables (standalone addon only), and onEnter / onLeave / onClick on a CreateFormDialog
kind = "item" row. Feature-detect with if W.IsDebug then (the item-row callbacks ship in the same
MINOR). Changed: the library prints nothing to chat unless debug is on. W:Diagnostics() records
exactly what it did before, so a consumer spec asserting on it is unaffected. A menu item whose
onClick raises now closes the menu and reports the error through geterrorhandler(); a consumer
wrapping its own handlers in pcall for this can drop the wrapper. No widget type Version moves.
New
/lagw debug(the operator: "you should add a / command to turn it on/off as well").onandoffforce the library's chat diagnostics whatever any addon set; no argument toggles;resethands the decision back to the addons. It answers in chat with the current state, and the choice is saved inLibAceGUIWidgetsDBwhen the standalone addon is installed (an embedded-only copy keeps it for the session). One registration per session; the handler finds the current copy through LibStub, so a newer MINOR loaded later still answers.- An item row can take the consumer's own hover and click (TOGProfessionMaster contract
1d7a76ba).row.onEnter(button, link),row.onLeave(button)androw.onClick(button, link, mouseButton)each replace the built-in tooltip, hide orHandleModifiedItemClickwhen given, so a consumer's hold-to-compare hover and rebound chat-link click survive the move onto the library row. They are read from the row spec at event time, so a named dialog reused across opens follows the spec. None given: exactly the old behaviour. No callback runs for a row with no item.
Fixed
- A menu pick whose handler raises no longer leaves the menu open (Peer Review
b64aa131, measured by TOGProfessionMaster). The row'sOnClickranitem.onClick()unprotected, so an error left before the close: the menu stayed up andOnMenuClosednever fired. I wrote it that way. It now runs the handler underxpcall(keeping its stack viadebugstackwhere the client has it), closes (or, on akeepOpenrow, refreshes the check), and then hands the error to the library's onereportErrorpath so it still reaches the player's error display.
Changed
- Every chat line is behind the debug switch. The empty-render line ("RowList has N row(s) but
is showing none...") appeared in a player's chat between two guild messages; it, the duplicate
column key report, the accent clash report and the PersistWindow/DockWindow refusal now go through
one
lib:_Say, which prints only whenconfig.debugis on. I shipped them unconditional; they are developer diagnostics about a consumer's code that a player cannot act on. A debug function that raises counts as off, so a consumer's broken toggle cannot break a report. - Tests. Specs for the debug gate (off by default,
true, a live function, a raising function),/lagw debugand its SavedVariables restore, the item-row callbacks, and a raising menu pick in both row kinds. The specs that assert on chat now turn debug on first. The suite runs 1011 checks, 0 failing. - Docs brought level with the code. README now names RowList
opts.headerFont, the form dropdown row'smenuWidth, the stepper'sGetValue()and its parts,LAGW-ClickBinding'sGetValue()/SetDisabled()/OnEnter/OnLeave,dock:Relayout()anddock.sizes, and whatOpenMenu,EscapeLayerandShowDialoghand back. The CurseForge page now mentions tick-box columns, text that shortens to fit, the form prompt's hint line and placement, and expandable-list lines with their own click, tooltip and colour. All of these shipped earlier and were only undocumented.
[v0.2.8] -- a copy box with a copy glyph, a click binding you set by doing it, and a dock that lets go
Compatibility -- MINOR 37 -> 38
New: W:ShowCopyBox(text, opts), W.COPY_ICON, the LAGW-CopyField (Version 2) and
LAGW-ClickBinding (Version 1) widget types, W:ClickBinding, W:ParseClickBinding,
W:ClickBindingText, W:IsClickBinding, W:UndockWindow and the DockWindow handle's :Undock().
Feature-detect with if W.ShowCopyBox then, if W.IsClickBinding then, if W.UndockWindow then, or
AceGUI:GetWidgetVersion(type). Nothing existing changes for a consumer. ShowDialog clears the copy
box's read-only guard and glyph on every show, so a plain prompt after a copy box behaves as before. No
existing widget type Version moves. New shipped files: LibAceGUIWidgets-ClickBinding.lua (in the TOC
after the DatePicker) and Textures/copy.tga.
New
LAGW-ClickBinding(TOGProfessionMaster inboxe0f3923e, for ItemDB's "Where to get it" click, which collides with Gargul's Alt+click). The player sets a modifier + mouse button by doing it: click to arm ("Hold Alt, Ctrl or Shift and click here"), then click again holding the modifiers. AceGUI's own Keybinding cannot record a Left or Right click at all.- The value is one canonical string: modifiers in the order
ALT-,CTRL-,SHIFT-, then the button as OnClick names it --"ALT-LeftButton","CTRL-SHIFT-RightButton". ItemDB already stores this form. - A click with no modifier is refused on every button, with a short message, and the widget stays armed. Escape (out of combat) or Cancel disarms and changes nothing. Armed in combat, it takes no keyboard, because it could not pass the other keys on; Cancel is the way out.
SetValue(silent, normalised; unreadable means unbound),GetValue,SetLabel,SetDisabled,SetDefault(adds a Default button).OnValueChanged(binding)fires on the player's changes, reset included, and only when the value changed. Hiding while armed disarms. Released disarmed with no value, label or default.- Frame-free helpers:
W:ClickBinding(alt, ctrl, shift, button),W:ParseClickBinding(binding)(any prefix order and case),W:ClickBindingText(binding)("Alt + Left Click", the client'sNOT_BOUNDfor none, modifier names fromALT_KEY_TEXTetc. where the client has them) andW:IsClickBinding(binding, button): the modifiers held now must match exactly, and a nil button matches on modifiers alone.
- The value is one canonical string: modifiers in the order
W:ShowCopyBox(text, opts)(TOGTools inbox737274f2). Addons cannot write the clipboard, so every one hand-rolled a popup whose box held the text, selected, for Ctrl+C. This is that popup as one call, on the sharedShowDialogframe. It returnsdialog, token, soW:CloseDialog(token)closes it.opts.parentdefaults toUIParent,opts.promptto "Press Ctrl+C to copy, then Escape.",opts.okTexttoCLOSEor "Close".opts.onShowandopts.onHidepass through toShowDialog.- One button, and it closes. Enter and Escape close too. Nothing is called with the text on any close.
- The box is focused and the whole text selected when it opens.
- Read-only. A typed or deleted character is put back and the text selected again. A click in the box selects all again on mouse up, so it is never left partly selected.
- Long text scrolls inside the single-line box; the dialog keeps
ShowDialog's size. - A copy glyph in the box, drawn the way the search box draws its magnifying glass: 14px at the
box's left, the text inset 20 to clear it, white while focused or holding text and 0.6 grey
otherwise. The picture is a new shipped file,
Textures/copy.tga(64x64 32-bit white line art of two overlapping sheets, the same format ascalendar.tga), andW.COPY_ICONnames it. If the file cannot be set, the box draws no glyph and no inset. A plainShowDialogafter a copy box has neither. - The hooks go on the shared edit box once, not once per show.
LAGW-CopyField(TOGTools inbox513d5779), the copy box inline in a window instead of a popup: an AceGUI widget type withSetText,GetTextandSetLabel. The box followsShowCopyBox's rules: the copy glyph tinted like the search box's magnifier, read-only, the whole text selected on focus and click, long text scrolling in the box. Enter and Escape drop focus. AceGUI EditBox's geometry (26 tall, 44 with a label), scaled; works in List and Flow. Released with no text, label or focus. Feature-detect withAceGUI:GetWidgetVersion("LAGW-CopyField") >= 2. It ships at Version 2: Version 1 existed only in the working tree before this release and raised "FontString:SetText(): Font not set" on every Create in game, because the label got its font after its firstSetText(TOGTools peer review63629c29).DockWindow:h:Undock()andW:UndockWindow(frame)(TOGTools inbox2472500e). The window leaves its target, a docked one is pinned where it is on screen, andstatus.dockedis left alone, so a laterDockWindowto a new target docks it again. Feature-detectif W.UndockWindow then.
Fixed
- A window docked to an AceGUI window followed that window's frame into another addon. AceGUI
pools frames:
Releasehands the same raw frame to the nextAceGUI:Create("Frame"), andDockWindow's hooks and list stayed on it, so when another addon showed that frame, our window re-docked onto theirs. TOGTools hit it docking its Explain window to its main window, and worked round it by re-pointing the dock at a hidden frame before every release. Now a target that is an AceGUI widget's frame is let go of automatically on that widget's release (throughOnWidgetRelease), so no consumer has to do anything. Re-pointingDockWindowat a new target also now leaves the old target's list, instead of staying on it and being skipped.
Tests
- Whole suite: 992 passed, 0 failed; coverage 5961/5961 executable lines (100%) over every shipped
file, the new
LibAceGUIWidgets-ClickBinding.luaincluded. Tests/copyfield_spec.lua(10 examples): a fresh construction through its whole lifecycle under the harness's "no SetText without a font" rule, text, label heights, the read-only guard, select-all on focus and click, Enter / Escape, no glyph without the file, the glyph's place and tint, the glyph following the scale, and a clean return from the pool.Tests/dock_spec.luagains 5 examples:Undockpins and stops following, a later re-dock works, re-pointing leaves the old list,UndockWindow, and the automatic let-go on a pooled target's release, including the next owner showing that frame.Tests/dialog_spec.luagains 11 examples: the glyph drawn, tinted and gone from the next plain prompt; no glyph when the file cannot be set; the TGA's shape; the defaults and the token, theoptsoverrides, a user edit put back, re-selection on click and focus, close on OK / Enter / Escape withonHideonce each, a later plain prompt left writable with only one hook installed, a 500-character line with the dialog size unchanged, and nil / number text.Tests/clickbinding_spec.lua(19 examples): the string's canonical order, parsing and refusals, the display text, the exact-match rule, capture on every button, the refusal of an unmodified click, Escape and other keys, combat, Default, silentSetValue, disabled, the label, the UI scale, hide-while-armed, and a clean return from the pool.- The
Tests/wowapiharness pin moves to8e053f2. From9c6c874its library manifest loads the newLibAceGUIWidgets-ClickBinding.lua(WoWAPITesting inbox00eb7ea8); from9c80f46a FontString with no font refusesSetTextwith "Font not set", as the client does (inbox6ceaea3f). - That rule turned two
formdialog_specexamples red: the harness has noGameFontRedSmall, which the client does define, so the error line had no font offline. Harness8e053f2defines it (inbox798ae268), and "is drawn in the red font" now asserts the font exists, because with it missing both sides of its comparison were nil and it passed on nothing. - Not verified in a client: that a mouse-up re-select beats the click's own cursor placement, and that
RegisterForClicks("AnyUp")delivers Middle / Button4 / Button5 to OnClick on every flavour.
[v0.2.7] -- a checkbox, a note and a paragraph in the form prompt, and the cell a list shows an entry in
Compatibility -- MINOR 36 -> 37
New: RowList:CellFrameFor, the kind = "check" form row, row.note, frame:SetNote,
frame.notes, opts.body, frame:SetBody and frame.body. Feature-detect on if rl.CellFrameFor then or
LibStub.minors["LibAceGUIWidgets-1.0"] >= 37. Nothing existing changes: a form dialog that uses
neither a check row nor a note is built with the same points and the same height. No widget type
Version moves.
New
RowList:CellFrameFor(target, columnKey)(Dibs inbox55767d11). It returns the frame on screen that showstarget's cell in that column: the mouse overlay for a column withonCellEnter,onCellLeaveoronCellClick, else the cell itself.targetis an entry, a predicate, or an index in the drawn order. It is nil for an entry not in the view or scrolled off the page, an unknown column, a heading row, a cell the row is not showing, and a detached list. It is read-only: no scroll, no repaint. It answers for the current columns, so it followsSetColumns. Dibs' spec found the row by readingrl.rowsandrl:_getSortedData(); this replaces both.CreateFormDialog: a checkbox row and a note under a row (Dibs inbox8d5165f7), so Dibs can drop its two hand-built officer dialogs.kind = "check"puts aUICheckButtonTemplatebox where the edit box goes, with the row's label to its right, wrapping.fields[i]is the box:GetValue()answers a boolean,SetValue(bool)is silent,row.valuesets the start,row.onChanged(value, dialog)hears a click, androw.tipTitle/row.tipBodygo on the box. The row is the usual 26 tall and follows the scale.row.note = "text"draws a small greyGameFontDisableSmallline under that row's field, wrapping, and the dialog grows by its height.frame:SetNote(i, text or nil)adds, changes or removes it after build; a removed note leaves no gap.frame.notes[i]is the FontString.opts.bodyandframe:SetBody(text or nil)(Dibs inbox33a2bbce) put a paragraph under the title and hint, above the rows, wrapped to the dialog's width on the scaledGameFontHighlightSmall. Setting it again, afterShowincluded, replaces the text and the height follows; nil or "" hides it and it takes no space.frame.bodyis the FontString. This is for Dibs' Absences dialog, whose text is rebuilt on every open.- The rows are now re-hung by one routine shared by
SetHint,SetNoteandSetBody, so changing one keeps the space of the others.
Tests
Tests/rowlist_minor37_spec.lua(9 examples) checks each answer by what the frame does or shows: entering the returned overlay reaches the column'sonCellEnterwith the entry, and a text cell shows that entry's text.Tests/formdialog_minor37_spec.lua(13 examples) pins the unchanged layout of a dialog that uses none of the additions, then the check row, the note and the body, including a note on the last row, a body with no rows, and aSetHintafter each.
Dev tooling
wow-version-replication.ps1no longer deletes the replicas of a file the source still has (TOGProfessionMaster peer review0f76f963). An editor that saves by replacing the file raises aDeletedevent for a moment before the new file lands; the watcher removed the file from every other WoW install on that event. ADeletedfor a path that still exists is now treated asChangedand copied. Dev-only: the script is not in the package. Not reproduced against a real replacing save.
[v0.2.6] -- the suite orange by default, and nothing left on a released widget
Compatibility -- MINOR 35 -> 36
New methods: W:OnWidgetRelease, W:WidgetFrameScripts, W:RestoreWidgetFrameScripts,
W:AttachRawFrames, W:DetachRawFrames, W:AttachWidgetTooltip, RowList:CellWidth,
RowList:GetScrollOffset, RowList:SetScrollOffset, W:IsMenuOpenFor, W:IsInputFocusedIn,
W:OnMenuClosed, W:Flash, W:StopFlash, W:FlashScreen, W:AnchorPopup, W:ClampWindow,
W:SetWindowProfile, W:GetWindowProfile, W:SetWindowOpacity, W:GetWindowOpacity,
W:SetWindowScale, W:GetWindowScale, W:EscapeLayer, W:DropEscapeLayer, and
IsExpanded on an expandable list, W:CreateStepper and the LAGW-Stepper widget type at Version 1,
W:CreateSecureActionButton, W:NewDockLayout.
Feature-detect on
if W.AttachWidgetTooltip then or LibStub.minors["LibAceGUIWidgets-1.0"] >= 36. Nothing existing
changes signature; ScrollToEntry gains an optional second argument. New RowList options:
hoverHighlight, sortCycle, col.sortNone, searchText, searchTokens, onScroll, isHeader,
indentStep, indentKey, headerFont, reorderable + onReorder, fitContent +
onHeightChanged, col.show / col.text / col.tip on a button column and col.iconTexCoord, all
off unless given. RowList now reads three entry fields: _header, _expanded (on a header row) and
_indent; an entry that already carried a truthy _header for its own reasons would now draw as a
heading. New field: W.DEFAULT_ACCENT. One visible change for existing calls: the
default accent is now the suite orange ffFF8000, not gold ffFFD100. An addon that never called
Configure{ accent } now draws orange header underlines, selection tints, BrandTabGroup bands and
W:Brand text. An addon that sets its own accent sees no change, and one that sets ffFF8000 stays
silent, because the same colour is never an accentClash. No consumer has to change code.
ClearFrame, GroupFrame and TLabel move 30 -> 31 together, because ClearFrame:SetInfoTooltip
now takes { minWidth } and AceGUI keeps the FIRST registration at a tie, so an older copy loaded
earlier would otherwise keep a ClearFrame that ignores it. LAGW-DatePicker stays at 3 and
LAGW-SearchBox at 1.
New
- Pool-safe additions to AceGUI widgets, undone on Release (TOGProfessionMaster inbox
efe864ec,afb62bf8). AceGUI pools each widget and its frames for every addon in the session. ItsReleaseemptieswidget.eventsbut never touches a raw script, a frame flag, or a raw frame parented into the widget, so all three followed the pooled frame into the next addon's window. TOGProfessionMaster carried six hand-rolled undo variants for this.W:OnWidgetRelease(widget, key, fn)runsfn(widget)once when AceGUI releases the widget, then forgets it. It chains onto the widget'sOnReleasemethod, the wayPersistWindowdoes, so the consumer'sSetCallback("OnRelease")slot stays free. A second call with the same key replaces the hook,falseremoves it, hooks run newest first, and one that raises is reported throughgeterrorhandlerwithout stopping the rest.W:WidgetFrameScripts(widget, { OnMouseDown = fn, ... } [, target])sets raw scripts onwidget.frame, on a named sub-frame ("editbox") or on a given frame. On Release each event gets back the script from before the first call, not nil, so a constructor's own dispatcher survives and the next owner'sSetCallbackstill fires. A second call replaces the handler and still restores the original.falserestores one event now, andW:RestoreWidgetFrameScripts(widget)restores everything now.W:AttachRawFrames(widget, frameOrList)records raw frames parented into the widget. On Release each is hidden, loses its points and goes back toUIParent. The frames are not destroyed, so the consumer's own pool keeps them.W:DetachRawFrames(widget)does it now.W:AttachWidgetTooltip(widget, title, body [, opts])isAttachTooltipfor an AceGUI widget.AttachTooltip(widget.frame, ...)hooks withHookScript, which cannot be removed, and leaves its text and the disabled-hover flag on the pooled frame. This one answers onwidget.frame, including the EditBox and Dropdown label area (whose mouse it enables), and on the widget'seditbox,buttonandslider. Each frame's existing handler runs first, so the consumer'sSetCallback("OnEnter")still fires. It draws throughShowTooltipwithopts.owner,opts.reasonandopts.titleColor, leaves throughHideTooltip, and keeps a disabled button's hover. A second call updates the text. On Release the scripts, the mouse flag and the motion flag all go back to their prior values.
- Tooltip
opts.minWidth, andW:AnchorPopup(TOGProfessionMaster inboxc8d6892f).ShowTooltip,AttachTooltipandAttachWidgetTooltiptakeminWidth(scale-1.0 px), so several paragraphs of help are not drawn as a tall narrow column. GameTooltip is shared with the whole UI and nothing resetsSetMinimumWidthby itself, so the previous value is saved as both halvesGetMinimumWidthreturns (width, forced) and put back when the tooltip hides, through a hook on itsOnHide, and before the next showing.DressBottomRowtakes it per icon astipMinWidth, and a ClearFrame'sSetInfoTooltip(tooltip, { minWidth })takes it too.AnchorPopup(popup, source, { gap })places a floating panel under its source, or over it when there is no room below, slides it left to stay on screen and clamps it. Both frames are measured in UIParent units, so a scaled window is placed right. W:Flash,W:StopFlashandW:FlashScreen, a finite attention flash (TOGProfessionMaster inbox759b8287).Breatheloops until stopped;Flash(region, { times, seconds, low, onDone })dips the alphatimestimes (3 by default, 0.25s each way, down to 0) and stops by itself with the alpha back at 1. It is a BOUNCE group counting its ownOnLoopcalls that turn back toFORWARD, one per dip, which is right whether the client callsOnLoopat each end of a pass or once per cycle (the docs do not say which).OnLoopis a script the Classic Era client uses itself (LFGFrame.lua). A second call restarts it on the same group.FlashScreen({ color, times, seconds })flashes a full-screen edge glow on one shared frame atFULLSCREEN_DIALOGstrata that never takes the mouse, then hides it. The glow is four flat-colour bars rather than a client texture, so there is no art to go missing on a flavour. The colour defaults to the accent; the hex parsing is now one local shared withAccentRGB.W:IsMenuOpenFor,W:IsInputFocusedInandW:OnMenuClosed(TOGProfessionMaster inbox8f507ee7), for a consumer that defers a redraw while the player has one of its menus open or is typing. Both queries walk UP from the anchor or the focused box to the window asked about, never down its tree, so they are cheap on every event. Another addon's open menu answers false; submenus are the same stack and count. The focus query asks the client'sGetCurrentKeyBoardFocuswhen it exists. That function has no call site in this box's Classic Era or Classic source trees, so the library also tracks focus on every edit box it builds (menu search, dialog, search box, form field, date picker) and falls back to that.OnMenuClosed(owner, fn)is one-shot per owner and also fires when another anchor's menu takes over the stack.- RowList
opts.hoverHighlight(TOGProfessionMaster inbox0c477b45) lights the row under the pointer.trueuses the stock quest-log title highlight, ADD-blended; a string is a texture path;{ r, g, b, a }is a flat colour. It is drawn onBORDERone sublevel under the selection tint, so it covers the banding and never hides the selection. Only one frame holds the mouse at a time, so the row gets itsOnLeavewhen the pointer moves onto one of its own mouse-enabled children (not yet seen in the client). Every child that takes the mouse (cell overlays, button cells, checkboxes, expanders, action icons) lights the row on enter, and a leave only puts it out whenrow:IsMouseOver()says the pointer is off the row entirely. A scroll moves entries under a pointer that never moved and fires no enter or leave, so each populate re-decides the highlight fromIsMouseOver. A row showing no entry never lights. It is opt-in because the row has to take the mouse. - RowList three-state header sort (TOGProfessionMaster inbox
1013eeea). Withopts.sortCycle = "three", orcol.sortNone = trueon one column, the third click on the sorted column clears the sort, so a list whose unsorted order means something (a category tree) can get back to it. AsortDescDefaultcolumn cycles desc -> asc -> none. The clear writes the same state asSetSort(nil), andonSortChangednow reports a cleared sort as(nil, nil, rl). Lists stay two-state unless they ask. col.formatgets the cell's width and a measure (TOGProfessionMaster inboxc5476af2), for a column that fits as many names as the space allows and then "+N". When the text is being drawn the formatter is called asformat(value, entry, width, measure):widthis the cell's pixel width at the current scale, andmeasure(text)is that text's rendered width in the column's own font. A parent resize repaints, so the formatter re-fits. The filter labels, the search and theautoFitmeasure pass neither argument, so they see the full text. Newrl:CellWidth(key)answers any column's width, the auto column included once a row has been laid out (nil before that);ColumnWidthkeeps answering nil for the auto column so a consumer summing fixed widths is unchanged. Two-argument formatters are unaffected.- RowList search over hidden text, by words (TOGProfessionMaster inbox
85482ed8), so searching "stamina" finds every recipe that gives stamina.opts.searchText(entry)returns extra text the search reads, colour escapes stripped. With it, or withopts.searchTokens = true,SetSearchmatches byW:SearchMatch's rule: every word of the query somewhere in the shown cells and the hidden text, any order, any case. The text is built once per entry and kept untilSetData,RefreshorSetColumns, sosearchTextdoes not run per keystroke. A list with neither keeps the plain-substring search it had. The search still ANDs with the column filters. - RowList scroll position, public (TOGProfessionMaster inbox
bdf1159c), so a tab reopens where the player left it.rl:GetScrollOffset()andrl:SetScrollOffset(n)work in rows; the setter clamps like every other scroll and repaints once.opts.onScroll(rl, offset)is called from the repaint whenever the offset differs from the last one reported, whatever moved it: the wheel, the scrollbar, the setter,ScrollToEntry, aSetDataor search that went back to the top, a clamp. It is not coalesced across a frame, as the request asked, because that needs a timer standing in for the end of the frame.rl:ScrollToEntry(target, opts)takesifNeeded(no scroll when the entry is already on screen; from below, it comes in at the bottom) andcontext = n(rows left beyond it, capped at one less than the page, then clamped at the list's ends). SetSearchnow repaints without dropping the per-entry search text built for the previous query.- RowList group header rows and indent (TOGProfessionMaster inbox
57b66577). A row is a heading whenopts.isHeader(entry)answers truthy or, without it, whenentry._headeris: a string is the heading's text,trueuses the indent column's text. A heading row hides every cell, cell overlay, strike line and action icon, swaps its banding for an accent strip at 0.10, and draws one line of text across the row in the accent colour (opts.headerFont, defaultGameFontNormal).entry._expandedtrue or false adds the client's own craft-window minus or plus in front; nil adds nothing. A click on a heading reachesonRowClickwith its entry, which is how a consumer toggles it; no selection or scroll predicate treats headings specially. A header sort sorts each heading's rows UNDER it and never moves a heading: rows before the first heading are their own group, and groups are flat, so a tree that wants nested groups sorted usesexternalSort. Column filters never judge a heading and its cells add no filter values orautoFitwidth. With a filter or search active, a heading stays while any row of its group survives, or when the search matches the heading's own text.entry._indentis a level: level xopts.indentStep(scale-1.0 px, default 12) moves the indent column's text right while its right edge stays, so it still clips at the column. The indent column isopts.indentKey's, else the first column that shows text; a heading is indented too. A formatter on that column is handed the narrower width. A pooled frame is re-laid only when the entry it shows changes level. - RowList button cells that hide per row (TOGProfessionMaster inbox
337bb7d3). On abuttoncolumn,col.show(entry)false hides the button on that row: no mouse, a blank cell, and an empty label for the search and filter.col.text(entry)is the label per row, used as given (noformat, no class tint).col.tip(entry, button)returnstitle, body[, reason]and is read on hover, so text about how old the data is was true when the player looked; the reason is the red last line, as onCreateExplainedButton, andtipwins overcellTip. Every one is re-read on each populate, so a sibling addon that loads late needs onlyrl:Refresh()._readonlystill disables and does not hide. - RowList
col.iconTexCoordandopts.fitContent(TOGProfessionMaster inbox8680f2cc).iconTexCoord = { l, r, t, b }, ortruefor the usual 0.07-0.93 crop ofInterface\Iconsart, is applied on every populate. A column without it sets the full texture back, because the holder is pooled and may have served a cropping column.fitContent = trueor{ maxRows = n }sets the PARENT's height to the header bar plus one scaled row height per drawn row, up tomaxRows, after which the list scrolls. It re-fits on every refresh, so a scale change, a search or new data resizes it.onHeightChanged(rl, h)hears each new height. No rows gives the header's height (0 without one), and that is not reported as an empty-render fault. The parent has to be anchored by its top alone: a frame anchored top and bottom ignoresSetHeight. - RowList drag to reorder (TOGProfessionMaster inbox
973a1ab1). Withopts.reorderableandopts.onReorder(fromIndex, toIndex, entry, rl), dragging a row lifts a copy of it atTOOLTIPstrata that follows the pointer, dims the row itself to 0.4, and draws a 2px accent line (scaled) at the row boundary nearest the pointer. Dropping callsonReorderwith the entry's index indataand the index it should have after the move. The list moves nothing itself, so the consumer reorders its model and callsSetData. The client's own drag threshold keeps a click a click, and a right-click still reachesonRowClick. Held on the first or last drawn row, the list scrolls one row every 0.1s. Escape cancels out of combat: the lifted row takes the keyboard and lets every other key through.EnableKeyboardis protected andSetPropagateKeyboardInputrestricted (Classic EraSimpleFrameAPIDocumentation.lua), so in combat the keyboard is not taken, and a drop off the list is the way to cancel. No call is made for a drop off the list, a drop in the slot it came from, or a drop after the data changed under the drag. A drag does not start while a header sort, a column filter or a search is active, because the drawn order must be the model's order. AnexternalSortlist drawsdataas given, so it can be dragged.Detachcancels a drag in progress. W:ClampWindowandW:SetWindowProfile(TOGProfessionMaster inbox72e8dd4b).ClampWindowcaps a window's size to the screen and moves it fully onto it, in the window's own units, and writes the result intowidget.status. It changes only what is off the screen, so the floor is undercut only when the screen itself is smaller.SetWindowProfile(widget, key, { width, height, resizable, minWidth, minHeight })switches a persisted window between named sizes. A locked profile snaps to its size and turns resizing off:SetEnabled(false)on a ClearFrame's handle (grips hidden),EnableResize(false)on AceGUI'sFrame, andSetResizable(false)on both so a drag is refused. A resizable profile restores its own size fromsaved.profiles[key](else its defaults), raised to its own floor, and that floor follows the scale. Leaving a resizable profile saves the frame's size under its key, and so does releasing the window while that profile is active (TOGProfessionMaster inbox7481d3a5): a window dragged and then closed on a resizable tab used to reopen at the size saved at the last tab switch.ForgetWindowsaves it before the record goes and before the widget's ownOnReleaseruns, through the same helper as the switch-away save. A locked size only ever lands in the table's ordinarywidth/height, so it cannot reach a resizable profile's save. Position is shared, and every switch ends inClampWindow.ForgetWindow, and so Release, turns resizing back to how it was before the first profile.W:SetWindowOpacity/W:GetWindowOpacity(TOGProfessionMaster inboxae090bd5) fade a window's background fills: its own backdrop, and the pane backdrop of every container inside it (border,treeframe, and the frame a container'scontentsits in, which is the only way to reach InlineGroup's). Text, icons, buttons, borders andSetAlphaare untouched. The fade is always from the stock colour, so it is idempotent and 1 is stock. Each widget remembers the frames it owns and restores them on its own Release, so a pane released by a tab switch goes back to the pool clean while the window stays open. A pane drawn after the call is not reached until the consumer calls it again.W:SetWindowScale/W:GetWindowScale(TOGProfessionMaster inboxe5e1586a) set one window'sframe:SetScale, clamped to 0.5-1.5 and separate from the session-wideW:SetScale(0.8-2.0), which still multiplies inside it. The top-left corner stays on the same screen point (anchor offsets are in the window's own units, so they are multiplied by old/new), then the window is clamped. It is saved aswindowScalein a persisted window's table, andPersistWindowre-applies it before the position, which was written in those units. Release puts the scale back to 1.W:EscapeLayer(window, childFrames)/W:DropEscapeLayer(window)(TOGProfessionMaster inboxd8f15682): Escape closes the most recently shown child first and the window on the press after the last child. Read in the Classic Era source (UIParentPanelManager.lua:1041),CloseSpecialWindowswalksUISpecialFrameswithpairsand hides every entry thatIsShown(), so a window and its popup on that list close together on one press. The classic_anniversary and classic trees have the same function at the same line. So the window is represented by one proxy frame, shown only while the window is, and the window and its children come off the list while the layer lasts. The proxy'sOnHidecloses the newest shown child and shows itself again, or hides the window when none is left. A hide the proxy did not get from the walk (its window or UIParent hidden) leaves its ownIsShownflag true and is ignored. Closing the window hides every shown child, which runs theirOnHide, and hides the proxy so a closed window cannot swallow the next press. It is plain Lua hiding plain frames, so it works in combat. The hooks go on the window frame once and read the layer at event time. An AceGUI window's layer ends on Release, and the names come back onto the list. Known cost:CloseWindowsalso callsCloseSpecialWindows(line 1087), so loss of control and other panel opens also close only the newest child of a layered window.CreateExpandableListat any depth (TOGProfessionMaster inbox7ab1cb56), for a profession -> specialisation -> member tree. A child may carrychildrenof its own, and the layout is now one recursive walk. A node with children is a group at any depth (header style, a toggle, indentedindentper level). A node without is a leaf: header style at depth 0 and child style below, which is exactly the old two-level picture. Expand state is keyed by the path of keys, so two "Main" specs under two professions keep their own state. A top-level group's path is its own key, as its state key was before.defaultExpandedandSetAllExpandedreach every depth, andlist:IsExpanded(key, ...)reads a path. Any node takescolor,onClick(node, mouseButton, rowFrame)(a group toggles and then calls it) andtooltip, a{ title, body }pair or a function read on hover. Every script is set on each layout from the node drawn there, and a row with no click and no tooltip takes no mouse, as before. A pooled group row puts its font's own colour back when the next node has none. A toggle re-lays the pooled rows; no frame is rebuilt.W:CreateStepperand theLAGW-Stepperwidget type (TOGProfessionMaster inboxea8db903):[-] [qty] [+] [MAX]. Typing keeps digits only as they are typed, and commits on Enter or focus loss, clamped tominandmax(). An emptied box and Escape revert.-and+step bystepand areCreateExplainedButtons, so at a bound they grey out and the tooltip's red last line says which bound. MAX setsmax(), read on every click; a max of 0 greys it.SetValueis silent, and a player's change callsonValueChanged(value, stepper)once, only when the value moved. Sizes are scale-1.0; the layout computes its widths from the scale, so it does not depend on which scale listener runs first.LAGW-Stepper(Version 1) wraps it and firesOnValueChanged(value). It builds MAX once and shows it only afterSetMaxButton(true), and is reset on acquire. Not delivered: holding a button to repeat, which needs a per-frame tick while held.CreateFormDialog: dropdown rows, item rows,SetHintandanchorTo(TOGProfessionMaster inbox6cf3b4e4), all opt-in; existing rows build exactly as before.- A
kind = "dropdown"row puts aCreateDropdownBoxwhere the edit box goes.items(query)returns{ text, value }pairs, built fresh on each open with the current one ticked. Choosing one callsonChanged(value, dialog)only when it changed.fields[i]hasGetValue()and a silentSetValue(value[, text]). - A
kind = "item"row shows the item's icon and link from the label to the right edge. Hover draws the item's own tooltip;SetHyperlinkis given the bareitem:part. A click goes to the client'sHandleModifiedItemClick.fields[i]:SetItem(link[, icon])changes it. The icon isrow.icon, elseC_Item.GetItemIconByID(link), which the Classic Era, Anniversary and Classic docs all declare. frame:SetHint(text or nil)adds, changes or removes the hint after build. The first row (or the error line) re-hangs under whichever of title and hint is showing, and the height is recomputed unless the caller gave one. OK/Cancel are anchored to the bottom, so they stay on the frame.opts.anchorTo(withpoint,relPoint,x,y; default TOPLEFT to the owner's TOPRIGHT, 4px right) andframe:SetAnchor(owner, ...)place the dialog by its owner on every show, clamped by the floating frame. A laterSetAnchorre-points a named dialog to whichever row opened it. An owner that is not visible leaves the dialog where it was.
- A
W:CreateSecureActionButton(TOGProfessionMaster inbox3d04549b), a secure button with combat guards. It registers forAnyUpandAnyDown: read in Classic EraSecureTemplates.lua:789-823,SecureActionButton_OnClickacts on exactly one of the two, chosen byuseOnKeyDownor theActionButtonUseKeyDownCVar, so it fires once under either setting.SetAction(attrs)and the button's ownSetPoint,ClearAllPoints,SetAllPoints,SetSize,SetWidth,SetHeight,Show,Hide,SetShown,SetEnabled,Enable,DisableandSetParent(wrapped on the instance) run now, or are queued in combat. One session-wide queue runs once, in order, onPLAYER_REGEN_ENABLED. A scale change in combat queues the resize as well. The factory returns nil in combat. The tooltip isCreateExplainedButton's and works while disabled;preClick/postClickare the consumer's. Not safe to pool:SetParentisIsProtectedFunctionin all three Classic trees, and a frame that parents a secure button is itself protected (Warcraft Wiki; web, not client source). So it belongs in a frame the consumer owns, never in a pooled AceGUI frame.W:NewDockLayout(TOGProfessionMaster inbox2f9efc9e) splits a container intotop,right,bottomandcenter(aliasleft). The panes are four raw frames the library owns, anchored into the container'scontent, so a live resize moves them through their anchors. No method of the pooled widget is replaced and none of its scripts is hooked. The sizes are re-decided by one extra frame of the library's over the content (itsOnSizeChanged) and by a scale listener.top,rightand a fixedbottomare scale-1.0; an"auto"bottom is the pixel height given toSetBottomHeight(px). The bottom spans the full width. The centre never collapses: the top stops one pixel short of the height, the bottom takes only what is left under it, and the right leaves the centreminCenter.SetPaneShown(name, bool)gives a pane's space to the centre. An AceGUI container's Release releases the dock (every frame hidden and handed to UIParent, the scale listener dropped); a raw frame usesdock:Release().
Changed
- The default accent is the suite orange (TOGProfessionMaster inbox
8f931ba9). The operator's words: "have the library change it's color to our orange, not sure why it's gold." Gold is Blizzard'sNORMAL_FONT_COLOR, not the suite's brand, and the one-setter rule stopped a consumer from fixing it locally without becoming a second setter.DEFAULT_ACCENTis now one literal, read by the config table,GetAccentandAccentRGB.AccentRGB's per-channel fallback for a malformed accent is now1, 0.5, 0to match. Colours the library draws as the client stay onNORMAL_FONT_COLOR: a tooltip header, the cooldown caption, the DatePicker label. - An upgrade in a live session moves the old default too.
lib.configsurvives an upgrade from an older copy, so a session where an addon with MINOR 35 or older loaded first would have stayed gold. On load, a config still holding the untouched gold default moves to orange. A consumer that set gold throughSetAccentorConfigure(recorded since MINOR 33) keeps it. PersistWindowclamps the restored window to the screen. A table saved on a larger screen or at a smaller UI scale used to restore a window partly or wholly off the screen, or bigger than it. It is now capped and moved on, and the table is updated to match. A window that fits is untouched.- README: "Auditing a consumer", a table from what a hand-rolled copy looks like in an addon's code
to the library call that replaces it, for every window, list, control and look-and-feel feature
with the MINOR that brought it. A feature was usually extracted FROM a consumer, and its original
often survives there (ClassicCalendar's World Buff docking still carries its own copy of what
became
DockWindow). A per-version list does not show an auditor any of that. - RowList counts its visible rows with a small allowance for rounding. At a scale where the row
height is not a whole number, a parent exactly n rows tall could divide to just under n and show
one row fewer.
fitContentsets exactly such heights, so the count now adds 1e-6 before flooring. - Peer Review's verdicts on this batch's self-audit (audit thread
231f14e7):- The stepper's
Refresh()andSetBoundsno longer rewrite the box while it has focus. A consumer refreshing onBAG_UPDATEused to erase a half-typed number. The typed number is clamped against the new bounds when it commits. A click,SetValue, Escape and show still rewrite it. - The stepper snaps to the nearest multiple of
stepcounted frommin, rather than to a whole number, so a fractional step (0.5) works and a decimal point can be typed for one. An integer step frommin0 lands exactly where it did. A step of zero or less falls back to 1 instead of dividing by zero.SetStepre-snaps the current value onto the new grid, asSetBoundsre-clamps it, so a click from a value set under the old step moves by exactly the new one. SetWindowScalere-applies the window's own anchors with the offsets times old/new, instead of re-placing it TOP/LEFT onUIParent. A docked or consumer-anchored window stays attached, and its anchored point stays on the same screen spot.- README: a dock's content goes in the panes only, since AceGUI children of the same container are laid out over them.
- The secure button's guard shadows widget methods with Lua fields. The Classic Era secure click path and the button's templates were read for a secure caller of any of them, and none exists; the reasoning, with line numbers, is in the code. Not verified in a client.
- The stepper's
Tests
Tests/poolsafe_spec.lua(20 examples) drives the four helpers through real AceGUI-3.0 and its stock Button, EditBox and SimpleGroup. Every "the next owner finds nothing" case re-acquires the released widget and asserts the pool handed back the same table, so none of them can pass on a fresh widget.Tests/tipwidth_spec.lua(9 examples) pins the tooltip floor through the harness's modelledSetMinimumWidth/GetMinimumWidth, forced flag included, and the popup placement at the screen's edges.Tests/flash_spec.lua(5 examples) drives each pass by calling the group's ownOnLoop, since the frame model builds animation groups but does not run them.Tests/interaction_spec.lua(8 examples) opens real menus on anchors nested inside a window and drives the focus query both with and without the client'sGetCurrentKeyBoardFocus.Tests/rowlisthover_spec.lua(10 examples) fires enter and leave in the client's order (old frame's leave, then the new frame's enter) with each row'sIsMouseOveranswering for the pointer. Each child is also entered from a dark row, so the child's own enter is proven to light it.Tests/rowlist_minor36_spec.luaholds the other RowList requests of this release, starting with the sort cycle (7 examples), the formatter width (4), the word search (6) and the scroll position (6).Tests/rowlist_groups_spec.lua(47 examples) covers headings and indent, the button cells, the icon crop,fitContentand the drag. It drives the drag by placing the harness cursor and calling the row'sOnDragStart/OnDragStopand the lifted row'sOnUpdate. The harness models neither keyboard getter, so the Escape tests recordEnableKeyboardandSetPropagateKeyboardInputcalls. None of this has been tried in the client.Tests/windowprofile_spec.lua(25 examples) drives the clamp, the profiles, the opacity and the window scale through real AceGUI-3.0ClearFrame,Frame,TabGroupandInlineGroup, and re-acquires the released widget where the point is that the next owner finds nothing.Tests/escapelayer_spec.lua(9 examples) presses Escape through a copy of the client's ownCloseSpecialWindowsloop, so the layer is tested against the walk it exists to work around.Tests/expandable_depth_spec.lua(7 examples) opens a three-level tree through the rows' own scripts, and the two existing expandable-list examples still pass unchanged.Tests/stepper_spec.lua(16 examples) drives the stepper through its own scripts and the widget type through real AceGUI-3.0, including a re-acquire from the pool.Tests/formdialog_rows_spec.lua(9 examples) drives the dropdown through a captured menu, the item row through stubbedC_ItemandHandleModifiedItemClick, and the hint and anchor through the frame model's anchor points.Tests/securebutton_spec.lua(9 examples) swapsInCombatLockdownto enter and leave combat and fires the queue's ownPLAYER_REGEN_ENABLEDhandler.Tests/docklayout_spec.lua(7 examples) reads the panes' anchors, and checks that the dock replaces no method on a real AceGUISimpleGroupand lets go on its Release.- The v0.1.11, v0.1.12 and v0.2.0 sections moved whole to
CHANGELOG_ARCHIVE.mdto keep this file under 120,000 characters. Tests/widgets_spec.luapins the new default,AccentRGB's fallback, and the upgrade path: a simulated MINOR 35 table is reloaded over and moves to orange unless gold was set explicitly.- The three specs that reset the accent after a test now reset it to
W.DEFAULT_ACCENTrather than to a gold literal. The RowList selection-tint test paints with green, so it still proves the tint is read at paint time now that orange is the default. - Two weak assertions rewritten (Writ inbox
943be786). Each wasis_nilon a literal key, which still passes if the key is misspelt or renamed.bootstrap_spec.luanow checksW:TriggerVersionCheck()returns false instead of readingW._vc.widgets_spec.luachecks that every config keyConfiguredid not name is unchanged and that none was added, instead ofW.config.classColors. wow-version-replication.ps1(dev only, not packaged) also copies into_classic_beta_, where the WoW Forever client installs.
Older releases (v0.2.5 and earlier) are in CHANGELOG_ARCHIVE.md, moved there
whole and unedited to keep this file under the size GitHub accepts as a release body.
This mod has no additional files

