MaCocoa 054

Capitolo 054 - Le correzioni

Ma perché nessuno mi dice che il meccanismo dei gruppi non funziona più? In questo capitolo eseguo le necessarie correzioni sulla classe CCE_ElemGroup, per poi passare ad alcuni argomenti sparsi: gestione degli stili del testo, riempimento automatico dei campi testo, menu contestuali.

Sorgenti: il titolo del capitolo è anche quello di un libro di Jonathan Franzen, edito da Einaudi.

Prima stesura: 25 agosto 2004.

Correzioni sui gruppi

Con le modifiche introdotte nei capitoli 51 e 52, gli elementi della classe CCE_ElemGroup hanno cessato di funzionare correttamente. In effetti, la riorganizzazione ha invalidati molti comportamenti: più che correggerli puntualmente, ho preferito uniformare il loro comportamento alla luce dell'introduzione dei Sistemi di Riferimento SdR dei singoli elementi. L'idea è che alla costruzione di un gruppo, al gruppo sia assegnato, come a tutti gli altri elementi, un SdR proprio, con una origine ed appropriate dimensioni orizzontali e verticali. Il disegno del gruppo quindi parte dopo una trasformazione di coordinate; a questo punto, le coordinate dei singoli elementi appartenenti al gruppo vanno modificate per tenere conto di questo fatto. In altri termini, alla costruzione di un gruppo di elementi, il SdR degli elementi va modificato per riferirlo non più alle coordinate della finestre, ma alle coordinate del gruppo di elementi. Ovviamente, alle distruzione di un gruppo, occorre modificare nuovamente i SdR degli elementi per tenere conto della trasformazione intermedia non più presente.

Il metodo preposto alla costruzione di un gruppo è stato sostanzialmente modificato, anche se a prima vista non appare:

- (void)
makeGroupFromSelected
{
    int        i , numElem, firstIdx = -1, lastIdx ;
    CCE_BasicForm * elem ;
    CCE_ElemGroup * newGrp ;
    // ciclo su tutti gli elementi del gruppo
    ...
    // quando arrivo qui, ho costruito un nuovo gruppo
    // se non c'e' nulla, faccio nulla
    ...
    // se c'e' un solo elemento, lo rimetto dov'era
    ...
    // finalmente, se arrivo qui, ho un gruppo vero e proprio
    // aggiusto tutti i valori del gruppo
    [ newGrp adaptModification ] ;
    // tutti gli elementi interni non sono selezionati
    [ newGrp cascadeSelection: NO ];
    // il gruppo invece rimane selezionato
    [ newGrp setCceIsSelected: YES ];
    // inserisco il gruppo nel posto opportuno
    [ elemArray insertObject: newGrp atIndex: lastIdx];
}

In effetti, ho sostituito tutta la parte successiva alla costruzione del vettore con gli elementi selezionati con la singola istruzione che invoca il metodo adaptModification. È qui che viene svolto tutto il lavoro importante.

