/* GTK+ application-level integration for the Mac OS X/Cocoa
 *
 * Copyright (C) 2007 Pioneer Research Center USA, Inc.
 * Copyright (C) 2007 Imendio AB
 * Copyright (C) 2009 Paul Davis
 *
 * This is a reimplementation in Cocoa of the sync-menu.c concept
 * from Imendio, although without the "set quit menu" API since
 * a Cocoa app needs to handle termination anyway.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; version 2.1
 * of the License.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#include <sigc++/signal.h>
#include <sigc++/slot.h>

#include <string.h>
#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>
#include <gtkmm2ext/gtkapplication.h>
#include <gtkmm2ext/gtkapplication-private.h>

#import <AppKit/NSMenu.h>
#import <AppKit/NSMenuItem.h>
#import <AppKit/NSCell.h>
#import <AppKit/NSEvent.h>
#import <AppKit/NSApplication.h>
#import <Foundation/NSString.h>
#import <Foundation/NSNotification.h>

#include <vector>

#define UNUSED_PARAMETER(a) (void) (a)

//#define DEBUG(format, ...) g_printerr ("%s: " format, G_STRFUNC, ## __VA_ARGS__)
#define DEBUG(format, ...)

/* TODO
 *
 * - Sync adding/removing/reordering items
 * - Create on demand? (can this be done with gtk+? ie fill in menu
     items when the menu is opened)
 * - Figure out what to do per app/window...
 *
 */

static gint _exiting = 0;
static std::vector<GtkMenuItem*> global_menu_items;
static gint _modal_state = 0;

static guint
gdk_quartz_keyval_to_ns_keyval (guint keyval)
{
	switch (keyval) {
	case GDK_BackSpace:
		return NSBackspaceCharacter;
	case GDK_Delete:
		return NSDeleteFunctionKey;
	case GDK_Pause:
		return NSPauseFunctionKey;
	case GDK_Scroll_Lock:
		return NSScrollLockFunctionKey;
	case GDK_Sys_Req:
		return NSSysReqFunctionKey;
	case GDK_Home:
		return NSHomeFunctionKey;
	case GDK_Left:
	case GDK_leftarrow:
		return NSLeftArrowFunctionKey;
	case GDK_Up:
	case GDK_uparrow:
		return NSUpArrowFunctionKey;
	case GDK_Right:
	case GDK_rightarrow:
		return NSRightArrowFunctionKey;
	case GDK_Down:
	case GDK_downarrow:
		return NSDownArrowFunctionKey;
	case GDK_Page_Up:
		return NSPageUpFunctionKey;
	case GDK_Page_Down:
		return NSPageDownFunctionKey;
	case GDK_End:
		return NSEndFunctionKey;
	case GDK_Begin:
		return NSBeginFunctionKey;
	case GDK_Select:
		return NSSelectFunctionKey;
	case GDK_Print:
		return NSPrintFunctionKey;
	case GDK_Execute:
		return NSExecuteFunctionKey;
	case GDK_Insert:
		return NSInsertFunctionKey;
	case GDK_Undo:
		return NSUndoFunctionKey;
	case GDK_Redo:
		return NSRedoFunctionKey;
	case GDK_Menu:
		return NSMenuFunctionKey;
	case GDK_Find:
		return NSFindFunctionKey;
	case GDK_Help:
		return NSHelpFunctionKey;
	case GDK_Break:
		return NSBreakFunctionKey;
	case GDK_Mode_switch:
		return NSModeSwitchFunctionKey;
	case GDK_F1:
		return NSF1FunctionKey;
	case GDK_F2:
		return NSF2FunctionKey;
	case GDK_F3:
		return NSF3FunctionKey;
	case GDK_F4:
		return NSF4FunctionKey;
	case GDK_F5:
		return NSF5FunctionKey;
	case GDK_F6:
		return NSF6FunctionKey;
	case GDK_F7:
		return NSF7FunctionKey;
	case GDK_F8:
		return NSF8FunctionKey;
	case GDK_F9:
		return NSF9FunctionKey;
	case GDK_F10:
		return NSF10FunctionKey;
	case GDK_F11:
		return NSF11FunctionKey;
	case GDK_F12:
		return NSF12FunctionKey;
	case GDK_F13:
		return NSF13FunctionKey;
	case GDK_F14:
		return NSF14FunctionKey;
	case GDK_F15:
		return NSF15FunctionKey;
	case GDK_F16:
		return NSF16FunctionKey;
	case GDK_F17:
		return NSF17FunctionKey;
	case GDK_F18:
		return NSF18FunctionKey;
	case GDK_F19:
		return NSF19FunctionKey;
	case GDK_F20:
		return NSF20FunctionKey;
	case GDK_F21:
		return NSF21FunctionKey;
	case GDK_F22:
		return NSF22FunctionKey;
	case GDK_F23:
		return NSF23FunctionKey;
	case GDK_F24:
		return NSF24FunctionKey;
	case GDK_F25:
		return NSF25FunctionKey;
	case GDK_F26:
		return NSF26FunctionKey;
	case GDK_F27:
		return NSF27FunctionKey;
	case GDK_F28:
		return NSF28FunctionKey;
	case GDK_F29:
		return NSF29FunctionKey;
	case GDK_F30:
		return NSF30FunctionKey;
	case GDK_F31:
		return NSF31FunctionKey;
	case GDK_F32:
		return NSF32FunctionKey;
	case GDK_F33:
		return NSF33FunctionKey;
	case GDK_F34:
		return NSF34FunctionKey;
	case GDK_F35:
		return NSF35FunctionKey;
	default:
		break;
	}

	return 0;
}

