MaCocoa 023

Capitolo 023 - Preferenze rivisitate

In questo capitolo raggruppo due argomenti con parecchi effetti collaterali: la ristrutturazione del meccanismo delle Preferenze (ve l'avevo detto...) con la sua estensione, e la manipolazione della NSOutlineView del catalogo con la variazione del numero di colonne.

Sorgenti: Documentazione Apple e un po' di sudore.

Primo inserimento: 2 settembre 2002

Ancora Preferenze

Avevo detto che non ero soddisfatto del meccanismo di gestione delle preferenze; in particolare non mi piaceva il fatto che unica classe svolgesse funzioni di Controller della finestra e come Model del dizionario delle preferenze. Quindi, il primo argomento è la divisione della classe PrefWinCtrl in due. Ne approfitto per migliorare due aspetti: ripulire ancor di più il meccanismo ed incrementare le possibilità di scelta dell'utente.

La mia versione definitiva (e direi anche riusabile) del meccanismo di preferenze si appoggia ad una classe che nomino UserPrefs così strutturata:

@interface UserPrefs : NSObject {
    // valori correnti delle preferenze
    NSMutableDictionary         *defCurrValues;
}
+ ( id ) getPrefValue: (id) key ;
+ ( NSMutableDictionary*) getPrefs ;
+ (void) loadPrefsFromDefault ;
+ (void) savePrefsToDefault: ( NSMutableDictionary*) newPref ;

La classe ha come unica variabile d'istanza un dizionario con l'elenco dei campi di Preferenze. Sarà inoltre cura della classe fare in modo che esista una sola istanza della stessa, comune a tutta l'applicazione; questo è il motivo per cui l'accesso avviene solo attraverso i quattro metodi di classe indicati.

Il primo metodo è quello utilizzato da qualunque oggetto che vuole conoscere il valore di una delle Preferenze. Deve solo inviare il seguente messaggio:

[ UserPrefs getPrefValue: <chiave>]

Sono stato nel dubbio se aggiungere metodi di comodo per recuperare direttamente i valori delle Preferenze, ma poi ho lasciato perdere...

Gli altri tre metodi sono adatti agli oggetti che sono coinvolti nella gestione dell'intero insieme delle Preferenze: nella fattispecie, il controllore della finestra delle Preferenze PrefWinCtrl.

La realizzazione dei metodi deriva dalla vecchia gestione: ho definito due variabili statiche, inizializzate a nil. La prima per tenere traccia dell'unica istanza della classe, e la seconda per mantenere il dizionario delle Preferenze, così come deciso dal programmatore

static UserPrefs        * locSharedPref = nil ;
static NSMutableDictionary     * defCodeValues = nil ;

Poi ci sono due metodi di facile realizzazione

+ ( id ) getPrefValue: (id) key ;
{
    return [[locSharedPref defCurrValues] objectForKey:key];
}
+ ( NSMutableDictionary*) getPrefs
    { return [ locSharedPref defCurrValues] ; }

Molto più interessante il metodo init, che svolge parecchio lavoro interessante:

- ( id )
init
{
    // mi preoccupo che non siano create piu' istanze delle preferenze
    if ( locSharedPref )
        return ( locSharedPref );
    // se arrivo qui devo costruire l'istanza condivisa
    self = [ super init ] ;
    // costruisco il dizionario delle preferenze
    defCodeValues = [ NSMutableDictionary dictionaryWithContentsOfFile:
        [[NSBundle mainBundle] pathForResource: @"defCodeValues" ofType: @"dict"]];
    [ defCodeValues retain ];
    // l' istanza condivisa e' proprio questa
    locSharedPref = self ;
    // carico le preferenze dal file
    [ UserPrefs loadPrefsFromDefault ];
    return (self );    // restituisco l'istanza
}

In primo luogo, l'inizializzazione ha luogo solo una volta, confrontando il valore della variabile locSharedPref. Il dizionario delle Preferenze costruito dal codice questa volta preleva i valori da un file (che poi si troverà all'interno del bundle dell'applicazione), per una più facile manutenzione (ricordo che nella realizzazione precedente il dizionario era inizializzato direttamente all'interno del codice con chiavi e valori). Importante il messaggio di retain inviato alla variabile: ho impiegato un bel po' di tempo per rendermi conto che il metodo dictionaryWithContentsOfFile: costruisce un oggetto autorelease, che scompariva dopo un po'....

