/*
 * gnc-main-window.c -- GtkWindow which represents the
 *	GnuCash main window.
 *
 * Copyright (C) 2003 Jan Arne Petersen <jpetersen@uni-bonn.de>
 * Copyright (C) 2003,2005,2006 David Hampton <hampton@employees.org>
 *
 * 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@gnu.org
 */

/** @addtogroup Windows
    @{ */
/** @addtogroup GncMainWindow Main Window functions.
    @{ */
/** @file gnc-main-window.c
    @brief Functions for adding content to a window.
    @author Copyright (C) 2003 Jan Arne Petersen <jpetersen@uni-bonn.de>
    @author Copyright (C) 2003,2005,2006 David Hampton <hampton@employees.org>
*/
#include "config.h"

#include <glib/gi18n.h>
#include <gdk/gdkkeysyms.h>

#include "gnc-plugin.h"
#include "gnc-plugin-manager.h"
#include "gnc-main-window.h"

#include "dialog-options.h"
#include "dialog-preferences.h"
#include "dialog-reset-warnings.h"
#include "dialog-transfer.h"
#include "dialog-utils.h"
#include "file-utils.h"
#include "gnc-component-manager.h"
#include "gnc-engine.h"
#include "gnc-file.h"
#include "gnc-filepath-utils.h"
#include "gnc-gkeyfile-utils.h"
#include "gnc-gnome-utils.h"
#include "gnc-gobject-utils.h"
#include "gnc-gui-query.h"
#include "gnc-hooks.h"
#include "gnc-session.h"
#include "gnc-state.h"
#include "gnc-ui.h"
#include "gnc-ui-util.h"
#include "gnc-uri-utils.h"
#include "core-utils/gnc-version.h"
#include "gnc-window.h"
#include "gnc-prefs.h"
#include "option-util.h"
// +JSLED
//#include "gnc-html.h"
#include "gnc-autosave.h"
#include "print-session.h"
#ifdef MAC_INTEGRATION
#include <gtkmacintegration/gtkosxapplication.h>
#endif
#ifdef HAVE_SYS_STAT_H
# include <sys/types.h>
# include <sys/stat.h> // for stat(2)
#endif

/** Names of signals generated by the main window. */
enum
{
    PAGE_ADDED,
    PAGE_CHANGED,
    LAST_SIGNAL
};

/** This label is used to provide a mapping from a visible page widget
 *  back to the corresponding GncPluginPage object. */
#define PLUGIN_PAGE_LABEL "plugin-page"

#define PLUGIN_PAGE_CLOSE_BUTTON "close-button"
#define PLUGIN_PAGE_TAB_LABEL    "label"

#define GNC_PREF_SHOW_CLOSE_BUTTON    "tab-close-buttons"
#define GNC_PREF_TAB_NEXT_RECENT      "tab-next-recent"
#define GNC_PREF_TAB_POSITION_TOP     "tab-position-top"
#define GNC_PREF_TAB_POSITION_BOTTOM  "tab-position-bottom"
#define GNC_PREF_TAB_POSITION_LEFT    "tab-position-left"
#define GNC_PREF_TAB_POSITION_RIGHT   "tab-position-right"
#define GNC_PREF_TAB_WIDTH            "tab-width"
#define GNC_PREF_TAB_COLOR            "show-account-color-tabs"
#define GNC_PREF_SAVE_CLOSE_EXPIRES   "save-on-close-expires"
#define GNC_PREF_SAVE_CLOSE_WAIT_TIME "save-on-close-wait-time"

#define GNC_MAIN_WINDOW_NAME "GncMainWindow"


/* Static Globals *******************************************************/

/** The debugging module that this .o belongs to.  */
static QofLogModule log_module = GNC_MOD_GUI;
/** A pointer to the parent class of an embedded window. */
static GObjectClass *parent_class = NULL;
/** An identifier that indicates a "main" window. */
static GQuark window_type = 0;
/** A list of all extant main windows. This is for convenience as the
 *  same information can be obtained from the object tracking code. */
static GList *active_windows = NULL;
/** Count down timer for the save changes dialog. If the timer reaches zero
 *  any changes will be saved and the save dialog closed automatically */
static guint secs_to_save = 0;
#define MSG_AUTO_SAVE _("Changes will be saved automatically in %u seconds")

/* Declarations *********************************************************/
static void gnc_main_window_class_init (GncMainWindowClass *klass);
static void gnc_main_window_init (GncMainWindow *window, GncMainWindowClass *klass);
static void gnc_main_window_finalize (GObject *object);
static void gnc_main_window_destroy (GtkObject *object);

static void gnc_main_window_setup_window (GncMainWindow *window);
static void gnc_window_main_window_init (GncWindowIface *iface);
#ifndef MAC_INTEGRATION
static void gnc_main_window_update_all_menu_items (void);
#endif

/* Callbacks */
static void gnc_main_window_add_widget (GtkUIManager *merge, GtkWidget *widget, GncMainWindow *window);
static void gnc_main_window_switch_page (GtkNotebook *notebook, gpointer *notebook_page, gint pos, GncMainWindow *window);
static void gnc_main_window_page_reordered (GtkNotebook *notebook, GtkWidget *child, guint pos, GncMainWindow *window);
static void gnc_main_window_plugin_added (GncPlugin *manager, GncPlugin *plugin, GncMainWindow *window);
static void gnc_main_window_plugin_removed (GncPlugin *manager, GncPlugin *plugin, GncMainWindow *window);
static void gnc_main_window_engine_commit_error_callback( gpointer data, QofBackendError errcode );

/* Command callbacks */
static void gnc_main_window_cmd_page_setup (GtkAction *action, GncMainWindow *window);
static void gnc_main_window_cmd_file_properties (GtkAction *action, GncMainWindow *window);
static void gnc_main_window_cmd_file_close (GtkAction *action, GncMainWindow *window);
static void gnc_main_window_cmd_file_quit (GtkAction *action, GncMainWindow *window);
static void gnc_main_window_cmd_edit_cut (GtkAction *action, GncMainWindow *window);
static void gnc_main_window_cmd_edit_copy (GtkAction *action, GncMainWindow *window);
static void gnc_main_window_cmd_edit_paste (GtkAction *action, GncMainWindow *window);
static void gnc_main_window_cmd_edit_preferences (GtkAction *action, GncMainWindow *window);
static void gnc_main_window_cmd_view_refresh (GtkAction *action, GncMainWindow *window);
static void gnc_main_window_cmd_view_toolbar (GtkAction *action, GncMainWindow *window);
static void gnc_main_window_cmd_view_summary (GtkAction *action, GncMainWindow *window);
static void gnc_main_window_cmd_view_statusbar (GtkAction *action, GncMainWindow *window);
static void gnc_main_window_cmd_actions_reset_warnings (GtkAction *action, GncMainWindow *window);
static void gnc_main_window_cmd_actions_rename_page (GtkAction *action, GncMainWindow *window);
static void gnc_main_window_cmd_window_new (GtkAction *action, GncMainWindow *window);
static void gnc_main_window_cmd_window_move_page (GtkAction *action, GncMainWindow *window);
#ifndef MAC_INTEGRATION
static void gnc_main_window_cmd_window_raise (GtkAction *action, GtkRadioAction *current, GncMainWindow *window);
#endif
static void gnc_main_window_cmd_help_tutorial (GtkAction *action, GncMainWindow *window);
static void gnc_main_window_cmd_help_contents (GtkAction *action, GncMainWindow *window);
static void gnc_main_window_cmd_help_about (GtkAction *action, GncMainWindow *window);

static void do_popup_menu(GncPluginPage *page, GdkEventButton *event);
static gboolean gnc_main_window_popup_menu_cb (GtkWidget *widget, GncPluginPage *page);
static GtkWidget *gnc_main_window_get_statusbar (GncWindow *window_in);
static void statusbar_notification_lastmodified(void);

#ifdef MAC_INTEGRATION
static void gnc_quartz_shutdown(GtkosxApplication *theApp, gpointer data);
static gboolean gnc_quartz_should_quit(GtkosxApplication *theApp, GncMainWindow *window);
static void gnc_quartz_set_menu(GncMainWindow* window);
#endif

/** The instance private data structure for an embedded window
 *  object. */
typedef struct GncMainWindowPrivate
{
    /** The dock (vbox) at the top of the window containing the
     *  menubar and toolbar.  These items are generated by the UI
     *  manager and stored here when the UI manager provides them
     *  to the main window. */
    GtkWidget *menu_dock;
    /** The toolbar created by the UI manager.  This pointer
     * provides easy access for showing/hiding the toolbar. */
    GtkWidget *toolbar;
    /** The notebook containing all the pages in this window. */
    GtkWidget *notebook;
    /** Show account color as background on tabs */
    gboolean show_color_tabs;
    /** A pointer to the status bar at the bottom edge of the
     *  window.  This pointer provides easy access for
     *  updating/showing/hiding the status bar. */
    GtkWidget *statusbar;
    /** A pointer to the progress bar at the bottom right of the
     *  window that is contained in the status bar.  This pointer
     *  provides easy access for updating the progressbar. */
    GtkWidget *progressbar;
    /** Pointer to the about dialog.  We need this so that we create
     *  only one, can attach to its activate-link signal, and can
     *  destroy it with the main window.
     */
    GtkWidget *about_dialog;

    /** The group of all actions provided by the main window
     *  itself.  This does not include any action provided by menu
     *  or content plugins. */
    GtkActionGroup *action_group;

    /** A list of all pages that are installed in this window. */
    GList *installed_pages;
    /** A list of pages in order of use (most recent -> least recent) */
    GList *usage_order;
    /** The currently selected page. */
    GncPluginPage *current_page;
    /** The identifier for this window's engine event handler. */
    gint event_handler_id;

    /** A hash table of all action groups that have been installed
     *  into this window. The keys are the name of an action
     *  group, the values are structures of type
     *  MergedActionEntry. */
    GHashTable *merged_actions_table;
} GncMainWindowPrivate;

#define GNC_MAIN_WINDOW_GET_PRIVATE(o)  \
   (G_TYPE_INSTANCE_GET_PRIVATE ((o), GNC_TYPE_MAIN_WINDOW, GncMainWindowPrivate))

/** This data structure maintains information about one action groups
 *  that has been installed in this window. */
typedef struct
{
    /** The merge identifier for this action group.  This number
     *  is provided by the UI manager. */
    guint merge_id;
    /** The action group itself.  This contains all actions added
     *  by a single menu or content plugin. */
    GtkActionGroup *action_group;
} MergedActionEntry;

/** A holding place for all the signals generated by the main window
 *  code. */
static guint main_window_signals[LAST_SIGNAL] = { 0 };


/** An array of all of the actions provided by the main window code.
 *  This includes some placeholder actions for the menus that are
 *  visible in the menu bar but have no action associated with
 *  them. */
static GtkActionEntry gnc_menu_actions [] =
{
    /* Toplevel */

    { "FileAction", NULL, N_("_File"), NULL, NULL, NULL, },
    { "EditAction", NULL, N_("_Edit"), NULL, NULL, NULL },
    { "ViewAction", NULL, N_("_View"), NULL, NULL, NULL },
    { "ActionsAction", NULL, N_("_Actions"), NULL, NULL, NULL },
    { "TransactionAction", NULL, N_("Tra_nsaction"), NULL, NULL, NULL },
    { "ReportsAction", NULL, N_("_Reports"), NULL, NULL, NULL },
    { "ToolsAction", NULL, N_("_Tools"), NULL, NULL, NULL },
    { "ExtensionsAction", NULL, N_("E_xtensions"), NULL, NULL, NULL },
    { "WindowsAction", NULL, N_("_Windows"), NULL, NULL, NULL },
    { "HelpAction", NULL, N_("_Help"), NULL, NULL, NULL },

    /* File menu */

    { "FileImportAction", NULL, N_("_Import"), NULL, NULL, NULL },
    { "FileExportAction", NULL, N_("_Export"), NULL, NULL, NULL },
    {
        "FilePrintAction", GTK_STOCK_PRINT, N_("_Print..."), "<control>p",
        N_("Print the currently active page"), NULL
    },
#ifndef GTK_STOCK_PAGE_SETUP
#    define GTK_STOCK_PAGE_SETUP NULL
#endif
    {
        "FilePageSetupAction", GTK_STOCK_PAGE_SETUP, N_("Pa_ge Setup..."), "<control><shift>p",
        N_("Specify the page size and orientation for printing"),
        G_CALLBACK (gnc_main_window_cmd_page_setup)
    },
    {
        "FilePropertiesAction", GTK_STOCK_PROPERTIES, N_("Proper_ties"), "<Alt>Return",
        N_("Edit the properties of the current file"),
        G_CALLBACK (gnc_main_window_cmd_file_properties)
    },
    {
        "FileCloseAction", GTK_STOCK_CLOSE, N_("_Close"), NULL,
        N_("Close the currently active page"),
        G_CALLBACK (gnc_main_window_cmd_file_close)
    },
    {
        "FileQuitAction", GTK_STOCK_QUIT, N_("_Quit"), NULL,
        N_("Quit this application"),
        G_CALLBACK (gnc_main_window_cmd_file_quit)
    },

    /* Edit menu */

    {
        "EditCutAction", GTK_STOCK_CUT, N_("Cu_t"), NULL,
        N_("Cut the current selection and copy it to clipboard"),
        G_CALLBACK (gnc_main_window_cmd_edit_cut)
    },
    {
        "EditCopyAction", GTK_STOCK_COPY, N_("_Copy"), NULL,
        N_("Copy the current selection to clipboard"),
        G_CALLBACK (gnc_main_window_cmd_edit_copy)
    },
    {
        "EditPasteAction", GTK_STOCK_PASTE, N_("_Paste"), NULL,
        N_("Paste the clipboard content at the cursor position"),
        G_CALLBACK (gnc_main_window_cmd_edit_paste)
    },
    {
        "EditPreferencesAction", GTK_STOCK_PREFERENCES, N_("Pr_eferences"), NULL,
        N_("Edit the global preferences of GnuCash"),
        G_CALLBACK (gnc_main_window_cmd_edit_preferences)
    },

    /* View menu */

    {
        "ViewSortByAction", NULL, N_("_Sort By..."), NULL,
        N_("Select sorting criteria for this page view"), NULL
    },
    {
        "ViewFilterByAction", NULL, N_("_Filter By..."), NULL,
        N_("Select the account types that should be displayed."), NULL
    },
    {
        "ViewRefreshAction", GTK_STOCK_REFRESH, N_("_Refresh"), "<control>r",
        N_("Refresh this window"),
        G_CALLBACK (gnc_main_window_cmd_view_refresh)
    },

    /* Actions menu */

    { "ScrubMenuAction", NULL, N_("_Check & Repair"), NULL, NULL, NULL },
    {
        "ActionsForgetWarningsAction", NULL, N_("Reset _Warnings..."), NULL,
        N_("Reset the state of all warning messages so they will be shown again."),
        G_CALLBACK (gnc_main_window_cmd_actions_reset_warnings)
    },
    {
        "ActionsRenamePageAction", NULL, N_("Re_name Page"), NULL,
        N_("Rename this page."),
        G_CALLBACK (gnc_main_window_cmd_actions_rename_page)
    },

    /* Windows menu */

    {
        "WindowNewAction", NULL, N_("_New Window"), NULL,
        N_("Open a new top-level GnuCash window."),
        G_CALLBACK (gnc_main_window_cmd_window_new)
    },
    {
        "WindowMovePageAction", NULL, N_("New Window with _Page"), NULL,
        N_("Move the current page to a new top-level GnuCash window."),
        G_CALLBACK (gnc_main_window_cmd_window_move_page)
    },

    /* Help menu */

    {
        "HelpTutorialAction", GTK_STOCK_HELP, N_("Tutorial and Concepts _Guide"), NULL,
        N_("Open the GnuCash Tutorial"),
        G_CALLBACK (gnc_main_window_cmd_help_tutorial)
    },
    {
        "HelpContentsAction", GTK_STOCK_HELP, N_("_Contents"), "F1",
        N_("Open the GnuCash Help"),
        G_CALLBACK (gnc_main_window_cmd_help_contents)
    },
    {
        "HelpAboutAction", GTK_STOCK_ABOUT, N_("_About"), NULL,
        N_("About GnuCash"),
        G_CALLBACK (gnc_main_window_cmd_help_about)
    },
};
/** The number of actions provided by the main window. */
static guint gnc_menu_n_actions = G_N_ELEMENTS (gnc_menu_actions);

/** An array of all of the toggle action provided by the main window
 *  code. */
static GtkToggleActionEntry toggle_actions [] =
{
    {
        "ViewToolbarAction", NULL, N_("_Toolbar"), NULL,
        N_("Show/hide the toolbar on this window"),
        G_CALLBACK (gnc_main_window_cmd_view_toolbar), TRUE
    },
    {
        "ViewSummaryAction", NULL, N_("Su_mmary Bar"), NULL,
        N_("Show/hide the summary bar on this window"),
        G_CALLBACK (gnc_main_window_cmd_view_summary), TRUE
    },
    {
        "ViewStatusbarAction", NULL, N_("Stat_us Bar"), NULL,
        N_("Show/hide the status bar on this window"),
        G_CALLBACK (gnc_main_window_cmd_view_statusbar), TRUE
    },
};
/** The number of toggle actions provided by the main window. */
static guint n_toggle_actions = G_N_ELEMENTS (toggle_actions);

#ifndef MAC_INTEGRATION
/** An array of all of the radio action provided by the main window
 *  code. */
static GtkRadioActionEntry radio_entries [] =
{
    { "Window0Action", NULL, N_("Window _1"), NULL, NULL, 0 },
    { "Window1Action", NULL, N_("Window _2"), NULL, NULL, 1 },
    { "Window2Action", NULL, N_("Window _3"), NULL, NULL, 2 },
    { "Window3Action", NULL, N_("Window _4"), NULL, NULL, 3 },
    { "Window4Action", NULL, N_("Window _5"), NULL, NULL, 4 },
    { "Window5Action", NULL, N_("Window _6"), NULL, NULL, 5 },
    { "Window6Action", NULL, N_("Window _7"), NULL, NULL, 6 },
    { "Window7Action", NULL, N_("Window _8"), NULL, NULL, 7 },
    { "Window8Action", NULL, N_("Window _9"), NULL, NULL, 8 },
    { "Window9Action", NULL, N_("Window _0"), NULL, NULL, 9 },
};

/** The number of radio actions provided by the main window. */
static guint n_radio_entries = G_N_ELEMENTS (radio_entries);
#endif

/** These are the "important" actions provided by the main window.
 *  Their labels will appear when the toolbar is set to "Icons and
 *  important text" (e.g. GTK_TOOLBAR_BOTH_HORIZ) mode. */
static const gchar *gnc_menu_important_actions[] =
{
    "FileCloseAction",
    NULL,
};


/** The following are in the main window so they will always be
 *  present in the menu structure, but they are never sensitive.
 *  These actions should be overridden in child windows where they
 *  have meaning. */
static const gchar *always_insensitive_actions[] =
{
    "FilePrintAction",
    NULL
};


/** The following items in the main window should be made insensitive
 *  at startup time.  The sensitivity will be changed by some later
 *  event. */
static const gchar *initially_insensitive_actions[] =
{
    "FileCloseAction",
    NULL
};


/** The following are in the main window so they will always be
 *  present in the menu structure, but they are always hidden.
 *  These actions should be overridden in child windows where they
 *  have meaning. */
