MaCocoa 037

Capitolo 037 - Chiare e fresche sorgenti

Continuo dai capitoli precedenti: qui si parla della sorgente dei dati.

Leggo la documentazione...

Primo inserimento: 5 gennaio 2004

La sorgente dati

La classe sorgente di dati è il cuore di tutte le rappresentazioni dei dati all'interno delle finestre. Ho definito una nuova classe CatDataSrc, erede della precedente LSDataSource. Le variabili d'istanza non sono molto cambiate:

@interface CatDataSrc : NSObject {
    // elenco dei volumi
    NSMutableArray    * startPoint ;
    // criterio di ordine
    NSString        * orderColumn ;
    // direzione
    BOOL            orderDirection ;
    // il documento cui fa riferimento
    CdCatDoc        * refDoc ;
    // volume correntemente selezionato
    int                currentVol ;
}

C'è in più una variabile intera che tiene conto del volume correntemente selezionato nella finestra CoverWinCtl; il suo utilizzo è spiegato successivamente.

I metodi per questa classe sono numerosi, e divisibili in vari gruppi. Il primo gruppo contiene i metodi per la gestione della struttura dati:

- (id)        init ;
- (void)    dealloc ;
- (void)    addFileEntry: (id) newEntry ;
- (void)    removeEntry: (FileStruct *) newEntry ;

I primi tre non sono molto diversi dalla realizzazione precedente; l'ultimo metodo è stato introdotto per semplificare al resto dell'applicazione il processo di eliminazione di un elemento. A parte le ultime due istruzioni (che saranno chiare più avanti), serve sostanzialmente ad isolare la funzione deleteItemFromHierarchy che, essendo chiamata ricorsivamente, conviene non sia pensata come metodo.

- (void )
removeEntry: (FileStruct *) fInfo
{
    //considero ricorsivamente gli start points e vado
    deleteItemFromHierarchy( startPoint, fInfo );    
    // ovviamente, il documento e' stato sporcato
    [ refDoc updateChangeCount: NSChangeDone ] ;
    // rinfresco il contenuto delle finestre
    [ refDoc refreshWinsWithListSts: nil coverSts: nil ];
}

Il secondo gruppo di metodi gestisce la tableView della finestra CoverWinCtl:

// per la visualizzazione della lista dei volumi
- (int)        numberOfRowsInTableView:(NSTableView *)tv;
- (id)        tableView:(NSTableView *)tv objectValueForTableColumn:(NSTableColumn *)tc row:(int)row;
- (BOOL)    tableView:(NSTableView *)tv shouldEditTableColumn:(NSTableColumn *)tc row:(int)row ;
// metodi delegati
- (void)    tableViewSelectionDidChange:(NSNotification *)aNotification

I primi tre metodi sono quelli chiamati in accordo alla gestione della sorgente di dati; il quarto metodo invece fa parte dei metodi che possono essere realizzati dalla classe delegata. Pur non essendo elegante metterli tutti assieme, ho preferito accorparli qui per averli sottomano tutti assieme, ben consapevole della loro differenza.

Il terzo gruppo di metodi gestisce le due outlineView, una della finestra ListWinCtl e l'altra della finestra CoverWinCtl.

// per la visualizzazione della lista dei file
- (int)    outlineView: (NSOutlineView *) outlineView numberOfChildrenOfItem: (id) item;
- (BOOL)outlineView: (NSOutlineView *) outlineView isItemExpandable: (id) item ;
- (id)    outlineView: (NSOutlineView *) outlineView child: (int) index ofItem: (id) item ;
- (id)    outlineView: (NSOutlineView *) outlineView objectValueForTableColumn: (NSTableColumn *) tableColumn byItem: (id) item;
- (void)outlineView: (NSOutlineView *) outlineView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn byItem:(id)item ;
// metodi delegati
- (BOOL)outlineView: (NSOutlineView *) oView shouldSelectTableColumn:(NSTableColumn *)tableColumn;
- (BOOL)outlineView: (NSOutlineView *) oView shouldEditTableColumn: (NSTableColumn *) tableColumn item: (id) item ;
- (void)outlineView: (NSOutlineView *) oView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)aTableColumn item:(id)item ;
- (void)outlineViewSelectionDidChange:(NSNotification *)notification

Anche qui, i primi cinque metodi sono propri di una sorgente dati, mentre gli altri quattro sono della classe delegata.

La lista dei volumi

