Ceux qui ont coûté cher. Presque tous ont un symptôme qui pointe ailleurs que la cause.
Xojo efface l'image d'un bouton hérité, indéfiniment
Mesuré le 17 septembre 2026 sur un DesktopButton posé dans une fenêtre. setImage: est bien reçu — juste après l'appel, [button image] rend l'image et imagePosition la valeur posée. Au clic SUIVANT, avant d'y toucher : image absente, imagePosition retombé à 0. Personne n'y avait touché entre-temps.
Ni la cellule ni le bezel ne sont en cause : cellule NSButtonCell ordinaire, bezel Push, qui dessinent parfaitement une icône hors de Xojo. XOJButton ne redéfinit aucune méthode de dessin — seulement des événements. Mais le framework appelle lui-même setImage:, setImagePosition: et setButtonType:, ce dernier remettant image et bezel, et il le fait APRÈS Opening puis à chaque passage.
Ce qui ne marche pas : Timer.CallLater(0) pour poser l'image plus tard, setNeedsDisplay: pour forcer le dessin. Il n'existe aucun point d'accroche avant le dessin d'un contrôle hérité.
Ce qui marche : HÉBERGER plutôt qu'hériter — NativeIconButtonControl. Le bouton est alors créé par la bibliothèque, Xojo ne le connaît pas et ne le reconfigure jamais.
Une icône au-dessus du titre sort du cadre, avec le bezel Push
Relevé au rendu, sur macOS 27. Avec ImagePosition = Above (ou Below) et le bezel Push, celui du bouton ordinaire, AppKit dessine l'icône dans le cadre et le titre DEHORS, sous la pastille. Essayé à 32, 40, 46, 52 et 60 points de haut : le cadre garde sa hauteur fixe et le texte reste à l'extérieur, à toutes les hauteurs.
La cause n'est pas la place manquante : le bezel Push a une hauteur propre, que le cadre du contrôle ne commande pas. fittingSize ne prévient de rien — il rend 24 points de haut pour Above comme pour un bouton sans image.
Ce qui marche : un bezel qui accepte de grandir — FlexiblePush (2) ou SmallSquare (10), vérifiés tous les deux. L'icône et le titre tiennent alors dans le cadre, empilés. Avec Leading, Trailing, Left ou Right, le bezel Push convient parfaitement.
La ligne qu'on glisse disparaît : l'image d'AppKit ne suffit pas
Relevé image par image sous macOS 27, sur deux enregistrements. L'image : pour une table à base de vues, l'en-tête dit qu'AppKit la compose des draggingImageComponents de NSTableCellView — son champ de texte et son image. Des cellules NSView ordinaires n'en fournissent presque rien : un titre blanc, puis plus rien. La sélection : en style Gap, la ligne saisie est masquée pendant tout le glisser ; ce qu'on déplace n'est plus visible nulle part. Et au milieu d'une ligne, AppKit propose DropOn ; le refuser laisse sans destination.
Ce qui marche, et reproduit Numbers : style Regular, pour que la ligne d'origine reste en place ; photographier les lignes dès pasteboardWriterForRow: par cacheDisplayInRect: sur la table ; poser dans la table une vue d'ombre — ces photos empilées dans une carte opaque à coins arrondis et liseré couleur d'accent, l'ombre portée par une vue extérieure qui ne coupe rien (une simple copie translucide se confondait avec le fond blanc de macOS 27) — que validateDrop: déplace à chaque mouvement d'après draggingLocation, et que draggingSession:endedAtPoint: retire ; vider l'image d'AppKit par setDraggingFrame:contents: ; recadrer DropOn en DropAbove par setDropRow:dropOperation:.
Sous macOS 27, un champ blanc sur une fenêtre blanche
Le champ n'a pas perdu sa bordure : il a perdu son contraste. Relevé sur un système 27.0 en apparence claire, windowBackgroundColor vaut désormais 255/255/255 — exactement le blanc de textBackgroundColor, l'intérieur du champ. Il ne reste qu'un filet d'environ 12 % de gris, que l'œil ne voit plus sur une capture réduite. Square et Rounded se dessinent alors à l'identique, et tous les champs sont concernés : DesktopTextField comme NativeTextField, c'est le même NSTextField.
Deux remèdes intuitifs ne marchent pas, mesurés eux aussi : poser un backgroundColor sur un champ à bezel est ignoré, et retirer le bezel pour un simple filet donne un trait dur, blanc vif en sombre. Ce qui marche est la mise en page des Réglages : poser les champs dans un cadre de groupe, dont le gris fait ressortir le blanc — et un NSBox se dessine aussi sous macOS 15, sans test de version. Attention au titre vide : il réserve quand même 12 points en haut du cadre. Augmenter le contraste, dans les réglages d'accessibilité, renforce les bordures ; c'est le choix de l'utilisateur, pas de l'application.
Sous macOS 27, les icônes des menus disparaissent sans erreur
Le code n'a pas changé, le symbole est bien posé par setImage:, et le menu s'ouvre sans icône. L'en-tête de NSMenuItem le dit en toutes lettres : à partir de macOS 27, AppKit « determines the visibility of menu item images, and will typically hide images ». La nouvelle propriété preferredImageVisibility vaut Automatic à la création — relevé sur un système 27.0.
Le remède tient en un réglage, Visible, derrière un test de sélecteur. NativeMenu le pose par défaut : un symbole passé en argument est une demande explicite, et le rendre au système reste possible par ImageVisibility = Automatic. Le piège ne concerne que les items à image : les pastilles d'un menu palette n'en ont pas.
Tester une méthode de CLASSE : deux idiomes, deux arguments différents
respondsToSelector: est un MESSAGE envoyé à un objet : il cherche dans la classe de cet objet. Pour une méthode de classe, on l'envoie donc à la classe elle-même. L'envoyer à la métaclasse fait chercher dans la méta-métaclasse, et ne trouve jamais rien.
La fonction du runtime class_respondsToSelector(cls, sel) est l'autre idiome : elle regarde les méthodes d'instance de la classe qu'on lui donne, si bien que pour une méthode de classe il faut lui passer la métaclasse. Les deux sont justes ; les mélanger ne l'est pas.
Le symptôme est muet et trompeur : une fonctionnalité se déclare indisponible sur une machine qui la prend en charge, sans erreur nulle part. Constaté sur les curseurs de macOS 15, rapportés absents sur un système 15.7.9.
Ne jamais déduire la géométrie d'un volet de celle d'un autre
Sous NSSplitViewController, deux volets pleine hauteur ont la même hauteur — en régime établi. AppKit ne les redimensionne pas au même instant, et une bascule en plein écran suffit à le montrer : placer le contenu de l'inspecteur d'après la hauteur que rapporte l'événement de redimensionnement du détail donne un cadre faux.
Le symptôme trompe deux fois. Trop haut à l'aller : l'origine d'AppKit étant EN BAS, le contenu sort par le haut du volet et le panneau paraît vide — ce qui se lit comme un défaut de dessin, pas comme un cadre trop grand. Trop court au retour : le contenu se retrouve collé en bas. Et rien n'apparaît en régime établi, seulement pendant la transition.
Chaque volet doit être lu par ses propres bounds. La valeur reçue dans l'événement ne sert que de repli pour la toute première passe, avant que le contrôleur n'ait posé le volet — à la construction, ses bounds valent encore ceux de sa fabrique.
La palette de personnalisation passe par le même delegate
toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar: sert deux usages : peupler la barre, et construire la feuille de personnalisation. Rendre le même NSToolbarItem dans les deux cas confond l'item affiché dans la feuille et celui de la barre : le glisser vers la barre ne fait rien, sans la moindre erreur. L'en-tête l'énonce — each toolbar receives its own distinct copies and each time this method is called a new instance, et la palette est construite par cette même méthode.
NSToolbarItem étant NSCopying, on rend une copie autoreleasée quand willBeInserted vaut NO, et l'instance réelle quand il vaut YES : les changements de propriétés à chaud sur un item déjà posé continuent alors de fonctionner.
Deux ordres décident du reste. autosavesConfiguration doit valoir YES AVANT setToolbar: — c'est à cet instant qu'AppKit relit la configuration enregistrée ; posé après, il ne fait plus qu'écrire, et la barre revient au jeu par défaut à chaque ouverture. Et displayMode et le style doivent être posés avant l'attache, comme valeurs de départ : posés après, ils écrasent à chaque fois le mode que l'utilisateur vient de choisir dans la feuille.
La clé d'enregistrement est NSToolbar Configuration <identifiant> dans les préférences de l'application — relevée, pas devinée. Aucune API ne la remet à zéro : il faut la supprimer et réinsérer les identifiants du jeu par défaut.
NSOutlineView compare les pointeurs qu'on lui remet
La source de données rend un « item » par nœud, et NSOutlineView conserve ces objets et les compare pour savoir ce qui est déplié et ce qui est sélectionné. Rendre un nouvel objet à chaque appel — même équivalent — fait perdre le repli et la sélection à chaque rafraîchissement.
Il faut donc des clés stables et retenues : une NSString par nœud, liée à son numéro — jamais à sa ligne, qui change dès qu'on insère ou déplace —, mise en cache dans un Dictionary, avec la table inverse pour retrouver le nœud depuis le pointeur. Une clé reste retenue jusqu'au vidage de l'arbre, même pour un nœud retiré : AppKit peut encore la tenir pendant l'animation de suppression. Et setOutlineTableColumn: désigne la colonne qui porte les triangles : sans lui, AppKit les place où il veut et l'indentation ne suit pas le texte.
Un identifiant de nœud ne doit pas être un numéro de ligne
Rendre l'indice de ligne comme identifiant est tentant : tant qu'on ne fait qu'ajouter en fin, rien ne bouge et tout marche. Mais retirer ou insérer au milieu décale toutes les lignes au-dessus — les identifiants que l'appelant a gardés, la table des parents, et les clés remises à NSOutlineView. Le dépliement et la sélection passent alors d'un nœud à l'autre, un enfant s'accroche au mauvais parent, et rien ne lève.
Le remède tient en un type propre, pas en un entier plus stable : un numéro jamais réutilisé mais resté Integer aurait compilé partout et coïncidé avec la ligne jusqu'à la première suppression. NativeOutlineNode fait refuser par le compilateur chaque endroit qui confondait les deux. Et comme un indice qu'AppKit ne connaît pas lève une exception Objective-C, l'animation n'est tentée que sur un parent affiché et déplié ; ailleurs, l'arbre est rechargé.
Un dossier n'est pas forcément un dossier
FolderItem.IsFolder rend True pour une application, un .mpkg, une bibliothèque Photos, un projet Xcode, un document RTFD. macOS appelle cela un paquet et lui seul le sait : il faut interroger NSURLIsPackageKey, NSURLIsApplicationKey et NSURLContentTypeKey. Un .pkg moderne, lui, n'est pas un dossier du tout — c'est un fichier plat.
Deux pièges dans le détail. L'ordre des questions : un alias vers une application est d'abord un alias, une application est un paquet avant d'être un dossier. Et il faut résoudre les liens symboliques — /Applications/Safari.app en est un, il passerait sinon pour un simple alias. Enfin, lisez les clés plutôt que de les recopier : NSURLIsApplicationKey vaut en réalité _NSURLIsApplicationKey, avec un tiret bas que rien n'annonce.
RealityKit est hors d'atteinte depuis Xojo
RealityKit remplace SceneKit depuis macOS 26, mais il n'expose aucune classe de vue à l'Objective-C : ni ARView, ni RealityView, ni Entity, ni ModelEntity. Vérifié à l'exécution — les seules classes ObjC du framework sont de l'interne Swift aux noms décorés (_TtC10RealityKit…). Ses en-têtes publics ne couvrent que les shaders Metal.
Xojo ne parlant que l'Objective-C, RealityKit ne s'atteint pas. SceneKit reste donc la seule voie praticable pour de la 3D en Xojo, tout déprécié qu'il soit — et il faudra le savoir le jour où Apple le retirera : le repli sera la vignette QuickLook, fixe.
La taille demandée à QLThumbnailImageCreate est une BOÎTE, et le 3D veut du 4:3
Le paramètre de taille n'est pas une consigne d'échelle mais une boîte dans laquelle la vignette doit tenir. Le générateur 3D rend du 4:3 : toute boîte plus étroite que ce rapport le fait échouer sans rien dire. Mesuré sur un STL — 840 × 340 rend Nil, 400 × 267 aussi, tandis que 512 × 512 rend 512 × 384 et 400 × 300 rend 400 × 300.
On demande donc un carré, qui contient n'importe quel rapport naturel, et l'on laisse la vue mettre à l'échelle. Au-delà de 1024 le générateur plafonne de lui-même. Le piège est vicieux parce que l'échec est silencieux et ressemble à « ce format n'est pas géré » : on retombe sur l'icône en croyant que QuickLook ne sait pas faire.
Fermer deux fois un QLPreviewView termine le processus en silence
close est définitif — la vue n'accepte plus rien ensuite — mais rien n'empêche de l'appeler une seconde fois, et cela fait disparaître l'application sans message, sans trace. Le cas arrive vite dès que deux chemins ferment : l'un au changement de page, l'autre avant de reconstruire.
Il faut donc un drapeau interne et le contrôler dans close comme chez l'appelant. Available() doit rendre False après fermeture, faute de quoi un appelant prudent se croira autorisé à fermer de nouveau.
Aperçu VIVANT ou VIGNETTE : la 3D rend le choix obligatoire
Un QLPreviewView installe une NSRemoteView servie par SceneKitQLPreviewExtension — un autre processus, qui de surcroît survit à son client et s'accumule. Sur un document 3D, cela détruit définitivement le glisser-déposer du processus : plus rien ne peut être glissé dans l'application, et le Finder refuse même de soulever un fichier. Ni close, ni la destruction de la vue, ni le changement de page n'y font quoi que ce soit — seule la sortie de l'application. Images, PDF, texte et son sont indemnes.
La vignette est la sortie : QLThumbnailImageCreate passe par SceneKitQLThumbnailExtension, celui que le Finder emploie sans cesse. Elle rend une image — aucune vue hors processus dans la fenêtre, aucun processus laissé derrière — avec le rendu 3D complet. Dépréciée depuis 10.15 mais toujours présente et synchrone (43 ms sur un STL), là où l'API moderne exige un bloc et un rappel hors du thread principal. Elle rend Nil sur les dossiers, les applications et les formats inconnus : on retombe alors sur l'icône du Finder, et la panne du jour où elle disparaîtra est déjà prévue.
Une classe hors AppKit n'existe pas tant qu'on ne charge pas son framework
QLPreviewView appartient à QuickLookUI, sous l'ombrelle Quartz — que Xojo ne lie jamais. Vérifié : un binaire qui ne lie pas Quartz ne trouve pas la classe, objc_getClass rend Nil. Il faut donc un dlopen explicite avant tout ClassRef.
Et sur le chemin complet : dlopen("Quartz") échoue, comme dlopen("AppKit") d'ailleurs — les noms courts que Xojo accepte dans un Lib ne sont pas ceux que dlopen résout. Bonne surprise en revanche : NSURL se conforme déjà au protocole QLPreviewItem, il n'y a aucune classe d'exécution à fabriquer.
Un mot-clé Xojo en membre d'énumération, et l'erreur accuse les usages
Soft est un mot-clé — celui de Soft Declare — et Xojo ignore la casse : Soft=1 dans un #tag Enum est une « Syntax error ». Le symptôme est trompeur au point d'être invalidant : Xojo signale chaque ligne qui écrit MonEnum.Soft et déclare le bloc de l'énumération sain, tant qu'il reste un usage à incriminer.
L'erreur ne tombe sur la déclaration qu'une fois toutes les références retirées. La règle à en tirer : quand une construction rigoureusement identique à du code qui compile est refusée, c'est un identifiant — et il faut aller lire la déclaration qu'il nomme, pas la ligne que le compilateur montre.
addRow: lève si la ligne racine n'est pas composée
Poser un prédicat simple sur un NSPredicateEditor le réduit à une seule ligne, sans racine composée. Le addRow: suivant ne reste pas inerte : il lève une NSRangeException dans -[NSRuleEditor _insertNewRowAtIndex:ofType:withParentRow:] — et une exception Objective-C termine le processus Xojo.
Le remède ne coûte rien : emballer le prédicat dans un ET à une seule sous-condition. Mesuré — la chaîne rendue par predicateFormat est identique, mais l'éditeur retrouve sa ligne composée. rowTypeForRow(0) permet de détecter l'état dangereux avant d'agir ; un éditeur vide, lui, accepte addRow: sans problème.
Un NSTableHeaderView à cadre nul est installé, et invisible
Masquer l'en-tête d'un tableau, c'est setHeaderView:nil — le NSScrollView replie alors la zone qu'il lui réservait. Le rendre en fabriquant un en-tête neuf marche aussi, à condition de lui donner une hauteur : à cadre nul il est bel et bien posé, le scroll view rétablit sa zone… d'une hauteur de zéro.
On ne voit donc rien, sans la moindre erreur pour le signaler. Mieux vaut garder l'en-tête d'origine — retenu, puisque setHeaderView:nil le relâche — que deviner une hauteur qui dépend du système et du style de table.
L'image de coche des menus est PARTAGÉE
paletteMenuWithColors:titles:selectionHandler: laisse aux pastilles l'image d'état ordinaire : NSMenuCheckmark, 18 × 17 points. Dessinée par-dessus une pastille d'environ 13 points, elle déborde et se retrouve rognée — on voit un arc blanc au lieu d'une coche.
Et la redimensionner sur place est exclu : deux palettes distinctes rendent le même objet, un item de menu ordinaire aussi. Il faut fabriquer une coche neuve par item — un symbole SF à la taille voulue, en mode template pour qu'elle se teigne selon le fond.
AppKit n'a pas d'alignement vertical - il faut placer le cadre
Un NSTextField centre son texte dans le cadre qu'on lui donne, et c'est tout : aucune propriété ne dit « en haut » ou « en bas ». L'alignement vertical d'une cellule s'obtient donc en plaçant le cadre du contrôle dans la cellule, pas en réglant quoi que ce soit.
Deux conséquences. L'origine d'une vue non flipped étant en bas à gauche, « en haut » veut dire le y le plus grand — l'inverse de l'intuition. Et le masque de redimensionnement doit se limiter à la largeur : laisser la hauteur suivre annulerait le placement qu'on vient de calculer.
Un style n'est pas un contrôle : où poser l'effet de bord de macOS 26
NSScrollEdgeEffectStyle n'a ni cadre ni vue : c'est un objet de style, et le SDK ne l'accepte que sur deux propriétés — preferredScrollEdgeEffectStyle de NSTitlebarAccessoryViewController et de NSSplitViewItemAccessoryViewController. En faire une classe de la bibliothèque aurait été du remplissage.
Il vit donc sur NativeWindowChrome, qui possède la fenêtre. SetScrollEdgeEffect rend le nombre d'accessoires effectivement stylés : zéro n'est pas une erreur mais le constat qu'il n'y en a aucun à styler — la fenêtre n'a d'accessoire de barre de titre que lorsque la barre de format RTF est ouverte.
Le double-clic de la table et l'édition d'une cellule visent le même geste
L'action de double-clic d'un NSTableView et le démarrage de l'édition d'un NSTextField logé dans une cellule répondent tous deux au second clic. Sur une colonne éditable ils se disputent la souris, et rien dans l'API ne le signale.
DoubleClickAction = False désarme l'action — un SEL nul passé à setDoubleAction:, et AppKit n'envoie plus rien. Même logique pour l'apparence : BezeledEditableCells choisit entre un champ qui ne se révèle qu'à la saisie (le renommage du Finder) et un champ qui s'annonce modifiable d'emblée. Aucune des deux n'est objectivement bonne, d'où l'option plutôt qu'un choix imposé.
Le tag d'un NSControl, pour retrouver la cellule qui agit
Quand une case à cocher ou un menu logé dans un tableau déclenche son action, le rappel ne reçoit que le contrôle — ni ligne, ni colonne. Le tag, un NSInteger libre sur tout NSControl, y porte l'indice plat de la cellule, le même que le stockage : ligne x colonnes + colonne.
On revient à l'un et l'autre par une division entière et un modulo. Un encodage du genre ligne x 1000 + colonne marcherait aussi, mais poserait une limite arbitraire ; réutiliser l'indice de stockage n'en pose aucune et ne laisse qu'une arithmétique à vérifier.
Un champ éditable prévient par DEUX chemins
L'action d'un NSTextField part sur Entrée ; controlTextDidEndEditing: part sur la perte du focus. Il faut les deux — on clique très souvent ailleurs sans valider — mais ils arrivent alors tous les deux pour une seule modification.
La parade n'est pas de choisir : c'est de ne rien faire quand la valeur n'a pas reellement changé. Le garde rend les deux chemins idempotents et l'événement ne part qu'une fois.
Une exception Objective-C ne se rattrape pas depuis Xojo
predicateWithFormat: lève une NSInvalidArgumentException sur une chaîne mal formée — vérifié. Un Try...Catch Xojo ne l'intercepte pas : l'exception traverse et termine le processus. Ce n'est pas une erreur qu'on gère, c'est un plantage.
Conséquence sur la forme de l'API : SetPredicateFormat ne doit recevoir que des chaînes produites par PredicateFormat, jamais une saisie d'utilisateur. La méthode le dit dans son propre commentaire plutôt que de le laisser découvrir.
Un NSSet n'a pas d'ordre - « le premier élément » n'existe pas
collectionView:didSelectItemsAtIndexPaths: rend un NSSet, pas un tableau. En tirer « le premier » indice n'a aucun sens : anyObject est le seul accès honnête quand on n'en veut qu'un.
La même prudence vaut pour selectionIndexPaths. Dès que la sélection multiple est permise, exposer un unique « index sélectionné » est une simplification qu'il faut assumer à voix haute.
Un NSCollectionViewItem n'a pas besoin de nib
Vérifié en Objective-C avant d'y compter : setView: sur un NSCollectionViewItem neuf fonctionne, et loadView n'est jamais appelé. C'est la ruse que NativeWindowChrome emploie déjà pour ses NSViewController, et elle évite tout le détour par un fichier d'interface.
Corollaire à ne pas oublier : un élément dont la vue est posée à la main ne dessine aucune sélection. Il faut la peindre soi-même depuis didSelect / didDeselect, en écrivant la couleur au moment du clic pour qu'elle soit résolue dans l'apparence courante.
L'IDE repose en silence un ancrage que la chrome interdit
Sous NSSplitViewController, la règle est : la taille par l'API Xojo — seul geste qui remet en page les enfants d'un DesktopPagePanel —, la position par AppKit. Pour que la seconde tienne, les vues de premier niveau du détail doivent avoir LockRight et LockBottom à False. Avec LockTop et LockBottom à True, Xojo tient la vue pour étirée du haut au bas de la fenêtre et la replace à Top = 0 — sous la barre d'outils — après le placement AppKit.
Le piège est qu'aucune ligne de code ne change : il suffit d'étirer un contrôle jusqu'au bord bas du conteneur dans le designer pour que l'IDE verrouille l'ancrage, sans rien dire. Le symptôme est franc — le titre de la page chevauche celui de la fenêtre, les libellés de la barre d'outils sont traversés par le contenu. Il se vérifie dans le .xojo_window, sur l'en-tête du bloc de premier niveau, avant tout Begin imbriqué. Remettre LockBottom à False ne perd pas la hauteur : l'événement DetailResized la recalcule à chaque passe.
Xojo est insensible à la casse : un identifiant qui ressemble à un mot-clé est le mot-clé
isa est l'opérateur IsA. sub, iF, dO, tO pareil. Et ce n'est pas réservé aux mots-clés : un paramètre nommé color masque le type Color, si bien que Color.RGB(0,0,0) devient une recherche de membre sur ce paramètre.
Une erreur de syntaxe sur la PREMIÈRE instruction d'une méthode arrête l'analyse de toute la classe : ses membres deviennent alors « inexistants » vu des autres classes, et son constructeur est rapporté avec un mauvais nombre d'arguments. Cinq erreurs pour un seul mauvais nom.
Le retain est tout le sujet
NSToolbar.delegate, NSToolbarItem.target, NSControl.target, NSTableView.dataSource, NSPopover.delegate : toutes des références faibles à remise à zéro. ObjCClass.CreateInstance rend donc Retain(init(alloc(cls))).
Sans ce retain, l'instance meurt aussitôt, la référence faible se vide, et aucun rappel ne part jamais. Le symptôme se lit comme « le runtime refuse les références faibles vers une classe créée dynamiquement ». Il ne les refuse pas ; l'objet était simplement désalloué.
La taille par l'API Xojo, la position par AppKit
Écrire DetailPanel.Width = w par la propriété Xojo est le seul geste qui remette les enfants du DesktopPagePanel en page. Puis corriger la position avec PlaceView, en coordonnées AppKit. Ne jamais lire la géométrie Xojo sous un split view controller — elle reste périmée.
Et retirer LockRight / LockBottom des vues de premier niveau : ce verrou est ce qui dit à Xojo « ce contrôle est à moi, je le redimensionne ». Sans quoi Xojo relayoute aussi, contre la largeur de la FENÊTRE, et vous voyez deux passes de mise en page.
reloadData désélectionne
Quatre façons de rafraîchir une liste, et prendre la mauvaise fabrique des bugs. L'appeler depuis la notification de changement de sélection vide la sélection juste avant que l'événement ne lise l'indice — le symptôme se lit « le clic ne fait rien ».
Et un simple setNeedsDisplay: ne fait PAS relire un getter redéfini comme isEmphasized : seul un vrai rechargement le fait, d'où ReloadPreservingSelection.
NSToolbarItemGroup : l'ordre du câblage est tout
AppKit route le clic d'un groupe vers le sous-item, jamais vers le groupe. Mais écrire target/action sur un sous-item DÉJÀ assemblé termine le processus à la première vraie passe de mise en page, sans exception ni événement Closing.
La séquence qui marche : setSubitems: avec un tableau vide → câbler chaque sous-item → setSubitems: avec la liste complète.
ObjCBlock est fourni par Xojo depuis 2019r2
New ObjCBlock(theDelegate As Object) puis .Handle. Le Delegate prend les arguments déclarés du bloc, sans le pointeur de bloc en tête ; bâti sur une méthode d'instance, il porte l'objet avec lui, donc pas besoin du registre de Cocoa.
Pour une API asynchrone, le bloc, son delegate ET l'objet propriétaire doivent tenir dans des PROPRIÉTÉS jusqu'au rappel — jamais des variables locales.
sizeToFit n'existe que sur NSControl
L'appeler sur une NSView simple lève un sélecteur inconnu. NativeSlider.Handle rend une vue hôte composite dès qu'il y a des icônes : NativeControlHost.Center conditionne donc l'appel à Cocoa.Responds.
NSTextAlignment n'a pas les mêmes valeurs selon l'architecture
Le SDK bascule sur TARGET_ABI_USES_IOS_VALUES : sur Apple Silicon, Centre = 1 et Droite = 2 ; sur Intel, l'inverse. D'où les #If TargetARM dans les pastilles et NativeTextField.SetAlignment.
Le SDK 26 a renommé tout NSBezelStyle
Rounded → Push, RegularSquare → FlexiblePush, TexturedRounded → Toolbar, RoundRect → AccessoryBarAction, Recessed → AccessoryBar, Inline → Badge. Les anciens noms ne sont plus que des alias dépréciés.
Et setButtonType: REMET le bezel et l'image à des valeurs propres au type : à appeler AVANT de régler BezelStyle, jamais après.
Trois API où la valeur n'est pas encore là où vous la cherchez
comboBoxSelectionDidChange: — stringValue n'est pas encore à jour, AppKit recopie l'élément choisi APRÈS le rappel : lire objectValueOfSelectedItem. clickedPathItem — valide UNIQUEMENT pendant l'envoi de l'action. NSAlert.suppressionButton — à lire APRÈS RunModal.
NSAlert : l'ordre d'ajout, pas l'ordre à l'écran
Le premier bouton ajouté est le bouton par défaut, donc celui de droite, celui que Retour déclenche. Avec une action destructrice il faut donc déplacer l'équivalent clavier — une action irréversible ne doit jamais partir par réflexe.
Et NSAlert n'a pas de notion de « destructif » : ce sont ses boutons qui l'ont, buttons étant un tableau de NSButton.
La barre RTF n'appartient pas à l'éditeur, mais à la fenêtre
usesInspectorBar installe un NSTitlebarAccessoryViewController sur la fenêtre — vue __NSInspectorBarView, 28 points, sous la barre d'outils et sur toute la largeur — et rétrécit la vue de contenu d'autant. Elle ne peut donc pas être confinée dans un cadre : elle est au niveau de la fenêtre, exactement comme dans TextEdit.
Toute mise en page qui positionne ses vues elle-même doit être refaite après l'appel, sinon la barre recouvre le contenu au lieu de le pousser. Et contrairement à ce qu'on suppose volontiers, ni le scroll view ni l'ordre des appels n'y changent rien — vérifié en Objective-C sans scroll view, réglé avant setDocumentView:, puis hors fenêtre : l'accessoire est installé dans les trois cas. Pour une barre dans le cadre il faut une autre voie : la barre du composant, ou l'accessoire de règle ci-dessous.
L'accessoire de règle plante si le client n'est pas posé d'abord
C'est la seule façon native de loger une barre dans la zone de texte : l'accessoire devient sous-vue de la NSRulerView, donc du scroll view, sans toucher à la fenêtre. Mais setAccessoryView: lève une exception si le clientView de la règle n'a pas été posé avant — « you must set the client view of the ruler before you can have an accessory view ». Ce n'est pas un appel sans effet, c'est un plantage.
Et la règle graduée vient avec : 60 points mesurés pour 28 d'accessoire. Pour une barre seule dans un cadre, celle de NativeRichTextEditor reste préférable.
Une commande de formatage part au premier répondant
changeFont: et changeColor: ne visent pas le contrôle qu'on croit : le panneau Police et le panneau Couleurs les envoient au premier répondant. Sans makeFirstResponder préalable, la commande part dans le vide, sans erreur.
Auto Layout et placement par cadre : ce qui se règle tout seul, et ce qui ne se règle pas
NSGridView et NSStackView travaillent en Auto Layout, alors que toute la bibliothèque place par cadre. Vérifié en Objective-C : construits par initWithFrame:, tous deux gardent translatesAutoresizingMaskIntoConstraints à YES — ils se placent donc par cadre comme le reste. Et addRowWithViews: comme addArrangedSubview: mettent ce drapeau à NO sur les vues qu'on leur confie : l'appelant n'a rien à régler.
Deux réserves. La fabrique de classe stackViewWithViews:, elle, rend une pile à NO — elle ignorerait son cadre ; d'où initWithFrame: dans le constructeur. Et fittingSize s'exprime en rectangles d'alignement : le cadre d'un NSButton déborde du sien de quelques points, donc une grille de boutons ajustée au pixel près paraît rognée — Refit prend un supplément pour cela.
Deux classes runtime de même nom : le sélecteur disparaît sans un mot
ObjCClass réutilise une classe déjà enregistrée — c'est voulu, et c'est ce qui évite la prolifération d'une fenêtre à l'autre. Mais si deux classes différentes demandent le même nom, la seconde hérite de la première et son AddMethod est un no-op silencieux : sa cible ne répond jamais à son sélecteur.
Le symptôme ne pointe nulle part. Un menu s'ouvre, on choisit un item, popUpMenuPositioningItem: rend YES — un item A bien été choisi — et rien ne se passe : l'action est partie au néant faute de répondant. Depuis, AddMethod vérifie sur une classe réutilisée que le sélecteur y est réellement, et lève sinon. Arrivé une fois, entre NativeMenuToolbarItem et NativeMenu, tous deux sur « VDSMenuTarget ».
Le menu palette ne peut pas être déroulé seul
Un menu en presentationStyle Palette doit être le sous-menu d'un item appartenant à un menu ordinaire. L'en-tête est net : il ne peut être ni ouvert par popUpMenuPositioningItem:, ni attaché directement à un menu surgissant ou à un item de barre d'outils. AddColorPalette impose donc cette forme — on lui donne le titre de l'item porteur, pas un menu à part.
Bonne nouvelle en revanche : son gestionnaire de sélection est facultatif. On passe Nil et on lit selectedItems après coup, ce qui évite d'avoir à fabriquer un bloc Objective-C. Et selectionMode n'agit qu'à l'intérieur d'un GROUPE — les items entre deux séparateurs.
Un événement Boolean non implémenté rend False
Ce détail dicte le nom des événements d'une bibliothèque. Un Event WillDrag() As Boolean aurait interdit tout glissement tant que l'appelant ne l'écrit pas — le contraire de ce qu'on attend d'un réglage par défaut.
D'où CancelDrag, inversé : rendre True annule, et ne rien écrire laisse passer. NativeDropView garde au contraire Dropped au sens direct, parce qu'un dépôt doit être traité pour avoir un sens.
Une sélection programmée prévient le délégué
selectRowIndexes:byExtendingSelection: déclenche tableViewSelectionDidChange: exactement comme un clic — NativeSidebar.SelectPage suffit donc à faire suivre la page, sans la pousser une seconde fois.
Avec une réserve mesurée : AppKit ne notifie pas une sélection inchangée. Viser la ligne déjà sélectionnée ne produit aucun événement — ce qui est correct, rien n'a bougé, mais un appelant qui comptait sur l'événement pour se synchroniser doit le savoir.
L'origine d'une vue non flipped est en bas à gauche
La frame d'un NSDraggingItem est exprimée dans les coordonnées de la vue qui ouvre la session. La laisser à (0,0) ne met pas la vignette « au début » : elle la colle dans le coin inférieur gauche, loin du curseur.
Pour qu'elle suive la souris : convertPoint:fromView: avec Nil — qui signifie « depuis les coordonnées de la fenêtre » — sur le point rendu par locationInWindow, puis un décalage d'une demi-vignette pour la centrer. Vérifiable sans souris : une vue en (100,50), un curseur en (180,110) dans la fenêtre, et l'on doit lire (80,60).
Un CGPoint par valeur se lit comme deux Double
Les sélecteurs de source de glissement — hitTest:, draggingSession:endedAtPoint:operation: — annoncent un CGPoint passé par valeur. Deux Double consécutifs occupent exactement les mêmes registres que la structure : le délégué Xojo peut donc les recevoir séparément, sans déclarer de structure.
Vérifié en Objective-C plutôt que supposé : une méthode installée avec l'encodage "@@:{CGPoint=dd}" mais implémentée comme (id, SEL, double, double) reçoit bien les bonnes valeurs.
Ce qu'on dépose n'est pas forcément un fichier
Un FolderItem issu d'un dépôt sait s'il est un dossier — IsFolder. Écrire « fichier » pour tout est une facilité, pas une limite du glisser-déposer.
Avec une nuance : un paquet — .app, .pkg — est un répertoire, donc IsFolder le compte parmi les dossiers. Pour le traiter comme un document, ce que fait le Finder, il faut regarder son extension. L'icône rendue par NSWorkspace.iconForFile:, elle, montre déjà la différence.
Deux façons de lire une constante exportée, et elles ne s'échangent pas
dlsym rend l'adresse du symbole. Pour une constante NSString — identifiant d'item de barre d'outils, type de presse-papiers — le symbole contient un pointeur vers l'objet : il faut déréférencer, slot.Ptr(0). Pour un symbole déclaré comme un tableau, tel _NSConcreteGlobalBlock, l'adresse est l'objet : déréférencer donnerait n'importe quoi.
D'où Cocoa.ExportedString pour le premier cas, et l'écriture directe pour le second. Les deux tournent dans la bibliothèque, à trois lignes d'écart.
Ce que le système ne retient pas pour vous
Un NSStatusItem disparaît dès que la variable qui le tient sort de portée : la barre de menus ne le retient pas. Et Remove n'est pas optionnel — sans lui, l'extra reste dans la barre jusqu'à la fin du processus, même l'objet Xojo détruit.
Même famille que les blocs asynchrones et les cibles d'action : dès qu'AppKit ne détient qu'une référence faible, ou aucune, c'est une PROPRIÉTÉ qu'il faut, jamais une locale.
Un NSOutlineView EST un NSTableView
La chaîne se lit au runtime : NSOutlineView → NSTableView → NSControl → NSView. Une arborescence n'est donc pas un contrôle voisin du tableau, c'est un tableau qui indente sa première colonne. En écrire deux classes indépendantes coûte deux fois le code des cellules, et la seconde finit toujours la plus pauvre : l'arbre n'avait ni case à cocher, ni menu, ni cellule éditable, ni couleur, ni alignement, ni en-tête — rien qu'AppKit interdisait, seulement ce que la duplication avait laissé de côté.
Faire hériter a supprimé dix implémentations jumelles et n'a laissé que dix redéfinitions : les sélecteurs de la source de données, la colonne qui porte les triangles de repli, le vidage, le refus du tri, la traduction ligne visible ↔ ligne de stockage, l'ajout de ligne devenu ajout de nœud, et trois relais qui lèvent en plus l'événement du nœud. La même parenté vaut ailleurs dans la bibliothèque : NativeSidebar et NativeOutlineSidebar tiennent de NativeSidebarBase, et quatre items de barre d'outils de NativeToolbarItem.
Xojo répartit les appels depuis Super.Constructor
Une redéfinition de sous-classe est bien atteinte depuis le constructeur de la classe de base : l'objet porte sa classe réelle dès l'allocation — modèle Java, pas C++. C'est ce qui permet à un constructeur unique de bâtir le commun et de laisser la sous-classe fournir ses différences. Une condition : la redéfinition ne doit toucher qu'à ses paramètres, les propriétés de la sous-classe n'étant pas encore posées à cet instant.
Et il faut le constater, pas le supposer. Un repli silencieux sur l'implémentation de base ne lève rien : on obtient un contrôle qui ne dessine simplement rien, symptôme trop discret pour qu'on remonte jusqu'à sa cause. Un Cocoa.Responds(mDelegate, "<sélecteur ajouté par la redéfinition>") juste après la construction, suivi d'un System.DebugLog en cas d'échec, change la supposition en constat pour le prix d'un appel.
Un repli n'est pas « ne rien faire »
Tout ce qui date de macOS 26 est posé derrière un Cocoa.Responds — le SÉLECTEUR interrogé, jamais le numéro de version. Mais la question suivante compte autant : que fait-on quand il est absent ? Pour une forme de bordure ou une prominence de teinte, ne rien faire est juste — le contrôle garde son aspect, et l'application est simplement moins fine. Pour placeholderStrings, ne rien faire laisserait le champ sans aucune invite : ce n'est plus une dégradation, c'est une régression. On y pose donc la PREMIÈRE des invites.
La règle se lit en une phrase : le repli doit rendre le meilleur comportement disponible, pas l'absence de comportement. Un test de sélecteur qui protège du plantage tout en cassant la fonction n'a fait que déplacer le défaut.
Une propriété de NSView appartient à toutes les classes
prefersCompactControlSizeMetrics (macOS 26) est déclarée sur NSView, pas sur un contrôle particulier : elle vaut donc pour les vingt-et-une classes que la bibliothèque enveloppe. La recopier dans chacune serait vingt fois la même ligne à corriger le jour où elle change. Elle est posée UNE fois, en méthode partagée de NativeControlHost, qui prend le Handle de n'importe quel contrôle.
Même raisonnement pour les énumérations : NSControlBorderShape sert à NSButton et à NSSegmentedControl, NSTintProminence à NSButton et à NSSlider. Les deux vivent dans NativeControlHost et les classes s'y réfèrent — la même raison qui fait qu'une arborescence hérite d'un tableau plutôt que de le recopier.
Lire le SDK, et sonder plutôt que supposer
Les seuils de NSLevelIndicator ne sont documentés que dans un sens — « values above the warning threshold ». Il n'existe aucun moyen documenté de les inverser : tout autre sens se peint à la main via fillColor.
Et quand un réglage « ne change rien » à l'écran : pousser une valeur absurde et reconstruire UNE fois. Si ça bouge, le chemin de code est vivant et les retouches étaient trop subtiles ; sinon, le binaire qui tourne n'est pas le code édité.