MaCocoa 027

Capitolo 027 - Solo Volumi, per favore

In questo capitolo, comincio con un po' di pulizia, discutendo come mai le dimensioni dei file catalogo sono enormi, e poi ripulisco un po' il codice introducendo (come mio solito) un file dove raccolgo le cose che non so dove mettere altrimenti. Dopo di che, l'argomento principe del capitolo è la limitazione della catalogazione ai soli volumi (ovvero, hard disk, cd ed altri rimovibili in generale), piuttosto che catalogare qualsiasi cosa che somigli ad un file. Nel fare questo, scopro tutta una serie di belle cose, utilizzando ancora classi interessanti come NSWorkspace, introducendo il concetto di sheet ed utilizzando perfino una barra da barbiere.

Sorgenti: Documentazione Apple, pazienza e fantasia.

Primo inserimento: 2 settembre 2002

Dimensioni Enormi

figura 01

figura 01

Quando, dopo l'ultimo capitolo, ho salvato un documento di tipo catalogo, ho scoperto che la dimensione del file era piuttosto grossa. Direi enorme. Ad esempio, catalogando un semplice volume (è un file .dmg di un programmino che ho scaricato da internet...), mi risulta un file di 2 Mega, addirittura di dimensioni maggiori del volume stesso. Dove sta il problema? Dopo lunghe considerazioni che risparmio al lettore, scopro che la quasi totalità della dimensione del file è occupato dalle icone associate ai file (bella scoperta: prima che salvassi anche le icone, le dimensioni dei file catalogo erano ragionevoli). In effetti, quando si recupera l'icona di un file da disco, è creato un oggetto della classe NSImage. Questo oggetto non contiene in realtà l'immagine dell'icona; o meglio, contiene una o più rappresentazioni dell'immagine come oggetti della classe NSImageRep. Ho scoperto che l'icona raccolta dal metodo iconForFile: è composta da più rappresentazioni, tra cui una di 128 pixel per 128; considerato che per ogni pixel ci vogliono 4 byte per specificarne il colore (32 bit), ovvero occorrono 64K di memoria per conservare una singola icona. Catalogate un po' di file, ed ecco che si raggiungono dimensioni di catalogo impressionanti (anche perché le icone non sono riciclate: un file all'interno del catalogo ha sempre la sua icona, anche se del tutto uguale e quella di un altro file). È giocoforza tentare di ridurre lo spazio occupato da un catalogo eliminando le rappresentazioni delle icone che non mi interessano. Allo scopo ho scritto una funzione (non un metodo!) che appunto esamina le varie rappresentazioni di una NSImage e tiene buona solo quella di dimensioni minori. Già che ci sono, svolgo qui dentro le operazioni di scalatura a 16x16 pixel:

void
reduceImageToIcon( NSImage * img )
{
    NSSize        smallIconSize ;
#if    REDUCE_ICON_ON_FILE
    int        minSize, minImgIndex, i ;
    NSImageRep     * imageRep ;
    // recupero la lista delle rappresentazioni
    NSArray        * tmp = [ img representations ] ;
    // per ora la rappresentazione minima e' la prima
    minImgIndex = 0 ;
    imageRep = [ tmp objectAtIndex: minImgIndex ];
    minSize = [ imageRep pixelsHigh ];
    // poi guardo tutte le altre rappresentazioni
    for ( i = 1 ; i < [ tmp count ] ; i ++ )
    {
        int        hh ;
        imageRep = [ tmp objectAtIndex: i ];
        // se la dimensione della rappresentazione corrente...
        hh = [ imageRep pixelsHigh ];
        // ...e' maggiore
        if ( hh >= minSize )
        {
            // la rappresentazione non mi interessa e la elimino
            [ img removeRepresentation: imageRep];
        }
        else    // ... se invece e' minore
        {
            // ho trovato una nuova rappresentazione minima
            // butto via quella precedente
            [ img removeRepresentation: [ tmp objectAtIndex: minImgIndex ] ];
            // mi segno questa rappresentazione
            minImgIndex = i ;
            minSize = hh ;
        }
    }
    // arrivato qui, dovrei avere una sola rappresentazione, la più piccola
#endif
    // dico di scalarla quando sara' ridimensionata
    [ img setScalesWhenResized: TRUE ];
    // perche' adesso la ridimensiono a 16x16
    smallIconSize.width = smallIconSize.height = 16 ;
    // ecco che la ridimensiono
    [ img setSize: smallIconSize];
}

