dakkar
Gtk2, Perl e Drag&Drop
http://www.perl.it/documenti/articoli/2007/09/gtk2-perl-e-dra.html
© Perl Mongers Italia. Tutti i diritti riservati.

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.

finestra principale di glade

Finestra principale di glade, con "palette" degli strumenti, e finestra della nostra applicazione.

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(Gtk2::Gdk->SELECTION_CLIPBOARD);
    $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. Se volete provare il programma, dovrete cambiare alcuni nomi "cablati" nella funzione send_email:

  • il From del messaggio deve essere qualcosa che possiate riconoscere, e che non venga cestinato dal vostro mail server
  • il To del messaggio deve essere un indirizzo su cui potete leggere la posta (se non lo cambiate, arriva a me...)
  • il valore di Host, attualmente exelion, deve essere il nome di una macchina che faccia da mail server (ovvero, che risponda a porta 25 secondo il protocollo SMTP).

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!

Note su come eseguire (o installare) il programma

Potete scaricare una tarball con dentro tutti i pezzi del programma.

Il procedimento per usarla è quello comune dei moduli Perl:

tar zxf URLQueue-1.0.tar.gz
cd URLQueue
perl Makefile.PL
make

Dopodiché, per provare il programma, potete scrivere:

perl -Iblib/lib script/URLQueue.pl

Se voleste installarlo, scrivendo make install, teniate presente che in mancanza di ulteriori informazioni il programma si installerà assieme a tutti gli altri moduli di sistema (probabilmente da qualche parte sotto /usr/lib/perl5). Se volete installarlo altrove, potete usare:

perl Makefile.pl INSTALL_BASE=~

che installerà i moduli dentro lib/perl5 sotto la vostra home directory, e il programma dentro bin, sempre nella vostra home. A quel punto, se aggiungete $HOME/lib/perl5 alla variabile di ambiente PERL5LIB, e $HOME/bin a PATH, dovreste poter lanciare il programma senza problemi.