/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "mozilla/ArrayUtils.h"
#include "mozilla/Assertions.h"
#include "mozilla/dom/Element.h"
#include "mozilla/Move.h"
#include "mozilla/Preferences.h"
#include "mozilla/TextEvents.h"
#include "nsAutoPtr.h"
#include "nsContentUtils.h"
#include "nsCRT.h"
#include "nsGkAtoms.h"
#include "nsGtkUtils.h"
#include "nsIContent.h"
#include "nsIDocument.h"
#include "nsIDOMDocument.h"
#include "nsIDOMEvent.h"
#include "nsIDOMEventTarget.h"
#include "nsIDOMKeyEvent.h"
#include "nsIDOMXULCommandEvent.h"
#include "nsIRunnable.h"
#include "nsReadableUtils.h"
#include "nsString.h"
#include "nsStyleContext.h"
#include "nsThreadUtils.h"

#include "nsMenu.h"
#include "nsMenuBar.h"
#include "nsMenuContainer.h"
#include "nsNativeMenuDocListener.h"

#include <gdk/gdk.h>
#include <gdk/gdkkeysyms.h>
#if (MOZ_WIDGET_GTK == 3)
#include <gdk/gdkkeysyms-compat.h>
#endif
#include <gdk/gdkx.h>
#include <gtk/gtk.h>

#include "nsMenuItem.h"

using namespace mozilla;

struct KeyCodeData {
    const char* str;
    size_t strlength;
    uint32_t keycode;
};