- (void)
adaptModification
{
    int        i , numElem ;
    CCE_BasicForm * elem ;
    float        xmin, xmax, ymin, ymax ;

    numElem = [ elemArray count ];
    elem = [ elemArray objectAtIndex: 0 ];
    ymin = [[ elem getElemTop] floatValue];
    xmin = [[ elem getElemLeft] floatValue];
    xmax = [[ elem getElemRight] floatValue];
    ymax = [[ elem getElemBottom] floatValue];
    // ciclo su tutti gli elementi del gruppo
    for ( i = 1 ; i < numElem ; i ++ )
    {
        elem = [ elemArray objectAtIndex: i ];
        // cerco i limiti del rettangolo circoscritto
        if ( ymin > [[ elem getElemTop] floatValue])
            ymin = [[ elem getElemTop] floatValue];
        if ( xmin > [[ elem getElemLeft] floatValue] )
            xmin = [[ elem getElemLeft] floatValue];
        if ( xmax < [[ elem getElemRight] floatValue] )
            xmax = [[ elem getElemRight] floatValue];
        if ( ymax < [[ elem getElemBottom] floatValue] )
            ymax = [[ elem getElemBottom] floatValue];
    }
    [ self setTheDrawPath: [NSBezierPath bezierPathWithRect:
            NSMakeRect( 0, 0, xmax - xmin, ymax - ymin) ] ];
    [ self setDrawPoint: NSMakePoint( xmin, ymin ) ];
    [ self setRotAngle: 0 ];
    [ self setLocalSize: NSMakeSize( xmax - xmin, ymax - ymin) ] ;
    [ self calcLocalTranform ];
    [ self buildHdlList ] ;
    // sposto le origini di ogni elemento riferendole al gruppo
    for ( i = 0 ; i < numElem ; i ++ )
    {
        elem = [ elemArray objectAtIndex: i ];
        [ elem setDrawPoint: NSMakePoint( [elem drawPoint].x -xmin, [elem drawPoint].y - ymin ) ];
        [ elem calcLocalTranform ];
    }
}

È rimasta inalterata la parte che calcola posizioni e dimensioni spaziali del gruppo cercando i punti estremi di tutti gli elementi del gruppo stesso. Con questi quattro punti estremi costruisco in primo luogo il percorso theDrawPath come il rettangolo che racchiude tutti gli elementi; gli stessi punti estremi servono per caratterizzare il punto di origine del SdR del gruppo e le dimensioni del gruppo stesso. In base a questi valori si possono costruire gli handle e calcolare le trasformazioni dei SdR. Successivamente, per ogni elemento appartenente al gruppo, eseguo un cambiamento nel SdR. Il cambiamento consiste nella sola traslazione, in quanto la costruzione del gruppo avviene necessariamente ad un angolo nullo.

Per disegnare gli handle, si deve utilizzare un metodo del tutto uguale a quello proprio degli elementi CCE_Rect. Ecco quindi che ho spostato la realizzazione del metodo buildHdList della classe CCE_Rect alla superclasse CCE_BasicForm (dove in precedenza era vuoto), in modo da evitarne la riscrituttra e sfruttando l'ereditarietà.

La distruzione di un gruppo avviene rovesciando le operazioni della costruzione, con alcune avvertenze:

- (void )
ungroupAndInsertIn: (CCE_ElemGroup *) masterArray startingFrom: (int) idx
{
    int        i , numElem ;
    CCE_BasicForm * elem ;
    // ciclo su tutti gli elementi del gruppo
    numElem = [ elemArray count ];
    // per ogni elemento presente, lo aggiungo
    for ( i = 0 ; i < numElem ; i ++ )
    {
        NSAffineTransform    * tf1 = [ NSAffineTransform transform ];
        elem = [ elemArray objectAtIndex: i ];
        [ elem setCceIsSelected: YES ] ;
        [ [masterArray elemArray] insertObject: elem atIndex: idx ];
        [ tf1 initWithTransform: localTF ];
        [ tf1 prependTransform: [ elem localTF ] ];
        [ elem setLocalTF: tf1 ] ;
        [ elem setDrawPoint: [ tf1 transformPoint: NSMakePoint(0, 0) ] ];
        [ elem setRotAngle: ( [elem rotAngle] + rotAngle) ];
        [ tf1 invert ];
        [ elem setReverTF: tf1 ];
    }
}

Bisogna calcolare una corretta trasformazione per ogni elemento del gruppo. Si tratta di una operazioen contettualmente molto semplice: occorre concatenare in maniera opportuna le due trasformazioni: da SdR della finestra al SdR del gruppo, e da SdR del gruppo al SdR dell'elemento. Per farlo, parto dalla trasformazione da finestra a gruppo (che inserisco in tf1) alla quale poi concateno (mettendola davanti col metodo prependTransform: in modo che sia eseguita concettualmente per seconda) quella propria dell'elemento. Con questa nuova trasformazione, calcolo l'inversa ed aggiusto insomma il nuovo SdR dell'elemento. Devo tuttavia ricalcolare punto di origine ed angolo: il metodo più semplice per il punto di origine è di ricavarlo dalla trasformazione, mentre per l'angolo semplicemente opero la somma tra l'angolo del gruppo e quello dell'elemento.

