MaCocoa 031

Capitolo 031 - Ricerca

Questo capitolo racconta di come ho costruito una finestra per effettuare ricerche di elementi all'interno di un catalogo e di come le funzioni di ricerca sono realizzate.

Sorgenti: nessuna

Primo inserimento: 7 ottobre 2002

Ricerche

Un catalogo serve a contenere informazioni, ed uno degli scopi della sua esistenza è di facilitare l'estrazione di informazioni utili. Va da sé quindi che una delle funzioni principali dell'applicazione CDCat dovrebbe essere la ricerca di elementi all'interno di un catalogo. La ricerca poi è bene che sia parametrizzabile, nel senso che devo poter definire un criterio di ricerca più o meno sofisticato. Per cominciare, mi limito a cercare elementi che rispondano a dei criteri semplici: uno dei campi di un elemento catalogato (ciò che è visualizzato in una colonna) deve essere uguale (o contenere) un termine di riferimento. Per i numeri, posso pensare di cercare valori maggiori, minori o uguali di un numero dato.

Una volta definito il criterio, passo ad estrarre da un catalogo gli elementi che soddisfano questo criterio; metterò il tutto all'interno di una tabella. Infine, facendo doppio clic sopra uno degli elementi estratti, è visualizzato l'elemento stesso all'interno del catalogo.

La finestra

figura 01

figura 01

figura 02

figura 02

figura 03

figura 03

figura 04

figura 04

Non sono mai stato bravo a disegnare le interfacce utente, e l'interfaccia della finestra di ricerca mostra tutta la sua pochezza. Definisco un campo per contenere il termine della ricerca, e due menu pop-up (oggetti della classe NSPopUpButton) per definire il criterio di ricerca. Il menu sulla destra presenta l'elenco dei campi dell'elemento di catalogo (che coincide con le colonne visualizzabili); il menu sulla sinistra (menu di confronto) invece dovrebbe essere contestuale alla scelta dell'altro menù. Molto semplicemente, se la scelta del menudei campi cade su una colonna che contiene una stringa, il menu di confronto permette di impostare una ricerca di stringa esatta (il campo dell'elemento deve coincidere con il testo impostato come termine della ricerca) oppure su stringa contenuta (il termine della ricerca deve essere contenuto all'interno del campo). Se invece la scelta del campo cade su una colonna che contiene un numero, il menu di confronto permette di impostare ricerche per valori maggiori, minori o uguali. La spiegazione è lunga e noiosa, e credo che chiunque abbia fatto una ricerca in precedenza capirà senza bisogno di ulteriori parole.

La finestra è completata dal pulsante che scatena la ricerca, e da una NSTableView destinata a contenere i risultati della ricerca stessa.

Già così, comincio ad avere dei problemi con l'interfaccia. Il primo problema è il comportamento dei vari elementi al ridimensionamento, che non mi piace per nulla. Allora, ho racchiuso il menu pop-up e la sua stringa di riferimento all'interno di una NSBox, completamente trasparente e senza bordi (praticamente invisibile). In questo modo, le specifiche di dimensionamento dinamico generano un comportamento che mi piace di più.

Il secondo problema è come associare il giusto menu di confronto; scopro che non si può fare da IB, ma dovrò scrivere un metodo apposta. Tuttavia, mi premunisco ed inserisco nel nib i due menu con le voci che mi interessano.

C'è poi il problema di capire quale voce di menu è selezionata correntemente. Scelgo il metodo del tag; ad ogni voce di menu assegno in IB un identificatore numerico nel campo Tag disponibile nella finestra di informazioni relativa alla voce. Poi, da programma, recupero il tag usando l'apposito metodo.

Concludo l'attività in IB definendo i vari outlet per collegare il controllore della finestra con i vari elementi dell'interfaccia, e dichiarando tre metodi/action: per cambiare il menu di confronto, per lanciare la ricerca, per evidenziare l'elemento trovato nel documento che lo contiene.

L'interfaccia della finestra

