Handle not fisheye case nicely
[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 background;
27         protected Gdk.Pixmap backing_store;
28
29         // Pango layouts cached for speed
30         protected const int steps = 5;
31         protected Gtk.CellRendererText[] renderers;
32         protected int max_renderer_size;
33
34         // Labels to show, extracted from the model
35         protected string[] label_cache;
36         protected bool base_layout_needed;
37
38         protected int cur_el;
39
40         // Layout information
41         protected int focus_first;
42         protected int focus_end;
43         protected int[] focus_starts;
44         protected bool focus_locked;
45         protected bool focus_layout_needed;
46         protected bool is_fisheye;
47
48         protected int _focus_size;
49         public int focus_size {
50                 get { return _focus_size; }
51                 set {
52                         _focus_size = value;
53                         focus_starts = new int[value+1];
54                         focus_layout_needed = true;
55                         queue_draw();
56                 }
57         }
58
59         protected int _label_column;
60         public int label_column {
61                 get { return _label_column; }
62                 set {
63                         _label_column = value;
64                         base_layout_needed = true;
65                         queue_draw();
66                 }
67         }
68
69         //public virtual signal void cursor_changed ();
70         public signal void row_activated(Gtk.TreePath path);
71
72         public FisheyeList()
73         {
74                 model = null;
75                 backing_store = null;
76
77                 label_cache = null;
78
79                 renderers = new Gtk.CellRendererText[steps];
80
81                 // Defaults for properties
82                 focus_size = 21;
83                 label_column = 0;
84
85                 cur_el = 0;
86                 focus_locked = false;
87
88                 add_events(Gdk.EventMask.POINTER_MOTION_MASK
89                          | Gdk.EventMask.BUTTON_PRESS_MASK
90                          | Gdk.EventMask.BUTTON_RELEASE_MASK);
91         }
92
93         public unowned Gtk.TreeModel get_model() { return model; }
94         public void set_model (Gtk.TreeModel? model)
95         {
96                 if (this.model != null)
97                 {
98                         this.model.row_changed -= on_row_changed;
99                         this.model.row_deleted -= on_row_deleted;
100                         this.model.row_has_child_toggled -= on_row_has_child_toggled;
101                         this.model.row_inserted -= on_row_inserted;
102                         this.model.rows_reordered -= on_rows_reordered;
103                 }
104                 this.model = model;
105                 this.model.row_changed += on_row_changed;
106                 this.model.row_deleted += on_row_deleted;
107                 this.model.row_has_child_toggled += on_row_has_child_toggled;
108                 this.model.row_inserted += on_row_inserted;
109                 this.model.rows_reordered += on_rows_reordered;
110                 base_layout_needed = true;
111                 queue_draw();
112         }
113
114         private void on_row_changed(Gtk.TreePath path, Gtk.TreeIter iter) { base_layout_needed = true; }
115         private void on_row_deleted(Gtk.TreePath path) { base_layout_needed = true; }
116         private void on_row_has_child_toggled(Gtk.TreePath path, Gtk.TreeIter iter) { base_layout_needed = true; }
117         private void on_row_inserted(Gtk.TreePath path, Gtk.TreeIter iter) { base_layout_needed = true; }
118         private void on_rows_reordered(Gtk.TreePath path, Gtk.TreeIter iter, void* new_order) { base_layout_needed = true; }
119
120         /* Mouse button got pressed over widget */
121         /*
122         public override bool button_press_event(Gdk.EventButton event)
123         {
124                 stderr.printf("Mouse pressed on %d %s\n", cur_el, label_cache[cur_el]);
125                 return false;
126         }
127         */
128
129         /* Mouse button got released */
130         public override bool button_release_event(Gdk.EventButton event)
131         {
132                 stderr.printf("Mouse released on %d %s\n", cur_el, label_cache[cur_el]);
133
134                 // Emit row_activated if applicable
135                 if (model != null)
136                 {
137                         Gtk.TreeIter iter;
138                         if (model.iter_nth_child(out iter, null, cur_el))
139                         {
140                                 Gtk.TreePath path = model.get_path(iter);
141                                 row_activated(path);
142                         }
143                 }
144                 return false;
145         }
146
147         /* Mouse pointer moved over widget */
148         public override bool motion_notify_event(Gdk.EventMotion event)
149         {
150                 int old_cur_el = cur_el;
151                 int x = (int)event.x;
152                 int y = (int)event.y;
153
154                 if (is_fisheye)
155                 {
156                         focus_locked = !focus_layout_needed && x < allocation.width/2 && y >= focus_starts[0] && y < focus_starts[focus_end - focus_first];
157
158                         if (focus_locked)
159                         {
160                                 for (int idx = focus_first; idx < focus_end; ++idx)
161                                         if (y < focus_starts[idx-focus_first+1])
162                                         {
163                                                 cur_el = idx;
164                                                 break;
165                                         }
166
167                         } else {
168                                 cur_el = y * label_cache.length / allocation.height;
169                                 if (old_cur_el != cur_el)
170                                         focus_layout_needed = true;
171                         }
172                 } else {
173                         cur_el = y / max_renderer_size;
174                         if (cur_el >= label_cache.length)
175                                 cur_el = label_cache.length - 1;
176                 }
177
178                 //stderr.printf("MOTION %f %f CE %d\n", event.x, event.y, cur_el);
179                 if (old_cur_el != cur_el)
180                 {
181                         queue_draw();
182                         old_cur_el = cur_el;
183                 }
184                 return false;
185         }
186
187         public override bool configure_event (Gdk.EventConfigure event)
188         {
189                 base_layout_needed = true;
190                 queue_draw();
191                 return false;
192         }
193
194         /* Widget is asked to draw itself */
195         public override bool expose_event (Gdk.EventExpose event)
196         {
197                 draw();
198
199                 window.draw_drawable(
200                         get_style().fg_gc[Gtk.StateType.NORMAL],
201                         backing_store,
202                         event.area.x, event.area.y,
203                         event.area.x, event.area.y,
204                         event.area.width, event.area.height);
205
206                 return false;
207         }
208
209         public override void style_set(Gtk.Style? previous_style)
210         {
211                 base_layout_needed = true;
212         }
213         public override void direction_changed(Gtk.TextDirection previous_direction)
214         {
215                 base_layout_needed = true;
216         }
217
218         protected int el_y(int idx)
219         {
220                 // Distorted position
221                 int pos = fisheye(idx);
222                 //stderr.printf("%d %f %f\n", idx, undy, pos);
223                 return pos;
224         }
225
226         protected Gtk.CellRendererText make_cell_renderer()
227         {
228                 var res = new Gtk.CellRendererText();
229                 res.font_desc = get_style().font_desc;
230                 res.ypad = 0;
231                 return res;
232         }
233
234         protected void base_layout()
235         {
236                 // Rebuild label cache
237                 if (model == null)
238                 {
239                         label_cache = new string[0];
240                 } else {
241                         Gtk.TreeIter iter;
242                         if (!model.get_iter_first(out iter))
243                         {
244                                 label_cache = new string[0];
245                         }
246                         else
247                         {
248                                 int count = model.iter_n_children(null);
249                                 label_cache = new string[count];
250
251                                 int i = 0;
252                                 do {
253                                         string val;
254                                         model.get(iter, _label_column, out val, -1);
255                                         label_cache[i] = val;
256                                         ++i;
257                                 } while (model.iter_next(ref iter));
258                         }
259                 }
260
261                 background = new Gdk.Pixmap(window, allocation.width, allocation.height, -1);
262                 backing_store = new Gdk.Pixmap(window, allocation.width, allocation.height, -1);
263
264                 // Recreate the renderers
265                 renderers[renderers.length-1] = make_cell_renderer();
266                 renderers[renderers.length-1].set_fixed_height_from_font(1);
267                 renderers[renderers.length-1].get_size(this, null, null, null, null, out max_renderer_size);
268
269                 is_fisheye = label_cache.length * max_renderer_size > allocation.height;
270
271                 if (is_fisheye)
272                 {
273                         renderers[0] = make_cell_renderer();
274                         renderers[0].size = Pango.SCALE;
275                         renderers[0].set_fixed_height_from_font(1);
276
277                         int step_size = 0;      // Size of the diminishing area
278                         for (int i = 1; i < renderers.length-1; ++i)
279                         {
280                                 renderers[i] = make_cell_renderer();
281                                 renderers[i].scale = (double)i / renderers.length;
282                                 renderers[i].set_fixed_height_from_font(1);
283                                 int size;
284                                 renderers[i].get_size(this, null, null, null, null, out size);
285                                 step_size += size;
286                         }
287
288                         //int focus_lines_count = allocation.height / (2 * max_renderer_size);
289                         //int focus_area_size = 2 * step_size + focus_lines_count * max_renderer_size;
290                         //int focus_area_start = pos(cur_el) - focus_lines_count*max_renderer_size/2 - step_size;
291                 }
292
293                 // Draw background
294                 draw_background(background);
295
296                 base_layout_needed = false;
297                 focus_layout_needed = true;
298         }
299
300         protected void focus_layout()
301         {
302                 if (label_cache.length == 0)
303                 {
304                         focus_first = 0;
305                         focus_end = 0;
306                         focus_starts[0] = 0;
307                 } else {
308                         if (_focus_size >= label_cache.length)
309                         {
310                                 focus_first = 0;
311                                 focus_end = label_cache.length;
312                         } else {
313                                 focus_first = cur_el -_focus_size/2;
314                                 if (focus_first < 0) focus_first = 0;
315
316                                 focus_end = focus_first + _focus_size;
317                                 if (focus_end > label_cache.length)
318                                 {
319                                         focus_end = label_cache.length;
320                                         focus_first = focus_end - _focus_size;
321                                 }
322                         }
323
324                         // Compute starting positions for all items in focus
325                         for (int idx = focus_first; idx <= focus_end; ++idx)
326                                 focus_starts[idx - focus_first] = el_y(idx);
327                 }
328                 focus_layout_needed = false;
329         }
330
331         protected void draw_background(Gdk.Drawable drawable)
332         {
333                 Gtk.Style style = get_style();
334
335                 // Background
336                 drawable.draw_rectangle(style.bg_gc[Gtk.StateType.NORMAL], true, 0, 0, allocation.width, allocation.height);
337
338                 if (!is_fisheye)
339                         return;
340
341                 // Focus movement area
342                 drawable.draw_rectangle(style.bg_gc[Gtk.StateType.INSENSITIVE], true,
343                         allocation.width/2, 0, allocation.width, allocation.height);
344
345                 for (int y = 0; y < allocation.height/2 - 30; y += 30)
346                 {
347                         Gtk.paint_arrow(style, (Gdk.Window*)drawable, Gtk.StateType.INSENSITIVE, Gtk.ShadowType.NONE,
348                                 null, this, null, Gtk.ArrowType.UP, false,
349                                 allocation.width/2, y, allocation.width/2, 10);
350                         Gtk.paint_arrow(style, (Gdk.Window*)drawable, Gtk.StateType.INSENSITIVE, Gtk.ShadowType.NONE,
351                                 null, this, null, Gtk.ArrowType.DOWN, false,
352                                 allocation.width/2, allocation.height-y-30, allocation.width/2, 10);
353                 }
354
355                 Gdk.Rectangle expose_area = Gdk.Rectangle();
356                 expose_area.x = expose_area.y = 0;
357                 expose_area.width = allocation.width;
358                 expose_area.height = allocation.height;
359
360                 Gdk.Rectangle cell_area = Gdk.Rectangle();
361                 cell_area.x = 0;
362                 cell_area.width = allocation.width;
363                 renderers[0].get_size(this, null, null, null, null, out cell_area.height);
364                 if (label_cache.length * cell_area.height >= allocation.height)
365                 {
366                         for (int y = 0; y < allocation.height; y += cell_area.height)
367                         {
368                                 int idx = y * label_cache.length / allocation.height;
369                                 cell_area.y = y;
370                                 renderers[0].text = label_cache[idx];
371                                 renderers[0].render((Gdk.Window*)drawable, this, 
372                                                 cell_area,
373                                                 cell_area,
374                                                 expose_area,
375                                                 0);
376                         }
377                 } else {
378                         int count = int.min(allocation.height/(2*cell_area.height), label_cache.length);
379                         for (int idx = 0; idx < count; ++idx)
380                         {
381                                 cell_area.y = idx * cell_area.height;
382                                 renderers[0].text = label_cache[idx];
383                                 renderers[0].render((Gdk.Window*)drawable, this, 
384                                                 cell_area,
385                                                 cell_area,
386                                                 expose_area,
387                                                 0);
388
389                                 cell_area.y = allocation.height-cell_area.y;
390                                 renderers[0].text = label_cache[label_cache.length-idx];
391                                 renderers[0].render((Gdk.Window*)drawable, this, 
392                                                 cell_area,
393                                                 cell_area,
394                                                 expose_area,
395                                                 0);
396                         }
397                 }
398         }
399
400         protected void draw()
401         {
402                 if (base_layout_needed)
403                         base_layout();
404                 if (focus_layout_needed)
405                         focus_layout();
406
407                 var drawable = backing_store;
408                 Gtk.Style style = get_style();
409                 Gdk.Rectangle expose_area = Gdk.Rectangle();
410                 expose_area.x = expose_area.y = 0;
411                 expose_area.width = allocation.width;
412                 expose_area.height = allocation.height;
413
414                 // Background
415                 drawable.draw_drawable(
416                         get_style().fg_gc[Gtk.StateType.NORMAL],
417                         background,
418                         0, 0, 0, 0,
419                         allocation.width, allocation.height);
420
421                 if (is_fisheye)
422                 {
423                         // Focus lock area
424                         drawable.draw_rectangle(style.bg_gc[Gtk.StateType.ACTIVE], true,
425                                 0, focus_starts[0], allocation.width/2, focus_starts[focus_end - focus_first]-focus_starts[0]);
426
427                         // Paint items around focus
428                         Gdk.Rectangle cell_area = Gdk.Rectangle();
429                         cell_area.x = 0;
430                         cell_area.width = allocation.width;
431                         for (int idx = 0; idx < focus_end-focus_first; ++idx)
432                         {
433                                 int y0 = focus_starts[idx];
434                                 int y1 = focus_starts[idx + 1];
435                                 cell_area.y = y0;
436                                 cell_area.height = y1-y0;
437
438                                 Gtk.CellRendererState rflags = 0;
439                                 if (idx + focus_first == cur_el)
440                                 {
441                                         rflags |= Gtk.CellRendererState.SELECTED | Gtk.CellRendererState.FOCUSED;
442                                         drawable.draw_rectangle(style.bg_gc[Gtk.StateType.SELECTED], true,
443                                                         cell_area.x, cell_area.y, cell_area.width, cell_area.height);
444                                 }
445                         
446                                 int size = (y1-y0);
447                                 if (size <= 0) size = 1;
448                                 if (size >= renderers.length) size = renderers.length - 1;
449                                 renderers[size].text = label_cache[idx + focus_first];
450                                 renderers[size].render((Gdk.Window*)drawable, this, 
451                                                 cell_area,
452                                                 cell_area,
453                                                 expose_area,
454                                                 rflags);
455
456                                 //Gdk.draw_line(drawable, style.fg_gc[itemState], 0, y0, allocation.width, y0);
457                         }
458                 } else {
459                         // Paint all items sequentially
460                         var renderer = renderers[renderers.length-1];
461                         Gdk.Rectangle cell_area = Gdk.Rectangle();
462                         cell_area.x = 0;
463                         cell_area.width = allocation.width;
464                         cell_area.height = max_renderer_size;
465                         for (int idx = 0; idx < label_cache.length; ++idx)
466                         {
467                                 cell_area.y = idx * cell_area.height;
468
469                                 Gtk.CellRendererState rflags = 0;
470                                 if (idx == cur_el)
471                                 {
472                                         rflags |= Gtk.CellRendererState.SELECTED | Gtk.CellRendererState.FOCUSED;
473                                         drawable.draw_rectangle(style.bg_gc[Gtk.StateType.SELECTED], true,
474                                                         cell_area.x, cell_area.y, cell_area.width, cell_area.height);
475                                 }
476
477                                 renderer.text = label_cache[idx];
478                                 renderer.render((Gdk.Window*)drawable, this, 
479                                                 cell_area,
480                                                 cell_area,
481                                                 expose_area,
482                                                 rflags);
483                         }
484                 }
485         }
486
487         /*
488          * The following function is adapted from Prefuse's FisheyeDistortion.java.
489          *
490          * A relevant annotation from Prefuse:
491          *
492          * For more details on this form of transformation, see Manojit Sarkar and 
493          * Marc H. Brown, "Graphical Fisheye Views of Graphs", in Proceedings of 
494          * CHI'92, Human Factors in Computing Systems, p. 83-91, 1992. Available
495          * online at <a href="http://citeseer.ist.psu.edu/sarkar92graphical.html">
496          * http://citeseer.ist.psu.edu/sarkar92graphical.html</a>. 
497          */
498
499         /*
500          * Distorts an item's coordinate.
501          * @param x the undistorted coordinate
502          * @param coordinate of the anchor or focus point
503          * @param d disortion factor
504          * @param min the beginning of the display
505          * @param max the end of the display
506          * @return the distorted coordinate
507          */
508         private int fisheye(int idx)
509         {
510                 // Autocompute distortion factor
511                 // 20 is the pixel size of the item at centre of focus
512                 double d = label_cache.length * 20 / allocation.height;
513                 if ( d <= 1 )
514                         return idx * allocation.height / label_cache.length;
515
516                 double a = (double)cur_el * allocation.height / label_cache.length;
517                 double x = (double)idx * allocation.height / label_cache.length;
518                 double max = (double)allocation.height;
519
520                 if (idx < cur_el)
521                 {
522                         double m = a;
523                         if ( m == 0 ) m = max;
524                         double v = (double)(a - x) / m;
525                         v = (double)(d+1)/(d+(1/v));
526                         return (int)Math.round(a - m*v);
527                 } else {
528                         double m = max-a;
529                         if ( m == 0 ) m = max;
530                         double v = (double)(x - a) / m;
531                         v = (double)(d+1)/(d+(1/v));
532                         return (int)Math.round(a + m*v);
533                 }
534         }
535 }
536
537 public class Fisheye : Gtk.Window
538 {
539         public Fisheye()
540         {
541                 title = "Fisheye";
542                 destroy += Gtk.main_quit;
543
544                 var list = new FisheyeList();
545                 add(list);
546
547                 var store = new Gtk.ListStore(1, typeof(string));
548                 Gtk.TreeIter iter;
549                 var infd = FileStream.open("/tmp/names", "r");
550                 if (infd == null)
551                 {
552                         for (int i = 0; i < 300; ++i)
553                         {
554                                 store.append(out iter);
555                                 store.set(iter, 0, "Antani %d".printf(i), -1);
556                         }
557                 } else {
558                         char buf[255];
559                         while (true)
560                         {
561                                 string line = infd.gets(buf);
562                                 if (line == null)
563                                         break;
564                                 store.append(out iter);
565                                 store.set(iter, 0, line, -1);
566                         }
567                 }
568
569                 list.set_model(store);
570         }
571 }
572
573 static int main (string[] args) {
574         Gtk.init (ref args);
575
576         var fe = new Fisheye();
577         fe.set_size_request(200, 300);
578         fe.show_all();
579
580         Gtk.main();
581
582         return 0;
583 }