[RFC] A differnt options system

Chris Shoemaker c.shoemaker at cox.net
Wed Feb 16 01:16:40 EST 2005


Background: I'm mainly interested in getting budgeting to work, but
along the way I discovered that the options system in gnucash wasn't
exactly up to doing what I needed and, frankly, it looked like it was
going to be a major headache to make it work for me.

So, on the side, I'm trying to come up with an options system that
does what I want, and maybe can be generally useful, too.  Consider
the attached a rough-draft.  All the usually first-draft caveats
apply.  It's probably buggy, etc.  (Although it passes enough unit
tests to suggest that at least the idea isn't fundamentally flawed.)

I'm very interested in feedback on this approach, and especially on a
good name for this beast.  The header file contains a mini-manifesto
covering my thought process on the matter.

If anyone wants a closer look, I have some examples of this thing in
operation, but they're mostly in code that's not ready for public
consumption yet.  Still, I could scrape something together...

-chris
-------------- next part --------------
/********************************************************************\
 * 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        *
 * 59 Temple Place - Suite 330        Fax:    +1-617-542-2652        *
 * Boston, MA  02111-1307,  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-prop-win.h"
#include "dialog-utils.h"  // for gnc_glade_xml_new
//#include "gnc-engine-util.h"

static short module = MOD_GUI;

#define GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), \
  GNC_TYPE_PROP_WIN, GncPropWinPrivate))

static GtkDialogClass *parent_class = NULL;
static GList *active_pw = NULL;

typedef struct {
    GladeXML *xml;
    GncPropWinCallback apply_cb;
    GncPropWinCallback close_cb;
    GncPropWinCallback help_cb;
    gpointer user_data;
    gboolean changed;
} GncPropWinPrivate;

void gnc_properties_window_finalize(GObject *pw)
{
    GncPropWinPrivate *priv;
    g_return_if_fail(pw);
    priv = GET_PRIVATE(pw);
    //g_free(priv); // not needed?!
    G_OBJECT_CLASS(parent_class)->finalize(pw);
}

static void
gnc_properties_window_class_init (GncPropWinClass *klass)
{
    GObjectClass *gobject_class;
    GtkObjectClass *object_class;
    
    parent_class = g_type_class_peek_parent (klass);

    gobject_class = G_OBJECT_CLASS (klass);
    object_class = GTK_OBJECT_CLASS (klass);
    
    g_type_class_add_private (gobject_class, sizeof (GncPropWinPrivate));
    
    /* GObject signals */
    gobject_class->finalize = gnc_properties_window_finalize;
    
#if DEBUG_REFERENCE_COUNTING
    gtk_quit_add (0, (GtkFunction)gnc_properties_window_report_references,
    	      NULL);
#endif
}

#if DEBUG_REFERENCE_COUNTING
static void
dump_model (GncPropWin *pw, gpointer dummy)
{
    g_warning("GncPropWin %p still exists.", pw);
}
static gint
gnc_properties_window_report_references (void)
{
  g_list_foreach(active_pw, (GFunc)dump_model, NULL);
  return 0;
}
#endif

static void
gnc_properties_window_init (GncPropWin *pw)
{
    active_pw = g_list_append (active_pw, pw);
}

GType gnc_properties_window_get_type (void)
{
    static GType t = 0;
  
    if (!t) {
	static const GTypeInfo info = {
	    sizeof (GncPropWinClass),
	    NULL, /* base_init */
	    NULL, /* base_final */
	    (GClassInitFunc) gnc_properties_window_class_init,
	    NULL, /* class final */
	    NULL, /* class data */
	    sizeof (GncPropWin),
	    0, /* n_preallocs */
	    (GInstanceInitFunc) gnc_properties_window_init,
	    NULL,
	};
	t = g_type_register_static (GTK_TYPE_DIALOG,
                                    "GncPropWin", &info, 0);
    }
    return t;
}

static void gnc_properties_window_set_changed(GncPropWin *pw, gboolean changed)
{
    GncPropWinPrivate *priv;

    priv = GET_PRIVATE(pw);
    priv->changed = changed;
    gtk_dialog_set_response_sensitive(&pw->parent, GTK_RESPONSE_APPLY, 
                                      changed);
}