static gboolean
keyval_is_keypad (guint keyval)
{
	switch (keyval) {
	case GDK_KP_F1:
	case GDK_KP_F2:
	case GDK_KP_F3:
	case GDK_KP_F4:
	case GDK_KP_Home:
	case GDK_KP_Left:
	case GDK_KP_Up:
	case GDK_KP_Right:
	case GDK_KP_Down:
	case GDK_KP_Page_Up:
	case GDK_KP_Page_Down:
	case GDK_KP_End:
	case GDK_KP_Begin:
	case GDK_KP_Insert:
	case GDK_KP_Delete:
	case GDK_KP_Equal:
	case GDK_KP_Multiply:
	case GDK_KP_Add:
	case GDK_KP_Separator:
	case GDK_KP_Subtract:
	case GDK_KP_Decimal:
	case GDK_KP_Divide:
	case GDK_KP_0:
	case GDK_KP_1:
	case GDK_KP_2:
	case GDK_KP_3:
	case GDK_KP_4:
	case GDK_KP_5:
	case GDK_KP_6:
	case GDK_KP_7:
	case GDK_KP_8:
	case GDK_KP_9:
		return TRUE;
		break;
	default:
		break;
	}
	return FALSE;
}

static guint
keyval_keypad_nonkeypad_equivalent (guint keyval)
{
	switch (keyval) {
	case GDK_KP_F1:
		return GDK_F1;
	case GDK_KP_F2:
		return GDK_F2;
	case GDK_KP_F3:
		return GDK_F3;
	case GDK_KP_F4:
		return GDK_F4;
	case GDK_KP_Home:
		return GDK_Home;
	case GDK_KP_Left:
		return GDK_Left;
	case GDK_KP_Up:
		return GDK_Up;
	case GDK_KP_Right:
		return GDK_Right;
	case GDK_KP_Down:
		return GDK_Down;
	case GDK_KP_Page_Up:
		return GDK_Page_Up;
	case GDK_KP_Page_Down:
		return GDK_Page_Down;
	case GDK_KP_End:
		return GDK_End;
	case GDK_KP_Begin:
		return GDK_Begin;
	case GDK_KP_Insert:
		return GDK_Insert;
	case GDK_KP_Delete:
		return GDK_Delete;
	case GDK_KP_Equal:
		return GDK_equal;
	case GDK_KP_Multiply:
		return GDK_asterisk;
	case GDK_KP_Add:
		return GDK_plus;
	case GDK_KP_Subtract:
		return GDK_minus;
	case GDK_KP_Decimal:
		return GDK_period;
	case GDK_KP_Divide:
		return GDK_slash;
	case GDK_KP_0:
		return GDK_0;
	case GDK_KP_1:
		return GDK_1;
	case GDK_KP_2:
		return GDK_2;
	case GDK_KP_3:
		return GDK_3;
	case GDK_KP_4:
		return GDK_4;
	case GDK_KP_5:
		return GDK_5;
	case GDK_KP_6:
		return GDK_6;
	case GDK_KP_7:
		return GDK_7;
	case GDK_KP_8:
		return GDK_8;
	case GDK_KP_9:
		return GDK_9;
	default:
		break;
	}

	return GDK_VoidSymbol;
}

static const gchar*
gdk_quartz_keyval_to_string (guint keyval)
{
	switch (keyval) {
	case GDK_space:
		return " ";
	case GDK_exclam:
		return "!";
	case GDK_quotedbl:
		return "\"";
	case GDK_numbersign:
		return "#";
	case GDK_dollar:
		return "$";
	case GDK_percent:
		return "%";
	case GDK_ampersand:
		return "&";
	case GDK_apostrophe:
		return "'";
	case GDK_parenleft:
		return "(";
	case GDK_parenright:
		return ")";
	case GDK_asterisk:
		return "*";
	case GDK_plus:
		return "+";
	case GDK_comma:
		return ",";
	case GDK_minus:
		return "-";
	case GDK_period:
		return ".";
	case GDK_slash:
		return "/";
	case GDK_0:
		return "0";
	case GDK_1:
		return "1";
	case GDK_2:
		return "2";
	case GDK_3:
		return "3";
	case GDK_4:
		return "4";
	case GDK_5:
		return "5";
	case GDK_6:
		return "6";
	case GDK_7:
		return "7";
	case GDK_8:
		return "8";
	case GDK_9:
		return "9";
	case GDK_colon:
		return ":";
	case GDK_semicolon:
		return ";";
	case GDK_less:
		return "<";
	case GDK_equal:
		return "=";
	case GDK_greater:
		return ">";
	case GDK_question:
		return "?";
	case GDK_at:
		return "@";
	case GDK_A:
	case GDK_a:
		return "a";
	case GDK_B:
	case GDK_b:
		return "b";
	case GDK_C:
	case GDK_c:
		return "c";
	case GDK_D:
	case GDK_d:
		return "d";
	case GDK_E:
	case GDK_e:
		return "e";
	case GDK_F:
	case GDK_f:
		return "f";
	case GDK_G:
	case GDK_g:
		return "g";
	case GDK_H:
	case GDK_h:
		return "h";
	case GDK_I:
	case GDK_i:
		return "i";
	case GDK_J:
	case GDK_j:
		return "j";
	case GDK_K:
	case GDK_k:
		return "k";
	case GDK_L:
	case GDK_l:
		return "l";
	case GDK_M:
	case GDK_m:
		return "m";
	case GDK_N:
	case GDK_n:
		return "n";
	case GDK_O:
	case GDK_o:
		return "o";
	case GDK_P:
	case GDK_p:
		return "p";
	case GDK_Q:
	case GDK_q:
		return "q";
	case GDK_R:
	case GDK_r:
		return "r";
	case GDK_S:
	case GDK_s:
		return "s";
	case GDK_T:
	case GDK_t:
		return "t";
	case GDK_U:
	case GDK_u:
		return "u";
	case GDK_V:
	case GDK_v:
		return "v";
	case GDK_W:
	case GDK_w:
		return "w";
	case GDK_X:
	case GDK_x:
		return "x";
	case GDK_Y:
	case GDK_y:
		return "y";
	case GDK_Z:
	case GDK_z:
		return "z";
	case GDK_bracketleft:
		return "[";
	case GDK_backslash:
		return "\\";
	case GDK_bracketright:
		return "]";
	case GDK_asciicircum:
		return "^";
	case GDK_underscore:
		return "_";
	case GDK_grave:
		return "`";
	case GDK_braceleft:
		return "{";
	case GDK_bar:
		return "|";
	case GDK_braceright:
		return "}";
	case GDK_asciitilde:
		return "~";
	default:
		break;
	}
	return NULL;
};

