// Urho3D editor user interface

XMLFile@ uiStyle;
XMLFile@ iconStyle;
UIElement@ uiMenuBar;
UIElement@ quickMenu;
Array<QuickMenuItem@> quickMenuItems;
FileSelector@ uiFileSelector;

const ShortStringHash UI_ELEMENT_TYPE("UIElement");
const ShortStringHash WINDOW_TYPE("Window");
const ShortStringHash MENU_TYPE("Menu");
const ShortStringHash TEXT_TYPE("Text");
const ShortStringHash CURSOR_TYPE("Cursor");

const String AUTO_STYLE("");    // Empty string means auto style, i.e. applying style according to UI-element's type automatically
const String TEMP_SCENE_NAME("_tempscene_.xml");
const ShortStringHash CALLBACK_VAR("Callback");
const ShortStringHash INDENT_MODIFIED_BY_ICON_VAR("IconIndented");

const int SHOW_POPUP_INDICATOR = -1;
const uint MAX_QUICK_MENU_ITEMS = 10;

Array<String> uiSceneFilters = {"*.xml", "*.bin", "*.*"};
Array<String> uiElementFilters = {"*.xml"};
Array<String> uiAllFilters = {"*.*"};
Array<String> uiScriptFilters = {"*.as", "*.*"};
uint uiSceneFilter = 0;
uint uiElementFilter = 0;
uint uiNodeFilter = 0;
uint uiImportFilter = 0;
uint uiScriptFilter = 0;
String uiScenePath = fileSystem.programDir + "Data/Scenes";
String uiElementPath = fileSystem.programDir + "Data/UI";
String uiNodePath = fileSystem.programDir + "Data/Objects";
String uiImportPath;
String uiScriptPath = fileSystem.programDir + "Data/Scripts";

bool uiFaded = false;
float uiMinOpacity = 0.3;
float uiMaxOpacity = 0.7;
bool uiHidden = false;

void CreateUI()
{
    // Remove all existing UI content in case we are reloading the editor script
    /// \todo The console will not be properly recreated as it has already been created once
    ui.root.RemoveAllChildren();

    uiStyle = cache.GetResource("XMLFile", "UI/DefaultStyle.xml");
    ui.root.defaultStyle = uiStyle;
    iconStyle = cache.GetResource("XMLFile", "UI/EditorIcons.xml");

    CreateCursor();
    CreateMenuBar();
    CreateQuickMenu();
    CreateHierarchyWindow();
    CreateAttributeInspectorWindow();
    CreateEditorSettingsDialog();
    CreateEditorPreferencesDialog();
    CreateMaterialEditor();
    CreateStatsBar();
    CreateConsole();
    CreateDebugHud();

    SubscribeToEvent("ScreenMode", "ResizeUI");
    SubscribeToEvent("MenuSelected", "HandleMenuSelected");
    SubscribeToEvent("KeyDown", "HandleKeyDown");
    SubscribeToEvent("KeyUp", "UnfadeUI");
    SubscribeToEvent("MouseButtonUp", "UnfadeUI");
}

void ResizeUI()
{
    // Resize menu bar
    uiMenuBar.SetFixedWidth(graphics.width);

    // Relayout stats bar
    Font@ font = cache.GetResource("Font", "Fonts/Ubuntu-L.ttf");
    if (graphics.width >= 1200)
    {
        SetupStatsBarText(editorModeText, font, 0, 24, HA_LEFT, VA_TOP);
        SetupStatsBarText(renderStatsText, font, 0, 24, HA_RIGHT, VA_TOP);
    }
    else
    {
        SetupStatsBarText(editorModeText, font, 0, 24, HA_LEFT, VA_TOP);
        SetupStatsBarText(renderStatsText, font, 0, 36, HA_LEFT, VA_TOP);
    }

    // Relayout windows
    Array<UIElement@> children = ui.root.GetChildren();
    for (uint i = 0; i < children.length; ++i)
    {
        if (children[i].type == WINDOW_TYPE)
            AdjustPosition(children[i]);
    }

    // Relayout root UI element
    editorUIElement.SetSize(graphics.width, graphics.height);
}

void AdjustPosition(Window@ window)
{
    IntVector2 position = window.position;
    IntVector2 size = window.size;
    IntVector2 extend = position + size;
    if (extend.x > graphics.width)
        position.x = Max(20, graphics.width - size.x - 20);
    if (extend.y > graphics.height)
        position.y = Max(40, graphics.height - size.y - 20);
    window.position = position;
}

void CreateCursor()
{
    Cursor@ cursor = Cursor("Cursor");
    cursor.SetStyleAuto(uiStyle);
    cursor.SetPosition(graphics.width / 2, graphics.height / 2);
    ui.cursor = cursor;
    if (GetPlatform() == "Android" || GetPlatform() == "iOS")
        ui.cursor.visible = false;
}

// AngelScript does not support closures (yet), but funcdef should do just fine as a workaround for a few cases here for now
funcdef bool MENU_CALLBACK();
Array<MENU_CALLBACK@> menuCallbacks;

void HandleQuickSearchChange(StringHash eventType, VariantMap& eventData)
{
    LineEdit@ search = eventData["Element"].GetUIElement();
    if (search is null)
        return;

    PerformQuickMenuSearch(search.text.ToLower().Trimmed());
}