static struct KeyCodeData gKeyCodes[] = {
#define NS_DEFINE_VK(aDOMKeyName, aDOMKeyCode) \
  { #aDOMKeyName, sizeof(#aDOMKeyName) - 1, aDOMKeyCode },
#include "mozilla/VirtualKeyCodeList.h"
#undef NS_DEFINE_VK
  { nullptr, 0, 0 }
};

struct KeyPair {
    uint32_t DOMKeyCode;
    guint GDKKeyval;
};

//
// Netscape keycodes are defined in widget/public/nsGUIEvent.h
// GTK keycodes are defined in <gdk/gdkkeysyms.h>
//
static const KeyPair gKeyPairs[] = {
    { NS_VK_CANCEL,     GDK_Cancel },
    { NS_VK_BACK,       GDK_BackSpace },
    { NS_VK_TAB,        GDK_Tab },
    { NS_VK_TAB,        GDK_ISO_Left_Tab },
    { NS_VK_CLEAR,      GDK_Clear },
    { NS_VK_RETURN,     GDK_Return },
    { NS_VK_SHIFT,      GDK_Shift_L },
    { NS_VK_SHIFT,      GDK_Shift_R },
    { NS_VK_SHIFT,      GDK_Shift_Lock },
    { NS_VK_CONTROL,    GDK_Control_L },
    { NS_VK_CONTROL,    GDK_Control_R },
    { NS_VK_ALT,        GDK_Alt_L },
    { NS_VK_ALT,        GDK_Alt_R },
    { NS_VK_META,       GDK_Meta_L },
    { NS_VK_META,       GDK_Meta_R },

    // Assume that Super or Hyper is always mapped to physical Win key.
    { NS_VK_WIN,        GDK_Super_L },
    { NS_VK_WIN,        GDK_Super_R },
    { NS_VK_WIN,        GDK_Hyper_L },
    { NS_VK_WIN,        GDK_Hyper_R },

    // GTK's AltGraph key is similar to Mac's Option (Alt) key.  However,
    // unfortunately, browsers on Mac are using NS_VK_ALT for it even though
    // it's really different from Alt key on Windows.
    // On the other hand, GTK's AltGrapsh keys are really different from
    // Alt key.  However, there is no AltGrapsh key on Windows.  On Windows,
    // both Ctrl and Alt keys are pressed internally when AltGr key is pressed.
    // For some languages' users, AltGraph key is important, so, web
    // applications on such locale may want to know AltGraph key press.
    // Therefore, we should map AltGr keycode for them only on GTK.
    { NS_VK_ALTGR,      GDK_ISO_Level3_Shift },
    { NS_VK_ALTGR,      GDK_ISO_Level5_Shift },
    // We assume that Mode_switch is always used for level3 shift.
    { NS_VK_ALTGR,      GDK_Mode_switch },

    { NS_VK_PAUSE,      GDK_Pause },
    { NS_VK_CAPS_LOCK,  GDK_Caps_Lock },
    { NS_VK_KANA,       GDK_Kana_Lock },
    { NS_VK_KANA,       GDK_Kana_Shift },
    { NS_VK_HANGUL,     GDK_Hangul },
    // { NS_VK_JUNJA,      GDK_XXX },
    // { NS_VK_FINAL,      GDK_XXX },
    { NS_VK_HANJA,      GDK_Hangul_Hanja },
    { NS_VK_KANJI,      GDK_Kanji },
    { NS_VK_ESCAPE,     GDK_Escape },
    { NS_VK_CONVERT,    GDK_Henkan },
    { NS_VK_NONCONVERT, GDK_Muhenkan },
    // { NS_VK_ACCEPT,     GDK_XXX },
    // { NS_VK_MODECHANGE, GDK_XXX },
    { NS_VK_SPACE,      GDK_space },
    { NS_VK_PAGE_UP,    GDK_Page_Up },
    { NS_VK_PAGE_DOWN,  GDK_Page_Down },
    { NS_VK_END,        GDK_End },
    { NS_VK_HOME,       GDK_Home },
    { NS_VK_LEFT,       GDK_Left },
    { NS_VK_UP,         GDK_Up },
    { NS_VK_RIGHT,      GDK_Right },
    { NS_VK_DOWN,       GDK_Down },
    { NS_VK_SELECT,     GDK_Select },
    { NS_VK_PRINT,      GDK_Print },
    { NS_VK_EXECUTE,    GDK_Execute },
    { NS_VK_PRINTSCREEN, GDK_Print },
    { NS_VK_INSERT,     GDK_Insert },
    { NS_VK_DELETE,     GDK_Delete },
    { NS_VK_HELP,       GDK_Help },

    // keypad keys
    { NS_VK_LEFT,       GDK_KP_Left },
    { NS_VK_RIGHT,      GDK_KP_Right },
    { NS_VK_UP,         GDK_KP_Up },
    { NS_VK_DOWN,       GDK_KP_Down },
    { NS_VK_PAGE_UP,    GDK_KP_Page_Up },
    // Not sure what these are
    //{ NS_VK_,       GDK_KP_Prior },
    //{ NS_VK_,        GDK_KP_Next },
    { NS_VK_CLEAR,      GDK_KP_Begin }, // Num-unlocked 5
    { NS_VK_PAGE_DOWN,  GDK_KP_Page_Down },
    { NS_VK_HOME,       GDK_KP_Home },
    { NS_VK_END,        GDK_KP_End },
    { NS_VK_INSERT,     GDK_KP_Insert },
    { NS_VK_DELETE,     GDK_KP_Delete },
    { NS_VK_RETURN,     GDK_KP_Enter },

    { NS_VK_NUM_LOCK,   GDK_Num_Lock },
    { NS_VK_SCROLL_LOCK,GDK_Scroll_Lock },

    // Function keys
    { NS_VK_F1,         GDK_F1 },
    { NS_VK_F2,         GDK_F2 },
    { NS_VK_F3,         GDK_F3 },
    { NS_VK_F4,         GDK_F4 },
    { NS_VK_F5,         GDK_F5 },
    { NS_VK_F6,         GDK_F6 },
    { NS_VK_F7,         GDK_F7 },
    { NS_VK_F8,         GDK_F8 },
    { NS_VK_F9,         GDK_F9 },
    { NS_VK_F10,        GDK_F10 },
    { NS_VK_F11,        GDK_F11 },
    { NS_VK_F12,        GDK_F12 },
    { NS_VK_F13,        GDK_F13 },
    { NS_VK_F14,        GDK_F14 },
    { NS_VK_F15,        GDK_F15 },
    { NS_VK_F16,        GDK_F16 },
    { NS_VK_F17,        GDK_F17 },
    { NS_VK_F18,        GDK_F18 },
    { NS_VK_F19,        GDK_F19 },
    { NS_VK_F20,        GDK_F20 },
    { NS_VK_F21,        GDK_F21 },
    { NS_VK_F22,        GDK_F22 },
    { NS_VK_F23,        GDK_F23 },
    { NS_VK_F24,        GDK_F24 },

    // context menu key, keysym 0xff67, typically keycode 117 on 105-key (Microsoft) 
    // x86 keyboards, located between right 'Windows' key and right Ctrl key
    { NS_VK_CONTEXT_MENU, GDK_Menu },
    { NS_VK_SLEEP,      GDK_Sleep },

    { NS_VK_ATTN,       GDK_3270_Attn },
    { NS_VK_CRSEL,      GDK_3270_CursorSelect },
    { NS_VK_EXSEL,      GDK_3270_ExSelect },
    { NS_VK_EREOF,      GDK_3270_EraseEOF },
    { NS_VK_PLAY,       GDK_3270_Play },
    //{ NS_VK_ZOOM,       GDK_XXX },
    { NS_VK_PA1,        GDK_3270_PA1 },
};

static guint
ConvertGeckoKeyNameToGDKKeyval(nsAString& aKeyName) {
    NS_ConvertUTF16toUTF8 keyName(aKeyName);
    ToUpperCase(keyName); // We want case-insensitive comparison with data
                          // stored as uppercase.

    uint32_t keyCode = 0;

    uint32_t keyNameLength = keyName.Length();
    const char* keyNameStr = keyName.get();
    for (uint16_t i = 0; i < ArrayLength(gKeyCodes); ++i) {
        if (keyNameLength == gKeyCodes[i].strlength &&
            !nsCRT::strcmp(gKeyCodes[i].str, keyNameStr)) {
            keyCode = gKeyCodes[i].keycode;
            break;
        }
    }

    // First, try to handle alphanumeric input, not listed in nsKeycodes:
    // most likely, more letters will be getting typed in than things in
    // the key list, so we will look through these first.

    if (keyCode >= NS_VK_A && keyCode <= NS_VK_Z) {
        // gdk and DOM both use the ASCII codes for these keys.
        return keyCode;
    }

    // numbers
    if (keyCode >= NS_VK_0 && keyCode <= NS_VK_9) {
        // gdk and DOM both use the ASCII codes for these keys.
        return keyCode - NS_VK_0 + GDK_0;
    }

    switch (keyCode) {
        // keys in numpad
        case NS_VK_MULTIPLY:  return GDK_KP_Multiply;
        case NS_VK_ADD:       return GDK_KP_Add;
        case NS_VK_SEPARATOR: return GDK_KP_Separator;
        case NS_VK_SUBTRACT:  return GDK_KP_Subtract;
        case NS_VK_DECIMAL:   return GDK_KP_Decimal;
        case NS_VK_DIVIDE:    return GDK_KP_Divide;
        case NS_VK_NUMPAD0:   return GDK_KP_0;
        case NS_VK_NUMPAD1:   return GDK_KP_1;
        case NS_VK_NUMPAD2:   return GDK_KP_2;
        case NS_VK_NUMPAD3:   return GDK_KP_3;
        case NS_VK_NUMPAD4:   return GDK_KP_4;
        case NS_VK_NUMPAD5:   return GDK_KP_5;
        case NS_VK_NUMPAD6:   return GDK_KP_6;
        case NS_VK_NUMPAD7:   return GDK_KP_7;
        case NS_VK_NUMPAD8:   return GDK_KP_8;
        case NS_VK_NUMPAD9:   return GDK_KP_9;
        // other prinable keys
        case NS_VK_SPACE:               return GDK_space;
        case NS_VK_COLON:               return GDK_colon;
        case NS_VK_SEMICOLON:           return GDK_semicolon;
        case NS_VK_LESS_THAN:           return GDK_less;
        case NS_VK_EQUALS:              return GDK_equal;
        case NS_VK_GREATER_THAN:        return GDK_greater;
        case NS_VK_QUESTION_MARK:       return GDK_question;
        case NS_VK_AT:                  return GDK_at;
        case NS_VK_CIRCUMFLEX:          return GDK_asciicircum;
        case NS_VK_EXCLAMATION:         return GDK_exclam;
        case NS_VK_DOUBLE_QUOTE:        return GDK_quotedbl;
        case NS_VK_HASH:                return GDK_numbersign;
        case NS_VK_DOLLAR:              return GDK_dollar;
        case NS_VK_PERCENT:             return GDK_percent;
        case NS_VK_AMPERSAND:           return GDK_ampersand;
        case NS_VK_UNDERSCORE:          return GDK_underscore;
        case NS_VK_OPEN_PAREN:          return GDK_parenleft;
        case NS_VK_CLOSE_PAREN:         return GDK_parenright;
        case NS_VK_ASTERISK:            return GDK_asterisk;
        case NS_VK_PLUS:                return GDK_plus;
        case NS_VK_PIPE:                return GDK_bar;
        case NS_VK_HYPHEN_MINUS:        return GDK_minus;
        case NS_VK_OPEN_CURLY_BRACKET:  return GDK_braceleft;
        case NS_VK_CLOSE_CURLY_BRACKET: return GDK_braceright;
        case NS_VK_TILDE:               return GDK_asciitilde;
        case NS_VK_COMMA:               return GDK_comma;
        case NS_VK_PERIOD:              return GDK_period;
        case NS_VK_SLASH:               return GDK_slash;
        case NS_VK_BACK_QUOTE:          return GDK_grave;
        case NS_VK_OPEN_BRACKET:        return GDK_bracketleft;
        case NS_VK_BACK_SLASH:          return GDK_backslash;
        case NS_VK_CLOSE_BRACKET:       return GDK_bracketright;
        case NS_VK_QUOTE:               return GDK_apostrophe;
    }

    // misc other things
    for (uint32_t i = 0; i < ArrayLength(gKeyPairs); ++i) {
        if (gKeyPairs[i].DOMKeyCode == keyCode) {
            return gKeyPairs[i].GDKKeyval;
        }
    }

    return 0;
}

class nsMenuItemUncheckSiblingsRunnable final : public Runnable {
public:
    NS_IMETHODIMP Run() {
        if (mMenuItem) {
            static_cast<nsMenuItem* >(mMenuItem.get())->UncheckSiblings();
        }
        return NS_OK;
    }