static gboolean
keyval_is_uppercase (guint keyval)
{
	switch (keyval) {
	case GDK_A:
	case GDK_B:
	case GDK_C:
	case GDK_D:
	case GDK_E:
	case GDK_F:
	case GDK_G:
	case GDK_H:
	case GDK_I:
	case GDK_J:
	case GDK_K:
	case GDK_L:
	case GDK_M:
	case GDK_N:
	case GDK_O:
	case GDK_P:
	case GDK_Q:
	case GDK_R:
	case GDK_S:
	case GDK_T:
	case GDK_U:
	case GDK_V:
	case GDK_W:
	case GDK_X:
	case GDK_Y:
	case GDK_Z:
		return TRUE;
	default:
		return FALSE;
	}
	return FALSE;
}

/* gtk/osx has a problem in that mac main menu events
   are handled using an "internal" event handling system that
   doesn't pass things back to the glib/gtk main loop. if we call
   gtk_main_iteration() block while in a menu event handler, then
   glib gets confused and thinks there are two threads running
   g_main_poll_func(). apps call call gdk_quartz_in_menu_event_handler()
   if they need to check this.
 */

static int _in_menu_event_handler = 0;

int
gdk_quartz_in_menu_event_handler ()
{
	return _in_menu_event_handler;
}

static gboolean
idle_call_activate (gpointer data)
{
	gtk_menu_item_activate ((GtkMenuItem*) data);
	return FALSE;
}

@interface GNSMenuItem : NSMenuItem <NSMenuItemValidation>
{
    @public
	GtkMenuItem* gtk_menu_item;
	GClosure*    accel_closure;
	bool         premodal;
}
- (id) initWithTitle:(NSString*) title andGtkWidget:(GtkMenuItem*) w;
- (void) activate:(id) sender;
- (BOOL) validateMenuItem:(NSMenuItem*) menuItem;
@end

@implementation GNSMenuItem
- (id) initWithTitle:(NSString*) title andGtkWidget:(GtkMenuItem*) w
{
	/* All menu items have the action "activate", which will be handled by this child class
	 */

	self = [ super initWithTitle:title action:@selector(activate:) keyEquivalent:@"" ];

	if (self) {
		/* make this handle its own action */
		[ self setTarget:self ];
		gtk_menu_item = w;
		accel_closure = 0;
	}
	return self;
}
- (void) activate:(id) sender
{
	UNUSED_PARAMETER(sender);
    // Hot Fix. Increase Priority.
	g_idle_add_full (G_PRIORITY_HIGH_IDLE, idle_call_activate, gtk_menu_item, NULL);
//    g_idle_add (idle_call_activate, gtk_menu_item);
}
- (BOOL) validateMenuItem:(NSMenuItem*) menuItem
{
	if (_modal_state > 0) {
		return false;
	}

	GtkAction* act = gtk_activatable_get_related_action (GTK_ACTIVATABLE(gtk_menu_item));

	if (act) {
		return gtk_action_get_sensitive (act);
	} else {
		return true;
	}
}
@end

static void push_menu_shell_to_nsmenu (GtkMenuShell *menu_shell,
				       NSMenu       *menu,
				       gboolean      toplevel,
				       gboolean      debug);

/*
 * utility functions
 */

static GtkWidget *
find_menu_label (GtkWidget *widget)
{
  GtkWidget *label = NULL;

  if (GTK_IS_LABEL (widget))
    return widget;

  if (GTK_IS_CONTAINER (widget))
    {
      GList *children;
      GList *l;

      children = gtk_container_get_children (GTK_CONTAINER (widget));

      for (l = children; l; l = l->next)
	{
	  label = find_menu_label ((GtkWidget*) l->data);
	  if (label)
	    break;
	}

      g_list_free (children);
    }

  return label;
}

static const gchar *
get_menu_label_text (GtkWidget  *menu_item,
		     GtkWidget **label)
{
  GtkWidget *my_label;

  my_label = find_menu_label (menu_item);
  if (label)
    *label = my_label;

  if (my_label)
    return gtk_label_get_text (GTK_LABEL (my_label));

  return NULL;
}

static gboolean
accel_find_func (GtkAccelKey * /*key*/,
		 GClosure    *closure,
		 gpointer     data)
{
  return (GClosure *) data == closure;
}


/*
 * CocoaMenu functions
 */

static GQuark cocoa_menu_quark = 0;