Il codice è una classica ricerca di minimo, con la variante che non appena trovo qualcosa più grande del minimo, lo elimino brutalmente.

Con questo metodo, inserito opportunamente all'interno del metodo initWithPath: della classe LSFileInfo:

// prelevo l'icona del file
tmpImg = [ [ NSWorkspace sharedWorkspace] iconForFile: aFile ] ;
// qui ci sono molte "rappresentazioni" dell'icona...
reduceImageToIcon( tmpImg );
// e poi l'assegno all'interno del file
[ self setFileIcon: tmpImg ];

figura 02

figura 02

le dimensioni dei file catalogo salvati su disco sono ancora importanti, ma decisamente più piccole (68K vs 2 Mega).

Una volta scritto il metodo, mi sono posto il problema di dove piazzarlo, ovvero, all'interno di quale file mantenerlo; dopo tutto, non si tratta di un metodo di una classe, ma di una funzione nemmeno troppo legata all'applicazione... Per il momento, come faccio di solito, lo piazzo all'interno di un file (una coppia di file, in realtà) che chiamo djZeroUtils.m e djZeroUtils.h. In questi file metterò tutto quello che non si riferisce ad una classe in particolare, ma è patrimonio comune dell'applicazione. Ad esempio, ho già trovato per un paio di #define di compilazione (utili per sperimentare sul codice senza perdere pezzi già funzionanti) ed una macro che utilizzo genericamente all'interno dell'applicazione per la costruzione di metodi accessor:

#define        OBJ_ACC_SET( var, new )        \
    { [new retain]; [var autorelease]; var = new; }

Infine, leggendo qui e lì la documentazione Apple, scopro che esiste già una funzione per la conversione dei codice tipo/creatore propri di ogni file all'interno dei sistemi Mac OS 9 e precedenti; quindi, ho modificato la classe formatter TOS9TCForm come segue:

- (NSString *)
stringForObjectValue:    (id)anObject
{
    // controllo che l'oggetto sia un numero...
    if (![anObject isKindOfClass:[NSNumber class]]) {
        return nil;
    }
    // la documentazione Apple afferma che e' meglio usare la funzione
    // sotto indicata piuttosto che fare da soli...
    return ( NSFileTypeForHFSTypeCode( [ anObject longValue ]) );
}

molto più compatta della mia realizzazione.

Solo Volumi

L'applicazione si chiama CDCat perché, nell'idea iniziale, intende catalogare CD. In realtà, fino a questo momento, la finestra con la NSOutlineView è in grado di conservare informazioni su qualsiasi file e cartella.

Voglio limitare le possibilità di catalogazione permettendo di aggiungere ad un catalogo solamente volumi. Per volume intendo un disco rigido (o meglio, le partizioni di un disco rigido), dischi rimovibili come CD, DVD (nelle varie accezioni: scrivibili, riscrivibili...), dischi ZIP, eccetera. Presumo (ma non sono attualmente in grado di provarlo) che si possano anche catalogare volumi di rete, sia locale sia remota.

Ora, per aggiungere elementi ad un catalogo, sono possibili due strade: attraverso un dialogo standard di apri file oppure attraverso drag'n'drop. Vado per ordine e comincio col dialogo.

Ebbene, non riesco a trovare una caratterizzazione del dialogo standard di apertura file che limiti la scelta ai soli volumi. Anche se sono possibili diverse strade, decido di seguirne una tutta mia.

figura 03

figura 03

L'idea è di mostrare una finestra, all'interno della quale presentare, mediante una NSTableView, l'elenco dei volumi correntemente presenti. L'utente seleziona da questa lista un elemento, che sarà aggiunto al catalogo. Mi piacerebbe che tale finestra fosse in realtà una sheet, ovvero quel tipo di finestra che, in Mac OS X, risulta modale per un documento ma non per l'applicazione (per capire cosa intendo, prendete ad esempio il dialogo di stampa dell'applicazione TextEdit). Queste finestre appaiono simpaticamente scivolando da sotto il titolo della finestra, e lì rimangono attaccate anche muovendo la finestra. Nel frattempo, tuttavia, l'applicazione non è bloccata, e si può lavorare su altri documenti.

La Finestra Modale

figura 04

figura 04

Per realizzare una finestra modale destinata a diventare una sheet, non seguo particolari precauzioni. Vado in IB, costruisco un oggetto della classe NSPanel, lo riempio con del testo, una NSTableView e due pulsanti, uno di Select e uno di Cancel. Nella NSTableView mostro l'elenco dei volumi, con Select dico di catalogare il volume selezionato. Cancel, ovviamente, interrompe la sessione e fa nulla.