Tutto questo meccanismo di costruzione e distruzione dei gruppi funziona perché ho modificato contestualmente anche il metodo di disegno. scompare il metodo drawRect:, sostituito anche qui dal metodo specificDrawing:. Con il metodo drawRect: generico, è effettuata la trasformazione di coordinate e il disegno degli handle. Quindi, all'interno del metodo specifico devo solo disegnare gli elementi:

- (void)
specificDrawing: (NSRect) aRect
{
    int        i , numElem ;
    CCE_BasicForm * elem ;

    numElem = [ elemArray count ];
    for ( i = numElem-1 ; i >= 0 ; i -- )
    {
        elem = [ elemArray objectAtIndex: i ];
        [ elem drawElement: aRect ];
    }
}

Qui utilizzo il metodo drawElement:, in quanto i singoli elementi devono poter effettuare un cambio di coordinate, che adesso, al contrario di quanto succede normalmente, riguarda il passaggio tra il SdR del gruppo e quello dell'elemento proprio.

A questo punto, un elemento della classe CCE_ElemGroup è trattato in maniera uniforme alle altre classi per quanto riguarda selezione, spostamento e rotazione; posso così eliminare i metodi specifici della classe che sovrascrivevano quelli generici. Ad esempio, per capire se il clic del mouse cade sopra un gruppo, è sufficiente controllare se il clic è contenuto all'interno del rettangolo che racchiude il gruppo. Ciò avviene tranquillamente col metodo generico, visto che tale controllo utilizza solamente il punto di origine e le dimensioni dell'elemento. Parimenti, lo spostamento e la rotazione dell'elemento, modificando solamente la trasformazione dell'elemento gruppo, non interessa più i singoli elementi del gruppo, che mantengono la loro relazione all'interno del SdR del gruppo stesso.

Ecco quindi che la classe CCE_ElemGroup non sovrascrive più i metodi di CCE_BasicForm, ma aggiunge solamente i metodi specifici della sua natura (manipolazione del gruppo, undo, eccetera).

Menu contestuali

Avere menu contestuali è sempre una bella cosa: semplificano la vita mostrando e permettendo solo le operazioni lecite in quel momento. voglio quindi aggiungere dei menu contestuali all'interno della finestra della copertina, che siano presenti quando sono selezionati uno o più elementi (e quindi, niente menu se non ci sono elementi selezionati). Come primo tentativo, utilizzo tre menu: uno attivo quando è selezionato un singolo elemento, uno quando sono selezionati più elementi, ed un terzo attivo solo quando il singolo elemento selezionato è della classe CCE_Text.

figura 01

figura 01

Costruisco direttamente questi tre menu all'interno di Interface Builder, nel file CoversWin.nib. Ho raccolto all'interno del menu le voci che hanno più o meno senso nei tre casi (arriverò più avanti al concetto di testo automatico per CCE_Text); ad esempio, per il menu relativo ad una selezione che coinvolge più elementi, è presente la voce che permette la costruzione di un gruppo, assente altrimenti. Aggiungo tre outlet alla CoverView, collegati a questi tre menu, per richiamarli quando necessario. Più complicato assegnare le azioni alle voci di menu. Infatti, normalmente i destinatari delle voci dei menu sono il First Responder (nel caso, la CoverView, e non ci sono quindi problemi); in alcuni casi, tuttavia (quelli che aprono una finestra, come ad esempio la voce Align...) il destinatario previsto è un metodo della classe AppDelegate. In mancanza di idee migliori, non ho fatto altro che copiare i metodi incriminati all'interno della classe CoverView, e rendere la CoverView destinataria anche di queste voci di menu. Mi trovo così con alcuni metodi duplicati, ma pazienza.

