MaCocoa 029

Capitolo 029 - Ordine, ordine

Finora i file all'interno della finestra di catalogo erano rappresentati secondo l'ordine con cui sono catalogati. Mi prefiggo di cambiare le cose, e di ordinare i file secondo i valori di una qualsiasi delle colonne che contengono gli attributi dei file.

Sorgenti: Dalla documentazione Apple.

Primo inserimento: 23 settembre 2002

Ordine ordine

Quando nella finestra del Finder trovate rappresentati i file contenuti in una directory nella vista elenco, è possibile ordinare i file secondo diversi criteri. Fondamentalmente, si utilizza uno degli attributi del file come chiave primaria per stabilire l'ordine, che può essere in una delle due direzioni possibili (per valori crescenti o per valori decrescenti). Ciò avviene selezionando la colonna che si vuole come chiave primaria; in Mac Os X, facendo clic sulla testa della colonna, si cambia la direzione dell'ordinamento. In Mac OS 9 ero invece abituato a fare clic su un riquadrino in alto a destro, all'incrocio tra la riga che contiene le teste delle colonne e la barra di scorrimento verticale.

figura 01

figura 01

Quel quadratino lì ha un nome particolare, è detto corner view ed è uno degli elementi costituenti la NSOutlineView, di cui finora ho tranquillamente dimenticato l'esistenza.

In questo capitolo intendo utilizzare quel quadratino per rappresentare la direzione d'ordinamento corrente, e nel contempo ordinare i file presenti secondo i valori di una delle colonne della NSOutlineView stessa.

Ordinare array

Per ottenere i miei scopi, devo modificare la classe LSDataSource. Preferisco modificarla piuttosto che farne una sottoclasse, altrimenti moltiplico in modo impressionante le classi presenti, cosa che non è mai pregevole dal punto di vista estetico.

La cosa più evidente è l'aggiunta di due nuove variabili d'istanza; la prima, orderColumn, specifica la chiave primaria di ordinamento. È una NSString che contiene l'identificatore della colonna prescelta. La seconda, orderDirection, è un semplice valore booleano che indica la direzione d'ordinamento.

Ho aggiunto subito i metodi accessor, che servon sempre e non fanno male. Mi sono anche ricordato di assegnare un buon valore iniziale e di deallogare la memoria alla distruzione dell'istanza.

Tutte le modifiche si possono incentrare nel metodo

- (id)    outlineView: (NSOutlineView *) outlineView child: (int) index
ofItem: (id) item ;

che ha subito una pesante modifica. In effetti, ordinare gli elementi del catalogo va fatto directory per directory; quindi, un posto per intervenire è quando la NSOutlineView richiede gli elementi da visualizzare, uno alla volta, procedendo per directory. Il metodo modificato fornisce per ogni elemento il 'figlio' i-esimo. Ero già intervenuto su questo metodo per limitare la visualizzazione dei file, limitandola a seconda delle preferenze espresse dall'utente. Adesso, invece che utilizzare l'ordine predefinito degli elementi, prima di fornire il figlio i-esimo, procedo ad ordinare la directory secondo quanto convenuto.

Agli scopi dell'ordinamento, all'interno della classe NSArray esiste un metodo apposito,

sortedArrayUsingFunction: <funzione di confronto> context: <contesto>

Questo metodo, invocato su di un array, produce un altro array, ordinato con chiave crescente secondo i risultati prodotti della funzione fornita come primo argomento. Questa funzione, detta funzione di confronto, è utilizzata per confrontare due elementi dello array, e deve restituire opportuni valori a seconda che il suo primo argomento sia minore, maggiore o guale al secondo argomento. Ogni volta che questa funzione è chiamata, gli viene passata come argomento il valore dell'argomento context: del metodo di ordinamento... Mi sto un po' pasticciando con la spiegazione, quindi provvedo a fornire l'esempio.

La funzione che effettua l'ordinamento deve essere definita così:

int nomeDellaFunzione(id num1, id num2, void *context)

ovvero, è una funzione che restituisce un intero (in realtà, un codice tra NSOrderedAscending, NSOrderedDescending o NSOrderedSame). I suoi argomenti sono due generici oggetti, ed il terzo argomento è appunto il valore di 'contesto'. Approfitto proprio di questo parametro per indicare il nome della colonna secondo cui voglio sia fatto l'ordinamento (per nome, per dimensione, eccetera).