static void gnc_properties_window_response_cb(GtkDialog *dlg,
                                              gint response, GncPropWin *pw)
{
    gboolean success = TRUE;
    GncPropWinPrivate *priv = GET_PRIVATE(pw);

    switch (response) {
    case GTK_RESPONSE_HELP:
	if (priv->help_cb)
            (priv->help_cb)(pw, priv->user_data);
	break;
    case GTK_RESPONSE_OK:
    case GTK_RESPONSE_APPLY:
	if (priv->apply_cb)
	    success = priv->apply_cb (pw, priv->user_data);
	
        if (success) 
            gnc_properties_window_set_changed(pw, FALSE);

	if ((response == GTK_RESPONSE_APPLY) || !success)
	    break;
        // fall through 
    default:
	if (priv->close_cb)
	    (priv->close_cb)(pw, priv->user_data);
	else gtk_widget_destroy(GTK_WIDGET(dlg));
    }
}

static void changed_cb(GObject *obj, gpointer pw)
{
    gnc_properties_window_set_changed(GNC_PROP_WIN(pw), TRUE);
}

static void 
gnc_properties_window_watch_for_changes(GtkWidget *wid, gpointer pw)
{
    if (GTK_IS_BUTTON(wid))
        g_signal_connect(G_OBJECT(wid), "clicked", G_CALLBACK(changed_cb), pw);
    
    if (GTK_IS_EDITABLE(wid) || GTK_IS_COMBO_BOX(wid))
        g_signal_connect(G_OBJECT(wid), "changed", G_CALLBACK(changed_cb), pw);
    
    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), pw);
    }

    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), pw);
    }
    //Possibly TODO: GtkCalendar?

    /* Recurse over all "contained" widgets */
    if (GTK_IS_CONTAINER(wid)) {
        gtk_container_foreach(GTK_CONTAINER(wid), 
                              gnc_properties_window_watch_for_changes, pw);
    }
}

GncPropWin *gnc_properties_window_new(const char* filename, 
				      const char* root)
{
    GncPropWin *pw;
    GncPropWinPrivate *priv;
    GtkDialog *dlg;
    GtkWidget *child;

    pw = g_object_new(GNC_TYPE_PROP_WIN, NULL);
    dlg = GTK_DIALOG(pw);  
    priv = GET_PRIVATE(pw);

    /* 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("GncPropWin root widget must not be a toplevel widget");
        return NULL;
    }
    gtk_container_add(GTK_CONTAINER(dlg->vbox), child);

    /* Prepare the dialog. */
    gtk_dialog_add_buttons(dlg, GTK_STOCK_HELP, GTK_RESPONSE_HELP, 
                           GTK_STOCK_OK, GTK_RESPONSE_OK, 
                           GTK_STOCK_APPLY, GTK_RESPONSE_APPLY, 
                           GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, NULL);

    g_signal_connect(dlg, "response", 
                     G_CALLBACK(gnc_properties_window_response_cb), pw);
    gtk_dialog_set_response_sensitive(dlg, GTK_RESPONSE_APPLY, FALSE);
    gnc_properties_window_watch_for_changes(child, (gpointer) pw);
    return pw;
}

void gnc_properties_window_set_cb(GncPropWin *pw, 
				  GncPropWinCallback apply_cb, 
				  GncPropWinCallback close_cb,
                                  GncPropWinCallback help_cb,
				  gpointer user_data) 
{
    GncPropWinPrivate *priv;
  
    priv = GET_PRIVATE(pw);
    priv->apply_cb = apply_cb;
    priv->close_cb = close_cb;
    priv->help_cb = help_cb;
    priv->user_data = user_data;
} 


/* Method 1 */
GtkWidget *gnc_properties_window_get_widget(GncPropWin *pw, const gchar* name)
{
    GncPropWinPrivate *priv;

    priv = GET_PRIVATE(pw);
    g_return_val_if_fail(name, NULL);
    return glade_xml_get_widget(priv->xml, name);
}

#define IS_A(wid, tname) (g_type_is_a(GTK_WIDGET_TYPE(wid), \
				      g_type_from_name(tname) ))

#define TYPE_ERROR(wid, tname) do {             \
    PERR("Expected %s, but found %s", (tname),  \
        g_type_name(GTK_WIDGET_TYPE(wid)));     \
    return FALSE;                               \
} while (0)

#define SPECIFIC_INIT(pw, name, wid)                        \
    GtkWidget *(wid);                                       \
    g_return_val_if_fail((pw) && (name), FALSE);            \
    (wid) = gnc_properties_window_get_widget((pw), (name)); \
    g_return_val_if_fail((wid), FALSE);          

/*
 *  Type-specific getter/setters.
 * 
 */