static const gchar *always_hidden_actions[] =
{
    "ViewSortByAction",
    "ViewFilterByAction",
    NULL
};


/** If a page is flagged as immutable, then the following actions
 *  cannot be performed on that page. */
static const gchar *immutable_page_actions[] =
{
    "FileCloseAction",
    NULL
};


/** The following actions can only be performed if there are multiple
 *  pages in a window. */
static const gchar *multiple_page_actions[] =
{
    "WindowMovePageAction",
    NULL
};


/************************************************************
 *                                                          *
 ************************************************************/
#define WINDOW_COUNT            "WindowCount"
#define WINDOW_STRING           "Window %d"
#define WINDOW_GEOMETRY         "WindowGeometry"
#define WINDOW_POSITION         "WindowPosition"
#define WINDOW_MAXIMIZED        "WindowMaximized"
#define TOOLBAR_VISIBLE         "ToolbarVisible"
#define STATUSBAR_VISIBLE       "StatusbarVisible"
#define SUMMARYBAR_VISIBLE      "SummarybarVisible"
#define WINDOW_FIRSTPAGE        "FirstPage"
#define WINDOW_PAGECOUNT        "PageCount"
#define WINDOW_PAGEORDER        "PageOrder"
#define PAGE_TYPE               "PageType"
#define PAGE_NAME               "PageName"
#define PAGE_STRING             "Page %d"

typedef struct
{
    GKeyFile *key_file;
    const gchar *group_name;
    gint window_num;
    gint page_num;
    gint page_offset;
} GncMainWindowSaveData;


/*  Iterator function to walk all pages in all windows, calling the
 *  specified function for each page. */
void
gnc_main_window_foreach_page (GncMainWindowPageFunc fn, gpointer user_data)
{
    GncMainWindowPrivate *priv;
    GncMainWindow *window;
    GncPluginPage *page;
    GList *w, *p;

    ENTER(" ");
    for (w = active_windows; w; w = g_list_next(w))
    {
        window = w->data;
        priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
        for (p = priv->installed_pages; p; p = g_list_next(p))
        {
            page = p->data;
            fn(page, user_data);
        }
    }
    LEAVE(" ");
}


/** Restore a single page to a window.  This function calls a page
 *  specific function to create the actual page.  It then handles all
 *  the common tasks such as insuring the page is installed into a
 *  window, updating the page name, and anything else that might be
 *  common to all pages.
 *
 *  @param window The GncMainWindow where the new page will be
 *  installed.
 *
 *  @param data A data structure containing state about the
 *  window/page restoration process. */
static void
gnc_main_window_restore_page (GncMainWindow *window,
                              GncMainWindowSaveData *data)
{
    GncMainWindowPrivate *priv;
    GncPluginPage *page;
    gchar *page_group, *page_type = NULL, *name = NULL;
    const gchar *class_type;
    GError *error = NULL;

    ENTER("window %p, data %p (key file %p, window %d, page start %d, page num %d)",
          window, data, data->key_file, data->window_num, data->page_offset,
          data->page_num);

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    page_group = g_strdup_printf(PAGE_STRING,
                                 data->page_offset + data->page_num);
    page_type = g_key_file_get_string(data->key_file, page_group,
                                      PAGE_TYPE, &error);
    if (error)
    {
        g_warning("error reading group %s key %s: %s",
                  page_group, PAGE_TYPE, error->message);
        goto cleanup;
    }

    /* See if the page already exists. */
    page = g_list_nth_data(priv->installed_pages, data->page_num);
    if (page)
    {
        class_type = GNC_PLUGIN_PAGE_GET_CLASS(page)->plugin_name;
        if (strcmp(page_type, class_type) != 0)
        {
            g_warning("error: page types don't match: state %s, existing page %s",
                      page_type, class_type);
            goto cleanup;
        }
    }
    else
    {
        /* create and install the page */
        page = gnc_plugin_page_recreate_page(GTK_WIDGET(window), page_type,
                                             data->key_file, page_group);
        if (page)
        {
            /* Does the page still need to be installed into the window? */
            if (page->window == NULL)
            {
                gnc_plugin_page_set_use_new_window(page, FALSE);
                gnc_main_window_open_page(window, page);
            }

            /* Restore the page name */
            name = g_key_file_get_string(data->key_file, page_group,
                                         PAGE_NAME, &error);
            if (error)
            {
                g_warning("error reading group %s key %s: %s",
                          page_group, PAGE_NAME, error->message);
                /* Fall through and still show the page. */
            }
            else
            {
                DEBUG("updating page name for %p to %s.", page, name);
                main_window_update_page_name(page, name);
                g_free(name);
            }
        }
    }

    LEAVE("ok");
cleanup:
    if (error)
        g_error_free(error);
    if (page_type)
        g_free(page_type);
    g_free(page_group);
}


/** Restore all the pages in a given window.  This function restores
 *  all the window specific attributes, then calls a helper function
 *  to restore all the pages that are contained in the window.
 *
 *  @param window The GncMainWindow whose pages should be restored.
 *
 *  @param data A data structure containing state about the
 *  window/page restoration process. */
static void
gnc_main_window_restore_window (GncMainWindow *window, GncMainWindowSaveData *data)
{
    GncMainWindowPrivate *priv;
    GtkAction *action;
    gint *pos, *geom, *order;
    gsize length;
    gboolean max, visible, desired_visibility;
    gchar *window_group;
    gint page_start, page_count, i;
    GError *error = NULL;

    /* Setup */
    ENTER("window %p, data %p (key file %p, window %d)",
          window, data, data->key_file, data->window_num);
    window_group = g_strdup_printf(WINDOW_STRING, data->window_num + 1);

    /* Deal with the uncommon case that the state file defines a window
     * but no pages. An example to get in such a situation can be found
     * here: https://bugzilla.gnome.org/show_bug.cgi?id=436479#c3
     * If this happens on the first window, we will open an account hierarchy
     * to avoid confusing the user by presenting a completely empty window.
     * If it happens on a later window, we'll just skip restoring that window.
     */
    if (!g_key_file_has_group (data->key_file, window_group) ||
        !g_key_file_has_key (data->key_file, window_group, WINDOW_PAGECOUNT, &error))
    {
        if (window)
        {
            gnc_main_window_restore_default_state (window);
            PINFO ("saved state had an empty first main window\n"
                   "an account hierarchy page was added automatically to avoid confusion");
        }
        else
            PINFO ("saved state had an empty main window, skipping restore");

        goto cleanup;
    }


    /* Get this window's notebook info */
    page_count = g_key_file_get_integer(data->key_file,
                                        window_group, WINDOW_PAGECOUNT, &error);
    if (error)
    {
        g_warning("error reading group %s key %s: %s",
                  window_group, WINDOW_PAGECOUNT, error->message);
        goto cleanup;
    }
    if (page_count == 0)
    {
        /* Should never happen, but has during alpha testing. Having this
         * check doesn't hurt anything. */
        goto cleanup;
    }
    page_start = g_key_file_get_integer(data->key_file,
                                        window_group, WINDOW_FIRSTPAGE, &error);
    if (error)
    {
        g_warning("error reading group %s key %s: %s",
                  window_group, WINDOW_FIRSTPAGE, error->message);
        goto cleanup;
    }

    /* Build a window if we don't already have one */
    if (window == NULL)
    {
        DEBUG("Window %d doesn't exist. Creating new window.", data->window_num);
        DEBUG("active_windows %p.", active_windows);
        if (active_windows)
            DEBUG("first window %p.", active_windows->data);
        window = gnc_main_window_new();
        gtk_widget_show(GTK_WIDGET(window));
    }

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    /* Get the window coordinates, etc. */
    geom = g_key_file_get_integer_list(data->key_file, window_group,
                                       WINDOW_GEOMETRY, &length, &error);
    if (error)
    {
        g_warning("error reading group %s key %s: %s",
                  window_group, WINDOW_GEOMETRY, error->message);
        g_error_free(error);
        error = NULL;
    }
    else if (length != 2)
    {
        g_warning("invalid number of values for group %s key %s",
                  window_group, WINDOW_GEOMETRY);
    }
    else
    {
        gtk_window_resize(GTK_WINDOW(window), geom[0], geom[1]);
        DEBUG("window (%p) size %dx%d", window, geom[0], geom[1]);
    }
    /* keep the geometry for a test whether the windows position
       is offscreen */

    pos = g_key_file_get_integer_list(data->key_file, window_group,
                                      WINDOW_POSITION, &length, &error);
    if (error)
    {
        g_warning("error reading group %s key %s: %s",
                  window_group, WINDOW_POSITION, error->message);
        g_error_free(error);
        error = NULL;
    }
    else if (length != 2)
    {
        g_warning("invalid number of values for group %s key %s",
                  window_group, WINDOW_POSITION);
    }
    else if ((pos[0] + (geom ? geom[0] : 0) < 0) ||
             (pos[0] > gdk_screen_width()) ||
             (pos[1] + (geom ? geom[1] : 0) < 0) ||
             (pos[1] > gdk_screen_height()))
    {
//    g_debug("position %dx%d, size%dx%d is offscreen; will not move",
//	    pos[0], pos[1], geom[0], geom[1]);
    }
    else
    {
        gtk_window_move(GTK_WINDOW(window), pos[0], pos[1]);
        DEBUG("window (%p) position %dx%d", window, pos[0], pos[1]);
    }
    if (geom)
    {
        g_free(geom);
    }
    if (pos)
    {
        g_free(pos);
    }

    max = g_key_file_get_boolean(data->key_file, window_group,
                                 WINDOW_MAXIMIZED, &error);
    if (error)
    {
        g_warning("error reading group %s key %s: %s",
                  window_group, WINDOW_MAXIMIZED, error->message);
        g_error_free(error);
        error = NULL;
    }
    else if (max)
    {
        gtk_window_maximize(GTK_WINDOW(window));
    }

    /* Common view menu items */
    action = gnc_main_window_find_action(window, "ViewToolbarAction");
    visible = gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(action));
    desired_visibility = g_key_file_get_boolean(data->key_file, window_group,
                         TOOLBAR_VISIBLE, &error);
    if (error)
    {
        g_warning("error reading group %s key %s: %s",
                  window_group, TOOLBAR_VISIBLE, error->message);
        g_error_free(error);
        error = NULL;
    }
    else if (visible != desired_visibility)
    {
        gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(action), desired_visibility);
    }

    action = gnc_main_window_find_action(window, "ViewSummaryAction");
    visible = gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(action));
    desired_visibility = g_key_file_get_boolean(data->key_file, window_group,
                         SUMMARYBAR_VISIBLE, &error);
    if (error)
    {
        g_warning("error reading group %s key %s: %s",
                  window_group, TOOLBAR_VISIBLE, error->message);
        g_error_free(error);
        error = NULL;
    }
    else if (visible != desired_visibility)
    {
        gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(action), desired_visibility);
    }

    action = gnc_main_window_find_action(window, "ViewStatusbarAction");
    visible = gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(action));
    desired_visibility = g_key_file_get_boolean(data->key_file, window_group,
                         STATUSBAR_VISIBLE, &error);
    if (error)
    {
        g_warning("error reading group %s key %s: %s",
                  window_group, TOOLBAR_VISIBLE, error->message);
        g_error_free(error);
        error = NULL;
    }
    else if (visible != desired_visibility)
    {
        gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(action), desired_visibility);
    }

    /* Now populate the window with pages. */
    for (i = 0; i < page_count; i++)
    {
        data->page_offset = page_start;
        data->page_num = i;
        gnc_main_window_restore_page(window, data);

        /* give the page a chance to display */
        while (gtk_events_pending ())
            gtk_main_iteration ();
    }

    /* Restore page ordering within the notebook. Use +1 notation so the
     * numbers in the page order match the page sections, at least for
     * the one window case. */
    order = g_key_file_get_integer_list(data->key_file, window_group,
                                        WINDOW_PAGEORDER, &length, &error);
    if (error)
    {
        g_warning("error reading group %s key %s: %s",
                  window_group, WINDOW_PAGEORDER, error->message);
        g_error_free(error);
        error = NULL;
    }
    else if (length != page_count)
    {
        g_warning("%s key %s length %" G_GSIZE_FORMAT " differs from window page count %d",
                  window_group, WINDOW_PAGEORDER, length, page_count);
    }
    else
    {
        /* Dump any list that might exist */
        g_list_free(priv->usage_order);
        priv->usage_order = NULL;
        /* Now rebuild the list from the key file. */
        for (i = 0; i < length; i++)
        {
            gpointer page = g_list_nth_data(priv->installed_pages, order[i] - 1);
            if (page)
            {
                priv->usage_order = g_list_append(priv->usage_order, page);
            }
        }
        gtk_notebook_set_current_page (GTK_NOTEBOOK(priv->notebook),
                                       order[0] - 1);
    }
    if (order)
    {
        g_free(order);
    }

    LEAVE("window %p", window);
cleanup:
    if (error)
        g_error_free(error);
    g_free(window_group);
}

void
gnc_main_window_restore_all_windows(const GKeyFile *keyfile)
{
    gint i, window_count;
    GError *error = NULL;
    GncMainWindowSaveData data;
    GncMainWindow *window;

    /* We use the same struct for reading and for writing, so we cast
       away the const. */
    data.key_file = (GKeyFile *) keyfile;
    window_count = g_key_file_get_integer(data.key_file, STATE_FILE_TOP,
                                          WINDOW_COUNT, &error);
    if (error)
    {
        g_warning("error reading group %s key %s: %s",
                  STATE_FILE_TOP, WINDOW_COUNT, error->message);
        g_error_free(error);
        LEAVE("can't read count");
        return;
    }

    /* Restore all state information on the open windows.  Window
       numbers in state file are 1-based. GList indices are 0-based. */
    gnc_set_busy_cursor (NULL, TRUE);
    for (i = 0; i < window_count; i++)
    {
        data.window_num = i;
        window = g_list_nth_data(active_windows, i);
        gnc_main_window_restore_window(window, &data);
    }
    gnc_unset_busy_cursor (NULL);

    statusbar_notification_lastmodified();
}

void
gnc_main_window_restore_default_state(GncMainWindow *window)
{
    GtkAction *action;

    /* The default state should be to have an Account Tree page open
     * in the window. */
    DEBUG("no saved state file");
    if (!window)
        window = g_list_nth_data(active_windows, 0);
    action = gnc_main_window_find_action(window, "ViewAccountTreeAction");
    gtk_action_activate(action);
}

/** Save the state of a single page to a disk.  This function handles
 *  all the common tasks such as saving the page type and name, and
 *  anything else that might be common to all pages.  It then calls a
 *  page specific function to save the actual page.
 *
 *  @param page The GncPluginPage whose state should be saved.
 *
 *  @param data A data structure containing state about the
 *  window/page saving process. */
static void
gnc_main_window_save_page (GncPluginPage *page, GncMainWindowSaveData *data)
{
    gchar *page_group;
    const gchar *plugin_name, *page_name;

    ENTER("page %p, data %p (key file %p, window %d, page %d)",
          page, data, data->key_file, data->window_num, data->page_num);
    plugin_name = gnc_plugin_page_get_plugin_name(page);
    page_name = gnc_plugin_page_get_page_name(page);
    if (!plugin_name || !page_name)
    {
        LEAVE("not saving invalid page");
        return;
    }
    page_group = g_strdup_printf(PAGE_STRING, data->page_num++);
    g_key_file_set_string(data->key_file, page_group, PAGE_TYPE, plugin_name);
    g_key_file_set_string(data->key_file, page_group, PAGE_NAME, page_name);

    gnc_plugin_page_save_page(page, data->key_file, page_group);
    g_free(page_group);
    LEAVE(" ");
}


/** Saves all the pages in a single window to a disk.  This function
 *  saves all the window specific attributes, then calls a helper
 *  function to save all the pages that are contained in the window.
 *
 *  @param window The GncMainWindow whose pages should be saved.
 *
 *  @param data A data structure containing state about the
 *  window/page saving process. */
static void
gnc_main_window_save_window (GncMainWindow *window, GncMainWindowSaveData *data)
{
    GncMainWindowPrivate *priv;
    GtkAction *action;
    gint i, num_pages, coords[4], *order;
    gboolean maximized, visible;
    gchar *window_group;

    /* Setup */
    ENTER("window %p, data %p (key file %p, window %d)",
          window, data, data->key_file, data->window_num);
    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    /* Check for bogus window structures. */
    num_pages = gtk_notebook_get_n_pages(GTK_NOTEBOOK(priv->notebook));
    if (0 == num_pages)
    {
        LEAVE("empty window %p", window);
        return;
    }

    /* Save this window's notebook info */
    window_group = g_strdup_printf(WINDOW_STRING, data->window_num++);
    g_key_file_set_integer(data->key_file, window_group,
                           WINDOW_PAGECOUNT, num_pages);
    g_key_file_set_integer(data->key_file, window_group,
                           WINDOW_FIRSTPAGE, data->page_num);

    /* Save page ordering within the notebook. Use +1 notation so the
     * numbers in the page order match the page sections, at least for
     * the one window case. */
    order = g_malloc(sizeof(gint) * num_pages);
    for (i = 0; i < num_pages; i++)
    {
        gpointer page = g_list_nth_data(priv->usage_order, i);
        order[i] = g_list_index(priv->installed_pages, page) + 1;
    }
    g_key_file_set_integer_list(data->key_file, window_group,
                                WINDOW_PAGEORDER, order, num_pages);
    g_free(order);

    /* Save the window coordinates, etc. */
    gtk_window_get_position(GTK_WINDOW(window), &coords[0], &coords[1]);
    gtk_window_get_size(GTK_WINDOW(window), &coords[2], &coords[3]);
    maximized = (gdk_window_get_state(gtk_widget_get_window ((GTK_WIDGET(window))))
                 & GDK_WINDOW_STATE_MAXIMIZED) != 0;
    g_key_file_set_integer_list(data->key_file, window_group,
                                WINDOW_POSITION, &coords[0], 2);
    g_key_file_set_integer_list(data->key_file, window_group,
                                WINDOW_GEOMETRY, &coords[2], 2);
    g_key_file_set_boolean(data->key_file, window_group,
                           WINDOW_MAXIMIZED, maximized);
    DEBUG("window (%p) position %dx%d, size %dx%d, %s", window,  coords[0], coords[1],
          coords[2], coords[3],
          maximized ? "maximized" : "not maximized");

    /* Common view menu items */
    action = gnc_main_window_find_action(window, "ViewToolbarAction");
    visible = gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(action));
    g_key_file_set_boolean(data->key_file, window_group,
                           TOOLBAR_VISIBLE, visible);
    action = gnc_main_window_find_action(window, "ViewSummaryAction");
    visible = gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(action));
    g_key_file_set_boolean(data->key_file, window_group,
                           SUMMARYBAR_VISIBLE, visible);
    action = gnc_main_window_find_action(window, "ViewStatusbarAction");
    visible = gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(action));
    g_key_file_set_boolean(data->key_file, window_group,
                           STATUSBAR_VISIBLE, visible);

    /* Save individual pages in this window */
    g_list_foreach(priv->installed_pages, (GFunc)gnc_main_window_save_page, data);

    g_free(window_group);
    LEAVE("window %p", window);
}