    nsMenuItemUncheckSiblingsRunnable(nsMenuItem* aMenuItem) :
        mMenuItem(aMenuItem) { };

private:
    nsWeakMenuObject mMenuItem;
};

bool
nsMenuItem::IsCheckboxOrRadioItem() const {
    return mType == eMenuItemType_Radio ||
           mType == eMenuItemType_CheckBox;
}

/* static */ void
nsMenuItem::item_activated_cb(DbusmenuMenuitem* menuitem,
                              guint timestamp,
                              gpointer user_data) {
    nsMenuItem* item = static_cast<nsMenuItem* >(user_data);
    item->Activate(timestamp);
}

void
nsMenuItem::Activate(uint32_t aTimestamp) {
    GdkWindow* window = gtk_widget_get_window(MenuBar()->TopLevelWindow());
    gdk_x11_window_set_user_time(
        window, std::min(aTimestamp, gdk_x11_get_server_time(window)));

    // We do this to avoid mutating our view of the menu until
    // after we have finished
    nsNativeMenuDocListener::BlockUpdatesScope updatesBlocker;

    if (!ContentNode()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::autocheck,
                                    nsGkAtoms::_false, eCaseMatters) &&
        (mType == eMenuItemType_CheckBox ||
         (mType == eMenuItemType_Radio && !mIsChecked))) {
        ContentNode()->SetAttr(kNameSpaceID_None, nsGkAtoms::checked,
                               mIsChecked ?
                               NS_LITERAL_STRING("false") :  NS_LITERAL_STRING("true"),
                               true);
    }