void PerformQuickMenuSearch(const String&in query)
{
    Menu@ menu = quickMenu.GetChild("ResultsMenu", true);
    if (menu is null)
        return;

    menu.RemoveAllChildren();
    uint limit = 0;

    if (query.length > 0)
    {
        int lastIndex = 0;
        uint score = 0;
        int index = 0;

        Array<QuickMenuItem@> filtered;
        {
            QuickMenuItem@ qi;
            for(uint x=0; x < quickMenuItems.length; x++)
            {
                @qi = quickMenuItems[x];
                int find = qi.action.Find(query, 0, false);
                if (find > -1)
                {
                    qi.sortScore = find;
                    filtered.Push(qi);
                }
            }
        }

        {
            QuickMenuItem@ a;
            QuickMenuItem@ b;
            for(uint x=0; x < filtered.length; x++)
            {
                for(uint y=0; y < filtered.length-1; y++)
                {
                    @a = filtered[y];
                    @b = filtered[y+1];
                    if(a.sortScore > b.sortScore)
                    {
                        @filtered[y+1] = a;
                        @filtered[y] = b;
                    }
                }
            }
        }

        {
            QuickMenuItem@ qi;
            limit = filtered.length > MAX_QUICK_MENU_ITEMS ? MAX_QUICK_MENU_ITEMS : filtered.length;
            for(uint x=0; x < limit; x++)
            {
                @qi = filtered[x];
                Menu@ item = CreateMenuItem(qi.action, qi.callback);
                item.SetMaxSize(1000,16);
                menu.AddChild(item);
            }
        }
    }

    menu.visible = limit > 0;
    menu.SetFixedHeight(limit * 16);
    quickMenu.SetFixedHeight(limit*16 + 62 + (menu.visible ? 6 : 0));
}

class QuickMenuItem
{
    String action;
    MENU_CALLBACK@ callback;
    uint sortScore = 0;
    QuickMenuItem(){}
    QuickMenuItem(String action, MENU_CALLBACK@ callback)
    {
        this.action = action;
        this.callback = callback;
    }
}

/// Create popup search menu.
void CreateQuickMenu()
{
    if (quickMenu !is null)
        return;

    quickMenu = ui.LoadLayout(cache.GetResource("XMLFile", "UI/EditorQuickMenu.xml"));
    quickMenu.enabled = false;
    quickMenu.visible = false;
    
    // Handle a dummy search in the quick menu to finalize its initial size to empty
    PerformQuickMenuSearch("");

    ui.root.AddChild(quickMenu);
    LineEdit@ search = quickMenu.GetChild("Search", true);
    SubscribeToEvent(search, "TextChanged", "HandleQuickSearchChange");
    UIElement@ closeButton = quickMenu.GetChild("CloseButton", true);
    SubscribeToEvent(closeButton, "Pressed", "ToggleQuickMenu");
}

void ToggleQuickMenu()
{
    quickMenu.enabled = !quickMenu.enabled && ui.cursor.visible;
    quickMenu.visible = quickMenu.enabled;
    if (quickMenu.enabled)
    {
        quickMenu.position = ui.cursorPosition - IntVector2(20,70);
        LineEdit@ search = quickMenu.GetChild("Search", true);
        search.text = "";
        search.focus = true;
    }
}