Di seguito, assegno l'istanza appena creata alla variabile condivisa locSharedPref, per poi invocare un metodo che carica le preferenze dal file dei Default.

Per quanto riguarda gli altri metodi, non ci sono differenze concettuali di rilievo nei confronti della realizzazione precedente (a parte una piccola ma interessante istruzione che vedrò più tardi):

+ (void) loadPrefsFromDefault ;
+ (void) savePrefsToDefault: ( NSMutableDictionary*) newPref ;

Da notare che sono comunque metodi di classe per poterli invocare sempre e comunque attraverso messaggi del tipo

[ UserPrefs loadPrefsFromDefault ] ;

che semplificano notevolmente la gestione.

figura 04

figura 04

Rimane da precisare il momento in cui la classe è istanziata. Nella versione precedente questo avveniva nel momento in cui la finestra era aperta per la prima volta; ciò non è una buona cosa (le Preferenze occorrono fin dal primo momento); in questa versione, faccio in modo che il lavoro venga svolto automaticamente. Basta costruire una istanza della classe all'interno di IB: si importa UserPrefs.h all'interno di MainMenu.nib in modo da avere tale classe nella lista. Da qui, si istanzia la classe, che compare dunque all'interno della finestra. In questo modo, al caricamento del menu principale, è costruita anche l'istanza della classe UserPrefs.

Nuovo controllore delle Preferenze

La classe PrefWinCntrl subisce una serie di modifiche importanti. In primo luogo, subisce un drastico ridimensionamento nelle variabili e nei metodi:

@interface PrefWinCtrl : NSWindowController
{
    IBOutlet NSButton    *expandBundleButton;
    IBOutlet NSButton    *showDotFilesButton;
    IBOutlet NSMatrix     *colDspMatrix;

    // contiene le pref in corso di manipolazione
    NSMutableDictionary     *defDispValues;    
}

+ ( id )    sharedPrefsCtrl ;
- ( id )    init ;
- (void)    windowDidLoad ;

- (void) userSelectUpdate:(id)sender;
- (void) userSelectRestore:(id)sender;
- (void) userSelectCancel:(id)sender;
- (void) updatePrefWindow;

- ( NSMutableDictionary*) defDispValues ;
- (void) setDefDispValues: ( NSMutableDictionary*) newPref ;

Rimane una unica variabile d'istanza (a parte gli outlet), per tenere conto dei valori delle Preferenze in corso di modifica. Tuttavia, la parte che ha subito più modifiche è nascosta dall'innocente outlet colDspMatrix.

Ho infatti deciso di aggiungere una (lunga) serie di Preferenze per visualizzare o mano, all'interno della finestra del catalogo, le varie colonne con gli attributi del file. Finora infatti la NSOutlineView che costruisce l'intera finestra del catalogo prevedeva un numero fisso di colonne (numero variato nel corso dei singoli capitoli di questa storia). Adesso, la NSOutlineView avrà una unica colonna sempre presente (il nome del file), mentre le altre colonne appaiono e scompaiono secondo i desideri dell'utente.

figura 01

figura 01

Per prima cosa, dunque, ho modificato l'aspetto della finestra delle preferenze. Ho aggiunto una NSBox per pura bellezza, all'interno della quale si trova una NSMatrix. Questo oggetto è in realtà un contenitore di altri oggetti, che li raggruppa ma soprattutto li tiene ordinati secondo, appunto, una matrice. Nel caso, la NSMatrix è costituita da dodici pulsanti di spunta (NSButtonCell), divisi su due colonne. Undici di questi pulsanti corrispondono alle undici possibili colonne pertinenti ai vari attributi del file. Il dodicesimo pulsante è stato disabilitato e reso trasparente.

