MaCocoa 060

Capitolo 060 - Ricerche approfondite

In questo capitolo riprendo in mano le funzioni di ricerca sugli elementi di un catalogo; le rendo più complete e sofisticate, e ci aggiungo qualche funzionalità. Il tutto, controllato da una finestra piuttosto esotica con alcuni punti degni di nota.

Sorgenti: nessuna, a parte la solita documentazione

Prima stesura: venerdì 24 settembre 2004

Nuove funzionalità

Voglio introdurre nuove meccanismi per le funzioni di ricerca. Attualmente è infatti possibile eseguire ricerche solo basandosi sul valore di uno dei campi informativi di un elemento del catalogo.

figura 01

figura 01

Giusto per fare mente locale, la finestra dove inserire i parametri di ricerca presenta solamente due menu pop up con i quali scegliere il campo sul quale eseguire la ricerca e l'operatore di confronto; un terzo campo permette di inserire un testo che funziona come parametro della ricerca.

Questa strategia ha un difetto evidente ed uno meno evidente; il difetto evidente è che il criterio di ricerca è sempre piuttosto semplice. Il difetto meno evidente è che si confrontano sempre stringhe, e talvolta si ottengono risultati bizzarri. Poi c'è anche il fatto che la ricerca non è completa (la ricerca sulle date non funziona tanto bene).

La prima idea per migliorare le funzioni di ricerca è di introdurre più criteri di ricerca. Ogni criterio è formato da una terna campo-operatore-valore; più criteri possono essere combinati tra loro in AND (un elemento è valido se soddisfa a tutti i criteri contemporaneamente) oppure in OR (un elemento è valido se soddisfa ad almeno uno dei criteri).

La seconda idea è di introdurre qualche criterio non basato esclusivamente sui campi di informazione di un elemento. Ad esempio, un criterio che individui tutti i file che hanno discendenti (le cartelle) o che non ne hanno per nulla (i file semplici), oppure ancora un criterio che individui i bundle (applicazioni, framework, eccetera).

La terza idea è che le operazioni di ricerca rientrano nei più generali processi di filtraggio. Una procedura di ricerca su una base di dati non è altro che una operazione di filtraggio: si parte da un insieme di elementi, e di questi se ne fanno passare, se ne filtrano, una certa parte, che soddisfano a dei determinati criteri. Mostrare l'elenco risultante è l'unico effetto di questa ricerca. Una possibilità interessante che mi è venuta in mente è di effettuare delle semplici operazioni su questi elementi. Nella fattispecie, ciò che mi interessa è impostare il campo is2Print a Vero (YES) o Falso (NO) secondo determinati criteri. Ad esempio, catalogando CD di programmi, mi interessa nelle copertine stampare solo le applicazioni presenti; se invece ci sono CD di file MP3 ben ordinati (sorvoliamo sugli aspetti legali della faccenda), mi interessa stampare solo le cartelle, perché i file presenti sono ordinatamente raccolti per autore, all'interno di cartelle con nome acconcio.

Interfaccia utente

In base a questi grossolani requisiti, ho cominciato a progettare l'interfaccia utente. Qui ho il primo problema di come l'utente possa stabilire diversi criteri.

figura 02

figura 02

Mi sono ispirato alla finestra con la quale si svolge lo stesso compito nel Finder di Mac Os X. Senza raggiungere la bellezza e la (nascosta) complessità di quella finestra, ho pensato di raccogliere il meccanismo di specifica dei criteri all'interno di una NSTableView. Ogni riga della tabella specifica un criterio, mentre le varie colonne sono utilizzare per determinare meglio i parametri del criterio.

figura 03

figura 03

In particolare, la tabella ha cinque colonne. Nella prima colonna c'è un menu pop up con il quale specificare il campo dell'elemento del catalogo sul quale opera il criterio; la seconda colonna specifica l'operatore (maggiore, minore, uguale, eccetera) di confronto, mentre con la terza colonna si determina direttamente (tramite tastiera) il valore di confronto. La quarta e la quinta colonna le riservo per due pulsanti, in grado di aggiungere o eliminare criteri. Rispetto alla finestra proposta dal Finder, oltre alla bruttezza di quanto realizzato da me, c'è anche il fatto che il Finder presenta in alcuni casi un ulteriore menu pop up (o altro controllo speciale) come specifica del valore, mentre nel mio caso c'è sempre un campo di testo.

La finestra è completata da altri due menu pop up. Col primo menu si specifica se i criteri vanno considerati in AND o in OR (è evidente che nel caso di un solo criterio, la distinzione AND/OR cade); col secondo menu invece determino l'azione da eseguire sugli elementi trovati.

figura 04

figura 04

Ho previsto l'azione standard di ricerca, con la quale gli elementi sono semplicemente visualizzati in una lista; l'azione di impostazione del campo is2Print: il pulsante assume lo stato corrispondente al risultato della valutazione dei criteri per quell'elemento; l'azione di aggiunta agli elementi da stampare: se un elemento soddisfa ai criteri, il suo campo is2Print è posto a Vero; l'azione opposta di rimozione dalla lista degli elementi in stampa.

Ci sono poi due pulsanti, uno per cominciare le operazioni di ricerca e l'altro per riportare le impostazioni dei criteri ad un valore di reset.

