Disconnect signals from old model
[gregoa/zavai.git] / src / fisheye.vala
1 /*
2  * zavai - simple interface to the OpenMoko (or to the FSO stack)
3  *
4  * Copyright (C) 2009  Enrico Zini <enrico@enricozini.org>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19  */
20
21 using GLib;
22
23 public class FisheyeList : Gtk.DrawingArea
24 {
25         protected Gtk.TreeModel model;
26         protected Gdk.Pixmap backing_store;
27
28         // Pango layouts cached for speed
29         protected const int max_font_size = 30;
30         protected Pango.Layout[] pango_cache;
31
32         // Labels to show, extracted from the model
33         protected string[] label_cache;
34         protected bool build_label_cache_needed;
35
36         protected int cur_el;
37
38         // Layout information
39         protected int focus_first;
40         protected int focus_end;
41         protected int[] focus_starts;
42         protected bool focus_locked;
43         protected int focus_centre;
44         protected bool focus_layout_needed;
45
46         protected int _focus_size;
47         public int focus_size {
48                 get { return _focus_size; }
49                 set {
50                         _focus_size = value;
51                         focus_starts = new int[value+1];
52                         focus_layout_needed = true;
53                         queue_draw();
54                 }
55         }
56
57         protected int _distortion_factor;
58         public int distortion_factor {
59                 get { return _distortion_factor; }
60                 set {
61                         _distortion_factor = value;
62                         focus_layout_needed = true;
63                         queue_draw();
64                 }
65         }
66
67         protected int _label_column;
68         public int label_column {
69                 get { return _label_column; }
70                 set {
71                         _label_column = value;
72                         build_label_cache_needed = true;
73                         queue_draw();
74                 }
75         }
76
77         public FisheyeList()
78         {
79                 model = null;
80                 backing_store = null;
81
82                 label_cache = null;
83
84                 pango_cache = new Pango.Layout[max_font_size];
85                 for (int i = 0; i < pango_cache.length; ++i)
86                         pango_cache[i] = null;
87
88                 // Defaults for properties
89                 focus_size = 20;
90                 distortion_factor = 30;
91                 label_column = 0;
92
93                 cur_el = 0;
94                 focus_centre = 0;
95                 focus_locked = false;
96
97                 add_events(Gdk.EventMask.POINTER_MOTION_MASK
98                          | Gdk.EventMask.BUTTON_PRESS_MASK
99                          | Gdk.EventMask.BUTTON_RELEASE_MASK);
100         }
101
102         public unowned Gtk.TreeModel get_model() { return model; }
103         public void set_model (Gtk.TreeModel? model)
104         {
105                 if (this.model != null)
106                 {
107                         this.model.row_changed -= on_row_changed;
108                         this.model.row_deleted -= on_row_deleted;
109                         this.model.row_has_child_toggled -= on_row_has_child_toggled;
110                         this.model.row_inserted -= on_row_inserted;
111                         this.model.rows_reordered -= on_rows_reordered;
112                 }
113                 this.model = model;
114                 this.model.row_changed += on_row_changed;
115                 this.model.row_deleted += on_row_deleted;
116                 this.model.row_has_child_toggled += on_row_has_child_toggled;
117                 this.model.row_inserted += on_row_inserted;
118                 this.model.rows_reordered += on_rows_reordered;
119                 build_label_cache_needed = true;
120                 queue_draw();
121         }
122
123         private void on_row_changed(Gtk.TreePath path, Gtk.TreeIter iter) { build_label_cache_needed = true; }
124         private void on_row_deleted(Gtk.TreePath path) { build_label_cache_needed = true; }
125         private void on_row_has_child_toggled(Gtk.TreePath path, Gtk.TreeIter iter) { build_label_cache_needed = true; }
126         private void on_row_inserted(Gtk.TreePath path, Gtk.TreeIter iter) { build_label_cache_needed = true; }
127         private void on_rows_reordered(Gtk.TreePath path, Gtk.TreeIter iter, void* new_order) { build_label_cache_needed = true; }
128
129         /* Mouse button got pressed over widget */
130         public override bool button_press_event(Gdk.EventButton event)
131         {
132                 stderr.printf("Mouse pressed on %d %s\n", cur_el, label_cache[cur_el]);
133                 return false;
134         }
135
136         /* Mouse button got released */
137         public override bool button_release_event(Gdk.EventButton event)
138         {
139                 stderr.printf("Mouse released on %d %s\n", cur_el, label_cache[cur_el]);
140                 // ...
141                 return false;
142         }
143
144         /* Mouse pointer moved over widget */
145         public override bool motion_notify_event(Gdk.EventMotion event)
146         {
147                 int old_cur_el = cur_el;
148                 int x = (int)event.x;
149                 int y = (int)event.y;
150
151                 focus_locked = !focus_layout_needed && x < allocation.width/2 && y >= focus_starts[0] && y < focus_starts[focus_end - focus_first];
152
153                 if (focus_locked)
154                 {
155                         for (int idx = focus_first; idx < focus_end; ++idx)
156                                 if (y < focus_starts[idx-focus_first+1])
157                                 {
158                                         cur_el = idx;
159                                         break;
160                                 }
161
162                 } else {
163                         cur_el = y * label_cache.length / allocation.height;
164                         if (old_cur_el != cur_el)
165                                 focus_layout_needed = true;
166                 }
167
168                 //stderr.printf("MOTION %f %f CE %d\n", event.x, event.y, cur_el);
169                 if (old_cur_el != cur_el)
170                 {
171                         queue_draw();
172                         old_cur_el = cur_el;
173                 }
174                 return false;
175         }
176
177         public override bool configure_event (Gdk.EventConfigure event)
178         {
179                 backing_store = new Gdk.Pixmap(window, allocation.width, allocation.height, -1);
180                 focus_layout_needed = true;
181                 queue_draw();
182                 return false;
183         }
184
185         /* Widget is asked to draw itself */
186         public override bool expose_event (Gdk.EventExpose event)
187         {
188                 if (backing_store == null)
189                         return false;
190
191                 draw(backing_store);
192
193                 window.draw_drawable(
194                         get_style().fg_gc[Gtk.StateType.NORMAL],
195                         backing_store,
196                         event.area.x, event.area.y,
197                         event.area.x, event.area.y,
198                         event.area.width, event.area.height);
199
200                 return false;
201         }
202
203         public override void style_set(Gtk.Style? previous_style)
204         {
205                 // Reset the pango cache if the pango context changes
206                 for (int i = 0; i < pango_cache.length; ++i)
207                         pango_cache[i] = null;
208         }
209         public override void direction_changed(Gtk.TextDirection previous_direction)
210         {
211                 // Reset the pango cache if the pango context changes
212                 for (int i = 0; i < pango_cache.length; ++i)
213                         pango_cache[i] = null;
214         }
215
216         protected int el_y(int idx)
217         {
218                 // Undistorted Y
219                 int undy = idx * allocation.height / label_cache.length;
220                 // Distorted position
221                 int pos = fisheye(undy, focus_centre, _distortion_factor, 0, allocation.height);
222                 //stderr.printf("%d %f %f\n", idx, undy, pos);
223                 return pos;
224         }
225
226         protected void build_label_cache()
227         {
228                 if (model == null)
229                 {
230                         label_cache = new string[0];
231                 } else {
232                         Gtk.TreeIter iter;
233                         if (!model.get_iter_first(out iter))
234                         {
235                                 label_cache = new string[0];
236                         }
237                         else
238                         {
239                                 int count = model.iter_n_children(null);
240                                 label_cache = new string[count];
241
242                                 int i = 0;
243                                 do {
244                                         string val;
245                                         model.get(iter, _label_column, out val, -1);
246                                         label_cache[i] = val;
247                                         ++i;
248                                 } while (model.iter_next(ref iter));
249                         }
250                 }
251
252                 build_label_cache_needed = false;
253                 focus_layout_needed = true;
254         }
255
256         protected void focus_layout()
257         {
258                 if (label_cache.length == 0)
259                 {
260                         focus_centre = 0;
261                         focus_first = 0;
262                         focus_end = 0;
263                         focus_starts[0] = 0;
264                 } else {
265                         // Anchor point
266                         focus_centre = cur_el*allocation.height/label_cache.length;
267
268                         focus_first = cur_el > _focus_size/2 ? cur_el-_focus_size/2 : 0;
269                         focus_end = focus_first + _focus_size;
270                         if (focus_end >= label_cache.length) focus_end = label_cache.length;
271
272                         // Compute starting positions for all items in focus
273                         for (int idx = focus_first; idx < focus_end; ++idx)
274                         {
275                                 int posprev = idx == 0 ? 0 : el_y(idx-1);
276                                 int pos = el_y(idx);
277                                 int posnext = idx == label_cache.length-1 ? 1 : el_y(idx+1);
278                                 int y0 = (pos+posprev)/2;
279                                 int y1 = (pos+posnext)/2;
280
281                                 focus_starts[idx - focus_first] = y0;
282                                 focus_starts[idx - focus_first + 1] = y1;
283                         }
284                 }
285                 focus_layout_needed = false;
286         }
287
288         protected void draw(Gdk.Drawable drawable)
289         {
290                 if (build_label_cache_needed)
291                         build_label_cache();
292                 if (focus_layout_needed)
293                         focus_layout();
294
295                 Gtk.Style style = get_style();
296
297                 // Background
298                 drawable.draw_rectangle(style.bg_gc[Gtk.StateType.NORMAL], true, 0, 0, allocation.width, allocation.height);
299
300                 // Focus lock area
301                 drawable.draw_rectangle(style.bg_gc[Gtk.StateType.ACTIVE], true,
302                         0, focus_starts[0], allocation.width/2, focus_starts[focus_end - focus_first]);
303
304                 // Focus movement area
305                 drawable.draw_rectangle(style.bg_gc[Gtk.StateType.INSENSITIVE], true,
306                         allocation.width/2, 0, allocation.width, allocation.height);
307
308                 // Create a Cairo context
309                 //var context = Gdk.cairo_create (drawable);
310
311                 // Paint items around focus
312                 //context.select_font_face(style.font_desc.get_family(), Cairo.FontSlant.NORMAL, Cairo.FontWeight.NORMAL);
313                 for (int idx = focus_first; idx < focus_end; ++idx)
314                 {
315                         int y0 = focus_starts[idx - focus_first];
316                         int y1 = focus_starts[idx - focus_first + 1];
317
318                         Gtk.StateType itemState = Gtk.StateType.NORMAL;
319                         if (idx == cur_el)
320                         {
321                                 itemState = Gtk.StateType.SELECTED;
322                                 drawable.draw_rectangle(style.bg_gc[itemState], true,
323                                         0, y0, allocation.width, y1-y0);
324                         }
325
326                 
327                         // TODO: cache pango contexts instead of fontdescs
328                         int size = (y1-y0)*80/100;
329                         if (size <= 0) size = 1;
330                         if (size >= pango_cache.length) size = pango_cache.length - 1;
331                         if (pango_cache[size] == null)
332                         {
333                                 var fd = style.font_desc.copy();
334                                 fd.set_absolute_size(size*Pango.SCALE);
335                                 var pc = create_pango_context();
336                                 pc.set_font_description(fd);
337                                 pango_cache[size] = new Pango.Layout(pc);
338                         }
339                         pango_cache[size].set_text(label_cache[idx], -1);
340                         var layout = pango_cache[size];
341                         //stderr.printf("AZAZA %p\n", layout.get_attributes());
342                         //var attrlist = layout.get_attributes().copy();
343                         //stderr.printf("AL %p\n", attrlist);
344                         //var attrlist = new Pango.AttrList();
345                         //stderr.printf("SIZE %d\n", y1-y0);
346                         //attrlist.insert(new Pango.AttrSize(y1-y0));
347                         //var attrlist = layout.get_attributes();
348                         //attrlist.change(new Pango.AttrSize(y1-y0));
349                         //layout.set_attributes(attrlist);
350                         //layout.set_height(y1-y0);
351                         //int w, h;
352                         //layout.get_pixel_size(out w, out h);
353                         Gdk.draw_layout(drawable, style.fg_gc[itemState], 0, y0, layout);
354                 }
355         }
356
357     /*
358      * The following function is adapted from Prefuse's FisheyeDistortion.java.
359      *
360      * A relevant annotation from Prefuse:
361      *
362      * For more details on this form of transformation, see Manojit Sarkar and 
363      * Marc H. Brown, "Graphical Fisheye Views of Graphs", in Proceedings of 
364      * CHI'92, Human Factors in Computing Systems, p. 83-91, 1992. Available
365      * online at <a href="http://citeseer.ist.psu.edu/sarkar92graphical.html">
366      * http://citeseer.ist.psu.edu/sarkar92graphical.html</a>. 
367      */
368
369     /*
370      * Distorts an item's coordinate.
371      * @param x the undistorted coordinate
372      * @param coordinate of the anchor or focus point
373      * @param d disortion factor
374      * @param min the beginning of the display
375      * @param max the end of the display
376      * @return the distorted coordinate
377      */
378     private int fisheye(int x, int a, int d, int min, int max)
379     {
380         if ( d != 0 ) {
381             bool left = x<a;
382             double v;
383             int m = (left ? a-min : max-a);
384             if ( m == 0 ) m = max-min;
385             v = (double)(x - a).abs() / m;
386             v = (double)(d+1)/(d+(1/v));
387             return (int)Math.round((left?-1:1)*m*v + a);
388         } else {
389             return x;
390         }
391     }
392 }
393
394 public class Fisheye : Gtk.Window
395 {
396         public Fisheye()
397         {
398                 title = "Fisheye";
399                 destroy += Gtk.main_quit;
400
401                 var list = new FisheyeList();
402                 add(list);
403
404                 var store = new Gtk.ListStore(1, typeof(string));
405                 Gtk.TreeIter iter;
406                 for (int i = 0; i < 300; ++i)
407                 {
408                         store.append(out iter);
409                         store.set(iter, 0, "Antani %d".printf(i), -1);
410                 }
411
412                 list.set_model(store);
413         }
414 }
415
416 static int main (string[] args) {
417         Gtk.init (ref args);
418
419         var fe = new Fisheye();
420         fe.set_size_request(200, 300);
421         fe.show_all();
422
423         Gtk.main();
424
425         return 0;
426 }