/*
 * SwamiguiRoot.c - Swami main GUI object
 *
 * Swami
 * Copyright (C) 1999-2014 Element Green <element@elementsofsound.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; version 2
 * of the License only.
 *
 * 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA or point your web browser to http://www.gnu.org.
 */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <stdio.h>
#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>
#include <string.h>
#include <errno.h>

#include <libswami/libswami.h>
#include <libswami/version.h>
#include <libinstpatch/IpatchParamProp.h>
#include <libinstpatch/IpatchXml.h>
#ifdef MAC_INTEGRATION
#include <gtkosxapplication.h>
#endif

#include "swamigui.h"
#include "i18n.h"
#include "../libswami/swami_priv.h"

enum
{
    PROP_0,
    PROP_MAIN_WINDOW,
    PROP_UPDATE_INTERVAL,
    PROP_QUIT_CONFIRM,
    PROP_SPLASH_ENABLE,
    PROP_SPLASH_DELAY,
    PROP_TIPS_ENABLE,
    PROP_TIPS_POSITION,
    PROP_PIANO_LOWER_KEYS,
    PROP_PIANO_UPPER_KEYS,
    PROP_DEFAULT_PATCH_TYPE,
    PROP_MIDDLE_EMUL_ENABLE,
    PROP_MIDDLE_EMUL_MOD,
    PROP_TREE_STORE_LIST,
    PROP_SELECTION_ORIGIN,
    PROP_SELECTION,
    PROP_SELECTION_SINGLE,
    PROP_SOLO_ITEM_ENABLE
};

enum
{
    QUIT,
    LAST_SIGNAL
};

/* Default splash delay in milliseconds */
#define SWAMIGUI_ROOT_DEFAULT_SPLASH_DELAY 5000

#define SWAMIGUI_ROOT_DEFAULT_LOWER_KEYS  "z,s,x,d,c,v,g,b,h,n,j,m,comma,l,period,semicolon,slash"
#define SWAMIGUI_ROOT_DEFAULT_UPPER_KEYS  "q,2,w,3,e,r,5,t,6,y,7,u,i,9,o,0,p,bracketleft,equal,bracketright"

/* external private global prototypes */
void  swamigui_util_destroy_unique_dialog(void);

void _swamigui_ifaces_init(void);  /* ifaces/ifaces.c */
void _swamigui_stock_icons_init(void);  /* icons.c */
void _swamigui_control_init(void);  /* SwamiguiControl.c */
void _swamigui_control_widgets_init(void);  /* SwamiguiControl_widgets.c */
void _swamigui_item_menu_init(void);
void _patch_funcs_init(void); /* General instrument patch functions */
void _swamigui_knob_init(void);
void _swamigui_pref_init(void); /* Preferences functions */
void _swamigui_sample_editor_init(void);
void _swamigui_splits_init(void);
void _swamigui_item_menu_actions_init(void);	/* SwamiguiItemMenu_actions.c */
void _swamigui_tree_store_init(void);
void _swamigui_panel_selector_init(void);
void _swamigui_prop_init(void);
void swami_deinit(void); /* free libswami  */
void swamigui_util_deinit(void);
void _swamigui_control_deinit(void); /* free GUI control system */
void _swamigui_item_menu_deinit(void);
void _patch_funcs_deinit(void); /* General instrument patch functions */
void _swamigui_pref_deinit(void);
void _swamigui_sample_editor_deinit(void);
void _swamigui_splits_deinit(void);
void _swamigui_knob_deinit(void);
void _swamigui_tree_store_deinit(void);
void _swamigui_panel_selector_deinit(void);
void _swamigui_prop_deinit(void);
void  swami_plugin_unload_all(void);

#ifdef PYTHON_SUPPORT
void _swamigui_python_init(int argc, char **argv);  /* in swami_python.c */
#endif

/* Local Prototypes */

static void swamigui_root_class_init(SwamiguiRootClass *klass);
static void swamigui_root_set_property(GObject *object, guint property_id,
                                       const GValue *value, GParamSpec *pspec);
static void swamigui_root_get_property(GObject *object, guint property_id,
                                       GValue *value, GParamSpec *pspec);
static void swamigui_root_quit_method(SwamiguiRoot *root);

static void swamigui_root_init(SwamiguiRoot *obj);
static void swamigui_root_finalize(GObject *object);
static gboolean swamigui_queue_test_func(SwamiControlQueue *queue,
        SwamiControl *control,
        SwamiControlEvent *event);
static gboolean swamigui_update_gui_timeout(gpointer data);

static void ctrl_prop_set_func(SwamiControl *control,
                               SwamiControlEvent *event, const GValue *value);
static void ctrl_add_set_func(SwamiControl *control, SwamiControlEvent *event,
                              const GValue *value);
static void ctrl_remove_set_func(SwamiControl *control,
                                 SwamiControlEvent *event,
                                 const GValue *value);

static void swamigui_cb_quit_response(GtkDialog *dialog, int response,
                                      gpointer user_data);
static void swamigui_real_quit(SwamiguiRoot *root);
static void swamigui_root_create_main_window(SwamiguiRoot *root);
static void swamigui_root_cb_solo_item(SwamiguiRoot *root, GParamSpec *pspec,
                                       gpointer user_data);
static void swamigui_root_update_solo_item(SwamiguiRoot *root, GObject *solo_item);
static void swamigui_root_connect_midi_keyboard_controls(SwamiguiRoot *root,
        GObject *midikey);
static gint swamigui_root_cb_main_window_delete(GtkWidget *widget,
        GdkEvent *event, gpointer data);
static guint *swamigui_root_parse_piano_keys(const char *str);
static char *swamigui_root_encode_piano_keys(const guint *keyvals);
static GtkWidget *sf2_prop_handler(GtkWidget *widg, GObject *obj);
static void current_date_btn_clicked(GtkButton *button, gpointer user_data);
static void cat_map_to_tree_store(GtkTreeStore *store,
                                  const IpatchSLIInstCatMapEntry *catmap,
                                  GtkTreeIter *parent);
static void category_changed_cb(GtkComboBox *combo, gpointer user_data);
static GtkWidget *sli_inst_prop_handler(GtkWidget *widg, GObject *obj);

#ifdef MAC_INTEGRATION
static void
osx_accel_map_foreach_lcb(gpointer data, const gchar *accel_path,
                          guint accel_key, GdkModifierType accel_mods,
                          gboolean changed);
static gboolean swamigui_root_cb_should_quit(GtkosxApplication *theApp,
        gpointer data);
#endif

static guint uiroot_signals[LAST_SIGNAL] = { 0 };

SwamiguiRoot *swamigui_root = NULL;
SwamiRoot *swami_root = NULL;

/* global enable bools, kind of hackish, but what is better? */
gboolean swamigui_disable_python =  TRUE;
gboolean swamigui_disable_plugins = FALSE;

static GObjectClass *parent_class = NULL;

/* used to determine if current thread is the GUI thread */
static GStaticPrivate is_gui_thread = G_STATIC_PRIVATE_INIT;

/* indicate that the library is initialized */
static gboolean initialized = FALSE;

/**
 * swamigui_init:
 * @argc: Number of arguments from main()
 * @argv: Command line arguments from main()
 *
 * Function to initialize Swami User Interface. Should be called before any
 * other Swamigui related functions. This function calls swami_init() as well.
 */