Conviene adesso mostrare l'interfaccia della classe controllore della finestra di ricerca, per commentarne il contenuto:

@interface FindWinCtrl : NSWindowController
{
    IBOutlet NSTextField    * FindText;
    IBOutlet NSPopUpButton * howOpt;
    IBOutlet NSTableView    * resultFld;
    IBOutlet NSPopUpButton * whereOpt;
    IBOutlet NSMenu     * stringMenu;
    IBOutlet NSMenu     * numberMenu;

    // documento cui si riferisce la ricerca
    CatalogDoc         * currCatalog ;
    // vettore con i risultati
    NSMutableArray     * foundItems;
}
// per cominciare la ricerca
- (IBAction)startSearch: (id)sender;
// cambiamento del menu a seconda del tipo di campo
- (IBAction)changeCfrMenu: (id)sender;
// per evidenziare l'elemento selezionato
- (IBAction)goToItem: (id)sender;

+ (id) sharedFind ;

- (int) numberOfRowsInTableView:(NSTableView *)tv;
- (id) tableView:(NSTableView *)tv objectValueForTableColumn:(NSTableColumn *)tc row:(int)row;
- (BOOL) tableView:(NSTableView *)tv shouldEditTableColumn:(NSTableColumn *)tc row:(int)row ;

- (CatalogDoc *)currCatalog ;
- (void)setCurrCatalog :(CatalogDoc *)newCurrCatalog ;
- (NSMutableArray *)foundItems;
- (void)setFoundItems:(NSMutableArray *)newFoundItems;

@end

Dopo i vari outlet, ci sono altre due variabili d'istanza. La seconda è un vettore destinatati a contenere il risultato della ricerca. Nel mio caso, si tratta di una collezione di elementi della classe FileStruct (o meglio, di una serie di puntatori agli elementi); il vettore è inizialmente vuoto e sarà riempito dalla procedura di ricerca. La prima variabile invece è (un puntatore a) il documento (l'oggetto della classe CatalogDoc) sul quale è stata effettuata la ricerca. Questa variabile sarà utile quando voglio evidenziare l'elemento trovato.

Poi sono elencati i vari metodi; i primi sono le citate azioni associate agli elementi dell'interfaccia; c'è poi il metodo di classe per l'apertura (ed eventuale creazione) della finestra; i tre metodi successivi servono alla visualizzazione degli elementi trovati all'interno della tabella; concludono la classe i metodi accessor standard.

Prima di scrivere codice specifico della ricerca, ne approfitto per scrivere quelle due righe di codice per aprire la finestra. In primo luogo torno in IB; elimino la voce Show Palette dal menu Window, ed associo il metodo showPalette alla voce Find... del menu Edit (che ho trovato già predefinito in MainMenu.lib). Torno in XCode e cambio il metodo showPalette: perché apra la nuova finestra FindWinCtrl.

Cambio menu al volo

Devo scrivere il codice per cambiare il menu di confronto a seconda della selezione corrente del menu dei campi di ricerca. In IB mi sono preoccupato di associare al NSPopUpButton di destra l'azione changeCfrMenu: dall'elenco delle possibili azioni. Il metodo conseguente è molto semplice, avendo fatto un uso accorto dei tag:

- (IBAction)
changeCfrMenu:(id)sender
{
    // uso il tag del menu che ho accortamente predisposto in IB
    switch ( [[ whereOpt selectedItem] tag ] ) {
    // questi sono tutte strighe
    case MENUTAG_FILENAME:         case MENUTAG_FULLPATH:    
    case MENUTAG_GROUPNAME:        case MENUTAG_OWNERNAME:    
    case MENUTAG_FILETYPE:
        [ howOpt setMenu: stringMenu ];
        break ;
    // questi sono numeri, oppure date
    case MENUTAG_MODDATE:         case MENUTAG_FILESIZE:
    case MENUTAG_POSIXPERM:        case MENUTAG_OSCREATOR:
    case MENUTAG_OSTYPE:         case MENUTAG_FSFILENUM:
    case MENUTAG_FSNUM:
        [ howOpt setMenu: numberMenu ];
        break ;
    }
}