In IB è possibile collegare l'intera NSMatrix ad un outlet, oppure i singoli pulsanti. Per mia educazione, ho collegato la NSMatrix, per poi accedere, in lettura ed impostazione, ai singoli pulsanti attraverso questo outlet.

Tornando in XCode, il metodo che mantiene il contenuto della finestra congruente con lo stato delle Preferenze è per tanto il seguente:

- (void)
updatePrefWindow
{

#define        MATRIX_SET_STATE( key, row, col)        \
    [colDspMatrix setState: ([ [defDispValues objectForKey: key] boolValue] ? 1 : 0)    \
                     atRow: row column: col ]


    // recupero il valore, ed imposto il bottone di conseguenza
    [expandBundleButton setState:[[defDispValues objectForKey:keyExpandBundle] boolValue] ? 1 : 0];
    [showDotFilesButton setState:[ [defDispValues objectForKey: keyShowDotFiles] boolValue] ? 1 : 0 ] ;

    MATRIX_SET_STATE( keyColModDate,     0, 0 );
    MATRIX_SET_STATE( keyColFileSize,     1, 0 );
    MATRIX_SET_STATE( keyColGroupName,     2, 0 );
    MATRIX_SET_STATE( keyColOwnerName,     3, 0 );
    MATRIX_SET_STATE( keyColPosixPerm,     4, 0 );
    MATRIX_SET_STATE( keyColFileType,     5, 0 );
    MATRIX_SET_STATE( keyColFullPath,     0, 1 );
    MATRIX_SET_STATE( keyColOSType,     1, 1 );
    MATRIX_SET_STATE( keyColCreator,     2, 1 );
    MATRIX_SET_STATE( keyColFsFileNum,     3, 1 );
    MATRIX_SET_STATE( keyColFsNum,         4, 1 );
}