gboolean gnc_properties_window_set_string(GncPropWin *pw, const gchar* name, 
                                          const gchar* val)
{
    SPECIFIC_INIT(pw, name, wid);

    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 TYPE_ERROR(wid, "GtkEntry");
    //TODO: font support?

    return TRUE;
}

gboolean gnc_properties_window_get_string(GncPropWin *pw, const gchar* name, 
                                          const gchar **val)
{
    SPECIFIC_INIT(pw, name, wid);

    if (IS_A(wid, "GtkEntry"))
	*val = gtk_entry_get_text(GTK_ENTRY(wid));
    else if (IS_A(wid, "GtkLabel"))
        *val = gtk_label_get_text(GTK_LABEL(wid));
    else TYPE_ERROR(wid, "GtkEntry");
    return TRUE;
}

gboolean gnc_properties_window_set_number(GncPropWin *pw, const gchar* name, 
                                          gdouble val)
{
    SPECIFIC_INIT(pw, name, wid);

    if (IS_A(wid, "GtkSpinButton"))
	gtk_spin_button_set_value(GTK_SPIN_BUTTON(wid), val);
    else TYPE_ERROR(wid, "GtkSpinButton");
    return TRUE;
}

gboolean gnc_properties_window_get_number(GncPropWin *pw, const gchar* name, 
                                          gdouble *val)
{
    SPECIFIC_INIT(pw, name, wid);

    if (IS_A(wid, "GtkSpinButton"))
	*val = gtk_spin_button_get_value(GTK_SPIN_BUTTON(wid));
    else TYPE_ERROR(wid, "GtkSpinButton");
    return TRUE;
}

gboolean gnc_properties_window_set_date(GncPropWin *pw, const gchar* name, 
                                        time_t val)
{
    SPECIFIC_INIT(pw, name, wid);

    if (IS_A(wid, "GnomeDateEdit"))
	gnome_date_edit_set_time((GnomeDateEdit *)wid, val);
    else
	TYPE_ERROR(wid, "GnomeDateEdit");
    return TRUE;
}

gboolean gnc_properties_window_get_date(GncPropWin *pw, const gchar* name,
                                        time_t *val)
{
    SPECIFIC_INIT(pw, name, wid);

    if (IS_A(wid, "GnomeDateEdit"))
	*val = gnome_date_edit_get_time((GnomeDateEdit *)wid);
    else TYPE_ERROR(wid, "GnomeDateEdit");
    return TRUE;
}

gboolean gnc_properties_window_set_combo_active(GncPropWin *pw, 
                                                const gchar* name, gint val)
{
    SPECIFIC_INIT(pw, name, wid);

    if (IS_A(wid, "GtkComboBox"))
        gtk_combo_box_set_active(GTK_COMBO_BOX(wid), val);
    else TYPE_ERROR(wid, "GtkComboBox");
    return TRUE;
}

gboolean gnc_properties_window_get_combo_active(GncPropWin *pw, 
                                                const gchar* name, gint *val)
{
    SPECIFIC_INIT(pw, name, wid);

    if (IS_A(wid, "GtkComboBox"))
	*val = gtk_combo_box_get_active(GTK_COMBO_BOX(wid));
    else TYPE_ERROR(wid, "GtkComboBox");
    return TRUE;
}

gboolean gnc_properties_window_set_boolean(GncPropWin *pw, const gchar* name,
                                           gboolean val)
{
    SPECIFIC_INIT(pw, name, wid);

    if (IS_A(wid, "GtkToggleButton"))
        gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(wid), val);
    else TYPE_ERROR(wid, "GtkToggleButton");
    return TRUE;
}

gboolean gnc_properties_window_get_boolean(GncPropWin *pw, const gchar* name,
                                           gboolean *val)
{
    SPECIFIC_INIT(pw, name, wid);

    if (IS_A(wid, "GtkToggleButton"))
	*val = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(wid));
    else TYPE_ERROR(wid, "GtkToggleButton");
    return TRUE;
}


