[gimp] app: various GimpDashboard improvements



commit ee4181916573ce71c2747c64957352e3e219c90e
Author: Ell <ell_se yahoo com>
Date:   Sat Dec 30 13:43:09 2017 -0500

    app: various GimpDashboard improvements
    
    Refactor GimpDashboard to autogenerate the UI based on a
    description of the different variables, fields, and groups.
    
    Allow individual groups to be expanded/collapsed, and individual
    fields to be enabled/disabled.  Save the relevant state in the
    dashboard's aux-info.
    
    Add fields for the new GeglStats properties, as per GEGL commit
    25c39ce6c9bb618f06ac96d118e624be66464d74.  The new fields are not
    enabled by default.
    
    Add "reset" action, to clear the history, and reset cumulative
    data.

 app/actions/dashboard-actions.c  |   14 +-
 app/actions/dashboard-commands.c |    9 +
 app/actions/dashboard-commands.h |    3 +
 app/widgets/gimpdashboard.c      | 2040 ++++++++++++++++++++++++++++++--------
 app/widgets/gimpdashboard.h      |   53 +-
 app/widgets/gimphelp-ids.h       |    1 +
 menus/dashboard-menu.xml         |    1 +
 7 files changed, 1652 insertions(+), 469 deletions(-)
---
diff --git a/app/actions/dashboard-actions.c b/app/actions/dashboard-actions.c
index 6acf18c..9ed2f4b 100644
--- a/app/actions/dashboard-actions.c
+++ b/app/actions/dashboard-actions.c
@@ -43,7 +43,13 @@ static const GimpActionEntry dashboard_actions[] =
   { "dashboard-update-interval", NULL,
     NC_("dashboard-action", "Update Interval") },
   { "dashboard-history-duration", NULL,
-    NC_("dashboard-action", "History Duration") }
+    NC_("dashboard-action", "History Duration") },
+
+  { "dashboard-reset", GIMP_ICON_RESET,
+    NC_("dashboard-action", "Reset"), NULL,
+    NC_("dashboard-action", "Reset cumulative data"),
+    G_CALLBACK (dashboard_reset_cmd_callback),
+    GIMP_HELP_DASHBOARD_RESET },
 };
 
 static const GimpToggleActionEntry dashboard_toggle_actions[] =
