## VDSTools — native macOS controls for Xojo, in pure Xojo

VDSTools 1.0 — native macOS controls for Xojo, in pure Xojo

I have been building a set of AppKit wrappers for my own apps, and it grew
into something worth releasing: 100 classes, written entirely in Xojo
Declares. No plugin, no external framework, no compiled Objective-C to ship
or sign — just code you drop into your project.

27 of them are controls you drop in the IDE and configure in the
Inspector: table and outline views with rich cells, token field, path bar,
combo button, date picker, colour well, switch, level indicator, and the rest.
Fifteen inherit from a Xojo control; twelve host an AppKit object.

Some of what it covers that Xojo does not expose:

  • NSToolbar, including the customisation sheet
  • NSSplitViewController window chrome, and source-list sidebars
  • View-based tables and outlines: checkboxes, two-line cells, progress bars,
    pop-up menus in cells, row reordering with a Numbers-like drag ghost
  • Window tabs and the system Window menu
  • QuickLook thumbnails, the Dock tile, popovers, NSGridView, NSStackView
  • The macOS 26/27 glass effect, and menu image visibility

Documentation: a developer reference in French and English, covering every
class and enumeration — and 56 traps, each with the symptom that sent me
looking in the wrong place first. That part may be the most useful bit.

Demo application: 34 pages, each showing one subject with its code in an
inspector, plus a second window with a page per placeable control and every
option adjustable live. Signed and notarised.

→ VDSTools — macOS natif pour Xojo

Requirements: macOS 15 or later, Xojo 2021r3 or later.

Pricing: €60 incl. VAT for the compiled library, €240 incl. VAT with the
source code — per developer, unlimited applications, royalty-free
distribution. Businesses in the EU outside France are invoiced net of tax.
Running from the IDE is free; a built application without a licence runs, but
carries a small watermark.

Happy to answer questions here, and to hear what is missing.

Wow! Let’s see a sample project.

Wow! Super impressive stuff.

I love the tables. It’s the smoothest implementation that I’ve seen.

1.1 is out.

New: NativeIconButtonControl, the 28th control you drop in the IDE — a
button with an SF Symbol beside its title. Symbol, icon position, hugging,
bezel and control size in the Inspector; a file image or a Picture from code.

It had to be a hosted control rather than a property on the inherited one.
Measured: setImage: on a DesktopButton is accepted, then wiped on the next
pass — image gone, imagePosition back to 0. Xojo reconfigures its own buttons
after Opening and keeps doing it; there is no hook before an inherited
control draws.

Also: ImagePosition and ImageHugsTitle on NativeButton, and a second trap
— with the Push bezel, an Above icon draws the title outside the frame.

101 classes, 58 traps.
Download: https://github.com/Valdemar-VDSC/VDSTools-dist/releases/latest
Documentation: VDSTools — macOS natif pour Xojo

Question about Sidebar.

Some Things are missing:

SelectedItem(Item As SidebarItem, Page As Integer, Title As String)

AddItem(Section, Title, SfSymbol, Color, Collapsed As Boolean = False, Tag As Variant)

Add a Section as a sub item in a Section

Collapse and expand a Section

Do you see a change to add these features to the Sidebar?

Nice idea, I take a look for next release that come later in October :wink:

VDSTools 1.2 — sidebar items, tags, collapsible sections, saved state

This one comes almost entirely from a comment left here on 1.1, so: thank you. Four things were missing from the sidebars, three of them are in. Everything is additive — SelectionChanged is untouched, no existing code has to be rewritten.

A row is an object now. NativeSidebarItem is the handle a sidebar hands back: Tag, Title, Section, Index, Page, IsValid. The link to the bar is weak, so holding handles does not keep a closed window alive.

Tags. Without one, a program identifies a row by its title — which is translated — or by its page number, which moves the moment you insert a row above it. A tag does neither:

mSidebar.AddItem(0, "Invoices", "doc.text", &c007AFF, "invoices")
Var it As NativeSidebarItem = mSidebar.LastItem

On the flat sidebar the tag is set apart, with SetTag and TagAt. An optional Variant on Add would have made every three-argument call ambiguous, since a Variant accepts a Color too.

A selection event that tells you what was selected:

