MaCocoa 030

Capitolo 030 - Un ordine migliore

Lo scorso capitolo mi ero lamentato dell'inefficienza dell'ordinamento. Metto assieme questa cosa con altri miglioramenti riguardanti l'interfaccia per dare luogo ad un nuovo ordine mondiale.

Sorgenti: Dalla documentazione Apple

Primo inserimento: 26 settembre 2002

Due funzioni anzi una

In primo luogo, ho deciso di utilizzare due funzioni al posto di una sola per effettuare l'ordinamento di un vettore; avevo già accennato alla cosa come una delle possibilità. Ebbene, neppure questo è vero. In realtà ne uso una sola, anzi tre.

Per ordinare un vettore, si deve definire una funzione ausiliaria di ordinamento che decida quale, fra due elementi di un vettore, venga prima nell'ordine deputato. Nella realizzazione del capitolo precedente, usavo sempre la stessa funzione, a prescindere dalla direzione di ordinamento. Solo dopo aver ordinato il vettore, eventualmente lo rovesciavo se l'ordine non era quello ascendente. Scegliendo invece la soluzione con due funzioni differenti per ordine ascendente e discendente, devo scrivere nel metodo chiamante qualcosa del tipo:

if ( dir == TRUE )
    newArray = [ vettore sortedArrayUsingFunction: ascSortFunc context: chiave] ;
else    newArray = [ vettore sortedArrayUsingFunction: desSortFunc context: chiave] ;

Ora, le due funzioni ascSortFunc e desSortFunc sono l'una opposta dell'altra. Potrei scrivere la seconda in termini della prima, rovesciando il risultato della prima, oppure, per evitare complicazioni (la funzione è passata come argomento ad un metodo di libreria...), passo per una terza funzione. Ecco la realizzazione finale:

int
ascSortFunc( id el1, id el2, void * ctx )
{    
    return ( lsSortFunction( el1, el2, ctx ));
}

int
desSortFunc( id el1, id el2, void * ctx )
{    
    return ( lsSortFunction( el2, el1, ctx ));
}

È facile vedere come le due funzioni forniscano proprio risultati di natura opposta, semplicemente perché scambio tra loro gli oggetti da confrontare (la genialità di questa soluzione mi abbacina ogni volta che la osservo).

E la funzione lsSortFunction non è altro che la mia vecchia funzione di ordine crescente, scritta all'origine, e che riporto qui solo per completezza:

int
lsSortFunction( id el1, id el2, void * ctx )
{
    id    s1, s2 ;
    // il confronto sulla dimensione del file (sempre per il solito misterioso
    // motivo i long long non funzionano con valueForKey
    if ( [ (NSString*)ctx isEqual: COLID_FILESIZE ] )
    {
        // ricavo le due dimensioni
        long long    tmp1 = [ (LSFileInfo*)el1 fileSize ] ;
        long long    tmp2 = [ (LSFileInfo*)el2 fileSize ] ;
        // e le confronto brutalmente; notare i valori di ritorno
        if (tmp1 < tmp2)
            return NSOrderedAscending;
        else if (tmp1 > tmp2)
            return NSOrderedDescending;
        return NSOrderedSame;
    }
    // per tutte le altre colonne, uso il polimorfismo (late binding)
    // ricavo i due elementi, non meglio specificando la natura
    // possono essere stringhe, numeri o da te
    s1 = [ (LSFileInfo*)el1 valueForKey: ctx ];
    s2 = [ (LSFileInfo*)el2 valueForKey: ctx ];
    // pero' tutti e tre i tipi rispondono al messaggio compare:
    // e quindi, l'istruzione seguente chiama il metodo appropriato
    return ( [ s1 compare: s2]);
    // con questa versione del codice, il compilatore mi da un po'
    // di warning, proprio su questa istruzione...
}

Ordine in loco