void
gnc_main_window_save_all_windows(GKeyFile *keyfile)
{
    GncMainWindowSaveData data;

    /* Set up the iterator data structures */
    data.key_file = keyfile;
    data.window_num = 1;
    data.page_num = 1;

    g_key_file_set_integer(data.key_file,
                           STATE_FILE_TOP, WINDOW_COUNT,
                           g_list_length(active_windows));
    /* Dump all state information on the open windows */
    g_list_foreach(active_windows, (GFunc)gnc_main_window_save_window, &data);
}


gboolean
gnc_main_window_finish_pending (GncMainWindow *window)
{
    GncMainWindowPrivate *priv;
    GList *item;

    g_return_val_if_fail(GNC_IS_MAIN_WINDOW(window), TRUE);

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    for (item = priv->installed_pages; item; item = g_list_next(item))
    {
        if (!gnc_plugin_page_finish_pending(item->data))
        {
            return FALSE;
        }
    }
    return TRUE;
}


gboolean
gnc_main_window_all_finish_pending (void)
{
    const GList *windows, *item;

    windows = gnc_gobject_tracking_get_list(GNC_MAIN_WINDOW_NAME);
    for (item = windows; item; item = g_list_next(item))
    {
        if (!gnc_main_window_finish_pending(item->data))
        {
            return FALSE;
        }
    }
    return TRUE;
}


/** See if the page already exists.  For each open window, look
 *  through the list of pages installed in that window and see if the
 *  specified page is there.
 *
 *  @internal
 *
 *  @param page The page to search for.
 *
 *  @return TRUE if the page is present in the window, FALSE otherwise.
 */
static gboolean
gnc_main_window_page_exists (GncPluginPage *page)
{
    GncMainWindow *window;
    GncMainWindowPrivate *priv;
    GList *walker;

    for (walker = active_windows; walker; walker = g_list_next(walker))
    {
        window = walker->data;
        priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
        if (g_list_find(priv->installed_pages, page))
        {
            return TRUE;
        }
    }
    return FALSE;
}

static gboolean auto_save_countdown (GtkWidget *dialog)
{
    GtkWidget *label;
    gchar *timeoutstr = NULL;

    /* Stop count down if user closed the dialog since the last time we were called */
    if (!GTK_IS_DIALOG (dialog))
        return FALSE; /* remove timer */

    /* Stop count down if count down text can't be updated */
    label = GTK_WIDGET (g_object_get_data (G_OBJECT (dialog), "count-down-label"));
    if (!GTK_IS_LABEL (label))
        return FALSE; /* remove timer */

    secs_to_save--;
    DEBUG ("Counting down: %d seconds", secs_to_save);

    timeoutstr = g_strdup_printf (MSG_AUTO_SAVE, secs_to_save);
    gtk_label_set_text (GTK_LABEL (label), timeoutstr);
    g_free (timeoutstr);

    /* Count down reached 0. Save and close dialog */
    if (!secs_to_save)
    {
        gtk_dialog_response (GTK_DIALOG(dialog), GTK_RESPONSE_APPLY);
        return FALSE; /* remove timer */
    }

    /* Run another cycle */
    return TRUE;
}


/** This function prompts the user to save the file with a dialog that
 *  follows the HIG guidelines.
 *
 *  @internal
 *
 *  @returns This function returns TRUE if the user clicked the Cancel
 *  button.  It returns FALSE if the closing of the window should
 *  continue.
 */
static gboolean
gnc_main_window_prompt_for_save (GtkWidget *window)
{
    QofSession *session;
    QofBook *book;
    GtkWidget *dialog, *msg_area, *label;
    gint response;
    const gchar *filename, *tmp;
    const gchar *title = _("Save changes to file %s before closing?");
    /* This should be the same message as in gnc-file.c */
    const gchar *message_hours =
        _("If you don't save, changes from the past %d hours and %d minutes will be discarded.");
    const gchar *message_days =
        _("If you don't save, changes from the past %d days and %d hours will be discarded.");
    time64 oldest_change;
    gint minutes, hours, days;

    session = gnc_get_current_session();
    book = qof_session_get_book(session);
    filename = qof_session_get_url(session);
    if (filename == NULL)
        filename = _("<unknown>");
    if ((tmp = strrchr(filename, '/')) != NULL)
        filename = tmp + 1;

    /* Remove any pending auto-save timeouts */
    gnc_autosave_remove_timer(book);

    dialog = gtk_message_dialog_new(GTK_WINDOW(window),
                                    GTK_DIALOG_MODAL,
                                    GTK_MESSAGE_WARNING,
                                    GTK_BUTTONS_NONE,
                                    title,
                                    filename);
    oldest_change = qof_book_get_session_dirty_time(book);
    minutes = (gnc_time (NULL) - oldest_change) / 60 + 1;
    hours = minutes / 60;
    minutes = minutes % 60;
    days = hours / 24;
    hours = hours % 24;
    if (days > 0)
    {
        gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog),
                message_days, days, hours);
    }
    else if (hours > 0)
    {
        gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog),
                message_hours, hours, minutes);
    }
    else
    {
        gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog),
                ngettext("If you don't save, changes from the past %d minute will be discarded.",
                         "If you don't save, changes from the past %d minutes will be discarded.",
                         minutes), minutes);
    }
    gtk_dialog_add_buttons(GTK_DIALOG(dialog),
                           _("Close _Without Saving"), GTK_RESPONSE_CLOSE,
                           GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
                           GTK_STOCK_SAVE, GTK_RESPONSE_APPLY,
                           NULL);
    gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_APPLY);

    /* If requested by the user, add a timeout to the question to save automatically
     * if the user doesn't answer after a chosen number of seconds.
     */
    if (gnc_prefs_get_bool (GNC_PREFS_GROUP_GENERAL, GNC_PREF_SAVE_CLOSE_EXPIRES))
    {
        gchar *timeoutstr = NULL;

        secs_to_save = gnc_prefs_get_int (GNC_PREFS_GROUP_GENERAL, GNC_PREF_SAVE_CLOSE_WAIT_TIME);
        timeoutstr = g_strdup_printf (MSG_AUTO_SAVE, secs_to_save);
        label = GTK_WIDGET(gtk_label_new (timeoutstr));
        g_free (timeoutstr);
        gtk_widget_show (label);

        msg_area = gtk_message_dialog_get_message_area (GTK_MESSAGE_DIALOG(dialog));
        gtk_box_pack_end (GTK_BOX(msg_area), label, TRUE, TRUE, 0);
        g_object_set (G_OBJECT (label), "xalign", 0.0, NULL);

        g_object_set_data (G_OBJECT (dialog), "count-down-label", label);
        g_timeout_add_seconds (1, (GSourceFunc)auto_save_countdown, dialog);
    }

    response = gtk_dialog_run (GTK_DIALOG (dialog));
    gtk_widget_destroy(dialog);

    switch (response)
    {
    case GTK_RESPONSE_APPLY:
        gnc_file_save();
        return FALSE;

    case GTK_RESPONSE_CLOSE:
        qof_book_mark_session_saved(book);
        return FALSE;

    default:
        return TRUE;
    }
}


static void
gnc_main_window_add_plugin (gpointer plugin,
                            gpointer window)
{
    g_return_if_fail (GNC_IS_MAIN_WINDOW (window));
    g_return_if_fail (GNC_IS_PLUGIN (plugin));

    ENTER(" ");
    gnc_plugin_add_to_window (GNC_PLUGIN (plugin),
                              GNC_MAIN_WINDOW (window),
                              window_type);
    LEAVE(" ");
}

static void
gnc_main_window_remove_plugin (gpointer plugin,
                               gpointer window)
{
    g_return_if_fail (GNC_IS_MAIN_WINDOW (window));
    g_return_if_fail (GNC_IS_PLUGIN (plugin));

    ENTER(" ");
    gnc_plugin_remove_from_window (GNC_PLUGIN (plugin),
                                   GNC_MAIN_WINDOW (window),
                                   window_type);
    LEAVE(" ");
}


static gboolean
gnc_main_window_timed_quit (gpointer dummy)
{
    if (gnc_file_save_in_progress())
        return TRUE;

    gnc_shutdown (0);
    return FALSE;
}

static gboolean
gnc_main_window_quit(GncMainWindow *window)
{
    QofSession *session;
    gboolean needs_save, do_shutdown;

    session = gnc_get_current_session();
    needs_save = qof_book_session_not_saved(qof_session_get_book(session)) &&
                 !gnc_file_save_in_progress();
    do_shutdown = !needs_save ||
                  (needs_save && !gnc_main_window_prompt_for_save(GTK_WIDGET(window)));

    if (do_shutdown)
    {
        g_timeout_add(250, gnc_main_window_timed_quit, NULL);
        return TRUE;
    }
    return FALSE;
}

static gboolean
gnc_main_window_delete_event (GtkWidget *window,
                              GdkEvent *event,
                              gpointer user_data)
{
    static gboolean already_dead = FALSE;

    if (already_dead)
        return TRUE;

    if (!gnc_main_window_finish_pending(GNC_MAIN_WINDOW(window)))
    {
        /* Don't close the window. */
        return TRUE;
    }

    if (g_list_length(active_windows) > 1)
        return FALSE;

    already_dead = gnc_main_window_quit(GNC_MAIN_WINDOW(window));
    return TRUE;
}


/** This function handles any event notifications from the engine.
 *  The only event it currently cares about is the deletion of a book.
 *  When a book is deleted, it runs through all installed pages
 *  looking for pages that reference the just (about to be?) deleted
 *  book.  It closes any page it finds so there are no dangling
 *  references to the book.
 *
 *  @internal
 *
 *  @param entity     The guid the item being added, deleted, etc.
 *
 *  @param type       The type of the item being added, deleted, etc. This
 *                    function only cares about a type of GNC_ID_BOOK.
 *
 *  @param event_type The type of the event.  This function only cares
 *                    about an event type of QOF_EVENT_DESTROY.
 *
 *  @param user_data  A pointer to the window data structure.
 */
static void
gnc_main_window_event_handler (QofInstance *entity,  QofEventId event_type,
                               gpointer user_data, gpointer event_data)
{
    GncMainWindow *window;
    GncMainWindowPrivate *priv;
    GncPluginPage *page;
    GList *item, *next;

    /* hard failures */
    g_return_if_fail(GNC_IS_MAIN_WINDOW(user_data));

    /* soft failures */
    if (!QOF_CHECK_TYPE(entity, QOF_ID_BOOK))
        return;
    if (event_type !=  QOF_EVENT_DESTROY)
        return;

    ENTER("entity %p, event %d, window %p, event data %p",
          entity, event_type, user_data, event_data);
    window = GNC_MAIN_WINDOW(user_data);
    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    /* This is not a typical list iteration.  We're removing while
     * we iterate, so we have to cache the 'next' pointer before
     * executing any code in the loop. */
    for (item = priv->installed_pages; item; item = next)
    {
        next = g_list_next(item);
        page = GNC_PLUGIN_PAGE(item->data);
        if (gnc_plugin_page_has_book (page, (QofBook *)entity))
            gnc_main_window_close_page (page);
    }
    LEAVE(" ");
}


/** Generate a title for this window based upon the Gnome Human
 *  Interface Guidelines, v2.0.  This title will be used as both the
 *  window title and the title of the "Window" menu item associated
 *  with the window.
 *
 *  As a side-effect, the save action is set sensitive iff the book
 *  is dirty, and the immutable_page_actions are set sensitive iff the page is
 *  mutable.
 *
 *  @param window The window whose title should be generated.
 *
 *  @return The title for the window.  It is the callers
 *  responsibility to free this string.
 *
 *  @internal
 */
static gchar *
gnc_main_window_generate_title (GncMainWindow *window)
{
    GncMainWindowPrivate *priv;
    GncPluginPage *page;
    QofBook *book;
    gboolean immutable;
    gchar *filename = NULL;
    const gchar *book_id = NULL;
    const gchar *dirty = "";
    const gchar *readonly_text = NULL;
    gchar *readonly;
    gchar *title;

    if (gnc_current_session_exist())
    {
        book_id = qof_session_get_url (gnc_get_current_session ());
        book = gnc_get_current_book();
        if (qof_book_session_not_saved (book))
            dirty = "*";
        if (qof_book_is_readonly(book))
        {
            /* Translators: This string is shown in the window title if this
            document is, well, read-only. */
            readonly_text = _("(read-only)");
        }
    }
    readonly = (readonly_text != NULL)
               ? g_strdup_printf(" %s", readonly_text)
               : g_strdup("");

    if (!book_id)
        filename = g_strdup(_("Unsaved Book"));
    else
    {
        if ( gnc_uri_is_file_uri ( book_id ) )
        {
            /* The filename is a true file.
             * The Gnome HIG 2.0 recommends only the file name (no path) be used. (p15) */
            gchar *path = gnc_uri_get_path ( book_id );
            filename = g_path_get_basename ( path );
            g_free ( path );
        }
        else
        {
            /* The filename is composed of database connection parameters.
             * For this we will show access_method://username@database[:port] */
            filename = gnc_uri_normalize_uri (book_id, FALSE);
        }
    }

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    page = priv->current_page;
    if (page)
    {
        /* The Gnome HIG 2.0 recommends the application name not be used. (p16)
         * but several developers prefer to use it anyway. */
        title = g_strdup_printf("%s%s%s - %s - GnuCash", dirty, filename, readonly,
                                gnc_plugin_page_get_page_name(page));
    }
    else
    {
        title = g_strdup_printf("%s%s%s - GnuCash", dirty, filename, readonly);
    }
    /* Update the menus based upon whether this is an "immutable" page. */
    immutable = page &&
                g_object_get_data (G_OBJECT (page), PLUGIN_PAGE_IMMUTABLE);
    gnc_plugin_update_actions(priv->action_group,
                              immutable_page_actions,
                              "sensitive", !immutable);
    /* Trigger sensitivity updtates of other actions such as Save/Revert */
    g_signal_emit_by_name (window, "page_changed", page);
    g_free( filename );
    g_free(readonly);

    return title;
}


/** Update the title bar on the specified window.  This routine uses
 *  the gnc_main_window_generate_title() function to create the title.
 *  It is called whenever the user switched pages in a window, as the
 *  title includes the name of the current page.
 *
 *  @param window The window whose title should be updated.
 *
 *  @internal
 */
static void
gnc_main_window_update_title (GncMainWindow *window)
{
    gchar *title;

    title = gnc_main_window_generate_title(window);
    gtk_window_set_title(GTK_WINDOW(window), title);
    g_free(title);
}

static void
gnc_main_window_update_all_titles (void)
{
    g_list_foreach(active_windows,
                   (GFunc)gnc_main_window_update_title,
                   NULL);
}

static void
gnc_main_window_book_dirty_cb (QofBook *book,
                               gboolean dirty,
                               gpointer user_data)
{
    gnc_main_window_update_all_titles();

    /* Auto-save feature */
    gnc_autosave_dirty_handler(book, dirty);
}

static void
gnc_main_window_attach_to_book (QofSession *session)
{
    QofBook *book;

    g_return_if_fail(session);

    book = qof_session_get_book(session);
    qof_book_set_dirty_cb(book, gnc_main_window_book_dirty_cb, NULL);
    gnc_main_window_update_all_titles();
#ifndef MAC_INTEGRATION
    gnc_main_window_update_all_menu_items();
#endif
}

static guint gnc_statusbar_notification_messageid = 0;
//#define STATUSBAR_NOTIFICATION_AUTOREMOVAL
#ifdef STATUSBAR_NOTIFICATION_AUTOREMOVAL
/* Removes the statusbar notification again that has been pushed to the
 * statusbar by generate_statusbar_lastmodified_message. */
static gboolean statusbar_notification_off(gpointer user_data_unused)
{
    GtkWidget *widget = gnc_ui_get_toplevel();
    //g_warning("statusbar_notification_off\n");
    if (gnc_statusbar_notification_messageid == 0)
        return FALSE;

    if (widget && GNC_IS_MAIN_WINDOW(widget))
    {
        GncMainWindow *mainwindow = GNC_MAIN_WINDOW(widget);
        GtkWidget *statusbar = gnc_main_window_get_statusbar(GNC_WINDOW(mainwindow));
        gtk_statusbar_remove(GTK_STATUSBAR(statusbar), 0, gnc_statusbar_notification_messageid);
        gnc_statusbar_notification_messageid = 0;
    }
    else
    {
        g_warning("oops, no GncMainWindow obtained\n");
    }
    return FALSE; // should not be called again
}
#endif // STATUSBAR_NOTIFICATION_AUTOREMOVAL

/* Creates a statusbar message stating the last modification time of the opened
 * data file. */
static gchar *generate_statusbar_lastmodified_message()
{
    gchar *message = NULL;
    const gchar *book_id = NULL;

    if (gnc_current_session_exist())
    {
        book_id = qof_session_get_url (gnc_get_current_session ());
    }

    if (!book_id)
        return NULL;
    else
    {
        if ( gnc_uri_is_file_uri ( book_id ) )
        {
#ifdef HAVE_SYS_STAT_H
            /* The filename is a true file. */
            gchar *filepath = gnc_uri_get_path ( book_id );
            gchar *filename = g_path_get_basename ( filepath );
            {
                // Access the mtime information through stat(2)
                struct stat statbuf;
                int r = stat(filepath, &statbuf);
                if (r == 0)
                {
                    // File mtime could be accessed ok
                    gint64 mtime = statbuf.st_mtime;
                    GDateTime *gdt = gnc_g_date_time_new_from_unix_local (mtime);
                    gchar *dummy_strftime_has_ampm = g_date_time_format (gdt, "%P");
                    /* Translators: This is the date and time that is shown in
                    the status bar after opening a file: The date and time of
                    last modification. The string is the format string for
                    glib's function g_date_time_format(), see there for an
                    explanation of the modifiers. First string is for a locale
                    that has the a.m. or p.m. string in its locale, second
                    string is for locales that do not have that string. */
                    gchar *time_string =
                        g_date_time_format (gdt, (dummy_strftime_has_ampm &&
						  strlen(dummy_strftime_has_ampm) > 0)
                                            ? _("Last modified on %a, %b %e, %Y at %I:%M%P")
                                            : _("Last modified on %x %X"));

                    g_date_time_unref (gdt);

                    //g_warning("got time %ld, str=%s\n", mtime, time_string);
                    /* Translators: This message appears in the status bar after opening the file. */
                    message = g_strdup_printf(_("File %s opened. %s"),
                                              filename, time_string);
                    g_free(time_string);
                    g_free(dummy_strftime_has_ampm);
                }
                else
                {
                    g_warning("Unable to read mtime for file %s\n", filepath);
                    // message is still NULL
                }
            }
            g_free(filename);
            g_free(filepath);
#else
            return NULL;
#endif
        }
        // If the URI is not a file but a database, we can maybe also show
        // something useful, but I have no idea how to obtain this information.
    }
    return message;
}

static void
statusbar_notification_lastmodified()
{
    // First look up the first GncMainWindow to set the statusbar there
    GList *iter;
    GtkWidget *widget = NULL;
    for (iter = active_windows; iter && !(widget && GNC_IS_MAIN_WINDOW(widget));
            iter = g_list_next(iter))
    {
        widget = iter->data;
    }
    if (widget && GNC_IS_MAIN_WINDOW(widget))
    {
        // Ok, we found a mainwindow where we can set a statusbar message
        GncMainWindow *mainwindow = GNC_MAIN_WINDOW(widget);
        GtkWidget *statusbar = gnc_main_window_get_statusbar(GNC_WINDOW(mainwindow));

        gchar *msg = generate_statusbar_lastmodified_message();
        if (msg)
        {
            gnc_statusbar_notification_messageid = gtk_statusbar_push(GTK_STATUSBAR(statusbar), 0, msg);
        }
        g_free(msg);

#ifdef STATUSBAR_NOTIFICATION_AUTOREMOVAL
        // Also register a timeout callback to remove that statusbar
        // notification again after 10 seconds
        g_timeout_add(10 * 1000, statusbar_notification_off, NULL); // maybe not needed anyway?
#endif
    }
    else
    {
        g_warning("uh oh, no GNC_IS_MAIN_WINDOW\n");
    }
}