figura 05

figura 05

Tutto ciò è racchiuso all'interno di un elemento NSBox, a suo volta parte di una NSSplitView. L'altra parte della NSSplitView è la tabella destinata a raccogliere i risultati della ricerca.

La parte più complicata dell'intera faccenda è la completa caratterizzazione della NSTableView destinata a contenere i criteri. Finora ho sempre utilizzato tabelle piuttosto semplici; la cosa più complicata è stato l'inserimento di una cella speciale per avere testo ed immagine (la classe ImageAndTextCell, per altro di terze parti), o di una cella sotto forma di pulsante di spunta (proprio la visualizzazione del campo is2Print). Ricordo che la visualizzazione dei dati all'interno di una tabella avviene tramite l'uso di oggetti della classe NSCell (o discendenti; normalmente si tratta di NSTextFieldCell). In entrambi questi casi per usare un tipo di cella differente ho dovuto utilizzare Cocoa ed una istruzione apposita. Ho scoperto che invece è possibile eseguire l'operazione anche in Interface Builder.

figura 06

figura 06

Si va nella palette Cocoa-Data e si trascina una (quella desiderata, ovviamente: NSPopUpButtonCell e NSButtonCell) delle piccole icone nella parte inferiore esattamente sullo header della colonna; l'operazione è segnalata dalla presenza di un triangolino in alto a destra. Selezionando il triangolino, si accede all'oggetto di tipo NSCell, per eventualmente manipolarne qualche parametro.

Con questa configurazione di file nib, ho avuto notevoli problemi: spesso e volentieri Interface Builder entrava in confusione e terminava la propria vita operativa senza salvare correttamente il file. Tuttavia, con infinita pazienza e ripetuti salvataggi, ne sono venuto fuori.

Ho così un nuovo file FilterWin.nib ed una nuova classe FilterWinCtl che vanno a rimpiazzare la precedente finestra (e la corrispondente classe) per effettuare ricerche.

I criteri di ricerca

Ho cercato di riutilizzare quanto più possibile le istruzioni già scritte in precedenza; in realtà poche cose si sono salvate, anche se l'impianto di base è rimasto lo stesso.

Per prima cosa, occorre spendere due parola sulla struttura dati destinata a contenere le specifiche dei criteri: è una buona e vecchia struttura C:

typedef struct    _filterElem {
    int            fieldId ;
    int            operId ;
    id            theValue ;
}
    FltElemStruct, * FltElemPtr ;

Il campo fieldId contiene un identificatore del campo (nome del file, data di creazione, eccetera); il campo operId contiene un identificatore che specifica il tipo di confronto (maggiore, minore, eccetera); il campo theValue contiene il valore con cui effettuare il confronto. Si tratta di un oggetto generico in quanto può contenere una stringa, un numero o una data.

Ho fatto in modo che un vettore di dieci di queste strutture sia compreso nelle variabili della classe FilterWinCtl:

    int                        numOfCriteri ;
    FltElemStruct            criteriFiltro[10] ;

Ho anche fatto in modo che i valori dei campi fieldId e operId corrispondano ai tag dei menu pop up.

C'è un po' di confusione riguardo i menu pop up: ce ne sono veramente tanti. I menu per la selezione AND/OR e per le azioni conseguenti alla ricerca sono di facile ed immediata gestione; in pratica impostano dei parametri che saranno letti al momento di effettuare la ricerca (quando si attiva il pulsante Find, ovviamente). Ben più complicata la gestione dei vari menu pop up all'interno della tabella. Qui c'è il menu della prima colonna, che è sempre lo stesso; il menu nella seconda tabella invece cambia a seconda della scelta effettuata nella prima colonna. Se il campo evidenziato dal primo menu è ad esempio il nome del file, il menu delle operazioni di confronto presenta le opzioni di uguale e diverso; se il campo è una data, il confronto si può fare per maggiore, minore o uguale. Del resto, lo stesso problema è già stato affrontato e risolto nella vecchia realizzazione. Qui le cose sono un po' più lunghe e noiose, perché più pignole. Ci sono diversi menu pop up relativi alle operazioni: uno per i confronti maggiore, minore, uguale, diversi, adatti per date e numeri; uno per uguale e diverso, adatti per campi come permessi di accesso, creatore, gruppo, eccetera; uno con uguale e contenuto in, per i classici confronti su stringhe.

Per far partire il meccanismo, ad ogni voce di menu è stato assegnato un diverso tag. Il menu della scelta dei campi fa riferimento al seguente metodo, lungo ma noioso:

- (IBAction)
popUpSelectField:(id)sender
{
    int        row = [listaCriteri selectedRow ] ;
    int        whois = [ sender tag ] ;
    
    if ( row >= numOfCriteri ) return ;
    switch ( whois ) {
    case MENUTAG_FILENAME:
    case MENUTAG_GROUPNAME:        
    case MENUTAG_OWNERNAME:
        criteriFiltro[row].fieldId = whois ;
        criteriFiltro[row].operId = MENUTAG_STR_ISCONTAINED ;
        // devo convertire il valore in una stringa
        convert2string( & criteriFiltro[row] ) ;
        break ;
    case MENUTAG_MODDATE:         
    case MENUTAG_CREATDATE :
        criteriFiltro[row].fieldId = whois ;
        criteriFiltro[row].operId = MENUTAG_NUM_EQ ;
        // devo convertire il valore in una data
        convert2date( & criteriFiltro[row] ) ;
        break ;
    case MENUTAG_POSIXPERM:     
    case MENUTAG_OSCREATOR:
    case MENUTAG_OSTYPE:        
    case MENUTAG_FILESIZE :
        criteriFiltro[row].fieldId = whois ;
        criteriFiltro[row].operId = MENUTAG_NUM_EQ ;
        // devo convertire il valore in un numero
        convert2number( & criteriFiltro[row] ) ;
        break ;
    case MENUTAG_SPECIAL :
        criteriFiltro[row].fieldId = whois ;
        criteriFiltro[row].operId = MENUTAG_NONE ;
        break ;    
    // non dovrebbe mai succedere ...
    default :
        criteriFiltro[row].fieldId = MENUTAG_FILENAME ;
        criteriFiltro[row].operId = MENUTAG_STR_ISCONTAINED ;
        break ;    
    }
    [ listaCriteri reloadData ] ;
}

Dopo aver recuperato la riga correntemente selezionata (che è anche l'indice del vettore dei criteri), si imposta il campo fieldId con il tag ricevuto; si predispone anche il campo operId perché visualizzi una operazione sensata sul tipo di dato. Il valore di confronto poi è convertito dal valore corrente ad un valore più adatto. Se ad esempio si passa da una stringa ad una data, è chiamata la funzione convert2date:

void
convert2date( FltElemPtr critPtr )
{
    if ( [ critPtr->theValue isKindOfClass:[NSString class] ] )
    {
        NSDate    * xx = [ NSDate dateWithString: critPtr->theValue ] ;
        [ critPtr->theValue release ];
        critPtr->theValue = [ xx copy ];
        [ critPtr->theValue retain ];
    }
    else if ( [ critPtr->theValue isKindOfClass:[NSNumber class] ] )
    {
        NSString    * xx = [ NSDate dateWithTimeIntervalSinceNow:
                [ critPtr->theValue floatValue] ] ;
        [ critPtr->theValue release ];
        critPtr->theValue = [ xx copy ];
        [ critPtr->theValue retain ];
    }
}

Così facendo, si evitano spiacevoli inconvenienti quando si cercano di confrontare mele (date) con pere (stringhe). Lo steso dicasi per gli altri casi.

Del tutto simile, ma molto più semplice, il metodo associato a tutti gli elementi dei menu pop up relativi alle operazioni:

- (IBAction)
popUpSelectOper:(id)sender
{
    int        row = [listaCriteri selectedRow ] ;
    int        whois = [ sender tag ] ;
    
    if ( row >= numOfCriteri ) return ;
    criteriFiltro[row].operId = whois ;
    [ listaCriteri reloadData ] ;
}

Visualizzare i criteri

Per visualizzare i criteri, occorre scrivere i due classici metodi di una sorgente di dati:

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

Qui ci sono una pio di complicazioni semplici: la prima riguarda il fatto che ci sono due tabelle cui fornire dati, e la classe FilterWinCtl funziona da dataSource e delegata per entrambe. La seconda è che non si possono fornire i valori ai menu pop up. Il primo problema si risolve fornendo ogni tabella di un tag, e verificando quale tabella è interessata dalla fornitura dati; oltretutto, per fornire i dati alla tabella dei risultati, utilizzo pari pari lo stesso meccanismo della vecchia realizzazione (lo array foundItems). Il secondo problema è stato risolto con il metodo (delegato)

- (void) tableView:(NSTableView *)tv willDisplayCell:(id)ac forTableColumn:(NSTableColumn *)tc row:(int)rowIndex ;

A questo punto, il numero di elementi presenti è semplice:

- (int)
numberOfRowsInTableView:    (NSTableView *)tableView
{
    if ( [ tableView tag ] == TABLEVIEWTAG_CRITERI )
            return (numOfCriteri);
    else    return [foundItems count];
}

Anche per fornire i valori alle singole celle ci vuole poco:

- (id)
tableView:            (NSTableView *)tableView
    objectValueForTableColumn:    (NSTableColumn *)tableColumn
    row:             (int)row
{
    if ( [ tableView tag ] == TABLEVIEWTAG_CRITERI )
    {
        // sono nella tabella criteri
        NSString * colId = [ tableColumn identifier] ;
        // l'unico valore sensato da restituire sta qui
        if ( [ colId isEqual: @"valueSpec" ] )
        {
            // se il campo specificato e' speciale
            if ( criteriFiltro[row].fieldId == MENUTAG_SPECIAL )
                return ( @" " );
            // altrimenti, restiuisco il valore nudo
            return ( criteriFiltro[row].theValue );
        }
        // giusto per ritornare qualcosa sempre
        return ( @"dummy" );
    }
    else
    {
        // sono nella tabella risultati
        CatFileInfo    * 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] );
    }
}

È arrivato il momento di parlare della voce di menu Select: cui corrisponde il tag MENUTAG_SPECIAL.

figura 07

figura 07