figura 02

figura 02

Per attribuire un menu contestuale, occorre avere a disposizione una istanza di una classe NSResponder (o NSView, che ne è una discendente). Però, gli elementi CCE_BasicForm (e discendenti) non discendono da questa classe. Ciò che si avvicina di più alla classe desiderata è la CoverView (che è appunto una sottoclasse di NSView), cui giocoforza devo attribuire il menu contestuale. Ce ne fosse uno solo, valido per tutta la CoverView, basta utilizzare il metodo setMenu:. Ma questo non è il mio caso; devo invece utilizzare un altro metodo, menuForEvent:. Si tratta di un metodo che di default restituisci nil (in assenza di menu contestuali) o il menu contestuale impostato con setMenu:. Devo sovrascriverlo per fare in modo che restituisca il menu appropriato in base alla posizione del clic dell'utente:

- (NSMenu *)
menuForEvent: (NSEvent *)theEvent
{
    NSPoint            whereClick;
    CCE_BasicForm * curElem ;
    whereClick = [self convertPoint:[theEvent locationInWindow] fromView:nil];
    curElem = [self getClickedElem:whereClick] ;
    if ( curElem == nil || ( ! [ curElem cceIsSelected]) )
        return ( nil ) ;
    curElem = [ AppDelegate getTheSelectedElem];
    if ( curElem == nil )
        return ( nil ) ;
    else if ( curElem == (CCE_BasicForm*)(-1) )
        return ( grpElemMenu );
    else if ( [curElem isKindOfClass:[CCE_Text class]] )
        return ( txtElemMenu );
    return ( stdElemMenu );
}

La logica è molto semplice: se il clic avviene fuori da ogni elemento, nessun menu. Se il clic avviene sopra un elemento, ma questo non è selezionato, nessun menu. Solo se il clic avviene sopra un elemento, e questo è selezionato, si comincia a ragionare. Se non è il solo elemento ad essere selezionato, il menu da visualizzare è quello proprio per un insieme di elementi. Se c'è un solo elemento selezionato, ed è della classe CCE_Text, mostro il menu specifico per il testo (noto che nel caso di attività di modifica in corso, il menu contestuale visualizzato è quello proprio di una NSTextView, diverso da quello da me previsto); altrimenti, mostro il menu standard per il singolo elemento.

Tutto molto semplice e pratico.

Testo automatico

Per fare degli esperimenti, avevo definito un elemento CCE_Text come contenitore della lista dei file di un volume. Avevo allo scopo definito una variabile d'istanza fileList di CoverView che individuasse tale elemento, e metodi specifici della classe CatDataSrc in grado di produrre tale lista sotto forma di testo. Il meccanismo era scatenato dalla classe CatDataSrc, al variare della selezione del volume corrente.

figura 03

figura 03

Voglio estendere questa possibilità, introducendo il concetto di testo automatico: un elemento della classe CCE_Text può essere caratterizzato dal fatto che il testo in esso contenuto è prodotto in maniera automatica a partire da informazioni presenti nel catalogo del volume. L'elenco dei file contenuti è una di queste possibilità; altre possibilità sono il nome del volume, la dimensione, la data di creazione, eccetera. Voglio inoltre lasciare all'utente la possibilità di scegliere quale testo inserire: è questo il motivo della presenza, all'interno del menu contestuale specifico degli elementi di testo, di un sottomenu che permette di specificare quale tipo di testo automatico attribuire. Cambia anche la prospettiva: il meccanismo adesso è gestito direttamente dalla classe CCE_Text, utilizzando i servizi forniti da CatDataSrc.