@@ -149,7 +155,7 @@ dashboard_actions_update (GimpActionGroup *group,
 #define SET_ACTIVE(action,condition) \
         gimp_action_group_set_action_active (group, action, (condition) != 0)
 
-  switch (dashboard->update_interval)
+  switch (gimp_dashboard_get_update_interval (dashboard))
     {
     case GIMP_DASHBOARD_UPDATE_INTERVAL_0_25_SEC:
       SET_ACTIVE ("dashboard-update-interval-0-25-sec", TRUE);
@@ -168,7 +174,7 @@ dashboard_actions_update (GimpActionGroup *group,
       break;
     }
 
-  switch (dashboard->history_duration)
+  switch (gimp_dashboard_get_history_duration (dashboard))
     {
     case GIMP_DASHBOARD_HISTORY_DURATION_15_SEC:
       SET_ACTIVE ("dashboard-history-duration-15-sec", TRUE);
@@ -188,7 +194,7 @@ dashboard_actions_update (GimpActionGroup *group,
     }
 
   SET_ACTIVE ("dashboard-low-swap-space-warning",
-              dashboard->low_swap_space_warning);
+              gimp_dashboard_get_low_swap_space_warning (dashboard));
 
 #undef SET_ACTIVE
 }
diff --git a/app/actions/dashboard-commands.c b/app/actions/dashboard-commands.c
index c7b110b..2259490 100644
--- a/app/actions/dashboard-commands.c
+++ b/app/actions/dashboard-commands.c
@@ -62,6 +62,15 @@ dashboard_history_duration_cmd_callback (GtkAction *action,
 }
 
 void
+dashboard_reset_cmd_callback (GtkAction *action,
+                              gpointer   data)
+{
+  GimpDashboard *dashboard = GIMP_DASHBOARD (data);
+
+  gimp_dashboard_reset (dashboard);
+}
+
+void
 dashboard_low_swap_space_warning_cmd_callback (GtkAction *action,
                                                gpointer   data)
 {
diff --git a/app/actions/dashboard-commands.h b/app/actions/dashboard-commands.h
index 4314a14..2151238 100644
--- a/app/actions/dashboard-commands.h
+++ b/app/actions/dashboard-commands.h
@@ -26,6 +26,9 @@ void   dashboard_history_duration_cmd_callback       (GtkAction *action,
                                                       GtkAction *current,
                                                       gpointer   data);
 
+void   dashboard_reset_cmd_callback                  (GtkAction *action,
+                                                      gpointer   data);
+
 void   dashboard_low_swap_space_warning_cmd_callback (GtkAction *action,
                                                       gpointer   data);
 
diff --git a/app/widgets/gimpdashboard.c b/app/widgets/gimpdashboard.c
index 37793f9..7f085a3 100644
--- a/app/widgets/gimpdashboard.c
+++ b/app/widgets/gimpdashboard.c
@@ -37,8 +37,10 @@
 #include "gimpdocked.h"
 #include "gimpdashboard.h"
 #include "gimpdialogfactory.h"
+#include "gimphelp-ids.h"
 #include "gimpmeter.h"
 #include "gimpsessioninfo-aux.h"
+#include "gimpwidgets-utils.h"
 #include "gimpwindowstrategy.h"
 
 #include "gimp-intl.h"
@@ -51,35 +53,384 @@
 #define LOW_SWAP_SPACE_WARNING_ON      /* swap occupied is above */ 0.90 /* of swap limit */
 #define LOW_SWAP_SPACE_WARNING_OFF     /* swap occupied is below */ 0.85 /* of swap limit */
 
-#define CACHE_OCCUPIED_COLOR           {0.3, 0.6, 0.3, 1.0}
-#define SWAP_OCCUPIED_COLOR            {0.8, 0.2, 0.2, 1.0}
-#define SWAP_SIZE_COLOR                {0.8, 0.6, 0.4, 1.0}
-#define SWAP_LED_COLOR                 {0.8, 0.4, 0.4, 1.0}
+
+typedef enum
+{
+  VARIABLE_NONE,
+  FIRST_VARIABLE,
+
+
+  /* cache */
+  VARIABLE_CACHE_OCCUPIED = FIRST_VARIABLE,
+  VARIABLE_CACHE_MAXIMUM,
+  VARIABLE_CACHE_LIMIT,
+
+  VARIABLE_CACHE_COMPRESSION,
+  VARIABLE_CACHE_HIT_MISS,
+
+  /* swap */
+  VARIABLE_SWAP_OCCUPIED,
+  VARIABLE_SWAP_SIZE,
+  VARIABLE_SWAP_LIMIT,
+
+  VARIABLE_SWAP_BUSY,
+
+
+  N_VARIABLES,
+
+  VARIABLE_SEPARATOR
+} Variable;
+
+typedef enum
+{
+  VARIABLE_TYPE_BOOLEAN,
+  VARIABLE_TYPE_SIZE,
+  VARIABLE_TYPE_SIZE_RATIO,
+  VARIABLE_TYPE_INT_RATIO
+} VariableType;
+
+typedef enum
+{
+  FIRST_GROUP,
+
+  GROUP_CACHE = FIRST_GROUP,
+  GROUP_SWAP,
+
+  N_GROUPS
+} Group;
+
+
+typedef struct _VariableInfo  VariableInfo;
+typedef struct _FieldInfo     FieldInfo;
+typedef struct _GroupInfo     GroupInfo;
+typedef struct _VariableData  VariableData;
+typedef struct _FieldData     FieldData;
+typedef struct _GroupData     GroupData;
+
+typedef void (* VariableSampleFunc) (GimpDashboard *dashboard,
+                                     Variable       variable);
+
+
+struct _VariableInfo
+{
+  const gchar        *name;
+  const gchar        *title;
+  const gchar        *description;
+  VariableType        type;
+  GimpRGB             color;
+  VariableSampleFunc  sample_func;
+  gpointer            data;
+};
+
+struct _FieldInfo
+{
+  Variable variable;
+  gboolean default_active;
+  gboolean meter_value;
+};
+
+struct _GroupInfo
+{
+  const gchar     *name;
+  const gchar     *title;
+  const gchar     *description;
+  gboolean         default_expanded;
+  gboolean         has_meter;
+  Variable         meter_limit;
+  Variable         meter_led;
+  const FieldInfo *fields;
+};
+
+struct _VariableData
+{
+  gboolean available;
+
+  union
+  {
+    gboolean  boolean;
+    guint64   size;
+    struct
+    {
+      guint64 antecedent;
+      guint64 consequent;
+    } size_ratio;
+    struct
+    {
+      gint    antecedent;
+      gint    consequent;
+    } int_ratio;
+  } value;
+};
+
+struct _FieldData
+{
+  gboolean   active;
+
+  GtkCheckMenuItem *menu_item;
+  GtkLabel         *value_label;
+};
+
+struct _GroupData
+{
+  gint         n_fields;
+  gint         n_meter_values;
+
+  GtkExpander *expander;
+  GtkButton   *menu_button;
+  GtkMenu     *menu;
+  GimpMeter   *meter;
+  GtkTable    *table;
+
+  FieldData   *fields;
+};
+
+struct _GimpDashboardPrivate
+{
+  Gimp                         *gimp;
+
+  VariableData                  variables[N_VARIABLES];
+  GroupData                     groups[N_GROUPS];
+
+  GThread                      *thread;
+  GMutex                        mutex;
+  GCond                         cond;
+  gboolean                      quit;
+  gboolean                      update_now;
+
+  gint                          update_idle_id;
+  gint                          low_swap_space_idle_id;
+
+  GimpDashboardUpdateInteval    update_interval;
+  GimpDashboardHistoryDuration  history_duration;
+  gboolean                      low_swap_space_warning;
+};
 
 
-static void       gimp_dashboard_docked_iface_init (GimpDockedInterface *iface);
+/*  local function prototypes  */
 
-static void       gimp_dashboard_dispose           (GObject             *object);
-static void       gimp_dashboard_finalize          (GObject             *object);
+static void       gimp_dashboard_docked_iface_init           (GimpDockedInterface *iface);
 
-static void       gimp_dashboard_map               (GtkWidget           *widget);
-static void       gimp_dashboard_unmap             (GtkWidget           *widget);
+static void       gimp_dashboard_constructed                 (GObject             *object);
+static void       gimp_dashboard_dispose                     (GObject             *object);
+static void       gimp_dashboard_finalize                    (GObject             *object);
 
-static void       gimp_dashboard_set_aux_info      (GimpDocked          *docked,
-                                                    GList               *aux_info);
-static GList    * gimp_dashboard_get_aux_info      (GimpDocked          *docked);
+static void       gimp_dashboard_map                         (GtkWidget           *widget);
+static void       gimp_dashboard_unmap                       (GtkWidget           *widget);
 
-static gboolean   gimp_dashboard_update            (GimpDashboard       *dashboard);
-static gboolean   gimp_dashboard_low_swap_space    (GimpDashboard       *dashboard);
-static gpointer   gimp_dashboard_sample            (GimpDashboard       *dashboard);
+static void       gimp_dashboard_set_aux_info                (GimpDocked          *docked,
+                                                              GList               *aux_info);
+static GList    * gimp_dashboard_get_aux_info                (GimpDocked          *docked);
 
-static void       gimp_dashboard_label_set_text    (GtkLabel            *label,
-                                                    const gchar         *text);
-static void       gimp_dashboard_label_set_size    (GtkLabel            *label,
-                                                    guint64              size,
-                                                    guint64              limit);
+static gboolean   gimp_dashboard_group_expander_button_press (GimpDashboard       *dashboard,
+                                                              GdkEventButton      *bevent,
+                                                              GtkWidget           *widget);
 
-static guint64    gimp_dashboard_get_swap_limit    (guint64              swap_size);
+static void       gimp_dashboard_group_menu_item_toggled     (GimpDashboard       *dashboard,
+                                                              GtkCheckMenuItem    *item);
+
+static gpointer   gimp_dashboard_sample                      (GimpDashboard       *dashboard);
+
+static gboolean   gimp_dashboard_update                      (GimpDashboard       *dashboard);
+static gboolean   gimp_dashboard_low_swap_space              (GimpDashboard       *dashboard);
+
+static void       gimp_dashboard_sample_gegl_config          (GimpDashboard       *dashboard,
+                                                              Variable             variable);
+static void       gimp_dashboard_sample_gegl_stats           (GimpDashboard       *dashboard,
+                                                              Variable             variable);
+static void       gimp_dashboard_sample_swap_limit           (GimpDashboard       *dashboard,
+                                                              Variable             variable);
+
+static void       gimp_dashboard_sample_object               (GimpDashboard       *dashboard,
+                                                              GObject             *object,
+                                                              Variable             variable);
+
+static void       gimp_dashboard_container_remove            (GtkWidget           *widget,
+                                                              GtkContainer        *container);
+
+static void       gimp_dashboard_group_menu_position         (GtkMenu             *menu,
+                                                              gint                *x,
+                                                              gint                *y,
+                                                              gboolean            *push_in,
+                                                              gpointer             user_data);
+
+static void       gimp_dashboard_update_groups               (GimpDashboard       *dashboard);
+static void       gimp_dashboard_update_group                (GimpDashboard       *dashboard,
+                                                              Group                group);
+static void       gimp_dashboard_update_group_values         (GimpDashboard       *dashboard,
+                                                              Group                group);
+
+static void       gimp_dashboard_field_set_active            (GimpDashboard       *dashboard,
+                                                              Group                group,
+                                                              gint                 field,
+                                                              gboolean             active);
+
+static gboolean   gimp_dashboard_variable_to_boolean         (GimpDashboard       *dashboard,
+                                                              Variable             variable);
+static gdouble    gimp_dashboard_variable_to_double          (GimpDashboard       *dashboard,
+                                                              Variable             variable);
+
+
+/*  static variables  */
+
+static const VariableInfo variables[] =
+{
+  /* cache variables */
+
+  [VARIABLE_CACHE_OCCUPIED] =
+  { .name             = "cache-occupied",
+    .title            = NC_("dashboard-variable", "Occupied"),
+    .description      = N_("Tile cache occupied size"),
+    .type             = VARIABLE_TYPE_SIZE,
+    .color            = {0.3, 0.6, 0.3, 1.0},
+    .sample_func      = gimp_dashboard_sample_gegl_stats,
+    .data             = "tile-cache-total"
+  },
+
+  [VARIABLE_CACHE_MAXIMUM] =
+  { .name             = "cache-maximum",
+    .title            = NC_("dashboard-variable", "Maximum"),
+    .description      = N_("Maximal tile cache occupied size during this session"),
+    .type             = VARIABLE_TYPE_SIZE,
+    .color            = {0.3, 0.7, 0.8, 1.0},
+    .sample_func      = gimp_dashboard_sample_gegl_stats,
+    .data             = "tile-cache-total-max"
+  },
+
+  [VARIABLE_CACHE_LIMIT] =
+  { .name             = "cache-limit",
+    .title            = NC_("dashboard-variable", "Limit"),
+    .description      = N_("Tile cache size limit"),
+    .type             = VARIABLE_TYPE_SIZE,
+    .sample_func      = gimp_dashboard_sample_gegl_config,
+    .data             = "tile-cache-size"
+  },
+
+  [VARIABLE_CACHE_COMPRESSION] =
+  { .name             = "cache-compression",
+    .title            = NC_("dashboard-variable", "Compression"),
+    .description      = N_("Tile cache compression ratio"),
+    .type             = VARIABLE_TYPE_SIZE_RATIO,
+    .sample_func      = gimp_dashboard_sample_gegl_stats,
+    .data             = "tile-cache-total\0"
+                        "tile-cache-total-uncloned"
+  },
+
+  [VARIABLE_CACHE_HIT_MISS] =
+  { .name             = "cache-hit-miss",
+    .title            = NC_("dashboard-variable", "Hit/Miss"),
+    .description      = N_("Tile cache hit/miss ratio"),
+    .type             = VARIABLE_TYPE_INT_RATIO,
+    .sample_func      = gimp_dashboard_sample_gegl_stats,
+    .data             = "tile-cache-hits\0"
+                        "tile-cache-misses"
+  },
+
+
+  /* swap variables */
+
+  [VARIABLE_SWAP_OCCUPIED] =
+  { .name             = "swap-occupied",
+    .title            = NC_("dashboard-variable", "Occupied"),
+    .description      = N_("Swap file occupied size"),
+    .type             = VARIABLE_TYPE_SIZE,
+    .color            = {0.8, 0.2, 0.2, 1.0},
+    .sample_func      = gimp_dashboard_sample_gegl_stats,
+    .data             = "swap-total"
+  },
+
+  [VARIABLE_SWAP_SIZE] =
+  { .name             = "swap-size",
+    .title            = NC_("dashboard-variable", "Size"),
+    .description      = N_("Swap file size"),
+    .type             = VARIABLE_TYPE_SIZE,
+    .color            = {0.8, 0.6, 0.4, 1.0},
+    .sample_func      = gimp_dashboard_sample_gegl_stats,
+    .data             = "swap-file-size"
+  },
+
+  [VARIABLE_SWAP_LIMIT] =
+  { .name             = "swap-limit",
+    .title            = NC_("dashboard-variable", "Limit"),
+    .description      = N_("Swap file size limit"),
+    .type             = VARIABLE_TYPE_SIZE,
+    .sample_func      = gimp_dashboard_sample_swap_limit,
+  },
+
+  [VARIABLE_SWAP_BUSY] =
+  { .name             = "swap-busy",
+    .title            = NC_("dashboard-variable", "Busy"),
+    .description      = N_("Whether there is work queued for the swap file"),
+    .type             = VARIABLE_TYPE_BOOLEAN,
+    .color            = {0.8, 0.4, 0.4, 1.0},
+    .sample_func      = gimp_dashboard_sample_gegl_stats,
+    .data             = "swap-busy"
+  }
+};
+
+static const GroupInfo groups[] =
+{
+  /* cache group */
+  [GROUP_CACHE] =
+  { .name             = "cache",
+    .title            = NC_("dashboard-group", "Cache"),
+    .description      = N_("In-memory tile cache"),
+    .default_expanded = TRUE,
+    .has_meter        = TRUE,
+    .meter_limit      = VARIABLE_CACHE_LIMIT,
+    .fields           = (const FieldInfo[])
+                        {
+                          { .variable       = VARIABLE_CACHE_OCCUPIED,
+                            .default_active = TRUE,
+                            .meter_value    = 2
+                          },
+                          { .variable       = VARIABLE_CACHE_MAXIMUM,
+                            .default_active = FALSE,
+                            .meter_value    = 1
+                          },
+                          { .variable       = VARIABLE_CACHE_LIMIT,
+                            .default_active = TRUE
+                          },
+
+                          { VARIABLE_SEPARATOR },
+
+                          { .variable       = VARIABLE_CACHE_COMPRESSION,
+                            .default_active = FALSE
+                          },
+                          { .variable       = VARIABLE_CACHE_HIT_MISS,
+                            .default_active = FALSE
+                          },
+
+                          {}
+                        }
+  },
+
+  /* swap group */
+  [GROUP_SWAP] =
+  { .name             = "swap",
+    .title            = NC_("dashboard-group", "Swap"),
+    .description      = N_("On-disk tile swap"),
+    .default_expanded = TRUE,
+    .has_meter        = TRUE,
+    .meter_limit      = VARIABLE_SWAP_LIMIT,
+    .meter_led        = VARIABLE_SWAP_BUSY,
+    .fields           = (const FieldInfo[])
+                        {
+                          { .variable       = VARIABLE_SWAP_OCCUPIED,
+                            .default_active = TRUE,
+                            .meter_value    = 2
+                          },
+                          { .variable       = VARIABLE_SWAP_SIZE,
+                            .default_active = TRUE,
+                            .meter_value    = 1
+                          },
+                          { .variable       = VARIABLE_SWAP_LIMIT,
+                            .default_active = TRUE
+                          },
+
+                          {}
+                        }
+  }
+};
 
 
 G_DEFINE_TYPE_WITH_CODE (GimpDashboard, gimp_dashboard, GIMP_TYPE_EDITOR,
@@ -100,27 +451,47 @@ gimp_dashboard_class_init (GimpDashboardClass *klass)
   GObjectClass   *object_class = G_OBJECT_CLASS (klass);
   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
 
-  object_class->dispose  = gimp_dashboard_dispose;
-  object_class->finalize = gimp_dashboard_finalize;
+  object_class->constructed = gimp_dashboard_constructed;
+  object_class->dispose     = gimp_dashboard_dispose;
+  object_class->finalize    = gimp_dashboard_finalize;
 
-  widget_class->map      = gimp_dashboard_map;
-  widget_class->unmap    = gimp_dashboard_unmap;
+  widget_class->map         = gimp_dashboard_map;
+  widget_class->unmap       = gimp_dashboard_unmap;
+
+  g_type_class_add_private (klass, sizeof (GimpDashboardPrivate));
 }
 
 static void
 gimp_dashboard_init (GimpDashboard *dashboard)
 {
-  GtkWidget *box;
-  GtkWidget *vbox;
-  GtkWidget *frame;
-  GtkWidget *table;
-  GtkWidget *meter;
-  GtkWidget *label;
-  gint       content_spacing;
-
-  dashboard->update_interval        = DEFAULT_UPDATE_INTERVAL;
-  dashboard->history_duration       = DEFAULT_HISTORY_DURATION;
-  dashboard->low_swap_space_warning = DEFAULT_LOW_SWAP_SPACE_WARNING;
+  GimpDashboardPrivate *priv;
+  GtkWidget            *box;
+  GtkWidget            *vbox;
+  GtkWidget            *expander;
+  GtkWidget            *hbox;
+  GtkWidget            *button;
+  GtkWidget            *image;
+  GtkWidget            *menu;
+  GtkWidget            *item;
+  GtkWidget            *frame;
+  GtkWidget            *vbox2;
+  GtkWidget            *meter;
+  GtkWidget            *table;
+  GtkWidget            *label;
+  gint                  content_spacing;
+  Group                 group;
+  gint                  field;
+
+  priv = dashboard->priv = G_TYPE_INSTANCE_GET_PRIVATE (dashboard,
+                                                        GIMP_TYPE_DASHBOARD,
+                                                        GimpDashboardPrivate);
+
+  g_mutex_init (&priv->mutex);
+  g_cond_init (&priv->cond);
+
+  priv->update_interval        = DEFAULT_UPDATE_INTERVAL;
+  priv->history_duration       = DEFAULT_HISTORY_DURATION;
+  priv->low_swap_space_warning = DEFAULT_LOW_SWAP_SPACE_WARNING;
 
   gtk_widget_style_get (GTK_WIDGET (dashboard),
                         "content-spacing", &content_spacing,
@@ -140,166 +511,183 @@ gimp_dashboard_init (GimpDashboard *dashboard)
   gtk_container_add (GTK_CONTAINER (box), vbox);
   gtk_widget_show (vbox);
 
+  /* construct the groups */
+  for (group = FIRST_GROUP; group < N_GROUPS; group++)
+    {
+      const GroupInfo *group_info = &groups[group];
+      GroupData       *group_data = &priv->groups[group];
 
-  /* cache frame */
-  frame = gimp_frame_new (NULL);
-  gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_NONE);
-  gtk_box_pack_start (GTK_BOX (vbox), frame, FALSE, FALSE, 0);
-  gtk_widget_show (frame);
+      group_data->n_fields       = 0;
+      group_data->n_meter_values = 0;
 
-  /* cache frame label */
-  box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
-  gtk_frame_set_label_widget (GTK_FRAME (frame), box);
-  gtk_widget_show (box);
+      for (field = 0; group_info->fields[field].variable; field++)
+        {
+          const FieldInfo *field_info = &group_info->fields[field];
 
-  label = gtk_label_new (_("Cache"));
-  gtk_label_set_xalign (GTK_LABEL (label), 0.0);
-  gimp_label_set_attributes (GTK_LABEL (label),
-                             PANGO_ATTR_WEIGHT, PANGO_WEIGHT_BOLD,
-                             -1);
-  gtk_box_pack_start (GTK_BOX (box), label, FALSE, FALSE, 0);
-  gtk_widget_show (label);
-
-  /* cache table */
-  table = gtk_table_new (3, 2, FALSE);
-  gtk_table_set_row_spacings (GTK_TABLE (table), content_spacing);
-  gtk_table_set_col_spacings (GTK_TABLE (table), 4);
-  gtk_container_add (GTK_CONTAINER (frame), table);
-  gtk_widget_show (table);
-
-  /* cache meter */
-  meter = dashboard->cache_meter = gimp_meter_new (1);
-  gimp_meter_set_color (GIMP_METER (meter),
-                        0, &(GimpRGB) CACHE_OCCUPIED_COLOR);
-  gimp_meter_set_history_resolution (GIMP_METER (meter),
-                                     dashboard->update_interval / 1000.0);
-  gimp_meter_set_history_duration (GIMP_METER (meter),
-                                   dashboard->history_duration / 1000.0);
-  gtk_table_attach (GTK_TABLE (table), meter, 0, 2, 0, 1,
-                    GTK_EXPAND | GTK_FILL, 0, 1, 0);
-  gtk_table_set_row_spacing (GTK_TABLE (table), 0, 2 * content_spacing);
-  gtk_widget_show (meter);
-
-  /* cache occupied field */
-  label = gtk_label_new (_("Occupied:"));
-  gtk_label_set_xalign (GTK_LABEL (label), 0.0);
-  gtk_table_attach (GTK_TABLE (table), label, 0, 1, 1, 2,
-                    GTK_FILL, 0, 0, 0);
-  gtk_widget_show (label);
-
-  label = dashboard->cache_occupied_label = gtk_label_new (_("N/A"));
-  gtk_label_set_xalign (GTK_LABEL (label), 0.0);
-  gtk_table_attach (GTK_TABLE (table), label, 1, 2, 1, 2,
-                    GTK_EXPAND | GTK_FILL, 0, 0, 0);
-  gtk_widget_show (label);
-
-  /* cache limit field */
-  label = gtk_label_new (_("Limit:"));
-  gtk_label_set_xalign (GTK_LABEL (label), 0.0);
-  gtk_table_attach (GTK_TABLE (table), label, 0, 1, 2, 3,
-                    GTK_FILL, 0, 0, 0);
-  gtk_widget_show (label);
-
-  label = dashboard->cache_limit_label = gtk_label_new (_("N/A"));
-  gtk_label_set_xalign (GTK_LABEL (label), 0.0);
-  gtk_table_attach (GTK_TABLE (table), label, 1, 2, 2, 3,
-                    GTK_EXPAND | GTK_FILL, 0, 0, 0);
-  gtk_widget_show (label);
-
-
-  /* swap frame */
-  frame = gimp_frame_new (NULL);
-  gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_NONE);
-  gtk_box_pack_start (GTK_BOX (vbox), frame, FALSE, FALSE, 0);
-  gtk_widget_show (frame);
-
-  /* swap frame label */
-  box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
-  gtk_frame_set_label_widget (GTK_FRAME (frame), box);
-  gtk_widget_show (box);
+          group_data->n_fields++;
+          group_data->n_meter_values = MAX (group_data->n_meter_values,
+                                            field_info->meter_value);
+        }
+
+      group_data->fields = g_new0 (FieldData, group_data->n_fields);
+
+      /* group expander */
+      expander = gtk_expander_new (NULL);
+      group_data->expander = GTK_EXPANDER (expander);
+      gtk_expander_set_expanded (GTK_EXPANDER (expander),
+                                 group_info->default_expanded);
+      gtk_expander_set_label_fill (GTK_EXPANDER (expander), TRUE);
+      gtk_box_pack_start (GTK_BOX (vbox), expander, FALSE, FALSE, 0);
+      gtk_widget_show (expander);
+
+      g_object_set_data (G_OBJECT (expander),
+                         "gimp-dashboard-group", GINT_TO_POINTER (group));
+      g_signal_connect_swapped (expander, "button-press-event",
+                                G_CALLBACK (gimp_dashboard_group_expander_button_press),
+                                dashboard);
+
+      /* group expander label box */
+      hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
+      gimp_help_set_help_data (hbox,
+                               g_dgettext (NULL, group_info->description),
+                               NULL);
+      gtk_expander_set_label_widget (GTK_EXPANDER (expander), hbox);
+      gtk_widget_show (hbox);
+
+      /* group expander label */
+      label = gtk_label_new (g_dpgettext2 (NULL, "dashboard-group",
+                                           group_info->title));
+      gtk_label_set_xalign (GTK_LABEL (label), 0.0);
+      gimp_label_set_attributes (GTK_LABEL (label),
+                                 PANGO_ATTR_WEIGHT, PANGO_WEIGHT_BOLD,
+                                 -1);
+      gtk_box_pack_start (GTK_BOX (hbox), label, TRUE, TRUE, 0);
+      gtk_widget_show (label);
+
+      /* group expander menu button */
+      button = gtk_button_new ();
+      group_data->menu_button = GTK_BUTTON (button);
+      gimp_help_set_help_data (button, _("Select fields"),
+                               NULL);
+      gtk_widget_set_can_focus (button, FALSE);
+      gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE);
+      gtk_box_pack_end (GTK_BOX (hbox), button, FALSE, FALSE, 0);
+      gtk_widget_show (button);
+
+      image = gtk_image_new_from_icon_name (GIMP_ICON_MENU_LEFT,
+                                            GTK_ICON_SIZE_MENU);
+      gtk_image_set_pixel_size (GTK_IMAGE (image), 12);
+      gtk_image_set_from_icon_name (GTK_IMAGE (image), GIMP_ICON_MENU_LEFT,
+                                    GTK_ICON_SIZE_MENU);
+      gtk_container_add (GTK_CONTAINER (button), image);
+      gtk_widget_show (image);
+
+      /* group menu */
+      menu = gtk_menu_new ();
+      group_data->menu = GTK_MENU (menu);
+      gtk_menu_attach_to_widget (GTK_MENU (menu), button, NULL);
+
+      for (field = 0; field < group_data->n_fields; field++)
+        {
+          const FieldInfo *field_info = &group_info->fields[field];
+          FieldData       *field_data = &group_data->fields[field];
+
+          if (field_info->variable != VARIABLE_SEPARATOR)
+            {
+              const VariableInfo *variable_info = &variables[field_info->variable];
+
+              item = gtk_check_menu_item_new_with_label (
+                g_dpgettext2 (NULL, "dashboard-field", variable_info->title));
+              field_data->menu_item = GTK_CHECK_MENU_ITEM (item);
+              gimp_help_set_help_data (item,
+                                       g_dgettext (NULL, variable_info->description),
+                                       NULL);
+
+              g_object_set_data (G_OBJECT (item),
+                                 "gimp-dashboard-group", GINT_TO_POINTER (group));
+              g_object_set_data (G_OBJECT (item),
+                                 "gimp-dashboard-field", GINT_TO_POINTER (field));
+              g_signal_connect_swapped (item, "toggled",
+                                        G_CALLBACK (gimp_dashboard_group_menu_item_toggled),
+                                        dashboard);
+
+              gimp_dashboard_field_set_active (dashboard, group, field,
+                                               field_info->default_active);
+            }
+          else
+            {
+              item = gtk_separator_menu_item_new ();
+            }
+
+          gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
+          gtk_widget_show (item);
+        }
+
+      /* group frame */
+      frame = gimp_frame_new (NULL);
+      gtk_container_add (GTK_CONTAINER (expander), frame);
+      gtk_widget_show (frame);
+
+      /* group vbox */
+      vbox2 = gtk_box_new (GTK_ORIENTATION_VERTICAL, 2 * content_spacing);
+      gtk_container_add (GTK_CONTAINER (frame), vbox2);
+      gtk_widget_show (vbox2);
+
+      /* group meter */
+      if (group_info->has_meter)
+        {
+          meter = gimp_meter_new (group_data->n_meter_values);
+          group_data->meter = GIMP_METER (meter);
+          gimp_help_set_help_data (meter,
+                                   g_dgettext (NULL, group_info->description),
+                                   NULL);
+          gimp_meter_set_history_resolution (GIMP_METER (meter),
+                                             priv->update_interval / 1000.0);
+          gimp_meter_set_history_duration (GIMP_METER (meter),
+                                           priv->history_duration / 1000.0);
+          gtk_box_pack_start (GTK_BOX (vbox2), meter, FALSE, FALSE, 0);
+          gtk_widget_show (meter);
+
+          if (group_info->meter_led)
+            {
+              gimp_meter_set_led_color (GIMP_METER (meter),
+                                        &variables[group_info->meter_led].color);
+            }
+
+          for (field = 0; field < group_data->n_fields; field++)
+            {
+              const FieldInfo *field_info = &group_info->fields[field];
+
+              if (field_info->meter_value)
+                {
+                  const VariableInfo *variable_info = &variables[field_info->variable];
+
+                  gimp_meter_set_value_color (GIMP_METER (meter),
+                                              field_info->meter_value - 1,
+                                              &variable_info->color);
+                }
+            }
+        }
 
-  label = gtk_label_new (_("Swap"));
-  gtk_label_set_xalign (GTK_LABEL (label), 0.0);
-  gimp_label_set_attributes (GTK_LABEL (label),
-                             PANGO_ATTR_WEIGHT, PANGO_WEIGHT_BOLD,
-                             -1);
-  gtk_box_pack_start (GTK_BOX (box), label, FALSE, FALSE, 0);
-  gtk_widget_show (label);
-
-  /* swap table */
-  table = gtk_table_new (4, 2, FALSE);
-  gtk_table_set_row_spacings (GTK_TABLE (table), content_spacing);
-  gtk_table_set_col_spacings (GTK_TABLE (table), 4);
-  gtk_container_add (GTK_CONTAINER (frame), table);
-  gtk_widget_show (table);
-
-  /* swap meter */
-  meter = dashboard->swap_meter = gimp_meter_new (2);
-  gimp_meter_set_color (GIMP_METER (meter),
-                        0, &(GimpRGB) SWAP_SIZE_COLOR);
-  gimp_meter_set_color (GIMP_METER (meter),
-                        1, &(GimpRGB) SWAP_OCCUPIED_COLOR);
-  gimp_meter_set_history_resolution (GIMP_METER (meter),
-                                     dashboard->update_interval / 1000.0);
-  gimp_meter_set_history_duration (GIMP_METER (meter),
-                                   dashboard->history_duration / 1000.0);
-  gimp_meter_set_led_color (GIMP_METER (meter),
-                            &(GimpRGB) SWAP_LED_COLOR);
-  gtk_table_attach (GTK_TABLE (table), meter, 0, 2, 0, 1,
-                    GTK_EXPAND | GTK_FILL, 0, 1, 0);
-  gtk_table_set_row_spacing (GTK_TABLE (table), 0, 2 * content_spacing);
-  gtk_widget_show (meter);
-
-  /* swap occupied field */
-  label = gtk_label_new (_("Occupied:"));
-  gtk_label_set_xalign (GTK_LABEL (label), 0.0);
-  gtk_table_attach (GTK_TABLE (table), label, 0, 1, 1, 2,
-                    GTK_FILL, 0, 0, 0);
-  gtk_widget_show (label);
-
-  label = dashboard->swap_occupied_label = gtk_label_new (_("N/A"));
-  gtk_label_set_xalign (GTK_LABEL (label), 0.0);
-  gtk_table_attach (GTK_TABLE (table), label, 1, 2, 1, 2,
-                    GTK_EXPAND | GTK_FILL, 0, 0, 0);
-  gtk_widget_show (label);
-
-  /* swap size field */
-  label = gtk_label_new (_("Size:"));
-  gtk_label_set_xalign (GTK_LABEL (label), 0.0);
-  gtk_table_attach (GTK_TABLE (table), label, 0, 1, 2, 3,
-                    GTK_FILL, 0, 0, 0);
-  gtk_widget_show (label);
-
-  label = dashboard->swap_size_label = gtk_label_new (_("N/A"));
-  gtk_label_set_xalign (GTK_LABEL (label), 0.0);
-  gtk_table_attach (GTK_TABLE (table), label, 1, 2, 2, 3,
-                    GTK_EXPAND | GTK_FILL, 0, 0, 0);
-  gtk_widget_show (label);
-
-  /* swap limit field */
-  label = gtk_label_new (_("Limit:"));
-  gtk_label_set_xalign (GTK_LABEL (label), 0.0);
-  gtk_table_attach (GTK_TABLE (table), label, 0, 1, 3, 4,
-                    GTK_FILL, 0, 0, 0);
-  gtk_widget_show (label);
-
-  label = dashboard->swap_limit_label = gtk_label_new (_("N/A"));
-  gtk_label_set_xalign (GTK_LABEL (label), 0.0);
-  gtk_table_attach (GTK_TABLE (table), label, 1, 2, 3, 4,
-                    GTK_EXPAND | GTK_FILL, 0, 0, 0);
-  gtk_widget_show (label);
-
-
-  /* sampler thread */
-  g_mutex_init (&dashboard->mutex);
-  g_cond_init (&dashboard->cond);
-
-  /* we use a separate thread for sampling, so that data is sampled even when
+      /* group table */
+      table = gtk_table_new (1, 1, FALSE);
+      group_data->table = GTK_TABLE (table);
+      gtk_table_set_row_spacings (GTK_TABLE (table), content_spacing);
+      gtk_table_set_col_spacings (GTK_TABLE (table), 4);
+      gtk_box_pack_start (GTK_BOX (vbox2), table, FALSE, FALSE, 0);
+      gtk_widget_show (table);
+
+      gimp_dashboard_update_group (dashboard, group);
+    }
+
+  /* sampler thread
+   *
+   * we use a separate thread for sampling, so that data is sampled even when
    * the main thread is busy
    */
-  dashboard->thread = g_thread_new ("dashboard",
-                                    (GThreadFunc) gimp_dashboard_sample,
-                                    dashboard);
+  priv->thread = g_thread_new ("dashboard",
+                               (GThreadFunc) gimp_dashboard_sample,
+                               dashboard);
 }
 
 static void