/** This data structure is used to describe the requested state of a
 *  GtkRadioAction, and us used to pass data among several
 *  functions. */
struct menu_update
{
    /** The name of the GtkRadioAction to be updated. */
    gchar    *action_name;

    /** The new label for this GtkRadioAction. */
    gchar    *label;

    /** Whether or not the GtkRadioAction should be visible. */
    gboolean  visible;
};

#ifndef MAC_INTEGRATION
/** Update the label on the specified GtkRadioAction in the specified
 *  window.  This action is displayed as a menu item in the "Windows"
 *  menu.  This function will end up being called whenever the front
 *  page is changed in any window, or whenever a window is added or
 *  deleted.
 *
 *  @param window The window whose menu item should be updated.
 *
 *  @param data A data structure containing the name of the
 *  GtkRadioAction, and describing the new state for this action.
 *
 *  @internal
 */
static void
gnc_main_window_update_one_menu_action (GncMainWindow *window,
                                        struct menu_update *data)
{
    GncMainWindowPrivate *priv;
    GtkAction* action;

    ENTER("window %p, action %s, label %s, visible %d", window,
          data->action_name, data->label, data->visible);
    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    action = gtk_action_group_get_action(priv->action_group, data->action_name);
    if (action)
        g_object_set(G_OBJECT(action),
                     "label", data->label,
                     "visible", data->visible,
                     (char *)NULL);
    LEAVE(" ");
}

/** Update the window selection GtkRadioAction for a specific window.
 *  This is fairly simple since the windows are listed in the same
 *  order that they appear in the active_windows list, so the index
 *  from the window list is used to generate the name of the action.
 *  If the code is ever changed to allow more than ten open windows in
 *  the menu, then the actions in the menu will need to be dynamically
 *  generated/deleted and it gets harder.
 *
 *  @param window The window whose menu item should be updated.
 *
 *  @internal
 */
static void
gnc_main_window_update_radio_button (GncMainWindow *window)
{
    GncMainWindowPrivate *priv;
    GtkAction *action, *first_action;
    GSList *action_list;
    gchar *action_name;
    gint index;

    ENTER("window %p", window);

    /* Show the new entry in all windows. */
    index = g_list_index(active_windows, window);
    if (index >= n_radio_entries)
    {
        LEAVE("window %d, only %d actions", index, n_radio_entries);
        return;
    }

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    action_name = g_strdup_printf("Window%dAction", index);
    action = gtk_action_group_get_action(priv->action_group, action_name);

    /* Block the signal so as not to affect window ordering (top to
     * bottom) on the screen */
    action_list = gtk_radio_action_get_group(GTK_RADIO_ACTION(action));
    if (action_list)
    {
        first_action = g_slist_last(action_list)->data;
        g_signal_handlers_block_by_func(G_OBJECT(first_action),
                                        G_CALLBACK(gnc_main_window_cmd_window_raise),
                                        window);
        DEBUG("blocked signal on %p, set %p active, window %p", first_action,
              action, window);
        gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(action), TRUE);
        g_signal_handlers_unblock_by_func(G_OBJECT(first_action),
                                          G_CALLBACK(gnc_main_window_cmd_window_raise),
                                          window);
    }
    g_free(action_name);
    LEAVE(" ");
}

/** In every window that the user has open, update the "Window" menu
 *  item that points to the specified window.  This keeps the "Window"
 *  menu items consistent across all open windows.  (These items
 *  cannot be shared because of the way the GtkUIManager code works.)
 *
 *  This function is called whenever the user switches pages in a
 *  window, or whenever a window is added or deleted.
 *
 *  @param window The window whose menu item should be updated in all
 *  open windows.
 *
 *  @internal
 */
static void
gnc_main_window_update_menu_item (GncMainWindow *window)
{
    struct menu_update data;
    gchar **strings, *title, *expanded;
    gint index;

    ENTER("window %p", window);
    index = g_list_index(active_windows, window);
    if (index > n_radio_entries)
    {
        LEAVE("skip window %d (only %d entries)", index, n_radio_entries);
        return;
    }

    /* Figure out the label name. Add the accelerator if possible. */
    title = gnc_main_window_generate_title(window);
    strings = g_strsplit(title, "_", 0);
    g_free(title);
    expanded = g_strjoinv("__", strings);
    if (index < 10)
    {
        data.label = g_strdup_printf("_%d %s", (index + 1) % 10, expanded);
        g_free(expanded);
    }
    else
    {
        data.label = expanded;
    }
    g_strfreev(strings);

    data.visible = TRUE;
    data.action_name = g_strdup_printf("Window%dAction", index);
    g_list_foreach(active_windows,
                   (GFunc)gnc_main_window_update_one_menu_action,
                   &data);
    g_free(data.action_name);
    g_free(data.label);

    LEAVE(" ");
}
#endif /* !MAC_INTEGRATION */

/** Update all menu entries for all window menu items in all windows.
 *  This function is called whenever a window is added or deleted.
 *  The worst case scenario is where the user has deleted the first
 *  window, so every single visible item needs to be updated.
 *
 *  @internal
 */

#ifndef MAC_INTEGRATION
static void
gnc_main_window_update_all_menu_items (void)
{
    struct menu_update data;
    gchar *label;
    gint i;

    ENTER("");
    /* First update the entries for all existing windows */
    g_list_foreach(active_windows,
                   (GFunc)gnc_main_window_update_menu_item,
                   NULL);
    g_list_foreach(active_windows,
                   (GFunc)gnc_main_window_update_radio_button,
                   NULL);

    /* Now hide any entries that aren't being used. */
    data.visible = FALSE;
    for (i = g_list_length(active_windows); i < n_radio_entries; i++)
    {
        data.action_name = g_strdup_printf("Window%dAction", i);
        label = g_strdup_printf("Window _%d", (i - 1) % 10);
        data.label = gettext(label);

        g_list_foreach(active_windows,
                       (GFunc)gnc_main_window_update_one_menu_action,
                       &data);

        g_free(data.action_name);
        g_free(label);
    }
    LEAVE(" ");
}
#endif /* !MAC_INTEGRATION */

/** Show/hide the close box on the tab of a notebook page.  This
 *  function first checks to see if the specified page has a close
 *  box, and if so, sets its visibility to the requested state.
 *
 *  @internal
 *
 *  @param page The GncPluginPage whose notebook tab should be updated.
 *
 *  @param new_value A pointer to the boolean that indicates whether
 *  or not the close button should be visible.
 */
static void
gnc_main_window_update_tab_close_one_page (GncPluginPage *page,
        gpointer user_data)
{
    gboolean *new_value = user_data;
    GtkWidget * close_button;

    ENTER("page %p, visible %d", page, *new_value);
    close_button = g_object_get_data(G_OBJECT (page), PLUGIN_PAGE_CLOSE_BUTTON);
    if (!close_button)
    {
        LEAVE("no close button");
        return;
    }

    if (*new_value)
        gtk_widget_show (close_button);
    else
        gtk_widget_hide (close_button);
    LEAVE(" ");
}


/** Show/hide the close box on all pages in all windows.  This function
 *  calls gnc_main_window_update_tab_close() for each plugin page in the
 *  application.
 *
 *  @internal
 *
 *  @param prefs Unused.
 *
 *  @param pref Unused.
 *
 *  @param user_data Unused.
 */
static void
gnc_main_window_update_tab_close (gpointer prefs, gchar *pref, gpointer user_data)
{
    gboolean new_value;

    ENTER(" ");
    new_value = gnc_prefs_get_bool (GNC_PREFS_GROUP_GENERAL, GNC_PREF_SHOW_CLOSE_BUTTON);
    gnc_main_window_foreach_page(
        gnc_main_window_update_tab_close_one_page,
        &new_value);
    LEAVE(" ");
}


/** Show/hide the account color on the tab of a notebook page.
 *
 *  @internal
 *
 *  @param page The GncPluginPage whose notebook tab should be updated.
 *
 *  @param user_data GncMainWindow.
 */
static void
gnc_main_window_update_tab_color_one_page (GncPluginPage *page,
        gpointer user_data)
{
    const gchar          *color_string;

    ENTER("page %p", page);
    color_string = gnc_plugin_page_get_page_color(page);
    main_window_update_page_color (page, color_string);
    LEAVE(" ");
}


/** Show/hide the account color on tabs.
 *
 *  @internal
 *
 *  @param prefs Unused.
 *
 *  @param pref Name of the preference that was changed.
 *
 *  @param user_data GncMainWindow.
 */
static void
gnc_main_window_update_tab_color (gpointer gsettings, gchar *pref, gpointer user_data)
{
    GncMainWindowPrivate *priv;
    GncMainWindow        *window;

    ENTER(" ");
    g_return_if_fail(GNC_IS_MAIN_WINDOW(user_data));
    window = user_data;
    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    if (g_strcmp0 (GNC_PREF_TAB_COLOR, pref) == 0)
        priv->show_color_tabs = gnc_prefs_get_bool (GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_COLOR);
    gnc_main_window_foreach_page (gnc_main_window_update_tab_color_one_page, window);
    LEAVE(" ");
}


/** Update the width of the label in the tab of a notebook page.  This
 *  function adjusts both the width and the ellipsize mode so that the tab
 *  label looks correct.  The special check for a zero value handles the
 *  case where a user hasn't set a tab width and the preference default isn't
 *  detected.
 *
 *  @internal
 *
 *  @param page The GncPluginPage whose notebook tab should be updated.
 *
 *  @param new_value The new width of the label in the tab.
 */
static void
gnc_main_window_update_tab_width_one_page (GncPluginPage *page,
        gpointer user_data)
{
    gint *new_value = user_data;
    GtkWidget *label;

    ENTER("page %p, visible %d", page, *new_value);
    label = g_object_get_data(G_OBJECT (page), PLUGIN_PAGE_TAB_LABEL);
    if (!label)
    {
        LEAVE("no label");
        return;
    }

    if (*new_value != 0)
    {
        gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_MIDDLE);
        gtk_label_set_max_width_chars(GTK_LABEL(label), *new_value);
    }
    else
    {
        gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_NONE);
        gtk_label_set_max_width_chars(GTK_LABEL(label), 100);
    }
    LEAVE(" ");
}


/** Update the tab label width in all pages in all windows.  This function
 *  calls gnc_main_window_update_tab_width() for each plugin page in the
 *  application.
 *
 *  @internal
 *
 *  @param prefs Unused.
 *
 *  @param pref Unused.
 *
 *  @param user_data Unused.
 */
static void
gnc_main_window_update_tab_width (gpointer prefs, gchar *pref, gpointer user_data)
{
    gint new_value;

    ENTER(" ");
    new_value = gnc_prefs_get_float (GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_WIDTH);
    gnc_main_window_foreach_page(
        gnc_main_window_update_tab_width_one_page,
        &new_value);
    LEAVE(" ");
}


/************************************************************
 *                 Tab Label Implementation                 *
 ************************************************************/
static gboolean
main_window_find_tab_items (GncMainWindow *window,
                            GncPluginPage *page,
                            GtkWidget **label_p,
                            GtkWidget **entry_p)
{
    GncMainWindowPrivate *priv;
    GtkWidget *tab_hbox, *widget, *tab_widget;
    GList *children, *tmp;

    ENTER("window %p, page %p, label_p %p, entry_p %p",
          window, page, label_p, entry_p);
    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    *label_p = *entry_p = NULL;

    if (!page->notebook_page)
    {
        LEAVE("invalid notebook_page");
        return FALSE;
    }

    tab_widget = gtk_notebook_get_tab_label(GTK_NOTEBOOK(priv->notebook),
                                           page->notebook_page);
    if (GTK_IS_EVENT_BOX (tab_widget))
        tab_hbox = gtk_bin_get_child(GTK_BIN(tab_widget));
    else if (GTK_IS_HBOX (tab_widget))
        tab_hbox = tab_widget;
    else
    {
        PWARN ("Unknown widget for tab label %p", tab_widget);
        return FALSE;
    }

    children = gtk_container_get_children(GTK_CONTAINER(tab_hbox));
    for (tmp = children; tmp; tmp = g_list_next(tmp))
    {
        widget = tmp->data;
        if (GTK_IS_LABEL(widget))
        {
            *label_p = widget;
        }
        else if (GTK_IS_ENTRY(widget))
        {
            *entry_p = widget;
        }
    }
    g_list_free(children);

    LEAVE("label %p, entry %p", *label_p, *entry_p);
    return (*label_p && *entry_p);
}

static gboolean
main_window_find_tab_widget (GncMainWindow *window,
                             GncPluginPage *page,
                             GtkWidget **widget_p)
{
    GncMainWindowPrivate *priv;

    ENTER("window %p, page %p, widget %p",
          window, page, widget_p);
    *widget_p = NULL;

    if (!page->notebook_page)
    {
        LEAVE("invalid notebook_page");
        return FALSE;
    }

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    *widget_p = gtk_notebook_get_tab_label(GTK_NOTEBOOK(priv->notebook),
                                           page->notebook_page);

    LEAVE("widget %p", *widget_p);
    return TRUE;
}

void
main_window_update_page_name (GncPluginPage *page,
                              const gchar *name_in)
{
    GncMainWindow *window;
    GncMainWindowPrivate *priv;
    GtkWidget *label, *entry;
    gchar *name, *old_page_name, *old_page_long_name;

    ENTER(" ");

    if ((name_in == NULL) || (*name_in == '\0'))
    {
        LEAVE("no string");
        return;
    }
    name = g_strstrip(g_strdup(name_in));

    /* Optimization, if the name hasn't changed, don't update X. */
    if (*name == '\0' || 0 == strcmp(name, gnc_plugin_page_get_page_name(page)))
    {
        g_free(name);
        LEAVE("empty string or name unchanged");
        return;
    }

    old_page_name = g_strdup( gnc_plugin_page_get_page_name(page));
    old_page_long_name = g_strdup( gnc_plugin_page_get_page_long_name(page));

    /* Update the plugin */
    gnc_plugin_page_set_page_name(page, name);

    /* Update the notebook tab */
    window = GNC_MAIN_WINDOW(page->window);
    if (!window)
    {
        g_free(old_page_name);
        g_free(old_page_long_name);
        g_free(name);
        LEAVE("no window widget available");
        return;
    }

    if (main_window_find_tab_items(window, page, &label, &entry))
        gtk_label_set_text(GTK_LABEL(label), name);

    /* Update Tooltip on notebook Tab */
    if (old_page_long_name && old_page_name
            && g_strrstr(old_page_long_name, old_page_name) != NULL)
    {
        gchar *new_page_long_name;
        gint string_position;
        GtkWidget *tab_widget;

        string_position = strlen(old_page_long_name) - strlen(old_page_name);
        new_page_long_name = g_strconcat(g_strndup(old_page_long_name, string_position), name, NULL);

        gnc_plugin_page_set_page_long_name(page, new_page_long_name);

        if (main_window_find_tab_widget(window, page, &tab_widget))
            gtk_widget_set_tooltip_text(tab_widget, new_page_long_name);

        g_free(new_page_long_name);
    }

    /* Update the notebook menu */
    if (page->notebook_page)
    {
        priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
        label = gtk_notebook_get_menu_label (GTK_NOTEBOOK(priv->notebook),
                                             page->notebook_page);
        gtk_label_set_text(GTK_LABEL(label), name);
    }

    /* Force an update of the window title */
    gnc_main_window_update_title(window);
    g_free(old_page_long_name);
    g_free(old_page_name);
    g_free(name);
    LEAVE("done");
}


void
main_window_update_page_color (GncPluginPage *page,
                               const gchar *color_in)
{
    GncMainWindow *window;
    GncMainWindowPrivate *priv;
    GtkWidget *tab_widget;
    GdkColor tab_color;
    gchar *color_string = NULL;
    gboolean want_color = FALSE;

    ENTER(" ");
    if (color_in)
        color_string = g_strstrip(g_strdup(color_in));

    if (color_string && *color_string != '\0')
        want_color = TRUE;

    /* Update the plugin */
    window = GNC_MAIN_WINDOW(page->window);
    if (want_color)
        gnc_plugin_page_set_page_color(page, color_string);
    else
        gnc_plugin_page_set_page_color(page, NULL);

    /* Update the notebook tab */
    main_window_find_tab_widget (window, page, &tab_widget);
    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    if (want_color && gdk_color_parse(color_string, &tab_color) && priv->show_color_tabs)
    {
        if (!GTK_IS_EVENT_BOX (tab_widget))
        {
            GtkWidget *event_box = gtk_event_box_new ();
            g_object_ref (tab_widget);
            gtk_notebook_set_tab_label (GTK_NOTEBOOK(priv->notebook),
                                        page->notebook_page, event_box);
            gtk_container_add (GTK_CONTAINER(event_box), tab_widget);
            g_object_unref (tab_widget);
            tab_widget = event_box;
        }
        gtk_widget_modify_bg(tab_widget, GTK_STATE_NORMAL, &tab_color);
        gtk_widget_modify_bg(tab_widget, GTK_STATE_ACTIVE, &tab_color);
    }
    else
    {
        if (GTK_IS_EVENT_BOX (tab_widget))
        {
            GtkWidget *tab_hbox = gtk_bin_get_child(GTK_BIN(tab_widget));
            g_object_ref (tab_hbox);
            gtk_container_remove (GTK_CONTAINER(tab_widget), tab_hbox);
            gtk_notebook_set_tab_label (GTK_NOTEBOOK(priv->notebook),
                                        page->notebook_page, tab_hbox);
            g_object_unref (tab_hbox);
        }
    }
    g_free(color_string);
    LEAVE("done");
}


static void
gnc_main_window_tab_entry_activate (GtkWidget *entry,
                                    GncPluginPage *page)
{
    GtkWidget *label, *entry2;

    g_return_if_fail(GTK_IS_ENTRY(entry));
    g_return_if_fail(GNC_IS_PLUGIN_PAGE(page));

    ENTER("");
    if (!main_window_find_tab_items(GNC_MAIN_WINDOW(page->window),
                                    page, &label, &entry2))
    {
        LEAVE("can't find required widgets");
        return;
    }

    main_window_update_page_name(page, gtk_entry_get_text(GTK_ENTRY(entry)));

    gtk_widget_hide(entry);
    gtk_widget_show(label);
    LEAVE("");
}


static gboolean
gnc_main_window_tab_entry_editing_done (GtkWidget *entry,
                                        GncPluginPage *page)
{
    ENTER("");
    gnc_main_window_tab_entry_activate(entry, page);
    LEAVE("");
    return FALSE;
}

static gboolean
gnc_main_window_tab_entry_focus_out_event (GtkWidget *entry,
        GdkEvent *event,
        GncPluginPage *page)
{
    ENTER("");
    gtk_cell_editable_editing_done(GTK_CELL_EDITABLE(entry));
    LEAVE("");
    return FALSE;
}

