MaCocoa 019

Capitolo 019 - Sesso Droga e Drag'n'Drop

Purtroppo, o per fortuna, in questo capitolo non si parla di sesso e di droga, ma solo di drag'n'drop. Anzi, a dirla tutta, solo della parte 'drop'. Scopo infatti di questo capitolo è di realizzare le funzioni di drop all'interno di una finestra del Catalogo. In questo modo è possibile aggiungere file e directory intere trascinandole da una finestra del Finder all'interno di una finestra di Catalogo.

Sorgenti: A ruota libera a partire dalla documentazione Apple.

Primo inserimento: 18 Febbraio 2002

Protocolli

Ho già parlato di protocolli, come di un meccanismo che aggiunge funzionalità ad una classe senza dover definire una sottoclasse. In pratica un protocollo è una collezione di metodi; una classe si dice aderire al protocollo se definisce tutti (o parte) dei metodi che fanno parte del protocollo.

Nel caso, interessa il protocollo NSDraggingDestination; raggruppa i metodi che il destinatario di una operazione di drag'n'drop (lasciate che chiami l'operazione draggare; è orribile, ma non posso ripetere ogni volta la locuzione precedente). Come è noto, quando seleziono qualcosa e la draggo in giro, il sistema operativo fornisce una sorta di immagine dell'oggetto draggato.

Contestualmente, sempre il sistema operativo si occupa di mandare dei messaggi agli oggetti sopra cui l'utente sta draggando l'oggetto, fino a perfezionare l'operazione di drag'n'drop quando l'utente rilascia il mouse e con lui l'oggetto draggato (lasciate che chiami questa operazione droppare).

NSDraggingDestination

Il protocollo consiste di sei metodi; diciamo che l'utente ha pescato un oggetto e lo sta draggando in giro. Ad un certo punto raggiunge l'oggetto della mia applicazione sul quale è possibile droppare l'oggetto draggato.

Non appena l'immagine draggata entra nello spazio del destinatario, il sistema operativo invia il messaggio draggingEntered: al destinatario. Finché l'oggetto rimane in zona, al destinatario è inviato il messaggio draggingUpdated:. Se l'utente cambia idea e si sposta fuori dell'area del destinatario, il sistema invia il messaggio draggingExited:. Se rientra, si ricomincia con draggingEntered:.

Se invece l'utente droppa l'oggetto sopra il destinatario, possono accadere due cose: se il destinatario non è in grado di trattare l'oggetto droppatogli addosso, questo ritorna immediatamente al suo posto e tutto finisce lì. Se invece il destinatario accetta la droppata, il sistema operativo invia tre messaggi: prepareForDragOperation:, in cui il destinatario può predisporsi al ricevimento dati, performDragOperation:, in cui il destinatario effettua finalmente l'operazione; infine, se tutto è andato bene, al destinatario è inviato il messaggio concludeDragOperation:.

La cosa sembra inutilmente complicata, ma in realtà ogni metodo ha la sua ragion d'essere, come scoprirò più tardi. Infatti, adesso cerco di capire due cose: cosa c'entrano gli Appunti (la PasteBoard) e come fare a dire che un destinatario è in grado di ricevere oggetti droppati.

NSPasteboard

A quanto pare, il meccanismo per draggare oggetti ha a che fare con gli Appunti, ovvero con quel luogo misterioso dove va a finire tutto ciò che copio o taglio, in quell'intervallo di tempo che intercorre tra l'operazione di copiatura e quella di incollaggio. Quanto copio qualcosa (o, nella fattispecie, quando comincio a draggare qualcosa), le informazioni dell'oggetto copiato o draggato vanno a finire in un archivio temporaneo d'Appunti, un oggetto della classe NSPasteboard.

Quando il destinatario cerca di capire con che oggetto draggato ha a che fare, ricava le informazioni recuperandole appunto da una NSPasteboard.

In effetti, ogni metodo del protocollo è parametrizzato nello stesso modo:

- (tipoRisposta) nomeDelMetodo: (id <NSDraggingInfo>) sender ;

In altre parole (e più chiaramente), quando il sistema operativo invia un messaggio NSDraggingDestination (ad esempio, draggingUpdated:), aggiunge come parametro un oggetto dal quale è possibile recuperare le informazioni tramite una pasteboard. Ancora poco chiaro? Ecco un esempio di codice (copio brutalmente dalla documentazione Apple):

- (BOOL)draggingUpdated:(id <NSDraggingInfo>)sender {
    NSPasteboard *dragPasteboard;

    // get the dragging pasteboard from the NSDraggingInfo
    dragPasteboard = [sender draggingPasteboard];
    // now use the pasteboard to get whatever information the destination wants
    types = [dragPasteboard types];
    // etc.
}

Cosa succede? La prima istruzione eseguita è copiare le informazioni dell'oggetto draggato localmente, su di un oggetto NSPasteboard, invocando il metodo draggingPasteboard:. A questo punto, si possono sfruttare i metodi propri della pasteboard per ricavare le informazioni. Non stupisca il metodo types:. Come sono diversi le tipologie degli oggetti che posso copiare negli Appunti (testo, immagini, eccetera), così sono diverse le tipologie degli oggetti che draggo. Qualche applicazione può accettare come oggetti droppati testi ed immagini (si pensi ad un elaboratore di testi), l'applicazione Catalogo accetta come oggetti droppati dei nomi di file (e directory).

Cosa droppare

Eccomi arrivato all'altra questione. Come indicare (e a chi) quali oggetti si possono droppare felicemente sulle finestre dell'applicazione. In primo luogo, appare chiaro che per droppare qualcosa, il destinatario deve essere rappresentato da una porzione di spazio sullo schermo. Questo significa che solo finestre (oggetti NSWindow) o viste (oggetti NSView) possono aderire al protocollo NSDraggingDestination. Considerato che la maggior parte degli oggetti visibili a video (forse tutti) sono in qualche modo sottoclassi di NSView, non c'è problema. Piuttosto, la difficoltà sta altrove; perché un oggetto possa accettare oggetti droppati, deve aderire al protocollo. Piglio ad esempio l'oggetto NSOutlineView che costituisce la maggior parte (tutto lo spazio utile, in effetti) della finestra Catalogo. L'oggetto è una sottoclasse di NSView, quindi può andar bene. Devo adesso trovare un meccanismo per attaccarci i metodi del protocollo. Il fatto è che non ho a disposizione i file .h e .m da modificare... L'unica possibilità che ho senza fare acrobazie è di sfruttare il meccanismo della delega. Anche di questo ho già parlato: in pratica una classe dice che una serie di messaggi non sono da lei trattati direttamente, ma solo delegati ad un'altra classe, indicata appunto come classe delegata.

Bene, per economizzare sulle classi e sugli oggetti, decido che la finestra del documento Catalogo è lei in prima persona destinataria delle operazioni di drop. Poiché ho già un delegato per tale finestra (che, guarda caso, è proprio la classe documento CatalogDoc), aggiungo la definizione dei sei metodi all'interno del file CatalogDoc.m.

Inoltre, dichiaro che tale finestra accetta come oggetti droppati solo dei nomi di file (quindi, path completi di file o directory). Ecco quindi che devo aggiungere la seguente istruzione all'interno del metodo windowControllerDidLoadNib:, ovvero quando l'ambiente operativo ha terminato di caricare l'interfaccia finestra e si appresta a visualizzarla a tutti.

- (void)windowControllerDidLoadNib:(NSWindowController *) aController
{
    // ... le solite cose gia' viste...    
    // registro la finestra che accetti drag
    [ [ aController window] registerForDraggedTypes:
        [NSArray arrayWithObject: NSFilenamesPboardType] ];

}

Esamino l'ultima complicata istruzione un passo alla volta. In primo luogo, devo recuperare la finestra vera e propria. Siamo in una classe NSDocument, quindi devo passare attraverso una classe NSWindowController prima di arrivare alla finestra: ecco quindi che piglio aController, che è proprio il controllore della finestra appena creata, e invio il messaggio window: per recuperare la finestra. Dico quindi che questa finestra accetta (si registra presso il sistema operativo) che siano droppati su di essa degli oggetti di un certo tipo (o di diversi tipi, se preferisco): devo usare il metodo registerForDraggedTypes:. L'argomento di questo metodo è appunto l'elenco dei tipi di oggetto che accetto; io ho scelto di accettare solo oggetti del tipo NSFilenamesPboardType, ovvero dei nomi di file. Tuttavia, ho dovuto ugualmente costruire un vettore (con un unico elemento) per passarlo come argomento.

Altri tipi di oggetti sono ad esempio del 'semplice' testo in formato RTF (NSRTFPboardType), oppure una immagine TIFF (NSTIFFPboardType), cose del genere (un elenco si trova nella documentazione di NSPasteboard).

I Metodi

Comincio allora a realizzare i sei metodi. Comincio con

- (unsigned int)draggingEntered:(id <NSDraggingInfo>)sender

che arriva alla finestra quando l'utente dragga qualcosa su di essa.

Il metodo deve restituire un valore che indica cosa se ne farà il destinatario dell'oggetto se questo venisse droppato. Ci sono diverse possibilità, tra cui la copia, il collegamento, eccetera. Io decido che ne faccio una sorta di collegamento, quindi restituisco la costante predefinita NSDragOperationLink. Noto che questo valore può influenzare il modo con cui l'oggetto draggato è rappresentato a video. Il sistema operativo infatti, in base al valore restituito, può modificare l'immagine che rappresenta l'oggetto spostata dal cursore.

Nel metodo qui sotto riportato controllo anche che l'oggetto draggato preveda di poter fornire una informazione sotto forma di nome di file.

- (unsigned int)draggingEntered:(id<NSDraggingInfo>)sender
{
    NSPasteboard *pboard;

    pboard = [sender draggingPasteboard];

    if ([[pboard types] indexOfObject:NSFilenamesPboardType] != NSNotFound) {
        return NSDragOperationLink;
    }
    return NSDragOperationNone;
}

Il metodo successivo è facile in quanto segue la stessa filosofia del precedente:

- (unsigned int)draggingUpdated:(id<NSDraggingInfo>)sender
{
    return NSDragOperationLink;
}

Qui è molto facile, continuo semplicemente a rispondere che farò un collegamento dell'oggetto draggato. In realtà, io rispondo NSDragOperationLink, non perché ci abbia ragionato a lungo sopra, ma solo perché mi sembra la risposta più ragionevole (e, soprattutto, funziona...).

Anche il terzo metodo è molto facile:

- (void)draggingExited:(id<NSDraggingInfo>)sender
{
    return ;
}

Non avendo fatto nulla di speciale, non devo fare cose speciali quando l'utente se va cambiando idea sul drag'n'drop (magari invece stava semplicemente passando sopra la mia finestra per andare altrove...). In casi di drag'n'drop più evoluti, potrebbe essere necessario fare qualcosa anche qui.

Sto arrivando al nocciolo della questione:

- (BOOL)prepareForDragOperation:(id<NSDraggingInfo>)sender
{
    return ( YES );
}

Il metodo sembra abbastanza facile; in realtà, il compito del metodo è di prepararsi ad effettuare l'operazione di ricezione dell'oggetto droppato (qui l'utente ha già rilasciato il mouse), e di rispondere YES oppure NO se pensa di poter eseguire con successo o meno l'operazione stessa. Il sistema operativo, in base alla risposta di questo metodo, decide se riportare l'oggetto a posto (nel punto di partenza) e chiudere qui tutta la faccenda (il metodo ha risposto NO), oppure tutta la storia arriva al suo fine naturale, il drop vero e proprio.

Questa operazione è propria del metodo successivo, che ha finalmente qualche riga di codice:

- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender
{
    NSPasteboard * dragPasteboard;
    FileStruct     * fInfo ;
    NSArray         * pList ;
    NSString     * fName ;
    int            i, numelem ;
    
    // get the dragging pasteboard from the NSDraggingInfo
    dragPasteboard = [sender draggingPasteboard];
    pList = [ dragPasteboard propertyListForType: NSFilenamesPboardType ] ;
    numelem = [ pList count ] ;
    for ( i = 0 ; i < numelem ; i++ )
    {
        fName = [ pList objectAtIndex: i ] ;
        fInfo = [[ FileStruct alloc ] initTreeFromPath: fName ];
        [ dataSource addFileEntry: fInfo ];
    }
    return ( YES) ;
}

Comincio dalla fine, cioè dal valore di ritorno, che è YES se l'operazione si è conclusa felicemente, oppure NO se ci sono stati problemi (in questo secondo caso credo che il sistema riporti velocemente l'oggetto da dove era partito). Nel codice, ho utilizzato molte variabili per chiarezza espositiva, andando contro la mia natura di raggruppare tutto in poche istruzione. Allora: per prima cosa, recupero le informazioni dell'oggetto droppato col metodo draggingPasteboard a chi ha appena droppato l'oggetto. Poi ricavo i dati dalla pasteboard col metodo propertyListForType: Indico esplicitamente che mi devono essere restituiti dati relativi al nome di un file. Non chiedetemi perché uso questo metodo piuttosto che un altro; vi basti sapere che ho esaminato col debugger cosa diavolo c'era dentro la dragPasteboard, e dopo avere interpretato un bel po' di numeri in esadecimale mi sono reso conto che era una property list che contenere un array... A questo punto dentro pList ho un array di nomi di file; non faccio altro che esaminarli uno ad uno ed aggiungerli alla lista degli elementi trattati dall'oggetto dataSource.

Concludo con l'ultimo metodo. L'operazione si è conclusa felicemente ed il sistema operativo mi dà un'occasione per portare via la spazzatura, spolverare e mettere un po' d'ordine.

- (void)concludeDragOperation:(id<NSDraggingInfo>)sender
{
    [ outlineView reloadData ];
    return ;
}

Ne approfitto allora per dire alla outlineView di ricaricare i dati, che se ne sono aggiunti di nuovi. In realtà, potevo farlo a conclusione del metodo precedente, e lasciare vuoto quest'ultimo metodo, ma mi dispiaceva avere un altro metodo vuoto...

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