/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
 * Copyright (C) 2001 Ximian, Inc.
 *
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 * Authors:
 *   Naba Kumar <naba@gnome.org>
 */

#include <config.h>

/**
 * SECTION:glade-app
 * @Short_Description: The central control point of the Glade core.
 *
 * This main control object is where we try to draw the line between
 * what is the Glade core and what is the main application. The main
 * application must derive from the GladeApp object and create an instance
 * to initialize the Glade core.
 */

#include "glade.h"
#include "glade-clipboard-view.h"
#include "glade-debug.h"
#include "glade-cursor.h"
#include "glade-catalog.h"
#include "glade-fixed.h"
#include "glade-marshallers.h"
#include "glade-accumulators.h"

#include <string.h>
#include <glib.h>
#include <glib/gstdio.h>
#include <glib/gi18n-lib.h>
#include <gdk/gdkkeysyms.h>
#include <gtk/gtkstock.h>

#define GLADE_CONFIG_FILENAME "glade-3.conf"

#define GLADE_APP_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), GLADE_TYPE_APP, GladeAppPrivate))

enum
{
	UPDATE_UI,
	LAST_SIGNAL
};

enum
{
	PROP_0,
	PROP_ACTIVE_PROJECT,
	PROP_POINTER_MODE
};

struct _GladeAppPrivate
{
	GtkWidget *window;
	
	GladePalette *palette;         /* See glade-palette */
	GladeProject *active_project;  /* Currently active project (if there is at least one
				        * project; then this is always valid) */
	GladeEditor *editor;           /* See glade-editor */
	GladeClipboard *clipboard;     /* See glade-clipboard */
	GList *catalogs;               /* See glade-catalog */

	GList *projects; /* The list of Projects */
	
	GKeyFile *config;/* The configuration file */

	GtkWindow *transient_parent; /* If set by glade_app_set_transient_parent(); this
				      * will be used as the transient parent of all toplevel
				      * GladeWidgets.
				      */
	GtkAccelGroup *accel_group;	/* Default acceleration group for this app */
	GList *undo_list, *redo_list;	/* Lists of buttons to refresh in update-ui signal */

	GladePointerMode pointer_mode;  /* Current mode for the pointer in the workspace */
};

static guint glade_app_signals[LAST_SIGNAL] = { 0 };

/* installation paths */
static gchar *catalogs_dir = NULL;
static gchar *modules_dir  = NULL;
static gchar *plugins_dir  = NULL;
static gchar *pixmaps_dir  = NULL;
static gchar *locale_dir   = NULL;

static GladeApp *singleton_app = NULL;

static void glade_init_check (void);

G_DEFINE_TYPE (GladeApp, glade_app, G_TYPE_OBJECT);


GType
glade_pointer_mode_get_type (void)
{
	static GType etype = 0;

	if (etype == 0) 
	{
		static const GEnumValue values[] = 
		{
			{ GLADE_POINTER_SELECT,      "select",      "Select widgets" },
			{ GLADE_POINTER_ADD_WIDGET,  "add",         "Add widgets" },
			{ GLADE_POINTER_DRAG_RESIZE, "drag-resize", "Drag and resize widgets" },
			{ 0, NULL, NULL }
		};
		etype = g_enum_register_static ("GladePointerMode", 
						values);
	}
	return etype;
}


/*****************************************************************
 *                    GObjectClass                               *
 *****************************************************************/
static GObject *
glade_app_constructor (GType                   type,
                       guint                   n_construct_properties,
                       GObjectConstructParam  *construct_properties)
{
	GObject *object;
	
	/* singleton */
	if (!singleton_app)
	{
		object = G_OBJECT_CLASS (glade_app_parent_class)->constructor (type,
									       n_construct_properties,
									       construct_properties);
		singleton_app = GLADE_APP (object);
	}
	else
	{
		g_object_ref (singleton_app);
	}
	  
	return G_OBJECT (singleton_app);
}



static void
glade_app_dispose (GObject *app)
{
	GladeAppPrivate *priv = GLADE_APP_GET_PRIVATE (app);
	
	if (priv->editor)
	{
		g_object_unref (priv->editor);
		priv->editor = NULL;
	}
	if (priv->palette)
	{
		g_object_unref (priv->palette);
		priv->palette = NULL;
	}
	if (priv->clipboard)
	{
		gtk_widget_destroy (GTK_WIDGET (priv->clipboard->view));
		priv->clipboard = NULL;
	}
	/* FIXME: Remove projects */
	
	if (priv->config)
	{
		g_key_file_free (priv->config);
		priv->config = NULL;
	}
	
	G_OBJECT_CLASS (glade_app_parent_class)->dispose (app);
}

static void
glade_app_finalize (GObject *app)
{
	g_free (catalogs_dir);
	g_free (modules_dir);
	g_free (pixmaps_dir);	
	g_free (locale_dir);
	
	glade_catalog_destroy_all ();

	G_OBJECT_CLASS (glade_app_parent_class)->finalize (app);
}

static void
glade_app_set_property (GObject      *object,
                        guint         property_id,
                        const GValue *value,
                        GParamSpec   *pspec)
{
	switch (property_id) 
	{
	case PROP_ACTIVE_PROJECT:
		glade_app_set_project (g_value_get_object (value));
		break;
	case PROP_POINTER_MODE:
		glade_app_set_pointer_mode (g_value_get_enum (value));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID
			(object, property_id, pspec);
		break;
	}
}