    nsIDocument* doc = ContentNode()->OwnerDoc();
    nsCOMPtr<nsIDOMEventTarget> target = do_QueryInterface(ContentNode());
    nsCOMPtr<nsIDOMDocument> domDoc = do_QueryInterface(doc);
    if (domDoc && target) {
        nsCOMPtr<nsIDOMEvent> event;
        domDoc->CreateEvent(NS_LITERAL_STRING("xulcommandevent"),
                            getter_AddRefs(event));
        nsCOMPtr<nsIDOMXULCommandEvent> command = do_QueryInterface(event);
        if (command) {
            command->InitCommandEvent(NS_LITERAL_STRING("command"),
                                      true, true, doc->GetInnerWindow(), 0,
                                      false, false, false, false, nullptr);

            event->SetTrusted(true);
            bool dummy;
            target->DispatchEvent(event, &dummy);
        }
    }

    // This kinda sucks, but Unity doesn't send a closed event
    // after activating a menuitem
    nsMenuObject* ancestor = Parent();
    while (ancestor && ancestor->Type() == eType_Menu) {
        static_cast<nsMenu* >(ancestor)->OnClose();
        ancestor = ancestor->Parent();
    }
}

void
nsMenuItem::CopyAttrFromNodeIfExists(nsIContent* aContent, nsIAtom* aAttribute) {
    nsAutoString value;
    if (aContent->GetAttr(kNameSpaceID_None, aAttribute, value)) {
        ContentNode()->SetAttr(kNameSpaceID_None, aAttribute, value, true);
    }
}