void
swamigui_init(int *argc, char **argv[])
{
    if(initialized)
    {
        return;
    }

    initialized = TRUE;

    /* initialize glib thread support (if it hasn't been already!) */
    if(!g_thread_supported())
    {
        g_thread_init(NULL);
    }

    gtk_set_locale();
    gtk_init_check(argc, argv);	/* initialize GTK */

    /* set the application/program name, so things work right with recent file chooser
     * even when binary name is different (such as lt-swami) */
    g_set_application_name("swami");

#ifdef MAC_INTEGRATION
    /* Create singleton app instance at startup, later calls will return this */
    GtkosxApplication *theApp = g_object_new(GTKOSX_TYPE_APPLICATION, NULL);
    theApp = NULL; /* we don't use it now, silence compiler warning */
#endif

    swami_init();

    /* install icon parameter property (for assigning icons to effect controls) */
    ipatch_param_install_property
    (g_param_spec_string("icon", "Icon", "Icon",
                         NULL, G_PARAM_READWRITE));

    /* install icon property for types (for assigning icons to object types) */
    ipatch_type_install_property
    (g_param_spec_string("icon", "Icon", "Icon",
                         NULL, G_PARAM_READWRITE));

    /* set type specific icons */
    ipatch_type_set(IPATCH_TYPE_DLS2, "icon", SWAMIGUI_STOCK_DLS, NULL);
    ipatch_type_set(IPATCH_TYPE_GIG, "icon", SWAMIGUI_STOCK_GIG, NULL);
    ipatch_type_set(IPATCH_TYPE_SLI, "icon", SWAMIGUI_STOCK_SLI, NULL);
    ipatch_type_set(IPATCH_TYPE_SF2, "icon", SWAMIGUI_STOCK_SOUNDFONT, NULL);

    swamigui_util_init();
    _swamigui_stock_icons_init();
    _swamigui_control_init();
    _swamigui_control_widgets_init();
    _swamigui_item_menu_init();
    /* General instrument patch functions */
    _patch_funcs_init();


    /* initialize Swamigui types */
    swamigui_bar_get_type();
    swamigui_bar_ptr_get_type();
    swamigui_bar_ptr_type_get_type();
    swamigui_control_adj_get_type();
    swamigui_control_flags_get_type();
    swamigui_control_midi_key_get_type();
    swamigui_control_object_flags_get_type();
    swamigui_control_rank_get_type();
    swamigui_item_menu_flags_get_type();
    swamigui_item_menu_get_type();
    /* knob widget */
    _swamigui_knob_init();
    swamigui_knob_get_type();
    swamigui_menu_get_type();
    swamigui_mod_edit_get_type();
    swamigui_panel_get_type();
    swamigui_panel_selector_get_type();
    swamigui_panel_sf2_gen_get_type();
    swamigui_paste_decision_get_type();
    swamigui_paste_get_type();
    swamigui_paste_status_get_type();
    swamigui_piano_get_type();
    /* preferences functions */
    _swamigui_pref_init();
    swamigui_pref_get_type();
    swamigui_prop_get_type();
    swamigui_quit_confirm_get_type();
    swamigui_root_get_type();
    swamigui_sample_canvas_get_type();
    /* Sample editor functions */
    _swamigui_sample_editor_init();
    swamigui_sample_editor_get_type();
    swamigui_sample_editor_marker_flags_get_type();
    swamigui_sample_editor_marker_id_get_type();
    swamigui_sample_editor_status_get_type();
    swamigui_spectrum_canvas_get_type();
    swamigui_spin_scale_get_type();
    _swamigui_splits_init();
    swamigui_splits_get_type();
    swamigui_splits_mode_get_type();
    swamigui_splits_status_get_type();
    swamigui_statusbar_get_type();
    swamigui_statusbar_pos_get_type();
    swamigui_tree_get_type();
    _swamigui_tree_store_init();
    swamigui_tree_store_get_type();
    swamigui_tree_store_patch_get_type();
    swamigui_tree_store_config_get_type();
    swamigui_util_unit_rgba_color_get_type();

    _swamigui_item_menu_actions_init();

    /* Register panel types and their order */
    _swamigui_panel_selector_init(); /* Must be called before registering */
    swamigui_register_panel_selector_type(SWAMIGUI_TYPE_PROP, 80);
    swamigui_register_panel_selector_type(SWAMIGUI_TYPE_SAMPLE_EDITOR, 90);
    swamigui_register_panel_selector_type(SWAMIGUI_TYPE_PANEL_SF2_GEN_MISC, 100);
    swamigui_register_panel_selector_type(SWAMIGUI_TYPE_PANEL_SF2_GEN_ENV, 105);
    swamigui_register_panel_selector_type(SWAMIGUI_TYPE_MOD_EDIT, 110);

    /* Register prop widget types */
    _swamigui_prop_init(); /* Must be called before registering */
    swamigui_register_prop_handler(IPATCH_TYPE_SF2, sf2_prop_handler);
    swamigui_register_prop_glade_widg(IPATCH_TYPE_SF2_PRESET, "PropSF2Preset");
    swamigui_register_prop_glade_widg(IPATCH_TYPE_SF2_INST, "PropSF2Inst");
    swamigui_register_prop_glade_widg(IPATCH_TYPE_SF2_IZONE, "PropSF2IZone");
    swamigui_register_prop_glade_widg(IPATCH_TYPE_SF2_SAMPLE, "PropSF2Sample");

    swamigui_register_prop_handler(IPATCH_TYPE_SLI_INST, sli_inst_prop_handler);
    swamigui_register_prop_glade_widg(IPATCH_TYPE_SLI_ZONE, "PropSF2IZone");
    swamigui_register_prop_glade_widg(IPATCH_TYPE_SLI_SAMPLE, "PropSF2Sample");

#ifdef PYTHON_SUPPORT

    if(!swamigui_disable_python)
    {
        _swamigui_python_init(*argc, *argv);
        swamigui_python_view_get_type();
    }

#endif

    if(!swamigui_disable_plugins)
    {
        swami_plugin_load_all();    /* load plugins */
    }
}

/**
 * swamigui_deinit:
 *
 * Function to deinitialize Swami User Interface. Should be called
 * when the application is completed.
 * This function calls swami_deinit() as well.
 */
void
swamigui_deinit(void)
{
    if (!initialized)
    {
        return; /* does nothing because swamigui has not been initialized */
    }
    initialized = FALSE;

    /* unload plugins if enabled */
    if (!swamigui_disable_plugins)
    {
        swami_plugin_unload_all();
    }

    /* Free property type registry */
    _swamigui_prop_deinit();

    /* Free pannel list */
    _swamigui_panel_selector_deinit();

    /* free SwamiguiTreeStore functions */
    _swamigui_tree_store_deinit();

    /* Free SwamiguiSplits functions */
    _swamigui_splits_deinit();

    /* Free Sample editor functions */
    _swamigui_sample_editor_deinit();

    /* free preferences functions */
    _swamigui_pref_deinit();

    /* free knob widget functions */
    _swamigui_knob_deinit();

    /* free General instrument patch functions */
    _patch_funcs_deinit();

    _swamigui_item_menu_deinit();

    /* Free GUI control system */
    _swamigui_control_deinit();

    swamigui_util_deinit();

    /* Free libswami library */
    swami_deinit();
}

/* Function used by libglade to initialize libswamigui widgets */
void
glade_module_register_widgets(void)
{
    swamigui_init(0, NULL);
}

GType
swamigui_root_get_type(void)
{
    static GType item_type = 0;

    if(!item_type)
    {
        static const GTypeInfo item_info =
        {
            sizeof(SwamiguiRootClass), NULL, NULL,
            (GClassInitFunc) swamigui_root_class_init, NULL, NULL,
            sizeof(SwamiguiRoot), 0,
            (GInstanceInitFunc) swamigui_root_init,
        };

        item_type = g_type_register_static(SWAMI_TYPE_ROOT, "SwamiguiRoot",
                                           &item_info, 0);
    }

    return (item_type);
}

static void
swamigui_root_class_init(SwamiguiRootClass *klass)
{
    GObjectClass *obj_class = G_OBJECT_CLASS(klass);

    parent_class = g_type_class_peek_parent(klass);

    obj_class->set_property = swamigui_root_set_property;
    obj_class->get_property = swamigui_root_get_property;
    obj_class->finalize = swamigui_root_finalize;

    klass->quit = swamigui_root_quit_method;

    uiroot_signals[QUIT] =
        g_signal_new("quit", G_TYPE_FROM_CLASS(klass),
                     G_SIGNAL_RUN_LAST,
                     G_STRUCT_OFFSET(SwamiguiRootClass, quit), NULL, NULL,
                     g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);

    g_object_class_install_property(obj_class, PROP_MAIN_WINDOW,
                                    g_param_spec_object("main-window",
                                            _("Main window"),
                                            _("Main window"),
                                            GTK_TYPE_WIDGET,
                                            G_PARAM_READABLE | IPATCH_PARAM_NO_SAVE));
    g_object_class_install_property(obj_class, PROP_UPDATE_INTERVAL,
                                    g_param_spec_int("update-interval",
                                            _("Update interval"),
                                            _("GUI update interval in milliseconds"),
                                            10, 1000, 100, G_PARAM_READWRITE));
    g_object_class_install_property(obj_class, PROP_QUIT_CONFIRM,
                                    g_param_spec_enum("quit-confirm",
                                            _("Quit confirm"),
                                            _("Quit confirmation method"),
                                            SWAMIGUI_TYPE_QUIT_CONFIRM,
                                            SWAMIGUI_QUIT_CONFIRM_UNSAVED,
                                            G_PARAM_READWRITE));
    g_object_class_install_property(obj_class, PROP_SPLASH_ENABLE,
                                    g_param_spec_boolean("splash-enable",
                                            _("Splash image enable"),
                                            _("Show splash on startup"),
                                            TRUE, G_PARAM_READWRITE));
    g_object_class_install_property(obj_class, PROP_SPLASH_DELAY,
                                    g_param_spec_uint("splash-delay",
                                            _("Splash delay"),
                                            _("Splash delay in milliseconds (0 to wait for button click)"),
                                            0, G_MAXUINT, SWAMIGUI_ROOT_DEFAULT_SPLASH_DELAY,
                                            G_PARAM_READWRITE));
    g_object_class_install_property(obj_class, PROP_TIPS_ENABLE,
                                    g_param_spec_boolean("tips-enable",
                                            _("Tips enable"),
                                            _("Show tips on startup"),
                                            TRUE, G_PARAM_READWRITE));
    g_object_class_install_property(obj_class, PROP_TIPS_POSITION,
                                    g_param_spec_int("tips-position",
                                            "Tips position",
                                            "Tips position",
                                            0, 255, 0, G_PARAM_READWRITE));
    g_object_class_install_property(obj_class, PROP_PIANO_LOWER_KEYS,
                                    g_param_spec_string("piano-lower-keys",
                                            "Piano lower keys",
                                            "Comma delimited list of GDK key names",
                                            SWAMIGUI_ROOT_DEFAULT_LOWER_KEYS,
                                            G_PARAM_READWRITE));
    g_object_class_install_property(obj_class, PROP_PIANO_UPPER_KEYS,
                                    g_param_spec_string("piano-upper-keys",
                                            "Piano upper keys",
                                            "Comma delimited list of GDK key names",
                                            SWAMIGUI_ROOT_DEFAULT_UPPER_KEYS,
                                            G_PARAM_READWRITE));
    g_object_class_install_property(obj_class, PROP_DEFAULT_PATCH_TYPE,
                                    g_param_spec_gtype("default-patch-type",
                                            "Default patch type",
                                            "Default patch type",
                                            IPATCH_TYPE_BASE,
                                            G_PARAM_READWRITE));
    g_object_class_install_property(obj_class, PROP_MIDDLE_EMUL_ENABLE,
                                    g_param_spec_boolean("middle-emul-enable",
                                            _("Middle button emulation enable"),
                                            _("Enable middle mouse button emulation"),
                                            TRUE, G_PARAM_READWRITE));
    g_object_class_install_property(obj_class, PROP_MIDDLE_EMUL_MOD,
                                    g_param_spec_int("middle-emul-mod",
                                            _("Middle button emulation modifier"),
                                            _("Middle mouse button emulation key modifier"),
                                            0, 4, 0, G_PARAM_READWRITE));
    g_object_class_install_property(obj_class, PROP_TREE_STORE_LIST,
                                    g_param_spec_object("tree-store-list", "Tree store list",
                                            "List of tree stores",
                                            IPATCH_TYPE_LIST,
                                            G_PARAM_READABLE | IPATCH_PARAM_NO_SAVE));
    g_object_class_install_property(obj_class, PROP_SELECTION_ORIGIN,
                                    g_param_spec_object("selection-origin", "Selection origin",
                                            "Origin of selection",
                                            G_TYPE_OBJECT,
                                            G_PARAM_READABLE | IPATCH_PARAM_NO_SAVE));
    g_object_class_install_property(obj_class, PROP_SELECTION,
                                    g_param_spec_object("selection", "Item selection",
                                            "Last item selection",
                                            IPATCH_TYPE_LIST,
                                            G_PARAM_READWRITE | IPATCH_PARAM_NO_SAVE));
    g_object_class_install_property(obj_class, PROP_SELECTION_SINGLE,
                                    g_param_spec_object("selection-single", "Single item selection",
                                            "Last single selected item",
                                            G_TYPE_OBJECT,
                                            G_PARAM_READWRITE | IPATCH_PARAM_NO_SAVE));
    g_object_class_install_property(obj_class, PROP_SOLO_ITEM_ENABLE,
                                    g_param_spec_boolean("solo-item-enable", "Solo item enable",
                                            "Enable solo audition of active instrument",
                                            FALSE, G_PARAM_READWRITE | IPATCH_PARAM_NO_SAVE));
}