Bene. Ora le cose si complicano. Una pecca della realizzazione del capitolo precedente era costringere l'ordinamento del vettore ad ogni invocazione del metodo outlineView:child:ofItem:. Per evitare troppe ordinazioni, ho pensato di ordinarlo una sola volta, la prima, e di utilizzare il vettore già ordinato le volte successive. In pratica, decido di ordinare direttamente in loco il vettore fileList della classe FileStruct. Allo scopo, aggiungo alla classe altre due variabili d'istanza, le stesse già viste nel caso della LSDataSource:

// criterio e direzione di ordine
NSString        * orderColumn ;
BOOL            orderDirection ;

Inoltre, aggiungo un metodo che realizzi l'ordinamento del vettore secondo chiave e direzione specificate:

- (void)
sortFileList: (NSString*) type
    direction: (BOOL) dir
{
    NSArray * newArray ;
    NSMutableArray * tmpArray ;

    // se non ci sono elementi da ordinare, abbiamo finito
    if ( ! [self fileList] ) return ;
    // il vettore e' gia' ordinato se chiave e direzione coincidono
    if ( [ orderColumn isEqual: type ] && dir == orderDirection )
        return ;
    // altrimenti, bisogna ordinare il vettore; uso una delle due funzioni
    // a seconda della direzione di ordinamento
    if ( dir == TRUE )
        newArray = [ [self fileList] sortedArrayUsingFunction: ascSortFunc context: type] ;
    else    newArray = [ [self fileList] sortedArrayUsingFunction: desSortFunc context: type] ;
    // trasformo il risultato in un NSMutableArray
    tmpArray = [ NSMutableArray arrayWithCapacity: [ newArray count]];
    [ tmpArray addObjectsFromArray: newArray ];
    // assegno le nuove variabili d'istanza
    [ self setFileList: tmpArray];
    [ self setOrderColumn: type ];
    orderDirection = dir ;
    return ;
}

A parte il controllo iniziale sul fatto che il vettore sia tale, controllo subito se il vettore non è già ordinato secondo la chiave e la direzione richiesta. Ciò avviene confrontando gli argomenti di invocazione del metodo con le variabili d'istanza. Se il vettore è già ordinato, ho già finito; se invece per qualche motivo occorre riordinare il vettore, procedo appunto al riordino. Una volta effettuata l'ordinazione, impongo il vettore risultante come nuovo valore di fileList; completo il tutto assegnando nuovi valori alle variabili d'istanza che memorizzano il criterio d'ordine del vettore.

Questo metodo è chiamato in due posti differenti.

In primo luogo, all'interno del metodo initTreeFromPath:, subito dopo la costruzione del vettore fileList con il contenuto della directory. Decido di ordinarlo per nome in ordine crescente:

##codice##
    fileList = [[ NSMutableArray alloc ] initWithCapacity: [ tmpfileList count] ];
    [ fileList setArray: tmpfileList ];
    [ self sortFileList: COLID_FILENAME direction: TRUE ];
##codice##

Ed in secondo luogo, nel punto chiave dove eseguire l'ordinamento, ovvero all'interno dell'ormai vecchia conoscenza outlineView:child:ofItem: (della classe LSDataSource), che ha subito una notevole revisione:

- (id)
outlineView:        (NSOutlineView *) outlineView
    child:        (int) index
    ofItem:        (id) item
{
    NSArray * newArray ;
    NSMutableArray * tmpArray ;
    if (item == nil)
    {
        // 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 ]);
    }
    // negli altri casi, recupero la lista
    [ item sortFileList: [ self orderColumn] direction: [ self orderDirection ] ];
    // se visualizzo anche i dotFiles, non c'e' problema
    if ( [[ UserPrefs getPrefValue:keyShowDotFiles] boolValue] )
        return( [item getFileAtIndex:index]);
    // altrimenti, e' un bel pasticcio, devo saltare i dotfiles
    return ( getNormalFile ( item, index ) );
}