void
nsMenuItem::UpdateState() {
    if (!IsCheckboxOrRadioItem()) {
        return;
    }

    mIsChecked = ContentNode()->AttrValueIs(kNameSpaceID_None,
                                            nsGkAtoms::checked,
                                            nsGkAtoms::_true,
                                            eCaseMatters);
    dbusmenu_menuitem_property_set_int(GetNativeData(),
                                       DBUSMENU_MENUITEM_PROP_TOGGLE_STATE,
                                       mIsChecked ?
                                         DBUSMENU_MENUITEM_TOGGLE_STATE_CHECKED : 
                                         DBUSMENU_MENUITEM_TOGGLE_STATE_UNCHECKED);
}

void
nsMenuItem::UpdateTypeAndState() {
    static nsIContent::AttrValuesArray attrs[] =
        { &nsGkAtoms::checkbox, &nsGkAtoms::radio, nullptr };
    int32_t type = ContentNode()->FindAttrValueIn(kNameSpaceID_None,
                                                  nsGkAtoms::type,
                                                  attrs, eCaseMatters);

    if (type >= 0 && type < 2) {
        if (type == 0) {
            dbusmenu_menuitem_property_set(GetNativeData(),
                                           DBUSMENU_MENUITEM_PROP_TOGGLE_TYPE,
                                           DBUSMENU_MENUITEM_TOGGLE_CHECK);
            mType = eMenuItemType_CheckBox;
        } else if (type == 1) {
            dbusmenu_menuitem_property_set(GetNativeData(),
                                           DBUSMENU_MENUITEM_PROP_TOGGLE_TYPE,
                                           DBUSMENU_MENUITEM_TOGGLE_RADIO);
            mType = eMenuItemType_Radio;
        }

        UpdateState();
    } else {
        dbusmenu_menuitem_property_remove(GetNativeData(),
                                          DBUSMENU_MENUITEM_PROP_TOGGLE_TYPE);
        dbusmenu_menuitem_property_remove(GetNativeData(),
                                          DBUSMENU_MENUITEM_PROP_TOGGLE_STATE);
        mType = eMenuItemType_Normal;
    }
}