static NSMenu *
cocoa_menu_get (GtkWidget *widget)
{
  return (NSMenu*) g_object_get_qdata (G_OBJECT (widget), cocoa_menu_quark);
}

static void
cocoa_menu_free (gpointer *ptr)
{
	NSMenu* menu = (NSMenu*) ptr;
	[menu release];
}

static void
cocoa_menu_connect (GtkWidget *menu,
		    NSMenu*    cocoa_menu)
{
	[cocoa_menu retain];

	if (cocoa_menu_quark == 0)
		cocoa_menu_quark = g_quark_from_static_string ("NSMenu");

	g_object_set_qdata_full (G_OBJECT (menu), cocoa_menu_quark,
				 cocoa_menu,
				 (GDestroyNotify) cocoa_menu_free);
}

/*
 * NSMenuItem functions
 */

static GQuark cocoa_menu_item_quark = 0;
static void cocoa_menu_item_connect (GtkWidget*   menu_item,
				     GNSMenuItem* cocoa_menu_item,
				     GtkWidget     *label);

static void
cocoa_menu_item_free (gpointer *ptr)
{
	GNSMenuItem* item = (GNSMenuItem*) ptr;
	[item release];
}

static GNSMenuItem *
cocoa_menu_item_get (GtkWidget *widget)
{
  return (GNSMenuItem*) g_object_get_qdata (G_OBJECT (widget), cocoa_menu_item_quark);
}

static void
cocoa_menu_item_update_state (NSMenuItem* cocoa_item,
			      GtkWidget      *widget)
{
  gboolean sensitive;
  gboolean visible;

  g_object_get (widget,
                "sensitive", &sensitive,
                "visible",   &visible,
                NULL);

  if (!sensitive) {
	  [cocoa_item setEnabled:NO];
  } else {
	  [cocoa_item setEnabled:YES];
  }

#if 0
  // requires OS X 10.5 or later
  if (!visible)
	  [cocoa_item setHidden:YES];
  else
	  [cocoa_item setHidden:NO];
#endif
}

static void
cocoa_menu_item_update_active (NSMenuItem *cocoa_item,
				GtkWidget  *widget)
{
  gboolean active;

  g_object_get (widget, "active", &active, NULL);

  if (active)
    [cocoa_item setState:NSOnState];
  else
    [cocoa_item setState:NSOffState];
}

static void
cocoa_menu_item_update_submenu (NSMenuItem *cocoa_item,
				GtkWidget      *widget)
{
  GtkWidget *submenu;

  g_return_if_fail (cocoa_item != NULL);
  g_return_if_fail (widget != NULL);

  submenu = gtk_menu_item_get_submenu (GTK_MENU_ITEM (widget));

  if (submenu)
    {
      GtkWidget* label = NULL;
      const gchar *label_text;
      NSMenu* cocoa_submenu;

      label_text = get_menu_label_text (widget, &label);

      /* create a new nsmenu to hold the GTK menu */

      if (label_text)
	      cocoa_submenu = [ [ NSMenu alloc ] initWithTitle:[ [ NSString alloc] initWithCString:label_text encoding:NSUTF8StringEncoding]];
      else
	      cocoa_submenu = [ [ NSMenu alloc ] initWithTitle:@""];

      [cocoa_submenu setAutoenablesItems:YES];
      cocoa_menu_connect (submenu, cocoa_submenu);

      /* connect the new nsmenu to the passed-in item (which lives in
	 the parent nsmenu)
	 (Note: this will release any pre-existing version of this submenu)
      */
      [ cocoa_item setSubmenu:cocoa_submenu];

      /* and push the GTK menu into the submenu */
      push_menu_shell_to_nsmenu (GTK_MENU_SHELL (submenu), cocoa_submenu, FALSE, FALSE);

      [ cocoa_submenu release ];
    }
}

static void
cocoa_menu_item_update_label (NSMenuItem *cocoa_item,
			       GtkWidget      *widget)
{
  const gchar *label_text;

  g_return_if_fail (cocoa_item != NULL);
  g_return_if_fail (widget != NULL);

  label_text = get_menu_label_text (widget, NULL);
  if (label_text)
	  [cocoa_item setTitle:[ [ NSString alloc] initWithCString:label_text encoding:NSUTF8StringEncoding]];
  else
	  [cocoa_item setTitle:@""];
}