Con questo menu introduco criteri non legati alla scelta campo-operazione-valore; i criteri introdotti riguardano la selezione di cartelle (o meglio, di file con qualche discendente), di bundle (applicazioni, framework, eccetera, cartelle caratterizzate dall'avere una data estesione (per riconoscere un bundle ho scritto una funzione apposta), di semplici file (che non hanno discendenti), la selezione di tutto ed anche di nessun elemento (poco utili nelle ricerche, ma più adatti alle impostazioni per caratterizzare la stampa).

Caratteristica di questo tipo di criteri è di non avere un valore di confronto, per cui la terza colonna della tabella non è utile; la riempio comunque con dei caratteri non significativi.

Torniamo alla visualizzazione della tabella dei criteri; devo ancora fare il grosso del lavoro, ovvero aggiustare la visualizzazione dei menu pop up; il metodo è piuttosto lungo, ma con molte parti poco interessanti; però in fondo c'è qualcosa di interessante.

Ricordo che il metodo è chiamato per ogni cella prima che questa sia disegnata; è utilizzato appunto per effettuare predisposizioni dell'ultimo momento:

- (void)
tableView:(NSTableView *)aTableView
    willDisplayCell:(id)aCell
    forTableColumn:(NSTableColumn *)aTableColumn
    row:(int)rowIndex
{
    if ( [ aTableView tag ] == TABLEVIEWTAG_CRITERI )
    {
        // sono nella tabella criteri
        NSString * colId = [ aTableColumn identifier] ;
        // menu pop up della scelta campo
        if ( [ colId isEqual: @"fieldSpec" ] )
        {
            // ricavo quale item e' da evidenziare
            int    qualemenu = (criteriFiltro[rowIndex].fieldId - MENUTAG_FILENAME) ;
            [ aCell selectItemAtIndex: qualemenu ] ;
        }
        // menu pop up delle operazione di confronto
        if ( [ colId isEqual: @"operSpec" ] )
        {
            // tag del menu da visualizzare
            // per avere l'indice, dovro' sottrarre il tag base
            int    newCode = criteriFiltro[rowIndex].operId ;
            // imposto menu e item selezionato in dipendenza
            // dalla scelta del campo
            switch ( criteriFiltro[rowIndex].fieldId ) {
            case MENUTAG_FILENAME:        // stringhe
            case MENUTAG_GROUPNAME:        // cfr: uguale
            case MENUTAG_OWNERNAME:        //    contenuto
                [ aCell setMenu: strMenu ] ;
                newCode -= MENUTAG_STR_ISCONTAINED ;
                break ;
            case MENUTAG_MODDATE:         // cfr: eguale, diverso
            case MENUTAG_CREATDATE:        //    maggiore, minore
            case MENUTAG_FILESIZE:
                [ aCell setMenu: operMenu ] ;
                newCode -= MENUTAG_NUM_EQ ;
                break ;
            case MENUTAG_OSTYPE:         // cfr: uguale
            case MENUTAG_POSIXPERM:     //    diverso
            case MENUTAG_OSCREATOR:
                [ aCell setMenu: compMenu ] ;
                newCode -= MENUTAG_NUM_EQ ;
                break ;
            case MENUTAG_SPECIAL :        // crf, nessuno
                [ aCell setMenu: typeMenu ] ;
                newCode -= MENUTAG_FOLDER ;
                break ;
            }
            [ aCell selectItemAtIndex: newCode ] ;
        }

Fino a qui non ci sono cose particolari degne di nota. Il trucco di tutto il meccanismo sta nell'avere associato ad ogni item dei menu tag con valori consecutivi; per passare dal tag all'indice dell'item all'interno del menu è quindi sufficiente sottrarre al tag il valore del tag del primo item del menu. Inoltre, a seconda del tipo di menu, alla cella è associato il menu corrispondente (strMenu, operMenu, eccetera, sono tutti outlet a menu predefiniti all'interno del file nib).

Adesso arriva il bello: il problema con la visualizzazione del campo dei valori sta nel fatto che uno stesso spazio è occupato da valori di natura diversa: stringhe, numeri, date, eccetera. Per aiutare l'utente a vedere questi dati, Cocoa introduce il concetto di Formatter. Ecco quindi che anche per queste celle assegno un formattatore adatto al tipo di dati da rappresentare:

        // aggiusto un formatter per visualizzare il dato
        // nella forma piu' acconcia
        if ( [ colId isEqual: @"valueSpec" ] )
        {
            TOS9TCForm        *myDF1 ;
            FileSizeForm     *myDF2 ;
            FilePosixPerm     *myDF3 ;
            NSDateFormatter *myDF4 ;
            
            switch ( criteriFiltro[rowIndex].fieldId ) {
            case MENUTAG_FILENAME:        // stringhe
            case MENUTAG_SPECIAL :
            case MENUTAG_GROUPNAME:        
            case MENUTAG_OWNERNAME:
                [ aCell setFormatter: nil ];
                break ;
            case MENUTAG_OSTYPE:         // os type
            case MENUTAG_OSCREATOR:
                myDF1 = [[[ TOS9TCForm alloc ] init ] autorelease ];
                [ aCell setFormatter: myDF1 ];
                break ;
            case MENUTAG_FILESIZE:        // dim file
                myDF2 = [[[ FileSizeForm alloc ] init ] autorelease ];
                [ aCell setFormatter: myDF2 ];
                break ;
            case MENUTAG_POSIXPERM:        // posix perm
                myDF3 = [[[ FilePosixPerm alloc ] init ] autorelease ];
                [ aCell setFormatter: myDF3 ];
                break ;
            case MENUTAG_MODDATE:         // date
            case MENUTAG_CREATDATE:
                myDF4 = [ [NSDateFormatter alloc]
                    initWithDateFormat: @"%d %b %Y"
                    allowNaturalLanguage: YES];
                [ aCell setFormatter: myDF4 ];
                break ;
            }
        }
    }
    // niente da fare per la tabella risultati
}

Prima di continuare, occorre fermarsi un momento su questi formattatori. Quando, a suo tempo, realizzai qualche classe di formattatori, mi ero limitato alla parte di visualizzazione (ovvero, come convertire un valore in una rappresentazione sotto forma di stringa). Ora, occorre anche il processo inverso, ovvero, partire da una rappresentazione sotto forma di stringa per ottenere un dato valore. Se questo processo è di serie con i formattatori standard (date, numeri), non lo è per i formattatori specifici della mia applicazione.

In effetti, ad esempio, nella realizzazione della classe FileSizeForm, ci sono sì due metodi alla bisogna, ma erano vuoti; occorre almeno scrivere il primo, che dovrebbe produrre un valore per l'oggetto di un dato tipo a partire da una stringa che lo rappresenta:

- (BOOL)
getObjectValue:            (id *)anObject
    forString:        (NSString *)string
    errorDescription:    (NSString **)error
{
    NSRange    rr ;
    float    xx, scale ;
    // dalla stringa estraggo l'eventuale numero presente
    xx = [ string floatValue ] ;
    // cerco di ricavare l'unita' di misura guardando se ci sono
    // sottostringhe di soli caratteri
    rr = [ string rangeOfCharacterFromSet: [ NSCharacterSet letterCharacterSet ]
        options: NSCaseInsensitiveSearch ] ;
    // se non trovo sottostringhe letterali, dco niente unita'
    scale = 1 ;
    // se invece ho trovato qualcosa, calcolo la scala
    if ( rr.location != NSNotFound )
    {
        // vedo che razza di carattere potrei avere
        unichar cc = [ string characterAtIndex: rr.location ] ;
        // tra quelli sensati (byte, Kappa, Mega, Giga)
        if ( cc == 'b' )
            scale = 1 ;
        else if ( cc == 'K' )
            scale = 1024 ;
        else if ( cc == 'M' )
            scale = 1024 * 1024 ;
        else if ( cc == 'G' )
            scale = 1024 *1024 * 1024 ;
    }
    // calcaolo la dimensione in byte e costruisco un NSNumber
    * anObject = [ NSNumber numberWithLong: (long)(xx * scale) ] ;
    return ( YES );
}

Le operazioni qui svolte sono piuttosto grossolane e intolleranti verso l'errore da parte dell'utente; però così facendo si possono inserire nella terza colonna valori come 100 K e 2 G per specificare dimensioni di file.

figura 08

figura 08

Operazioni concettualmente similari sono svolte per le classi TOS9TCForm e FilePosixPerm.

Ancora due concetti: permettere la modifica della cella di testo per impostare il valore solo quando necessario, con il metodo seguente (delegato):

- (BOOL)
tableView: (NSTableView *)aTableView
    shouldEditTableColumn:    (NSTableColumn *)aTableColumn
    row: (int)rowIndex
{
    if ( [ aTableView tag ] == TABLEVIEWTAG_CRITERI )
    {
        NSString * colId = [ aTableColumn identifier] ;
        if ( [ colId isEqual: @"valueSpec" ] )
        {
            if ( criteriFiltro[rowIndex].fieldId == MENUTAG_SPECIAL )
                    return NO ;
        }
        return YES ;
    }
    else return NO ;
}

Se la tabella è quella dei criteri, normalmente si risponde che tutto si può editare; a meno che non si tratti della colonna in cui specificare il valore di confronto, e il criterio è speciale.

Corrispondentemente, c'è il metodo per assegnare i valori nella tabella alla struttura dati:

- (void)
tableView:(NSTableView *)aTableView
    setObjectValue:(id)anObject
    forTableColumn:(NSTableColumn *)aTableColumn
    row:(int)rowIndex
{
    if ( [ aTableView tag ] == TABLEVIEWTAG_CRITERI )
    {
        NSString * colId = [ aTableColumn identifier] ;
        if ( [ colId isEqual: @"valueSpec" ] )
        {
            [ criteriFiltro[rowIndex].theValue release ] ;
            criteriFiltro[rowIndex].theValue = anObject ;
            [ criteriFiltro[rowIndex].theValue retain ] ;
        }
    }
}

Gestire i criteri

All'inizializzazione della classe FilterWinCtl occorre predisporre la struttura dati dei criteri:

- (void)
resetCriteriFiltro
{
    numOfCriteri = 1 ;
    criteriFiltro[0].fieldId = MENUTAG_FILENAME ;
    criteriFiltro[0].operId = MENUTAG_STR_ISEQUAL ;
    criteriFiltro[0].theValue = [ NSString stringWithString: @"che ne so" ] ;
    [ criteriFiltro[0].theValue retain ] ;
}

In questo modo, all'apertura della finestra, si presenta un criterio di default (ricerca sul campo con il nome del file, per stringhe uguali). Questa è anche la situazione che si presenta quando si fa clic sul pulsante di Clear All.

Quando l'utente vuole aggiungere un criterio, fa clic sul pulsante presente nella quarta colonna di uno criteri presenti; questo provoca l'esecuzione del seguente metodo:

- (IBAction)
addAnotherCrit:(id)sender
{
    if ( numOfCriteri >= 10 ) return ;
    criteriFiltro[numOfCriteri].fieldId = MENUTAG_FILENAME ;
    criteriFiltro[numOfCriteri].operId = MENUTAG_STR_ISCONTAINED ;
    criteriFiltro[numOfCriteri].theValue =
        [ NSString stringWithString: @"che ne so" ] ;
    [ criteriFiltro[numOfCriteri].theValue retain ] ;
    numOfCriteri += 1 ;
    [ logicPop setEnabled: YES ] ;
    [ listaCriteri reloadData ] ;
}

Le operazioni sono semplici: si aggiunge un criterio, con valori più o meno a caso. C'è da abilitare il menu pop up AND/OR, che inizialmente è disabilitato (perché di default c'è un solo criterio presente).

Un po' più complicato, ma di poco, il processo di eliminazione di un criterio, che si ha quando l'utente fa clic sul pulsante presente nella quinta colonna della tabella.

- (IBAction)
removeCrit:(id)sender
{
    int        row = [listaCriteri selectedRow ] ;
    int        i ;
    
    if ( numOfCriteri <= 1 ) return ;
    for ( i = row ; i < numOfCriteri-1 ; i ++ )
    {
        criteriFiltro[i].fieldId = criteriFiltro[i+1].fieldId ;
        criteriFiltro[i].operId = criteriFiltro[i+1].operId ;
        if (criteriFiltro[i].theValue != criteriFiltro[i+1].theValue)
        {
            [ criteriFiltro[i].theValue release ];
            criteriFiltro[i].theValue =
                [ criteriFiltro[i+1].theValue copy ];
            [ criteriFiltro[i].theValue retain ];
        }
    }
    [ criteriFiltro[numOfCriteri-1].theValue release ];
    numOfCriteri -=1 ;
    if ( numOfCriteri <= 1 )
        [ logicPop setEnabled: NO ] ;
    [ listaCriteri reloadData ] ;
}

Bisogna gestire il vettore, spostando indietro tutti i valori presenti di un passo, a partire dall'elemento da cancellare; in fondo, se i criteri si sono ridotti ad uno solo, disabilito nuovamente il menu pop up per AND/OR.

La nuova ricerca

A questo punto non rimane altro che scrivere il metodo che effettua la ricerca vera e propria. Si tratta sostanzialmente della vecchia procedura, rivista per tenere conto della presenza di più criteri. Cambiano i metodi nel nome e negli argomenti, ma il concetto è lo stesso. Si parte dal metodo attivato quando si fa clic sul pulsante Find:

- (IBAction)
filterElem:(id)sender
{
    int        i ;
    // 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: (CdCatDoc*)[[ NSApp orderedDocuments] objectAtIndex: 0] ];
    // pulisco i risultati della ricerca precedente
    [ self setFoundItems: [ NSMutableArray array ]];
    // e li tolgo dalla finestra di ricerca
    [ resultFld reloadData ];
    [ resultMsg setStringValue: [ NSString
        stringWithFormat: @"Found %d elements", [ foundItems count] ]];
    // ripulisco criteriFiltro mettendo una guardia a fine vettore
    for ( i = numOfCriteri ; i < 10 ; i++ )
        criteriFiltro[i].fieldId = -1 ;
    [ [ [ self currCatalog ] dataSource ] critSelect: criteriFiltro
                withLogic: [ [ logicPop selectedItem ] tag ]
                oper:        [ [ operPop selectedItem ] tag ]
                placeIn:    foundItems];
        // forzo il rinfresco della finestra dei risultati
    [ resultFld reloadData ];
    [ resultMsg setStringValue: [ NSString
        stringWithFormat: @"Found %d elements", [ foundItems count] ] ];
}

Se confrontate il vecchio metodo, le istruzioni sono le stesse: c'è solo una predisposizione del vettore criteriFiltro e il nuovo metodo di ricerca critSelect:...

Questo si riferisce alla classe CatDataSrc ed ancora una volta si limita a spazzare i volumi presenti passando gli argomenti:

- (void)
critSelect: (FltElemPtr) fltCrit withLogic: (int) logic
        oper: (int) operaz placeIn: (NSMutableArray *) where
{
    // 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 critSelect: fltCrit withLogic: logic oper: operaz placeIn: where ];
    }
}

