[baobab] Port chart widgets to Vala



commit d6b347265c66c5fa059d2a5ae0e1285aeb23e311
Author: Stefano Facchini <stefano facchini gmail com>
Date:   Sun Jun 9 17:37:18 2013 +0200

    Port chart widgets to Vala
    
    Almost a line-by-line translation of the original C code. Main
    differences are:
      * _freeze() and _thaw() methods are removed (now useless as a spinner is
        used for the busy state
      * GObject properties are used throughout the code
      * contextual menu implemented with GMenu/GActions
      * slight rework of the subfolder tips code in RingsChart

 src/Makefile.am            |   12 +-
 src/baobab-chart.c         | 1772 --------------------------------------------
 src/baobab-chart.h         |  161 ----
 src/baobab-chart.vala      |  630 ++++++++++++++++
 src/baobab-menu.ui         |   18 +
 src/baobab-ringschart.c    |  683 -----------------
 src/baobab-ringschart.h    |   74 --
 src/baobab-ringschart.vala |  399 ++++++++++
 src/baobab-treemap.c       |  354 ---------
 src/baobab-treemap.h       |   65 --
 src/baobab-treemap.vala    |  200 +++++
 src/baobab-window.vala     |    8 +-
 src/baobab.vapi            |   36 -
 13 files changed, 1252 insertions(+), 3160 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index b1ad1c4..4e92fd0 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -16,14 +16,12 @@ baobab_VALAFLAGS = \
        --pkg gio-unix-2.0                      \
        --gresources=baobab.gresource.xml
 
-noinst_HEADERS = \
-       baobab-chart.h                  \
-       baobab-treemap.h                \
-       baobab-ringschart.h
-
 VALA_SOURCES = \
        baobab-application.vala         \
        baobab-cellrenderers.vala       \
+       baobab-chart.vala               \
+       baobab-treemap.vala             \
+       baobab-ringschart.vala          \
        baobab-connect-server.vala      \
        baobab-location.vala            \
        baobab-location-list.vala       \
@@ -40,11 +38,7 @@ egg_list_box_sources = \
 baobab_SOURCES = \
        $(VALA_SOURCES)                 \
        fixes.vapi                      \
-       baobab.vapi                     \
        config.vapi                     \
-       baobab-chart.c                  \
-       baobab-treemap.c                \
-       baobab-ringschart.c             \
        $(egg_list_box_sources)         \
        $(BUILT_SOURCES)
 
diff --git a/src/baobab-chart.vala b/src/baobab-chart.vala
new file mode 100644
index 0000000..b2ec662
--- /dev/null
+++ b/src/baobab-chart.vala
@@ -0,0 +1,630 @@
+/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* Baobab - disk usage analyzer
+ *
+ * Copyright (C) 2006, 2007, 2008  Igalia
+ * Copyright (C) 2013  Stefano Facchini <stefano facchini gmail com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA  02110-1301  USA
+ *
+ * Authors of the original code:
+ *   Felipe Erias <femorandeira igalia com>
+ *   Pablo Santamaria <psantamaria igalia com>
+ *   Jacobo Aragunde <jaragunde igalia com>
+ *   Eduardo Lima <elima igalia com>
+ *   Mario Sanchez <msanchez igalia com>
+ *   Miguel Gomez <magomez igalia com>
+ *   Henrique Ferreiro <hferreiro igalia com>
+ *   Alejandro Pinheiro <apinheiro igalia com>
+ *   Carlos Sanmartin <csanmartin igalia com>
+ *   Alejandro Garcia <alex igalia com>
+ */
+
+namespace Baobab {
+
+    public abstract class ChartItem {
+        public string name;
+        public string size;
+        public uint depth;
+        public double rel_start;
+        public double rel_size;
+        public Gtk.TreeIter iter;
+        public bool visible;
+        public bool has_any_child;
+        public bool has_visible_children;
+        public Gdk.Rectangle rect;
+
+        public unowned List<ChartItem> parent;
+    }
+
+    public abstract class Chart : Gtk.Widget {
+
+        protected const uint MAX_DEPTH = 8;
+        protected const uint MIN_DEPTH = 1;
+
+        const Gdk.RGBA TANGO_COLORS[] = {{0.94, 0.16, 0.16, 1.0}, /* tango: ef2929 */
+                                         {0.68, 0.49, 0.66, 1.0}, /* tango: ad7fa8 */
+                                         {0.45, 0.62, 0.82, 1.0}, /* tango: 729fcf */
+                                         {0.54, 0.89, 0.20, 1.0}, /* tango: 8ae234 */
+                                         {0.91, 0.73, 0.43, 1.0}, /* tango: e9b96e */
+                                         {0.99, 0.68, 0.25, 1.0}}; /* tango: fcaf3e */
+
+        uint name_column;
+        uint size_column;
+        uint info_column;
+        uint percentage_column;
+        uint valid_column;
+
+        bool model_changed;
+
+        Gtk.Menu context_menu = null;
+
+        List<ChartItem> items;
+
+        uint max_depth_ = MAX_DEPTH;
+        public uint max_depth {
+            set {
+                var m = value.clamp (MIN_DEPTH, MAX_DEPTH);
+                if (max_depth_ == m) {
+                    return;
+                }
+                max_depth_ = m;
+                model_changed = true;
+                queue_draw ();
+            }
+            get {
+                return max_depth_;
+            }
+        }
+
+        Gtk.TreeModel model_;
+        public Gtk.TreeModel model {
+            set {
+                if (model_ == value) {
+                    return;
+                }
+
+                if (model_ != null) {
+                    disconnect_model_signals (model_);
+                }
+
+                model_ = value;
+                root = null;
+
+                connect_model_signals (model_);
+
+                queue_draw ();
+            }
+            get {
+                return model_;
+            }
+        }
+
+        public void set_model_with_columns (Gtk.TreeModel m,
+                                            uint          name_column_,
+                                            uint          size_column_,
+                                            uint          info_column_,
+                                            uint          percentage_column_,
+                                            uint          valid_column_,
+                                            Gtk.TreePath? root_) {
+            model = m;
+            if (root_ != null) {
+                root = root_;
+            }
+
+            name_column = name_column_;
+            size_column = size_column_;
+            info_column = info_column_;
+            percentage_column = percentage_column_;
+            valid_column = valid_column_;
+        }
+
+        Gtk.TreeRowReference? root_;
+        public Gtk.TreePath? root {
+            set {
+                if (root_ != null) {
+                    var current_root = root_.get_path ();
+                    if (current_root != null && current_root.compare (value) == 0) {
+                        return;
+                    }
+                } else if (value == null) {
+                    return;
+                }
+                root_ = (value != null) ? new Gtk.TreeRowReference (model, value) : null;
+
+                queue_draw ();
+            }
+            owned get {
+                if (root_ != null) {
+                    var path = root_.get_path ();
+                    if (path != null) {
+                        return path;
+                    }
+                    root_ = null;
+                }
+                return new Gtk.TreePath.first ();
+            }
+        }
+
+        ChartItem? highlighted_item_ = null;
+        public ChartItem? highlighted_item {
+            set {
+                if (highlighted_item_ == value) {
+                    return;
+                }
+
+                if (highlighted_item_ != null) {
+                    get_window ().invalidate_rect (highlighted_item_.rect, true);
+                }
+                if (value != null) {
+                    get_window ().invalidate_rect (value.rect, true);
+                }
+
+                highlighted_item_ = value;
+            }
+            get {
+                return highlighted_item_;
+            }
+        }
+
+        public virtual signal void item_activated (Gtk.TreeIter iter) {
+            root = model.get_path (iter);
+        }
+
+        protected virtual void post_draw  (Cairo.Context cr) {
+        }
+
+        protected abstract void draw_item (Cairo.Context cr, ChartItem item, bool highlighted);
+        protected abstract void calculate_item_geometry (ChartItem item);
+
+        protected abstract bool is_point_over_item (ChartItem item, double x, double y);
+
+        protected abstract void get_item_rectangle (ChartItem item);
+
+        protected abstract bool can_zoom_in ();
+        protected abstract bool can_zoom_out ();
+
+        protected abstract ChartItem create_new_chartitem ();
+
+        SimpleActionGroup action_group;
+
+        const ActionEntry[] action_entries = {
+            { "move-up", move_up_root },
+            { "zoom-in", zoom_in },
+            { "zoom-out", zoom_out }
+        };
+
+        construct {
+            action_group = new SimpleActionGroup ();
+            action_group.add_action_entries (action_entries, this);
+            insert_action_group ("chart", action_group);
+        }
+
+        public override void realize () {
+            Gtk.Allocation allocation;
+            get_allocation (out allocation);
+            set_realized (true);
+
+            Gdk.WindowAttr attributes = {};
+            attributes.window_type = Gdk.WindowType.CHILD;
+            attributes.x = allocation.x;
+            attributes.y = allocation.y;
+            attributes.width = allocation.width;
+            attributes.height = allocation.height;
+            attributes.wclass = Gdk.WindowWindowClass.INPUT_OUTPUT;
+            //attributes.visual = gtk_widget_get_visual (widget);
+            attributes.event_mask = this.get_events () | Gdk.EventMask.EXPOSURE_MASK | 
Gdk.EventMask.ENTER_NOTIFY_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK | Gdk.EventMask.BUTTON_PRESS_MASK | 
Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.POINTER_MOTION_HINT_MASK | Gdk.EventMask.SCROLL_MASK;
+
+            var window = new Gdk.Window (get_parent_window (), attributes, Gdk.WindowAttributesType.X | 
Gdk.WindowAttributesType.Y);
+
+            set_window (window);
+            window.set_user_data (this);
+
+            get_style_context ().set_background (window);
+        }
+
+        public override void size_allocate (Gtk.Allocation allocation) {
+            set_allocation (allocation);
+            if (get_realized ()) {
+                get_window ().move_resize (allocation.x,
+                                           allocation.y,
+                                           allocation.width,
+                                           allocation.height);
+
+                foreach (ChartItem item in items) {
+                    item.has_visible_children = false;
+                    item.visible = false;
+                    calculate_item_geometry (item);
+                }
+            }
+        }
+
+        public override bool motion_notify_event (Gdk.EventMotion event) {
+            bool found = false;
+
+            for (unowned List<ChartItem> node = items.last (); node != null; node = node.prev) {
+                var item = node.data;
+                if (item.visible && is_point_over_item (item, event.x, event.y)) {
+                    highlighted_item = item;
+                    has_tooltip = true;
+                    found = true;
+                    break;
+                }
+            }
+
+            if (!found) {
+                highlighted_item = null;
+                has_tooltip = false;
+            }
+
+            Gdk.Event.request_motions (event);
+
+            return false;
+        }
+
+        unowned List<ChartItem> add_item (uint depth, double rel_start, double rel_size, Gtk.TreeIter iter) {
+            string name;
+            uint64 size;
+            model.get (iter, name_column, out name, size_column, out size, -1);
+
+            var item = create_new_chartitem ();
+            item.name = name;
+            item.size = format_size (size);
+            item.depth = depth;
+            item.rel_start = rel_start;
+            item.rel_size = rel_size;
+            item.has_any_child = false;
+            item.visible = false;
+            item.has_visible_children = false;
+            item.iter = iter;
+            item.parent = null;
+
+            items.prepend (item);
+
+            unowned List<ChartItem> ret = items;
+            return ret;
+        }
+
+        void get_items (Gtk.TreePath root_path) {
+            unowned List<ChartItem> node = null;
+            Gtk.TreeIter initial_iter = {0};
+            double size;
+            Gtk.TreePath model_root_path;
+            Gtk.TreeIter model_root_iter;
+            Gtk.TreeIter child_iter = {0};
+            unowned List<ChartItem> child_node;
+            double rel_start;
+
+            items = null;
+
+            if (!model.get_iter (out initial_iter, root_path)) {
+                model_changed = false;
+                return;
+            }
+
+            model_root_path = new Gtk.TreePath.first ();
+            model.get_iter (out model_root_iter, model_root_path);
+            model.get (model_root_iter, percentage_column, out size, -1);
+
+            node = add_item (0, 0, 100, initial_iter);
+
+            do {
+                ChartItem item = node.data;
+                item.has_any_child = model.iter_children (out child_iter, item.iter);
+
+                calculate_item_geometry (item);
+
+                if (!item.visible) {
+                    node = node.prev;
+                    continue;
+                }
+
+                if ((item.has_any_child) && (item.depth < max_depth + 1)) {
+                    rel_start = 0;
+                    do {
+                        model.get (child_iter, percentage_column, out size, -1);
+                        child_node = add_item (item.depth + 1, rel_start, size, child_iter);
+                        var child = child_node.data;
+                        child.parent = node;
+                        rel_start += size;
+                    } while (model.iter_next (ref child_iter));
+                }
+
+                node = node.prev;
+            } while (node != null);
+
+            items.reverse ();
+
+            model_changed = false;
+        }
+
+        void draw_chart (Cairo.Context cr) {
+            cr.save ();
+
+            foreach (ChartItem item in items) {
+                Gdk.Rectangle clip;
+                if (Gdk.cairo_get_clip_rectangle (cr, out clip) &&
+                    item.visible &&
+                    clip.intersect (item.rect, null) &&
+                    (item.depth <= max_depth)) {
+                    bool highlighted = (item == highlighted_item);
+                    draw_item (cr, item, highlighted);
+                }
+            }
+
+            cr.restore ();
+
+            post_draw (cr);
+        }
+
+        void update_draw (Gtk.TreePath path) {
+            if (!get_realized ()) {
+                return;
+            }
+
+            var root_depth = root.get_depth ();
+            var node_depth = path.get_depth ();
+
+            if (((node_depth - root_depth) <= max_depth) &&
+                (root.is_ancestor (path) ||
+                 root.compare (path) == 0)) {
+                queue_draw ();
+            }
+        }
+
+        void row_changed (Gtk.TreeModel model,
+                          Gtk.TreePath path,
+                          Gtk.TreeIter iter) {
+            model_changed = true;
+            update_draw (path);
+        }
+
+        void row_inserted (Gtk.TreeModel model,
+                           Gtk.TreePath path,
+                           Gtk.TreeIter iter) {
+            model_changed = true;
+            update_draw (path);
+        }
+
+        void row_deleted (Gtk.TreeModel model,
+                          Gtk.TreePath path) {
+            model_changed = true;
+            update_draw (path);
+        }
+
+        void row_has_child_toggled (Gtk.TreeModel model,
+                                    Gtk.TreePath path,
+                                    Gtk.TreeIter iter) {
+            model_changed = true;
+            update_draw (path);
+        }
+
+        void rows_reordered (Gtk.TreeModel model,
+                             Gtk.TreePath path,
+                             Gtk.TreeIter? iter,
+                             void *new_order) {
+            model_changed = true;
+            update_draw (path);
+        }
+
+        public override bool draw (Cairo.Context cr) {
+            if (name_column == percentage_column) {
+                // Columns not set
+                return false;
+            }
+
+            if (model != null) {
+                cr.set_source_rgb (1, 1, 1);
+                cr.fill_preserve ();
+
+                if (model_changed || items == null) {
+                    get_items (root);
+                } else {
+                    var current_path = model.get_path (items.data.iter);
+                    if (root.compare (current_path) != 0) {
+                        get_items (root);
+                    }
+                }
+
+                draw_chart (cr);
+            }
+
+            return false;
+        }
+
+        Gdk.RGBA interpolate_colors (Gdk.RGBA colora, Gdk.RGBA colorb, double percentage) {
+            var color = Gdk.RGBA ();
+
+            double diff;
+
+            diff = colora.red - colorb.red;
+            color.red = colora.red - diff * percentage;
+            diff = colora.green - colorb.green;
+            color.green = colora.green - diff * percentage;
+            diff = colora.blue - colorb.blue;
+            color.blue = colora.blue - diff * percentage;
+
+            color.alpha = 1.0;
+
+            return color;
+        }
+
+        protected Gdk.RGBA get_item_color (double rel_position, uint depth, bool highlighted) {
+            const Gdk.RGBA level_color = {0.83, 0.84, 0.82, 1.0};
+            const Gdk.RGBA level_color_hi = {0.88, 0.89, 0.87, 1.0};
+
+
+            var color = Gdk.RGBA ();
+
+            double intensity = 1 - (((depth - 1) * 0.3) / MAX_DEPTH);
+
+            if (depth == 0) {
+                color = level_color;
+            } else {
+                int color_number = (int) (rel_position / (100.0/3));
+                int next_color_number = (color_number + 1) % 6;
+
+                color = interpolate_colors (TANGO_COLORS[color_number],
+                                            TANGO_COLORS[next_color_number],
+                                            (rel_position - color_number * 100/3) / (100/3));
+                color.red *= intensity;
+                color.green *= intensity;
+                color.blue *= intensity;
+            }
+
+            if (highlighted) {
+                if (depth == 0) {
+                    color = level_color_hi;
+                } else {
+                    double maximum = double.max (color.red, double.max (color.green, color.blue));
+                    color.red /= maximum;
+                    color.green /= maximum;
+                    color.blue /= maximum;
+                }
+            }
+
+            return color;
+        }
+
+        protected override bool button_press_event (Gdk.EventButton event) {
+            if (event.type == Gdk.EventType.BUTTON_PRESS) {
+                if (((Gdk.Event) (&event)).triggers_context_menu ()) {
+                    show_popup_menu (event);
+                    return true;
+                }
+
+                switch (event.button) {
+                case 1:
+                    if (highlighted_item != null) {
+                        var path = model.get_path (highlighted_item.iter);
+                        if (root.compare (path) == 0) {
+                            move_up_root ();
+                        } else {
+                            item_activated (highlighted_item.iter);
+                        }
+                    }
+                    break;
+                case 2:
+                    move_up_root ();
+                    break;
+                }
+
+                return true;
+            }
+
+            return false;
+        }
+
+        protected override bool scroll_event (Gdk.EventScroll event) {
+            Gdk.EventMotion *e = (Gdk.EventMotion *) (&event);
+            switch (event.direction) {
+            case Gdk.ScrollDirection.LEFT:
+            case Gdk.ScrollDirection.UP:
+                zoom_out ();
+                motion_notify_event (*e);
+                break;
+            case Gdk.ScrollDirection.RIGHT:
+            case Gdk.ScrollDirection.DOWN:
+                zoom_in ();
+                motion_notify_event (*e);
+                break;
+            case Gdk.ScrollDirection.SMOOTH:
+                break;
+            }
+
+            return false;
+        }
+
+        public void move_up_root () {
+            Gtk.TreeIter iter, parent_iter;
+
+            model.get_iter (out iter, root);
+            if (model.iter_parent (out parent_iter, iter)) {
+                root = model.get_path (parent_iter);
+                item_activated (parent_iter);
+            }
+        }
+
+        public void zoom_in () {
+            if (can_zoom_in ()) {
+                max_depth--;
+            }
+       }
+
+        public void zoom_out () {
+            if (can_zoom_out ()) {
+                max_depth++;
+            }
+        }
+
+        void ensure_context_menu () {
+            if (context_menu != null) {
+                return;
+            }
+
+            var builder = new Gtk.Builder ();
+            try {
+                builder.add_from_resource ("/org/gnome/baobab/ui/baobab-menu.ui");
+            } catch (Error e) {
+                error ("loading context menu from resources: %s", e.message);
+            }
+
+            var menu_model = builder.get_object ("chartmenu") as MenuModel;
+            context_menu = new Gtk.Menu.from_model (menu_model);
+            context_menu.attach_to_widget (this, null);
+        }
+
+        void show_popup_menu (Gdk.EventButton? event) {
+            ensure_context_menu ();
+
+            if (event != null) {
+                context_menu.popup (null, null, null, event.button, event.time);
+            } else {
+                context_menu.popup (null, null, null, 0, Gtk.get_current_event_time ());
+            }
+        }
+
+        void connect_model_signals (Gtk.TreeModel m) {
+            m.row_changed.connect (row_changed);
+            m.row_inserted.connect (row_inserted);
+            m.row_has_child_toggled.connect (row_has_child_toggled);
+            m.row_deleted.connect (row_deleted);
+            m.rows_reordered.connect (rows_reordered);
+        }
+
+        void disconnect_model_signals (Gtk.TreeModel m) {
+            m.row_changed.disconnect (row_changed);
+            m.row_inserted.disconnect (row_inserted);
+            m.row_has_child_toggled.disconnect (row_has_child_toggled);
+            m.row_deleted.disconnect (row_deleted);
+            m.rows_reordered.disconnect (rows_reordered);
+        }
+
+        protected override bool query_tooltip (int x, int y, bool keyboard_tooltip, Gtk.Tooltip tooltip) {
+            if (highlighted_item == null ||
+                highlighted_item.name == null ||
+                highlighted_item.size == null) {
+                return false;
+            }
+
+            tooltip.set_tip_area (highlighted_item.rect);
+
+            var markup = highlighted_item.name + "\n" + highlighted_item.size;
+            tooltip.set_markup (Markup.escape_text (markup));
+
+            return true;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/baobab-menu.ui b/src/baobab-menu.ui
index 75566d1..50647bb 100644
--- a/src/baobab-menu.ui
+++ b/src/baobab-menu.ui
@@ -83,4 +83,22 @@
       </section>
     </submenu>
   </menu>
+  <menu id="chartmenu">
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Move to parent folder</attribute>
+        <attribute name="action">chart.move-up</attribute>
+      </item>
+    </section>
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">Zoom _in</attribute>
+        <attribute name="action">chart.zoom-in</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">Zoom _out</attribute>
+        <attribute name="action">chart.zoom-out</attribute>
+      </item>
+    </section>
+  </menu>
 </interface>
diff --git a/src/baobab-ringschart.vala b/src/baobab-ringschart.vala
new file mode 100644
index 0000000..cd04447
--- /dev/null
+++ b/src/baobab-ringschart.vala
@@ -0,0 +1,399 @@
+/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* Baobab - disk usage analyzer
+ *
+ * Copyright (C) 2008  Igalia
+ * Copyright (C) 2013  Stefano Facchini <stefano facchini gmail com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA  02110-1301  USA
+ *
+ * Authors of the original code:
+ *   Felipe Erias <femorandeira igalia com>
+ *   Pablo Santamaria <psantamaria igalia com>
+ *   Jacobo Aragunde <jaragunde igalia com>
+ *   Eduardo Lima <elima igalia com>
+ *   Mario Sanchez <msanchez igalia com>
+ *   Miguel Gomez <magomez igalia com>
+ *   Henrique Ferreiro <hferreiro igalia com>
+ *   Alejandro Pinheiro <apinheiro igalia com>
+ *   Carlos Sanmartin <csanmartin igalia com>
+ *   Alejandro Garcia <alex igalia com>
+ */
+
+namespace Baobab {
+
+    class RingschartItem : ChartItem {
+        public double min_radius;
+        public double max_radius;
+        public double start_angle;
+        public double angle;
+        public bool continued;
+    }
+
+    public class Ringschart : Chart {
+
+        const int ITEM_BORDER_WIDTH = 1;
+        const int CHART_PADDING = 13;
+        const double ITEM_MIN_ANGLE = 0.03;
+        const double EDGE_ANGLE = 0.004;
+
+        const int SUBFOLDER_TIP_PADDING = 3;
+
+        int subtip_timeout;
+        uint tips_timeout_id = 0;
+        bool drawing_subtips = false;
+        List<ChartItem> subtip_items;
+
+        void subtips_update () {
+            if (drawing_subtips) {
+                queue_draw ();
+            }
+            drawing_subtips = false;
+
+            if (tips_timeout_id != 0) {
+                Source.remove (tips_timeout_id);
+                tips_timeout_id = 0;
+            }
+
+            subtip_items = null;
+
+            if (highlighted_item != null) {
+                tips_timeout_id = Timeout.add (subtip_timeout, () => {
+                    drawing_subtips = true;
+                    queue_draw ();
+                    return false;
+                });
+            }
+        }
+
+        construct {
+            subtip_timeout = Gtk.Settings.get_default ().gtk_tooltip_timeout * 2;
+
+            notify["max-depth"].connect (subtips_update);
+            notify["highlighted-item"].connect (subtips_update);
+        }
+
+        protected override ChartItem create_new_chartitem () {
+            return (new RingschartItem () as ChartItem);
+        }
+
+        protected override void post_draw (Cairo.Context cr) {
+            if (!drawing_subtips) {
+                return;
+            }
+
+            Gtk.Allocation allocation;
+            get_allocation (out allocation);
+
+            var q_width = allocation.width / 2;
+            var q_height = allocation.height / 2;
+            var q_angle = Math.atan2 (q_height, q_width);
+
+            Gdk.Rectangle last_rect = Gdk.Rectangle ();
+
+            foreach (ChartItem item in subtip_items) {
+                RingschartItem ringsitem = item as RingschartItem;
+
+                // get the middle angle
+                var middle_angle = ringsitem.start_angle + ringsitem.angle / 2;
+                // normalize the middle angle (take it to the first quadrant
+                var middle_angle_n = middle_angle;
+                while (middle_angle_n > Math.PI / 2) {
+                    middle_angle_n -= Math.PI;
+                }
+                middle_angle_n = Math.fabs (middle_angle_n);
+
+                // get the pango layout and its enclosing rectangle
+                var layout = create_pango_layout (null);
+                var markup = "<span size=\"small\">" + Markup.escape_text (item.name) + "</span>";
+                layout.set_markup (markup, -1);
+                layout.set_indent (0);
+                layout.set_spacing (0);
+                Pango.Rectangle layout_rect;
+                layout.get_pixel_extents (null, out layout_rect);
+
+                // get the center point of the tooltip rectangle
+                double tip_x, tip_y;
+                if (middle_angle_n < q_angle) {
+                    tip_x = q_width - layout_rect.width / 2 - SUBFOLDER_TIP_PADDING * 2;
+                    tip_y = Math.tan (middle_angle_n) * tip_x;
+                } else {
+                    tip_y = q_height - layout_rect.height / 2 - SUBFOLDER_TIP_PADDING * 2;
+                    tip_x = tip_y / Math.tan (middle_angle_n);
+                }
+
+                // get the tooltip rectangle
+                Cairo.Rectangle tooltip_rect = Cairo.Rectangle ();
+                tooltip_rect.x = q_width + tip_x - layout_rect.width / 2 - SUBFOLDER_TIP_PADDING;
+                tooltip_rect.y = q_height + tip_y - layout_rect.height / 2 - SUBFOLDER_TIP_PADDING;
+                tooltip_rect.width = layout_rect.width + SUBFOLDER_TIP_PADDING * 2;
+                tooltip_rect.height = layout_rect.height + SUBFOLDER_TIP_PADDING * 2;
+
+                // check tooltip's width is not greater than half of the widget
+                if (tooltip_rect.width > q_width) {
+                    continue;
+                }
+
+                // translate tooltip rectangle and edge angles to the original quadrant
+                var a = middle_angle;
+                int i = 0;
+                while (a > Math.PI / 2) {
+                    if (i % 2 == 0) {
+                        tooltip_rect.x = allocation.width - tooltip_rect.x - tooltip_rect.width;
+                    } else {
+                        tooltip_rect.y = allocation.height - tooltip_rect.y - tooltip_rect.height;
+                    }
+                    i++;
+                    a -= Math.PI / 2;
+                }
+
+                // get the Gdk.Rectangle of the tooltip (with a little padding)
+                Gdk.Rectangle _rect = Gdk.Rectangle ();
+                _rect.x = (int) (tooltip_rect.x - 1);
+                _rect.y = (int) (tooltip_rect.y - 1);
+                _rect.width = (int) (tooltip_rect.width + 2);
+                _rect.height = (int) (tooltip_rect.height + 2);
+
+                // check if tooltip overlaps
+                if (!_rect.intersect (last_rect, null)) {
+                    last_rect = _rect;
+
+                    // finally draw the tooltip!
+
+                    // TODO: do not hardcode colors
+
+                    // avoid blurred lines
+                    tooltip_rect.x = (int) Math.floor (tooltip_rect.x) + 0.5;
+                    tooltip_rect.y = (int) Math.floor (tooltip_rect.y) + 0.5;
+
+                    var middle_radius = ringsitem.min_radius + (ringsitem.max_radius - ringsitem.min_radius) 
/ 2;
+                    var sector_center_x = q_width + middle_radius * Math.cos (middle_angle);
+                    var sector_center_y = q_height + middle_radius * Math.sin (middle_angle);
+
+                    // draw line from sector center to tooltip center
+                    cr.set_line_width (1);
+                    cr.move_to (sector_center_x, sector_center_y);
+                    cr.set_source_rgb (0.8275, 0.8431, 0.8118); // tango: #d3d7cf
+                    cr.line_to (tooltip_rect.x + tooltip_rect.width / 2,
+                                tooltip_rect.y + tooltip_rect.height / 2);
+                    cr.stroke ();
+
+                    // draw a tiny circle in sector center
+                    cr.set_source_rgba (1.0, 1.0, 1.0, 0.6666);
+                    cr.arc (sector_center_x, sector_center_y, 1.0, 0, 2 * Math.PI);
+                    cr.stroke ();
+
+                    // draw tooltip box
+                    cr.set_line_width (0.5);
+                    cr.rectangle (tooltip_rect.x, tooltip_rect.y, tooltip_rect.width, tooltip_rect.height);
+                    cr.set_source_rgb (0.8275, 0.8431, 0.8118);  // tango: #d3d7cf
+                    cr.fill_preserve ();
+                    cr.set_source_rgb (0.5333, 0.5412, 0.5216);  // tango: #888a85
+                    cr.stroke ();
+
+                    // draw the text inside the box
+                    cr.set_source_rgb (0, 0, 0);
+                    cr.move_to (tooltip_rect.x + SUBFOLDER_TIP_PADDING, tooltip_rect.y + 
SUBFOLDER_TIP_PADDING);
+                    Pango.cairo_show_layout (cr, layout);
+                    cr.set_line_width (1);
+                    cr.stroke ();
+                }
+            }
+        }
+
+        void draw_sector (Cairo.Context cr,
+                          double center_x,
+                          double center_y,
+                          double radius,
+                          double thickness,
+                          double init_angle,
+                          double final_angle,
+                          Gdk.RGBA fill_color,
+                          bool continued,
+                          uint border) {
+            cr.set_line_width (border);
+            if (radius > 0) {
+                cr.arc (center_x, center_y, radius, init_angle, final_angle);
+            }
+            cr.arc_negative (center_x, center_y, radius + thickness, final_angle, init_angle);
+
+            cr.close_path ();
+
+            Gdk.cairo_set_source_rgba (cr, fill_color);
+            cr.fill_preserve ();
+            cr.set_source_rgb (0, 0, 0);
+            cr.stroke ();
+
+            if (continued) {
+                cr.set_line_width (3);
+                cr.arc (center_x, center_y, radius + thickness + 4, init_angle + EDGE_ANGLE, final_angle - 
EDGE_ANGLE);
+                cr.stroke ();
+            }
+        }
+
+        protected override void draw_item (Cairo.Context cr, ChartItem item, bool highlighted) {
+            RingschartItem ringsitem = item as RingschartItem;
+
+            if (drawing_subtips) {
+                if (highlighted_item != null &&
+                    item.parent != null &&
+                    item.parent.data == highlighted_item) {
+                    subtip_items.prepend (item);
+                }
+            }
+
+            var fill_color = get_item_color (ringsitem.start_angle / Math.PI * 99,
+                                             item.depth,
+                                             highlighted);
+
+            Gtk.Allocation allocation;
+            get_allocation (out allocation);
+
+            draw_sector (cr,
+                         allocation.width / 2,
+                         allocation.height / 2,
+                         ringsitem.min_radius,
+                         ringsitem.max_radius - ringsitem.min_radius,
+                         ringsitem.start_angle,
+                         ringsitem.start_angle + ringsitem.angle,
+                         fill_color,
+                         ringsitem.continued,
+                         ITEM_BORDER_WIDTH);
+        }
+
+        protected override void calculate_item_geometry (ChartItem item) {
+            RingschartItem ringsitem = item as RingschartItem;
+
+            ringsitem.continued = false;
+            ringsitem.visible = false;
+
+            Gtk.Allocation allocation;
+            get_allocation (out allocation);
+            var max_radius = int.min (allocation.width / 2, allocation.height / 2) - CHART_PADDING;
+            var thickness = max_radius / (max_depth + 1);
+
+            if (ringsitem.parent == null) {
+                ringsitem.min_radius = 0;
+                ringsitem.max_radius = thickness;
+                ringsitem.start_angle = 0;
+                ringsitem.angle = 2 * Math.PI;
+            } else {
+                var parent = item.parent.data as RingschartItem;
+                ringsitem.min_radius = ringsitem.depth * thickness;
+                if (ringsitem.depth > max_depth) {
+                    return;
+                } else {
+                    ringsitem.max_radius = ringsitem.min_radius + thickness;
+                }
+
+                ringsitem.angle = parent.angle * ringsitem.rel_size / 100;
+                if (ringsitem.angle < ITEM_MIN_ANGLE) {
+                    return;
+                }
+
+                ringsitem.start_angle = parent.start_angle + parent.angle * ringsitem.rel_start / 100;
+                ringsitem.continued = (ringsitem.has_any_child) && (ringsitem.depth == max_depth);
+                parent.has_visible_children = true;
+            }
+
+            ringsitem.visible = true;
+            get_item_rectangle (item);
+        }
+
+        void get_point_min_rect (double cx, double cy, double radius, double angle, ref Gdk.Rectangle r) {
+            double x, y;
+            x = cx + Math.cos (angle) * radius;
+            y = cy + Math.sin (angle) * radius;
+
+            r.x = int.min (r.x, (int)x);
+            r.y = int.min (r.y, (int)y);
+            r.width = int.max (r.width, (int)x);
+            r.height = int.max (r.height, (int)y);
+        }
+
+        protected override void get_item_rectangle (ChartItem item) {
+            RingschartItem ringsitem = item as RingschartItem;
+            Gdk.Rectangle rect = Gdk.Rectangle ();
+            double cx, cy, r1, r2, a1, a2;
+            Gtk.Allocation allocation;
+
+            get_allocation (out allocation);
+            cx = allocation.width / 2;
+            cy = allocation.height / 2;
+            r1 = ringsitem.min_radius;
+            r2 = ringsitem.max_radius;
+            a1 = ringsitem.start_angle;
+            a2 = ringsitem.start_angle + ringsitem.angle;
+
+            rect.x = allocation.width;
+            rect.y = allocation.height;
+            rect.width = 0;
+            rect.height = 0;
+
+            get_point_min_rect (cx, cy, r1, a1, ref rect);
+            get_point_min_rect (cx, cy, r2, a1, ref rect);
+            get_point_min_rect (cx, cy, r1, a2, ref rect);
+            get_point_min_rect (cx, cy, r2, a2, ref rect);
+
+            if ((a1 <= Math.PI / 2) && (a2 >= Math.PI / 2)) {
+                rect.height = (int) double.max (rect.height, cy + Math.sin (Math.PI / 2) * r2);
+            }
+
+            if ((a1 <= Math.PI) && (a2 >= Math.PI)) {
+                rect.x = (int) double.min (rect.x, cx + Math.cos (Math.PI) * r2);
+            }
+
+            if ((a1 <= Math.PI * 1.5) && (a2 >= Math.PI * 1.5)) {
+                rect.y = (int) double.min (rect.y, cy + Math.sin (Math.PI * 1.5) * r2);
+            }
+
+            if ((a1 <= Math.PI * 2) && (a2 >= Math.PI * 2)) {
+                rect.width = (int) double.max (rect.width, cx + Math.cos (Math.PI * 2) * r2);
+            }
+
+            rect.width -= rect.x;
+            rect.height -= rect.y;
+
+            ringsitem.rect = rect;
+        }
+
+        protected override bool is_point_over_item (ChartItem item, double x, double y) {
+            var ringsitem = item as RingschartItem;
+
+            Gtk.Allocation allocation;
+            get_allocation (out allocation);
+
+            x -= allocation.width / 2;
+            y -= allocation.height / 2;
+
+            var radius = Math.sqrt (x * x + y * y);
+            var angle = Math.atan2 (y, x);
+            angle = (angle > 0) ? angle : (angle + 2 * Math.PI);
+
+            return ((radius >= ringsitem.min_radius) &&
+                    (radius <= ringsitem.max_radius) &&
+                    (angle >= ringsitem.start_angle) &&
+                    (angle <= (ringsitem.start_angle + ringsitem.angle)));
+        }
+
+        protected override bool can_zoom_out () {
+            return (max_depth < MAX_DEPTH);
+        }
+
+        protected override bool can_zoom_in () {
+            return (max_depth > MIN_DEPTH);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/baobab-treemap.vala b/src/baobab-treemap.vala
new file mode 100644
index 0000000..a38140f
--- /dev/null
+++ b/src/baobab-treemap.vala
@@ -0,0 +1,200 @@
+/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* Baobab - disk usage analyzer
+ *
+ * Copyright (C) 2008  Igalia
+ * Copyright (C) 2013  Stefano Facchini <stefano facchini gmail com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA  02110-1301  USA
+ *
+ * Authors of the original code:
+ *   Fabio Marzocca  <thesaltydog gmail com>
+ *   Paolo Borelli <pborelli katamail com>
+ *   Miguel Gomez <magomez igalia com>
+ *   Eduardo Lima Mitev <elima igalia com>
+ */
+
+namespace Baobab {
+
+    class TreemapItem : ChartItem {
+        public Cairo.Rectangle cr_rect;
+    }
+
+    public class Treemap : Chart {
+
+        const int ITEM_BORDER_WIDTH = 1;
+        const int ITEM_PADDING = 6;
+        const int ITEM_TEXT_PADDING = 3;
+
+        const int ITEM_MIN_WIDTH = 3;
+        const int ITEM_MIN_HEIGHT = 3;
+
+        uint max_visible_depth;
+        bool more_visible_children;
+
+        protected override ChartItem create_new_chartitem () {
+            return (new TreemapItem () as ChartItem);
+        }
+
+        void draw_rectangle (Cairo.Context cr,
+                             double x,
+                             double y,
+                             double width,
+                             double height,
+                             Gdk.RGBA fill_color,
+                             string text,
+                             bool show_text) {
+            uint border = ITEM_BORDER_WIDTH;
+
+            cr.stroke ();
+
+            cr.set_line_width (border);
+            cr.rectangle (x + border, y + border, width - border * 2, height - border * 2);
+            Gdk.cairo_set_source_rgba (cr, fill_color);
+            cr.fill_preserve ();
+            cr.set_source_rgb (0, 0, 0);
+            cr.stroke ();
+
+            if (show_text) {
+                var layout = create_pango_layout (null);
+                var markup = Markup.escape_text (text);
+                layout.set_markup (markup, -1);
+
+                Pango.Rectangle rect;
+                layout.get_pixel_extents (null, out rect);
+
+                if ((rect.width + ITEM_TEXT_PADDING * 2 <= width) &&
+                    (rect.height + ITEM_TEXT_PADDING * 2 <= height)) {
+                    cr.move_to (x + width / 2 - rect.width / 2, y + height / 2 - rect.height / 2);
+                    Pango.cairo_show_layout (cr, layout);
+                }
+            }
+        }
+
+        protected override void draw_item (Cairo.Context cr, ChartItem item, bool highlighted) {
+            Cairo.Rectangle rect;
+            Gdk.RGBA fill_color = {0};
+            Gtk.Allocation allocation;
+            double width = 0, height = 0;
+
+            rect = (item as TreemapItem).cr_rect;
+            get_allocation (out allocation);
+
+            if ((item.depth % 2) != 0) {
+                fill_color = get_item_color (rect.x / allocation.width * 200,
+                                             item.depth, highlighted);
+                width = rect.width - ITEM_PADDING;
+                height = rect.height;
+            } else {
+                fill_color = get_item_color (rect.y / allocation.height * 200,
+                                             item.depth, highlighted);
+                width = rect.width;
+                height = rect.height - ITEM_PADDING;
+            }
+
+            draw_rectangle (cr, rect.x, rect.y, width, height, fill_color, item.name, 
(!item.has_visible_children));
+        }
+
+        protected override void calculate_item_geometry (ChartItem item) {
+            TreemapItem treemapitem = item as TreemapItem;
+            Cairo.Rectangle p_area = Cairo.Rectangle ();
+
+            if (item.depth == 0) {
+                max_visible_depth = 0;
+                more_visible_children = false;
+            }
+
+            item.visible = false;
+            if (item.parent == null) {
+                Gtk.Allocation allocation;
+                get_allocation (out allocation);
+                p_area.x = -ITEM_PADDING / 2;
+                p_area.y = -ITEM_PADDING / 2;
+                p_area.width = allocation.width + ITEM_PADDING * 2;
+                p_area.height = allocation.height + ITEM_PADDING * 2;
+            } else {
+                p_area = (item.parent.data as TreemapItem).cr_rect;
+            }
+
+            if (item.depth % 2 != 0) {
+                var width = p_area.width - ITEM_PADDING;
+
+                treemapitem.cr_rect.x = p_area.x + (item.rel_start * width / 100) + ITEM_PADDING;
+                treemapitem.cr_rect.y = p_area.y + ITEM_PADDING;
+                treemapitem.cr_rect.width = width * item.rel_size / 100;
+                treemapitem.cr_rect.height = p_area.height - ITEM_PADDING * 3;
+            } else {
+                var height = p_area.height - ITEM_PADDING;
+
+                treemapitem.cr_rect.x = p_area.x + ITEM_PADDING;
+                treemapitem.cr_rect.y = p_area.y + (item.rel_start * height / 100) + ITEM_PADDING;
+                treemapitem.cr_rect.width = p_area.width - ITEM_PADDING * 3;
+                treemapitem.cr_rect.height = height * item.rel_size / 100;
+            }
+            if ((treemapitem.cr_rect.width - ITEM_PADDING < ITEM_MIN_WIDTH) ||
+                (treemapitem.cr_rect.height - ITEM_PADDING < ITEM_MIN_HEIGHT)) {
+                return;
+            }
+
+            treemapitem.cr_rect.x = Math.floor (treemapitem.cr_rect.x) + 0.5;
+            treemapitem.cr_rect.y = Math.floor (treemapitem.cr_rect.y) + 0.5;
+            treemapitem.cr_rect.width = Math.floor (treemapitem.cr_rect.width);
+            treemapitem.cr_rect.height = Math.floor (treemapitem.cr_rect.height);
+
+            item.visible = true;
+
+            if (item.parent != null) {
+                item.parent.data.has_visible_children = true;
+            }
+
+            get_item_rectangle (item);
+
+            if (item.depth == max_depth + 1) {
+                more_visible_children = true;
+            } else {
+                max_visible_depth = uint.max (max_visible_depth, item.depth);
+            }
+        }
+
+        protected override void get_item_rectangle (ChartItem item) {
+            var crect = (item as TreemapItem).cr_rect;
+
+            item.rect.x = (int) crect.x;
+            item.rect.y = (int) crect.y;
+
+            if (item.depth % 2 != 0) {
+                item.rect.width = (int) crect.width - ITEM_PADDING;
+                item.rect.height = (int) crect.height;
+            } else {
+                item.rect.width = (int) crect.width;
+                item.rect.height = (int) crect.height - ITEM_PADDING;
+            }
+        }
+
+        protected override bool is_point_over_item (ChartItem item, double x, double y) {
+            var rect = item.rect;
+            return ((x >= rect.x) && (x <= (rect.x + rect.width)) &&
+                    (y >= rect.y) && (y <= (rect.y + rect.height)));
+        }
+
+        protected override bool can_zoom_out () {
+            return more_visible_children;
+        }
+
+        protected override bool can_zoom_in () {
+            return (max_visible_depth > 1);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/baobab-window.vala b/src/baobab-window.vala
index bd42a18..5ba87b4 100644
--- a/src/baobab-window.vala
+++ b/src/baobab-window.vala
@@ -453,8 +453,8 @@ namespace Baobab {
                 Gtk.TreeIter iter;
                 if (selection.get_selected (null, out iter)) {
                     var path = active_location.scanner.get_path (iter);
-                    rings_chart.set_root (path);
-                    treemap_chart.set_root (path);
+                    rings_chart.root = path;
+                    treemap_chart.root = path;
                 }
             });
         }
@@ -476,15 +476,11 @@ namespace Baobab {
             if (busy) {
                 cursor = busy_cursor;
                 disable_drop ();
-                rings_chart.freeze_updates ();
-                treemap_chart.freeze_updates ();
                 (lookup_action ("active-chart") as SimpleAction).set_enabled (false);
                 chart_stack.visible_child = spinner;
                 spinner.start ();
             } else {
                 enable_drop ();
-                rings_chart.thaw_updates ();
-                treemap_chart.thaw_updates ();
                 (lookup_action ("active-chart") as SimpleAction).set_enabled (true);
                 spinner.stop ();
                 lookup_action ("active-chart").change_state (ui_settings.get_value ("active-chart"));


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]