Sono stato (altrove) criticato per il mio uso smodato di macro (il costrutto #define all'interno del metodo). Secondo me un uso accorto delle macro permette di rendere il codice molto più leggibile ed ordinato. Ad ogni modo, a parte le due istruzioni iniziali che non dovrebbero presentare sorprese, nelle istruzioni successive (le macro) si svolge l'accesso ai singoli pulsanti della NSMatrix. Esiste allo scopo un metodo apposito, che permette di impostare lo stato delle celle dandone le coordinate, ovvero il numero di riga e di colonna. Occorre giusto notare che gli indici partono da Zero (seguendo la convenzione C) piuttosto che da Uno (la convenzione naturale).

Perfettamente simmetrico il metodo di aggiornamento delle Preferenze, attivato quando l'utente fa clic sul pulsante di Ok.

- (void)
userSelectUpdate:(id)sender
{

#define        MATRIX_GET_STATE( key, row, col )                        \
    [defDispValues setObject:                                                             \
        ([[colDspMatrix cellAtRow: row column:col] state ] ? lsPrefsYes : lsPrefsNo)     \
        forKey:key]


    // devo recuperare i valori dalla finestra
    [defDispValues setObject:
        ([expandBundleButton state] ? lsPrefsYes : lsPrefsNo)
        forKey:keyExpandBundle];
    [defDispValues setObject:
        ([showDotFilesButton state] ? lsPrefsYes : lsPrefsNo)
        forKey:keyShowDotFiles];

    MATRIX_GET_STATE( keyColModDate,     0, 0 );
    MATRIX_GET_STATE( keyColFileSize,     1, 0 );
    MATRIX_GET_STATE( keyColGroupName,     2, 0 );
    MATRIX_GET_STATE( keyColOwnerName,     3, 0 );
    MATRIX_GET_STATE( keyColPosixPerm,     4, 0 );
    MATRIX_GET_STATE( keyColFileType,     5, 0 );
    MATRIX_GET_STATE( keyColFullPath,     0, 1 );
    MATRIX_GET_STATE( keyColOSType,     1, 1 );
    MATRIX_GET_STATE( keyColCreator,     2, 1 );
    MATRIX_GET_STATE( keyColFsFileNum,     3, 1 );
    MATRIX_GET_STATE( keyColFsNum,         4, 1 );
        
    // salvo le preference su file
    [ UserPrefs savePrefsToDefault: defDispValues ];
    // e per finire nascondo la finestra
    [[self window] setIsVisible: FALSE ];
}

Qui la cosa è leggermente più pasticciata, dal momento che per leggere lo stato di una cella, dapprima occorre recuperare l'oggetto cella (col metodo cellAtRow:column:) e poi inviargli il messaggio state.

Alla fine, aggiorno le preferenze scrivendole sul file, e nascondo la finestra.

Gli altri metodi della classe PrefWinCtrl non sono particolarmente interessanti e non hanno subito modifiche di rilievo.

Ricordo che la visualizzazione della finestra avviene sempre attraverso l'uso della classe delegata AppDelegate.

Modificare l'aspetto di NSOutlineView

Ora che all'interno delle Preferenze c'è scritto quali colonne devono essere visualizzate all'interno della finestra, occorre scrivere un po' di codice per adeguare la NSOutlineView a quanto richiesto. Allo scopo, ho scritto il seguente metodo:

- (void)
prefsUpdated: ( NSNotification *) notification
{
    // recupero le preferences e mostro o meno la colonna
    setupColumn( outlineView, [[ UserPrefs getPrefValue: keyColFullPath] boolValue], COLID_FULLPATH );
    setupColumn( outlineView, [[ UserPrefs getPrefValue: keyColModDate] boolValue], COLID_MODDATE );
    setupColumn( outlineView, [[ UserPrefs getPrefValue: keyColFileSize] boolValue], COLID_FILESIZE );
    setupColumn( outlineView, [[ UserPrefs getPrefValue: keyColGroupName] boolValue], COLID_GROUPNAME );
    setupColumn( outlineView, [[ UserPrefs getPrefValue: keyColOwnerName] boolValue], COLID_OWNERNAME );
    setupColumn( outlineView, [[ UserPrefs getPrefValue: keyColPosixPerm] boolValue], COLID_POSIXPERM );
    setupColumn( outlineView, [[ UserPrefs getPrefValue: keyColFileType] boolValue], COLID_FILETYPE );
    setupColumn( outlineView, [[ UserPrefs getPrefValue: keyColCreator] boolValue], COLID_OSCREATOR );
    setupColumn( outlineView, [[ UserPrefs getPrefValue: keyColOSType] boolValue], COLID_OSTYPE );
    setupColumn( outlineView, [[ UserPrefs getPrefValue: keyColFsFileNum] boolValue], COLID_FSFILENUM );
    setupColumn( outlineView, [[ UserPrefs getPrefValue: keyColFsNum] boolValue], COLID_FSNUM );
    [ outlineView reloadData ];
    [ outlineView sizeLastColumnToFit ] ;
}

Questo metodo non serve a molto se non spiego prima la funzione seguente:

void
setupColumn( NSOutlineView *outView, bool addRem, NSString * colId )
{
    NSTableColumn    *tmpTC ;
    // vedo se esiste una colonna relativa
    tmpTC = [ outView tableColumnWithIdentifier: colId] ;
    // se non c'e', e devo rimuoverla...
    if ( (tmpTC == nil) && (addRem == FALSE) )
        return ;     // devo fare nulla
    // se c'e', e devo aggiungerla
    if ( tmpTC != nil && addRem )
        return ;    // devo fare nulla
    // se non c'e', e devo aggiungerla
    if ( tmpTC == nil && addRem )
    {
        // creo la NSTableColumn con l'identificatore
        tmpTC = [ [NSTableColumn alloc] initWithIdentifier: colId ];
        // imposto il titolo della colonna
        [[tmpTC headerCell] setStringValue: NSLocalizedString( colId, colId ) ];
        // attacco eventualmente un formattatore
        attachFormatter( tmpTC, colId );
        // aggiungo la colonna
        [outView addTableColumn: tmpTC ];
        return ;    // ho finito
    }
    // se arrivo qui, sicuramente c'e' la colonna e devo rimuoverla
    [outView removeTableColumn: tmpTC ];
}

Questa funzione intende aggiungere o eliminare una colonna di una NSOutlineView. L'operazione da effettuare è data dal valore della variabile booleana addRem: se questa è TRUE, devo aggiungere la colonna, se FALSE devo rimuoverla. La colonna da aggiungere dovrà avere l'identificatore specificato dall'argomento colID.

In primo luogo, vedo se tale colonna esiste o meno. Se infatti la colonna non esiste e devo rimuoverla, devo fare nulla. Ugualmente, se la colonna esiste e devo aggiungerla, devo fare nulla.

Le operazioni più interessanti si hanno quando la colonna non c'è e bisogna aggiungerla. Si costruisce allora un oggetto della classe NSTableColumn con opportuno identificatore; gli si assegna un titolo (ricavo il titolo utilizzando l'identificatore della colonna e prelevandolo da un file di stringhe localizzate; ho già fatto ciò in uno dei primi capitoli di Macocoa); se la colonna è adatta, gli assegno un formatter (ci arrivo tra breve); alla fine, aggiungo la colonna alla NSOutlineView.