Come sempre, il lavoro sporco è svolto all'interno della classe FileStruct.

- (void)
critSelect: (FltElemPtr) fltCrit withLogic: (int) logic
        oper: (int) operaz placeIn: (NSMutableArray *) where
{
    FileStruct    * item ;
    NSEnumerator     * enumerator = [[self fileList] objectEnumerator];
    // in ogni caso, vedo se l'elemento corrente va bene
    BOOL    isok = checkCriterio( self, fltCrit, logic );
    // intanto inserisco l'elemento filtrato nel risultato
    // se la modalita' e' ricerca, ho anche finito
    if ( isok )
        [ where addObject: self ] ;

Fino a qui, a parte la differente funzione invocata per capire se l'elemento risponde ai criteri impostati, non c'è nulla di nuovo. Qui inserisco alcune istruzioni per gestire i casi relativi all'impostazione delle variabili is2Print:

    // se sono su filtro stampa, distinguo i casi
    if ( operaz == FILTER_OPER_SETPRINT )
    {
        // assegno lo stato secondo valutazione
        [ self setIs2Print: isok ] ;
    }
    if ( operaz == FILTER_OPER_ADDPRINT )
    {
        // aggiungo elementi alla stampa
        if ( isok || is2Print )
            [ self setIs2Print: YES ] ;
    }
    if ( operaz == FILTER_OPER_REMPRINT )
    {
        // rimuovi gli elmenti dalal stampa
        if ( isok && is2Print )
            [ self setIs2Print: NO ] ;
    }
    // esploro i figli    
    while (item = [enumerator nextObject])
    {
        [ item critSelect: fltCrit withLogic: logic oper: operaz placeIn: where ];
    }
}

