MaCocoa 049

Capitolo 049 - Scrivere in una view

Riprendo in mano la classe CCE_Text per disegnare stringhe di testo all'interno di una view.

Sorgenti: documentazione Apple

Prima stesura: 6 agosto 2004

Così fan tutti

Quando si tratta di disegnare del testo all'interno di una finestra, normalmente si usa una variabile della classe NSTextView, che fornisce già pronte tutta una serie di funzionalità adatte alla maggior parte dei casi. Tuttavia, nel mio caso, un po' per snobismo, un po' per imparare meglio come funzionano le cose, provo a farne a meno. L'idea è di migliorare la classe CCE_Text in modo che sia in grado di rappresentare all'interno della CoverView testo di vario tipo. In particolare mi interessa riportare l'elenco dei file di un volume, come se fosse una NSOutlineView (ma con alcune interessanti differenze).

Per prima cosa, mi rinfresco le idee con un po' di teoria. L'architettura preposta alla rappresentazione del testo all'interno di Cocoa è composta da alcune classi, inquadrate all'interno del paradigma MVC (Model, View, Controller). La classe modello dovrebbe essere quella che contiene il modello dei dati: nel caso di testo, deve contenere appunto il testo, secondo una qualche rappresentazione (che si spera ricca, flessibile, facile da usare, eccetera). Questa classe si chiama NSTextStorage, ed è una sottoclasse di NSMutableAttributedString, a suo volta sottoclasse di NSAttributedString (che non è una sottoclasse di NSString!). NSAttributedString (per stringhe definite staticamente) e NSMutableAttributedString (per stringhe capaci di cambiare nel tempo) sono classi che gestiscono stringhe dotate di attributi; gli attributi sono il font, il colore, lo stile, il kerning, eccetera, ovvero tutti quegli attributi tipografici che siamo abituati ad utilizzare quando scriviamo del testo in bella copia. Le classi mettono a disposizione metodi per costruire queste stringhe a partire da normali stringhe, da file RFT e HTML e (a partire da Mac OS X 10.3) perfino documenti di Microsoft Word. Forniscono metodi per la manipolazione della stringa, degli attributi e di varia utilità. come esempio fornisco il gruppo di istruzioni seguente:

temp = [ [ NSMutableAttributedString alloc] initWithString: @"riga uno\nriga due\nriga tre\nriga quattro\nriga cinque\nriga sei" ];
locFont = [ NSFont fontWithName: @"Arial" size: 12 ] ;
locFont = [ [NSFontManager sharedFontManager] convertFont: locFont toHaveTrait: NSBoldFontMask];
[ temp addAttribute: NSFontAttributeName value: locFont range: NSMakeRange(0, [ temp length] ) ];

Costruisco in primo luogo una NSMutableAttributedString partendo da una semplice stringa; costruisco poi una variabile di tipo NSFont, tale da specificare un font Arial, di dimensione 12, cui aggiungo la specifica (tratto) di Grassetto; attribuisco infine questo font a tutta la stringa. Il metodo addAttribute:value:range: aggiunge infatti l'attributo di font specificato mediante il valore al range dei caratteri che va da zero alla intera lunghezza della stringa. Specificando un diverso range, potevo giocare sulla rappresentazione della stringa in vari modi divertenti.

Tornando alla classe NSTextStorage, questa classe aggiunge alla capacità di modellare il testo anche il collegamento con le sezioni View e Controller del paradigma.

La classe NSLayoutManager funziona appunto da classe Controller, che governa l'interazione tra la classe modello/sorgente del testo e le classi preposte alla visualizzazione di questi testi. La classe è responsabile della trasformazione dei caratteri in glifi; un carattere è la rappresentazione astratta di un carattere, mentre un glifo è la rappresentazione grafica di questo carattere. La lettera p è un carattere, il simbolo grafico 'p' è un glifo; ovviamente, ad un carattere possono corrispondere diversi glifi, a seconda della rappresentazione (font, dimensione, eccetera).

La classe NSTextContainer regola infine la parte View del paradigma, e stabilisce come i caratteri (no!, i glifi) sono rappresentati a video. Stabilisce quindi la zona dello schermo dove andare a disegnare i vari glifi. Normalmente, questa classe è associata ad una NSTextView, con la quale collabora a rappresentare sullo schermo il testo.