Parto definendo una nuova variabile d'istanza della classe CCE_Text, l'intero automaticText, il cui valore discrimina quale tipo di testo automatico è presente; se il valore è nullo, il testo non è automatico, ed è di libero accesso da parte dell'utente (in realtà, anche gli altri testi sono di libera modifica; tuttavia, selezionando un altro volume nella lista, il testo sarà modificato in accordo alle direttive impartite, ed ogni precedente modifica sarà perduta).

All'interno del file CoverView.m è quindi presente il metodo che è invocato dalle voci di menu del testo automatico; ogni voce chiama lo stesso metodo, ed ho fatto in modo che ogni voce abbia un tag corrispondente agli identificatori del testo automatico che ho in precedenza definito:

#define        CATAUTOTEXT_NULL            0
#define        CATAUTOTEXT_FILELIST        50
#define        CATAUTOTEXT_VOLNAME            51
#define        CATAUTOTEXT_MODDATE            53
#define        CATAUTOTEXT_CREATDATE        52
#define        CATAUTOTEXT_VOLSIZE            54
#define        CATAUTOTEXT_VOLFREESIZE        55

- (IBAction)
autoText4SelElem: (id)sender
{
    int        uot = [ sender tag ] ;
    CCE_Text    * myTxt = (CCE_Text*) [ AppDelegate getTheSelectedElem] ;
    [ myTxt setAutomaticText: uot ];
    [ myTxt updAutoText: [[ winCtl document] dataSource ] ] ;
    // forzo un ridisegno
    [ self setNeedsDisplay: YES ];
}

Il metodo è certo che è presente un solo elemento selezionato, e che questo sia di tipo testo: non è al momento possibile chiamare il metodo autoText4SelElem se non attraverso menu contestuale, e questo menu compare solo se è selezionato un solo elemento ed è di classe CCE_Text. Il metodo imposta la variabile d'istanza secondo il tag del menu, e poi costringe l'elemento ad aggiornare il testo contenuto.

Il metodo updAutoText: ha tre realizzazioni distinte. È assolutamente vuoto nel caso generale; passa il messaggio agli elementi del gruppo nel caso di un elemento CCE_ElemGroup; contiene istruzioni interessanti nel caso di CCE_Text.

- (void)
updAutoText: (CatDataSrc *) catalog
{
    return ;
}

- (void)
updAutoText: (CatDataSrc *) catalog
{
    // passo la palla agli elementi del gruppo
    [ elemArray makeObjectsPerformSelector: @selector(updAutoText:) withObject: catalog ] ;
}

- (void)
updAutoText: (CatDataSrc *) catalog
{
    if ( automaticText == CATAUTOTEXT_NULL )
        return ;
    switch ( automaticText ) {
    case CATAUTOTEXT_FILELIST :
        [ catalog setCACFileListFor: textStorage ] ;
        break ;
    case CATAUTOTEXT_VOLNAME :
        [ catalog setCACAttribFor: textStorage ofName: COLID_FILENAME ] ;
        break ;
    case CATAUTOTEXT_MODDATE :
        [ catalog setCACAttribFor: textStorage ofName: COLID_MODDATE ] ;
        break ;
    case CATAUTOTEXT_CREATDATE :
        [ catalog setCACAttribFor: textStorage ofName: COLID_CREATDATE ] ;
        break ;
    case CATAUTOTEXT_VOLSIZE :
        [ catalog setCACAttribFor: textStorage ofName: @"volSize" ] ;
        break ;
    case CATAUTOTEXT_VOLFREESIZE :
        [ catalog setCACAttribFor: textStorage ofName: @"volFreeSize" ] ;
        break ;
    }
}

In realtà il grosso lavoro di produzione del testo è svolto da un paio di metodi di CatDataSrc.

Tuttavia, la prima modifica della classe CatDataSrc riguarda il metodo chiamato quando cambia la selezione nella tabella che elenca i volumi disponibili:

- (void)
tableViewSelectionDidChange:(NSNotification *)aNotification
{
    NSTableView        * ov = (NSTableView*)[ aNotification object ] ;
    CoverView        * cv = [[refDoc coverWin] cvrView] ;
    // predispongo il volume in selezione corrente
    currentVol = [ ov selectedRow ] ;
    [ [[cv theElements] elemArray] makeObjectsPerformSelector:
            @selector(updAutoText:) withObject: self ] ;
    // forzo un rinfresco della vocerView
    [ cv setNeedsDisplay: YES ];
}

figura 04

figura 04

In precedenza, il metodo, oltre ad aggiornare la variabile currentVol, suo scopo principale, costruiva un testo con l'elenco dei file e lo attribuiva all'eventuale elemento CCE_Text deputato. Adesso, tale operazione non è svolta esplicitamente; piuttosto, è lasciato al gruppo di elementi theElements (che è appunto quello che raccoglie tutti gli elementi di una copertina) di eseguire le operazioni di aggiornamento. Qui uso per la prima volta il metodo makeObjectsPerformSelector:withboject:. Si tratta di un metodo tipico di un NSArray (e discendenti) per far si che ogni elemento del vettore esegua una dato metodo (individuato tramite il costrutto @selector(.)) con argomento specificato. Finora ho sempre eseguito questa operazione attraverso un esplicito ciclo for, anche perché quasi sempre mi interessava che l'ordine di esecuzione fosse quello indicato (e che questo metodo non specifica).

Le operazioni di costruzione del testo sono svolte da due metodi; esamino per primo il più semplice.

- (void)
setCACAttribFor: (NSTextStorage *) txtSto ofName: (NSString*) colId
{
    VolInfo            * localItem ;
    NSMutableDictionary    * curStyle ;
    NSTextStorage * newFileList ;
    NSString        * localString ;
    id                valloasape ;
    // costruisco un nuovo contenitore per il testo, che calcolo subito
    if ( currentVol == -1 )
    {
        localString = [    NSString stringWithFormat: @"##%@##", colId ] ;
    }
    else
    {
        localItem = [ startPoint objectAtIndex: currentVol ] ;
        valloasape =[ localItem valueForKey: colId ] ;
        localString = [    NSString stringWithString: [valloasape description ] ] ;
    }
    newFileList = [[NSTextStorage alloc] initWithString: localString ];
    // adatto il font corrente
    ...
    // aggiorno la stringa
    [ txtSto setAttributedString: newFileList ] ;
}

Questo metodo deve produrre un testo in base all'argomento specificato; questo argomento è uno degli identificatori delle colonne per la presentazione dei dati, estesamente utilizzati per le NSOutlineView.

figura 05

figura 05

Il metodo verifica che ci sia un volume correntemente selezionato; in caso contrario, produce un testo di default con il nome del campo richiesto. Per produrre il testo in condizioni normali, ricorro al metodo description, che velocemente e senza troppi patemi produce una descrizione sotto forma di stringa del contenuto della colonna desiderata. Salto alcune istruzioni intermedie (argomento del prossimo paragrafo), per chiudere con l'ultima istruzione, che sostituisce il contenuto dell'istanza di NSTextStorage con la nuova stringa calcolata.

Simile nel concetto ma un po' più laborioso l'altro metodo, che recupera dalla vecchia realizzazione la sequenza di costruzione della lista dei file:

- (void)
setCACFileListFor: (NSTextStorage *) txtSto
{
    NSMutableDictionary    * curStyle ;
    NSTextStorage * newFileList ;
    int                elNum, jj, nn ;
    if ( currentVol == -1 )
    {
        newFileList = [[NSTextStorage alloc] initWithString:@"##Fil List##"];
    }
    else
    {
        // costruisco un nuovo contenitore per il testo
        newFileList = [[NSTextStorage alloc] initWithString:@""];
    }
    ...
    // recupero il primo livello del volume
    if ( currentVol == -1 )
    {
        // aggiorno la stringa
        [ txtSto setAttributedString: newFileList ] ;
        return ;
    }
    // se arrivo qui, ho un vero e proprio volume
    elNum = [ self outlineView: nil numberOfChildrenOfItem: nil ];
    // esploro il primo livello
    for ( jj = 0 ; jj < elNum; jj ++ )
    {
        FileStruct * elem ;
        // esamino tutti gli elementi uno ad uno
        elem = [ self outlineView:     nil child: jj ofItem: nil ];
        // controllo se ha figli
        nn = [ self outlineView: nil numberOfChildrenOfItem: elem ];
        // se non e' da stampare, salto
        if ( ! [ elem is2Print ] ) continue ;
        // se e' una directory, espando di un livello
        if (nn > 0 )
            addStringExpandItem (newFileList, curStyle, self, elem, nn, 0 );
        else    // lo aggiungo al testo gia' predisposto
            terminalWithLevel( newFileList, curStyle, [ elem fileName], 0, NO );
    }
    // aggiorno la stringa
    [ txtSto setAttributedString: newFileList ] ;
}

Rispetto alla versione precedente, c'è una importante modifica, riguardante la caratterizzazione del testo. Prima il testo aveva lo stile caratterizzato in maniera esplicita attraverso degli attributi (cosa che richiedeva anche un brutto espediente per impostarli su una stringa ancora vuota); qui invece utilizzo un NSDictionary per impostare una serie di attributi ai paragrafi.

Lo stile dei paragrafi

La cosa migliore per impostare lo stile di un testo è utilizzare appunti uno Stile. Lo Stile è un concetto che tutti gli elaboratori di testo hanno fatto conoscere, come una serie di attributi caratterizzanti l'aspetto di un testo: il paragrafo, l'allineamento, margini, eccetera. Anche Cocoa mette a disposizione questo meccanismo: la sua realizzazione più immediata la trovate nell'applicazione TextEdit, nella barra superiore di ogni finestra.

Uno stile è realizzato attraverso la classe NSParagraphStyle (e discendenti); tra le caratteristiche che è possibile impostare di un paragrafo, ci sono: l'allineamento del testo (a destra, centrato, eccetera), l'interlinea, i rientri (a destra, a sinistra, specifico della prima linea di testo), i punti di tabulazione, eccetera.

figura 06

figura 06