Come si vede, si fa prima a leggere le istruzioni che a spiegare: se l'operazione scelta è di impostare il parametro is2Print, si esegue il semplice metodo setIs2Print. Se l'operazione è di aggiungere gli elementi individuati tramite la ricerca all'insieme degli elementi che saranno stampati, si esegue l'operazione logica di OR tra il valore corrente di is2Print e il risultato della selezione; se infine l'operazione è di togliere l'elemento dalla stampa, si esegue lo AND logica tra il valore corrente ed il risultato della selezione. Per convincervi del funzionamento delle operazione, non c'è altro che farvi da soli qualche esempio.

Scegliere con criterio

Rimane da esaminare la funzione checkCriterio; questa esegue sull'oggetto passato come primo argomento la valutazione dei criteri (secondo argomento) adoperando la logica AND/OR specificata nel terzo argomento.

La funzione è piuttosto lunga, ma si divide facilmente in diversi blocchi, che si ripetono nel concetto con lievi differenze. Tuttavia, prima di cominciare, alcune nozioni di logica.

Se la logica è AND, un elemento si considera trovato (e la funzione restituisce YES) se tutti i criteri sono soddisfatti. Non appena uno dei criteri risulti NO, è inutile continuare: sono sicuro che l'elemento non fa parte del gruppo. Quindi, per lavorare in logica AND, si parte da un valore YES, e si continua a valutare i criteri finché uno risponde NO (ed allora restituisco NO) oppure sono finiti (ed allora rispondo YES).

