[Gnucash-changes] New files for Chris Shoemaker's rewrite of budgeting functionality.

David Hampton hampton at cvs.gnucash.org
Thu Oct 27 20:40:44 EDT 2005


Log Message:
-----------
New files for Chris Shoemaker's rewrite of budgeting functionality.

Tags:
----
gnucash-gnome2-dev

Added Files:
-----------
    gnucash/src/backend/file:
        gnc-budget-xml-v2.c
        gnc-recurrence-xml-v2.c
    gnucash/src/gnome:
        gnc-plugin-budget.c
        gnc-plugin-budget.h
        gnc-plugin-page-budget.c
        gnc-plugin-page-budget.h
        gncmod-budget.c
    gnucash/src/gnome/ui:
        gnc-plugin-budget-ui.xml
        gnc-plugin-page-budget-ui.xml
    gnucash/src/gnome-utils:
        gnc-dialog.c
        gnc-dialog.h
        gnc-recurrence.c
        gnc-recurrence.h
        gnc-tree-model-budget.c
        gnc-tree-model-budget.h
    gnucash/src/gnome-utils/test:
        test-gnc-dialog.c
        test-gnc-recurrence.c
    gnucash/src/report/standard-reports:
        budget.scm

Revision Data
-------------
--- /dev/null
+++ src/backend/file/gnc-recurrence-xml-v2.c
@@ -0,0 +1,128 @@
+/*
+ * gnc-recurrence-xml-v2.c -- xml routines for Recurrence
+ *
+ * Copyright (C) 2005 Chris Shoemaker <c.shoemaker at cox.net>
+ *
+ * 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, contact:
+ *
+ * Free Software Foundation           Voice:  +1-617-542-5942
+ * 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652
+ * Boston, MA  02110-1301,  USA       gnu at gnu.org
+ */
+
+
+#include "config.h"
+
+#include <glib.h>
+#include <string.h>
+
+#include "gnc-xml.h"
+#include "gnc-xml-helper.h"
+#include "gnc-engine-util.h"
+#include "gnc-trace.h"
+
+#include "sixtp.h"
+#include "sixtp-utils.h"
+#include "sixtp-parsers.h"
+#include "sixtp-utils.h"
+#include "sixtp-dom-parsers.h"
+#include "sixtp-dom-generators.h"
+#include "io-gncxml-v2.h"
+#include "Recurrence.h"
+
+static QofLogModule log_module = GNC_MOD_IO;
+
+const gchar *recurrence_version_string = "1.0.0";
+#define recurrence_root          "gnc:recurrence"
+#define recurrence_mult          "recurrence:mult"
+#define recurrence_period_type   "recurrence:period_type"
+#define recurrence_start         "recurrence:start"
+
+//TODO: I think three of these functions rightly belong in Recurrence.c.
+
+static gboolean
+recurrence_period_type_handler(xmlNodePtr node, gpointer d)
+{
+    PeriodType pt;
+    char *nodeTxt;
+
+    nodeTxt = dom_tree_to_text(node);
+    g_return_val_if_fail(nodeTxt, FALSE);
+    pt = recurrencePeriodTypeFromString(nodeTxt);
+    ((Recurrence *) d)->ptype = pt;
+    g_free(nodeTxt);
+    return (pt != -1);
+}
+
+static gboolean
+recurrence_start_date_handler(xmlNodePtr node, gpointer r)
+{
+    GDate *d;
+
+    d = dom_tree_to_gdate(node);
+    g_return_val_if_fail(d, FALSE);
+    g_return_val_if_fail(g_date_valid(d), FALSE);
+    ((Recurrence *) r)->start = *d;
+    g_date_free(d);
+    return TRUE;
+}
+
+static gboolean
+recurrence_mult_handler(xmlNodePtr node, gpointer r)
+{
+    return dom_tree_to_guint16(node, &((Recurrence *)r)->mult);
+}
+
+static struct dom_tree_handler recurrence_dom_handlers[] = {
+    { recurrence_mult, recurrence_mult_handler, 1, 0 },
+    { recurrence_period_type, recurrence_period_type_handler, 1, 0 },
+    { recurrence_start, recurrence_start_date_handler, 1, 0 },
+    { NULL, NULL, 0, 0 }
+};
+
+Recurrence *
+dom_tree_to_recurrence(xmlNodePtr node)
+{
+    gboolean successful;
+    Recurrence *r;
+
+    r = g_new(Recurrence, 1);
+    successful = dom_tree_generic_parse (node, recurrence_dom_handlers, r);
+    if (!successful) {
+        PERR ("failed to parse recurrence node");
+        xmlElemDump(stdout, NULL, node);
+        g_free(r);
+        r = NULL;
+    }
+    return r;
+}
+
+xmlNodePtr
+recurrence_to_dom_tree(const gchar *tag, const Recurrence *r)
+{
+    xmlNodePtr n;
+    PeriodType pt;
+    GDate d;
+
+    n = xmlNewNode(NULL, tag);
+    xmlSetProp(n, "version", recurrence_version_string );
+    xmlAddChild(n, guint_to_dom_tree(recurrence_mult,
+                                     recurrenceGetMultiplier(r)));
+    pt = recurrenceGetPeriodType(r);
+    xmlAddChild(n, text_to_dom_tree(recurrence_period_type,
+                                    recurrencePeriodTypeToString(pt)));
+    d = recurrenceGetDate(r);
+    xmlAddChild(n, gdate_to_dom_tree(recurrence_start, &d));
+    return n;
+}
--- /dev/null
+++ src/backend/file/gnc-budget-xml-v2.c
@@ -0,0 +1,226 @@
+/*
+ * gnc-budget-xml-v2.c -- budget xml i/o implementation
+ *
+ * Copyright (C) 2005 Chris Shoemaker <c.shoemaker at cox.net>
+ *
+ * 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, contact:
+ *
+ * Free Software Foundation           Voice:  +1-617-542-5942
+ * 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652
+ * Boston, MA  02110-1301,  USA       gnu at gnu.org
+ */
+
+
+#include "config.h"
+
+#include <glib.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "gnc-xml-helper.h"
+#include "sixtp.h"
+#include "sixtp-utils.h"
+#include "sixtp-parsers.h"
+#include "sixtp-utils.h"
+#include "sixtp-dom-parsers.h"
+#include "sixtp-dom-generators.h"
+
+#include "gnc-xml.h"
+#include "io-gncxml-gen.h"
+#include "io-gncxml-v2.h"
+
+static QofLogModule log_module = GNC_MOD_IO;
+
+const gchar *budget_version_string = "2.0.0";
+
+/* ids */
+#define gnc_budget_string       "gnc:budget"
+#define bgt_id_string           "bgt:id"
+#define bgt_name_string         "bgt:name"
+#define bgt_description_string  "bgt:description"
+#define bgt_num_periods_string  "bgt:num-periods"
+#define bgt_recurrence_string   "bgt:recurrence"
+#define bgt_slots_string        "bgt:slots"
+
+xmlNodePtr
+gnc_budget_dom_tree_create(GncBudget *bgt)
+{
+    xmlNodePtr ret;
+    KvpFrame *kf;
+
+    ENTER ("(budget=%p)", bgt);
+
+    ret = xmlNewNode(NULL, gnc_budget_string);
+    xmlSetProp(ret, "version", budget_version_string);
+
+    /* field: GUID */
+    xmlAddChild(ret, guid_to_dom_tree(bgt_id_string,
+                                      gnc_budget_get_guid(bgt)));
+    /* field: char* name */
+    xmlAddChild(ret, text_to_dom_tree(bgt_name_string,
+                                      gnc_budget_get_name(bgt)));
+    /* field: char* description */
+    xmlAddChild(ret, text_to_dom_tree(bgt_description_string,
+				      gnc_budget_get_description(bgt)));
+    /* field: guint num_periods */
+    xmlAddChild(ret, guint_to_dom_tree(bgt_num_periods_string,
+                                       gnc_budget_get_num_periods(bgt)));
+    /* field: Recurrence*  */
+    xmlAddChild(ret, recurrence_to_dom_tree(bgt_recurrence_string,
+                                            gnc_budget_get_recurrence(bgt)));
+    /* slots */
+    kf = qof_instance_get_slots(QOF_INSTANCE(bgt));
+    if (kf) {
+        xmlNodePtr kvpnode = kvp_frame_to_dom_tree(bgt_slots_string, kf);
+        if (kvpnode)
+            xmlAddChild(ret, kvpnode);
+    }
+
+    LEAVE (" ");
+    return ret;
+}
+
+/***********************************************************************/
+static inline gboolean
+set_string(xmlNodePtr node, GncBudget* bgt,
+	       void (*func)(GncBudget *bgt, const gchar *txt))
+{
+    gchar* txt = dom_tree_to_text(node);
+    g_return_val_if_fail(txt, FALSE);
+
+    func(bgt, txt);
+    g_free(txt);
+    return TRUE;
+}
+
+static gboolean
+budget_id_handler (xmlNodePtr node, gpointer bgt)
+{
+    GUID *guid;
+
+    guid = dom_tree_to_guid(node);
+    g_return_val_if_fail(guid, FALSE);
+    qof_entity_set_guid(QOF_ENTITY(bgt), guid);
+    g_free(guid);
+    return TRUE;
+}
+
+static gboolean
+budget_name_handler (xmlNodePtr node, gpointer bgt)
+{
+    return set_string(node, GNC_BUDGET(bgt), gnc_budget_set_name);
+}
+
+static gboolean
+budget_description_handler (xmlNodePtr node, gpointer bgt)
+{
+    return set_string(node, GNC_BUDGET(bgt), gnc_budget_set_description);
+}
+
+static gboolean
+budget_num_periods_handler (xmlNodePtr node, gpointer bgt)
+{
+    guint num_periods;
+
+    if (dom_tree_to_guint(node, &num_periods)) {
+        gnc_budget_set_num_periods(GNC_BUDGET(bgt), num_periods);
+        return TRUE;
+    } else
+        return FALSE;
+}
+
+static gboolean
+budget_recurrence_handler (xmlNodePtr node, gpointer bgt)
+{
+  Recurrence *r;
+
+  if ((r = dom_tree_to_recurrence(node)) == NULL)
+      return FALSE;
+
+  gnc_budget_set_recurrence(GNC_BUDGET(bgt), r);
+  g_free(r);
+  return TRUE;
+}
+
+static gboolean
+budget_slots_handler (xmlNodePtr node, gpointer bgt)
+{
+    return dom_tree_to_kvp_frame_given(
+        node, qof_instance_get_slots(QOF_INSTANCE(bgt)));
+}
+
+static struct dom_tree_handler budget_handlers[] = {
+    { bgt_id_string, budget_id_handler, 1, 0 },
+    { bgt_name_string, budget_name_handler, 0, 0 },
+    { bgt_description_string, budget_description_handler, 0, 0 },
+    { bgt_num_periods_string, budget_num_periods_handler, 1, 0 },
+    { bgt_recurrence_string, budget_recurrence_handler, 1, 0 },
+    { bgt_slots_string, budget_slots_handler, 0, 0},
+    { NULL, 0, 0, 0 }
+};
+
+static gboolean
+gnc_budget_end_handler(gpointer data_for_children,
+                       GSList* data_from_children, GSList* sibling_data,
+                       gpointer parent_data, gpointer global_data,
+                       gpointer *result, const gchar *tag)
+{
+    GncBudget *bgt;
+    xmlNodePtr tree = (xmlNodePtr)data_for_children;
+    gxpf_data *gdata = (gxpf_data*)global_data;
+    QofBook *book = gdata->bookdata;
+
+    if (parent_data) {
+        return TRUE;
+    }
+
+    /* OK.  For some messed up reason this is getting called again with a
+       NULL tag.  So we ignore those cases */
+    if(!tag) {
+        return TRUE;
+    }
+
+    g_return_val_if_fail(tree, FALSE);
+
+    bgt = dom_tree_to_budget(tree, book);
+    xmlFreeNode(tree);
+    if(bgt != NULL) {
+        /* ends up calling book_callback */
+        gdata->cb(tag, gdata->parsedata, bgt);
+    }
+
+    return bgt != NULL;
+}
+
+
+GncBudget*
+dom_tree_to_budget (xmlNodePtr node, QofBook *book)
+{
+    GncBudget *bgt;
+
+    bgt = gnc_budget_new(book);
+    if (!dom_tree_generic_parse (node, budget_handlers, bgt)) {
+        PERR ("failed to parse budget tree");
+        gnc_budget_free(bgt);
+        bgt = NULL;
+    }
+    return bgt;
+}
+
+sixtp*
+gnc_budget_sixtp_parser_create(void)
+{
+    return sixtp_dom_parser_new(gnc_budget_end_handler, NULL, NULL);
+}
+/* ======================  END OF FILE ===================*/
--- /dev/null
+++ src/gnome/gncmod-budget.c
@@ -0,0 +1,71 @@
+/*********************************************************************
+ * Copyright (C) 2005 Chris Shoemaker <c.shoemaker at cox.net>         *
+ *
+ * gncmod-budget.c
+ * module definition/initialization for budget
+ *
+ * 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, contact:
+ *
+ * Free Software Foundation           Voice:  +1-617-542-5942
+ * 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652
+ * Boston, MA  02110-1301,  USA       gnu at gnu.org
+ */
+
+#include "config.h"
+#include <stdio.h>
+#include <glib.h>
+#include <libguile.h>
+
+#include "gnc-module.h"
+#include "gnc-module-api.h"
+#include "gnc-plugin-budget.h"
+
+/* version of the gnc module system interface we require */
+int libgncmod_budget_gnc_module_system_interface = 0;
+
+/* module versioning uses libtool semantics. */
+int libgncmod_budget_gnc_module_current  = 0;
+int libgncmod_budget_gnc_module_revision = 0;
+int libgncmod_budget_gnc_module_age      = 0;
+
+/* forward references */
+char *libgncmod_budget_gnc_module_path(void);
+char *libgncmod_budegt_gnc_module_description(void);
+int libgncmod_budget_gnc_module_init(int refcount);
+int libgncmod_budget_gnc_module_end(int refcount);
+
+char * libgncmod_budget_gnc_module_path(void) {
+  return g_strdup("gnucash/gnome/");
+}
+
+char * libgncmod_budget_gnc_module_description(void) {
+  return g_strdup("Support for Budgets");
+}
+
+int libgncmod_budget_gnc_module_init(int refcount)
+{
+  /* load the engine (we depend on it) */
+  if(!gnc_module_load("gnucash/engine", 0)) {
+    return FALSE;
+  }
+
+  /* Add menu items with C callbacks */
+  gnc_plugin_budget_create_plugin();
+
+  return TRUE;
+}
+
+int libgncmod_budget_gnc_module_end(int refcount) {
+  return TRUE;
+}
--- /dev/null
+++ src/gnome/gnc-plugin-page-budget.h
@@ -0,0 +1,77 @@
+/* Copyright (C) 2005 Chris Shoemaker <c.shoemaker at cox.net>
+ *
+ * gnc-plugin-page-budget.h --
+ *   (based on gnc-plugin-page-account-tree.h)
+ *
+ * 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, contact:
+ *
+ * Free Software Foundation           Voice:  +1-617-542-5942
+ * 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652
+ * Boston, MA  02110-1301,  USA       gnu at gnu.org
+ */
+
+/** @addtogroup UI
+    @{ */
+/** @file gnc-plugin-page-budget.h
+    @brief
+*/
+
+#ifndef __GNC_PLUGIN_PAGE_BUDGET_H
+#define __GNC_PLUGIN_PAGE_BUDGET_H
+
+#include <gtk/gtkwindow.h>
+
+#include "gnc-plugin-page.h"
+#include "gnc-budget.h"
+
+G_BEGIN_DECLS
+
+/* type macros */
+#define GNC_TYPE_PLUGIN_PAGE_BUDGET            (gnc_plugin_page_budget_get_type ())
+#define GNC_PLUGIN_PAGE_BUDGET(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), GNC_TYPE_PLUGIN_PAGE_BUDGET, GncPluginPageBudget))
+#define GNC_PLUGIN_PAGE_BUDGET_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), GNC_TYPE_PLUGIN_PAGE_BUDGET, GncPluginPageBudgetClass))
+#define GNC_IS_PLUGIN_PAGE_BUDGET(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GNC_TYPE_PLUGIN_PAGE_BUDGET))
+#define GNC_IS_PLUGIN_PAGE_BUDGET_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GNC_TYPE_PLUGIN_PAGE_BUDGET))
+#define GNC_PLUGIN_PAGE_BUDGET_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), GNC_TYPE_PLUGIN_PAGE_BUDGET, GncPluginPageBudgetClass))
+
+#define GNC_PLUGIN_PAGE_BUDGET_NAME "gnc-plugin-page-budget"
+
+/* typedefs & structures */
+typedef struct GncPluginPageBudgetPrivate GncPluginPageBudgetPrivate;
+
+typedef struct {
+	GncPluginPage parent;
+	GncPluginPageBudgetPrivate *priv;
+} GncPluginPageBudget;
+
+typedef struct {
+    GncPluginPageClass parent;
+} GncPluginPageBudgetClass;
+
+/* function prototypes */
+GType gnc_plugin_page_budget_get_type (void);
+
+
+/** Create a new "budget" plugin page.
+ *
+ *  @return The newly created plugin page.
+ */
+GncPluginPage *gnc_plugin_page_budget_new  (GncBudget *budget);
+
+void gnc_budget_gui_delete_budget(GncBudget *budget);
+
+G_END_DECLS
+
+#endif /* __GNC_PLUGIN_PAGE_BUDGET_H */
+/** @} */
--- /dev/null
+++ src/gnome/gnc-plugin-budget.c
@@ -0,0 +1,277 @@
+/* Copyright (C) 2005 Chris Shoemaker <c.shoemaker at cox.net>
+ *
+ * gnc-plugin-budget.c --
+ *   (based on gnc-plugin-account-tree.c)
+ *
+ * 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, contact:
+ *
+ * Free Software Foundation           Voice:  +1-617-542-5942
+ * 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652
+ * Boston, MA  02110-1301,  USA       gnu at gnu.org
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include "gnc-plugin-budget.h"
+#include "gnc-plugin-page-budget.h"
+#include "gnc-tree-model-budget.h"
+
+#include "gnc-trace.h"
+#include "gnc-ui-util.h"
+#include "gnc-ui.h"
+#include "gnc-component-manager.h"
+
+#include "messages.h"
+
+#define PLUGIN_ACTIONS_NAME "gnc-plugin-budget-actions"
+#define PLUGIN_UI_FILENAME  "gnc-plugin-budget-ui.xml"
+
+static QofLogModule log_module = GNC_MOD_GUI;
+
+static void gnc_plugin_budget_class_init (GncPluginBudgetClass *klass);
+static void gnc_plugin_budget_init (GncPluginBudget *plugin);
+static void gnc_plugin_budget_finalize (GObject *object);
+
+/* Command Callbacks */
+static void gnc_plugin_budget_cmd_new_budget (GtkAction *action,
+					      GncMainWindowActionData *data);
+static void gnc_plugin_budget_cmd_open_budget (GtkAction *action,
+					      GncMainWindowActionData *data);
+
+#if 0
+/* plugin window interface */
+static GncPluginPage *gnc_plugin_budget_create_page (GncPlugin *plugin,
+						     const gchar *uri);
+#endif
+
+static GtkActionEntry gnc_plugin_actions [] = {
+    { "NewBudgetAction", NULL, N_("New Budget"), NULL,
+      N_("Create a new Budget"),
+      G_CALLBACK (gnc_plugin_budget_cmd_new_budget) },
+
+    { "OpenBudgetAction", NULL, N_("Open Budget"), NULL,
+      N_("Open an existing Budget"),
+      G_CALLBACK (gnc_plugin_budget_cmd_open_budget) },
+};
+static guint gnc_plugin_n_actions = G_N_ELEMENTS (gnc_plugin_actions);
+
+struct GncPluginBudgetPrivate {
+    gpointer dummy;
+};
+
+static GObjectClass *parent_class = NULL;
+
+GType
+gnc_plugin_budget_get_type (void)
+{
+    static GType gnc_plugin_budget_type = 0;
+
+    if (!gnc_plugin_budget_type) {
+        static const GTypeInfo our_info = {
+            sizeof (GncPluginBudgetClass),
+            NULL,		/* base_init */
+            NULL,		/* base_finalize */
+            (GClassInitFunc) gnc_plugin_budget_class_init,
+            NULL,		/* class_finalize */
+            NULL,		/* class_data */
+            sizeof (GncPluginBudget),
+            0,		/* n_preallocs */
+            (GInstanceInitFunc) gnc_plugin_budget_init
+        };
+
+        gnc_plugin_budget_type = g_type_register_static(
+            GNC_TYPE_PLUGIN, "GncPluginBudget", &our_info, 0);
+    }
+
+    return gnc_plugin_budget_type;
+}
+
+GncPlugin * gnc_plugin_budget_new (void)
+{
+    GncPluginBudget *plugin;
+    ENTER(" ");
+    plugin = g_object_new (GNC_TYPE_PLUGIN_BUDGET, NULL);
+    LEAVE(" ");
+    return GNC_PLUGIN (plugin);
+}
+
+static void
+gnc_plugin_budget_class_init (GncPluginBudgetClass *klass)
+{
+    GObjectClass *object_class = G_OBJECT_CLASS (klass);
+    GncPluginClass *plugin_class = GNC_PLUGIN_CLASS (klass);
+
+    ENTER (" ");
+    parent_class = g_type_class_peek_parent (klass);
+    object_class->finalize = gnc_plugin_budget_finalize;
+
+    /* CAS: I'm still unsure how much needs to be overridden here. */
+
+    /* function overrides */
+    //plugin_class->create_page  = gnc_plugin_budget_create_page;
+
+    plugin_class->plugin_name  = GNC_PLUGIN_BUDGET_NAME;
+    plugin_class->actions_name = PLUGIN_ACTIONS_NAME;
+    plugin_class->actions      = gnc_plugin_actions;
+    plugin_class->n_actions    = gnc_plugin_n_actions;
+    plugin_class->ui_filename  = PLUGIN_UI_FILENAME;
+
+    LEAVE (" ");
+}
+
+static void
+gnc_plugin_budget_init(GncPluginBudget *plugin)
+{
+    plugin->priv = g_new0(GncPluginBudgetPrivate, 1);
+}
+
+static void
+gnc_plugin_budget_finalize(GObject *object)
+{
+    GncPluginBudget *plugin = GNC_PLUGIN_BUDGET(object);
+
+    g_return_if_fail(GNC_IS_PLUGIN_BUDGET (object));
+    g_return_if_fail(plugin->priv != NULL);
+    ENTER(" ");
+    g_free (plugin->priv);
+    (parent_class->finalize)(object);
+    ENTER(" ");
+
+}
+
+/************************************************************
+ *              Plugin Function Implementation              *
+ ************************************************************/
+
+#if 0
+static GncPluginPage *
+gnc_plugin_budget_create_page (GncPlugin *plugin,
+			       const gchar *uri)
+{
+    g_return_val_if_fail (GNC_IS_PLUGIN_BUDGET (plugin), NULL);
+    g_return_val_if_fail (uri != NULL, NULL);
+
+    ENTER("");
+    LEAVE("");
+    /* FIXME add better URI handling */
+    if (strcmp ("default:", uri)) {
+        return NULL;
+    }
+
+    return NULL;
+}
+#endif
+
+
+/************************************************************
+ *                    Command Callbacks                     *
+ ************************************************************/
+
+/* Make a new budget; put it in a page; open the page. */
+static void
+gnc_plugin_budget_cmd_new_budget (GtkAction *action,
+				  GncMainWindowActionData *data)
+{
+    GncBudget *budget;
+    GncPluginPage *page;
+
+    g_return_if_fail (data != NULL);
+
+    budget = gnc_budget_new(gnc_get_current_book());
+    page = gnc_plugin_page_budget_new(budget);
+    gnc_main_window_open_page (data->window, page);
+}
+
+static void just_get_one(QofEntity *ent, gpointer data)
+{
+    GncBudget **bgt = (GncBudget**)data;
+    if (bgt && !*bgt) *bgt = GNC_BUDGET(ent);
+}
+
+/* If only one budget exists, open it; otherwise user selects one to open */
+static void
+gnc_plugin_budget_cmd_open_budget (GtkAction *action,
+                                   GncMainWindowActionData *data)
+{
+    guint count;
+    QofBook *book;
+    GncBudget *bgt;
+    QofCollection *col;
+    g_return_if_fail (data != NULL);
+
+    book = gnc_get_current_book();
+    col = qof_book_get_collection(book, GNC_ID_BUDGET);
+    count = qof_collection_count(col);
+    if (count > 0) {
+        if (count == 1) {
+            qof_collection_foreach(col, just_get_one, &bgt);
+        } else {
+            bgt = gnc_budget_gui_select_budget(book);
+        }
+
+        if (bgt) gnc_main_window_open_page(
+            data->window, gnc_plugin_page_budget_new(bgt));
+    } else { /* if no budgets exist yet, just open a new budget */
+        gnc_plugin_budget_cmd_new_budget(action, data);
+    }
+}
+
+/************************************************************
+ *                     Other Functions                      *
+ ************************************************************/
+
+GncBudget *
+gnc_budget_gui_select_budget(QofBook *book)
+{
+    GncBudget *bgt;
+    GtkDialog *dlg;
+    GtkTreeView *tv;
+    GtkTreeIter iter;
+    GtkTreeSelection *sel;
+    GtkTreeModel *tm;
+    gint response;
+    gboolean ok;
+
+    dlg = GTK_DIALOG(gtk_dialog_new_with_buttons(
+                         "Select a Budget", NULL, GTK_DIALOG_MODAL,
+                         GTK_STOCK_OK, GTK_RESPONSE_OK,
+                         GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, NULL));
+
+    tv = GTK_TREE_VIEW(gtk_tree_view_new());
+    sel = gtk_tree_view_get_selection(tv);
+    gtk_tree_selection_set_mode(sel, GTK_SELECTION_BROWSE);
+    tm = gnc_tree_model_budget_new(book);
+    gnc_tree_view_budget_set_model(tv, tm);
+    gtk_container_add(GTK_CONTAINER(dlg->vbox), GTK_WIDGET(tv));
+    gtk_widget_show_all(GTK_WIDGET(dlg));
+
+    bgt = NULL;
+    response = gtk_dialog_run(dlg);
+    switch (response) {
+    case GTK_RESPONSE_OK:
+        ok = gtk_tree_selection_get_selected(sel, &tm, &iter);
+        if (ok) {
+            bgt = gnc_tree_model_budget_get_budget(tm, &iter);
+        }
+        break;
+    default:
+        break;
+    }
+
+    gtk_widget_destroy(GTK_WIDGET(dlg));
+    return bgt;
+}
+
--- /dev/null
+++ src/gnome/gnc-plugin-page-budget.c
@@ -0,0 +1,824 @@
+/*
+ * gnc-plugin-page-budget.c --
+ *
+ * Copyright (C) 2005 Chris Shoemaker <c.shoemaker at cox.net>
+ *   (based on gnc-plugin-page-account-tree.c)
+ *
+ * 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, contact:
+ *
+ * Free Software Foundation           Voice:  +1-617-542-5942
+ * 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652
+ * Boston, MA  02110-1301,  USA       gnu at gnu.org
+ */
+
+
+/*
+ * TODO:
+ *
+ * *) I'd like to be able to update the budget estimates on a per cell
+ * basis, instead of a whole row (account) at one time.  But, that
+ * would require some major coding.
+ *
+ * *) Right now, the account-type filter is not saved anywhere.  Where
+ * should it be saved?  Per budget?  Gconf?
+ *
+ *
+ */
+
+#include "config.h"
+
+#include <gtk/gtk.h>
+#include <glade/glade.h>
+
+#include "gnc-plugin-page-register.h"
+#include "gnc-budget.h"
+
+#include "dialog-options.h"
+#include "dialog-utils.h"
+#include "gnc-gnome-utils.h"
+#include "gnc-html.h"
+#include "gnc-icons.h"
+#include "gnc-plugin-page-budget.h"
+#include "gnc-plugin-budget.h"
+
+#include "gnc-session.h"
+#include "gnc-tree-view-account.h"
+#include "gnc-ui.h"
+#include "gnc-ui-util.h"
+#include "option-util.h"
+#include "libguile.h"
+#include "gnc-main-window.h"
+#include "gnc-component-manager.h"
+
+#include "messages.h"
+#include "gnc-engine-util.h"
+#include "gnc-date.h"
+#include "gnc-trace.h"
+
+#include "gnc-dialog.h"
+#include "gnc-recurrence.h"
+#include "Recurrence.h"
+#include "gnc-tree-model-account-types.h"
+
+/* This static indicates the debugging module that this .o belongs to.  */
+static QofLogModule log_module = GNC_MOD_BUDGET;
+
+#define PLUGIN_PAGE_BUDGET_CM_CLASS "plugin-page-budget"
+
+/************************************************************
+ *                        Prototypes                        *
+ ************************************************************/
+/* Plugin Actions */
+static void
+gnc_plugin_page_budget_class_init (GncPluginPageBudgetClass *klass);
+static void gnc_plugin_page_budget_init (GncPluginPageBudget *plugin_page);
+static void gnc_plugin_page_budget_finalize (GObject *object);
+
+static GtkWidget *
+gnc_plugin_page_budget_create_widget (GncPluginPage *plugin_page);
+static void gnc_plugin_page_budget_destroy_widget (GncPluginPage *plugin_page);
+
+static gboolean gnc_plugin_page_budget_button_press_cb(
+    GtkWidget *widget, GdkEventButton *event, GncPluginPage *page);
+static void gnc_plugin_page_budget_double_click_cb(
+    GtkTreeView *treeview, GtkTreePath *path, GtkTreeViewColumn *col,
+    GncPluginPageBudget *page);
+
+static void gnc_plugin_page_budget_view_refresh (GncPluginPageBudget *page);
+
+/* Command Callbacks */
+static void gnc_plugin_page_budget_cmd_delete_budget(
+    GtkAction *action, GncPluginPageBudget *page);
+static void gnc_plugin_page_budget_cmd_view_options(
+    GtkAction *action, GncPluginPageBudget *page);
+static void gnc_plugin_page_budget_cmd_estimate_budget(
+    GtkAction *action, GncPluginPageBudget *page);
+
+
+
+static GtkActionEntry gnc_plugin_page_budget_actions [] = {
+    /* Toplevel */
+    { "FakeToplevel", "", NULL, NULL, NULL, NULL },
+
+    /* TODO: maybe there should be menu entries, too? */
+
+    /* Toolbar buttons */
+    { "DeleteBudgetAction", GNC_STOCK_DELETE_BUDGET, N_("_Delete Budget"),
+      NULL, N_("Delete the budget"),
+      G_CALLBACK (gnc_plugin_page_budget_cmd_delete_budget) },
+    { "OptionsBudgetAction", GTK_STOCK_PROPERTIES, N_("Budget Options"),
+      NULL, N_("Edit the budget view options"),
+      G_CALLBACK (gnc_plugin_page_budget_cmd_view_options) },
+    { "EstimateBudgetAction", GTK_STOCK_EXECUTE, N_("Estimate Budget"),
+      NULL,
+      N_("Estimate a budget value for the selected cells"),
+      G_CALLBACK (gnc_plugin_page_budget_cmd_estimate_budget) },
+};
+
+static guint gnc_plugin_page_budget_n_actions =
+    G_N_ELEMENTS (gnc_plugin_page_budget_actions);
+
+// TODO: What's all this do?
+/*
+static const gchar *actions_requiring_budget[] = {
+  "OpenBudgetAction",
+  "BudgetViewOptionsAction",
+  "DeleteBudgetAction",
+  NULL
+};
+*/
+
+/* DRH - Suggest this be added to libegg */
+/*
+static action_short_labels short_labels[] = {
+
+  { "OpenBudgetAction", 	    N_("Open") },
+  //{ "EditBudgetAction", 	    N_("Edit") },
+  //{ "EditBudgetOptionsAction",      N_("Options") },
+  { "NewBudgetAction",    	    N_("New") },
+  { "DeleteBudgetAction", 	    N_("Delete") },
+  { NULL, NULL },
+};
+*/
+
+struct GncPluginPageBudgetPrivate
+{
+    GtkActionGroup *action_group;
+    guint merge_id;
+    GtkUIManager *ui_merge;
+
+    GtkWidget *widget;        /* ends up being a vbox */
+    GtkTreeView *tree_view;
+
+    gint component_id;
+
+    GncBudget* budget;
+    GUID key;
+    GncDialog* d;
+
+    GList *period_col_list;
+    guint32 acct_types;
+};
+
+static GObjectClass *parent_class = NULL;
+
+GType
+gnc_plugin_page_budget_get_type (void)
+{
+    static GType gnc_plugin_page_budget_type = 0;
+
+    if (gnc_plugin_page_budget_type == 0) {
+        static const GTypeInfo our_info = {
+            sizeof (GncPluginPageBudgetClass),
+            NULL,
+            NULL,
+            (GClassInitFunc) gnc_plugin_page_budget_class_init,
+            NULL,
+            NULL,
+            sizeof (GncPluginPageBudget),
+            0,
+            (GInstanceInitFunc) gnc_plugin_page_budget_init
+        };
+
+        gnc_plugin_page_budget_type =
+            g_type_register_static (GNC_TYPE_PLUGIN_PAGE,
+                                    "GncPluginPageBudget", &our_info, 0);
+    }
+
+    return gnc_plugin_page_budget_type;
+}
+
+GncPluginPage *
+gnc_plugin_page_budget_new (GncBudget *budget)
+{
+    GncPluginPageBudget *plugin_page;
+
+    g_return_val_if_fail(GNC_IS_BUDGET(budget), NULL);
+    ENTER(" ");
+    plugin_page = g_object_new(GNC_TYPE_PLUGIN_PAGE_BUDGET, NULL);
+
+    plugin_page->priv->budget = budget;
+    plugin_page->priv->key = *gnc_budget_get_guid(budget);
+    LEAVE("new budget page %p", plugin_page);
+    return GNC_PLUGIN_PAGE(plugin_page);
+}
+
+static void
+gnc_plugin_page_budget_class_init (GncPluginPageBudgetClass *klass)
+{
+    GObjectClass *object_class = G_OBJECT_CLASS (klass);
+    GncPluginPageClass *gnc_plugin_class = GNC_PLUGIN_PAGE_CLASS(klass);
+
+    parent_class = g_type_class_peek_parent (klass);
+
+    object_class->finalize = gnc_plugin_page_budget_finalize;
+
+    gnc_plugin_class->tab_icon        = GNC_STOCK_BUDGET;
+    gnc_plugin_class->plugin_name     = GNC_PLUGIN_PAGE_BUDGET_NAME;
+    gnc_plugin_class->create_widget   = gnc_plugin_page_budget_create_widget;
+    gnc_plugin_class->destroy_widget  = gnc_plugin_page_budget_destroy_widget;
+}
+
+static void
+gnc_plugin_page_budget_init (GncPluginPageBudget *plugin_page)
+{
+    GtkActionGroup *action_group;
+    GncPluginPageBudgetPrivate *priv;
+    GncPluginPage *parent;
+    const gchar *url = NULL;
+    //int options_id;
+    //SCM find_options;
+    //SCM temp;
+    URLType type;
+
+    ENTER("page %p", plugin_page);
+    priv = plugin_page->priv = g_new0 (GncPluginPageBudgetPrivate, 1);
+
+    /* Init parent declared variables */
+    parent = GNC_PLUGIN_PAGE(plugin_page);
+    g_object_set(G_OBJECT(plugin_page),
+		 "page-name",      _("Budget"),
+		 "page-uri",       "default:",
+		 "ui-description", "gnc-plugin-page-budget-ui.xml",
+		 NULL);
+
+    /* change me when the system supports multiple books */
+    gnc_plugin_page_add_book(parent, gnc_get_current_book());
+
+    /* Create menu and toolbar information */
+    action_group =
+      gnc_plugin_page_create_action_group(parent,
+					  "GncPluginPageBudgetActions");
+    gtk_action_group_add_actions (action_group,
+                                  gnc_plugin_page_budget_actions,
+                                  gnc_plugin_page_budget_n_actions,
+                                  plugin_page);
+    // FIXME? needed?
+    //gnc_gnome_utils_init_short_names (action_group, short_labels);
+
+    // FIXME: need to test this url case
+    if(!url) {
+    } else {
+        char * location = NULL;
+        char * label = NULL;
+
+        /* if an URL is specified, it should look like
+         * gnc-budget:id=17 .  We want to get the number out,
+         * then look up the options in the global DB. */
+        type = gnc_html_parse_url(NULL, url, &location, &label);
+        g_free (location);
+        g_free (label);
+    }
+
+    LEAVE("page %p, priv %p, action group %p",
+          plugin_page, plugin_page->priv, action_group);
+}
+
+static void
+gnc_plugin_page_budget_finalize (GObject *object)
+{
+    GncPluginPageBudget *page;
+    GncPluginPageBudgetPrivate *priv;
+
+    ENTER("object %p", object);
+    page = GNC_PLUGIN_PAGE_BUDGET (object);
+    g_return_if_fail (GNC_IS_PLUGIN_PAGE_BUDGET (page));
+    priv = page->priv;
+    g_return_if_fail (priv != NULL);
+
+    g_list_free(priv->period_col_list);
+    g_free (priv);
+
+    G_OBJECT_CLASS (parent_class)->finalize (object);
+    LEAVE(" ");
+}
+
+
+/* Component Manager Callback Functions */
+static void
+gnc_plugin_page_budget_close_cb (gpointer user_data)
+{
+    GncPluginPage *page = GNC_PLUGIN_PAGE(user_data);
+    gnc_main_window_close_page (page);
+}
+
+static void
+gnc_plugin_page_budget_refresh_cb(GHashTable *changes, gpointer user_data)
+{
+    GncPluginPageBudget *page;
+    const EventInfo* ei;
+
+    page = GNC_PLUGIN_PAGE_BUDGET(user_data);
+    if (changes) {
+        ei = gnc_gui_get_entity_events(changes, &page->priv->key);
+        if (ei) {
+            if (ei->event_mask & GNC_EVENT_DESTROY) {
+                gnc_plugin_page_budget_close_cb(user_data);
+                return;
+            }
+            if (ei->event_mask & GNC_EVENT_MODIFY) {
+                DEBUG("refreshing budget view because budget was modified");
+                gnc_plugin_page_budget_view_refresh(page);
+            }
+        }
+    }
+}
+
+
+/*
+ * GncPluginPage Fucntions
+ */
+static GtkWidget *
+gnc_plugin_page_budget_create_widget (GncPluginPage *plugin_page)
+{
+    GncPluginPageBudget *page;
+    GtkTreeSelection *selection;
+    GtkTreeView *tree_view;
+    GtkWidget *scrolled_window;
+
+    ENTER("page %p", plugin_page);
+    page = GNC_PLUGIN_PAGE_BUDGET (plugin_page);
+    if (page->priv->widget != NULL) {
+        LEAVE("widget = %p", page->priv->widget);
+        return page->priv->widget;
+    }
+
+    page->priv->widget = gtk_vbox_new (FALSE, 0);
+    gtk_widget_show (page->priv->widget);
+
+    scrolled_window = gtk_scrolled_window_new (NULL, NULL);
+    gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
+                                    GTK_POLICY_AUTOMATIC,
+                                    GTK_POLICY_AUTOMATIC);
+    gtk_widget_show (scrolled_window);
+    gtk_box_pack_start (GTK_BOX (page->priv->widget), scrolled_window,
+                        TRUE, TRUE, 0);
+
+    tree_view = gnc_tree_view_account_new(FALSE);
+    gnc_tree_view_configure_columns(
+        GNC_TREE_VIEW(tree_view), "Name", NULL);
+    page->priv->tree_view = tree_view;
+    selection = gtk_tree_view_get_selection(tree_view);
+    gtk_tree_selection_set_mode(selection, GTK_SELECTION_MULTIPLE);
+
+    g_signal_connect (G_OBJECT (tree_view), "button-press-event",
+                      G_CALLBACK (gnc_plugin_page_budget_button_press_cb),
+                      plugin_page);
+    g_signal_connect (G_OBJECT (tree_view), "row-activated",
+                      G_CALLBACK (gnc_plugin_page_budget_double_click_cb),
+                      page);
+
+    gtk_tree_view_set_headers_visible(tree_view, TRUE);
+    gtk_widget_show (GTK_WIDGET (tree_view));
+    gtk_container_add (GTK_CONTAINER (scrolled_window),
+                       GTK_WIDGET(tree_view));
+
+    page->priv->component_id =
+        gnc_register_gui_component(PLUGIN_PAGE_BUDGET_CM_CLASS,
+                                   gnc_plugin_page_budget_refresh_cb,
+                                   gnc_plugin_page_budget_close_cb,
+                                   page);
+
+    gnc_gui_component_set_session (page->priv->component_id,
+                                   gnc_get_current_session());
+
+    gnc_gui_component_watch_entity (page->priv->component_id,
+                                    gnc_budget_get_guid(page->priv->budget),
+                                    GNC_EVENT_DESTROY | GNC_EVENT_MODIFY);
+
+    gnc_plugin_page_budget_view_refresh(page);
+
+    LEAVE("widget = %p", page->priv->widget);
+    return page->priv->widget;
+}
+
+static void
+gnc_plugin_page_budget_destroy_widget (GncPluginPage *plugin_page)
+{
+    GncPluginPageBudget *page;
+
+    ENTER("page %p", plugin_page);
+    page = GNC_PLUGIN_PAGE_BUDGET (plugin_page);
+    if (page->priv->widget) {
+        g_object_unref(G_OBJECT(page->priv->widget));
+        page->priv->widget = NULL;
+    }
+
+    gnc_gui_component_clear_watches (page->priv->component_id);
+
+    if (page->priv->component_id != NO_COMPONENT) {
+        gnc_unregister_gui_component(page->priv->component_id);
+        page->priv->component_id = NO_COMPONENT;
+    }
+
+    LEAVE("widget destroyed");
+}
+
+/** This button press handler calls the common button press handler
+ *  for all pages.  The GtkTreeView eats all button presses and
+ *  doesn't pass them up the widget tree, even when doesn't do
+ *  anything with them.  The only way to get access to the button
+ *  presses in an account tree page is here on the tree view widget.
+ *  Button presses on all other pages are caught by the signal
+ *  registered in gnc-main-window.c. */
+static gboolean
+gnc_plugin_page_budget_button_press_cb (GtkWidget *widget,
+					GdkEventButton *event,
+					GncPluginPage *page)
+{
+  gboolean result;
+
+  g_return_val_if_fail(GNC_IS_PLUGIN_PAGE(page), FALSE);
+
+  ENTER("widget %p, event %p, page %p", widget, event, page);
+  result = gnc_main_window_button_press_cb(widget, event, page);
+  LEAVE(" ");
+  return result;
+}
+
+static void
+gnc_plugin_page_budget_double_click_cb (GtkTreeView        *treeview,
+					GtkTreePath        *path,
+					GtkTreeViewColumn  *col,
+					GncPluginPageBudget *page)
+{
+    GtkWidget *window;
+    GncPluginPage *new_page;
+    Account *account;
+
+    g_return_if_fail(GNC_IS_PLUGIN_PAGE_BUDGET (page));
+    account = gnc_tree_view_account_get_account_from_path(
+        GNC_TREE_VIEW_ACCOUNT(treeview), path);
+    if (account == NULL)
+        return;
+
+    window = GNC_PLUGIN_PAGE(page)->window;
+    new_page = gnc_plugin_page_register_new(account, FALSE);
+    gnc_main_window_open_page(GNC_MAIN_WINDOW(window), new_page);
+}
+
+/* Command callbacks */
+
+static void
+gnc_plugin_page_budget_cmd_delete_budget (GtkAction *action,
+					  GncPluginPageBudget *page)
+{
+  GncBudget *budget;
+
+  budget = page->priv->budget;
+  g_return_if_fail (GNC_IS_BUDGET(budget));
+  gnc_budget_gui_delete_budget(budget);
+
+}
+
+/******************************/
+/*       Options Dialog       */
+/******************************/
+
+static gboolean
+gnc_plugin_page_budget_options_apply_cb (GncDialog * d,
+					 gpointer user_data)
+{
+    GncPluginPageBudgetPrivate *priv = user_data;
+    const gchar *name;
+    gchar *desc;
+    gint num_periods;
+    GncRecurrence *gr;
+    const Recurrence *r;
+    GtkTreeView *tv;
+    guint32 sel_mask;
+
+    if(!priv)
+        return TRUE;
+
+    ENTER(" ");
+    name = gnc_dialog_get_string(d, "BudgetName");
+    if (name) {
+        gnc_budget_set_name(priv->budget, name);
+        DEBUG("%s", name);
+    }
+
+
+    //FIXME: this is special broken case where we actually do need to
+    //free because widget is a GtkTextView
+    desc = (gchar *) gnc_dialog_get_string(d, "BudgetDescription");
+    gnc_budget_set_description(priv->budget, desc);
+    g_free(desc);
+
+    num_periods = gnc_dialog_get_int(d, "BudgetNumPeriods");
+    gnc_budget_set_num_periods(priv->budget, num_periods);
+
+    gr = GNC_RECURRENCE(gnc_dialog_get_widget(d, "BudgetRecurrenceEntry"));
+    r = gnc_recurrence_get(gr);
+    gnc_budget_set_recurrence(priv->budget, r);
+
+
+    tv = GTK_TREE_VIEW(gnc_dialog_get_widget(
+                           d, "AccountTypesTreeView"));
+    sel_mask = gnc_tree_model_account_types_get_selection(tv);
+    priv->acct_types = sel_mask;
+    gnc_tree_view_account_set_filter(
+        GNC_TREE_VIEW_ACCOUNT(priv->tree_view),
+        gnc_tree_view_account_filter_by_type_selection,
+        GUINT_TO_POINTER(sel_mask), NULL);
+    LEAVE(" ");
+    return TRUE;
+}
+
+static gboolean
+gnc_plugin_page_budget_options_help_cb (GncDialog *d,
+					gpointer user_data)
+{
+  GtkWidget *dialog;
+
+  dialog = gtk_message_dialog_new (NULL,
+				   GTK_DIALOG_DESTROY_WITH_PARENT,
+				   GTK_MESSAGE_INFO,
+				   GTK_BUTTONS_OK,
+				   "Set the budget options using this dialog.");
+
+  gtk_dialog_run (GTK_DIALOG (dialog));
+  gtk_widget_destroy (dialog);
+  return TRUE;
+}
+
+static gboolean
+gnc_plugin_page_budget_options_close_cb (GncDialog *d,
+					 gpointer user_data)
+{
+  GncPluginPageBudgetPrivate *priv = user_data;
+
+  g_return_val_if_fail(priv, TRUE);
+
+  gtk_widget_destroy(GTK_WIDGET(d));
+  priv->d = NULL;
+  return TRUE;
+}
+
+
+static void
+gnc_budget_gui_show_options(GncDialog *pw, GncBudget *budget,
+                            GncPluginPageBudget *page)
+{
+    GtkTreeView *tv;
+    GtkTreeModel *tm;
+    GtkTreeSelection *sel;
+    GncRecurrence *gr;
+    GncPluginPageBudgetPrivate *priv;
+
+
+    g_return_if_fail (GNC_IS_PLUGIN_PAGE_BUDGET (page));
+    priv = page->priv;
+
+    gnc_dialog_set_string(pw, "BudgetName",
+                          gnc_budget_get_name(budget));
+    gnc_dialog_set_string(pw, "BudgetDescription",
+                          gnc_budget_get_description(budget));
+    gnc_dialog_set_int(pw, "BudgetNumPeriods",
+                       gnc_budget_get_num_periods(budget));
+    gr = GNC_RECURRENCE(gnc_dialog_get_widget(
+                            pw, "BudgetRecurrenceEntry"));
+    gnc_recurrence_set(gr, gnc_budget_get_recurrence(budget));
+
+    tv = GTK_TREE_VIEW(gnc_dialog_get_widget(
+                           pw, "AccountTypesTreeView"));
+    tm = gnc_tree_model_account_types_master();
+    gtk_tree_view_set_model(tv, tm);
+    gtk_tree_view_insert_column_with_attributes(
+        tv, -1, "Account Types", gtk_cell_renderer_text_new(),
+        "text", GNC_TREE_MODEL_ACCOUNT_TYPES_COL_NAME, NULL);
+    sel = gtk_tree_view_get_selection(tv);
+    gtk_tree_selection_set_mode(sel, GTK_SELECTION_MULTIPLE);
+
+    //FIXME: this is just a default, need to save and set actual value.
+    if (priv->acct_types == 0)
+        priv->acct_types = 1 << INCOME | 1 << EXPENSE;
+    gnc_tree_model_account_types_set_selection(tv, priv->acct_types);
+}
+
+
+static void
+gnc_plugin_page_budget_cmd_view_options (GtkAction *action,
+                                         GncPluginPageBudget *page)
+{
+    GncPluginPageBudgetPrivate *priv;
+
+    g_return_if_fail (GNC_IS_PLUGIN_PAGE_BUDGET (page));
+    priv = page->priv;
+
+
+    if (!priv->d) {
+        priv->d = gnc_dialog_new(GNC_BUDGET_GUI_FILE, "BudgetOptions");
+        gtk_window_set_title(GTK_WINDOW(priv->d), "Budget Options");
+        gnc_dialog_set_cb(priv->d,
+                          gnc_plugin_page_budget_options_apply_cb,
+                          gnc_plugin_page_budget_options_close_cb,
+                          gnc_plugin_page_budget_options_help_cb,
+                          priv);
+    }
+
+    gnc_budget_gui_show_options(priv->d, priv->budget, page);
+    gtk_widget_show_all(GTK_WIDGET(priv->d));
+}
+
+
+void
+gnc_budget_gui_delete_budget(GncBudget *budget)
+{
+    const char *name;
+
+    g_return_if_fail(GNC_IS_BUDGET(budget));
+    name = gnc_budget_get_name (budget);
+    if (!name)
+        name = "Unnamed Budget";
+
+
+    if (gnc_verify_dialog (NULL, FALSE, "Delete %s?", name)) {
+        gnc_suspend_gui_refresh ();
+        gnc_budget_free(budget);
+        // Views should close themselves because the CM will notify them.
+        gnc_resume_gui_refresh ();
+    }
+
+}
+
+
+static void
+estimate_budget_helper(GtkTreeModel *model, GtkTreePath *path,
+                       GtkTreeIter *iter, gpointer data)
+{
+    Account *acct;
+    guint num_periods, i;
+    gnc_numeric num;
+    GncPluginPageBudgetPrivate *priv;
+    GncPluginPageBudget *page = data;
+
+
+    g_return_if_fail(GNC_IS_PLUGIN_PAGE_BUDGET(page));
+    priv = page->priv;
+
+    acct = gnc_tree_view_account_get_account_from_path(
+        GNC_TREE_VIEW_ACCOUNT(priv->tree_view), path);
+
+    num_periods = g_list_length(priv->period_col_list);
+
+    for (i = 0; i < num_periods; i++) {
+        num = gnc_budget_get_account_period_actual_value(
+            priv->budget, acct, i);
+        if (!gnc_numeric_check(num)) {
+            if (gnc_reverse_balance (acct))
+                num = gnc_numeric_neg (num);
+
+            gnc_budget_set_account_period_value(
+                priv->budget, acct, i, num);
+        }
+    }
+
+}
+
+static void
+gnc_plugin_page_budget_cmd_estimate_budget(GtkAction *action,
+                                           GncPluginPageBudget *page)
+{
+    GncPluginPageBudgetPrivate *priv;
+    GtkTreeSelection *sel;
+
+    g_return_if_fail (GNC_IS_PLUGIN_PAGE_BUDGET (page));
+    priv = page->priv;
+
+    sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(priv->tree_view));
+    gtk_tree_selection_selected_foreach(sel, estimate_budget_helper, page);
+
+
+
+}
+
+static gchar *
+budget_col_source(Account *account, GtkTreeViewColumn *col,
+                  GtkCellRenderer *cell)
+{
+    GncBudget *budget;
+    guint period_num;
+    gnc_numeric numeric;
+    gchar amtbuff[100]; //FIXME: overkill, where's the #define?
+
+    budget = GNC_BUDGET(g_object_get_data(G_OBJECT(col), "budget"));
+    period_num = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(col),
+                                                    "period_num"));
+    numeric = gnc_budget_get_account_period_value(budget, account, period_num);
+
+    if (gnc_numeric_zero_p(numeric))
+        amtbuff[0] = '\0';
+    else
+        xaccSPrintAmount (amtbuff, numeric,
+                          gnc_account_print_info (account, FALSE));
+
+    return g_strdup(amtbuff);
+}
+
+static void
+budget_col_edited(Account *account, GtkTreeViewColumn *col,
+                  const gchar *new_text)
+{
+    GncBudget *budget;
+    guint period_num;
+    gnc_numeric numeric;
+
+    if (!(xaccParseAmount (new_text, TRUE, &numeric, NULL)))
+        return;
+
+    period_num = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(col),
+                                                    "period_num"));
+
+    budget = GNC_BUDGET(g_object_get_data(G_OBJECT(col), "budget"));
+
+    gnc_budget_set_account_period_value(budget, account, period_num, numeric);
+}
+
+static void
+gnc_plugin_page_budget_refresh_col_titles(GncPluginPageBudget *page)
+{
+    const Recurrence *r;
+    GDate date, nextdate;
+    GtkTreeViewColumn *col;
+    guint titlelen;
+    gint num_periods_visible;
+    gchar title[MAX_DATE_LENGTH];
+    GncPluginPageBudgetPrivate *priv;
+    GList *col_list;
+    gint i;
+
+    g_return_if_fail(GNC_IS_PLUGIN_PAGE_BUDGET(page));
+    priv = page->priv;
+
+    col_list = priv->period_col_list;
+    num_periods_visible = g_list_length(col_list);
+
+   /* Show the dates in column titles */
+    r = gnc_budget_get_recurrence(priv->budget);
+    date = r->start;
+    for (i = 0; i < num_periods_visible; i++) {
+        col = GTK_TREE_VIEW_COLUMN(g_list_nth_data(col_list, i));
+        titlelen = g_date_strftime(title, MAX_DATE_LENGTH, "%x", &date);
+        if (titlelen > 0)
+            gtk_tree_view_column_set_title(col, title);
+        recurrenceNextInstance(r, &date, &nextdate);
+        date = nextdate;
+    }
+
+}
+
+static void
+gnc_plugin_page_budget_view_refresh (GncPluginPageBudget *page)
+{
+    GncPluginPageBudgetPrivate *priv;
+    gint num_periods, num_periods_visible;
+    GtkTreeViewColumn *col;
+    GList *col_list;
+    gint i;
+
+    g_return_if_fail(GNC_IS_PLUGIN_PAGE_BUDGET(page));
+    priv = page->priv;
+
+    num_periods = gnc_budget_get_num_periods(priv->budget);
+    col_list = priv->period_col_list;
+    num_periods_visible = g_list_length(col_list);
+
+    /* Hide any unneeded extra columns */
+    for (i = num_periods_visible-1; i >= num_periods; i--) {
+        col = GTK_TREE_VIEW_COLUMN((g_list_last(col_list))->data);
+        //maybe better to just destroy it?
+        gtk_tree_view_column_set_visible(col, FALSE);
+        col_list = g_list_delete_link(col_list, g_list_last(col_list));
+    }
+
+    gnc_tree_view_configure_columns(
+        GNC_TREE_VIEW(priv->tree_view), NULL);
+
+    /* Create any needed columns */
+    num_periods_visible = g_list_length(col_list);
+    for (i = num_periods_visible; i < num_periods; i++) {
+        col = gnc_tree_view_account_add_custom_column(
+            GNC_TREE_VIEW_ACCOUNT(priv->tree_view), "",
+            budget_col_source,
+            budget_col_edited);
+        g_object_set_data(G_OBJECT(col), "budget", priv->budget);
+        g_object_set_data(G_OBJECT(col), "period_num", GUINT_TO_POINTER(i));
+        col_list = g_list_append(col_list, col);
+    }
+    num_periods_visible = num_periods;
+    priv->period_col_list = col_list;
+
+    gnc_plugin_page_budget_refresh_col_titles(page);
+}
--- /dev/null
+++ src/gnome/gnc-plugin-budget.h
@@ -0,0 +1,69 @@
+/*
+ * gnc-plugin-budget.h --
+ *
+ * Copyright (C) 2005 Chris Shoemaker <c.shoemaker at cox.net>
+ *
+ * Based on gnc-plugin-account.h, by:
+ *   Jan Arne Petersen <jpetersen at uni-bonn.de>
+ *
+ * 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, contact:
+ *
+ * Free Software Foundation           Voice:  +1-617-542-5942
+ * 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652
+ * Boston, MA  02110-1301,  USA       gnu at gnu.org
+ */
+
+#ifndef __GNC_PLUGIN_BUDGET_H
+#define __GNC_PLUGIN_BUDGET_H
+
+#include <gtk/gtkwindow.h>
+#include "gnc-plugin.h"
+#include "gnc-budget.h"
+
+G_BEGIN_DECLS
+
+/* type macros */
+#define GNC_TYPE_PLUGIN_BUDGET            (gnc_plugin_budget_get_type ())
+#define GNC_PLUGIN_BUDGET(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), GNC_TYPE_PLUGIN_BUDGET, GncPluginBudget))
+#define GNC_PLUGIN_BUDGET_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), GNC_TYPE_PLUGIN_BUDGET, GncPluginBudgetClass))
+#define GNC_IS_PLUGIN_BUDGET(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GNC_TYPE_PLUGIN_BUDGET))
+#define GNC_IS_PLUGIN_BUDGET_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GNC_TYPE_PLUGIN_BUDGET))
+#define GNC_PLUGIN_BUDGET_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), GNC_TYPE_PLUGIN_BUDGET, GncPluginBudgetClass))
+
+#define GNC_PLUGIN_BUDGET_NAME "gnc-plugin-budget"
+#define GNC_BUDGET_GUI_FILE    "budget.glade"
+
+/* typedefs & structures */
+typedef struct GncPluginBudgetPrivate GncPluginBudgetPrivate;
+
+typedef struct {
+  GncPlugin parent;
+  GncPluginBudgetPrivate *priv;
+} GncPluginBudget;
+
+typedef struct {
+  GncPluginClass parent;
+} GncPluginBudgetClass;
+
+/* function prototypes */
+GType gnc_plugin_budget_get_type(void);
+GncPlugin *gnc_plugin_budget_new(void);
+
+/* Launch the budget list dialog.*/
+GncBudget * gnc_budget_gui_select_budget(QofBook *book);
+
+
+G_END_DECLS
+
+#endif /* __GNC_PLUGIN_BUDGET_H */
--- /dev/null
+++ src/gnome/ui/gnc-plugin-page-budget-ui.xml
@@ -0,0 +1,27 @@
+<ui>
+  <menubar>
+    <menu name="Edit" action="EditAction">
+      <placeholder name="EditSelectedPlaceholder">
+        <menuitem name="Estimate" action="EstimateBudgetAction"/>
+        <menuitem name="Delete" action="DeleteBudgetAction"/>
+      </placeholder>
+      <menuitem name="Options" action="OptionsBudgetAction"/>
+    </menu>
+  </menubar>
+
+  <popup name="MainPopup" action="FakeToplevel">
+    <placeholder name="PopupPlaceholder1">
+      <menuitem name="Options" action="OptionsBudgetAction"/>
+    </placeholder>
+  </popup>
+
+  <toolbar name="DefaultToolbar">
+    <placeholder name="DefaultToolbarPlaceholder">
+      <separator name="ToolbarSep3"/>
+      <toolitem name="Options" action="OptionsBudgetAction"/>
+      <separator name="ToolbarSep4"/>
+      <toolitem name="Estimate" action="EstimateBudgetAction"/>
+      <toolitem name="Delete" action="DeleteBudgetAction"/>
+    </placeholder>
+  </toolbar>
+</ui>
--- /dev/null
+++ src/gnome/ui/gnc-plugin-budget-ui.xml
@@ -0,0 +1,17 @@
+<ui>
+  <menubar>
+    <menu name="File" action="FileAction">
+      <menu name="FileNewMenu" action="FileNewMenuAction">
+        <placeholder name="FileNewBottonPlaceholder">
+          <menuitem name="FileNewBudget" action="NewBudgetAction"/>
+        </placeholder>
+      </menu>
+      <menu name="FileOpenMenu" action="FileOpenMenuAction">
+        <placeholder name="FileOpenBottomPlaceholder">
+          <menuitem name="FileOpenBudget" action="OpenBudgetAction"/>
+        </placeholder>
+      </menu>
+    </menu>
+  </menubar>
+</ui>
+
--- /dev/null
+++ src/gnome-utils/gnc-tree-model-budget.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2005, Chris Shoemaker <c.shoemaker at cox.net>
+ *
+ * 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, contact:
+ *
+ * Free Software Foundation           Voice:  +1-617-542-5942
+ * 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652
+ * Boston, MA  02110-1301,  USA       gnu at gnu.org
+ */
+
+/** @addtogroup gnome-util
+ *     @{ */
+
+/* This file provides some utilities for working with the list of
+ * budgets in a book.  TODO: This file is poorly named, since it
+ * covers both model and view.*/
+
+#ifndef __GNC_TREE_MODEL_BUDGET_H__
+#define __GNC_TREE_MODEL_BUDGET_H__
+
+#include <gtk/gtk.h>
+#include "gnc-budget.h"
+
+/* The budget list columns. */
+enum {
+    BUDGET_GUID_COLUMN,
+    BUDGET_NAME_COLUMN,
+    BUDGET_DESCRIPTION_COLUMN,
+    BUDGET_LIST_NUM_COLS
+};
+
+GtkTreeModel * gnc_tree_model_budget_new(QofBook *book);
+
+void gnc_tree_view_budget_set_model(GtkTreeView *tv, GtkTreeModel *tm);
+
+GncBudget *gnc_tree_model_budget_get_budget(GtkTreeModel *tm,
+                                            GtkTreeIter *iter);
+
+void gnc_tree_model_budget_get_iter_for_budget(GtkTreeModel *tm,
+                                               GtkTreeIter *iter,
+                                               GncBudget *bgt);
+
+#endif // __GNC_TREE_MODEL_BUDGET_H__
--- /dev/null
+++ src/gnome-utils/gnc-tree-model-budget.c
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2005, Chris Shoemaker <c.shoemaker at cox.net>
+ *
+ * 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, contact:
+ *
+ * Free Software Foundation           Voice:  +1-617-542-5942
+ * 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652
+ * Boston, MA  02110-1301,  USA       gnu at gnu.org
+ */
+
+/** @addtogroup gnome-util
+ *     @{ */
+
+#include "gnc-tree-model-budget.h"
+#include "gnc-budget.h"
+#include "gnc-ui-util.h"
+
+/* Add the new budget object to the tree model.  */
+static void add_budget_to_model(QofEntity* data, gpointer user_data )
+{
+    GtkTreeIter iter;
+    GncBudget* budget = GNC_BUDGET(data);
+    GtkTreeModel* treeModel = user_data;
+
+    g_return_if_fail(GNC_IS_BUDGET(budget));
+    g_return_if_fail(budget && treeModel);
+
+    gtk_list_store_append (GTK_LIST_STORE(treeModel), &iter);
+    gtk_list_store_set (GTK_LIST_STORE(treeModel), &iter,
+                        BUDGET_GUID_COLUMN, gnc_budget_get_guid(budget),
+                        BUDGET_NAME_COLUMN, gnc_budget_get_name(budget),
+                        BUDGET_DESCRIPTION_COLUMN,
+                        gnc_budget_get_description(budget), -1);
+}
+
+/* CAS: Even though it works, something feels not-quite-right with
+ * this design.  The idea here is to _not_ provide yet another
+ * implementation of GtkTreeModel, this time for budgets.  Instead,
+ * right now, we're using the already implemented GtkListStore.  This
+ * has a couple consequences: 1) We allocate a new store upon every
+ * call, so the memory is owned by caller.  2) The model won't reflect
+ * later updates to the book, so the model shouldn't be expected to
+ * track asynchronous changes.
+ *
+ * If, for some reason, I decide I can't live with or remove those
+ * consequences, I still think there must be some better way than
+ * re-implementing GtkTreeModel.  One idea I'm toying with is to
+ * implement a GtkTreeModel for QofCollections, which would offer only
+ * the GUID as a field.  Then, TreeViews could add their own columns
+ * with custom CellDataFuncs to display the object-specific fields.
+ * Or, something like that.  :)
+ *
+ */
+GtkTreeModel *
+gnc_tree_model_budget_new(QofBook *book)
+{
+    GtkListStore* store;
+
+    store = gtk_list_store_new (BUDGET_LIST_NUM_COLS,
+                                G_TYPE_POINTER,
+                                G_TYPE_STRING,
+                                G_TYPE_STRING);
+    
+    qof_collection_foreach(qof_book_get_collection(book, GNC_ID_BUDGET), 
+                           add_budget_to_model, GTK_TREE_MODEL(store));
+
+    return GTK_TREE_MODEL(store);
+}
+
+void
+gnc_tree_view_budget_set_model(GtkTreeView *tv, GtkTreeModel *tm)
+{
+    GtkCellRenderer *renderer;
+    GtkTreeViewColumn *column;
+
+    gtk_tree_view_set_model (tv, tm);
+
+    /* column for name */
+    renderer = gtk_cell_renderer_text_new ();
+    column = gtk_tree_view_column_new_with_attributes (
+        "Name", renderer, "text", BUDGET_NAME_COLUMN, NULL);
+    gtk_tree_view_append_column (tv, column);
+
+    /* column for description */
+    renderer = gtk_cell_renderer_text_new ();
+    column = gtk_tree_view_column_new_with_attributes (
+        "Description", renderer, "text", BUDGET_DESCRIPTION_COLUMN, NULL);
+    gtk_tree_view_append_column (tv, column);
+
+}
+
+GncBudget *
+gnc_tree_model_budget_get_budget(GtkTreeModel *tm, GtkTreeIter *iter)
+{
+    GncBudget *bgt;
+    GValue gv = { 0 };
+    GUID *guid;
+
+    gtk_tree_model_get_value(tm, iter, BUDGET_GUID_COLUMN, &gv);
+    guid = (GUID *) g_value_get_pointer(&gv);
+    g_value_unset(&gv);
+
+    bgt = gnc_budget_lookup(guid, gnc_get_current_book());
+    return bgt;
+}
+
+void 
+gnc_tree_model_budget_get_iter_for_budget(GtkTreeModel *tm, GtkTreeIter *iter,
+                                          GncBudget *bgt)
+{
+    GValue gv = { 0 };
+    const GUID *guid1;
+    GUID *guid2;
+
+    g_return_if_fail(GNC_BUDGET(bgt));
+
+    guid1 = gnc_budget_get_guid(bgt);
+    for (gtk_tree_model_get_iter_first(tm, iter);
+         gtk_list_store_iter_is_valid(GTK_LIST_STORE(tm), iter);
+         gtk_tree_model_iter_next(tm, iter)) {
+
+        gtk_tree_model_get_value(tm, iter, BUDGET_GUID_COLUMN, &gv);
+        guid2 = (GUID *) g_value_get_pointer(&gv);
+        g_value_unset(&gv);
+
+        if (guid_equal(guid1, guid2))
+            return;
+    }
+}
--- /dev/null
+++ src/gnome-utils/gnc-dialog.c
@@ -0,0 +1,677 @@
+/* Copyright (C) 2005 Chris Shoemaker <c.shoemaker at cox.net>
+ *
+ * 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, contact:
+ *
+ * Free Software Foundation           Voice:  +1-617-542-5942
+ * 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652
+ * Boston, MA  02110-1301,  USA       gnu at gnu.org
+ */
+
+#include <gtk/gtk.h>
+#include <glade/glade.h>
+
+#include "gnc-trace.h"
+#include "gnome.h"         // for gnome_date_edit
+#include "gnc-dialog.h"
+#include "gnc-gobject-utils.h"
+#include "dialog-utils.h"  // for gnc_glade_xml_new
+
+static QofLogModule log_module = GNC_MOD_GUI;
+
+#define GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), \
+  GNC_TYPE_DIALOG, GncDialogPrivate))
+
+struct _GncDialog {
+    GtkDialog parent;
+};
+
+struct _GncDialogClass {
+    GtkDialogClass parent;
+    void (*changed) (GncDialog *d);
+};
+
+enum {
+    GNC_DIALOG_CHANGED,
+    LAST_SIGNAL
+};
+
+static gint gnc_dialog_signals [LAST_SIGNAL] = { GNC_DIALOG_CHANGED };
+static GtkDialogClass *parent_class = NULL;
+
+typedef struct {
+    GladeXML *xml;
+    GncDialogCallback apply_cb;
+    GncDialogCallback close_cb;
+    GncDialogCallback help_cb;
+    GtkWidget *cancel_btn;
+    GtkWidget *ok_btn;
+    GtkWidget *help_btn;
+
+    gpointer user_data;
+    gboolean changed;
+} GncDialogPrivate;
+
+static void
+gnc_dialog_finalize(GObject *d)
+{
+    g_return_if_fail(d);
+    /* We don't own any references, so do nothing */
+    gnc_gobject_tracking_forget(d);
+    G_OBJECT_CLASS(parent_class)->finalize(d);
+}
+
+static void
+gnc_dialog_class_init (GncDialogClass *klass)
+{
+    GObjectClass *gobject_class;
+
+    parent_class = g_type_class_peek_parent (klass);
+
+    gobject_class = G_OBJECT_CLASS (klass);
+
+    g_type_class_add_private (gobject_class, sizeof (GncDialogPrivate));
+
+    gnc_dialog_signals [GNC_DIALOG_CHANGED] =
+        g_signal_new ("changed",
+		  G_OBJECT_CLASS_TYPE (gobject_class),
+		  G_SIGNAL_RUN_FIRST,
+		  G_STRUCT_OFFSET (struct _GncDialogClass, changed),
+		  NULL,
+		  NULL,
+		  g_cclosure_marshal_VOID__VOID,
+		  G_TYPE_NONE,
+		  0);
+
+    /* GObject signals */
+    gobject_class->finalize = gnc_dialog_finalize;
+}
+
+static void
+gnc_dialog_init (GncDialog *d, GncDialogClass *klass)
+{
+  gnc_gobject_tracking_remember(G_OBJECT(d), G_OBJECT_CLASS(klass));
+}
+
+GType gnc_dialog_get_type (void)
+{
+    static GType t = 0;
+
+    if (!t) {
+	static const GTypeInfo info = {
+	    sizeof (struct _GncDialogClass),
+	    NULL, /* base_init */
+	    NULL, /* base_final */
+	    (GClassInitFunc) gnc_dialog_class_init,
+	    NULL, /* class final */
+	    NULL, /* class data */
+	    sizeof (struct _GncDialog),
+	    0, /* n_preallocs */
+	    (GInstanceInitFunc) gnc_dialog_init,
+	    NULL,
+	};
+	t = g_type_register_static (GTK_TYPE_DIALOG,
+                                    "GncDialog", &info, 0);
+    }
+    return t;
+}
+
+static void gnc_dialog_set_changed(GncDialog *_d, gboolean changed)
+{
+    GncDialogPrivate *priv;
+    struct _GncDialog *d = _d;
+
+    priv = GET_PRIVATE(d);
+    if (!priv->changed && changed)
+        gtk_dialog_set_response_sensitive(&d->parent, GTK_RESPONSE_OK,
+                                          changed);
+    priv->changed = changed;
+    if (changed)
+        g_signal_emit(G_OBJECT(d), gnc_dialog_signals[GNC_DIALOG_CHANGED], 0);
+}
+
+static void gnc_dialog_response_cb(GtkDialog *dlg,
+                                              gint response, GncDialog *d)
+{
+    gboolean success = TRUE;
+    GncDialogPrivate *priv = GET_PRIVATE(d);
+
+    switch (response) {
+    case GTK_RESPONSE_HELP:
+	if (priv->help_cb)
+            priv->help_cb(d, priv->user_data);
+	break;
+    case GTK_RESPONSE_OK:
+        //case GTK_RESPONSE_APPLY:
+	if (priv->apply_cb) {
+	    success = priv->apply_cb(d, priv->user_data);
+            if (success)
+                gnc_dialog_set_changed(d, FALSE);
+        }
+
+	if (!success)
+	    break;
+        // fall through
+    default:
+	if (priv->close_cb)
+	    success = priv->close_cb(d, priv->user_data);
+        else
+            success = TRUE;
+
+	if (success)
+            gtk_widget_destroy(GTK_WIDGET(dlg));
+    }
+}
+
+static void changed_cb(GObject *obj, gpointer d)
+{
+    gnc_dialog_set_changed(GNC_DIALOG(d), TRUE);
+}
+
+static void
+gnc_dialog_watch_for_changes(GtkWidget *wid, gpointer d)
+{
+    if (GTK_IS_BUTTON(wid))
+        g_signal_connect(G_OBJECT(wid), "clicked", G_CALLBACK(changed_cb), d);
+
+    if (GTK_IS_EDITABLE(wid) || GTK_IS_COMBO_BOX(wid))
+        g_signal_connect(G_OBJECT(wid), "changed", G_CALLBACK(changed_cb), d);
+
+    if (GTK_IS_TREE_VIEW(wid)) {
+        GtkTreeSelection *sel =
+            gtk_tree_view_get_selection(GTK_TREE_VIEW(wid));
+        g_signal_connect(G_OBJECT(sel), "changed", G_CALLBACK(changed_cb), d);
+    }
+
+    if (GTK_IS_TEXT_VIEW(wid)) {
+        GtkTextBuffer *buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(wid));
+        g_signal_connect(G_OBJECT(buf), "changed", G_CALLBACK(changed_cb), d);
+    }
+    //Possibly TODO: GtkCalendar?
+
+    /* Recurse over all "contained" widgets */
+    if (GTK_IS_CONTAINER(wid)) {
+        gtk_container_foreach(GTK_CONTAINER(wid),
+                              gnc_dialog_watch_for_changes, d);
+    }
+}
+
+GncDialog *gnc_dialog_new(const char* filename,
+				      const char* root)
+{
+    GncDialog *d;
+    GncDialogPrivate *priv;
+    GtkDialog *dlg;
+    GtkWidget *child;
+
+    d = g_object_new(GNC_TYPE_DIALOG, NULL);
+    dlg = GTK_DIALOG(d);
+    priv = GET_PRIVATE(d);
+
+    /* Load in the glade portion and plug it in. */
+    priv->xml = gnc_glade_xml_new(filename, root);
+    child = glade_xml_get_widget(priv->xml, root);
+    if (GTK_WIDGET_TOPLEVEL(child)) {
+        PERR("GncDialog root widget must not be a toplevel widget");
+        return NULL;
+    }
+
+    gtk_container_add(GTK_CONTAINER(dlg->vbox), child);
+
+    /* Prepare the dialog. */
+    priv->help_btn = gtk_dialog_add_button(dlg, GTK_STOCK_HELP,
+                                           GTK_RESPONSE_HELP);
+    priv->cancel_btn = gtk_dialog_add_button(dlg, GTK_STOCK_CANCEL,
+                                             GTK_RESPONSE_CANCEL);
+    priv->ok_btn = gtk_dialog_add_button(dlg, GTK_STOCK_OK,
+                                         GTK_RESPONSE_OK);
+
+    g_signal_connect(dlg, "response",
+                     G_CALLBACK(gnc_dialog_response_cb), d);
+
+    glade_xml_signal_autoconnect_full(priv->xml,
+                                      gnc_glade_autoconnect_full_func, d);
+    gnc_dialog_watch_for_changes(child, (gpointer) d);
+    gtk_dialog_set_response_sensitive(dlg, GTK_RESPONSE_OK, FALSE);
+    return d;
+}
+
+void gnc_dialog_set_cb(GncDialog *d, GncDialogCallback apply_cb,
+                       GncDialogCallback close_cb,
+                       GncDialogCallback help_cb,
+                       gpointer user_data)
+{
+    GncDialogPrivate *priv;
+
+    priv = GET_PRIVATE(d);
+    priv->apply_cb = apply_cb;
+    priv->close_cb = close_cb;
+    priv->help_cb = help_cb;
+    priv->user_data = user_data;
+
+    if (apply_cb == NULL)
+        gtk_widget_hide(priv->ok_btn);
+    if (help_cb == NULL)
+        gtk_widget_hide(priv->help_btn);
+}
+
+void gnc_dialog_block_until_close(GncDialog *d)
+{
+    gint result;
+    g_return_if_fail(d);
+
+    do {
+        result = gtk_dialog_run(GTK_DIALOG(d));
+    } while (result != GTK_RESPONSE_DELETE_EVENT);
+}
+
+/* There are certain containers that the type-specific functions don't
+   operate on.  But, the API user might have used
+   gnc_dialog_get_widget() to get the container widget, and then added
+   their own widget to the container.  For the purpose of the
+   type-specific functions, we'll consider references to those
+   containers as references to their child.  (But only one
+   child.)
+  */
+static GtkWidget *gnc_dialog_get_widget_smart(GtkWidget *w)
+{
+    g_return_val_if_fail(w, NULL);
+
+    if (GTK_IS_BOX(w)) {
+        GList *children = gtk_container_get_children(GTK_CONTAINER(w));
+        if (g_list_length(children) == 1) {
+            return gnc_dialog_get_widget_smart(GTK_WIDGET(children->data));
+        }
+    }
+    return w;
+}
+
+
+/* Method 1 */
+GtkWidget *gnc_dialog_get_widget(GncDialog *d, const gchar* name)
+{
+    GncDialogPrivate *priv;
+
+    priv = GET_PRIVATE(d);
+    g_return_val_if_fail(name, NULL);
+    return glade_xml_get_widget(priv->xml, name);
+}
+
+void gnc_dialog_set_sensitive(GncDialog *d, const gchar* name, gboolean sen)
+{
+    gtk_widget_set_sensitive(gnc_dialog_get_widget(d, name), sen);
+}
+
+#define IS_A(wid, tname) (g_type_is_a(GTK_WIDGET_TYPE(wid), \
+				      g_type_from_name(tname) ))
+
+#define TYPE_ERROR(wid, tname, failval) do {             \
+    PERR("Expected %s, but found %s", (tname),  \
+        g_type_name(GTK_WIDGET_TYPE(wid)));     \
+    return (failval);                              \
+} while (0)
+
+#define SPECIFIC_INIT(d, name, wid, failval)               \
+    GtkWidget *(wid);                                      \
+    g_return_val_if_fail((d) && (name), (failval));        \
+    (wid) = gnc_dialog_get_widget((d), (name));            \
+    (wid) = gnc_dialog_get_widget_smart((wid));            \
+    g_return_val_if_fail((wid), (failval));
+
+/*
+ *  Type-specific getter/setters.
+ *
+ */
+gboolean gnc_dialog_set_string(GncDialog *d, const gchar* name,
+                               const gchar* val)
+{
+    SPECIFIC_INIT(d, name, wid, FALSE);
+
+    if (IS_A(wid, "GtkEntry"))
+	gtk_entry_set_text(GTK_ENTRY(wid), val);
+    else if (IS_A(wid, "GtkLabel"))
+        gtk_label_set_text(GTK_LABEL(wid), val);
+    else if (IS_A(wid, "GtkCombo")) //deprecated
+        gtk_entry_set_text(GTK_ENTRY(GTK_COMBO(wid)->entry), val);
+    else if (IS_A(wid, "GtkTextView")) {
+        GtkTextBuffer *buf;
+        buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(wid));
+        gtk_text_buffer_set_text(buf, val, -1);
+    } else TYPE_ERROR(wid, "GtkEntry or GtkLabel or GtkTextView", FALSE);
+    //TODO: font support?
+
+    return TRUE;
+}
+
+const gchar * gnc_dialog_get_string(GncDialog *d, const gchar* name)
+{
+    SPECIFIC_INIT(d, name, wid, NULL);
+
+    if (IS_A(wid, "GtkEntry"))
+	return gtk_entry_get_text(GTK_ENTRY(wid));
+    else if (IS_A(wid, "GtkLabel"))
+        return gtk_label_get_text(GTK_LABEL(wid));
+    else if (IS_A(wid, "GtkCombo")) //deprecated
+        return gtk_entry_get_text(GTK_ENTRY(GTK_COMBO(wid)->entry));
+    else if (IS_A(wid, "GtkTextView")) {
+        GtkTextBuffer *buf;
+        GtkTextIter start, end;
+        buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(wid));
+        gtk_text_buffer_get_bounds(buf, &start, &end);
+        return gtk_text_buffer_get_text(buf, &start, &end, TRUE);
+        //FIXME: LEAK: callers are expecting NOT to own the mem.
+    } else if (IS_A(wid, "GtkComboBoxEntry")) {
+        gint col;
+        GtkTreeModel *tm;
+        GtkTreeIter iter;
+        GType type;
+        col = gtk_combo_box_entry_get_text_column(GTK_COMBO_BOX_ENTRY(wid));
+        tm = gtk_combo_box_get_model(GTK_COMBO_BOX(wid));
+        type = gtk_tree_model_get_column_type(tm, col);
+        if (type != G_TYPE_STRING)
+            return NULL;
+        if (!gtk_combo_box_get_active_iter(GTK_COMBO_BOX(wid), &iter)) {
+            GValue val;
+            gtk_tree_model_get_value(tm, &iter, col, &val);
+            return g_value_get_string(&val);
+        } else return NULL;
+    } else TYPE_ERROR(wid, "GtkEntry or GtkLabel or GtkTextView", NULL);
+}
+
+gboolean gnc_dialog_set_double(GncDialog *d, const gchar* name, gdouble val)
+{
+    SPECIFIC_INIT(d, name, wid, FALSE);
+
+    if (IS_A(wid, "GtkSpinButton"))
+	gtk_spin_button_set_value(GTK_SPIN_BUTTON(wid), val);
+    else TYPE_ERROR(wid, "GtkSpinButton", FALSE);
+    return TRUE;
+    //TODO: string conversion?
+}
+
+gdouble gnc_dialog_get_double(GncDialog *d, const gchar* name)
+{
+    SPECIFIC_INIT(d, name, wid, 0.0);
+
+    if (IS_A(wid, "GtkSpinButton"))
+	return gtk_spin_button_get_value(GTK_SPIN_BUTTON(wid));
+    else TYPE_ERROR(wid, "GtkSpinButton", 0.0);
+}
+gboolean gnc_dialog_set_int(GncDialog *d, const gchar* name, gint val)
+{
+    SPECIFIC_INIT(d, name, wid, FALSE);
+
+    if (IS_A(wid, "GtkSpinButton"))
+	gtk_spin_button_set_value(GTK_SPIN_BUTTON(wid), (gdouble)val);
+    else TYPE_ERROR(wid, "GtkSpinButton", FALSE);
+    return TRUE;
+}
+
+gint gnc_dialog_get_int(GncDialog *d, const gchar* name)
+{
+    SPECIFIC_INIT(d, name, wid, 0);
+
+    if (IS_A(wid, "GtkSpinButton"))
+	return gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(wid));
+    else TYPE_ERROR(wid, "GtkSpinButton", 0);
+}
+
+gboolean gnc_dialog_set_date(GncDialog *d, const gchar* name, time_t val)
+{
+    SPECIFIC_INIT(d, name, wid, FALSE);
+
+    if (IS_A(wid, "GnomeDateEdit"))
+	gnome_date_edit_set_time((GnomeDateEdit *)wid, val);
+    else TYPE_ERROR(wid, "GnomeDateEdit", FALSE);
+    return TRUE;
+}
+
+time_t gnc_dialog_get_date(GncDialog *d, const gchar* name)
+{
+    SPECIFIC_INIT(d, name, wid, ((time_t)(-1)));
+
+    if (IS_A(wid, "GnomeDateEdit"))
+	return gnome_date_edit_get_time((GnomeDateEdit *)wid);
+    else TYPE_ERROR(wid, "GnomeDateEdit", ((time_t)(-1)));
+}
+
+
+gboolean gnc_dialog_set_index(GncDialog *d, const gchar* name, gint val)
+{
+    SPECIFIC_INIT(d, name, wid, FALSE);
+
+    if (IS_A(wid, "GtkComboBox"))
+        gtk_combo_box_set_active(GTK_COMBO_BOX(wid), val);
+    else if (IS_A(wid, "GtkOptionMenu"))
+        gtk_option_menu_set_history(GTK_OPTION_MENU(wid),
+                                    (guint)(val < 0 ? -val : val));
+    else TYPE_ERROR(wid, "GtkComboBox", FALSE); // GtkOptionMenu is deprecated.
+    return TRUE;
+}
+
+gint gnc_dialog_get_index(GncDialog *d, const gchar* name)
+{
+    SPECIFIC_INIT(d, name, wid, -1);
+
+    if (IS_A(wid, "GtkComboBox"))
+	return gtk_combo_box_get_active(GTK_COMBO_BOX(wid));
+    else if (IS_A(wid, "GtkOptionMenu"))
+        return gtk_option_menu_get_history(GTK_OPTION_MENU(wid));
+    else TYPE_ERROR(wid, "GtkComboBox", -1); // GtkOptionMenu is deprecated.
+}
+
+gboolean gnc_dialog_set_boolean(GncDialog *d, const gchar* name,
+                                gboolean val)
+{
+    SPECIFIC_INIT(d, name, wid, FALSE);
+
+    if (IS_A(wid, "GtkToggleButton"))
+        gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(wid), val);
+    else TYPE_ERROR(wid, "GtkToggleButton", FALSE);
+    return TRUE;
+}
+
+gboolean gnc_dialog_get_boolean(GncDialog *d, const gchar* name)
+{
+    SPECIFIC_INIT(d, name, wid, FALSE);
+
+    if (IS_A(wid, "GtkToggleButton"))
+	return gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(wid));
+    else TYPE_ERROR(wid, "GtkToggleButton", FALSE);
+}
+
+
+/* Method 3 */
+/* getters and setters */
+static gpointer gd_gtk_entry_get_text(gpointer w)
+{
+    return (gpointer)gtk_entry_get_text(GTK_ENTRY(w));
+}
+static gboolean gd_gtk_entry_set_text(gpointer wid, gpointer val)
+{
+    g_return_val_if_fail(GTK_IS_ENTRY(wid), FALSE);
+    gtk_entry_set_text(GTK_ENTRY(wid), (gchar *) val);
+    return TRUE;
+
+}
+static gpointer gd_gtk_spin_button_get_value(gpointer w)
+{
+    static gdouble d;
+    d = gtk_spin_button_get_value(GTK_SPIN_BUTTON(w));
+    return ((gpointer) &d);
+}
+static gboolean gd_gtk_spin_button_set_value(gpointer w, gpointer d)
+{
+    g_return_val_if_fail(GTK_IS_SPIN_BUTTON(w), FALSE);
+    gtk_spin_button_set_value(GTK_SPIN_BUTTON(w), *(gdouble *)d);
+    return TRUE;
+}
+#if 0
+static const gpointer gd_gtk_toggle_button_get_active(GtkWidget *w)
+{
+    static gboolean b;
+    b = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(w));
+    return ((gpointer) &b);
+}
+static void gd_gtk_toggle_button_set_active(GtkWidget *w, gpointer b)
+{
+    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(w), *(gboolean *)b);
+}
+static const gpointer gd_gtk_combo_box_get_active(GtkWidget *w)
+{
+    static gint i;
+    i = gtk_combo_box_get_active(GTK_COMBO_BOX(w));
+    return ((gpointer) &i);
+}
+static void gd_gtk_combo_box_set_active(GtkWidget *w, gpointer b)
+{
+    gtk_combo_box_set_active(GTK_COMBO_BOX(w), *(gint *)b);
+}
+static const gpointer gd_gtk_text_view_get_buffer(GtkWidget *w)
+{
+    return (gpointer)gtk_text_view_get_buffer(GTK_TEXT_VIEW(w));
+}
+static void gd_gtk_text_view_set_buffer(GtkWidget *w, gpointer b)
+{
+    gtk_text_view_set_buffer(GTK_TEXT_VIEW(w), GTK_TEXT_BUFFER(b));
+}
+static const gpointer gd_gnome_date_edit_get_time(GtkWidget *w)
+{
+    static time_t t;
+    t = gnome_date_edit_get_time(GNOME_DATE_EDIT(w));
+    return ((gpointer) &t);
+}
+static void gd_gnome_date_edit_set_time(GtkWidget *w, gpointer t)
+{
+    gnome_date_edit_set_time(GNOME_DATE_EDIT(w), *(time_t *)t);
+}
+
+/* Order is important. Children before parents. */
+static struct prop_type {
+    gchar *widget_type;
+    GD_Getter_Func getter;
+    GD_Setter_Func setter;
+} prop_types[] = {
+    {"GnomeDateEdit", gd_gnome_date_edit_get_time,
+     gd_gnome_date_edit_set_time },
+    {"GtkLabel", (GD_Getter_Func) gtk_label_get_label,
+     (GD_Setter_Func) gtk_label_set_label},
+    {"GtkToggleButton", gd_gtk_toggle_button_get_active,
+     gd_gtk_toggle_button_set_active},
+    {"GtkComboBox", gd_gtk_combo_box_get_active,
+     gd_gtk_combo_box_set_active},
+};
+
+#define NUM_PROP_TYPES \
+  (sizeof(prop_types) / sizeof(struct prop_type))
+
+static gint
+find_prop_type(GncDialog *d, GtkWidget *wid)
+{
+    gint i;
+    struct prop_type pt;
+
+    for(i = 0; i < NUM_PROP_TYPES; i++) {
+	pt = prop_types[i];
+	if (IS_A(wid, pt.widget_type))
+	    return i;
+    }
+    return -1;
+}
+#endif
+
+typedef gpointer (*GD_Getter_Func)(GtkWidget *w);
+typedef void (*GD_Setter_Func)(GtkWidget *w, gpointer val);
+
+typedef struct {
+    GncDialogGetter getter;
+    GncDialogSetter setter;
+    GncDialogSetter filler;
+} custom_type;
+
+
+void gnc_dialog_register_testing_types(void)
+{
+    gnc_dialog_register_custom(g_type_from_name("GtkSpinButton"),
+                               gd_gtk_spin_button_get_value,
+                               gd_gtk_spin_button_set_value, NULL);
+    gnc_dialog_register_custom(g_type_from_name("GtkEntry"),
+                               gd_gtk_entry_get_text,
+                               gd_gtk_entry_set_text, NULL);
+
+}
+
+static GHashTable *custom_types;
+
+gboolean gnc_dialog_set_custom(GncDialog *d, const gchar* name,
+                               const gpointer val)
+{
+    GType i;
+    custom_type *custom_spec = NULL;
+    SPECIFIC_INIT(d, name, wid, FALSE);
+
+    g_return_val_if_fail(custom_types, FALSE);
+    i = G_TYPE_FROM_INSTANCE(wid);
+    custom_spec = g_hash_table_lookup(
+        custom_types, &i);
+
+    g_return_val_if_fail(custom_spec, FALSE);
+
+    if (custom_spec->setter(wid, val)) {
+        gnc_dialog_set_changed(d, TRUE);
+        return TRUE;
+    }
+    return FALSE;
+}
+
+gpointer gnc_dialog_get_custom(GncDialog *d, const gchar* name)
+{
+    GType i;
+    custom_type *custom_spec = NULL;
+    SPECIFIC_INIT(d, name, wid, NULL);
+
+    g_return_val_if_fail(custom_types, NULL);
+    i = G_TYPE_FROM_INSTANCE(wid);
+    custom_spec = g_hash_table_lookup(
+        custom_types, &i);
+    g_return_val_if_fail(custom_spec, NULL);
+
+    return custom_spec->getter(wid);
+}
+
+gboolean gnc_dialog_fill_custom(GncDialog *d, const char* name);
+
+
+void gnc_dialog_register_custom(GType widgetType, GncDialogGetter getter,
+                                GncDialogSetter setter,
+                                GncDialogSetter filler)
+{
+    custom_type *ct = g_new0(custom_type, 1);
+    GType *key = g_new0(GType, 1);
+
+    if (custom_types == NULL) {
+        custom_types = g_hash_table_new_full(
+            g_int_hash, g_int_equal, g_free, g_free);
+    }
+    ct->getter = getter;
+    ct->setter = setter;
+    ct->filler = filler;
+    *key = widgetType;
+    PINFO("registering with GType %d", (int)widgetType);
+    g_hash_table_insert(custom_types, key, ct);
+}
+
+void gnc_dialog_unregister_custom(GType widgetType)
+{
+    g_hash_table_remove(custom_types, &widgetType);
+}
--- /dev/null
+++ src/gnome-utils/gnc-dialog.h
@@ -0,0 +1,169 @@
+/* Copyright (C) 2005 Chris Shoemaker <c.shoemaker at cox.net>
+ *
+ * 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, contact:
+ *
+ * Free Software Foundation           Voice:  +1-617-542-5942
+ * 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652
+ * Boston, MA  02110-1301,  USA       gnu at gnu.org
+ */
+
+
+#ifndef GNC_DIALOG_H
+#define GNC_DIALOG_H
+
+#include <time.h>
+
+GType gnc_dialog_get_type (void);
+
+/* type macros */
+#define GNC_TYPE_DIALOG            (gnc_dialog_get_type ())
+#define GNC_DIALOG(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), \
+                                      GNC_TYPE_DIALOG, GncDialog))
+#define GNC_DIALOG_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), \
+                                      GNC_TYPE_DIALOG, GncDialogClass))
+#define GNC_IS_DIALOG(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \
+                                      GNC_TYPE_DIALOG))
+#define GNC_IS_DIALOG_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), \
+                                      GNC_TYPE_DIALOG))
+#define GNC_DIALOG_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), \
+                                      GNC_TYPE_DIALOG, GncDialogClass))
+
+typedef struct _GncDialog GncDialog;
+typedef struct _GncDialogClass GncDialogClass;
+
+/**** PROTOTYPES *************************************************/
+
+/* filename is a glade filenames; and root is the name of the root
+   widget you want to show. */
+GncDialog *gnc_dialog_new(const char *filename,
+                          const char *root);
+
+
+typedef gboolean (*GncDialogCallback) (GncDialog *d, gpointer user_data);
+
+/* The apply callback is optional, but useful dialogs will probably
+ * supply one.  The apply callback should return FALSE if any values
+ * are invalid.  In that case, the dialog will not close automatically
+ * after the user clicks OK, and the changed state will not be marked
+ * clean.  If you supply NULL, for the apply callback, no "OK" button
+ * will be visible.
+ *
+ * The close callback is optional.  If you provide a close callback,
+ * its return value should be TRUE if you want to proceed with the
+ * close.  There's no destroy notifier for user_data, but you can
+ * treat the close_cb as one.  So if you must pass this function its
+ * own copy of user_data, free it from within close_cb.
+ *
+ * The help callback return value is not checked.
+ *
+ * Any callback may be NULL, in which case it's not used.  If help_cb
+ * is NULL, no help button is shown.
+ */
+void gnc_dialog_set_cb(GncDialog *d,
+                       GncDialogCallback apply_cb,
+                       GncDialogCallback close_cb,
+                       GncDialogCallback help_cb,
+                       gpointer user_data);
+
+/* By default, GncDialog works best in asynchronous environments,
+ * where your program execution flow isn't waiting for the dialog to
+ * close.  But, if you're using the dialog to do something like fetch
+ * a value you want to return on the stack, then you have to block the
+ * current thread until the dialog is closed.  Calling this function
+ * will do exactly that.
+ */
+void gnc_dialog_block_until_close(GncDialog *d);
+
+/* This is a catch-all interface to whatever kind of widgets may have
+ * been specified in the glade file.  Once you have you widget you can
+ * use whatever interface that widget offers to set and get widget
+ * state.  You _have_ to use if the widget type isn't supported by the
+ * type-specific or type-generic interfaces below.
+ */
+GtkWidget *gnc_dialog_get_widget(GncDialog *d, const gchar* name);
+
+void gnc_dialog_set_sensitive(GncDialog *d, const gchar* name, gboolean sen);
+
+/* Infers val type from widget type *
+*/
+
+/* Type-generic getter/setter: Be careful with these.  They are NOT
+ * type safe.  Also, if they prove to be more trouble than they're
+ * worth, they'll go away.
+ *
+ * These functions try to use the widget type to infer the type of
+ * data pointed at by val.  They will return FALSE if they are unable
+ * to infer value type.  The inferences made are:
+ *
+ * Widget Type ---> Value Type
+ * ===========      ==========
+ * GnomeDateEdit     GDate *
+ * GtkSpinButton     gdouble *
+ * GtkToggleButton   gboolean *
+ * GtkEntry          gchar *
+ * GtkLabel          gchar *
+ * GtkTextView       GtkTextBuffer *
+ * GtkComboBox       gint *
+ *
+ * WARNING: For the given widget type you must cast the corresponding
+ * value type to/from the passed gpointer.  Having mis-matched widget
+ * and value types will likely cause a revolt among the electrons.
+ *
+ */
+gboolean gnc_dialog_set(GncDialog *d, const char* name, const gpointer val);
+gpointer gnc_dialog_get(GncDialog *d, const char* name);
+
+/* Type-specific getter/setters */
+gboolean     gnc_dialog_set_string(GncDialog *d, const char* name,
+                               const gchar* val);
+const gchar* gnc_dialog_get_string(GncDialog *d, const char* name);
+
+gboolean gnc_dialog_set_double(GncDialog *d, const char* name, gdouble val);
+gdouble  gnc_dialog_get_double(GncDialog *d, const char* name);
+
+gboolean gnc_dialog_set_int(GncDialog *d, const char* name, gint val);
+gint  gnc_dialog_get_int(GncDialog *d, const char* name);
+
+gboolean gnc_dialog_set_date(GncDialog *d, const char* name, time_t val);
+time_t   gnc_dialog_get_date(GncDialog *d, const char* name);
+
+gboolean gnc_dialog_set_index(GncDialog *d, const char* name, gint val);
+gint     gnc_dialog_get_index(GncDialog *d, const char* name);
+
+gboolean gnc_dialog_set_boolean(GncDialog *d, const char* name, gboolean val);
+gboolean gnc_dialog_get_boolean(GncDialog *d, const char* name);
+
+/* Possible TODO: there are more types that could be added here.
+
+Maybe currency/gnc_commodity *
+
+*/
+
+gpointer gnc_dialog_get_custom(GncDialog *d, const char* name);
+gboolean gnc_dialog_set_custom(GncDialog *d, const char* name, gpointer val);
+gboolean gnc_dialog_fill_custom(GncDialog *d, const char* name);
+
+/* should return true for success */
+typedef gboolean (*GncDialogSetter) (gpointer widget, gpointer val);
+typedef gpointer (*GncDialogGetter) (gpointer widget);
+//typedef gboolean (*GncDialogFiller) (gpointer widget, gpointer data);
+
+void gnc_dialog_register_custom(GType widgetType, GncDialogGetter getter,
+                                GncDialogSetter setter,
+                                GncDialogSetter filler);
+
+void gnc_dialog_unregister_custom(GType widgetType);
+void gnc_dialog_register_testing_types(void);
+
+#endif
--- /dev/null
+++ src/gnome-utils/gnc-recurrence.h
@@ -0,0 +1,66 @@
+/* gnc-recurrence.h:
+ *
+ *  GncRecurrence is a minimal GUI for specifying a Recurrence.
+ *
+ *  You see, small is _nice_.  :)
+ *
+ */
+
+/* Copyright (C) 2005, Chris Shoemaker <c.shoemaker at cox.net>  *
+ *
+ * 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, contact:
+ *
+ * Free Software Foundation           Voice:  +1-617-542-5942
+ * 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652
+ * Boston, MA  02111-1301,  USA       gnu at gnu.org
+ */
+
+#ifndef GNC_RECURRENCE_H
+#define GNC_RECURRENCE_H
+
+#include <glib.h>
+#include "Recurrence.h"
+
+#define GNC_TYPE_RECURRENCE	  (gnc_recurrence_get_type())
+#define GNC_RECURRENCE(obj)	  G_TYPE_CHECK_INSTANCE_CAST  \
+   (obj, GNC_TYPE_RECURRENCE, GncRecurrence)
+#define GNC_RECURRENCE_CLASS(klass) G_TYPE_CHECK_CLASS_CAST \
+   (klass, GNC_TYPE_RECURRENCE, GncRecurrence)
+#define GNC_IS_RECURRENCE(obj)     G_TYPE_CHECK_INSTANCE_TYPE \
+   (obj, GNC_TYPE_RECURRENCE)
+
+typedef struct _GncRecurrence GncRecurrence;
+typedef struct _GncRecurrenceComp GncRecurrenceComp;
+
+GType gnc_recurrence_get_type(void);
+GtkWidget * gnc_recurrence_new(void);
+
+void gnc_recurrence_set(GncRecurrence *gr, const Recurrence *r);
+
+/* The returned Recurrence is internally owned and is only valid as
+   long as the GncRecurrence is around. */
+const Recurrence * gnc_recurrence_get(GncRecurrence *gr);
+
+/* "composite" recurrences */
+void gnc_recurrence_comp_set_list(GncRecurrenceComp *grc, const GList *r);
+
+/* The GList is newly-allocated, but the Recurrences are internally
+   owned. */
+GList * gnc_recurrence_comp_get_list(GncRecurrenceComp *grc);
+
+/* This works, but is not used.  Kind of experimental... */
+GtkWidget * gnc_recurrence_comp_new(void);
+GType gnc_recurrence_comp_get_type(void);
+
+#endif
--- /dev/null
+++ src/gnome-utils/gnc-recurrence.c
@@ -0,0 +1,562 @@
+/* gnc-recurrence.c:
+ *
+ */
+
+/* Copyright (C) 2005, Chris Shoemaker <c.shoemaker at cox.net>
+ *
+ * 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, contact:
+ *
+ * Free Software Foundation           Voice:  +1-617-542-5942
+ * 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652
+ * Boston, MA  02110-1301,  USA       gnu at gnu.org
+ */
+
+#include "config.h"
+#include <glade/glade.h>
+#include <gnome.h>
+#include <glib.h>
+
+#include "dialog-utils.h"
+#include "gnc-recurrence.h"
+#include "Recurrence.h"
+#include "gnc-trace.h"
+#include "gnc-gdate-utils.h"
+
+static QofLogModule log_module = GNC_MOD_GUI;
+
+struct _GncRecurrence {
+    GtkVBox widget;
+
+    GnomeDateEdit *gde_start;
+    GtkComboBox *gcb_period;
+    GtkCheckButton *gcb_eom;
+    GtkSpinButton *gsb_mult;
+    GtkCheckButton *nth_weekday;
+    GladeXML *xml;
+
+    Recurrence recurrence;
+};
+
+typedef struct {
+    GtkVBoxClass parent_class;
+    void (*changed) (GncRecurrence *gr);
+} GncRecurrenceClass;
+
+typedef enum {
+    GNCRECURRENCE_CHANGED,
+    LAST_SIGNAL
+} GNCR_Signals;
+
+typedef enum {
+    GNCR_DAY,
+    GNCR_WEEK,
+    GNCR_MONTH,
+    GNCR_YEAR,
+} UIPeriodType;
+
+static GObjectClass *parent_class = NULL;
+
+static UIPeriodType get_pt_ui(GncRecurrence *gr)
+{
+    return (gtk_combo_box_get_active(gr->gcb_period));
+}
+
+static void set_pt_ui(GncRecurrence *gr, PeriodType pt)
+{
+    UIPeriodType idx;
+    switch (pt) {
+    case PERIOD_DAY:
+        idx = 0; break;
+    case PERIOD_WEEK:
+        idx = 1; break;
+    case PERIOD_MONTH:
+    case PERIOD_END_OF_MONTH:
+    case PERIOD_NTH_WEEKDAY:
+    case PERIOD_LAST_WEEKDAY:
+        idx = 2; break;
+    case PERIOD_YEAR:
+        idx = 3; break;
+    default: return;
+    }
+    gtk_combo_box_set_active(gr->gcb_period, idx);
+
+    gtk_toggle_button_set_active(
+        GTK_TOGGLE_BUTTON(gr->nth_weekday),
+        (pt == PERIOD_NTH_WEEKDAY || pt == PERIOD_LAST_WEEKDAY));
+
+    gtk_toggle_button_set_active(
+        GTK_TOGGLE_BUTTON(gr->gcb_eom),
+        (pt == PERIOD_END_OF_MONTH || pt == PERIOD_LAST_WEEKDAY));
+}
+
+static gboolean
+is_ambiguous_relative(const GDate *date)
+{
+    GDateDay d;
+    guint8 dim;
+
+    d = g_date_get_day(date);
+    dim = g_date_get_days_in_month(
+        g_date_get_month(date), g_date_get_year(date));
+    return ((d - 1) / 7 == 3) && (dim - d < 7);
+}
+
+static gboolean
+is_ambiguous_absolute(const GDate *date)
+{
+    return (g_date_is_last_of_month(date) &&
+            (g_date_get_day(date) < 31));
+}
+
+static void
+something_changed( GtkWidget *wid, gpointer d )
+{
+    UIPeriodType pt;
+    GDate start;
+    time_t t;
+    gboolean show_last, use_wd;
+    GncRecurrence *gr = GNC_RECURRENCE(d);
+
+
+    pt = get_pt_ui(gr);
+    t = gnome_date_edit_get_time(gr->gde_start);
+    g_date_set_time(&start, t);
+
+    if (pt == GNCR_MONTH)
+        g_object_set(G_OBJECT(gr->nth_weekday), "visible", TRUE, NULL);
+    else {
+        g_object_set(G_OBJECT(gr->nth_weekday), "visible", FALSE, NULL);
+        gtk_toggle_button_set_active(
+            GTK_TOGGLE_BUTTON(gr->nth_weekday), FALSE);
+    }
+    use_wd = gtk_toggle_button_get_active(
+        GTK_TOGGLE_BUTTON(gr->nth_weekday));
+    //TODO: change label
+
+    /* The case under which we show the "end of month" flag is very
+       narrow, because we can almost always DTRT without it. */
+    if (pt == GNCR_MONTH) {
+        if (use_wd)
+            show_last = is_ambiguous_relative(&start);
+        else
+            show_last = is_ambiguous_absolute(&start);
+    } else {
+        show_last = FALSE;
+        gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(gr->gcb_eom), FALSE);
+    }
+    g_object_set(G_OBJECT(gr->gcb_eom), "visible", show_last, NULL);
+
+    g_signal_emit_by_name(d, "changed", NULL);  // not sure if NULL is needed
+}
+
+static void
+gnc_recurrence_init( GncRecurrence *gr )
+{
+    GtkVBox *vb;
+
+    recurrenceSet(&gr->recurrence, 1, PERIOD_MONTH, NULL);
+
+    gr->xml = gnc_glade_xml_new("budget.glade", "RecurrenceEntryVBox");
+    vb = GTK_VBOX(glade_xml_get_widget(gr->xml, "RecurrenceEntryVBox"));
+    gr->gde_start = GNOME_DATE_EDIT(glade_xml_get_widget(gr->xml,
+                                                         "GDE_StartDate"));
+    gtk_widget_set_no_show_all(GTK_WIDGET(gr->gde_start), TRUE);
+    gr->gcb_period = GTK_COMBO_BOX(glade_xml_get_widget(gr->xml,
+                                                        "GCB_PeriodType"));
+    gr->gsb_mult = GTK_SPIN_BUTTON(glade_xml_get_widget(gr->xml, "GSB_Mult"));
+    gr->gcb_eom = GTK_CHECK_BUTTON(glade_xml_get_widget(gr->xml,
+                                                        "GCB_EndOfMonth"));
+    gr->nth_weekday = GTK_CHECK_BUTTON(glade_xml_get_widget(gr->xml,
+                                                            "GCB_NthWeekday"));
+    gtk_widget_set_no_show_all(GTK_WIDGET(gr->gcb_eom), TRUE);
+    gtk_widget_set_no_show_all(GTK_WIDGET(gr->nth_weekday), TRUE);
+
+
+    gtk_container_add( GTK_CONTAINER(&gr->widget), GTK_WIDGET(vb) );
+
+    gnc_recurrence_set(gr, &gr->recurrence);
+    something_changed( GTK_WIDGET(gr), gr);
+
+    /* respond to changes */
+    g_signal_connect( G_OBJECT(gr->gde_start), "date_changed",
+    		      G_CALLBACK(something_changed), gr );
+    g_signal_connect( G_OBJECT(gr->gcb_period), "changed",
+    		      G_CALLBACK(something_changed), gr );
+    g_signal_connect( G_OBJECT(gr->gsb_mult), "value-changed",
+    		      G_CALLBACK(something_changed), gr );
+    g_signal_connect( G_OBJECT(gr->gcb_eom), "toggled",
+    		      G_CALLBACK(something_changed), gr );
+    g_signal_connect( G_OBJECT(gr->nth_weekday), "toggled",
+    		      G_CALLBACK(something_changed), gr );
+
+    gtk_widget_show_all( GTK_WIDGET(&gr->widget) );
+}
+
+void
+gnc_recurrence_set(GncRecurrence *gr, const Recurrence *r)
+{
+    PeriodType pt;
+    guint mult;
+    GDate start;
+
+    g_return_if_fail(gr && r);
+    pt = recurrenceGetPeriodType(r);
+    mult = recurrenceGetMultiplier(r);
+    start = recurrenceGetDate(r);
+
+    gtk_spin_button_set_value(gr->gsb_mult, (gdouble) mult);
+
+    // is there some better way?
+    {
+        time_t t;
+        t = gnc_timet_get_day_start_gdate (&start);
+        gnome_date_edit_set_time(gr->gde_start, t);
+    }
+
+    set_pt_ui(gr, pt);
+}
+
+const Recurrence *
+gnc_recurrence_get(GncRecurrence *gr)
+{
+    time_t t;
+    guint mult;
+    UIPeriodType period;
+    PeriodType pt;
+    GDate start;
+    gboolean use_eom = FALSE, rel;
+
+    mult = (guint) gtk_spin_button_get_value_as_int(gr->gsb_mult);
+    t = gnome_date_edit_get_time(gr->gde_start);
+    g_date_set_time(&start, t);
+    period = get_pt_ui(gr);
+
+    switch (period) {
+    case GNCR_DAY:
+        pt = PERIOD_DAY; break;
+    case GNCR_WEEK:
+        pt = PERIOD_WEEK; break;
+    case GNCR_MONTH:
+        rel = gtk_toggle_button_get_active(
+            GTK_TOGGLE_BUTTON(gr->nth_weekday));
+        if (rel) {
+            if (is_ambiguous_relative(&start)) {
+                use_eom = gtk_toggle_button_get_active(
+                    GTK_TOGGLE_BUTTON(gr->gcb_eom));
+            } else {
+                GDateDay d;
+                d = g_date_get_day(&start);
+
+                use_eom = ((d - 1) / 7 == 4);
+            }
+            if (use_eom)
+                pt = PERIOD_LAST_WEEKDAY;
+            else pt = PERIOD_NTH_WEEKDAY;
+        } else {
+            if (g_date_is_last_of_month(&start) &&
+                (g_date_get_day(&start) < 31)) {
+                // ambiguous, need to examine the checkbox
+                use_eom = gtk_toggle_button_get_active(
+                    GTK_TOGGLE_BUTTON(gr->gcb_eom));
+            } else {
+                // if it's the last dom, use eom anyway because it's the 31st.
+                use_eom = g_date_is_last_of_month(&start);
+            }
+            if (use_eom)
+                pt = PERIOD_END_OF_MONTH;
+            else pt = PERIOD_MONTH;
+        }
+        break;
+    case GNCR_YEAR:
+        pt = PERIOD_YEAR; break;
+    default:
+        pt = PERIOD_INVALID;
+    }
+
+
+    recurrenceSet(&gr->recurrence, mult, pt, &start);
+    return &gr->recurrence;
+
+}
+static void
+gnc_recurrence_finalize(GObject *o)
+{
+    GncRecurrence *gr = GNC_RECURRENCE(o);
+
+    if (gr)
+        G_OBJECT_CLASS (parent_class)->finalize (o);
+}
+
+static void
+gnc_recurrence_class_init( GncRecurrenceClass *klass )
+{
+    GObjectClass *object_class;
+    static gint signals[LAST_SIGNAL] = { 0 };
+
+    object_class = G_OBJECT_CLASS (klass);
+    signals[GNCRECURRENCE_CHANGED] =
+        g_signal_new ("changed",
+                      G_OBJECT_CLASS_TYPE (object_class),
+                      G_SIGNAL_RUN_FIRST,
+                      G_STRUCT_OFFSET (GncRecurrenceClass, changed),
+                      NULL,
+                      NULL,
+                      g_cclosure_marshal_VOID__VOID,
+                      G_TYPE_NONE,
+                      0);
+
+    parent_class = g_type_class_peek_parent (klass);
+    object_class->finalize = gnc_recurrence_finalize;
+}
+
+GType
+gnc_recurrence_get_type()
+{
+    static GType type = 0;
+    if (type == 0) {
+        static GTypeInfo typeinfo = {
+            sizeof(GncRecurrenceClass),
+            NULL, NULL,
+            (GClassInitFunc)gnc_recurrence_class_init,
+            NULL, NULL,
+            sizeof(GncRecurrence),
+            0,
+            (GInstanceInitFunc)gnc_recurrence_init
+        };
+
+        type = g_type_register_static (GTK_TYPE_VBOX, "GncRecurrence",
+                                       &typeinfo, 0);
+    }
+    return type;
+}
+
+GtkWidget *
+gnc_recurrence_new()
+{
+    GncRecurrence *gr;
+
+    ENTER(" ");
+    gr = g_object_new(gnc_recurrence_get_type(), NULL);
+    LEAVE(" ");
+    return GTK_WIDGET(gr);
+}
+
+/* TODO: Maybe this stuff should go into another file.
+ *
+ */
+
+struct _GncRecurrenceComp {
+    GtkScrolledWindow widget;
+
+    GtkVBox *vbox;
+    GtkHBox *hbox;
+    GtkHButtonBox *hbb;
+    gint num_rec;
+    GtkButton *buttRemove;
+    GtkButton *buttAdd;
+
+    GList *rlist;
+};
+
+typedef struct {
+    GtkScrolledWindowClass parent_class;
+    void (*changed) (GncRecurrenceComp *gr);
+} GncRecurrenceCompClass;
+
+typedef enum {
+    GNCRECURRENCECOMP_CHANGED,
+    GNCRC_LAST_SIGNAL
+} GNCRC_Signals;
+
+static void grc_changed(GtkWidget *w, gpointer data)
+{
+    g_signal_emit_by_name(data, "changed", NULL);
+}
+static void addRecurrence(GncRecurrenceComp *grc, GncRecurrence *gr)
+{
+
+    gtk_box_pack_start(GTK_BOX(grc->vbox), GTK_WIDGET(gr),
+                       FALSE, FALSE, 3);
+    g_signal_connect( G_OBJECT(gr), "changed",
+                      G_CALLBACK(grc_changed), grc );
+    grc->num_rec++;
+
+    g_object_set(G_OBJECT(grc->buttRemove), "sensitive",
+                 (grc->num_rec > 1), NULL);
+    g_signal_emit_by_name(G_OBJECT(grc), "changed", NULL);
+
+
+}
+static void removeRecurrence(GncRecurrenceComp *grc)
+{
+    GList *children, *last;
+
+    grc->num_rec--;
+
+    children = gtk_container_get_children(GTK_CONTAINER(grc->vbox));
+    last = g_list_last(children);
+    gtk_widget_destroy(GTK_WIDGET(last->data));
+    g_list_free(children);
+    g_signal_emit_by_name(G_OBJECT(grc), "changed", NULL);
+
+
+    g_object_set(G_OBJECT(grc->buttRemove), "sensitive",
+                 (grc->num_rec > 1), NULL);
+
+}
+
+static void addClicked(GtkButton *b, gpointer data)
+{
+    GncRecurrenceComp *grc = data;
+    GncRecurrence *gr;
+
+    gr = GNC_RECURRENCE(gnc_recurrence_new());
+    addRecurrence(grc, gr);
+}
+
+static void removeClicked(GtkButton *b, gpointer data)
+{
+    GncRecurrenceComp *grc = data;
+
+    if (grc->num_rec > 1)
+        removeRecurrence(grc);
+}
+
+void
+gnc_recurrence_comp_set_list(GncRecurrenceComp *grc, const GList *rlist)
+{
+    const GList *iter;
+
+    g_return_if_fail(grc);
+
+    while (grc->num_rec > 0)
+        removeRecurrence(grc);
+
+    for (iter = rlist; iter; iter = iter->next) {
+        GncRecurrence *gr = GNC_RECURRENCE(gnc_recurrence_new());
+
+        gnc_recurrence_set(gr, (Recurrence *)iter->data);
+        addRecurrence(grc, gr);
+    }
+}
+
+GList *
+gnc_recurrence_comp_get_list(GncRecurrenceComp *grc)
+{
+    GList *rlist = NULL, *children;
+    gint i;
+
+
+    children = gtk_container_get_children(GTK_CONTAINER(grc->vbox));
+    for (i = 0; i < g_list_length(children); i++) {
+        GncRecurrence *gr;
+        const Recurrence *r;
+        gr = GNC_RECURRENCE(g_list_nth_data(children, i));
+        r = gnc_recurrence_get(gr);
+        rlist = g_list_append(rlist, (gpointer)r);
+    }
+    g_list_free(children);
+    return rlist;
+}
+
+
+static void
+gnc_recurrence_comp_init(GncRecurrenceComp *grc)
+{
+    GtkWidget *vb;
+
+    grc->hbb = GTK_HBUTTON_BOX(gtk_hbutton_box_new());
+    grc->vbox = GTK_VBOX(gtk_vbox_new(FALSE, 1));
+    grc->rlist = NULL;
+
+    grc->buttAdd = GTK_BUTTON(gtk_button_new_from_stock(GTK_STOCK_ADD));
+    g_signal_connect(G_OBJECT(grc->buttAdd), "clicked",
+                     G_CALLBACK(addClicked), grc);
+    grc->buttRemove = GTK_BUTTON(gtk_button_new_from_stock(GTK_STOCK_REMOVE));
+    g_signal_connect(G_OBJECT(grc->buttRemove), "clicked",
+                     G_CALLBACK(removeClicked), grc);
+
+    gtk_box_pack_start(GTK_BOX(grc->hbb), GTK_WIDGET(grc->buttAdd),
+                     FALSE, FALSE, 3);
+    gtk_box_pack_start(GTK_BOX(grc->hbb), GTK_WIDGET(grc->buttRemove),
+                     FALSE, FALSE, 3);
+
+    vb = gtk_vbox_new(FALSE, 1);
+    gtk_box_pack_start(GTK_BOX(vb), GTK_WIDGET(grc->hbb),
+                       FALSE, FALSE, 3);
+    gtk_box_pack_start(GTK_BOX(vb), GTK_WIDGET(grc->vbox),
+                       FALSE, FALSE, 3);
+
+    gtk_scrolled_window_add_with_viewport(
+        GTK_SCROLLED_WINDOW(grc), GTK_WIDGET(vb));
+    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(grc),
+                                   GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
+
+    grc->num_rec = 0;
+    gtk_widget_show_all(GTK_WIDGET(grc));
+    addClicked(NULL, grc);
+}
+
+static void
+gnc_recurrence_comp_class_init( GncRecurrenceCompClass *klass )
+{
+    GObjectClass *object_class;
+    static gint signals[GNCRC_LAST_SIGNAL] = { 0 };
+
+    object_class = G_OBJECT_CLASS (klass);
+    signals[GNCRECURRENCECOMP_CHANGED] =
+        g_signal_new ("changed",
+                      G_OBJECT_CLASS_TYPE (object_class),
+                      G_SIGNAL_RUN_FIRST,
+                      G_STRUCT_OFFSET (GncRecurrenceCompClass, changed),
+                      NULL,
+                      NULL,
+                      g_cclosure_marshal_VOID__VOID,
+                      G_TYPE_NONE,
+                      0);
+
+    //parent_class = g_type_class_peek_parent (klass);
+    //object_class->finalize = gnc_recurrence_finalize;
+}
+
+GType
+gnc_recurrence_comp_get_type()
+{
+    static GType type = 0;
+    if (type == 0) {
+        static GTypeInfo typeinfo = {
+            sizeof(GncRecurrenceCompClass),
+            NULL, NULL,
+            (GClassInitFunc)gnc_recurrence_comp_class_init,
+            NULL, NULL,
+            sizeof(GncRecurrenceComp),
+            0,
+            (GInstanceInitFunc)gnc_recurrence_comp_init
+        };
+
+        type = g_type_register_static (GTK_TYPE_SCROLLED_WINDOW,
+                                       "GncRecurrenceComp", &typeinfo, 0);
+    }
+    return type;
+}
+
+GtkWidget *
+gnc_recurrence_comp_new()
+{
+    GncRecurrenceComp *grc;
+    grc = g_object_new(gnc_recurrence_comp_get_type(), NULL);
+    return GTK_WIDGET(grc);
+}
+
+/* ========================= END OF FILE =========================== */
--- /dev/null
+++ src/gnome-utils/test/test-gnc-recurrence.c
@@ -0,0 +1,106 @@
+/* Copyright (C) 2005, Chris Shoemaker <c.shoemaker at cox.net>
+ * This file is free software.  See COPYING for details. */
+
+/* test-gnc-recurrence.c:
+ *
+ *     When you close the window, a text description of the
+ * recurrence is printed.
+ *
+ */
+
+#include <glib.h>
+#include <gtk/gtk.h>
+#include <stdio.h>
+#include "gnc-recurrence.h"
+#include "Recurrence.h"
+
+static GtkWidget * mainwin;
+static GncRecurrence *rw;
+static GncRecurrenceComp *grc;
+
+static void get_list(GtkWidget *w)
+{
+    gchar *s;
+    GList *rlist;
+    rlist = gnc_recurrence_comp_get_list(grc);
+    s = recurrenceListToString(rlist);
+    printf("%s\n", s);
+
+    g_free(s);
+    g_list_free(rlist);
+}
+
+static void changed(GtkWidget *widget)
+{
+    gchar *s;
+    const Recurrence *r;
+
+    r = gnc_recurrence_get(rw);
+    s = recurrenceToString(r);
+    printf("%s\n", s);
+    g_free(s);
+}
+
+static void die(GtkWidget *widget)
+{
+    gtk_main_quit();
+}
+
+static void show_gnc_recurrence()
+{
+    GDate d;
+    Recurrence *r;
+    GList *rl = NULL;
+
+    rw = GNC_RECURRENCE(gnc_recurrence_new());
+
+    r = g_new(Recurrence, 1);
+    g_list_append(rl, r);
+    g_date_set_dmy(&d, 17, 4, 2005);
+    recurrenceSet(r, 1, PERIOD_WEEK, &d);
+
+    gnc_recurrence_set(rw, r);
+    g_free(r);
+
+    gtk_container_add(GTK_CONTAINER(mainwin), GTK_WIDGET(rw));
+    g_signal_connect(rw, "changed", G_CALLBACK(changed), NULL);
+}
+
+static void show_gnc_recurrence_comp()
+{
+    GList *rlist = NULL;
+    Recurrence r[2];
+
+    grc = (GncRecurrenceComp *)gnc_recurrence_comp_new();
+
+    gtk_container_add(GTK_CONTAINER(mainwin), GTK_WIDGET(grc));
+
+    recurrenceSet(&r[0], 1, PERIOD_MONTH, NULL);
+    rlist = g_list_append(rlist, &r[0]);
+    recurrenceSet(&r[1], 1, PERIOD_YEAR, NULL);
+    rlist = g_list_append(rlist, &r[1]);
+
+    gnc_recurrence_comp_set_list(grc, rlist);
+    g_list_free(rlist);
+
+    g_signal_connect(grc, "changed", G_CALLBACK(get_list), NULL);
+    //rlist = gnc_recurrence_comp_get_list(grc);
+}
+
+
+int main (int argc, char ** argv)
+{
+    gtk_init(&argc, &argv);
+
+    mainwin = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+    g_signal_connect(mainwin, "delete-event", G_CALLBACK(die), NULL);
+
+    if (argc > 1)
+        show_gnc_recurrence();
+    else
+        show_gnc_recurrence_comp();
+
+    gtk_widget_show_all(mainwin);
+    gtk_main();
+    return 0;
+}
--- /dev/null
+++ src/gnome-utils/test/test-gnc-dialog.c
@@ -0,0 +1,145 @@
+/* Copyright (C) 2005, Chris Shoemaker <c.shoemaker at cox.net>
+ * This file is free software.  See COPYING for details. */
+
+#include <glib.h>
+#include <gtk/gtk.h>
+#include <stdio.h>
+#include "gnc-recurrence.h"
+#include "Recurrence.h"
+#include "qofbook.h"
+#include "gnc-dialog.h"
+#include "gnc-tree-view-account.h"
+
+//static GtkWidget * mainwin;
+static GncDialog *pw;
+//static QofBook *book;
+
+
+static gboolean apply_cb (GncDialog *pw, gpointer _n)
+{
+    const gchar *s;
+    gdouble d;
+    gpointer p;
+    gboolean b;
+    gint i;
+
+    s = gnc_dialog_get_custom(pw, "SampleEntry");
+    printf("Entry: %s\n", s);
+    s = gnc_dialog_get_string(pw, "SampleLabel");
+    printf("Label: %s\n", s);
+
+    p = gnc_dialog_get_custom(pw, "SampleSpinButton");
+    d = *(double *)p;
+    printf("SpinButton: %f\n", d);
+
+    b = gnc_dialog_get_boolean(pw, "SampleToggleButton");
+    printf("ToggleButton: %s\n", b?"true":"false");
+
+    i = gnc_dialog_get_index(pw, "SampleComboBox");
+    printf("ComboBox: %d\n", i);
+
+    /*
+    gnc_dialog_get(pw, "SampleEntry", &s);
+    gnc_dialog_get(pw, "SampleLabel", &s);
+    printf("Label: %s\n", s);
+    gnc_dialog_get(pw, "SampleSpinButton", &d);
+    printf("SpinButton: %f\n", *d);
+    gnc_dialog_get(pw, "SampleToggleButton", &b);
+    printf("ToggleButton: %s\n", *b?"true":"false");
+    gnc_dialog_get(pw, "SampleComboBox", &i);
+    printf("ComboBox: %d\n", *i);
+    */
+
+    //r = gnc_dialog_get(pw, "SampleCustomGncRecurrence");
+    //s = recurrenceToString(r);
+    //printf("Recurrence: %s\n", s);
+    //g_free(s);
+    return TRUE;
+
+}
+
+static gboolean close_cb (GncDialog *pw, gpointer _n)
+{
+    gtk_widget_destroy(GTK_WIDGET(pw));
+    gtk_main_quit();
+    return TRUE;
+}
+
+static void test_setters(GncDialog *pw)
+{
+    gdouble d = 3.0;
+    //GDate date;
+    //gboolean b = TRUE;
+    //gint i = 2;
+    //Recurrence *r;
+    //GList *rl;
+
+    gnc_dialog_set_custom(pw, "SampleEntry", "entrytest");
+    //gnc_dialog_set(pw, "SampleLabel", "labeltest");
+    gnc_dialog_set_custom(pw, "SampleSpinButton", &d);
+    //gnc_dialog_set(pw, "SampleToggleButton", &b);
+    //gnc_dialog_set(pw, "SampleComboBox", &i);
+
+    /*
+    r = recurrenceNew();
+    entry = recurrenceEntryNew();
+    recurrenceAddEntry(r, entry);
+    g_date_set_dmy(&date, 17, 4, 2005);
+    recurrenceEntrySet(entry, 2, PERIOD_WEEKLY, &date);
+    gnc_dialog_set(pw, "SampleCustomGncRecurrence", r);
+    g_free(r);
+    */
+}
+
+static void init_widgets(GncDialog *pw)
+{
+    //GtkComboBox *cbox;
+    GtkListStore *ls;
+    GtkTreeIter iter;
+    GtkCellRenderer *cell;
+    int i;
+
+    ls = gtk_list_store_new(1, G_TYPE_STRING);
+    for (i = 0; i<5; i++) {
+        gtk_list_store_append(ls, &iter);
+        gtk_list_store_set(ls, &iter, 0, "item", -1);
+    }
+
+    //cbox = GTK_COMBO_BOX(gnc_dialog_get_widget(pw, "SampleComboBox"));
+    //gtk_combo_box_set_model(cbox, GTK_TREE_MODEL(ls));
+
+    cell = gtk_cell_renderer_text_new();
+    //gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(cbox), cell, TRUE);
+    //gtk_cell_layout_add_attribute(GTK_CELL_LAYOUT(cbox), cell, "text", 0);
+
+    {
+        /*
+          GncTreeViewAccount *account_view;
+
+          account_view = GNC_TREE_VIEW_ACCOUNT(gnc_dialog_get_widget(pw, "SampleCustomTreeViewAccount"));
+
+          gnc_tree_view_account_configure_columns (account_view, "name", NULL);
+        */
+    }
+}
+
+int main (int argc, char ** argv)
+{
+ // g_log_set_always_fatal( G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING );
+
+  gtk_init(&argc, &argv);
+
+  g_type_init();
+  pw = gnc_dialog_new("budget.glade", "SampleOptions");
+  gnc_dialog_set_cb(pw, apply_cb, close_cb, NULL, NULL);
+
+  gnc_dialog_register_testing_types();
+  init_widgets(pw);
+
+  test_setters(pw);
+  gtk_widget_show_all(GTK_WIDGET(pw));
+
+  gtk_main();
+  return 0;
+
+}
--- /dev/null
+++ src/report/standard-reports/budget.scm
@@ -0,0 +1,376 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; budget.scm: budget report
+;;
+;; (C) 2005 by Chris Shoemaker <c.shoemaker at cox.net>
+;;
+;; based on cash-flow.scm by:
+;; Herbert Thoma <herbie at hthoma.de>
+;;
+;; 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, contact:
+;;
+;; Free Software Foundation           Voice:  +1-617-542-5942
+;; 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652
+;; Boston, MA  02110-1301,  USA       gnu at gnu.org
+;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(define-module (gnucash report budget))
+(use-modules (gnucash main)) ;; FIXME: delete after we finish modularizing.
+(use-modules (ice-9 slib))
+(use-modules (gnucash gnc-module))
+
+(require 'printf)
+(require 'sort)
+
+(gnc:module-load "gnucash/report/report-system" 0)
+(gnc:module-load "gnucash/gnome-utils" 0) ;for gnc:html-build-url
+
+(define reportname (N_ "Budget Report"))
+
+;; define all option's names so that they are properly defined
+;; in *one* place.
+;;(define optname-from-date (N_ "From"))
+;;(define optname-to-date (N_ "To"))
+
+(define optname-display-depth (N_ "Account Display Depth"))
+(define optname-show-subaccounts (N_ "Always show sub-accounts"))
+(define optname-accounts (N_ "Account"))
+
+(define optname-price-source (N_ "Price Source"))
+(define optname-show-rates (N_ "Show Exchange Rates"))
+(define optname-show-full-names (N_ "Show Full Account Names"))
+
+(define optname-budget (N_ "Budget"))
+
+;; options generator
+(define (budget-report-options-generator)
+  (let ((options (gnc:new-options)))
+
+    (gnc:register-option
+     options
+     (gnc:make-budget-option
+      gnc:pagename-general optname-budget
+      "a" (N_ "Budget")))
+
+    ;; date interval
+    ;;(gnc:options-add-date-interval!
+    ;; options gnc:pagename-general
+    ;; optname-from-date optname-to-date "a")
+
+    (gnc:options-add-price-source!
+     options gnc:pagename-general optname-price-source "c" 'weighted-average)
+
+    ;;(gnc:register-option
+    ;; options
+    ;; (gnc:make-simple-boolean-option
+    ;;  gnc:pagename-general optname-show-rates
+    ;;  "d" (N_ "Show the exchange rates used") #f))
+
+    (gnc:register-option
+     options
+     (gnc:make-simple-boolean-option
+      gnc:pagename-general optname-show-full-names
+      "e" (N_ "Show full account names (including parent accounts)") #t))
+
+    ;; accounts to work on
+    (gnc:options-add-account-selection!
+     options gnc:pagename-accounts
+     optname-display-depth optname-show-subaccounts
+     optname-accounts "a" 2
+     (lambda ()
+       (gnc:filter-accountlist-type
+        '(bank cash asset stock mutual-fund)
+        (gnc:group-get-subaccounts (gnc:get-current-group))))
+     #f)
+
+    ;; Set the general page as default option tab
+    (gnc:options-set-default-section options gnc:pagename-general)
+
+    options)
+  )
+
+(define (gnc:html-table-add-budget-values!
+         html-table acct-table budget params)
+
+  (define (gnc:html-table-add-budget-line!
+           html-table rownum colnum
+           budget acct exchange-fn)
+    (let* ((num-periods (gnc:budget-get-num-periods budget))
+           (period 0)
+           )
+      (while (< period num-periods)
+             (let* ((bgt-col (+ (* period 2) colnum 1))
+                    (act-col (+ 1 bgt-col))
+
+                    (comm (gnc:account-get-commodity acct))
+                    (numeric-val (gnc:budget-get-account-period-value
+                                  budget acct period))
+
+                    (bgt-val (gnc:make-gnc-monetary
+                              comm numeric-val))
+                    (numeric-val (gnc:budget-get-account-period-actual-value
+                                  budget acct period))
+                    (act-val (gnc:make-gnc-monetary
+                              comm numeric-val))
+                    (reverse-balance? (gnc:account-reverse-balance? acct))
+                    )
+
+               (cond (reverse-balance? (set! act-val
+                                       (gnc:monetary-neg act-val))))
+
+
+               (gnc:html-table-set-cell!
+                html-table
+                rownum bgt-col bgt-val)
+
+               (gnc:html-table-set-cell!
+                html-table
+                rownum act-col act-val)
+
+               (set! period (+ period 1))
+               )
+             )
+      )
+    )
+  (define (gnc:html-table-add-budget-headers!
+           html-table colnum budget)
+    (let* ((num-periods (gnc:budget-get-num-periods budget))
+           (period 0)
+           )
+
+      ;; prepend 2 empty rows
+      (gnc:html-table-prepend-row! html-table '())
+      (gnc:html-table-prepend-row! html-table '())
+
+      ;; make the column headers
+      (while (< period num-periods)
+             (let* ((bgt-col (+ (* period 2) colnum 1))
+                    (act-col (+ 1 bgt-col))
+                    (date (gnc:budget-get-period-start-date budget period))
+                    )
+               (gnc:html-table-set-cell!
+                html-table 0 bgt-col (gnc:print-date date))
+
+               (gnc:html-table-set-cell!
+                html-table
+                1 bgt-col "Bgt")
+
+               (gnc:html-table-set-cell!
+                html-table
+                1 act-col "Act")
+
+               (set! period (+ period 1))
+               )
+             )
+      )
+    )
+
+  (let* ((num-rows (gnc:html-acct-table-num-rows acct-table))
+	 (rownum 0)
+         (numcolumns (gnc:html-table-num-columns html-table))
+	 ;;(html-table (or html-table (gnc:make-html-table)))
+	 (get-val (lambda (alist key)
+		    (let ((lst (assoc-ref alist key)))
+		      (if lst (car lst) lst))))
+         ;; WARNING: we implicitly depend here on the details of
+         ;; gnc:html-table-add-account-balances.  Specifically, we
+         ;; assume that it makes twice as many columns as it uses for
+          ;; account labels.  For now, that seems to be a valid
+         ;; assumption.
+         (colnum (quotient numcolumns 2))
+
+	 )
+
+    ''(display (list "colnum: " colnum  "numcolumns: " numcolumns))
+    ;; call gnc:html-table-add-budget-line! for each account
+    (while (< rownum num-rows)
+           (let* ((env (append
+			(gnc:html-acct-table-get-row-env acct-table rownum)
+			params))
+                  (acct (get-val env 'account))
+                  (exchange-fn (get-val env 'exchange-fn))
+                  )
+             (gnc:html-table-add-budget-line!
+              html-table rownum colnum
+              budget acct exchange-fn)
+             (set! rownum (+ rownum 1)) ;; increment rownum
+             )
+           ) ;; end of while
+
+    ;; column headers
+    (gnc:html-table-add-budget-headers! html-table colnum budget)
+
+    )
+  ) ;; end of define
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; budget-renderer
+;; set up the document and add the table
+;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(define (budget-renderer report-obj)
+  (define (get-option pagename optname)
+    (gnc:option-value
+     (gnc:lookup-option
+      (gnc:report-options report-obj) pagename optname)))
+
+  (gnc:report-starting reportname)
+
+  ;; get all option's values
+  (let* ((budget (get-option gnc:pagename-general optname-budget))
+         (display-depth (get-option gnc:pagename-accounts
+                                    optname-display-depth))
+         (show-subaccts? (get-option gnc:pagename-accounts
+                                     optname-show-subaccounts))
+         (accounts (get-option gnc:pagename-accounts
+                               optname-accounts))
+         (row-num 0) ;; ???
+	 (work-done 0)
+	 (work-to-do 0)
+         ;;(report-currency (get-option gnc:pagename-general
+         ;;                             optname-report-currency))
+         (show-full-names? (get-option gnc:pagename-general
+                                       optname-show-full-names))
+         (separator (gnc:account-separator-char))
+
+         (doc (gnc:make-html-document))
+         ;;(table (gnc:make-html-table))
+         ;;(txt (gnc:make-html-text))
+         )
+
+    ;; is account in list of accounts?
+    (define (same-account? a1 a2)
+      (string=? (gnc:account-get-guid a1) (gnc:account-get-guid a2)))
+
+    (define (same-split? s1 s2)
+      (string=? (gnc:split-get-guid s1) (gnc:split-get-guid s2)))
+
+    (define account-in-list?
+      (lambda (account accounts)
+        (cond
+          ((null? accounts) #f)
+          ((same-account? (car accounts) account) #t)
+          (else (account-in-list? account (cdr accounts))))))
+
+    (define split-in-list?
+      (lambda (split splits)
+	(cond
+	 ((null? splits) #f)
+	 ((same-split? (car splits) split) #t)
+	 (else (split-in-list? split (cdr splits))))))
+
+    (define account-in-alist
+      (lambda (account alist)
+        (cond
+	   ((null? alist) #f)
+           ((same-account? (caar alist) account) (car alist))
+           (else (account-in-alist account (cdr alist))))))
+
+    ;; helper for sorting of account list
+    (define (account-full-name<? a b)
+      (string<? (gnc:account-get-full-name a) (gnc:account-get-full-name b)))
+
+    ;; helper for account depth
+    (define (account-get-depth account)
+      (define (account-get-depth-internal account-internal depth)
+        (let ((parent (gnc:account-get-parent-account account-internal)))
+          (if parent
+            (account-get-depth-internal parent (+ depth 1))
+            depth)))
+      (account-get-depth-internal account 1))
+
+    (define (accounts-get-children-depth accounts)
+      (apply max
+	     (map (lambda (acct)
+		    (let ((children
+			   (gnc:account-get-immediate-subaccounts acct)))
+		      (if (null? children)
+			  1
+			  (+ 1 (accounts-get-children-depth children)))))
+		  accounts)))
+    ;; end of defines
+
+    ;; add subaccounts if requested
+    (if show-subaccts?
+        (let ((sub-accounts (gnc:acccounts-get-all-subaccounts accounts)))
+          (for-each
+            (lambda (sub-account)
+              (if (not (account-in-list? sub-account accounts))
+                  (set! accounts (append accounts sub-accounts))))
+            sub-accounts)))
+
+    (if (not (or (null? accounts) (null? budget) (not budget)))
+
+        (let* ((tree-depth (if (equal? display-depth 'all)
+                               (accounts-get-children-depth accounts)
+                               display-depth))
+               ;;(account-disp-list '())
+
+               ;; Things seem to crash if I don't set 'end-date to
+               ;; _something_ but the actual value isn't used.
+               (env (list (list 'end-date (gnc:get-today))
+                          (list 'display-tree-depth tree-depth)
+                          (list 'depth-limit-behavior 'flatten)
+                          ))
+               (acct-table #f)
+               (html-table (gnc:make-html-table))
+               (params '())
+               (report-name (get-option gnc:pagename-general
+                                        gnc:optname-reportname))
+               )
+
+          (gnc:html-document-set-title!
+           doc (sprintf #f (_ "%s - %s")
+                        report-name (gnc:budget-get-name budget)))
+
+          (set! accounts (sort accounts account-full-name<?))
+
+          (set! acct-table
+                (gnc:make-html-acct-table/env/accts env accounts))
+
+          ;; We do this in two steps: First the account names...  the
+          ;; add-account-balances will actually compute and add a
+          ;; bunch of current account balances, too, but we'll
+          ;; overwrite them.
+          (set! html-table (gnc:html-table-add-account-balances
+                            #f acct-table params))
+
+          ;; ... then the budget values
+          (gnc:html-table-add-budget-values!
+           html-table acct-table budget params)
+
+          ;; hmmm... I expected that add-budget-values would have to
+          ;; clear out any unused columns to the right, out to the
+          ;; table width, since the add-account-balance had put stuff
+          ;; there, but it doesn't seem to matter.
+
+          (gnc:html-document-add-object! doc html-table)
+          )
+
+        ;; error condition: either no accounts or no budgets specified
+        (gnc:html-document-add-object!
+         doc
+         (gnc:html-make-generic-options-warning
+	  reportname (gnc:report-id report-obj))))
+
+    (gnc:report-finished)
+    doc))
+
+(gnc:define-report
+ 'version 1
+ 'name reportname
+ 'menu-path (list gnc:menuname-income-expense)
+ 'options-generator budget-report-options-generator
+ 'renderer budget-renderer)
+


More information about the gnucash-changes mailing list