aboutsummaryrefslogblamecommitdiffstats
path: root/lib/egg/egg-menu-merge.c
blob: 71c7fb8e02ee43790cbf0708451af961b895ed2b (plain) (tree)
1
2
3
4
5



                                 
                    
























                                                                              
                                                                              

                                                                            
                                                                              














                                                                       
                          























                                                    













                                         


                                                    






                                                       








































                                                                                 
                                                                         

                                                            
                                                                         










































































                                                                                


























































































                                                                                
                                                                 














                                                                              
                                                                


































































































                                                                     
                                                                             













                                                                               
                                                                             
















                                                                               
                                                                     
















                                                                               
                                                                             














                                                                               
                                                                             














                                                                               
                                                                             


















                                                                               
                                                                             
















                                                                               
                                                                             




















                                                                               
                                                                     


















                                                                               
                                                                     






























































































                                                                                  
 











































































                                                                       
                                                                       







































































































































                                                                                     

                                                                                          





















                                                                         

                                                                        






















                                             




                                                         






































                                                                              
                            





































































































































                                                                                     
                                                                      



                                                             
                                                                        






















































                                                                           
                                                            
                                                                                  























                                                                                   
                                                                        







































































                                                                          






































                                                                   
#include <string.h>
#include "egg-menu-merge.h"
#include "eggtoolbar.h"
#include "eggseparatortoolitem.h"
#include "eggintl.h"

#define NODE_INFO(node) ((EggMenuMergeNode *)node->data)

typedef struct {
  guint merge_id;
  GQuark action_quark;
} NodeUIReference;

static void   egg_menu_merge_class_init    (EggMenuMergeClass *class);
static void   egg_menu_merge_init          (EggMenuMerge *merge);

static void   egg_menu_merge_queue_update  (EggMenuMerge *self);
static void   egg_menu_merge_dirty_all     (EggMenuMerge *self);

static GNode *get_child_node               (EggMenuMerge *self, GNode *parent,
                        const gchar *childname,
                        gint childname_length,
                        EggMenuMergeNodeType node_type,
                        gboolean create, gboolean top);
static GNode *egg_menu_merge_get_node      (EggMenuMerge *self,
                        const gchar *path,
                        EggMenuMergeNodeType node_type,
                        gboolean create);
static guint egg_menu_merge_next_merge_id  (EggMenuMerge *self);

static void  egg_menu_merge_node_prepend_ui_reference (EggMenuMergeNode *node,
                               guint merge_id,
                               GQuark action_quark);
static void  egg_menu_merge_node_remove_ui_reference  (EggMenuMergeNode *node,
                               guint merge_id);

enum {
  ADD_WIDGET,
  REMOVE_WIDGET,
  LAST_SIGNAL
};

static guint merge_signals[LAST_SIGNAL] = { 0 };

static GMemChunk *merge_node_chunk = NULL;

GType
egg_menu_merge_get_type (void)
{
  static GtkType type = 0;

  if (!type)
    {
      static const GTypeInfo type_info =
      {
        sizeof (EggMenuMergeClass),
        (GBaseInitFunc) NULL,
        (GBaseFinalizeFunc) NULL,
        (GClassInitFunc) egg_menu_merge_class_init,
        (GClassFinalizeFunc) NULL,
        NULL,
        
        sizeof (EggMenuMerge),
        0, /* n_preallocs */
        (GInstanceInitFunc) egg_menu_merge_init,
      };

      type = g_type_register_static (G_TYPE_OBJECT,
                     "EggMenuMerge",
                     &type_info, 0);
    }
  return type;
}

static GObjectClass *parent_class = NULL;

static void
egg_menu_merge_finalize (GObject *object)
{
  EggMenuMerge *merge;

  merge = EGG_MENU_MERGE (object);
  if (merge->update_tag != 0)
    {
      g_source_remove(merge->update_tag);
    }
}

static void
egg_menu_merge_class_init (EggMenuMergeClass *class)
{
  GObjectClass *object_class;

  parent_class = g_type_class_peek_parent (class);
  object_class = G_OBJECT_CLASS(class);

  object_class->finalize     = egg_menu_merge_finalize;

  if (!merge_node_chunk)
    merge_node_chunk = g_mem_chunk_create(EggMenuMergeNode, 64,
                      G_ALLOC_AND_FREE);

  merge_signals[ADD_WIDGET] =
    g_signal_new ("add_widget",
          G_OBJECT_CLASS_TYPE (class),
          G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE,
          G_STRUCT_OFFSET (EggMenuMergeClass, add_widget), NULL, NULL,
          g_cclosure_marshal_VOID__OBJECT,
          G_TYPE_NONE, 1,
          GTK_TYPE_WIDGET);
  merge_signals[REMOVE_WIDGET] =
    g_signal_new ("remove_widget",
          G_OBJECT_CLASS_TYPE (class),
          G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE,
          G_STRUCT_OFFSET (EggMenuMergeClass, remove_widget), NULL, NULL,
          g_cclosure_marshal_VOID__OBJECT,
          G_TYPE_NONE, 1,
          GTK_TYPE_WIDGET);  
  
}


