MaCocoa 022

Capitolo 022 - Informazioni e notai

Scopo del capitolo è di realizzare una finestra di 'ispezione', ovvero una finestra all'interno della quale sono presentate tutte le informazioni relative ad un file.

Sorgenti: Copio, semplifico e stravolgo un esempio su Learning Cocoa.

Primo inserimento: 2 settembre 2002

Finestra di informazioni

figura 01

figura 01

figura 03

figura 03

figura 04

figura 04

La finestra principale del catalogo è nata densa di informazioni, e si è via via spopolata. In effetti, terrei il nome del file, la data di modifica e la dimensione. La visualizzazione del resto è più o meno opzionale. Decido quindi di realizzare una finestra aggiuntiva in cui, dato un file, sono riportate per esteso le informazioni (il gruppo, il proprietario, il path completo, i permessi, eccetera). La finestra dovrebbe funzionare come la corrispondente finestra del Finder: selezionando un elemento all'interno del catalogo, automaticamente sulla finestra di info compaiono le informazioni pertinenti.

Ho collegato tramite una lunga sequenza di outlet il File's Owner ai vari campi della finestra, in modo da poterli riferire facilmente. Salvo tutto.

Delega all'applicazione

figura 05

figura 05

Adesso devo collegare una qualche voce di menu con la finestra delle informazioni. Rimango in IB ed apro MainMenu.lib, dove appunto si trova il menù dell'applicazione. Aggiungo una voce di menu proprio sotto la simile voce che attiva la palette dei comandi; la chiamo Show Info. Ora si tratta di attribuire a questa voce un qualche metodo che apra la finestra.

Con questa, siamo alla terza finestra che deve essere unica all'interno dell'applicazione: c'è la palette dei comandi, poi la finestra delle preferenze, ed adesso questa finestra delle informazioni. Seguendo Learning Cocoa, utilizzo un altro (e tre) meccanismo per fare ciò; consiste nel definire una nuova classe, AppDelegate, tramite IB e direttamente all'interno del file MainMenu.nib. Alla classe aggiungo una action, showInfoPanel, che invita la finestra a farsi vedere. Faccio una istanza di questa classe, e collego la voce di menu seguendo il paradigma target/action. A questo punto torno in XCode e scrivo il metodo:

- (IBAction)showInfoPanel:(id)sender
{
    // mostro la finestra invocando l'istanza condivisa
    [ [ InfoWinCtrl sharedInfoWinCtrl ] showWindow: sender ] ;
}

Tengo a mente il metodo di classe sharedInfoWinCtrl e vado a scrivere tutti i vari metodi del controllore della finestra delle informazioni.

Il controllore della finestra delle info

Fondamentalmente, deve scrivere questi tre metodi:

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

Con il primo metodo (fintamente) di classe, mi procuro un metodo per accedere tranquillamente all'unica istanza, condivisa a tutta l'applicazione, della finestra delle info. Il secondo ed il terzo metodo partecipano alla costruzione della finestra.

+ ( id )
sharedInfoWinCtrl
{
    static    InfoWinCtrl    * locSharedWinCtrl = nil ;
    
    if ( ! locSharedWinCtrl )
    {
        locSharedWinCtrl = [[ InfoWinCtrl allocWithZone: [self zone]] init ] ;
    }
    return ( locSharedWinCtrl );
}

Qui definisco una variabile statica, inizialmente nulla; la prima volta che questo metodo viene invocato, è costruito l'oggetto; tutte le altre volte, l'oggetto già definito è subito pronto.

La prima volta poi che questo metodo è chiamato è necessariamente eseguito anche questo metodo di init, che giustamente si preoccupa di caricare la finestra dal nib corretto:

- (id )
init
{
    self = [ self initWithWindowNibName: @"infoPanel" ];
    if ( self )
    {
        [ self setWindowFrameAutosaveName: @"Info" ];
    }
    return ( self );
}

Infine, sempre la prima volta, ma quando la finestra ha finito di caricarsi, è eseguito il seguente metodo:

- (void)
windowDidLoad
{
    // costruisco i formattatori che mi servono
    // nota come riuso bellamente quelli per la outlineView
    TOS9TCForm     *myDF1 = [[[ TOS9TCForm alloc ] init ] autorelease ];
    FileSizeForm     *myDF2 = [[[ FileSizeForm alloc ] init ] autorelease ];
    FilePosixPerm     *myDF3 = [[[ FilePosixPerm alloc ] init ] autorelease ];
    [ super windowDidLoad ] ;
    // installo i formattatori
    [ osCreator setFormatter: myDF1 ];
    [ osType setFormatter: myDF1 ];
    [ fileSize setFormatter: myDF2 ];
    [ posixPerm setFormatter: myDF3 ];
    // altro codice che per ora non interessa
}

Qui ne approfitto per associare ad alcuni campi della finestra dei formatter (proprio quelli che a suo tempo avevo costruito per la NSOutlineView, e che adesso non servono più), ricordando per inciso che per la data ho inserito in IB il formatter standard per le date.

Unificazione delle gestioni

Questo meccanismo mi piace talmente tanto che lo utilizzo anche per le finestre delle preferenze e dei comandi.

Se per la palette di comandi le cose si risolvono facilmente (rinomino anche qualche file), molto più complicata è la trasformazione della finestra delle preferenze: ne approfitto anche per fare un po' di riordino.

Dal nib principale elimino classe ed istanza di Preferences, che non servono più; ho infatti aggiunto altre due action alla classe AppDelegate in modo che permettano la visualizzazione delle finestre.

Trasformo la classe Preferences da semplice sottoclasse di NSObject a sottoclasse di NSWindowController; adesso la classe si chiama PrefWinCtrl. Ripulisco un po' i file e li adatto alla nuova situazione. Fondamentalmente si tratta di scrivere i due metodi init e windowDidLoad smembrando (ed eliminando) il vecchio metodo showPanel.

- (id )
init
{
    self = [ self initWithWindowNibName: @"prefsPane" ];
    if ( self )
    {
        [ self setWindowFrameAutosaveName: @"Preferences" ];
        // carico le preferenze dal file
        defCurrValues = [[[self class] preferencesFromDefaults] copyWithZone:[self zone]];
        // sono sia def-curr che def-file
        defFileValues = [defCurrValues retain];
        // assegno gli stessi valori anche a def-disp
        [self discardDisplayedValues];
        // inizializzo queste due variabili che mi servono poi
        lsPrefsYes = [[NSNumber alloc] initWithBool:YES];
        lsPrefsNo = [[NSNumber alloc] initWithBool:NO];
    }
    return ( self );
}

- (void)
windowDidLoad
{
    [ super windowDidLoad ] ;
    // non voglio che compaia nella lista delle finestre
    [[self window] setExcludedFromWindowsMenu:YES];
    // questo lo ignoro
    [[self window] setMenu:nil];
    // inserisco i def-curr nella finestra
    [self updatePrefWindow];
    // la piazzo al centro dello schermo
    [[self window] center];
    // porto la finestra davanti a tutti
    [[self window] makeKeyAndOrderFront:nil];
}

Nella baraonda scompaiono alcuni metodi perché accorpati in altri, ed altri sono semplificati. Vi rimando al progetto completo per vedere tutte le modifiche.

Non sono tuttavia soddisfatto; ho mescolato allegramente una classe Controller con una classe Model, e sarebbe meglio tenerle separate... Prima o poi farò qualche altra modifica, me lo sento.

Accadrà nel prossimo capitolo.

Notifiche

Arrivo adesso alla parte più interessante di tutto il capitolo, ovvero come riconoscere dall'interno della finestra delle Info che qualcosa è cambiato in una delle NSOutlineView che costituiscono il cuore del catalogo. Utilizzo l'interessante meccanismo delle notifiche.