/* Method 3 */
/* getters and setters */
static const gpointer gpw_gtk_entry_get_text(GtkWidget *w)
{
    return (gpointer)gtk_entry_get_text(GTK_ENTRY(w));
}
static const gpointer gpw_gtk_spin_button_get_value(GtkWidget *w)
{
    static gdouble d;
    d = gtk_spin_button_get_value(GTK_SPIN_BUTTON(w));
    return ((gpointer) &d);
}
static void gpw_gtk_spin_button_set_value(GtkWidget *w, gpointer d)
{
    gtk_spin_button_set_value(GTK_SPIN_BUTTON(w), *(gdouble *)d); 
}
static const gpointer gpw_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 gpw_gtk_toggle_button_set_active(GtkWidget *w, gpointer b)
{
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(w), *(gboolean *)b);
}
static const gpointer gpw_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 gpw_gtk_combo_box_set_active(GtkWidget *w, gpointer b)
{
    gtk_combo_box_set_active(GTK_COMBO_BOX(w), *(gint *)b);
}
static const gpointer gpw_gtk_text_view_get_buffer(GtkWidget *w)
{
    return (gpointer)gtk_text_view_get_buffer(GTK_TEXT_VIEW(w));
}
static void gpw_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 gpw_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 gpw_gnome_date_edit_set_time(GtkWidget *w, gpointer t)
{
    gnome_date_edit_set_time(GNOME_DATE_EDIT(w), *(time_t *)t);
}

typedef const gpointer (*GPW_Getter_Func)(GtkWidget *w);
typedef void (*GPW_Setter_Func)(GtkWidget *w, gpointer val);

/* Order is important. Children before parents. */
static struct prop_type {
    gchar *widget_type;
    GPW_Getter_Func getter;
    GPW_Setter_Func setter;
} prop_types[] = {
    {"GtkSpinButton", gpw_gtk_spin_button_get_value, 
     gpw_gtk_spin_button_set_value},
    {"GnomeDateEdit", gpw_gnome_date_edit_get_time, 
     gpw_gnome_date_edit_set_time },
    {"GtkEntry", (GPW_Getter_Func) gpw_gtk_entry_get_text, 
     (GPW_Setter_Func) gtk_entry_set_text },
    {"GtkLabel", (GPW_Getter_Func) gtk_label_get_label, 
     (GPW_Setter_Func) gtk_label_set_label},
    {"GtkToggleButton", gpw_gtk_toggle_button_get_active, 
     gpw_gtk_toggle_button_set_active},
    {"GtkComboBox", gpw_gtk_combo_box_get_active, 
     gpw_gtk_combo_box_set_active},
    {"GtkTextView", gpw_gtk_text_view_get_buffer,
     gpw_gtk_text_view_set_buffer},
};

#define NUM_PROP_TYPES \
  (sizeof(prop_types) / sizeof(struct prop_type))

gint find_prop_type(GncPropWin *pw, 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;
}


gboolean gnc_properties_window_set(GncPropWin *pw, const gchar* name, 
				   const gpointer val)
{
    gint i;
    SPECIFIC_INIT(pw, name, wid);

    i = find_prop_type(pw, wid);
    g_return_val_if_fail(i != -1, FALSE);

    prop_types[i].setter(wid, val);
    gnc_properties_window_set_changed(pw, TRUE);
    return TRUE;
}

gboolean gnc_properties_window_get(GncPropWin *pw, const gchar* name, 
                                   gpointer *val)
{
    gint i;
    SPECIFIC_INIT(pw, name, wid);

    i = find_prop_type(pw, wid);
    g_return_val_if_fail(i != -1, FALSE);

    *val = prop_types[i].getter(wid);
    return TRUE;
}
-------------- next part --------------
/********************************************************************\
 * 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        *
 * 59 Temple Place - Suite 330        Fax:    +1-617-542-2652        *
 * Boston, MA  02111-1307,  USA       gnu at gnu.org                    *
 *                                                                   *
\********************************************************************/

/* [Note: This file is a work-in-progress.  In particular, I don't
   like the names GncPropWin and gnc_properties_window_*.  When I
   settle on a better name, I'll search and replace it.] */