static void
egg_menu_merge_init (EggMenuMerge *self)
{
  guint merge_id;
  GNode *node;

  self->accel_group = gtk_accel_group_new();

  self->root_node = NULL;
  self->action_groups = NULL;

  self->last_merge_id = 0;


  merge_id = egg_menu_merge_next_merge_id(self);
  node = get_child_node(self, NULL, "Root", 4,
            EGG_MENU_MERGE_ROOT, TRUE, FALSE);
  egg_menu_merge_node_prepend_ui_reference(NODE_INFO(node), merge_id, 0);
  node = get_child_node(self, self->root_node, "popups", 6,
            EGG_MENU_MERGE_POPUPS, TRUE, FALSE);
  egg_menu_merge_node_prepend_ui_reference(NODE_INFO(node), merge_id, 0);
}

EggMenuMerge *
egg_menu_merge_new (void)
{
  return g_object_new(EGG_TYPE_MENU_MERGE, NULL);
}

void
egg_menu_merge_insert_action_group (EggMenuMerge *self,
                 EggActionGroup *action_group, gint pos)
{
  g_return_if_fail (EGG_IS_MENU_MERGE(self));
  g_return_if_fail (EGG_IS_ACTION_GROUP(action_group));
  g_return_if_fail (g_list_find(self->action_groups, action_group) == NULL);

  g_object_ref(action_group);
  self->action_groups = g_list_insert(self->action_groups, action_group, pos);

  /* dirty all nodes, as action bindings may change */
  egg_menu_merge_dirty_all(self);
}

void
egg_menu_merge_remove_action_group (EggMenuMerge *self,
                    EggActionGroup *action_group)
{
  g_return_if_fail (EGG_IS_MENU_MERGE(self));
  g_return_if_fail (EGG_IS_ACTION_GROUP(action_group));
  g_return_if_fail (g_list_find(self->action_groups, action_group) != NULL);

  self->action_groups = g_list_remove(self->action_groups, action_group);
  g_object_unref(action_group);

  /* dirty all nodes, as action bindings may change */
  egg_menu_merge_dirty_all(self);
}

GtkWidget *
egg_menu_merge_get_widget (EggMenuMerge *self, const gchar *path)
{
  GNode *node;

  /* ensure that there are no pending updates before we get the
   * widget */
  egg_menu_merge_ensure_update(self);

  node = egg_menu_merge_get_node(self, path, EGG_MENU_MERGE_UNDECIDED, FALSE);
  return NODE_INFO(node)->proxy;
}

static GNode *
get_child_node(EggMenuMerge *self, GNode *parent,
           const gchar *childname, gint childname_length,
           EggMenuMergeNodeType node_type,
           gboolean create, gboolean top)
{
  GNode *child = NULL;

  g_return_val_if_fail(parent == NULL ||
           (NODE_INFO(parent)->type != EGG_MENU_MERGE_MENUITEM &&
        NODE_INFO(parent)->type != EGG_MENU_MERGE_TOOLITEM), NULL);

  if (parent)
    {
      if (childname)
    {
      for (child = parent->children; child != NULL; child = child->next)
        {
          if (strlen(NODE_INFO(child)->name) == childname_length &&
          !strncmp(NODE_INFO(child)->name, childname, childname_length))
        {
          /* if undecided about node type, set it */
          if (NODE_INFO(child)->type == EGG_MENU_MERGE_UNDECIDED)
            NODE_INFO(child)->type = node_type;
          return child;
        }
        }
    }
      if (!child && create)
    {
      EggMenuMergeNode *mnode;

      mnode = g_chunk_new0(EggMenuMergeNode, merge_node_chunk);
      mnode->type = node_type;
      mnode->name = g_strndup(childname, childname_length);
      mnode->dirty = TRUE;

      if (top)
        child = g_node_prepend_data(parent, mnode);
      else
        child = g_node_append_data(parent, mnode);
    }
    }
  else
    {
      /* handle root node */
      if (self->root_node)
    {
      child = self->root_node;
      if (strncmp(NODE_INFO(child)->name, childname, childname_length) !=0)
        g_warning("root node name '%s' doesn't match '%s'",
              childname, NODE_INFO(child)->name);
      if (NODE_INFO(child)->type != EGG_MENU_MERGE_ROOT)
        g_warning("base element must be of type ROOT");
    }
      else if (create)
    {
      EggMenuMergeNode *mnode;

      mnode = g_chunk_new0(EggMenuMergeNode, merge_node_chunk);
      mnode->type = node_type;
      mnode->name = g_strndup(childname, childname_length);
      mnode->dirty = TRUE;
      
      child = self->root_node = g_node_new(mnode);
    }
    }

  return child;
}

static GNode *
egg_menu_merge_get_node(EggMenuMerge *self, const gchar *path,
            EggMenuMergeNodeType node_type, gboolean create)
{
  const gchar *pos, *end;
  GNode *parent, *node;

  end = path + strlen(path);
  pos = path;
  parent = node = NULL;
  while (pos < end)
    {
      const gchar *slash;
      gsize length;

      slash = strchr(pos, '/');
      if (slash)
    length = slash - pos;
      else
    length = strlen(pos);

      node = get_child_node(self, parent, pos, length, EGG_MENU_MERGE_UNDECIDED,
                create, FALSE);
      if (!node)
    return NULL;

      pos += length + 1; /* move past the node name and the slash too */
      parent = node;
    }

  if (NODE_INFO(node)->type == EGG_MENU_MERGE_UNDECIDED)
    NODE_INFO(node)->type = node_type;
  return node;
}

static guint
egg_menu_merge_next_merge_id (EggMenuMerge *self)
{
  self->last_merge_id++;

  return self->last_merge_id;
}