L'ultimo caso rimasto, di eliminare una colonna presente, si sbriga con una sola istruzione col metodo removeTableColumn:.

Sono adesso in grado di spiegare il metodo prefsUpdated:. Dimentichiamo il suo argomento. Il metodo è costruito essenzialmente da una ripetuta invocazione della funzione utilizzando come argomenti gli identificatori delle varie colonne e lo stato di visualizzazione delle stesse, così come prelevato dalle preferenze.

figura 02

figura 02

Il metodo è concluso da due istruzioni; la prima dice alla NSOutlineView di ridisegnarsi, dal momento che, molto probabilmente, è cambiato l'aspetto. La seconda è presente per ragioni estetiche: quando riduco il numero di colonne, può verificarsi che queste occupino meno spazio di quanto disponibile nella finestra, lasciando inestetici spazi non occupati da alcuno. Col metodo sizeLastColumnToFit si fa appunto in modo che l'ultima colonna si espanda fino a coprire tutto lo spazio disponibile.

Rimane la funzione per attribuire i formatter alle colonne:

void
attachFormatter( NSTableColumn *tc, NSString * colId )
{
    if ( [ colId isEqual: COLID_OSTYPE ] )
    {
        TOS9TCForm     *myDF1 = [[[ TOS9TCForm alloc ] init ] autorelease ];
        [[tc dataCell ] setFormatter: myDF1 ];
        return ;
    }
    if ( [ colId isEqual: COLID_OSCREATOR ] )
    {
        TOS9TCForm     *myDF1 = [[[ TOS9TCForm alloc ] init ] autorelease ];
        [[tc dataCell ] setFormatter: myDF1 ];
        return ;
    }
    if ( [ colId isEqual: COLID_FILESIZE ] )
    {
        FileSizeForm     *myDF2 = [[[ FileSizeForm alloc ] init ] autorelease ];
        [[tc dataCell ] setFormatter: myDF2 ];
        return ;
    }
    if ( [ colId isEqual: COLID_POSIXPERM ] )
    {
        FilePosixPerm     *myDF3 = [[[ FilePosixPerm alloc ] init ] autorelease ];
        [[tc dataCell ] setFormatter: myDF3 ];
        return ;
    }
    if ( [ colId isEqual: COLID_MODDATE ] )
    {
        NSDateFormatter *myDF4 = [[NSDateFormatter alloc]
            initWithDateFormat:@"%d %b %Y" allowNaturalLanguage:YES];
        [[tc dataCell ] setFormatter: myDF4 ];
        return ;
    }
}

L'unica cosa degna di nota è la sezione dedicata al formatter della colonna relativa alla data di modifica; fino ad adesso questo formatter è stato applicato direttamente in IB; qui bisogna inserirlo esplicitamente dal momento che la colonna è stata costruita ex-novo.

Ancora notifiche

L'ultima cosa da fare è agganciare l'aggiornamento della NSOutlineView con l'aggiornamento delle preferenze. I più accorti tra voi avranno il sospetto che tale aggancio sarà realizzato attraverso il meccanismo delle notifiche (come l'argomento del metodo prefsUpdated: faceva prevedere).