/// Create top menu bar.
void CreateMenuBar()
{
    uiMenuBar = BorderImage("MenuBar");
    ui.root.AddChild(uiMenuBar);
    uiMenuBar.enabled = true;
    uiMenuBar.style = "EditorMenuBar";
    uiMenuBar.SetLayout(LM_HORIZONTAL);
    uiMenuBar.opacity = uiMaxOpacity;
    uiMenuBar.SetFixedWidth(graphics.width);

    {
        Menu@ menu = CreateMenu("File");
        Window@ popup = menu.popup;
        popup.AddChild(CreateMenuItem("New scene", @ResetScene, 'N', QUAL_SHIFT | QUAL_CTRL));
        popup.AddChild(CreateMenuItem("Open scene...", @PickFile, 'O', QUAL_CTRL));
        popup.AddChild(CreateMenuItem("Save scene", @SaveSceneWithExistingName, 'S', QUAL_CTRL));
        popup.AddChild(CreateMenuItem("Save scene as...", @PickFile, 'S', QUAL_SHIFT | QUAL_CTRL));
        CreateChildDivider(popup);

        Menu@ childMenu = CreateMenuItem("Load node", null, SHOW_POPUP_INDICATOR);
        Window@ childPopup = CreatePopup(childMenu);
        childPopup.AddChild(CreateMenuItem("As replicated...", @PickFile, 0, 0, true, "Load node as replicated..."));
        childPopup.AddChild(CreateMenuItem("As local...", @PickFile, 0, 0, true, "Load node as local..."));
        popup.AddChild(childMenu);

        popup.AddChild(CreateMenuItem("Save node as...", @PickFile));
        CreateChildDivider(popup);
        popup.AddChild(CreateMenuItem("Import model...", @PickFile));
        popup.AddChild(CreateMenuItem("Import scene...", @PickFile));
        CreateChildDivider(popup);
        popup.AddChild(CreateMenuItem("Run script...", @PickFile));
        popup.AddChild(CreateMenuItem("Set resource path...", @PickFile));
        CreateChildDivider(popup);
        popup.AddChild(CreateMenuItem("Exit", @Exit));
        FinalizedPopupMenu(popup);
        uiMenuBar.AddChild(menu);
    }

    {
        Menu@ menu = CreateMenu("Edit");
        Window@ popup = menu.popup;
        popup.AddChild(CreateMenuItem("Undo", @Undo, 'Z', QUAL_CTRL));
        popup.AddChild(CreateMenuItem("Redo", @Redo, 'Y', QUAL_CTRL));
        CreateChildDivider(popup);
        popup.AddChild(CreateMenuItem("Cut", @Cut, 'X', QUAL_CTRL));
        popup.AddChild(CreateMenuItem("Copy", @Copy, 'C', QUAL_CTRL));
        popup.AddChild(CreateMenuItem("Paste", @Paste, 'V', QUAL_CTRL));
        popup.AddChild(CreateMenuItem("Delete", @Delete, KEY_DELETE, QUAL_ANY));
        popup.AddChild(CreateMenuItem("Select all", @SelectAll, 'A', QUAL_CTRL));
        CreateChildDivider(popup);
        popup.AddChild(CreateMenuItem("Reset to default", @ResetToDefault));
        CreateChildDivider(popup);
        popup.AddChild(CreateMenuItem("Reset position", @SceneResetPosition));
        popup.AddChild(CreateMenuItem("Reset rotation", @SceneResetRotation));
        popup.AddChild(CreateMenuItem("Reset scale", @SceneResetScale));
        popup.AddChild(CreateMenuItem("Enable/disable", @SceneToggleEnable, 'E', QUAL_CTRL));
        popup.AddChild(CreateMenuItem("Unparent", @SceneUnparent, 'U', QUAL_CTRL));
        CreateChildDivider(popup);
        popup.AddChild(CreateMenuItem("Toggle update", @ToggleSceneUpdate, 'P', QUAL_CTRL));
        popup.AddChild(CreateMenuItem("Stop test animation", @StopTestAnimation));
        CreateChildDivider(popup);
        popup.AddChild(CreateMenuItem("Rebuild navigation data", @SceneRebuildNavigation));
        FinalizedPopupMenu(popup);
        uiMenuBar.AddChild(menu);
    }

    {
        Menu@ menu = CreateMenu("Create");
        Window@ popup = menu.popup;
        popup.AddChild(CreateMenuItem("Replicated node", @PickNode, 0, 0, true, "Create Replicated node"));
        popup.AddChild(CreateMenuItem("Local node", @PickNode, 0, 0, true, "Create Local node"));
        CreateChildDivider(popup);

        Menu@ childMenu = CreateMenuItem("Component", null, SHOW_POPUP_INDICATOR);
        Window@ childPopup = CreatePopup(childMenu);
        String[] objectCategories = GetObjectCategories();
        for (uint i = 0; i < objectCategories.length; ++i)
        {
            // Skip the UI category for the component menus
            if (objectCategories[i] == "UI")
                continue;

            Menu@ menu = CreateMenuItem(objectCategories[i], null, SHOW_POPUP_INDICATOR);
            Window@ popup = CreatePopup(menu);
            String[] componentTypes = GetObjectsByCategory(objectCategories[i]);
            for (uint j = 0; j < componentTypes.length; ++j)
                popup.AddChild(CreateIconizedMenuItem(componentTypes[j], @PickComponent, 0, 0, "", true, "Create " + componentTypes[j]));
            childPopup.AddChild(menu);
        }
        FinalizedPopupMenu(childPopup);
        popup.AddChild(childMenu);

        childMenu = CreateMenuItem("Builtin object", null, SHOW_POPUP_INDICATOR);
        childPopup = CreatePopup(childMenu);
        String[] objects = { "Box", "Cone", "Cylinder", "Plane", "Pyramid", "Sphere" };
        for (uint i = 0; i < objects.length; ++i)
            childPopup.AddChild(CreateIconizedMenuItem(objects[i], @PickBuiltinObject, 0, 0, "Node", true, "Create " + objects[i]));
        popup.AddChild(childMenu);
        CreateChildDivider(popup);

        childMenu = CreateMenuItem("UI-element", null, SHOW_POPUP_INDICATOR);
        childPopup = CreatePopup(childMenu);
        String[] uiElementTypes = GetObjectsByCategory("UI");
        for (uint i = 0; i < uiElementTypes.length; ++i)
        {
            if (uiElementTypes[i] != "UIElement")
                childPopup.AddChild(CreateIconizedMenuItem(uiElementTypes[i], @PickUIElement, 0, 0, "", true, "Create " + uiElementTypes[i]));
        }
        CreateChildDivider(childPopup);
        childPopup.AddChild(CreateIconizedMenuItem("UIElement", @PickUIElement));
        popup.AddChild(childMenu);

        FinalizedPopupMenu(popup);
        uiMenuBar.AddChild(menu);
    }

    {
        Menu@ menu = CreateMenu("UI-layout");
        Window@ popup = menu.popup;
        popup.AddChild(CreateMenuItem("Open UI-layout...", @PickFile, 'O', QUAL_ALT));
        popup.AddChild(CreateMenuItem("Save UI-layout", @SaveUILayoutWithExistingName, 'S', QUAL_ALT));
        popup.AddChild(CreateMenuItem("Save UI-layout as...", @PickFile));
        CreateChildDivider(popup);
        popup.AddChild(CreateMenuItem("Close UI-layout", @CloseUILayout, 'C', QUAL_ALT));
        popup.AddChild(CreateMenuItem("Close all UI-layouts", @CloseAllUILayouts));
        CreateChildDivider(popup);
        popup.AddChild(CreateMenuItem("Load child element...", @PickFile));
        popup.AddChild(CreateMenuItem("Save child element as...", @PickFile));
        CreateChildDivider(popup);
        popup.AddChild(CreateMenuItem("Set default style...", @PickFile));
        FinalizedPopupMenu(popup);
        uiMenuBar.AddChild(menu);
    }

    {
        Menu@ menu = CreateMenu("View");
        Window@ popup = menu.popup;
        popup.AddChild(CreateMenuItem("Hierarchy", @ShowHierarchyWindow, 'H', QUAL_CTRL));
        popup.AddChild(CreateMenuItem("Attribute inspector", @ShowAttributeInspectorWindow, 'I', QUAL_CTRL));
        popup.AddChild(CreateMenuItem("Material editor", @ShowMaterialEditor));
        popup.AddChild(CreateMenuItem("Editor settings", @ShowEditorSettingsDialog));
        popup.AddChild(CreateMenuItem("Editor preferences", @ShowEditorPreferencesDialog));
        CreateChildDivider(popup);
        popup.AddChild(CreateMenuItem("Hide editor", @ToggleUI, KEY_F12, QUAL_ANY));
        FinalizedPopupMenu(popup);
        uiMenuBar.AddChild(menu);
    }

    BorderImage@ spacer = BorderImage("MenuBarSpacer");
    uiMenuBar.AddChild(spacer);
    spacer.style = "EditorMenuBar";

    BorderImage@ logo = BorderImage("Logo");
    logo.texture = cache.GetResource("Texture2D", "Textures/Logo.png");
    logo.SetFixedWidth(64);
    uiMenuBar.AddChild(logo);
}