La gestione di queste caratteristiche di testo può avvenire ad esempio attraverso un menu standard, predefinito in Interface Builder: è il menu Text, che ho aggiunto, così com'è al mio menu standard (e d anche contestuale). Notare che nei menu sono presenti anche delle voci per visualizzare e nascondere i righelli, gli stessi righelli che si trovano nell'applicazione TextEdit (anche se all'interno di un elemento CCE_Text sono piuttosto infelici a vedersi).

Nel caso, raccolgo un po' di queste caratteristiche, le unisco ad una specifica di carattere, e costruisco un NSDictionary col metodo seguente:

- (NSDictionary *)
getTextAttr
{
    NSFont                * locFont = [ NSFont fontWithName: @"Georgia" size: 14 ];
    NSMutableDictionary *textAttributes = [[[NSMutableDictionary alloc] initWithCapacity:2] autorelease];
    NSMutableParagraphStyle *paraStyle = [[NSMutableParagraphStyle alloc] init];
    [ paraStyle setAlignment: NSCenterTextAlignment ] ;
    [ paraStyle setLineBreakMode: NSLineBreakByTruncatingTail ];
    [textAttributes setObject: locFont forKey:NSFontAttributeName];
    [textAttributes setObject: paraStyle forKey:NSParagraphStyleAttributeName];
    return ( textAttributes );
}

Nel dizionario ho dunque specificato di utilizzare il carattere Georgia, 14 punti, allinearlo al centro, e, nel caso in cui la linea occupi più spazio di quanto visualizzabile, troncarne la visualizzazione omettendo gli ultimi caratteri. Inserisco il tutto dentro un dizionario, che poi utilizzo, ad esempio, in sede di costruzione di un elemento di testo:

...
        temp = [ [ NSMutableAttributedString alloc] initWithString: @"Volume Name" ];
        [ temp setAttributes: [ self getTextAttr] range: NSMakeRange(0, [temp length])];
        tmpText = [[ CCE_Text alloc] initWithId: [self getNextObjId] inView: self
            withAttributedString: temp
            inRect: NSMakeRect(CM2PX(x1), CM2PX(y1), CM2PX(BACK_LABEL_VER_DIM), CM2PX(BACK_LABEL_HOR_DIM) )
            withAngle: 90 ];
        [ tmpText setAutomaticText: CATAUTOTEXT_VOLNAME ] ;
...

Con il precedente frammento di codice, costruisco una stringa, imposto i precedenti attributi di testo, e poi utilizzo questa stringa per definire un elemento CCE_Text, il quale, guarda caso, sarà un testo a riempimento automatico (col nome del volume).

Quando, all'interno dei metodi di CatDataSrc precedenti, produco il testo automatico, recupero questi attributi (in realtà, i primi che trovo, che l'utente potrebbe aver combinato un po' di modifiche sul testo rappresentato) e li impongo al nuovo testo prodotto; ad esempio, all'interno di setCACFileListFor ci sono le seguenti istruzioni (che vanno a rimpiazzare ciò che poco sopra erano i tre puntini di sospensione):

...
    curStyle = [ NSMutableDictionary dictionaryWithCapacity: 2 ] ;
    [ curStyle addEntriesFromDictionary:
        [ txtSto fontAttributesInRange: NSMakeRange(0, [txtSto length]) ] ] ;
    [ curStyle addEntriesFromDictionary:
        [ txtSto rulerAttributesInRange: NSMakeRange(0, [txtSto length]) ] ];    
...

Costruisco un dizionario, e ci inserisco la specifica di font e la specifica di righello (al momento, lo stile del paragrafo) del testo che intendo sostituire.

Quando poi, all'interno della funzione terminalWithLevel devo aggiungere una riga di testo, utilizzo proprio questo dizionario (che mi sono premunito di passare come parametro alla funzione):

void
terminalWithLevel ( NSTextStorage* str, NSMutableDictionary * curStyle, NSString *fname, int lev, BOOL boldface )
{
    NSString        * xxx ;
    NSMutableAttributedString * temp ;
    NSFont            * locFont ;
    NSMutableDictionary    * tmpD = [ NSMutableDictionary dictionaryWithDictionary: curStyle ] ;

    // costruisco la stringa col nome del file
    xxx = [ NSString stringWithFormat: @"%@%@\n", prefixLevString( lev ), fname ] ;
    // la trasformo in una AttributedString
    temp = [ [ NSMutableAttributedString alloc] initWithString: xxx ];
    // recupero il font attualmente in uso
    locFont = (NSFont*) [ tmpD objectForKey: NSFontAttributeName ] ;
    // se il caso, aggiungo il tratto Grassetto alla stringa
    if ( boldface )
        locFont = [ [NSFontManager sharedFontManager] convertFont: locFont toHaveTrait: NSBoldFontMask];
    // assegno gli attributi alla stringa
    [ tmpD setObject: locFont forKey:NSFontAttributeName];
    [ temp setAttributes: tmpD range: NSMakeRange(0, [temp length])];
    // e poi l'aggiungo all'ambaradan
    [ str appendAttributedString: temp ] ;
}

La prima cosa che faccio è una copia del dizionario, in quanto successivamente potrei aver bisogno di modificarlo localmente (per mettere in grassetto la directory, ad esempio). L'istruzione setObject:forKey: rimpiazza la descrizione di font corrente con quella specifica del caso, e poi l'istruzione con setAttributes:range: impone alla stringa le caratteristiche desiderate.

Così facendo, quando si deve aggiornare il testo automatico, non sono perse le eventuali formattazioni (diverse da quelle standard) che l'utente ha effettuato sul testo.

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