/*
 * 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 <glib/gi18n.h>
#include <gtk/gtk.h>
#include <gdk/gdk.h>
#include <gdk/gdkkeysyms.h>
#include "dialog-options.hpp"
#include <libguile.h>

#include <config.h>


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

#include "dialog-preferences.h"
#include "dialog-reset-warnings.h"
#include "dialog-transfer.h"
#include "dialog-utils.h"
#include "engine-helpers.h"
#include "file-utils.h"
#include "gnc-component-manager.h"
#include "dialog-doclink-utils.h"
#include "gnc-engine.h"
#include "gnc-features.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-gtk-utils.h"
#include "gnc-hooks.h"
#include "gnc-icons.h"
#include "gnc-session.h"
#include "gnc-state.h"
#include "gnc-ui.h"
#include "gnc-ui-util.h"
#include <gnc-glib-utils.h>
#include "gnc-uri-utils.h"
#include "gnc-version.h"
#include "gnc-warnings.h"
#include "gnc-window.h"
#include "gnc-prefs.h"
#include "gnc-optiondb.h"
#include "gnc-autosave.h"
#include "print-session.h"
#ifdef MAC_INTEGRATION
#include <gtkmacintegration/gtkosxapplication.h>
#endif
#ifdef HAVE_SYS_STAT_H
# define __need_system_sys_stat_h //To block Guile-2.0's evil substitute
# 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,
    MENU_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_PREF_TAB_OPEN_ADJACENT    "tab-open-adjacent"

#define GNC_MAIN_WINDOW_NAME "GncMainWindow"

#define DIALOG_BOOK_OPTIONS_CM_CLASS "dialog-book-options"

/**
 * Processes selected options in the Book Options dialog: checks book_currency
 * and use_split_action_for_num to see if features kvp should be set. To be used
 * where ever a new book situation requires book option selection (e.g., not
 * just in Book Options dialog opened from main window but also in new-file
 * assistant).
 *
 *  @param GncOptionDB * options.
 *
 *  @return TRUE if gnc_gui_refresh_all should be called; otherwise FALSE.
 **/
extern gboolean gnc_book_options_dialog_apply_helper(GncOptionDB * options);

/** Max number of windows allowed */
[[maybe_unused]] constexpr auto gnc_main_window_max_number {10};

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

/** The debugging module that this .o belongs to.  */
static QofLogModule log_module = GNC_MOD_GUI;
/** 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 = nullptr;
/** 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_constructed (GObject *object);
static void gnc_main_window_finalize (GObject *object);
static void gnc_main_window_destroy (GtkWidget *widget);

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

/* Callbacks */
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_redirect (GSimpleAction *simple, GVariant *paramter, gpointer user_data);
static void gnc_main_window_cmd_page_setup (GSimpleAction *simple, GVariant *paramter, gpointer user_data);
static void gnc_main_window_cmd_file_properties (GSimpleAction *simple, GVariant *paramter, gpointer user_data);
static void gnc_main_window_cmd_file_close (GSimpleAction *simple, GVariant *paramter, gpointer user_data);
static void gnc_main_window_cmd_file_quit (GSimpleAction *simple, GVariant *paramter, gpointer user_data);
static void gnc_main_window_cmd_edit_cut (GSimpleAction *simple, GVariant *paramter, gpointer user_data);
static void gnc_main_window_cmd_edit_copy (GSimpleAction *simple, GVariant *paramter, gpointer user_data);
static void gnc_main_window_cmd_edit_paste (GSimpleAction *simple, GVariant *paramter, gpointer user_data);
static void gnc_main_window_cmd_edit_preferences (GSimpleAction *simple, GVariant *paramter, gpointer user_data);
static void gnc_main_window_cmd_view_refresh (GSimpleAction *simple, GVariant *paramter, gpointer user_data);
static void gnc_main_window_cmd_view_toolbar (GSimpleAction *simple, GVariant *paramter, gpointer user_data);
static void gnc_main_window_cmd_view_summary (GSimpleAction *simple, GVariant *paramter, gpointer user_data);
static void gnc_main_window_cmd_view_statusbar (GSimpleAction *simple, GVariant *paramter, gpointer user_data);

static void gnc_main_window_cmd_view_tab_position (GSimpleAction *simple, GVariant *parameter, gpointer user_data);

static void gnc_main_window_cmd_actions_reset_warnings (GSimpleAction *simple, GVariant *paramter, gpointer user_data);
static void gnc_main_window_cmd_actions_rename_page (GSimpleAction *simple, GVariant *paramter, gpointer user_data);
static void gnc_main_window_cmd_window_new (GSimpleAction *simple, GVariant *paramter, gpointer user_data);
static void gnc_main_window_cmd_window_move_page (GSimpleAction *simple, GVariant *paramter, gpointer user_data);
#ifndef MAC_INTEGRATION
static void gnc_main_window_cmd_window_raise (GSimpleAction *simple, GVariant *parameter, gpointer user_data);
#endif
static void gnc_main_window_cmd_help_tutorial (GSimpleAction *simple, GVariant *paramter, gpointer user_data);
static void gnc_main_window_cmd_help_contents (GSimpleAction *simple, GVariant *paramter, gpointer user_data);
static void gnc_main_window_cmd_help_about (GSimpleAction *simple, GVariant *paramter, gpointer user_data);

static void do_popup_menu(GncPluginPage *page, GdkEventButton *event);
static GtkWidget *gnc_main_window_get_statusbar (GncWindow *window_in);
static void statusbar_notification_lastmodified (void);
static void gnc_main_window_update_tab_position (gpointer prefs, gchar *pref, gpointer user_data);
static void gnc_main_window_remove_prefs (GncMainWindow *window);

#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
static void gnc_main_window_init_menu_updaters (GncMainWindow *window);


/** 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 menubar */
    GtkWidget *menubar;
    /** The menubar_model */
    GMenuModel *menubar_model;
    /** The toolbar. 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;
    /** 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;
    /** Array for window position. */
    gint pos[2];
    /** Set when restoring plugin pages */
    gboolean restoring_pages;

    const gchar   *previous_plugin_page_name;
    const gchar   *previous_menu_qualifier;

    /** The accelerator group for the window */
    GtkAccelGroup *accel_group;

    GHashTable    *display_item_hash;

} GncMainWindowPrivate;

G_DEFINE_TYPE_WITH_CODE(GncMainWindow, gnc_main_window, GTK_TYPE_APPLICATION_WINDOW,
                        G_ADD_PRIVATE (GncMainWindow)
                        G_IMPLEMENT_INTERFACE (GNC_TYPE_WINDOW,
                                       gnc_window_main_window_init))

#define GNC_MAIN_WINDOW_GET_PRIVATE(o)  \
   ((GncMainWindowPrivate*)gnc_main_window_get_instance_private((GncMainWindow*)o))

/** 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 GActionEntry gnc_menu_actions [] =
{
    { "FilePageSetupAction", gnc_main_window_cmd_page_setup, nullptr, nullptr, nullptr },
    { "FilePropertiesAction", gnc_main_window_cmd_file_properties, nullptr, nullptr, nullptr },
    { "FileCloseAction", gnc_main_window_cmd_file_close, nullptr, nullptr, nullptr },
    { "FilePrintAction", gnc_main_window_cmd_redirect, nullptr, nullptr, nullptr },
    { "FileQuitAction", gnc_main_window_cmd_file_quit, nullptr, nullptr, nullptr },

    { "EditCutAction", gnc_main_window_cmd_edit_cut, nullptr, nullptr, nullptr },
    { "EditCopyAction", gnc_main_window_cmd_edit_copy, nullptr, nullptr, nullptr },
    { "EditPasteAction", gnc_main_window_cmd_edit_paste, nullptr, nullptr, nullptr },
    { "EditPreferencesAction", gnc_main_window_cmd_edit_preferences, nullptr, nullptr, nullptr },

    { "ActionsForgetWarningsAction", gnc_main_window_cmd_actions_reset_warnings, nullptr, nullptr, nullptr },
    { "ActionsRenamePageAction", gnc_main_window_cmd_actions_rename_page, nullptr, nullptr, nullptr },

    { "TransactionAction", nullptr, nullptr, nullptr, nullptr },

    { "ViewSortByAction", nullptr, nullptr, nullptr, nullptr },
    { "ViewFilterByAction", nullptr, nullptr, nullptr, nullptr },
    { "ViewRefreshAction", gnc_main_window_cmd_view_refresh, nullptr, nullptr, nullptr },
    { "ViewToolbarAction", gnc_main_window_cmd_view_toolbar, nullptr, "true", nullptr },
    { "ViewSummaryAction", gnc_main_window_cmd_view_summary, nullptr, "true", nullptr },
    { "ViewStatusbarAction", gnc_main_window_cmd_view_statusbar, nullptr, "true", nullptr },
    { "ViewTabPositionAction",  gnc_main_window_cmd_view_tab_position, "i", "@i 0", nullptr },

    { "ScheduledAction", nullptr, nullptr, nullptr, nullptr },

    { "ExtensionsAction", nullptr, nullptr, nullptr, nullptr },

    { "WindowNewAction", gnc_main_window_cmd_window_new, nullptr, nullptr, nullptr },
    { "WindowMovePageAction", gnc_main_window_cmd_window_move_page, nullptr, nullptr, nullptr },
#ifndef MAC_INTEGRATION
    { "WindowAction",  gnc_main_window_cmd_window_raise, "i", "@i 0", nullptr },
#endif
    { "HelpTutorialAction", gnc_main_window_cmd_help_tutorial, nullptr, nullptr, nullptr },
    { "HelpContentsAction", gnc_main_window_cmd_help_contents, nullptr, nullptr, nullptr },
    { "HelpAboutAction", gnc_main_window_cmd_help_about, nullptr, nullptr, nullptr },
};
/** The number of actions provided by the main window. */
static guint gnc_menu_n_actions = G_N_ELEMENTS(gnc_menu_actions);

/** 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",
    nullptr
};


/** 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",
    nullptr
};


/** 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",
    nullptr
};


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


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


/************************************************************
 *                                                          *
 ************************************************************/
#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;


gboolean
gnc_main_window_is_restoring_pages (GncMainWindow *window)
{
    GncMainWindowPrivate *priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    return priv->restoring_pages;
}


/*  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)
{
    ENTER(" ");
    for (auto w = active_windows; w; w = g_list_next(w))
    {
        auto window{static_cast<GncMainWindow*>(w->data)};
        auto priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
        for (auto p = priv->installed_pages; p; p = g_list_next(p))
        {
            auto page{static_cast<GncPluginPage*>(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.
 * 
 *  @return true if page was added else false.
 *  */
static gboolean
gnc_main_window_restore_page (GncMainWindow *window,
                              GncMainWindowSaveData *data)
{
    GncMainWindowPrivate *priv;
    GncPluginPage *page = nullptr;
    gchar *page_group, *page_type = nullptr, *name = nullptr;
    const gchar *class_type;
    GError *error = nullptr;

    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 = static_cast<GncPluginPage*>(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 == nullptr)
            {
                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);

    return (page ? true : false);
}

static bool
intersects_some_monitor(const GdkRectangle& rect)
{
    auto display = gdk_display_get_default();
    if (!display)
        return false;

    int n = gdk_display_get_n_monitors(display);
    for (int i = 0; i < n; ++i)
    {
        auto monitor = gdk_display_get_monitor(display, i);
        GdkRectangle monitor_geometry;
        gdk_monitor_get_geometry(monitor, &monitor_geometry);
        DEBUG("Monitor %d: position (%d,%d), size %dx%d\n", i,
                        monitor_geometry.x, monitor_geometry.y,
                        monitor_geometry.width, monitor_geometry.height);
        if (gdk_rectangle_intersect(&rect, &monitor_geometry, nullptr))
            return true;
    }

    return false;
}

static void
set_window_geometry(GncMainWindow *window, GncMainWindowSaveData *data, gchar *window_group)
{
    gsize length;
    GError *error = nullptr;
    gint *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 = nullptr;
    }
    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 */
    gint *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 = nullptr;
    }
    else if (length != 2)
    {
        g_warning("invalid number of values for group %s key %s",
                  window_group, WINDOW_POSITION);
    }
    else if (pos)
    {
        // Prevent restoring coordinates if this would move the window off-screen
        // If missing geom, use height=width=1 to make the intersection check work
        GdkRectangle geometry{pos[0], pos[1], geom ? geom[0] : 1, geom ? geom[1] : 1};
        if (intersects_some_monitor(geometry))
        {
            gtk_window_move(GTK_WINDOW(window), geometry.x, geometry.y);
            auto priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
            priv->pos[0] = geometry.x;
            priv->pos[1] = geometry.y;
            DEBUG("window (%p) position (%d,%d)", window, geometry.x, geometry.y);
        }
        else
        {
            DEBUG("position (%d,%d), size %dx%d is offscreen; will not move",
                  geometry.x, geometry.y, geometry.width, geometry.height);
        }
    }
    g_free(geom);
    g_free(pos);

    gboolean 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 = nullptr;
    }
    else if (max)
    {
        gtk_window_maximize(GTK_WINDOW(window));
    }
}