@@ -315,32 +703,44 @@ gimp_dashboard_docked_iface_init (GimpDockedInterface *iface)
 }
 
 static void
-gimp_dashboard_dispose (GObject *object)
+gimp_dashboard_constructed (GObject *object)
 {
   GimpDashboard *dashboard = GIMP_DASHBOARD (object);
 
-  if (dashboard->thread)
+  G_OBJECT_CLASS (parent_class)->constructed (object);
+
+  gimp_editor_add_action_button (GIMP_EDITOR (dashboard), "dashboard",
+                                 "dashboard-reset", NULL);
+}
+
+static void
+gimp_dashboard_dispose (GObject *object)
+{
+  GimpDashboard        *dashboard = GIMP_DASHBOARD (object);
+  GimpDashboardPrivate *priv      = dashboard->priv;
+
+  if (priv->thread)
     {
-      g_mutex_lock (&dashboard->mutex);
+      g_mutex_lock (&priv->mutex);
 
-      dashboard->quit = TRUE;
-      g_cond_signal (&dashboard->cond);
+      priv->quit = TRUE;
+      g_cond_signal (&priv->cond);
 
-      g_mutex_unlock (&dashboard->mutex);
+      g_mutex_unlock (&priv->mutex);
 
-      g_clear_pointer (&dashboard->thread, g_thread_join);
+      g_clear_pointer (&priv->thread, g_thread_join);
     }
 
-  if (dashboard->timeout_id)
+  if (priv->update_idle_id)
     {
-      g_source_remove (dashboard->timeout_id);
-      dashboard->timeout_id = 0;
+      g_source_remove (priv->update_idle_id);
+      priv->update_idle_id = 0;
     }
 
-  if (dashboard->low_swap_space_idle_id)
+  if (priv->low_swap_space_idle_id)
     {
-      g_source_remove (dashboard->low_swap_space_idle_id);
-      dashboard->low_swap_space_idle_id = 0;
+      g_source_remove (priv->low_swap_space_idle_id);
+      priv->low_swap_space_idle_id = 0;
     }
 
   G_OBJECT_CLASS (parent_class)->dispose (object);
@@ -349,10 +749,15 @@ gimp_dashboard_dispose (GObject *object)
 static void
 gimp_dashboard_finalize (GObject *object)
 {
-  GimpDashboard *dashboard = GIMP_DASHBOARD (object);
+  GimpDashboard        *dashboard = GIMP_DASHBOARD (object);
+  GimpDashboardPrivate *priv      = dashboard->priv;
+  gint                  i;
 
-  g_mutex_clear (&dashboard->mutex);
-  g_cond_clear (&dashboard->cond);
+  for (i = FIRST_GROUP; i < N_GROUPS; i++)
+    g_free (priv->groups[i].fields);
+
+  g_mutex_clear (&priv->mutex);
+  g_cond_clear (&priv->cond);
 
   G_OBJECT_CLASS (parent_class)->finalize (object);
 }
@@ -364,25 +769,25 @@ gimp_dashboard_map (GtkWidget *widget)
 
   GTK_WIDGET_CLASS (parent_class)->map (widget);
 
-  if (! dashboard->timeout_id)
-    {
-      dashboard->timeout_id = g_timeout_add (dashboard->update_interval,
-                                             (GSourceFunc) gimp_dashboard_update,
-                                             dashboard);
-    }
+  gimp_dashboard_update (dashboard);
 }
 
 static void
 gimp_dashboard_unmap (GtkWidget *widget)
 {
-  GimpDashboard *dashboard = GIMP_DASHBOARD (widget);
+  GimpDashboard        *dashboard = GIMP_DASHBOARD (widget);
+  GimpDashboardPrivate *priv      = dashboard->priv;
+
+  g_mutex_lock (&priv->mutex);
 
-  if (dashboard->timeout_id)
+  if (priv->update_idle_id)
     {
-      g_source_remove (dashboard->timeout_id);
-      dashboard->timeout_id = 0;
+      g_source_remove (priv->update_idle_id);
+      priv->update_idle_id = 0;
     }
 
+  g_mutex_unlock (&priv->mutex);
+
   GTK_WIDGET_CLASS (parent_class)->unmap (widget);
 }
 