Per ora ho questa realizzazione; la cosa più pedissequa da fare è di esaminare l'argomento, ovvero l'attributo delle informazioni del file secondo cui confrontare i due elementi, estrarre l'attributo stesso e decidere quale dei due elementi è minore dell'altro in base al valore di questo attributo. Tuttavia, sfruttando il fatto che gli attributi sono solo di tre tipi, numeri, stringhe e date, ho raggruppato le operazioni per i primi due tipi. Ho definito un array con i nomi degli attributi di tipo omogeneo, ed ho verificato l'appartenenza dell'argomento a questi gruppi. Ho comunque dovuto esaminare a parte l'unico caso di data ed il confronto sulla dimensione del file, per i soliti problemi del metodo valueForKey: con i numeri long long. Ciò premesso, la funzione è di scorrevole lettura.

int
mySortFunc( id el1, id el2, void * ctx )
{    
    // costruisco questi due array per evitare noiosi confronti
    NSArray *id_string = [NSArray arrayWithObjects:
        COLID_FILENAME, COLID_FULLPATH, COLID_GROUPNAME,
        COLID_OWNERNAME, COLID_FILETYPE, nil ];
    NSArray *id_number = [NSArray arrayWithObjects:
        COLID_FILESIZE, COLID_POSIXPERM, COLID_OSCREATOR,
        COLID_OSTYPE, COLID_FSFILENUM, COLID_FSNUM, nil ];
    // devo trattare in maniera differente il confronto tra date
    if ( [ (NSString*)ctx isEqual: COLID_MODDATE ] )
    {
        // ricavo le date
        NSDate    * d1 = [ (LSFileInfo*)el1 modDate];
        NSDate    * d2 = [ (LSFileInfo*)el2 modDate];
        // ed uso il confronto tra date
        return ( [ d1 compare: d2]);
    }
    // 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;
    }
    // poi, per tutte le colonne che sono rappresentate da stringa
    if ( [ id_string containsObject: (NSString*)ctx ] )
    {
        // ricavo le strighe
        NSString *s1 = [ (LSFileInfo*)el1 valueForKey: ctx ];
        NSString *s2 = [ (LSFileInfo*)el2 valueForKey: ctx ];
        // ed uso il confronto tra stringhe
        return ( [ s1 compare: s2]);
    }
    if ( [ id_number containsObject: (NSString*)ctx ] )
    {
        // ricavo i due numeri
        NSNumber * s1 = [ (LSFileInfo*)el1 valueForKey: ctx ];
        NSNumber * s2 = [ (LSFileInfo*)el2 valueForKey: ctx ];
        // ed uso il confronto tra stringhe
        return ( [ s1 compare: s2]);
    }
    // qui ci arrivo solo se la colonna non e' una di quelle indicate
    // ritorno qualcosa a caso...
    return ( NSOrderedSame );
}

A questo punto diventa comprensibile il metodo outlineView:... principale. Invece che pigliare direttamente l'elemento i-esimo del vettore che contiene tutti i figli del file in esame, prima costruisco un vettore con gli elementi ordinati secondo il criterio richiesto. Se la direzione di ordinamento è decrescente, mi limito a rovesciare brutalmente lo array stesso, invocando una funzione da me scritta (non ho trovato un metodo apposito nella classe NSMutableArray, e tanto meno in NSArray).

All'ultimo momento mi sono poi accorto di non poter più utilizzare la funzione che avevo scritto in precedenza quando si devono saltare i dotFiles (si chiamava getNormalFile), ed ho dovuto scriverne un'altra, che ho chiamato getArrayNormalFile.