/** 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;
    GAction *action;
    gint *order;
    gsize length;
    gsize page_start, page_count, i;
    GError *error = nullptr;
    GSList *added_page_offsets = nullptr;
    gint offset = 0;

    /* Setup */
    ENTER("window %p, data %p (key file %p, window %d)",
          window, data, data->key_file, data->window_num);
    gchar *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://bugs.gnucash.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 == nullptr)
    {
        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();
    }

    /* Get the window coordinates, etc. */
    set_window_geometry(window, data, window_group);

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    // need to add the accelerator keys
    gnc_add_accelerator_keys_for_menu (GTK_WIDGET(priv->menubar), priv->menubar_model, priv->accel_group);

    /* Common view menu items */
    action = gnc_main_window_find_action (window, "ViewToolbarAction");
    if (action)
    {
        GVariant *state = g_action_get_state (G_ACTION(action));
        gboolean visible = g_variant_get_boolean (state);
        gboolean 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 = nullptr;
        }
        else if (visible != desired_visibility)
        {
            g_action_activate (action, nullptr);
        }
        g_variant_unref (state);
    }

    action = gnc_main_window_find_action (window, "ViewSummaryAction");
    if (action)
    {
        GVariant *state = g_action_get_state (G_ACTION(action));
        gboolean visible = g_variant_get_boolean (state);
        gboolean 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, SUMMARYBAR_VISIBLE, error->message);
            g_error_free (error);
            error = nullptr;
        }
        else if (visible != desired_visibility)
        {
            g_action_activate (action, nullptr);
        }
        g_variant_unref (state);
    }

    action = gnc_main_window_find_action (window, "ViewStatusbarAction");
    if (action)
    {
        GVariant *state = g_action_get_state (G_ACTION(action));
        gboolean visible = g_variant_get_boolean (state);
        gboolean 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, STATUSBAR_VISIBLE, error->message);
            g_error_free (error);
            error = nullptr;
        }
        else if (visible != desired_visibility)
        {
            g_action_activate (action, nullptr);
        }
        g_variant_unref (state);
    }
    priv->restoring_pages = TRUE;
    /* Now populate the window with pages. */
    for (i = 0; i < page_count; i++)
    {
        data->page_offset = page_start;
        data->page_num = i;
        gboolean page_added = gnc_main_window_restore_page (window, data);

        added_page_offsets = g_slist_append (added_page_offsets, GINT_TO_POINTER(offset));

        if (!page_added) // if page not added, increase offset to compensate
            offset ++;

        /* give the page a chance to display */
        while (gtk_events_pending ())
            gtk_main_iteration ();
    }
    priv->restoring_pages = FALSE;
    /* 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 = nullptr;
    }
    else if (length != page_count)
    {
        g_warning("%s key %s length %" G_GSIZE_FORMAT " differs from window page count %" G_GSIZE_FORMAT,
                  window_group, WINDOW_PAGEORDER, length, page_count);
    }
    else
    {
        /* Dump any list that might exist */
        g_list_free(priv->usage_order);
        priv->usage_order = nullptr;
        /* Now rebuild the list from the key file. */
        for (i = 0; i < length; i++)
        {
            gint offset = GPOINTER_TO_INT(g_slist_nth_data (added_page_offsets, order[i] - 1));
            gpointer page = g_list_nth_data (priv->installed_pages, order[i] - 1 - offset);
            if (page)
            {
                priv->usage_order = g_list_append(priv->usage_order, page);
            }
        }
        gtk_notebook_set_current_page (GTK_NOTEBOOK(priv->notebook),
                                       order[0] - 1 - offset);

        g_signal_emit_by_name (window, "page_changed",
                               g_list_nth_data (priv->usage_order, 0));
    }
    if (order)
    {
        g_free(order);
    }

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

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

    /* 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 (nullptr, TRUE);
    for (i = 0; i < window_count; i++)
    {
        data.window_num = i;
        auto window{static_cast<GncMainWindow*>(g_list_nth_data(active_windows,
                                                                i))};
        gnc_main_window_restore_window(window, &data);
    }
    gnc_unset_busy_cursor (nullptr);

    statusbar_notification_lastmodified();
}

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

    /* The default state should be to have an Account Tree page open
     * in the window. */
    DEBUG("no saved state file");
    if (!window)
        window = static_cast<GncMainWindow*>(g_list_nth_data(active_windows, 0));
    gtk_widget_show (GTK_WIDGET(window));
    action = gnc_main_window_find_action_in_group (window,
                                                   "gnc-plugin-account-tree-actions",
                                                   "ViewAccountTreeAction");
    g_action_activate (action, nullptr);
}