static void
glade_app_get_property (GObject      *object,
                        guint         property_id,
                        GValue       *value,
                        GParamSpec   *pspec)
{
	GladeApp *app = GLADE_APP (object);


	switch (property_id) 
	{
	case PROP_ACTIVE_PROJECT:
		g_value_set_object (value, app->priv->active_project);
		break;
	case PROP_POINTER_MODE:
		g_value_set_enum (value, app->priv->pointer_mode);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(object,property_id,pspec);
		break;
	}
}

/*****************************************************************
 *                    GladeAppClass                              *
 *****************************************************************/
static void
glade_app_refresh_undo_redo_button (GladeApp *app,
				    GtkWidget *button,
				    gboolean undo)
{
	GladeCommand *command = NULL;
	static GtkTooltips *button_tips = NULL;
	GladeProject *project;
	gchar *desc;

	if (button_tips == NULL)
		button_tips = gtk_tooltips_new ();
	
	if ((project = glade_app_get_project ()) != NULL)
	{
		if (undo)
			command = glade_project_next_undo_item (project);
		else
			command = glade_project_next_redo_item (project);
	}

	/* Change tooltips */
	desc = g_strdup_printf ((undo) ? _("Undo: %s") : _("Redo: %s"),
			command ? command->description : _("the last action"));
	gtk_tooltips_set_tip (GTK_TOOLTIPS (button_tips), button, desc, NULL);
	g_free (desc);

	/* Set sensitivity on the button */
	gtk_widget_set_sensitive (button, command != NULL);
}

static void
glade_app_update_ui_default (GladeApp *app)
{
	GList *list;
	
	for (list = app->priv->undo_list; list; list = list->next)
		if (list->data)
			glade_app_refresh_undo_redo_button (app, list->data, TRUE);

	for (list = app->priv->redo_list; list; list = list->next)
		if (list->data)
			glade_app_refresh_undo_redo_button (app, list->data, FALSE);
}

static void
on_palette_button_clicked (GladePalette *palette, GladeApp *app)
{
	GladeWidgetAdaptor *adaptor;
	GladeWidget        *widget;

	g_return_if_fail (GLADE_IS_PALETTE (palette));
	adaptor = glade_palette_get_current_item (palette);

	/* class may be NULL if the selector was pressed */
	if (adaptor && GWA_IS_TOPLEVEL (adaptor))
	{
		widget = glade_command_create (adaptor, NULL, NULL, app->priv->active_project);
		
		/* if this is a top level widget set the accel group */
		if (widget && app->priv->accel_group && GTK_IS_WINDOW (widget->object))
		{
			gtk_window_add_accel_group (GTK_WINDOW (widget->object),
						    app->priv->accel_group);
		}

		glade_palette_deselect_current_item (palette, FALSE);
	}
}

static gboolean
clipboard_view_on_delete_cb (GtkWidget *clipboard_view, GdkEvent *e, GladeApp *app)
{
	glade_util_hide_window (GTK_WINDOW (clipboard_view));
	return TRUE;
}

static GKeyFile *
glade_app_config_load (GladeApp *app)
{
	GKeyFile *config = g_key_file_new ();
	gchar *filename;

	filename = g_build_filename (g_get_user_config_dir (), GLADE_CONFIG_FILENAME, NULL);

	g_key_file_load_from_file (config, filename, G_KEY_FILE_NONE, NULL);
	
	g_free (filename);
	
	return config;
}

const gchar *
glade_app_get_catalogs_dir (void)
{
	glade_init_check ();
	
	return catalogs_dir;
}

const gchar *
glade_app_get_modules_dir (void)
{
	glade_init_check ();

	return modules_dir;
}

const gchar *
glade_app_get_plugins_dir (void)
{
	glade_init_check ();

	return plugins_dir;
}


const gchar *
glade_app_get_pixmaps_dir (void)
{
	glade_init_check ();

	return pixmaps_dir;
}

const gchar *
glade_app_get_locale_dir (void)
{
	glade_init_check ();
	
	return locale_dir;
}

/* build package paths at runtime */
static void
build_package_paths (void)
{
#ifdef G_OS_WIN32
	gchar *prefix;
	
	prefix = g_win32_get_package_installation_directory (NULL, NULL);
	pixmaps_dir  = g_build_filename (prefix, "share", PACKAGE, "pixmaps", NULL);
	catalogs_dir = g_build_filename (prefix, "share", PACKAGE, "catalogs", NULL);
	modules_dir  = g_build_filename (prefix, "lib", PACKAGE, "modules", NULL);
	locale_dir   = g_build_filename (prefix, "share", "locale", NULL);
	g_free (prefix);
#else
	catalogs_dir = g_strdup (GLADE_CATALOGSDIR);
	modules_dir  = g_strdup (GLADE_MODULESDIR);
	plugins_dir  = g_strdup (GLADE_PLUGINSDIR);
	pixmaps_dir  = g_strdup (GLADE_PIXMAPSDIR);
	locale_dir   = g_strdup (GLADE_LOCALEDIR);
#endif
}

/* initialization function for libgladeui (not GladeApp) */
static void
glade_init_check (void)
{
	static gboolean initialised = FALSE;
	
	if (initialised)
		return;

	build_package_paths ();

	bindtextdomain (GETTEXT_PACKAGE, locale_dir);
	bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");

	initialised = TRUE;
}