static void
cocoa_menu_item_update_accelerator (NSMenuItem *cocoa_item,
				     GtkWidget *widget)
{
  GtkWidget *label;

  g_return_if_fail (cocoa_item != NULL);
  g_return_if_fail (widget != NULL);

  /* important note: this function doesn't do anything to actually change
     key handling. Its goal is to get Cocoa to display the correct
     accelerator as part of a menu item. Actual accelerator handling
     is still done by GTK, so this is more cosmetic than it may
     appear.
  */

  get_menu_label_text (widget, &label);

  if (GTK_IS_ACCEL_LABEL (label) &&
	    GTK_ACCEL_LABEL (label)->accel_closure)
	{
		GtkAccelKey *key;

		key = gtk_accel_group_find (GTK_ACCEL_LABEL (label)->accel_group,
					    accel_find_func,
				  GTK_ACCEL_LABEL (label)->accel_closure);

		if (key            &&
		    key->accel_key &&
		    key->accel_flags & GTK_ACCEL_VISIBLE)
		{
			guint modifiers = 0;
			const gchar* str = NULL;
			guint actual_key = key->accel_key;

			if (keyval_is_keypad (actual_key)) {
				if ((actual_key = keyval_keypad_nonkeypad_equivalent (actual_key)) == GDK_VoidSymbol) {
					/* GDK_KP_Separator */
					[cocoa_item setKeyEquivalent:@""];
					return;
				}
				modifiers |= NSNumericPadKeyMask;
			}

			/* if we somehow got here with GDK_A ... GDK_Z rather than GDK_a ... GDK_z, then take note
			   of that and make sure we use a shift modifier.
			*/

			if (keyval_is_uppercase (actual_key)) {
				modifiers |= NSShiftKeyMask;
			}

			str = gdk_quartz_keyval_to_string (actual_key);

			if (str) {
				unichar ukey = str[0];
				[cocoa_item setKeyEquivalent:[NSString stringWithCharacters:&ukey length:1]];
			} else {
				unichar ukey = gdk_quartz_keyval_to_ns_keyval (actual_key);
				if (ukey != 0) {
					[cocoa_item setKeyEquivalent:[NSString stringWithCharacters:&ukey length:1]];
				} else {
					/* cannot map this key to Cocoa key equivalent */
					[cocoa_item setKeyEquivalent:@""];
					return;
				}
			}

			if (key->accel_mods || modifiers)
			{
				if (key->accel_mods & GDK_SHIFT_MASK) {
					modifiers |= NSShiftKeyMask;
				}

				/* gdk/quartz maps Alt/Option to Mod1 */

				if (key->accel_mods & (GDK_MOD1_MASK)) {
					modifiers |= NSAlternateKeyMask;
				}

				if (key->accel_mods & GDK_CONTROL_MASK) {
					modifiers |= NSControlKeyMask;
				}

				/* our modified gdk/quartz maps Command to Mod2 */

				if (key->accel_mods & GDK_MOD2_MASK) {
					modifiers |= NSCommandKeyMask;
				}
			}

			[cocoa_item setKeyEquivalentModifierMask:modifiers];
			return;
		}
	}

	/*  otherwise, clear the menu shortcut  */
	[cocoa_item setKeyEquivalent:@""];
}

static void
cocoa_menu_item_accel_changed (GtkAccelGroup*   /*accel_group*/,
			       guint            /*keyval*/,
				GdkModifierType /*modifier*/,
				GClosure        *accel_closure,
				GtkWidget       *widget)
{
  GNSMenuItem *cocoa_item;
  GtkWidget      *label;

  if (_exiting)
    return;

  cocoa_item = cocoa_menu_item_get (widget);
  get_menu_label_text (widget, &label);

  if (GTK_IS_ACCEL_LABEL (label) &&
      GTK_ACCEL_LABEL (label)->accel_closure == accel_closure)
    cocoa_menu_item_update_accelerator (cocoa_item, widget);
}

static void
cocoa_menu_item_update_accel_closure (GNSMenuItem *cocoa_item,
				      GtkWidget      *widget)
{
  GtkAccelGroup *group;
  GtkWidget     *label;

  get_menu_label_text (widget, &label);

  if (cocoa_item->accel_closure)
    {
      group = gtk_accel_group_from_accel_closure (cocoa_item->accel_closure);

      g_signal_handlers_disconnect_by_func (group,
					    (void*) cocoa_menu_item_accel_changed,
					    widget);

      g_closure_unref (cocoa_item->accel_closure);
      cocoa_item->accel_closure = NULL;
    }

  if (GTK_IS_ACCEL_LABEL (label)) {
    cocoa_item->accel_closure = GTK_ACCEL_LABEL (label)->accel_closure;
  }

  if (cocoa_item->accel_closure)
    {
      g_closure_ref (cocoa_item->accel_closure);

      group = gtk_accel_group_from_accel_closure (cocoa_item->accel_closure);

      g_signal_connect_object (group, "accel-changed",
			       G_CALLBACK (cocoa_menu_item_accel_changed),
			       widget, (GConnectFlags) 0);
    }

  cocoa_menu_item_update_accelerator (cocoa_item, widget);
}

static void
cocoa_menu_item_notify_label (GObject    *object,
			      GParamSpec *pspec,
			      gpointer)
{
  GNSMenuItem *cocoa_item;

  if (_exiting)
    return;

  cocoa_item = cocoa_menu_item_get (GTK_WIDGET (object));

  if (!strcmp (pspec->name, "label"))
    {
      cocoa_menu_item_update_label (cocoa_item,
				     GTK_WIDGET (object));
    }
  else if (!strcmp (pspec->name, "accel-closure"))
    {
      cocoa_menu_item_update_accel_closure (cocoa_item,
					     GTK_WIDGET (object));
    }
}

static void
cocoa_menu_item_notify (GObject        *object,
			GParamSpec     *pspec,
			NSMenuItem *cocoa_item)
{
  if (_exiting)
    return;

  if (!strcmp (pspec->name, "sensitive") ||
      !strcmp (pspec->name, "visible"))
    {
      cocoa_menu_item_update_state (cocoa_item, GTK_WIDGET (object));
    }
  else if (!strcmp (pspec->name, "active"))
    {
      cocoa_menu_item_update_active (cocoa_item, GTK_WIDGET (object));
    }
  else if (!strcmp (pspec->name, "submenu"))
    {
      cocoa_menu_item_update_submenu (cocoa_item, GTK_WIDGET (object));
    }
}