bool Exit()
{
    engine.Exit();
    return true;
}

bool PickFile()
{
    Menu@ menu = GetEventSender();
    if (menu is null)
        return false;

    String action = menu.name;
    if (action.empty)
        return false;

    // File (Scene related)
    if (action == "Open scene...")
    {
        CreateFileSelector("Open scene", "Open", "Cancel", uiScenePath, uiSceneFilters, uiSceneFilter);
        SubscribeToEvent(uiFileSelector, "FileSelected", "HandleOpenSceneFile");
    }
    else if (action == "Save scene as..." || action == "Save scene")
    {
        CreateFileSelector("Save scene as", "Save", "Cancel", uiScenePath, uiSceneFilters, uiSceneFilter);
        uiFileSelector.fileName = GetFileNameAndExtension(editorScene.fileName);
        SubscribeToEvent(uiFileSelector, "FileSelected", "HandleSaveSceneFile");
    }
    else if (action == "As replicated..." || action == "Load node as replicated...")
    {
        instantiateMode = REPLICATED;
        CreateFileSelector("Load node", "Load", "Cancel", uiNodePath, uiSceneFilters, uiNodeFilter);
        SubscribeToEvent(uiFileSelector, "FileSelected", "HandleLoadNodeFile");
    }
    else if (action == "As local..." || action == "Load node as local...")
    {
        instantiateMode = LOCAL;
        CreateFileSelector("Load node", "Load", "Cancel", uiNodePath, uiSceneFilters, uiNodeFilter);
        SubscribeToEvent(uiFileSelector, "FileSelected", "HandleLoadNodeFile");
    }
    else if (action == "Save node as...")
    {
        if (editNode !is null && editNode !is editorScene)
        {
            CreateFileSelector("Save node", "Save", "Cancel", uiNodePath, uiSceneFilters, uiNodeFilter);
            uiFileSelector.fileName = GetFileNameAndExtension(instantiateFileName);
            SubscribeToEvent(uiFileSelector, "FileSelected", "HandleSaveNodeFile");
        }
    }
    else if (action == "Import model...")
    {
        CreateFileSelector("Import model", "Import", "Cancel", uiImportPath, uiAllFilters, uiImportFilter);
        SubscribeToEvent(uiFileSelector, "FileSelected", "HandleImportModel");
    }
    else if (action == "Import scene...")
    {
        CreateFileSelector("Import scene", "Import", "Cancel", uiImportPath, uiAllFilters, uiImportFilter);
        SubscribeToEvent(uiFileSelector, "FileSelected", "HandleImportScene");
    }
    else if (action == "Run script...")
    {
        CreateFileSelector("Run script", "Run", "Cancel", uiScriptPath, uiScriptFilters, uiScriptFilter);
        SubscribeToEvent(uiFileSelector, "FileSelected", "HandleRunScript");
    }
    else if (action == "Set resource path...")
    {
        CreateFileSelector("Set resource path", "Set", "Cancel", sceneResourcePath, uiAllFilters, 0);
        uiFileSelector.directoryMode = true;
        SubscribeToEvent(uiFileSelector, "FileSelected", "HandleResourcePath");
    }
    // UI-element
    else if (action == "Open UI-layout...")
    {
        CreateFileSelector("Open UI-layout", "Open", "Cancel", uiElementPath, uiElementFilters, uiElementFilter);
        SubscribeToEvent(uiFileSelector, "FileSelected", "HandleOpenUILayoutFile");
    }
    else if (action == "Save UI-layout as..." || action == "Save UI-layout")
    {
        if (editUIElement !is null)
        {
            UIElement@ element = GetTopLevelUIElement(editUIElement);
            if (element is null)
                return false;

            CreateFileSelector("Save UI-layout as", "Save", "Cancel", uiElementPath, uiElementFilters, uiElementFilter);
            uiFileSelector.fileName = GetFileNameAndExtension(element.GetVar(FILENAME_VAR).GetString());
            SubscribeToEvent(uiFileSelector, "FileSelected", "HandleSaveUILayoutFile");
        }
    }
    else if (action == "Load child element...")
    {
        if (editUIElement !is null)
        {
            CreateFileSelector("Load child element", "Load", "Cancel", uiElementPath, uiElementFilters, uiElementFilter);
            SubscribeToEvent(uiFileSelector, "FileSelected", "HandleLoadChildUIElementFile");
        }
    }
    else if (action == "Save child element as...")
    {
        if (editUIElement !is null)
        {
            CreateFileSelector("Save child element", "Save", "Cancel", uiElementPath, uiElementFilters, uiElementFilter);
            uiFileSelector.fileName = GetFileNameAndExtension(editUIElement.GetVar(CHILD_ELEMENT_FILENAME_VAR).GetString());
            SubscribeToEvent(uiFileSelector, "FileSelected", "HandleSaveChildUIElementFile");
        }
    }
    else if (action == "Set default style...")
    {
        CreateFileSelector("Set default style", "Set", "Cancel", uiElementPath, uiElementFilters, uiElementFilter);
        SubscribeToEvent(uiFileSelector, "FileSelected", "HandleUIElementDefaultStyle");
    }

    return true;
}