void
nsMenuItem::UpdateAccel() {
    nsIDocument* doc = ContentNode()->GetUncomposedDoc();
    if (doc) {
        nsCOMPtr<nsIContent> oldKeyContent;
        oldKeyContent.swap(mKeyContent);

        nsAutoString key;
        ContentNode()->GetAttr(kNameSpaceID_None, nsGkAtoms::key, key);
        if (!key.IsEmpty()) {
            mKeyContent = doc->GetElementById(key);
        }

        if (mKeyContent != oldKeyContent) {
            if (oldKeyContent) {
                DocListener()->UnregisterForContentChanges(oldKeyContent);
            }
            if (mKeyContent) {
                DocListener()->RegisterForContentChanges(mKeyContent, this);
            }
        }
    }

    if (!mKeyContent) {
        dbusmenu_menuitem_property_remove(GetNativeData(),
                                          DBUSMENU_MENUITEM_PROP_SHORTCUT);
        return;
    }

    nsAutoString modifiers;
    mKeyContent->GetAttr(kNameSpaceID_None, nsGkAtoms::modifiers, modifiers);

    uint32_t modifier = 0;

    if (!modifiers.IsEmpty()) {
        char* str = ToNewUTF8String(modifiers);
        char* token = strtok(str, ", \t");
        while(token) {
            if (nsCRT::strcmp(token, "shift") == 0) {
                modifier |= GDK_SHIFT_MASK;
            } else if (nsCRT::strcmp(token, "alt") == 0) {
                modifier |= GDK_MOD1_MASK;
            } else if (nsCRT::strcmp(token, "meta") == 0) {
                modifier |= GDK_META_MASK;
            } else if (nsCRT::strcmp(token, "control") == 0) {
                modifier |= GDK_CONTROL_MASK;
            } else if (nsCRT::strcmp(token, "accel") == 0) {
                int32_t accel = Preferences::GetInt("ui.key.accelKey");
                if (accel == nsIDOMKeyEvent::DOM_VK_META) {
                    modifier |= GDK_META_MASK;
                } else if (accel == nsIDOMKeyEvent::DOM_VK_ALT) {
                    modifier |= GDK_MOD1_MASK;
                } else {
                    modifier |= GDK_CONTROL_MASK;
                }
            }

            token = strtok(nullptr, ", \t");
        }

        free(str);
    }

    nsAutoString keyStr;
    mKeyContent->GetAttr(kNameSpaceID_None, nsGkAtoms::key, keyStr);

    guint key = 0;
    if (!keyStr.IsEmpty()) {
        key = gdk_unicode_to_keyval(*keyStr.BeginReading());
    }

    if (key == 0) {
        mKeyContent->GetAttr(kNameSpaceID_None, nsGkAtoms::keycode, keyStr);
        if (!keyStr.IsEmpty()) {
            key = ConvertGeckoKeyNameToGDKKeyval(keyStr);
        }
    }

    if (key == 0) {
        key = GDK_VoidSymbol;
    }

    if (key != GDK_VoidSymbol) {
        dbusmenu_menuitem_property_set_shortcut(GetNativeData(), key,
                                                static_cast<GdkModifierType>(modifier));
    } else {
        dbusmenu_menuitem_property_remove(GetNativeData(),
                                          DBUSMENU_MENUITEM_PROP_SHORTCUT);
    }
}

nsMenuBar*
nsMenuItem::MenuBar() {
    nsMenuObject* tmp = this;
    while (tmp->Parent()) {
        tmp = tmp->Parent();
    }

    MOZ_ASSERT(tmp->Type() == eType_MenuBar, "The top-level should be a menubar");

    return static_cast<nsMenuBar* >(tmp);
}

void
nsMenuItem::UncheckSiblings() {
    if (!ContentNode()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
                                    nsGkAtoms::radio, eCaseMatters)) {
        // If we're not a radio button, we don't care
        return;
    }

    nsAutoString name;
    ContentNode()->GetAttr(kNameSpaceID_None, nsGkAtoms::name, name);

    nsIContent* parent = ContentNode()->GetParent();
    if (!parent) {
        return;
    }

    uint32_t count = parent->GetChildCount();
    for (uint32_t i = 0; i < count; ++i) {
        nsIContent* sibling = parent->GetChildAt(i);

        nsAutoString otherName;
        sibling->GetAttr(kNameSpaceID_None, nsGkAtoms::name, otherName);

        if (sibling != ContentNode() && otherName == name && 
            sibling->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
                                 nsGkAtoms::radio, eCaseMatters)) {
            sibling->UnsetAttr(kNameSpaceID_None, nsGkAtoms::checked, true);
        }
    }
}

void
nsMenuItem::InitializeNativeData() {
    g_signal_connect(G_OBJECT(GetNativeData()),
                     DBUSMENU_MENUITEM_SIGNAL_ITEM_ACTIVATED,
                     G_CALLBACK(item_activated_cb), this);
    mNeedsUpdate = true;
}

void
nsMenuItem::UpdateContentAttributes() {
    nsIDocument* doc = ContentNode()->GetUncomposedDoc();
    if (!doc) {
        return;
    }

    nsAutoString command;
    ContentNode()->GetAttr(kNameSpaceID_None, nsGkAtoms::command, command);
    if (command.IsEmpty()) {
        return;
    }

    nsCOMPtr<nsIContent> commandContent = doc->GetElementById(command);
    if (!commandContent) {
        return;
    }

    if (commandContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled,
                                    nsGkAtoms::_true, eCaseMatters)) {
        ContentNode()->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
                               NS_LITERAL_STRING("true"), true);
    } else {
        ContentNode()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, true);
    }

    CopyAttrFromNodeIfExists(commandContent, nsGkAtoms::checked);
    CopyAttrFromNodeIfExists(commandContent, nsGkAtoms::accesskey);
    CopyAttrFromNodeIfExists(commandContent, nsGkAtoms::label);
    CopyAttrFromNodeIfExists(commandContent, nsGkAtoms::hidden);
}