Per capire come funziona, pensiamo alla redazione di un giornale. Il giornale è costituito da una serie di notizie, che vari giornalisti (eventualmente sparsi per il mondo) forniscono ad un centro di smistamento (la redazione). Esistono quindi una serie di agenti che producono notizie. D'altra parte, chiunque può abbonarsi al giornale; nel mondo moderno, è possibile pensare che qualcuno si abboni alle sole notizie di sport, addirittura alle sole notizie di cricket. Esistono quindi una serie di agenti che utilizzano queste notizie, e sono informati in tempo reale dell'accadimento delle notizie cui sono abbonati.

Tornando a noi, diciamo che all'interno di ogni applicazione il sistema operativo fornisce una redazione. A questa redazione i vari oggetti presenti all'interno della applicazione si rivolgono annunciando il verificarsi di determinati eventi (che so, un oggetto dedito a calcoli furibondi può annunciare il progredire delle operazioni, rendendo disponibile lo stato di avanzamento dei calcoli). Questa pubblicazione di eventi avviene automaticamente (come accade per molti oggetti dell'interfaccia) oppure esplicitamente, tramite messaggi appositi. D'altra parte, ogni oggetto può abbonarsi a certe categorie di eventi, ovvero ricevere messaggi in corrispondenza del verificarsi di determinati eventi. La redazione alacremente riceve eventi da molti oggetti, verifica se ci sono oggetti abbonati a questi eventi, se il caso li reinstrada, altrimenti, se nessuno è interessato, li butta via.

La cosa interessante del meccanismo è che chi produce gli eventi non deve sapere chi riceve, anzi, produce eventi in tutta tranquillità senza porsi il problema di capire se a qualcuno possano interessare. Viceversa, lo stesso evento può interessare a diversi agenti, e questi possono utilizzarlo in maniera concorrente e con comportamenti differenti.

Gestire la finestra Info

Tornando al mio problema, scopro che gli oggetti della classe NSOutlineView pubblicano una notevole quantità di eventi; si trovano a notificare accadimenti quali lo spostamento ed il ridimensionamento di una colonna, l'espansione o la contrazione di un elemento, e, cosa che mi interessa, quando cambia la selezione corrente. Queste notifiche avvengono automaticamente, e normalmente cadono nel vuoto dal momento che nessuno si è abbonato. Detto questo, risulta molto semplice modificare l'aspetto della finestra di informazioni in corrispondenza di un cambio di selezione all'interno di una NSOutlineView.

In primo luogo, occorre sottoscrivere l'abbonamento. Ciò si ottiene con la seguente istruzione all'interno del metodo winwowDidLoad:

[[ NSNotificationCenter defaultCenter] addObserver: self
    selector: @selector( selectionChanged: )
    name: NSOutlineViewSelectionDidChangeNotification object: nil ] ;

Dapprima si rintraccia la redazione, ricavando un oggetto di classe NSNotificationCenter; a questo oggetto gli si invia un messaggio dal nome lungo come:

addObserver: selector:name:object:

in cui si dice di aggiungere un abbonamento a nome di qualcuno (l'argomento di observer), a tutti gli eventi di un certo tipo (l'argomento di name) coinvolgenti un determinato oggetto (l'argomento di object). Quando ciò accade, la redazione deve inviare un certo messaggio (l'argomento di selector), ovviamente a chi ha sottoscritto l'abbonamento. Nel mio caso, dico di inviare il messaggio selectionChanged: (che è un metodo che presto scriverò) quando cambia la selezione corrente all'interno di una NSOutlineView; avendo specificato nil come oggetto mittente di interesse, verranno inviate tutte le notifiche di quel tipo, indipendentemente da chi ha generato la notifica stessa (il che va bene: qualsiasi NSOutlineView all'interno dell'applicazione sarà un catalogo).

Non rimane altro che scrivere il metodo che svolge tutto il lavoro; a parte le prime righe, è tutta bassa manovalanza:

- (void )
selectionChanged: ( NSNotification *) notification
{
    int    row ;
    FileStruct    * locItem ;
    // guardo qual e' la outlineView interessata
    NSOutlineView    * outView = [ notification object ] ;
    // vedo se ci sono selezioni in corso
    row = [ outView selectedRow] ;
    // se non ce ne sono, faccio nulla
    if ( row == -1 )
        return ;
    // recupero l'elemento selezionato
    locItem = [ outView itemAtRow: row ] ;
    // adesso, piu' o meno ordinatamente, estraggo i vari campi dell'elemento
    // e li attribuisco agli elementi dell'interfaccia della finestra di info
    // stringa per stringa
    [ fileType setStringValue: [ locItem fileType]];
    [ groupName setStringValue: [ locItem ownGroupName]];
    [ ownerName setStringValue: [ locItem ownerName]];
    [ fullPath setStringValue: [ locItem fileFullPath]];
    // piglio l'intero e lo traformo in stringa
    [ fsFileNum setStringValue: [ NSString stringWithFormat: @"%d", [ locItem fsFileNum]] ];
    [ fsNum setStringValue: [ NSString stringWithFormat: @"%d", [ locItem fsNum]]] ;
    // questi hanno un formattatore appiccicato, gli passo direttamente l'intero
    [ fileSize setIntValue:[ locItem fileSize] ];    
    [ osCreator setIntValue: [ locItem creatorCode]] ;
    [ osType setIntValue: [ locItem typeCode] ];
    [ posixPerm setIntValue: [ locItem filePosixPerm] ];
    // qui, con il formattatore di data, gli passo direttamente l'oggetto NSDate
    [ modDate setObjectValue: [ locItem modDate]];
    // infine, il nome del file e' il titolo della finestra
    [[ self window ] setTitleWithRepresentedFilename: [ locItem fileFullPath ]] ;
}

L'argomento del metodo è un oggetto di tipo NSNotification, che contiene alcune informazioni sulla notifica che ha scatenato tutta la faccenda; ciò che mi interessa è sapere qual è la NSOutlineView che ha generato l'evento; ciò si ottiene velocemente estraendo la variabile object. Dopo, è un gioco da ragazzi: dalla NSOutlineView ricavo la linea selezionata (se ce ne è una), dalla riga ricavo direttamente l'oggetto di classe FileStruct, e da qui i vari elementi per riempire la finestra delle Info. C'è solo da fare un po' di attenzione quando ho dei campi con un formatter attaccato; e poi il colpo di mano finale, con cui si cambia il titolo della finestra di informazioni assegnandogli il nome del file. Il metodo utilizzato, al posto del più ovvio setTitle, aggiunge un tocco di classe aggiuntivo: nel titolo della finestra è aggiunta anche l'icona del file.

Due minuti dopo: troppo bello per essere vero. Se il file non è in linea, non solo l'icona non viene riportata, ma anche il nome del file non è cambiato. Occorre procedere ad una modifica. Tuttavia, anche se cambio l'ultima istruzione con

if ( [ [NSFileManager defaultManager] fileExistsAtPath: [ locItem fileFullPath ] ] )
    [[ self window ] setTitleWithRepresentedFilename: [ locItem fileFullPath ]] ;
else     [[ self window ] setTitle: [ locItem fileName ]] ;

dove mi chiedo se il file sotto esame esiste, e solo se esiste uso il metodo che mostra l'icona, le cose non vanno bene. Una volta che il file non esiste più, l'ultima icona utilizzata rimane lì, dal momento che setTitle non pulisce completamente il titolo della finestra (provate ad aggiungere un CD o un hard disk esterno ad un catalogo, giocate un po' con la finestra delle Info con il volume in linea, poi espellete il CD e continuate a giocare con la finestra delle Info; capirete meglio ciò che sto faticosamente cercando di spiegare).

figura 06

figura 06

Alla fine, mi accontento di utilizzare setTitle.

Ci sono molte cose che mi disturbano ancora. Ad esempio, cambiando finestra di catalogo, la finestra delle Info non si aggiorna (dopotutto, la selezione non è cambiata). Deselezionando un elemento, la finestra delle info non cambia, e lascia esposte le informazioni relative all'ultimo file selezionato. Se apro la finestra delle info con un elemento già selezionato, la finestra delle info non presente le informazioni relative (dopo tutto, l'evento si è già verificato...). Ma di questo mi preoccuperò in altri tempi.

Accadrà nel capitolo 26.

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