promotional bannermobile promotional banner

LibAceGUIWidgets

LibAceGUIWidgets is a shared AceGUI-3.0 widget and styling library. It registers reusable, drop-in AceGUI widget types and helpers so addons share one consistent look instead of each rebuilding the same frames.
Back to Files

LibAceGUIWidgets-v0.10.0

File nameLibAceGUIWidgets-LibAceGUIWidgets-v0.10.0.zip
Uploader
PmptastyPmptasty
Uploaded
Oct 31, 2026
Downloads
7.4K
Size
744.6 KB
Flavors
RetailClassicMoP ClassicTitan Reforged ClassicClassic TBCForever
File ID
9384864
Type
R
Release
Supported game versions
  • 12.1.0
  • 12.0.7
  • 12.0.5
  • 11.2.7
  • 5.5.4
  • 5.5.3
  • 4.4.2
  • 3.80.2
  • 3.80.0
  • 3.4.5
  • 2.5.6
  • 2.5.5
  • 1.60.1
  • 1.15.9
  • 1.15.8

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.lua and broke three times: a UIParent holder (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; show state driver, and a re-sync on PLAYER_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 calls opts.onCreate(button) once it has built it after combat (there is no button to return before then), and opts.name keys the button -- a second call by name re-attaches the same one, since a secure frame cannot be destroyed. Specs in Tests/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 / y offsets 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 run onCreate once, with the later call's panel and opts; and a build queued in combat is dropped, with no onCreate, if the panel's widget is released before combat ends (Release leaves frame.obj in 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 shares TOOLTIP with 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 GetNumLetters nor strlenutf8, and lib._OwnFloor with 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) returns span, collapse: span[ci] = stopIdx for each spanning text column (0 = runs to the last column's edge), collapse[ci] = true for each collapsed column -- the same answer the private _rowPlan gave, which a consumer's spec was reading and which had already been renamed once (_spanPlan). Differs from _rowPlan in 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. _rowPlan stays private. Specs in Tests/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 private visibleRowCount they were reading); rl:ScrollPage(direction) moves one page, direction > 0 down and < 0 up, clamped like SetScrollOffset and returning the offset that stood; and opts.pageWithShift = true makes 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 of direction counts; 0 or a non-number moves nothing. Opt-in because Shift + wheel may already mean something on the consumer's frame; a client without IsShiftKeyDown scrolls by rows. The README's MINOR table also gains its missing 44-47 rows. Specs in Tests/rowlist_page_spec.lua. Not verified in the client.

Tests and docs

  • The v0.2.5 section moved whole to CHANGELOG_ARCHIVE.md to 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 = true or span = 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 whose show answers 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.format is 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 in Tests/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 carry collapse = true. On a row where it draws nothing -- the same test span uses -- 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:CellWidth still reports the uncollapsed width, and col.format on the auto column is handed the row's own, wider width. Ignored on a column before or at the auto column. Specs in Tests/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 takes icon, texCoord, tipTitle / tipBody (read on hover), onClick, checked and shown predicates and a key; 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 like DressBottomRow: cached on the frame, idempotent, and W:UndressSideTabs hides them all. Drawn, not built on CommunitiesFrameTabTemplate as the request suggested: its mixin's OnClick calls GetParent():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 inherits RightSideTabTemplate instead, and this draws the Classic look there too. The core now exposes lib._tipPart for satellites. Specs in Tests/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. tipPart called a tipTitle / tipBody function bare, though safeCall is the library's one dispatch for consumer callbacks, so the error escaped OnEnter, the rest of the handler (the other half of the tooltip, and Show) 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 through safeCall: the error is reported and that half of the tooltip is left out while the other half draws. Covers DressBottomRow and DressSideTabs.

  • DressBottomRow and AttachHeaderButton ran onClick bare for the same reason; both now go through safeCall.

  • A non-string spec key could overwrite a slot in the returned table. key = 2 replaced the second icon or tab, and in DressSideTabs key = "_lagwFrame" broke :Refresh(). Both now take only string keys, and DressSideTabs also 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 in Tests/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's get / set, a form dialog item row's onEnter / onLeave / onClick, a form row button's onClick and the ClearFrame settings button's handler now go through safeCall. A setter that errors is reported and the box still shows get(), 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 in Tests/checkbox_spec.lua, Tests/formdialog_rows_spec.lua and Tests/formdialog_spec.lua, seen failing before the fix; the settings button's spec in Tests/older_paths_spec.lua was 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's show / text, the icon function, 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 through safeCall. Each error is reported and the rest of the draw or click still runs: a failed sortValue sorts by the raw value, a failed format shows the raw value, a failed onCellClick falls through to onClick / onRowClick, a failed onCellLeave still hides the tooltip, and a tiebreak that raises is reported once and the ties keep their SetData order, so table.sort never sees an inconsistent comparison. Specs in Tests/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 whose onHide raised on OK never ran its onAccept (OK hides first), and a BindCooldownButton whose onClick raised never started its cooldown, so the rate limit was lost. Also covered: the menu's checked functions (a raise left the menu half-drawn), search itemsFor, submenu children, a dropdown box's items, the Stepper's max and onValueChanged, a flash's onDone, ShowDialog's onAccept and onShow (a raise lost the returned token), CreateSearchBox's onChanged, a form check row's onChanged, a form dropdown row's items, a form dialog's onAccept, the cooldown's seconds and format (binding a button mid-cooldown raised out of BindCooldownButton), an expandable list's onClick and tip (both of its return values kept), the ClearFrame info tooltip, the character counter's onUpdate (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's today function, which now falls back to local time. Specs in Tests/callback_safecall_spec.lua, seen failing first (25 of 28 red before the fix; the other three pin multi-value returns and a dropdown onChanged the menu row's own xpcall already caught).

  • A tooltip-placement or floor function that errored escaped unreported (Peer Review, threads 64514880 and c48ddae3). The session-wide tooltipOwner, a ShowTooltip call's opts.owner, RowList's own tooltipOwner (link and icon hovers), TLabel's tooltip, and a PersistWindow / SetWindowProfile minWidth / minHeight function were all called bare. Each now goes through safeCall: 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 private lib:_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 before PersistWindow (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 in Tests/widgets_spec.lua, Tests/scale_spec.lua, Tests/rowlist_minor34_spec.lua, Tests/older_paths_spec.lua and Tests/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 in Tests/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 = 600 to 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 before PersistWindow. Spec in Tests/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). PersistWindow and SetWindowProfile registered 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 in Tests/scale_spec.lua (with and without a resize handle) and Tests/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.lua on 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). New already 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 in Tests/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 MakeResizable on 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 in Tests/scale_spec.lua, seen failing first.

  • A second PersistWindow on 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 in Tests/scale_spec.lua, seen failing first.

  • A huge rowCount or a stray high key in actions could hang a RowList (Peer Review G2, thread c48ddae3). rowCount = 1e9 built a row per step, and an actions table with a key like 1e9 was walked one index at a time up to it. rowCount is 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 in Tests/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. rowCount is still the starting pool, and the pool still never shrinks. Specs in Tests/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.lua now 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 parentHeight is 0 for such a height, so code reading it never meets inf or NaN, and the raw value is in a new parentHeightRaw field (eeecd1f8, F2), now listed in the README. Four examples in Tests/rowlist_poolcap_spec.lua (infinite, NaN, negative infinite, -3e9), with no onEmptyRender hook, were seen failing first; they also pin parentHeightRaw to 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 to SetPoint (Peer Review 957daeed); all five failed against the old code (top 24, 24, -inf, -3000000000, -50).

  • A tiebreak that gave contradictory answers made SetData raise and drew no rows. One that answers "before" both ways round can make table.sort raise, and that error escaped. It is now reported once and the list sorts without the tiebreak, so tied rows keep their SetData order. Example in Tests/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 (and lib._safeCall, which satellites use) now returns ok, firstReturn rather 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_Refresh and the SideTabs predicates use it. Every existing caller ignores the return or already read ok, so none changes behaviour. A separate safeGet local 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 local HEADER table and the thirteen FORM_* constants one local FORM table; nothing outside the file sees either. New Tests/localheadroom_spec.lua guards 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.lua no 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 tooltipOwner now 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 through AnchorTooltip.
  • 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:SetScale never reached it: Tests/windowprofile_spec.lua saw 450 wide instead of 600 when it ran after Tests/scale_spec.lua. windowprofile_spec and 22 more files now call ace.fresh("AceGUI-3.0") before reloading; Tests/forgetwindow_spec.lua needed two in-file fixes afterwards.
  • Tests/rowlist_spec.lua runs in about 3 seconds instead of about 40 (Peer Review G2). Two examples compared frames with assert.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 use assert.is_true(a ~= b, ...). The same change in Tests/dialog_spec.lua and Tests/dock_spec.lua (seven asserts) took those two files from about 4.9 seconds to about 1.4, and Tests/scale_spec.lua has it too. The harness side is reported to WoWAPITesting.
  • The Tests/wowapi harness pin moves to f1797a7, where a passing negated assertion no longer builds the positive one's message and repr() is bounded. Nothing needed adopting; the comment in Tests/scale_spec.lua that described the old cost is gone, and the assert.is_true(a ~= b, ...) lines stay. The whole suite passed in normal and reverse order on the new pin.
  • Tests/scale_spec.lua puts 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 bare debug.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's preClick and postClick ran twice and were not told which half acted. UGR's macro tool re-armed its next batch in the down's postClick, and the up then ran that batch. opts.clicks = "up" registers "AnyUp" alone and sets the useOnKeyDown attribute 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 in Tests/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). _recomputeVisibleRows floored 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 in Tests/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:New now takes such a height, or a non-number, as the default. Spec in Tests/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) lays W:CreateExplainedButton buttons left to right with a right-justified status line on the same row. Each spec is { text, tip, onClick, width, reason, key }; a reason (string, or a function asked at build, on every hover and on strip:Refresh()) greys the button and is the red last tooltip line. Returns the strip frame itself, carrying .frame, .buttons, .status and SetStatus / 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 follows W: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 in Tests/buttonstrip_spec.lua. The harness loads the new file since WoWAPITesting pin 23d0039 (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 .editbox or .editBox; it follows SetMaxLetters (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 own GetNumLetters() against GetMaxLetters(), 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 in Tests/charcounter_spec.lua. Not verified in the client.
  • LAGW-Chip can be an AceConfig toggle's dialogControl (UGR inbox 20d63ff4, so an options page draws its switches as toggle tokens). Version 2 adds SetLabel (= SetText), SetTriState (accepted; a nil value draws off), SetDescription (shown as the chip's hover tooltip through ShowTooltip, cleared on acquire and release), SetImage / SetImageSize (ignored). Inside AceConfigDialog a chip with no width sizes to its label: AceConfigDialog's own 170px default is read as "no width" once SetTriState has marked the chip as a toggle, and every other width is honoured (width = 1 and "normal" reach the chip as the same 170, so they draw label-sized). An explicit width now survives SetLabel / SetText; before, every relabel shrank the chip back to its label, which a language switch would have hit (Peer Review on audit 509928c3). Specs in Tests/chip_aceconfig_spec.lua drive 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 row i's label (the text right of the box on a check row); nil or "" clears it; an index with no row does nothing, as SetNote does. f.labels[i] is the FontString. Spec in Tests/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 in Tests/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 painter BindCooldownButton registered 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 in Tests/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 UISpecialFrames entry; 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. ShowDialog now 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 on UISpecialFrames at the next ShowDialog that finds no layer, not when the show ends: an Escape-closed prompt ends inside the client's pairs walk 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. New W:RemoveEscapeChild(window, frame) takes one child out of a layer and re-lists its name; it, EscapeLayer and DropEscapeLayer are 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 in Tests/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 frame RemoveEscapeChild is 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 on UISpecialFrames as before (Peer Review on audit 4e6d3a5b). Specs in Tests/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:BindCooldownButton registered 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 in Tests/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, xpcall with a debugstack handler. One file-scope safeCall now does the second for every dispatch site: OnMenuClosed callbacks, scale listeners, the pool-safe release functions, cooldown listeners and every callback the Resize satellite fires (through lib._safeCall). Menu clicks share its withStack handler. Spec in Tests/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:AttachWidgetTooltip on an AceGUI MultiLineEditBox said nothing over its text area. AceGUI names that box editBox, and only editbox was looked for. Spec in Tests/poolsafe_spec.lua.

