Use a TreeModel
[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         // TODO: If you create and keep a PangoLayout using this context, you
30         // must deal with changes to the context by calling
31         // pango_layout_context_changed() on the layout in response to the
32         // "style-set" and "direction-changed" signals for the widget.
33         protected Pango.Layout[] pango_cache;
34
35         // Labels to show, extracted from the model
36         protected string[] label_cache;
37         protected bool build_label_cache_needed;
38
39         protected int cur_el;
40
41         // Layout information
42         protected int focus_first;
43         protected int focus_end;
44         protected int[] focus_starts;
45         protected bool focus_locked;
46         protected int focus_centre;
47         protected bool focus_layout_needed;
48
49         protected int _focus_size;
50         public int focus_size {
51                 get { return _focus_size; }
52                 set {
53                         _focus_size = value;
54                         focus_starts = new int[value+1];
55                         focus_layout_needed = true;
56                         queue_draw();
57                 }
58         }
59
60         protected int _distortion_factor;
61         public int distortion_factor {
62                 get { return _distortion_factor; }
63                 set {
64                         _distortion_factor = value;
65                         focus_layout_needed = true;
66                         queue_draw();
67                 }
68         }
69
70         public FisheyeList()
71         {
72                 model = null;
73                 backing_store = null;
74
75                 label_cache = null;
76                 build_label_cache_needed = true;
77
78                 pango_cache = new Pango.Layout[30];
79                 for (int i = 0; i < 30; ++i)
80                         pango_cache[i] = null;
81
82                 // Defaults for properties
83                 focus_size = 20;
84                 distortion_factor = 30;
85
86                 cur_el = 0;
87                 focus_centre = 0;
88                 focus_locked = false;
89
90                 add_events(Gdk.EventMask.POINTER_MOTION_MASK
91                          | Gdk.EventMask.BUTTON_PRESS_MASK
92                          | Gdk.EventMask.BUTTON_RELEASE_MASK);
93         }
94
95         public unowned Gtk.TreeModel get_model() { return model; }
96         public void set_model (Gtk.TreeModel? model)
97         {
98                 this.model = model;
99                 build_label_cache_needed = true;
100                 queue_draw();
101         }
102
103         /* Mouse button got pressed over widget */
104         public override bool button_press_event(Gdk.EventButton event)
105         {
106                 stderr.printf("Mouse pressed on %d %s\n", cur_el, label_cache[cur_el]);
107                 return false;
108         }
109
110         /* Mouse button got released */
111         public override bool button_release_event(Gdk.EventButton event)
112         {
113                 stderr.printf("Mouse released on %d %s\n", cur_el, label_cache[cur_el]);
114                 // ...
115                 return false;
116         }
117
118         /* Mouse pointer moved over widget */
119         public override bool motion_notify_event(Gdk.EventMotion event)
120         {
121                 int old_cur_el = cur_el;
122                 int x = (int)event.x;
123                 int y = (int)event.y;
124
125                 focus_locked = !focus_layout_needed && x < allocation.width/2 && y >= focus_starts[0] && y < focus_starts[focus_end - focus_first];
126
127                 if (focus_locked)
128                 {
129                         for (int idx = focus_first; idx < focus_end; ++idx)
130                                 if (y < focus_starts[idx-focus_first+1])
131                                 {
132                                         cur_el = idx;
133                                         break;
134                                 }
135
136                 } else {
137                         cur_el = y * label_cache.length / allocation.height;
138                         if (old_cur_el != cur_el)
139                                 focus_layout_needed = true;
140                 }
141
142                 //stderr.printf("MOTION %f %f CE %d\n", event.x, event.y, cur_el);
143                 if (old_cur_el != cur_el)
144                 {
145                         queue_draw();
146                         old_cur_el = cur_el;
147                 }
148                 return false;
149         }
150
151         public override bool configure_event (Gdk.EventConfigure event)
152         {
153                 backing_store = new Gdk.Pixmap(window, allocation.width, allocation.height, -1);
154                 focus_layout_needed = true;
155                 queue_draw();
156                 return false;
157         }
158
159         /* Widget is asked to draw itself */
160         public override bool expose_event (Gdk.EventExpose event)
161         {
162                 if (backing_store == null)
163                         return false;
164
165                 draw(backing_store);
166
167                 window.draw_drawable(
168                         get_style().fg_gc[Gtk.StateType.NORMAL],
169                         backing_store,
170                         event.area.x, event.area.y,
171                         event.area.x, event.area.y,
172                         event.area.width, event.area.height);
173
174                 return false;
175         }
176
177         protected int el_y(int idx)
178         {
179                 // Undistorted Y
180                 int undy = idx * allocation.height / label_cache.length;
181                 // Distorted position
182                 int pos = fisheye(undy, focus_centre, distortion_factor, 0, allocation.height);
183                 //stderr.printf("%d %f %f\n", idx, undy, pos);
184                 return pos;
185         }
186
187         protected void build_label_cache()
188         {
189                 if (model == null)
190                 {
191                         label_cache = new string[0];
192                 } else {
193                         Gtk.TreeIter iter;
194                         if (!model.get_iter_first(out iter))
195                         {
196                                 label_cache = new string[0];
197                         }
198                         else
199                         {
200                                 int count = model.iter_n_children(null);
201                                 label_cache = new string[count];
202
203                                 int i = 0;
204                                 do {
205                                         string val;
206                                         model.get(iter, 0, out val, -1);
207                                         label_cache[i] = val;
208                                         ++i;
209                                 } while (model.iter_next(ref iter));
210                         }
211                 }
212
213                 build_label_cache_needed = false;
214                 focus_layout_needed = true;
215         }
216
217         protected void focus_layout()
218         {
219                 if (label_cache.length == 0)
220                 {
221                         focus_centre = 0;
222                         focus_first = 0;
223                         focus_end = 0;
224                         focus_starts[0] = 0;
225                 } else {
226                         // Anchor point
227                         focus_centre = cur_el*allocation.height/label_cache.length;
228
229                         focus_first = cur_el > focus_size/2 ? cur_el-focus_size/2 : 0;
230                         focus_end = focus_first + focus_size;
231                         if (focus_end >= label_cache.length) focus_end = label_cache.length;
232
233                         // Compute starting positions for all items in focus
234                         for (int idx = focus_first; idx < focus_end; ++idx)
235                         {
236                                 int posprev = idx == 0 ? 0 : el_y(idx-1);
237                                 int pos = el_y(idx);
238                                 int posnext = idx == label_cache.length-1 ? 1 : el_y(idx+1);
239                                 int y0 = (pos+posprev)/2;
240                                 int y1 = (pos+posnext)/2;
241
242                                 focus_starts[idx - focus_first] = y0;
243                                 focus_starts[idx - focus_first + 1] = y1;
244                         }
245                 }
246                 focus_layout_needed = false;
247         }
248
249         protected void draw(Gdk.Drawable drawable)
250         {
251                 if (build_label_cache_needed)
252                         build_label_cache();
253                 if (focus_layout_needed)
254                         focus_layout();
255
256                 Gtk.Style style = get_style();
257
258                 // Background
259                 drawable.draw_rectangle(style.bg_gc[Gtk.StateType.NORMAL], true, 0, 0, allocation.width, allocation.height);
260
261                 // Focus lock area
262                 drawable.draw_rectangle(style.bg_gc[Gtk.StateType.ACTIVE], true,
263                         0, focus_starts[0], allocation.width/2, focus_starts[focus_end - focus_first]);
264
265                 // Focus movement area
266                 drawable.draw_rectangle(style.bg_gc[Gtk.StateType.INSENSITIVE], true,
267                         allocation.width/2, 0, allocation.width, allocation.height);
268
269                 // Create a Cairo context
270                 //var context = Gdk.cairo_create (drawable);
271
272                 // Paint items around focus
273                 //context.select_font_face(style.font_desc.get_family(), Cairo.FontSlant.NORMAL, Cairo.FontWeight.NORMAL);
274                 for (int idx = focus_first; idx < focus_end; ++idx)
275                 {
276                         int y0 = focus_starts[idx - focus_first];
277                         int y1 = focus_starts[idx - focus_first + 1];
278
279                         Gtk.StateType itemState = Gtk.StateType.NORMAL;
280                         if (idx == cur_el)
281                         {
282                                 itemState = Gtk.StateType.SELECTED;
283                                 drawable.draw_rectangle(style.bg_gc[itemState], true,
284                                         0, y0, allocation.width, y1-y0);
285                         }
286
287                 
288                         // TODO: cache pango contexts instead of fontdescs
289                         int size = (y1-y0)*80/100;
290                         if (size <= 0) size = 1;
291                         if (size >= pango_cache.length) size = pango_cache.length - 1;
292                         if (pango_cache[size] == null)
293                         {
294                                 var fd = style.font_desc.copy();
295                                 fd.set_absolute_size(size*Pango.SCALE);
296                                 var pc = create_pango_context();
297                                 pc.set_font_description(fd);
298                                 pango_cache[size] = new Pango.Layout(pc);
299                         }
300                         pango_cache[size].set_text(label_cache[idx], -1);
301                         var layout = pango_cache[size];
302                         //stderr.printf("AZAZA %p\n", layout.get_attributes());
303                         //var attrlist = layout.get_attributes().copy();
304                         //stderr.printf("AL %p\n", attrlist);
305                         //var attrlist = new Pango.AttrList();
306                         //stderr.printf("SIZE %d\n", y1-y0);
307                         //attrlist.insert(new Pango.AttrSize(y1-y0));
308                         //var attrlist = layout.get_attributes();
309                         //attrlist.change(new Pango.AttrSize(y1-y0));
310                         //layout.set_attributes(attrlist);
311                         //layout.set_height(y1-y0);
312                         //int w, h;
313                         //layout.get_pixel_size(out w, out h);
314                         Gdk.draw_layout(drawable, style.fg_gc[itemState], 0, y0, layout);
315                 }
316         }
317
318     /*
319      * The following function is adapted from Prefuse's FisheyeDistortion.java.
320      *
321      * A relevant annotation from Prefuse:
322      *
323      * For more details on this form of transformation, see Manojit Sarkar and 
324      * Marc H. Brown, "Graphical Fisheye Views of Graphs", in Proceedings of 
325      * CHI'92, Human Factors in Computing Systems, p. 83-91, 1992. Available
326      * online at <a href="http://citeseer.ist.psu.edu/sarkar92graphical.html">
327      * http://citeseer.ist.psu.edu/sarkar92graphical.html</a>. 
328      */
329
330     /*
331      * Distorts an item's coordinate.
332      * @param x the undistorted coordinate
333      * @param coordinate of the anchor or focus point
334      * @param d disortion factor
335      * @param min the beginning of the display
336      * @param max the end of the display
337      * @return the distorted coordinate
338      */
339     private int fisheye(int x, int a, int d, int min, int max)
340     {
341         if ( d != 0 ) {
342             bool left = x<a;
343             double v;
344             int m = (left ? a-min : max-a);
345             if ( m == 0 ) m = max-min;
346             v = (double)(x - a).abs() / m;
347             v = (double)(d+1)/(d+(1/v));
348             return (int)Math.round((left?-1:1)*m*v + a);
349         } else {
350             return x;
351         }
352     }
353 }
354
355 public class Fisheye : Gtk.Window
356 {
357         public Fisheye()
358         {
359                 title = "Fisheye";
360                 destroy += Gtk.main_quit;
361
362                 var list = new FisheyeList();
363                 add(list);
364
365                 var store = new Gtk.ListStore(1, typeof(string));
366                 Gtk.TreeIter iter;
367                 for (int i = 0; i < 300; ++i)
368                 {
369                         store.append(out iter);
370                         store.set(iter, 0, "Antani %d".printf(i), -1);
371                 }
372
373                 list.set_model(store);
374         }
375 }
376
377 static int main (string[] args) {
378         Gtk.init (ref args);
379
380         var fe = new Fisheye();
381         fe.set_size_request(200, 300);
382         fe.show_all();
383
384         Gtk.main();
385
386         return 0;
387 }