summaryrefslogtreecommitdiff
path: root/document.rest.txt
blob: 6c48458f15a19748e995ebb3db6009ee5e1deaee (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
========================
 Gtk2, Perl e Drag&Drop
========================
:Author: dakkar <dakkar@thenautilus.net>
 
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!