static void
cocoa_menu_item_connect (GtkWidget*   menu_item,
			 GNSMenuItem* cocoa_item,
			 GtkWidget     *label)
{
	GNSMenuItem* old_item = cocoa_menu_item_get (menu_item);

	[cocoa_item retain];

	if (cocoa_menu_item_quark == 0)
		cocoa_menu_item_quark = g_quark_from_static_string ("NSMenuItem");

	g_object_set_qdata_full (G_OBJECT (menu_item), cocoa_menu_item_quark,
				 cocoa_item,
				 (GDestroyNotify) cocoa_menu_item_free);

	if (!old_item) {

		g_signal_connect (menu_item, "notify",
				  G_CALLBACK (cocoa_menu_item_notify),
				  cocoa_item);

		if (label)
			g_signal_connect_swapped (label, "notify::label",
						  G_CALLBACK (cocoa_menu_item_notify_label),
						  menu_item);
	}
}

static void
add_menu_item (NSMenu* cocoa_menu, GtkWidget* menu_item, int index)
{
	GtkWidget* label      = NULL;
	GNSMenuItem *cocoa_item;

	DEBUG ("add %s to menu %s separator ? %d\n", get_menu_label_text (menu_item, NULL),
	       [[cocoa_menu title] cStringUsingEncoding:NSUTF8StringEncoding],
	       GTK_IS_SEPARATOR_MENU_ITEM(menu_item));

	cocoa_item = cocoa_menu_item_get (menu_item);

	if (cocoa_item)
		return;

	if (GTK_IS_SEPARATOR_MENU_ITEM (menu_item)) {
		cocoa_item = [NSMenuItem separatorItem];
		DEBUG ("\ta separator\n");
	} else {

		if (!GTK_WIDGET_VISIBLE (menu_item)) {
			DEBUG ("\tnot visible\n");
			return;
		}

		const gchar* label_text = get_menu_label_text (menu_item, &label);

		if (label_text)
			cocoa_item = [ [ GNSMenuItem alloc] initWithTitle:[ [ NSString alloc] initWithCString:label_text encoding:NSUTF8StringEncoding]
				       andGtkWidget:(GtkMenuItem*)menu_item];
		else
			cocoa_item = [ [ GNSMenuItem alloc] initWithTitle:@"" andGtkWidget:(GtkMenuItem*)menu_item];
		DEBUG ("\tan item\n");
	}

	/* connect GtkMenuItem and NSMenuItem so that we can notice changes to accel/label/submenu etc. */
	cocoa_menu_item_connect (menu_item, (GNSMenuItem*) cocoa_item, label);
	cocoa_menu_item_update_state (cocoa_item, menu_item);

	if (index >= 0)
		[ cocoa_menu insertItem:cocoa_item atIndex:index];
	else
		[ cocoa_menu addItem:cocoa_item];

	if (!GTK_WIDGET_IS_SENSITIVE (menu_item))
		[cocoa_item setState:NSOffState];

#if MAC_OS_X_VERSION_MIN_REQUIRED > MAC_OS_X_VERSION_10_4
	if (!GTK_WIDGET_VISIBLE (menu_item))
		[cocoa_item setHidden:YES];
#endif

	if (GTK_IS_CHECK_MENU_ITEM (menu_item))
		cocoa_menu_item_update_active (cocoa_item, menu_item);

	if (!GTK_IS_SEPARATOR_MENU_ITEM (menu_item))
		cocoa_menu_item_update_accel_closure (cocoa_item, menu_item);

	if (gtk_menu_item_get_submenu (GTK_MENU_ITEM (menu_item)))
		cocoa_menu_item_update_submenu (cocoa_item, menu_item);

	[ cocoa_item release];

	if (GTK_IS_CHECK_MENU_ITEM (menu_item)) {
		GtkMenuItem* mitem = GTK_MENU_ITEM(menu_item);
		global_menu_items.push_back (mitem);
	}
}

static void
push_menu_shell_to_nsmenu (GtkMenuShell *menu_shell,
			   NSMenu*       cocoa_menu,
			   gboolean      /*toplevel*/,
			   gboolean      /*debug*/)
{
  GList         *children;
  GList         *l;

  children = gtk_container_get_children (GTK_CONTAINER (menu_shell));

  for (l = children; l; l = l->next)
    {
      GtkWidget   *menu_item = (GtkWidget*) l->data;

      if (GTK_IS_TEAROFF_MENU_ITEM (menu_item))
	continue;

      if (g_object_get_data (G_OBJECT (menu_item), "gtk-empty-menu-item"))
	continue;

      add_menu_item (cocoa_menu, menu_item, -1);
    }

  g_list_free (children);
}


static gulong emission_hook_id = 0;

static gboolean
parent_set_emission_hook (GSignalInvocationHint* /*ihint*/,
			  guint                  /*n_param_values*/,
			  const GValue*          param_values,
			  gpointer               data)
{
  GtkWidget *instance = (GtkWidget*) g_value_get_object (param_values);

  if (GTK_IS_MENU_ITEM (instance))
    {
      GtkWidget *previous_parent = (GtkWidget*) g_value_get_object (param_values + 1);
      GtkWidget *menu_shell      = NULL;

      if (GTK_IS_MENU_SHELL (previous_parent))
	{
	  menu_shell = previous_parent;
        }
      else if (GTK_IS_MENU_SHELL (instance->parent))
	{
	  menu_shell = instance->parent;
	}

      if (menu_shell)
        {
	  NSMenu *cocoa_menu = cocoa_menu_get (menu_shell);

	  if (cocoa_menu)
	    {
	      push_menu_shell_to_nsmenu (GTK_MENU_SHELL (menu_shell),
					 cocoa_menu,
					 cocoa_menu == (NSMenu*) data,
					 FALSE);
	    }
        }
    }

  return TRUE;
}