static void
egg_menu_merge_node_prepend_ui_reference (EggMenuMergeNode *node,
                      guint merge_id, GQuark action_quark)
{
  NodeUIReference *reference;

  reference = g_new (NodeUIReference, 1);
  reference->action_quark = action_quark;
  reference->merge_id = merge_id;

  /* Prepend the reference */
  node->uifiles = g_list_prepend (node->uifiles, reference);

  node->dirty = TRUE;
}

static void
egg_menu_merge_node_remove_ui_reference (EggMenuMergeNode *node,
                     guint merge_id)
{
  GList *p;

  for (p = node->uifiles; p != NULL; p = p->next)
    {
      NodeUIReference *reference = p->data;
      
      if (reference->merge_id == merge_id)
    {
      node->uifiles = g_list_remove_link (node->uifiles, p);
      node->dirty = TRUE;
      g_free (reference);

      break;
    }
    }
}

/* -------------------- The UI file parser -------------------- */

typedef enum {
  STATE_START,
  STATE_ROOT,
  STATE_MENU,
  STATE_TOOLBAR,
  STATE_POPUPS,
  STATE_MENUITEM,
  STATE_TOOLITEM,
  STATE_END
} ParseState;

typedef struct _ParseContext ParseContext;
struct _ParseContext
{
  ParseState state;
  ParseState prev_state;

  EggMenuMerge *self;

  GNode *current;

  guint merge_id;
};