@@ -394,8 +799,10 @@ static void
 gimp_dashboard_set_aux_info (GimpDocked *docked,
                              GList      *aux_info)
 {
-  GimpDashboard *dashboard = GIMP_DASHBOARD (docked);
-  GList         *list;
+  GimpDashboard        *dashboard = GIMP_DASHBOARD (docked);
+  GimpDashboardPrivate *priv      = dashboard->priv;
+  gchar                *name;
+  GList                *list;
 
   parent_docked_iface->set_aux_info (docked, aux_info);
 
@@ -432,269 +839,447 @@ gimp_dashboard_set_aux_info (GimpDocked *docked,
           gimp_dashboard_set_low_swap_space_warning (dashboard,
                                                      ! strcmp (aux->value, "yes"));
         }
-    }
-}
+      else
+        {
+          Group group;
+          gint  field;
 
-static GList *
-gimp_dashboard_get_aux_info (GimpDocked *docked)
-{
-  GimpDashboard      *dashboard = GIMP_DASHBOARD (docked);
-  GList              *aux_info;
-  GimpSessionInfoAux *aux;
-  gchar              *value;
+          for (group = FIRST_GROUP; group < N_GROUPS; group++)
+            {
+              const GroupInfo *group_info = &groups[group];
+              GroupData       *group_data = &priv->groups[group];
 
-  aux_info = parent_docked_iface->get_aux_info (docked);
+              name = g_strdup_printf ("%s-expanded", group_info->name);
 
-  value    = g_strdup_printf ("%d", dashboard->update_interval);
-  aux      = gimp_session_info_aux_new (AUX_INFO_UPDATE_INTERVAL, value);
-  aux_info = g_list_append (aux_info, aux);
-  g_free (value);
+              if (! strcmp (aux->name, name))
+                {
+                  gboolean expanded = ! strcmp (aux->value, "yes");
 
-  value    = g_strdup_printf ("%d", dashboard->history_duration);
-  aux      = gimp_session_info_aux_new (AUX_INFO_HISTORY_DURATION, value);
-  aux_info = g_list_append (aux_info, aux);
-  g_free (value);
+                  gtk_expander_set_expanded (group_data->expander, expanded);
 
-  value    = dashboard->low_swap_space_warning ? "yes" : "no";
-  aux      = gimp_session_info_aux_new (AUX_INFO_LOW_SWAP_SPACE_WARNING, value);
-  aux_info = g_list_append (aux_info, aux);
+                  g_free (name);
+                  goto next_aux_info;
+                }
 
-  return aux_info;
+              g_free (name);
+
+              for (field = 0; field < group_data->n_fields; field++)
+                {
+                  const FieldInfo *field_info = &group_info->fields[field];
+
+                  if (field_info->variable != VARIABLE_SEPARATOR)
+                    {
+                      const VariableInfo *variable_info = &variables[field_info->variable];
+
+                      name = g_strdup_printf ("%s-%s-active",
+                                              group_info->name,
+                                              variable_info->name);
+
+                      if (! strcmp (aux->name, name))
+                        {
+                          gboolean active = ! strcmp (aux->value, "yes");
+
+                          gimp_dashboard_field_set_active (dashboard,
+                                                           group, field,
+                                                           active);
+
+                          g_free (name);
+                          goto next_aux_info;
+                        }
+
+                      g_free (name);
+                    }
+                }
+            }
+        }
+next_aux_info: ;
+    }
+
+  gimp_dashboard_update_groups (dashboard);
 }
 
