Analys av Cocotrons NSTableView implementation

En fortsättning på min tidigare förvirrade Samlingsvyartikel . Målet är att begripa hur en hemgjord samlingsvy (alltså en vy som presenterar och tillåter editering av många likartade objekt) kan göras. Typexemplet är NSTableView.

Har funnit god inspiration hos Cocotron . Där i källkoden hittar man alla (?) AppKit-klasser implementerade av Christopher Lloyds mfl.

Så här följer djupanalys av vad NSTableView gör i Cocotrons tappning. Notera att detta kräver en god Cocoa-kunskap för att följa.

Ola Bäckström 25 juni 2008

Bakgrund

En NSTableView ärver från NSControl. Det är alltså en NSView som kan hantera input från användaren (och naturligtvis presentera output). De flesta standard-kontroller lämnar över delar av ansvaret till objekt av typen NSActionCell. Dessa celler kan presentera information (alltså “rita” via drawWithFrame:inView:) men också lagra information (se NCCells metoder setObjectValue:, setStringValue:, setDoubleValue: etc).

Men hur snacket mellan kontrollen och actioncellen ser ut är inte särskilt tydligt beskrivet i Apples enda (?) sida om detta . Det som står där gäller inte helt riktigt för NSTableView heller, utan passar bättre på enklare klasser som NSButtonView.

Så om du vill veta mer … häng med!

Open source alternativet Cocotron

Cocotronprojektets uttalade mål är:

…is to provide an easy to use cross-platform solution for Objective-C development. In particular, source code level compatibility with … Apple’s frameworks…

Så trots att det är klasser avsedda att köras på icke-OS X-maskiner (Windows eller Linux) så är det intressant att läsa källkoden. Apples egen hemliga AppKit-källkod kan inte avvika väldigt mycket, eftersom klassernas uppförsel är tänkt vara de samma. Kan nämna att källkoden ser ut att vara i bra skick och att det måste ha varit ett hästjobb att i princip skriva om hela Cocoa från början.

NSTableView, intressanta delar i implementationen

Det jag speciellt vill veta är detta:

  1. Ansvarsfördelning NSControl mot NSActionCell när det gäller hantering av användarinput.
  2. På vilket sätt bindings kommer in i bilden. Är NSActionCell involverat över huvud taget?

Det som vi inte får fram i denna dissektion är hur IB-paletter fungerar och inte heller någonting om NSEditor. Det får bli en senare fråga.
Bindings kommer vi bara delvis in på, men jag kan svara redan nu att NSActionCell har inget direkt med bindings att göra. Däremot håller objekt av klassen NSTableColumn reda på bindingarna, medan själva NSTableView ser till att dataflödet uppströms fungerar, mha KVC.

Till saken, nu spanar vi in i Cocotrons implementation av NSTableView.m :

Snabböversikt

Den stora majoriteten av metoderna är setters/getters för alla de olika attributen (eller properties, på obj-c-språk), exempel är rowHeight och intercellSpacing . Inget konstigt där.

Sedan finns några enkla hjälpfunktioner för att lokalisera rader/kolumner/celler i vyn. Exempel är

rectOfRow:
rectOfColumn:
rowAtPoint:
frameOfCellAtColumn:
rowsInRect:

de gör det man skulle tro de gör. Bra med tydliga metodnamn!

Det finns ett antal getters/setters för datakälla (dataSource) och delegat, verkar ganska rättframt, på några ställen kollas om datakällan verkligen svarar på tilltal. Noteras är att klassen inte kräver att man bara väljer en metod att förse den med tabellinformation: det är fullt möjligt att både ange datasource och via bindings knyta den till en NSArrayController. Det verkar dock som databinding har prioritet över datakälla.

Scenario: Användare klickar på något inne i vyn.

Eftersom vi ärver in från NSResponder (indirekt via NSControl) så kommer ett musklick att generera ett anrop till mouseDown:. Implementationen här är intressant; jag listar en pseudo-kod baserat på den riktiga källkoden:

-(void)mouseDown:(NSEvent *)event {
    Finn aktuell rad och kolumn som ligger muspekaren befinner sig på.
    Finn vilken cell, låt oss kalla den clickedCell, som ligger på denna rad och kolumn. 
    OM ( clickedCell ärver från NSButtonCell )
        Se till att clickedCell har rätt värde enl. ev. datakälla.
        Tala om för clickedCell att den blev tryckt på.
        Informera dataKällan (dataSource) om nytt cell-värde.  // men binding då? går det inte knyta en knapp i Cocotron?
        Hissa NeedDisply-flaggan för vyn.
    ANNARS
        OM ( det är ett enkelklick )
			Långt tjorvigt block som reder ut om det påverkar markeringen (bla beroende på ALT- eller SHIFT-knappar).
			Långt tjorvigt block som reder ut om det är en början till drag&drop.
			Skicka target-action.
		OM ( det är ett dubbelklick )
			OM ( datakällan eller ev. binding tillåter editering)
				Påbörja celleditering genom att anropa egna metoden editColumn:row:withEvent:select: 
			ANNARS
        		Skicka target-doubleAction.
}

Koden här är i verkligheten än mer grisig, men den ska alltså separera på ett antal olika operationer, knapptryckning, markering och drag&drop samt slutligen editering av cellinnehåll. Notera att i fallen drag&drop och editering så slutförs inte operationen här: NSTableView håller reda på att en av dessa operationer har påbörjats.

Jag misstänker att det saknas lite kod runt knapptryckning som har med databinding att göra: det verkar inte vara möjligt att knyta upp en knapp.

Anta att användaren dubbelklickat, för att editera en cell, då blir innehållet i editColumn:row:withEvent:select:
en rackarns viktig funktion. Denna anropas internt från 2 andra platser, nämligen

  1. mouseDown:, som vi tittat på ovan. Det gäller
  2. textDidEndEditing:. Aha, Eureka, här kommer Field editor in i bilden. Den här metoden anropas av fälteditorn när en celleditering är färdig. Lite beroende på konfiguration så kan en ny celleditering påbörjas. Trycker användaren på TAB så ska cellen till höger påbörjas att editeras.

Men hur kan fälteditorn anropa något i ett NSTableView-objekt? Jo, vid varje texteditering så riggas en delegat till fälteditorn -och i det här fallet sätts då själva NSTableView-objektet. Notera att det inte är den editerade cellen som är delegat!

Scenario: Editering av textfält påbörjas

Nu behöver vi grotta i editColumn:row:withEvent:select:


-(void)editColumn:(int)column row:(int)row withEvent:(NSEvent *)event select:(BOOL)select {

	Ett litet stycke som verifierar att: 
		1. kolumn tillåter editering
		2. delegat tillåter editering
		3. datakälla (dataSource) tillåter editering ELLER databinding är read-write

	// nedan börjar allvaret...
	Kopiera (via NSCopying) den aktuella cellen, kalla denna nya cell för editingCell 
	OM ( editingCell ärver från NSTextFieldCell )    
		Justera lite attribut på editingCell, för att se till att den ritar exakt över den ursprungliga cellen.
		Se till att editingCell har rätt värde enl. ev. datakälla.
		Se till att editingCell har rätt värde enl. ev. databinding.
		Begär att få tag på fönstrets fält-editor.
		Be editingCell att konfigurera fält-editorn.   // via NSCells metod setUpFieldEditorAttributes:		 
		OM (select-flaggan hissad)
		 	anropa editingCells	selectWithFrame:inView:editor:delegate:start:length: 
		ANNARS
			anropa editingCells editWithFrame:inView:editor:delegate:event:

	Hissa NeedDisply-flaggan för vyn.
}   

Här tror jag Cocotron har genat lite, de tillåter alltså bara texteditering om tabellen består av NSTextFieldCell. Men i AppKit kan man stoppa in egna celler.

aha…tag ett djupt andetag nu… som du ser anropas editWithFrame:... eller selectWithFrame:... på cellen, dessa anropar i sin tur en privat/hemlig metod _setupFieldEditorWithFrame:controlView:editor:delegate: ärvt från NSCell. Det är den metoden som gör de viktiga anropen för att “sätta i gång” fälteditorn:

[view addSubview:editor];
[[view window] makeFirstResponder:editor];
[editor setDelegate:delegate];

notera att mer än ovanstående görs; saker som har med scrollbarhet (NSClipView) att göra.

OJOJ. Jag tror jag måste göra en figur baserat på detta. Kanske ett lämpligt ämne för en kommande artikel?

,

---
---