La gestione della tableView della finestra CoverWinCtl è molto semplice e lineare. Ciò che deve essere visualizzato è in pratica il vettore startPoint. L'unico metodo a presentare qualcosa di interessante è il quarto (delegato).

- (int)        numberOfRowsInTableView:(NSTableView *)tv
{
    // numero di volumi
    return ( [ startPoint count]);
}

- (id)        tableView:(NSTableView *)tv objectValueForTableColumn:(NSTableColumn *)tc row:(int)row
{
    // c'e' sempre e solo una sola colonna
    // predispongo l'immagine da mostrare
    [ [tc dataCell] setImage: [[startPoint objectAtIndex: row] fileIcon ] ];
    // restituisco il volume i-esimo
    return ( [ [startPoint objectAtIndex: row ] fileName ]);
}

- (BOOL)    tableView:(NSTableView *)tv shouldEditTableColumn:(NSTableColumn *)tc row:(int)row
{
    return ( NO );
}

- (void)
tableViewSelectionDidChange:(NSNotification *)aNotification
{
    NSTableView        * ov = (NSTableView*)[ aNotification object ] ;
    // e' cambiata la selezione
    currentVol = [ ov selectedRow ] ;
    // rinfresco le finestra
    [ refDoc refreshWinsWithListSts: nil coverSts: @"aloha" ];
}

Il metodo tableViewSelectionDidChange: è invocato tramite una notifica alla classe delegata. La notifica avviene quando cambia la selezione all'interno della tableView. Anche se la notifica è possibile solo a partire da una sola tableView, ho voluto esplicitamente ricavare l'oggetto della notifica (la NSTableView la cui selezione è mutata) come esercizio. Da questo oggetto ricavo l'indice della riga selezionata (che è -1 nel caso in cui non ci siano righe selezionate), e lo assegno alla variabile d'istanza currentVol. Questa variabile sarà usata per visualizzare correttamente la outlineView della finestra CoverWinCtl.

Due liste una sorgente

figura 08

figura 08

Il problema di utilizzare una unica sorgente dati per visualizzare elementi all'interno di due distinte outlineView (distinte anche per come è organizzato il contenuto) sta nel riconoscere univocamente la outlineView che invoca i metodi. Il meccanismo è molto semplice: attribuisco alle due outlineView un codice di tag differente. Occorre quindi usare Interface Builder per assegnare due valori di tag differenti alle due outlineView. Poi, all'interno dei metodi, la prima istruzione eseguita discrimina proprio questo codice.

Se il codice di tag è caratteristico della outlineView della finestra ListWinCtl, si utilizza essenzialmente il codice della versione precedente. Se invece occorre fornire i dati della outlineView della finestra CoverWinCtl, ci sono due situazioni di cui tenere conto.