static void
start_element_handler (GMarkupParseContext *context,
               const gchar *element_name,
               const gchar **attribute_names,
               const gchar **attribute_values,
               gpointer user_data,
               GError **error)
{
  ParseContext *ctx = user_data;
  EggMenuMerge *self = ctx->self;

  gint i;
  const gchar *node_name;
  GQuark verb_quark;
  gboolean top;

  gboolean raise_error = TRUE;
  gchar *error_attr = NULL;

  //g_message("starting element %s", element_name);

  /* work out a name for this node.  Either the name attribute, or
   * element name */
  node_name = element_name;
  verb_quark = 0;
  top = FALSE;
  for (i = 0; attribute_names[i] != NULL; i++)
    {
      if (!strcmp(attribute_names[i], "name"))
    {
      node_name = attribute_values[i];
    }
      else if (!strcmp(attribute_names[i], "verb"))
    {
      verb_quark = g_quark_from_string(attribute_values[i]);
    }
      else if (!strcmp(attribute_names[i], "pos"))
    {
      top = !strcmp(attribute_values[i], "top");
    }
    }
  /* if no verb, then set it to the node's name */
  if (verb_quark == 0)
    verb_quark = g_quark_from_string(node_name);

  switch (element_name[0])
    {
    case 'R':
      if (ctx->state == STATE_START && !strcmp(element_name, "Root"))
    {
      ctx->state = STATE_ROOT;
      ctx->current = self->root_node;
      raise_error = FALSE;

      egg_menu_merge_node_prepend_ui_reference (NODE_INFO (ctx->current),
                            ctx->merge_id, verb_quark);
    }
      break;
    case 'm':
      if (ctx->state == STATE_ROOT && !strcmp(element_name, "menu"))
    {
      ctx->state = STATE_MENU;
      ctx->current = get_child_node(self, ctx->current,
                    node_name, strlen(node_name),
                    EGG_MENU_MERGE_MENUBAR,
                    TRUE, FALSE);
      if (NODE_INFO(ctx->current)->action_name == 0)
        NODE_INFO(ctx->current)->action_name = verb_quark;

      egg_menu_merge_node_prepend_ui_reference (NODE_INFO (ctx->current),
                            ctx->merge_id, verb_quark);
      NODE_INFO(ctx->current)->dirty = TRUE;

      raise_error = FALSE;
    }
      else if (ctx->state == STATE_MENU && !strcmp(element_name, "menuitem"))
    {
      GNode *node;

      ctx->state = STATE_MENUITEM;
      node = get_child_node(self, ctx->current,
                node_name, strlen(node_name),
                EGG_MENU_MERGE_MENUITEM,
                TRUE, top);
      if (NODE_INFO(node)->action_name == 0)
        NODE_INFO(node)->action_name = verb_quark;

      egg_menu_merge_node_prepend_ui_reference (NODE_INFO (node),
                            ctx->merge_id, verb_quark);
      NODE_INFO(node)->dirty = TRUE;

      raise_error = FALSE;
    }
      break;
    case 'd':
      if (ctx->state == STATE_ROOT && !strcmp(element_name, "dockitem"))
    {
      ctx->state = STATE_TOOLBAR;
      ctx->current = get_child_node(self, ctx->current,
                    node_name, strlen(node_name),
                    EGG_MENU_MERGE_TOOLBAR,
                    TRUE, FALSE);
      if (NODE_INFO(ctx->current)->action_name == 0)
        NODE_INFO(ctx->current)->action_name = verb_quark;

      egg_menu_merge_node_prepend_ui_reference (NODE_INFO (ctx->current),
                            ctx->merge_id, verb_quark);
      NODE_INFO(ctx->current)->dirty = TRUE;

      raise_error = FALSE;
    }
      break;
    case 'p':
      if (ctx->state == STATE_ROOT && !strcmp(element_name, "popups"))
    {
      ctx->state = STATE_POPUPS;
      ctx->current = get_child_node(self, ctx->current,
                    node_name, strlen(node_name),
                    EGG_MENU_MERGE_POPUPS,
                    TRUE, FALSE);

      egg_menu_merge_node_prepend_ui_reference (NODE_INFO (ctx->current),
                            ctx->merge_id, verb_quark);
      NODE_INFO(ctx->current)->dirty = TRUE;

      raise_error = FALSE;
    }
      else if (ctx->state == STATE_POPUPS && !strcmp(element_name, "popup"))
    {
      ctx->state = STATE_MENU;
      ctx->current = get_child_node(self, ctx->current,
                    node_name, strlen(node_name),
                    EGG_MENU_MERGE_MENU,
                    TRUE, FALSE);
      if (NODE_INFO(ctx->current)->action_name == 0)
        NODE_INFO(ctx->current)->action_name = verb_quark;

      egg_menu_merge_node_prepend_ui_reference (NODE_INFO (ctx->current),
                            ctx->merge_id, verb_quark);
      NODE_INFO(ctx->current)->dirty = TRUE;

      raise_error = FALSE;
    }
      else if ((ctx->state == STATE_MENU || ctx->state == STATE_TOOLBAR) &&
           !strcmp(element_name, "placeholder"))
    {
      if (ctx->state == STATE_MENU)
        ctx->current = get_child_node(self, ctx->current,
                      node_name, strlen(node_name),
                      EGG_MENU_MERGE_MENU_PLACEHOLDER,
                      TRUE, top);
      else
        ctx->current = get_child_node(self, ctx->current,
                      node_name, strlen(node_name),
                      EGG_MENU_MERGE_TOOLBAR_PLACEHOLDER,
                      TRUE, top);

      egg_menu_merge_node_prepend_ui_reference (NODE_INFO (ctx->current),
                            ctx->merge_id, verb_quark);
      NODE_INFO(ctx->current)->dirty = TRUE;

      raise_error = FALSE;
    }
      break;
    case 's':
      if (ctx->state == STATE_MENU && !strcmp(element_name, "submenu"))
    {
      ctx->state = STATE_MENU;
      ctx->current = get_child_node(self, ctx->current,
                    node_name, strlen(node_name),
                    EGG_MENU_MERGE_MENU,
                    TRUE, top);
      if (NODE_INFO(ctx->current)->action_name == 0)
        NODE_INFO(ctx->current)->action_name = verb_quark;

      egg_menu_merge_node_prepend_ui_reference (NODE_INFO (ctx->current),
                            ctx->merge_id, verb_quark);
      NODE_INFO(ctx->current)->dirty = TRUE;

      raise_error = FALSE;
    }
      else if ((ctx->state == STATE_MENU || ctx->state == STATE_TOOLBAR) &&
           !strcmp(element_name, "separator"))
    {
      GNode *node;

      if (ctx->state == STATE_MENU)
        ctx->state = STATE_MENUITEM;
      else
        ctx->state = STATE_TOOLITEM;
      node = get_child_node(self, ctx->current,
                node_name, strlen(node_name),
                EGG_MENU_MERGE_SEPARATOR,
                TRUE, top);
      if (NODE_INFO(node)->action_name == 0)
        NODE_INFO(node)->action_name = verb_quark;

      egg_menu_merge_node_prepend_ui_reference (NODE_INFO (node),
                            ctx->merge_id, verb_quark);
      NODE_INFO(node)->dirty = TRUE;

      raise_error = FALSE;
    }
      break;
    case 't':
      if (ctx->state == STATE_TOOLBAR && !strcmp(element_name, "toolitem"))
    {
      GNode *node;

      ctx->state = STATE_TOOLITEM;
      node = get_child_node(self, ctx->current,
                node_name, strlen(node_name),
                EGG_MENU_MERGE_TOOLITEM,
                TRUE, top);
      if (NODE_INFO(node)->action_name == 0)
        NODE_INFO(node)->action_name = verb_quark;

      egg_menu_merge_node_prepend_ui_reference (NODE_INFO (node),
                            ctx->merge_id, verb_quark);
      NODE_INFO(node)->dirty = TRUE;

      raise_error = FALSE;
    }
      break;
    default:
      break;
    }
  if (raise_error)
    {
      gint line_number, char_number;
 
      g_markup_parse_context_get_position (context,
                       &line_number, &char_number);
      if (error_attr)
    g_set_error (error,
             G_MARKUP_ERROR,
             G_MARKUP_ERROR_UNKNOWN_ATTRIBUTE,
             _("Unknown attribute '%s' on line %d char %d"),
             error_attr,
             line_number, char_number);
      else
    g_set_error (error,
             G_MARKUP_ERROR,
             G_MARKUP_ERROR_UNKNOWN_ELEMENT,
             _("Unknown tag '%s' on line %d char %d"),
             element_name,
             line_number, char_number);
    }
}

static void
end_element_handler (GMarkupParseContext *context,
             const gchar *element_name,
             gpointer user_data,
             GError **error)
{
  ParseContext *ctx = user_data;
  EggMenuMerge *self = ctx->self;

  //g_message("ending element %s (state=%d)", element_name, ctx->state);

  switch (ctx->state)
    {
    case STATE_START:
      g_warning("shouldn't get any end tags in start state");
      /* should we GError here? */
      break;
    case STATE_ROOT:
      if (ctx->current != self->root_node)
    g_warning("we are in STATE_ROOT, but the current node isn't the root");
      ctx->current = NULL;
      ctx->state = STATE_END;
      break;
    case STATE_MENU:
      ctx->current = ctx->current->parent;
      if (NODE_INFO(ctx->current)->type == EGG_MENU_MERGE_ROOT) /* menubar */
    ctx->state = STATE_ROOT;
      else if (NODE_INFO(ctx->current)->type == EGG_MENU_MERGE_POPUPS) /* popup */
    ctx->state = STATE_POPUPS;
      /* else, stay in STATE_MENU state */
      break;
    case STATE_TOOLBAR:
      ctx->current = ctx->current->parent;
      /* we conditionalise this test, in case we are closing off a
       * placeholder */
      if (NODE_INFO(ctx->current)->type == EGG_MENU_MERGE_ROOT)
    ctx->state = STATE_ROOT;
      /* else, stay in STATE_TOOLBAR state */
      break;
    case STATE_POPUPS:
      ctx->current = ctx->current->parent;
      ctx->state = STATE_ROOT;
      break;
    case STATE_MENUITEM:
      ctx->state = STATE_MENU;
      break;
    case STATE_TOOLITEM:
      ctx->state = STATE_TOOLBAR;
      break;
    case STATE_END:
      g_warning("shouldn't get any end tags at this point");
      /* should do an error here */
      break;
    }
}