Definisco una nuova classe ChooseVolCtrl, sottoclasse di NSWindowController, e la attribuisco al File's Owner. Aggiungo un outlet verso la tabella e due azioni corrispondenti ai due pulsanti.

Ricordo che per la corretta visualizzazione di una tabella, questa ha bisogno di una classe che funzioni da sorgente di dati. Per non moltiplicare il numero delle classi, utilizzo lo stesso File's Owner come sorgente dei dati; questo significa che devo fare un collegamento tra la tabella ed il File's Owner, e che nella realizzazione della classe ChooseVolCtrl, oltre ai propri metodi, ci saranno anche dei metodi per la visualizzazione della tabella. Collego poi un altro po' di cose tra loro (la finestra al proprietario, i pulsanti alle azioni, cose del genere), faccio costruire i file della classe ChooseVolCtrl, e torno in XCode a scrivere i metodi.

Volumi ed Enumerator

Il primo problema da risolvere è trovare la lista dei volumi attualmente disponibili. Mi viene in aiuto la classe NSWorkspace, già utilizzata per il recupero delle icone. Esiste un metodo adatto allo scopo denominato mountedLocalVolumePaths:. Il metodo restituisce un vettore con il percorso completo dei volumi presenti, cosa non particolarmente bella; infatti, lo hard disk del computer (meglio, la partizione principale dove si trova il sistema operativo) è rappresentato dal path '/', effettivamente poco significativo. Di più: un eventuale CD di nome 'pippo' montato sulla scrivania appare nella lista come '/Volumes/pippo'. Per fortuna esiste il metodo displayNameAtPath: della classe NSFileManager che traduce questi path nel nomi cui siamo abituati.

Prima di passare al metodo che recupera i nomi dei volumi, stabilisco che la tabella della finestra presenterà anch'essa, come la NSOutlineView del documento catalogo, il nome del volume e l'icona che lo rappresenta. Mi occorre quindi anche salvare l'icona del volume. Questo spiega finalmente le variabili d'istanza della classe:

@interface ChooseVolCtrl : NSWindowController
{
    IBOutlet NSTableView *listaVolumi;
    // vettore con i nomi dei volumi
    NSMutableArray     * volumeNames;
    // vettore con i percorsi dei volumi
    NSMutableArray     * volumePaths;
    // vettore con le icone dei volumi
    NSMutableArray     * volumeIcons;
}

Ci sono tre vettori, che conservano i nomi per la visualizzazione, i percorsi completi per accederci quando si dovranno catalogare, e le icone per la visualizzazione.

Ho quindi scritto un metodo che esplora il mondo alla ricerca dei volumi presenti:

- (void)
checkLocalVolumes
{
    NSString    * volPath ;
    // recupero l'elenco dei volumi correntemente montati
    NSArray        * tmp = [ [NSWorkspace sharedWorkspace] mountedLocalVolumePaths ] ;
    // ne faccio un enumerator
    NSEnumerator     * enumerator = [tmp objectEnumerator];
    // costruisco tre vettori per tenere nomi ed icone
    NSMutableArray    * tmpVolNames = [NSMutableArray array] ;
    NSMutableArray    * tmpVolPaths = [NSMutableArray array] ;
    NSMutableArray    * tmpVolIcons = [NSMutableArray array] ;

    // ciclo sui volumi correntemente montati
    while ((volPath = [enumerator nextObject]))
    {
        // recupero l'icona del volume
        NSImage        * tmpImg = [ [ NSWorkspace sharedWorkspace] iconForFile: volPath ] ;
        reduceImageToIcon( tmpImg ) ;        // la riduco ai minimi termini
        [ tmpVolIcons addObject: tmpImg ];    // aggiungo l'icona al vettore
        [ tmpVolPaths addObject: volPath ];    // aggiungo il path al vettore
        // aggiungo il nome del volume al vettore
        [ tmpVolNames addObject: [[ NSFileManager defaultManager] displayNameAtPath: volPath] ];
    }
    // aggiorno i vettori d'istanza coi nomi ed icone
    [ self setVolumeNames: tmpVolNames ];
    [ self setVolumePaths: tmpVolPaths ];
    [ self setVolumeIcons: tmpVolIcons ];
}