/** 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;
    GAction *action;
    gint i, num_pages, coords[4], *order;
    gboolean maximized, minimized, visible = true;
    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 = static_cast<int*>(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;
    minimized = (gdk_window_get_state(gtk_widget_get_window ((GTK_WIDGET(window))))
                 & GDK_WINDOW_STATE_ICONIFIED) != 0;

    if (minimized)
    {
        gint *pos = priv->pos;
        g_key_file_set_integer_list(data->key_file, window_group,
                                    WINDOW_POSITION, &pos[0], 2);
        DEBUG("window minimized (%p) position (%d,%d)", window, pos[0], pos[1]);
    }
    else
        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 (%d,%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");
    if (action)
    {
        GVariant *state = g_action_get_state (G_ACTION(action));
        visible = g_variant_get_boolean (state);
        g_variant_unref (state);
    }
    g_key_file_set_boolean (data->key_file, window_group,
                            TOOLBAR_VISIBLE, visible);
    action = gnc_main_window_find_action (window, "ViewSummaryAction");
    if (action)
    {
        GVariant *state = g_action_get_state (G_ACTION(action));
        visible = g_variant_get_boolean (state);
        g_variant_unref (state);
    }
    g_key_file_set_boolean (data->key_file, window_group,
                            SUMMARYBAR_VISIBLE, visible);
    action = gnc_main_window_find_action (window, "ViewStatusbarAction");
    if (action)
    {
        GVariant *state = g_action_get_state (G_ACTION(action));
        visible = g_variant_get_boolean (state);
        g_variant_unref (state);
    }
    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(static_cast<GncPluginPage*>(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(static_cast<GncMainWindow*>(item->data)))
        {
            return FALSE;
        }
    }
    if (gnc_gui_refresh_suspended ())
    {
        gnc_warning_dialog (nullptr, "%s", "An operation is still running, wait for it to complete before quitting.");
        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)
{
    GncMainWindowPrivate *priv;
    GList *walker;

    for (walker = active_windows; walker; walker = g_list_next(walker))
    {
        auto window{static_cast<GncMainWindow*>(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 = nullptr;

    /* 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 */

    /* Protect against rolling over to MAXUINT */
    if (secs_to_save)
        --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;
    guint timer_source = 0;
    if (!gnc_current_session_exist())
        return FALSE;
    session = gnc_get_current_session();
    book = qof_session_get_book(session);
    if (!qof_book_session_not_saved(book))
        return FALSE;
    filename = qof_session_get_url(session);
    if (!strlen (filename))
        filename = _("<unknown>");
    if ((tmp = strrchr(filename, '/')) != nullptr)
        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 (nullptr) - 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,
                           _("_Cancel"), GTK_RESPONSE_CANCEL,
                           _("_Save"), GTK_RESPONSE_APPLY,
                           nullptr);
    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 = nullptr;

        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, nullptr);

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

    response = gtk_dialog_run (GTK_DIALOG (dialog));
    if (timer_source)
        g_source_remove (timer_source);
    gtk_widget_destroy(dialog);

    switch (response)
    {
    case GTK_RESPONSE_APPLY:
        gnc_file_save (GTK_WINDOW (window));
        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 = TRUE;
    if (gnc_current_session_exist())
    {
        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)
    {
        GList *w, *next;

        /* This is not a typical list iteration. There is a possibility
         * that the window may be removed from the active_windows list so
         * we have to cache the 'next' pointer before executing any code
         * in the loop. */
        for (w = active_windows; w; w = next)
        {
            GncMainWindowPrivate *priv;
            GncMainWindow *window = static_cast<GncMainWindow*>(w->data);

            next = g_list_next (w);

            window->window_quitting = TRUE; //set window_quitting on all windows

            priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

            // if there are no pages destroy window
            if (priv->installed_pages == NULL)
                gtk_widget_destroy (GTK_WIDGET(window));
        }
        /* remove the preference callbacks from the main window */
        gnc_main_window_remove_prefs (window);
        g_timeout_add(250, gnc_main_window_timed_quit, nullptr);
        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_list_length_cmp (active_windows, 1) > 0)
    {
        gint response;
        GtkWidget *dialog;
        gchar *message = _("This window is closing and will not be restored.");

        dialog = gtk_message_dialog_new (GTK_WINDOW (window),
                                         GTK_DIALOG_DESTROY_WITH_PARENT,
                                         GTK_MESSAGE_QUESTION,
                                         GTK_BUTTONS_NONE,
                                         "%s", _("Close Window?"));
        gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG(dialog),
                                                  "%s", message);

        gtk_dialog_add_buttons (GTK_DIALOG(dialog),
                              _("_Cancel"), GTK_RESPONSE_CANCEL,
                              _("_OK"), GTK_RESPONSE_YES,
                               (gchar *)NULL);
        gtk_dialog_set_default_response (GTK_DIALOG(dialog), GTK_RESPONSE_YES);
        response = gnc_dialog_run (GTK_DIALOG(dialog), GNC_PREF_WARN_CLOSING_WINDOW_QUESTION);
        gtk_widget_destroy (dialog);

        if (response == GTK_RESPONSE_CANCEL)
            return TRUE;
    }

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

    if (gnc_list_length_cmp (active_windows, 1) > 0)
        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);
    }

    if (GTK_IS_WIDGET(window) && window->window_quitting)
        gtk_widget_destroy (GTK_WIDGET(window));

    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 = nullptr;
    const gchar *uri = nullptr;
    const gchar *dirty = "";
    const gchar *readonly_text = nullptr;
    gchar *readonly;
    gchar *title;

    if (gnc_current_session_exist())
    {
        uri = 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 != nullptr)
               ? g_strdup_printf(" %s", readonly_text)
               : g_strdup("");

    if (!uri || g_strcmp0 (uri, "") == 0)
        filename = g_strdup(_("Unsaved Book"));
    else
    {
        if (gnc_uri_targets_local_fs (uri))
        {
            /* 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 ( uri );
            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 (uri, 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_set_actions_enabled (G_ACTION_MAP(window),
                                    immutable_page_actions,
                                    !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,
                   nullptr);
}

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, nullptr);
    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)
{
    GncMainWindow *mainwindow = GNC_MAIN_WINDOW (gnc_ui_get_main_window (nullptr));
    //g_warning("statusbar_notification_off\n");
    if (gnc_statusbar_notification_messageid == 0)
        return FALSE;

    if (mainwindow)
    {
        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 = nullptr;
    const gchar *uri = nullptr;

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

    if (!(uri && strlen (uri)))
        return nullptr;
    else
    {
        if (gnc_uri_targets_local_fs (uri))
        {
            /* The filename is a true file. */
            gchar *filepath = gnc_uri_get_path ( uri );
            gchar *filename = g_path_get_basename ( filepath );
            GFile *file = g_file_new_for_uri (uri);
            GFileInfo *info = g_file_query_info (file,
                                                 G_FILE_ATTRIBUTE_TIME_MODIFIED,
                                                 G_FILE_QUERY_INFO_NONE,
                                                 NULL, NULL);

            if (info && g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_TIME_MODIFIED))
            {
                // Access the mtime information through stat(2)
                struct stat statbuf;
                int r = stat(filepath, &statbuf);
                if (r == 0)
                {
                    /* 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 a format string using
                    boost::date_time's format flags, see the boost docs for an
                    explanation of the modifiers. */
                    char *time_string = gnc_print_time64(statbuf.st_mtime,
                     _("Last modified on %a, %b %d, %Y at %I:%M %p"));
                    //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);
                    free(time_string);
                }
                else
                {
                    g_warning("Unable to read mtime for file %s\n", filepath);
                    // message is still nullptr
                }
            }
            g_free(filename);
            g_free(filepath);
            g_object_unref (info);
            g_object_unref (file);
        }
        // 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 = nullptr;
    for (iter = active_windows; iter && !(widget && GNC_IS_MAIN_WINDOW(widget));
            iter = g_list_next(iter))
    {
        widget = static_cast<GtkWidget*>(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, nullptr); // 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
 *  GAction, and is used to pass data among several functions. */
struct menu_update
{
    /** The name of the GAction to be updated. */
    gchar    *action_name;

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

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

    /** Index number in active windows list */
    gint index;
};

#ifndef MAC_INTEGRATION
/** Update the label on the menu item specified by the GAction 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 GAction 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;
    GncMenuModelSearch *gsm = g_new0 (GncMenuModelSearch, 1);
    GMenuItem *item;
    gint pos;

    ENTER("window %p, action %s, label %s, index %d, visible %d", window,
          data->action_name, data->label, data->index, data->visible);

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    gsm->search_action_label = nullptr;
    gsm->search_action_name = "WindowsPlaceholder1";  // placeholder
    gsm->search_action_target = nullptr;

    if (!gnc_menubar_model_find_item (priv->menubar_model, gsm))
    {
        LEAVE("Could not find placeholder 'WindowsPlaceholder1' for windows entries");
        g_free (gsm);
        return;
    }

    pos = gsm->index + data->index + 1;

    if (!data->visible)
    {
        if (pos < g_menu_model_get_n_items (gsm->model))
            g_menu_remove (G_MENU(gsm->model), pos);

        g_free (gsm);
        LEAVE(" ");
        return;
    }

    item = g_menu_item_new (data->label, "mainwin.WindowAction");
    g_menu_item_set_attribute (item, G_MENU_ATTRIBUTE_TARGET, "i", data->index);

    if (pos < g_menu_model_get_n_items (gsm->model))
        g_menu_remove (G_MENU(gsm->model), pos);
    g_menu_insert_item (G_MENU(gsm->model), pos, item);
    g_object_unref (item);

    g_free (gsm);
    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)
{
    GAction *action;
    gsize index;

    ENTER("window %p", window);

    /* Show the new entry in all windows. */
    index = g_list_index (active_windows, window);

    if (index >= gnc_main_window_max_number)
    {
        LEAVE("window %" G_GSIZE_FORMAT ", only %d actions", index, gnc_main_window_max_number);
        return;
    }

    action = g_action_map_lookup_action (G_ACTION_MAP(window),
                                         "WindowAction");

    /* Block the signal so as not to affect window ordering (top to
     * bottom) on the screen */
    g_signal_handlers_block_by_func (G_OBJECT(action),
                                     (gpointer)gnc_main_window_cmd_window_raise,
                                     window);

    DEBUG("blocked signal on action %p, window %p", action, window);
    g_action_change_state (G_ACTION(action), g_variant_new_int32 (index));

    g_signal_handlers_unblock_by_func (G_OBJECT(action),
                                       (gpointer)gnc_main_window_cmd_window_raise,
                                       window);
    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.
 *
 *  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;
    gsize index;

    ENTER("window %p", window);

    index = g_list_index (active_windows, window);

    if (index >= gnc_main_window_max_number)
    {
        LEAVE("skip window %" G_GSIZE_FORMAT " (only %d entries)", index, gnc_main_window_max_number);
        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 < gnc_main_window_max_number)
    {
        data.label = g_strdup_printf ("_%" G_GSIZE_FORMAT " %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%" G_GSIZE_FORMAT "Action", index);
    data.index = 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;

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

    g_list_foreach (active_windows,
                    (GFunc)gnc_main_window_update_radio_button,
                    nullptr);

    /* Now hide any entries that aren't being used. */
    data.visible = FALSE;
    // need i to descend from gnc_main_window_max_number
    for (gsize i = gnc_main_window_max_number - 1; i > 0 && i >= g_list_length (active_windows); i--)
    {
        data.index = i;
        data.action_name = g_strdup_printf ("Window%dAction", data.index);
        data.label = g_strdup_printf ("mywin%" G_GSIZE_FORMAT, i % 10);

        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 */

/** 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)
{
    auto new_value{static_cast<gboolean*>(user_data)};
    ENTER("page %p, visible %d", page, *new_value);
    auto close_button{static_cast<GtkWidget*>(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)
{
    ENTER(" ");
    g_return_if_fail(GNC_IS_MAIN_WINDOW(user_data));
    auto window{static_cast<GncMainWindow*>(user_data)};
    auto 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(" ");
}


/** This data structure allows the passing of the tab width and
 *  whether the tab layout is on the left or right.
 */
typedef struct
{
    gint tab_width;
    gboolean tabs_left_right;
} TabWidth;

static TabWidth *
populate_tab_width_struct (void)
{
    TabWidth *tw;

    tw = g_new0 (TabWidth, 1);
    tw->tab_width = gnc_prefs_get_float (GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_WIDTH);
    tw->tabs_left_right = gnc_prefs_get_bool (GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_POSITION_LEFT) ||
                          gnc_prefs_get_bool (GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_POSITION_RIGHT);

    return tw;
}

/** Set the tab label ellipsize value.
 *  When the tabs are on the left or right, the label width is set to
 *  the tab_width value. Doing this maintains a steady notepad header
 *  width for the tabs.
 *
 *  When the tabs are on the top or bottom, the label width is set to
 *  the number of characters when shorter than tab_width so they take
 *  up less room.
 *
 *  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 label GtkLabel for the tab.
 *
 *  @param tab_width Tab width the user has set in preferences.
 *
 *  @param tab_left_right Whether the tab layout is on the left or right.
 *
 */
static void
gnc_main_window_set_tab_ellipsize (GtkWidget *label, gint tab_width, gboolean tab_left_right)
{
    const gchar *lab_text = gtk_label_get_text (GTK_LABEL(label));

    if (tab_width != 0)
    {
        gint text_length = g_utf8_strlen (lab_text, -1);
        if (text_length < tab_width)
        {
            if (tab_left_right) // tabs position is left or right
                gtk_label_set_width_chars (GTK_LABEL(label), tab_width);
            else // tabs position is top or bottom
                gtk_label_set_width_chars (GTK_LABEL(label), text_length);

            gtk_label_set_ellipsize (GTK_LABEL(label), PANGO_ELLIPSIZE_NONE);
        }
        else
        {
            gtk_label_set_width_chars (GTK_LABEL(label), tab_width);
            gtk_label_set_ellipsize (GTK_LABEL(label), PANGO_ELLIPSIZE_MIDDLE);
        }
    }
    else
    {
        gtk_label_set_width_chars (GTK_LABEL(label), 15);
        gtk_label_set_ellipsize (GTK_LABEL(label), PANGO_ELLIPSIZE_NONE);
    }
}


/** 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.
 *
 *  @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)
{
    auto tw{static_cast<TabWidth*>(user_data)};

    ENTER("page %p, tab width %d, tabs on left or right %d",
           page, tw->tab_width, tw->tabs_left_right);

    auto label{static_cast<GtkWidget *>(g_object_get_data(G_OBJECT (page), PLUGIN_PAGE_TAB_LABEL))};
    if (!label)
    {
        LEAVE("no label");
        return;
    }
    gnc_main_window_set_tab_ellipsize (label, tw->tab_width, tw->tabs_left_right);
    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)
{
    TabWidth *tw;

    ENTER(" ");

    tw = populate_tab_width_struct ();

    gnc_main_window_foreach_page (gnc_main_window_update_tab_width_one_page, tw);
    g_free (tw);

    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 = nullptr;

    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_BOX (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 = static_cast<GtkWidget*>(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 = nullptr;

    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_long_name (GncPluginPage *page,
                                   const gchar *long_name_in)
{
    GtkWidget *tab_widget;

    ENTER(" ");

    if ((long_name_in == nullptr) || (*long_name_in == '\0'))
    {
        LEAVE("no string");
        return;
    }
    gchar *long_name = g_strstrip (g_strdup (long_name_in));
    const gchar *old_long_name = gnc_plugin_page_get_page_long_name (page);

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

    gnc_plugin_page_set_page_long_name (page, long_name);

    GncMainWindow *window = GNC_MAIN_WINDOW(page->window);
    if (!window)
    {
        g_free (long_name);
        LEAVE("no window widget available");
        return;
    }

    /* Update the notebook tab tooltip */
    if (main_window_find_tab_widget (window, page, &tab_widget))
        gtk_widget_set_tooltip_text (tab_widget, long_name);

    g_free (long_name);
    LEAVE("");
}

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

    ENTER(" ");

    if ((name_in == nullptr) || (*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;
    }

    /* 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(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);

    /* Adjust the label width for new text */
    tw = populate_tab_width_struct ();
    gnc_main_window_update_tab_width_one_page (page, tw);
    g_free (tw);

    /* 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(name);
    LEAVE("done");
}


void
main_window_update_page_color (GncPluginPage *page,
                               const gchar *color_in)
{
    GncMainWindow *window;
    GncMainWindowPrivate *priv;
    GtkWidget *tab_widget;
    GdkRGBA tab_color;
    gchar *color_string = nullptr;
    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, nullptr);

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

    if (want_color && gdk_rgba_parse(&tab_color, color_string) && priv->show_color_tabs)
    {
        GtkCssProvider *provider = gtk_css_provider_new();
        GtkStyleContext *stylectxt;
        gchar *col_str, *widget_css;

        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;
        }

        stylectxt = gtk_widget_get_style_context (GTK_WIDGET (tab_widget));
        col_str = gdk_rgba_to_string (&tab_color);
        widget_css = g_strconcat ("*{\n  background-color:", col_str, ";\n}\n", nullptr);

        gtk_css_provider_load_from_data (provider, widget_css, -1, nullptr);
        gtk_style_context_add_provider (stylectxt, GTK_STYLE_PROVIDER (provider),
                                        GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
        g_object_unref (provider);
        g_free (col_str);
        g_free (widget_css);
    }
    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");
}


void
main_window_update_page_set_read_only_icon (GncPluginPage *page,
                                            gboolean read_only)
{
    GncMainWindow *window;
    GtkWidget *tab_widget;
    GtkWidget *image = NULL;
    GList *children;
    gchar *image_name = NULL;
    const gchar *icon_name;

    ENTER(" ");

    g_return_if_fail (page && page->window);

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

    window = GNC_MAIN_WINDOW(page->window);

    /* Get the notebook tab widget */
    main_window_find_tab_widget (window, page, &tab_widget);

    if (!tab_widget)
    {
        LEAVE("no tab widget");
        return;
    }

    if (GTK_IS_EVENT_BOX(tab_widget))
        tab_widget = gtk_bin_get_child (GTK_BIN(tab_widget));

    children = gtk_container_get_children (GTK_CONTAINER(tab_widget));
    /* For each, walk the list of container children to get image widget */
    for (GList *child = children; child; child = g_list_next (child))
    {
        GtkWidget *widget = static_cast<GtkWidget*>(child->data);
        if (GTK_IS_IMAGE(widget))
            image = widget;
    }
    g_list_free (children);

    if (!image)
    {
        LEAVE("no image to replace");
        return;
    }

    g_object_get (image, "icon-name", &image_name, NULL);

    if (read_only)
        icon_name = "changes-prevent-symbolic";
    else
        icon_name = GNC_PLUGIN_PAGE_GET_CLASS(page)->tab_icon;

    if (g_strcmp0 (icon_name, image_name) == 0)
    {
        LEAVE("page icon the same, no need to replace");
        g_free (image_name);
        return;
    }
    gtk_container_remove (GTK_CONTAINER(tab_widget), image);
    image = gtk_image_new_from_icon_name (icon_name, GTK_ICON_SIZE_MENU);
    gtk_widget_show (image);

    gtk_container_add (GTK_CONTAINER(tab_widget), image);
    gtk_widget_set_margin_start (GTK_WIDGET(image), 5);
    gtk_box_reorder_child (GTK_BOX(tab_widget), image, 0);

    g_free (image_name);
    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                  *
 ************************************************************/



/** 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);
    GtkWidgetClass *gtkwidget_class = GTK_WIDGET_CLASS(klass);

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

    object_class->constructed = gnc_main_window_constructed;
    object_class->finalize = gnc_main_window_finalize;

    /* GtkWidget signals */
    gtkwidget_class->destroy = gnc_main_window_destroy;

    /**
     * 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),
                      nullptr, nullptr,
                      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 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),
                      nullptr, nullptr,
                      g_cclosure_marshal_VOID__OBJECT,
                      G_TYPE_NONE, 1,
                      G_TYPE_OBJECT);

    /**
     * GncMainWindow::menu_changed:
     * @param window: the #GncMainWindow
     * @param page: the #GncPluginPage
     *
     * The "menu_changed" signal is emitted when the menu has been
     * changed. This can be used to adjust other menu actions.
     */
    main_window_signals[MENU_CHANGED] =
        g_signal_new ("menu_changed",
                      G_OBJECT_CLASS_TYPE (object_class),
                      G_SIGNAL_RUN_FIRST,
                      G_STRUCT_OFFSET (GncMainWindowClass, menu_changed),
                      nullptr, nullptr,
                      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,
                           (gpointer)gnc_main_window_update_tab_close,
                           nullptr);
    gnc_prefs_register_cb (GNC_PREFS_GROUP_GENERAL,
                           GNC_PREF_TAB_WIDTH,
                           (gpointer)gnc_main_window_update_tab_width,
                           nullptr);

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

}


/** Initialize a new instance of a gnucash main window.  This function
 *  initializes the object private storage space.
 *
 *  @param window The new object instance created by the object system.
 *  */
static void
gnc_main_window_init (GncMainWindow *window)
{
    GncMainWindowPrivate *priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    // Set the name for this dialog so it can be easily manipulated with css
    gtk_widget_set_name (GTK_WIDGET(window), "gnc-id-main-window");

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

    priv->restoring_pages = FALSE;

    priv->display_item_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, nullptr);

    priv->previous_plugin_page_name = nullptr;
    priv->previous_menu_qualifier = nullptr;

    priv->accel_group = gtk_accel_group_new ();
    gtk_window_add_accel_group (GTK_WINDOW(window), priv->accel_group);

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

    gnc_prefs_register_cb (GNC_PREFS_GROUP_GENERAL,
                           GNC_PREF_TAB_COLOR,
                           (gpointer)gnc_main_window_update_tab_color,
                           window);

    gnc_main_window_setup_window (window);
}

/** The object has been fully constructed.
 * This function adds the object to the tracking system.
 *
 *  @param obj The new object instance created by the object
 *  system.
 */
static void
gnc_main_window_constructed (GObject *obj)
{
    gnc_gobject_tracking_remember(obj);

    G_OBJECT_CLASS (gnc_main_window_parent_class)->constructed (obj);
}

/** 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 != nullptr);
    g_return_if_fail (GNC_IS_MAIN_WINDOW (object));

    if (active_windows == nullptr)
    {
        /* 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 (gnc_main_window_parent_class)->finalize (object);
}


static void
gnc_main_window_remove_prefs (GncMainWindow *window)
{
    // remove the registered preference callbacks setup in this file.
    gnc_prefs_remove_cb_by_func (GNC_PREFS_GROUP_GENERAL,
                                 GNC_PREF_TAB_COLOR,
                                 (gpointer)gnc_main_window_update_tab_color,
                                 window);

    gnc_prefs_remove_cb_by_func (GNC_PREFS_GROUP_GENERAL,
                                 GNC_PREF_SHOW_CLOSE_BUTTON,
                                 (gpointer)gnc_main_window_update_tab_close,
                                 nullptr);

    gnc_prefs_remove_cb_by_func (GNC_PREFS_GROUP_GENERAL,
                                 GNC_PREF_TAB_WIDTH,
                                 (gpointer)gnc_main_window_update_tab_width,
                                 nullptr);

    gnc_prefs_remove_cb_by_func (GNC_PREFS_GROUP_GENERAL,
                                 GNC_PREF_TAB_POSITION_TOP,
                                 (gpointer)gnc_main_window_update_tab_position,
                                 window);

    gnc_prefs_remove_cb_by_func (GNC_PREFS_GROUP_GENERAL,
                                 GNC_PREF_TAB_POSITION_BOTTOM,
                                 (gpointer)gnc_main_window_update_tab_position,
                                 window);

    gnc_prefs_remove_cb_by_func (GNC_PREFS_GROUP_GENERAL,
                                 GNC_PREF_TAB_POSITION_LEFT,
                                 (gpointer)gnc_main_window_update_tab_position,
                                 window);

    gnc_prefs_remove_cb_by_func (GNC_PREFS_GROUP_GENERAL,
                                 GNC_PREF_TAB_POSITION_RIGHT,
                                 (gpointer)gnc_main_window_update_tab_position,
                                 window);

    // remove the registered negative color preference callback.
    if (gnc_prefs_get_reg_negative_color_pref_id() > 0 && window->window_quitting)
    {
        gnc_prefs_remove_cb_by_id (GNC_PREFS_GROUP_GENERAL,
                                   gnc_prefs_get_reg_negative_color_pref_id());
        gnc_prefs_set_reg_negative_color_pref_id (0);
    }

    // remove the registered auto_raise_lists preference callback.
    if (gnc_prefs_get_reg_auto_raise_lists_id() > 0 && window->window_quitting)
    {
        gnc_prefs_remove_cb_by_id (GNC_PREFS_GROUP_GENERAL_REGISTER,
                                   gnc_prefs_get_reg_auto_raise_lists_id());
        gnc_prefs_set_reg_auto_raise_lists_id (0);
    }
}


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

    g_return_if_fail (widget != nullptr);
    g_return_if_fail (GNC_IS_MAIN_WINDOW (widget));

    window = GNC_MAIN_WINDOW (widget);
#ifdef MAC_INTEGRATION
    auto entry = g_list_find (active_windows, window);
    if (entry && (entry->next || entry->prev))
        gnc_quartz_set_menu (GNC_MAIN_WINDOW (entry->next ? entry->next->data : entry->prev->data));
#endif
    active_windows = g_list_remove (active_windows, window);

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

        /* 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(nullptr);
#ifndef MAC_INTEGRATION
        /* Update the "Windows" menu in all other windows */
        gnc_main_window_update_all_menu_items();
#endif
        /* remove the preference callbacks from the main window */
        gnc_main_window_remove_prefs (window);

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

        g_hash_table_destroy (priv->display_item_hash);

        /* 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);
    }

    GTK_WIDGET_CLASS (gnc_main_window_parent_class)->destroy (widget);
}


static gboolean
gnc_main_window_key_press_event (GtkWidget *widget, GdkEventKey *event, gpointer user_data)
{
    GncMainWindowPrivate *priv;
    GdkModifierType modifiers;

    g_return_val_if_fail (GNC_IS_MAIN_WINDOW(widget), FALSE);

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(widget);

    modifiers = gtk_accelerator_get_default_mod_mask ();

    if ((event->state & modifiers) == (GDK_CONTROL_MASK | GDK_MOD1_MASK)) // Ctrl+Alt+
    {
        const gchar *account_key = C_ ("lower case key for short cut to 'Accounts'", "a");
        guint account_keyval = gdk_keyval_from_name (account_key);

        if ((account_keyval == event->keyval) || (account_keyval == gdk_keyval_to_lower (event->keyval)))
        {
            gint page = 0;

            for (GList *item = priv->installed_pages; item; item = g_list_next (item))
            {
                 const gchar *pname = gnc_plugin_page_get_plugin_name (GNC_PLUGIN_PAGE(item->data));

                 if (g_strcmp0 (pname, "GncPluginPageAccountTree") == 0)
                 {
                     gtk_notebook_set_current_page (GTK_NOTEBOOK(priv->notebook), page);
                     return TRUE;
                 }
                 page++;
            }
        }
        else if ((GDK_KEY_Menu == event->keyval) || (GDK_KEY_space == event->keyval))
        {
            GList *menu = gtk_menu_get_for_attach_widget (GTK_WIDGET(priv->notebook));

            if (menu)
            {
                gtk_menu_popup_at_widget (GTK_MENU(menu->data),
                                          GTK_WIDGET(priv->notebook),
                                          GDK_GRAVITY_SOUTH,
                                          GDK_GRAVITY_SOUTH,
                                          NULL);
                return TRUE;
            }
        }
    }
    return FALSE;
}


/*  Create a new gnc main window plugin.
 */
GncMainWindow *
gnc_main_window_new (void)
{
    auto window{static_cast<GncMainWindow*>(g_object_new (GNC_TYPE_MAIN_WINDOW, nullptr))};
    gtk_window_set_default_size(GTK_WINDOW(window), 800, 600);

    auto old_window = gnc_ui_get_main_window (nullptr);
    if (old_window)
    {
        gint width, height;
        gtk_window_get_size (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);
    window->window_quitting = FALSE;
    window->just_plugin_prefs = FALSE;
#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 );

    // set up a callback for notebook navigation
    g_signal_connect (G_OBJECT(window), "key-press-event",
                      G_CALLBACK(gnc_main_window_key_press_event),
                      NULL);

    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 GtkBox 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;
    gint current_position = -1;

    page->window = GTK_WIDGET(window);
    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    notebook = GTK_NOTEBOOK (priv->notebook);

    if (!priv->restoring_pages
            && gnc_prefs_get_bool (GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_OPEN_ADJACENT))
        current_position = g_list_index (priv->installed_pages, priv->current_page) + 1;

    priv->installed_pages = g_list_insert (priv->installed_pages, page, current_position);
    priv->usage_order = g_list_prepend (priv->usage_order, page);
    gtk_notebook_insert_page_menu (notebook, page->notebook_page,
                                   tab_hbox, menu_label, current_position);
    gtk_notebook_set_tab_reorderable (notebook, page->notebook_page, TRUE);
    gnc_plugin_page_inserted (page);
    if (!priv->restoring_pages)
        gtk_notebook_set_current_page (notebook, current_position);

    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),
                                         (gpointer)gnc_main_window_popup_menu_cb, page);
    g_signal_handlers_disconnect_by_func(G_OBJECT(page->notebook_page),
                                         (gpointer)gnc_main_window_button_press_cb, page);

    // Remove the page_changed signal callback
    gnc_plugin_page_disconnect_page_changed (GNC_PLUGIN_PAGE(page));

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

    /* 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 = static_cast<GncPluginPage*>(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, nullptr, -1, window);
        //g_signal_emit (window, main_window_signals[PAGE_CHANGED], 0, nullptr);
    }

    gnc_plugin_page_removed (page);

    gnc_window_set_status (GNC_WINDOW(window), page, nullptr);
}


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


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 nullptr, 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, *lab_text;
    GtkWidget *image;
    GList *tmp;
    TabWidth *tw;

    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 == nullptr)
            {
                break;
            }
        }
        if (tmp == nullptr)
            window = gnc_main_window_new ();
        gtk_widget_show(GTK_WIDGET(window));
    }
    else if ((window == nullptr) && active_windows)
    {
        window = static_cast<GncMainWindow*>(active_windows->data);
    }

    page->window = GTK_WIDGET(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.
     */
    icon = GNC_PLUGIN_PAGE_GET_CLASS(page)->tab_icon;
    lab_text = gnc_plugin_page_get_page_name(page);
    label = gtk_label_new (lab_text);
    g_object_set_data (G_OBJECT (page), PLUGIN_PAGE_TAB_LABEL, label);

    tw = populate_tab_width_struct ();
    gnc_main_window_update_tab_width_one_page (page, tw);
    g_free (tw);

    gtk_widget_show (label);

    tab_hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);

    if (g_strcmp0 (gnc_plugin_page_get_plugin_name (page), "GncPluginPageAccountTree") == 0)
        gtk_widget_set_name (GTK_WIDGET(tab_hbox), "gnc-id-account-page-tab-box");

    gtk_box_set_homogeneous (GTK_BOX (tab_hbox), FALSE);
    gtk_widget_show (tab_hbox);

    if (icon != nullptr)
    {
        image = gtk_image_new_from_icon_name (icon, GTK_ICON_SIZE_MENU);
        gtk_widget_show (image);
        gtk_box_pack_start (GTK_BOX (tab_hbox), image, FALSE, FALSE, 0);
        gtk_widget_set_margin_start (GTK_WIDGET(image), 5);
        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_icon_name ("window-close", GTK_ICON_SIZE_MENU);
        gtk_widget_show(close_image);
        gtk_widget_get_preferred_size (close_image, &requisition, nullptr);
        gtk_widget_set_size_request(close_button, requisition.width + 4,
                                    requisition.height + 2);
        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);
        gtk_widget_set_margin_end (GTK_WIDGET(close_button), 5);
        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 == nullptr)
    {
        if (window->window_quitting)
        {
            GncPluginManager *manager = gnc_plugin_manager_get ();
            GList *plugins = gnc_plugin_manager_get_plugins (manager);

            /* remove only the preference callbacks from the window plugins */
            window->just_plugin_prefs = TRUE;
            g_list_foreach (plugins, gnc_main_window_remove_plugin, window);
            window->just_plugin_prefs = FALSE;
            g_list_free (plugins);

            /* remove the preference callbacks from the main window */
            gnc_main_window_remove_prefs (window);
        }
        if (window && (gnc_list_length_cmp (active_windows, 1) > 0))
            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,
                                      GSimpleActionGroup *group)
{
    g_return_if_fail (GNC_IS_MAIN_WINDOW(window));
    g_return_if_fail (group_name != nullptr);
    g_return_if_fail (G_IS_SIMPLE_ACTION_GROUP(group));

    gtk_widget_insert_action_group (GTK_WIDGET(window), group_name,
                                    G_ACTION_GROUP(group));
}