È interessante notare la catena d'uso di queste classi. Per un dato testo (ad un esempio, una noiosa relazione sullo stato aziendale di venti pagine) esiste una sola classe NSTextStorage, che contiene il testo nudo e i vari attributi al testo (font, dimensioni, colori, eccetera). C'è poi in genere un solo NSLayoutManager che regola la visualizzazione; collegate a questa classe ci sono in genere più istanze di NSTextContainer che determinare le zone di rappresentazione del testo (le varie pagine, o le varie colonne della relazione). Infine, NSTextContainer si appoggia qualche altra classe per la visualizzazione vera e propria.

La classe CCE_Text

Avevo già scritto la classe CCE_Text in grado di gestire delle stringhe di testo; in effetti, la classe è già a buon punto, anche se la maggior parte delle istruzioni erano state scritte per mera copia da un esempio e non le avevo del tutto capite. Ne approfitto per ripulirne e cambiare qualcosa.

In primo luogo, modifico il metodo preposto all'inizializzazione.

- (id) initWithAttributedString: (NSAttributedString *) theText
        inRect: (NSRect) aRect
        andRotation: (float) angle
{
    self = [ super init ];
    if ( self )
    {
        NSTextStorage * loctextStorage ;
        NSLayoutManager *layoutManager;
        NSTextContainer *textContainer;
        // layout gestisce come il testo e' rappresentato
        loctextStorage = [[[NSTextStorage alloc] initWithAttributedString: theText] autorelease ];
        layoutManager = [[[NSLayoutManager alloc] init] autorelease ];
        [loctextStorage addLayoutManager:layoutManager];
        // indica dove il testo e' rappresentato
        textContainer = [[[NSTextContainer alloc] init] autorelease ];
        // dovrei specificare un NSSize per il textcontainer
        // lo faro' piu' pardi...
        [layoutManager addTextContainer:textContainer];
        // assegno il textStorage
        [ self setTextStorage: loctextStorage ];        
        // per ora me ne faccio nulla
        rotAngle = angle ;
        theTextCont = textContainer ;
        [ self setElemLimit: aRect ];
    }
    return ( self );
}

figura 01

figura 01