static void
swamigui_root_set_property(GObject *object, guint property_id,
                           const GValue *value, GParamSpec *pspec)
{
    SwamiguiRoot *root = SWAMIGUI_ROOT(object);
    IpatchList *list;
    GObject *obj = NULL;
    int ival;

    switch(property_id)
    {
    case PROP_UPDATE_INTERVAL:
        ival = g_value_get_int(value);

        if(ival != root->update_interval)
        {
            root->update_interval = ival;

            /* remove old timeout and install new one */
            if(root->update_timeout_id)
            {
                g_source_remove(root->update_timeout_id);
            }

            root->update_timeout_id = g_timeout_add(root->update_interval,
                                                    (GSourceFunc)swamigui_update_gui_timeout, root);
        }

        break;

    case PROP_QUIT_CONFIRM:
        root->quit_confirm = g_value_get_enum(value);
        break;

    case PROP_SPLASH_ENABLE:
        root->splash_enable = g_value_get_boolean(value);
        break;

    case PROP_SPLASH_DELAY:
        root->splash_delay = g_value_get_uint(value);
        break;

    case PROP_TIPS_ENABLE:
        root->tips_enable = g_value_get_boolean(value);
        break;

    case PROP_TIPS_POSITION:
        root->tips_position = g_value_get_int(value);
        break;

    case PROP_PIANO_LOWER_KEYS:
        g_free(root->piano_lower_keys);
        root->piano_lower_keys =
            swamigui_root_parse_piano_keys(g_value_get_string(value));
        break;

    case PROP_PIANO_UPPER_KEYS:
        g_free(root->piano_upper_keys);
        root->piano_upper_keys =
            swamigui_root_parse_piano_keys(g_value_get_string(value));
        break;

    case PROP_DEFAULT_PATCH_TYPE:
        root->default_patch_type = g_value_get_gtype(value);
        break;

    case PROP_MIDDLE_EMUL_ENABLE:
        root->middle_emul_enable = g_value_get_boolean(value);
        break;

    case PROP_MIDDLE_EMUL_MOD:
        root->middle_emul_mod = g_value_get_int(value);
        break;

    case PROP_TREE_STORE_LIST:
        if(root->tree_stores)
        {
            g_object_unref(root->tree_stores);
        }

        root->tree_stores = IPATCH_LIST(g_value_dup_object(value));
        break;

    case PROP_SELECTION:
        if(root->selection)
        {
            g_object_unref(root->selection);
        }

        obj = g_value_dup_object(value);

        if(obj)
        {
            root->selection = IPATCH_LIST(obj);
        }
        else
        {
            root->selection = NULL;
        }

        g_object_notify(G_OBJECT(root), "selection-single");
        break;

    case PROP_SELECTION_SINGLE:
        if(root->selection)
        {
            g_object_unref(root->selection);
        }

        obj = g_value_dup_object(value);

        if(obj)
        {
            list = ipatch_list_new();
            list->items = g_list_append(list->items, obj);
            root->selection = list;
        }
        else
        {
            root->selection = NULL;
        }

        g_object_notify(G_OBJECT(root), "selection");
        break;

    case PROP_SOLO_ITEM_ENABLE:
        if(root->solo_item_enabled != g_value_get_boolean(value))
        {
            root->solo_item_enabled = !root->solo_item_enabled;

            if(root->solo_item_enabled && root->selection && root->selection->items
                    && !root->selection->items->next)
            {
                obj = root->selection->items->data;
            }

            swamigui_root_update_solo_item(root, obj);
        }

        break;

    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
        break;
    }
}

static void
swamigui_root_get_property(GObject *object, guint property_id,
                           GValue *value, GParamSpec *pspec)
{
    SwamiguiRoot *root = SWAMIGUI_ROOT(object);
    GObject *obj = NULL;

    switch(property_id)
    {
    case PROP_MAIN_WINDOW:
        g_value_set_object(value, G_OBJECT(root->main_window));
        break;

    case PROP_UPDATE_INTERVAL:
        g_value_set_int(value, root->update_interval);
        break;

    case PROP_QUIT_CONFIRM:
        g_value_set_enum(value, root->quit_confirm);
        break;

    case PROP_SPLASH_ENABLE:
        g_value_set_boolean(value, root->splash_enable);
        break;

    case PROP_SPLASH_DELAY:
        g_value_set_uint(value, root->splash_delay);
        break;

    case PROP_TIPS_ENABLE:
        g_value_set_boolean(value, root->tips_enable);
        break;

    case PROP_TIPS_POSITION:
        g_value_set_int(value, root->tips_position);
        break;

    case PROP_PIANO_LOWER_KEYS:
        g_value_take_string(value, swamigui_root_encode_piano_keys(root->piano_lower_keys));
        break;

    case PROP_PIANO_UPPER_KEYS:
        g_value_take_string(value, swamigui_root_encode_piano_keys(root->piano_upper_keys));
        break;

    case PROP_DEFAULT_PATCH_TYPE:
        g_value_set_gtype(value, root->default_patch_type);
        break;

    case PROP_MIDDLE_EMUL_ENABLE:
        g_value_set_boolean(value, root->middle_emul_enable);
        break;

    case PROP_MIDDLE_EMUL_MOD:
        g_value_set_int(value, root->middle_emul_mod);
        break;

    case PROP_TREE_STORE_LIST:
        g_value_set_object(value, root->tree_stores);
        break;

    case PROP_SELECTION_ORIGIN:
        if(root->selection)
        {
            obj = swami_object_get_origin(G_OBJECT(root->selection));
        }

        g_value_take_object(value, obj);
        break;

    case PROP_SELECTION:
        g_value_set_object(value, root->selection);
        break;

    case PROP_SELECTION_SINGLE:
        if(root->selection && root->selection->items
                && !root->selection->items->next)
        {
            obj = root->selection->items->data;
        }

        g_value_set_object(value, obj);
        break;

    case PROP_SOLO_ITEM_ENABLE:
        g_value_set_boolean(value, root->solo_item_enabled);
        break;

    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
        break;
    }
}

static void
swamigui_root_quit_method(SwamiguiRoot *root)
{
    swamigui_root_save_prefs(root);
    gtk_main_quit();
}