static void
cleanup (GMarkupParseContext *context,
     GError *error,
     gpointer user_data)
{
  ParseContext *ctx = user_data;
  EggMenuMerge *self = ctx->self;

  ctx->current = NULL;
  /* should also walk through the tree and get rid of nodes related to
   * this UI file's tag */

  egg_menu_merge_remove_ui (self, ctx->merge_id);
}

static GMarkupParser ui_parser = {
  start_element_handler,
  end_element_handler,
  NULL,
  NULL,
  cleanup
};

guint
egg_menu_merge_add_ui_from_string (EggMenuMerge *self,
                   const gchar *buffer, guint length,
                   GError **error)
{
  ParseContext ctx = { 0 };
  GMarkupParseContext *context;
  gboolean res = TRUE;

  g_return_val_if_fail(EGG_IS_MENU_MERGE(self), FALSE);
  g_return_val_if_fail(buffer != NULL, FALSE);

  ctx.state = STATE_START;
  ctx.self = self;
  ctx.current = NULL;
  ctx.merge_id = egg_menu_merge_next_merge_id (self);

  context = g_markup_parse_context_new(&ui_parser, 0, &ctx, NULL);
  if (length < 0)
    length = strlen(buffer);

  if (g_markup_parse_context_parse(context, buffer, length, error))
    {
      if (!g_markup_parse_context_end_parse(context, error))
    res = FALSE;
    }
  else
    res = FALSE;

  g_markup_parse_context_free (context);

  egg_menu_merge_queue_update(self);

  if (res)
    return ctx.merge_id;
  return 0;
}

guint
egg_menu_merge_add_ui_from_file (EggMenuMerge *self,
                 const gchar *filename,
                 GError **error)
{
  gchar *buffer;
  gint length;
  guint res;

  if (!g_file_get_contents (filename, &buffer, &length, error))
    return 0;

  res = egg_menu_merge_add_ui_from_string(self, buffer, length, error);
  g_free(buffer);

  return res;
}

static gboolean
remove_ui (GNode *node, gpointer user_data)
{
  guint merge_id = GPOINTER_TO_UINT (user_data);

  egg_menu_merge_node_remove_ui_reference (NODE_INFO (node), merge_id);

  return FALSE; /* continue */
}

void
egg_menu_merge_remove_ui (EggMenuMerge *self, guint merge_id)
{
  g_node_traverse(self->root_node, G_POST_ORDER, G_TRAVERSE_ALL, -1,
          remove_ui, GUINT_TO_POINTER (merge_id));

  egg_menu_merge_queue_update(self);
}

/* -------------------- Updates -------------------- */


static EggAction *
get_action_by_name (EggMenuMerge *merge, const char *action_name)
{
  GList *tmp;

  if (!action_name)
    return NULL;
  
  /* lookup name */
  for (tmp = merge->action_groups; tmp != NULL; tmp = tmp->next)
    {
      EggActionGroup *action_group = tmp->data;
      EggAction *action;
      
      action = egg_action_group_get_action (action_group, action_name);

      if (action)
    return action;
    }

  return NULL;
}

static gboolean
find_menu_position (GNode *node, GtkWidget **menushell_p, gint *pos_p)
{
  GtkWidget *menushell;
  gint pos;

  g_return_val_if_fail(node != NULL, FALSE);
  g_return_val_if_fail(NODE_INFO(node)->type == EGG_MENU_MERGE_MENU ||
               NODE_INFO(node)->type == EGG_MENU_MERGE_MENU_PLACEHOLDER ||
               NODE_INFO(node)->type == EGG_MENU_MERGE_MENUITEM ||
               NODE_INFO(node)->type == EGG_MENU_MERGE_SEPARATOR,
               FALSE);

  /* first sibling -- look at parent */
  if (node->prev == NULL)
    {
      GNode *parent;

      parent = node->parent;
      switch (NODE_INFO(parent)->type)
    {
    case EGG_MENU_MERGE_MENUBAR:
      menushell = NODE_INFO(parent)->proxy;
      pos = 0;
      break;
    case EGG_MENU_MERGE_MENU:
      menushell = NODE_INFO(parent)->proxy;
      if (GTK_IS_MENU_ITEM(menushell))
        menushell = gtk_menu_item_get_submenu(GTK_MENU_ITEM(menushell));
      pos = 0;
      break;
    case EGG_MENU_MERGE_MENU_PLACEHOLDER:
      menushell = gtk_widget_get_parent(NODE_INFO(parent)->proxy);
      g_return_val_if_fail(GTK_IS_MENU_SHELL(menushell), FALSE);
      pos = g_list_index(GTK_MENU_SHELL(menushell)->children,
                 NODE_INFO(parent)->proxy) + 1;
      break;
    default:
      g_warning("%s: bad parent node type %d", G_STRLOC,
            NODE_INFO(parent)->type);
      return FALSE;
    }
    }
  else
    {
      GtkWidget *prev_child;
      GNode *sibling;

      sibling = node->prev;
      if (NODE_INFO(sibling)->type == EGG_MENU_MERGE_MENU_PLACEHOLDER)
    prev_child = NODE_INFO(sibling)->extra; /* second Separator */
      else
    prev_child = NODE_INFO(sibling)->proxy;

      g_return_val_if_fail(GTK_IS_WIDGET(prev_child), FALSE);
      menushell = gtk_widget_get_parent(prev_child);
      g_return_val_if_fail(GTK_IS_MENU_SHELL(menushell), FALSE);

      pos = g_list_index(GTK_MENU_SHELL(menushell)->children, prev_child) + 1;
    }

  if (menushell_p)
    *menushell_p = menushell;
  if (pos_p)
    *pos_p = pos;

  return TRUE;
}