static void
update_menu_model (GncMainWindow *window, const gchar *ui_filename,
                   const gchar **ui_updates)
{
    GncMainWindowPrivate *priv;
    GError *error = nullptr;
    gchar *res_name;
    GtkBuilder *builder = gtk_builder_new ();
    GMenuModel *menu_model_part;
    GncMenuModelSearch *gsm = g_new0 (GncMenuModelSearch, 1);

    g_return_if_fail (GNC_IS_MAIN_WINDOW (window));
    g_return_if_fail (ui_filename != nullptr);
    g_return_if_fail (ui_updates != nullptr);

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    gtk_builder_set_translation_domain (builder, PROJECT_NAME);
    res_name = g_strconcat (GNUCASH_RESOURCE_PREFIX "/", ui_filename, NULL);

    gtk_builder_add_from_resource (builder, res_name, &error);
    g_free (res_name);

    if (error)
    {
        g_critical ("Failed to load, Error %s", error->message);
        g_error_free (error);
        return;
    }

    for (gint i = 0; ui_updates[i]; i++)
    {
        menu_model_part = (GMenuModel *)gtk_builder_get_object (builder, ui_updates[i]);

        gsm->search_action_label = nullptr;
        gsm->search_action_name = ui_updates[i];
        gsm->search_action_target = nullptr;

        if (gnc_menubar_model_find_item (priv->menubar_model, gsm))
            g_menu_insert_section (G_MENU(gsm->model), gsm->index, NULL, G_MENU_MODEL(menu_model_part));
        else
            PERR("Could not find '%s' in menu model", ui_updates[i]);
    }
    g_free (gsm);
    g_object_unref (builder);
}