static void
glade_app_init (GladeApp *app)
{
	static gboolean initialized = FALSE;
	
	app->priv = GLADE_APP_GET_PRIVATE (app);	
	
	glade_init_check ();	
	
	if (!initialized)
	{
		gtk_icon_theme_append_search_path (gtk_icon_theme_get_default (),
						   GLADE_DATADIR G_DIR_SEPARATOR_S "pixmaps");	
	
		glade_cursor_init ();
		
		initialized = TRUE;
	}

	app->priv->accel_group = NULL;
	
	/* Initialize app objects */
	app->priv->catalogs = (GList *) glade_catalog_load_all ();
	
	/* Create palette */
	app->priv->palette = (GladePalette *) glade_palette_new (app->priv->catalogs);
	g_object_ref_sink (app->priv->palette);
	
	g_signal_connect (G_OBJECT (app->priv->palette), "toggled",
			  G_CALLBACK (on_palette_button_clicked), app);

	/* Create Editor */
	app->priv->editor = GLADE_EDITOR (glade_editor_new ());
	g_object_ref_sink (GTK_OBJECT (app->priv->editor));
	
	glade_editor_refresh (app->priv->editor);
	
	/* Create clipboard */
	app->priv->clipboard = glade_clipboard_new ();
	app->priv->clipboard->view = glade_clipboard_view_new (app->priv->clipboard);
	gtk_window_set_title (GTK_WINDOW (app->priv->clipboard->view), _("Clipboard"));
	g_signal_connect_after (G_OBJECT (app->priv->clipboard->view), "delete_event",
			  G_CALLBACK (clipboard_view_on_delete_cb),
			  app);

	/* Load the configuration file */
	app->priv->config = glade_app_config_load (app);
	
	/* Undo/Redo button list */
	app->priv->undo_list = app->priv->redo_list = NULL;
}

static void
glade_app_class_init (GladeAppClass * klass)
{
	GObjectClass *object_class;
	
	object_class = G_OBJECT_CLASS (klass);
	
	object_class->constructor  = glade_app_constructor;
	object_class->dispose      = glade_app_dispose;
	object_class->finalize     = glade_app_finalize;
	object_class->get_property = glade_app_get_property;
	object_class->set_property = glade_app_set_property;

	klass->update_ui_signal = glade_app_update_ui_default;
	klass->show_properties  = NULL;
	klass->hide_properties  = NULL;

	/**
	 * GladeApp::update-ui:
	 * @gladeapp: the #GladeApp which received the signal.
	 *
	 * Emitted when a project name changes or a cut/copy/paste/delete occurred.
	 */
	glade_app_signals[UPDATE_UI] =
		g_signal_new ("update-ui",
			      G_TYPE_FROM_CLASS (object_class),
			      G_SIGNAL_RUN_FIRST,
			      G_STRUCT_OFFSET (GladeAppClass,
					       update_ui_signal),
			      NULL, NULL,
			      g_cclosure_marshal_VOID__VOID,
			      G_TYPE_NONE, 0);

	g_object_class_install_property 
		(object_class, PROP_ACTIVE_PROJECT,
		 g_param_spec_object 
		 ("active-project", _("Active Project"), 
		  _("The active project"),
		  GLADE_TYPE_PROJECT, G_PARAM_READWRITE));

	g_object_class_install_property 
		(object_class, PROP_POINTER_MODE,
		 g_param_spec_enum 
		 ("pointer-mode", _("Pointer Mode"), 
		  _("Current mode for the pointer in the workspace"),
		  GLADE_TYPE_POINTER_MODE,
		  GLADE_POINTER_SELECT,
		  G_PARAM_READWRITE));
	
	g_type_class_add_private (klass, sizeof (GladeAppPrivate));
}

/*****************************************************************
 *                       Public API                              *
 *****************************************************************/
static void
on_widget_name_changed_cb (GladeProject *project,
			   GladeWidget *widget,
			   GladeEditor *editor)
{
	glade_editor_update_widget_name (editor);
}

static void
on_project_selection_changed_cb (GladeProject *project, GladeApp *app)
{
	GList *list;
	gint num;

	g_return_if_fail (GLADE_IS_PROJECT (project));
	g_return_if_fail (GLADE_IS_APP (app));

	/* Only update the editor if the selection has changed on
	 * the currently active project.
	 */
	if (app->priv->editor &&
	    (project == glade_app_get_project ()))
	{
		list = glade_project_selection_get (project);
		num = g_list_length (list);
		if (num == 1 && !GLADE_IS_PLACEHOLDER (list->data))
			glade_editor_load_widget (app->priv->editor,
						  glade_widget_get_from_gobject
						  (G_OBJECT (list->data)));
		else
			glade_editor_load_widget (app->priv->editor, NULL);
	}
}

/**
 * glade_app_config_save
 *
 * Saves the GKeyFile to "g_get_user_config_dir()/GLADE_CONFIG_FILENAME"
 *
 * Return 0 on success.
 */