Ho predefinito tutti i tag all'interno del file FindWinCtrl.h utilizzando il vecchio concetto #define.

Lancio della ricerca

Il clic sul pulsante Find della finestra deve lanciare la ricerca. Il metodo relativo, anche se piuttosto lungo, ricicla in realtà molti vecchi concetti, tra cui la sheet che informa l'utente delle operazioni in corso:

- (IBAction)
startSearch:(id)sender
{
    // inizializzo questo array per selezionare poi la colonna
    NSArray    * colIdArr = [NSArray arrayWithObjects:
        COLID_FILENAME, COLID_FULLPATH, COLID_MODDATE,
        COLID_FILESIZE, COLID_GROUPNAME, COLID_OWNERNAME,
        COLID_POSIXPERM, COLID_FILETYPE, COLID_OSCREATOR,
        COLID_OSTYPE, COLID_FSFILENUM, COLID_FSNUM,
        nil];
    // recupero cosa c'e' da cercare
    NSString    * txt2search = [ FindText stringValue];
    // recupero le opzioni di ricerca
    int        how = [[ howOpt selectedItem] tag ] ;
    // ed il campo dove cercare
    int        where = [[ whereOpt selectedItem] tag ] ;
    // mi faccio la finestra di attesa
    WaitPanCtrl     *tmpCVCtrl = [[ WaitPanCtrl alloc] init ] ;
    // faccio comparire la sheet che dice di attendere
    [ NSApp beginSheet: [ tmpCVCtrl window ]
        modalForWindow: [ NSApp mainWindow ]
        modalDelegate: self
        didEndSelector: nil
        contextInfo: nil
    ];
    // imposto il contenuto della shett stessa
    [ tmpCVCtrl setMainText: @"Searching" ];
    [ tmpCVCtrl animation: self ];
    // recupero il catalogo dove effettuare la ricerca
    // piglio tutti i documenti dell'applicazione, e scelgo quello che sta
    // davanti a tutti gli altri... non che questo meccanismo mi piaccia...
    [ self setCurrCatalog: (CatalogDoc*)[[ NSApp orderedDocuments] objectAtIndex: 0] ];
    
    // pulisco i risultati della ricerca precedente
    [self setFoundItems: [ NSMutableArray array ]];    
    // e li tolgo dalla finestra di ricerca
    [ resultFld reloadData ];
    // adesso eseguo la ricerca, in maniera brutale, sulla sorgente dati
    [ [ [self currCatalog] dataSource ] searchFor: txt2search
        column: [ colIdArr objectAtIndex: where]
        options: how placeIn: foundItems];
    // forzo il rinfresco della finestra dei risultati
    [ resultFld reloadData ];
    // nascondo e chiudo la sheet
    [ [ tmpCVCtrl window ] setIsVisible: FALSE ];
    [ NSApp endSheet: [ tmpCVCtrl window ] ];
}

All'inizio, ho definito un vettore per tenere gli identificatori dei vari campi; l'identificatore utilizzato è estratto utilizzando come indice il valore del tag del menu prescelto. Il giochetto riesce perché ho definito i tag in maniera accorta (il tag Zero al nome del file, il tag 1 al path completo, e così via). Ci sono poi un po' di istruzioni per la visualizzazione della sheet WaitPanCtrl, molto simili a quelle già viste in un capitolo precedente. Altre istruzioni sono essenzialmente cosmetiche, per pulire la finestra dei risultati e per costringere il rinfresco della stessa una volta terminata la ricerca.

In effetti, in questo metodo, di funzioni di ricerca non ce ne sono. C'è solo una istruzione che invia un messaggio alla sorgente dei dati perché esegua la ricerca. È lì il cuore del problema.

Chi cerca...

Dal momento che il catalogo è organizzato ad albero, ovvero in una struttura dati che si presta alla ricorsività, ancora una volta la procedura di ricerca avviene in maniera ricorsiva. La ricerca è lanciata sempre dalla sorgente dati:

- (void)
searchFor: (id) item2search
    column: (NSString*) colId
    options: (int) opt
    placeIn: (NSMutableArray*) foundItems
{
    // piglio un elenco di tutti i volumi presenti
    NSEnumerator     * enumerator = [[self startPoint] objectEnumerator];
    FileStruct    * item ;
        // li esamino uno ad uno
    while (item = [enumerator nextObject])
    {    
        // e faccio una ricerca in profondita' sugli elementi contenuti
        [ item searchItemFor: item2search column: colId
            options: opt placeIn: foundItems ];
    }
}

Il metodo si limita ad estrarre un elenco dei volumi presenti nel catalogo e di eseguire la ricerca su ciascuno di essi. La ricerca è realizzata dal seguente metodo della classe FileStruct:

- (void)
searchItemFor: (id) item2search
    column: (NSString*) colId
    options: (int) opt
    placeIn: (NSMutableArray*) foundItems
{
    // piglio un elenco di tutti gli elementi presenti
    NSEnumerator     * enumerator = [[self fileList] objectEnumerator];
    FileStruct    * item ;
    // prima verifico se l'elemento corrente mi va bene
    if ( checkSearch( self, item2search, colId, opt) )
        [ foundItems addObject: self ] ;
    // poi esamino tutti i file contenuti
    while (item = [enumerator nextObject])
    {
        [ item searchItemFor: item2search column: colId
            options: opt placeIn: foundItems ];
    }
}

Ancora una volta, sto spostando il cuore del problema. In effetti questo metodo chiama la funzione checkSearch, che verifica se un dato elemento centra il criterio di ricerca. Poi, gestisce la discesa ricorsiva all'interno della struttura dati.

Insomma, finalmente, la funzione che realizza materialmente la ricerca, lunga e noiosa, ma molto semplice:

BOOL
checkSearch( LSFileInfo * item, id searchItem, NSString * colId, int opt )
{
    // colonne che contengono stringhe
    NSArray *id_string = [NSArray arrayWithObjects:
        COLID_FILENAME, COLID_FULLPATH, COLID_GROUPNAME,
        COLID_OWNERNAME, COLID_FILETYPE, nil ];
    // colonne che contengono numeri
    NSArray *id_number = [NSArray arrayWithObjects:
        COLID_FILESIZE, COLID_POSIXPERM, COLID_OSCREATOR,
        COLID_OSTYPE, COLID_FSFILENUM, COLID_FSNUM, nil ];

    // tratto a parte le date
    if ( [ colId isEqual: COLID_MODDATE ] )
    {
        // confronto tra data
        return ( [ [ item modDate] isEqualToDate: searchItem]);
    }
    // confronto sulla dimensione del file (sempre per il solito misterioso
    // motivo i long long non funzionano con valueForKey )
    if ( [ colId isEqual: COLID_FILESIZE ] )
    {
        // ricavo le due dimensioni
        long long    tmp1 = [ item fileSize ] ;
        long long    tmp2 = (long long) [ searchItem doubleValue ] ;
        // e le confronto brutalmente secondo l'opzione
        switch ( opt ) {
        case MENUTAG_NUM_GREAT :
            return ( tmp1 > tmp2 );
        case MENUTAG_NUM_GREATEQ :
            return ( tmp1 >= tmp2 );
        case MENUTAG_NUM_EQ :
            return ( tmp1 == tmp2 );
        case MENUTAG_NUM_LESSEQ :
            return ( tmp1 <= tmp2 );
        case MENUTAG_NUM_LESS :
            return ( tmp1 < tmp2 );
        default :
            return ( FALSE );
        }
    }
    // poi, per tutte le colonne che sono rappresentate da stringa
    if ( [ id_string containsObject: colId ] )
    {
        // ricavo la striga
        NSString *s1 = [ item valueForKey: colId ];
        // a seconda delle opzioni, faccio adeguato confronto
        if ( opt == MENUTAG_STR_ISEQUAL )
        {
            // confronto esatto tra stringhe
            return ( [ s1 isEqualToString: searchItem ]);
        }
        else if ( opt == MENUTAG_STR_ISCONTIANED )
        {
            NSRange    xx = [ s1 rangeOfString: searchItem ];
            return ( xx.location != NSNotFound ) ;
        }
    return ( FALSE );
    }
    // poi, per tutti gli altri numeri
    if ( [ id_number containsObject: colId ] )
    {
        // ricavo il numero
        NSNumber    * n1 = [ item valueForKey: colId ];
        NSComparisonResult    xx ;
        // a seconda delle opzioni, faccio il confronto
        switch ( opt ) {
        case MENUTAG_NUM_GREAT :
        case MENUTAG_NUM_GREATEQ :
            xx = [ n1 compare: searchItem ];
            return ( xx == NSOrderedDescending ) ;
        case MENUTAG_NUM_EQ :
            // confronto esatto
            return ( [ n1 isEqualToNumber: searchItem ]);
        case MENUTAG_NUM_LESSEQ :
        case MENUTAG_NUM_LESS :
            xx = [ n1 compare: searchItem ];
            return ( xx == NSOrderedAscending ) ;
        default :
            return ( FALSE );
        }
    }
    return FALSE ;
}