static gboolean
find_toolbar_position (GNode *node, GtkWidget **toolbar_p, gint *pos_p)
{
  GtkWidget *toolbar;
  gint pos;

  g_return_val_if_fail(node != NULL, FALSE);
  g_return_val_if_fail(NODE_INFO(node)->type == EGG_MENU_MERGE_TOOLBAR ||
               NODE_INFO(node)->type == EGG_MENU_MERGE_TOOLBAR_PLACEHOLDER ||
               NODE_INFO(node)->type == EGG_MENU_MERGE_TOOLITEM ||
               NODE_INFO(node)->type == EGG_MENU_MERGE_SEPARATOR,
               FALSE);

  /* first sibling -- look at parent */
  if (node->prev == NULL)
    {
      GNode *parent;

      parent = node->parent;
      switch (NODE_INFO(parent)->type)
    {
    case EGG_MENU_MERGE_TOOLBAR:
      toolbar = NODE_INFO(parent)->proxy;
      pos = 0;
      break;
    case EGG_MENU_MERGE_TOOLBAR_PLACEHOLDER:
      toolbar = gtk_widget_get_parent(NODE_INFO(parent)->proxy);
      g_return_val_if_fail(EGG_IS_TOOLBAR(toolbar), FALSE);
      pos = egg_toolbar_get_item_index (EGG_TOOLBAR(toolbar),
                        EGG_TOOL_ITEM (NODE_INFO(parent)->proxy)) + 1;
      break;
    default:
      g_warning("%s: bad parent node type %d", G_STRLOC,
            NODE_INFO(parent)->type);
      return FALSE;
    }
    }
  else
    {
      GtkWidget *prev_child;
      GNode *sibling;

      sibling = node->prev;
      if (NODE_INFO(sibling)->type == EGG_MENU_MERGE_TOOLBAR_PLACEHOLDER)
    prev_child = NODE_INFO(sibling)->extra; /* second Separator */
      else
    prev_child = NODE_INFO(sibling)->proxy;

      g_return_val_if_fail(GTK_IS_WIDGET(prev_child), FALSE);
      toolbar = gtk_widget_get_parent(prev_child);
      g_return_val_if_fail(EGG_IS_TOOLBAR(toolbar), FALSE);

      pos = egg_toolbar_get_item_index (EGG_TOOLBAR(toolbar),
                    EGG_TOOL_ITEM (prev_child)) + 1;
    }

  if (toolbar_p)
    *toolbar_p = toolbar;
  if (pos_p)
    *pos_p = pos;

  return TRUE;
}