gint
glade_app_config_save ()
{
	GIOChannel *channel;
	GIOStatus   status;
	gchar *data=NULL, *filename;
	const gchar *config_dir = g_get_user_config_dir ();
	GError *error = NULL;
	gsize size, written, bytes_written = 0;
	static gboolean error_shown = FALSE;
	GladeApp *app;

	/* If we had any errors; wait untill next session to retry.
	 */
	if (error_shown) return -1;

	app = glade_app_get ();
	
	/* Just in case... try to create the config directory */
	if (g_file_test (config_dir, G_FILE_TEST_IS_DIR) == FALSE)
	{
		if (g_file_test (config_dir, G_FILE_TEST_EXISTS))
		{
			/* Config dir exists but is not a directory */
			glade_util_ui_message
				(glade_app_get_window(),
				 GLADE_UI_ERROR,
				 _("Trying to save private data to %s directory "
				   "but it is a regular file.\n"
				   "No private data will be saved in this session"), 
				 config_dir);
			error_shown = TRUE;
			return -1;
		}
		else if (g_mkdir (config_dir, S_IRWXU) != 0)
		{
			/* Doesnt exist; failed to create */
			glade_util_ui_message
				(glade_app_get_window(),
				 GLADE_UI_ERROR,
				 _("Failed to create directory %s to save private data.\n"
				   "No private data will be saved in this session"), config_dir);
			error_shown = TRUE;
			return -1;
		}
	} 

	filename = g_build_filename (config_dir, GLADE_CONFIG_FILENAME, NULL);
	
	if ((channel = g_io_channel_new_file (filename, "w", &error)) != NULL)
	{
		if ((data = g_key_file_to_data (app->priv->config, &size, &error)) != NULL)
		{
			
			/* Implement loop here */
			while ((status = g_io_channel_write_chars
				(channel, 
				 data + bytes_written, /* Offset of write */
				 size - bytes_written, /* Size left to write */
				 &written, &error)) != G_IO_STATUS_ERROR &&
			       (bytes_written + written) < size)
				bytes_written += written;

			if (status == G_IO_STATUS_ERROR)
			{
				glade_util_ui_message
					(glade_app_get_window(),
					 GLADE_UI_ERROR,
					 _("Error writing private data to %s (%s).\n"
					   "No private data will be saved in this session"), 
					 filename, error->message);
				 error_shown = TRUE;
			}
			g_free (data);
		} 
		else
		{
			glade_util_ui_message
				(glade_app_get_window(),
				 GLADE_UI_ERROR,
				 _("Error serializing configuration data to save (%s).\n"
				   "No private data will be saved in this session"), 
				 error->message);
			error_shown = TRUE;
		}
		g_io_channel_shutdown(channel, TRUE, NULL);
		g_io_channel_unref (channel);
	}
	else
	{
		glade_util_ui_message
			(glade_app_get_window(),
			 GLADE_UI_ERROR,
			 _("Error opening %s to write private data (%s).\n"
			   "No private data will be saved in this session"), 
			 filename, error->message);
		error_shown = TRUE;
	}
	g_free (filename);
	
	if (error)
	{
		g_error_free (error);
		return -1;
	}
	return 0;
}

void
glade_app_set_transient_parent (GtkWindow *parent)
{
	GList     *projects, *objects;
	GladeApp  *app;
	
	g_return_if_fail (GTK_IS_WINDOW (parent));

	app = glade_app_get ();
	app->priv->transient_parent = parent;

	/* Loop over all projects/widgets and set_transient_for the toplevels.
	 */
	for (projects = glade_app_get_projects (); /* projects */
	     projects; projects = projects->next) 
		for (objects = (GList *) glade_project_get_objects (GLADE_PROJECT (projects->data));  /* widgets */
		     objects; objects = objects->next)
			if (GTK_IS_WINDOW (objects->data))
				gtk_window_set_transient_for
					(GTK_WINDOW (objects->data), parent);
}

GtkWindow *
glade_app_get_transient_parent (void)
{
	GtkWindow *parent;
	GladeApp  *app = glade_app_get ();

	parent = app->priv->transient_parent;
	
	return parent;
}

GladeApp *
glade_app_get (void)
{
	if (!singleton_app)
		g_critical ("No available GladeApp");
	return singleton_app;
}

void
glade_app_update_ui (void)
{
	GladeApp *app = glade_app_get ();

	g_signal_emit (G_OBJECT (app),
		       glade_app_signals[UPDATE_UI], 0);
}

void
glade_app_set_window (GtkWidget *window)
{
	GladeApp *app = glade_app_get ();

	app->priv->window = window;
}

GtkWidget *
glade_app_get_window (void)
{
	GladeApp  *app = glade_app_get ();
	return app->priv->window;
}

GladeEditor *
glade_app_get_editor (void)
{
	GladeApp *app    = glade_app_get ();
	return app->priv->editor;
}

GladePalette *
glade_app_get_palette (void)
{
	GladeApp *app = glade_app_get ();
	return app->priv->palette;
}

GladeClipboard *
glade_app_get_clipboard (void)
{
	GladeApp       *app       = glade_app_get ();
	return app->priv->clipboard;
}

GtkWidget *
glade_app_get_clipboard_view (void)
{
	GladeApp *app = glade_app_get ();
	return app->priv->clipboard->view;
}

GladeProject *
glade_app_get_project (void)
{
	GladeApp *app = glade_app_get ();
	return app->priv->active_project;
}

GList *
glade_app_get_projects (void)
{
	GladeApp *app = glade_app_get ();
	return app->priv->projects;
}

GKeyFile *
glade_app_get_config (void)
{
	GladeApp *app = glade_app_get ();
	return app->priv->config;
}