Sub SelectedItem(item As NativeSidebarItem, page As Integer, title As String)
  Select Case item.Tag.StringValue
  Case "invoices"
    ...
  End Select
End Sub

Sections collapse and expand from code, and can start collapsed:

Var sec As Integer = mSidebar.AddSection("Archive", True)   // starts collapsed
mSidebar.ExpandSection(sec)
If mSidebar.SectionExpanded(sec) Then ...

Plus ItemAt, CurrentItem and SelectPage on the way past.

And the state survives a quit. Either AppKit keeps it, under a name of your choosing — mSidebar.AutosaveName = "MainSidebar" — or you keep it yourself, as JSON, to put in a preference or attach to a document: SaveState / RestoreState.

Two traps measured on the way, both now in the reference:

  • Turning a Sub into a Function breaks every existing call. Xojo refuses to ignore a returned value — “You must use the value returned by this function” — so a published method cannot grow a return type without breaking a customer’s code. That is why AddItem stayed a Sub and LastItem gives you the handle.
  • An NSOutlineView restores its autosaved expansion during reloadData, not when you set autosaveName. Set the name after loading the rows and it will save the state faithfully and never once restore it.

102 classes, 798 public methods, 60 documented traps. Pure Xojo — Declares only. macOS 15+, Xojo 2021r3+. Library 60 € TTC, source 240 € TTC — GitHub - Valdemar-VDSC/VDSTools-dist: VDSTools — chrome de fenêtre, barre d'outils et contrôles macOS natifs pour Xojo, en Xojo pur. Documentation et versions distribuées. · GitHub


VDSTools 1.3.0 — what changed since 1.2.0

Two releases since the sidebar one. The first came out of a customer’s request, the second out of using the thing.

Row icons are no longer limited to SF Symbols. A customer was working around it by writing the cell’s imageView.image with declares on a timer, because the library put its symbol back every time a row view was rebuilt. A Picture now works anywhere a symbol did:

mTable.SetRowImage(0, pic)    // and SetNodeImage(node, pic) on the outline

mSidebar.Add("Shared", pic) // or SetImage(index, pic) afterwards

mOutline.SetImage(section, item, pic) // hierarchical sidebar

It survives a refresh, which was the whole point of asking: the image lives in the model, not on a view, and is re-read on every cell build. So it holds through a reload, a sort, a row move, a collapsed section and scrolling — exactly like a symbol name. Image and symbol are mutually exclusive: setting one clears the other, so there are never two icons to arbitrate. A bitmap is not tinted — contentTintColor only means something on a template image — and it is scaled down proportionally, never enlarged.

One asymmetry worth knowing, because it is deliberate: on the sidebars SetImage reloads the list itself, as SetBadge always has; on the table SetRowImage writes to the model and leaves the refresh to you, as SetRowIcon always has. Each class keeps its own rule.

A leak fixed in both sidebars. Cells, their labels, their icons, the badges and the row views were allocated and never released — up to nine objects per cell build, which means on every reload and every time a row scrolled back into view. It is the fix that went into the table in September and was never carried over. Harmless-looking with 18-point symbols; with images, every reload left megabytes behind. If you build AppKit views by hand, the rule is worth stealing: a view added to a parent is released straight after, and a view handed to AppKit goes out autorelease, never release — AppKit retains it after you return.

1.2.1 — a hosted control now follows Enabled. NativeIconButtonControl hosts a real NSButton. Enabled belongs to DesktopCanvas, a subclass cannot override it, and AppKit does not propagate setEnabled: to subviews — so the canvas went dim alone while the button stayed drawn as active and, being the view on top, went on receiving clicks. A button you believed disabled still acted. Fixed, with SetEnabled for when you want it without waiting for a draw. The other twelve hosted controls have the same blind spot; it is written down next to the fix.

102 classes, 804 public methods, 61 documented traps. Pure Xojo — Declares only, no plugin, no external framework, no compiled Objective-C. macOS 15 and later, Xojo 2021r3 and later.

1.3.1 is out — a crash in 1.3.0, and the trap behind it

If you picked up 1.3.0 and used the new row images, rebuild against 1.3.1: selecting a row whose icon was a Picture crashed a built application. Reported by a customer within the day, with a stack trace that did the diagnosis for me.