bool PickNode()
{
    Menu@ menu = GetEventSender();
    if (menu is null)
        return false;

    String action = GetActionName(menu.name);
    if (action.empty)
        return false;

    CreateNode(action == "Replicated node" ? REPLICATED : LOCAL);
    return true;
}

bool PickComponent()
{
    if (editNodes.empty)
        return false;

    Menu@ menu = GetEventSender();
    if (menu is null)
        return false;

    String action = GetActionName(menu.name);
    if (action.empty)
        return false;

    CreateComponent(action);
    return true;
}

bool PickBuiltinObject()
{
    Menu@ menu = GetEventSender();
    if (menu is null)
        return false;

    String action = GetActionName(menu.name);
    if (action.empty)
        return false;

    CreateBuiltinObject(action);
    return true;
}

bool PickUIElement()
{
    Menu@ menu = GetEventSender();
    if (menu is null)
        return false;

    String action = GetActionName(menu.name);
    if (action.empty)
        return false;

    return NewUIElement(action);
}

// When calling items from the quick menu, they have "Create" prepended for clarity. Strip that now to get the object name to create
String GetActionName(const String&in name)
{
    if (name.StartsWith("Create"))
        return name.Substring(7);
    else
        return name;
}

void HandleMenuSelected(StringHash eventType, VariantMap& eventData)
{
    Menu@ menu = eventData["Element"].GetUIElement();
    if (menu is null)
        return;

    HandlePopup(menu);

    quickMenu.visible = false;
    quickMenu.enabled = false;

    // Execute the callback if available
    Variant variant = menu.GetVar(CALLBACK_VAR);
    if (!variant.empty)
        menuCallbacks[variant.GetUInt()]();
}

Menu@ CreateMenuItem(const String&in title, MENU_CALLBACK@ callback = null, int accelKey = 0, int accelQual = 0, bool addToQuickMenu = true, String quickMenuText="")
{
    Menu@ menu = Menu(title);
    menu.defaultStyle = uiStyle;
    menu.style = AUTO_STYLE;
    menu.SetLayout(LM_HORIZONTAL, 0, IntRect(8, 2, 8, 2));
    if (accelKey > 0)
        menu.SetAccelerator(accelKey, accelQual);
    if (callback !is null)
    {
        menu.vars[CALLBACK_VAR] = menuCallbacks.length;
        menuCallbacks.Push(callback);
    }

    Text@ menuText = Text();
    menu.AddChild(menuText);
    menuText.style = "EditorMenuText";
    menuText.text = title;

    if (addToQuickMenu)
        AddQuickMenuItem(callback, quickMenuText.empty ? title : quickMenuText);

    if (accelKey != 0)
    {
        UIElement@ spacer = UIElement();
        spacer.minWidth = menuText.indentSpacing;
        spacer.height = menuText.height;
        menu.AddChild(spacer);
        menu.AddChild(CreateAccelKeyText(accelKey, accelQual));
    }

    return menu;
}

void AddQuickMenuItem(MENU_CALLBACK@ callback, String text)
{
    if (callback is null)
        return;

    bool exists = false;
    for (uint i=0;i<quickMenuItems.length;i++)
    {
        if (quickMenuItems[i].action == text)
        {
            exists = true;
            break;
        }
    }
    if (!exists)
        quickMenuItems.Push(QuickMenuItem(text, callback));
}

Menu@ CreateIconizedMenuItem(const String&in title, MENU_CALLBACK@ callback = null, int accelKey = 0, int accelQual = 0, const String&in iconType = "", bool addToQuickMenu=true, String quickMenuText="")
{
    Menu@ menu = Menu(title);
    menu.defaultStyle = uiStyle;
    menu.style = AUTO_STYLE;
    menu.SetLayout(LM_VERTICAL, 0, IntRect(8, 2, 8, 2));
    if (accelKey > 0)
        menu.SetAccelerator(accelKey, accelQual);
    if (callback !is null)
    {
        menu.vars[CALLBACK_VAR] = menuCallbacks.length;
        menuCallbacks.Push(callback);
    }

    Text@ menuText = Text();
    menu.AddChild(menuText);
    menuText.style = "EditorMenuText";
    menuText.text = title;
    // If icon type is not provided, use the title instead
    IconizeUIElement(menuText, iconType.empty ? title : iconType);

    if (addToQuickMenu)
        AddQuickMenuItem(callback, quickMenuText.empty ? title : quickMenuText);

    if (accelKey != 0)
    {
        menuText.layoutMode = LM_HORIZONTAL;
        menuText.AddChild(CreateAccelKeyText(accelKey, accelQual));
    }

    return menu;
}

