LibAceGUIWidgets-v0.5.0
What's new
Changelog
[v0.5.0] -- a button strip with a status line, a live character counter, toggle chips on an options page, a form label you can rename, and a scrollbar that fits a short list
Compatibility -- MINOR 42 -> 43
New: W:CreateButtonStrip (new satellite LibAceGUIWidgets-ButtonStrip.lua, listed in the TOC);
feature-detect with if W.CreateButtonStrip then. New: W:AttachCharCounter / W:DetachCharCounter;
feature-detect with if W.AttachCharCounter then. New: a CreateFormDialog frame has
f:SetLabel(i, text) and f.labels[i]; feature-detect with if f.SetLabel then. New: W:OffCooldownTick, W:UnbindCooldownButton and W:RemoveEscapeChild; feature-detect
each with if W.<name> then. Changed: a ShowDialog prompt inside an Escape-layered window joins
that layer for the show (detect with if W.RemoveEscapeChild then). Changed: LAGW-Chip
widget Version 1 -> 2 (the AceConfigDialog toggle surface) and LAGW-SearchBox Version 1 -> 2
(SetShowCounter); feature-detect with AceGUI:GetWidgetVersion(type) >= 2. Changed: a RowList host
too short for both scroll arrows hides them. Nothing for an existing consumer to do; every addition
is opt-in.
New
- A row of explained buttons with a status line (Ultimate Guild Roster inbox 31133c51, item 1,
so UGR can delete the strip it and FastGuildInvite hand-rolled).
W:CreateButtonStrip(parent, specs, opts)laysW:CreateExplainedButtonbuttons left to right with a right-justified status line on the same row. Each spec is{ text, tip, onClick, width, reason, key }; areason(string, or a function asked at build, on every hover and onstrip:Refresh()) greys the button and is the red last tooltip line. Returns the strip frame itself, carrying.frame,.buttons,.statusandSetStatus/Refresh/GetButton(n | key | text), because UGR anchors other frames to its strip. Every default size is UGR's own (GUI/UI.lua:18-57) and followsW:SetScale. The status line is bounded on the left by the last button so a long status is clipped instead of running over the buttons (UGR's was right-anchored only). Specs inTests/buttonstrip_spec.lua. The harness loads the new file since WoWAPITesting pin23d0039(inbox c94d8892). Not verified in the client, including the clipping itself. - A live "n / max" character counter on an edit box (UGR inbox 31133c51, item 2).
W:AttachCharCounter(target[, opts])works on a raw EditBox (single- or multi-line) or an AceGUI widget with.editboxor.editBox; it followsSetMaxLetters(hooked) and every text change, hides while the max is 0, turns red at the limit, and detaches on Release. It counts with the box's ownGetNumLetters()againstGetMaxLetters(), the pair Blizzard's own counters use (SecureUIPanelTemplates.lua:142, Classic Era tree).LAGW-SearchBox:SetShowCounter(bool)uses it and narrows the text box by the width of "max / max" so it does not jitter. Specs inTests/charcounter_spec.lua. Not verified in the client. LAGW-Chipcan be an AceConfig toggle'sdialogControl(UGR inbox 20d63ff4, so an options page draws its switches as toggle tokens). Version 2 addsSetLabel(=SetText),SetTriState(accepted; a nil value draws off),SetDescription(shown as the chip's hover tooltip throughShowTooltip, cleared on acquire and release),SetImage/SetImageSize(ignored). Inside AceConfigDialog a chip with nowidthsizes to its label: AceConfigDialog's own 170px default is read as "no width" onceSetTriStatehas marked the chip as a toggle, and every other width is honoured (width = 1and"normal"reach the chip as the same 170, so they draw label-sized). An explicit width now survivesSetLabel/SetText; before, every relabel shrank the chip back to its label, which a language switch would have hit (Peer Review on audit 509928c3). Specs inTests/chip_aceconfig_spec.luadrive the real AceConfigRegistry and AceConfigDialog. Not verified in the client.- A form dialog's row label can be renamed (UGR inbox 1a2fe53f, for a language switch without a
/reload).f:SetLabel(i, text)sets rowi's label (the text right of the box on acheckrow); nil or""clears it; an index with no row does nothing, asSetNotedoes.f.labels[i]is the FontString. Spec inTests/formdialog_setlabel_spec.lua. - A cooldown listener can be dropped (UGR inbox 58585085).
W:OffCooldownTick(name, fn)removes every registration of that function for that name and returns whether there was one; it never touches the clock. Removing a listener from inside a tick neither calls it again nor skips the next one: the slot is blanked during the fan-out and the list compacted after it. Specs inTests/cooldown_spec.lua. - A cooldown button can be unbound (UGR follow-up on inbox 58585085). UGR's language switch
throws its Sync tab away and builds a new button, so the rebind fix below never sees the old one.
W:UnbindCooldownButton(button)drops the painterBindCooldownButtonregistered for that button and returns true, or false for a button that is not bound; the clock and the button's click handler are left alone. Specs inTests/cooldown_spec.lua.
Fixed
One Escape in combat closed a prompt AND the window it sat in (Peer Review inbox e2a2b784, found reviewing UGR's audit ace8973e). The shared dialog is its own
UISpecialFramesentry; inside a window with an Escape layer, the client's walk hid the dialog and the layer's proxy on the same press, and the proxy, finding no child up, closed the window. Out of combat the dialog's own key handler or focused box usually took Escape first, so this hit a prompt shown in combat or an edit prompt whose box had lost focus.ShowDialognow walks up from its parent to the first window with a layer and joins the dialog to it for that show only; finishing the show takes it out again. The dialog's name goes back onUISpecialFramesat the nextShowDialogthat finds no layer, not when the show ends: an Escape-closed prompt ends inside the client'spairswalk of that list, and Lua 5.1 leaves the walk undefined if the list gains an entry (Peer Review, same thread). So addon A's window closing later cannot cancel addon B's prompt, and a consumer that added the dialog to its layer itself keeps it. NewW:RemoveEscapeChild(window, frame)takes one child out of a layer and re-lists its name; it,EscapeLayerandDropEscapeLayerare documented as not to be called from an OnHide that Escape reaches, for the same reason. A closing window now snapshots its shown children before hiding them, since a child's hide can now take it out of the list being walked. Specs inTests/dialog_escapelayer_spec.lua; with the join disabled, 7 of its 11 fail. Not verified in the client.A prompt over a window's floating panel or form still lost the panel to one Escape (Peer Review inbox ace03ea9, UGR's "Clear a range" form and its confirmation). Those panels are parented to UIParent and join the window's layer as children, so the walk up from the prompt's parent never reached the window. A layer child now carries the windows whose layers it was added to, and
ShowDialog's walk joins the most recently added of those that is shown and still holds it: in combat the first Escape closes the prompt, the second the panel, the third the window. A list, not one owner, because a frame can be a child of several layers (the shared frameRemoveEscapeChildis documented for), and dropping one layer must not cost the other its order; a window that adds the child again moves to the end. A hidden owner is never joined, because its proxy takes no press; with no shown owner the prompt is listed onUISpecialFramesas before (Peer Review on audit 4e6d3a5b). Specs inTests/dialog_escapelayer_spec.lua; four of the five new ones were seen red with their part of the fix disabled; the fifth (a child that left its layer) holds without the lookup too. Not verified in the client.Rebinding a cooldown button stacked another listener (UGR inbox 58585085, found by Peer Review on thread 8ae2f42a).
W:BindCooldownButtonregistered a painter on every call and nothing removed one, so UGR's Sync tab, rebuilt on each language switch, left the old button's painter running every tick until/reload. A button bound again now drops its earlier painter first, whether the new bind uses the same name or another. Specs inTests/cooldown_spec.lua. Not verified in the client.One cooldown listener that errored silenced the rest. The error escaped the tick, so every later listener missed it. Each listener is now called protected and the error goes to the client's error handler. Spec in
Tests/cooldown_spec.lua.A consumer callback's error report now carries the callback's own stack (Peer Review N1 on audit 3ffffa8a). The library called consumer callbacks two ways:
pcall, whose report has already lost the stack of the code that raised, and, for menu clicks only,xpcallwith adebugstackhandler. One file-scopesafeCallnow does the second for every dispatch site:OnMenuClosedcallbacks, scale listeners, the pool-safe release functions, cooldown listeners and every callback the Resize satellite fires (throughlib._safeCall). Menu clicks share itswithStackhandler. Spec inTests/cooldown_spec.lua.A RowList in a short host drew its scrollbar upside down (TOGProfessionMaster inbox 782ce874, their one-row shopping list). The lane was inset one arrow height at each end, so a host shorter than the header plus both arrows put its top below its bottom. Below that height the arrows now hide and the lane spans what is under the header, clamped so it is zero tall at worst; a host that grows back gets its arrows back, re-evaluated on every refresh. Hosts that drew correctly before are laid out exactly as before. Spec in
Tests/rowlist_shorthost_spec.lua, confirmed red without the fix. Not verified in the client, including how the 24px thumb looks in a very short lane.W:AttachWidgetTooltipon an AceGUI MultiLineEditBox said nothing over its text area. AceGUI names that boxeditBox, and onlyeditboxwas looked for. Spec inTests/poolsafe_spec.lua.
Tests
Tests/escapelayer_spec.luadrives Escape through the harness'sframes.pressEscapeinstead of its own copy of the walk (Peer Review inbox 721966e5). Harness pin moved to05cb0c8.
[v0.4.0] -- a menu row's tooltip draws above the menu, an expandable list's rows fill its width, and a prompt can look like a window
Compatibility -- MINOR 41 -> 42
New: CreateExpandableList takes opts.valueColumn = { offset, width, justify }; feature-detect
with LibStub.minors["LibAceGUIWidgets-1.0"] >= 42. A list created without it draws as before.
New: ShowDialog and ShowCopyBox take style = "window" and title; feature-detect with
if W.DIALOG_STYLES and W.DIALOG_STYLES.window then. A prompt without style draws as before, on
the same LibAceGUIWidgets_Dialog frame.
Changed: while a tooltip drawn by this library is anchored to a frame in TOOLTIP strata
(an OpenMenu level, the DatePicker, anything built with W:CreateFloatingFrame), GameTooltip's
frame level is raised 50 above the highest TOOLTIP-strata level among the frame and its parents,
and put back when the tooltip hides or the library shows its next one. Changed: CreateExpandableList
anchors its content's width to the scroll box instead of setting it, and re-lays out from the
scroll box's OnSizeChanged (hooked) instead of list.frame's (which it no longer sets). Nothing for
a consumer to do.
New
- An expandable list can line its values up in one column (TOGProfessionMaster inbox a79b0269,
so the Guild tab's counts sit under a "Characters" header instead of at the window's right edge).
opts.valueColumn = { offset, width, justify }anchors every row's value, header and child style at every depth, LEFT at the row's left plusoffset,widthwide, justifiedjustify(defaultRIGHT). Rows themselves are never indented (only their glyph and label are), so depth does not move the column. Both numbers are scale-1.0 and are re-applied each time a row is acquired, so they followW:SetScale. The label's right edge stays 6px before the value, so a long name truncates before the column; the space to its right stays empty and the row highlight still spans the row. Specs inTests/expandable_depth_spec.lua: a depth-0 and a depth-2 value share the offset, width, justify and left edge; the column follows the scale; a list without the option stays right-anchored. Not verified in the client. - A prompt can look like a window (ClassicCalendar inbox 82cae97e, so their delete confirm
matches their World Buff and Auto-Moderator windows).
ShowDialog(parent, { style = "window", title = ... })draws Blizzard'sBasicFrameTemplateWithInset(title bar, close X, inset;Blizzard_UIPanelTemplates/Classic/UIPanelTemplates.xml:676,:714,:751, Classic Era tree) instead of theUI-DialogBoxbackdrop, 22px taller with the body 22px lower to clear the bar. The window look is a second shared frame,LibAceGUIWidgets_DialogWindowonW._windowDialog, because a frame cannot change template once built and restyling the one plain frame per show would leak into the next addon's prompt. The two frames share every rule: showing either closes a prompt open in the other first (itsonHidebefore the newonShow),CloseDialog(token)looks in both, and both are inUISpecialFrames. The X is wired to the same close as Cancel rather than the template'sUIPanelCloseButtonscript, soonHideruns once.ShowCopyBoxpassesstyleandtitlethrough.W.DIALOG_STYLES = { plain = true, window = true }is the feature check. Specs inTests/dialog_spec.lua: the style is advertised; the window frame carries the title, X and inset and the plain one does not; the X runsonHideonce and never accepts; a replaced prompt'sonHideruns across styles andCloseDialogfinds either; the window frame is one Escape-list entry;ShowCopyBoxcarries the style. Not verified in the client.
Fixed
- A menu row's tooltip drew behind the menu (ClassicCalendar inbox d8a239c7, from a player
report on the Templates menu's "Loot system" row).
CreateFloatingFrameputs every menu level at strataTOOLTIP, level 100 + 10 per depth.GameTooltipis alsoTOOLTIPstrata, declared with no frame level (Blizzard_GameTooltip/Classic/GameTooltip.xml:13, Classic Era tree), andshowTooltipnever raised it, so in the same strata the menu's higher level won and the tooltip was covered wherever it overlapped. Every tooltip the library anchors now lifts the level and restores it through the sameOnHidehook that already restores the minimum width. The lift is inW:AnchorTooltipitself (so form-dialog item rows, RowList icons and hyperlinks, and the ClearFrame info button get it), plusShowTooltip'sopts.ownerbranch, the TLabel tooltip, and RowList's owntooltipOwnerbranch. Specs inTests/menu_spec.lua: a root row's and a submenu row's tooltip sit above their menu and the level comes back on hide;AnchorTooltipalone raises over a floating panel; a frame outsideTOOLTIPstrata leaves the level alone. Not verified in the client. - An expandable list's rows were drawn 40px wide, every label as ".." (TOGProfessionMaster
inbox 0f350cea, their Guild tab).
CreateExpandableListset the content's width from a one-timescroll:GetWidth()read, floored at 40, and re-read it only from the outer frame'sOnSizeChanged. A list placed in an AceGUI group before the group was laid out read a width of about 0, and nothing corrected it once the scroll box was sized. The content's width now comes from its two top corners anchored to the scroll box, less the bar and 2px, exactly the rule theCreateScrollFramenote already states, so it is right in any sizing order. The re-layout (for the scroll range) now hangs off the scroll box's own size change; the outer frame's handler is gone, because the scroll box fills that frame and hooking both laid out twice per resize. A consumer that hookedlist.box.scroll'sOnSizeChangedto calllist._relayout()can drop that hook. Spec inTests/expandable_depth_spec.lua: a list filled in an unsized parent, which is then sized and resized, has rows exactly as wide as the scroll box less the bar. Confirmed red with the width pinned at 40. Not verified in the client.
[v0.3.1] -- a window that re-lays-out while you drag its edge, and a docked ClearFrame that hears its title bar
Compatibility -- MINOR 40 -> 41
No new API. Changed: a ClearFrame now carries its own onResize on its resize handle. At most once
per frame draw during a drag, it calls the widget's DoLayout, but only when AceGUI's own content
hook has not already laid out at the current size. onResizeStop (the size and position saved to
the status table) is unchanged. A consumer that set its own onResize
through W:GetResizeHandle(win).onResize = fn replaces this one, exactly as before; through
W:MakeResizable(win, { onResize = fn }) the same. ClearFrame, GroupFrame and TLabel move
32 -> 33 together, so an older ClearFrame without the live re-layout cannot win a tie. Changed:
W:DockWindow also hears a drop on a ClearFrame's title bar, and a docked AceGUI window gives up its
dock on Release; a consumer that hooked the title bar itself can drop that hook.
Changed
- ClearFrame makes sure its contents are laid out during a resize drag (Dibs contract
DIBSREQ-LAGW-003, the half still open after MINOR 30 madeMakeResizablemerge callbacks). Before, the constructor passedonResizeStoponly. AceGUI'sRegisterAsContaineralready hooks the content frame'sOnSizeChanged(ContentResize), which callsDoLayoutand records the size incontent.width/content.height. In the harness model that hook already re-lays-out the contents during a drag, twice per cursor move (once per dimension). So the newonResizere-lays-out only a size that hook did not record, and the draw adds nothing on top of it. The first cut of this change calledDoLayoutunconditionally, which doubled the work; the README (MakeResizable) already warned against that, and the guard came out of reading it. The callback readsf.objat call time, because AceGUI pools the frame. Not verified in the client: whether the content hook fires during a real drag as it does in the model. If it does, Dibs'C.LiveRelayoutis a secondDoLayoutper draw and can go. Specs inTests/scale_spec.lua: a drag lays out at least once and the draw adds none, and a size the hook missed is laid out once on the draw and not again.
Fixed
- A docked ClearFrame now undocks and re-docks when you move it by its title bar (PersonalShopper
inbox bd091818).
W:DockWindowheard a drop only through the frame'sOnDragStop, but aClearFrameis moved by its title bar, which callsStopMovingOrSizingfrom its ownOnMouseUp, soOnDragStopnever fired andh:Moved()was never called. Dragging a docked ClearFrame away left it docked in the saved preference, and dropping one at the edge never docked it.DockWindownow also hooks the title bar'sOnMouseUpon any widget whosetitleis a child of the docked frame. PersonalShopper's own hook on the title bar can go; if it stays,Moved()simply runs twice. - Releasing a docked AceGUI window lets go of its dock. The window's frame went back to AceGUI's
pool still owned by
DockWindow, so the next addon'sAceGUI:Creategot a frame whoseOnShowdocked it to our target, and thatPersistWindowrefused as ours. Found by the new spec above, whose released ClearFrame made a later spec file'sPersistWindowfail. On release the handle now undocks and drops its record on the frame, and its script hooks (which cannot be removed) do nothing once the frame is no longer theirs. Specs inTests/dock_spec.lua, against a real ClearFrame. Not verified in a game client.
[v0.3.0] -- toggle chips, a tab strip that scrolls, a top-right window button, a strip row without an empty caption band, and Whitemane in the TOC
Compatibility -- MINOR 39 -> 40
New: the LAGW-Chip AceGUI widget type (Version 1) and W:AddChips, the LAGW-TabStrip widget
type (Version 1), and W:AttachHeaderButton / W:DetachHeaderButton with ClearFrame:SetHeaderButton.
Feature-detect with AceGUI:GetWidgetVersion("LAGW-Chip") or if W.AddChips then,
AceGUI:GetWidgetVersion("LAGW-TabStrip"), and if W.AttachHeaderButton then /
if win.SetHeaderButton then. Changed: a LAGW-Strip row with no
caption on any of its controls no longer reserves the caption line, so a strip whose second row is
captionless is shorter by one caption line; a consumer that sized a window floor from
W:StripRowWidth is unaffected (only the height moved). ClearFrame, GroupFrame and TLabel
move 31 -> 32 together, because a ClearFrame without SetHeaderButton must not win a tie.
New
LAGW-Chip, an on/off pill button (Grouper contractc98a1eab; their operator: "ask widgets to add a toggle chip, i think it could be good").SetText,SetValue/GetValue,SetDisabled, andOnValueChanged(value)on a click, as AceGUI's CheckBox does;SetValueis silent. On is the library accent (tinted fill, accent border, white text), off is the dark fill and grey border of the dropdown box; hover adds the button highlight; disabled is dimmed and ignores clicks but keeps its tooltip. Sized to its label, following the UI scale. The frame is a Button, soW:AttachWidgetTooltipanswers on it, and it drops intoW:StripAddlike any control.W:AddChips(container, order, labels, values, onChanged)builds one chip per key inorder, lit fromvalues[key]. A click writesvalues[key]and then callsonChanged(key, value, chip). Returns the chips keyed by key.LAGW-TabStrip, tabs added at runtime that scroll with<and>(Dibs contractcb965300; their operator: "all you have to do is have < and > arrows at either side of the tabs so you can scroll back and forth between tabs").AddTab(value, text)(a value already there is re-labelled),SetTabText,RemoveTab,ClearTabs,SelectTab(value)(silent, and scrolls the tab into view),GetSelected,GetTabs,SetMaxTabWidth,GetVisibleRange, andOnTabSelected(value)on a player's click of another tab. A tab is its label plus 10 a side, capped at 140 (scale-1.0); a longer label is cut short and shows in full on hover. The arrows appear only when the tabs overflow, each scrolls one tab, and each is disabled at its end. The strip only: the consumer draws what a tab shows. Looks like the chips: the selected tab in the accent, the rest grey.- A text button in a window's top-right corner (Grouper contract
12c62f8b; their operator: "can you add a button in the top right to switch the user back to the legacy UI ... we'll need the legacy ui to have the same button to switch to the new UI").W:AttachHeaderButton(widget, text, onClick, tipTitle, tipBody)works on a stock AceGUIFrameand on aClearFrame, which also has it aswin:SetHeaderButton(...); both go through the one builder, so the two windows cannot drift apart. Inside the border, 14 in from the right, its bottom level with the content's top (27 down), sized to its label (10 a side, at least 40) and lifted above the resize strips. On a ClearFrame it sits a further 4 in and 6 down (Grouper inboxe62d2977): with the stock numbers the operator saw it straddle the ClearFrame's top border ("it kinda overlaps the border in the new UI") while the stock Frame's was "perfect". I have not found the cause in the code; the offset is set from what was seen, and the operator rechecked it in game after the move: "perfect". Calling again re-labels the same button;text = nilhides it. It follows the UI scale on a ClearFrame and stays at 1.0 on a stock Frame, whose chrome does not scale. Release hides it and drops its handler, tooltip and scale listener, so AceGUI's pool cannot hand it to another addon's window;W:DetachHeaderButtondoes that early.
Changed
- A strip row with no captions has no caption band (raised with the chip request). I reserved the caption line on every row, so a captionless second row sat under an empty band.
Fixed
- The TOC now lists 38000 (Whitemane) beside 38002 (Titan Reforged) (Peer Review
2bd6dff2). I left it out, so on Whitemane the library read as out of date while Grouper, which depends on it there, was current. 38002 stays.
Tests and docs
- Tests. New
Tests/chip_spec.lua,Tests/tabstrip_spec.luaandTests/headerbutton_spec.lua(placement against a real AceGUI Frame and ClearFrame, the scale, and what a released window keeps);Tests/strip_spec.luacovers a captionless row, andTests/dock_spec.luacovers two windows sharing one saved table. The suite runs 1049 checks, 0 failing, and every shipped.luafile is at 100% line coverage (6380/6380). - Docs. README's filter-strip entry now says a captionless row reserves no caption line. The CurseForge page lists the chips and the tab strip, carries a MINOR 40 example and the two new widget type names, and keeps the last five versions under Recent Updates.
- Docs made to agree with the code and with each other (a full README-against-CurseForge read).
The CurseForge page said a window given both PersistWindow and DockWindow is kept in the
diagnostics record; it is only refused and printed (
otherOwnercalls_Say, not_Report). It said a dialog parented into a window "closes with that window"; since MINOR 33 it goes off screen with it and comes back still open,onHideunrun. It said/lagw debugis remembered, which holds only when the library is installed as its own addon. README still said the three legacy widget types register at 31, and never mentioned the filter menu's Select all / Clear all, which act on every value drawn or not. The CurseForge page now also names the smaller helpers the README documents (scale helpers, named cooldowns,TriggerVersionCheck, the click-binding text helpers,ToggleMenu,SetAllExpanded, RowList column widths, and the read-back partners).
[v0.2.9] -- library chat lines only in debug, a /lagw switch, your own item-row hover, and a menu that closes on a bad pick
Compatibility -- MINOR 38 -> 39
New: the debug field on Configure (true, or a function returning a boolean), W:IsDebug(),
W:SetDebug(true|false|nil), the /lagw debug [on|off|reset] slash command, the LibAceGUIWidgetsDB
SavedVariables (standalone addon only), and onEnter / onLeave / onClick on a CreateFormDialog
kind = "item" row. Feature-detect with if W.IsDebug then (the item-row callbacks ship in the same
MINOR). Changed: the library prints nothing to chat unless debug is on. W:Diagnostics() records
exactly what it did before, so a consumer spec asserting on it is unaffected. A menu item whose
onClick raises now closes the menu and reports the error through geterrorhandler(); a consumer
wrapping its own handlers in pcall for this can drop the wrapper. No widget type Version moves.
New
/lagw debug(the operator: "you should add a / command to turn it on/off as well").onandoffforce the library's chat diagnostics whatever any addon set; no argument toggles;resethands the decision back to the addons. It answers in chat with the current state, and the choice is saved inLibAceGUIWidgetsDBwhen the standalone addon is installed (an embedded-only copy keeps it for the session). One registration per session; the handler finds the current copy through LibStub, so a newer MINOR loaded later still answers.- An item row can take the consumer's own hover and click (TOGProfessionMaster contract
1d7a76ba).row.onEnter(button, link),row.onLeave(button)androw.onClick(button, link, mouseButton)each replace the built-in tooltip, hide orHandleModifiedItemClickwhen given, so a consumer's hold-to-compare hover and rebound chat-link click survive the move onto the library row. They are read from the row spec at event time, so a named dialog reused across opens follows the spec. None given: exactly the old behaviour. No callback runs for a row with no item.
Fixed
- A menu pick whose handler raises no longer leaves the menu open (Peer Review
b64aa131, measured by TOGProfessionMaster). The row'sOnClickranitem.onClick()unprotected, so an error left before the close: the menu stayed up andOnMenuClosednever fired. I wrote it that way. It now runs the handler underxpcall(keeping its stack viadebugstackwhere the client has it), closes (or, on akeepOpenrow, refreshes the check), and then hands the error to the library's onereportErrorpath so it still reaches the player's error display.
Changed
- Every chat line is behind the debug switch. The empty-render line ("RowList has N row(s) but
is showing none...") appeared in a player's chat between two guild messages; it, the duplicate
column key report, the accent clash report and the PersistWindow/DockWindow refusal now go through
one
lib:_Say, which prints only whenconfig.debugis on. I shipped them unconditional; they are developer diagnostics about a consumer's code that a player cannot act on. A debug function that raises counts as off, so a consumer's broken toggle cannot break a report. - Tests. Specs for the debug gate (off by default,
true, a live function, a raising function),/lagw debugand its SavedVariables restore, the item-row callbacks, and a raising menu pick in both row kinds. The specs that assert on chat now turn debug on first. The suite runs 1011 checks, 0 failing. - Docs brought level with the code. README now names RowList
opts.headerFont, the form dropdown row'smenuWidth, the stepper'sGetValue()and its parts,LAGW-ClickBinding'sGetValue()/SetDisabled()/OnEnter/OnLeave,dock:Relayout()anddock.sizes, and whatOpenMenu,EscapeLayerandShowDialoghand back. The CurseForge page now mentions tick-box columns, text that shortens to fit, the form prompt's hint line and placement, and expandable-list lines with their own click, tooltip and colour. All of these shipped earlier and were only undocumented.
[v0.2.8] -- a copy box with a copy glyph, a click binding you set by doing it, and a dock that lets go
Compatibility -- MINOR 37 -> 38
New: W:ShowCopyBox(text, opts), W.COPY_ICON, the LAGW-CopyField (Version 2) and
LAGW-ClickBinding (Version 1) widget types, W:ClickBinding, W:ParseClickBinding,
W:ClickBindingText, W:IsClickBinding, W:UndockWindow and the DockWindow handle's :Undock().
Feature-detect with if W.ShowCopyBox then, if W.IsClickBinding then, if W.UndockWindow then, or
AceGUI:GetWidgetVersion(type). Nothing existing changes for a consumer. ShowDialog clears the copy
box's read-only guard and glyph on every show, so a plain prompt after a copy box behaves as before. No
existing widget type Version moves. New shipped files: LibAceGUIWidgets-ClickBinding.lua (in the TOC
after the DatePicker) and Textures/copy.tga.
New
LAGW-ClickBinding(TOGProfessionMaster inboxe0f3923e, for ItemDB's "Where to get it" click, which collides with Gargul's Alt+click). The player sets a modifier + mouse button by doing it: click to arm ("Hold Alt, Ctrl or Shift and click here"), then click again holding the modifiers. AceGUI's own Keybinding cannot record a Left or Right click at all.- The value is one canonical string: modifiers in the order
ALT-,CTRL-,SHIFT-, then the button as OnClick names it --"ALT-LeftButton","CTRL-SHIFT-RightButton". ItemDB already stores this form. - A click with no modifier is refused on every button, with a short message, and the widget stays armed. Escape (out of combat) or Cancel disarms and changes nothing. Armed in combat, it takes no keyboard, because it could not pass the other keys on; Cancel is the way out.
SetValue(silent, normalised; unreadable means unbound),GetValue,SetLabel,SetDisabled,SetDefault(adds a Default button).OnValueChanged(binding)fires on the player's changes, reset included, and only when the value changed. Hiding while armed disarms. Released disarmed with no value, label or default.- Frame-free helpers:
W:ClickBinding(alt, ctrl, shift, button),W:ParseClickBinding(binding)(any prefix order and case),W:ClickBindingText(binding)("Alt + Left Click", the client'sNOT_BOUNDfor none, modifier names fromALT_KEY_TEXTetc. where the client has them) andW:IsClickBinding(binding, button): the modifiers held now must match exactly, and a nil button matches on modifiers alone.
- The value is one canonical string: modifiers in the order
W:ShowCopyBox(text, opts)(TOGTools inbox737274f2). Addons cannot write the clipboard, so every one hand-rolled a popup whose box held the text, selected, for Ctrl+C. This is that popup as one call, on the sharedShowDialogframe. It returnsdialog, token, soW:CloseDialog(token)closes it.opts.parentdefaults toUIParent,opts.promptto "Press Ctrl+C to copy, then Escape.",opts.okTexttoCLOSEor "Close".opts.onShowandopts.onHidepass through toShowDialog.- One button, and it closes. Enter and Escape close too. Nothing is called with the text on any close.
- The box is focused and the whole text selected when it opens.
- Read-only. A typed or deleted character is put back and the text selected again. A click in the box selects all again on mouse up, so it is never left partly selected.
- Long text scrolls inside the single-line box; the dialog keeps
ShowDialog's size. - A copy glyph in the box, drawn the way the search box draws its magnifying glass: 14px at the
box's left, the text inset 20 to clear it, white while focused or holding text and 0.6 grey
otherwise. The picture is a new shipped file,
Textures/copy.tga(64x64 32-bit white line art of two overlapping sheets, the same format ascalendar.tga), andW.COPY_ICONnames it. If the file cannot be set, the box draws no glyph and no inset. A plainShowDialogafter a copy box has neither. - The hooks go on the shared edit box once, not once per show.
LAGW-CopyField(TOGTools inbox513d5779), the copy box inline in a window instead of a popup: an AceGUI widget type withSetText,GetTextandSetLabel. The box followsShowCopyBox's rules: the copy glyph tinted like the search box's magnifier, read-only, the whole text selected on focus and click, long text scrolling in the box. Enter and Escape drop focus. AceGUI EditBox's geometry (26 tall, 44 with a label), scaled; works in List and Flow. Released with no text, label or focus. Feature-detect withAceGUI:GetWidgetVersion("LAGW-CopyField") >= 2. It ships at Version 2: Version 1 existed only in the working tree before this release and raised "FontString:SetText(): Font not set" on every Create in game, because the label got its font after its firstSetText(TOGTools peer review63629c29).DockWindow:h:Undock()andW:UndockWindow(frame)(TOGTools inbox2472500e). The window leaves its target, a docked one is pinned where it is on screen, andstatus.dockedis left alone, so a laterDockWindowto a new target docks it again. Feature-detectif W.UndockWindow then.
Fixed
- A window docked to an AceGUI window followed that window's frame into another addon. AceGUI
pools frames:
Releasehands the same raw frame to the nextAceGUI:Create("Frame"), andDockWindow's hooks and list stayed on it, so when another addon showed that frame, our window re-docked onto theirs. TOGTools hit it docking its Explain window to its main window, and worked round it by re-pointing the dock at a hidden frame before every release. Now a target that is an AceGUI widget's frame is let go of automatically on that widget's release (throughOnWidgetRelease), so no consumer has to do anything. Re-pointingDockWindowat a new target also now leaves the old target's list, instead of staying on it and being skipped.
Tests
- Whole suite: 992 passed, 0 failed; coverage 5961/5961 executable lines (100%) over every shipped
file, the new
LibAceGUIWidgets-ClickBinding.luaincluded. Tests/copyfield_spec.lua(10 examples): a fresh construction through its whole lifecycle under the harness's "no SetText without a font" rule, text, label heights, the read-only guard, select-all on focus and click, Enter / Escape, no glyph without the file, the glyph's place and tint, the glyph following the scale, and a clean return from the pool.Tests/dock_spec.luagains 5 examples:Undockpins and stops following, a later re-dock works, re-pointing leaves the old list,UndockWindow, and the automatic let-go on a pooled target's release, including the next owner showing that frame.Tests/dialog_spec.luagains 11 examples: the glyph drawn, tinted and gone from the next plain prompt; no glyph when the file cannot be set; the TGA's shape; the defaults and the token, theoptsoverrides, a user edit put back, re-selection on click and focus, close on OK / Enter / Escape withonHideonce each, a later plain prompt left writable with only one hook installed, a 500-character line with the dialog size unchanged, and nil / number text.Tests/clickbinding_spec.lua(19 examples): the string's canonical order, parsing and refusals, the display text, the exact-match rule, capture on every button, the refusal of an unmodified click, Escape and other keys, combat, Default, silentSetValue, disabled, the label, the UI scale, hide-while-armed, and a clean return from the pool.- The
Tests/wowapiharness pin moves to8e053f2. From9c6c874its library manifest loads the newLibAceGUIWidgets-ClickBinding.lua(WoWAPITesting inbox00eb7ea8); from9c80f46a FontString with no font refusesSetTextwith "Font not set", as the client does (inbox6ceaea3f). - That rule turned two
formdialog_specexamples red: the harness has noGameFontRedSmall, which the client does define, so the error line had no font offline. Harness8e053f2defines it (inbox798ae268), and "is drawn in the red font" now asserts the font exists, because with it missing both sides of its comparison were nil and it passed on nothing. - Not verified in a client: that a mouse-up re-select beats the click's own cursor placement, and that
RegisterForClicks("AnyUp")delivers Middle / Button4 / Button5 to OnClick on every flavour.
[v0.2.7] -- a checkbox, a note and a paragraph in the form prompt, and the cell a list shows an entry in
Compatibility -- MINOR 36 -> 37
New: RowList:CellFrameFor, the kind = "check" form row, row.note, frame:SetNote,
frame.notes, opts.body, frame:SetBody and frame.body. Feature-detect on if rl.CellFrameFor then or
LibStub.minors["LibAceGUIWidgets-1.0"] >= 37. Nothing existing changes: a form dialog that uses
neither a check row nor a note is built with the same points and the same height. No widget type
Version moves.
New
RowList:CellFrameFor(target, columnKey)(Dibs inbox55767d11). It returns the frame on screen that showstarget's cell in that column: the mouse overlay for a column withonCellEnter,onCellLeaveoronCellClick, else the cell itself.targetis an entry, a predicate, or an index in the drawn order. It is nil for an entry not in the view or scrolled off the page, an unknown column, a heading row, a cell the row is not showing, and a detached list. It is read-only: no scroll, no repaint. It answers for the current columns, so it followsSetColumns. Dibs' spec found the row by readingrl.rowsandrl:_getSortedData(); this replaces both.CreateFormDialog: a checkbox row and a note under a row (Dibs inbox8d5165f7), so Dibs can drop its two hand-built officer dialogs.kind = "check"puts aUICheckButtonTemplatebox where the edit box goes, with the row's label to its right, wrapping.fields[i]is the box:GetValue()answers a boolean,SetValue(bool)is silent,row.valuesets the start,row.onChanged(value, dialog)hears a click, androw.tipTitle/row.tipBodygo on the box. The row is the usual 26 tall and follows the scale.row.note = "text"draws a small greyGameFontDisableSmallline under that row's field, wrapping, and the dialog grows by its height.frame:SetNote(i, text or nil)adds, changes or removes it after build; a removed note leaves no gap.frame.notes[i]is the FontString.opts.bodyandframe:SetBody(text or nil)(Dibs inbox33a2bbce) put a paragraph under the title and hint, above the rows, wrapped to the dialog's width on the scaledGameFontHighlightSmall. Setting it again, afterShowincluded, replaces the text and the height follows; nil or "" hides it and it takes no space.frame.bodyis the FontString. This is for Dibs' Absences dialog, whose text is rebuilt on every open.- The rows are now re-hung by one routine shared by
SetHint,SetNoteandSetBody, so changing one keeps the space of the others.
Tests
Tests/rowlist_minor37_spec.lua(9 examples) checks each answer by what the frame does or shows: entering the returned overlay reaches the column'sonCellEnterwith the entry, and a text cell shows that entry's text.Tests/formdialog_minor37_spec.lua(13 examples) pins the unchanged layout of a dialog that uses none of the additions, then the check row, the note and the body, including a note on the last row, a body with no rows, and aSetHintafter each.
Dev tooling
wow-version-replication.ps1no longer deletes the replicas of a file the source still has (TOGProfessionMaster peer review0f76f963). An editor that saves by replacing the file raises aDeletedevent for a moment before the new file lands; the watcher removed the file from every other WoW install on that event. ADeletedfor a path that still exists is now treated asChangedand copied. Dev-only: the script is not in the package. Not reproduced against a real replacing save.
[v0.2.6] -- the suite orange by default, and nothing left on a released widget
Compatibility -- MINOR 35 -> 36
New methods: W:OnWidgetRelease, W:WidgetFrameScripts, W:RestoreWidgetFrameScripts,
W:AttachRawFrames, W:DetachRawFrames, W:AttachWidgetTooltip, RowList:CellWidth,
RowList:GetScrollOffset, RowList:SetScrollOffset, W:IsMenuOpenFor, W:IsInputFocusedIn,
W:OnMenuClosed, W:Flash, W:StopFlash, W:FlashScreen, W:AnchorPopup, W:ClampWindow,
W:SetWindowProfile, W:GetWindowProfile, W:SetWindowOpacity, W:GetWindowOpacity,
W:SetWindowScale, W:GetWindowScale, W:EscapeLayer, W:DropEscapeLayer, and
IsExpanded on an expandable list, W:CreateStepper and the LAGW-Stepper widget type at Version 1,
W:CreateSecureActionButton, W:NewDockLayout.
Feature-detect on
if W.AttachWidgetTooltip then or LibStub.minors["LibAceGUIWidgets-1.0"] >= 36. Nothing existing
changes signature; ScrollToEntry gains an optional second argument. New RowList options:
hoverHighlight, sortCycle, col.sortNone, searchText, searchTokens, onScroll, isHeader,
indentStep, indentKey, headerFont, reorderable + onReorder, fitContent +
onHeightChanged, col.show / col.text / col.tip on a button column and col.iconTexCoord, all
off unless given. RowList now reads three entry fields: _header, _expanded (on a header row) and
_indent; an entry that already carried a truthy _header for its own reasons would now draw as a
heading. New field: W.DEFAULT_ACCENT. One visible change for existing calls: the
default accent is now the suite orange ffFF8000, not gold ffFFD100. An addon that never called
Configure{ accent } now draws orange header underlines, selection tints, BrandTabGroup bands and
W:Brand text. An addon that sets its own accent sees no change, and one that sets ffFF8000 stays
silent, because the same colour is never an accentClash. No consumer has to change code.
ClearFrame, GroupFrame and TLabel move 30 -> 31 together, because ClearFrame:SetInfoTooltip
now takes { minWidth } and AceGUI keeps the FIRST registration at a tie, so an older copy loaded
earlier would otherwise keep a ClearFrame that ignores it. LAGW-DatePicker stays at 3 and
LAGW-SearchBox at 1.
New
- Pool-safe additions to AceGUI widgets, undone on Release (TOGProfessionMaster inbox
efe864ec,afb62bf8). AceGUI pools each widget and its frames for every addon in the session. ItsReleaseemptieswidget.eventsbut never touches a raw script, a frame flag, or a raw frame parented into the widget, so all three followed the pooled frame into the next addon's window. TOGProfessionMaster carried six hand-rolled undo variants for this.W:OnWidgetRelease(widget, key, fn)runsfn(widget)once when AceGUI releases the widget, then forgets it. It chains onto the widget'sOnReleasemethod, the wayPersistWindowdoes, so the consumer'sSetCallback("OnRelease")slot stays free. A second call with the same key replaces the hook,falseremoves it, hooks run newest first, and one that raises is reported throughgeterrorhandlerwithout stopping the rest.W:WidgetFrameScripts(widget, { OnMouseDown = fn, ... } [, target])sets raw scripts onwidget.frame, on a named sub-frame ("editbox") or on a given frame. On Release each event gets back the script from before the first call, not nil, so a constructor's own dispatcher survives and the next owner'sSetCallbackstill fires. A second call replaces the handler and still restores the original.falserestores one event now, andW:RestoreWidgetFrameScripts(widget)restores everything now.W:AttachRawFrames(widget, frameOrList)records raw frames parented into the widget. On Release each is hidden, loses its points and goes back toUIParent. The frames are not destroyed, so the consumer's own pool keeps them.W:DetachRawFrames(widget)does it now.W:AttachWidgetTooltip(widget, title, body [, opts])isAttachTooltipfor an AceGUI widget.AttachTooltip(widget.frame, ...)hooks withHookScript, which cannot be removed, and leaves its text and the disabled-hover flag on the pooled frame. This one answers onwidget.frame, including the EditBox and Dropdown label area (whose mouse it enables), and on the widget'seditbox,buttonandslider. Each frame's existing handler runs first, so the consumer'sSetCallback("OnEnter")still fires. It draws throughShowTooltipwithopts.owner,opts.reasonandopts.titleColor, leaves throughHideTooltip, and keeps a disabled button's hover. A second call updates the text. On Release the scripts, the mouse flag and the motion flag all go back to their prior values.
- Tooltip
opts.minWidth, andW:AnchorPopup(TOGProfessionMaster inboxc8d6892f).ShowTooltip,AttachTooltipandAttachWidgetTooltiptakeminWidth(scale-1.0 px), so several paragraphs of help are not drawn as a tall narrow column. GameTooltip is shared with the whole UI and nothing resetsSetMinimumWidthby itself, so the previous value is saved as both halvesGetMinimumWidthreturns (width, forced) and put back when the tooltip hides, through a hook on itsOnHide, and before the next showing.DressBottomRowtakes it per icon astipMinWidth, and a ClearFrame'sSetInfoTooltip(tooltip, { minWidth })takes it too.AnchorPopup(popup, source, { gap })places a floating panel under its source, or over it when there is no room below, slides it left to stay on screen and clamps it. Both frames are measured in UIParent units, so a scaled window is placed right. W:Flash,W:StopFlashandW:FlashScreen, a finite attention flash (TOGProfessionMaster inbox759b8287).Breatheloops until stopped;Flash(region, { times, seconds, low, onDone })dips the alphatimestimes (3 by default, 0.25s each way, down to 0) and stops by itself with the alpha back at 1. It is a BOUNCE group counting its ownOnLoopcalls that turn back toFORWARD, one per dip, which is right whether the client callsOnLoopat each end of a pass or once per cycle (the docs do not say which).OnLoopis a script the Classic Era client uses itself (LFGFrame.lua). A second call restarts it on the same group.FlashScreen({ color, times, seconds })flashes a full-screen edge glow on one shared frame atFULLSCREEN_DIALOGstrata that never takes the mouse, then hides it. The glow is four flat-colour bars rather than a client texture, so there is no art to go missing on a flavour. The colour defaults to the accent; the hex parsing is now one local shared withAccentRGB.W:IsMenuOpenFor,W:IsInputFocusedInandW:OnMenuClosed(TOGProfessionMaster inbox8f507ee7), for a consumer that defers a redraw while the player has one of its menus open or is typing. Both queries walk UP from the anchor or the focused box to the window asked about, never down its tree, so they are cheap on every event. Another addon's open menu answers false; submenus are the same stack and count. The focus query asks the client'sGetCurrentKeyBoardFocuswhen it exists. That function has no call site in this box's Classic Era or Classic source trees, so the library also tracks focus on every edit box it builds (menu search, dialog, search box, form field, date picker) and falls back to that.OnMenuClosed(owner, fn)is one-shot per owner and also fires when another anchor's menu takes over the stack.- RowList
opts.hoverHighlight(TOGProfessionMaster inbox0c477b45) lights the row under the pointer.trueuses the stock quest-log title highlight, ADD-blended; a string is a texture path;{ r, g, b, a }is a flat colour. It is drawn onBORDERone sublevel under the selection tint, so it covers the banding and never hides the selection. Only one frame holds the mouse at a time, so the row gets itsOnLeavewhen the pointer moves onto one of its own mouse-enabled children (not yet seen in the client). Every child that takes the mouse (cell overlays, button cells, checkboxes, expanders, action icons) lights the row on enter, and a leave only puts it out whenrow:IsMouseOver()says the pointer is off the row entirely. A scroll moves entries under a pointer that never moved and fires no enter or leave, so each populate re-decides the highlight fromIsMouseOver. A row showing no entry never lights. It is opt-in because the row has to take the mouse. - RowList three-state header sort (TOGProfessionMaster inbox
1013eeea). Withopts.sortCycle = "three", orcol.sortNone = trueon one column, the third click on the sorted column clears the sort, so a list whose unsorted order means something (a category tree) can get back to it. AsortDescDefaultcolumn cycles desc -> asc -> none. The clear writes the same state asSetSort(nil), andonSortChangednow reports a cleared sort as(nil, nil, rl). Lists stay two-state unless they ask. col.formatgets the cell's width and a measure (TOGProfessionMaster inboxc5476af2), for a column that fits as many names as the space allows and then "+N". When the text is being drawn the formatter is called asformat(value, entry, width, measure):widthis the cell's pixel width at the current scale, andmeasure(text)is that text's rendered width in the column's own font. A parent resize repaints, so the formatter re-fits. The filter labels, the search and theautoFitmeasure pass neither argument, so they see the full text. Newrl:CellWidth(key)answers any column's width, the auto column included once a row has been laid out (nil before that);ColumnWidthkeeps answering nil for the auto column so a consumer summing fixed widths is unchanged. Two-argument formatters are unaffected.- RowList search over hidden text, by words (TOGProfessionMaster inbox
85482ed8), so searching "stamina" finds every recipe that gives stamina.opts.searchText(entry)returns extra text the search reads, colour escapes stripped. With it, or withopts.searchTokens = true,SetSearchmatches byW:SearchMatch's rule: every word of the query somewhere in the shown cells and the hidden text, any order, any case. The text is built once per entry and kept untilSetData,RefreshorSetColumns, sosearchTextdoes not run per keystroke. A list with neither keeps the plain-substring search it had. The search still ANDs with the column filters. - RowList scroll position, public (TOGProfessionMaster inbox
bdf1159c), so a tab reopens where the player left it.rl:GetScrollOffset()andrl:SetScrollOffset(n)work in rows; the setter clamps like every other scroll and repaints once.opts.onScroll(rl, offset)is called from the repaint whenever the offset differs from the last one reported, whatever moved it: the wheel, the scrollbar, the setter,ScrollToEntry, aSetDataor search that went back to the top, a clamp. It is not coalesced across a frame, as the request asked, because that needs a timer standing in for the end of the frame.rl:ScrollToEntry(target, opts)takesifNeeded(no scroll when the entry is already on screen; from below, it comes in at the bottom) andcontext = n(rows left beyond it, capped at one less than the page, then clamped at the list's ends). SetSearchnow repaints without dropping the per-entry search text built for the previous query.- RowList group header rows and indent (TOGProfessionMaster inbox
57b66577). A row is a heading whenopts.isHeader(entry)answers truthy or, without it, whenentry._headeris: a string is the heading's text,trueuses the indent column's text. A heading row hides every cell, cell overlay, strike line and action icon, swaps its banding for an accent strip at 0.10, and draws one line of text across the row in the accent colour (opts.headerFont, defaultGameFontNormal).entry._expandedtrue or false adds the client's own craft-window minus or plus in front; nil adds nothing. A click on a heading reachesonRowClickwith its entry, which is how a consumer toggles it; no selection or scroll predicate treats headings specially. A header sort sorts each heading's rows UNDER it and never moves a heading: rows before the first heading are their own group, and groups are flat, so a tree that wants nested groups sorted usesexternalSort. Column filters never judge a heading and its cells add no filter values orautoFitwidth. With a filter or search active, a heading stays while any row of its group survives, or when the search matches the heading's own text.entry._indentis a level: level xopts.indentStep(scale-1.0 px, default 12) moves the indent column's text right while its right edge stays, so it still clips at the column. The indent column isopts.indentKey's, else the first column that shows text; a heading is indented too. A formatter on that column is handed the narrower width. A pooled frame is re-laid only when the entry it shows changes level. - RowList button cells that hide per row (TOGProfessionMaster inbox
337bb7d3). On abuttoncolumn,col.show(entry)false hides the button on that row: no mouse, a blank cell, and an empty label for the search and filter.col.text(entry)is the label per row, used as given (noformat, no class tint).col.tip(entry, button)returnstitle, body[, reason]and is read on hover, so text about how old the data is was true when the player looked; the reason is the red last line, as onCreateExplainedButton, andtipwins overcellTip. Every one is re-read on each populate, so a sibling addon that loads late needs onlyrl:Refresh()._readonlystill disables and does not hide. - RowList
col.iconTexCoordandopts.fitContent(TOGProfessionMaster inbox8680f2cc).iconTexCoord = { l, r, t, b }, ortruefor the usual 0.07-0.93 crop ofInterface\Iconsart, is applied on every populate. A column without it sets the full texture back, because the holder is pooled and may have served a cropping column.fitContent = trueor{ maxRows = n }sets the PARENT's height to the header bar plus one scaled row height per drawn row, up tomaxRows, after which the list scrolls. It re-fits on every refresh, so a scale change, a search or new data resizes it.onHeightChanged(rl, h)hears each new height. No rows gives the header's height (0 without one), and that is not reported as an empty-render fault. The parent has to be anchored by its top alone: a frame anchored top and bottom ignoresSetHeight. - RowList drag to reorder (TOGProfessionMaster inbox
973a1ab1). Withopts.reorderableandopts.onReorder(fromIndex, toIndex, entry, rl), dragging a row lifts a copy of it atTOOLTIPstrata that follows the pointer, dims the row itself to 0.4, and draws a 2px accent line (scaled) at the row boundary nearest the pointer. Dropping callsonReorderwith the entry's index indataand the index it should have after the move. The list moves nothing itself, so the consumer reorders its model and callsSetData. The client's own drag threshold keeps a click a click, and a right-click still reachesonRowClick. Held on the first or last drawn row, the list scrolls one row every 0.1s. Escape cancels out of combat: the lifted row takes the keyboard and lets every other key through.EnableKeyboardis protected andSetPropagateKeyboardInputrestricted (Classic EraSimpleFrameAPIDocumentation.lua), so in combat the keyboard is not taken, and a drop off the list is the way to cancel. No call is made for a drop off the list, a drop in the slot it came from, or a drop after the data changed under the drag. A drag does not start while a header sort, a column filter or a search is active, because the drawn order must be the model's order. AnexternalSortlist drawsdataas given, so it can be dragged.Detachcancels a drag in progress. W:ClampWindowandW:SetWindowProfile(TOGProfessionMaster inbox72e8dd4b).ClampWindowcaps a window's size to the screen and moves it fully onto it, in the window's own units, and writes the result intowidget.status. It changes only what is off the screen, so the floor is undercut only when the screen itself is smaller.SetWindowProfile(widget, key, { width, height, resizable, minWidth, minHeight })switches a persisted window between named sizes. A locked profile snaps to its size and turns resizing off:SetEnabled(false)on a ClearFrame's handle (grips hidden),EnableResize(false)on AceGUI'sFrame, andSetResizable(false)on both so a drag is refused. A resizable profile restores its own size fromsaved.profiles[key](else its defaults), raised to its own floor, and that floor follows the scale. Leaving a resizable profile saves the frame's size under its key, and so does releasing the window while that profile is active (TOGProfessionMaster inbox7481d3a5): a window dragged and then closed on a resizable tab used to reopen at the size saved at the last tab switch.ForgetWindowsaves it before the record goes and before the widget's ownOnReleaseruns, through the same helper as the switch-away save. A locked size only ever lands in the table's ordinarywidth/height, so it cannot reach a resizable profile's save. Position is shared, and every switch ends inClampWindow.ForgetWindow, and so Release, turns resizing back to how it was before the first profile.W:SetWindowOpacity/W:GetWindowOpacity(TOGProfessionMaster inboxae090bd5) fade a window's background fills: its own backdrop, and the pane backdrop of every container inside it (border,treeframe, and the frame a container'scontentsits in, which is the only way to reach InlineGroup's). Text, icons, buttons, borders andSetAlphaare untouched. The fade is always from the stock colour, so it is idempotent and 1 is stock. Each widget remembers the frames it owns and restores them on its own Release, so a pane released by a tab switch goes back to the pool clean while the window stays open. A pane drawn after the call is not reached until the consumer calls it again.W:SetWindowScale/W:GetWindowScale(TOGProfessionMaster inboxe5e1586a) set one window'sframe:SetScale, clamped to 0.5-1.5 and separate from the session-wideW:SetScale(0.8-2.0), which still multiplies inside it. The top-left corner stays on the same screen point (anchor offsets are in the window's own units, so they are multiplied by old/new), then the window is clamped. It is saved aswindowScalein a persisted window's table, andPersistWindowre-applies it before the position, which was written in those units. Release puts the scale back to 1.W:EscapeLayer(window, childFrames)/W:DropEscapeLayer(window)(TOGProfessionMaster inboxd8f15682): Escape closes the most recently shown child first and the window on the press after the last child. Read in the Classic Era source (UIParentPanelManager.lua:1041),CloseSpecialWindowswalksUISpecialFrameswithpairsand hides every entry thatIsShown(), so a window and its popup on that list close together on one press. The classic_anniversary and classic trees have the same function at the same line. So the window is represented by one proxy frame, shown only while the window is, and the window and its children come off the list while the layer lasts. The proxy'sOnHidecloses the newest shown child and shows itself again, or hides the window when none is left. A hide the proxy did not get from the walk (its window or UIParent hidden) leaves its ownIsShownflag true and is ignored. Closing the window hides every shown child, which runs theirOnHide, and hides the proxy so a closed window cannot swallow the next press. It is plain Lua hiding plain frames, so it works in combat. The hooks go on the window frame once and read the layer at event time. An AceGUI window's layer ends on Release, and the names come back onto the list. Known cost:CloseWindowsalso callsCloseSpecialWindows(line 1087), so loss of control and other panel opens also close only the newest child of a layered window.CreateExpandableListat any depth (TOGProfessionMaster inbox7ab1cb56), for a profession -> specialisation -> member tree. A child may carrychildrenof its own, and the layout is now one recursive walk. A node with children is a group at any depth (header style, a toggle, indentedindentper level). A node without is a leaf: header style at depth 0 and child style below, which is exactly the old two-level picture. Expand state is keyed by the path of keys, so two "Main" specs under two professions keep their own state. A top-level group's path is its own key, as its state key was before.defaultExpandedandSetAllExpandedreach every depth, andlist:IsExpanded(key, ...)reads a path. Any node takescolor,onClick(node, mouseButton, rowFrame)(a group toggles and then calls it) andtooltip, a{ title, body }pair or a function read on hover. Every script is set on each layout from the node drawn there, and a row with no click and no tooltip takes no mouse, as before. A pooled group row puts its font's own colour back when the next node has none. A toggle re-lays the pooled rows; no frame is rebuilt.W:CreateStepperand theLAGW-Stepperwidget type (TOGProfessionMaster inboxea8db903):[-] [qty] [+] [MAX]. Typing keeps digits only as they are typed, and commits on Enter or focus loss, clamped tominandmax(). An emptied box and Escape revert.-and+step bystepand areCreateExplainedButtons, so at a bound they grey out and the tooltip's red last line says which bound. MAX setsmax(), read on every click; a max of 0 greys it.SetValueis silent, and a player's change callsonValueChanged(value, stepper)once, only when the value moved. Sizes are scale-1.0; the layout computes its widths from the scale, so it does not depend on which scale listener runs first.LAGW-Stepper(Version 1) wraps it and firesOnValueChanged(value). It builds MAX once and shows it only afterSetMaxButton(true), and is reset on acquire. Not delivered: holding a button to repeat, which needs a per-frame tick while held.CreateFormDialog: dropdown rows, item rows,SetHintandanchorTo(TOGProfessionMaster inbox6cf3b4e4), all opt-in; existing rows build exactly as before.- A
kind = "dropdown"row puts aCreateDropdownBoxwhere the edit box goes.items(query)returns{ text, value }pairs, built fresh on each open with the current one ticked. Choosing one callsonChanged(value, dialog)only when it changed.fields[i]hasGetValue()and a silentSetValue(value[, text]). - A
kind = "item"row shows the item's icon and link from the label to the right edge. Hover draws the item's own tooltip;SetHyperlinkis given the bareitem:part. A click goes to the client'sHandleModifiedItemClick.fields[i]:SetItem(link[, icon])changes it. The icon isrow.icon, elseC_Item.GetItemIconByID(link), which the Classic Era, Anniversary and Classic docs all declare. frame:SetHint(text or nil)adds, changes or removes the hint after build. The first row (or the error line) re-hangs under whichever of title and hint is showing, and the height is recomputed unless the caller gave one. OK/Cancel are anchored to the bottom, so they stay on the frame.opts.anchorTo(withpoint,relPoint,x,y; default TOPLEFT to the owner's TOPRIGHT, 4px right) andframe:SetAnchor(owner, ...)place the dialog by its owner on every show, clamped by the floating frame. A laterSetAnchorre-points a named dialog to whichever row opened it. An owner that is not visible leaves the dialog where it was.
- A
W:CreateSecureActionButton(TOGProfessionMaster inbox3d04549b), a secure button with combat guards. It registers forAnyUpandAnyDown: read in Classic EraSecureTemplates.lua:789-823,SecureActionButton_OnClickacts on exactly one of the two, chosen byuseOnKeyDownor theActionButtonUseKeyDownCVar, so it fires once under either setting.SetAction(attrs)and the button's ownSetPoint,ClearAllPoints,SetAllPoints,SetSize,SetWidth,SetHeight,Show,Hide,SetShown,SetEnabled,Enable,DisableandSetParent(wrapped on the instance) run now, or are queued in combat. One session-wide queue runs once, in order, onPLAYER_REGEN_ENABLED. A scale change in combat queues the resize as well. The factory returns nil in combat. The tooltip isCreateExplainedButton's and works while disabled;preClick/postClickare the consumer's. Not safe to pool:SetParentisIsProtectedFunctionin all three Classic trees, and a frame that parents a secure button is itself protected (Warcraft Wiki; web, not client source). So it belongs in a frame the consumer owns, never in a pooled AceGUI frame.W:NewDockLayout(TOGProfessionMaster inbox2f9efc9e) splits a container intotop,right,bottomandcenter(aliasleft). The panes are four raw frames the library owns, anchored into the container'scontent, so a live resize moves them through their anchors. No method of the pooled widget is replaced and none of its scripts is hooked. The sizes are re-decided by one extra frame of the library's over the content (itsOnSizeChanged) and by a scale listener.top,rightand a fixedbottomare scale-1.0; an"auto"bottom is the pixel height given toSetBottomHeight(px). The bottom spans the full width. The centre never collapses: the top stops one pixel short of the height, the bottom takes only what is left under it, and the right leaves the centreminCenter.SetPaneShown(name, bool)gives a pane's space to the centre. An AceGUI container's Release releases the dock (every frame hidden and handed to UIParent, the scale listener dropped); a raw frame usesdock:Release().
Changed
- The default accent is the suite orange (TOGProfessionMaster inbox
8f931ba9). The operator's words: "have the library change it's color to our orange, not sure why it's gold." Gold is Blizzard'sNORMAL_FONT_COLOR, not the suite's brand, and the one-setter rule stopped a consumer from fixing it locally without becoming a second setter.DEFAULT_ACCENTis now one literal, read by the config table,GetAccentandAccentRGB.AccentRGB's per-channel fallback for a malformed accent is now1, 0.5, 0to match. Colours the library draws as the client stay onNORMAL_FONT_COLOR: a tooltip header, the cooldown caption, the DatePicker label. - An upgrade in a live session moves the old default too.
lib.configsurvives an upgrade from an older copy, so a session where an addon with MINOR 35 or older loaded first would have stayed gold. On load, a config still holding the untouched gold default moves to orange. A consumer that set gold throughSetAccentorConfigure(recorded since MINOR 33) keeps it. PersistWindowclamps the restored window to the screen. A table saved on a larger screen or at a smaller UI scale used to restore a window partly or wholly off the screen, or bigger than it. It is now capped and moved on, and the table is updated to match. A window that fits is untouched.- README: "Auditing a consumer", a table from what a hand-rolled copy looks like in an addon's code
to the library call that replaces it, for every window, list, control and look-and-feel feature
with the MINOR that brought it. A feature was usually extracted FROM a consumer, and its original
often survives there (ClassicCalendar's World Buff docking still carries its own copy of what
became
DockWindow). A per-version list does not show an auditor any of that. - RowList counts its visible rows with a small allowance for rounding. At a scale where the row
height is not a whole number, a parent exactly n rows tall could divide to just under n and show
one row fewer.
fitContentsets exactly such heights, so the count now adds 1e-6 before flooring. - Peer Review's verdicts on this batch's self-audit (audit thread
231f14e7):- The stepper's
Refresh()andSetBoundsno longer rewrite the box while it has focus. A consumer refreshing onBAG_UPDATEused to erase a half-typed number. The typed number is clamped against the new bounds when it commits. A click,SetValue, Escape and show still rewrite it. - The stepper snaps to the nearest multiple of
stepcounted frommin, rather than to a whole number, so a fractional step (0.5) works and a decimal point can be typed for one. An integer step frommin0 lands exactly where it did. A step of zero or less falls back to 1 instead of dividing by zero.SetStepre-snaps the current value onto the new grid, asSetBoundsre-clamps it, so a click from a value set under the old step moves by exactly the new one. SetWindowScalere-applies the window's own anchors with the offsets times old/new, instead of re-placing it TOP/LEFT onUIParent. A docked or consumer-anchored window stays attached, and its anchored point stays on the same screen spot.- README: a dock's content goes in the panes only, since AceGUI children of the same container are laid out over them.
- The secure button's guard shadows widget methods with Lua fields. The Classic Era secure click path and the button's templates were read for a secure caller of any of them, and none exists; the reasoning, with line numbers, is in the code. Not verified in a client.
- The stepper's
Tests
Tests/poolsafe_spec.lua(20 examples) drives the four helpers through real AceGUI-3.0 and its stock Button, EditBox and SimpleGroup. Every "the next owner finds nothing" case re-acquires the released widget and asserts the pool handed back the same table, so none of them can pass on a fresh widget.Tests/tipwidth_spec.lua(9 examples) pins the tooltip floor through the harness's modelledSetMinimumWidth/GetMinimumWidth, forced flag included, and the popup placement at the screen's edges.Tests/flash_spec.lua(5 examples) drives each pass by calling the group's ownOnLoop, since the frame model builds animation groups but does not run them.Tests/interaction_spec.lua(8 examples) opens real menus on anchors nested inside a window and drives the focus query both with and without the client'sGetCurrentKeyBoardFocus.Tests/rowlisthover_spec.lua(10 examples) fires enter and leave in the client's order (old frame's leave, then the new frame's enter) with each row'sIsMouseOveranswering for the pointer. Each child is also entered from a dark row, so the child's own enter is proven to light it.Tests/rowlist_minor36_spec.luaholds the other RowList requests of this release, starting with the sort cycle (7 examples), the formatter width (4), the word search (6) and the scroll position (6).Tests/rowlist_groups_spec.lua(47 examples) covers headings and indent, the button cells, the icon crop,fitContentand the drag. It drives the drag by placing the harness cursor and calling the row'sOnDragStart/OnDragStopand the lifted row'sOnUpdate. The harness models neither keyboard getter, so the Escape tests recordEnableKeyboardandSetPropagateKeyboardInputcalls. None of this has been tried in the client.Tests/windowprofile_spec.lua(25 examples) drives the clamp, the profiles, the opacity and the window scale through real AceGUI-3.0ClearFrame,Frame,TabGroupandInlineGroup, and re-acquires the released widget where the point is that the next owner finds nothing.Tests/escapelayer_spec.lua(9 examples) presses Escape through a copy of the client's ownCloseSpecialWindowsloop, so the layer is tested against the walk it exists to work around.Tests/expandable_depth_spec.lua(7 examples) opens a three-level tree through the rows' own scripts, and the two existing expandable-list examples still pass unchanged.Tests/stepper_spec.lua(16 examples) drives the stepper through its own scripts and the widget type through real AceGUI-3.0, including a re-acquire from the pool.Tests/formdialog_rows_spec.lua(9 examples) drives the dropdown through a captured menu, the item row through stubbedC_ItemandHandleModifiedItemClick, and the hint and anchor through the frame model's anchor points.Tests/securebutton_spec.lua(9 examples) swapsInCombatLockdownto enter and leave combat and fires the queue's ownPLAYER_REGEN_ENABLEDhandler.Tests/docklayout_spec.lua(7 examples) reads the panes' anchors, and checks that the dock replaces no method on a real AceGUISimpleGroupand lets go on its Release.- The v0.1.11, v0.1.12 and v0.2.0 sections moved whole to
CHANGELOG_ARCHIVE.mdto keep this file under 120,000 characters. Tests/widgets_spec.luapins the new default,AccentRGB's fallback, and the upgrade path: a simulated MINOR 35 table is reloaded over and moves to orange unless gold was set explicitly.- The three specs that reset the accent after a test now reset it to
W.DEFAULT_ACCENTrather than to a gold literal. The RowList selection-tint test paints with green, so it still proves the tint is read at paint time now that orange is the default. - Two weak assertions rewritten (Writ inbox
943be786). Each wasis_nilon a literal key, which still passes if the key is misspelt or renamed.bootstrap_spec.luanow checksW:TriggerVersionCheck()returns false instead of readingW._vc.widgets_spec.luachecks that every config keyConfiguredid not name is unchanged and that none was added, instead ofW.config.classColors. wow-version-replication.ps1(dev only, not packaged) also copies into_classic_beta_, where the WoW Forever client installs.
[v0.2.5] -- a released window takes its floor with it, one search box, and a filter strip that lines up
Compatibility -- MINOR 34 -> 35
New methods: W:ForgetWindow(widget), W:NewStrip, W:StripAdd, W:StripRowWidth,
W:FitCheckBox. New widget type: LAGW-SearchBox at Version 1. New AceGUI layout: LAGW-Strip.
Feature-detect on type(W.ForgetWindow) == "function", if W.NewStrip then,
AceGUI:GetWidgetVersion("LAGW-SearchBox"), or LibStub.minors["LibAceGUIWidgets-1.0"] >= 35. One behaviour changes for existing calls:
PersistWindow wraps the widget's OnRelease, so releasing a persisted window now forgets it. The
widget type's own OnRelease still runs. The three widget types stay at Version 30 and
LAGW-DatePicker at 3. The TOC is unchanged.
New
LAGW-SearchBox, the search box as an AceGUI widget (Questbook inbox293177a6).CreateSearchBoxreturns a raw frame, so TOGBankClassic (TOGBankSearchBox) and Questbook (QuestbookSearchBox, a copy of TOGBank's) each wrapped it as their own AceGUI type to put it in a Flow row. This is that wrapper, with every method either copy had, so both can switch without losing one.SetTextis silent. The player's typing and the clear-X fireOnTextChanged(text), and the clear-X fires from its ownOnClickbecause the template empties the box with a programmaticSetText("").SetPlaceholder(nil)restores "Search", andOnEnter/OnLeave/OnEnterPressedpass through. The box is released empty. Its 24px height follows the UI scale, as the box's font already did. It lives in the core file, not a satellite, so the harness manifest did not have to change. Pinned byTests/searchboxwidget_spec.lua.LAGW-Strip, a filter-strip layout (Questbook inboxc2be4737), from Questbook'sFilterStrip.lua. AceGUI's Flow cannot line a strip up: a labelled EditBox puts its box 25px down a 44px frame, a labelled Dropdown 14px down a 40px one, and buttons, checkboxes and the search box are 24px with no label. Controls now go in unlabelled, andW:StripAdd(strip, control, caption)adds the caption as a separate goldTLabel. The layout puts every caption on one line and centres every control on one line under it, with one gap between each pair. A row that runs out of width wraps, and a hidden control takes no space and hides its caption.W:NewStrip()makes the group,W:StripRowWidth(strip)reports the widest row for a window floor, andW:FitCheckBox(cb)sizes a checkbox to its label. Two differences from Questbook's copy:- The gap, the row gap and the caption line follow the UI scale. A row is at least 26px and grows to
its tallest control, so a scaled
LAGW-SearchBoxstill centres. The strip re-lays itself on a scale change. - State lives in the group's
userdata, which AceGUI empties on release, so a pooled group cannot read its last owner's captions.
LAGW-SearchBoxnow re-runs its parent's layout when the scale changes its height, since AceGUI'sSetHeightdoes not. Pinned byTests/strip_spec.lua.- The gap, the row gap and the caption line follow the UI scale. A row is at least 26px and grows to
its tallest control, so a scaled
Fixed
- A released persisted window no longer resizes the next addon's window (TOGBankClassic inbox
3d96003a). AceGUI's widget pool is shared by every addon in the session.PersistWindowleft three things on the pooled widget and frame when it was released: its record, that record's scale listener, and the resize floor it had raised. The next scale change then raised whatever window the pool had handed that frame to. TOGBank measured a released Guild Bank window growing from 900 to 1568 wide onSetScale(2). Release now drops the record and its listener and restores the bounds captured before the firstPersistWindow. AClearFrame's resize handle is restored, not destroyed, because it is built once in the constructor and a destroyed one leaves the pooled frame with no grips.W:ForgetWindow(widget)does the same without a release. Pinned byTests/forgetwindow_spec.lua, which includes TOGBank's contract check.
Tests and docs
- 714 passed, 0 failed, and every executable line of every shipped file is reached: 4050/4050
across
LibAceGUIWidgets-1.0.lua,-Resize.lua,-RowList.lua,-DatePicker.luaandLibAceGUIWidgets.lua. The docs had claimed that for the whole library while it held only for the core file and RowList; the gate had been run on the core file alone. The missing lines:ForgetWindow'sSetMinResizebranch, for clients older thanSetResizeBounds.- The resize handle restoring a saved position (
ApplyStatuswith bothleftandtop). SetRangepulling an open calendar's month into the new range.- The weekday-name backfill for a date popup built by a pre-MINOR-30 copy.
- The whole
LibAceGUIWidgets.luabootstrap, now pinned byTests/bootstrap_spec.lua.
README.mdwas checked against every public function, constant, option and widget method in the code, and now documents what it lacked:- The named cooldowns (MINOR 24) and the version helpers.
- The ClearFrame, GroupFrame and TLabel methods and callbacks, and the DatePicker's
OpenCalendar/CloseCalendar/IsCalendarOpen/GetTextandW:DatePickerPopup(). - The
W.DatePickerhelpersymd,isLeap,stepMonth,weekStartandGLYPH. - The options of
OpenMenu(width,search,itemsFor),CreateDropdownBox,ShowDialog,CreateSearchBox,CreateFormDialog(strata,level,closeText, row-buttonwidth/onClick),CreateScrollFrame(barWidth),CreateExpandableList(its group and child fields andSetAllExpanded),ShowTooltip(titleColor,reason,owner),MakeLabelandStartCooldown(interval). - The RowList basics:
rowCount,header,headerTip,sortable = false,font,color = "class",classDisplay, thecheckboxcolumn andboxSize,SetData'spreserveScroll, andClampOffset. - The DockWindow handle's
IsDockEnabled/Dock/Restore/IsNearTarget, and the resize handle'sSetStatusTable/ApplyStatus/SaveStatus/Destroy. SetAccent/GetAccent,ClassColor,SetTooltipOwner/GetTooltipOwner,TabLabelRegion,SYMBOL_FONT/SYMBOLS, andclassDisplay/refontHookin theConfigureexample.
- Statements the code contradicted, corrected:
- README and CurseForge dated the
PersistWindow/DockWindowrefusal to MINOR 31, or said "nothing enforces that". It shipped in MINOR 32 (v0.2.2). The two code comments that said 31 are corrected too. - A
checkboxcolumn writesentry[key]only when it has anonToggle. - A dialog parented into a window comes back with it, still open, rather than dying with it (MINOR 33).
- The expandable list's toggle is an ASCII
-, not a minus sign. - v0.1.11 brought four additions, not three.
- The two README contract intros had not kept up with their own bullets.
- The menu-item and
CloseMenuForparagraphs sat inside the form-dialog entry; they are under Menus now.
- README and CurseForge dated the
- The CurseForge page also gained the tab highlight, RowList icon columns, the named cooldowns, search in menus and dropdowns, and the diagnostics record.
[v0.2.4] -- a RowList that filters, searches and sizes itself, tooltips that give the anchor back, and two crashes gone
Compatibility -- MINOR 33 -> 34
New methods: W:HideTooltip(), W:ShowTooltip(), W:MenuCapacity(), RowList:SetSearch(),
RowList:ClearFilters() and RowList:Detach(); W:AttachTooltip takes an optional fourth
opts. Feature-detect on type(W.HideTooltip) == "function" or
LibStub.minors["LibAceGUIWidgets-1.0"] >= 34. The RowList options and column fields below are
opt-in fields on tables the caller passes, so gate those on the MINOR. Two behaviours change for
existing calls: every tooltip this library draws now hands the anchor back to the client's default
when the pointer leaves, rather than only hiding; and RowList:Refresh() / ClampOffset() now
rebuild the sorted view instead of reusing the cached one, which is what their documentation always
promised. Nothing else changes for a list that does not use the new fields: a header without
filterable still sorts on click, rows do not take hyperlink clicks unless hyperlinks is set, and
_getSortedData still returns data itself when nothing narrows or sorts it. The three widget
types stay at Version 30 and LAGW-DatePicker at 3. The TOC is unchanged.
New
RowList: everything FastGuildInvite's own list has that this one did not (inbox
7e28e319), so FGI can retireGUI/RowList.lua, the file this list was extracted from.opts.externalSort+opts.onSortChanged(key, desc, rl). WithexternalSorta header click sets the key and the arrow and fires the callback, and the rows are drawn exactly as the consumer ordered them. FGI's Announce, Eligibility and Messages tabs sort parent rows with their children attached, which no flat per-row comparator can express.onSortChangedfires on an ordinary list too, for every sort the user makes, and never for the consumer's ownSetSort.- An
expandercolumn: a +/- tree toggle bound toentry[key], tri-state as FGI's is (trueexpanded,falsecollapsed,nilhidden, for a leaf row). The click reports the new value tocol.onToggle(entry, newValue, idx, rl)and does not write the entry, because the owner rebuilds the data and a write here would flip it twice. - A
buttoncolumn: a clickable text cell with a hover highlight,col.onClick(entry, idx, rl, mouseButton, cellFrame)andcol.cellTip(entry), whose tooltip is titled with the header. It honourscol.justify, forwards a right-click toonRowClickso a row menu still works over it, and builds no hover overlay, which would sit on top of it and take its clicks. AcellTipthat answers nil or empty shows no tooltip at all; the first build drew a header-only one, found by FGI's suite on adoption. entry._readonly = truedisables that row's checkbox and button cells, re-applied in both directions on every populate because the row frames are pooled.opts.refontHookandopts.classDisplayoverride thelib.confighooks for one list. FGI asked whether the session-global config was the right home for a per-consumer hook, and it is not:lib.configis one table for every addon in the session, so a hook set there by one consumer re-fonts and re-labels every other consumer's lists. The global hooks still apply to a list that sets none of its own.
RowList: the four things TOGTools' 11 tables need (inbox
c571dee6).- A
filterablecolumn's header opens a column menu: sort ascending and descending, Select all / Clear all, and one checkbox per distinct value of what the cell shows, with colour codes, icons and link wrappers stripped. The values cascade (a column's list comes from the rows every other filter lets through),col.filterGroup(entry)nests them under group submenus, and the list is capped at 200 with a line saying how many more there are. The menu has a search box that narrows the values as the player types. A header whose column is filtering shows an asterisk after its name.opts.headerMenuputs the menu on every header. The menu isW:OpenMenu, so it uses the same strata, pooling and click-outside close as every other menu here. A second click on the same header closes it. rl:SetSearch(text): a case-insensitive plain substring over every text and button cell's shown text (never a checkbox'strue), composed with the filters; nil or empty clears it, and the list goes back to its top.rl:ClearFilters()drops every column filter.col.autoFit(+col.minWidth): the column is as wide as its widest cell over all ofdata(not the filtered view, so it does not jump while the player filters) plus 8px, and never narrower than its header. It is re-measured onSetData,SetColumnsand a scale change, and it counts as a fixed column, not a second auto-width one.opts.hyperlinks:|Hlinks in cells hover into the tooltip (player links skipped) and click as they do in chat. A player right-click opens the chat-name menu, handedDEFAULT_CHAT_FRAMEoff Retail because Classic's Whisper reads the frame'seditBoxand a row has none; other clicks go throughHandleModifiedItemClickand then a chat-link insert. Opt-in, because a row that takes hyperlink clicks takes the mouse. Feature-detected onSetHyperlinksEnabled. The row behaviour is not verified in a client, only offline.
- A
W:ShowTooltip(anchor, title, body[, opts]): the library's one title/body tooltip draw, made public so the RowListbuttoncolumn'scellTipgoes through it rather than a copy.One function for what a cell shows. The draw, the filter labels, the search and the autoFit measurement all read
RowList:_cellText, so "what you see is what you filter" holds by construction rather than by four copies agreeing.The scroll range counts the rows drawn, not
#data, now that a filter or a search can make them differ.Refresh,ClampOffset,SetData(…, true)and the mouse wheel all read it.A second round of consumer requests, from mapping real lists onto the first.
opts.tooltipOwner(frame)(FGI inboxdf5ec079) anchors every tooltip the list draws -- header, action icon, button cell, hyperlink -- in place oflib.config.tooltipOwneror the default, for that list only. FGI's owner also re-fonts non-Latin tooltip lines, which the global owner would have forced on every other addon's tooltips.rl:Detach()(TOGTools inboxc571dee6, item 5): call it from the host widget'sOnRelease. It closes a column menu the list opened, hides the rows, header and scrollbar, re-parents them to UIParent, and makes every later repaint, resize, rescale, scroll, wheel andSetColumnsa no-op. AceGUI pools the frame a list is built in, and aHookScripton it cannot be removed, so without this a released list goes on re-laying its rows inside the next widget to get the frame.col.align(item 6): the OPT-IN way to right-align a text cell.col.justifyis still ignored on text cells, because consumers already pass it and honouring it now would move their columns.col.gapBefore(item 7): extra space, a scale-1.0 value, to a column's left in the header and in every row, on both placement chains.col.sortDescDefault(item 8): a column whose first header click sorts high-to-low.
W:MenuCapacity(withSearch): how many menu rows fit in 80% of the screen at the current scale. A menu does not scroll and is clamped to the screen, so a row past that number cannot be reached.
Tests/rowlist_minor34_spec.lua covers all of the above, and RowList is at 100% line coverage
(1217/1217). LibAceGUIWidgets-1.0.lua is at 100% too (1896/1896, was 95.77%): the new
Tests/older_paths_spec.lua drives 80 lines no spec had reached, all older than this release --
ClearFrame's title-bar move, Close, info icon, settings gear, status text and release; TLabel's image
layout, spacer height, tooltip owner, texcoord and font fallbacks; the dropdown box's click and search;
the search box's escape; the form dialog's hint; the scroll box's wheel; SetAllExpanded; a
TOP/BOTTOM dock; and PersistWindow on a client with only GetMinResize. The features FGI needs most (externalSort, read-only rows) and the H1 crash below were
each broken on purpose, and the spec went red before the code was put back.
Fixed in this release before it shipped
Peer review of this MINOR (thread af2ebd61) found these in the working tree; none reached a player.
H1, and older than this release: a
SetColumnswith an identical column set left the column widths keyed by the OLD column tables._colWis keyed by table, and the same-set branch adopted the new tables without deriving their widths, so the next grow, resize or rescale raisedattempt to perform arithmetic on a nil value. This has been live since MINOR 33 for any consumer that follows the file's own advice to callSetColumnson every repaint, which Dibs' Tracker does. The same-set branch now re-derives the widths.H2:
RefreshandClampOffsetdrew a stale snapshot. Both are documented for use after the consumer changes the data in place, but the filtered and sorted view was cached, so a removed row stayed drawn and clickable andonRowClickcould hand back a deleted entry. The publicRefreshandClampOffsetnow drop the cache; the list's own repaints that change no data (a resize, a scale change, a scroll-to) keep it.H3: a hyperlink click passed the bare
item:...link toHandleModifiedItemClick, which wants the full|Htext, so shift-click inserted nothing and a plain click opened nothing. Every non-player click now goes throughSetItemRef(link, text, ...)as a chat frame's does.M1: Select all and Clear all now cover every value, not only the ones the menu has room to draw.
M2: the filter menu draws only as many values as
MenuCapacitysays fit, with a line counting the rest; a group submenu and the group list are capped the same way. The first build could draw 200 rows, about 3,700px at scale 1.M3: a value excluded by a filter and then gone from new data is dropped from the filter, so the header's asterisk no longer points at a value the menu can't show.
M4: a read-only row's button cell is disabled, and a disabled Button fires no
OnEnterunlessSetMotionScriptsWhileDisabledis set, so itscellTipwent silent. It is set now.L2: a button column now calls
onCellEnter,onCellLeaveandonCellClickfrom its own scripts; the first build silently ignored them there.L3: Select all and Clear all redraw the open menu in place and keep the search text; re-opening it cleared the box.
W:OpenMenu's rows now read their check state from the item they carry at click time, not the one the click handler was built for.L4: filter values sort as the header does -- numbers by value, text case-folded.
L5: a column set that changes only
font,justify,alignorgapBeforeis a rebuild, not a no-op.L6:
headerMenuleaves a header that can neither sort nor filter inert, instead of opening a title-only menu.L7: a filterable column's header is measured with its asterisk, so a tight column fits it.
L1: a leave no longer hides a tooltip another frame or addon has taken since.
ShowTooltiprecords who the tooltip was actually handed to, andHideTooltip(frame)does nothing unless that owner still holds it. The guard is on the RECORDED owner, not the hovered frame, so a tooltip a consumer'stooltipOwneranchored elsewhere is still hidden rather than left stuck on screen.
Not changed, with the reason. M5: with hyperlinks on, a link click may
also run the row's OnMouseDown. That ordering is client behaviour I can't reproduce offline, so it
stays open (not verified).
Fixed
RowList:SetColumnswith a new column set raised in the client on any list with a plain text column --FontString:SetScript(): Doesn't have a "OnClick" script, which left the list half-rebuilt. Shipped in MINOR 33 (v0.2.3), whenSetColumnsarrived. Found by ItemDB in the client on Classic Era (inbox91e50ebd). Releasing a pooled cell cleared itsOnClickwhenever the cell had aSetScriptmethod. A text cell (a FontString) and an icon cell (a plain Frame) both have one, and neither has anOnClickscript, so the client raised. The clear now runs only for the kinds built as buttons: checkbox, expander and button. The offline suite never saw it because the test harness acceptsSetScript("OnClick")on a FontString; the new spec makes the text and icon cells raise the way the client does, and fails against the old guard. A same-setSetColumnsreturns before the release, which is why a consumer calling it on every repaint never hit it.- A tooltip left the GameTooltip owned by our frame after it hid. Found by Questbook on
adopting
DressBottomRow(inbox7271976e):BottomIcon_OnLeavewasGameTooltip:Hide()and nothing else, andHidedoes not clear the ownerAnchorTooltipset. The next addon to callGameTooltip:Show()without aSetOwnerof its own drew its tooltip at our icon, at the bottom of our window, until something re-anchored it. Questbook's hand-rolled row had always handed the anchor back; the shared one did not. The same bareHidewas in seven more leave handlers --AttachTooltip, the menu rows,CreateExplainedButton, the ClearFrame info and settings icons,TLabel's control, and RowList's row icons -- so the fix is one function,W:HideTooltip(), that hides and then callsGameTooltip_SetDefaultAnchor(GameTooltip, UIParent)(present in the Classic Era tree atSharedTooltipTemplates.lua:87, feature-detected all the same), and all eight sites go through it. Questbook's workaround of re-settingOnLeaveon our icons after each dress is no longer needed.Tests/bottomrow_spec.luapins the hand-back and the no-raise path on a client without the function.
Older releases (v0.2.3 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