- (id)
outlineView:        (NSOutlineView *) outlineView
    child:        (int) index
    ofItem:        (id) item
{
    NSArray * newArray ;
    NSMutableArray * tmpArray ;
    if (item == nil)
    {
        // ordino gli elementi secondo criterio
        newArray = [ [self startPoint] sortedArrayUsingFunction: mySortFunc
            context: [ self orderColumn]] ;
        tmpArray = [ NSMutableArray arrayWithCapacity: [ newArray count]];
        [ tmpArray addObjectsFromArray: newArray ];
        // se la direzione e' FALSE, rovescio il vettore
        if ( [ self orderDirection ] == FALSE )
            reverseArray( tmpArray) ;
        return( [ tmpArray objectAtIndex: index ]);
    }
    // negli altri casi, recupero la lista
    newArray = [[ item fileList] sortedArrayUsingFunction: mySortFunc
            context: [ self orderColumn]] ;    
    tmpArray = [ NSMutableArray arrayWithCapacity: [ newArray count]];
    [ tmpArray addObjectsFromArray: newArray ];
        if ( [ self orderDirection ] == FALSE )
            reverseArray( tmpArray) ;
    // se visualizzo anche i dotFiles, non c'e' problema
    if ( [[ UserPrefs getPrefValue:keyShowDotFiles] boolValue] )
        return( [ tmpArray objectAtIndex: index ]);
    // altrimenti, e' un bel pasticcio, devo saltare i dotfiles
    return ( getArrayNormalFile ( tmpArray, index ) );
}

Mostro adesso le due funzioni citate e non ancora descritte.

Per rovesciare un array, la prima cosa che mi è venuta in mente è stata quella di scambiare tra loro il primo elemento con l'ultimo, il secondo con il penultimo, e così via, fino a che gli scambi non raggiungevano il centro del vettore. La cosa funziona sia con un numero pari che dispari di elementi, basta solo ricordarsi di fermarsi al momento giusto....

void
reverseArray ( NSMutableArray * locArr )
{
    // conto quanti elementi ci sono
    short    numelem = [ locArr count];
    short    i ;
    // e li scambio a coppie
    for ( i= 0 ; i < numelem / 2 ; i ++ )
    {
        // uso il metodo apposito...
        [ locArr exchangeObjectAtIndex: (i) withObjectAtIndex: (numelem-i-1) ];
    }
}

La funzione ha una storia lunga: in primo luogo, il rovesciamento in loco dello array mi costringe a dichiararlo NSMutableArray; alternativamente, avrei potuto crearne uno nuovo.... In secondo luogo, avrei potuto fare il tutto in un'altra maniera: se al posto di usare sempre la stessa funzione d'ordinamento ne definivo due, una speculare all'altra (la prima mySortFunc restituisce 'maggiore' dove la seconda, diciamo revSortFunc, restituisce 'minore') potevo eseguire l'ordinamento in ordine crescente o decrescente scegliendo l'una o l'altra funzione, più o meno come nel frammento di codice seguente:

if ( [ self orderDirection ])
    newArray = [ [self startPoint] sortedArrayUsingFunction: mySortFunc
            context: [ self orderColumn]] ;
else    newArray = [ [self startPoint] sortedArrayUsingFunction: revSortFunc
            context: [ self orderColumn]] ;

Infine, potevo estendere il contesto inserendo nel parametro context non solo l'identificatore della colonna con cui ordinare il vettore, ma anche la direzione di ordinamento...

Manca la funzione per estrarre un elemento dal vettore, tenendo eventualmente conto del fatto che si devono saltare i dotFiles:

FileStruct *
getArrayNormalFile ( NSArray * directory, short index )
{
    short    i, count ;

    count = 0 ;    // contatore corrente
    for ( i = 0 ; i < [directory count] ; i++ )
    {
        FileStruct * currFile ;
        // recupero il file
        currFile = [directory objectAtIndex:i] ;
        // se devo saltarlo, passo avanti
        if ( skipDotFile( [ currFile fileName ] ) )
            continue ;
        // sono arrivato all'indice richiesto ?
        if ( count == index )
            // si, restituisco l'elemento corrente
            return ( currFile );
        // se arrivo qui, non ho ancora raggiunto l'elemento
        count += 1 ;
    }
    // ovviamente, qui non dovrei mai arrivare...
    return ( nil) ;
}

Non c'è molto da dire: in pratica si salta un livello gerarchico, utilizzando direttamente il vettore degli elementi piuttosto che ricavarlo indirettamente.

Un ordine alternativo

Non sono del tutto soddisfatto di come ho realizzato la cosa. In effetti, la visualizzazione di ogni elemento provoca sempre un nuovo riordinamento, con conseguente creazione di vettori, copiatura, eccetera. Un gran lavorio che forse si può evitare, ma che non ho voglia al momento di modificare.

L'unico miglioramento che mi sono permesso riguarda la funzione di ordinamento, che ho riscritto in questo modo:

int
mySortFunc( 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...
}