/*  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,
                               GActionEntry *actions,
                               guint n_actions,
                               const gchar **ui_updates,
                               const gchar *ui_filename,
                               gpointer user_data)
{
    GncMainWindowActionData *data;
    GSimpleActionGroup *simple_action_group;

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

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

    simple_action_group = g_simple_action_group_new ();

    g_action_map_add_action_entries (G_ACTION_MAP(simple_action_group),
                                     actions,
                                     n_actions,
                                     data);

    gtk_widget_insert_action_group (GTK_WIDGET(window), group_name,
                                    G_ACTION_GROUP(simple_action_group));

    if (ui_filename)
        update_menu_model (window, ui_filename, ui_updates);
}


/*  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)
{
    g_return_if_fail (GNC_IS_MAIN_WINDOW (window));
    g_return_if_fail (group_name != nullptr);

    gtk_widget_insert_action_group (GTK_WIDGET(window), group_name, nullptr);
}

GAction *
gnc_main_window_find_action (GncMainWindow *window, const gchar *action_name)
{
    GAction *action = nullptr;

    g_return_val_if_fail (GNC_IS_MAIN_WINDOW(window), nullptr);
    g_return_val_if_fail (action_name != nullptr, nullptr);

    action = g_action_map_lookup_action (G_ACTION_MAP(window),
                                         action_name);

    return action;
}

GAction *
gnc_main_window_find_action_in_group (GncMainWindow *window,
                                      const gchar *group_name,
                                      const gchar *action_name)
{
    GAction *action = nullptr;

    g_return_val_if_fail (GNC_IS_MAIN_WINDOW(window), nullptr);
    g_return_val_if_fail (group_name != nullptr, nullptr);
    g_return_val_if_fail (action_name != nullptr, nullptr);

    auto action_group = gtk_widget_get_action_group (GTK_WIDGET(window), group_name);

    if (action_group)
        action = g_action_map_lookup_action (G_ACTION_MAP(action_group), action_name);

    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.
 */
GSimpleActionGroup *
gnc_main_window_get_action_group (GncMainWindow *window,
                                  const gchar *group_name)
{
    g_return_val_if_fail (GNC_IS_MAIN_WINDOW(window), nullptr);
    g_return_val_if_fail (group_name != nullptr, nullptr);

    auto action_group = gtk_widget_get_action_group (GTK_WIDGET(window), group_name);
    return (GSimpleActionGroup*)action_group;
}

GtkWidget *
gnc_main_window_toolbar_find_tool_item (GncMainWindow *window, const gchar *action_name)
{
    GncMainWindowPrivate *priv;

    g_return_val_if_fail (GNC_IS_MAIN_WINDOW(window), nullptr);
    g_return_val_if_fail (action_name != nullptr, nullptr);

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    return gnc_find_toolbar_item (priv->toolbar, action_name);
}

GtkWidget *
gnc_main_window_menu_find_menu_item (GncMainWindow *window, const gchar *action_name)
{
    GncMainWindowPrivate *priv;
    GtkWidget *menu_item;

    g_return_val_if_fail (GNC_IS_MAIN_WINDOW(window), nullptr);
    g_return_val_if_fail (action_name != nullptr, nullptr);

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    menu_item = GTK_WIDGET(g_hash_table_lookup (priv->display_item_hash, action_name));

    if (!menu_item)
    {
        menu_item = gnc_menubar_model_find_menu_item (priv->menubar_model, priv->menubar, action_name);

        g_hash_table_insert (priv->display_item_hash, g_strdup (action_name), menu_item);
    }
    return menu_item;
}


void
gnc_main_window_menu_add_accelerator_keys (GncMainWindow *window)
{
    GncMainWindowPrivate *priv;

    g_return_if_fail (GNC_IS_MAIN_WINDOW(window));

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    gnc_add_accelerator_keys_for_menu (priv->menubar, priv->menubar_model, priv->accel_group);
}


gboolean
gnc_main_window_update_menu_for_action (GncMainWindow *window,
                                        const gchar *action_name,
                                        const gchar *label,
                                        const gchar *tooltip)
{
    GncMainWindowPrivate *priv;
    gboolean found = false;

    g_return_val_if_fail (GNC_IS_MAIN_WINDOW(window), false);
    g_return_val_if_fail (action_name != nullptr, false);
    g_return_val_if_fail (label != nullptr, false);

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    found =  gnc_menubar_model_update_item (priv->menubar_model, action_name,
                                            nullptr, _(label), nullptr, _(tooltip));

    // add tooltip redirect call backs
    gnc_plugin_add_menu_tooltip_callbacks (priv->menubar,
                                           priv->menubar_model,
                                           priv->statusbar);

    return found;
}

void
gnc_main_window_set_vis_of_items_by_action (GncMainWindow *window,
                                            const gchar **action_names,
                                            gboolean vis)
{
    GncMainWindowPrivate *priv;

    g_return_if_fail (GNC_IS_MAIN_WINDOW(window));

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    for (gint i = 0; action_names[i]; i++)
    {
        GtkWidget *tool_item = gnc_find_toolbar_item (priv->toolbar, action_names[i]);
        GtkWidget *menu_item = gnc_main_window_menu_find_menu_item (window, action_names[i]);

        if (menu_item)
        {
            PINFO("Found menu_item %p with action name '%s', seting vis to '%s'",
                    menu_item, action_names[i], vis ? "true" : "false");
            gtk_widget_set_visible (menu_item, vis);
        }
        else
            PINFO("Did not find menu_item with action name '%s' to set vis '%s'",
                   action_names[i], vis ? "true" : "false");

        if (tool_item)
        {
            PINFO("Found tool_item %p with action name '%s', seting vis to '%s'",
                    tool_item, action_names[i], vis ? "true" : "false");
            gtk_widget_set_visible (tool_item, vis);
        }
        else
             PINFO("Did not find tool_item with action name '%s' to set vis '%s'",
                    action_names[i], vis ? "true" : "false");
    }
}


void
gnc_main_window_init_short_names (GncMainWindow *window,
                                  GncToolBarShortNames *toolbar_labels)
{
    GncMainWindowPrivate *priv;

    g_return_if_fail (GNC_IS_MAIN_WINDOW(window));
    g_return_if_fail (toolbar_labels != nullptr);

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    gnc_plugin_init_short_names (priv->toolbar, toolbar_labels);
}


static void
gnc_main_window_update_toolbar (GncMainWindow *window, GncPluginPage *page,
                                const gchar *toolbar_qualifier)
{
    GncMainWindowPrivate *priv;
    GtkBuilder *builder;
    GAction *action;

    g_return_if_fail (GNC_IS_MAIN_WINDOW(window));
    g_return_if_fail (GNC_IS_PLUGIN_PAGE(page));

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    builder = gnc_plugin_page_get_builder (page);

    if (builder)
    {
        gchar *toolbar_name;
        gtk_container_remove (GTK_CONTAINER(priv->menu_dock), priv->toolbar);

        if (toolbar_qualifier)
            toolbar_name = g_strconcat ("mainwin-toolbar-", toolbar_qualifier, nullptr);
        else
            toolbar_name = g_strdup ("mainwin-toolbar");

        priv->toolbar = (GtkWidget *)gtk_builder_get_object (builder, toolbar_name);

        if (!priv->toolbar)
            priv->toolbar = (GtkWidget *)gtk_builder_get_object (builder, "mainwin-toolbar");

        g_object_set (priv->toolbar, "toolbar-style", GTK_TOOLBAR_BOTH, NULL);
        gtk_container_add (GTK_CONTAINER(priv->menu_dock), priv->toolbar);
        g_free (toolbar_name);
    }

    action = gnc_main_window_find_action (window, "ViewToolbarAction");

    // set visibility of toolbar
    if (action)
    {
        GVariant *state = g_action_get_state (G_ACTION(action));
        gtk_widget_set_visible (priv->toolbar, g_variant_get_boolean (state));
        g_variant_unref (state);
    }
    // add tooltip redirect call backs
    gnc_plugin_add_toolbar_tooltip_callbacks (priv->toolbar, priv->statusbar);
}


void
gnc_main_window_update_menu_and_toolbar (GncMainWindow *window,
                                         GncPluginPage *page,
                                         const gchar **ui_updates)
{
    GncMainWindowPrivate *priv;
    const gchar *plugin_page_actions_group_name;
    GtkBuilder *builder;
    const gchar *menu_qualifier;

    GMenuModel *menu_model_part;
#ifdef MAC_INTEGRATION
    auto theApp{static_cast<GtkosxApplication *>(g_object_new(GTKOSX_TYPE_APPLICATION, nullptr))};
#endif
    g_return_if_fail (GNC_IS_MAIN_WINDOW(window));
    g_return_if_fail (page != nullptr);
    g_return_if_fail (ui_updates != nullptr);

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    builder = gnc_plugin_page_get_builder (page);

    if (!builder)
        return;

    menu_qualifier = gnc_plugin_page_get_menu_qualifier (page);

    plugin_page_actions_group_name = gnc_plugin_page_get_simple_action_group_name (page);

    if (!plugin_page_actions_group_name)
        return;

    gtk_widget_insert_action_group (GTK_WIDGET(window), gnc_plugin_page_get_simple_action_group_name (page),
                                    G_ACTION_GROUP(gnc_plugin_page_get_action_group (page)));

    if ((g_strcmp0 (priv->previous_plugin_page_name,
                    plugin_page_actions_group_name) == 0) &&
        (g_strcmp0 (priv->previous_menu_qualifier,
                    menu_qualifier) == 0))
        return;

    priv->previous_plugin_page_name = plugin_page_actions_group_name;
    priv->previous_menu_qualifier = menu_qualifier;

    gnc_main_window_update_toolbar (window, page, menu_qualifier);

    // reset hash table and remove added menu items
    g_hash_table_remove_all (priv->display_item_hash);
    gnc_menubar_model_remove_items_with_attrib (priv->menubar_model,
                                                GNC_MENU_ATTRIBUTE_TEMPORARY);

    GncMenuModelSearch *gsm = g_new0 (GncMenuModelSearch, 1);
    for (gint i = 0; ui_updates[i]; i++)
    {
        gchar *menu_name;

        if (menu_qualifier)
            menu_name = g_strconcat (ui_updates[i], "-", menu_qualifier, nullptr);
        else
            menu_name = g_strdup (ui_updates[i]);

        menu_model_part = (GMenuModel *)gtk_builder_get_object (builder, menu_name);

        if (!menu_model_part)
            menu_model_part = (GMenuModel *)gtk_builder_get_object (builder, ui_updates[i]);

        gsm->search_action_label = nullptr;
        gsm->search_action_name = ui_updates[i];
        gsm->search_action_target = nullptr;

        if (gnc_menubar_model_find_item (priv->menubar_model, gsm))
            g_menu_insert_section (G_MENU(gsm->model), gsm->index,
                                   nullptr, G_MENU_MODEL(menu_model_part));
        else
            PERR("Could not find '%s' in menu model", ui_updates[i]);

        g_free (menu_name);
    }

    // add tooltip redirect call backs
    gnc_plugin_add_menu_tooltip_callbacks (priv->menubar, priv->menubar_model, priv->statusbar);

    // need to add the accelerator keys
    gnc_add_accelerator_keys_for_menu (priv->menubar, priv->menubar_model, priv->accel_group);
#ifdef MAC_INTEGRATION
    gtkosx_application_sync_menubar (theApp);
    g_object_unref (theApp);
#endif
    // need to signal menu has been changed
    g_signal_emit_by_name (window, "menu_changed", page);

    g_free (gsm);
}


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

    g_return_if_fail (GNC_IS_MAIN_WINDOW(user_data));

    window = GNC_MAIN_WINDOW(user_data);

    ENTER ("window %p", window);

    /* Ignore notification of the preference that is being set to false when
     * the choice of tab position changes. */
    if (pref && !gnc_prefs_get_bool (GNC_PREFS_GROUP_GENERAL, pref))
        return;

    if (gnc_prefs_get_bool (GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_POSITION_BOTTOM))
    {
        position = GTK_POS_BOTTOM;
        item = 1;
    }
    else if (gnc_prefs_get_bool (GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_POSITION_LEFT))
    {
        position = GTK_POS_LEFT;
        item = 2;
    }
    else if (gnc_prefs_get_bool (GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_POSITION_RIGHT))
    {
        position = GTK_POS_RIGHT;
        item = 3;
    }

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

    action = g_action_map_lookup_action (G_ACTION_MAP(window),
                                         "ViewTabPositionAction");

    g_signal_handlers_block_by_func (G_OBJECT(action),
                                     (gpointer)gnc_main_window_cmd_view_tab_position,
                                     window);
    g_action_change_state (G_ACTION(action), g_variant_new_int32 (item));
    g_signal_handlers_unblock_by_func (G_OBJECT(action),
                                       (gpointer)gnc_main_window_cmd_view_tab_position,
                                       window);

    gnc_main_window_update_tab_width (nullptr, (char*)GNC_PREF_TAB_WIDTH, nullptr);

    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));
    GAction *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), nullptr, nullptr);

        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, nullptr, nullptr);

        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");
    g_simple_action_set_enabled (G_SIMPLE_ACTION(action), can_copy);

    action = gnc_main_window_find_action (window, "EditCutAction");
    g_simple_action_set_enabled (G_SIMPLE_ACTION(action), can_cut);

    action = gnc_main_window_find_action (window, "EditPasteAction");
    g_simple_action_set_enabled (G_SIMPLE_ACTION(action), can_paste);
}

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

    action = gnc_main_window_find_action (window, "EditCopyAction");
    g_simple_action_set_enabled (G_SIMPLE_ACTION(action), true);

    action = gnc_main_window_find_action (window, "EditCutAction");
    g_simple_action_set_enabled (G_SIMPLE_ACTION(action), true);

    action = gnc_main_window_find_action (window, "EditPasteAction");
    g_simple_action_set_enabled (G_SIMPLE_ACTION(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 = gnc_main_window_menu_find_menu_item (window, "EditAction");

    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);
}