static void
parent_set_emission_hook_remove (GtkWidget*, gpointer)
{
	g_signal_remove_emission_hook (g_signal_lookup ("parent-set", GTK_TYPE_WIDGET),
				       emission_hook_id);
}

/* Building "standard" Cocoa/OS X menus */

#warning You can safely ignore the next warning about a duplicate interface definition
@interface NSApplication(NSWindowsMenu)
	- (void)setAppleMenu:(NSMenu *)aMenu;
@end

static NSMenu* _main_menubar = 0;
static NSMenu* _window_menu = 0;
static NSMenu* _app_menu = 0;

static int
add_to_menubar (NSMenu *menu)
{
	NSMenuItem *dummyItem = [[NSMenuItem alloc] initWithTitle:@""
				 action:nil keyEquivalent:@""];
	[dummyItem setSubmenu:menu];
	[_main_menubar addItem:dummyItem];
	[dummyItem release];
	return 0;
}

#if 0
static int
add_to_app_menu (NSMenu *menu)
{
	NSMenuItem *dummyItem = [[NSMenuItem alloc] initWithTitle:@""
				 action:nil keyEquivalent:@""];
	[dummyItem setSubmenu:menu];
	[_app_menu addItem:dummyItem];
	[dummyItem release];
	return 0;
}
#endif

static int
create_apple_menu ()
{
	NSMenuItem *menuitem;
	// Create the application (Apple) menu.
	_app_menu = [[NSMenu alloc] initWithTitle: @"Apple Menu"];

	NSMenu *menuServices = [[NSMenu alloc] initWithTitle: @"Services"];
	[NSApp setServicesMenu:menuServices];

	[_app_menu addItem: [NSMenuItem separatorItem]];
	menuitem = [[NSMenuItem alloc] initWithTitle: @"Services"
		    action:nil keyEquivalent:@""];
	[menuitem setSubmenu:menuServices];
	[_app_menu addItem: menuitem];
	[menuitem release];
	[_app_menu addItem: [NSMenuItem separatorItem]];
	menuitem = [[NSMenuItem alloc] initWithTitle:@"Hide"
		    action:@selector(hide:) keyEquivalent:@""];
	[menuitem setTarget: NSApp];
	[_app_menu addItem: menuitem];
	[menuitem release];
	menuitem = [[NSMenuItem alloc] initWithTitle:@"Hide Others"
		    action:@selector(hideOtherApplications:) keyEquivalent:@""];
	[menuitem setTarget: NSApp];
	[_app_menu addItem: menuitem];
	[menuitem release];
	menuitem = [[NSMenuItem alloc] initWithTitle:@"Show All"
		    action:@selector(unhideAllApplications:) keyEquivalent:@""];
	[menuitem setTarget: NSApp];
	[_app_menu addItem: menuitem];
	[menuitem release];
	[_app_menu addItem: [NSMenuItem separatorItem]];
	menuitem = [[NSMenuItem alloc] initWithTitle:@"Quit"
		    action:@selector(terminate:) keyEquivalent:@"q"];
	[menuitem setTarget: NSApp];
	[_app_menu addItem: menuitem];
	[menuitem release];

	[NSApp setAppleMenu:_app_menu];
	add_to_menubar (_app_menu);

	return 0;
}

#if 0
static int
add_to_window_menu (NSMenu *menu)
{
	NSMenuItem *dummyItem = [[NSMenuItem alloc] initWithTitle:@""
				 action:nil keyEquivalent:@""];
	[dummyItem setSubmenu:menu];
	[_window_menu addItem:dummyItem];
	[dummyItem release];
	return 0;
}

static int
create_window_menu ()
{
	_window_menu = [[NSMenu alloc] initWithTitle: @"Window"];

	[_window_menu addItemWithTitle:@"Minimize"
	 action:@selector(performMiniaturize:) keyEquivalent:@""];
	[_window_menu addItem: [NSMenuItem separatorItem]];
	[_window_menu addItemWithTitle:@"Bring All to Front"
	 action:@selector(arrangeInFront:) keyEquivalent:@""];

	[NSApp setWindowsMenu:_window_menu];
	add_to_menubar(_window_menu);

	return 0;
}
#endif

/*
 * public functions
 */

extern "C" void
gtk_application_set_menu_bar (GtkMenuShell *menu_shell)
{
  NSMenu* cocoa_menubar;

  g_return_if_fail (GTK_IS_MENU_SHELL (menu_shell));

  if (cocoa_menu_quark == 0)
    cocoa_menu_quark = g_quark_from_static_string ("NSMenu");

  if (cocoa_menu_item_quark == 0)
    cocoa_menu_item_quark = g_quark_from_static_string ("NSMenuItem");

  cocoa_menubar = [ [ NSApplication sharedApplication] mainMenu];

  /* turn off auto-enabling for the menu - its silly and slow and
     doesn't really make sense for a Gtk/Cocoa hybrid menu.
   */

  [cocoa_menubar setAutoenablesItems:YES];

  emission_hook_id =
    g_signal_add_emission_hook (g_signal_lookup ("parent-set",
						 GTK_TYPE_WIDGET),
				0,
				parent_set_emission_hook,
				cocoa_menubar, NULL);


  g_signal_connect (menu_shell, "destroy",
		    G_CALLBACK (parent_set_emission_hook_remove),
		    NULL);

  push_menu_shell_to_nsmenu (menu_shell, cocoa_menubar, TRUE, FALSE);
}