gboolean
glade_app_is_project_loaded (const gchar *project_path)
{
	GladeApp *app;
	GList    *list;
	gboolean  loaded = FALSE;

	if (project_path == NULL)
		return FALSE;

	app = glade_app_get ();

	for (list = app->priv->projects; list; list = list->next)
	{
		GladeProject *cur_project = GLADE_PROJECT (list->data);

		if ((loaded = glade_project_get_path (cur_project) && 
		     (strcmp (glade_project_get_path (cur_project), project_path) == 0)))
			break;
	}

	return loaded;
}

/**
 * glade_app_get_project_by_path:
 * @project_path: The path of an open project
 *
 * Finds an open project with @path
 *
 * Returns: A #GladeProject, or NULL if no such open project was found
 */
GladeProject*
glade_app_get_project_by_path (const gchar *project_path)
{
	GladeApp *app;
	GList    *l;
	gchar *canonical_path;

	if (project_path == NULL)
		return NULL;

	app = glade_app_get ();

	canonical_path = glade_util_canonical_path (project_path);

	for (l = app->priv->projects; l; l = l->next)
	{
		GladeProject *project = (GladeProject *) l->data;

		if (glade_project_get_path (project) && strcmp (canonical_path, glade_project_get_path (project)) == 0) {
			g_free (canonical_path);
			return project;
		}
	}

	g_free (canonical_path);

	return NULL;
}

void
glade_app_show_properties (gboolean raise)
{
	GladeApp  *app  = glade_app_get ();

	if (GLADE_APP_GET_CLASS (app)->show_properties)
		GLADE_APP_GET_CLASS (app)->show_properties (app, raise);
	else 
		g_critical ("%s not implemented\n", G_GNUC_FUNCTION);
}

void
glade_app_hide_properties (void)
{
	GladeApp  *app  = glade_app_get ();

	if (GLADE_APP_GET_CLASS (app)->hide_properties)
		GLADE_APP_GET_CLASS (app)->hide_properties (app);
	else 
		g_critical ("%s not implemented\n", G_GNUC_FUNCTION);

}

void
glade_app_update_instance_count (GladeProject *project)
{
	GladeApp  *app;
	GList *l;
	gint temp, max = 0, i = 0, uncounted_projects = 0;
	gchar *project_name;

	g_return_if_fail (GLADE_IS_PROJECT (project));
		
	if (glade_project_get_instance_count (project) > 0)
		return;

	project_name = glade_project_get_name (project);

	app = glade_app_get ();

	for (l = app->priv->projects; l; l = l->next)
	{
		GladeProject *prj = GLADE_PROJECT (l->data);
		gchar *name = glade_project_get_name (project);

		if (prj != project && !g_utf8_collate (name, project_name))
		{
			i++;
			temp = MAX (glade_project_get_instance_count (prj) + 1, i);
			max  = MAX (temp, max);

			if (glade_project_get_instance_count (prj) < 1)
				uncounted_projects++;
		}
		
		g_free (name);
	}
	
	g_free (project_name);

	/* Dont reset the initially opened project */
	if (uncounted_projects > 1 || g_list_find (app->priv->projects, project) == NULL)
	{
		glade_project_set_instance_count (project, MAX (max, i));
	}
}

void
glade_app_add_project (GladeProject *project)
{
	GladeApp  *app;
 	g_return_if_fail (GLADE_IS_PROJECT (project));

	/* If the project was previously loaded, don't re-load */
	if (glade_app_is_project_loaded (glade_project_get_path (project)))
	{
		glade_app_set_project (project);
		return;
	}
	glade_app_update_instance_count (project);

	app = glade_app_get ();
	
	g_object_ref (project);
	
	app->priv->projects = g_list_append (app->priv->projects, project);
	
	/* connect to the project signals so that the editor can be updated */
	g_signal_connect (G_OBJECT (project), "widget_name_changed",
			  G_CALLBACK (on_widget_name_changed_cb), app->priv->editor);
	g_signal_connect (G_OBJECT (project), "selection_changed",
			  G_CALLBACK (on_project_selection_changed_cb), app);

	/* add acceleration groups to every top level widget */
	if (app->priv->accel_group)
		glade_project_set_accel_group (project, app->priv->accel_group);
	
	glade_app_set_project (project);

	/* XXX I think the palette & editor should detect this by itself */
	gtk_widget_set_sensitive (GTK_WIDGET (app->priv->palette), TRUE);
	gtk_widget_set_sensitive (GTK_WIDGET (app->priv->editor), TRUE);

}

void
glade_app_remove_project (GladeProject *project)
{
	GladeApp *app;
	g_return_if_fail (GLADE_IS_PROJECT (project));

	app = glade_app_get ();

	app->priv->projects = g_list_remove (app->priv->projects, project);

	/* this is needed to prevent clearing the selection of a closed project 
	 */
	app->priv->active_project = NULL;
	
	/* If no more projects */
	if (app->priv->projects == NULL)
	{
		/* XXX I think the palette & editor should detect this. */
		gtk_widget_set_sensitive (GTK_WIDGET (app->priv->palette), FALSE);

		/* set loaded widget to NULL first so that we dont mess
		 * around with sensitivity of the editor children.
		 */
		glade_editor_load_widget (app->priv->editor, NULL);
		gtk_widget_set_sensitive (GTK_WIDGET (app->priv->editor), FALSE);
	} 
	else
		glade_app_set_project (g_list_last (app->priv->projects)->data);

	/* Its safe to just release the project as the project emits a
	 * "close" signal and everyone is responsable for cleaning up at
	 * that point.
	 */
	g_object_unref (project);

}