static void
update_node (EggMenuMerge *self, GNode *node)
{
  EggMenuMergeNode *info;
  GNode *child;
  EggAction *action;
  GList *tmp;

  g_return_if_fail (node != NULL);
  g_return_if_fail (NODE_INFO(node) != NULL);

  info = NODE_INFO(node);

  for (tmp = info->uifiles; tmp != NULL; tmp = tmp->next)
    {
      NodeUIReference *ref = tmp->data;
    }

  if (NODE_INFO(node)->dirty)
    {
      const gchar *action_name;
      NodeUIReference *ref;

      if (info->uifiles == NULL) {
    /* We may need to remove this node.
     * This must be done in post order
     */
    goto recurse_children;
      }

      ref = info->uifiles->data;
      action_name = g_quark_to_string (ref->action_quark);
      action = get_action_by_name (self, action_name);

      NODE_INFO(node)->dirty = FALSE;

      /* Check if the node doesn't have an action and must have an action */
      if (action == NULL &&
      info->type != EGG_MENU_MERGE_MENUBAR &&
      info->type != EGG_MENU_MERGE_TOOLBAR &&
      info->type != EGG_MENU_MERGE_SEPARATOR &&
      info->type != EGG_MENU_MERGE_MENU_PLACEHOLDER &&
      info->type != EGG_MENU_MERGE_TOOLBAR_PLACEHOLDER)
    {
      /* FIXME: Should we warn here? */
      goto recurse_children;
    }

      /* If the widget already has a proxy and the action hasn't changed, then
       * we don't have to do anything.
       */
      if (info->proxy != NULL &&
      action == info->action)
    {
      goto recurse_children;
    }
      
      info->action = action;

      switch (info->type)
    {
    case EGG_MENU_MERGE_MENUBAR:
      if (info->proxy == NULL)
        {
          info->proxy = gtk_menu_bar_new ();
          gtk_widget_show (info->proxy);
          g_signal_emit (self, merge_signals[ADD_WIDGET], 0, info->proxy);
        }
      break;
    case EGG_MENU_MERGE_MENU:
      {
        GtkWidget *prev_submenu = NULL;
        /* remove the proxy if it is of the wrong type ... */
        if (info->proxy &&  G_OBJECT_TYPE(info->proxy) !=
        EGG_ACTION_GET_CLASS(info->action)->menu_item_type)
          {
        prev_submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(info->proxy));
        if (prev_submenu)
          {
            g_object_ref (prev_submenu);
            gtk_menu_item_set_submenu(GTK_MENU_ITEM(info->proxy),NULL);
          }
        gtk_container_remove(GTK_CONTAINER(info->proxy->parent),
                     info->proxy);
        info->proxy = NULL;
        }
        /* create proxy if needed ... */
        if (info->proxy == NULL)
          {
        GtkWidget *menushell;
        gint pos;
        GNode *parent;

        parent = node->parent;

        if (parent && NODE_INFO (parent)->type == EGG_MENU_MERGE_POPUPS)
          {
            info->proxy = gtk_menu_new();
          }
        else if (find_menu_position(node, &menushell, &pos))
          {
            GtkWidget *menu;
            info->proxy = egg_action_create_menu_item (info->action);
            menu = gtk_menu_new();
            gtk_menu_item_set_submenu(GTK_MENU_ITEM(info->proxy), menu);
            gtk_menu_set_accel_group (GTK_MENU (menu), self->accel_group);
            gtk_menu_shell_insert (GTK_MENU_SHELL (menushell),
                       info->proxy, pos);
          }
          }
        else
          {
        egg_action_connect_proxy (info->action, info->proxy);
          }
        if (prev_submenu)
          {
        gtk_menu_item_set_submenu (GTK_MENU_ITEM (info->proxy),
                       prev_submenu);
        g_object_unref (prev_submenu);
          }
      }
      break;
    case EGG_MENU_MERGE_UNDECIDED:
      g_warning("found 'undecided node!");
      break;
    case EGG_MENU_MERGE_ROOT:
      break;
    case EGG_MENU_MERGE_TOOLBAR:
      if (info->proxy == NULL)
        {
          info->proxy = egg_toolbar_new ();
          gtk_widget_show (info->proxy);
          g_signal_emit (self, merge_signals[ADD_WIDGET], 0, info->proxy);
        }
      break;
    case EGG_MENU_MERGE_MENU_PLACEHOLDER:
      /* create menu items for placeholders if necessary ... */
      if (!GTK_IS_SEPARATOR_MENU_ITEM (info->proxy) ||
          !GTK_IS_SEPARATOR_MENU_ITEM (info->extra))
        {
          if (info->proxy)
        gtk_container_remove(GTK_CONTAINER(info->proxy->parent),
                     info->proxy);
          if (info->extra)
        gtk_container_remove(GTK_CONTAINER(info->extra->parent),
                     info->extra);
          info->proxy = NULL;
          info->extra = NULL;
        }
      if (info->proxy == NULL)
        {
          GtkWidget *menushell;
          gint pos;

          if (find_menu_position(node, &menushell, &pos))
        {
          NODE_INFO(node)->proxy = gtk_separator_menu_item_new();
          gtk_menu_shell_insert(GTK_MENU_SHELL(menushell),
                    NODE_INFO(node)->proxy, pos);
          //gtk_widget_show(NODE_INFO(node)->proxy);

          NODE_INFO(node)->extra = gtk_separator_menu_item_new();
          gtk_menu_shell_insert(GTK_MENU_SHELL(menushell),
                    NODE_INFO(node)->extra, pos+1);
          //gtk_widget_show(NODE_INFO(node)->extra);
        }
        }
      break;
    case EGG_MENU_MERGE_TOOLBAR_PLACEHOLDER:
      /* create toolbar items for placeholders if necessary ... */
      if (!EGG_IS_SEPARATOR_TOOL_ITEM (info->proxy) ||
          !EGG_IS_SEPARATOR_TOOL_ITEM (info->extra))
        {
          if (info->proxy)
        gtk_container_remove(GTK_CONTAINER(info->proxy->parent),
                     info->proxy);
          if (info->extra)
        gtk_container_remove(GTK_CONTAINER(info->extra->parent),
                     info->extra);
          info->proxy = NULL;
          info->extra = NULL;
        }
      if (info->proxy == NULL)
        {
          GtkWidget *toolbar;
          gint pos;

          if (find_toolbar_position(node, &toolbar, &pos))
        {
          EggToolItem *item;

          item = egg_separator_tool_item_new();
          egg_toolbar_insert(EGG_TOOLBAR(toolbar), item, pos);
          NODE_INFO(node)->proxy = GTK_WIDGET (item);
          //gtk_widget_show(NODE_INFO(node)->proxy);

          item = egg_separator_tool_item_new();
          egg_toolbar_insert(EGG_TOOLBAR(toolbar), item, pos+1);
          NODE_INFO(node)->extra = GTK_WIDGET (item);
          //gtk_widget_show(NODE_INFO(node)->extra);
        }
        }
      break;
    case EGG_MENU_MERGE_POPUPS:
      break;
    case EGG_MENU_MERGE_MENUITEM:
      /* remove the proxy if it is of the wrong type ... */
      if (info->proxy &&  G_OBJECT_TYPE(info->proxy) !=
          EGG_ACTION_GET_CLASS(info->action)->menu_item_type)
        {
          gtk_container_remove(GTK_CONTAINER(info->proxy->parent),
                   info->proxy);
          info->proxy = NULL;
        }
      /* create proxy if needed ... */
      if (info->proxy == NULL)
        {
          GtkWidget *menushell;
          gint pos;

          if (find_menu_position(node, &menushell, &pos))
        {
          info->proxy = egg_action_create_menu_item (info->action);

          gtk_menu_shell_insert (GTK_MENU_SHELL (menushell),
                     info->proxy, pos);
        }
        }
      else
        {
          gtk_menu_item_set_submenu(GTK_MENU_ITEM(info->proxy), NULL);
          egg_action_connect_proxy (info->action, info->proxy);
        }
      break;
    case EGG_MENU_MERGE_TOOLITEM:
      /* remove the proxy if it is of the wrong type ... */
      if (info->proxy &&  G_OBJECT_TYPE(info->proxy) !=
          EGG_ACTION_GET_CLASS(info->action)->toolbar_item_type)
        {
          gtk_container_remove(GTK_CONTAINER(info->proxy->parent),
                   info->proxy);
          info->proxy = NULL;
        }
      /* create proxy if needed ... */
      if (info->proxy == NULL)
        {
          GtkWidget *toolbar;
          gint pos;

          if (find_toolbar_position(node, &toolbar, &pos))
        {
          info->proxy = egg_action_create_tool_item (info->action);

          egg_toolbar_insert (EGG_TOOLBAR (toolbar),
                            EGG_TOOL_ITEM (info->proxy), pos);
        }
        }
      else
        {
          egg_action_connect_proxy (info->action, info->proxy);
        }
      break;
    case EGG_MENU_MERGE_SEPARATOR:
      if (NODE_INFO (node->parent)->type == EGG_MENU_MERGE_TOOLBAR ||
          NODE_INFO (node->parent)->type == EGG_MENU_MERGE_TOOLBAR_PLACEHOLDER)
        {
          GtkWidget *toolbar;
          gint pos;

          if (EGG_IS_SEPARATOR_TOOL_ITEM(info->proxy))
        {
          gtk_container_remove(GTK_CONTAINER(info->proxy->parent),
                       info->proxy);
          info->proxy = NULL;
        }

          if (find_toolbar_position(node, &toolbar, &pos))
        {
          EggToolItem *item = egg_separator_tool_item_new();
          egg_toolbar_insert (EGG_TOOLBAR (toolbar), item, pos);
          info->proxy = GTK_WIDGET (item);
          gtk_widget_show(info->proxy);
        }
        }
      else
        {
          GtkWidget *menushell;
          gint pos;

          if (GTK_IS_SEPARATOR_MENU_ITEM(info->proxy))
        {
          gtk_container_remove(GTK_CONTAINER(info->proxy->parent),
                       info->proxy);
          info->proxy = NULL;
        }

          if (find_menu_position(node, &menushell, &pos))
        {
          info->proxy = gtk_separator_menu_item_new();
          gtk_menu_shell_insert (GTK_MENU_SHELL (menushell),
                     info->proxy, pos);
          gtk_widget_show(info->proxy);
        }
        }
      break;
    }

      /* if this node has a widget, but it is the wrong type, remove it */
    }

 recurse_children:
  /* process children */
  child = node->children;
  while (child)
    {
      GNode *current;

      current = child;
      child = current->next;
      update_node (self, current);
    }

  /* handle cleanup of dead nodes */
  if (node->children == NULL && NODE_INFO(node)->uifiles == NULL)
    {
      if (NODE_INFO(node)->proxy)
    gtk_widget_destroy(NODE_INFO(node)->proxy);
      if ((NODE_INFO(node)->type == EGG_MENU_MERGE_MENU_PLACEHOLDER ||
       NODE_INFO(node)->type == EGG_MENU_MERGE_TOOLBAR_PLACEHOLDER) &&
      NODE_INFO(node)->extra)
    gtk_widget_destroy(NODE_INFO(node)->extra);
      g_chunk_free(NODE_INFO(node), merge_node_chunk);
      g_node_destroy(node);
    }
}