static gboolean
gnc_main_window_tab_entry_key_press_event (GtkWidget *entry,
        GdkEventKey *event,
        GncPluginPage *page)
{
    if (event->keyval == GDK_KEY_Escape)
    {
        GtkWidget *label, *entry2;

        g_return_val_if_fail(GTK_IS_ENTRY(entry), FALSE);
        g_return_val_if_fail(GNC_IS_PLUGIN_PAGE(page), FALSE);

        ENTER("");
        if (!main_window_find_tab_items(GNC_MAIN_WINDOW(page->window),
                                        page, &label, &entry2))
        {
            LEAVE("can't find required widgets");
            return FALSE;
        }

        gtk_entry_set_text(GTK_ENTRY(entry), gtk_label_get_text(GTK_LABEL(label)));
        gtk_widget_hide(entry);
        gtk_widget_show(label);
        LEAVE("");
    }
    return FALSE;
}

/************************************************************
 *                   Widget Implementation                  *
 ************************************************************/

/*  Get the type of a gnc main window.
 */
GType
gnc_main_window_get_type (void)
{
    static GType gnc_main_window_type = 0;

    if (gnc_main_window_type == 0)
    {
        static const GTypeInfo our_info =
        {
            sizeof (GncMainWindowClass),
            NULL,
            NULL,
            (GClassInitFunc) gnc_main_window_class_init,
            NULL,
            NULL,
            sizeof (GncMainWindow),
            0,
            (GInstanceInitFunc) gnc_main_window_init
        };

        static const GInterfaceInfo plugin_info =
        {
            (GInterfaceInitFunc) gnc_window_main_window_init,
            NULL,
            NULL
        };

        gnc_main_window_type = g_type_register_static (GTK_TYPE_WINDOW,
                               GNC_MAIN_WINDOW_NAME,
                               &our_info, 0);
        g_type_add_interface_static (gnc_main_window_type,
                                     GNC_TYPE_WINDOW,
                                     &plugin_info);
    }

    return gnc_main_window_type;
}


/** Initialize the class for a new gnucash main window.  This will set
 *  up any function pointers that override functions in the parent
 *  class, and also initialize the signals that this class of widget
 *  can generate.
 *
 *  @param klass The new class structure created by the object system.
 */
static void
gnc_main_window_class_init (GncMainWindowClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS (klass);
    GtkObjectClass *gtkobject_class = GTK_OBJECT_CLASS(klass);

    parent_class = g_type_class_peek_parent (klass);

    window_type = g_quark_from_static_string ("gnc-main-window");

    object_class->finalize = gnc_main_window_finalize;

    /* GtkObject signals */
    gtkobject_class->destroy = gnc_main_window_destroy;

    g_type_class_add_private(klass, sizeof(GncMainWindowPrivate));

    /**
     * GncMainWindow::page_added:
     * @param window: the #GncMainWindow
     * @param page: the #GncPluginPage
     *
     * The "page_added" signal is emitted when a new page is added
     * to the notebook of a GncMainWindow.  This can be used to
     * attach a signal from the page so that menu actions can be
     * adjusted based upon events that occur within the page
     * (e.g. an account is selected.)
     */
    main_window_signals[PAGE_ADDED] =
        g_signal_new ("page_added",
                      G_OBJECT_CLASS_TYPE (object_class),
                      G_SIGNAL_RUN_FIRST,
                      G_STRUCT_OFFSET (GncMainWindowClass, page_added),
                      NULL, NULL,
                      g_cclosure_marshal_VOID__OBJECT,
                      G_TYPE_NONE, 1,
                      G_TYPE_OBJECT);

    /**
     * GncMainWindow::page_changed:
     * @param window: the #GncMainWindow
     * @param page: the #GncPluginPage
     *
     * The "page_changed" signal is emitted when a new page is
     * selected in the notebook of a GncMainWindow.  This can be
     * used to to adjust menu actions based upon which page is
     * currently displayed in a window.
     */
    main_window_signals[PAGE_CHANGED] =
        g_signal_new ("page_changed",
                      G_OBJECT_CLASS_TYPE (object_class),
                      G_SIGNAL_RUN_FIRST,
                      G_STRUCT_OFFSET (GncMainWindowClass, page_changed),
                      NULL, NULL,
                      g_cclosure_marshal_VOID__OBJECT,
                      G_TYPE_NONE, 1,
                      G_TYPE_OBJECT);

    gnc_prefs_register_cb (GNC_PREFS_GROUP_GENERAL,
                           GNC_PREF_SHOW_CLOSE_BUTTON,
                           gnc_main_window_update_tab_close,
                           NULL);
    gnc_prefs_register_cb (GNC_PREFS_GROUP_GENERAL,
                           GNC_PREF_TAB_WIDTH,
                           gnc_main_window_update_tab_width,
                           NULL);

    gnc_hook_add_dangler(HOOK_BOOK_SAVED,
                         (GFunc)gnc_main_window_update_all_titles, NULL);
    gnc_hook_add_dangler(HOOK_BOOK_OPENED,
                         (GFunc)gnc_main_window_attach_to_book, NULL);

}


/** Initialize a new instance of a gnucash main window.  This function
 *  initializes the object private storage space.  It also adds the
 *  new object to a list (for memory tracking purposes).
 *
 *  @param window The new object instance created by the object system.
 *
 *  @param klass A pointer to the class data structure for this
 *  object. */
static void
gnc_main_window_init (GncMainWindow *window,
                      GncMainWindowClass *klass)
{
    GncMainWindowPrivate *priv;

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    priv->merged_actions_table =
        g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);

    priv->event_handler_id =
        qof_event_register_handler(gnc_main_window_event_handler, window);

    /* Get the show_color_tabs value preference */
    priv->show_color_tabs = gnc_prefs_get_bool(GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_COLOR);
    priv->about_dialog = NULL;

    gnc_prefs_register_cb (GNC_PREFS_GROUP_GENERAL,
                           GNC_PREF_TAB_COLOR,
                           gnc_main_window_update_tab_color,
                           window);

    gnc_main_window_setup_window (window);
    gnc_gobject_tracking_remember(G_OBJECT(window),
                                  G_OBJECT_CLASS(klass));
}


/** Finalize the GncMainWindow object.  This function is called from
 *  the G_Object level to complete the destruction of the object.  It
 *  should release any memory not previously released by the destroy
 *  function (i.e. the private data structure), then chain up to the
 *  parent's destroy function.
 *
 *  @param object The object being destroyed.
 *
 *  @internal
 */
static void
gnc_main_window_finalize (GObject *object)
{
    g_return_if_fail (object != NULL);
    g_return_if_fail (GNC_IS_MAIN_WINDOW (object));

    if (active_windows == NULL)
    {
        /* Oops. User killed last window and we didn't catch it. */
        g_idle_add((GSourceFunc)gnc_shutdown, 0);
    }

    gnc_gobject_tracking_forget(object);
    G_OBJECT_CLASS (parent_class)->finalize (object);
}


static void
gnc_main_window_destroy (GtkObject *object)
{
    GncMainWindow *window;
    GncMainWindowPrivate *priv;
    GncPluginManager *manager;
    GList *plugins;

    g_return_if_fail (object != NULL);
    g_return_if_fail (GNC_IS_MAIN_WINDOW (object));

    window = GNC_MAIN_WINDOW (object);

    active_windows = g_list_remove (active_windows, window);

    /* Do these things once */
    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    if (priv->merged_actions_table)
    {

        /* Close any pages in this window */
        while (priv->current_page)
            gnc_main_window_close_page(priv->current_page);

        if (gnc_window_get_progressbar_window() == GNC_WINDOW(window))
            gnc_window_set_progressbar_window(NULL);
#ifndef MAC_INTEGRATION
        /* Update the "Windows" menu in all other windows */
        gnc_main_window_update_all_menu_items();
#endif
        gnc_prefs_remove_cb_by_func (GNC_PREFS_GROUP_GENERAL,
                                     GNC_PREF_TAB_COLOR,
                                     gnc_main_window_update_tab_color,
                                     window);

        qof_event_unregister_handler(priv->event_handler_id);
        priv->event_handler_id = 0;

        g_hash_table_destroy (priv->merged_actions_table);
        priv->merged_actions_table = NULL;

        /* GncPluginManager stuff */
        manager = gnc_plugin_manager_get ();
        plugins = gnc_plugin_manager_get_plugins (manager);
        g_list_foreach (plugins, gnc_main_window_remove_plugin, window);
        g_list_free (plugins);
    }
    if (priv->about_dialog)
	g_object_unref (priv->about_dialog);
    GTK_OBJECT_CLASS (parent_class)->destroy (object);
}


/*  Create a new gnc main window plugin.
 */
GncMainWindow *
gnc_main_window_new (void)
{
    GncMainWindow *window;
    GtkWidget *old_window;

    window = g_object_new (GNC_TYPE_MAIN_WINDOW, NULL);
    gtk_window_set_default_size(GTK_WINDOW(window), 800, 600);

    old_window = gnc_ui_get_toplevel();
    if (old_window)
    {
        gint width, height;
        gtk_window_get_size (GTK_WINDOW (old_window), &width, &height);
        gtk_window_resize (GTK_WINDOW (window), width, height);
        if ((gdk_window_get_state((gtk_widget_get_window (GTK_WIDGET(old_window))))
                & GDK_WINDOW_STATE_MAXIMIZED) != 0)
        {
            gtk_window_maximize (GTK_WINDOW (window));
        }
    }
    active_windows = g_list_append (active_windows, window);
    gnc_main_window_update_title(window);
#ifdef MAC_INTEGRATION
    gnc_quartz_set_menu(window);
#else
    gnc_main_window_update_all_menu_items();
#endif
    gnc_engine_add_commit_error_callback( gnc_main_window_engine_commit_error_callback, window );

    return window;
}

/************************************************************
 *                     Utility Functions                    *
 ************************************************************/

static void
gnc_main_window_engine_commit_error_callback( gpointer data,
        QofBackendError errcode )
{
    GncMainWindow* window = GNC_MAIN_WINDOW(data);
    GtkWidget* dialog;
    const gchar *reason = _("Unable to save to database.");
    if ( errcode == ERR_BACKEND_READONLY )
        reason = _("Unable to save to database: Book is marked read-only.");
    dialog = gtk_message_dialog_new( GTK_WINDOW(window),
                                     GTK_DIALOG_DESTROY_WITH_PARENT,
                                     GTK_MESSAGE_ERROR,
                                     GTK_BUTTONS_CLOSE,
                                     "%s",
                                     reason );
    gtk_dialog_run(GTK_DIALOG (dialog));
    gtk_widget_destroy(dialog);

}

/** Connect a GncPluginPage to the window.  This function will insert
 *  the page in to the window's notebook and its list of active pages.
 *  It will also emit the "inserted" signal on the page, and the
 *  "add_page" signal on the window.
 *
 *  @param window The window where the new page should be added.
 *
 *  @param page The GncPluginPage that should be added to the window.
 *  The visible widget for this plugin must have already been created.
 *
 *  @param tab_hbox The widget that should be added into the notebook
 *  tab for this page.  Generally this is a GtkLabel, but could also
 *  be a GtkHBox containing an icon and a label.
 *
 *  @param menu_label The widget that should be added into the
 *  notebook popup menu for this page.  This should be a GtkLabel.
 */
static void
gnc_main_window_connect (GncMainWindow *window,
                         GncPluginPage *page,
                         GtkWidget *tab_hbox,
                         GtkWidget *menu_label)
{
    GncMainWindowPrivate *priv;
    GtkNotebook *notebook;

    page->window = GTK_WIDGET(window);
    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    notebook = GTK_NOTEBOOK (priv->notebook);
    priv->installed_pages = g_list_append (priv->installed_pages, page);
    priv->usage_order = g_list_prepend (priv->usage_order, page);
    gtk_notebook_append_page_menu (notebook, page->notebook_page,
                                   tab_hbox, menu_label);
    gtk_notebook_set_tab_reorderable (notebook, page->notebook_page, TRUE);
    gnc_plugin_page_inserted (page);
    gtk_notebook_set_current_page (notebook, -1);
    if (GNC_PLUGIN_PAGE_GET_CLASS(page)->window_changed)
        (GNC_PLUGIN_PAGE_GET_CLASS(page)->window_changed)(page, GTK_WIDGET(window));
    g_signal_emit (window, main_window_signals[PAGE_ADDED], 0, page);

    g_signal_connect(G_OBJECT(page->notebook_page), "popup-menu",
                     G_CALLBACK(gnc_main_window_popup_menu_cb), page);
    g_signal_connect_after(G_OBJECT(page->notebook_page), "button-press-event",
                           G_CALLBACK(gnc_main_window_button_press_cb), page);
}


/** Disconnect a GncPluginPage page from the window.  If this page is
 *  currently foremost in the window's notebook, its user interface
 *  actions will be disconnected and the page's summarybar widget (if
 *  any) will be removed.  The page is then removed from the window's
 *  notebook and its list of active pages.
 *
 *  @param window The window the page should be removed from.
 *
 *  @param page The GncPluginPage that should be removed from the
 *  window.
 *
 *  @internal
 */
static void
gnc_main_window_disconnect (GncMainWindow *window,
                            GncPluginPage *page)
{
    GncMainWindowPrivate *priv;
    GtkNotebook *notebook;
    GncPluginPage *new_page;
    gint page_num;

    /* Disconnect the callbacks */
    g_signal_handlers_disconnect_by_func(G_OBJECT(page->notebook_page),
                                         G_CALLBACK(gnc_main_window_popup_menu_cb), page);
    g_signal_handlers_disconnect_by_func(G_OBJECT(page->notebook_page),
                                         G_CALLBACK(gnc_main_window_button_press_cb), page);

    /* Disconnect the page and summarybar from the window */
    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    if (priv->current_page == page)
    {
        gnc_plugin_page_unmerge_actions (page, window->ui_merge);
        gnc_plugin_page_unselected (page);
        priv->current_page = NULL;
    }

    /* Remove it from the list of pages in the window */
    priv->installed_pages = g_list_remove (priv->installed_pages, page);
    priv->usage_order = g_list_remove (priv->usage_order, page);

    /* Switch to the last recently used page */
    notebook = GTK_NOTEBOOK (priv->notebook);
    if (gnc_prefs_get_bool(GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_NEXT_RECENT))
    {
        new_page = g_list_nth_data (priv->usage_order, 0);
        if (new_page)
        {
            page_num =  gtk_notebook_page_num(notebook, new_page->notebook_page);
            gtk_notebook_set_current_page(notebook, page_num);
            /* This may have caused WebKit to schedule  a timer interrupt which it
               sometimes  forgets to cancel before deleting the object.  See
               <https://bugs.webkit.org/show_bug.cgi?id=119003>.   Get around this
               by flushing all events to get rid of the timer interrupt. */
            while (gtk_events_pending())
                gtk_main_iteration();
        }
    }

    /* Remove the page from the notebook */
    page_num =  gtk_notebook_page_num(notebook, page->notebook_page);
    gtk_notebook_remove_page (notebook, page_num);

    if ( gtk_notebook_get_current_page(notebook) == -1)
    {
        /* Need to synthesize a page changed signal when the last
         * page is removed.  The notebook doesn't generate a signal
         * for this, therefore the switch_page code in this file
         * never gets called to generate this signal. */
        gnc_main_window_switch_page(notebook, NULL, -1, window);
        //g_signal_emit (window, main_window_signals[PAGE_CHANGED], 0, NULL);
    }

    gnc_plugin_page_removed (page);

    gtk_ui_manager_ensure_update (window->ui_merge);
    gnc_window_set_status (GNC_WINDOW(window), page, NULL);
}


/************************************************************
 *                                                          *
 ************************************************************/


void
gnc_main_window_display_page (GncPluginPage *page)
{
    GncMainWindow *window;
    GncMainWindowPrivate *priv;
    GtkNotebook *notebook;
    gint page_num;

    window = GNC_MAIN_WINDOW (page->window);
    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    notebook = GTK_NOTEBOOK (priv->notebook);
    page_num = gtk_notebook_page_num(notebook, page->notebook_page);
    gtk_notebook_set_current_page (notebook, page_num);
    gtk_window_present(GTK_WINDOW(window));
}


/*  Display a data plugin page in a window.  If the page already
 *  exists in any window, then that window will be brought to the
 *  front and the notebook switch to display the specified page.  If
 *  the page is new then it will be added to the specified window.  If
 *  the window is NULL, the new page will be added to the first
 *  window.
 */
void
gnc_main_window_open_page (GncMainWindow *window,
                           GncPluginPage *page)
{
    GncMainWindowPrivate *priv;
    GtkWidget *tab_hbox;
    GtkWidget *label, *entry;
    const gchar *icon, *text, *color_string;
    GtkWidget *image;
    GList *tmp;
    gint width;
    GdkColor tab_color;

    ENTER("window %p, page %p", window, page);
    if (window)
        g_return_if_fail (GNC_IS_MAIN_WINDOW (window));
    g_return_if_fail (GNC_IS_PLUGIN_PAGE (page));
    g_return_if_fail (gnc_plugin_page_has_books(page));

    if (gnc_main_window_page_exists(page))
    {
        gnc_main_window_display_page(page);
        return;
    }

    /* Does the page want to be in a new window? */
    if (gnc_plugin_page_get_use_new_window(page))
    {
        /* See if there's a blank window. If so, use that. */
        for (tmp = active_windows; tmp; tmp = g_list_next(tmp))
        {
            window = GNC_MAIN_WINDOW(tmp->data);
            priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
            if (priv->installed_pages == NULL)
            {
                break;
            }
        }
        if (tmp == NULL)
            window = gnc_main_window_new ();
        gtk_widget_show(GTK_WIDGET(window));
    }
    else if ((window == NULL) && active_windows)
    {
        window = active_windows->data;
    }

    page->window = GTK_WIDGET(window);
    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    page->notebook_page = gnc_plugin_page_create_widget (page);
    g_object_set_data (G_OBJECT (page->notebook_page),
                       PLUGIN_PAGE_LABEL, page);

    /*
     * The page tab.
     */
    width = gnc_prefs_get_float(GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_WIDTH);
    icon = GNC_PLUGIN_PAGE_GET_CLASS(page)->tab_icon;
    label = gtk_label_new (gnc_plugin_page_get_page_name(page));
    if (width != 0)
    {
        gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_MIDDLE);
        gtk_label_set_max_width_chars(GTK_LABEL(label), width);
    }
    gtk_widget_show (label);

    tab_hbox = gtk_hbox_new (FALSE, 6);
    gtk_widget_show (tab_hbox);

    if (icon != NULL)
    {
        image = gtk_image_new_from_stock (icon, GTK_ICON_SIZE_MENU);
        gtk_widget_show (image);
        gtk_box_pack_start (GTK_BOX (tab_hbox), image, FALSE, FALSE, 0);
        gtk_box_pack_start (GTK_BOX (tab_hbox), label, TRUE, TRUE, 0);
    }
    else
        gtk_box_pack_start (GTK_BOX (tab_hbox), label, TRUE, TRUE, 0);

    text = gnc_plugin_page_get_page_long_name(page);
    if (text)
    {
        gtk_widget_set_tooltip_text(tab_hbox, text);
    }

    entry = gtk_entry_new();
    gtk_widget_hide (entry);
    gtk_box_pack_start (GTK_BOX (tab_hbox), entry, TRUE, TRUE, 0);
    g_signal_connect(G_OBJECT(entry), "activate",
                     G_CALLBACK(gnc_main_window_tab_entry_activate), page);
    g_signal_connect(G_OBJECT(entry), "focus-out-event",
                     G_CALLBACK(gnc_main_window_tab_entry_focus_out_event),
                     page);
    g_signal_connect(G_OBJECT(entry), "key-press-event",
                     G_CALLBACK(gnc_main_window_tab_entry_key_press_event),
                     page);
    g_signal_connect(G_OBJECT(entry), "editing-done",
                     G_CALLBACK(gnc_main_window_tab_entry_editing_done),
                     page);

    /* Add close button - Not for immutable pages */
    if (!g_object_get_data (G_OBJECT (page), PLUGIN_PAGE_IMMUTABLE))
    {
        GtkWidget *close_image, *close_button;
        GtkRequisition requisition;

        close_button = gtk_button_new();
        gtk_button_set_relief(GTK_BUTTON(close_button), GTK_RELIEF_NONE);
        close_image = gtk_image_new_from_stock(GTK_STOCK_CLOSE, GTK_ICON_SIZE_MENU);
        gtk_widget_show(close_image);
        gtk_widget_size_request(close_image, &requisition);
        gtk_widget_set_size_request(close_button, requisition.width + 4,
                                    requisition.height + 2);
        gtk_button_set_alignment(GTK_BUTTON(close_button), 0.5, 0.5);
        gtk_container_add(GTK_CONTAINER(close_button), close_image);
        if (gnc_prefs_get_bool(GNC_PREFS_GROUP_GENERAL, GNC_PREF_SHOW_CLOSE_BUTTON))
            gtk_widget_show (close_button);
        else
            gtk_widget_hide (close_button);

        g_signal_connect_swapped (G_OBJECT (close_button), "clicked",
                                  G_CALLBACK(gnc_main_window_close_page), page);

        gtk_box_pack_start (GTK_BOX (tab_hbox), close_button, FALSE, FALSE, 0);

        g_object_set_data (G_OBJECT (page), PLUGIN_PAGE_CLOSE_BUTTON, close_button);
    }

    /*
     * The popup menu
     */
    label = gtk_label_new (gnc_plugin_page_get_page_name(page));

    /*
     * Now install it all in the window.
     */
    gnc_main_window_connect(window, page, tab_hbox, label);

    color_string = gnc_plugin_page_get_page_color(page);
    main_window_update_page_color (page, color_string);
    LEAVE("");
}


