X-Git-Url: https://git.toastfreeware.priv.at/gregoa/zavai.git/blobdiff_plain/d1c73924649cfcf6d30a5543f99af7a523c6e31b..ec931a97b9880f43af2ccf0d5049e4e2a860a5bc:/src/fisheye.vala diff --git a/src/fisheye.vala b/src/fisheye.vala index 2c36102..33efc2a 100644 --- a/src/fisheye.vala +++ b/src/fisheye.vala @@ -20,51 +20,139 @@ using GLib; -public class FisheyeList : Gtk.DrawingArea +protected struct FocusInfo { - private string[] list; + int y; + int renderer; +} + + +public class FisheyeListView : Gtk.DrawingArea +{ + protected Gtk.TreeModel model; + protected Gdk.Pixmap background; + protected Gdk.Pixmap backing_store; + + // Renderers used at different sizes + protected const int steps = 5; + protected Gtk.CellRendererText[] renderers; + protected int[] renderer_sizes; + protected int max_renderer_size; + + // Labels to show, extracted from the model + protected string[] label_cache; + protected bool base_layout_needed; + protected int cur_el; - protected double distortion_factor; - // Number of items shown before and after the focus element + // Layout information protected int focus_first; protected int focus_end; - protected int focus_size; - protected double[] focus_starts; + protected FocusInfo[] focus_info; protected bool focus_locked; - protected double focus_centre; + protected bool focus_layout_needed; + protected bool is_fisheye; + protected int focus_step_size; + protected int focus_area_start; + + // Number of elements in full focus + protected int _focus_size; + public int focus_size { + get { return _focus_size; } + set { + _focus_size = value; + focus_info = new FocusInfo[_focus_size+2*steps+1]; + focus_layout_needed = true; + queue_draw(); + } + } + + protected int _label_column; + public int label_column { + get { return _label_column; } + set { + _label_column = value; + base_layout_needed = true; + queue_draw(); + } + } - public FisheyeList() + //public virtual signal void cursor_changed (); + public signal void row_activated(Gtk.TreePath path); + + public FisheyeListView() { - list = new string[300]; - for (int i = 0; i < 300; ++i) - list[i] = "Antani %d".printf(i); + model = null; + backing_store = null; + + label_cache = null; + + renderers = new Gtk.CellRendererText[steps]; + renderer_sizes = new int[steps]; + + // Defaults for properties + focus_size = 10; + label_column = 0; cur_el = 0; - focus_centre = 0.0; - focus_size = 20; - focus_starts = new double[focus_size + 1]; focus_locked = false; - distortion_factor = 30; - focus_layout(); add_events(Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK); } + public unowned Gtk.TreeModel get_model() { return model; } + public void set_model (Gtk.TreeModel? model) + { + if (this.model != null) + { + this.model.row_changed -= on_row_changed; + this.model.row_deleted -= on_row_deleted; + this.model.row_has_child_toggled -= on_row_has_child_toggled; + this.model.row_inserted -= on_row_inserted; + this.model.rows_reordered -= on_rows_reordered; + } + this.model = model; + this.model.row_changed += on_row_changed; + this.model.row_deleted += on_row_deleted; + this.model.row_has_child_toggled += on_row_has_child_toggled; + this.model.row_inserted += on_row_inserted; + this.model.rows_reordered += on_rows_reordered; + base_layout_needed = true; + queue_draw(); + } + + private void on_row_changed(Gtk.TreePath path, Gtk.TreeIter iter) { base_layout_needed = true; } + private void on_row_deleted(Gtk.TreePath path) { base_layout_needed = true; } + private void on_row_has_child_toggled(Gtk.TreePath path, Gtk.TreeIter iter) { base_layout_needed = true; } + private void on_row_inserted(Gtk.TreePath path, Gtk.TreeIter iter) { base_layout_needed = true; } + private void on_rows_reordered(Gtk.TreePath path, Gtk.TreeIter iter, void* new_order) { base_layout_needed = true; } + /* Mouse button got pressed over widget */ + /* public override bool button_press_event(Gdk.EventButton event) { - stderr.printf("Mouse pressed on %d %s\n", cur_el, list[cur_el]); + stderr.printf("Mouse pressed on %d %s\n", cur_el, label_cache[cur_el]); return false; } + */ /* Mouse button got released */ public override bool button_release_event(Gdk.EventButton event) { - stderr.printf("Mouse released on %d %s\n", cur_el, list[cur_el]); - // ... + //stderr.printf("Mouse released on %d %s\n", cur_el, label_cache[cur_el]); + + // Emit row_activated if applicable + if (model != null) + { + Gtk.TreeIter iter; + if (model.iter_nth_child(out iter, null, cur_el)) + { + Gtk.TreePath path = model.get_path(iter); + row_activated(path); + } + } return false; } @@ -72,24 +160,29 @@ public class FisheyeList : Gtk.DrawingArea public override bool motion_notify_event(Gdk.EventMotion event) { int old_cur_el = cur_el; - double x = event.x / allocation.width; - double y = event.y / allocation.height; - - focus_locked = x < 0.5 && y >= focus_starts[0] && y < focus_starts[focus_end - focus_first]; + int x = (int)event.x; + int y = (int)event.y; - if (focus_locked) + if (is_fisheye) { - for (int idx = focus_first; idx < focus_end; ++idx) - if (y < focus_starts[idx-focus_first+1]) - { - cur_el = idx; - break; - } - + focus_locked = !focus_layout_needed && x < allocation.width/2 && y >= focus_area_start+focus_info[0].y && y < focus_area_start+focus_info[focus_end - focus_first].y; + + if (y < focus_area_start+focus_info[0].y || y >= focus_area_start+focus_info[focus_end - focus_first].y) + cur_el = y * (label_cache.length+1) / allocation.height; + else + for (int idx = 0; idx < focus_info.length; ++idx) + if (y - focus_area_start < focus_info[idx].y + renderer_sizes[focus_info[idx].renderer]) + { + cur_el = idx + focus_first; + break; + } + + if (!focus_locked && old_cur_el != cur_el) + focus_layout_needed = true; } else { - cur_el = (int)Math.round(y * list.length); - if (old_cur_el != cur_el) - focus_layout(); + cur_el = y / max_renderer_size; + if (cur_el >= label_cache.length) + cur_el = label_cache.length - 1; } //stderr.printf("MOTION %f %f CE %d\n", event.x, event.y, cur_el); @@ -101,171 +194,394 @@ public class FisheyeList : Gtk.DrawingArea return false; } - /* Widget is asked to draw itself */ - public override bool expose_event (Gdk.EventExpose event) { - - // Create a Cairo context - var cr = Gdk.cairo_create (this.window); + public override bool configure_event (Gdk.EventConfigure event) + { + base_layout_needed = true; + queue_draw(); + return false; + } - // Set clipping area in order to avoid unnecessary drawing - cr.rectangle(event.area.x, event.area.y, - event.area.width, event.area.height); - cr.clip(); + /* Widget is asked to draw itself */ + public override bool expose_event (Gdk.EventExpose event) + { + draw(); - draw(cr); + window.draw_drawable( + get_style().fg_gc[Gtk.StateType.NORMAL], + backing_store, + event.area.x, event.area.y, + event.area.x, event.area.y, + event.area.width, event.area.height); return false; } - protected double el_y(int idx) + public override void style_set(Gtk.Style? previous_style) + { + base_layout_needed = true; + } + public override void direction_changed(Gtk.TextDirection previous_direction) { - double layout_min = 0; - double layout_max = 1; - // Undistorted Y - double undy = (double)idx/list.length; - // Distorted position - double pos = fisheye(undy, focus_centre, distortion_factor, layout_min, layout_max); - //stderr.printf("%d %f %f\n", idx, undy, pos); - return pos; + base_layout_needed = true; } - protected void focus_layout() + protected Gtk.CellRendererText make_cell_renderer() { - // Anchor point - focus_centre = (double)cur_el/list.length; + var res = new Gtk.CellRendererText(); + res.font_desc = get_style().font_desc; + res.ypad = 0; + return res; + } - focus_first = cur_el > focus_size/2 ? cur_el-focus_size/2 : 0; - focus_end = focus_first + focus_size; - if (focus_end >= list.length) focus_end = list.length; + protected void base_layout() + { + // Rebuild label cache + if (model == null) + { + label_cache = new string[0]; + } else { + Gtk.TreeIter iter; + if (!model.get_iter_first(out iter)) + { + label_cache = new string[0]; + } + else + { + int count = model.iter_n_children(null); + label_cache = new string[count]; + + int i = 0; + do { + string val; + model.get(iter, _label_column, out val, -1); + label_cache[i] = val; + ++i; + } while (model.iter_next(ref iter)); + } + } + + background = new Gdk.Pixmap(window, allocation.width, allocation.height, -1); + backing_store = new Gdk.Pixmap(window, allocation.width, allocation.height, -1); - // Compute starting positions for all items in focus - for (int idx = focus_first; idx < focus_end; ++idx) + // Recreate the renderers + renderers[renderers.length-1] = make_cell_renderer(); + renderers[renderers.length-1].set_fixed_height_from_font(1); + renderers[renderers.length-1].get_size(this, null, null, null, null, out max_renderer_size); + renderer_sizes[renderers.length-1] = max_renderer_size; + + is_fisheye = label_cache.length * max_renderer_size > allocation.height; + + if (is_fisheye) { - double posprev = idx == 0 ? 0 : el_y(idx-1); - double pos = el_y(idx); - double posnext = idx == list.length-1 ? 1 : el_y(idx+1); - double y0 = (pos+posprev)/2; - double y1 = (pos+posnext)/2; - - focus_starts[idx - focus_first] = y0; - focus_starts[idx - focus_first + 1] = y1; + renderers[0] = make_cell_renderer(); + renderers[0].size = Pango.SCALE; + renderers[0].set_fixed_height_from_font(1); + int min_renderer_size; + renderers[0].get_size(this, null, null, null, null, out min_renderer_size); + renderer_sizes[0] = min_renderer_size; + + focus_step_size = 0; // Size of the diminishing area + for (int i = 1; i < renderers.length-1; ++i) + { + renderers[i] = make_cell_renderer(); + renderers[i].scale = (double)i / renderers.length; + renderers[i].set_fixed_height_from_font(1); + int size; + renderers[i].get_size(this, null, null, null, null, out size); + renderer_sizes[i] = size; + focus_step_size += size; + } } + + // Draw background + draw_background(background); + + base_layout_needed = false; + focus_layout_needed = true; } - protected void draw(Cairo.Context context) + protected int el_renderer(int idx) { - Gtk.Style style = get_style(); - /* - public enum StateType { - NORMAL, - ACTIVE, - PRELIGHT, - SELECTED, - INSENSITIVE + int fs2 = _focus_size/2; + int renderer_idx; + if (idx < cur_el) + renderer_idx = idx - (cur_el-fs2-steps); + else + renderer_idx = (cur_el+fs2+steps) - idx; + if (renderer_idx < 0) + return 0; + if (renderer_idx >= renderer_sizes.length) + return renderer_sizes.length-1; + return renderer_idx; } - style.bg[Gtk.StateType.NORMAL]; - */ + protected void focus_layout() + { + if (!is_fisheye || label_cache.length == 0) + { + focus_first = 0; + focus_end = 0; + focus_layout_needed = false; + return; + } - // Background - Gdk.cairo_set_source_color(context, style.bg[Gtk.StateType.NORMAL]); - context.paint(); + focus_first = cur_el - _focus_size/2 - steps; + if (focus_first < 0) focus_first = 0; + if (focus_first + focus_info.length > label_cache.length) + focus_first = label_cache.length - focus_info.length; + + /* + int full_focus_start = anchor_y - (_focus_size/2) * max_renderer_size; + int full_focus_end = anchor_y + (_focus_size/2) * max_renderer_size; + int steps_start = full_focus_start - focus_step_size; + */ + + int cur_pos = 0; + int cur_idx = 0; + int focus_area_pre = 0; + while (cur_pos < allocation.height && cur_idx < focus_info.length) + { + if (focus_first + cur_idx == cur_el) + focus_area_pre = cur_pos; + focus_info[cur_idx].y = cur_pos; + focus_info[cur_idx].renderer = el_renderer(focus_first + cur_idx); +// stderr.printf("LAYOUT %d+[0-%d-%d] item %d/%d: pos %d rend %d rsz %d\n", +// focus_first, cur_idx, focus_info.length, focus_first + cur_idx, label_cache.length, +// cur_pos, focus_info[cur_idx].renderer, renderer_sizes[focus_info[cur_idx].renderer]); + cur_pos += renderer_sizes[focus_info[cur_idx].renderer]; + ++cur_idx; + } + + focus_info[cur_idx].y = cur_pos; + focus_info[cur_idx].renderer = 0; + focus_end = focus_first + cur_idx; + + int anchor_y = cur_el * allocation.height / (label_cache.length+1); + int focus_area_size = cur_pos; + + focus_area_start = anchor_y - focus_area_pre; + if (focus_area_start < 0) focus_area_start = 0; + if (focus_area_start + focus_area_size > allocation.height) + focus_area_start = allocation.height - focus_area_size; + +// stderr.printf("FA [0 [%d+%d=%d] %d]\n", focus_area_start, focus_area_size, focus_area_start+focus_area_size, allocation.height); + + focus_layout_needed = false; + } + + protected void draw_background(Gdk.Drawable drawable) + { + Gtk.Style style = get_style(); - // Normalise coordinates - context.translate (0, 0); - context.scale(allocation.width, allocation.height); + // Background + drawable.draw_rectangle(style.bg_gc[Gtk.StateType.NORMAL], true, 0, 0, allocation.width, allocation.height); - // Focus lock area - Gdk.cairo_set_source_color(context, style.bg[Gtk.StateType.ACTIVE]); - context.new_path(); - context.rectangle(0, focus_starts[0], 0.5, focus_starts[focus_end - focus_first]); - context.fill(); - context.stroke(); + if (!is_fisheye) + return; // Focus movement area - Gdk.cairo_set_source_color(context, style.bg[Gtk.StateType.INSENSITIVE]); - context.new_path(); - context.rectangle(0.5, 0, 1, 1); - context.fill(); - context.stroke(); - - // Paint items around focus - context.select_font_face(style.font_desc.get_family(), Cairo.FontSlant.NORMAL, Cairo.FontWeight.NORMAL); - for (int idx = focus_first; idx < focus_end; ++idx) + drawable.draw_rectangle(style.bg_gc[Gtk.StateType.INSENSITIVE], true, + allocation.width/2, 0, allocation.width, allocation.height); + + for (int y = 0; y < allocation.height/2 - 30; y += 30) { - double y0 = focus_starts[idx - focus_first]; - double y1 = focus_starts[idx - focus_first + 1]; + Gtk.paint_arrow(style, (Gdk.Window*)drawable, Gtk.StateType.INSENSITIVE, Gtk.ShadowType.NONE, + null, this, null, Gtk.ArrowType.UP, false, + allocation.width/2, y, allocation.width/2, 10); + Gtk.paint_arrow(style, (Gdk.Window*)drawable, Gtk.StateType.INSENSITIVE, Gtk.ShadowType.NONE, + null, this, null, Gtk.ArrowType.DOWN, false, + allocation.width/2, allocation.height-y-30, allocation.width/2, 10); + } + + Gdk.Rectangle expose_area = Gdk.Rectangle(); + expose_area.x = expose_area.y = 0; + expose_area.width = allocation.width; + expose_area.height = allocation.height; - if (idx == cur_el) + Gdk.Rectangle cell_area = Gdk.Rectangle(); + cell_area.x = 0; + cell_area.width = allocation.width; + cell_area.height = renderer_sizes[0]; + if (label_cache.length * cell_area.height >= allocation.height) + { + for (int y = 0; y < allocation.height; y += cell_area.height) + { + int idx = y * label_cache.length / allocation.height; + cell_area.y = y; + renderers[0].text = label_cache[idx]; + renderers[0].render((Gdk.Window*)drawable, this, + cell_area, + cell_area, + expose_area, + 0); + } + } else { + int count = int.min(allocation.height/(2*cell_area.height), label_cache.length); + for (int idx = 0; idx < count; ++idx) { - Gdk.cairo_set_source_color(context, style.bg[Gtk.StateType.SELECTED]); - context.new_path(); - context.rectangle(0, y0, 1, y1-y0); - context.fill(); - context.stroke(); - - Gdk.cairo_set_source_color(context, style.fg[Gtk.StateType.SELECTED]); - } else { - Gdk.cairo_set_source_color(context, style.fg[Gtk.StateType.NORMAL]); + cell_area.y = idx * cell_area.height; + renderers[0].text = label_cache[idx]; + renderers[0].render((Gdk.Window*)drawable, this, + cell_area, + cell_area, + expose_area, + 0); + + cell_area.y = allocation.height-cell_area.y; + renderers[0].text = label_cache[label_cache.length-idx]; + renderers[0].render((Gdk.Window*)drawable, this, + cell_area, + cell_area, + expose_area, + 0); } + } + } + + protected void draw() + { + if (base_layout_needed) + base_layout(); + if (focus_layout_needed) + focus_layout(); + + var drawable = backing_store; + Gtk.Style style = get_style(); + Gdk.Rectangle expose_area = Gdk.Rectangle(); + expose_area.x = expose_area.y = 0; + expose_area.width = allocation.width; + expose_area.height = allocation.height; - context.new_path(); - context.set_font_size(y1-y0); - Cairo.FontExtents extents; - context.font_extents(out extents); - context.move_to(0, y1-extents.descent); - context.text_path(list[idx]); - context.fill(); - context.stroke(); + // Background + drawable.draw_drawable( + get_style().fg_gc[Gtk.StateType.NORMAL], + background, + 0, 0, 0, 0, + allocation.width, allocation.height); + + if (is_fisheye) + { + // Focus lock area + drawable.draw_rectangle(style.bg_gc[Gtk.StateType.ACTIVE], true, + 0, focus_area_start + focus_info[0].y, allocation.width/2, focus_info[focus_end - focus_first].y); + + // Paint items around focus + Gdk.Rectangle cell_area = Gdk.Rectangle(); + cell_area.x = 0; + cell_area.width = allocation.width; + for (int idx = 0; idx < focus_info.length; ++idx) + { + var renderer = renderers[focus_info[idx].renderer]; + cell_area.y = focus_area_start + focus_info[idx].y; + cell_area.height = renderer_sizes[focus_info[idx].renderer]; + + Gtk.CellRendererState rflags = 0; + if (idx + focus_first == cur_el) + { + rflags |= Gtk.CellRendererState.SELECTED | Gtk.CellRendererState.FOCUSED; + drawable.draw_rectangle(style.bg_gc[Gtk.StateType.SELECTED], true, + cell_area.x, cell_area.y, cell_area.width, cell_area.height); + } + + renderer.text = label_cache[idx + focus_first]; + renderer.render((Gdk.Window*)drawable, this, + cell_area, + cell_area, + expose_area, + rflags); + + //Gdk.draw_line(drawable, style.fg_gc[itemState], 0, y0, allocation.width, y0); + } + } else { + // Paint all items sequentially + var renderer = renderers[renderers.length-1]; + Gdk.Rectangle cell_area = Gdk.Rectangle(); + cell_area.x = 0; + cell_area.width = allocation.width; + cell_area.height = max_renderer_size; + for (int idx = 0; idx < label_cache.length; ++idx) + { + cell_area.y = idx * cell_area.height; + + Gtk.CellRendererState rflags = 0; + if (idx == cur_el) + { + rflags |= Gtk.CellRendererState.SELECTED | Gtk.CellRendererState.FOCUSED; + drawable.draw_rectangle(style.bg_gc[Gtk.StateType.SELECTED], true, + cell_area.x, cell_area.y, cell_area.width, cell_area.height); + } + + renderer.text = label_cache[idx]; + renderer.render((Gdk.Window*)drawable, this, + cell_area, + cell_area, + expose_area, + rflags); + } } } - /* - * The following function is adapted from Prefuse's FisheyeDistortion.java. - * - * A relevant annotation from Prefuse: - * - * For more details on this form of transformation, see Manojit Sarkar and - * Marc H. Brown, "Graphical Fisheye Views of Graphs", in Proceedings of - * CHI'92, Human Factors in Computing Systems, p. 83-91, 1992. Available - * online at - * http://citeseer.ist.psu.edu/sarkar92graphical.html. - */ - - /* - * Distorts an item's coordinate. - * @param x the undistorted coordinate - * @param coordinate of the anchor or focus point - * @param d disortion factor - * @param min the beginning of the display - * @param max the end of the display - * @return the distorted coordinate - */ - private double fisheye(double x, double a, double d, double min, double max) - { - if ( d != 0 ) { - bool left = x + * http://citeseer.ist.psu.edu/sarkar92graphical.html. + * + * See also http://www.cs.umd.edu/hcil/fisheyemenu/ + */ } public class Fisheye : Gtk.Window { + Gtk.ListStore model; + FisheyeListView flv; + public Fisheye() { title = "Fisheye"; destroy += Gtk.main_quit; - var list = new FisheyeList(); - add(list); + flv = new FisheyeListView(); + add(flv); + + model = new Gtk.ListStore(1, typeof(string)); + Gtk.TreeIter iter; + var infd = FileStream.open("/tmp/names", "r"); + if (infd == null) + { + for (int i = 0; i < 300; ++i) + { + model.append(out iter); + model.set(iter, 0, "Antani %d".printf(i), -1); + } + } else { + char buf[255]; + while (true) + { + string line = infd.gets(buf); + if (line == null) + break; + model.append(out iter); + model.set(iter, 0, line, -1); + } + } + + flv.set_model(model); + + flv.row_activated += on_row_activated; + //stderr.printf("Mouse released on %d %s\n", cur_el, label_cache[cur_el]); + } + + public void on_row_activated(Gtk.TreePath path) + { + Gtk.TreeIter iter; + model.get_iter(out iter, path); + string val; + model.get(iter, 0, out val, -1); + stdout.printf("Clicked on %s\n", val); } } @@ -273,7 +589,7 @@ static int main (string[] args) { Gtk.init (ref args); var fe = new Fisheye(); - fe.set_size_request(300, 500); + fe.set_size_request(200, 300); fe.show_all(); Gtk.main();