/* Additional init operations are performed in swamigui_root_activate() */
static void
swamigui_root_init(SwamiguiRoot *root)
{
    IpatchList *list;
    GType type;

    /* having global vars for the Swami instance makes things easier */
    swamigui_root = root;
    swami_root = SWAMI_ROOT(root);

#ifdef PYTHON_SUPPORT

    if(!swamigui_disable_python)
    {
        swamigui_python_set_root();
    }

#endif

    root->update_interval = 40;
    root->quit_confirm = SWAMIGUI_QUIT_CONFIRM_UNSAVED;
    root->splash_enable = TRUE;
    root->splash_delay = SWAMIGUI_ROOT_DEFAULT_SPLASH_DELAY;
    root->tips_enable = TRUE;
    root->default_patch_type = IPATCH_TYPE_SF2;

    root->piano_lower_keys
        = swamigui_root_parse_piano_keys(SWAMIGUI_ROOT_DEFAULT_LOWER_KEYS);
    root->piano_upper_keys
        = swamigui_root_parse_piano_keys(SWAMIGUI_ROOT_DEFAULT_UPPER_KEYS);

    root->middle_emul_enable = TRUE;

    swami_object_set(G_OBJECT(root),
                     "name", "Swami",
                     "flags", SWAMI_OBJECT_SAVE | SWAMI_OBJECT_USER,
                     NULL);

    /* create instrument tree store */
    root->patch_store = SWAMIGUI_TREE_STORE
                        (swamigui_tree_store_patch_new());  /* ++ ref */
    swami_object_set(G_OBJECT(root->patch_store), "name", "Patches", NULL);
    swami_root_add_object(SWAMI_ROOT(root), G_OBJECT(root->patch_store));

    /* create config tree store */
    root->config_store = swamigui_tree_store_config_new();  /* ++ ref */
    swami_object_set(G_OBJECT(root->config_store), "name", "Config", NULL);
    swami_root_add_object(SWAMI_ROOT(root), G_OBJECT(root->config_store));

    /* create GUI control queue */
    root->ctrl_queue = swami_control_queue_new();  /* ++ ref control queue */

    /* setup the GUI queue test function and is_gui_thread private */
    g_static_private_set(&is_gui_thread, GUINT_TO_POINTER(TRUE), NULL);
    swami_control_queue_set_test_func(root->ctrl_queue,
                                      swamigui_queue_test_func);

    /* create queued patch item property changed listener */
    root->ctrl_prop = swami_control_func_new();  /* ++ ref new control */
    swami_control_func_assign_funcs(root->ctrl_prop, NULL /* get_func */,
                                    ctrl_prop_set_func, NULL /* destroy_func */,
                                    root);

    /* this control will never send (receives events only), disables event loop check */
    SWAMI_CONTROL(root->ctrl_prop)->flags &= ~SWAMI_CONTROL_SENDS;

    /* create queued patch item add control listener */
    root->ctrl_add = swami_control_func_new();  /* ++ ref new control */
    swami_control_func_assign_funcs(root->ctrl_add, NULL /* get_func */,
                                    ctrl_add_set_func, NULL /* destroy_func */,
                                    root);

    /* this control will never send (receives events only), disables event loop check */
    SWAMI_CONTROL(root->ctrl_add)->flags &= ~SWAMI_CONTROL_SENDS;

    /* create queued patch item remove control listener */
    root->ctrl_remove = swami_control_func_new();  /* ++ ref new control */
    swami_control_func_assign_funcs(root->ctrl_remove, NULL /* get_func */,
                                    ctrl_remove_set_func,
                                    NULL /* destroy_func */, root);

    /* this control will never send (receives events only), disables event loop check */
    SWAMI_CONTROL(root->ctrl_remove)->flags &= ~SWAMI_CONTROL_SENDS;

    /* set the queue of the controls */
    swami_control_set_queue(SWAMI_CONTROL(root->ctrl_prop), root->ctrl_queue);
    swami_control_set_queue(SWAMI_CONTROL(root->ctrl_add), root->ctrl_queue);
    swami_control_set_queue(SWAMI_CONTROL(root->ctrl_remove),
                            root->ctrl_queue);

    /* connect controls to ipatch event senders */
    swami_control_connect(swami_patch_get_prop_title_control(),
                          SWAMI_CONTROL(root->ctrl_prop), 0);
    swami_control_connect(swami_patch_get_add_control(),
                          SWAMI_CONTROL(root->ctrl_add), 0);
    swami_control_connect(swami_patch_get_remove_control(),
                          SWAMI_CONTROL(root->ctrl_remove), 0);

    /* add patch store to tree store list */
    list = ipatch_list_new();
    list->items = g_list_append(list->items, g_object_ref(root->patch_store));
    list->items = g_list_append(list->items, g_object_ref(root->config_store));
    root->tree_stores = list;

    /* create the FluidSynth wavetable object */
    type = swami_type_get_default(SWAMI_TYPE_WAVETBL);

    /* create new wavetable object */
    if(type && (root->wavetbl = g_object_new(type, NULL)))
    {
        swami_object_set(root->wavetbl, "name", "FluidSynth1", NULL);
        swami_root_add_object(SWAMI_ROOT(root), G_OBJECT(root->wavetbl));
        swamigui_tree_store_insert(root->config_store, G_OBJECT(root->wavetbl),
                                   NULL, NULL, NULL, 0, NULL);
    }
}

static void
swamigui_root_finalize(GObject *object)
{
    SwamiguiRoot *root = SWAMIGUI_ROOT(object);
    GList *p;

    g_object_unref(root->patch_store);
    g_object_unref(root->config_store);
    g_object_unref(root->tree_stores);

    if(root->selection)
    {
        g_object_unref(root->selection);
    }

    gtk_object_destroy(GTK_OBJECT(root->main_window));

    if(root->wavetbl)
    {
        swami_wavetbl_close(root->wavetbl);
        g_object_unref(root->wavetbl);
    }

    if(root->solo_item)
    {
        g_object_unref(root->solo_item);
    }

    g_object_unref(root->ctrl_queue);

    if(root->update_timeout_id)
    {
        g_source_remove(root->update_timeout_id);
    }

    /* disconnect and unref controls */
    swami_control_disconnect_unref(SWAMI_CONTROL(root->ctrl_prop));
    swami_control_disconnect_unref(SWAMI_CONTROL(root->ctrl_add));
    swami_control_disconnect_unref(SWAMI_CONTROL(root->ctrl_remove));

    /* disconnect all (only) properties controls */
    {
        GSList *c = root->ctrl_list;
        while(c)
        {
            swami_control_disconnect_all((SwamiControl *)(c->data));
            c = c->next;
        }
        g_slist_free (root->ctrl_list);
    }

    g_free(root->piano_lower_keys);
    g_free(root->piano_upper_keys);

    if(root->loaded_xml_config)
    {
        ipatch_xml_destroy(root->loaded_xml_config);
    }

    // Free widgets in panel cache
    for(p = root->panel_cache; p; p = g_list_delete_link(p, p))
    {
        g_object_unref(p->data);
    }

    if(parent_class->finalize)
    {
        parent_class->finalize(object);
    }
}

/* GUI queue test function (checks if queuing of events is required) */
static gboolean
swamigui_queue_test_func(SwamiControlQueue *queue, SwamiControl *control,
                         SwamiControlEvent *event)
{
    /* are we in the GUI thread? */
    gboolean bval = GPOINTER_TO_UINT(g_static_private_get(&is_gui_thread));
    return (!bval);	/* FALSE sends event immediately, TRUE queues */
}

/* routine called at regular intervals to synchronize GUI with patch objects */
static gboolean
swamigui_update_gui_timeout(gpointer data)
{
    SwamiguiRoot *root = SWAMIGUI_ROOT(data);

    swami_control_queue_run(root->ctrl_queue);

    /* Update splits widget if its splits-item has changed */
    if(root->splits_changed)
    {
        swamigui_splits_item_changed(SWAMIGUI_SPLITS(root->splits));
        root->splits_changed = FALSE;
    }

    return (TRUE);
}

/* patch item title property change control value set function (listens for
   item property change events) */
static void
ctrl_prop_set_func(SwamiControl *control, SwamiControlEvent *event,
                   const GValue *value)
{
    SwamiguiRoot *root = SWAMIGUI_ROOT(SWAMI_CONTROL_FUNC_DATA(control));
    SwamiEventPropChange *prop_change;

    prop_change = g_value_get_boxed(&event->value);
    swamigui_tree_store_changed(root->patch_store, prop_change->object);
}

/* patch item add control value set function (listens for item add events) */
static void
ctrl_add_set_func(SwamiControl *control, SwamiControlEvent *event,
                  const GValue *value)
{
    SwamiguiRoot *root = SWAMIGUI_ROOT(SWAMI_CONTROL_FUNC_DATA(control));
    IpatchItem *item, *splits_item;

    item = IPATCH_ITEM(g_value_get_boxed(&event->value));
    swamigui_tree_store_add(root->patch_store, G_OBJECT(item));

    /* Update splits widget if its active item changed */
    g_object_get(root->splits, "splits-item", &splits_item, NULL);

    if(splits_item == ipatch_item_peek_parent(item))
    {
        root->splits_changed = TRUE;
    }
}

/* patch item remove control value set function (listens for item remove
   events) */
static void
ctrl_remove_set_func(SwamiControl *control, SwamiControlEvent *event,
                     const GValue *value)
{
    SwamiguiRoot *root = SWAMIGUI_ROOT(SWAMI_CONTROL_FUNC_DATA(control));
    SwamiEventItemRemove *remove;
    IpatchItem *splits_item;

    remove = g_value_get_boxed(&event->value);
    swamigui_tree_store_remove(root->patch_store, G_OBJECT(remove->item));

    /* Update splits widget if its active item changed */
    g_object_get(root->splits, "splits-item", &splits_item, NULL);

    if(splits_item == ipatch_item_peek_parent(remove->item))
    {
        root->splits_changed = TRUE;
    }
}

/**
 * swamigui_root_new:
 *
 * Create a new Swami root user interface object which is a subclass of
 * #SwamiRoot.
 *
 * Returns: New Swami user interface object
 */
SwamiguiRoot *
swamigui_root_new(void)
{
    return (SWAMIGUI_ROOT(g_object_new(SWAMIGUI_TYPE_ROOT, NULL)));
}

/**
 * swamigui_root_activate:
 * @root: Swami GUI root object
 *
 * Activates Swami GUI by creating main window, loading plugins, and displaying
 * tips and splash image.  Separate from object init function so that preferences
 * can be loaded or not.
 */
void
swamigui_root_activate(SwamiguiRoot *root)
{
    GError *err = NULL;

    g_return_if_fail(SWAMIGUI_IS_ROOT(root));

    /* GUI update timeout */
    root->update_timeout_id = g_timeout_add(root->update_interval,
                                            (GSourceFunc)swamigui_update_gui_timeout, root);

    if(root->wavetbl)
    {
        if(!swami_wavetbl_open(SWAMI_WAVETBL(root->wavetbl), &err))
        {
            g_warning("Failed to initialize wavetable driver '%s'",
                      ipatch_gerror_message(err));
            g_clear_error(&err);
        }
    }

    /* create main window and controls */
    swamigui_root_create_main_window(root);

    /* pop up swami tip window if enabled */
    if(root->tips_enable)
    {
        swamigui_help_swamitips_create(root);
    }

    /* display splash (with timeout) only if not disabled */
    if(root->splash_enable)
    {
        swamigui_splash_display(root->splash_delay);
    }
}

/**
 * swamigui_root_quit:
 * @root: Swami GUI root object
 *
 * Quit Swami GUI. Pops a quit confirmation depending on preferences.
 */