/* This is used to prevent the tab having focus */
static gboolean
gnc_main_window_page_focus_in (GtkWidget *widget, GdkEvent  *event,
                               gpointer user_data)
{
    auto window{static_cast<GncMainWindow *>(user_data)};
    GncPluginPage *page = gnc_main_window_get_current_page (window);

    g_signal_emit (window, main_window_signals[PAGE_CHANGED], 0, page);
    return FALSE;
}

static GAction *
gnc_main_window_get_redirect (GncMainWindow *window, const gchar *action_name)
{
    GncMainWindowPrivate *priv;
    GAction *action = nullptr;
    const gchar *group_name;

    g_return_val_if_fail (GNC_IS_MAIN_WINDOW (window), nullptr);
    g_return_val_if_fail (action_name != nullptr, nullptr);

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    group_name = gnc_plugin_page_get_simple_action_group_name (priv->current_page);

    PINFO("action anme is '%s', group_name is '%s'", action_name, group_name);

    if (group_name)
    {
        action = gnc_main_window_find_action_in_group (window, group_name, action_name);

        if (!action)
            action = gnc_plugin_page_get_action (priv->current_page, action_name);
    }

    PINFO("Redirect action is %p for action anme '%s' and group_name '%s'",
           action, action_name, group_name);
    return action;
}

static void
main_window_realize_cb (GtkWidget *widget, gpointer user_data)
{
    GncMainWindow *window = (GncMainWindow*)user_data;
    GncMainWindowPrivate *priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    gnc_add_accelerator_keys_for_menu (GTK_WIDGET(priv->menubar), priv->menubar_model, priv->accel_group);

    /* need to signal menu has been changed, this will call the
       business function 'bind_extra_toolbuttons_visibility' */
    g_signal_emit_by_name (window, "menu_changed", nullptr);
}