Sfrutto il polimorfismo ovvero il late-binding, una delle caratteristiche qualificanti della programmazione object-oriented. Poiché tutti e tre i tipi di dati (numeri, stringhe e date) sono in grado di rispondere correttamente al messaggio compare:, non mi preoccupo di stabilire in anticipo la natura degli elementi da confrontare, ma lascio che sia l'ambiente operativo a decidere quale dei tre metodi utilizzare a seconda della natura dell'oggetto, nota solo al momento dell'esecuzione (va da sé che per il solito motivo estraggo il confronto per le dimensioni dei file...). Non so se questa realizzazione sia computazionalmente più efficiente della precedente; certo, è più compatta.

L'ordine dell'ordine

Una volta che ho scritto le istruzioni per l'ordinamento degli elementi da visualizzare, occorre informare la visualizzazione di come procedere all'ordinamento. In altre parole, devo trovare il posto più adatto per assegnare un valore alla variabile d'istanza orderColumn della classe sorgente di dati.

La prima soluzione che ho trovato, che non è detto sia la migliore, è di sfruttare una delle tante notifiche cha la NSOutlineView produce. In particolare, l'ambiente operativo invoca il metodo outlineView:shouldSelectTableColumn: al delegato della NSOutlineView ogni volta che l'utente fa clic per selezionare una intera colonna. Scopo del metodo è di decidere se la colonna possa o meno essere selezionata. Nel mio caso, risponde sempre e senz'altro di sì, ma ne approfitto per impostare il criterio di ordinamento:

- (BOOL)
outlineView:(NSOutlineView *)outlineView
    shouldSelectTableColumn:(NSTableColumn *)tableColumn
{
    // assegno la nuova chiave di ordinamento al dataSource
    [[ self dataSource] setOrderColumn: [ tableColumn identifier]];
    // rinfresco la finestra con i nuovi dati
    [ [self outlineView] reloadData ];
    return ( YES );
}

Con queste istruzioni, cambio il criterio di ordinamento, ma non ho ancora un meccanismo che imponga la direzione dell'ordinamento stesso. Dicevo all'inizio che volevo utilizzare la cornerView, il quadratino in alto a destra della finestra, all'incrocio tra la riga delle intestazioni e la colonna dove si trova la barra di scorrimento verticale. La cornerView è parte strutturale della NSOutlineView, e si gestisce passando attraverso di essa.

Decido che questa view è in realtà un pulsante, quindi un oggetto della classe NSButton. Non ho la possibilità di impostare questa view all'interno di IB, quindi lo faccio da programma:

- (void)
windowControllerDidLoadNib:    (NSWindowController *) aController
{
    NSButton    * sortBtn ;    
##codice##
    sortBtn = [[ NSButton alloc] init ] ;
    [ sortBtn setImage: [NSImage imageNamed: @"imgUp"] ];
    [ sortBtn setTarget: self ];
    [ sortBtn setAction: @selector( changeSortOrder:) ];
    [ outlineView setCornerView: sortBtn ];
##codice##
}

figura 02

figura 02

figura 03

figura 03

Come si può vedere, la cosa è molto semplice: costruisco un oggetto NSButton; gli attribuisco una immagine; tramite il meccanismo target/action faccio in modo che ogni clic su di esso invochi un opportuno metodo; dico alla NSOutlineView di utilizzare questo pulsante come cornerView.

Il metodo invocato è molto semplice:

- (void)
changeSortOrder: (id) sender
{
    // rovescio l'ordine: esamino il valore corrente...
    if ( [ [ self dataSource] orderDirection ])
    {
        // ... e lo rovescio
        [ [ self dataSource] setOrderDirection: FALSE ];
        // adeguo l'immagine della corner view
        [[[self outlineView] cornerView] setImage: [NSImage imageNamed: @"imgDw"]];
    }
    else    
    {
        [ [ self dataSource] setOrderDirection: TRUE ];
        // adeguo l'immagine della corner view
        [[[self outlineView] cornerView] setImage: [NSImage imageNamed: @"imgUp"]];
    }
    // rinfresco la finestra con i nuovi dati
    [ [self outlineView] reloadData ];
}

Si limita infatti a cambiare l'immagine del pulsante a seconda della direzione di ordinamento, di modificare il contenuto delle variabile d'istanza e di forzare il rinfresco dei dati della NSOutlineView.

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