/* Design Notes:
 * 
 *    GncPropWin is a derivative of GtkDialog.  It is designed to
 * provide a generic container for presenting a set of user-changeable
 * values.  Typically, these values might be the properties of a
 * user-created object (like an Account), or the options that affect a
 * particular view of an object, or some values that the user needs to
 * supply in order to perform some action, or overall program
 * "preferences".  The point is, this is a pretty generic API for
 * allowing the user to see and change some values.
 * 
 *    Conceptually, GncPropWin is similar to GNCOptionWin (found in
 * src/gnome-utils/dialog-options.h).  In fact, parts of GncPropWin
 * are modeled after GNCOptionWin.  However, there are significant
 * implementation differences that make GncPropWin a better choice
 * than GNCOptionWin for some cases.  Here, we're going to describe
 * some of the similarities and differences.
 *
 * The "Options Triad":
 *
 *    I call GNCOptionWin, GncOptionDB, and options.scm the "options
 * triad."  GNCOptionWin is a dialog wrapper for GncOptionDB, which is
 * the C interface to an options system that is written in Scheme (see
 * src/app-utils/options.scm).  Unfortunately, there is very little
 * documentation for GNCOptionWin and GncOptionDB.  (Actually, there's
 * just a few comments in the code.  Maybe some of the comments here
 * will grow into some documentation that can then be moved to
 * GNCOptionWin.)  The underlying options system is well-documented in
 * gnucash-design.info.
 * 
 *    The options triad can be a bit confusing, but a few tips can
 * ease the understanding.  The options system is actually much more
 * than just a way to display and modify option values.  It is also
 * intended to be the primary storage container for those option
 * values.  That means that the option values "live" somewhere in the
 * Scheme interpreter.  On the C side of things, GnuCash has access
 * (getting and setting) to the option values though guile bindings.
 * As a consequence, using the options system for new options
 * (I'm not talking about new option types.) requires at
 * least a bit of scheme coding.
 * 
 *    A typical scenario might roughly go like this: (Lots of details
 * are left out here - this is only intended as a conceptual outline.)
 *
 * 1) Specify the option list in a scheme file. See
 * src/app-utils/prefs.scm for an example.
 *
 * 2) Create an GncOptionDB from the scheme specification with:
 *
 *      GncOptionDB* gnc_option_db_new(SCM options);
 *
 * 3) Make a GNCOptionWin: 
 *
 *      optwin = gnc_options_dialog_new("MyTitle");
 *
 * 4) Connect the GncOptionDB to the GNCOptionWin with: 
 *
 *      gnc_build_options_dialog_contents(GNCOptionWin *, GncOptionDB *);
 *
 * 5) Register callbacks with the GNCOptionWin: 
 *
 *      gnc_options_dialog_set_{apply|help|close}_cb(GNCOptionWin *, 
 *          GNCOptionWinCallback thunk, gpointer cb_data);
 * 
 * 6) From the "apply" callback, call gnc_option_db_commit(GncOptionDB *).
 * 
 * If you only want to access your values from the Scheme side, then
 * that may be all that is necessary, because the option values are
 * _stored_ in scheme.  However, if you want to access the option
 * values from the C side then in your callbacks (most likely the
 * "apply" callback) you'll probably have things like:
 *
 *    val = gnc_option_db_lookup_{type}_option(myOptionDB, 
 *        "MySection", "MyOptionName", somedefaultvalue);
 *
 * and when you first show the GNCOptionWin, you'd probably have
 * things like:
 *
 *    gnc_option_db_set_{type}_option(myOptionDB, 
 *        "MySection", "MyOptionName", val); 
 *
 *    Now, if everything works perfectly, this is not too complicated,
 * because the casual C programmer only had to do a little light
 * scheme programming when he/she specified the option list (which
 * _is_ documented).  Plus, the examples of the overall program
 * preferences are helpful, e.g. src/app-utils/prefs.scm.  Conceptually,
 * the C programmer can probably ignore the fact that the values are
 * stored in scheme, because some of GncOptionDB's API hides that
 * detail, allowing the mental model to be one of getting and setting
 * from the _GUI_.
 *
 * 
 * But... there are cases where everything doesn't work perfectly, and
 * there are cases where things work, but painfully:
 *
 * 1) GncOptionWin's option layout is quite flat.  Linear layout order
 * can be specified in the scheme option definition, but that's it.
 * 
 * 2) Some option types are buggy.
 *
 * 3) Not all GnuCash values have option types, and new GnuCash values
 * that you invent for sure won't, (unless you make them).  The
 * options triad supports a fairly complete set of simple option
 * types.  This is good because extending the underlying options
 * system to support new option types requires a _deep_ understanding
 * of Scheme, Guile, Gtk+/Gnome, and existing GnuCash users (Here and
 * henceforth, by "user" I usually mean code that uses some API.) of
 * the option system (currently, Reports options, business options and
 * top-level GnuCash preferences).
 *
 * 4) Reusing groups of options isn't easy.  There's probably some way
 * to define the option list as a concatenation of smaller option
 * lists, but I don't think there are any examples of this.
 *
 * 5) If you're storing option values in C as enumerated types
 * (multi-choice option type), you'll need to specify the enumeration
 * in C and in Scheme, and keep them in sync.
 *
 * 6) Your options are displayed in a GtkNotebook page.  I hope that's
 * what you want, because that's the way it is.
 *
 *    Of course these problems are probably solvable by improving
 * GNCOptionWin and GncOptionDB and the Scheme options system.  But,
 * that's not easy.  (Feel free to prove otherwise.)  The fact is, the
 * complexity of the "options triad" is beyond the comprehension of an
 * averagely bright high-school student.  And outside of its
 * author(s), it seems there are very few who understand its depths
 * (GncPropWin author excluded).
 *
 *     That said, there are cases where the options triad is clearly
 * what you need.  For example: 
 *
 * 1) You need access to the option values from Scheme, or 
 *
 * 2) You need the option values to have a life-time longer than that
 * of any C-side object.
 *
 *     Furthermore, there are several qualities that make the options
 * triad appealing even when the above requirements are absent:
 * 
 * 1) The concept of "sections" is useful for organizing large sets of
 * options.
 *
 * 2) The GncOptionDB API hides the GTK+ details of setting and
 * getting values to/from the widgets.
 *
 * 3) The scheme option type definitions hide the GTK+ details.
 *
 * 4) The combination of 2) and 3) above mean that, conceivably, a
 * programmer could set/get option values without even knowing about
 * GTK+, (e.g. GtkEntry, or GtkSpinButton).  More realistically, it
 * means a programmer spends less time looking up things like
 * gtk_entry_set_text() and gtk_spin_button_get_value() in the GTK+
 * API Reference.
 *
 *
 *
 * *** GncPropWin ***
 * 
 *    The design goal of GncPropWin is to replicate some of the most
 * useful qualities of the options triad while making a drastically
 * different implementation choice that will, hopefully, lead to an
 * extensible and maintainable options system.
 *
 * XML vs. Scheme:
 *
 *    Specifically, GncPropWin expects the option list and layout to
 * be defined in XML instead of Scheme.  Given GnuCash's extensive
 * investment in Scheme, this may prove controversial.  
 *
 *    Scheme has its pros and cons. <shameless grin> Scheme is a real
 * programming language, so it has computational power and
 * expressiveness that XML will never have.  OTOH, the task of
 * setting/getting option values (which is essentially a programming
 * language task and must be defined in whatever programming language
 * you're working in) is distinct from the task of enumerating the
 * list of options and their layout (which is not a computational task
 * but a descriptive task).
 *
 *    Tools like glade really drive this point home.  After all, it is
 * natural to specify option layout in the same place you specify the
 * option list itself.  Even options.scm does this, which means using
 * Scheme to specify GUI layout -- probably not Scheme's strong suit.
 *
 *    Another selling point for XML is libglade.  Yes, exchanging
 * guile for libglade is just trading libraries, but it's not an even
 * trade.  Guile is a big deal -- full language bindings between a
 * strongly-typed, compiled language and a loosely-typed, interpreted
 * language with garbage collection.  Just take it for granted, you're
 * going to be reading the manual.  OTOH, libglade is very specific in
 * its task and is, comparatively, _very_ simple.
 *
 * No long-term option value storage:
 *
 *    Another significant design difference is the option value
 * storage.  GncPropWin is meant to be considered just a GUI tool,
 * like a dialog window, and doesn't provide any long-term (longer
 * than the dialog's lifetime) option value storage.  So, you must set
 * the option values every time you create the GncPropWin and get the
 * option values before the GncPropWin is destroyed.  (Which you have
 * to do anyway for the options triad if you're setting object
 * property values for C objects that don't have Scheme storage.)
 *
 *    Even though GncPropWin is less ambitious than the option triad
 * in that respect, I consider this a design feature, not a
 * short-coming, because it decouples value storage from the gui.  For
 * C object properties, this is good because the object will usually
 * provide their own property value storage as fields in the object's
 * structure.  For things like top-level program preferences this may
 * be not-so-good, because option value persistence no longer comes
 * "automatically".  
 *  
 *
 * Composability
 * 
 *    To encourage reuse of option groups and their associated GUI
 * layouts, an options system should support composition of option
 * groups into larger option groups.  The option triad probably
 * supports this with regular scheme list operations, but I haven't
 * seen an example.
 *
 *    GncPropWin supports composition of option groups.  First,
 * libglade will support composition through the use of the "custom
 * widget".  This is very easy.  But, it requires that you create a
 * composite GtkWidget that derives from some GtkWidget class.
 *
 * [Originally I had composability method that went like: "After
 * loading the root glade file, GncPropWin will search for specially
 * named containers, recursively loading other glade files and
 * re-parenting them into the specially named container."  I decided
 * that was overkill and an unnecessary maintenance burden.
 * libglade's support for custom widgets should be sufficient.
 * Besides, it fosters good design by encouraging the encapsulation of
 * related "options" into a composite widget.]
 * 
 * Type-Safety
 *
 *    Type-safety was a sticky issue in the design of GncPropWin.
 * Currently there are 3 levels of type-safety offered by GncPropWin's
 * API, and they can all be used interchangeably.
 * 
 *    Method 1: The safest method is to use
 * gnc_properties_window_get_widget() to get the GtkWidget and then
 * directly call the widget's own getter/setter functions.  This is
 * also the way to manipulate the widget in any other way, such as
 * changing its visual properties.  Pros: very flexible and powerful,
 * and works for new widget types without any modification to
 * GncPropWin.  Cons: doesn't hide any of the GTK+ details of the
 * option widgets, so users must know and use the GTK+ API.
 *
 *    Method 2: Another, slightly less safe, method is to use the
 * type-specific getter/setter wrappers provided by GncPropWin.  These
 * will at least complain if the widget's value type isn't appropriate
 * for the function that was used.  However, if the widget's value
 * type is especially complex, then type-specific wrappers may not
 * exist (yet).  In that case, you can either implement them, or use
 * the first method above.  You should be able to infer the list of
 * supported types from the list of function prototypes available in
 * gnc-prop-win.h.  This is roughly equivalent to the API provided by
 * GNCOptionWin.
 *
 *    Method 3: Finally, a convenient, but completely type-UNsafe,
 * method is to use the type-generic getter/setter functions provided
 * by GncPropWin, i.e. gnc_properties_window_{gs}et.  These methods
 * use gpointers to pass and return value types.  This means lots of
 * casting and big pains if caller and function don't agree about the
 * inferred value type.  So if you use this interface, be sure to get
 * the types right.  Be aware that some widgets can legitimately use
 * multiple value types (e.g. GtkSpinButton) but GncPropWin can only
 * use one.  As with the type-specific wrappers, not all widget to
 * value type inferences are implemented, so if your favorite pair is
 * missing, just implement them or use the first method.  This method
 * will complain if it can't infer the value type from the widget
 * type.
 * 
 *    Type Safety Summary and Recommendations: If method 2 gives the
 * user rope, method 3 gives them rope tied in a noose.  Even though
 * method 3 is the easiest to use (when used correctly), I think it's
 * of dubious long-term value.  It's probably more trouble that it's
 * worth.  Furthermore, I'd recommend against sophisticated
 * type-handlers for method 2; let the user revert to method 1 for any
 * non-trivial types.  Note, the current implementation of this method
 * is not thread safe.
 *
 *
 * Usage Example:
 * 
 *    GncPropWin is pretty easy to use.  A typical usage scenario
 * roughly goes:
 *
 * 1) Specify your options, option widgets, and option layout in a
 * glade file.
 *
 * 2) When you want to present the options to the user call
 * gnc_properties_window_new() with the name of your glade file and
 * the root widget.
 *
 * 3) Set any default option values, previous option value state, or
 * option view state by using the gnc_properties_window_get_widget()
 * or gnc_properties_set*() functions.
 *
 * 4) Hook in your apply and close callbacks by calling
 * gnc_properties_window_set_cb().
 *
 * 5) In your apply callback, use gnc_properties_window_get_widget()
 * or gnc_properties_get*() functions to get the option values and
 * perform the appropriate actions with the new values.
 *
 * Comparison
 *
 *     Currently, I believe that all the option types supported by the
 * options triad are also supported by GncPropWin.  But, in some cases,
 * that's only because of the all-powerful fall-back:
 * gnc_properties_window_get_widget().  With that, you get back
 * whatever type of widget was specified in the glade file, and you
 * can call any method offered by the widget.  
 *
 *     If you compare the convenient, type-specific interfaces offered
 * by GNCOptionDB and GncPropWin, GncPropWin supports most, but not
 * all of the option types GNCOptionDB does.  Currently, fonts, colors
 * and currencies don't have type-specific convenience interfaces.
 *
 *     Does GncPropWin retain any of the benefits offered by the
 * options triad?  Let's consider some of the benefits mentioned above: 
 * 
 * 1) the "sections" concept for option organization: GncPropWin
 * doesn't offer (or impose) this organization, but makes it very easy
 * to implement.  The GUI designer can group options into GtkNotebook
 * pages, and use consistent name prefixes.  But, better yet, the GUI
 * designer could also choose any other organizational scheme.
 * 
 * 2) Hiding details of GTK+: On the specification side, GTK+ details
 * are only as abstracted as Glade makes them.  On the use side, the
 * type-specific interface hides as much GTK+ as GNCOptionDB does.
 * Obviously, gnc_properties_window_get_widget() doesn't hide any
 * GTK+, but hey, that's the price of power.
 *
 *     What benefits do we lose?  Well, chiefly, option value
 * persistence.  Converting any users of the options triad to use
 * GncPropWin will requires providing C-side storage for the option
 * values, unless the values were already being stored on both sides.
 * Some of the values may be candidates for storage with GConf.
 * 
 *     Are there any new benefits?  I hope so. :) I hope the chief new
 * benefit will be maintainability.  GncPropWin is quite a bit simpler
 * than the options triad, and it could be made even simpler if the
 * type generic interface was dropped.  It also seems to be more
 * powerful in terms of supported option types.  Newly invented widget
 * types are supported automatically, and anything that derives from
 * GtkWidget can be accessed.
 *
 *     What next?  I hope to use (and hope others can use) GncPropWin
 * for new option dialogs, and maybe eventually to convert some users
 * of the options triad to use GncPropWin.
 *
 */


