MaCocoa 058

Capitolo 058 - Mela C Mela V

Benché l'argomento più succoso del capitolo sia la realizzazione delle operazioni di Copia ed Incolla sugli elementi della copertina, sono presenti anche altri interessanti pezzi di codice: gli elementi di testo sono in grado di ripartire il loro testo su più colonne, e sono corretti una serie di errori. Ma per cominciare si parla di XML.

Sorgente: quando non si sa dove sbattere la testa, si copia da qualcuno (nel caso, dall'esempio Sketch).

Prima stesura: 13 settembre 2004

Proprietà XML

Non sapendo come realizzare le operazioni di copia ed incolla sulla finestra che contiene la copertina, sono andato a guardare un esempio, ovvero Sketch, fornito di serie con i Developer's Tool. Nel farlo, ho notato che gli elementi grafici sono passati nella clipboard (o Archivio Appunti) in formato XML. è una cosa simpatica ed interessante, ed anche piuttosto facile.

In effetti, non si tratta che di realizzare una versione alternativa dell'accoppiata encodeWithCoder/initWithCoder. Pigliamo ad esempio il metodo che produce una property list (che è proprio un documento che segue gli standard XML per conservare informazioni) per un elemento generico CCE_BasicForm:

- (NSMutableDictionary *)
getXMLDescription
{
    // un dizionario per contenere le varie proprieta' XML
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    // nome della classe
    [dict setObject: NSStringFromClass([self class])
            forKey: kStrElem_ClassName ];
    // tutte le altre variabili d'istanza fondamentali
    [dict setObject:[NSString stringWithFormat:@"%d", objID]
            forKey: kStrElem_objID ];
    [dict setObject:NSStringFromPoint(drawPoint)
            forKey: kStrElem_drawPoint ];
    [dict setObject:[NSString stringWithFormat:@"%f", rotAngle]
            forKey: kStrElem_rotAngle ];
    [dict setObject:[NSString stringWithFormat:@"%d", numOfHdl]
            forKey: kStrElem_numOfHdl ];
    [dict setObject:NSStringFromSize(localSize)
            forKey: kStrElem_localSize ];
    [dict setObject:[NSString stringWithFormat:@"%f", cceLineWidth]
            forKey: kStrElem_cceLineWidth ];
    [dict setObject:[NSArchiver archivedDataWithRootObject: cceFillColor ]
            forKey: kStrElem_cceFillColor ];
    [dict setObject:[NSArchiver archivedDataWithRootObject: cceLineColor ]
            forKey: kStrElem_cceLineColor ];
    [dict setObject:(cceIsFilled ? @"YES" : @"NO")
            forKey: kStrElem_cceIsFilled ];
    [dict setObject:(cceIsStroked ? @"YES" : @"NO")
            forKey: kStrElem_cceIsStroked ];
    [dict setObject:(cceIsLocked ? @"YES" : @"NO")
            forKey: kStrElem_cceIsLocked ];
    return dict;
}

Costruisco un dizionario, all'interno del quale inserisco, con opportune chiavi, le varie variabili d'istanza in un formato opportuno. Numeri, rettangoli, punti, eccetera, sotto forma di stringhe, mentre oggetti più complicati (come NSColor) in un formato d'archivio. Da notare che all'inizio è inserito anche il nome della classe dell'elemento. In effetti questo sarà un punto chiave per la procedura opposta, che costruisce un elemento a partire da una property list. La procedura è divisa in due metodi: il primo metodo serve proprio a costruire un elemento:

+ (id)
buildElemFromXMLDesc:(NSDictionary *)dict
{
    id elem = nil;
    // determino il tipo della classe
    Class theClass = NSClassFromString(
            [dict objectForKey: kStrElem_ClassName ]);
    // se ce l'ho, vado a costruire l'elemento
    if (theClass)
    {
        // costruisco l'elemento
        elem = [ [ [ theClass alloc ] init] autorelease];
        // e poi finisco di caricarlo con i suoi dati
        if (elem)
        {
            [elem loadElemXMLDesc:dict];
        }
    }
    return elem;
}

Si tratta di un metodo di classe (che un + davanti al nome del metodo); estrae dal dizionario il nome della classe, ed in base a tale nome costruisce un elemento di classe corretta. Passa poi il controllo ad un altro metodo per terminare le operazioni di riempimento delle variabili d'istanza. Nella realizzazione base, il metodo è il seguente:

- (void)
loadElemXMLDesc:(NSDictionary *)dict
{
    // estraggo ogni singola variabile e l'assegno
    objID = [ [ dict objectForKey: kStrElem_objID ] intValue ] ;
    drawPoint = NSPointFromString( [dict objectForKey: kStrElem_drawPoint ] ) ;
    rotAngle = [ [ dict objectForKey: kStrElem_rotAngle ] floatValue ] ;
    numOfHdl = [ [ dict objectForKey: kStrElem_numOfHdl ] intValue ] ;
    localSize = NSSizeFromString( [ dict objectForKey: kStrElem_localSize ] ) ;
    [ self calcLocalTranform ];
    [ self setTheDrawPath: [ NSBezierPath bezierPathWithRect:
            NSMakeRect(0, 0, localSize.width, localSize.height) ] ];
    [ self buildHdlList ];
    cceLineWidth = [ [dict objectForKey: kStrElem_cceLineWidth ] floatValue ] ;
    [ self setCceFillColor: [ NSUnarchiver unarchiveObjectWithData:
                        [dict objectForKey: kStrElem_cceFillColor ] ] ];
    [ self setCceLineColor: [ NSUnarchiver unarchiveObjectWithData:
                        [dict objectForKey: kStrElem_cceLineColor ] ] ];
    cceIsFilled = [ [dict objectForKey: kStrElem_cceIsFilled ]
                            isEqualToString:@"YES"];
    cceIsLocked = [ [dict objectForKey: kStrElem_cceIsLocked ]
                            isEqualToString:@"YES"];
    cceIsStroked = [ [dict objectForKey: kStrElem_cceIsStroked ]
                            isEqualToString:@"YES"];
    cceIsSelected = FALSE ;
}

È in effetti concettualmente molto simile ad initWithCoder:: anche qui sono inizializzate tutte le variabili d'istanza estraendone i valori dal dizionario fornito come parametro.

Come è costume, alcune sottoclassi di CCE_BasicForm devono riscrivere i due metodi getXMLDescription e loadElemXMLDesc nel caso in cui possiedano variabili d'istanza aggiuntive (CCE_Text, CCE_Image e CCE_ElemGroup). Non si deve riscrivere buildElemFromXMLDesc, che è unico per tutta la gerarchia degli elementi.

Mi interessa in particolare evidenziare i metodi per la classe CCE_ElemGroup:

- (NSMutableDictionary *)
getXMLDescription
{
    // la descrizione dell'elemento stesso
    NSMutableDictionary * dict = [super getXMLDescription];
    // ed un array con la descrizione degli elementi del gruppo
    NSMutableArray        * newArr = [ NSMutableArray arrayWithCapacity: 1 ] ;
    int                    i , numElem ;
    // ciclo sugli elementi del gruppo
    numElem = [ elemArray count ];
    for ( i = 0 ; i < numElem ; i ++ )
    {
        CCE_BasicForm        * elem ;
        NSMutableDictionary * dictx ;
        // piglio l'elemento
        elem = [ elemArray objectAtIndex: i ];
        // e costruisco un dizionario con le sue proprieta'
        dictx = [ elem getXMLDescription ];
        // che aggiungo allo array
        [ newArr addObject: dictx ];
    }
    // al dizionario aggiungo appunto lo array
    [dict setObject: newArr forKey: kStrElem_elemArray ];
    return ( dict );
}

Per la costruzione della property list, oltre alle variabili standard, bisogna aggiungere un vettore, che contiene tutte le descrizioni degli elementi del gruppo.

Speculare la costruzione di un elemento a partire dalla property list:

- (void)
loadElemXMLDesc:(NSDictionary *)dict
{
    NSArray        * newArr ;
    int            i ;
    // predispongo le variabili comuni
    [super loadElemXMLDesc:dict];
    // recupero lo array con i vari elementi del gruppo
    newArr = [ NSArray arrayWithArray:(NSArray *)
                [dict objectForKey: kStrElem_elemArray ] ] ;
    // guardo i singoli elementi
    for ( i = 0 ; i < [ newArr count ] ; i ++ )
    {
        CCE_BasicForm        * elem ;
        NSMutableDictionary * dictx ;
        // recupero la descrizione dell'elemento in XML
        dictx = [ newArr objectAtIndex: i ];
        // costruisco l'elemento stesso
        elem = [ CCE_BasicForm buildElemFromXMLDesc: dictx ] ;
        // e lo aggungo al gruppo di elementi
        [ elemArray addObject: elem ] ;
    }
}

Per vedere cosa salta fuori da un meccanismo del genere, ho aggiunto ai pulsanti di debug presenti nella CoverWinCtl una serie di istruzioni per scrivere i dati su file (che ho copiato brutalmente dalla documentazione Apple):

- (void)
clicBtn02: (id)sender
{
    NSString    * path = @"/Users/djzero00/Temporaneo/lista.plist";
    NSData        * xmlData;
    NSString    * error;
    
    xmlData = [NSPropertyListSerialization
            dataFromPropertyList:[[ cvrView theElements] getXMLDescription ]
            format:NSPropertyListXMLFormat_v1_0
            errorDescription:&error];
    if(xmlData)
    {
        NSLog(@"No error creating XML data.");
        [xmlData writeToFile:path atomically:YES];
    }
    else
    {
        NSLog(error);
        [error release];
    }
}

In pratica a partire dalla property list dell'elemento theElements (che, essendo il gruppo principale, contiene tutti gli elementi di una copertina), si costruisce una rappresentazione della stessa sotto forma di dati nella variabile xmlData, che poi è scritta all'interno di un file.

figura 01

figura 01

figura 02

figura 02

Il contenuto del file lista.plist si può esaminare tramite un qualsiasi editor di testo oppure tramite l'applicazione Property List Editor, fornita di serie con Mac Os X. Ricordo che le preferenze dell'applicazione (ma non solo, anche molte altre caratteristiche) sono conservate in property list o file XML, niente altri che file di tipo testo con una opportuna struttura imposta da una serie di tag.

Ma sto divagando. Torno al meccanismo di copia ed incolla.

Copia e Taglia

Per realizzare il meccanismo di Copia, Taglia ed Incolla, bisogna realizzare tre metodi, che sono ovviamente chiamati:

- (IBAction) copy:(id)sender ;
- (IBAction) cut:(id)sender ;
- (IBAction) paste:(id)sender ;

Questi metodi devono essere inseriti all'interno di un First Responder; se il First Responder corrente è uno predefinito (come accade ad esempio quando si sta editando un campo di testo, o un elemento CCE_Text), il meccanismo è già attivo. Non è ancora attivo invece quando il First Responder è quello normale per una CoverView, cioé la CoverView stessa. In effetti, bisogna dire ad una CoverView cosa copiare e cosa incollare.

L'idea alla base del meccanismo è la seguente: se ho un po' di elementi selezionati nella copertina, posso provare a copiarli (o tagliarli). Nel caso, prendo una rappresentazione di questi elementi sotto forma di property list e la inserisco all'interno della clipboard. Se invece c'è qualcosa nella clipboard, si può provare ad incollare gli eventuali elementi CCE_BasicForm presenti all'interno della CoverView, estraendoli da una property list.

La clipboard è realizzata attraverso la classe NSPasteboard (l'ho già incontrata parlando di drag and drop); questa classe può contenere un po' di tutto, sia oggetti predefiniti come testo, immagini, stringhe, eccetera, ma anche tipi strani, proprietari (come sarà nel caso di elementi CCE_BasicForm).

Sbrigo subito il meccanismo di Cut (Taglia), che è di una semplicità inaudita:

- (IBAction)
cut:(id)sender
{
    [self copy:sender];
    [ self deleteSelectedElements ];
    [[theElements undoManager] setActionName: @"CoverView -> cut"];
}

In effetti, l'operazione di tagliare non è altro che una operazione di copiatura seguita dalla cancellazione degli elementi coinvolti.

Le cose si complicano col metodo di copia. Ora, all'interno di una classe NSPasteboard posso inserire solo dati racchiusi in una classe NSData. La cosa è complicata dal fatto che è possibile eseguire operazioni di copia su più di un elemento alla volta (su tutti quelli selezionati). Allora, i dati sono così costruiti: costruisco un NSDictionary con all'interno un unico oggetto, che è però un NSMutableArray; questo array consiste di una serie di NSDictionary, uno per ogni elemento selezionato.

In base a quanto detto, si spiegano le operazioni di copia:

- (IBAction)
copy:(id)sender
{
    NSMutableDictionary * copyPasteDict ;
    NSMutableArray    * elementsDict, * selArr ;
    int                i, numElem ;
    NSString        * dictRepAsStr ;
    // accedo alla clipboard
    NSPasteboard    * pb = [NSPasteboard generalPasteboard];
    // recupero gli elementi selezionati
    selArr = [ theElements getSelectedGraphics ];
    numElem = [ selArr count ];
    // se non ce ne sono, ho gia' finito
    if ( numElem == 0 ) return ;
    // dico che nella clipboard ci sara' un mio tipo proprietario
    [ pb declareTypes: [NSArray arrayWithObjects:CCE_ELEMTYPE, nil]
                owner:nil];
    // mi serve un NSData da mettere nella clipboard
    // costruisco un dizionario con gli elementi selezionati
    copyPasteDict = [ NSMutableDictionary dictionary ];
    // il dizionario e' composto da un array
    elementsDict = [NSMutableArray arrayWithCapacity: numElem ];
    for (i = 0; i < numElem; i++)
    {
        // ogni elemento dello array e' un dictionary che raccoglie
        // la descrizione dell'elemento in XML
        NSMutableDictionary * elemDict ;
        // descrizione dell'elemento in XML
        elemDict = [[selArr objectAtIndex:i] getXMLDescription] ;
        // aggiungo il dizionario allo array
        [elementsDict addObject: elemDict ];
    }
    // il dizionario principale e' composto dal solo array
    [ copyPasteDict setObject:elementsDict forKey: @"Lista Elementi"];
    // trasformo il dizionario in una stringa
    dictRepAsStr = [copyPasteDict description];
    // e quindi in un NSData che inserisco nella clipboard
    [ pb setData:[dictRepAsStr dataUsingEncoding: NSASCIIStringEncoding]
                forType: CCE_ELEMTYPE ];
}

Piglio la NSPasteboard generica (ce ne sarebbero altre di specializzate in font, in righello, eccetera, ma non mi interessano). La prima operazioneda fare è di specificare che tipo di dati saranno inseriti all'interno della clipboard. Dico che sono dati CCE_ELEMTYPE (una stringa che dice @"LS CdCatalog Element"). Seguono poi le operazioni di costruzione del dizionario generale, e del vettore dei dizionari propri di ogni elemento. Alla fine, il dizionario principale è convertito prima in una stringa e poi in un oggetto NSData, che può finalmente essere passato alla clipboard.

figura 03

figura 03

Poiché ciò che ho copiato nella clipboard è in formato proprietario, comprensibile solo dalla mia applicazione, non se ne può vedere direttamente in contenuto con qualche applicazione d'utilità. Si può però usare l'esempio ClipboardViewer (fornito coi Developer's Tool, nella cartella Examples/AppKit), che appunto visualizza i dati grezzi presenti nella clipboard.

figura 04

figura 04

figura 05

figura 05

Si tratta ancora di una property list, questa volta però in formato puramente testo (non è stata effettuata l'operazione di serializzazione, che avrebbe rappresentato il tutto con i tag).

Incolla

L'operazione opposta, per incollare gli elementi eventualmente presenti nella clipboard all'interno della CoverView, è a questo punto piuttosto facile da spiegare.

- (IBAction)
paste:(id)sender
{
    NSArray        * elementsDict ;
    int            i, numElem ;
    NSDictionary * copyPasteDict ;
    NSString    * dictRepAsStr ;
    // accedo alla clipboard
    NSPasteboard * pb = [NSPasteboard generalPasteboard];
    // guardo che tipo di dati e' presente
    NSString    * type = [pb availableTypeFromArray:
                        [NSArray arrayWithObjects:CCE_ELEMTYPE, nil]];
    // se ci sono tipi di dati che posso trattare
    if (! [type isEqualToString:CCE_ELEMTYPE])
        return ;
    // se arrivo qui, ho dati per me nella clipboard
    // estraggo i dati sotto forma di stringa
    dictRepAsStr = [[NSString allocWithZone:[self zone]]
                initWithData:[pb dataForType:type]
                encoding:NSASCIIStringEncoding];
    // che trasformo in un dizionario
    copyPasteDict = [dictRepAsStr propertyList];
    [dictRepAsStr release];
    // dal quale ricavo poi lo array con la descrizione degli elementi
    elementsDict = [copyPasteDict objectForKey:@"Lista Elementi"];
    numElem = [elementsDict count];
    // non dovrebbe accadere...
    if ( numElem <= 0 )    return ;
    // deseleziono tutto
    [ theElements cascadeSelection: NO ] ;
    // per ogni elemento dello array costruisco un l'elemento descritto
    for (i = 0; i < numElem ; i++)
    {
        id elem ;
        elem = [ CCE_BasicForm buildElemFromXMLDesc: [elementsDict objectAtIndex:i]] ;
        [ elem setOwnerView: self ];
        [ elem setCceIsSelected: YES ];
        [ theElements addElem:elem atIndex: 0 ];
    }
    [[theElements undoManager] setActionName: @"CoverView -> paste"];
}

Si accede alla clipboard, e si cerca di capire se al suo interno si trovano dati di interesse: devono essere del tipo CCE_ELEMTYPE, dacché al momento l'applicazione non è in grado di interpretare altri dati. Se è il caso, le operazioni sono concettualmente lineari: si estraggono i dati, li si trasformano in una stringa, si trasforma la stringa in una property list, che è il dizionario principale. Da questo dizionario principale recupero il vettore dei dizionari dei singoli elementi. Per ogni dizionario, costruisco l'elemento con metodo apposito, e poi lo inserisco all'interno del vettore theElements, cioè lo aggiungo agli elementi della CoverView.

Non occorre nemmeno predisporre strani meccanismi per lo Undo, in quanto già presenti all'interno dei metodi addElem e deleteSelectedElements.

Intervallo: correzioni

Usando un po' l'applicazione, mi sono accorto di qualche errore che mi è scappato nei capitoli precedenti.

La classe PrtMrgWinCtl, ad esempio, sbagliava a gestire i margini di stampa, per cui, in realtà, l'intero meccanismo di predisposizione dei margini di stampa non funzionava. L'errore stava nell'assegnazione alla variabile sbagliata dei valori dei margini destro, superiore e inferiore. Pazienza.

Già che c'ero, mi sono accorto che l'impostazione dei margini e del formato della pagina di una copertina andava perduta tra una sessione e l'altra. Questo perché la variabile pInfo (appunto della classe NSPrintInfo, nata a questo scopo) non era coinvolta nelle operazioni di salvataggio del template, pur essendo una variabile d'istanza di CoverView. Messo a posto anche questo (ma questo significa che il formato dei template è cambiato; poco male, ne ho fatti altri più bellini).

L'errore più brutto però coinvolgeva gli elementi CCE_Line, che subivano misteriose metamorfosi quando modificati attraverso la finestra ElemInfoWinCtl. Il problema è dovuto al metodo resizeMeWith: utilizzato, che era quello generico. Per la classe CCE_Line va invece riscritto per tenere conto che la dimensione height, per via del meccanismo di definizione delle linee, è sempre nullo. Ho dunque sovrascritto il metodo:

- (void)
resizeMeWith: (NSSize) newSize
{
    NSSize        realSize ;
    float        aaa = rotAngle/180*M_PI ;
    // gestione dello undo del movimento
    [[self undoManager] setActionName: @"BasicForm -> resizeMe"];
    [[[self undoManager] prepareWithInvocationTarget: [ownerView theElements]]
        resizeElem: self withSize: localSize ];
    // ricalcolo la posizione del secondo punto
    realSize.width = newSize.width * cos ( aaa ) + newSize.height * sin ( aaa ) ;
    realSize.height = newSize.width * sin ( aaa ) + newSize.height * cos ( aaa ) ;
    [self buildMeFromPt: drawPoint withSize: realSize ];
}

Infine, c'era un errore quando si cercava di selezionare più elementi tracciando un rettangolo che li racchiudesse tutti. Se la costruzione del rettangolo partiva dall'alto a sinistra ed andava verso il basso a destra, tutto andava bene. Se invece si procedeva in altro modo, poiché il rettangolo risultava con almeno una dimensione negativa, le successive istruzioni che cercavano di determinare le intersezioni tra il rettangolo di selezione e i rettangoli delimitanti gli altri elementi fallivano. Ho quindi modificato il metodo handleMouseClick di CoverView:

- (void)
handleMouseClick: (NSEvent *)theEvent
{
    ...
    curElem = [self getClickedElem:whereClick];
    // se c'e' un elemento sotto il mouse
    if ( curElem )
    {
        ...
    }
    else
    {
        int        i , numElem ;
        CCE_BasicForm * elem ;
        NSRect selRect, locrect ;
        // comincio la selezione multipla
        selRect = [ self handleRectSelect: theEvent ];
        locrect = arrangeRect( selRect.size );
        selRect.origin.x += locrect.origin.x ;
        selRect.origin.y += locrect.origin.y ;
        selRect.size = locrect.size ;
        // ciclo su tutti gli elementi del gruppo
        numElem = [ [ theElements elemArray] count ];
        for ( i = 0 ; i < numElem ; i ++ )
        {
            elem = [ [ theElements elemArray] objectAtIndex: i ];
            [ elem selectMeIfInRect: selRect addingToSelez: shiftKeyPress] ;
        }
    }
}

Ho corretto il rettangolo con l'ormai consueta funzione arrangeRect(.), costruendone uno con dimensioni sicuramente positive.

Già che c'ero, ho introdotto il metodo associato alla voce di menu Select All o Seleziona Tutto, che è inspiegabilmente assente. È piuttosto semplice, basta sapere il nome giusto, che per altro si può vedere esaminando la voce di menu:

- (IBAction)
selectAll:(id)sender
{
    [ theElements cascadeSelection: YES ] ;
}

Colonne multiple

Ho scoperto che è piuttosto semplice fare in modo che il testo presentato all'interno di una elemento CCE_Text sia distribuito su più colonne. La maggior parte del lavoro è svolto dalle classi di Cocoa predisposte alla bisogna, ovvero NSLayoutManager e NSTextContainer.

Prima di tutto, ho però bisogno di un meccanismo che permetta all'utente di stabilire su quante colonne distribuire il testo. Ho così modificato il menu contestuale associato agli elementi CCE_Text: ho eliminato le voci per l'impostazione del testo automatico, e le ho sostituite con l'unica voce Text Params. Selezionando questa voce, faccio comparire una nuova finestra.

figura 06

figura 06

In questa finestra ripristino il menu del testo automatico (sotto forma di menu pop up, ed aggiungo un campo di testo dove impostare il numero delle colonne.

Non mi dilungo sulle solite questioni relative alle finestre accessorie e sulle modifiche alla classe CoverView per la gestione del tutto, ma mi limito a dire che con queste finestra si modificano le due variabili d'istanza automaticText e (nuova) numOfColums.

Adesso, in base al valore di numOfColums, bisogna predisporre la classe NSTextStorage perché la sua visualizzazione sia corretta.

C'è un primo metodo, prepareTextStorage, che è chiamato da tutti i metodi che inizializzano un elemento della classe CCE_Text, che si limita ad aggiungere un layout manager all'elemento:

- (void)
prepareTextStorage: (NSTextStorage *) ts
{
    NSLayoutManager *layoutManager;
    // layout gestisce come il testo e' rappresentato
    layoutManager = [[NSLayoutManager alloc] init] ;
    [ ts addLayoutManager:layoutManager];
    [ self adjustTextColumn ] ;
    [ layoutManager release ] ;
}

Il lavoro più interessante è svolto invece all'interno di adjustTextColumn, da chiamare anche ogni volta che cambia il numero delle colonne:

- (void)
adjustTextColumn
{
    int                i, notcAr ;
    NSLayoutManager    * lm ;
    // recupero il layout manager del textStorage
    lm = [ [ textStorage layoutManagers ] objectAtIndex: 0] ;
    // guardo quanti textContainer sono presenti
    notcAr = [[ lm textContainers ] count ];
    // li tolgo tutti, che non mi servono piu'
    for ( i = (notcAr-1); i >= 0 ; i -- )
        [ lm removeTextContainerAtIndex: i ];
    // ci aggiungo quelli nuovi in base al numero di colonne
    for ( i = 0; i < numOfColumns ; i ++ )
    {
        NSTextContainer * tc;
        // ricalcolo le dimensioni per non averne negative
        NSRect locrect = arrangeRect( localSize );
        // costruisco un nuovo textContainer
        tc = [[NSTextContainer alloc] init ];
        // dovrei specificare un NSSize per il textcontainer
        [ tc setContainerSize:
                NSMakeSize( locrect.size.width / numOfColumns - 2,
                            locrect.size.height) ];
        // lo aggiungo al layout manager
        [ lm addTextContainer: tc];
        [ tc release ] ;
    }
}

Il metodo è piuttosto semplice: toglie tutti gli oggetti NSTextStorage presenti, e ne aggiunge tanti quante sono le colonne. Le dimensioni di questi NSTextContainer sono poi le stesse in altezza, mentre in larghezza dipendono appunto dal numero delle colonne presenti.

Quando poi si deve disegnare l'elemento CCE_Text, si procede così:

- (void)
specificDrawing: (NSRect) inRect
{
    // determino punto e dimensione di dove disegnare
    // per tenere conto di eventuali dimensioni negative
    // disegno del testo
    NSRect locrect = arrangeRect( localSize );
    [ super specificDrawing: inRect ] ;
    // se sto modificando il testo...
    if ( ! editingText || [NSGraphicsContext currentContextDrawingToScreen] )
    {
        int            i ;
        NSLayoutManager *layoutManager ;
        NSArray    * tcAr ;
        // recupero il layout manager
        layoutManager = [ [ textStorage layoutManagers ] objectAtIndex: 0] ;
        // recupero tutti i textContainer (sono numOfColumns)
        tcAr = [ layoutManager textContainers ] ;
        // li spazzolo tutti
        for ( i = 0 ; i < numOfColumns ; i ++ )
        {
            NSRange glyphRange ;
            NSPoint    pt ;
            NSTextContainer * tc1 ;
            // recupero i textContainer in ordine
            tc1 = [ tcAr objectAtIndex: i ] ;
            // predispongo la loro dimensione
            [ tc1 setContainerSize:
                NSMakeSize( locrect.size.width / numOfColumns - 2,
                            locrect.size.height) ];
            // recupero il range del testo
            glyphRange = [layoutManager glyphRangeForTextContainer: tc1 ];
            // calcolo il punto di origine del singolo textContainer
            pt = NSMakePoint(    locrect.origin.x +
                                    i * locrect.size.width / numOfColumns + 2 ,
                                locrect.origin.y );
            // disegno i caratteri nel singolo textContainer
            [ layoutManager drawGlyphsForGlyphRange:glyphRange
                    atPoint: pt ];
        }
    }
    else
    {    // sto editando il testo e non sto stampando
        // e' tutto fatto dalla NSTextView
    }
}

In pratica, si esaminano in successione i vari NSTextContainer, in numero pari a quello delle colonne; per ognuno si essi si ricalcolano le dimensioni, e si determina il punto dove sarà rappresentato. L'ordinata di questo punto è sempre la stessa, mentre l'ascissa è determinata tenendo conto della dimensione delle colonne.

figura 07

figura 07

Quando invece il testo è in corso di modifica, continuo a mostrare sempre una NSTextView, ma larga quanto una colonna.

Licenza Creative Commons
Eccetto dove diversamente specificato, i contenuti di questo sito sono rilasciati sotto Licenza Creative Commons.
Pagina a cura di Livio Sandel (macocoa2012@gmail.com).