Qui ho preso alcune decisioni non banali, con diverse conseguenze. La prima decisione ha a che fare con gli elementi di primo livello, in pratica con l'elenco dei volumi. Questi sono i figli dell'elemento iniziale della NSOutlineView, di valore nil, e sono trattati dalle istruzioni contenute delle parentesi graffe più interne. Per non complicarmi la vita (dopotutto, dovrebbero essere pochi i volumi contenuti all'interno di un catalogo), ho lasciato che il vettore sia ordinato ogni volta che il metodo è invocato per l'elemento nil.

La seconda decisione è automatica, una volta notato che, avendo introdotto il metodo sortFileList:direction:, non serve più la vecchia funzione getArrayNormalFile(.), ma si può tranquillamente ritornare alla precedente getNormalFile(.) (quando dicevo che avrei fatto e disfatto, non scherzavo...).

Infine, c'è il problema di dove piazzare le funzioni di ordinamento: mi servono sia per la classe FileStruct, ma anche per la LSDataSource. Trattandosi di funzioni, non ho trovato di meglio che inserirle nel file bidone djZeroUtils.m.

(quasi) Come il Finder

Messo a punto il meccanismo interno, comincio a ripulire l'interfaccia. Diciamolo, l'icona in alto a destra non piace a nessuno. L'idea è di effettuare l'ordinamento degli elementi all'interno di un documento catalogo più o meno come fa il Finder.

La prima cosa che volevo fare era fare in modo che la direzione di ordinamento e la colonna con funzione di chiave fossero individuabili come succede nel Finder, con un bel triangolino di fianco al nome della colonna. Mi stavo avventurando in cose pericolosissime (come far diventare la cella di intestazione una ImageAndTextCell modificata) quando mi sono imbattuto, più o meno per caso, in una coppia di metodi della classe NSOutlineView che fanno proprio al caso mio: si chiamano indicatorImageInTalbeColumn: e setIndicatorImageInTalbeColumn:. Traduco testualmente dalla documentazione Apple:

Una indicator Image è una qualsiasi (piccola) immagine che è disegnata sul lato destro di una intestazione di colonna. Un esempio del suo uso è nell'applicazione Mail per indicare la direzione di ordinamento della colonna corrente che governa l'ordine dei messaggi in una mailbox.

Bingo.

figura 01

figura 01

Ecco allora la disciplina per effettuare gli ordinamenti all'interno di una finestra di catalogo. Facendo clic su di una colonna, il catalogo è ordinato secondo quella colonna. Facendo doppio clic su di una colonna, si rovescia la direzione di ordinamento secondo quella colonna.

Quanto detto comporta alcune modifiche nel metodo windowControllerDidLoadNib:; scompare la porzione di codice dedicata alla cornerView (anzi, scompare la cornerView), ed entrano due nuove porzioni di codice, la prima per inizializzare l'ordinamento all'interno della sorgente di dati, e la seconda per introdurre l'immagine nella colonna col nome del file (la sola che sono sicuro esistere all'inizio). Ne approfitto anche per impostare il metodo da chiamare quando si fa doppio clic sulla NSOutlineView:

    ##codice##
    // imposto l'ordine iniziale
    [ dataSource setOrderColumn: [ NSString stringWithString: COLID_FILENAME]];
    [ dataSource setOrderDirection: TRUE ];
    ##codice##
    tableColumn = [outlineView tableColumnWithIdentifier: COLID_FILENAME];
    ##codice##
    // assegno alla colonna presente l'immagine che indica l'ordinamento
    [ outlineView setIndicatorImage: [NSImage imageNamed: @"imgUpS"] inTableColumn: tableColumn ];
    [ outlineView setTarget: self ];
    // facendo doppio clic, si cambia la direzione di ordinamento
    [ outlineView setDoubleAction: @selector( changeSortOrder:) ];
    ##codice##