Un costrutto nuovo (e la classe relativa) che compare qui è lo enumerator. Con un oggetto della classe NSEnumerator posso esplorare facilmente vettori, dizionari, insomma insiemi più o meno ordinati di oggetti. Si usa in modo molto semplice: dapprima si costruisce un enumerator relativo all'insieme che vi vuole esplorare. A questo enumerator poi, continuo a chiedere il prossimo oggetto dell'insieme, fino a che non restituisce ni". È proprio quello che ho fatto per estrarre i path dei volumi presenti, recuperare il nome e l'icona, ed inserire il tutto all'interno delle variabili d'istanza.

A questo punto, i due metodi principali per fornire i dati alla tabella si scrivono presto:

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

Il numero di righe da visualizzare è evidentemente pari al numero di elementi di uno qualsiasi dei tre vettori; invece, l'oggetto alla riga row indicata è l'elemento row-esimo del vettore che mantiene i nomi dei volumi, ma tale valore è restituito solo dopo aver assegnato alla cella l'icona del volume stesso per la visualizzazione.

- (id)
tableView:            (NSTableView *)tableView
    objectValueForTableColumn:    (NSTableColumn *)tableColumn
    row:             (int)row
{
    // assegno alla cella l'immagine
    [((ImageAndTextCell*) [tableColumn dataCell]) setImage: [volumeIcons objectAtIndex: row] ];
    // e poi restituisco il nome del volume
    return ( [volumeNames objectAtIndex: row] );
}

Montaggi e Smontaggi

Uno dei problemi della lista di volumi è che questa lista non è costante nel tempo; in altri termini, la lista cambia se si rende disponibile un nuovo CD, o viene espulso un altro CD, o se in rete si perde una connessione o se acquista una nuova. Si tratta di mantenere aggiornata la lista dei volumi, fotografata ad un certo istante, con la situazione corrente. Ancora una volta mi viene in aiuto la classe NSWorkspace, che rende disponibili delle notifiche quando un volume è montato (annuncia la sua disponibilità) o smontato (non è più presente perché ad esempio espulso). Occorre sottoscrivere un paio di abbonamenti; lo faccio nel metodo windowDidLoad:, assieme ad un altro paio di cose:

- (void)
windowDidLoad
{
    NSTableColumn *tableColumn = nil;
    ImageAndTextCell *imageAndTextCell = nil;

    // metto a posto l'elenco dei volumi presenti
    [self checkLocalVolumes ];
    // attenzione a quale notification center attaccarsi!!
    // dopo di che, dico che mi segnalino operazioni coi volumi
    [[ [NSWorkspace sharedWorkspace] notificationCenter] addObserver: self
        selector: @selector( situationChanged: )
        name: NSWorkspaceDidMountNotification object: nil ] ;
    [[ [NSWorkspace sharedWorkspace] notificationCenter] addObserver: self
        selector: @selector( situationChanged: )
        name: NSWorkspaceDidUnmountNotification object: nil ] ;
    // l'unica colonna ha come cella una ImageAndTextCell
    tableColumn = [listaVolumi tableColumnWithIdentifier: @"listaVol"];
    // creo l'oggetto autorelease perche' passa in carico alla tableColumn
    imageAndTextCell = [[[ImageAndTextCell alloc] init] autorelease];
    [imageAndTextCell setEditable: NO];
    [tableColumn setDataCell:imageAndTextCell];
    [ listaVolumi reloadData ];
}