-static gboolean
-gimp_dashboard_update (GimpDashboard *dashboard)
+static GList *
+gimp_dashboard_get_aux_info (GimpDocked *docked)
 {
-  /* cache */
-  {
-    guint64  cache_occupied;
-    guint64  cache_limit;
-
-    g_object_get (gegl_stats (),
-                  "tile-cache-total", &cache_occupied,
-                  NULL);
-    g_object_get (gegl_config (),
-                  "tile-cache-size",  &cache_limit,
-                  NULL);
-
-    gimp_meter_set_range (GIMP_METER (dashboard->cache_meter),
-                          0.0, cache_limit);
-
-    gimp_dashboard_label_set_size (GTK_LABEL (dashboard->cache_occupied_label),
-                                   cache_occupied, cache_limit);
-    gimp_dashboard_label_set_size (GTK_LABEL (dashboard->cache_limit_label),
-                                   cache_limit, 0);
-  }
+  GimpDashboard        *dashboard = GIMP_DASHBOARD (docked);
+  GimpDashboardPrivate *priv      = dashboard->priv;
+  GList                *aux_info;
+  GimpSessionInfoAux   *aux;
+  gchar                *name;
+  gchar                *value;
+  Group                 group;
+  gint                  field;
 
-  /* swap */
-  {
-    guint64  swap_occupied;
-    guint64  swap_size;
-    guint64  swap_limit;
-    gboolean swap_busy;
+  aux_info = parent_docked_iface->get_aux_info (docked);
 
-    g_mutex_lock (&dashboard->mutex);
+  if (priv->update_interval != DEFAULT_UPDATE_INTERVAL)
+    {
+      value    = g_strdup_printf ("%d", priv->update_interval);
+      aux      = gimp_session_info_aux_new (AUX_INFO_UPDATE_INTERVAL, value);
+      aux_info = g_list_append (aux_info, aux);
+      g_free (value);
+    }
 
-    g_object_get (gegl_stats (),
-                  "swap-total",     &swap_occupied,
-                  "swap-file-size", &swap_size,
-                  "swap-busy",      &swap_busy,
-                  NULL);
-    swap_limit = gimp_dashboard_get_swap_limit (swap_size);
+  if (priv->history_duration != DEFAULT_HISTORY_DURATION)
+    {
+      value    = g_strdup_printf ("%d", priv->history_duration);
+      aux      = gimp_session_info_aux_new (AUX_INFO_HISTORY_DURATION, value);
+      aux_info = g_list_append (aux_info, aux);
+      g_free (value);
+    }
 
-    g_mutex_unlock (&dashboard->mutex);
+  if (priv->low_swap_space_warning != DEFAULT_LOW_SWAP_SPACE_WARNING)
+    {
+      value    = priv->low_swap_space_warning ? "yes" : "no";
+      aux      = gimp_session_info_aux_new (AUX_INFO_LOW_SWAP_SPACE_WARNING, value);
+      aux_info = g_list_append (aux_info, aux);
+    }
 
-    gimp_meter_set_range (GIMP_METER (dashboard->swap_meter),
-                          0.0, swap_limit ? swap_limit : swap_size);
-    gimp_meter_set_led_visible (GIMP_METER (dashboard->swap_meter), swap_busy);
+  for (group = FIRST_GROUP; group < N_GROUPS; group++)
+    {
+      const GroupInfo *group_info = &groups[group];
+      GroupData       *group_data = &priv->groups[group];
+      gboolean         expanded   = gtk_expander_get_expanded (group_data->expander);
 
-    gimp_dashboard_label_set_size (GTK_LABEL (dashboard->swap_occupied_label),
-                                   swap_occupied, swap_limit);
-    gimp_dashboard_label_set_size (GTK_LABEL (dashboard->swap_size_label),
-                                   swap_size, swap_limit);
+      if (expanded != group_info->default_expanded)
+        {
+          name     = g_strdup_printf ("%s-expanded", group_info->name);
+          value    = expanded ? "yes" : "no";
+          aux      = gimp_session_info_aux_new (name, value);
+          aux_info = g_list_append (aux_info, aux);
+          g_free (name);
+        }
 
-    if (swap_limit)
-      {
-        gimp_dashboard_label_set_size (GTK_LABEL (dashboard->swap_limit_label),
-                                       swap_limit, 0);
-      }
-    else
-      {
-        gimp_dashboard_label_set_text (GTK_LABEL (dashboard->swap_limit_label),
-                                       _("N/A"));
-      }
-  }
+      for (field = 0; field < group_data->n_fields; field++)
+        {
+          const FieldInfo *field_info = &group_info->fields[field];
+          FieldData       *field_data = &group_data->fields[field];
+          gboolean         active     = field_data->active;
 
-  return G_SOURCE_CONTINUE;
+          if (field_info->variable != VARIABLE_SEPARATOR)
+            {
+              const VariableInfo *variable_info = &variables[field_info->variable];
+
+              if (active != field_info->default_active)
+                {
+                  name = g_strdup_printf ("%s-%s-active",
+                                          group_info->name,
+                                          variable_info->name);
+                  value    = active ? "yes" : "no";
+                  aux      = gimp_session_info_aux_new (name, value);
+                  aux_info = g_list_append (aux_info, aux);
+                  g_free (name);
+                }
+            }
+        }
+    }
+
+  return aux_info;
 }
 
 static gboolean
-gimp_dashboard_low_swap_space (GimpDashboard *dashboard)
+gimp_dashboard_group_expander_button_press (GimpDashboard  *dashboard,
+                                            GdkEventButton *bevent,
+                                            GtkWidget      *widget)
 {
-  if (dashboard->gimp)
+  GimpDashboardPrivate *priv = dashboard->priv;
+  Group                 group;
+  GroupData            *group_data;
+  GtkAllocation         expander_allocation;
+  GtkAllocation         allocation;
+
+  group      = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (widget),
+                                                   "gimp-dashboard-group"));
+  group_data = &priv->groups[group];
+
+  gtk_widget_get_allocation (GTK_WIDGET (group_data->expander),
+                             &expander_allocation);
+  gtk_widget_get_allocation (GTK_WIDGET (group_data->menu_button),
+                             &allocation);
+
+  allocation.x -= expander_allocation.x;
+  allocation.y -= expander_allocation.y;
+
+  if (bevent->button == 1                         &&
+      bevent->x >= allocation.x                   &&
+      bevent->x < allocation.x + allocation.width &&
+      bevent->y >= allocation.y                   &&
+      bevent->y < allocation.y + allocation.height)
     {
-      GdkScreen *screen;
-      gint       monitor;
+      gtk_menu_popup (group_data->menu,
+                      NULL, NULL,
+                      gimp_dashboard_group_menu_position,
+                      group_data->menu_button,
+                      bevent->button, bevent->time);
 
-      monitor = gimp_get_monitor_at_pointer (&screen);
+      return TRUE;
+    }
 
-      gimp_window_strategy_show_dockable_dialog (
-        GIMP_WINDOW_STRATEGY (gimp_get_window_strategy (dashboard->gimp)),
-        dashboard->gimp,
-        gimp_dialog_factory_get_singleton (),
-        screen, monitor,
-        "gimp-dashboard");
+  return FALSE;
+}
 
-      gimp_dashboard_update (dashboard);
+static void
+gimp_dashboard_group_menu_item_toggled (GimpDashboard    *dashboard,
+                                        GtkCheckMenuItem *item)
+{
+  GimpDashboardPrivate *priv = dashboard->priv;
+  Group                 group;
+  GroupData            *group_data;
+  gint                  field;
+  FieldData            *field_data;
 
-      g_mutex_lock (&dashboard->mutex);
+  group      = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (item),
+                                                   "gimp-dashboard-group"));
+  group_data = &priv->groups[group];
 
-      dashboard->low_swap_space_idle_id = 0;
+  field      = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (item),
+                                                   "gimp-dashboard-field"));
+  field_data = &group_data->fields[field];
 
-      g_mutex_unlock (&dashboard->mutex);
-    }
+  field_data->active = gtk_check_menu_item_get_active (item);
 
-  return G_SOURCE_REMOVE;
+  gimp_dashboard_update_group (dashboard, group);
 }
 
 static gpointer
 gimp_dashboard_sample (GimpDashboard *dashboard)
 {
-  GimpDashboardUpdateInteval update_interval;
-  gint64                     end_time;
-  gboolean                   seen_low_swap_space = FALSE;
+  GimpDashboardPrivate       *priv                = dashboard->priv;
+  GimpDashboardUpdateInteval  update_interval;
+  gint64                      end_time;
+  gboolean                    seen_low_swap_space = FALSE;
 
-  g_mutex_lock (&dashboard->mutex);
+  g_mutex_lock (&priv->mutex);
 
-  update_interval = dashboard->update_interval;
+  update_interval = priv->update_interval;
   end_time        = g_get_monotonic_time ();
 
-  while (! dashboard->quit)
+  while (! priv->quit)
     {
-      if (! g_cond_wait_until (&dashboard->cond, &dashboard->mutex, end_time))
+      if (! g_cond_wait_until (&priv->cond, &priv->mutex, end_time) ||
+          priv->update_now)
         {
-          /* cache */
-          {
-            guint64 cache_occupied;
-            gdouble sample[1];
+          gboolean varaibles_changed = FALSE;
+          Variable variable;
+          Group    group;
+          gint     field;
 
-            g_object_get (gegl_stats (),
-                          "tile-cache-total", &cache_occupied,
-                          NULL);
+          /* sample all varaibles */
+          for (variable = FIRST_VARIABLE; variable < N_VARIABLES; variable++)
+            {
+              const VariableInfo *variable_info      = &variables[variable];
+              const VariableData *variable_data      = &priv->variables[variable];
+              VariableData        prev_variable_data = *variable_data;
 
-            sample[0] = cache_occupied;
-            gimp_meter_add_sample (GIMP_METER (dashboard->cache_meter), sample);
-          }
+              variable_info->sample_func (dashboard, variable);
 
-          /* swap */
-          {
-            guint64 swap_occupied;
-            guint64 swap_size;
-            gdouble sample[2];
+              varaibles_changed = varaibles_changed ||
+                                  memcmp (variable_data, &prev_variable_data,
+                                          sizeof (VariableData));
+            }
 
-            g_object_get (gegl_stats (),
-                          "swap-total",     &swap_occupied,
-                          "swap-file-size", &swap_size,
-                          NULL);
+          /* add samples to meters */
+          for (group = FIRST_GROUP; group < N_GROUPS; group++)
+            {
+              const GroupInfo *group_info = &groups[group];
+              GroupData       *group_data = &priv->groups[group];
+              gdouble         *sample;
 
-            sample[0] = swap_size;
-            sample[1] = swap_occupied;
-            gimp_meter_add_sample (GIMP_METER (dashboard->swap_meter), sample);
+              if (! group_info->has_meter)
+                continue;
 
-            if (dashboard->low_swap_space_warning)
-              {
-                guint64 swap_limit = gimp_dashboard_get_swap_limit (swap_size);
+              sample = g_new (gdouble, group_data->n_meter_values);
 
-                if (swap_limit)
-                  {
-                    if (! seen_low_swap_space &&
-                        swap_occupied >= LOW_SWAP_SPACE_WARNING_ON * swap_limit)
-                      {
-                        if (! dashboard->low_swap_space_idle_id)
-                          {
-                            dashboard->low_swap_space_idle_id =
-                              g_idle_add_full (G_PRIORITY_HIGH,
-                                               (GSourceFunc) gimp_dashboard_low_swap_space,
-                                               dashboard, NULL);
-                          }
-
-                        seen_low_swap_space = TRUE;
-                      }
-                    else if (seen_low_swap_space &&
-                             swap_occupied <= LOW_SWAP_SPACE_WARNING_OFF * swap_limit)
-                      {
-                        if (dashboard->low_swap_space_idle_id)
-                          {
-                            g_source_remove (dashboard->low_swap_space_idle_id);
+              for (field = 0; field < group_data->n_fields; field++)
+                {
+                  const FieldInfo *field_info = &group_info->fields[field];
 
-                            dashboard->low_swap_space_idle_id = 0;
-                          }
+                  if (field_info->meter_value)
+                    {
+                      sample[field_info->meter_value - 1] =
+                        gimp_dashboard_variable_to_double (dashboard,
+                                                           field_info->variable);
+                    }
+                }
 
-                        seen_low_swap_space = FALSE;
-                      }
-                  }
-              }
-          }
+              gimp_meter_add_sample (group_data->meter, sample);
+
+              g_free (sample);
+            }
+
+          if (varaibles_changed)
+            {
+              /* check for low swap space */
+              if (priv->low_swap_space_warning                      &&
+                  priv->variables[VARIABLE_SWAP_OCCUPIED].available &&
+                  priv->variables[VARIABLE_SWAP_LIMIT].available)
+                {
+                  guint64 swap_occupied;
+                  guint64 swap_limit;
+
+                  swap_occupied = priv->variables[VARIABLE_SWAP_OCCUPIED].value.size;
+                  swap_limit    = priv->variables[VARIABLE_SWAP_LIMIT].value.size;
+
+                  if (! seen_low_swap_space &&
+                      swap_occupied >= LOW_SWAP_SPACE_WARNING_ON * swap_limit)
+                    {
+                      if (! priv->low_swap_space_idle_id)
+                        {
+                          priv->low_swap_space_idle_id =
+                            g_idle_add_full (G_PRIORITY_HIGH,
+                                             (GSourceFunc) gimp_dashboard_low_swap_space,
+                                             dashboard, NULL);
+                        }
+
+                      seen_low_swap_space = TRUE;
+                    }
+                  else if (seen_low_swap_space &&
+                           swap_occupied <= LOW_SWAP_SPACE_WARNING_OFF * swap_limit)
+                    {
+                      if (priv->low_swap_space_idle_id)
+                        {
+                          g_source_remove (priv->low_swap_space_idle_id);
+
+                          priv->low_swap_space_idle_id = 0;
+                        }
+
+                      seen_low_swap_space = FALSE;
+                    }
+                }
+
+              /* enqueue update source */
+              if (! priv->update_idle_id &&
+                  gtk_widget_get_mapped (GTK_WIDGET (dashboard)))
+                {
+                  priv->update_idle_id = g_idle_add_full (
+                    G_PRIORITY_DEFAULT,
+                    (GSourceFunc) gimp_dashboard_update,
+                    dashboard, NULL);
+                }
+            }
+
+          priv->update_now = FALSE;
 
           end_time = g_get_monotonic_time () +
                      update_interval * G_TIME_SPAN_SECOND / 1000;
         }
 
-      if (dashboard->update_interval != update_interval)
+      if (priv->update_interval != update_interval)
         {
-          update_interval = dashboard->update_interval;
+          update_interval = priv->update_interval;
           end_time        = g_get_monotonic_time () +
                             update_interval * G_TIME_SPAN_SECOND / 1000;
         }
     }
 
-  g_mutex_unlock (&dashboard->mutex);
+  g_mutex_unlock (&priv->mutex);
 
   return NULL;
 }
 