L'idea è di fare in modo che quando le preferenze sono aggiornate, l'oggetto UserPrefs invia un messaggio di notifica al resto dell'applicazione. Ecco quindi come si presenta il metodo savePrefsToDefault:, compresa l'ultima istruzione che vi avevo preavvertimento.

+ (void)
savePrefsToDefault: ( NSMutableDictionary*) locDict
{
    // recupero il file dei defaults
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    // inserisco ordinatamente i vari elementi
    writeBoolDefault( locDict, defaults, keyExpandBundle );
    writeBoolDefault( locDict, defaults, keyShowDotFiles );

    writeBoolDefault( locDict, defaults, keyExpandBundle );
    writeBoolDefault( locDict, defaults, keyShowDotFiles );
    writeBoolDefault( locDict, defaults, keyColFullPath );
    writeBoolDefault( locDict, defaults, keyColModDate );
    writeBoolDefault( locDict, defaults, keyColFileSize );
    writeBoolDefault( locDict, defaults, keyColGroupName );
    writeBoolDefault( locDict, defaults, keyColOwnerName );
    writeBoolDefault( locDict, defaults, keyColPosixPerm );
    writeBoolDefault( locDict, defaults, keyColFileType );
    writeBoolDefault( locDict, defaults, keyColCreator );
    writeBoolDefault( locDict, defaults, keyColOSType );
    writeBoolDefault( locDict, defaults, keyColFsFileNum );
    writeBoolDefault( locDict, defaults, keyColFsNum );
    // dico a tutti che le pref sono state aggiornate
    [[NSNotificationCenter defaultCenter]
        postNotificationName: PREF_UPDATE_NOTIFICATION object:self];
}

A parte la serie di scritture dei vari campi delle Preferenze, l'ultima istruzione invia appunto una notifica dal nome PREF_UPDATE_NOTIFICATION alla redazione; da notare come non si preoccupi per nulla del fatto che ci sia qualcuno ad ascoltare.

Corrispondentemente, all'interno della classe CatalogDoc da qualche parte ci deve essere un abbonamento a queste notifiche: eseguo tutto ciò (e qualcosa di più) all'interno del metodo windowControllerDidLoadNib::

- (void)
windowControllerDidLoadNib:    (NSWindowController *) aController
{
    ### vecchie istruzioni ###

    [ outlineView setAutosaveName: @"outlineViewOption" ];
    [ outlineView setAutosaveTableColumns: YES ];
    // mi abbono all'evento: pref aggiornate
    [[ NSNotificationCenter defaultCenter] addObserver: self
        selector: @selector( prefsUpdated: )
        name: PREF_UPDATE_NOTIFICATION object: nil ] ;
    // recupero le pref ed adeguo la finestra
    // in realta', faccio finta che siano state aggiornate le prefs
    [[NSNotificationCenter defaultCenter]
        postNotificationName: PREF_UPDATE_NOTIFICATION object:self];
}

La penultima istruzione è la sottoscrizione dell'abbonamento: quando un qualsiasi oggetto (argomento nil) esegue una notifica PREF_UPDATE_NOTIFICATION, invia all'istanza di CatalogDoc (l'argomento dell'Observer è self) il messaggio prefsUpdated:. Per essere poi sicuro che la finestra del catalogo si costruisca congruentemente alle preferenze espresse, si invia una notifica di preferenze aggiornate. Lo so bene che le preferenze non sono state aggiornate, ma ciò costringe la finestra appena costruita ad adeguarsi, attraverso il metodo prefsUpdated: (che per altro avrei potuto inviare direttamente: ma in tal caso avrei dovuto costruito un adeguato oggetto NSNotification da passare come argomento).

figura 03

figura 03

Mi rimangono da spiegare le altre due istruzioni presenti nel pezzo di codice mostrato. Queste non servono altro che a mantenere all'interno delle Preferenze la situazione delle colonne (le dimensioni orizzontali, visto che quali colonne sono presenti è già regolato da altre opzioni). a causa di queste due istruzione, all'interno del file delle Preferenze compaiono nuovi campi che appunto mantengono queste informazioni.

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