Al caricamento della finestra, faccio la fotografia dello stato attuale dei volumi presenti con il metodo checkLocalVolumes. Poi, sottoscrivo due abbonamenti alle notifiche NSWorkspaceDidMountNotification e NSWorkspaceDidUnmountNotification. Da notare il fornitore di questi abbonamenti, che non è il nostro solito, ma quello specifico della classe NSWorkspace (ci ho messo un bel po' prima di capirlo; poi, è bastato, come sempre, leggere meglio la documentazione). Quando è disponibile un nuovo volume, o uno che era presente non è più in linea, è invocato il metodo situationChanged:, all'interno del quale aggiornerò la situazione. Il metodo poi prosegue installando la cella per la visualizzazione di testo ed immagine, e forzando l'aggiornamento della tabella per mostrare l'elenco dei volumi appena rilevato.

Ecco allora il metodo selectionChanged:, invocato quando succede qualcosa ai volumi presenti:

- (void )
situationChanged: ( NSNotification *) notification
{
    // in realta', nella notifica, c'e' scritto cosa e' successo...
    // ma e' per me piu' comodo aggiornare da zero la lista dei volumi
    [ self checkLocalVolumes ];
    // dico che la lista deve essere rinfrescata
    [ listaVolumi reloadData ];
}

Questo metodo, molto semplicemente, si limita ad esplorare nuovamente la situazione e a rinfrescare la tabella.

All'interno di questa classe ci sono altri due metodi, ma li commento dopo aver modificato la classe CatalogDoc.

Aggiungere Volumi

Una volta predisposta la classe ChooseVolCtrl occorre riscrivere il metodo addFileItem: della classe CatalogDoc per tenere conto di queste nuove esigenze. Questo metodo adesso apre la finestra come una sheet e poi gestisce le operazioni di catalogazione una volta che l'utente ha selezionato qualcosa (ma anche no). Il metodo è molto semplice, solo perché il grosso del lavoro è svolto altrove.

- (void)
addFileItem: (id)sender
{
    // se devo aggiungere solamente volumi, costruisco una finestra
    // in cui è presente la lista dei volumi; sara' una sheet
    // costruisco la finestra
    ChooseVolCtrl *tmpCVCtrl = [[ ChooseVolCtrl alloc] init ] ;
    // comincio la procedura: mi metto modale per il documento
    // alla chisura della sheet, sara' invocato il metodo addVolume:...
    [ NSApp beginSheet: [ tmpCVCtrl window ]
        modalForWindow: [ NSApp mainWindow ]
        modalDelegate: self
        didEndSelector: @selector( addVolume2Cat:returnCode:contextInfo: )
        contextInfo: nil
    ];
}

Si costruisce la finestra, e poi si lancia una sheet. Questa sheet è una finestra che scivola da sotto il titolo di una finestra, ed impedisce ogni lavorio sulla finestra oscurata (non impedisce però di lavorare su altri documenti all'interno dell'applicazione); la sheet non ha titolo, e rimane appiccicata alla finestra comunque questa si muova.

La costruzione di una sheet è un messaggio inviato all'applicazione nel suo complesso (la cosa mi sembra ragionevole, dal momento che bisogna aggiustare il gestore degli eventi): deve sapere quale finestra è la sheet vera e propria (primo argomento) e quale finestra verrà bloccata da questa sheet (secondo argomento). Il controller della sheet (nel caso, ChooseVolCtrl) gestisce l'interazione, e poi, quando l'utente ha risposto ai quesiti posti (nel mio caso, ha selezionato uno dei volumi e ha fatto clic su Select o su Cancel) restituisce il controllo alla finestra. Se nel metodo beginSheet:... è specificato il terzo argomento didEndSelector: (come nel mio caso), allora è invocato proprio il metodo specificato da questo argomento. Il metodo, dal nome piuttosto lungo, si aspetta tre parametri: il primo è l'oggetto sheet che è appena terminato; il secondo è un valore di ritorno fornito dalla sheet stessa (ci arrivo in un momento), il terzo sono informazioni che il metodo chiamante vuole passare a questo metodo chiamato, inserite proprio nell'ultimo argomento (nel mio caso, non ho trovato nulla da interessante da dire).

Noto che il metodo beginSheet:... non blocca le operazioni del metodo (in altre parole, se dopo l'istruzione con questo metodo ci fosse qualche altra istruzione, questa sarebbe eseguita); tuttavia, poiché la sheet intercetta ogni evento destinato alla finestra, non è che si possa fare molto (se non lavori in background, ma questo è un altro discorso).

In pratica, dopo quest'istruzione il controllo delle operazioni passa alla sheet; all'interno della sheet l'utente seleziona un volume, poi fa clic su Select o su Cancel. I metodi corrispondenti, della classe ChooseVolCtrl sono allora i seguenti:

- (IBAction)
selectButton:    (id)sender
{
    // annullo l'abbonamento
    [[ [NSWorkspace sharedWorkspace] notificationCenter] removeObserver: self ];
    // butto via la finestra
    [[self window] setIsVisible: FALSE ];
    // ho finito di lavorare modale con la sheet
    // il codice di ritorno e' la riga selezionata
    [NSApp endSheet: [self window] returnCode: [ listaVolumi selectedRow ] ];
}

Il metodo cancelButton: è del tutto simile; l'unica differenza è l'argomento returnCode, pari a -1. L'idea infatti è che la sheet comunichi alla finestra la riga selezionata dall'utente come l'argomento returnCode, che corrisponde ad un elemento nel vettore variabile d'istanza. La finestra di catalogo preleva poi l'elemento, che è il percorso del volume, e procede alla catalogazione: ecco quindi il metodo addVolume2Cat:..., ancora una volta molto semplice perché il grosso del lavoro è svolto altrove:

- (void)
addVolume2Cat:        (NSWindow *)sheet
    returnCode:    (int)returnCode
    contextInfo:    (void *)contextInfo
{
    NSString    * volName ;

    // se il codice di ritorno e' -1, non ho nulla da fare
    if ( returnCode== -1 )
        return ;
    // se arrivo qui, ho da montare un volume
    // recupero il percorso completo del volume...
    // dalla finestra recupero il controllore, da qui il vettore dei percorsi, e dal vettore
    // piglio l'elemento di indice returnCode
    volName = [[((ChooseVolCtrl*) [sheet windowController]) volumePaths] objectAtIndex: returnCode ];
    [ self performAddFilesModal: [ NSArray arrayWithObject: volName]];
    // ovviamente, il documento e' stato sporcato
    [ self updateChangeCount: NSChangeDone ] ;
    // rinfresco la finestra con i nuovi dati
    [ outlineView reloadData ];
}

Il metodo sfrutta il primo argomento, la finestra della sheet, per accedere alle variabili d'istanza del controllore di questa finestra; in questo modo recupera l'elemento d'indice indicato da returnCode, vale a dire, il percorso del volume da catalogare. Con questo path, costruisce un vettore di un solo elemento che passa al metodo performAddFilesModal:, per poi segnalare che il documento si è sporcato e che è il caso di aggiornare il contenuto della finestra.

Ma prima di passare avanti, un momento di pausa con un giochetto interessante.

Doppio Clic e Select

Lavorando con la lista dei volumi, ho trovato noioso selezionare il volume e poi fare clic su Select. Mi piacerebbe fare doppio clic sul volume, e lanciare in automatico la catalogazione. In altre parole, mi piacerebbe che la tabella risponda ad un doppio clic su di un elemento come il pulsante Select. Per fare questo, ho fatto una cosa semplice (ma non so se efficiente e soprattutto, non so se esiste un meccanismo equivalente già all'interno di Cocoa; non importa, nel farlo ho imparato qualcosa): ho creato una sottoclasse di NSTableView e ne ho riscritto un metodo per cambiarne il comportamento.

figura 06

figura 06

Torno in IB, e faccio una sottoclasse di NSTableView chiamata DblClkTableVw. Faccio in modo la tabella diventi di questa classe (si fa nella finestra Info, pannello Custom class). Genero i file della classe e torno in XCode.

Qui riscrivo il metodo mouseDown: (che in realtà è proprio di NSControl di cui la NSTableView è comunque una sottoclasse), che è inviato quando si fa clic col mouse sulla tabella:

- (void)
mouseDown:    (NSEvent *)theEvent
{
    // processo normalmente il comando
    [ super mouseDown: theEvent] ;
    // poi conto quanti clic ci sono stati
    // se sono due (o piu'), dico che c'e' stato un doppio-clic
    if ( [theEvent clickCount] >= 2 )
    {
        // che e' come aver scelto il pulsante Select
        // per individuare il destinatario, uso un trucco...
        [ self sendAction: @selector( selectButton: ) to: [self dataSource] ] ;
    }
}

In primo luogo, faccio in modo che l'evento sia trattato normalmente; poi, aggiungo il mio codice specifico: conto il numero di clic ravvicinati del mouse. Se due o più, mando il messaggio selectButton: al controllore della finestra. Per individuare tale controllore, uso la strada più breve a disposizione, ovvero sfrutto il fatto che il controllore della finestra è anche il delegato della tabella.

Nuovo Drop

Ho estratto il codice dell'inserimento del volume nel catalogo perché utilizzo questo metodo anche all'interno del metodo che realizza le operazioni di drag'n'drop.

C'è una fondamentale modifica da fare: le operazioni di drop si accettano solamente se gli elementi droppati sono dei volumi. Ho concentrato le modifiche all'interno del metodo prepareForDragOperation:. Ricordo che questo metodo è invocato quando l'utente rilascia il mouse dopo la draggatura e poco prima della droppatura. Il metodo risponde YES se l'operazione si può fare (quindi, se tutti gli elementi draggati sono path corrispondenti a volumi) e NO altrimenti (nella vecchia versione rispondeva sempre YES, dal momento che accettava qualunque path).

- (BOOL)
prepareForDragOperation:(id<nsdragginginfo>)sender
{
    // questo e' il momento di decidere se fare il drop
    NSPasteboard *pboard;

    pboard = [sender draggingPasteboard];
    // in primo luogo, droppo solo path di file
    if ([[pboard types] indexOfObject:NSFilenamesPboardType] != NSNotFound)
    {
        // recupero la lista degli elementi draggati
        NSArray        * pList = [ pboard propertyListForType: NSFilenamesPboardType ] ;
        // la metto in un enumerator
        NSEnumerator     * enumerator = [pList objectEnumerator];
        // recupero anche la lista dei volumi correntemente mondati
        NSArray        * volumes = [ [NSWorkspace sharedWorkspace] mountedLocalVolumePaths ] ;
        NSString    * volPath ;
        // dico che accetto il drop solo di volumi
        while ((volPath = [enumerator nextObject]))
        {
            // se cerco di droppare un elemento che non e' un volume
            if ( ! [ volumes containsObject: volPath ] )
                return ( NO );    // il drop non si fa
        }
        // se arrivo qui, tutti gli elementi droppati sono volumi,
        // posso eseguire l'operazione
        return ( YES );
    }
    // se arrivo qui, gli elementi droppati non sono nemmeno path...
    return ( NO );    
}

La cattiva programmazione mi ha fatto scrivere il metodo qui sopra, che esegue le operazioni in maniera piuttosto pedestre: in pratica verifica che ogni elemento presente nella lista degli elementi draggati si trovi anche all'interno della lista dei volumi correnti. Non appena un elemento non risponde a questo criterio, risponde NO e l'operazione di drop è cancellata. Forse c'è un meccanismo migliore e più efficiente, ma non ho voglia di cercarlo.

Se il metodo precedente risponde YES, si può procedere alla droppatura, e quindi alla catalogazione degli elementi droppati: questo è un compito per...

- (BOOL)
performDragOperation:(id<nsdragginginfo>)sender
{
    NSPasteboard     * dragPasteboard;
    NSArray        * pList ;

    // recupero la pasteboard
    dragPasteboard = [sender draggingPasteboard];
    // da qui, la lista degli elementi draggati
    pList = [ dragPasteboard propertyListForType: NSFilenamesPboardType ] ;
    [ self performAddFilesModal: pList ];
    // ovviamente, il documento e' stato sporcato
    [ self updateChangeCount: NSChangeDone ] ;
    return ( YES) ;
}

che diventa molto semplice: si piglia la lista degli elementi droppati e la si passa al metodo performAddFilesModal:.

La Barra del Barbiere

Sarebbe giunto il momento di vedere il metodo performAddFilesModal:, se non fosse che all'interno di questo metodo c'è un altro concetto.

figura 07

figura 07

Visto che l'operazione di catalogazione è piuttosto lunga, è grazioso informare l'utente dell'attesa mostrandogli una finestra di cortesia. La cosa più bella sarebbe mostrargli una barra che si colora con l'avanzamento delle operazioni (come molti programmi di installazione ci hanno abituato); tuttavia, nella catalogazione al momento ignoro lo stato di avanzamento. Sfrutto allora una barra tipo barbiere, ovvero una barra con righe animate bianche ed azzurre. Vado ancora una volta in IB e costruisco una finestra, con un testo e un oggetto della classe NSProgressIndicator (la citata barra del barbiere). La chiamo WaitingPanel, faccio come al solito un controllore WaitPanCtrl, outlet e collegamenti vari (vado via veloce, ormai ne ho fatte a bizzeffe di queste finestre).

L'interfaccia verso il mondo di questa finestra è composta da tre metodi (fintamente) di classe: come al solito, ci sarà una sola istanza di questa classe, la finestra riutilizzata da chiunque ne avesse bisogno.

+ (void) setText: (NSString*) title ;
+ (void) animate: (id) sender ;
+ (void) showHide: (BOOL) state ;

Con il primo metodo imposto la stringa di testo, con il secondo faccio animare la barra, con il terzo controllo la visibilità della finestra.

+ (void) setText: (NSString*) title
{
    // chiamo un metodo interno
    [ [ WaitPanCtrl sharedProgress] setMainText: title ];
}

+ (void) animate: (id) sender
{
    // chiamo un metodo interno
    [ [ WaitPanCtrl sharedProgress] animation: sender ];
}

+ (void) showHide: (BOOL) state
{
    // mostro/nascondo direttamente la fienstra
    [[[ WaitPanCtrl sharedProgress] window] setIsVisible: state ];
}

I primi due metodi si appoggiano sui metodi seguenti (non posso raggiungere gli outlet dentro i metodi di classe...):

- (void )
setMainText: (NSString*) title
{
    // banale, imposto la stringa
    [ infoText setStringValue: title ] ;
    // forzo il rinfresco della finestra
    [ infoText displayIfNeeded ];
}

- (void)
animation: (id) sender
{
    // passo di animazione
    [ progressBar animate: sender ] ;
    // forzo il rinfresco della finestra
    [ progressBar displayIfNeeded ];
}

È fondamentale il messaggio displayIfNeeded, perché normalmente la finestra è rinfrescata ad ogni loop degli eventi, e qui (ho scoperto con fatica) il loop non gira, visto che la barra impedirà la ricezione degli eventi stessi.

Aggiungi un File

figura 08

figura 08

Finalmente ci siamo; ecco il metodo per aggiungere un elenco di file ad un catalogo. L'idea è di mostrare la finestra di attesa e di bloccare l'intera applicazione fino a che l'operazione non è completata (lo so che non è una bella cosa, ma per il momento si fa così).

- (void)
performAddFilesModal: ( NSArray *)volList
{
    NSModalSession     session ;
    NSEnumerator     * enumerator = [volList objectEnumerator];
    NSString    * volPath ;

Esamino i path attraverso un enumerator. Estraggo subito il primo path per predisporre la finestra con il testo e per cominciare l'animazione della barra da barbiere:

    volPath = [enumerator nextObject] ;
    // mostro la finestra di attesa con apposita stringa
    [ WaitPanCtrl showHide: TRUE ];
    [ WaitPanCtrl setText: [NSString stringWithFormat: @"Cataloging %@",
        [[ NSFileManager defaultManager] displayNameAtPath: volPath] ]];
    // faccio partire l'animazione della barra del barbiere
    [ WaitPanCtrl animate: self ];

Uso i metodi di classe come se fossero variabili globali per accedere alla finestra dell'attesa. Da notare come il testo usa il nome del volume e non il percorso.

    // comincio una sessione modale per l'intera applicazione
    session = [NSApp beginModalSessionForWindow: [ [ WaitPanCtrl sharedProgress] window ] ];
    // eseguo la sessione
    [NSApp runModalSession:session] ;

Con queste due istruzioni comincio ed eseguo una sessione modale per l'applicazione, ovvero, tutte le finestre dell'applicazione non ricevono eventi fino a che questa sessione non termina. Tuttavia, l'attività del metodo prosegue (sono gli eventi che sono bloccati, non le operazioni), ed anzi, sfrutto proprio questa caratteristica per procedere con le operazioni di catalogazione

    while ( volPath )
    {
        // costruisco l'alberatura dei file a partire dalla scelta
        FileStruct     * fInfo = [[ FileStruct alloc ] initTreeFromPath: volPath ];
        // aggiungo la cosa al catalogo
        [ dataSource addFileEntry: fInfo ];
        volPath = [enumerator nextObject] ;
        if ( volPath == nil ) break ;

Qui ho recuperato le informazioni sul file corrente (e, per la ricorsività della faccenda, di tutto il volume); passo poi al volume successivo, a meno che lo enumerator restituisca nil. In questo caso, ho finito di esaminare i volumi, ed esco dal while.

        // mostro la finestra di attesa con apposita stringa
        [ WaitPanCtrl setText: [NSString stringWithFormat: @"Cataloging %@",
            [[ NSFileManager defaultManager] displayNameAtPath: volPath] ]];
        // faccio partire l'animazione della barra del barbiere
        [ WaitPanCtrl animate: self ];

Essendo cambiato il file sotto esame, cambio la scritta sulla finestra, e già che ci sono faccio un giro di barbiere.

    }
    // finisco la sessione
    [NSApp endModalSession: session];
    // nascondo la finestra di attesa
    [ WaitPanCtrl showHide: FALSE ];
}

Al termine, dichiaro chiusa la sessione modale e nascondo la finestra. Adesso le finestre riprendono a ricevere eventi.

Per rendere più amena l'animazione, all'interno del metodo initWithPath: della classe LSFileInfo ho aggiungo l'istruzione

[ WaitPanCtrl animate: self ];

per fare in modo che la barra si muova un po'.

Questo lungo capitolo termina qui.

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