Innanzitutto, occorre verificare se c'è un elemento selezionato (un volume) nella tableView. È questo il compito della variabile currentVol, predisposta come visto in precedenza. Se non ci sono volumi selezionati, la outlineView rimane vuota. Se c'è un elemento selezionato, allora bisogna saltare il primo livello (l'elenco dei volumi) e passare al primo livello del file selezionato. Ho adattato questa tecnica cercando di unificare la trattazione il più possibile, evitando troppi distinguo. Ne è venuto fuori una cosa di cui non sono pienamente soddisfatto, ma che per il momento tengo così.

Vado per ordine.

- (int)    
outlineView:            (NSOutlineView *)outlineView
    numberOfChildrenOfItem:    (id)item
{
    VolInfo * localItem ;
    // se sto visualizzando la outlineView della CoverCtlWin
    if ( [ outlineView tag] == TAG_CDCATDOC_FILELIST )
    {
        // se non c'e' alcuna selezione, devo mostrare nulla
        if ( currentVol == -1 )
            return ( 0 ) ;
        // se arrivo qui, c'e' un elemento selezionato
        // se l'item e' nil, stiamo parlando della radice
        if (item == nil)
            // salto di un livello proponendo il volume come radice
            localItem = [ startPoint objectAtIndex: currentVol ];
        // altrimenti e' un elemento standard
        else localItem = item ;
        // da qui si prosegue normalmente
    }
    else if ( [ outlineView tag] == TAG_CDCATDOC_FULLLIST )
    {
        // se l'item e' nil, stiamo parlando della radice
        if (item == nil)
            // dico quindi che ci sono tanti elementi quanti presenti nel vettore
            return( [ [self startPoint] count ]);
        // imposto l'elemento radice come quello corrente
        localItem = item ;
    }
    // da qui in poi, la trattazione e' la stessa
    // se arrivo qui, mi si chiede quali figli ha un file
    // tratto subito i file normali, che non hanno figli
    if ( [localItem numOfFiles] == 0 )
        return ( 0 ) ;
    // se arrivo qui, ho una directory
    // se devo mostrare anche i dotfiles, conto tutto
    if ( [[ UserPrefs getPrefValue: keyShowDotFiles] boolValue] )
        return ( [localItem numOfFiles] ) ;
    // se arrivo qui, devo eliminare dal computo i dotFiles
    // ed alora, mi tocca esaminare tutti i file e contare quelli
    // che mi interessano
    return ( countNormalFiles( localItem ) ) ;
}

Per capire quanti figli ha un elemento, distinguo subito le due outlineView. Se sto procurando i dati per la finestra CoverWinCtl, mi chiedo subito se c'è un elemento selezionato. Se così non è, devo visualizzare nulla. Se c'è un elemento selezionato, allora dovrò visualizzare l'elemento item. Se però item è nil, significa che mi sto chiedendo quanti elementi ci sono al livello più alto della outlineView. La tecnica è di saltare un livello, impostando il valore della variabile localItem con item stesso se non è nil, oppure con il volume correntemente selezionato. L'uso della variabile localItem procura una modifica anche nella parte relativa alla outline della finestra ListWinCtl: imposto il valore corrente di item se questo non è nil. Da qui in poi, le istruzioni proseguono come nella versione precedente.

Un po' più semplice il metodo in cui ci si chiede se l'elemento è espandile (se ha figli).

- (BOOL)
outlineView:            (NSOutlineView *)outlineView
    isItemExpandable:    (id)item
{    
    // se sto visualizzando la outlineView della CoverCtlWin
    if ( [ outlineView tag] == TAG_CDCATDOC_FILELIST )
    {
        // se non c'e' alcuna selezione, devo mostrare nulla
        if ( currentVol == -1 )
            return ( NO ) ;
        // se arrivo qui, c'e' un elemento selezionato
        if (item == nil)
        {
            VolInfo * curVol ;
            // mi si chiede se si puo' espandere.. dipende da
            // quanti elementi ci sono all'interno del volume
            // (che succede se non ci sono elementi?)
            curVol = [ startPoint objectAtIndex: currentVol ];
            return ( ([curVol numOfFiles] == 0) ? NO : YES );
        }
        // altrimenti, e' un elemento standard
    }
    else if ( [ outlineView tag] == TAG_CDCATDOC_FULLLIST )
    {
        if (item == nil)
            return ( YES ) ;
    }
    // se non ha figli, e' un file, non si espande
    if ( [item numOfFiles] == 0 )
        return ( NO ) ;
    // se arrivo qui, l'item ha figli
    // se devo espandere anche i bundle, espando tutto
    if ( [[ UserPrefs getPrefValue:keyExpandBundle] boolValue] )
        return ( YES ) ;
    // se arrivo qui, non devo espandere i bundle
    // espando allora i non bundle
    return (! checkIfBundleDirectory([ item fileName ]) ) ;
}

La risposta è certamente NO in assenza di volumi selezionati nella finestra CoverWinCtl. Nel caso di elemento nil, la risposta dipende dal numero di elemento presenti all'interno del volume (e sarà quasi certamente YES). In tutti gli altri casi, l'elemento non subisce un trattamento differente dal caso standard.

Sulla stessa falsariga il metodo per rintracciare il figlio i-esimo di un elemento.

- (id)
outlineView:        (NSOutlineView *) outlineView
    child:        (int) index
    ofItem:        (id) item
{
    VolInfo * localItem ;
    // se sto visualizzando la outlineView della CoverCtlWin
    if ( [ outlineView tag] == TAG_CDCATDOC_FILELIST )
    {
        // se non c'e' alcuna selezione, devo dire nulla
        if ( currentVol == -1 )
            return ( nil ) ;
        // c'e' un elemento selezionato
        if (item == nil)
                localItem = [ startPoint objectAtIndex: currentVol ];
        else    localItem = item ;
    }
    else if ( [ outlineView tag] == TAG_CDCATDOC_FULLLIST )
    {
        if (item == nil)
        {
            NSArray            * newArray ;
            NSMutableArray * tmpArray ;
            // ordino gli elementi secondo criterio
            if ( [ self orderDirection ] )
                    newArray = [ [self startPoint] sortedArrayUsingFunction: ascSortFunc
                        context: [ self orderColumn]] ;
            else    newArray = [ [self startPoint] sortedArrayUsingFunction: desSortFunc
                        context: [ self orderColumn]] ;
            // trasformo il risultato in un NSMutableArray
            tmpArray = [ NSMutableArray arrayWithCapacity: [ newArray count]];
            [ tmpArray addObjectsFromArray: newArray ];
            return( [ tmpArray objectAtIndex: index ]);
        }
        else localItem = item ;
    }
    // negli altri casi, recupero la lista
    [ localItem sortFileList: [ self orderColumn] direction: [ self orderDirection ] ];
    // se visualizzo anche i dotFiles, non c'e' problema
    if ( [[ UserPrefs getPrefValue:keyShowDotFiles] boolValue] )
        return( [localItem getFileAtIndex:index]);
    // altrimenti, e' un bel pasticcio, devo saltare i dotfiles
    return ( getNormalFile ( localItem, index ) );
}

C'è qualcosa di diverso dalla soluzione precedente quando, nella outlineView della finestra CoverWinCtl, ci si chiede il figlio i-esimo dell'elemento nil, cioè il livello superiore. In questo caso l'elemento nil è sostituito dal volume ed il metodo procede come per tutti gli elementi standard.

C'è da notare una cosa: poiché la sorgente di dati è unica, l'ordinamento imposto dalla finestra ListWinCtl si riflette anche sulla finestra CoverWinCtl. C'è una significativa eccezione, che è la lista dei Volumi; la finestra CoverWinCtl presenta (nella tableView) i volumi nell'ordine in cui sono stati inseriti all'interno del documento. In effetti, mentre l'elenco dei file è riordinato in loco dal metodo sortFileList:direction:, il vettore startPoint non è mai rimpiazzato da una sua versione ordinata. In questo modo è possibile, per la visualizzazione nella finestra CoverWinCtl, rintracciare il volume utilizzando semplicemente il metodo objectAtIndex:.

Gli altri metodi (e funzioni) della sorgente dati non hanno subito modifiche particolari. è invece interessante valutare il metodo attivato ad ogni cambio di selezione nella outlineView.

- (void)
outlineViewSelectionDidChange:(NSNotification *)notification
{
    FileStruct        * currsel ;
    NSString        * statusStr ;
    NSOutlineView * ov ;
    // se sto visualizzando la outlineView della ListWinCtl
    ov = (NSOutlineView*)[ notification object ] ;
    if ( [ ov tag] == TAG_CDCATDOC_FULLLIST )
    {
        // e' cambiata la selezione
        int cs = [ ov selectedRow ] ;
        if ( cs == -1 )
        {
            // imposto il testo della barra di stato
            statusStr = [ NSString stringWithString: @@"No Volume selected@"];
            [ refDoc refreshWinsWithListSts: statusStr coverSts: nil ];
            return ;
        }
        // se arrivo qui, c'e' qualcosa di selezionato
        currsel = [ ov itemAtRow: cs ];
        // se l'elemento selezionato e' uno dei volumi
        if ([ startPoint indexOfObject: currsel] != NSNotFound)
        {
            // imposto la stringa con info utili
            unsigned long long d1, d2, d3 ;
            d1 = [ currsel volSize];
            d2 = [ currsel volFreeSize] ;
            d3 = [ currsel fileSize] ;
            statusStr = [ NSString stringWithFormat: @"Volume size: %@ Used: %@ Free %@",
                fileSizeFromLongLong( d1), fileSizeFromLongLong( d3), fileSizeFromLongLong(d2) ];
        }
        else    statusStr = [ NSString stringWithFormat: @"Full path: %@", [ currsel fileFullPath]];
        // rinfresco le finestre associate alla dataSource
        [ refDoc refreshWinsWithListSts: statusStr coverSts: nil ];
    }
}

figura 09

figura 09

Qui la cosa è interessante (per il momento) solo nel caso della finestra ListWinCtl. Ad ogni modifica di selezione, rintraccio la riga, e dalla riga l'elemento; in base all'elemento aggiorno adeguatamente la barra di stato. Se l'elemento è un volume, ne dico dimensione e spazio impegnato; se è un normale file, ne riporto il nome (giusto perché non avevo altre idee).

La funzione fileSizeFromLongLong ricorda un po' un metodo della classe dei formattatori:

NSString *
fileSizeFromLongLong(unsigned long long fSize )
{
    unsigned long long    fSizeK ;
    float             fsizeM, fsizeG ;
    
    // se il file e' piccolo, mostro byte
    if ( fSize < 1024 )
    {
        long    tmp = fSize ;
        return ( [ NSString stringWithFormat: @@" %4d b@", tmp] );
    }
    // la dimensione e' in byte, divido per 1024 ed arrotondo, ottengo K
    fSizeK = (long) (( fSize / 1024.0 ) + 0.5 );
    // se il file e' medio, mostro K
    if ( fSizeK < 1024 )
    {
        long    tmp = fSizeK ;
        return ( [ NSString stringWithFormat: @@" %4d K@", tmp] );
    }
    // se inferiore al giga, restituisco mega con tre decimali
    fsizeM = ( fSizeK / 1024.0 ) ;
    if ( fsizeM < 1024 )
        return ( [ NSString stringWithFormat: @@" %6.3f M@", fsizeM] );
    // negli altri casi, ritorno Giga, con tre cifre decimali
    fsizeG = ( fsizeM / 1024.0 ) ;
    return ( [ NSString stringWithFormat: @@" %6.3f G@", fsizeG] );
}

Per chiudere la questione, bisogna parlare del metodo refreshWinsWithListSts: che è comparso qui e lì nel codice finora visto. Ebbene, c'è il problema di sincronizzare l'aspetto delle due finestre perché rispecchino entrambe la stessa situazione della sorgente dati. In particolare questo significa che l'aggiornamento della visualizzazione deve avvenire più o meno assieme, in modo da non avere situazioni difformi tra le due finestre. Per fare questo mi sono inventato un metodo del documento, appunto quello citato, che forza l'aggiornamento.

- (void)
refreshWinsWithListSts: (NSString*) sts1Bar coverSts: (NSString*) sts2Bar
{
    // recupero tutti i controllori di finestre del documento
    NSEnumerator     * enumerator = [[ self windowControllers ] objectEnumerator];
    NSWindowController * winCtl ;
    
    winCtl = [enumerator nextObject] ;
    // finche' ci sono controllori
    while ( winCtl )
    {
        // dico di aggiornare il contenuto della finestra
        [ winCtl refreshWindow ] ;
        // ed anche la barra di stato
        [ winCtl setStsStrings: sts1Bar andSts: sts2Bar] ;
        // passo al controllore successivo
        winCtl = [enumerator nextObject] ;
    }
}

In pratica, piglia l'elenco degli oggetti NSWindowController e a ciascuno invia gli stessi messaggi. Ogni controllore di finestra realizza in maniera opportuna, ciascuno per la propria competenza, i metodi corrispondenti; in questo modo, sfruttando il polimorfismo, ogni finestra si aggiorna da sé. Ad esempio, per la finestra CoverWinCtl, il metodo sarà così realizzato:

- (void)
refreshWindow
{
    [ volList reloadData ];
    [ fileList reloadData ];
}

Il metodo setStsStrings:andSts: vuole invece provare ad aggiornare separatamente le due barre di stato delle due finestre, ma per il momento non è un gran vedere.

Pannello informazioni

figura 10

figura 10

Per concludere, occorre notare un paio di modifiche nella gestione del pannello delle informazioni. A parte la ristrutturazione della finestra con l'eliminazione dei dati non più visualizzati, sono da notare i tre metodi attivati dalle notifiche di cambiamento selezione, o meglio, il solo metodo seguente:

- (void )
mainWindowChanged: ( NSNotification *) notification
{
    NSWindowController *winCtl ;
    winCtl = [[ notification object] windowController] ;    
    [ self updateInfo: [ winCtl getMainView ] ];
}

Il metodo updateInfo:, per funzionare correttamente, ha bisogno di conoscere la outlineView di riferimento. Questa, ovviamente, cambia, quando cambio la finestra; tuttavia, la nuova finestra può essere ListWinCtl, oppure CoverWinCtl.

Nel dubbio, lascio fare al controllore della finestra sfruttando il polimorfismo: il metodo getMainView non fa altro che restituire, finestra per finestra, la outlineView corretta. Ad esempio, per la ListWinCtl:

- (NSOutlineView*) getMainView
{
    return ( fullList );
}

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