Parto dall'idea che la classe accetti come parametro di inizializzazione una NSAttributedString, costruisco in successione una NSTextStorage, una NSLayoutManager ed una NSTextContainer (una sola; per ora avrò tutto il testo racchiuso all'interno di un unico rettangolo). Ciascuna delle tre classi nasce autorelease, perché poi sono retained nel momento in cui sono associate alla classe collegata (NSLayoutManager da NSTextStorage, NSTextContainer da NSLayoutManager). Infine, conservo (e faccio retain) la classe NSTextStorage all'interno di una variabile d'istanza della classe CCE_Text. Per comodità (serve accedervi spesso per vari motivi) convervo anche un riferimento alla classe NSTextContainer (faccio una assegnazione diretta ad una variabile d'istanza, e quindi non aggiungo retain).

C'è solamente una importante istruzione che qui non è presente, in quanto nascosta all'interno del metodo setElemLimit, che ho riscritto per la classe CCE_Text.

Per la corretta rappresentazione del testo occorre infatti individuare il rettangolo all'interno del quale il testo è rappresentato. Ecco quindi la varsione specifica del metodo:

- (void)setElemLimit:(NSRect)newElemLimit {
elemLimit = newElemLimit;
        [ self buildHdlList ];
        [ theTextCont setContainerSize: elemLimit.size ];
}

figura 02

figura 02

Ora, grazie alle mie superbe capacità di programmatore (sto scherzando), l'attribuzione setContainerSize: è l'unica istruzione che mancava alla classe CCE_Text per partecipare con successo a tutte le operazioni di manipolazione del testo: selezione, spostamento, ridimensionamento. Provare per credere.

Inoltre, per semplificare (non che quello presente fosse sbagliato, anzi) il metodo di disegno, ho scritto:

- (void) drawElement: (NSRect) inRect
{
    // recupero il primo layout
    NSLayoutManager *layoutManager = [ [ textStorage layoutManagers ] objectAtIndex: 0] ;
    // recupero il range del testo
    NSRange glyphRange = [layoutManager glyphRangeForTextContainer: theTextCont ];
    // disegno del testo
    [ layoutManager drawGlyphsForGlyphRange:glyphRange atPoint: elemLimit.origin ];
    [ self drawHandles ] ;
}

Qui recupero il NSLayoutManager, e faccio calcolare quanti e quali caratteri si possono rappresentare all'interno del NSTextContainer corrente. Ottengo così la variabile glyphRange, che dice (è un range, quindi posizione di partenza e numero di elementi) quali e quanti elementi sono rappresentabili. Quindi, visto che non devo fare altro, passo direttamente a disegnare i glifi a partire dal punto in alta a sinistra del rettangolo.

Rimane da vedere un esempio d'uso di questa classe, ad esempio attraverso il frammento che trovate all'interno del metodo insertBasicDraw della classe CoverView:

CCE_Text        * tmpText ;
NSFont            * locFont ;
NSMutableAttributedString * temp ;

temp = [ [ NSMutableAttributedString alloc] initWithString: @"riga uno\nriga due\nriga tre\nriga quattro\nriga cinque\nriga sei" ];
locFont = [ NSFont fontWithName: @"Arial" size: 12 ] ;
locFont = [ [NSFontManager sharedFontManager] convertFont: locFont toHaveTrait: NSBoldFontMask];
[ temp addAttribute: NSFontAttributeName value: locFont range: NSMakeRange(0, [ temp length] ) ];
// calcolo il punto
x1 = STARTPOINT_X + CUTSIGN_DIM + CUTSIGN_DIST + CUTSIGN_DIM ;
y1 = STARTPOINT_Y + CUTSIGN_DIM + CUTSIGN_DIST + CUTSIGN_DIM ;
tmpText = [[ CCE_Text alloc] initWithAttributedString: temp
            inRect: NSMakeRect(CM2PX(x1), CM2PX(y1), 200, 200) andRotation: 0.0 ];
[ tmpText setOwnerView: self ];

Le prime quattro istruzioni le ho già spiegate in precedenza, mentre le successive due sono piuttosto banali; finalmente c'è la costruzione di una istanza di CCE_Text, passando come parametri la NSAttributedString (si, lo so, è in realtà NSMutableAttributedString, ma funziona lo stesso) ed il rettangolo di iniziale rappresentazione. L'angolo di rotazione è al momento disattivo (non perché non funzioni, ma perché manda in malora la selezione e lo spostamento degli oggetti... penso che riprenderò la questione quanto prima).

La lista dei file

Ora che so come scrivere del testo all'interno della CoverView, voglio utilizzare la classe CCE_Text per riportare la lista dei file, come se fosse una NSOutlineView. Al contrario della lista, il testo non è manipolabile, e sono presenti solo le voci che, nella finestra di catalogo, riportano il box di spunta is2Print a Vero (la cosa era stata predisposta nel capitolo 34).

Per fare questo, ho bisogno di un meccanismo per costruire una NSMutableAttributedString contenente tutta la lista di interesse, e di una strategia di aggiornamento del contenuto della finestra quando l'utente cambia la selezione del volume. In realtà il tutto confluisce nella modifica del metodo tableViewSelectionDidChange della classe sorgente di dati CatDataSrc.

figura 05

figura 05

Come è già stato spiegato, la lista dai volumi della finestra della copertina (un oggetto della classe NSTableView) utilizza la classe sorgente dei dati CatDataSrc per recuperare i dati da visualizzare. Utilizza poi la stessa classe (ma solo per comodità di localizzazione del codice) come classe delegata al trattamento delle operazioni scatenate dall'utente al cambio di selezione del volume.

Riscrivo quindi il metodo tableViewSelectionDidChange: per tenere conto del fatto che voglio aggiornare il contenuto della CoverView. Lo discuto in due momenti:

- (void)
tableViewSelectionDidChange:(NSNotification *)aNotification
{
    NSTextStorage * newFileList ;
    CCE_Text        * wxw ;
    int                elNum, jj, nn ;
    NSTableView        * ov = (NSTableView*)[ aNotification object ] ;
    // e' cambiata la selezione
    NSRange            myrange ;

    currentVol = [ ov selectedRow ] ;
    newFileList = [[NSTextStorage alloc] initWithString:@"test"];
    ...
    wxw = [ [[refDoc coverWin] cvrView] fileList];    
    // imposto gli attributi per tutto il range
    myrange = NSMakeRange(0, [ newFileList length] ) ;
    [ newFileList addAttribute: NSForegroundColorAttributeName value: [NSColor blueColor] range: myrange ];
    [[ wxw textStorage] setAttributedString: newFileList ] ;
    [ [[refDoc coverWin] cvrView] setNeedsDisplay: YES ];
}

All'interno della classe CoverView ho definito una nuova variabile d'istanza, fileList, che punta ad una specifica istanza di un oggetto della classe CCE_Text, quella destinata appunto a contenere la lista dei file del volume. È utilizzata per poter rimpiazzare il contenuto corrente con un nuovo contenuto (al momento, la sola scritta "test", in un bel colore blu). A parte il lungo giro di messaggi necessario per raggiungere l'oggetto CCE_Text, non ci sono cose particolari da notare.

La parte più interessante è invece come ricavare la lista dei file. Ora, non esiste un meccanismo esplicito per esaminare il contenuto della classe sorgente di dati. La classe CatDataSrc infatti realizza una serie di metodi che sono sfruttati da oggetti NSOutlineView per estrarre i dati. Il procedimento di estrazione è trasparente per il mio codice, ed è interamente gestito da Cocoa. Se quindi volessi recuperare la lista dei file presenti all'interno della sorgenti dati, dovrei scrivere nuovi metodi che gestiscano e regolino questo processo. In realtà, posso ricorrere ad un trucco interessante: simulo la presenza di una NSOutlineView ed utilizzo i metodi standard di interrogazione standard della sorgente dati.

A suo tempo, avevo fatto in modo che la stessa classe CatDataSrc potesse funzionare sia per la NSOutlineView della finestra con la lista dei volumi che per una NSOutlineView che volevo proditoriamente utilizzare all'interno della CoverView. Bastava distinguere il richiedente tramite l'attribuzione di un apposito tag alla NSOutlineView. Qui ricorro ad un trucco simile: decido che se il parametro NSOutlineView (indicante la tabella richiedente) è vuoto (nil), allora, procedo tranquillo come se la richiesta provenisse dalla NSOutlineview interna alla CoverView (che poi in realtà non esiste più, visto che la sto proprio rimpiazzando con un CCE_Text).

Per fare questo, rimpiazzo in ciascun metodo outlineView:qualcosa: una istruzione di controllo sul tag; è in pratica la prima istruzione di ciascun metodo:

- (int)    
outlineView: (NSOutlineView *)outlineView
    numberOfChildrenOfItem:    (id)item
{
    VolInfo * localItem ;
    // se sto recuperando dati per la CoverCtlWin
    if ( (outlineView == nil) || ([ outlineView tag] == TAG_CDCATDOC_FILELIST) )
    {
        if ( currentVol == -1 )
            return ( 0 ) ;
        if (item == nil)
            localItem = [ startPoint objectAtIndex: currentVol ];
        else localItem = item ;
    }
    else if ( [ outlineView tag] == TAG_CDCATDOC_FULLLIST )
    {
        /* eccetera */
    }
    /* eccetera */
}

Una volta messi a posto questi metodi, sono pronto per riempire gli spazi che prima avevo lasciato vuoti.

- (void)
tableViewSelectionDidChange:(NSNotification *)aNotification
{
    ...
    currentVol = [ ov selectedRow ] ;
    newFileList = [[NSTextStorage alloc] initWithString:@""];
    elNum = [ self outlineView: nil numberOfChildrenOfItem: nil ];
    for ( jj = 0 ; jj < elNum; jj ++ )
    {
        FileStruct * elem ;
        elem = [ self outlineView:     nil child: jj ofItem: nil ];
        nn = [ self outlineView: nil numberOfChildrenOfItem: elem ];
        // se non e' da stampare, salto
        if ( ! [ elem is2Print ] ) continue ;
        if (nn > 0 )
            addStringExpandItem (newFileList, self, elem, nn, 0 );
        else
        {
            terminalWithLevel( newFileList, [ elem fileName], 0, NO );
        }
    }
    wxw = [ [[refDoc coverWin] cvrView] fileList];    
    ...
}

La prima nuova istruzione:

elNum = [ self outlineView: nil numberOfChildrenOfItem: nil ];

chiede in pratica quanti elementi di primo livello si trovano all'interno del volume selezionato. Entrando nel metodo outlineView:numberOfChildrenOfItem: con tutti i parametri a nil, questo restituisce alla fine il numero degli elementi selezionati.

figura 03

figura 03

Proseguendo nella lista delle istruzioni, eseguo un ciclo per tutti gli elementi di primo livello; li estraggo uno ad uno col metodo outlineView:child:ofItem: e poi verifico se hanno a loro volta figli (cioé, se sono in realtà cartelle o bundle o quello che è). Tuttavia, l'esplorazione (nel caso di cartelle) o l'aggiunta alla lista (file standard) è eseguita solamente se il campo is2Print dell'elemento sotto esame è Vero; in caso contrario, si passa all'elemento successivo.

L'esplorazione e l'aggiunta alla lista procedono ricorsivamente, tramite aggiunte successive alla stringa newFileList di apposite aggiunte. Se ad esempio si sta considerando un file normale, è eseguita la procedura terminalWithLevel. Questa si limita ad aggiungere il nome del file alla fine della stringa:

void
terminalWithLevel ( NSTextStorage* str, NSString *fname, int lev, BOOL boldface )
{
    NSString    * xxx ;
    NSMutableAttributedString * temp ;
    NSFont * locFont ;

    xxx = [ NSString stringWithFormat: @"%@%@\n", prefixLevString( lev ), fname ] ;
    temp = [ [ NSMutableAttributedString alloc] initWithString: xxx ];
    locFont = [ NSFont fontWithName: @"Arial" size: 9 ] ;
    if ( boldface )
        locFont = [ [NSFontManager sharedFontManager] convertFont: locFont toHaveTrait: NSBoldFontMask];
    [ temp addAttribute: NSFontAttributeName value: locFont range: NSMakeRange(0, [ temp length] ) ];
    [ str appendAttributedString: temp ] ;
}

Come già accaduto in altri casi, utilizzo un meccanismo di indentazione per evidenziare la profondità del file all'interno della gerarchia delle cartelle. La funzione prefixLevString produce una stringa di lev caratteri (spazi, nel caso, ma potrebbero essere tabulazioni o altro) che precedono il nome del file vero e proprio. Uno dei parametri della funzione permette di assegnare alla stringa il tratto grassetto prima di aggiungerla in coda alla lista.

Il meccanismo ricorsivo che esplora tutto l'albero delle cartelle è nascosto all'interno della seguente procedura:

void
addStringExpandItem ( NSTextStorage* str, CatDataSrc * myDataSrc, FileStruct * elem, int nn, int lev )
{
    int            jj, nnx ;

    terminalWithLevel( str, [ elem fileName], lev, YES );
    for ( jj = 0 ; jj < nn; jj ++ )
    {
        FileStruct * elemx ;
        elemx = [ myDataSrc outlineView: nil child: jj ofItem: elem ];
        nnx = [ myDataSrc outlineView: nil numberOfChildrenOfItem: elemx ];
        if ( ! [ elemx is2Print ] ) continue ;
        if ( nnx > 0 )
        {
            addStringExpandItem ( str, myDataSrc, elemx, nnx, lev+1 );
        }
        else
        {
            terminalWithLevel( str, [ elemx fileName], lev+1, NO );
        }
    }
}

figura 04

figura 04

Per prima cosa, aggiungo il nome della cartella (perché sono sicuro che questa procedura è chiamata avendo come argomento una cartella), ma la scrivo in carattere grassetto per evidenziare la cosa. Poi, procedo ricorsivamente ad esaminare il contenuto della cartella, aggiungendo direttamente il nome del file nel caso di vero file, o chiamando ricorsivamente la procedura nel caso di ulteriori cartelle.

Avrei potuto certamente fare di meglio, ma per il momento mi fermo qui. Ah, pregasi notare come il meccanismo di Undo funziona anche per questi campi di testo, in caso di spostamento e ridimensionamento.

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).