The cause is worth passing on, because it has nothing to do with this library. Picture.CopyOSHandle returns an autoreleased object, despite the “Copy” in its name — so releasing it yourself is an over-release. It hides well: the view retains the image, so the drawing is correct, and the object only dies when the pool drains, under a view still pointing at it. The crash lands on the next refresh, somewhere else, with no Xojo frames in the stack — and never from the IDE.

The rule, now written down with the case as its evidence: a handle returned by the Xojo framework is lent, not handed over. Objective-C’s “copy” naming convention does not apply to it. What you allocated yourself is another matter — that, you always give back.

https://github.com/Valdemar-VDSC/VDSTools-dist/releases/latest

1.3.2 — subtitle columns no longer truncate

In a two-line cell — a title with a smaller line under it, the Mail pattern — the text was cut at around a hundred points however wide the column was, and a reload did not help.

The cause is a small lesson in composing AppKit views by hand. A cell is built against a holding width of 120 points, and AppKit resizes it to the real column afterwards; anything you place inside only follows if it carries an autoresizing mask. The label, the level indicator and the popup had one. The box that stacks the two text fields did not, so it stayed at its holding width for ever — and a newly built cell came out exactly the same, which is what made it look permanent.

My thanks to Øjvind, who found this one and the two before it inside a week, each time with the measurements already done — frame widths and autoresizing masks read cell by cell, which left me nothing to guess at. Reports like that are rarer than the bugs they describe.

https://github.com/Valdemar-VDSC/VDSTools-dist/releases/latest

VDSTools 1.4.0 — row images, drag-and-drop fixes, and dragging rows out of a table

Five versions have gone out since 1.3.0, four of them driven by one buyer’s reports. Here is the whole arc in one post.

1.4.0 — dragging a row out, without reordering the table. AllowsRowDragging is new and independent of AllowsRowReordering. Until now reordering gated everything: with it False, pasteboardWriterForRow: returned Niland the source operation mask was left at zero, so no drag started at all — not to a sidebar, not to another application. The name said “reordering”, the code said “all dragging”. The two now combine freely, and dragging to another application is permitted for the first time, as a copy. Two more additions make the payload yours without a single Declare:

AddHandler myTableControl.Table.WritingRow, AddressOf TableWritingRow

Private Sub TableWritingRow(sender As NativeTableView, row As Integer, pasteboardItem As Ptr)
  NativeTableView.AddDragType(pasteboardItem, "public.utf8-plain-text", sender.CellAt(row, 1))
End Sub

What the table writes by itself is the row index under a private type, which means nothing outside it — so without this, a drop on Mail or the Finder receives nothing.

1.3.4 — something follows the cursor outside the table. The drag shadow is a view placed inside the table, so it could show nothing past its own edges. The library now composes a Mail-style stack: up to three offset thumbnails, with AppKit’s own red count badge. One or the other, never both — the card while you are inside the table, the stack as soon as you leave — and the swap happens on the source side, in draggingSession:movedToPoint:, since a view that is no longer the destination has no say over the session’s items. Each thumbnail is a crop of the row at 1:1, not the whole row shrunk: a row is wide and short, and reduced it becomes an unreadable band.

1.3.3 — three drag defects, all reported with measurements. The shadow was only moved from validateDrop, which AppKit calls when the proposed destination changes rather than on every move — five times across a 95-point drag, hence jumps of 11 to 24 points; the table is now instantiated from a subclass overriding draggingUpdated:. The insertion line no longer crosses the shadow: AppKit re-fronts its destination view on every update, so subview order cannot win, and the shadow’s layer carries a high zPosition instead. And the insertion line goes to the nearest row edge, instead of oscillating as “on” was re-proposed as “above”.

1.3.2 — subtitle text was truncated regardless of column width. A cell is built against a 120-point placeholder width and then resized by AppKit; what you put in only stretches if it carries an autoresizing mask. The label, gauge and popup had one; the box stacking title and subtitle did not, so it stayed 116 points wide inside a 400-point column. Nothing to do on the caller’s side any more.

1.3.1 — a crash fix, and it needs a rebuild. Picture.CopyOSHandle(MacNSImage) returns an autoreleased object despite the “Copy” in its name. The library released it, believing it owned it: an over-release that only ever hit builtapplications, never the IDE, and surfaced as EXC_BAD_ACCESS in objc_release under objc_autoreleasePoolPop with no Xojo frame in the stack. If you shipped anything using 1.3.0’s row images, rebuild it against 1.3.1 or later.