#ifndef GNC_PROP_WIN_H
#define GNC_PROP_WIN_H

#include <glade/glade.h>
#include <time.h>

GType gnc_properties_window_get_type (void);

/* type macros */
#define GNC_TYPE_PROP_WIN            (gnc_properties_window_get_type ())
#define GNC_PROP_WIN(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), \
                                      GNC_TYPE_PROP_WIN, GncPropWin))
#define GNC_PROP_WIN_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), \
                                      GNC_TYPE_PROP_WIN, GncPropWinClass))
#define GNC_IS_PROP_WIN(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \
                                      GNC_TYPE_PROP_WIN))
#define GNC_IS_PROP_WIN_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), \
                                      GNC_TYPE_PROP_WIN))
#define GNC_PROP_WIN_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), \
                                      GNC_TYPE_PROP_WIN, GncPropWinClass))

typedef struct {
    GtkDialog parent;
} GncPropWin;

typedef struct {
    GtkDialogClass parent;
} GncPropWinClass;


/**** PROTOTYPES *************************************************/

/* filename is a glade filenames; and root is the name of the root
   widget you want to show. */
GncPropWin *gnc_properties_window_new(const char *filename,
				      const char *root);


typedef gboolean (*GncPropWinCallback) (GncPropWin *pw, gpointer user_data);