Per inciso, mi sono accorto di un errore che finora mi era sfuggito: il metodo outlineView:shouldEditTableColumn:item: fino ad ora era tranquillamente posizionato all'interno della classe LSDataSource. Non è quello il suo posto, visto che si tratta di un metodo che la NSOutlineView richiede alla classe delegata (che è CatalogDoc) e non alla classe sorgente di dati (LSDataSource). Il metodo va dunque spostato all'interno del file CatalogDoc.m. Ricordo che questo metodo, che si limita a rispondere NO, indica che nessuna cella della NSOutlineView è editabile; questo fa sì che un qualsiasi doppio clic all'interno della NSOutlineView si traduca automaticamente nell'invocazione del metodo changeSortOrder:.

Ciò ha conseguenze appunto sul metodo changeSortOrder:, che deve sincerarsi in primo luogo che sia selezionata una colonna affinché il doppio clic abbia senso. Stabilito ciò, determina la colonna, cambia l'indicatore di direzione nella colonna, rovescia la direzione di ordinamento e provoca il rinfresco dell'intera finestra:

- (void)
changeSortOrder: (id) sender
{
    NSTableColumn    * tableColumn ;

    // voglio che sia selezionata una intera colonna
    if ( [ sender selectedColumn ] == -1 ) return ;
    // determino allora quale colonna e' selezionata
    tableColumn = [ [outlineView tableColumns] objectAtIndex: [ outlineView clickedColumn ]];
    // rovescio l'ordine negando il valore corrente
    [ [ self dataSource] setOrderDirection: ! [ [ self dataSource] orderDirection ] ];
    // aggiusto l'immagine nello header della colonna
    if ( [ [ self dataSource] orderDirection ])
        [ sender setIndicatorImage: [NSImage imageNamed: @"imgUpS"] inTableColumn: tableColumn ];
    else    [ sender setIndicatorImage: [NSImage imageNamed: @"imgDwS"] inTableColumn: tableColumn ];
    // rinfresco la finestra con i nuovi dati
    [ outlineView reloadData ];
}

Infine, ritorno sul metodo outlineView:shouldSelectTableColumn:, che scatena generalmente il meccanismo di ordinamento:

- (BOOL)
outlineView:(NSOutlineView *) oView
    shouldSelectTableColumn:(NSTableColumn *)tableColumn
{
    NSArray        * tabLists = [ oView tableColumns ];
    NSEnumerator     * enumerator = [tabLists objectEnumerator];
    NSTableColumn    * tc ;

    // in mancanza di metodi migliori, tolgo a tutte le colonne
    // l'indicatore di ordinamento (ho gia' ricavato un enumerator
    // con tutte le colonne al momento presenti)
    while ((tc = [enumerator nextObject]))
        [ oView setIndicatorImage: nil inTableColumn: tc ];
    // imposto l'immagine nello header della colonna
    if ( [ [ self dataSource] orderDirection ])
        [ oView setIndicatorImage: [NSImage imageNamed: @"imgUpS"] inTableColumn: tableColumn ];
    else    [ oView setIndicatorImage: [NSImage imageNamed: @"imgDwS"] inTableColumn: tableColumn ];
    // assegno la nuova chiave di ordinamento al dataSource
    [ [ self dataSource] setOrderColumn: [ tableColumn identifier]];
    // rinfresco la finestra con i nuovi dati
    [ oView reloadData ];
    return YES;
}

Quando l'utente fa clic su di una colonna, occorre ripulire le altre colonna da ogni simbolo di ordinamento, ed inserirlo, nella direzione corretta, sulla colonna prescelta. Poi, si scatena il nuovo ordinamento impostando il nuovo valore della chiave di ordinamento e forzando il ridisegno della finestra.

Ho pasticciato parecchio su i file prima di arrivare a questa situazione, quindi è possibile che mi sia dimenticato di qualche modifica qui e lì...

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