void
swamigui_root_quit(SwamiguiRoot *root)
{
    IpatchList *list;
    IpatchIter iter;
    GObject *obj;
    gboolean changed = FALSE;
    GtkWidget *popup;
    int quit_confirm;
    char *s;

    /* destroy any dialog actally open */
    swamigui_util_destroy_unique_dialog();

    /* Look among "patch items" if one of them has been changed */
    list = swami_root_get_patch_items(SWAMI_ROOT(root));   /* ++ ref list */
    ipatch_list_init_iter(list, &iter);
    obj = ipatch_iter_first(&iter);

    while(obj)
    {
        g_object_get(obj, "changed", &changed, NULL);

        if(changed)
        {
            break;
        }

        obj = ipatch_iter_next(&iter);
    }

    g_object_unref(list);	/* -- unref list */

    /* "quit-confirm" property instructs how to ask the user for
       quit confirmation:
       -SWAMIGUI_QUIT_CONFIRM_NEVER, quit immediately without confirmation.
       -SWAMIGUI_QUIT_CONFIRM_UNSAVED, quit immediately if nothing has been changed
        otherwise a confirmation dialog is opened.
    */
    g_object_get(root, "quit-confirm", &quit_confirm, NULL);

    if(quit_confirm == SWAMIGUI_QUIT_CONFIRM_NEVER
            || (quit_confirm == SWAMIGUI_QUIT_CONFIRM_UNSAVED && !changed))
    {
        swamigui_real_quit(root); /* quit immediately */
        return;
    }

    /* open a confirmation dialog */
    if(changed)
    {
        s = _("Unsaved files, and you want to quit?");
    }
    else
    {
        s = _("Are you sure you want to quit?");
    }

    popup = gtk_message_dialog_new(NULL, 0,
                                   GTK_MESSAGE_QUESTION,
                                   GTK_BUTTONS_NONE, "%s", s);
    /* Set the dialogue modal */
    gtk_window_set_modal(GTK_WINDOW(popup), TRUE);

    /* Set the dialog centered above the top level swami Window  */
    gtk_window_set_transient_for(GTK_WINDOW(popup), GTK_WINDOW(root->main_window));

    gtk_dialog_add_buttons(GTK_DIALOG(popup),
                           GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
                           GTK_STOCK_QUIT, GTK_RESPONSE_OK,
                           NULL);
    g_signal_connect(popup, "response",
                     G_CALLBACK(swamigui_cb_quit_response), root);
    gtk_widget_show(popup);
}

static void
swamigui_cb_quit_response(GtkDialog *dialog, int response, gpointer user_data)
{
    SwamiguiRoot *root = SWAMIGUI_ROOT(user_data);

    gtk_widget_destroy(GTK_WIDGET(dialog));

    if(response == GTK_RESPONSE_OK)
    {
        swamigui_real_quit(root);
    }
}

static void
swamigui_real_quit(SwamiguiRoot *root)
{
    g_signal_emit(root, uiroot_signals [QUIT], 0);
}

/**
 * swamigui_root_save_prefs:
 * @root: Swami GUI root object
 *
 * Save application preferences to XML config file.  Only stores config if it
 * differs from existing stored config on disk.
 *
 * Returns: %TRUE on success, %FALSE otherwise (error message will be logged)
 */
gboolean
swamigui_root_save_prefs(SwamiguiRoot *root)
{
    char *config_dir, *pref_filename;
    GError *local_err = NULL;
    GNode *xmltree, *node, *pnode, *dupnode;
    GList *plugins, *p;
    SwamiPlugin *plugin;
    char *xmlstr;
    char *curxml;
    const char *attr_val;

    xmltree = ipatch_xml_new_node(NULL, "swami", NULL,	/* ++ alloc XML tree */
                                  "version", SWAMI_VERSION,
                                  NULL);

    /* Encode the root object */
    if(!ipatch_xml_encode_object(xmltree, G_OBJECT(root), FALSE, &local_err))
    {
        g_critical(_("Failed to save Swami preferences: %s"),
                   ipatch_gerror_message(local_err));
        g_clear_error(&local_err);
        ipatch_xml_destroy(xmltree);		/* -- free XML tree */
        return (FALSE);
    }

    /* Encode plugin preferences */
    plugins = swami_plugin_get_list();	/* ++ alloc list */

    for(p = plugins; p; p = p->next)
    {
        plugin = (SwamiPlugin *)(p->data);

        if(plugin->save_xml)
        {
            node = ipatch_xml_new_node(xmltree, "plugin", NULL,
                                       "name", G_TYPE_MODULE(plugin)->name,
                                       NULL);

            if(!swami_plugin_save_xml(plugin, node, &local_err))
            {
                g_warning(_("Failed to save plugin %s preferences: %s"),
                          G_TYPE_MODULE(plugin)->name, ipatch_gerror_message(local_err));
                ipatch_xml_destroy(node);	/* -- free the plugin node */
                g_clear_error(&local_err);
            }
        }
    }

    g_list_free(plugins);	/* -- free list */

    /* Check if there is any plugin data which was loaded at startup which is no
     * longer present and append it to the config if so.  In particular this
     * prevents plugin preferences from being lost if they are not loaded at
     * startup. */
    if(root->loaded_xml_config)
    {
        for(node = root->loaded_xml_config->children; node; node = node->next)
        {
            if(!ipatch_xml_test_name(node, "plugin"))
            {
                continue;
            }

            attr_val = ipatch_xml_get_attribute(node, "name");

            if(!attr_val)
            {
                continue;
            }

            for(pnode = xmltree->children; pnode; pnode = pnode->next)
                if(ipatch_xml_test_name(pnode, "plugin")
                        && ipatch_xml_test_attribute(pnode, "name", attr_val))
                {
                    break;
                }

            if(!pnode)        /* Not found in new XML config? */
            {
                dupnode = ipatch_xml_copy(node);        /* Duplicate the plugin info */
                g_node_append(xmltree, dupnode);        /* Append to xmltree */
            }
        }
    }

    config_dir = g_build_filename(g_get_user_config_dir(), "swami", NULL);	/* ++ alloc */

    if(!g_file_test(config_dir, G_FILE_TEST_EXISTS))
    {
        if(g_mkdir_with_parents(config_dir, 0755) == -1)
        {
            g_critical(_("Failed to create Swami config directory '%s': %s"),
                       config_dir, g_strerror(errno));
            g_free(config_dir);	/* -- free */
            ipatch_xml_destroy(xmltree);		/* -- free XML tree */
            return (FALSE);
        }
    }

    pref_filename = g_build_filename(config_dir, "preferences.xml", NULL);	/* ++ alloc */
    g_free(config_dir);	/* -- free config dir */

    xmlstr = ipatch_xml_to_str(xmltree, 2);       /* ++ alloc xmlstr */
    ipatch_xml_destroy(xmltree);	/* -- free XML tree */

    /* Check if config matches what is already saved - return TRUE if it matches */
    if(g_file_get_contents(pref_filename, &curxml, NULL, NULL))     /* ++ alloc curxml data */
    {
        if(strcmp(xmlstr, curxml) == 0)
        {
            g_free(curxml);           /* -- free current XML data */
            g_free(xmlstr);           /* -- free xmlstr */
            g_free(pref_filename);	/* -- free pref_filename */
            return (TRUE);
        }

        g_free(curxml);             /* -- free current XML data */
    }

    if(!g_file_set_contents(pref_filename, xmlstr, -1, &local_err))
    {
        g_critical(_("Failed to save XML preferences to '%s': %s"),
                   pref_filename, ipatch_gerror_message(local_err));
        g_clear_error(&local_err);
        g_free(xmlstr);             /* -- free xmlstr */
        g_free(pref_filename);	/* -- free */
        return (FALSE);
    }

    g_free(xmlstr);               /* -- free xmlstr */
    g_free(pref_filename);	/* -- free */

    return (TRUE);
}

/**
 * swamigui_root_load_prefs:
 * @root: Swami GUI root object
 *
 * Restore application preferences from XML config files.
 *
 * Returns: %TRUE on success, %FALSE otherwise (error message will be logged)
 */
gboolean
swamigui_root_load_prefs(SwamiguiRoot *root)
{
    GNode *xmltree, *n;
    char *pref_filename;
    GParamSpec *pspec;
    GObjectClass *obj_class;
    SwamiPlugin *plugin;
    GError *err = NULL;
    const char *name;

    /* ++ alloc */
    pref_filename = g_build_filename(g_get_user_config_dir(), "swami",
                                     "preferences.xml", NULL);

    /* No preferences stored yet? - Return success */
    if(!g_file_test(pref_filename, G_FILE_TEST_EXISTS))
    {
        return (TRUE);
    }

    xmltree = ipatch_xml_load_from_file(pref_filename, &err);	/* ++ alloc XML tree */

    if(!xmltree)
    {
        g_critical(_("Failed to load preferences from '%s': %s"), pref_filename,
                   ipatch_gerror_message(err));
        g_clear_error(&err);
        g_free(pref_filename);	/* -- free filename */
        return (FALSE);
    }

    name = ipatch_xml_get_name(xmltree);

    if(!name || strcmp(name, "swami") != 0)
    {
        g_critical(_("File '%s' is not a Swami preferences file"), pref_filename);
        ipatch_xml_destroy(xmltree);	/* -- destroy XML tree */
        g_free(pref_filename);	/* -- free filename */
        return (FALSE);
    }

    obj_class = G_OBJECT_GET_CLASS(root);

    for(n = xmltree->children; n; n = n->next)
    {
        name = ipatch_xml_get_name(n);

        if(!name)
        {
            continue;
        }

        if(strcmp(name, "prop") == 0)	/* Root preference value */
        {
            name = ipatch_xml_get_attribute(n, "name");

            if(!name)
            {
                continue;
            }

            pspec = g_object_class_find_property(obj_class, name);

            if(!pspec || (pspec->flags & IPATCH_PARAM_NO_SAVE))
            {
                g_warning(_("Invalid Swami property '%s' in preferences"), name);
                continue;
            }

            if(!ipatch_xml_decode_property(n, G_OBJECT(root), pspec, &err))
            {
                g_critical(_("Failed to decode Swami preference property '%s': %s"),
                           pspec->name, ipatch_gerror_message(err));
                g_clear_error(&err);
                continue;
            }
        }
        else if(strcmp(name, "plugin") == 0)	/* Plugin state */
        {
            name = ipatch_xml_get_attribute(n, "name");

            if(!name)
            {
                continue;
            }

            plugin = swami_plugin_find(name);

            if(plugin)
            {
                if(!swami_plugin_load_xml(plugin, n, &err))
                {
                    g_critical(_("Failed to load plugin '%s' preferences: %s"),
                               name, ipatch_gerror_message(err));
                    g_clear_error(&err);
                }
            }
        }
    }

    g_free(pref_filename);	/* -- free filename */

    /* Set the loaded_xml_config variable so it reflects the last loaded XML config */
    if(root->loaded_xml_config)
    {
        ipatch_xml_destroy(root->loaded_xml_config);
    }

    root->loaded_xml_config = xmltree;    /* !! root takes over reference */

    return (TRUE);
}