Ci sono essenzialmente quattro grosse ripartizioni della funzione, corrispondente alle quattro categorie di campi sui quali si può effettuare la ricerca. Le date e la dimensione del file sono trattate a parte, la prima per via della natura dell'oggetto, la seconda per il solito motivo dell'estrazione dei numeri long long. Il grosso del lavoro coinvolge le restanti due categorie, di ricerca in un testo e di ricerca in un numero.

Per ciascuna categoria (a dire il vero, ho trascurato le date...) si considera il criterio di confronto, e si restituisce il valore Vero se il criterio è soddisfatto, Falso altrimenti. Con questo valore di ritorno, il metodo searchForItem:... decide se aggiungere o menu l'elemento in esame alla lista dei risultati della ricerca.

La combinazione dei metodi e della funzione sopra descritti esamina l'intero catalogo ed aggiunge al vettore dei risultati tutti gli elementi che soddisfano al criterio scelto. In realtà, ci sono ancora alcune cose da perfezionare (le date, e il fatto che le directory sarebbe bene non considerarle in certe serie di confronti), ma l'impianto mi sembra funzionare.

Mostrare i risultati

Avendo fatto molta pratica con le NSOutlineView e con le NSTableView, predisporre i metodi per la visualizzazione dei risultati della ricerca è un gioco da ragazzi.

figura 07

figura 07

Sono da scrivere i soliti tre metodi per fornire i dati, già presenti in forma di vettore all'interno della classe FindWinCtrl (che ho provveduto in IB a specificare come sorgente di dati della tabella). Li riporto giusto per completezza, in quanto non c'è nulla di nuovo:

- (int)
numberOfRowsInTableView:    (NSTableView *)tableView
{
    return [foundItems count];
}

- (id)
tableView:         (NSTableView *)tableView
    objectValueForTableColumn: (NSTableColumn *)tableColumn
    row:             (int)row
{
    LSFileInfo     * item = [foundItems objectAtIndex: row] ;
    NSString    * colId = [ tableColumn identifier] ;

    // ho al momento due colonne: il nome o l'intero percorso
    if ( [ colId isEqual: COLID_FILENAME ] )
        return ( [ item fileName] );            
    return ( [ item fileFullPath] );    
}

- (BOOL)
tableView:             (NSTableView *)aTableView
    shouldEditTableColumn: (NSTableColumn *)aTableColumn
    row:            (int)rowIndex
{
    return NO ;
}

- (void)
windowDidLoad
{
    // un doppio clic su di una riga mostra l'elemento selezionato
    // (risultato di una ricerca) all'interno del documento che lo contiene
    [ resultFld setDoubleAction: @selector( goToItem:) ];
}