/// Create a child divider in parent with vertical layout mode. It works on other parent element as well, not just parent menu.
void CreateChildDivider(UIElement@ parent)
{
    BorderImage@ divider = parent.CreateChild("BorderImage", "Divider");
    divider.style = "EditorDivider";
}

Window@ CreatePopup(Menu@ baseMenu)
{
    Window@ popup = Window();
    popup.defaultStyle = uiStyle;
    popup.style = AUTO_STYLE;
    popup.SetLayout(LM_VERTICAL, 1, IntRect(2, 6, 2, 6));
    baseMenu.popup = popup;
    baseMenu.popupOffset = IntVector2(0, baseMenu.height);

    return popup;
}

Menu@ CreateMenu(const String&in title)
{
    Menu@ menu = CreateMenuItem(title);
    menu.SetFixedWidth(menu.width);
    CreatePopup(menu);

    return menu;
}

Text@ CreateAccelKeyText(int accelKey, int accelQual)
{
    Text@ accelKeyText = Text();
    accelKeyText.defaultStyle = uiStyle;
    accelKeyText.style = "EditorMenuText";
    accelKeyText.textAlignment = HA_RIGHT;

    String text;
    if (accelKey == KEY_DELETE)
        text = "Del";
    else if (accelKey == KEY_SPACE)
        text = "Space";
    // Cannot use range as the key constants below do not appear to be in sequence
    else if (accelKey == KEY_F1)
        text = "F1";
    else if (accelKey == KEY_F2)
        text = "F2";
    else if (accelKey == KEY_F3)
        text = "F3";
    else if (accelKey == KEY_F4)
        text = "F4";
    else if (accelKey == KEY_F5)
        text = "F5";
    else if (accelKey == KEY_F6)
        text = "F6";
    else if (accelKey == KEY_F7)
        text = "F7";
    else if (accelKey == KEY_F8)
        text = "F8";
    else if (accelKey == KEY_F9)
        text = "F9";
    else if (accelKey == KEY_F10)
        text = "F10";
    else if (accelKey == KEY_F11)
        text = "F11";
    else if (accelKey == KEY_F12)
        text = "F12";
    else if (accelKey == SHOW_POPUP_INDICATOR)
        text = ">";
    else
        text.AppendUTF8(accelKey);
    if (accelQual & QUAL_ALT > 0)
        text = "Alt+" + text;
    if (accelQual & QUAL_SHIFT > 0)
        text = "Shift+" + text;
    if (accelQual & QUAL_CTRL > 0)
        text = "Ctrl+" + text;
    accelKeyText.text = text;

    return accelKeyText;
}

void FinalizedPopupMenu(Window@ popup)
{
    // Find the maximum menu text width
    Array<UIElement@> children = popup.GetChildren();
    int maxWidth = 0;
    for (uint i = 0; i < children.length; ++i)
    {
        UIElement@ element = children[i];
        if (element.type != MENU_TYPE)    // Skip if not menu item
            continue;

        int width = element.children[0].width;
        if (width > maxWidth)
            maxWidth = width;
    }

    // Adjust the indent spacing to slightly wider than the maximum width
    maxWidth += 20;
    for (uint i = 0; i < children.length; ++i)
    {
        UIElement@ element = children[i];
        if (element.type != MENU_TYPE)
            continue;
        Menu@ menu = element;

        Text@ menuText = menu.children[0];
        if (menuText.numChildren == 1)    // Skip if menu text does not have accel
            menuText.children[0].indentSpacing = maxWidth;

        // Adjust the popup offset taking the indentation into effect
        if (menu.popup !is null)
            menu.popupOffset = IntVector2(menu.width, 0);
    }
}

void CreateFileSelector(const String&in title, const String&in ok, const String&in cancel, const String&in initialPath, Array<String>@ filters,
    uint initialFilter)
{
    // Within the editor UI, the file selector is a kind of a "singleton". When the previous one is overwritten, also
    // the events subscribed from it are disconnected, so new ones are safe to subscribe.
    uiFileSelector = FileSelector();
    uiFileSelector.defaultStyle = uiStyle;
    uiFileSelector.title = title;
    uiFileSelector.path = initialPath;
    uiFileSelector.SetButtonTexts(ok, cancel);
    uiFileSelector.SetFilters(filters, initialFilter);
    uiFileSelector.window.priority = 1000;    // Ensure when it is visible then it has the highest priority (in front of all others UI)
    CenterDialog(uiFileSelector.window);
}

void CloseFileSelector(uint&out filterIndex, String&out path)
{
    // Save filter & path for next time
    filterIndex = uiFileSelector.filterIndex;
    path = uiFileSelector.path;

    uiFileSelector = null;
}

void CloseFileSelector()
{
    uiFileSelector = null;
}

void CreateConsole()
{
    Console@ console = engine.CreateConsole();
    console.defaultStyle = uiStyle;
    console.numRows = 16;
}