/**
 * glade_app_set_project:
 * @project: A #GladeProject
 *
 * Sets the active project in the #GladeApp to @project.
 */
void
glade_app_set_project (GladeProject *project)
{
	GladeApp *app = glade_app_get();

	g_return_if_fail (GLADE_IS_PROJECT (project));

	if (app->priv->active_project == project)
		return;

	if (!g_list_find (app->priv->projects, project))
	{
		g_warning ("Could not set project because it could not "
			   " be found in the app->priv->project list\n");
		return;
	}

	/* clear the selection in the previous project */
	if (app->priv->active_project)
		glade_project_selection_clear (app->priv->active_project, FALSE);

	app->priv->active_project = project;

	/* (XXX really ?) trigger the selection changed signal to update the editor */
	glade_project_selection_changed (project);
	
	/* Update UI */
	glade_app_update_ui ();

	g_object_notify (G_OBJECT (app), "active-project");
}


/**
 * glade_app_set_pointer_mode:
 * @mode: A #GladePointerMode
 *
 * Sets the #GladePointerMode
 */
void
glade_app_set_pointer_mode (GladePointerMode mode)
{
	GladeApp *app = glade_app_get();

	app->priv->pointer_mode = mode;

	g_object_notify (G_OBJECT (app), "pointer-mode");
}


/**
 * glade_app_get_pointer_mode:
 *
 * Gets the current #GladePointerMode
 * 
 * Returns: The #GladePointerMode
 */
GladePointerMode
glade_app_get_pointer_mode (void)
{
	GladeApp *app = glade_app_get();

	return app->priv->pointer_mode;
}

/**
 * glade_app_command_copy:
 * @app: A #GladeApp
 *
 * Copy the active project's selection (the new copies
 * will end up on the clipboard and will be set as
 * the clipboards selection).
 */
void
glade_app_command_copy (void)
{
	GladeApp           *app;
	GList              *widgets = NULL, *list;
	GladeWidget        *widget;
	gboolean            failed = FALSE;

	app = glade_app_get();
	if (app->priv->active_project == NULL)
	{
		return;
	}

	for (list = glade_app_get_selection ();
	     list && list->data; list = list->next)
	{
		widget  = glade_widget_get_from_gobject (GTK_WIDGET (list->data));
		widgets = g_list_prepend (widgets, widget);
		
		g_assert (widget);
		if (widget->internal)
		{
			glade_util_ui_message
				(glade_app_get_window(),
				 GLADE_UI_WARN,
				 _("You cannot copy a widget "
				   "internal to a composite widget."));
			failed = TRUE;
			break;
		}
	}

	if (failed == FALSE && widgets != NULL)
	{
		glade_command_copy (widgets);
		glade_app_update_ui ();
	}
	else if (widgets == NULL)
		glade_util_ui_message (glade_app_get_window(),
				       GLADE_UI_INFO,
				       _("No widget selected."));

	if (widgets) g_list_free (widgets);

}

/**
 * glade_app_command_cut:
 * @app: A #GladeApp
 *
 * Cut the active project's selection (the cut objects
 * will end up on the clipboard and will be set as
 * the clipboards selection).
 */
void
glade_app_command_cut (void)
{
	GladeApp           *app;
	GList              *widgets = NULL, *list;
	GladeWidget        *widget;
	gboolean            failed = FALSE;

	app = glade_app_get();
	if (app->priv->active_project == NULL)
		return;
	
	for (list = glade_app_get_selection ();
	     list && list->data; list = list->next)
	{
		widget  = glade_widget_get_from_gobject (GTK_WIDGET (list->data));
		widgets = g_list_prepend (widgets, widget);
		
		g_assert (widget);
		if (widget->internal)
		{
			glade_util_ui_message
				(glade_app_get_window(),
				 GLADE_UI_WARN,
				 _("You cannot cut a widget "
				   "internal to a composite widget."));
			failed = TRUE;
			break;
		}
	}

	if (failed == FALSE && widgets != NULL)
	{
		glade_command_cut (widgets);
		glade_app_update_ui ();
	}
	else if (widgets == NULL)
		glade_util_ui_message (glade_app_get_window(),
				       GLADE_UI_INFO,
				       _("No widget selected."));

	if (widgets) g_list_free (widgets);

}

/**
 * glade_app_command_paste:
 * @placeholder: A #GladePlaceholder
 *
 * Paste the clipboard selection to the active project's 
 * selection (the project must have only one object selected).
 */
