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
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.
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.
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.
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.
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##
}
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.
Eccetto dove diversamente specificato, i contenuti di questo sito sono rilasciati sotto Licenza Creative Commons.
Pagina a cura di Livio Sandel (macocoa2012@gmail.com).