extern "C" void
gtk_application_add_app_menu_item (GtkApplicationMenuGroup *group,
				   GtkMenuItem     *menu_item)
{
  // we know that the application menu is always the submenu of the first item in the main menu
  NSMenu* mainMenu;
  NSMenu *appMenu;
  NSMenuItem *firstItem;
  GList   *list;
  gint     index = 0;

  mainMenu = [NSApp mainMenu];
  firstItem = [ mainMenu itemAtIndex:0];
  appMenu = [ firstItem submenu ];

  g_return_if_fail (group != NULL);
  g_return_if_fail (GTK_IS_MENU_ITEM (menu_item));

  for (list = _gtk_application_menu_groups; list; list = g_list_next (list))
    {
      GtkApplicationMenuGroup *list_group = (GtkApplicationMenuGroup*) list->data;

      index += g_list_length (list_group->items);

      /*  adjust index for the separator between groups, but not
       *  before the first group
       */
      if (list_group->items && list->prev)
	index++;

      if (group == list_group)
	{
		/*  add a separator before adding the first item, but not
		 *  for the first group
		 */

		if (!group->items && list->prev)
		   {
			   [appMenu insertItem:[NSMenuItem separatorItem] atIndex:index+1];
			   index++;
		   }
		DEBUG ("Add to APP menu bar %s\n", get_menu_label_text (GTK_WIDGET(menu_item), NULL));
		add_menu_item (appMenu, GTK_WIDGET(menu_item), index+1);

		group->items = g_list_append (group->items, menu_item);
		gtk_widget_hide (GTK_WIDGET (menu_item));
		return;
	}
    }

  if (!list)
    g_warning ("%s: app menu group %p does not exist",
	       G_STRFUNC, group);
}

/* application delegate, currently in C++ */

#include <gtkmm2ext/application.h>
#include <glibmm/ustring.h>

namespace Gtk {
	namespace Application {
		sigc::signal<void,bool> ActivationChanged;
		sigc::signal<void,const Glib::ustring&> ShouldLoad;
		sigc::signal<void> ShouldQuit;
	}
}

@interface GtkApplicationNotificationObject : NSObject {}
- (GtkApplicationNotificationObject*) init;
@end

@implementation GtkApplicationNotificationObject
- (GtkApplicationNotificationObject*) init
{
	self = [ super init ];

	if (self) {
		[[NSNotificationCenter defaultCenter] addObserver:self
		 selector:@selector(appDidBecomeActive:)
		 name:NSApplicationDidBecomeActiveNotification
		 object:[NSApplication sharedApplication]];

		[[NSNotificationCenter defaultCenter] addObserver:self
		 selector:@selector(appDidBecomeInactive:)
		 name:NSApplicationWillResignActiveNotification
		 object:[NSApplication sharedApplication]];
	}

	return self;
}

- (void)appDidBecomeActive:(NSNotification *) notification
{
	UNUSED_PARAMETER(notification);
	Gtkmm2ext::Application::instance()->ActivationChanged (true);
}

- (void)appDidBecomeInactive:(NSNotification *) notification
{
	UNUSED_PARAMETER(notification);
	Gtkmm2ext::Application::instance()->ActivationChanged (false);
}

@end

@interface GtkApplicationDelegate : NSObject
-(BOOL) application:(NSApplication*) app openFile:(NSString*) file;
- (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication *) app;
@end

@implementation GtkApplicationDelegate
-(BOOL) application:(NSApplication*) app openFile:(NSString*) file
{
	UNUSED_PARAMETER(app);
	Glib::ustring utf8_path ([file UTF8String]);
	Gtkmm2ext::Application::instance()->ShouldLoad (utf8_path);
	return 1;
}
- (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication *) app
{
	UNUSED_PARAMETER(app);
	if (_modal_state > 0) {
		return NSTerminateCancel;
	}
	Gtkmm2ext::Application::instance()->ShouldQuit ();
	return NSTerminateCancel;
}
@end

static void
gdk_quartz_modal_notify (GdkWindow*, gboolean modal)
{
	/* this global will control sensitivity of our app menu items, via validateMenuItem */
	if (modal) {
		++_modal_state;
	} else if (_modal_state > 0) {
		--_modal_state;
	}

	/* Need to notify GTK that actions are insensitive where necessary */

	for (auto & mitem : global_menu_items) {
		GtkAction* act = gtk_activatable_get_related_action (GTK_ACTIVATABLE(mitem));
		if (act) {
			gtk_action_set_sensitive (act, 0 == _modal_state);
		}
	}
}

/* Basic setup */

extern "C" int
gtk_application_init ()
{
	gdk_window_set_modal_notify (gdk_quartz_modal_notify);

	_main_menubar = [[NSMenu alloc] initWithTitle: @""];

	if (!_main_menubar)
		return -1;

	[NSApp setMainMenu: _main_menubar];
	create_apple_menu ();
	// create_window_menu ();

	/* this will stick around for ever ... is that OK ? */

	[ [GtkApplicationNotificationObject alloc] init];
	[ NSApp setDelegate: [GtkApplicationDelegate new]];

	return 0;
}

extern "C" void
gtk_application_ready ()
{
	[ NSApp finishLaunching ];
        [[NSApplication sharedApplication] activateIgnoringOtherApps : YES];
}

extern "C" void
gtk_application_hide ()
{
    [NSApp performSelector:@selector(hide:)];
}

extern "C" void
gtk_application_cleanup()
{
	_exiting = 1;

	if (_window_menu) {
		[ _window_menu release ];
		_window_menu = 0;
	}
	if (_app_menu) {
		[ _app_menu release ];
	        _app_menu = 0;
	}
	if (_main_menubar) {
		[ _main_menubar release ];
	        _main_menubar = 0;
	}
}