-static void
-gimp_dashboard_label_set_text (GtkLabel    *label,
-                               const gchar *text)
+static gboolean
+gimp_dashboard_update (GimpDashboard *dashboard)
 {
-  /* the strcmp() reduces the overhead of gtk_label_set_text() when the text
-   * isn't changed
-   */
-  if (strcmp (gtk_label_get_text (label), text))
-    gtk_label_set_text (label, text);
+  GimpDashboardPrivate *priv = dashboard->priv;
+  Group                 group;
+
+  g_mutex_lock (&priv->mutex);
+
+  for (group = FIRST_GROUP; group < N_GROUPS; group++)
+    {
+      const GroupInfo *group_info = &groups[group];
+      GroupData       *group_data = &priv->groups[group];
+
+      if (group_info->has_meter && group_info->meter_led)
+        {
+          gboolean active;
+
+          active = gimp_dashboard_variable_to_boolean (dashboard,
+                                                       group_info->meter_led);
+
+          gimp_meter_set_led_active (group_data->meter, active);
+        }
+
+      gimp_dashboard_update_group_values (dashboard, group);
+    }
+
+  priv->update_idle_id = 0;
+
+  g_mutex_unlock (&priv->mutex);
+
+  return G_SOURCE_REMOVE;
 }
 
-static void
-gimp_dashboard_label_set_size (GtkLabel *label,
-                               guint64   size,
-                               guint64   limit)
+static gboolean
+gimp_dashboard_low_swap_space (GimpDashboard *dashboard)
 {
-  gchar *text;
+  GimpDashboardPrivate *priv = dashboard->priv;
 
-  text = g_format_size_full (size, G_FORMAT_SIZE_IEC_UNITS);
-
-  if (limit)
+  if (priv->gimp)
     {
-      gchar *temp;
+      GdkScreen *screen;
+      gint       monitor;
+      gint       field;
+
+      gtk_expander_set_expanded (priv->groups[GROUP_SWAP].expander, TRUE);
+
+      for (field = 0; field < priv->groups[GROUP_SWAP].n_fields; field++)
+        {
+          const FieldInfo *field_info = &groups[GROUP_SWAP].fields[field];
 
-      temp = g_strdup_printf ("%s (%d%%)", text, ROUND (100.0 * size / limit));
-      g_free (text);
+          if (field_info->variable == VARIABLE_SWAP_OCCUPIED ||
+              field_info->variable == VARIABLE_SWAP_LIMIT)
+            {
+              gimp_dashboard_field_set_active (dashboard,
+                                               GROUP_SWAP, field, TRUE);
+            }
+        }
 
-      text = temp;
+      gimp_dashboard_update_groups (dashboard);
+
+      monitor = gimp_get_monitor_at_pointer (&screen);
+
+      gimp_window_strategy_show_dockable_dialog (
+        GIMP_WINDOW_STRATEGY (gimp_get_window_strategy (priv->gimp)),
+        priv->gimp,
+        gimp_dialog_factory_get_singleton (),
+        screen, monitor,
+        "gimp-dashboard");
+
+      g_mutex_lock (&priv->mutex);
+
+      priv->low_swap_space_idle_id = 0;
+
+      g_mutex_unlock (&priv->mutex);
     }
 
-  gimp_dashboard_label_set_text (label, text);
+  return G_SOURCE_REMOVE;
+}
+
+static void
+gimp_dashboard_sample_gegl_config (GimpDashboard *dashboard,
+                                   Variable       variable)
+{
+  gimp_dashboard_sample_object (dashboard, G_OBJECT (gegl_config ()), variable);
+}
 
-  g_free (text);
+static void
+gimp_dashboard_sample_gegl_stats (GimpDashboard *dashboard,
+                                  Variable       variable)
+{
+  gimp_dashboard_sample_object (dashboard, G_OBJECT (gegl_stats ()), variable);
 }
 