void
nsMenuItem::Update(nsStyleContext* aStyleContext) {
    if (mNeedsUpdate) {
        mNeedsUpdate = false;

        UpdateTypeAndState();
        UpdateAccel();
        UpdateLabel();
        UpdateSensitivity();
    }

    UpdateVisibility(aStyleContext);
    UpdateIcon(aStyleContext);
}

bool
nsMenuItem::IsCompatibleWithNativeData(DbusmenuMenuitem* aNativeData) const {
    return nsCRT::strcmp(dbusmenu_menuitem_property_get(aNativeData,
                                                        DBUSMENU_MENUITEM_PROP_TYPE),
                         "separator") != 0;
}

nsMenuObject::PropertyFlags
nsMenuItem::SupportedProperties() const {
    return static_cast<nsMenuObject::PropertyFlags>(
        nsMenuObject::ePropLabel |
        nsMenuObject::ePropEnabled |
        nsMenuObject::ePropVisible |
        nsMenuObject::ePropIconData |
        nsMenuObject::ePropShortcut |
        nsMenuObject::ePropToggleType |
        nsMenuObject::ePropToggleState
    );
}

void
nsMenuItem::OnAttributeChanged(nsIContent* aContent, nsIAtom* aAttribute) {
    MOZ_ASSERT(aContent == ContentNode() || aContent == mKeyContent,
               "Received an event that wasn't meant for us!");

    if (aContent == ContentNode() && aAttribute == nsGkAtoms::checked &&
        aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked,
                              nsGkAtoms::_true, eCaseMatters)) {
        nsContentUtils::AddScriptRunner(
            new nsMenuItemUncheckSiblingsRunnable(this));
    }

    if (mNeedsUpdate) {
        return;
    }

    if (!Parent()->IsBeingDisplayed()) {
        mNeedsUpdate = true;
        return;
    }

    if (aContent == ContentNode()) {
        if (aAttribute == nsGkAtoms::key) {
            UpdateAccel();
        } else if (aAttribute == nsGkAtoms::label ||
                   aAttribute == nsGkAtoms::accesskey ||
                   aAttribute == nsGkAtoms::crop) {
            UpdateLabel();
        } else if (aAttribute == nsGkAtoms::disabled) {
            UpdateSensitivity();
        } else if (aAttribute == nsGkAtoms::type) {
            UpdateTypeAndState();
        } else if (aAttribute == nsGkAtoms::checked) {
            UpdateState();
        } else if (aAttribute == nsGkAtoms::hidden ||
                   aAttribute == nsGkAtoms::collapsed) {
            RefPtr<nsStyleContext> sc = GetStyleContext();
            UpdateVisibility(sc);
        } else if (aAttribute == nsGkAtoms::image) {
            RefPtr<nsStyleContext> sc = GetStyleContext();
            UpdateIcon(sc);
        }
    } else if (aContent == mKeyContent &&
               (aAttribute == nsGkAtoms::key ||
                aAttribute == nsGkAtoms::keycode ||
                aAttribute == nsGkAtoms::modifiers)) {
        UpdateAccel();
    }
}

nsMenuItem::nsMenuItem(nsMenuContainer* aParent, nsIContent* aContent) :
    nsMenuObject(aParent, aContent),
    mType(eMenuItemType_Normal),
    mIsChecked(false),
    mNeedsUpdate(false) {
    MOZ_COUNT_CTOR(nsMenuItem);
}

nsMenuItem::~nsMenuItem() {
    if (DocListener() && mKeyContent) {
        DocListener()->UnregisterForContentChanges(mKeyContent);
    }

    if (GetNativeData()) {
        g_signal_handlers_disconnect_by_func(GetNativeData(),
                                             FuncToGpointer(item_activated_cb),
                                             this);
    }

    MOZ_COUNT_DTOR(nsMenuItem);
}

nsMenuObject::EType
nsMenuItem::Type() const {
    return eType_MenuItem;
}