/**
 * swamigui_root_patch_load:
 *
 * Load an instrument patch file and displays a log error message if it fails.
 * The log message is displayed on the console and optionally on a Gtk message
 * dialog.
 *
 * In the case of a file loaded with error, the message displayed
 * is critical.
 * Because the function can ignore a file already loaded, for this case
 * the error returned is not critical and the message displayed is an
 * information only.
 *
 * @root: Swami root object to load into.
 * @filename, Name and path of file to load.
 * @item: Location to store pointer to object that has been loaded into Swami
 *   root object (or NULL). Remember to unref the object when done with it
 *   (not necessary of course if NULL was passed).
 * @parent: window parent of the Gtk message dialog created to display the log
 *  message. If NULL no message dialog is created.
 * @return: TRUE on success, FALSE otherwise
 */
gboolean swamigui_root_patch_load(SwamiRoot *root, const char *filename,
                                  IpatchItem **item, GtkWindow *parent)
{
    GError *err = NULL;

    g_return_val_if_fail((parent == NULL ) || GTK_IS_WINDOW(parent), FALSE);

    if (!swami_root_patch_load (root, filename, item, &err))
    {
        /* prepare a CRITICAL log message */
        GLogLevelFlags log_level = G_LOG_LEVEL_CRITICAL;
        static const char *log_msg = "Failed to load file '%s': %s";
        GtkMessageType gtk_type = GTK_MESSAGE_ERROR;

        if(err && err->code == SWAMI_ERROR_ALREADY_LOADED)
        {
            /* prepare an INFO log message */
            log_level = G_LOG_LEVEL_INFO;
            log_msg = "Ignore file '%s': %s";
            gtk_type = GTK_MESSAGE_INFO;
        }

        /* displaying log message on the console */
        g_log(G_LOG_DOMAIN, log_level, log_msg, filename,
              ipatch_gerror_message(err));

        /* optionally displaying log message on a GUI dialog */
        if(parent)
        {
            GtkWidget *msgdialog;
            msgdialog = gtk_message_dialog_new(GTK_WINDOW(parent), 0, gtk_type,
                                               GTK_BUTTONS_OK, log_msg, filename,
                                               ipatch_gerror_message(err));
            gtk_dialog_run(GTK_DIALOG(msgdialog));
            gtk_widget_destroy(msgdialog);
        }

        g_clear_error(&err);
        return FALSE;
    }
    return TRUE;
}

/**
 * swamigui_get_root:
 * @gobject: An object registered to a #SwamiguiRoot object
 *
 * Gets a #SwamiguiObject associated with a @gobject. A convenience function
 * really as swami_get_root() will return the same object but casted to
 * #SwamiRoot instead.
 *
 * Returns: A #SwamiguiRoot object or %NULL if @gobject not registered to one.
 * Returned object's refcount is not incremented.
 */
SwamiguiRoot *
swamigui_get_root(gpointer gobject)
{
    SwamiguiRoot *root;

    g_return_val_if_fail(G_IS_OBJECT(gobject), NULL);

    if(SWAMIGUI_IS_ROOT(gobject))
    {
        root = (SwamiguiRoot *)gobject;
    }
    else
    {
        root = (SwamiguiRoot *)swami_get_root(G_OBJECT(gobject));
    }

    /* if no root object is assigned to 'gobject' then just return the global */
    if(!root)
    {
        return (swamigui_root);
    }

    return (root);
}

/*
 * Getter returning swamigui_root.
 * Useful when libswamigui is used as a shared library
   For example, plugins fluidsynth_gui and fluidsynth_plugin need swamigui_root.
 */
SwamiguiRoot *
swamigui_get_swamigui_root(void)
{
    return (swamigui_root);
}