-static guint64
-gimp_dashboard_get_swap_limit (guint64 swap_size)
+static void
+gimp_dashboard_sample_swap_limit (GimpDashboard *dashboard,
+                                  Variable       variable)
 {
-  static guint64  free_space      = 0;
-  static gboolean has_free_space  = FALSE;
-  static gint64   last_check_time = 0;
-  gint64          time;
+  GimpDashboardPrivate *priv            = dashboard->priv;
+  VariableData         *variable_data   = &priv->variables[variable];
+  static guint64        free_space      = 0;
+  static gboolean       has_free_space  = FALSE;
+  static gint64         last_check_time = 0;
+  gint64                time;
 
   /* we don't have a config option for limiting the swap size, so we simply
    * return the free space available on the filesystem containing the swap
@@ -742,10 +1327,522 @@ gimp_dashboard_get_swap_limit (guint64 swap_size)
       last_check_time = time;
     }
 
-  /* the swap limit is the sum of free_space and swap_size, since the swap
-   * itself occupies space in the filesystem
-   */
-  return has_free_space ? free_space + swap_size : 0;
+  variable_data->available = has_free_space;
+
+  if (has_free_space)
+    {
+      variable_data->value.size = free_space;
+
+      if (priv->variables[VARIABLE_SWAP_SIZE].available)
+        {
+          /* the swap limit is the sum of free_space and swap_size, since the
+           * swap itself occupies space in the filesystem
+           */
+          variable_data->value.size +=
+            priv->variables[VARIABLE_SWAP_SIZE].value.size;
+        }
+    }
+}
+
+static void
+gimp_dashboard_sample_object (GimpDashboard *dashboard,
+                              GObject       *object,
+                              Variable       variable)
+{
+  GimpDashboardPrivate *priv          = dashboard->priv;
+  GObjectClass         *klass         = G_OBJECT_GET_CLASS (object);
+  const VariableInfo   *variable_info = &variables[variable];
+  VariableData         *variable_data = &priv->variables[variable];
+
+  variable_data->available = FALSE;
+
+  switch (variable_info->type)
+    {
+    case VARIABLE_TYPE_BOOLEAN:
+      if (g_object_class_find_property (klass, variable_info->data))
+        {
+          variable_data->available = TRUE;
+
+          g_object_get (object,
+                        variable_info->data, &variable_data->value.boolean,
+                        NULL);
+        }
+      break;
+
+    case VARIABLE_TYPE_SIZE:
+      if (g_object_class_find_property (klass, variable_info->data))
+        {
+          variable_data->available = TRUE;
+
+          g_object_get (object,
+                        variable_info->data, &variable_data->value.size,
+                        NULL);
+        }
+      break;
+
+    case VARIABLE_TYPE_SIZE_RATIO:
+      {
+        const gchar *antecedent = variable_info->data;
+        const gchar *consequent = antecedent + strlen (antecedent) + 1;
+
+        if (g_object_class_find_property (klass, antecedent) &&
+            g_object_class_find_property (klass, consequent))
+          {
+            variable_data->available = TRUE;
+
+            g_object_get (object,
+                          antecedent, &variable_data->value.size_ratio.antecedent,
+                          consequent, &variable_data->value.size_ratio.consequent,
+                          NULL);
+          }
+      }
+      break;
+
+    case VARIABLE_TYPE_INT_RATIO:
+      {
+        const gchar *antecedent = variable_info->data;
+        const gchar *consequent = antecedent + strlen (antecedent) + 1;
+
+        if (g_object_class_find_property (klass, antecedent) &&
+            g_object_class_find_property (klass, consequent))
+          {
+            variable_data->available = TRUE;
+
+            g_object_get (object,
+                          antecedent, &variable_data->value.int_ratio.antecedent,
+                          consequent, &variable_data->value.int_ratio.consequent,
+                          NULL);
+          }
+      }
+      break;
+    }
+}
+
+static void
+gimp_dashboard_container_remove (GtkWidget    *widget,
+                                 GtkContainer *container)
+{
+  gtk_container_remove (container, widget);
+}
+
+static void
+gimp_dashboard_group_menu_position (GtkMenu  *menu,
+                                    gint     *x,
+                                    gint     *y,
+                                    gboolean *push_in,
+                                    gpointer  user_data)
+{
+  gimp_button_menu_position (user_data, menu, GTK_POS_LEFT, x, y);
+}
+
+static void
+gimp_dashboard_update_groups (GimpDashboard *dashboard)
+{
+  Group group;
+
+  for (group = FIRST_GROUP; group < N_GROUPS; group++)
+    gimp_dashboard_update_group (dashboard, group);
+}
+
+static void
+gimp_dashboard_update_group (GimpDashboard *dashboard,
+                             Group          group)
+{
+  GimpDashboardPrivate *priv       = dashboard->priv;
+  const GroupInfo      *group_info = &groups[group];
+  GroupData            *group_data = &priv->groups[group];
+  gint                  n_rows;
+  gboolean              add_separator;
+  gint                  field;
+
+  n_rows        = 0;
+  add_separator = FALSE;
+
+  for (field = 0; field < group_data->n_fields; field++)
+    {
+      const FieldInfo *field_info = &group_info->fields[field];
+      const FieldData *field_data = &group_data->fields[field];
+
+      if (field_info->variable != VARIABLE_SEPARATOR)
+        {
+          if (group_info->has_meter && field_info->meter_value)
+            {
+              gimp_meter_set_value_active (group_data->meter,
+                                           field_info->meter_value - 1,
+                                           field_data->active);
+            }
+
+          if (field_data->active)
+            {
+              if (add_separator)
+                {
+                  add_separator = FALSE;
+                  n_rows++;
+                }
+
+              n_rows++;
+            }
+        }
+      else
+        {
+          if (n_rows > 0)
+            add_separator = TRUE;
+        }
+    }
+
+  gtk_container_foreach (GTK_CONTAINER (group_data->table),
+                         (GtkCallback) gimp_dashboard_container_remove,
+                         group_data->table);
+  gtk_table_resize (group_data->table, MAX (n_rows, 1), 3);
+
+  n_rows        = 0;
+  add_separator = FALSE;
+
+  for (field = 0; field < group_data->n_fields; field++)
+    {
+      const FieldInfo *field_info = &group_info->fields[field];
+      FieldData       *field_data = &group_data->fields[field];
+
+      if (field_info->variable != VARIABLE_SEPARATOR)
+        {
+          const VariableInfo *variable_info = &variables[field_info->variable];
+          GtkWidget          *separator;
+          GtkWidget          *color_area;
+          GtkWidget          *label;
+          const gchar        *description;
+          gchar              *str;
+
+          if (! field_data->active)
+            continue;
+
+          description = g_dgettext (NULL, variable_info->description);
+
+          if (add_separator)
+            {
+              separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
+              gtk_table_attach (group_data->table, separator,
+                                0, 3, n_rows, n_rows + 1,
+                                GTK_EXPAND | GTK_FILL, 0,
+                                0, 0);
+              gtk_widget_show (separator);
+
+              add_separator = FALSE;
+              n_rows++;
+            }
+
+          if (variable_info->color.a)
+            {
+              color_area = gimp_color_area_new (&variable_info->color,
+                                                GIMP_COLOR_AREA_FLAT, 0);
+              gimp_help_set_help_data (color_area, description,
+                                       NULL);
+              gtk_widget_set_size_request (color_area, 5, 5);
+              gtk_table_attach (group_data->table, color_area,
+                                0, 1, n_rows, n_rows + 1,
+                                0, 0,
+                                0, 0);
+              gtk_widget_show (color_area);
+            }
+
+          str = g_strdup_printf ("%s:",
+                                 g_dpgettext2 (NULL, "dashboard-variable",
+                                               variable_info->title));
+
+          label = gtk_label_new (str);
+          gimp_help_set_help_data (label, description,
+                                   NULL);
+          gtk_label_set_xalign (GTK_LABEL (label), 0.0);
+          gtk_table_attach (group_data->table, label,
+                            1, 2, n_rows, n_rows + 1,
+                            GTK_FILL, 0,
+                            0, 0);
+          gtk_widget_show (label);
+
+          g_free (str);
+
+          label = gtk_label_new (NULL);
+          field_data->value_label = GTK_LABEL (label);
+          gimp_help_set_help_data (label, description,
+                                   NULL);
+          gtk_label_set_xalign (GTK_LABEL (label), 0.0);
+          gtk_table_attach (group_data->table, label,
+                            2, 3, n_rows, n_rows + 1,
+                            GTK_EXPAND | GTK_FILL, 0,
+                            0, 0);
+          gtk_widget_show (label);
+
+          n_rows++;
+        }
+      else
+        {
+          if (n_rows > 0)
+            add_separator = TRUE;
+        }
+    }
+
+  g_mutex_lock (&priv->mutex);
+
+  gimp_dashboard_update_group_values (dashboard, group);
+
+  g_mutex_unlock (&priv->mutex);
+}
+
+static void
+gimp_dashboard_update_group_values (GimpDashboard *dashboard,
+                                    Group          group)
+{
+  GimpDashboardPrivate *priv       = dashboard->priv;
+  const GroupInfo      *group_info = &groups[group];
+  GroupData            *group_data = &priv->groups[group];
+  gdouble               limit      = 0.0;
+  gint                  field;
+
+  if (group_info->has_meter)
+    {
+      if (group_info->meter_limit)
+        {
+          const VariableData *variable_data = &priv->variables[group_info->meter_limit];
+
+          if (variable_data->available)
+            {
+              limit = gimp_dashboard_variable_to_double (dashboard,
+                                                        group_info->meter_limit);
+            }
+          else
+            {
+              for (field = 0; field < group_data->n_fields; field++)
+                {
+                  const FieldInfo *field_info = &group_info->fields[field];
+                  const FieldData *field_data = &group_data->fields[field];
+
+                  if (field_info->meter_value && field_data->active)
+                    {
+                      gdouble value;
+
+                      value = gimp_dashboard_variable_to_double (dashboard,
+                                                                 field_info->variable);
+
+                      limit = MAX (limit, value);
+                    }
+                }
+            }
+
+          gimp_meter_set_range (group_data->meter, 0.0, limit);
+        }
+
+      if (group_info->meter_led)
+        {
+          gimp_meter_set_led_active (
+            group_data->meter,
+            gimp_dashboard_variable_to_boolean (dashboard,
+                                                group_info->meter_led));
+        }
+    }
+
+  for (field = 0; field < group_data->n_fields; field++)
+    {
+      const FieldInfo *field_info = &group_info->fields[field];
+      const FieldData *field_data = &group_data->fields[field];
+
+      if (field_data->active)
+        {
+          const VariableInfo *variable_info = &variables[field_info->variable];
+          const VariableData *variable_data = &priv->variables[field_info->variable];
+          /* Tranlators: "N/A" is an abbreviation for "not available" */
+          const gchar        *str           = C_("dashboard-value", "N/A");
+          gboolean            free_str      = FALSE;
+
+          if (variable_data->available)
+            {
+              switch (variable_info->type)
+                {
+                case VARIABLE_TYPE_BOOLEAN:
+                  str = variable_data->value.boolean ? C_("dashboard-value", "Yes") :
+                                                       C_("dashboard-value", "No");
+                  break;
+
+                case VARIABLE_TYPE_SIZE:
+                  str      = g_format_size_full (variable_data->value.size,
+                                                 G_FORMAT_SIZE_IEC_UNITS);
+                  free_str = TRUE;
+                  break;
+
+                case VARIABLE_TYPE_SIZE_RATIO:
+                  {
+                    if (variable_data->value.size_ratio.consequent)
+                      {
+                        gdouble value;
+
+                        value = 100.0 * variable_data->value.size_ratio.antecedent /
+                                        variable_data->value.size_ratio.consequent;
+
+                        str      = g_strdup_printf ("%d%%", SIGNED_ROUND (value));
+                        free_str = TRUE;
+                      }
+                  }
+                  break;
+
+                case VARIABLE_TYPE_INT_RATIO:
+                  {
+                    gdouble  min;
+                    gdouble  max;
+                    gdouble  antecedent;
+                    gdouble  consequent;
+
+                    antecedent = variable_data->value.int_ratio.antecedent;
+                    consequent = variable_data->value.int_ratio.consequent;
+
+                    min = MIN (ABS (antecedent), ABS (consequent));
+                    max = MAX (ABS (antecedent), ABS (consequent));
+
+                    if (min)
+                      {
+                        antecedent /= min;
+                        consequent /= min;
+                      }
+                    else if (max)
+                      {
+                        antecedent /= max;
+                        consequent /= max;
+                      }
+
+                    if (max)
+                      {
+                        str      = g_strdup_printf ("%g:%g",
+                                                    RINT (100.0 * antecedent) / 100.0,
+                                                    RINT (100.0 * consequent) / 100.0);
+                        free_str = TRUE;
+                      }
+                  }
+                  break;
+                }
+
+              if (field_info->meter_value && limit)
+                {
+                  gdouble  value;
+                  gchar   *tmp;
+
+                  value = gimp_dashboard_variable_to_double (dashboard,
+                                                             field_info->variable);
+
+                  tmp = g_strdup_printf ("%s (%d%%)",
+                                         str,
+                                         SIGNED_ROUND (100.0 * value / limit));
+
+                  if (free_str)
+                    g_free ((gpointer) str);
+
+                  str      = tmp;
+                  free_str = TRUE;
+                }
+            }
+
+          /* the strcmp() reduces the overhead of gtk_label_set_text() when the
+           * text hasn't changed
+           */
+          if (strcmp (gtk_label_get_text (field_data->value_label), str))
+            gtk_label_set_text (field_data->value_label, str);
+
+          if (free_str)
+            g_free ((gpointer) str);
+        }
+    }
+}
+
+static void
+gimp_dashboard_field_set_active (GimpDashboard *dashboard,
+                                 Group          group,
+                                 gint           field,
+                                 gboolean       active)
+{
+  GimpDashboardPrivate *priv       = dashboard->priv;
+  GroupData            *group_data = &priv->groups[group];
+  FieldData            *field_data = &group_data->fields[field];
+
+  if (active != field_data->active)
+    {
+      field_data->active = active;
+
+      g_signal_handlers_block_by_func (field_data->menu_item,
+                                       gimp_dashboard_group_menu_item_toggled,
+                                       dashboard);
+
+      gtk_check_menu_item_set_active (field_data->menu_item, active);
+
+      g_signal_handlers_unblock_by_func (field_data->menu_item,
+                                         gimp_dashboard_group_menu_item_toggled,
+                                         dashboard);
+    }
+}
+
+static gboolean
+gimp_dashboard_variable_to_boolean (GimpDashboard *dashboard,
+                                    Variable       variable)
+{
+  GimpDashboardPrivate *priv          = dashboard->priv;
+  const VariableInfo   *variable_info = &variables[variable];
+  const VariableData   *variable_data = &priv->variables[variable];
+
+  if (variable_data->available)
+    {
+      switch (variable_info->type)
+        {
+        case VARIABLE_TYPE_BOOLEAN:
+          return variable_data->value.boolean;
+
+        case VARIABLE_TYPE_SIZE:
+          return variable_data->value.size > 0;
+
+        case VARIABLE_TYPE_SIZE_RATIO:
+          return variable_data->value.size_ratio.antecedent != 0 &&
+                 variable_data->value.size_ratio.consequent != 0;
+
+        case VARIABLE_TYPE_INT_RATIO:
+          return variable_data->value.int_ratio.antecedent != 0 &&
+                 variable_data->value.int_ratio.consequent != 0;
+        }
+    }
+
+  return FALSE;
+}
+
+static gdouble
+gimp_dashboard_variable_to_double (GimpDashboard *dashboard,
+                                   Variable       variable)
+{
+  GimpDashboardPrivate *priv          = dashboard->priv;
+  const VariableInfo   *variable_info = &variables[variable];
+  const VariableData   *variable_data = &priv->variables[variable];
+
+  if (variable_data->available)
+    {
+      switch (variable_info->type)
+        {
+        case VARIABLE_TYPE_BOOLEAN:
+          return variable_data->value.boolean ? 1.0 : 0.0;
+
+        case VARIABLE_TYPE_SIZE:
+          return variable_data->value.size;
+
+        case VARIABLE_TYPE_SIZE_RATIO:
+          if (variable_data->value.size_ratio.consequent)
+            {
+              return (gdouble) variable_data->value.size_ratio.antecedent /
+                     (gdouble) variable_data->value.size_ratio.consequent;
+            }
+          break;
+
+        case VARIABLE_TYPE_INT_RATIO:
+          if (variable_data->value.int_ratio.consequent)
+            {
+              return (gdouble) variable_data->value.int_ratio.antecedent /
+                     (gdouble) variable_data->value.int_ratio.consequent;
+            }
+          break;
+        }
+    }
+
+  return 0.0;
 }
 
 