/* The apply callback will only be called if GncPropWin detected that
 * some widget state changed, but false positives are possible.  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 provide a close callback, it should at least do something
 * like gtk_destroy_widget(GTK_WIDGET(Ph)).  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 close and help callbacks return values are 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_properties_window_set_cb(GncPropWin *pw,
                                  GncPropWinCallback apply_cb,
                                  GncPropWinCallback close_cb,
                                  GncPropWinCallback help_cb,
                                  gpointer user_data);

/* 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_properties_window_get_widget(GncPropWin *pw, 
                                            const gchar* name);


/* 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_properties_window_set(GncPropWin *pw, const char* name, 
				   const gpointer val);
gboolean gnc_properties_window_get(GncPropWin *pw, const char* name, 
                                   gpointer *val);


/* Type-specific getter/setters */
gboolean gnc_properties_window_set_string(GncPropWin *pw, const char* name,
                                          const gchar* val);
gboolean gnc_properties_window_get_string(GncPropWin *pw, 
                                          const char* name, const gchar **val);

gboolean gnc_properties_window_set_number(GncPropWin *pw, const char* name,
                                          gdouble val);
gboolean gnc_properties_window_get_number(GncPropWin *pw, const char* name, 
                                          gdouble *val);

gboolean gnc_properties_window_set_date(GncPropWin *pw, const char* name,
                                        time_t val);
gboolean gnc_properties_window_get_date(GncPropWin *pw, const char* name, 
                                        time_t *val);

gboolean gnc_properties_window_set_combo_active(GncPropWin *pw, 
                                                const char* name, gint val);
gboolean gnc_properties_window_get_combo_active(GncPropWin *pw, 
                                                const char* name, gint *val);

gboolean gnc_properties_window_set_boolean(GncPropWin *pw, const char* name,
                                           gboolean val);
gboolean gnc_properties_window_get_boolean(GncPropWin *pw, const char* name, 
                                           gboolean *val);

/* Possible TODO: there are more types that could be added here.

Maybe currency/gnc_commodity *

*/

#endif


More information about the gnucash-devel mailing list