/*  Remove a data plugin page from a window and display the previous
 *  page.  If the page removed was the last page in the window, and
 *  there is more than one window open, then the entire window will be
 *  destroyed.
 */
void
gnc_main_window_close_page (GncPluginPage *page)
{
    GncMainWindow *window;
    GncMainWindowPrivate *priv;

    if (!page || !page->notebook_page)
        return;

    if (!gnc_plugin_page_finish_pending(page))
        return;

    if (!GNC_IS_MAIN_WINDOW (page->window))
        return;

    window = GNC_MAIN_WINDOW (page->window);
    if (!window)
    {
        g_warning("Page is not in a window.");
        return;
    }

    gnc_main_window_disconnect(window, page);
    gnc_plugin_page_destroy_widget (page);
    g_object_unref(page);

    /* If this isn't the last window, go ahead and destroy the window. */
    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    if (priv->installed_pages == NULL)
    {
        if (g_list_length(active_windows) > 1)
        {
            gtk_widget_destroy(GTK_WIDGET(window));
        }
    }
}


/*  Retrieve a pointer to the page that is currently at the front of
 *  the specified window.  Any plugin that needs to manipulate its
 *  menus based upon the currently selected menu page should connect
 *  to the "page_changed" signal on a window.  The callback function
 *  from that signal can then call this function to obtain a pointer
 *  to the current page.
 */
GncPluginPage *
gnc_main_window_get_current_page (GncMainWindow *window)
{
    GncMainWindowPrivate *priv;

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    return priv->current_page;
}


/*  Manually add a set of actions to the specified window.  Plugins
 *  whose user interface is not hard coded (e.g. the menu-additions
 *  plugin) must create their actions at run time, then use this
 *  function to install them into the window.
 */
void
gnc_main_window_manual_merge_actions (GncMainWindow *window,
                                      const gchar *group_name,
                                      GtkActionGroup *group,
                                      guint merge_id)
{
    GncMainWindowPrivate *priv;
    MergedActionEntry *entry;

    g_return_if_fail (GNC_IS_MAIN_WINDOW (window));
    g_return_if_fail (group_name != NULL);
    g_return_if_fail (GTK_IS_ACTION_GROUP(group));
    g_return_if_fail (merge_id > 0);

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    entry = g_new0 (MergedActionEntry, 1);
    entry->action_group = group;
    entry->merge_id = merge_id;
    gtk_ui_manager_ensure_update (window->ui_merge);
    g_hash_table_insert (priv->merged_actions_table, g_strdup (group_name), entry);
}


/*  Add a set of actions to the specified window.  This function
 *  should not need to be called directly by plugin implementors.
 *  Correctly assigning values to the GncPluginClass fields during
 *  plugin initialization will cause this routine to be automatically
 *  called.
 */
void
gnc_main_window_merge_actions (GncMainWindow *window,
                               const gchar *group_name,
                               GtkActionEntry *actions,
                               guint n_actions,
                               GtkToggleActionEntry *toggle_actions,
                               guint n_toggle_actions,
                               const gchar *filename,
                               gpointer user_data)
{
    GncMainWindowPrivate *priv;
    GncMainWindowActionData *data;
    MergedActionEntry *entry;
    GError *error = NULL;
    gchar *pathname;

    g_return_if_fail (GNC_IS_MAIN_WINDOW (window));
    g_return_if_fail (group_name != NULL);
    g_return_if_fail (actions != NULL);
    g_return_if_fail (n_actions > 0);
    g_return_if_fail (filename != NULL);

    pathname = gnc_filepath_locate_ui_file (filename);
    if (pathname == NULL)
        return;

    data = g_new0 (GncMainWindowActionData, 1);
    data->window = window;
    data->data = user_data;

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    entry = g_new0 (MergedActionEntry, 1);
    entry->action_group = gtk_action_group_new (group_name);
    gnc_gtk_action_group_set_translation_domain (entry->action_group, GETTEXT_PACKAGE);
    gtk_action_group_add_actions (entry->action_group, actions, n_actions, data);
    if (toggle_actions != NULL && n_toggle_actions > 0)
    {
        gtk_action_group_add_toggle_actions (entry->action_group,
                                             toggle_actions, n_toggle_actions,
                                             data);
    }
    gtk_ui_manager_insert_action_group (window->ui_merge, entry->action_group, 0);
    entry->merge_id = gtk_ui_manager_add_ui_from_file (window->ui_merge, pathname, &error);
    g_assert(entry->merge_id || error);
    if (entry->merge_id)
    {
        gtk_ui_manager_ensure_update (window->ui_merge);
        g_hash_table_insert (priv->merged_actions_table, g_strdup (group_name), entry);
    }
    else
    {
        g_critical("Failed to load ui file.\n  Filename %s\n  Error %s",
                   filename, error->message);
        g_error_free(error);
        g_free(entry);
    }
    g_free(pathname);
}


/*  Remove a set of actions from the specified window.  This function
 *  should not need to be called directly by plugin implementors.  It
 *  will automatically be called when a plugin is removed from a
 *  window.
 */
void
gnc_main_window_unmerge_actions (GncMainWindow *window,
                                 const gchar *group_name)
{
    GncMainWindowPrivate *priv;
    MergedActionEntry *entry;

    g_return_if_fail (GNC_IS_MAIN_WINDOW (window));
    g_return_if_fail (group_name != NULL);

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    if (priv->merged_actions_table == NULL)
        return;
    entry = g_hash_table_lookup (priv->merged_actions_table, group_name);

    if (entry == NULL)
        return;

    gtk_ui_manager_remove_action_group (window->ui_merge, entry->action_group);
    gtk_ui_manager_remove_ui (window->ui_merge, entry->merge_id);
    gtk_ui_manager_ensure_update (window->ui_merge);

    g_hash_table_remove (priv->merged_actions_table, group_name);
}


/*  Force a full update of the user interface for the specified
 *  window.  This can be an expensive function, but is needed because
 *  the gtk ui manager doesn't always seem to update properly when
 *  actions are changed.
 */
void
gnc_main_window_actions_updated (GncMainWindow *window)
{
    GtkActionGroup *force;

    g_return_if_fail (GNC_IS_MAIN_WINDOW (window));

    /* Unfortunately gtk_ui_manager_ensure_update doesn't work
     * here.  Force a full update by adding and removing an empty
     * action group.
     */
    force = gtk_action_group_new("force_update");
    gtk_ui_manager_insert_action_group (window->ui_merge, force, 0);
    gtk_ui_manager_ensure_update (window->ui_merge);
    gtk_ui_manager_remove_action_group (window->ui_merge, force);
    g_object_unref(force);
}


GtkAction *
gnc_main_window_find_action (GncMainWindow *window, const gchar *name)
{
    GtkAction *action = NULL;
    const GList *groups, *tmp;

    groups = gtk_ui_manager_get_action_groups(window->ui_merge);
    for (tmp = groups; tmp; tmp = g_list_next(tmp))
    {
        action = gtk_action_group_get_action(GTK_ACTION_GROUP(tmp->data), name);
        if (action)
            break;
    }
    return action;
}


/*  Retrieve a specific set of user interface actions from a window.
 *  This function can be used to get an group of action to be
 *  manipulated when the front page of a window has changed.
 */
GtkActionGroup *
gnc_main_window_get_action_group (GncMainWindow *window,
                                  const gchar *group_name)
{
    GncMainWindowPrivate *priv;
    MergedActionEntry *entry;

    g_return_val_if_fail (GNC_IS_MAIN_WINDOW (window), NULL);
    g_return_val_if_fail (group_name != NULL, NULL);

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    if (priv->merged_actions_table == NULL)
        return NULL;
    entry = g_hash_table_lookup (priv->merged_actions_table, group_name);

    if (entry == NULL)
        return NULL;

    return entry->action_group;
}

static void
gnc_main_window_update_tab_position (gpointer prefs, gchar *pref, gpointer user_data)
{
    GncMainWindow *window;
    GtkPositionType position = GTK_POS_TOP;
    GncMainWindowPrivate *priv;

    window = GNC_MAIN_WINDOW(user_data);

    ENTER ("window %p", window);
    if (gnc_prefs_get_bool (GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_POSITION_BOTTOM))
        position = GTK_POS_BOTTOM;
    else if (gnc_prefs_get_bool (GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_POSITION_LEFT))
        position = GTK_POS_LEFT;
    else if (gnc_prefs_get_bool (GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_POSITION_RIGHT))
        position = GTK_POS_RIGHT;

    priv = GNC_MAIN_WINDOW_GET_PRIVATE (window);
    gtk_notebook_set_tab_pos (GTK_NOTEBOOK (priv->notebook), position);

    LEAVE ("");
}

/*
 * Based on code from Epiphany (src/ephy-window.c)
 */
static void
gnc_main_window_update_edit_actions_sensitivity (GncMainWindow *window, gboolean hide)
{
    GncMainWindowPrivate *priv;
    GncPluginPage *page;
    GtkWidget *widget = gtk_window_get_focus (GTK_WINDOW (window));
    GtkAction *action;
    gboolean can_copy = FALSE, can_cut = FALSE, can_paste = FALSE;

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    page = priv->current_page;
    if (page && GNC_PLUGIN_PAGE_GET_CLASS(page)->update_edit_menu_actions)
    {
        (GNC_PLUGIN_PAGE_GET_CLASS(page)->update_edit_menu_actions)(page, hide);
        return;
    }

    if (GTK_IS_EDITABLE (widget))
    {
        gboolean has_selection;

        has_selection = gtk_editable_get_selection_bounds
                        (GTK_EDITABLE (widget), NULL, NULL);

        can_copy = has_selection;
        can_cut = has_selection;
        can_paste = TRUE;
    }
    else if (GTK_IS_TEXT_VIEW (widget))
    {
        gboolean has_selection;
        GtkTextBuffer *text_buffer;

        text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW(widget));
        has_selection = gtk_text_buffer_get_selection_bounds
                        (text_buffer, NULL, NULL);

        can_copy = has_selection;
        can_cut = has_selection;
        can_paste = TRUE;
    }
    else
    {
#ifdef ORIGINAL_EPIPHANY_CODE
        /* For now we assume all actions are possible */
        can_copy = can_cut = can_paste = TRUE;
#else
        /* If its not a GtkEditable, we don't know what to do
         * with it. */
        can_copy = can_cut = can_paste = FALSE;
#endif
    }

    action = gnc_main_window_find_action (window, "EditCopyAction");
    gtk_action_set_sensitive (action, can_copy);
    gtk_action_set_visible (action, !hide || can_copy);
    action = gnc_main_window_find_action (window, "EditCutAction");
    gtk_action_set_sensitive (action, can_cut);
    gtk_action_set_visible (action, !hide || can_cut);
    action = gnc_main_window_find_action (window, "EditPasteAction");
    gtk_action_set_sensitive (action, can_paste);
    gtk_action_set_visible (action,  !hide || can_paste);
}

static void
gnc_main_window_enable_edit_actions_sensitivity (GncMainWindow *window)
{
    GtkAction *action;

    action = gnc_main_window_find_action (window, "EditCopyAction");
    gtk_action_set_sensitive (action, TRUE);
    gtk_action_set_visible (action, TRUE);
    action = gnc_main_window_find_action (window, "EditCutAction");
    gtk_action_set_sensitive (action, TRUE);
    gtk_action_set_visible (action, TRUE);
    action = gnc_main_window_find_action (window, "EditPasteAction");
    gtk_action_set_sensitive (action, TRUE);
    gtk_action_set_visible (action, TRUE);
}

static void
gnc_main_window_edit_menu_show_cb (GtkWidget *menu,
                                   GncMainWindow *window)
{
    gnc_main_window_update_edit_actions_sensitivity (window, FALSE);
}

static void
gnc_main_window_edit_menu_hide_cb (GtkWidget *menu,
                                   GncMainWindow *window)
{
    gnc_main_window_enable_edit_actions_sensitivity (window);
}

static void
gnc_main_window_init_menu_updaters (GncMainWindow *window)
{
    GtkWidget *edit_menu_item, *edit_menu;

    edit_menu_item = gtk_ui_manager_get_widget
                     (window->ui_merge, "/menubar/Edit");
    edit_menu = gtk_menu_item_get_submenu (GTK_MENU_ITEM (edit_menu_item));

    g_signal_connect (edit_menu, "show",
                      G_CALLBACK (gnc_main_window_edit_menu_show_cb), window);
    g_signal_connect (edit_menu, "hide",
                      G_CALLBACK (gnc_main_window_edit_menu_hide_cb), window);
}

/* CS: This callback functions will set the statusbar text to the
 * "tooltip" property of the currently selected GtkAction.
 *
 * This code is directly copied from gtk+/test/testmerge.c.
 * Thanks to (L)GPL! */
typedef struct _ActionStatus ActionStatus;
struct _ActionStatus
{
    GtkAction *action;
    GtkWidget *statusbar;
};

static void
action_status_destroy (gpointer data)
{
    ActionStatus *action_status = data;

    g_object_unref (action_status->action);
    g_object_unref (action_status->statusbar);

    g_free (action_status);
}

static void
set_tip (GtkWidget *widget)
{
    ActionStatus *data;
    gchar *tooltip;

    data = g_object_get_data (G_OBJECT (widget), "action-status");

    if (data)
    {
        g_object_get (data->action, "tooltip", &tooltip, NULL);

        gtk_statusbar_push (GTK_STATUSBAR (data->statusbar), 0,
                            tooltip ? tooltip : "");

        g_free (tooltip);
    }
}

static void
unset_tip (GtkWidget *widget)
{
    ActionStatus *data;

    data = g_object_get_data (G_OBJECT (widget), "action-status");

    if (data)
        gtk_statusbar_pop (GTK_STATUSBAR (data->statusbar), 0);
}

static void
connect_proxy (GtkUIManager *merge,
               GtkAction    *action,
               GtkWidget    *proxy,
               GtkWidget    *statusbar)
{
    if (GTK_IS_MENU_ITEM (proxy))
    {
        ActionStatus *data;

        data = g_object_get_data (G_OBJECT (proxy), "action-status");
        if (data)
        {
            g_object_unref (data->action);
            g_object_unref (data->statusbar);

            data->action = g_object_ref (action);
            data->statusbar = g_object_ref (statusbar);
        }
        else
        {
            data = g_new0 (ActionStatus, 1);

            data->action = g_object_ref (action);
            data->statusbar = g_object_ref (statusbar);

            g_object_set_data_full (G_OBJECT (proxy), "action-status",
                                    data, action_status_destroy);

            g_signal_connect (proxy, "select",  G_CALLBACK (set_tip), NULL);
            g_signal_connect (proxy, "deselect", G_CALLBACK (unset_tip), NULL);
        }
    }
}
/* CS: end copied code from gtk+/test/testmerge.c */

static void
gnc_main_window_window_menu (GncMainWindow *window)
{
    guint merge_id;
#ifdef MAC_INTEGRATION
    gchar *filename = gnc_filepath_locate_ui_file("gnc-windows-menu-ui-quartz.xml");
#else
    gchar *filename = gnc_filepath_locate_ui_file("gnc-windows-menu-ui.xml");
    GncMainWindowPrivate *priv;
#endif
    GError *error = NULL;
    g_assert(filename);
    merge_id = gtk_ui_manager_add_ui_from_file(window->ui_merge, filename,
               &error);
    g_free(filename);
    g_assert(merge_id);
#ifndef MAC_INTEGRATION
    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    gtk_action_group_add_radio_actions (priv->action_group,
                                        radio_entries, n_radio_entries,
                                        0,
                                        G_CALLBACK(gnc_main_window_cmd_window_raise),
                                        window);
#endif
};