@@ -764,72 +1861,151 @@ gimp_dashboard_new (Gimp            *gimp,
                             "ui-path",         "/dashboard-popup",
                             NULL);
 
-  dashboard->gimp = gimp;
+  dashboard->priv->gimp = gimp;
 
   return GTK_WIDGET (dashboard);
 }
 
 void
+gimp_dashboard_reset (GimpDashboard *dashboard)
+{
+  GimpDashboardPrivate *priv;
+  Group                 group;
+
+  g_return_if_fail (GIMP_IS_DASHBOARD (dashboard));
+
+  priv = dashboard->priv;
+
+  g_mutex_lock (&priv->mutex);
+
+  gegl_reset_stats ();
+
+  for (group = FIRST_GROUP; group < N_GROUPS; group++)
+    {
+      GroupData *group_data = &priv->groups[group];
+
+      if (group_data->meter)
+        gimp_meter_clear_history (group_data->meter);
+    }
+
+  priv->update_now = TRUE;
+  g_cond_signal (&priv->cond);
+
+  g_mutex_unlock (&priv->mutex);
+}
+
+void
 gimp_dashboard_set_update_interval (GimpDashboard              *dashboard,
                                     GimpDashboardUpdateInteval  update_interval)
 {
+  GimpDashboardPrivate *priv;
+
   g_return_if_fail (GIMP_IS_DASHBOARD (dashboard));
 
-  if (update_interval != dashboard->update_interval)
+  priv = dashboard->priv;
+
+  if (update_interval != priv->update_interval)
     {
-      g_mutex_lock (&dashboard->mutex);
+      Group group;
 
-      dashboard->update_interval = update_interval;
+      g_mutex_lock (&priv->mutex);
 
-      gimp_meter_set_history_resolution (GIMP_METER (dashboard->cache_meter),
-                                         update_interval / 1000.0);
-      gimp_meter_set_history_resolution (GIMP_METER (dashboard->swap_meter),
-                                         update_interval / 1000.0);
+      priv->update_interval = update_interval;
 
-      if (dashboard->timeout_id)
+      for (group = FIRST_GROUP; group < N_GROUPS; group++)
         {
-          g_source_remove (dashboard->timeout_id);
+          GroupData *group_data = &priv->groups[group];
 
-          dashboard->timeout_id = g_timeout_add (update_interval,
-                                                 (GSourceFunc) gimp_dashboard_update,
-                                                 dashboard);
+          if (group_data->meter)
+            {
+              gimp_meter_set_history_resolution (group_data->meter,
+                                                 update_interval / 1000.0);
+            }
         }
 
-      g_cond_signal (&dashboard->cond);
+      priv->update_now = TRUE;
+      g_cond_signal (&priv->cond);
 
-      g_mutex_unlock (&dashboard->mutex);
+      g_mutex_unlock (&priv->mutex);
     }
 }
 
+GimpDashboardUpdateInteval
+gimp_dashboard_get_update_interval (GimpDashboard *dashboard)
+{
+  g_return_val_if_fail (GIMP_IS_DASHBOARD (dashboard), DEFAULT_UPDATE_INTERVAL);
+
+  return dashboard->priv->update_interval;
+}
+
 void
 gimp_dashboard_set_history_duration (GimpDashboard                *dashboard,
                                      GimpDashboardHistoryDuration  history_duration)
 {
+  GimpDashboardPrivate *priv;
+
   g_return_if_fail (GIMP_IS_DASHBOARD (dashboard));
 
-  if (history_duration != dashboard->history_duration)
+  priv = dashboard->priv;
+
+  if (history_duration != priv->history_duration)
     {
-      dashboard->history_duration = history_duration;
+      Group group;
+
+      g_mutex_lock (&priv->mutex);
+
+      priv->history_duration = history_duration;
 
-      gimp_meter_set_history_duration (GIMP_METER (dashboard->cache_meter),
-                                       history_duration / 1000.0);
-      gimp_meter_set_history_duration (GIMP_METER (dashboard->swap_meter),
-                                       history_duration / 1000.0);
+      for (group = FIRST_GROUP; group < N_GROUPS; group++)
+        {
+          GroupData *group_data = &priv->groups[group];
+
+          if (group_data->meter)
+            {
+              gimp_meter_set_history_duration (group_data->meter,
+                                               history_duration / 1000.0);
+            }
+        }
+
+      priv->update_now = TRUE;
+      g_cond_signal (&priv->cond);
+
+      g_mutex_unlock (&priv->mutex);
     }
 }
 
+GimpDashboardHistoryDuration
+gimp_dashboard_get_history_duration (GimpDashboard *dashboard)
+{
+  g_return_val_if_fail (GIMP_IS_DASHBOARD (dashboard), DEFAULT_HISTORY_DURATION);
+
+  return dashboard->priv->history_duration;
+}
+
 void
 gimp_dashboard_set_low_swap_space_warning (GimpDashboard *dashboard,
                                            gboolean       low_swap_space_warning)
 {
+  GimpDashboardPrivate *priv;
+
   g_return_if_fail (GIMP_IS_DASHBOARD (dashboard));
 
-  if (low_swap_space_warning != dashboard->low_swap_space_warning)
+  priv = dashboard->priv;
+
+  if (low_swap_space_warning != priv->low_swap_space_warning)
     {
-      g_mutex_lock (&dashboard->mutex);
+      g_mutex_lock (&priv->mutex);
 
-      dashboard->low_swap_space_warning = low_swap_space_warning;
+      priv->low_swap_space_warning = low_swap_space_warning;
 
-      g_mutex_unlock (&dashboard->mutex);
+      g_mutex_unlock (&priv->mutex);
     }
 }
+
+gboolean
+gimp_dashboard_get_low_swap_space_warning (GimpDashboard *dashboard)
+{
+  g_return_val_if_fail (GIMP_IS_DASHBOARD (dashboard), DEFAULT_LOW_SWAP_SPACE_WARNING);
+
+  return dashboard->priv->low_swap_space_warning;
+}
diff --git a/app/widgets/gimpdashboard.h b/app/widgets/gimpdashboard.h
index dc6ee3a..e8e9249 100644
--- a/app/widgets/gimpdashboard.h
+++ b/app/widgets/gimpdashboard.h
@@ -33,34 +33,14 @@
 #define GIMP_DASHBOARD_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), GIMP_TYPE_DASHBOARD, 
GimpDashboardClass))
 
 
-typedef struct _GimpDashboardClass GimpDashboardClass;
+typedef struct _GimpDashboardPrivate GimpDashboardPrivate;
+typedef struct _GimpDashboardClass   GimpDashboardClass;
 
 struct _GimpDashboard
 {
-  GimpEditor                    parent_instance;
+  GimpEditor            parent_instance;
 
-  Gimp                         *gimp;
-
-  GtkWidget                    *cache_meter;
-  GtkWidget                    *cache_occupied_label;
-  GtkWidget                    *cache_limit_label;
-
-  GtkWidget                    *swap_meter;
-  GtkWidget                    *swap_occupied_label;
-  GtkWidget                    *swap_size_label;
-  GtkWidget                    *swap_limit_label;
-
-  gint                          timeout_id;
-  gint                          low_swap_space_idle_id;
-
-  GThread                      *thread;
-  GMutex                        mutex;
-  GCond                         cond;
-  gboolean                      quit;
-
-  GimpDashboardUpdateInteval    update_interval;
-  GimpDashboardHistoryDuration  history_duration;
-  gboolean                      low_swap_space_warning;
+  GimpDashboardPrivate *priv;
 };
 
 struct _GimpDashboardClass
@@ -69,17 +49,24 @@ struct _GimpDashboardClass
 };
 
 
-GType       gimp_dashboard_get_type                   (void) G_GNUC_CONST;
+GType                          gimp_dashboard_get_type                   (void) G_GNUC_CONST;
+
+GtkWidget                    * gimp_dashboard_new                        (Gimp                         *gimp,
+                                                                          GimpMenuFactory              
*menu_factory);
+
+void                           gimp_dashboard_reset                      (GimpDashboard                
*dashboard);
+
+void                           gimp_dashboard_set_update_interval        (GimpDashboard                
*dashboard,
+                                                                          GimpDashboardUpdateInteval    
update_interval);
+GimpDashboardUpdateInteval     gimp_dashboard_get_update_interval        (GimpDashboard                
*dashboard);
 
-GtkWidget * gimp_dashboard_new                        (Gimp                         *gimp,
-                                                       GimpMenuFactory              *menu_factory);
+void                           gimp_dashboard_set_history_duration       (GimpDashboard                
*dashboard,
+                                                                          GimpDashboardHistoryDuration  
history_duration);
+GimpDashboardHistoryDuration   gimp_dashboard_get_history_duration       (GimpDashboard                
*dashboard);
 
-void        gimp_dashboard_set_update_interval        (GimpDashboard                *dashboard,
-                                                       GimpDashboardUpdateInteval    update_interval);
-void        gimp_dashboard_set_history_duration       (GimpDashboard                *dashboard,
-                                                       GimpDashboardHistoryDuration  history_duration);
-void        gimp_dashboard_set_low_swap_space_warning (GimpDashboard                *dashboard,
-                                                       gboolean                      low_swap_space_warning);
+void                           gimp_dashboard_set_low_swap_space_warning (GimpDashboard                
*dashboard,
+                                                                          gboolean                      
low_swap_space_warning);
+gboolean                       gimp_dashboard_get_low_swap_space_warning (GimpDashboard                
*dashboard);
 
 
 #endif  /*  __GIMP_DASHBOARD_H__  */
diff --git a/app/widgets/gimphelp-ids.h b/app/widgets/gimphelp-ids.h
index f8cec31..19f55cd 100644
--- a/app/widgets/gimphelp-ids.h
+++ b/app/widgets/gimphelp-ids.h
@@ -665,6 +665,7 @@
 #define GIMP_HELP_DASHBOARD_DIALOG                "gimp-dashboard-dialog"
 #define GIMP_HELP_DASHBOARD_UPDATE_INTERVAL       "gimp-dashboard-update-interval"
 #define GIMP_HELP_DASHBOARD_HISTORY_DURATION      "gimp-dashboard-history-duration"
+#define GIMP_HELP_DASHBOARD_RESET                 "gimp-dashboard-reset"
 #define GIMP_HELP_DASHBOARD_LOW_SWAP_SPACE_WARNING "gimp-dashboard-low-swap-space-warning"
 
 #define GIMP_HELP_DOCK                            "gimp-dock"
diff --git a/menus/dashboard-menu.xml b/menus/dashboard-menu.xml
index 9bdc05c..30548a7 100644
--- a/menus/dashboard-menu.xml
+++ b/menus/dashboard-menu.xml
@@ -17,6 +17,7 @@
       <menuitem action="dashboard-history-duration-120-sec" />
       <menuitem action="dashboard-history-duration-240-sec" />
     </menu>
+    <menuitem action="dashboard-reset" />
     <separator />
     <menuitem action="dashboard-low-swap-space-warning" />
   </popup>



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