void CreateDebugHud()
{
    engine.CreateDebugHud();
    debugHud.defaultStyle = uiStyle;
    debugHud.mode = DEBUGHUD_SHOW_NONE;
}

void CenterDialog(UIElement@ element)
{
    IntVector2 size = element.size;
    element.SetPosition((graphics.width - size.x) / 2, (graphics.height - size.y) / 2);
}

void UpdateWindowTitle()
{
    String sceneName = GetFileNameAndExtension(editorScene.fileName);
    if (sceneName.empty || sceneName == TEMP_SCENE_NAME)
        sceneName = "Untitled";
    if (sceneModified)
        sceneName += "*";
    graphics.windowTitle = "Urho3D editor - " + sceneName;
}

void HandlePopup(Menu@ menu)
{
    // Close the top level menu now unless the selected menu item has another popup
    if (menu.popup !is null)
        return;

    for (;;)
    {
        UIElement@ menuParent = menu.parent;
        if (menuParent is null)
            break;

        Menu@ nextMenu = menuParent.vars["Origin"].GetUIElement();
        if (nextMenu is null)
            break;
        else
            menu = nextMenu;
    }

    if (menu.parent is uiMenuBar)
        menu.showPopup = false;
}

String ExtractFileName(VariantMap& eventData, bool forSave = false)
{
    String fileName;

    // Check for OK
    if (eventData["OK"].GetBool())
    {
        String filter = eventData["Filter"].GetString();
        fileName = eventData["FileName"].GetString();
        // Add default extension for saving if not specified
        if (GetExtension(fileName).empty && forSave && filter != "*.*")
            fileName = fileName + filter.Substring(1);
    }
    return fileName;
}

void HandleOpenSceneFile(StringHash eventType, VariantMap& eventData)
{
    CloseFileSelector(uiSceneFilter, uiScenePath);
    LoadScene(ExtractFileName(eventData));
}

void HandleSaveSceneFile(StringHash eventType, VariantMap& eventData)
{
    CloseFileSelector(uiSceneFilter, uiScenePath);
    SaveScene(ExtractFileName(eventData, true));
}

void HandleLoadNodeFile(StringHash eventType, VariantMap& eventData)
{
    CloseFileSelector(uiNodeFilter, uiNodePath);
    LoadNode(ExtractFileName(eventData));
}

void HandleSaveNodeFile(StringHash eventType, VariantMap& eventData)
{
    CloseFileSelector(uiNodeFilter, uiNodePath);
    SaveNode(ExtractFileName(eventData, true));
}

void HandleImportModel(StringHash eventType, VariantMap& eventData)
{
    CloseFileSelector(uiImportFilter, uiImportPath);
    ImportModel(ExtractFileName(eventData));
}

void HandleImportScene(StringHash eventType, VariantMap& eventData)
{
    CloseFileSelector(uiImportFilter, uiImportPath);
    ImportScene(ExtractFileName(eventData));
}

void ExecuteScript(const String&in fileName)
{
    if (fileName.empty)
        return;

    File@ file = File(fileName, FILE_READ);
    if (file.open)
    {
        String scriptCode;
        while (!file.eof)
            scriptCode += file.ReadLine() + "\n";
        file.Close();

        if (script.Execute(scriptCode))
            log.Info("Script " + fileName + " ran successfully");
    }
}

void HandleRunScript(StringHash eventType, VariantMap& eventData)
{
    CloseFileSelector(uiScriptFilter, uiScriptPath);
    ExecuteScript(ExtractFileName(eventData));
}

void HandleResourcePath(StringHash eventType, VariantMap& eventData)
{
    CloseFileSelector();
    SetResourcePath(ExtractFileName(eventData), false);
}

void HandleOpenUILayoutFile(StringHash eventType, VariantMap& eventData)
{
    CloseFileSelector(uiElementFilter, uiElementPath);
    OpenUILayout(ExtractFileName(eventData));
}

void HandleSaveUILayoutFile(StringHash eventType, VariantMap& eventData)
{
    CloseFileSelector(uiElementFilter, uiElementPath);
    SaveUILayout(ExtractFileName(eventData, true));
}

void HandleLoadChildUIElementFile(StringHash eventType, VariantMap& eventData)
{
    CloseFileSelector(uiElementFilter, uiElementPath);
    LoadChildUIElement(ExtractFileName(eventData));
}

void HandleSaveChildUIElementFile(StringHash eventType, VariantMap& eventData)
{
    CloseFileSelector(uiElementFilter, uiElementPath);
    SaveChildUIElement(ExtractFileName(eventData, true));
}

void HandleUIElementDefaultStyle(StringHash eventType, VariantMap& eventData)
{
    CloseFileSelector(uiElementFilter, uiElementPath);
    SetUIElementDefaultStyle(ExtractFileName(eventData));
}