static void
gnc_main_window_setup_window (GncMainWindow *window)
{
    GncMainWindowPrivate *priv;
    GtkWidget *main_vbox;
    guint merge_id;
    GncPluginManager *manager;
    GList *plugins;
    GError *error = NULL;
    gchar *filename;

    ENTER(" ");

    /* Catch window manager delete signal */
    g_signal_connect (G_OBJECT (window), "delete-event",
                      G_CALLBACK (gnc_main_window_delete_event), window);

    /* Create widgets and add them to the window */
    main_vbox = gtk_vbox_new (FALSE, 0);
    gtk_widget_show (main_vbox);
    gtk_container_add (GTK_CONTAINER (window), main_vbox);

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    priv->menu_dock = gtk_vbox_new (FALSE, 0);
    gtk_widget_show (priv->menu_dock);
    gtk_box_pack_start (GTK_BOX (main_vbox), priv->menu_dock,
                        FALSE, TRUE, 0);

    priv->notebook = gtk_notebook_new ();
    g_object_set(G_OBJECT(priv->notebook),
                 "scrollable", TRUE,
                 "enable-popup", TRUE,
                 (char *)NULL);
    gtk_widget_show (priv->notebook);
    g_signal_connect (G_OBJECT (priv->notebook), "switch-page",
                      G_CALLBACK (gnc_main_window_switch_page), window);
    g_signal_connect (G_OBJECT (priv->notebook), "page-reordered",
                      G_CALLBACK (gnc_main_window_page_reordered), window);
    gtk_box_pack_start (GTK_BOX (main_vbox), priv->notebook,
                        TRUE, TRUE, 0);

    priv->statusbar = gtk_statusbar_new ();
    gtk_widget_show (priv->statusbar);
    gtk_box_pack_start (GTK_BOX (main_vbox), priv->statusbar,
                        FALSE, TRUE, 0);
    gtk_statusbar_set_has_resize_grip( GTK_STATUSBAR(priv->statusbar), TRUE );

    priv->progressbar = gtk_progress_bar_new ();
    gtk_progress_bar_set_text(GTK_PROGRESS_BAR(priv->progressbar), " ");
    gtk_widget_show (priv->progressbar);
    gtk_box_pack_start (GTK_BOX (priv->statusbar), priv->progressbar,
                        FALSE, TRUE, 0);
    gtk_progress_bar_set_pulse_step(GTK_PROGRESS_BAR(priv->progressbar),
                                    0.01);

    window->ui_merge = gtk_ui_manager_new ();

    /* Create menu and toolbar information */
    priv->action_group = gtk_action_group_new ("MainWindowActions");
    gnc_gtk_action_group_set_translation_domain (priv->action_group, GETTEXT_PACKAGE);
    gtk_action_group_add_actions (priv->action_group, gnc_menu_actions,
                                  gnc_menu_n_actions, window);
    gtk_action_group_add_toggle_actions (priv->action_group,
                                         toggle_actions, n_toggle_actions,
                                         window);
    gnc_plugin_update_actions(priv->action_group,
                              initially_insensitive_actions,
                              "sensitive", FALSE);
    gnc_plugin_update_actions(priv->action_group,
                              always_insensitive_actions,
                              "sensitive", FALSE);
    gnc_plugin_update_actions(priv->action_group,
                              always_hidden_actions,
                              "visible", FALSE);
    gnc_plugin_set_important_actions (priv->action_group,
                                      gnc_menu_important_actions);
    gtk_ui_manager_insert_action_group (window->ui_merge, priv->action_group, 0);

    g_signal_connect (G_OBJECT (window->ui_merge), "add_widget",
                      G_CALLBACK (gnc_main_window_add_widget), window);
    /* Use the "connect-proxy" signal for tooltip display in the
       status bar */
    g_signal_connect (G_OBJECT (window->ui_merge), "connect-proxy",
                      G_CALLBACK (connect_proxy), priv->statusbar);

    filename = gnc_filepath_locate_ui_file("gnc-main-window-ui.xml");

    /* Can't do much without a ui. */
    g_assert (filename);

    merge_id = gtk_ui_manager_add_ui_from_file (window->ui_merge,
               filename, &error);
    g_assert(merge_id || error);
    if (merge_id)
    {
        gtk_window_add_accel_group (GTK_WINDOW (window),
                                    gtk_ui_manager_get_accel_group(window->ui_merge));
        gtk_ui_manager_ensure_update (window->ui_merge);
    }
    else
    {
        g_critical("Failed to load ui file.\n  Filename %s\n  Error %s",
                   filename, error->message);
        g_error_free(error);
        g_assert(merge_id != 0);
    }
    g_free(filename);
    gnc_main_window_window_menu(window);
    gnc_prefs_register_cb (GNC_PREFS_GROUP_GENERAL,
                           GNC_PREF_TAB_POSITION_TOP,
                           gnc_main_window_update_tab_position,
                           window);
    gnc_prefs_register_cb (GNC_PREFS_GROUP_GENERAL,
                           GNC_PREF_TAB_POSITION_BOTTOM,
                           gnc_main_window_update_tab_position,
                           window);
    gnc_prefs_register_cb (GNC_PREFS_GROUP_GENERAL,
                           GNC_PREF_TAB_POSITION_LEFT,
                           gnc_main_window_update_tab_position,
                           window);
    gnc_prefs_register_cb (GNC_PREFS_GROUP_GENERAL,
                           GNC_PREF_TAB_POSITION_RIGHT,
                           gnc_main_window_update_tab_position,
                           window);
    gnc_main_window_update_tab_position(NULL, NULL, window);

    gnc_main_window_init_menu_updaters(window);

    /* Testing */
    /* Now update the "eXtensions" menu */
    if (!gnc_prefs_is_extra_enabled())
    {
        GtkAction*  action;

        action = gtk_action_group_get_action(priv->action_group,
                                             "ExtensionsAction");
        gtk_action_set_visible(action, FALSE);
    }

    /* GncPluginManager stuff */
    manager = gnc_plugin_manager_get ();
    plugins = gnc_plugin_manager_get_plugins (manager);
    g_list_foreach (plugins, gnc_main_window_add_plugin, window);
    g_list_free (plugins);

    g_signal_connect (G_OBJECT (manager), "plugin-added",
                      G_CALLBACK (gnc_main_window_plugin_added), window);
    g_signal_connect (G_OBJECT (manager), "plugin-removed",
                      G_CALLBACK (gnc_main_window_plugin_removed), window);

    LEAVE(" ");
}

#ifdef MAC_INTEGRATION
/* Event handlers for the shutdown process.  Gnc_quartz_shutdown is
 * connected to NSApplicationWillTerminate, the last chance to do
 * anything before quitting. The problem is that it's launched from a
 * CFRunLoop, not a g_main_loop, and if we call anything that would
 * affect the main_loop we get an assert that we're in a subidiary
 * loop.
 */
static void
gnc_quartz_shutdown (GtkosxApplication *theApp, gpointer data)
{
    /* Do Nothing. It's too late. */
}
/* Should quit responds to NSApplicationBlockTermination; returning
 * TRUE means "don't terminate", FALSE means "do terminate". If we
 * decide that it's OK to terminate, then we queue a gnc_shutdown for
 * the next idle time (because we're not running in the main loop) and
 * then tell the OS not to terminate. That gives the gnc_shutdown an
 * opportunity to shut down.
 */
static gboolean
gnc_quartz_should_quit (GtkosxApplication *theApp, GncMainWindow *window)
{
    QofSession *session;
    gboolean needs_save;

    if (!gnc_main_window_all_finish_pending() ||
            gnc_file_save_in_progress())
    {
        return TRUE;
    }
    session = gnc_get_current_session();
    needs_save = qof_book_session_not_saved(qof_session_get_book(session)) &&
                 !gnc_file_save_in_progress();
    if (needs_save && gnc_main_window_prompt_for_save(GTK_WIDGET(window)))
        return TRUE;

    g_timeout_add(250, gnc_main_window_timed_quit, NULL);
    return TRUE;
}

static void
gnc_quartz_set_menu(GncMainWindow* window)
{
    GtkosxApplication *theApp = g_object_new(GTKOSX_TYPE_APPLICATION, NULL);
    GtkWidget       *menu;
    GtkWidget       *item;

    menu = gtk_ui_manager_get_widget (window->ui_merge, "/menubar");
    if (GTK_IS_MENU_ITEM (menu))
        menu = gtk_menu_item_get_submenu (GTK_MENU_ITEM (menu));
    gtk_widget_hide(menu);
    gtkosx_application_set_menu_bar (theApp, GTK_MENU_SHELL (menu));

    item = gtk_ui_manager_get_widget (window->ui_merge,
                                      "/menubar/File/FileQuit");
    if (GTK_IS_MENU_ITEM (item))
        gtk_widget_hide (GTK_WIDGET (item));

    item = gtk_ui_manager_get_widget (window->ui_merge,
                                      "/menubar/Help/HelpAbout");
    if (GTK_IS_MENU_ITEM (item))
    {
        gtkosx_application_insert_app_menu_item (theApp, GTK_WIDGET (item), 0);
    }

    item = gtk_ui_manager_get_widget (window->ui_merge,
                                      "/menubar/Edit/EditPreferences");
    if (GTK_IS_MENU_ITEM (item))
    {
        gtkosx_application_insert_app_menu_item (theApp,
                gtk_separator_menu_item_new (), 1);
        gtkosx_application_insert_app_menu_item (theApp, GTK_WIDGET (item), 2);
    }

    item = gtk_ui_manager_get_widget (window->ui_merge,
                                      "/menubar/Help");
    gtkosx_application_set_help_menu(theApp, GTK_MENU_ITEM(item));
    item = gtk_ui_manager_get_widget (window->ui_merge,
                                      "/menubar/Windows");
    gtkosx_application_set_window_menu(theApp, GTK_MENU_ITEM(item));
    g_signal_connect(theApp, "NSApplicationBlockTermination",
                     G_CALLBACK(gnc_quartz_should_quit), window);
    gtkosx_application_set_use_quartz_accelerators (theApp, FALSE);
    g_object_unref (theApp);

}
#endif //MAC_INTEGRATION

/* Callbacks */
static void
gnc_main_window_add_widget (GtkUIManager *merge,
                            GtkWidget *widget,
                            GncMainWindow *window)
{
    GncMainWindowPrivate *priv;

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    if (GTK_IS_TOOLBAR (widget))
    {
        priv->toolbar = widget;
    }

    gtk_box_pack_start (GTK_BOX (priv->menu_dock), widget, FALSE, FALSE, 0);
    gtk_widget_show (widget);
}

/** Should a summary bar be visible in this window?  In order to
 *  prevent synchronization issues, the "ViewSummaryBar"
 *  GtkToggleAction is the sole source of information for whether or
 *  not any summary bar should be visible in a window.
 *
 *  @param window A pointer to the window in question.
 *
 *  @param action If known, a pointer to the "ViewSummaryBar"
 *  GtkToggleAction.  If NULL, the function will look up this action.
 *
 *  @return TRUE if the summarybar should be visible.
 */
static gboolean
gnc_main_window_show_summarybar (GncMainWindow *window, GtkAction *action)
{
    GncMainWindowPrivate *priv;

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    if (action == NULL)
        action = gtk_action_group_get_action(priv->action_group,
                                             "ViewSummaryAction");
    if (action == NULL)
        return TRUE;
    return gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(action));
}

/** This function is invoked when the GtkNotebook switches pages.  It
 *  is responsible for updating the rest of the window contents
 *  outside of the notebook.  I.E. Updating the user interface, the
 *  summary bar, etc.  This function also emits the "page_changed"
 *  signal from the window so that any plugin can also learn about the
 *  fact that the page has changed.
 *
 *  @internal
 */
static void
gnc_main_window_switch_page (GtkNotebook *notebook,
                             gpointer *notebook_page,
                             gint pos,
                             GncMainWindow *window)
{
    GncMainWindowPrivate *priv;
    GtkWidget *child;
    GncPluginPage *page;
    gboolean immutable, visible;

    ENTER("Notebook %p, page, %p, index %d, window %p",
          notebook, notebook_page, pos, window);
    g_return_if_fail (GNC_IS_MAIN_WINDOW (window));

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    if (priv->current_page != NULL)
    {
        page = priv->current_page;
        gnc_plugin_page_unmerge_actions (page, window->ui_merge);
        gnc_plugin_page_unselected (page);
    }

    child = gtk_notebook_get_nth_page (notebook, pos);
    if (child)
    {
        page = g_object_get_data (G_OBJECT (child), PLUGIN_PAGE_LABEL);
    }
    else
    {
        page = NULL;
    }

    priv->current_page = page;

    if (page != NULL)
    {
        /* Update the user interface (e.g. menus and toolbars */
        gnc_plugin_page_merge_actions (page, window->ui_merge);
        visible = gnc_main_window_show_summarybar(window, NULL);
        gnc_plugin_page_show_summarybar (page, visible);

        /* Allow page specific actions */
        gnc_plugin_page_selected (page);
        gnc_window_update_status (GNC_WINDOW(window), page);

        /* Update the page reference info */
        priv->usage_order = g_list_remove (priv->usage_order, page);
        priv->usage_order = g_list_prepend (priv->usage_order, page);
    }

    gnc_plugin_update_actions(priv->action_group,
                              multiple_page_actions,
                              "sensitive",
                              g_list_length(priv->installed_pages) > 1);

    gnc_main_window_update_title(window);
#ifndef MAC_INTEGRATION
    gnc_main_window_update_menu_item(window);
#endif
    g_signal_emit (window, main_window_signals[PAGE_CHANGED], 0, page);
    LEAVE(" ");
}

/** This function is invoked when a GtkNotebook tab gets reordered by
 *  drag and drop. It adjusts the list installed_pages to reflect the new
 *  ordering so that GnuCash saves and restores the tabs correctly.
 *
 *  @internal
 */
static void
gnc_main_window_page_reordered (GtkNotebook *notebook,
                                GtkWidget *child,
                                guint pos,
                                GncMainWindow *window)
{
    GncMainWindowPrivate *priv;
    GncPluginPage *page;
    GList *old_link;

    ENTER("Notebook %p, child %p, index %d, window %p",
          notebook, child, pos, window);
    g_return_if_fail (GNC_IS_MAIN_WINDOW (window));

    if (!child) return;

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    page = g_object_get_data (G_OBJECT (child), PLUGIN_PAGE_LABEL);
    if (!page) return;

    old_link = g_list_find (priv->installed_pages, page);
    if (!old_link) return;

    priv->installed_pages = g_list_delete_link (priv->installed_pages,
                            old_link);
    priv->installed_pages = g_list_insert (priv->installed_pages,
                                           page, pos);

    LEAVE(" ");
}

static void
gnc_main_window_plugin_added (GncPlugin *manager,
                              GncPlugin *plugin,
                              GncMainWindow *window)
{
    g_return_if_fail (GNC_IS_MAIN_WINDOW (window));
    g_return_if_fail (GNC_IS_PLUGIN (plugin));

    gnc_plugin_add_to_window (plugin, window, window_type);
}

static void
gnc_main_window_plugin_removed (GncPlugin *manager,
                                GncPlugin *plugin,
                                GncMainWindow *window)
{
    g_return_if_fail (GNC_IS_MAIN_WINDOW (window));
    g_return_if_fail (GNC_IS_PLUGIN (plugin));

    gnc_plugin_remove_from_window (plugin, window, window_type);
}


/* Command callbacks */
static void
gnc_main_window_cmd_page_setup (GtkAction *action,
                                GncMainWindow *window)
{
    GtkWindow *gtk_window;

    g_return_if_fail(GNC_IS_MAIN_WINDOW(window));

    gtk_window = gnc_window_get_gtk_window(GNC_WINDOW(window));
    gnc_ui_page_setup(gtk_window);
}

static void
gnc_book_options_dialog_apply_cb(GNCOptionWin * optionwin,
                                 gpointer user_data)
{
    GNCOptionDB * options = user_data;
    kvp_frame *slots = qof_book_get_slots (gnc_get_current_book ());
    gboolean use_split_action_for_num_before =
        qof_book_use_split_action_for_num_field (gnc_get_current_book ());
    gboolean use_split_action_for_num_after;

    if (!options) return;

    gnc_option_db_commit (options);
    gnc_option_db_save_to_kvp (options, slots, TRUE);
    qof_book_kvp_changed (gnc_get_current_book());
    use_split_action_for_num_after =
        qof_book_use_split_action_for_num_field (gnc_get_current_book ());
    if (use_split_action_for_num_before != use_split_action_for_num_after)
    {
        gnc_book_option_num_field_source_change_cb (use_split_action_for_num_after);
        gnc_gui_refresh_all ();
    }
}

static void
gnc_book_options_dialog_close_cb(GNCOptionWin * optionwin,
                                 gpointer user_data)
{
    GNCOptionDB * options = user_data;

    gnc_options_dialog_destroy(optionwin);
    gnc_option_db_destroy(options);
}

GtkWidget *
gnc_book_options_dialog_cb (gboolean modal, gchar *title)
{
    kvp_frame *slots = qof_book_get_slots (gnc_get_current_book ());
    GNCOptionDB *options;
    GNCOptionWin *optionwin;

    options = gnc_option_db_new_for_type (QOF_ID_BOOK);
    gnc_option_db_load_from_kvp (options, slots);
    gnc_option_db_clean (options);

    optionwin = gnc_options_dialog_new_modal (modal,
                (title ? title : _( "Book Options")));
    gnc_options_dialog_build_contents (optionwin, options);

    gnc_options_dialog_set_book_options_help_cb (optionwin);

    gnc_options_dialog_set_apply_cb (optionwin,
                                     gnc_book_options_dialog_apply_cb,
                                     (gpointer)options);
    gnc_options_dialog_set_close_cb (optionwin,
                                     gnc_book_options_dialog_close_cb,
                                     (gpointer)options);
    if (modal)
        gnc_options_dialog_set_new_book_option_values (options);
    return gnc_options_dialog_widget (optionwin);
}

static void
gnc_main_window_cmd_file_properties (GtkAction *action, GncMainWindow *window)
{
    gnc_book_options_dialog_cb (FALSE, NULL);
}

static void
gnc_main_window_cmd_file_close (GtkAction *action, GncMainWindow *window)
{
    GncMainWindowPrivate *priv;
    GncPluginPage *page;

    g_return_if_fail(GNC_IS_MAIN_WINDOW(window));

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    page = priv->current_page;
    gnc_main_window_close_page(page);
}

static void
gnc_main_window_cmd_file_quit (GtkAction *action, GncMainWindow *window)
{
    if (!gnc_main_window_all_finish_pending())
        return;

    gnc_main_window_quit(window);
}

static void
gnc_main_window_cmd_edit_cut (GtkAction *action, GncMainWindow *window)
{
    GtkWidget *widget = gtk_window_get_focus (GTK_WINDOW (window));
    GtkTextBuffer *text_buffer;
    GtkClipboard *clipboard;
    gboolean editable;

    if (GTK_IS_EDITABLE (widget))
    {
        gtk_editable_cut_clipboard (GTK_EDITABLE (widget));
    }
    else if (GTK_IS_TEXT_VIEW (widget))
    {
        text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW(widget));
        clipboard = gtk_widget_get_clipboard (GTK_WIDGET(text_buffer),
                                              GDK_SELECTION_CLIPBOARD);
        editable = gtk_text_view_get_editable (GTK_TEXT_VIEW (widget));
        gtk_text_buffer_cut_clipboard (text_buffer, clipboard, editable);
    }
}

static void
gnc_main_window_cmd_edit_copy (GtkAction *action, GncMainWindow *window)
{
    GtkWidget *widget = gtk_window_get_focus (GTK_WINDOW (window));
    GtkTextBuffer *text_buffer;
    GtkClipboard *clipboard;

    if (GTK_IS_EDITABLE (widget))
    {
        gtk_editable_copy_clipboard (GTK_EDITABLE (widget));
    }
    else if (GTK_IS_TEXT_VIEW (widget))
    {
        text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW(widget));
        clipboard = gtk_widget_get_clipboard (GTK_WIDGET(text_buffer),
                                              GDK_SELECTION_CLIPBOARD);
        gtk_text_buffer_copy_clipboard (text_buffer, clipboard);
    }
}

static void
gnc_main_window_cmd_edit_paste (GtkAction *action, GncMainWindow *window)
{
    GtkWidget *widget = gtk_window_get_focus (GTK_WINDOW (window));
    GtkTextBuffer *text_buffer;
    GtkClipboard *clipboard;

    if (GTK_IS_EDITABLE (widget))
    {
        gtk_editable_paste_clipboard (GTK_EDITABLE (widget));
    }
    else if (GTK_IS_TEXT_VIEW (widget))
    {
        text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW(widget));
        clipboard = gtk_widget_get_clipboard (GTK_WIDGET(text_buffer),
                                              GDK_SELECTION_CLIPBOARD);
        gtk_text_buffer_paste_clipboard (text_buffer, clipboard, NULL, FALSE);
    }
}