static void
gnc_main_window_setup_window (GncMainWindow *window)
{
    GncMainWindowPrivate *priv;
    GtkWidget *main_vbox;
    GtkBuilder *builder;
    GncPluginManager *manager;
    GList *plugins;
    GError *error = nullptr;
    GAction *action;

    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_box_new (GTK_ORIENTATION_VERTICAL, 0);
    gtk_box_set_homogeneous (GTK_BOX (main_vbox), FALSE);
    gtk_widget_show (main_vbox);
    gtk_container_add (GTK_CONTAINER (window), main_vbox);

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    priv->menu_dock = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
    gtk_box_set_homogeneous (GTK_BOX (priv->menu_dock), FALSE);
    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 *)nullptr);
    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);
    g_signal_connect (G_OBJECT (priv->notebook), "focus-in-event",
                      G_CALLBACK (gnc_main_window_page_focus_in), 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);

    priv->progressbar = gtk_progress_bar_new ();
    gtk_progress_bar_set_show_text (GTK_PROGRESS_BAR(priv->progressbar), TRUE);
    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);

    builder = gtk_builder_new ();
    gtk_builder_set_translation_domain (builder, PROJECT_NAME);
    gtk_builder_add_from_resource (builder, GNUCASH_RESOURCE_PREFIX "/gnc-main-window.ui", &error);

    if (error)
    {
        g_critical ("Failed to load, Error %s", error->message);
        g_error_free (error);
        return;
    }

    g_action_map_add_action_entries (G_ACTION_MAP(window),
                                     gnc_menu_actions,
                                     gnc_menu_n_actions,
                                     window);

    priv->menubar_model = (GMenuModel *)gtk_builder_get_object (builder, "mainwin-menu");
    priv->menubar = gtk_menu_bar_new_from_model (priv->menubar_model);
    gtk_container_add (GTK_CONTAINER(priv->menu_dock), priv->menubar);
    gtk_widget_show (GTK_WIDGET(priv->menubar));

    priv->toolbar = (GtkWidget *)gtk_builder_get_object (builder, "mainwin-toolbar");
    g_object_set (priv->toolbar, "toolbar-style", GTK_TOOLBAR_BOTH, NULL);
    gtk_container_add (GTK_CONTAINER(priv->menu_dock), GTK_WIDGET(priv->toolbar));
    gtk_widget_show (GTK_WIDGET(priv->toolbar));

    g_object_unref (builder);

    gnc_plugin_set_actions_enabled (G_ACTION_MAP(window),
                                    initially_insensitive_actions,
                                    FALSE);
    gnc_plugin_set_actions_enabled (G_ACTION_MAP(window),
                                    always_insensitive_actions,
                                    FALSE);

    gnc_main_window_set_vis_of_items_by_action (window, always_hidden_actions,
                                                false);

    gtk_widget_insert_action_group (GTK_WIDGET(window), "mainwin",
                                    G_ACTION_GROUP(window));

    gnc_prefs_register_cb (GNC_PREFS_GROUP_GENERAL,
                           GNC_PREF_TAB_POSITION_TOP,
                           (gpointer)gnc_main_window_update_tab_position,
                           window);
    gnc_prefs_register_cb (GNC_PREFS_GROUP_GENERAL,
                           GNC_PREF_TAB_POSITION_BOTTOM,
                           (gpointer)gnc_main_window_update_tab_position,
                           window);
    gnc_prefs_register_cb (GNC_PREFS_GROUP_GENERAL,
                           GNC_PREF_TAB_POSITION_LEFT,
                           (gpointer)gnc_main_window_update_tab_position,
                           window);
    gnc_prefs_register_cb (GNC_PREFS_GROUP_GENERAL,
                           GNC_PREF_TAB_POSITION_RIGHT,
                           (gpointer)gnc_main_window_update_tab_position,
                           window);
    gnc_main_window_update_tab_position (nullptr, nullptr, window);

    gnc_main_window_init_menu_updaters (window);

    /* Disable the Transaction menu */
    action = gnc_main_window_find_action (window, "TransactionAction");
    g_simple_action_set_enabled (G_SIMPLE_ACTION(action), false);
    /* Disable the Schedule menu */
    action = gnc_main_window_find_action (window, "ScheduledAction");
    g_simple_action_set_enabled (G_SIMPLE_ACTION(action), false);

    /* Now update the "eXtensions" menu */
    if (!gnc_prefs_is_extra_enabled())
    {
        action = gnc_main_window_find_action (window, "ExtensionsAction");
        g_simple_action_set_enabled (G_SIMPLE_ACTION(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);

    // need to add the accelerator keys this way, mainly for --nofile
    g_signal_connect (G_OBJECT(window), "realize",
                      G_CALLBACK(main_window_realize_cb), 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". gnc_main_window_quit() queues
 * a timer that starts an orderly shutdown in 250ms and if we tell macOS it's OK
 * to quit GnuCash gets terminated instead of doing its orderly shutdown,
 * leaving the book locked.
 */
static gboolean
gnc_quartz_should_quit (GtkosxApplication *theApp, GncMainWindow *window)
{
    if (gnc_main_window_all_finish_pending())
        gnc_main_window_quit (window);
    return TRUE;
}
/* Enable GtkMenuItem accelerators */
static gboolean
can_activate_cb(GtkWidget *widget, guint signal_id, gpointer data)
{
    //return gtk_widget_is_sensitive (widget);
    return TRUE;
}

static void
gnc_quartz_set_menu (GncMainWindow* window)
{
    GncMainWindowPrivate *priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    auto theApp{static_cast<GtkosxApplication *>(g_object_new(GTKOSX_TYPE_APPLICATION, nullptr))};
    GtkWidget       *item = nullptr;
    GClosure *quit_closure;

    gtk_widget_hide (priv->menubar);
    gtk_widget_set_no_show_all (priv->menubar, true);

    gtkosx_application_set_menu_bar (theApp, GTK_MENU_SHELL(priv->menubar));

    // File Quit
    item = gnc_main_window_menu_find_menu_item (window, "FileQuitAction");
    if (item)
        gtk_widget_hide (GTK_WIDGET(item));

    quit_closure = g_cclosure_new (G_CALLBACK (gnc_quartz_should_quit),
                                   window, NULL);
    gtk_accel_group_connect (priv->accel_group, 'q', GDK_META_MASK,
                             GTK_ACCEL_MASK, quit_closure);


    // Help About
    item = gnc_main_window_menu_find_menu_item (window, "HelpAboutAction");
    if (item)
    {
        gtk_widget_hide (item);
        gtkosx_application_insert_app_menu_item (theApp, GTK_WIDGET(item), 0);
    }

    // Edit Preferences
    item = gnc_main_window_menu_find_menu_item (window, "EditPreferencesAction");
    if (item)
    {
        gtk_widget_hide (GTK_WIDGET(item));
        gtkosx_application_insert_app_menu_item (theApp, GTK_WIDGET(item), 2);
    }

    // Help Menu
    item = gnc_main_window_menu_find_menu_item (window, "HelpAction");
    if (item)
        gtkosx_application_set_help_menu (theApp, GTK_MENU_ITEM(item));
    // Windows Menu
    item = gnc_main_window_menu_find_menu_item (window, "WindowsAction");
    if (item)
        gtkosx_application_set_window_menu (theApp, GTK_MENU_ITEM(item));

    g_signal_connect (theApp, "NSApplicationBlockTermination",
                      G_CALLBACK(gnc_quartz_should_quit), window);

    g_signal_connect (priv->menubar, "can-activate-accel",
                      G_CALLBACK (can_activate_cb), nullptr);

    gtkosx_application_set_use_quartz_accelerators (theApp, FALSE);
    g_object_unref (theApp);
}
#endif //MAC_INTEGRATION

/* Callbacks */

/** 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 nullptr, the function will look up this action.
 *
 *  @return TRUE if the summarybar should be visible.
 */
static gboolean
gnc_main_window_show_summarybar (GncMainWindow *window, GAction *action)
{
    GVariant *state;
    gboolean visible;

    if (action == nullptr)
        action = g_action_map_lookup_action (G_ACTION_MAP(window),
                                             "ViewSummaryAction");
    if (action == nullptr)
        return TRUE;

    state = g_action_get_state (G_ACTION(action));

    visible = g_variant_get_boolean (state);

    g_variant_unref (state);

    return visible;
}

/** 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 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 != nullptr)
    {
        page = priv->current_page;
        gnc_plugin_page_unselected (page);
    }

    child = gtk_notebook_get_nth_page (notebook, pos);
    if (child)
    {
        page = static_cast<GncPluginPage*>(g_object_get_data (G_OBJECT (child), PLUGIN_PAGE_LABEL));
    }
    else
    {
        page = nullptr;
    }

    priv->current_page = page;

    if (page != nullptr)
    {
        /* Update the user interface (e.g. menus and toolbars */
        gnc_plugin_page_merge_actions (page);
        visible = gnc_main_window_show_summarybar (window, nullptr);
        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_set_actions_enabled (G_ACTION_MAP(window),
                                    multiple_page_actions,
                                    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 = static_cast<GncPluginPage*>(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_redirect (GSimpleAction *simple,
                              GVariant      *parameter,
                              gpointer       user_data)
{
    GncMainWindow *window = (GncMainWindow*)user_data;
    GAction *redirect_action;

    PINFO("Redirect action_is %p, name is '%s'", simple, g_action_get_name (G_ACTION(simple)));

    redirect_action = gnc_main_window_get_redirect (window, g_action_get_name (G_ACTION(simple)));

    if (redirect_action)
    {
        PINFO("Found action %p", redirect_action);
        g_action_activate (redirect_action, nullptr);
        return;
    }
}

static void
gnc_main_window_cmd_page_setup (GSimpleAction *simple,
                                GVariant      *parameter,
                                gpointer       user_data)
{
    GncMainWindow *window = (GncMainWindow*)user_data;
    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);
}

gboolean
gnc_book_options_dialog_apply_helper(GncOptionDB * options)
{
    QofBook *book = gnc_get_current_book ();
    gboolean use_split_action_for_num_before =
        qof_book_use_split_action_for_num_field (book);
    gint use_read_only_threshold_before =
        qof_book_get_num_days_autoreadonly (book);
    gboolean use_split_action_for_num_after;
    gint use_read_only_threshold_after;
    gboolean return_val = FALSE;
    GList *results = nullptr, *iter;

    if (!options) return return_val;

    results = gnc_option_db_commit (options);
    for (iter = results; iter; iter = iter->next)
    {
        GtkWidget *dialog = gtk_message_dialog_new(gnc_ui_get_main_window (nullptr),
                                                   (GtkDialogFlags)0,
                                                   GTK_MESSAGE_ERROR,
                                                   GTK_BUTTONS_OK,
                                                   "%s",
                                                   (char*)iter->data);
        gtk_dialog_run(GTK_DIALOG(dialog));
        gtk_widget_destroy(dialog);
        g_free (iter->data);
    }
    g_list_free (results);
    qof_book_begin_edit (book);
    qof_book_save_options (book, gnc_option_db_save, options, TRUE);
    use_split_action_for_num_after =
        qof_book_use_split_action_for_num_field (book);

    // mark cached value as invalid so we get new value
    book->cached_num_days_autoreadonly_isvalid = FALSE;
    use_read_only_threshold_after = qof_book_get_num_days_autoreadonly (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);
        return_val = TRUE;
    }
    if (use_read_only_threshold_before != use_read_only_threshold_after)
        return_val = TRUE;

    qof_book_commit_edit (book);
    return return_val;
}

static void
gnc_book_options_dialog_apply_cb(GncOptionsDialog * optionwin,
                                 gpointer user_data)
{
    auto options{static_cast<GncOptionDB *>(user_data)};

    if (!options) return;

    if (gnc_book_options_dialog_apply_helper (options))
        gnc_gui_refresh_all ();
}

static void
gnc_book_options_dialog_close_cb(GncOptionsDialog * optionwin,
                                 gpointer user_data)
{
    auto options{static_cast<GncOptionDB *>(user_data)};

    delete optionwin;
    gnc_option_db_destroy(options);
}

/** Calls gnc_book_option_num_field_source_change to initiate registered
 * callbacks when num_field_source book option changes so that
 * registers/reports can update themselves; sets feature flag */
void
gnc_book_option_num_field_source_change_cb (gboolean num_action)
{
    gnc_suspend_gui_refresh ();
    if (num_action)
    {
        /* Set a feature flag in the book for use of the split action field as number.
         * This will prevent older GnuCash versions that don't support this feature
         * from opening this file. */
        gnc_features_set_used (gnc_get_current_book(),
                               GNC_FEATURE_NUM_FIELD_SOURCE);
    }
    gnc_book_option_num_field_source_change (num_action);
    gnc_resume_gui_refresh ();
}

static gboolean
show_handler (const char *class_name, gint component_id,
              gpointer user_data, gpointer iter_data)
{
    auto optwin{static_cast<GncOptionsDialog*>(user_data)};

    if (!optwin)
        return(FALSE);

    auto widget = optwin->get_widget();
    gtk_window_present(GTK_WINDOW(widget));
    return(TRUE);
}

GtkWidget *
gnc_book_options_dialog_cb (gboolean modal, gchar *title, GtkWindow* parent)
{
    auto book = gnc_get_current_book ();

    auto options = gnc_option_db_new();
    gnc_option_db_book_options(options);
    qof_book_load_options (book, gnc_option_db_load, options);
    gnc_option_db_clean (options);

    /* Only allow one Book Options dialog if called from file->properties
       menu */
    if (gnc_forall_gui_components(DIALOG_BOOK_OPTIONS_CM_CLASS,
                                  show_handler, nullptr))
    {
        return nullptr;
    }
    auto optionwin = new GncOptionsDialog (modal,
                                           (title ? title : _( "Book Options")),
                                           DIALOG_BOOK_OPTIONS_CM_CLASS, parent);
    optionwin->build_contents(options);
    optionwin->set_book_help_cb();
    optionwin->set_apply_cb(gnc_book_options_dialog_apply_cb,
                            (gpointer)options);
    optionwin->set_close_cb ( gnc_book_options_dialog_close_cb,
                              (gpointer)options);
    if (modal)
        gnc_options_dialog_set_new_book_option_values (options);
    return optionwin->get_widget();
}

static void
gnc_main_window_cmd_file_properties (GSimpleAction *simple,
                                     GVariant      *parameter,
                                     gpointer       user_data)
{
    GncMainWindow *window = (GncMainWindow*)user_data;
    gnc_book_options_dialog_cb (FALSE, nullptr, GTK_WINDOW (window));
}

static void
gnc_main_window_cmd_file_close (GSimpleAction *simple,
                                GVariant      *parameter,
                                gpointer       user_data)
{
    GncMainWindow *window = (GncMainWindow*)user_data;
    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 (GSimpleAction *simple,
                               GVariant      *parameter,
                               gpointer       user_data)
{
    GncMainWindow *window = (GncMainWindow*)user_data;
    if (!gnc_main_window_all_finish_pending())
        return;

    gnc_main_window_quit(window);
}

static void
gnc_main_window_cmd_edit_cut (GSimpleAction *simple,
                              GVariant      *parameter,
                              gpointer       user_data)
{
    GncMainWindow *window = (GncMainWindow*)user_data;
    GtkWidget *widget = gtk_window_get_focus (GTK_WINDOW(window));
    GAction *redirect_action;

    PINFO("Copy action_is %p, name is '%s'", simple, g_action_get_name (G_ACTION(simple)));

    redirect_action = gnc_main_window_get_redirect (window, g_action_get_name (G_ACTION(simple)));

    if (redirect_action)
    {
        PINFO("Found action %p", redirect_action);
        g_action_activate (redirect_action, nullptr);
        return;
    }

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

        if (clipboard)
            gtk_text_buffer_cut_clipboard (text_buffer, clipboard, editable);
    }
}

static void
gnc_main_window_cmd_edit_copy (GSimpleAction *simple,
                               GVariant      *parameter,
                               gpointer       user_data)
{
    GncMainWindow *window = (GncMainWindow*)user_data;
    GtkWidget *widget = gtk_window_get_focus (GTK_WINDOW(window));
    GAction *redirect_action;

    PINFO("Copy action_is %p, name is '%s'", simple, g_action_get_name (G_ACTION(simple)));

    redirect_action = gnc_main_window_get_redirect (window, g_action_get_name (G_ACTION(simple)));

    if (redirect_action)
    {
        PINFO("Found action %p", redirect_action);
        g_action_activate (redirect_action, nullptr);
        return;
    }

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

static void
gnc_main_window_cmd_edit_paste (GSimpleAction *simple,
                                GVariant      *parameter,
                                gpointer       user_data)
{
    GncMainWindow *window = (GncMainWindow*)user_data;
    GtkWidget *widget = gtk_window_get_focus (GTK_WINDOW(window));
    GAction *redirect_action;

    PINFO("Paste action_is %p, name is '%s'", simple, g_action_get_name (G_ACTION(simple)));

    redirect_action = gnc_main_window_get_redirect (window, g_action_get_name (G_ACTION(simple)));

    if (redirect_action)
    {
        PINFO("Found action %p", redirect_action);
        g_action_activate (redirect_action, nullptr);
        return;
    }

    if (GTK_IS_EDITABLE(widget))
    {
        gtk_editable_paste_clipboard (GTK_EDITABLE(widget));
    }
    else if (GTK_IS_TEXT_VIEW(widget))
    {
        auto clipboard = gtk_widget_get_clipboard (widget, GDK_SELECTION_CLIPBOARD);

        if (clipboard)
        {
            auto text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW(widget));
            auto editable = gtk_text_view_get_editable (GTK_TEXT_VIEW(widget));
            gtk_text_buffer_paste_clipboard (text_buffer, clipboard, nullptr,
                                             editable);
        }
    }
}

static void
gnc_main_window_cmd_edit_preferences (GSimpleAction *simple,
                                      GVariant      *parameter,
                                      gpointer       user_data)
{
    GncMainWindow *window = (GncMainWindow*)user_data;
    gnc_preferences_dialog (GTK_WINDOW(window));
}

static void
gnc_main_window_cmd_view_refresh (GSimpleAction *simple,
                                  GVariant      *parameter,
                                  gpointer       user_data)
{
}

static void
gnc_main_window_cmd_actions_reset_warnings (GSimpleAction *simple,
                                            GVariant      *parameter,
                                            gpointer       user_data)
{
    GncMainWindow *window = (GncMainWindow*)user_data;
    gnc_reset_warnings_dialog(GTK_WINDOW(window));
}

static void
gnc_main_window_cmd_actions_rename_page (GSimpleAction *simple,
                                         GVariant      *parameter,
                                         gpointer       user_data)
{
    GncMainWindow *window = (GncMainWindow*)user_data;
    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 (GSimpleAction *simple,
                                  GVariant      *parameter,
                                  gpointer       user_data)
{
    GncMainWindow *window = (GncMainWindow*)user_data;
    GncMainWindowPrivate *priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    GVariant *state = g_action_get_state (G_ACTION(simple));

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    g_action_change_state (G_ACTION(simple), g_variant_new_boolean (!g_variant_get_boolean (state)));

    if (!g_variant_get_boolean (state))
        gtk_widget_show (priv->toolbar);
    else
        gtk_widget_hide (priv->toolbar);

    g_variant_unref (state);
}

static void
gnc_main_window_cmd_view_summary (GSimpleAction *simple,
                                  GVariant      *parameter,
                                  gpointer       user_data)
{
    GncMainWindow *window = (GncMainWindow*)user_data;
    GncMainWindowPrivate *priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    GList *item;
    gboolean visible;

    visible = gnc_main_window_show_summarybar (window, G_ACTION(simple));

    g_action_change_state (G_ACTION(simple), g_variant_new_boolean (!visible));

    for (item = priv->installed_pages; item; item = g_list_next (item))
    {
        gnc_plugin_page_show_summarybar (static_cast<GncPluginPage*>(item->data),
                                         !visible);
    }
}

static void
gnc_main_window_cmd_view_statusbar (GSimpleAction *simple,
                                    GVariant      *parameter,
                                    gpointer       user_data)
{
    GncMainWindow *window = (GncMainWindow*)user_data;
    GncMainWindowPrivate *priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);
    GVariant *state = g_action_get_state (G_ACTION(simple));

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    g_action_change_state (G_ACTION(simple), g_variant_new_boolean (!g_variant_get_boolean (state)));

    if (!g_variant_get_boolean (state))
        gtk_widget_show (priv->statusbar);
    else
        gtk_widget_hide (priv->statusbar);

    g_variant_unref (state);
}

static void
gnc_main_window_cmd_window_new (GSimpleAction *simple,
                                GVariant      *paramter,
                                gpointer       user_data)
{
    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 (GSimpleAction *simple,
                                      GVariant      *paramter,
                                      gpointer       user_data)
{
    GncMainWindow *window = (GncMainWindow*)user_data;
    GncMainWindowPrivate *priv;
    GncMainWindow *new_window;
    GncPluginPage *page;
    GtkNotebook *notebook;
    GtkWidget *tab_widget, *menu_widget;

    ENTER("action %p, window %p", simple, 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;
    }

#ifndef MAC_INTEGRATION
    if (gnc_list_length_cmp (active_windows, gnc_main_window_max_number) == 0)
        gnc_info_dialog (GTK_WINDOW(window), "%s",
            _("The maximum number of window menu entries reached, no more entries will be added."));
#endif /* !MAC_INTEGRATION */

    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);

    // Remove the page_changed signal callback
    gnc_plugin_page_disconnect_page_changed (GNC_PLUGIN_PAGE(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");
}

static void
gnc_main_window_cmd_view_tab_position (GSimpleAction *simple,
                                       GVariant      *parameter,
                                       gpointer       user_data)
{
    gint item = g_variant_get_int32 (parameter);

    g_action_change_state (G_ACTION(simple), parameter);

    if (item < 0 || item > 3)
        return;

    if (item != 0 && gnc_prefs_get_bool (GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_POSITION_TOP))
        gnc_prefs_set_bool (GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_POSITION_TOP, FALSE);

    if (item != 1 && gnc_prefs_get_bool (GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_POSITION_BOTTOM))
        gnc_prefs_set_bool (GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_POSITION_BOTTOM, FALSE);

    if (item != 2 && gnc_prefs_get_bool (GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_POSITION_LEFT))
        gnc_prefs_set_bool (GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_POSITION_LEFT, FALSE);

    if (item != 3 && gnc_prefs_get_bool (GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_POSITION_RIGHT))
        gnc_prefs_set_bool (GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_POSITION_RIGHT, FALSE);

    switch (item)
    {
    case 0:
        gnc_prefs_set_bool (GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_POSITION_TOP, TRUE);
        break;

    case 1:
        gnc_prefs_set_bool (GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_POSITION_BOTTOM, TRUE);
        break;

    case 2:
        gnc_prefs_set_bool (GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_POSITION_LEFT, TRUE);
        break;

    case 3:
        gnc_prefs_set_bool (GNC_PREFS_GROUP_GENERAL, GNC_PREF_TAB_POSITION_RIGHT, TRUE);
        break;
    }

}

#ifndef MAC_INTEGRATION
static void
gnc_main_window_cmd_window_raise (GSimpleAction *simple,
                                  GVariant      *parameter,
                                  gpointer       user_data)
{
    GncMainWindow *window = (GncMainWindow*)user_data;
    GncMainWindow *new_window;
    gint item;

    g_return_if_fail (G_IS_SIMPLE_ACTION(simple));
    g_return_if_fail (GNC_IS_MAIN_WINDOW(window));

    item = g_variant_get_int32 (parameter);

    ENTER("action %p, window %p, item %d", simple, window, item);

    g_action_change_state (G_ACTION(simple), parameter);

    new_window = static_cast<GncMainWindow*>(g_list_nth_data (active_windows, item));
    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, window);
    LEAVE(" ");
}
#endif /* !MAC_INTEGRATION */

static void
gnc_main_window_cmd_help_tutorial (GSimpleAction *simple,
                                   GVariant      *paramter,
                                   gpointer       user_data)
{
    GncMainWindow *window = (GncMainWindow*)user_data;
    gnc_gnome_help (GTK_WINDOW(window), DF_GUIDE, NULL);
}

static void
gnc_main_window_cmd_help_contents (GSimpleAction *simple,
                                   GVariant      *paramter,
                                   gpointer       user_data)
{
    GncMainWindow *window = (GncMainWindow*)user_data;
    gnc_gnome_help (GTK_WINDOW(window), DF_MANUAL, 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 nullptr. The caller is responsible
 *  for freeing this string.
 */
static gchar *
get_file (const gchar *partial)
{
    gchar *filename, *text = nullptr;
    gsize length;

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


/** 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 nullptr. 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 nullptr;

    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_doclink (GTK_WINDOW(dialog), uri);
    return TRUE;
}

static gboolean
link_button_cb (GtkLinkButton *button, gpointer user_data)
{
   const gchar *uri = gtk_link_button_get_uri (button);
   gchar *escaped_uri = g_uri_escape_string (uri, ":/.\\", true);
   gnc_launch_doclink (GTK_WINDOW(user_data), escaped_uri);
   g_free (escaped_uri);
   return TRUE;
}

static void
add_about_paths (GtkDialog *dialog)
{
    GtkWidget *page_vbox = gnc_get_dialog_widget_from_id (dialog, "page_vbox");
    GtkWidget *grid;
    gint i = 0;

    if (!page_vbox)
    {
        PWARN("Unable to find AboutDialog 'page_vbox' Widget");
        return;
    }

    grid = gtk_grid_new ();

    for (const auto& ep : gnc_list_all_paths ())
    {
        gchar *env_name = g_strconcat (ep.env_name, ":", NULL);
        GtkWidget *label = gtk_label_new (env_name);
        const gchar *uri = gnc_uri_create_uri ("file", NULL, 0, NULL, NULL, ep.env_path);
        gchar *display_uri = gnc_doclink_get_unescaped_just_uri (uri);
        GtkWidget *widget = gtk_link_button_new_with_label (uri, display_uri);

        gtk_grid_attach (GTK_GRID(grid), label, 0, i, 1, 1);
        gtk_widget_set_halign (label, GTK_ALIGN_END);
        gtk_grid_attach (GTK_GRID(grid), widget, 1, i, 1, 1);
        gtk_widget_set_halign (widget, GTK_ALIGN_START);
        gtk_widget_set_margin_top (widget, 0);
        gtk_widget_set_margin_bottom (widget, 0);

        if (ep.modifiable)
        {
            GtkWidget *mod_lab = gtk_label_new (_("(user modifiable)"));
            gtk_grid_attach (GTK_GRID(grid), mod_lab, 2, i, 1, 1);
            gtk_widget_show (mod_lab);
        }
        g_signal_connect (widget, "activate-link",
                          G_CALLBACK(link_button_cb), dialog);
        i++;

        g_free (display_uri);
        g_free (env_name);
    }
    gtk_container_add_with_properties (GTK_CONTAINER(page_vbox), grid,
                                       "position", 1, NULL);
    gtk_widget_show_all (grid);
}

/** Create and display the "about" dialog for gnucash.
 */
static void
gnc_main_window_cmd_help_about (GSimpleAction *simple,
                                GVariant      *paramter,
                                gpointer       user_data)
{
    GncMainWindow *window = (GncMainWindow*)user_data;
    /* Translators: %s will be replaced with the current year */
    gchar *copyright = g_strdup_printf(_("Copyright © 1997-%s The GnuCash contributors."),
                                       GNC_VCS_REV_YEAR);
    gchar **authors = get_file_strsplit("AUTHORS");
    gchar **documenters = get_file_strsplit("DOCUMENTERS");
    gchar *license = get_file("LICENSE");
    GtkIconTheme *icon_theme = gtk_icon_theme_get_default ();
    GdkPixbuf *logo = gtk_icon_theme_load_icon (icon_theme,
                                                GNC_ICON_APP,
                                                128,
                                                GTK_ICON_LOOKUP_USE_BUILTIN,
                                                nullptr);
    gchar *version = g_strdup_printf ("%s: %s\n%s: %s\nFinance::Quote: %s",
                                      _("Version"), gnc_version(),
                                      _("Build ID"), gnc_build_id(),
                                      gnc_quote_source_fq_version ()
                                      ? gnc_quote_source_fq_version ()
                                      : "-");
    GtkDialog *dialog = GTK_DIALOG (gtk_about_dialog_new ());
    g_object_set (G_OBJECT (dialog),
                  "authors", authors,
                  "documenters", documenters,
                  "comments", _("Accounting for personal and small business finance."),
                  "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", PACKAGE_URL,
                  "website-label", _("Visit the GnuCash website."),
                  nullptr);

    g_free(version);
    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 (dialog, "activate-link",
                      G_CALLBACK (url_signal_cb), nullptr);

    // Add environment paths
    add_about_paths (dialog);

    /* Set dialog to resize. */
    gtk_window_set_resizable(GTK_WINDOW (dialog), TRUE);

    gtk_window_set_transient_for (GTK_WINDOW (dialog),
                                  GTK_WINDOW (window));
    gtk_dialog_run (dialog);
    gtk_widget_destroy (GTK_WIDGET (dialog));
}


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

void
gnc_main_window_show_all_windows(void)
{
    GList *window_iter;
#ifdef MAC_INTEGRATION
    auto theApp{static_cast<GtkosxApplication *>(g_object_new(GTKOSX_TYPE_APPLICATION, nullptr))};
#endif
    for (window_iter = active_windows; window_iter != nullptr; 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), nullptr);
    gtkosx_application_ready(theApp);
    g_object_unref (theApp);
#endif
}

GtkWindow *
gnc_ui_get_gtk_window (GtkWidget *widget)
{
    GtkWidget *toplevel;

    if (!widget)
        return nullptr;

    toplevel = gtk_widget_get_toplevel (widget);
    if (toplevel && GTK_IS_WINDOW (toplevel))
        return GTK_WINDOW (toplevel);
    else
        return nullptr;
}

GtkWindow *
gnc_ui_get_main_window (GtkWidget *widget)
{
    GList *window;

    GtkWindow *toplevel = gnc_ui_get_gtk_window (widget);
    while (toplevel && !GNC_IS_MAIN_WINDOW (toplevel))
        toplevel = gtk_window_get_transient_for(toplevel);

    if (toplevel)
        return toplevel;

    for (window = active_windows; window; window = window->next)
        if (gtk_window_is_active (GTK_WINDOW (window->data)))
            return static_cast<GtkWindow*>(window->data);

    for (window = active_windows; window; window = window->next)
        if (gtk_widget_get_mapped (GTK_WIDGET(window->data)))
            return static_cast<GtkWindow*>(window->data);

    return nullptr;
}


/** 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), nullptr);
    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), nullptr);

    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), nullptr);

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


/** Retrieve the menu 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_menubar (GncWindow *window)
{
    GncMainWindowPrivate *priv;

    g_return_val_if_fail (GNC_IS_MAIN_WINDOW(window), nullptr);

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    return priv->menubar;
}

/** Retrieve the tool 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_toolbar (GncWindow *window)
{
    GncMainWindowPrivate *priv;

    g_return_val_if_fail (GNC_IS_MAIN_WINDOW(window), nullptr);

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    return priv->toolbar;
}

/** Retrieve the menubar model 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 GMenuModel *
gnc_main_window_get_menubar_model (GncWindow *window)
{
    GncMainWindowPrivate *priv;

    g_return_val_if_fail (GNC_IS_MAIN_WINDOW(window), nullptr);

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    return priv->menubar_model;
}

/** Retrieve the accelerator group 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 GtkAccelGroup *
gnc_main_window_get_accel_group (GncWindow *window)
{
    GncMainWindowPrivate *priv;

    g_return_val_if_fail (GNC_IS_MAIN_WINDOW(window), nullptr);

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    return priv->accel_group;
}

/** 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 (GncWindowInterface *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->get_menubar         = gnc_main_window_get_menubar;
    iface->get_toolbar         = gnc_main_window_get_toolbar;
    iface->get_menubar_model   = gnc_main_window_get_menubar_model;
    iface->get_accel_group     = gnc_main_window_get_accel_group;
}


/*  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)
{
    GtkBuilder *builder;
    GMenuModel *menu_model;
    GtkWidget *menu;
    const gchar *menu_qualifier;
    gchar *popup_menu_name;
    GncWindow* gnc_window;
    GtkWidget *statusbar;

    g_return_if_fail (GNC_IS_PLUGIN_PAGE(page));

    ENTER("page %p, event %p", page, event);

    gnc_window = GNC_WINDOW(GNC_PLUGIN_PAGE(page)->window);

    statusbar = gnc_window_get_statusbar (gnc_window);

    builder = gnc_plugin_page_get_builder (page);

    menu_qualifier = gnc_plugin_page_get_menu_popup_qualifier (page);

    if (!menu_qualifier)
        menu_qualifier = gnc_plugin_page_get_menu_qualifier (page);

    if (builder == nullptr)
    {
        LEAVE("no builder");
        return;
    }

    if (menu_qualifier)
        popup_menu_name = g_strconcat ("mainwin-popup-", menu_qualifier, nullptr);
    else
        popup_menu_name = g_strdup ("mainwin-popup");

    menu_model = (GMenuModel *)gtk_builder_get_object (builder, popup_menu_name);

    if (!menu_model)
        menu_model = (GMenuModel *)gtk_builder_get_object (builder, "mainwin-popup");

    menu = gtk_menu_new_from_model (menu_model);

    if (!menu)
    {
        LEAVE("no menu");
        return;
    }

    // add tooltip redirect call backs
    gnc_plugin_add_menu_tooltip_callbacks (menu, menu_model, statusbar);

    gtk_menu_attach_to_widget (GTK_MENU(menu), GTK_WIDGET(page->window), nullptr);
    gtk_menu_popup_at_pointer (GTK_MENU(menu), (GdkEvent *) event);

    g_free (popup_menu_name);

    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.
 */
gboolean
gnc_main_window_popup_menu_cb (GtkWidget *widget,
                               GncPluginPage *page)
{
    ENTER("widget %p, page %p", widget, page);
    do_popup_menu(page, nullptr);
    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;
}

void
gnc_main_window_all_action_set_sensitive (const gchar *action_name,
                                          gboolean sensitive)
{
    for (auto tmp = active_windows; tmp; tmp = g_list_next(tmp))
    {
        auto action{gnc_main_window_find_action (static_cast<GncMainWindow*>(tmp->data), action_name)};
        g_simple_action_set_enabled (G_SIMPLE_ACTION(action), sensitive);
    }
}

GMenuModel *
gnc_main_window_get_menu_model (GncMainWindow *window)
{
    GncMainWindowPrivate *priv;

    g_return_val_if_fail (GNC_IS_MAIN_WINDOW(window), nullptr);

    priv = GNC_MAIN_WINDOW_GET_PRIVATE(window);

    return priv->menubar_model;
}

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