void HandleKeyDown(StringHash eventType, VariantMap& eventData)
{
    int key = eventData["Key"].GetInt();

    if (key == KEY_ESC)
    {
        if (uiHidden)
            UnhideUI();
        else if (console.visible)
            console.visible = false;
        else if (quickMenu.visible)
        {
            quickMenu.visible = false;
            quickMenu.enabled = false;
        }
        else
        {
            UIElement@ front = ui.frontElement;
            if (front is settingsDialog || front is preferencesDialog)
            {
                ui.focusElement = null;
                front.visible = false;
            }
        }
    }

    // Ignore other keys when UI has a modal element
    else if (ui.HasModalElement())
        return;

    else if (key == KEY_F1)
        console.Toggle();
    else if (key == KEY_F2)
        ToggleRenderingDebug();
    else if (key == KEY_F3)
        TogglePhysicsDebug();
    else if (key == KEY_F4)
        ToggleOctreeDebug();

    else if (eventData["Qualifiers"].GetInt() == QUAL_CTRL)
    {
        if (key == '1')
            editMode = EDIT_MOVE;
        else if (key == '2')
            editMode = EDIT_ROTATE;
        else if (key == '3')
            editMode = EDIT_SCALE;
        else if (key == '4')
            editMode = EDIT_SELECT;
        else if (key == '5')
            axisMode = AxisMode(axisMode ^ AXIS_LOCAL);
        else if (key == '6')
        {
            --pickMode;
            if (pickMode < PICK_GEOMETRIES)
                pickMode = MAX_PICK_MODES - 1;
        }
        else if (key == '7')
        {
            ++pickMode;
            if (pickMode >= MAX_PICK_MODES)
                pickMode = PICK_GEOMETRIES;
        }
        else if (key == 'W')
        {
            fillMode = FillMode(fillMode + 1);
            if (fillMode > FILL_POINT)
                fillMode = FILL_SOLID;

            // Update camera fill mode
            camera.fillMode = fillMode;
        }
        else if (key == KEY_SPACE)
        {
            if (ui.cursor.visible)
                ToggleQuickMenu();
        }
        else
            SteppedObjectManipulation(key);
    }
}

void UnfadeUI()
{
    FadeUI(false);
}

void FadeUI(bool fade = true)
{
    if (uiHidden || uiFaded == fade)
        return;

    float opacity = (uiFaded = fade) ? uiMinOpacity : uiMaxOpacity;
    Array<UIElement@> children = ui.root.GetChildren();
    for (uint i = 0; i < children.length; ++i)
    {
        // Texts, popup&modal windows (which are anyway only in ui.modalRoot), and editorUIElement are excluded
        if (children[i].type != TEXT_TYPE && children[i] !is editorUIElement)
            children[i].opacity = opacity;
    }
}

bool ToggleUI()
{
    HideUI(!uiHidden);
    return true;
}

void UnhideUI()
{
    HideUI(false);
}

void HideUI(bool hide = true)
{
    if (uiHidden == hide)
        return;

    bool visible = !(uiHidden = hide);
    Array<UIElement@> children = ui.root.GetChildren();
    for (uint i = 0; i < children.length; ++i)
    {
        // Cursor and editorUIElement are excluded
        if (children[i].type != CURSOR_TYPE && children[i] !is editorUIElement)
        {
            if (visible)
            {
                if (!children[i].visible)
                    children[i].visible = children[i].vars["HideUI"].GetBool();
            }
            else
            {
                children[i].vars["HideUI"] = children[i].visible;
                children[i].visible = false;
            }
        }
    }
}

void IconizeUIElement(UIElement@ element, const String&in iconType)
{
    // Check if the icon has been created before
    BorderImage@ icon = element.GetChild("Icon");

    // If iconType is empty, it is a request to remove the existing icon
    if (iconType.empty)
    {
        // Remove the icon if it exists
        if (icon !is null)
            icon.Remove();

        // Revert back the indent but only if it is indented by this function
        if (element.vars[INDENT_MODIFIED_BY_ICON_VAR].GetBool())
            element.indent = 0;

        return;
    }

    // The UI element must itself has been indented to reserve the space for the icon
    if (element.indent == 0)
    {
        element.indent = 1;
        element.vars[INDENT_MODIFIED_BY_ICON_VAR] = true;
    }

    // If no icon yet then create one with the correct indent and size in respect to the UI element
    if (icon is null)
    {
        // The icon is placed at one indent level less than the UI element
        icon = BorderImage("Icon");
        icon.indent = element.indent - 1;
        icon.SetFixedSize(element.indentWidth - 2, 14);
        element.InsertChild(0, icon);   // Ensure icon is added as the first child
    }

    // Set the icon type
    if (!icon.SetStyle(iconType, iconStyle))
        icon.SetStyle("Unknown", iconStyle);    // If fails then use an 'unknown' icon type
    icon.color = Color(1,1,1,1); // Reset to enabled color
}

void SetIconEnabledColor(UIElement@ element, bool enabled, bool partial = false)
{
    BorderImage@ icon = element.GetChild("Icon");
    if (icon !is null)
    {
        if (partial)
        {
            icon.colors[C_TOPLEFT] = Color(1,1,1,1);
            icon.colors[C_BOTTOMLEFT] = Color(1,1,1,1);
            icon.colors[C_TOPRIGHT] = Color(1,0,0,1);
            icon.colors[C_BOTTOMRIGHT] = Color(1,0,0,1);
        }
        else
            icon.color = enabled ? Color(1,1,1,1) : Color(1,0,0,1);
    }
}

void UpdateDirtyUI()
{
    // Perform hierarchy selection latently after the new selections are finalized (used in undo/redo action)
    if (!hierarchyUpdateSelections.empty)
    {
        hierarchyList.SetSelections(hierarchyUpdateSelections);
        hierarchyUpdateSelections.Clear();
    }

    // Perform some event-triggered updates latently in case a large hierarchy was changed
    if (attributesFullDirty || attributesDirty)
        UpdateAttributeInspector(attributesFullDirty);
}