static void
gnc_main_window_cmd_edit_preferences (GtkAction *action, GncMainWindow *window)
{
    gnc_preferences_dialog ();
}

static void
gnc_main_window_cmd_view_refresh (GtkAction *action, GncMainWindow *window)
{
}

static void
gnc_main_window_cmd_actions_reset_warnings (GtkAction *action, GncMainWindow *window)
{
    gnc_reset_warnings_dialog(GTK_WINDOW(window));
}

static void
gnc_main_window_cmd_actions_rename_page (GtkAction *action, GncMainWindow *window)
{
    GncMainWindowPrivate *priv;
    GncPluginPage *page;
    GtkWidget *label, *entry;

    ENTER(" ");
    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    page = priv->current_page;
    if (!page)
    {
        LEAVE("No current page");
        return;
    }

    if (!main_window_find_tab_items(window, page, &label, &entry))
    {
        LEAVE("can't find required widgets");
        return;
    }

    gtk_entry_set_text(GTK_ENTRY(entry), gtk_label_get_text(GTK_LABEL(label)));
    gtk_editable_select_region(GTK_EDITABLE(entry), 0, -1);
    gtk_widget_hide(label);
    gtk_widget_show(entry);
    gtk_widget_grab_focus(entry);
    LEAVE("opened for editing");
}

static void
gnc_main_window_cmd_view_toolbar (GtkAction *action, GncMainWindow *window)
{
    GncMainWindowPrivate *priv;

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    if (gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(action)))
    {
        gtk_widget_show (priv->toolbar);
    }
    else
    {
        gtk_widget_hide (priv->toolbar);
    }
}

static void
gnc_main_window_cmd_view_summary (GtkAction *action, GncMainWindow *window)
{
    GncMainWindowPrivate *priv;
    GList *item;
    gboolean visible;

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    visible = gnc_main_window_show_summarybar(window, action);
    for (item = priv->installed_pages; item; item = g_list_next(item))
    {
        gnc_plugin_page_show_summarybar(item->data, visible);
    }
}

static void
gnc_main_window_cmd_view_statusbar (GtkAction *action, GncMainWindow *window)
{
    GncMainWindowPrivate *priv;

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    if (gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(action)))
    {
        gtk_widget_show (priv->statusbar);
    }
    else
    {
        gtk_widget_hide (priv->statusbar);
    }
}

static void
gnc_main_window_cmd_window_new (GtkAction *action, GncMainWindow *window)
{
    GncMainWindow *new_window;

    /* Create the new window */
    ENTER(" ");
    new_window = gnc_main_window_new ();
    gtk_widget_show(GTK_WIDGET(new_window));
    LEAVE(" ");
}

static void
gnc_main_window_cmd_window_move_page (GtkAction *action, GncMainWindow *window)
{
    GncMainWindowPrivate *priv;
    GncMainWindow *new_window;
    GncPluginPage *page;
    GtkNotebook *notebook;
    GtkWidget *tab_widget, *menu_widget;

    ENTER("action %p,window %p", action, window);

    /* Setup */
    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    page = priv->current_page;
    if (!page)
    {
        LEAVE("invalid page");
        return;
    }
    if (!page->notebook_page)
    {
        LEAVE("invalid notebook_page");
        return;
    }

    notebook = GTK_NOTEBOOK (priv->notebook);
    tab_widget = gtk_notebook_get_tab_label (notebook, page->notebook_page);
    menu_widget = gtk_notebook_get_menu_label (notebook, page->notebook_page);

    /* Ref the page components, then remove it from its old window */
    g_object_ref(page);
    g_object_ref(tab_widget);
    g_object_ref(menu_widget);
    g_object_ref(page->notebook_page);
    gnc_main_window_disconnect(window, page);

    /* Create the new window */
    new_window = gnc_main_window_new ();
    gtk_widget_show(GTK_WIDGET(new_window));

    /* Now add the page to the new window */
    gnc_main_window_connect (new_window, page, tab_widget, menu_widget);

    /* Unref the page components now that we're done */
    g_object_unref(page->notebook_page);
    g_object_unref(menu_widget);
    g_object_unref(tab_widget);
    g_object_unref(page);

    /* just a little debugging. :-) */
    DEBUG("Moved page %p from window %p to new window %p",
          page, window, new_window);
    DEBUG("Old window current is %p, new window current is %p",
          priv->current_page, priv->current_page);

    LEAVE("page moved");
}

#ifndef MAC_INTEGRATION
static void
gnc_main_window_cmd_window_raise (GtkAction *action,
                                  GtkRadioAction *current,
                                  GncMainWindow *old_window)
{
    GncMainWindow *new_window;
    gint value;

    g_return_if_fail(GTK_IS_ACTION(action));
    g_return_if_fail(GTK_IS_RADIO_ACTION(current));
    g_return_if_fail(GNC_IS_MAIN_WINDOW(old_window));

    ENTER("action %p, current %p, window %p", action, current, old_window);
    value = gtk_radio_action_get_current_value(current);
    new_window = g_list_nth_data(active_windows, value);
    gtk_window_present(GTK_WINDOW(new_window));
    /* revert the change in the radio group
     * impossible while handling "changed" (G_SIGNAL_NO_RECURSE) */
    g_idle_add((GSourceFunc)gnc_main_window_update_radio_button, old_window);
    LEAVE(" ");
}
#endif /* !MAC_INTEGRATION */

static void
gnc_main_window_cmd_help_tutorial (GtkAction *action, GncMainWindow *window)
{
    gnc_gnome_help (HF_GUIDE, NULL);
}

static void
gnc_main_window_cmd_help_contents (GtkAction *action, GncMainWindow *window)
{
    gnc_gnome_help (HF_HELP, NULL);
}

/** This is a helper function to find a data file and suck it into
 *  memory.
 *
 *  @param partial The name of the file relative to the gnucash
 *  specific shared data directory.
 *
 *  @return The text of the file or NULL. The caller is responsible
 *  for freeing this string.
 */
static gchar *
get_file (const gchar *partial)
{
    gchar *filename, *text = NULL;
    gsize length;

    filename = gnc_filepath_locate_doc_file(partial);
    if (filename && g_file_get_contents(filename, &text, &length, NULL))
    {
	if (length)
	{
	    g_free(filename);
	    return text;
	}
        g_free(text);
    }
    g_free (filename);
    return NULL;
}


/** This is a helper function to find a data file, suck it into
 *  memory, and split it into an array of strings.
 *
 *  @param partial The name of the file relative to the gnucash
 *  specific shared data directory.
 *
 *  @return The text of the file as an array of strings, or NULL. The
 *  caller is responsible for freeing all the strings and the array.
 */
static gchar **
get_file_strsplit (const gchar *partial)
{
    gchar *text, **lines;

    text = get_file(partial);
    if (!text)
        return NULL;

    lines = g_strsplit_set(text, "\r\n", -1);
    g_free(text);
    return lines;
}
/** URL activation callback.
 *  Use our own function to activate the URL in the users browser
 *  instead of gtk_show_uri(), which requires gvfs.
 *  Signature described in gtk docs at GtkAboutDialog activate-link signal.
 */

static gboolean
url_signal_cb (GtkAboutDialog *dialog, gchar *uri, gpointer data)
{
    gnc_launch_assoc (uri);
    return TRUE;
}

/** Create and display the "about" dialog for gnucash.
 *
 *  @param action The GtkAction for the "about" menu item.
 *
 *  @param window The main window whose menu item was activated.
 */
static void
gnc_main_window_cmd_help_about (GtkAction *action, GncMainWindow *window)
{
    GncMainWindowPrivate *priv;

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    if (priv->about_dialog == NULL)
    {
	const gchar *fixed_message = _("The GnuCash personal finance manager. "
                                   "The GNU way to manage your money!");
	gchar *copyright = g_strdup_printf(_("© 1997-%s Contributors"),
			                             GNUCASH_BUILD_YEAR);
	gchar **authors = get_file_strsplit("AUTHORS");
	gchar **documenters = get_file_strsplit("DOCUMENTERS");
	gchar *license = get_file("LICENSE");
	gchar *message, *tmp_msg;
	GdkPixbuf *logo = gnc_gnome_get_gdkpixbuf ("gnucash-icon-48x48.png");

#ifdef GNUCASH_SCM
    /* Development version */
    /* Translators: 1st %s is a fixed message, which is translated independently;
                    2nd %s is the scm type (svn/svk/git/bzr);
                    3rd %s is the scm revision number;
                    4th %s is the build date */
	tmp_msg = g_strdup_printf(_("%s\nThis copy was built from %s rev %s on %s."),
				  fixed_message, GNUCASH_SCM, GNUCASH_SCM_REV,
				  GNUCASH_BUILD_DATE);
#else
    /* Translators: 1st %s is a fixed message, which is translated independently;
                    2nd %s is the scm (svn/svk/git/bzr) revision number;
                    3rd %s is the build date */
	tmp_msg = g_strdup_printf(_("%s\nThis copy was built from rev %s on %s."),
				  fixed_message, GNUCASH_SCM_REV,
				  GNUCASH_BUILD_DATE);
#endif
        message = g_strdup_printf ("%s\n\nFinance::Quote: %s", tmp_msg,
                                   gnc_quote_source_fq_version () ? gnc_quote_source_fq_version () : "-");
        g_free (tmp_msg);
	priv->about_dialog = gtk_about_dialog_new ();
	g_object_set (priv->about_dialog,
		      "authors", authors,
		      "documenters", documenters,
		      "comments", message,
		      "copyright", copyright,
		      "license", license,
		      "logo", logo,
		      "name", "GnuCash",
     /* Translators: the following string will be shown in Help->About->Credits
      * Enter your name or that of your team and an email contact for feedback.
      * The string can have multiple rows, so you can also add a list of
      * contributors. */
		      "translator-credits", _("translator_credits"),
		      "version", VERSION,
		      "website", "http://www.gnucash.org",
		      NULL);

	g_free(message);
	g_free(copyright);
	if (license)     g_free(license);
	if (documenters) g_strfreev(documenters);
	if (authors)     g_strfreev(authors);
	g_object_unref (logo);
	g_signal_connect (priv->about_dialog, "activate-link",
			  G_CALLBACK (url_signal_cb), NULL);
	g_signal_connect (priv->about_dialog, "response",
			  G_CALLBACK (gtk_widget_hide), NULL);
	gtk_window_set_transient_for (GTK_WINDOW (priv->about_dialog),
				      GTK_WINDOW (window));
    }
    gtk_dialog_run (GTK_DIALOG (priv->about_dialog));
}


/************************************************************
 *                                                          *
 ************************************************************/

void
gnc_main_window_show_all_windows(void)
{
    GList *window_iter;
#ifdef MAC_INTEGRATION
    GtkosxApplication *theApp = g_object_new(GTKOSX_TYPE_APPLICATION, NULL);
#endif
    for (window_iter = active_windows; window_iter != NULL; window_iter = window_iter->next)
    {
        gtk_widget_show(GTK_WIDGET(window_iter->data));
    }
#ifdef MAC_INTEGRATION
    g_signal_connect(theApp, "NSApplicationWillTerminate",
                     G_CALLBACK(gnc_quartz_shutdown), NULL);
    gtkosx_application_ready(theApp);
    g_object_unref (theApp);
#endif
}

/** Get a pointer to the first active top level window or NULL
 *  if there is none.
 *
 *  @return A pointer to a GtkWindow object. */
GtkWidget *
gnc_ui_get_toplevel (void)
{
    GList *window;

    for (window = active_windows; window; window = window->next)
        if (gtk_window_is_active (GTK_WINDOW (window->data)))
            return window->data;

    return NULL;
}


/** Retrieve the gtk window associated with a main window object.
 *  This function is called via a vector off a generic window
 *  interface.
 *
 *  @param window A pointer to a generic window. */
static GtkWindow *
gnc_main_window_get_gtk_window (GncWindow *window)
{
    g_return_val_if_fail (GNC_IS_MAIN_WINDOW (window), NULL);
    return GTK_WINDOW(window);
}


/** Retrieve the status bar associated with a main window object.
 *  This function is called via a vector off a generic window
 *  interface.
 *
 *  @param window_in A pointer to a generic window. */
static GtkWidget *
gnc_main_window_get_statusbar (GncWindow *window_in)
{
    GncMainWindowPrivate *priv;
    GncMainWindow *window;

    g_return_val_if_fail (GNC_IS_MAIN_WINDOW (window_in), NULL);

    window = GNC_MAIN_WINDOW(window_in);
    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    return priv->statusbar;
}


/** Retrieve the progress bar associated with a main window object.
 *  This function is called via a vector off a generic window
 *  interface.
 *
 *  @param window_in A pointer to a generic window. */
static GtkWidget *
gnc_main_window_get_progressbar (GncWindow *window_in)
{
    GncMainWindowPrivate *priv;
    GncMainWindow *window;

    g_return_val_if_fail (GNC_IS_MAIN_WINDOW (window_in), NULL);

    window = GNC_MAIN_WINDOW(window_in);
    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    return priv->progressbar;
}


static void
gnc_main_window_all_ui_set_sensitive (GncWindow *unused, gboolean sensitive)
{
    GncMainWindow *window;
    GncMainWindowPrivate *priv;
    GList *groupp, *groups, *winp, *tmp;
    GtkWidget *close_button;

    for (winp = active_windows; winp; winp = g_list_next(winp))
    {
        window = winp->data;
        priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

        groups = gtk_ui_manager_get_action_groups(window->ui_merge);
        for (groupp = groups; groupp; groupp = g_list_next(groupp))
        {
            gtk_action_group_set_sensitive(GTK_ACTION_GROUP(groupp->data), sensitive);
        }

        for (tmp = priv->installed_pages; tmp; tmp = g_list_next(tmp))
        {
            close_button = g_object_get_data(tmp->data, PLUGIN_PAGE_CLOSE_BUTTON);
            if (!close_button)
                continue;
            gtk_widget_set_sensitive (close_button, sensitive);
        }
    }
}


/** Initialize the generic window interface for a main window.
 *
 *  @param iface A pointer to the interface data structure to
 *  populate. */
static void
gnc_window_main_window_init (GncWindowIface *iface)
{
    iface->get_gtk_window  = gnc_main_window_get_gtk_window;
    iface->get_statusbar   = gnc_main_window_get_statusbar;
    iface->get_progressbar = gnc_main_window_get_progressbar;
    iface->ui_set_sensitive = gnc_main_window_all_ui_set_sensitive;
}


/*  Set the window where all progressbar updates should occur.  This
 *  is a wrapper around the gnc_window_set_progressbar_window()
 *  function.
 */
void
gnc_main_window_set_progressbar_window (GncMainWindow *window)
{
    GncWindow *gncwin;
    gncwin = GNC_WINDOW(window);
    gnc_window_set_progressbar_window(gncwin);
}


/** Popup a contextual menu.  This function ends up being called when
 *  the user right-clicks in the context of a window, or uses the
 *  keyboard context-menu request key combination (Shift-F10 by
 *  default).
 *
 *  @param page This is the GncPluginPage corresponding to the visible
 *  page.
 *
 *  @param event The event parameter passed to the "button-press"
 *  callback.  May be null if there was no event (aka keyboard
 *  request).
 */
static void
do_popup_menu(GncPluginPage *page, GdkEventButton *event)
{
    GtkUIManager *ui_merge;
    GtkWidget *menu;
    int button, event_time;

    g_return_if_fail(GNC_IS_PLUGIN_PAGE(page));

    ENTER("page %p, event %p", page, event);
    ui_merge = gnc_plugin_page_get_ui_merge(page);
    if (ui_merge == NULL)
    {
        LEAVE("no ui merge");
        return;
    }

    menu = gtk_ui_manager_get_widget(ui_merge, "/MainPopup");
    if (!menu)
    {
        LEAVE("no menu");
        return;
    }

    if (event)
    {
        button = event->button;
        event_time = event->time;
    }
    else
    {
        button = 0;
        event_time = gtk_get_current_event_time ();
    }

    gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, button, event_time);
    LEAVE(" ");
}


/** Callback function invoked when the user requests that Gnucash
 *  popup the contextual menu via the keyboard context-menu request
 *  key combination (Shift-F10 by default).
 *
 *  @param page This is the GncPluginPage corresponding to the visible
 *  page.
 *
 *  @param widget Whatever widget had focus when the user issued the
 *  keyboard context-menu request.
 *
 *  @return Always returns TRUE to indicate that the menu request was
 *  handled.
 */
static gboolean
gnc_main_window_popup_menu_cb (GtkWidget *widget,
                               GncPluginPage *page)
{
    ENTER("widget %p, page %p", widget, page);
    do_popup_menu(page, NULL);
    LEAVE(" ");
    return TRUE;
}


/*  Callback function invoked when the user clicks in the content of
 *  any Gnucash window.  If this was a "right-click" then Gnucash will
 *  popup the contextual menu.
 */
gboolean
gnc_main_window_button_press_cb (GtkWidget *whatever,
                                 GdkEventButton *event,
                                 GncPluginPage *page)
{
    g_return_val_if_fail(GNC_IS_PLUGIN_PAGE(page), FALSE);

    ENTER("widget %p, event %p, page %p", whatever, event, page);
    /* Ignore double-clicks and triple-clicks */
    if (event->button == 3 && event->type == GDK_BUTTON_PRESS)
    {
        do_popup_menu(page, event);
        LEAVE("menu shown");
        return TRUE;
    }

    LEAVE("other click");
    return FALSE;
}


/* CS: Code copied from gtk/gtkactiongroup.c */
static gchar *
dgettext_swapped (const gchar *msgid,
                  const gchar *domainname)
{
    /* CS: Pass this through dgettext if and only if msgid is
       nonempty. */
    return (msgid && *msgid) ? dgettext (domainname, msgid) : (gchar*) msgid;
}

/*
 * This is copied into GnuCash from Gtk in order to fix problems when
 * empty msgids were passed through gettext().
 *
 * See http://bugzilla.gnome.org/show_bug.cgi?id=326200 . If that bug
 * is fixed in the gtk that we can rely open, then
 * gnc_gtk_action_group_set_translation_domain can be replaced by
 * gtk_action_group_set_translation_domain again.
 */
void
gnc_gtk_action_group_set_translation_domain (GtkActionGroup *action_group,
        const gchar    *domain)
{
    g_return_if_fail (GTK_IS_ACTION_GROUP (action_group));

    gtk_action_group_set_translate_func (action_group,
                                         (GtkTranslateFunc)dgettext_swapped,
                                         g_strdup (domain),
                                         g_free);
}
/* CS: End of code copied from gtk/gtkactiongroup.c */

void
gnc_main_window_all_action_set_sensitive (const gchar *action_name,
        gboolean sensitive)
{
    GList *tmp;
    GtkAction *action;

    for (tmp = active_windows; tmp; tmp = g_list_next(tmp))
    {
        action = gnc_main_window_find_action (tmp->data, action_name);
        gtk_action_set_sensitive (action, sensitive);
    }
}

GtkUIManager *gnc_main_window_get_uimanager (GncMainWindow *window)
{
    g_assert(window);
    return window->ui_merge;
}

/** @} */
/** @} */