È da notare solo l'ultimo metodo eseguito al caricamento della finestra, che associa alla tabella una azione da eseguire quando si fa doppio clic su di essa. Lo scopo del metodo goToItem: è di evidenziare nella finestra di catalogo l'elemento trovato presente nella lista dei risultati e sul quale si è fatto clic.

...Trova

Ho impiegato più tempo a sviluppare questa parte di ritrovamento piuttosto che la parte di ricerca precedente. Sembrava una sciocchezza (avevo dopotutto tutte le informazioni che mi servivano), ma poi ho dovuto adottare una soluzione grezza e non so fino a che punto corretta.

Il punto di partenza è il metodo indicato da un doppio clic sulla tabella dei risultati. Come sempre, il metodo svolge una serie di compiti estetici, ma non fa molti passi in avanti. In primo luogo verifica che ci sia una riga selezionata, poi che esista ancora il catalogo sul quale è stata eseguita la ricerca (nel frattempo l'utente potrebbe averlo chiuso). In caso di problemi, mostra un bel dialogo di avvertimento, che la funzione NSBeginAlertSheet mette a disposizione.

- (IBAction)
goToItem:(id)sender
{
    FileStruct    * item ;
    // verifico che ci sia una riga selezionata
    if ( [ sender selectedRow ] == -1 ) return ;
    // recupero l'elemento selezionato
    item = [ foundItems objectAtIndex: [ sender selectedRow ]];
    // controllo che il documento cui si riferisce la ricerca sia ancora presente
    if ( ! [ [ NSApp orderedDocuments] containsObject: [self currCatalog] ] )
    {
        // il documento non c'e' piu', informo l'utente con un alert
        NSBeginAlertSheet(@"Missing Catalog", @"Ok, ok", nil, nil,
            [self window], nil, nil, nil, nil,
            @"The Catalog Document is no longer present") ;
        // non ho altro da fare...
        return ;
    }
    // qui abbiamo il catalogo, ed abbiamo l'item; lo cerchiamo e lo apriamo
    [[ [self currCatalog] dataSource ] expandDStoItem: item
        inOutlineView: [ [self currCatalog] outlineView ] ];
    // forzo il redisplay della finestra del catalogo
    [ [ [self currCatalog] outlineView ] reloadData ];
    // metto la finestra del catalogo davanti alle altre
    [ [self currCatalog] showWindows ] ;
}

Poi c'è la consueta invocazione di un metodo alla sorgente dati; di seguito, forza il rinfresco della finestra di catalogo e la porta davanti a tutte.

Ora ho un bel problema: l'idea è di rendere evidente l'oggetto selezionato nella finestra di catalogo. Il più delle volte, questo oggetto sarà nascosto, in quanto non necessariamente la vista NSOutlineView è espansa fino all'elemento richiesto. Occorre quindi espandere, con l'apposito metodo expandItem:, tutti gli elementi dell'alberatura che portano fino all'oggetto trovato.

All'inizio pensavo di fare come al solito, con una procedura ricorsiva, ma così non funziona. L'approccio da seguire deve essere diretto: espandere il volume, e poi le directory in ordine di percorso. Purtroppo, non sono riuscito a trovare niente di meglio di una procedura ad hoc, che esamina il percorso completo del file ed espande gli elementi che trova coincidenti. Come al solito, la procedura si svolge in due passi. Il primo è il metodo expandDStoItem: sopra invocato:

- (void)
expandDStoItem: (FileStruct*) expItem
    inOutlineView: (NSOutlineView *) outView
{
    // recupero il path completo dell'elemento obiettivo
    NSString    * fPath = [expItem fileFullPath] ;
    // lo spezzo ordinamente nei vari elementi
    NSEnumerator    * pathComps = [[fPath pathComponents] objectEnumerator];
    // piglio anche la lista dei volumi
    NSEnumerator    * listavolumi = [[self startPoint] objectEnumerator];
    FileStruct     * item ;
    // pulisco i primi elementi del percorso completo dell'obiettivo
    NSString    * name = [pathComps nextObject] ;     // /
    name = [pathComps nextObject] ;            // Volume
    name = [pathComps nextObject] ;            // nome volume
    
    // cerco nella lista dei volumi il volume che mi serve
    while (item = [listavolumi nextObject])
    {
        // quando ho trovato il volume...
        if ( [[item fileName] isEqual: name] )
        {
            int    thisRow = [outView rowForItem: item];
            // dico che questo item va espanso perche' fa parte della
            // catena che porta all'obiettivo
            [ outView expandItem: item ];
            // lo seleziono
            [ outView selectRow: thisRow byExtendingSelection: FALSE ];
            // e lo porto in bella vista
            [ outView scrollRowToVisible: thisRow ];
            // poi proseguo nell'espansione
            [ item expandFStoItem: pathComps inOutlineView: outView] ;
            return ;
        }
    }
}

Dopo aver recuperato il path completo dell'elemento da evidenziare, lo divido nei vari nomi parziali che lo compongono (la classe NSString fornisce vari metodi appositi per la manipolazione di path). Il primo passo termina cercando all'interno della lista dei volumi contenuti nel catalogo il volume interessato. Una volta trovato il volume, espando l'elemento, lo seleziono e poi porto la selezione all'interno della parte visibile della NSOulineView; poi, proseguo nell'espansione con il secondo passo. Da notare come, una volta trovato l'elemento, non vengano esaminati i successivi: è questa la funzione del return subito dopo la chiamata del metodo expandFStoItem:

- (void)
expandFStoItem: (NSEnumerator*) pathComp
    inOutlineView: (NSOutlineView *) outView
{
    // piglio l'elenco dei file contenuti
    NSEnumerator    * enumerator = [[self fileList] objectEnumerator];
    // questo e' il (pezzo di) nome dell'elemento obiettivo
    NSString    * name = [ pathComp nextObject ];
    FileStruct     * item ;
    // esamino tutti gli elementi presenti
    while (item = [enumerator nextObject])
    {
        // se il nome coincide con quello cercato
        if ( [[item fileName] isEqual: name] )
        {
            int    thisRow = [outView rowForItem: item];
            // dico che questo item va espanso perche' fa parte della
            // catena che porta all'obiettivo
            [ outView expandItem: item ];
            // lo seleziono
            [ outView selectRow: thisRow byExtendingSelection: FALSE ];
            // e lo porto in bella vista
            [ outView scrollRowToVisible: thisRow ];
            // poi proseguo nell'espansione
            [ item expandFStoItem: pathComp inOutlineView: outView] ;
            // non devo esplorare gli altri elementi...
            return ;
        }
    }
}

Il secondo passo è piuttosto simile: si cerca fra gli elementi della directory corrente quello che corrisponde al pezzo di percorso cercato; una volta trovato, si espande l'elemento, lo si seleziona e lo si porta in evidenza. Ancora una volta non si prosegue con gli elementi della directory una volta trovato quello giusto.

figura 08

figura 08

Il meccanismo funziona, anche se espande una volta di troppo. C'è infatti il problema di terminare l'espansione al posto giusto. Con i due metodi sopra descritti, il termine dell'espansione si ha quanto, all'interno di expandFStoItem, l'istruzione che assegna un valore alla variabile name restituisce nil (fine del percorso). Tuttavia, questo avviene troppo tardi, ad un livello inferiore a quello dell'elemento trovato (per cui questo è stato già espanso).

Avevo quasi rinunciato a risolvere questa imperfezione quando mi è venuta l'ispirazione: basta aggiungere come ultima istruzione del metodo expandDStoItem: il collassamento dell'elemento selezionato!

    [ item expandFStoItem: pathComps inOutlineView: outView] ;
    [ outView collapseItem: expItem ];
    return ;

In effetti, al termine della discesa dell'alberatura generata da expandFStoItem:, l'elemento da evidenziare, appunto expItem, si trova bellamente in vista all'interno della NSOutlineView (cosa non sempre vera all'inizio, prima di effettuare l'evidenziazione del risultato), per cui il metodo collapseItem: trova sicuramente questo elemento, e chiude la visualizzazione degli elementi interni.

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