/* Create main window and controls */
static void
swamigui_root_create_main_window(SwamiguiRoot *root)
{
    SwamiPropTree *proptree;
    SwamiControl *ctrl, *rootsel_ctrl;
    SwamiControl *store_ctrl;
    GtkWidget *vbox;
    GtkWidget *tempbox;
    GtkWidget *vpaned, *hpaned;
    GtkWidget *widg;
    GType type;
    SwamiControl *midihub;
    SwamiguiControlMidiKey *midikey;
    GObject *piano;
    SwamiControl *pctrl, *tctrl;
    SwamiControlMidi *wctrl;

    /* get root selection property control */
    rootsel_ctrl = swami_get_control_prop_by_name(G_OBJECT(root), "selection");
    /* rootsel_ctrl must be disconnected on finalization */
    root->ctrl_list = g_slist_append(root->ctrl_list, rootsel_ctrl);

    root->main_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_default_size(GTK_WINDOW(root->main_window), 1024, 768);
    gtk_window_set_title(GTK_WINDOW(root->main_window), "Swami");

    g_signal_connect(root->main_window, "delete_event",
                     G_CALLBACK(swamigui_root_cb_main_window_delete), root);
#ifdef MAC_INTEGRATION
    GtkosxApplication *theApp = g_object_new(GTKOSX_TYPE_APPLICATION, NULL);

    g_signal_connect(theApp, "NSApplicationBlockTermination",
                     G_CALLBACK(swamigui_root_cb_should_quit), root);
#endif

    /* add the item menu keyboard accelerators */
    gtk_window_add_accel_group(GTK_WINDOW(root->main_window),
                               swamigui_item_menu_accel_group);

    vbox = gtk_vbox_new(FALSE, 0);
    gtk_widget_show(vbox);
    gtk_container_add(GTK_CONTAINER(root->main_window), vbox);

    hpaned = gtk_hpaned_new();
    gtk_widget_show(hpaned);
    gtk_box_pack_start(GTK_BOX(vbox), hpaned, TRUE, TRUE, 0);

    tempbox = gtk_vbox_new(FALSE, 0);
    gtk_widget_show(tempbox);
    gtk_paned_pack1(GTK_PANED(hpaned), tempbox, TRUE, TRUE);

    widg = swamigui_menu_new();
#ifndef MAC_INTEGRATION
    gtk_widget_show(widg);
    gtk_box_pack_start(GTK_BOX(tempbox), widg, FALSE, FALSE, 0);
#else /* menu is handled a bit differently on OS X */
    GtkUIManager *mgr = (SWAMIGUI_MENU(widg))->ui;
    GtkWidget *mitem;

    /* Set the global menu bar to the main menu */
    mitem = gtk_ui_manager_get_widget(mgr, "/MenuBar");
    gtkosx_application_set_menu_bar(theApp, GTK_MENU_SHELL(mitem));
    /* Move Help/About and Edit/Preferences menu items to the app menu */
    mitem = gtk_ui_manager_get_widget(mgr, "/MenuBar/HelpMenu/About");
    gtkosx_application_insert_app_menu_item(theApp, mitem, 0);
    mitem = gtk_separator_menu_item_new();
    gtkosx_application_insert_app_menu_item(theApp, mitem, 1);
    mitem = gtk_ui_manager_get_widget(mgr, "/MenuBar/EditMenu/Preferences");
    gtkosx_application_insert_app_menu_item(theApp, mitem, 2);
    /* Hide Quit menu item which is added automatically to the app menu */
    mitem = gtk_ui_manager_get_widget(mgr, "/MenuBar/FileMenu/Quit");
    gtk_widget_hide(mitem);
    /* Designate Help menu and create Window menu */
    mitem = gtk_ui_manager_get_widget(mgr, "/MenuBar/HelpMenu");
    gtkosx_application_set_help_menu(theApp, GTK_MENU_ITEM(mitem));
    gtkosx_application_set_window_menu(theApp, NULL);
    /* Convert menu accelerator key modifiers */
    gtk_accel_map_foreach(NULL, osx_accel_map_foreach_lcb);
#endif

    root->tree = swamigui_tree_new(root->tree_stores);
    gtk_widget_show(root->tree);
    gtk_box_pack_start(GTK_BOX(tempbox), root->tree, TRUE, TRUE, 0);

    /* connect tree selection to root selection (++ ref ctrl) */
    ctrl = swami_get_control_prop_by_name(G_OBJECT(root->tree), "selection");
    swami_control_connect(ctrl, rootsel_ctrl, SWAMI_CONTROL_CONN_BIDIR);
    g_object_unref(ctrl);	/* -- unref tree control */

    /* HACK - Set initial selection (so that swamigui_root "selection" is not NULL)
     * We need an empty selection, since it has the origin of the tree.
     * So no problems occur with tree menu accelerators. */
    swamigui_tree_set_selection(SWAMIGUI_TREE(root->tree), NULL);

    vpaned = gtk_vpaned_new();
    gtk_widget_show(vpaned);
    gtk_paned_pack2(GTK_PANED(hpaned), vpaned, TRUE, TRUE);

    tempbox = gtk_vbox_new(FALSE, 0);
    gtk_widget_show(tempbox);
    gtk_paned_pack1(GTK_PANED(vpaned), tempbox, TRUE, TRUE);

    /* create FluidSynth interface */
    type = g_type_from_name("FluidSynthGuiControl");

    if(type)
    {
        widg = g_object_new(type, NULL);
        gtk_widget_show_all(widg);
        gtk_box_pack_start(GTK_BOX(tempbox), widg, FALSE, FALSE, 2);
    }

    /* create splits widget */
    root->splits = swamigui_splits_new();
    gtk_widget_show(root->splits);
    gtk_box_pack_start(GTK_BOX(tempbox), root->splits, TRUE, TRUE, 0);

    /* connect root selection to splits (++ ref ctrl) */
    ctrl = swami_get_control_prop_by_name(G_OBJECT(root->splits), "item-selection");
    swami_control_connect(rootsel_ctrl, ctrl, SWAMI_CONTROL_CONN_BIDIR);
    g_object_unref(ctrl);	/* -- unref tree control */

    root->panel_selector = swamigui_panel_selector_new(root);
    gtk_widget_show(root->panel_selector);
    gtk_paned_pack2(GTK_PANED(vpaned), root->panel_selector, FALSE, TRUE);

    /* connect root selection to panel selector (++ ref ctrl) */
    ctrl = swami_get_control_prop_by_name(G_OBJECT(root->panel_selector),
                                          "item-selection");
    swami_control_connect(rootsel_ctrl, ctrl, 0);
    g_object_unref(ctrl);	/* -- unref tree control */

    gtk_paned_set_position(GTK_PANED(hpaned), 300);
    gtk_paned_set_position(GTK_PANED(vpaned), 400);

    /* create statusbar and pack it */
    root->statusbar = SWAMIGUI_STATUSBAR(swamigui_statusbar_new());
    gtk_widget_show(GTK_WIDGET(root->statusbar));
    gtk_box_pack_start(GTK_BOX(vbox), GTK_WIDGET(root->statusbar),
                       FALSE, FALSE, 2);

    /* connect root selection to GUI switcher widget property tree variables */
    proptree = SWAMI_ROOT(root)->proptree;

    swami_prop_tree_add_value(proptree, G_OBJECT(root), 0, "item-selection",
                              rootsel_ctrl);

    /* create SwamiguiRoot tree store property control (++ ref new object) */
    store_ctrl = swami_get_control_prop_by_name(G_OBJECT(root),
                 "tree-store-list");
    /* FIXME - kill hackish property tree (alternatives?) */
    swami_prop_tree_add_value(proptree, G_OBJECT(root), 0, "store-list",
                              store_ctrl);

    g_object_unref(store_ctrl);	/* -- unref creator's reference */
    g_object_unref(rootsel_ctrl);   /* -- unref root control */


    /* Control section of init */


    /* create MIDI hub control */
    midihub = SWAMI_CONTROL	/* ++ ref */
              (swami_root_new_object(SWAMI_ROOT(root), "SwamiControlHub"));

    /* create MIDI keyboard control */
    midikey = SWAMIGUI_CONTROL_MIDI_KEY /* ++ ref */
              (swami_root_new_object(SWAMI_ROOT(root), "SwamiguiControlMidiKey"));

    /* connect keyboard control to MIDI hub */
    swami_control_connect(SWAMI_CONTROL(midikey), midihub, 0);

    /* Connect splits widget keyboard controls to MIDI keyboard object */
    swamigui_root_connect_midi_keyboard_controls(root, G_OBJECT(midikey));

    g_object_unref(midikey);	/* -- unref creator's ref */


    g_object_get(root->splits, "piano", &piano, NULL);  /* ++ ref piano */
    g_object_get(piano, "midi-control", &pctrl, NULL);  /* ++ ref piano MIDI ctrl */

    /* connect piano to MIDI hub */
    swami_control_connect(pctrl, midihub, SWAMI_CONTROL_CONN_BIDIR);
    g_object_unref(pctrl); 	/* -- unref */
    g_object_unref(piano);	/* -- unref */

    if(root->wavetbl)
    {
        /* get wavetable MIDI control */
        wctrl = swami_wavetbl_get_control(SWAMI_WAVETBL(root->wavetbl), 0);

        /* set initial bank/program number */
        swami_control_midi_send(wctrl, SWAMI_MIDI_BANK_SELECT, 0, 127, -1);
        swami_control_midi_send(wctrl, SWAMI_MIDI_PROGRAM_CHANGE, 0, 127, -1);

        /* connect wavetable to MIDI hub */
        swami_control_connect(midihub, SWAMI_CONTROL(wctrl),
                              SWAMI_CONTROL_CONN_BIDIR
                              | SWAMI_CONTROL_CONN_PRIORITY_HIGH);
        g_object_unref(wctrl);  /* -- unref */

        /* get control connected to root selection-single property (++ ref) */
        tctrl = SWAMI_CONTROL(swami_get_control_prop_by_name
                              (G_OBJECT(root), "selection-single"));

        /* get prop control to wavetbl temp item (++ ref ctrl) */
        pctrl = SWAMI_CONTROL(swami_get_control_prop_by_name
                              (G_OBJECT(root->wavetbl), "active-item"));
        swami_control_connect(tctrl, pctrl, 0);

        g_object_unref(tctrl);  /* -- unref */
        g_object_unref(pctrl);  /* -- unref */
        /* tctrl must be disconnected on root finalization */
        root->ctrl_list = g_slist_append(root->ctrl_list, tctrl);

        g_signal_connect(root, "notify::selection-single",
                         G_CALLBACK(swamigui_root_cb_solo_item), NULL);
    }

    g_object_unref(midihub);	/* -- unref creator's reference */
    /* midihub must be disconnected on root finalization */
    root->ctrl_list = g_slist_append(root->ctrl_list, midihub);

    gtk_widget_show(root->main_window);
#ifdef MAC_INTEGRATION
    /* Tell OS X that we are ready launching now */
    gtkosx_application_ready(theApp);
#endif
}

/* Callback for SwamiguiRoot::selection-single to update Wavetbl solo-item if enabled */
static void
swamigui_root_cb_solo_item(SwamiguiRoot *root, GParamSpec *pspec, gpointer user_data)
{
    GObject *solo_item;

    if(!root->solo_item_enabled)
    {
        return;
    }

    g_object_get(root, "selection-single", &solo_item, NULL);     /* ++ ref solo item */
    swamigui_root_update_solo_item(root, solo_item);

    if(solo_item)
    {
        g_object_unref(solo_item);    /* -- unref solo item */
    }
}

static void
swamigui_root_update_solo_item(SwamiguiRoot *root, GObject *solo_item)
{
    GtkTreeIter iter;
    int category;

    /* If no wavetbl driver active - return */
    if(!root->wavetbl)
    {
        return;
    }

    /* Valid solo items are instrument or sample zones */
    if(solo_item)
    {
        ipatch_type_get(G_OBJECT_TYPE(solo_item), "category", &category, NULL);

        if(category != IPATCH_CATEGORY_SAMPLE_REF && category != IPATCH_CATEGORY_INSTRUMENT_REF)
        {
            solo_item = NULL;
        }
    }

    /* If previous solo item, restore its icon */
    if(root->solo_item)
    {
        swamigui_tree_store_change(root->patch_store, root->solo_item, NULL,
                                   root->solo_item_icon);
        g_object_unref(root->solo_item);    // -- unref solo item
        root->solo_item = NULL;
        root->solo_item_icon = NULL;        // !! solo_item_icon is a static string pointer, do not free
    }

    if(solo_item)
    {
        if(swamigui_tree_store_item_get_node(root->patch_store, solo_item, &iter))
        {
            root->solo_item = g_object_ref(solo_item);                // ++ ref solo item

            /* !! icon column is a static string pointer - not allocated */
            gtk_tree_model_get(GTK_TREE_MODEL(root->patch_store), &iter,
                               SWAMIGUI_TREE_STORE_ICON_COLUMN, &root->solo_item_icon,
                               -1);

            gtk_tree_store_set(GTK_TREE_STORE(root->patch_store), &iter,
                               SWAMIGUI_TREE_STORE_ICON_COLUMN, GTK_STOCK_MEDIA_PLAY,
                               -1);
        }
        else
        {
            solo_item = NULL;    /* Shouldn't fail to find item in tree, but.. */
        }
    }

    g_object_set(root->wavetbl, "solo-item", solo_item, NULL);
}

static void
swamigui_root_connect_midi_keyboard_controls(SwamiguiRoot *root, GObject *midikey)
{
    GtkWidget *widget;
    int i;

    struct
    {
        char *widg_name;
        char *prop_name;
    } names[] =
    {
        { "SpinBtnLowerOctave", "lower-octave" },
        { "ChkBtnJoinOctaves", "join-octaves" },
        { "SpinBtnUpperOctave", "upper-octave" },
        { "SpinBtnLowerVelocity", "lower-velocity" },
        { "ChkBtnSameVelocity", "same-velocity" },
        { "SpinBtnUpperVelocity", "upper-velocity" }
    };

    for(i = 0; i < G_N_ELEMENTS(names); i++)
    {
        widget = swamigui_util_glade_lookup(SWAMIGUI_SPLITS(root->splits)->gladewidg,
                                            names[i].widg_name);
        swamigui_control_prop_connect_widget(midikey, names[i].prop_name,
                                             G_OBJECT(widget));
    }
}

/* called when main window is closed */
static gint
swamigui_root_cb_main_window_delete(GtkWidget *widget, GdkEvent *event,
                                    gpointer data)
{
    SwamiguiRoot *root = SWAMIGUI_ROOT(data);
    swamigui_root_quit(root);

    return (TRUE);
}

/* converts a string of comma delimited GDK key symbols into a newly allocated
   zero terminated array of key values */
static guint *
swamigui_root_parse_piano_keys(const char *str)
{
    guint *keyvals;
    char **keynames;
    int count = 0, v = 0, n = 0;

    if(!str)
    {
        return (NULL);
    }

    keynames = g_strsplit(str, ",", 0);

    while(keynames[count])
    {
        count++;    /* count key symbols */
    }

    keyvals = g_malloc((count + 1) * sizeof(guint));

    while(keynames[n])
    {
        keyvals[v] = gdk_keyval_from_name(keynames[n]);

        if(keyvals[v] != GDK_VoidSymbol)
        {
            v++;    /* ignore invalid key names */
        }

        n++;
    }

    keyvals[v] = 0;		/* zero terminated */
    g_strfreev(keynames);

    return (keyvals);
}