Tests

  • Tests/escapelayer_spec.lua drives Escape through the harness's frames.pressEscape instead of its own copy of the walk (Peer Review inbox 721966e5). Harness pin moved to 05cb0c8.

[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 plus offset, width wide, justified justify (default RIGHT). 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 follow W: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 in Tests/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's BasicFrameTemplateWithInset (title bar, close X, inset; Blizzard_UIPanelTemplates/Classic/UIPanelTemplates.xml:676, :714, :751, Classic Era tree) instead of the UI-DialogBox backdrop, 22px taller with the body 22px lower to clear the bar. The window look is a second shared frame, LibAceGUIWidgets_DialogWindow on W._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 (its onHide before the new onShow), CloseDialog(token) looks in both, and both are in UISpecialFrames. The X is wired to the same close as Cancel rather than the template's UIPanelCloseButton script, so onHide runs once. ShowCopyBox passes style and title through. W.DIALOG_STYLES = { plain = true, window = true } is the feature check. Specs in Tests/dialog_spec.lua: the style is advertised; the window frame carries the title, X and inset and the plain one does not; the X runs onHide once and never accepts; a replaced prompt's onHide runs across styles and CloseDialog finds either; the window frame is one Escape-list entry; ShowCopyBox carries 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). CreateFloatingFrame puts every menu level at strata TOOLTIP, level 100 + 10 per depth. GameTooltip is also TOOLTIP strata, declared with no frame level (Blizzard_GameTooltip/Classic/GameTooltip.xml:13, Classic Era tree), and showTooltip never 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 same OnHide hook that already restores the minimum width. The lift is in W:AnchorTooltip itself (so form-dialog item rows, RowList icons and hyperlinks, and the ClearFrame info button get it), plus ShowTooltip's opts.owner branch, the TLabel tooltip, and RowList's own tooltipOwner branch. Specs in Tests/menu_spec.lua: a root row's and a submenu row's tooltip sit above their menu and the level comes back on hide; AnchorTooltip alone raises over a floating panel; a frame outside TOOLTIP strata 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). CreateExpandableList set the content's width from a one-time scroll:GetWidth() read, floored at 40, and re-read it only from the outer frame's OnSizeChanged. 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 the CreateScrollFrame note 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 hooked list.box.scroll's OnSizeChanged to call list._relayout() can drop that hook. Spec in Tests/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 made MakeResizable merge callbacks). Before, the constructor passed onResizeStop only. AceGUI's RegisterAsContainer already hooks the content frame's OnSizeChanged (ContentResize), which calls DoLayout and records the size in content.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 new onResize re-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 called DoLayout unconditionally, which doubled the work; the README (MakeResizable) already warned against that, and the guard came out of reading it. The callback reads f.obj at 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.LiveRelayout is a second DoLayout per draw and can go. Specs in Tests/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:DockWindow heard a drop only through the frame's OnDragStop, but a ClearFrame is moved by its title bar, which calls StopMovingOrSizing from its own OnMouseUp, so OnDragStop never fired and h: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. DockWindow now also hooks the title bar's OnMouseUp on any widget whose title is 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's AceGUI:Create got a frame whose OnShow docked it to our target, and that PersistWindow refused as ours. Found by the new spec above, whose released ClearFrame made a later spec file's PersistWindow fail. 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 in Tests/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 contract c98a1eab; their operator: "ask widgets to add a toggle chip, i think it could be good"). SetText, SetValue / GetValue, SetDisabled, and OnValueChanged(value) on a click, as AceGUI's CheckBox does; SetValue is 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, so W:AttachWidgetTooltip answers on it, and it drops into W:StripAdd like any control.
  • W:AddChips(container, order, labels, values, onChanged) builds one chip per key in order, lit from values[key]. A click writes values[key] and then calls onChanged(key, value, chip). Returns the chips keyed by key.
  • LAGW-TabStrip, tabs added at runtime that scroll with < and > (Dibs contract cb965300; 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, and OnTabSelected(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 AceGUI Frame and on a ClearFrame, which also has it as win: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 inbox e62d2977): 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 = nil hides 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:DetachHeaderButton does 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.lua and Tests/headerbutton_spec.lua (placement against a real AceGUI Frame and ClearFrame, the scale, and what a released window keeps); Tests/strip_spec.lua covers a captionless row, and Tests/dock_spec.lua covers two windows sharing one saved table. The suite runs 1049 checks, 0 failing, and every shipped .lua file 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 (otherOwner calls _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, onHide unrun. It said /lagw debug is 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"). on and off force the library's chat diagnostics whatever any addon set; no argument toggles; reset hands the decision back to the addons. It answers in chat with the current state, and the choice is saved in LibAceGUIWidgetsDB when 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) and row.onClick(button, link, mouseButton) each replace the built-in tooltip, hide or HandleModifiedItemClick when 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's OnClick ran item.onClick() unprotected, so an error left before the close: the menu stayed up and OnMenuClosed never fired. I wrote it that way. It now runs the handler under xpcall (keeping its stack via debugstack where the client has it), closes (or, on a keepOpen row, refreshes the check), and then hands the error to the library's one reportError path 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 when config.debug is 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 debug and 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's menuWidth, the stepper's GetValue() and its parts, LAGW-ClickBinding's GetValue() / SetDisabled() / OnEnter / OnLeave, dock:Relayout() and dock.sizes, and what OpenMenu, EscapeLayer and ShowDialog hand 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 inbox e0f3923e, 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's NOT_BOUND for none, modifier names from ALT_KEY_TEXT etc. where the client has them) and W:IsClickBinding(binding, button): the modifiers held now must match exactly, and a nil button matches on modifiers alone.
  • W:ShowCopyBox(text, opts) (TOGTools inbox 737274f2). 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 shared ShowDialog frame. It returns dialog, token, so W:CloseDialog(token) closes it.

    • opts.parent defaults to UIParent, opts.prompt to "Press Ctrl+C to copy, then Escape.", opts.okText to CLOSE or "Close". opts.onShow and opts.onHide pass through to ShowDialog.
    • 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 as calendar.tga), and W.COPY_ICON names it. If the file cannot be set, the box draws no glyph and no inset. A plain ShowDialog after a copy box has neither.
    • The hooks go on the shared edit box once, not once per show.
  • LAGW-CopyField (TOGTools inbox 513d5779), the copy box inline in a window instead of a popup: an AceGUI widget type with SetText, GetText and SetLabel. The box follows ShowCopyBox'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 with AceGUI: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 first SetText (TOGTools peer review 63629c29).

  • DockWindow: h:Undock() and W:UndockWindow(frame) (TOGTools inbox 2472500e). The window leaves its target, a docked one is pinned where it is on screen, and status.docked is left alone, so a later DockWindow to a new target docks it again. Feature-detect if W.UndockWindow then.

Fixed

  • A window docked to an AceGUI window followed that window's frame into another addon. AceGUI pools frames: Release hands the same raw frame to the next AceGUI:Create("Frame"), and DockWindow'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 (through OnWidgetRelease), so no consumer has to do anything. Re-pointing DockWindow at 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.lua included.
  • 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.lua gains 5 examples: Undock pins 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.lua gains 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, the opts overrides, a user edit put back, re-selection on click and focus, close on OK / Enter / Escape with onHide once 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, silent SetValue, disabled, the label, the UI scale, hide-while-armed, and a clean return from the pool.
  • The Tests/wowapi harness pin moves to 8e053f2. From 9c6c874 its library manifest loads the new LibAceGUIWidgets-ClickBinding.lua (WoWAPITesting inbox 00eb7ea8); from 9c80f46 a FontString with no font refuses SetText with "Font not set", as the client does (inbox 6ceaea3f).
  • That rule turned two formdialog_spec examples red: the harness has no GameFontRedSmall, which the client does define, so the error line had no font offline. Harness 8e053f2 defines it (inbox 798ae268), 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 inbox 55767d11). It returns the frame on screen that shows target's cell in that column: the mouse overlay for a column with onCellEnter, onCellLeave or onCellClick, else the cell itself. target is 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 follows SetColumns. Dibs' spec found the row by reading rl.rows and rl:_getSortedData(); this replaces both.
  • CreateFormDialog: a checkbox row and a note under a row (Dibs inbox 8d5165f7), so Dibs can drop its two hand-built officer dialogs.
    • kind = "check" puts a UICheckButtonTemplate box 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.value sets the start, row.onChanged(value, dialog) hears a click, and row.tipTitle / row.tipBody go on the box. The row is the usual 26 tall and follows the scale.
    • row.note = "text" draws a small grey GameFontDisableSmall line 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.body and frame:SetBody(text or nil) (Dibs inbox 33a2bbce) put a paragraph under the title and hint, above the rows, wrapped to the dialog's width on the scaled GameFontHighlightSmall. Setting it again, after Show included, replaces the text and the height follows; nil or "" hides it and it takes no space. frame.body is 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, SetNote and SetBody, 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's onCellEnter with 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 a SetHint after each.