static gboolean
do_updates(EggMenuMerge *self)
{
  /* this function needs to check through the tree for dirty nodes.
   * For such nodes, it needs to do the following:
   *
   * 1) check if they are referenced by any loaded UI files anymore.
   *    In which case, the proxy widget should be destroyed, unless
   *    there are any subnodes.
   *
   * 2) lookup the action for this node again.  If it is different to
   *    the current one (or if no previous action has been looked up),
   *    the proxy is reconnected to the new action (or a new proxy widget
   *    is created and added to the parent container).
   */

  update_node (self, self->root_node);

  self->update_tag = 0;
  return FALSE;
}

static void
egg_menu_merge_queue_update (EggMenuMerge *self)
{
  if (self->update_tag != 0)
    return;

  self->update_tag = g_idle_add((GSourceFunc)do_updates, self);
}

void
egg_menu_merge_ensure_update (EggMenuMerge *self)
{
  if (self->update_tag != 0)
    {
      g_source_remove(self->update_tag);
      do_updates(self);
    }
}

static gboolean
dirty_traverse_func (GNode *node, gpointer data)
{
  NODE_INFO(node)->dirty = TRUE;
  return FALSE;
}

static void
egg_menu_merge_dirty_all (EggMenuMerge *self)
{
  g_node_traverse(self->root_node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
          dirty_traverse_func, NULL);
  egg_menu_merge_queue_update(self);
}