void
glade_app_command_paste (GladePlaceholder *placeholder)
{
	GladeApp           *app;
	GladeClipboard     *clipboard;
	GList              *list;
	GladeWidget        *widget = NULL, *parent;
	gint                placeholder_relations = 0;
	GladeFixed	   *fixed = NULL;

	app = glade_app_get();
	if (app->priv->active_project == NULL)
		return;

	list      = glade_project_selection_get (app->priv->active_project);
	clipboard = glade_app_get_clipboard ();

	/* If there is a selection, paste in to the selected widget, otherwise
	 * paste into the placeholder's parent.
	 */
	parent = list ? glade_widget_get_from_gobject (list->data) : 
		 (placeholder) ? glade_placeholder_get_parent (placeholder) : NULL;
	
	if (parent && GLADE_IS_FIXED (parent)) fixed = GLADE_FIXED (parent);

	/* Check if parent is actually a container of any sort */
	if (parent && 
	    !glade_widget_adaptor_is_container (parent->adaptor))
	{
		glade_util_ui_message (glade_app_get_window(),
				       GLADE_UI_INFO,
				       _("Unable to paste to the selected parent"));
		return;
	}
		
	/* Check if selection is good */
	if ((list = glade_app_get_selection ()) != NULL)
	{
		if (g_list_length (list) != 1)
		{
			glade_util_ui_message (glade_app_get_window(),
					       GLADE_UI_INFO,
					       _("Unable to paste to multiple widgets"));

			return;
		}
	}
	
	/* Check if we have anything to paste */
	if (g_list_length (clipboard->selection) == 0)
	{
		glade_util_ui_message (glade_app_get_window (), GLADE_UI_INFO,
				    _("No widget selected on the clipboard"));

		return;
	}
	
	/* Check that we have compatible heirarchies */
	for (list = clipboard->selection; 
	     list && list->data; list = list->next)
	{
		widget = list->data;

		if (!GWA_IS_TOPLEVEL (widget->adaptor) && parent)
		{
			/* Count placeholder relations
			 */
			if (glade_widget_placeholder_relation (parent, widget))
				placeholder_relations++;
		}

		/* Check if there is no parent and at least on of the pasted
		 * widgets is not a toplevel 
		 */
		else if (!GWA_IS_TOPLEVEL (widget->adaptor) && !parent)
		{
			glade_util_ui_message (glade_app_get_window (),
					       GLADE_UI_INFO, 
					       _("Unable to paste widget %s without a parent"),
					       widget->name);
			return;
		}
	}

	g_assert (widget);

	/* A GladeFixed that doesnt use placeholders can only paste one
	 * at a time
 	 */
	if (GTK_WIDGET_TOPLEVEL (widget->object) == FALSE &&
	    parent && fixed && !GWA_USE_PLACEHOLDERS (parent->adaptor) &&
	    g_list_length (clipboard->selection) != 1) 
	{
		glade_util_ui_message (glade_app_get_window (), 
				       GLADE_UI_INFO,
				       _("Only one widget can be pasted at a "
					 "time to this container"));
		return;
	}

	/* Check that enough placeholders are available */
	if (parent && 
	    GWA_USE_PLACEHOLDERS (parent->adaptor) &&
	    glade_util_count_placeholders (parent) < placeholder_relations)
	{
		glade_util_ui_message (glade_app_get_window (), 
				       GLADE_UI_INFO,
				       _("Insufficient amount of placeholders in "
					 "target container"));
		return;
	}

	glade_command_paste (clipboard->selection, parent, placeholder);
	glade_app_update_ui ();
}


/**
 * glade_app_command_delete:
 *
 * Delete the active project's selection.
 */
void
glade_app_command_delete (void)
{
	GladeApp           *app;
	GList              *widgets = NULL, *list;
	GladeWidget        *widget;
	gboolean            failed = FALSE;

	app = glade_app_get();
	if (app->priv->active_project == NULL)
		return;

	for (list = glade_app_get_selection ();
	     list && list->data; list = list->next)
	{
		widget  = glade_widget_get_from_gobject (GTK_WIDGET (list->data));
		widgets = g_list_prepend (widgets, widget);
		
		g_assert (widget);
		if (widget->internal)
		{
			glade_util_ui_message
				(glade_app_get_window(),
				 GLADE_UI_WARN,
				 _("You cannot delete a widget "
				   "internal to a composite widget."));
			failed = TRUE;
			break;
		}
	}

	if (failed == FALSE && widgets != NULL)
	{
		glade_command_delete (widgets);
		glade_app_update_ui ();
	}
	else if (widgets == NULL)
		glade_util_ui_message (glade_app_get_window(),
				       GLADE_UI_INFO,
				       _("No widget selected."));

	if (widgets) g_list_free (widgets);
}

/**
 * glade_app_command_delete_clipboard:
 *
 * Delete the clipboard's selection.
 */
void
glade_app_command_delete_clipboard (void)
{
	GladeClipboard  *clipboard;
	GladeWidget     *gwidget;
	GList           *list;

	clipboard = glade_app_get_clipboard ();

	if (clipboard->selection == NULL)
		glade_util_ui_message (glade_app_get_window (), GLADE_UI_INFO,
				    _("No widget selected on the clipboard"));

	for (list = clipboard->selection; list; list = list->next)
	{
		gwidget = list->data;
		if (gwidget->internal)
		{
			glade_util_ui_message
				(glade_app_get_window(),
				 GLADE_UI_WARN,
				 _("You cannot delete a widget "
				   "internal to a composite widget."));
			return;
		}
	}

	glade_command_delete (clipboard->selection);
	glade_app_update_ui ();
}


void
glade_app_command_undo (void)
{
	GladeApp *app = glade_app_get();
	if (app->priv->active_project)
	{
		glade_project_undo (app->priv->active_project);
		glade_editor_refresh (app->priv->editor);
		/* Update UI. */
		glade_app_update_ui ();
	}
}

void
glade_app_command_redo (void)
{
	GladeApp *app = glade_app_get();
	if (app->priv->active_project)
	{
		glade_project_redo (app->priv->active_project);
		glade_editor_refresh (app->priv->editor);
		/* Update UI. */
		glade_app_update_ui ();
	}
}

/*
 * glade_app_set_accel_group:
 *
 * Sets @accel_group to app.
 * The acceleration group will be attached to every toplevel widget in this application.
 */