1.3.0 — your own image as a row icon, where only an SF Symbol was accepted: SetRowImage, SetNodeImage, NativeSidebar.Add(title, image) and SetImage. As persistent as a symbol — the image lives in the model and is re-read on every cell build, so it survives reload, sort, row moves, section collapse and scrolling. Image and symbol exclude one another. That version also fixed a view leak in both sidebars: up to nine objects per cell build.

102 classes, 805 public methods, 62 documented AppKit traps. The demo shows all of it — toggles on the Table and Outline pages, and AllowsRowDragging in the Placeables window: tick it, leave reordering off, and drop a few rows onto TextEdit.

VDSTools 1.5.0 — everything the table could not do yet

Two halves, and the second closes a gap the first one opened.

The table’s API had holes, and they are filled. Forty-six methods and seven properties on NativeTableView, all backed by something NSTableView already knew how to do: the readers that were missing opposite every SetCell… setter — background, text colour, bold, alignments, row icon and image, column title, width and kind; hit testing with RowAtPoint, ColumnAtPoint and CellFrame, in the host view’s coordinates, which is what you need to open a contextual menu on the right row; ScrollPosition in points, SetContentInsets — the one that matters under a translucent title bar, where the first row is otherwise born beneath the blur — and the scroller properties; AllowsEmptySelection and SelectAll; column renaming and hiding, with no index shifting; free tags on rows, cells and columns that travel with their row through sorting, moving, insertion and removal; a tooltip per cell, where Xojo has one per control; the mixed checkboxstate; EditCell; grid colour; and Content plus delimited import and export with RFC 4180 quoting.

And the table now accepts what comes from outside — the missing half of 1.4.0, where a row learned to leave but nothing could enter. AcceptDragType, with AcceptFileDrop and AcceptTextDrop shortcuts, raises AcceptDrop with the row and the place: on a row, on an edge between two, or on the control. ShouldRefuseDrop says no while the cursor is still moving, before the user lets go. Three shared readers — DragFiles, DragText, DragHasType — read what the drop carries without a single Declare.

Two toolbar requests from one buyer. NativeMenuToolbarItem.OpensMenuOnClick makes a pure pull-down, where a click opens the menu instead of running an action and leaving the menu to a press-and-hold. And SetIcon takes a symbol weight and scale — as enumerations, not a Double, because NSFontWeightRegular is zero and a parameter defaulting to zero cannot tell “regular” from “leave it alone”.

Two defects, both found by writing other things. Sorting lost the subtitles: SortBy rebuilt ten parallel arrays and forgot the eleventh, so the text changed row while the subtitle stayed. And two tables shared the same private drag type — a class constant — so dragging a row from one to the other looked like an internal reorder, and the source table’s index was read as the destination’s. Asleep since 1.1.0; nobody had put two tables side by side. The type now carries an instance number, which means it deliberately carries nothing between two tables: to move a row across, write a public type in WritingRow and read it back with DragText.

The demo shows exactly that — a narrow “Done” list beside the main table, tasks moving both ways with their icon and subtitle, carried as two flavours on the same dragged item: plain text for TextEdit, a richer type of our own for the other table.

A pitfall worth knowing if you write declares of your own. A runtime Objective-C class is registered once per process, and the ones this library creates are named — VDSTableDS, XPKToolbarDelegate, a score of others. If your code grafts a method onto one of them, yours is what AppKit calls, and the library has no way of knowing. It now logs when the implementation in place is not its own, naming the class and the selector. That is the 63rd documented pitfall.

1.5.1, the day after

Three corrections, no new API. The log above spoke in both cases at first — including when our own classes simply met themselves — which buried the one line that mattered under dozens that did not; it compares implementations now. An internal warning fired on the recommended use of OpensMenuOnClick, and it was hiding a real gap: a pull-down added to a toolbar could never go back to being an action button, because the toolbar only remembered its target for items that respond to clicks. And SymbolWeights and SymbolScales were named in a signature without their values being listed anywhere.

102 classes, 853 public methods, 63 documented pitfalls. Next is already named: 1.6.0, “the model”