/* converts a zero terminated array of key values into a newly allocated
   comma delimited GDK key symbol string */
static char *
swamigui_root_encode_piano_keys(const guint *keyvals)
{
    char **keynames;
    char *s;
    int i, count = 0;

    if(!keyvals)
    {
        return (NULL);
    }

    while(keyvals[count])
    {
        count++;    /* count number of key values */
    }

    /* allocate for array of key name strings */
    keynames = g_malloc((count + 1) * sizeof(char *));

    for(i = 0; i < count; i++)	/* loop over each key */
    {
        keynames[i] = gdk_keyval_name(keyvals[i]);   /* get key name */

        if(!keynames[i])   /* if no key name for key, fail */
        {
            g_critical("No GDK key name for key '%d'", keyvals[i]);
            g_free(keynames);
            return (NULL);
        }
    }

    keynames[count] = NULL;
    s = g_strjoinv(",", keynames);   /* create the comma delimited key string */
    g_free(keynames);

    return (s);
}

/* handler for IpatchSF2 glade property interface.
 * Needed in order to handle "Current Date" button. */
static GtkWidget *
sf2_prop_handler(GtkWidget *widg, GObject *obj)
{
    GtkWidget *btn, *entry;

    if(!widg)	/* create widget? */
    {
        widg = swamigui_util_glade_create("PropSF2");
        btn = swamigui_util_glade_lookup(widg, "BtnCurrentDate");
        entry = swamigui_util_glade_lookup(widg, "PROP::date");
        g_signal_connect(btn, "clicked", G_CALLBACK(current_date_btn_clicked), entry);
        swamigui_control_glade_prop_connect(widg, obj);
    }
    else
    {
        swamigui_control_glade_prop_connect(widg, obj);    /* change object */
    }

    return (widg);
}

/* callback when "Current Date" button is clicked */
static void
current_date_btn_clicked(GtkButton *button, gpointer user_data)
{
    struct tm *date;
    char datestr[64];
    time_t t;

    t = time(NULL);
    date = localtime(&t);
    g_return_if_fail(date != NULL);

    /* NOTE: SoundFont standard says that conventionally the date is in the format
     * "April 3, 2008".  This isn't international friendly though, so we instead
     * use the format YYYY-MM-DD which we have deemed OK, because of the use of
     * the word "conventionally" in the spec. */
    strftime(datestr, sizeof(datestr), "%Y-%m-%d", date);

    gtk_entry_set_text(GTK_ENTRY(user_data), datestr);
}

/* handler for IpatchSLIInst glade property interface.
 * Creates the Category combo box and selects its initial item */
static GtkWidget *
sli_inst_prop_handler(GtkWidget *widg, GObject *obj)
{
    GtkWidget *combo;
    GtkTreeModel *model;
    GtkCellRenderer *cell;
    gchar *catpath;
    GtkTreeIter iter;

    if(!widg)  /* create widget? */
    {
        /* get combo box widget */
        widg = swamigui_util_glade_create("PropSLIInst");
        combo = swamigui_util_glade_lookup(widg, "ComboCategory");
        gtk_combo_box_set_wrap_width(GTK_COMBO_BOX(combo), 2);
        /* create and attach a tree model */
        /* ++ ref new store */
        model = GTK_TREE_MODEL(gtk_tree_store_new(1, G_TYPE_STRING));
        cat_map_to_tree_store(GTK_TREE_STORE(model),
                              ipatch_sli_inst_get_cat_map(), NULL);
        gtk_combo_box_set_model(GTK_COMBO_BOX(combo), model);
        g_object_unref(model);  /* -- unref store */
        /* set cell renderer */
        cell = gtk_cell_renderer_text_new();
        gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combo), cell, FALSE);
        gtk_cell_layout_add_attribute(GTK_CELL_LAYOUT(combo), cell, "text", 0);
        /* connect callback function */
        g_signal_connect(combo, "changed", G_CALLBACK(category_changed_cb), NULL);
    }
    else
    {
        combo = swamigui_util_glade_lookup(widg, "ComboCategory");
    }

    /* connect prop controls to supplied object */
    swamigui_control_glade_prop_connect(widg, obj);
    /* FIXME: setting object data may be a hack due to the lack of understanding
     * if a better way exists to get this in the callback function */
    g_object_set_data(G_OBJECT(combo), "controlled-object", obj);

    /* set combo box to match category of the object */
    /* (setting active item in combo box emits a changed signal) */
    model = gtk_combo_box_get_model(GTK_COMBO_BOX(combo));
    catpath = ipatch_sli_inst_get_category_as_path(IPATCH_SLI_INST(obj));

    if(gtk_tree_model_get_iter_from_string(model, &iter, catpath))
    {
        gtk_combo_box_set_active_iter(GTK_COMBO_BOX(combo), &iter);
    }

    g_free(catpath);

    return (widg);
}

static void
cat_map_to_tree_store(GtkTreeStore *store, const IpatchSLIInstCatMapEntry *catmap, GtkTreeIter *parent)
{
    GtkTreeIter child;
    guint i;
    gchar *label, *prefix;
    const gchar **inst_cat_strings = ipatch_sli_inst_get_cat_strings();

    g_return_if_fail(store != NULL && catmap != NULL);

    for(i = 0; catmap[i].code != '@'; i++)
    {
        if(parent)
        {
            gtk_tree_model_get(GTK_TREE_MODEL(store), parent, 0, &prefix, -1);
            label = g_strjoin(" | ", prefix,
                              inst_cat_strings[catmap[i].name_idx], NULL);
            g_free(prefix);
        }
        else
        {
            label = g_strdup (inst_cat_strings[catmap[i].name_idx]);
        }

        gtk_tree_store_append(store, &child, parent);
        gtk_tree_store_set(store, &child, 0, label, -1);
        g_free(label);

        if(catmap[i].submap)  /* recursion for subcat maps */
        {
            cat_map_to_tree_store(store, catmap[i].submap, &child);
        }
    }

    /* Add Other category only at the topmost level */
    if(!parent)
    {
        gtk_tree_store_append(store, &child, parent);
        gtk_tree_store_set(store, &child, 0,
                           inst_cat_strings[catmap[i].name_idx], -1);
    }
}

/* callback when Category combo box is changed */
static void
category_changed_cb(GtkComboBox *combo, gpointer user_data)
{
    const IpatchSLIInstCatMapEntry *catmap = ipatch_sli_inst_get_cat_map();
    IpatchSLIInst *inst;
    gboolean ret;
    GtkTreeModel *model;
    GtkTreeIter iter;
    GtkTreePath *path;
    gint *pinds;
    guint old_cat, new_cat, i, d;

    inst = g_object_get_data(G_OBJECT(combo), "controlled-object");
    g_return_if_fail(IPATCH_IS_SLI_INST(inst));

    model = gtk_combo_box_get_model(combo);
    ret = gtk_combo_box_get_active_iter(combo, &iter);
    g_return_if_fail(ret == TRUE);
    path = gtk_tree_model_get_path(model, &iter);  /* ++ ref path */
    pinds = gtk_tree_path_get_indices(path);
    d = gtk_tree_path_get_depth(path);

    for(new_cat = 0, i = 0; catmap && i < d; i++)
    {
        new_cat <<= 8;
        new_cat |= catmap[pinds[i]].code;
        catmap = catmap[pinds[i]].submap;
    }

    if(i < 2)  /* treat subcat as Other if only main category is selected */
    {
        new_cat <<= 8;
        new_cat |= '@';
    }

    gtk_tree_path_free(path);  /* -- unref path */

    g_object_get(inst, "category", &old_cat, NULL);

    if(new_cat != old_cat)
    {
        g_object_set(inst, "category", new_cat, NULL);
    }
}

#ifdef MAC_INTEGRATION
/* Callback to convert Ctrl to Command in menu accelerator keys */
static void
osx_accel_map_foreach_lcb(gpointer data, const gchar *accel_path,
                          guint accel_key, GdkModifierType accel_mods,
                          gboolean changed)
{
    if(accel_mods & GDK_CONTROL_MASK)
    {
        accel_mods &= ~GDK_CONTROL_MASK;
        accel_mods |= GDK_META_MASK;

        gtk_accel_map_change_entry(accel_path, accel_key, accel_mods, FALSE);
    }
}

/* Callback called for NSApplicationShouldTerminate notification
 * we return TRUE to veto the termination here because the user can
 * cancel the quit dialog */
static gboolean swamigui_root_cb_should_quit(GtkosxApplication *theApp,
        gpointer data)
{
    swamigui_root_quit(SWAMIGUI_ROOT(data));
    return TRUE;
}
#endif

/**
 * swamigui_root_is_middle_click:
 * @root: Root object or NULL to use global root object
 * @event: Button event
 *
 * Check if a button event is a middle click, taking into account middle button emulation settings.
 */
gboolean
swamigui_root_is_middle_click(SwamiguiRoot *root, GdkEventButton *event)
{
    g_return_val_if_fail(!root || SWAMIGUI_IS_ROOT(root), FALSE);
    g_return_val_if_fail(event != NULL, FALSE);

    if(event->type == GDK_BUTTON_PRESS && event->button == 2)
    {
        return (TRUE);
    }

    if(!root)
    {
        root = swamigui_root;
    }

    if(!root)
    {
        return (FALSE);
    }

    // Middle button emulation enabled, left mouse button click and modifier held?
    return (event->type == GDK_BUTTON_PRESS && event->button == 1
            && root->middle_emul_enable && (event->state & (GDK_MOD1_MASK << root->middle_emul_mod)));
}