void
glade_app_set_accel_group (GtkAccelGroup *accel_group)
{
	GladeApp *app;
	GList *l;
	GladeProject *project;
	g_return_if_fail(GTK_IS_ACCEL_GROUP (accel_group));

	app = glade_app_get ();

	for (l = app->priv->projects; l; l = l->next)
	{
		project = l->data;
		glade_project_set_accel_group (project, accel_group);
	}
	
	app->priv->accel_group = accel_group;
}

static gboolean
glade_app_undo_button_destroyed (GtkWidget *button, GladeApp *app)
{
	app->priv->undo_list = g_list_remove (app->priv->undo_list, button);
	return FALSE;
}

static gboolean
glade_app_redo_button_destroyed (GtkWidget *button, GladeApp *app)
{
	app->priv->redo_list = g_list_remove (app->priv->redo_list, button);
	return FALSE;
}

static GtkWidget *
glade_app_undo_redo_button_new (GladeApp *app, gboolean undo)
{
	GtkWidget *button;
	
	button = gtk_button_new_from_stock ((undo) ? 
					    GTK_STOCK_UNDO : 
					    GTK_STOCK_REDO);
	
	g_signal_connect_swapped (button, "clicked",
				  (undo) ? G_CALLBACK (glade_app_command_undo) : 
					   G_CALLBACK (glade_app_command_redo),
				  app);
	
	if (undo)
	{
		app->priv->undo_list = g_list_prepend (app->priv->undo_list, button);
		g_signal_connect (button, "destroy",
				  G_CALLBACK (glade_app_undo_button_destroyed),
				  app);
	}
	else
	{
		app->priv->redo_list = g_list_prepend (app->priv->redo_list, button);
		g_signal_connect (button, "destroy",
				  G_CALLBACK (glade_app_redo_button_destroyed),
				  app);
	}
	
	glade_app_refresh_undo_redo_button (app, button, undo);
	
	return button;
}

/*
 * glade_app_undo_button_new:
 *
 * Creates a new GtkButton undo widget.
 * The button will be automatically updated with @app's undo stack.
 */
GtkWidget *
glade_app_undo_button_new (void)
{
	GladeApp  *app    = glade_app_get();
	return glade_app_undo_redo_button_new (app, TRUE);
}

/*
 * glade_app_redo_button_new:
 *
 * Creates a new GtkButton redo widget.
 * The button will be automatically updated with @app's redo stack.
 */
GtkWidget *
glade_app_redo_button_new (void)
{
	GladeApp  *app    = glade_app_get();
	return glade_app_undo_redo_button_new (app, FALSE);
}

GList*
glade_app_get_selection (void)
{
	GList *selection = NULL, *list;
	GladeProject *project;

	for (list = glade_app_get_projects (); 
	     list && list->data; list = list->next)
	{
		/* Only one project may have selection at a time
		 */
		project = list->data;
		if (glade_project_selection_get (project))
		{
			selection = glade_project_selection_get (project);
			break;
		}
	}
	return selection;
}


gboolean
glade_app_is_selected (GObject *object)
{
	return (g_list_find (glade_app_get_selection (), object) != NULL);
}

void
glade_app_selection_set (GObject  *object,
			 gboolean  emit_signal)
{
	GList        *list;
	GladeProject *project;

	for (list = glade_app_get_projects ();
	     list && list->data; list = list->next)
	{
		project = list->data;
		if (glade_project_has_object (project, object))
			glade_project_selection_set (project, 
						     object, 
						     emit_signal);
		else
			glade_project_selection_clear (project, emit_signal);
	}

	/* Instead of calling selection_set after all
	 * the selection_clear calls (lazy).
	 */
	if (GTK_IS_WIDGET (object))
		glade_util_add_selection (GTK_WIDGET (object));
}

void
glade_app_selection_add (GObject      *object,
			 gboolean      emit_signal)
{
	GList        *list;
	GladeWidget  *widget   = glade_widget_get_from_gobject (object),  
		*selected;
	GladeProject *project  = glade_widget_get_project (widget);

	/* Ignore request if the there is a selection 
	 * from another project.
	 */
	if ((list = glade_app_get_selection ()) != NULL)
	{
		selected = glade_widget_get_from_gobject (list->data);
		if (glade_widget_get_project (selected) != project)
			return;
	}
	glade_project_selection_add (project, object, emit_signal);
}

void
glade_app_selection_remove (GObject      *object,
			    gboolean      emit_signal)
{
	GladeWidget  *widget   = glade_widget_get_from_gobject (object);
	GladeProject *project  = glade_widget_get_project (widget);;

	glade_project_selection_remove (project, object, emit_signal);
}

void
glade_app_selection_clear (gboolean      emit_signal)
{
	GList        *list;
	GladeProject *project;

	glade_util_clear_selection ();
	for (list = glade_app_get_projects ();
	     list && list->data; list = list->next)
	{
		project = list->data;
		glade_project_selection_clear (project, emit_signal);
	}
}

void
glade_app_selection_changed (void)
{
	GList        *list;
	GladeProject *project;

	for (list = glade_app_get_projects ();
	     list && list->data; list = list->next)
	{
		project = list->data;
		glade_project_selection_changed (project);
	}
}

GladeApp*
glade_app_new (void)
{
	return g_object_new (GLADE_TYPE_APP, NULL);
}



syntax highlighted by Code2HTML, v. 0.9.1