======================== Gtk2, Perl e Drag&Drop ======================== :Author: dakkar Il bisogno ========== Avevo bisogno di un modo semplice per tenere una coda di "cose da leggere". Mi capita spesso di trovare in rete dei riferimenti interessanti, ma di non avere tempo / modo di leggerli sul momento. Certo, ci sono i "bookmark", ma li sento un po' troppo permanenti (sì, uso un'applicazione web per gestirmi i bookmark in modo centralizzato, ma quello è materiale per un altro articolo). Per cui ho pensato di avere un piccolo "riquadro" su cui trascinare delle URL, e ritrovarmele in una cartella del mio account di posta, da cui posso riprenderle con comodo, e cancellare dopo lette. Gli strumenti ============= Ovviamente il programma si scrive in Perl. Dovendo produrre un'applicazione grafica, io penso a gtk+ 2. Ci sono anche tanti altri toolkit, ma a me piace gtk+ 2. Per cui ci serve l'apposito modulo (chiamato, curiosamente, ``Gtk2``). Per disegnare l'interfaccia, ``glade``, e per caricarla, ``Gtk2::GladeXML::Simple``. Per mandare la posta, ``Email::Send``. Disegnare l'interfaccia ======================= Molti esempi di uso di gtk+ che si vedono in giro richiedono una marea di codice per creare i vari "widget" e attaccarci le funzioni di gestione. Troppo scomodo. Il sistema furbo si chiama Glade: è un programma che permette di disegnare l'interfaccia utente, e di salvarne la descrizione in un file di testo (permette anche di generare codice, ma in generale è una cattiva idea, e in particolare non genera codice Perl). Inoltre, è possibile definire quali metodi verranno invocati dai vari eventi dell'interfaccia. Per cui, con un minimo di lavoro, si ottiene il file ``main.glade``, che descrive la piccola finestra della nostra applicazione. .. figure:: main-window.png :alt: finestra principale di glade Finestra principale di glade, con "palette" degli strumenti, e finestra della nostra applicazione. .. figure:: events-callbacks.png :alt: finestra con eventi e metodi Finestra in cui vengono associati metodi dell'applicazione a eventi dell'interfaccia. Programma minimale ================== Iniziamo col caricare l'interfaccia appena definita. Io di solito uso un approccio a "controller": ogni finestra ha il suo controller, e il programma principale si limita a istanziare la prima finestra. La nostra applicazione ha una sola finestra, per cui avremo un solo controller. Il programma principale, ``URLQueue.pl``:: use strict; use warnings; use Gtk2 '-init'; use URLQueue::MainController; my $main_controller=URLQueue::MainController->new(); $main_controller->run; Ovvero: carichiamo ``Gtk2``, istanziamo il controller, e passiamogli la palla. Il controller, al minimo, ``URLQueue/MainController.pm``:: package URLQueue::MainController; use strict; use warnings; use base 'Gtk2::GladeXML::Simple'; use Path::Class; sub new { my ($class,%params)=@_; my $glade_file=file(__FILE__)->parent->file('main.glade'); my $self=$class->SUPER::new($glade_file); return $self; } 1; Notare la classe base: il modulo ``Gtk2::GladeXML::Simple`` ci riduce al minimo il lavoro, preoccupandosi di istanziare l'interfaccia dal file di Glade, e gestendo l'avvio del ciclo degli eventi. Per chi non lo conoscesse ancora, ``Path::Class`` è *il* modulo per gestire i percorsi di file. Se cominciate a usarlo, non potrete più farne a meno. Qualche evento ============== Se scrivete davvero i due file, e li eseguite (ovviamente dovete anche mettere il file ``main.glade`` nella stessa directory del controller), vi accorgerete che l'interfaccia funziona: potete spostare la finestra, ridimensionarla, ridurla a icona, chiuderla (e poco altro, visto che non abbiamo ancora scritto codice!). Vi potreste pure accorgere che, chiudendo la finestra, il programma non termina. Cosa sta succedendo? Sta succedendo che il ciclo degli eventi di gtk+ non è legato a una finestra: per quel che lo riguarda, potreste stare aspettando un segnale, una connessione di rete, un timeout, o qualsiasi altra cosa. Bisogna dirgli esplicitamente di terminare! Per questo, in ``main.glade`` ho associato all'evento ``destroy`` della finestra la subroutine ``quit`` (nel controller):: sub quit { Gtk2->main_quit; } Aggiungendo questa, la chiusura della finestra causa la terminazione del programma. Drag & drop =========== A questo punto possiamo cominciare con la parte interessante: gestire il trascinamento. Prima, un avvertimento: la documentazione di ``Gtk2`` (il modulo Perl) è alquanto scarna, limitandosi a un elenco di segnature di metodi e eventi. Per capire come usarlo, si deve far riferimento alla documentazione della libreria C di gtk+ (e gdk, e glib). Non proprio il modo più comodo di questo mondo, ma neppure impossibile. Peraltro, la maggior parte degli aspetti "dolorosi" dell'uso di quelle librerie in C viene eliminata usando Perl: il modulo ``Gtk2`` è fatto *molto* bene, e nasconde tutte le complessità sotto una serie di moduli molto "perlici". Tornando al trascinamento, la nostra applicazione vuole essere soltanto *destinazione*, non sorgente, di d&d. Questo ci semplifica la vita. Per essere destinazione, dobbiamo: 1) dichiarare i tipi che ci interessano 2) quando qualcuno ci trascina sopra qualcosa, decidere se la vogliamo o no 3) quando avviene il rilascio, richiedere i dati alla sorgente 4) quando arrivano i dati, usarli Notare la separazione tra gli ultimi due punti: ricevere dati è asincrono, in quanto può richiedere parecchie operazioni e interazioni con la sorgente, di cui fortunatamente si occupa la libreria. Cominciamo col dichiarare i tipi che ci interessano, nel costruttore del controller:: my $target_list=Gtk2::TargetList->new(); $target_list->add_uri_targets(1); $target_list->add_text_targets(2); $self->{input}->drag_dest_set('all', [qw(default copy move link private ask)], []); $self->{input}->drag_dest_set_target_list($target_list); Qui abbiamo da notare un po' di cose. Innanzi tutto, ``$self->{input}``: il modulo ``Gtk2::GladeXML::Simple`` espone come membri dell'oggetto tutti gli "widget" dichiarati del file Glade. Dopodiché, ``Gtk2::TargetList`` definisce un insieme di tipi accettabili: siccome non voglio preoccuparmi dei loro "veri nomi", uso i metodi di comodo per dichiarare che mi va bene testo e URI. I parametri che passo (1 e 2) sono dei numeri che mi serviranno per distinguere quale tipo ho ricevuto, quando mi arriveranno i dati davvero. Le ultime due chiamate associano i tipi al widget, per cui la libreria si preoccuperà (sperabilmente) di rifiutare altri tipi. A questo punto scriviamo la subroutine associata al "qualcuno sta trascinando roba sopra di me":: sub drag_motion { my ($self, $widget, $context, $x, $y, $time) = @_; $context->status($context->suggested_action,$time); return 1; } La chiamata a ``$context->status`` indica che accettiamo quel che l'utente vuole passarci. Il ``return 1`` indica che abbiamo gestito noi l'evento, e non serve passarlo al gestore predefinito. I parametri passati alla subroutine sono: ``$widget``: l'oggetto sul quale sta avvenendo il trascinamento (nel nostro caso sarà sempre ``$self->{input}``) ``$context``: oggetto che contiene varie informazioni per gestire il trascinamento ``$x`` e ``$y``: coordinate del mouse ``$time``: momento in cui l'evento è stato generato Quando finalmente l'utente decide di rilasciare i dati, il file Glade chiede di invocare questo metodo:: sub drag_drop { my ($self, $widget, $context, $x, $y, $time) = @_; if (my $atom=$context->targets) { $widget->drag_get_data($context, $atom, $time); return 1; } return 0; } Che si legge: se riusciamo a capire che dati ci vuole dare, li prendiamo; altrimenti lasciamo fare al gestore predefinito. Infine, quando riceviamo i dati:: sub drag_data_received { my ($self, $widget, $context, $x, $y, $data, $info, $time) = @_; if ($info==1) { $self->handle_uris($data->get_uris) } elsif ($info==2) { $self->handle_text($data->get_text); } else { warn "What is $info??"; } return 1; } Ricordate 1 e 2, visti prima? Ora ci tornano utili: se ``$info`` è 1, vuol dire che abbiamo ricevuto delle URI, se è 2 abbiamo ricevuto testo. Passiamo i dati alle funzioni apposite, e abbiamo finito. Clipboard ========= Beh, avremmo finito se non volessimo gestire anche il copia & incolla. Ma noi vogliamo, per cui dobbiamo scrivere un po' più di codice. Non molto: solo due funzioni. Prima funzione: siccome il nostro widget principale è una "text entry" (per nessun motivo particolare), può ricevere l'evento "incolla". In quel caso facciamo questo:: sub paste_clipboard { my ($self,$widget)=@_; my $clipboard=Gtk2::Clipboard->get(); $clipboard->request_text(sub{$self->handle_text($_[1])}); return 1; } Ovvero: prendiamo la "clipboard" predefinita, chiediamo il testo, e (quando ci arriverà, è sempre asincrono) lo passiamo alla funzione apposita. Seconda funzione: quando l'utente preme "tasto centrale", facciamo questo:: sub button_release { my ($self,$widget,$event)=@_; if ($event->button==2) { my $clipboard=Gtk2::Clipboard->get(Gtk2::Gdk->SELECTION_PRIMARY); $clipboard->request_text(sub{$self->handle_text($_[1])}); return 1; } return 0; } Non esiste un modo per agganciarsi al solo "tasto centrale", per cui discriminiamo con un ``if`` apposito. Il codice è sostanzialmente identico a prima, ma usiamo la "primary selection". Che cosa è? Stranezze di X11, per indovinare quale clipboard usare sono andato a tentativi. Usiamo i dati ============= Non scendo nei dettagli delle funzioni ``handle_text`` e ``handle_uris``, visto che sono lunghette e non direttamente collegate a gtk+. Darò solo qualche cenno. Quando ricevo delle URI, voglio sapere a cosa puntano, e inviare un messaggio contenente i "titoli" assieme alle URI. Per questo uso ``URI::Title``. Quando ricevo del testo, voglio controllare se non sia fatto di sole URI, e se sì ricadere sul caso precedente. Per farlo uso ``URI::Find``. Infine, per mandare il messaggio, uso ``Email::Send``. Una nota: ``handle_text`` ha un controllo per evitare di spedire due volte di fila lo stesso testo. Questo serve sia per evitarmi messaggi duplicati se premo due volte il tasto centrale, sia soprattutto per aggirare una stranezza di Firefox: se trascino un collegamento da Firefox, la mia applicazione lo riceve due volte. Boh!