Dev tooling

  • wow-version-replication.ps1 no longer deletes the replicas of a file the source still has (TOGProfessionMaster peer review 0f76f963). An editor that saves by replacing the file raises a Deleted event for a moment before the new file lands; the watcher removed the file from every other WoW install on that event. A Deleted for a path that still exists is now treated as Changed and 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. Its Release empties widget.events but 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) runs fn(widget) once when AceGUI releases the widget, then forgets it. It chains onto the widget's OnRelease method, the way PersistWindow does, so the consumer's SetCallback("OnRelease") slot stays free. A second call with the same key replaces the hook, false removes it, hooks run newest first, and one that raises is reported through geterrorhandler without stopping the rest.
    • W:WidgetFrameScripts(widget, { OnMouseDown = fn, ... } [, target]) sets raw scripts on widget.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's SetCallback still fires. A second call replaces the handler and still restores the original. false restores one event now, and W: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 to UIParent. 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]) is AttachTooltip for an AceGUI widget. AttachTooltip(widget.frame, ...) hooks with HookScript, which cannot be removed, and leaves its text and the disabled-hover flag on the pooled frame. This one answers on widget.frame, including the EditBox and Dropdown label area (whose mouse it enables), and on the widget's editbox, button and slider. Each frame's existing handler runs first, so the consumer's SetCallback("OnEnter") still fires. It draws through ShowTooltip with opts.owner, opts.reason and opts.titleColor, leaves through HideTooltip, 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, and W:AnchorPopup (TOGProfessionMaster inbox c8d6892f). ShowTooltip, AttachTooltip and AttachWidgetTooltip take minWidth (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 resets SetMinimumWidth by itself, so the previous value is saved as both halves GetMinimumWidth returns (width, forced) and put back when the tooltip hides, through a hook on its OnHide, and before the next showing. DressBottomRow takes it per icon as tipMinWidth, and a ClearFrame's SetInfoTooltip(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:StopFlash and W:FlashScreen, a finite attention flash (TOGProfessionMaster inbox 759b8287). Breathe loops until stopped; Flash(region, { times, seconds, low, onDone }) dips the alpha times times (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 own OnLoop calls that turn back to FORWARD, one per dip, which is right whether the client calls OnLoop at each end of a pass or once per cycle (the docs do not say which). OnLoop is 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 at FULLSCREEN_DIALOG strata 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 with AccentRGB.
  • W:IsMenuOpenFor, W:IsInputFocusedIn and W:OnMenuClosed (TOGProfessionMaster inbox 8f507ee7), 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's GetCurrentKeyBoardFocus when 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 inbox 0c477b45) lights the row under the pointer. true uses 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 on BORDER one 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 its OnLeave when 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 when row: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 from IsMouseOver. 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). With opts.sortCycle = "three", or col.sortNone = true on 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. A sortDescDefault column cycles desc -> asc -> none. The clear writes the same state as SetSort(nil), and onSortChanged now reports a cleared sort as (nil, nil, rl). Lists stay two-state unless they ask.
  • col.format gets the cell's width and a measure (TOGProfessionMaster inbox c5476af2), 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 as format(value, entry, width, measure): width is the cell's pixel width at the current scale, and measure(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 the autoFit measure pass neither argument, so they see the full text. New rl:CellWidth(key) answers any column's width, the auto column included once a row has been laid out (nil before that); ColumnWidth keeps 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 with opts.searchTokens = true, SetSearch matches by W: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 until SetData, Refresh or SetColumns, so searchText does 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() and rl: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, a SetData or 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) takes ifNeeded (no scroll when the entry is already on screen; from below, it comes in at the bottom) and context = n (rows left beyond it, capped at one less than the page, then clamped at the list's ends).
  • SetSearch now 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 when opts.isHeader(entry) answers truthy or, without it, when entry._header is: a string is the heading's text, true uses 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, default GameFontNormal). entry._expanded true or false adds the client's own craft-window minus or plus in front; nil adds nothing. A click on a heading reaches onRowClick with 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 uses externalSort. Column filters never judge a heading and its cells add no filter values or autoFit width. 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._indent is a level: level x opts.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 is opts.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 a button column, 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 (no format, no class tint). col.tip(entry, button) returns title, 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 on CreateExplainedButton, and tip wins over cellTip. Every one is re-read on each populate, so a sibling addon that loads late needs only rl:Refresh(). _readonly still disables and does not hide.
  • RowList col.iconTexCoord and opts.fitContent (TOGProfessionMaster inbox 8680f2cc). iconTexCoord = { l, r, t, b }, or true for the usual 0.07-0.93 crop of Interface\Icons art, 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 = true or { maxRows = n } sets the PARENT's height to the header bar plus one scaled row height per drawn row, up to maxRows, 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 ignores SetHeight.
  • RowList drag to reorder (TOGProfessionMaster inbox 973a1ab1). With opts.reorderable and opts.onReorder(fromIndex, toIndex, entry, rl), dragging a row lifts a copy of it at TOOLTIP strata 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 calls onReorder with the entry's index in data and the index it should have after the move. The list moves nothing itself, so the consumer reorders its model and calls SetData. The client's own drag threshold keeps a click a click, and a right-click still reaches onRowClick. 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. EnableKeyboard is protected and SetPropagateKeyboardInput restricted (Classic Era SimpleFrameAPIDocumentation.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. An externalSort list draws data as given, so it can be dragged. Detach cancels a drag in progress.
  • W:ClampWindow and W:SetWindowProfile (TOGProfessionMaster inbox 72e8dd4b). ClampWindow caps a window's size to the screen and moves it fully onto it, in the window's own units, and writes the result into widget.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's Frame, and SetResizable(false) on both so a drag is refused. A resizable profile restores its own size from saved.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 inbox 7481d3a5): a window dragged and then closed on a resizable tab used to reopen at the size saved at the last tab switch. ForgetWindow saves it before the record goes and before the widget's own OnRelease runs, through the same helper as the switch-away save. A locked size only ever lands in the table's ordinary width/height, so it cannot reach a resizable profile's save. Position is shared, and every switch ends in ClampWindow. ForgetWindow, and so Release, turns resizing back to how it was before the first profile.
  • W:SetWindowOpacity / W:GetWindowOpacity (TOGProfessionMaster inbox ae090bd5) 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's content sits in, which is the only way to reach InlineGroup's). Text, icons, buttons, borders and SetAlpha are 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 inbox e5e1586a) set one window's frame:SetScale, clamped to 0.5-1.5 and separate from the session-wide W: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 as windowScale in a persisted window's table, and PersistWindow re-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 inbox d8f15682): 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), CloseSpecialWindows walks UISpecialFrames with pairs and hides every entry that IsShown(), 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's OnHide closes 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 own IsShown flag true and is ignored. Closing the window hides every shown child, which runs their OnHide, 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: CloseWindows also calls CloseSpecialWindows (line 1087), so loss of control and other panel opens also close only the newest child of a layered window.
  • CreateExpandableList at any depth (TOGProfessionMaster inbox 7ab1cb56), for a profession -> specialisation -> member tree. A child may carry children of its own, and the layout is now one recursive walk. A node with children is a group at any depth (header style, a toggle, indented indent per 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. defaultExpanded and SetAllExpanded reach every depth, and list:IsExpanded(key, ...) reads a path. Any node takes color, onClick(node, mouseButton, rowFrame) (a group toggles and then calls it) and tooltip, 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:CreateStepper and the LAGW-Stepper widget type (TOGProfessionMaster inbox ea8db903): [-] [qty] [+] [MAX]. Typing keeps digits only as they are typed, and commits on Enter or focus loss, clamped to min and max(). An emptied box and Escape revert. - and + step by step and are CreateExplainedButtons, so at a bound they grey out and the tooltip's red last line says which bound. MAX sets max(), read on every click; a max of 0 greys it. SetValue is silent, and a player's change calls onValueChanged(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 fires OnValueChanged(value). It builds MAX once and shows it only after SetMaxButton(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, SetHint and anchorTo (TOGProfessionMaster inbox 6cf3b4e4), all opt-in; existing rows build exactly as before.
    • A kind = "dropdown" row puts a CreateDropdownBox where the edit box goes. items(query) returns { text, value } pairs, built fresh on each open with the current one ticked. Choosing one calls onChanged(value, dialog) only when it changed. fields[i] has GetValue() and a silent SetValue(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; SetHyperlink is given the bare item: part. A click goes to the client's HandleModifiedItemClick. fields[i]:SetItem(link[, icon]) changes it. The icon is row.icon, else C_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 (with point, relPoint, x, y; default TOPLEFT to the owner's TOPRIGHT, 4px right) and frame:SetAnchor(owner, ...) place the dialog by its owner on every show, clamped by the floating frame. A later SetAnchor re-points a named dialog to whichever row opened it. An owner that is not visible leaves the dialog where it was.
  • W:CreateSecureActionButton (TOGProfessionMaster inbox 3d04549b), a secure button with combat guards. It registers for AnyUp and AnyDown: read in Classic Era SecureTemplates.lua:789-823, SecureActionButton_OnClick acts on exactly one of the two, chosen by useOnKeyDown or the ActionButtonUseKeyDown CVar, so it fires once under either setting. SetAction(attrs) and the button's own SetPoint, ClearAllPoints, SetAllPoints, SetSize, SetWidth, SetHeight, Show, Hide, SetShown, SetEnabled, Enable, Disable and SetParent (wrapped on the instance) run now, or are queued in combat. One session-wide queue runs once, in order, on PLAYER_REGEN_ENABLED. A scale change in combat queues the resize as well. The factory returns nil in combat. The tooltip is CreateExplainedButton's and works while disabled; preClick / postClick are the consumer's. Not safe to pool: SetParent is IsProtectedFunction in 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 inbox 2f9efc9e) splits a container into top, right, bottom and center (alias left). The panes are four raw frames the library owns, anchored into the container's content, 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 (its OnSizeChanged) and by a scale listener. top, right and a fixed bottom are scale-1.0; an "auto" bottom is the pixel height given to SetBottomHeight(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 centre minCenter. 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 uses dock: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's NORMAL_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_ACCENT is now one literal, read by the config table, GetAccent and AccentRGB. AccentRGB's per-channel fallback for a malformed accent is now 1, 0.5, 0 to match. Colours the library draws as the client stay on NORMAL_FONT_COLOR: a tooltip header, the cooldown caption, the DatePicker label.
  • An upgrade in a live session moves the old default too. lib.config survives 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 through SetAccent or Configure (recorded since MINOR 33) keeps it.
  • PersistWindow clamps 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. fitContent sets 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() and SetBounds no longer rewrite the box while it has focus. A consumer refreshing on BAG_UPDATE used 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 step counted from min, 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 from min 0 lands exactly where it did. A step of zero or less falls back to 1 instead of dividing by zero. SetStep re-snaps the current value onto the new grid, as SetBounds re-clamps it, so a click from a value set under the old step moves by exactly the new one.
    • SetWindowScale re-applies the window's own anchors with the offsets times old/new, instead of re-placing it TOP/LEFT on UIParent. 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.

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 modelled SetMinimumWidth/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 own OnLoop, 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's GetCurrentKeyBoardFocus.
  • 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's IsMouseOver answering 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.lua holds 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, fitContent and the drag. It drives the drag by placing the harness cursor and calling the row's OnDragStart / OnDragStop and the lifted row's OnUpdate. The harness models neither keyboard getter, so the Escape tests record EnableKeyboard and SetPropagateKeyboardInput calls. 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.0 ClearFrame, Frame, TabGroup and InlineGroup, 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 own CloseSpecialWindows loop, 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 stubbed C_Item and HandleModifiedItemClick, and the hint and anchor through the frame model's anchor points.
  • Tests/securebutton_spec.lua (9 examples) swaps InCombatLockdown to enter and leave combat and fires the queue's own PLAYER_REGEN_ENABLED handler.
  • Tests/docklayout_spec.lua (7 examples) reads the panes' anchors, and checks that the dock replaces no method on a real AceGUI SimpleGroup and lets go on its Release.
  • The v0.1.11, v0.1.12 and v0.2.0 sections moved whole to CHANGELOG_ARCHIVE.md to keep this file under 120,000 characters.
  • Tests/widgets_spec.lua pins 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_ACCENT rather 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 was is_nil on a literal key, which still passes if the key is misspelt or renamed. bootstrap_spec.lua now checks W:TriggerVersionCheck() returns false instead of reading W._vc. widgets_spec.lua checks that every config key Configure did not name is unchanged and that none was added, instead of W.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