Se la logica è OR, un elemento si considera trovato (e la funzione restituisce YES) se almeno un criterio è soddisfatti. Non appena uno dei criteri risulta YES, è inutile continuare: sono sicuro che l'elemento fa parte del gruppo. Quindi, per lavorare in logica OR, si parte da un valore NO, e si continua a valutare i criteri finché uno risponde YES (ed allora restituisco YES) oppure sono finiti (ed allora rispondo NO).

Come si può vedere, i due paragrafi precedenti, pur differenti, sono molto simili tra loro (un matematico direbbe che sono duali). Per questo userò lo stesso meccanismo di lavorazione nelle due logiche, nascondendo dentro funzioni apposta il fatto che la logica sia AND piuttosto che OR:

BOOL
initWithLogic ( int logic )
{
    if ( logic == FILTER_LOGIC_AND )
        return ( YES ) ;
    return ( NO );
}

BOOL
setWithLogic ( BOOL val1, BOOL val2, int logic )
{
    if ( logic == FILTER_LOGIC_AND )
        return ( val1 && val2 ) ;
    return ( val1 || val2 );
}

BOOL
stopWithLogic( BOOL val, int logic )
{
    if ( logic == FILTER_LOGIC_AND )
        return ( val == NO ) ;
    return ( val == YES );
}

Con queste tre funzioni ausiliarie, la funzione principale che decide sulla ricerca diventa:

BOOL
checkCriterio( FileStruct * item, FltElemPtr fltCrit, int logic )
{
    NSComparisonResult    xxx ;
    NSRange                xx ;
    NSString            *s1 ;
    NSNumber            * n1 ;
    long long            tmp1, tmp2 ;
    long                tmp3, tmp4 ;
    int                    i ;
    BOOL                tmpVal ;
    NSArray                * id_string = [NSArray arrayWithObjects:
        COLID_FILENAME, COLID_MODDATE, COLID_CREATDATE, COLID_FILESIZE,
        COLID_GROUPNAME, COLID_OWNERNAME, COLID_POSIXPERM,
        COLID_OSCREATOR, COLID_OSTYPE,nil ];
    // inizializzo il risultato con valore di partenza
    BOOL                select = initWithLogic( logic) ;
    // esamino ordinatamente tutti i criteri
    for ( i = 0 ; i < 10 ; i++ )
    {
        // sentinella
        if ( fltCrit[i].fieldId == -1 )
            break ;

Ricordate? Prima di scatenare le operazioni di ricerca avevo impostato qeuesto valore -1 per indicare che non ci sono più criteri; con l'istruzione break esco dal ciclo for.

Ora cominciano diversi blocchi di operazioni, a seconda del tipo di confronti possibili; non li mostro tutti, ma su alcuni tiro via.

        // le operazioni dipendono dal campo prescelto
        switch ( fltCrit[i].fieldId ) {
        // - - - - - - - - - - - - - - - - - - -
        case MENUTAG_FILENAME:            // sono tutte stringhe
        case MENUTAG_GROUPNAME :        // cfr: uguale e diverso
        case MENUTAG_OWNERNAME :
            // ricavo la striga
            s1 = [ item valueForKey: [ id_string objectAtIndex: fltCrit[i].fieldId] ];
            // a seconda delle opzioni, faccio il confronto
            switch ( fltCrit[i].operId ) {
            case MENUTAG_STR_ISEQUAL :
                tmpVal = [ s1 isEqualToString: fltCrit[i].theValue ] ;
                // confronto esatto tra stringhe
                break ;
            case MENUTAG_STR_ISCONTAINED :
                xx = [ s1 rangeOfString: fltCrit[i].theValue ];
                tmpVal = (xx.location != NSNotFound) ;
                break;
            default:    // non succede, ma in caso ...
                tmpVal = NO ;
            }
            select = setWithLogic( select, tmpVal, logic );
            break ;

Qui sto considerando un campo stringa. Alla variabile select assegno, con le opportune considerazioni, il risultato del confronto.

        case MENUTAG_POSIXPERM:            // sono numeri, ma con strana
        case MENUTAG_OSCREATOR :        // rappresentazione;
        case MENUTAG_OSTYPE :            // cfr: uguale e diverso
            ...
            break ;
        case MENUTAG_MODDATE :        // una data, confronto pieno
            ...
            break;
        case MENUTAG_CREATDATE :        // una data, confronto pieno
            xxx = ( [ [ item creatDate] compare: fltCrit[i].theValue] ) ;
            switch ( fltCrit[i].operId ) {
            case MENUTAG_NUM_EQ :
                tmpVal = (xxx == NSOrderedSame) ;
                break;
            case MENUTAG_NUM_DIFFERENT :
                tmpVal = (xxx != NSOrderedSame) ;
                break;
            case MENUTAG_NUM_GREAT :
                tmpVal = (xxx == NSOrderedDescending) ;
                break;
            case MENUTAG_NUM_LESS :
                tmpVal = (xxx == NSOrderedAscending) ;
                break;
            default:    // non succede, ma in caso ...
                tmpVal = NO ;
            }
            select = setWithLogic( select, tmpVal, logic );
            break;
        case MENUTAG_FILESIZE :        // un numero, confronto pieno
            ...
            break ;

Nel caso di un confronto di date, uso il metodo apposito, ed in base al criterio produco un corretto risultato per la variabile tmpVal, che poi genera ancora un valore per la variabile select.

        case MENUTAG_SPECIAL :        // confronti sparsi...
            switch ( fltCrit[i].operId ) {
            case MENUTAG_FOLDER :    // figli > 0
                tmpVal = [ item numOfFiles]> 0 ;
                break ;
            case MENUTAG_BUNDLE :    // funzione apposta
                tmpVal = checkIfBundleDirectory( [item fileName]) ;
                break ;
            case MENUTAG_SIMPLEFILE :    // figlio = 0
                tmpVal = [ item numOfFiles] == 0 ;
                break ;
            case MENUTAG_ALLFILES :    // sempre YES
                tmpVal = YES ;
                break ;
            case MENUTAG_NONE :        // sempre NO
                tmpVal = NO ;
                break ;
            default:    // non succede, ma in caso ...
                tmpVal = NO ;
                break ;
            }
            select = setWithLogic( select, tmpVal, logic );
            break;

Nei confronti di tipo speciale, si produce velocemente il risultato in tmpVal, e di qui, ancora una volta, il valore select.

Finalmente siamo arrivati alla fine:

        default:        // non succede, ma in caso ...
            select = NO ;
            break ;
        }
        // verifico se vale la pena continuare
        if ( stopWithLogic( select, logic ) )
            break ;
    }
    // restituisco la valutazione di tutti i criteri
    return ( select );
}

La funzione stopWithLogic(.) controlla se si deve continuare col prossimo criterio o risparmiare operazioni inutile in quanto il risultato è già stabilito. L'ultima istruzione, infine, restituisce il valore select, che è il risultato definitivo della valutazione dei criteri.

Questo è tutto. Sono piuttosto soddisfatto di ciò che ho realizzato, soprattutto dal punto di vista concettuale. Come detto, dal punto di vista estetico le cose lasciano piuttosto a desiderare; ed anche in quanto a completezza, ci sono molti punti delicati. Però ci sono molte funzionalità ottenute con poco sforzo (la ricerca sul Finder non ha le condizioni in OR!), cosa è sempre positiva.

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