<commit_before>// Attribute editor
//
// Functions that you must implement:
// - void SetAttributeEditorID(UIElement@ attrEdit, Array<Serializable@>@ serializables);
// - void PostEditAttribute(Array<Serializable@>@ serializables, uint index);
// - Array<Serializable@>@ GetAttributeEditorTargets(UIElement@ attrEdit);

const uint MIN_NODE_ATTRIBUTES = 4;
const uint MAX_NODE_ATTRIBUTES = 8;
const int ATTRNAME_WIDTH = 150;
const int ATTR_HEIGHT = 19;
bool inLoadAttributeEditor = false;

bool showNonEditableAttribute = false;

const ShortStringHash textType("Text");
const ShortStringHash containerType("UIElement");
const StringHash textChangedEventType("TextChanged");

Color normalTextColor(1.0f, 1.0f, 1.0f);
Color modifiedTextColor(1.0f, 0.8f, 0.5f);
Color nonEditableTextColor(0.7f, 0.7f, 0.7f);

String sceneResourcePath;

UIElement@ SetEditable(UIElement@ element, bool editable)
{
    element.active = editable;
    element.colors[C_TOPLEFT] = editable ? element.colors[C_BOTTOMRIGHT] : nonEditableTextColor;
    element.colors[C_BOTTOMLEFT] = element.colors[C_TOPLEFT];
    element.colors[C_TOPRIGHT] = element.colors[C_TOPLEFT];
    return element;
}

UIElement@ SetValue(LineEdit@ element, const String&in value, bool sameValue)
{
    element.text = sameValue ? value : STRIKED_OUT;
    element.cursorPosition = 0;
    return element;
}

UIElement@ SetValue(CheckBox@ element, bool value, bool sameValue)
{
    element.checked = sameValue ? value : false;
    return element;
}

UIElement@ SetValue(DropDownList@ element, int value, bool sameValue)
{
    element.selection = sameValue ? value : M_MAX_UNSIGNED;
    return element;
}

UIElement@ CreateAttributeEditorParentWithSeparatedLabel(ListView@ list, const String&in name, uint index, uint subIndex, bool suppressedSeparatedLabel = false)
{
    UIElement@ editorParent = UIElement("Edit" + String(index) + "_" + String(subIndex));
    editorParent.vars["Index"] = index;
    editorParent.vars["SubIndex"] = subIndex;
    editorParent.SetLayout(LM_VERTICAL, 2);
    list.AddItem(editorParent);

    if (suppressedSeparatedLabel)
    {
        UIElement@ placeHolder = UIElement();
        editorParent.AddChild(placeHolder);
    }
    else
    {
        Text@ attrNameText = Text();
        attrNameText.SetStyle(uiStyle, "EditorAttributeText");
        attrNameText.text = name;
        editorParent.AddChild(attrNameText);
    }

    return editorParent;
}

UIElement@ CreateAttributeEditorParent(ListView@ list, const String&in name, uint index, uint subIndex)
{
    UIElement@ editorParent = UIElement("Edit" + String(index) + "_" + String(subIndex));
    editorParent.vars["Index"] = index;
    editorParent.vars["SubIndex"] = subIndex;
    editorParent.SetLayout(LM_HORIZONTAL);
    editorParent.SetFixedHeight(ATTR_HEIGHT);
    list.AddItem(editorParent);

    Text@ attrNameText = Text();
    attrNameText.SetStyle(uiStyle, "EditorAttributeText");
    attrNameText.text = name;
    attrNameText.SetFixedWidth(ATTRNAME_WIDTH);
    editorParent.AddChild(attrNameText);

    return editorParent;
}

LineEdit@ CreateAttributeLineEdit(UIElement@ parent, Array<Serializable@>@ serializables, uint index, uint subIndex)
{
    LineEdit@ attrEdit = LineEdit();
    attrEdit.SetStyle(uiStyle, "EditorAttributeEdit");
    attrEdit.SetFixedHeight(ATTR_HEIGHT - 2);
    attrEdit.vars["Index"] = index;
    attrEdit.vars["SubIndex"] = subIndex;
    SetAttributeEditorID(attrEdit, serializables);
    parent.AddChild(attrEdit);
    return attrEdit;
}

void CreateAttributeEditor(ListView@ list, Array<Serializable@>@ serializables, uint index)
{
    AttributeInfo info = serializables[0].attributeInfos[index];
    CreateAttributeEditor(list, serializables, info.name, info.type, info.enumNames, index, 0);
}

UIElement@ CreateStringAttributeEditor(ListView@ list, Array<Serializable@>@ serializables, const String&in name, VariantType type, Array<String>@ enumNames, uint index, uint subIndex)
{
    UIElement@ parent = CreateAttributeEditorParent(list, name, index, subIndex);
    LineEdit@ attrEdit = CreateAttributeLineEdit(parent, serializables, index, subIndex);
    attrEdit.dragDropMode = DD_TARGET;
    SubscribeToEvent(attrEdit, "TextChanged", "EditAttribute");
    SubscribeToEvent(attrEdit, "TextFinished", "EditAttribute");
    return parent;
}

UIElement@ CreateBoolAttributeEditor(ListView@ list, Array<Serializable@>@ serializables, const String&in name, VariantType type, Array<String>@ enumNames, uint index, uint subIndex)
{
    UIElement@ parent = CreateAttributeEditorParent(list, name, index, subIndex);
    CheckBox@ attrEdit = CheckBox();
    attrEdit.style = uiStyle;
    attrEdit.SetFixedSize(16, 16);
    attrEdit.vars["Index"] = index;
    attrEdit.vars["SubIndex"] = subIndex;
    SetAttributeEditorID(attrEdit, serializables);
    parent.AddChild(attrEdit);
    SubscribeToEvent(attrEdit, "Toggled", "EditAttribute");
    return parent;
}

UIElement@ CreateNumAttributeEditor(ListView@ list, Array<Serializable@>@ serializables, const String&in name, VariantType type, Array<String>@ enumNames, uint index, uint subIndex)
{
    UIElement@ parent = CreateAttributeEditorParent(list, name, index, subIndex);
    uint numCoords = type - VAR_FLOAT + 1;
    if (type == VAR_QUATERNION)
        numCoords = 3;
    else if (type == VAR_COLOR || type == VAR_INTRECT)
        numCoords = 4;
    else if (type == VAR_INTVECTOR2)
        numCoords = 2;
    
    for (uint i = 0; i < numCoords; ++i)
    {
        LineEdit@ attrEdit = CreateAttributeLineEdit(parent, serializables, index, subIndex);
        SubscribeToEvent(attrEdit, "TextChanged", "EditAttribute");
        SubscribeToEvent(attrEdit, "TextFinished", "EditAttribute");
    }     
    return parent;    
}

UIElement@ CreateIntAttributeEditor(ListView@ list, Array<Serializable@>@ serializables, const String&in name, VariantType type, Array<String>@ enumNames, uint index, uint subIndex)
{
    UIElement@ parent = CreateAttributeEditorParent(list, name, index, subIndex);
    // Check for enums
    if (enumNames is null || enumNames.empty)
    {
        // No enums, create a numeric editor
        LineEdit@ attrEdit = CreateAttributeLineEdit(parent, serializables, index, subIndex);
        SubscribeToEvent(attrEdit, "TextChanged", "EditAttribute");
        SubscribeToEvent(attrEdit, "TextFinished", "EditAttribute");
    }
    else
    {
        DropDownList@ attrEdit = DropDownList();
        attrEdit.style = uiStyle;
        attrEdit.SetFixedHeight(ATTR_HEIGHT - 2);
        attrEdit.resizePopup = true;
        attrEdit.vars["Index"] = index;
        attrEdit.vars["SubIndex"] = subIndex;
        attrEdit.SetLayout(LM_HORIZONTAL, 0, IntRect(4, 1, 4, 1));
        SetAttributeEditorID(attrEdit, serializables);

        for (uint i = 0; i < enumNames.length; ++i)
        {
            Text@ choice = Text();
            choice.SetStyle(uiStyle, "EditorEnumAttributeText");
            choice.text = enumNames[i];
            attrEdit.AddItem(choice);
        }
        parent.AddChild(attrEdit);
        SubscribeToEvent(attrEdit, "ItemSelected", "EditAttribute");
    }

    return parent;
}

UIElement@ CreateResourceRefAttributeEditor(ListView@ list, Array<Serializable@>@ serializables, const String&in name, VariantType type, Array<String>@ enumNames, uint index, uint subIndex, bool suppressedSeparatedLabel = false)
{
    UIElement@ parent;
    ShortStringHash resourceType;
    VariantType attrType = serializables[0].attributeInfos[index].type;
    if (attrType == VAR_RESOURCEREF)
        resourceType = serializables[0].attributes[index].GetResourceRef().type;
    else if (attrType == VAR_RESOURCEREFLIST)
        resourceType = serializables[0].attributes[index].GetResourceRefList().type;
    else if (attrType == VAR_VARIANTVECTOR)
        resourceType = serializables[0].attributes[index].GetVariantVector()[subIndex].GetResourceRef().type;

     // Create the attribute name on a separate non-interactive line to allow for more space
    parent = CreateAttributeEditorParentWithSeparatedLabel(list, name, index, subIndex, suppressedSeparatedLabel);

    UIElement@ container = UIElement();
    container.SetLayout(LM_HORIZONTAL, 4, IntRect(name.StartsWith("   ") ? 20 : 10, 0, 4, 0));    // Left margin is indented more when the name is so
    container.SetFixedHeight(ATTR_HEIGHT);
    parent.AddChild(container);
        
    LineEdit@ attrEdit = CreateAttributeLineEdit(container, serializables, index, subIndex);
    attrEdit.vars["Type"] = resourceType.value;
    SubscribeToEvent(attrEdit, "TextFinished", "EditAttribute");

    Button@ pickButton = Button();
    pickButton.style = uiStyle;
    pickButton.SetFixedSize(36, ATTR_HEIGHT - 2);
    pickButton.vars["Index"] = index;
    pickButton.vars["SubIndex"] = subIndex;
    SetAttributeEditorID(pickButton, serializables);

    Text@ pickButtonText = Text();
    pickButtonText.SetStyle(uiStyle, "EditorAttributeText");
    pickButtonText.SetAlignment(HA_CENTER, VA_CENTER);
    pickButtonText.text = "Pick";
    pickButton.AddChild(pickButtonText);
    container.AddChild(pickButton);
    SubscribeToEvent(pickButton, "Released", "PickResource");

    Button@ openButton = Button();
    openButton.style = uiStyle;
    openButton.SetFixedSize(36, ATTR_HEIGHT - 2);
    openButton.vars["Index"] = index;
    openButton.vars["SubIndex"] = subIndex;
    SetAttributeEditorID(openButton, serializables);

    Text@ openButtonText = Text();
    openButtonText.SetStyle(uiStyle, "EditorAttributeText");
    openButtonText.SetAlignment(HA_CENTER, VA_CENTER);
    openButtonText.text = "Open";
    openButton.AddChild(openButtonText);
    container.AddChild(openButton);
    SubscribeToEvent(openButton, "Released", "OpenResource");
    return parent;
}

UIElement@ CreateAttributeEditor(ListView@ list, Array<Serializable@>@ serializables, const String&in name, VariantType type, Array<String>@ enumNames, uint index, uint subIndex, bool suppressedSeparatedLabel = false)
{
    UIElement@ parent;

    AttributeInfo info = serializables[0].attributeInfos[index];
    if (type == VAR_STRING || type == VAR_BUFFER)
        parent = CreateStringAttributeEditor(list, serializables, name, type, enumNames, index, subIndex);
    else if (type == VAR_BOOL)
        parent = CreateBoolAttributeEditor(list, serializables, name, type, enumNames, index, subIndex);
    else if ((type >= VAR_FLOAT && type <= VAR_VECTOR4) || type == VAR_QUATERNION || type == VAR_COLOR || type == VAR_INTVECTOR2 || type == VAR_INTRECT)
        parent = CreateNumAttributeEditor(list, serializables, name, type, enumNames, index, subIndex);
    else if (type == VAR_INT)
        parent = CreateIntAttributeEditor(list, serializables, name, type, enumNames, index, subIndex);
    else if (type == VAR_RESOURCEREF)
        parent = CreateResourceRefAttributeEditor(list, serializables, name, type, enumNames, index, subIndex, suppressedSeparatedLabel);
    else if (type == VAR_RESOURCEREFLIST)
    {
        uint numRefs = serializables[0].attributes[index].GetResourceRefList().length;
        for (uint i = 0; i < numRefs; ++i)
            CreateAttributeEditor(list, serializables, name, VAR_RESOURCEREF, null, index, i, i > 0);
    }
    else if (type == VAR_VARIANTVECTOR)
    {
        VectorStruct@ vectorStruct = GetVectorStruct(serializables, index);
        if (vectorStruct is null)
            return null;
        uint nameIndex = 0;

        Array<Variant>@ vector = serializables[0].attributes[index].GetVariantVector();
        for (uint i = 0; i < vector.length; ++i)
        {
            CreateAttributeEditor(list, serializables, vectorStruct.variableNames[nameIndex], vector[i].type, null, index, i);
            ++nameIndex;
            if (nameIndex >= vectorStruct.variableNames.length)
                nameIndex = vectorStruct.restartIndex;
        }
    }
    else if (type == VAR_VARIANTMAP)
    {
        VariantMap map = serializables[0].attributes[index].GetVariantMap();
        Array<ShortStringHash>@ keys = map.keys;
        for (uint i = 0; i < keys.length; ++i)
        {
            Variant value = map[keys[i]];
            parent = CreateAttributeEditor(list, serializables, scene.GetVarName(keys[i]) + " (Var)", value.type, null, index, i);
            // Add the variant key to the parent
            parent.vars["Key"] = keys[i].value;
        }
    }

    return parent;
}

uint GetAttributeEditorCount(Array<Serializable@>@ serializables)
{
    uint count = 0;

    if (!serializables.empty)
    {
        /// \todo When multi-editing, this only counts the editor count of the first serializable
        for (uint i = 0; i < serializables[0].numAttributes; ++i)
        {
            AttributeInfo info = serializables[0].attributeInfos[i];
            if (!showNonEditableAttribute && info.mode & AM_NOEDIT != 0)
                continue;
            if (info.type == VAR_RESOURCEREFLIST)
                count += serializables[0].attributes[i].GetResourceRefList().length;
            else if (info.type == VAR_VARIANTVECTOR && GetVectorStruct(serializables, i) !is null)
                count += serializables[0].attributes[i].GetVariantVector().length;
            else if (info.type == VAR_VARIANTMAP)
                count += serializables[0].attributes[i].GetVariantMap().length;
            else
                ++count;
        }
    }

    return count;
}

UIElement@ GetAttributeEditorParent(UIElement@ parent, uint index, uint subIndex)
{
    return parent.GetChild("Edit" + String(index) + "_" + String(subIndex), true);
}

void LoadAttributeEditor(ListView@ list, Array<Serializable@>@ serializables, bool editable, uint index)
{
    UIElement@ parent = GetAttributeEditorParent(list, index, 0);
    if (parent is null)
        return;

    inLoadAttributeEditor = true;

    AttributeInfo info = serializables[0].attributeInfos[index];
    bool sameValue = true;
    Variant value = serializables[0].attributes[index];
    for (uint i = 1; i < serializables.length; ++i)
    {
        if (serializables[i].attributes[index] != value)
        {
            sameValue = false;
            break;
        }
    }

    // Attribute with different values from multiple-select is loaded with default/empty value and non-editable
    LoadAttributeEditor(parent, value, value.type, info.enumNames, info.defaultValue, editable && sameValue, sameValue);

    inLoadAttributeEditor = false;
}

void LoadAttributeEditor(UIElement@ parent, Variant value, VariantType type, Array<String>@ enumNames, Variant defaultValue, bool editable, bool sameValue = true)
{
    uint index = parent.vars["Index"].GetUInt();

    // Assume the first child is always a text label element or a container that containing a text label element
    UIElement@ label = parent.children[0];
    if (label.type == containerType)
        label = label.children[0];
    if (label.type == textType)
    {
        bool modified;
        if (defaultValue.type == VAR_NONE || defaultValue.type == VAR_RESOURCEREFLIST)
            modified = !value.IsZero();
        else
            modified = value != defaultValue;
        cast<Text>(label).color = (editable ? (modified ? modifiedTextColor : normalTextColor) : nonEditableTextColor);
    }
    
    if (type == VAR_FLOAT || type == VAR_STRING || type == VAR_BUFFER)
        SetEditable(SetValue(parent.children[1], value.ToString(), sameValue), editable);
    else if (type == VAR_BOOL)
        SetEditable(SetValue(parent.children[1], value.GetBool(), sameValue), editable);
    else if (type == VAR_INT)
    {
        if (enumNames is null || enumNames.empty)
            SetEditable(SetValue(parent.children[1], value.ToString(), sameValue), editable);
        else
            SetEditable(SetValue(parent.children[1], value.GetInt(), sameValue), editable);
    }
    else if (type == VAR_RESOURCEREF)
    {
        SetEditable(SetValue(parent.children[1].children[0], cache.GetResourceName(value.GetResourceRef().id), sameValue), editable);
        SetEditable(parent.children[1].children[1], editable);  // If editable then can pick
        SetEditable(parent.children[1].children[2], sameValue); // If same value then can open
    }
    else if (type == VAR_RESOURCEREFLIST)
    {
        UIElement@ list = parent.parent;
        ResourceRefList refList = value.GetResourceRefList();
        for (uint subIndex = 0; subIndex < refList.length; ++subIndex)
        {
            parent = GetAttributeEditorParent(list, index, subIndex);
            if (parent is null)
                break;
            SetEditable(SetValue(parent.children[1].children[0], cache.GetResourceName(refList.ids[subIndex]), sameValue), editable);
        }
    }
    else if (type == VAR_VARIANTVECTOR)
    {
        UIElement@ list = parent.parent;
        Array<Variant>@ vector = value.GetVariantVector();
        for (uint i = 0; i < vector.length; ++i)
        {
            parent = GetAttributeEditorParent(list, index, i);
            if (parent is null)
                break;
            LoadAttributeEditor(parent, vector[i], vector[i].type, null, Variant(), editable, sameValue);
        }
    }
    else if (type == VAR_VARIANTMAP)
    {
        UIElement@ list = parent.parent;
        VariantMap map = value.GetVariantMap();
        Array<ShortStringHash>@ keys = map.keys;
        for (uint i = 0; i < keys.length; ++i)
        {
            parent = GetAttributeEditorParent(list, index, i);
            if (parent is null)
                break;
            Variant value = map[keys[i]];
            LoadAttributeEditor(parent, value, value.type, null, Variant(), editable, sameValue);
        }
    }
    else
    {
        // Convert Quaternion value to Vector3 value first
        if (type == VAR_QUATERNION)
            value = value.GetQuaternion().eulerAngles;
        Array<String> components = value.ToString().Split(' ');
        for (uint i = 0; i < components.length; ++i)
            SetEditable(SetValue(parent.children[i + 1], components[i], sameValue), editable);
    }
}

void StoreAttributeEditor(UIElement@ parent, Array<Serializable@>@ serializables, uint index, uint subIndex)
{
    AttributeInfo info = serializables[0].attributeInfos[index];

    if (info.type == VAR_RESOURCEREFLIST)
    {
        for (uint i = 0; i < serializables.length; ++i)
        {
            ResourceRefList refList = serializables[i].attributes[index].GetResourceRefList();
            Variant newValue = GetEditorValue(parent, VAR_RESOURCEREF, null);
            ResourceRef ref = newValue.GetResourceRef();
            refList.ids[subIndex] = ref.id;
            serializables[i].attributes[index] = Variant(refList);
        }
    }
    else if (info.type == VAR_VARIANTVECTOR)
    {
        for (uint i = 0; i < serializables.length; ++i)
        {
            Array<Variant>@ vector = serializables[i].attributes[index].GetVariantVector();
            Variant newValue = GetEditorValue(parent, vector[subIndex].type, null);
            vector[subIndex] = newValue;
            serializables[i].attributes[index] = Variant(vector);
        }
    }
    else if (info.type == VAR_VARIANTMAP)
    {
        for (uint i = 0; i < serializables.length; ++i)
        {
            VariantMap map = serializables[0].attributes[index].GetVariantMap();
            ShortStringHash key(parent.vars["Key"].GetUInt());
            Variant newValue = GetEditorValue(parent, map[key].type, null);
            map[key] = newValue;
            serializables[i].attributes[index] = Variant(map);
        }
    }
    else
    {
        Variant newValue = GetEditorValue(parent, info.type, info.enumNames);
        for (uint i = 0; i < serializables.length; ++i) {
            serializables[i].attributes[index] = newValue;
        }
    }
}

Variant GetEditorValue(UIElement@ parent, VariantType type, Array<String>@ enumNames)
{
    if (type == VAR_STRING)
    {
        LineEdit@ attrEdit = parent.children[1];
        return Variant(attrEdit.text.Trimmed());
    }
    if (type == VAR_BOOL)
    {
        CheckBox@ attrEdit = parent.children[1];
        return Variant(attrEdit.checked);
    }
    if (type == VAR_FLOAT)
    {
        LineEdit@ attrEdit = parent.children[1];
        return Variant(attrEdit.text.ToFloat());
    }
    if (type == VAR_VECTOR2)
    {
        LineEdit@ attrEditX = parent.children[1];
        LineEdit@ attrEditY = parent.children[2];
        Vector2 vec(attrEditX.text.ToFloat(), attrEditY.text.ToFloat());
        return Variant(vec);
    }
    if (type == VAR_VECTOR3)
    {
        LineEdit@ attrEditX = parent.children[1];
        LineEdit@ attrEditY = parent.children[2];
        LineEdit@ attrEditZ = parent.children[3];
        Vector3 vec(attrEditX.text.ToFloat(), attrEditY.text.ToFloat(), attrEditZ.text.ToFloat());
        return Variant(vec);
    }
    if (type == VAR_VECTOR4)
    {
        LineEdit@ attrEditX = parent.children[1];
        LineEdit@ attrEditY = parent.children[2];
        LineEdit@ attrEditZ = parent.children[3];
        LineEdit@ attrEditW = parent.children[4];
        Vector4 vec(attrEditX.text.ToFloat(), attrEditY.text.ToFloat(), attrEditZ.text.ToFloat(), attrEditW.text.ToFloat());
        return Variant(vec);
    }
    if (type == VAR_COLOR)
    {
        LineEdit@ attrEditR = parent.children[1];
        LineEdit@ attrEditG = parent.children[2];
        LineEdit@ attrEditB = parent.children[3];
        LineEdit@ attrEditA = parent.children[4];
        Color col(attrEditR.text.ToFloat(), attrEditG.text.ToFloat(), attrEditB.text.ToFloat(), attrEditA.text.ToFloat());
        return Variant(col);
    }
    if (type == VAR_QUATERNION)
    {
        LineEdit@ attrEditX = parent.children[1];
        LineEdit@ attrEditY = parent.children[2];
        LineEdit@ attrEditZ = parent.children[3];
        Vector3 vec(attrEditX.text.ToFloat(), attrEditY.text.ToFloat(), attrEditZ.text.ToFloat());
        return Variant(Quaternion(vec));
    }
    if (type == VAR_INT)
    {
        if (enumNames is null || enumNames.empty)
        {
            LineEdit@ attrEdit = parent.children[1];
            return Variant(attrEdit.text.ToInt());
        }
        else
        {
            DropDownList@ attrEdit = parent.children[1];
            return Variant(attrEdit.selection);
        }
    }
    if (type == VAR_RESOURCEREF)
    {
        LineEdit@ attrEdit = parent.children[0];
        ResourceRef ref;
        ref.id = StringHash(attrEdit.text.Trimmed());
        ref.type = ShortStringHash(attrEdit.vars["Type"].GetUInt());
        return Variant(ref);
    }
    if (type == VAR_INTVECTOR2)
    {
        LineEdit@ attrEditX = parent.children[1];
        LineEdit@ attrEditY = parent.children[2];
        IntVector2 vec(attrEditX.text.ToInt(), attrEditY.text.ToInt());
        return Variant(vec);
    }
    if (type == VAR_INTRECT)
    {
        LineEdit@ attrEditLeft = parent.children[1];
        LineEdit@ attrEditTop = parent.children[2];
        LineEdit@ attrEditRight = parent.children[3];
        LineEdit@ attrEditBottom = parent.children[3];
        IntRect vec(attrEditLeft.text.ToInt(), attrEditTop.text.ToInt(),
                       attrEditRight.text.ToInt(), attrEditBottom.text.ToInt());
        return Variant(vec);
    }
    return Variant();
}

void UpdateAttributes(Array<Serializable@>@ serializables, ListView@ list, bool fullUpdate)
{
    // If attributes have changed structurally, do a full update
    uint count = GetAttributeEditorCount(serializables);
    if (fullUpdate == false)
    {
        if (list.contentElement.numChildren != count)
            fullUpdate = true;
    }

    // Remember the old scroll position so that a full update does not feel as jarring
    IntVector2 oldViewPos = list.viewPosition;

    if (fullUpdate)
    {
        list.RemoveAllItems();

        // Resize the node editor according to the number of variables, up to a certain maximum
        if (list.name == "NodeAttributeList")
        {
            uint maxAttrs = Clamp(count, MIN_NODE_ATTRIBUTES, MAX_NODE_ATTRIBUTES);
            list.SetFixedHeight(maxAttrs * ATTR_HEIGHT + 2);
        }
    }

    if (serializables.empty)
        return;

    // If there are many serializables, they must share same attribute structure
    for (uint i = 0; i < serializables[0].numAttributes; ++i)
    {
        AttributeInfo info = serializables[0].attributeInfos[i];
        if (!showNonEditableAttribute && info.mode & AM_NOEDIT != 0)
            continue;

        if (fullUpdate)
            CreateAttributeEditor(list, serializables, i);

        LoadAttributeEditor(list, serializables, info.mode & AM_NOEDIT == 0, i);
    }
    
    if (fullUpdate)
        list.viewPosition = oldViewPos;
}

void EditAttribute(StringHash eventType, VariantMap& eventData)
{
    // Changing elements programmatically may cause events to be sent. Stop possible infinite loop in that case.
    if (inLoadAttributeEditor)
    {
        return;
    }

    UIElement@ attrEdit = eventData["Element"].GetUIElement();
    UIElement@ parent = attrEdit.parent;
    Array<Serializable@>@ serializables = GetAttributeEditorTargets(attrEdit);
    if (serializables.empty)
        return;

    uint index = attrEdit.vars["Index"].GetUInt();
    uint subIndex = attrEdit.vars["SubIndex"].GetUInt();
    bool intermediateEdit = eventType == textChangedEventType;

    StoreAttributeEditor(parent, serializables, index, subIndex);
    for (uint i = 0; i < serializables.length; ++i)
        serializables[i].ApplyAttributes();

    // Do the editor logic after attribute has been edited.
    PostEditAttribute(serializables, index);

    // If not an intermediate edit, reload the editor fields with validated values
    // (attributes may have interactions; therefore we load everything, not just the value being edited)
    if (!intermediateEdit)
        UpdateAttributes(false);
}

// Resource picker functionality

class ResourcePicker
{
    String resourceType;
    String lastPath;
    uint lastFilter;
    Array<String> filters;

    ResourcePicker(const String&in resourceType_, const String&in filter_)
    {
        resourceType = resourceType_;
        filters.Push(filter_);
        filters.Push("*.*");
        lastFilter = 0;
    }

    ResourcePicker(const String&in resourceType_, const Array<String>@ filters_)
    {
        resourceType = resourceType_;
        filters = filters_;
        filters.Push("*.*");
        lastFilter = 0;
    }
};

Array<ResourcePicker@> resourcePickers;
Array<Serializable@> resourceTargets;
uint resourcePickIndex = 0;
uint resourcePickSubIndex = 0;
ResourcePicker@ resourcePicker = null;

void InitResourcePicker()
{
    // Fill resource picker data
    Array<String> imageFilters = {"*.png", "*.jpg"};
    Array<String> textureFilters = {"*.dds", "*.png", "*.jpg", "*.bmp", "*.ktx", "*.pvr"};
    Array<String> soundFilters = {"*.wav","*.ogg"};
    resourcePickers.Push(ResourcePicker("Animation", "*.ani"));
    resourcePickers.Push(ResourcePicker("Image", imageFilters));
    resourcePickers.Push(ResourcePicker("Model", "*.mdl"));
    resourcePickers.Push(ResourcePicker("Material", "*.xml"));
    resourcePickers.Push(ResourcePicker("Texture2D", textureFilters));
    resourcePickers.Push(ResourcePicker("TextureCube", "*.xml"));
    resourcePickers.Push(ResourcePicker("ScriptFile", "*.as"));
    resourcePickers.Push(ResourcePicker("XMLFile", "*.xml"));
    resourcePickers.Push(ResourcePicker("Sound", soundFilters));
    sceneResourcePath = AddTrailingSlash(fileSystem.programDir + "Data");
}<commit_msg>Allow alternate extension .material for material files in the editor.<commit_after>// Attribute editor
//
// Functions that you must implement:
// - void SetAttributeEditorID(UIElement@ attrEdit, Array<Serializable@>@ serializables);
// - void PostEditAttribute(Array<Serializable@>@ serializables, uint index);
// - Array<Serializable@>@ GetAttributeEditorTargets(UIElement@ attrEdit);

const uint MIN_NODE_ATTRIBUTES = 4;
const uint MAX_NODE_ATTRIBUTES = 8;
const int ATTRNAME_WIDTH = 150;
const int ATTR_HEIGHT = 19;
bool inLoadAttributeEditor = false;

bool showNonEditableAttribute = false;

const ShortStringHash textType("Text");
const ShortStringHash containerType("UIElement");
const StringHash textChangedEventType("TextChanged");

Color normalTextColor(1.0f, 1.0f, 1.0f);
Color modifiedTextColor(1.0f, 0.8f, 0.5f);
Color nonEditableTextColor(0.7f, 0.7f, 0.7f);

String sceneResourcePath;

UIElement@ SetEditable(UIElement@ element, bool editable)
{
    element.active = editable;
    element.colors[C_TOPLEFT] = editable ? element.colors[C_BOTTOMRIGHT] : nonEditableTextColor;
    element.colors[C_BOTTOMLEFT] = element.colors[C_TOPLEFT];
    element.colors[C_TOPRIGHT] = element.colors[C_TOPLEFT];
    return element;
}

UIElement@ SetValue(LineEdit@ element, const String&in value, bool sameValue)
{
    element.text = sameValue ? value : STRIKED_OUT;
    element.cursorPosition = 0;
    return element;
}

UIElement@ SetValue(CheckBox@ element, bool value, bool sameValue)
{
    element.checked = sameValue ? value : false;
    return element;
}

UIElement@ SetValue(DropDownList@ element, int value, bool sameValue)
{
    element.selection = sameValue ? value : M_MAX_UNSIGNED;
    return element;
}

UIElement@ CreateAttributeEditorParentWithSeparatedLabel(ListView@ list, const String&in name, uint index, uint subIndex, bool suppressedSeparatedLabel = false)
{
    UIElement@ editorParent = UIElement("Edit" + String(index) + "_" + String(subIndex));
    editorParent.vars["Index"] = index;
    editorParent.vars["SubIndex"] = subIndex;
    editorParent.SetLayout(LM_VERTICAL, 2);
    list.AddItem(editorParent);

    if (suppressedSeparatedLabel)
    {
        UIElement@ placeHolder = UIElement();
        editorParent.AddChild(placeHolder);
    }
    else
    {
        Text@ attrNameText = Text();
        attrNameText.SetStyle(uiStyle, "EditorAttributeText");
        attrNameText.text = name;
        editorParent.AddChild(attrNameText);
    }

    return editorParent;
}

UIElement@ CreateAttributeEditorParent(ListView@ list, const String&in name, uint index, uint subIndex)
{
    UIElement@ editorParent = UIElement("Edit" + String(index) + "_" + String(subIndex));
    editorParent.vars["Index"] = index;
    editorParent.vars["SubIndex"] = subIndex;
    editorParent.SetLayout(LM_HORIZONTAL);
    editorParent.SetFixedHeight(ATTR_HEIGHT);
    list.AddItem(editorParent);

    Text@ attrNameText = Text();
    attrNameText.SetStyle(uiStyle, "EditorAttributeText");
    attrNameText.text = name;
    attrNameText.SetFixedWidth(ATTRNAME_WIDTH);
    editorParent.AddChild(attrNameText);

    return editorParent;
}

LineEdit@ CreateAttributeLineEdit(UIElement@ parent, Array<Serializable@>@ serializables, uint index, uint subIndex)
{
    LineEdit@ attrEdit = LineEdit();
    attrEdit.SetStyle(uiStyle, "EditorAttributeEdit");
    attrEdit.SetFixedHeight(ATTR_HEIGHT - 2);
    attrEdit.vars["Index"] = index;
    attrEdit.vars["SubIndex"] = subIndex;
    SetAttributeEditorID(attrEdit, serializables);
    parent.AddChild(attrEdit);
    return attrEdit;
}

void CreateAttributeEditor(ListView@ list, Array<Serializable@>@ serializables, uint index)
{
    AttributeInfo info = serializables[0].attributeInfos[index];
    CreateAttributeEditor(list, serializables, info.name, info.type, info.enumNames, index, 0);
}

UIElement@ CreateStringAttributeEditor(ListView@ list, Array<Serializable@>@ serializables, const String&in name, VariantType type, Array<String>@ enumNames, uint index, uint subIndex)
{
    UIElement@ parent = CreateAttributeEditorParent(list, name, index, subIndex);
    LineEdit@ attrEdit = CreateAttributeLineEdit(parent, serializables, index, subIndex);
    attrEdit.dragDropMode = DD_TARGET;
    SubscribeToEvent(attrEdit, "TextChanged", "EditAttribute");
    SubscribeToEvent(attrEdit, "TextFinished", "EditAttribute");
    return parent;
}

UIElement@ CreateBoolAttributeEditor(ListView@ list, Array<Serializable@>@ serializables, const String&in name, VariantType type, Array<String>@ enumNames, uint index, uint subIndex)
{
    UIElement@ parent = CreateAttributeEditorParent(list, name, index, subIndex);
    CheckBox@ attrEdit = CheckBox();
    attrEdit.style = uiStyle;
    attrEdit.SetFixedSize(16, 16);
    attrEdit.vars["Index"] = index;
    attrEdit.vars["SubIndex"] = subIndex;
    SetAttributeEditorID(attrEdit, serializables);
    parent.AddChild(attrEdit);
    SubscribeToEvent(attrEdit, "Toggled", "EditAttribute");
    return parent;
}

UIElement@ CreateNumAttributeEditor(ListView@ list, Array<Serializable@>@ serializables, const String&in name, VariantType type, Array<String>@ enumNames, uint index, uint subIndex)
{
    UIElement@ parent = CreateAttributeEditorParent(list, name, index, subIndex);
    uint numCoords = type - VAR_FLOAT + 1;
    if (type == VAR_QUATERNION)
        numCoords = 3;
    else if (type == VAR_COLOR || type == VAR_INTRECT)
        numCoords = 4;
    else if (type == VAR_INTVECTOR2)
        numCoords = 2;
    
    for (uint i = 0; i < numCoords; ++i)
    {
        LineEdit@ attrEdit = CreateAttributeLineEdit(parent, serializables, index, subIndex);
        SubscribeToEvent(attrEdit, "TextChanged", "EditAttribute");
        SubscribeToEvent(attrEdit, "TextFinished", "EditAttribute");
    }     
    return parent;    
}

UIElement@ CreateIntAttributeEditor(ListView@ list, Array<Serializable@>@ serializables, const String&in name, VariantType type, Array<String>@ enumNames, uint index, uint subIndex)
{
    UIElement@ parent = CreateAttributeEditorParent(list, name, index, subIndex);
    // Check for enums
    if (enumNames is null || enumNames.empty)
    {
        // No enums, create a numeric editor
        LineEdit@ attrEdit = CreateAttributeLineEdit(parent, serializables, index, subIndex);
        SubscribeToEvent(attrEdit, "TextChanged", "EditAttribute");
        SubscribeToEvent(attrEdit, "TextFinished", "EditAttribute");
    }
    else
    {
        DropDownList@ attrEdit = DropDownList();
        attrEdit.style = uiStyle;
        attrEdit.SetFixedHeight(ATTR_HEIGHT - 2);
        attrEdit.resizePopup = true;
        attrEdit.vars["Index"] = index;
        attrEdit.vars["SubIndex"] = subIndex;
        attrEdit.SetLayout(LM_HORIZONTAL, 0, IntRect(4, 1, 4, 1));
        SetAttributeEditorID(attrEdit, serializables);

        for (uint i = 0; i < enumNames.length; ++i)
        {
            Text@ choice = Text();
            choice.SetStyle(uiStyle, "EditorEnumAttributeText");
            choice.text = enumNames[i];
            attrEdit.AddItem(choice);
        }
        parent.AddChild(attrEdit);
        SubscribeToEvent(attrEdit, "ItemSelected", "EditAttribute");
    }

    return parent;
}

UIElement@ CreateResourceRefAttributeEditor(ListView@ list, Array<Serializable@>@ serializables, const String&in name, VariantType type, Array<String>@ enumNames, uint index, uint subIndex, bool suppressedSeparatedLabel = false)
{
    UIElement@ parent;
    ShortStringHash resourceType;
    VariantType attrType = serializables[0].attributeInfos[index].type;
    if (attrType == VAR_RESOURCEREF)
        resourceType = serializables[0].attributes[index].GetResourceRef().type;
    else if (attrType == VAR_RESOURCEREFLIST)
        resourceType = serializables[0].attributes[index].GetResourceRefList().type;
    else if (attrType == VAR_VARIANTVECTOR)
        resourceType = serializables[0].attributes[index].GetVariantVector()[subIndex].GetResourceRef().type;

     // Create the attribute name on a separate non-interactive line to allow for more space
    parent = CreateAttributeEditorParentWithSeparatedLabel(list, name, index, subIndex, suppressedSeparatedLabel);

    UIElement@ container = UIElement();
    container.SetLayout(LM_HORIZONTAL, 4, IntRect(name.StartsWith("   ") ? 20 : 10, 0, 4, 0));    // Left margin is indented more when the name is so
    container.SetFixedHeight(ATTR_HEIGHT);
    parent.AddChild(container);
        
    LineEdit@ attrEdit = CreateAttributeLineEdit(container, serializables, index, subIndex);
    attrEdit.vars["Type"] = resourceType.value;
    SubscribeToEvent(attrEdit, "TextFinished", "EditAttribute");

    Button@ pickButton = Button();
    pickButton.style = uiStyle;
    pickButton.SetFixedSize(36, ATTR_HEIGHT - 2);
    pickButton.vars["Index"] = index;
    pickButton.vars["SubIndex"] = subIndex;
    SetAttributeEditorID(pickButton, serializables);

    Text@ pickButtonText = Text();
    pickButtonText.SetStyle(uiStyle, "EditorAttributeText");
    pickButtonText.SetAlignment(HA_CENTER, VA_CENTER);
    pickButtonText.text = "Pick";
    pickButton.AddChild(pickButtonText);
    container.AddChild(pickButton);
    SubscribeToEvent(pickButton, "Released", "PickResource");

    Button@ openButton = Button();
    openButton.style = uiStyle;
    openButton.SetFixedSize(36, ATTR_HEIGHT - 2);
    openButton.vars["Index"] = index;
    openButton.vars["SubIndex"] = subIndex;
    SetAttributeEditorID(openButton, serializables);

    Text@ openButtonText = Text();
    openButtonText.SetStyle(uiStyle, "EditorAttributeText");
    openButtonText.SetAlignment(HA_CENTER, VA_CENTER);
    openButtonText.text = "Open";
    openButton.AddChild(openButtonText);
    container.AddChild(openButton);
    SubscribeToEvent(openButton, "Released", "OpenResource");
    return parent;
}

UIElement@ CreateAttributeEditor(ListView@ list, Array<Serializable@>@ serializables, const String&in name, VariantType type, Array<String>@ enumNames, uint index, uint subIndex, bool suppressedSeparatedLabel = false)
{
    UIElement@ parent;

    AttributeInfo info = serializables[0].attributeInfos[index];
    if (type == VAR_STRING || type == VAR_BUFFER)
        parent = CreateStringAttributeEditor(list, serializables, name, type, enumNames, index, subIndex);
    else if (type == VAR_BOOL)
        parent = CreateBoolAttributeEditor(list, serializables, name, type, enumNames, index, subIndex);
    else if ((type >= VAR_FLOAT && type <= VAR_VECTOR4) || type == VAR_QUATERNION || type == VAR_COLOR || type == VAR_INTVECTOR2 || type == VAR_INTRECT)
        parent = CreateNumAttributeEditor(list, serializables, name, type, enumNames, index, subIndex);
    else if (type == VAR_INT)
        parent = CreateIntAttributeEditor(list, serializables, name, type, enumNames, index, subIndex);
    else if (type == VAR_RESOURCEREF)
        parent = CreateResourceRefAttributeEditor(list, serializables, name, type, enumNames, index, subIndex, suppressedSeparatedLabel);
    else if (type == VAR_RESOURCEREFLIST)
    {
        uint numRefs = serializables[0].attributes[index].GetResourceRefList().length;
        for (uint i = 0; i < numRefs; ++i)
            CreateAttributeEditor(list, serializables, name, VAR_RESOURCEREF, null, index, i, i > 0);
    }
    else if (type == VAR_VARIANTVECTOR)
    {
        VectorStruct@ vectorStruct = GetVectorStruct(serializables, index);
        if (vectorStruct is null)
            return null;
        uint nameIndex = 0;

        Array<Variant>@ vector = serializables[0].attributes[index].GetVariantVector();
        for (uint i = 0; i < vector.length; ++i)
        {
            CreateAttributeEditor(list, serializables, vectorStruct.variableNames[nameIndex], vector[i].type, null, index, i);
            ++nameIndex;
            if (nameIndex >= vectorStruct.variableNames.length)
                nameIndex = vectorStruct.restartIndex;
        }
    }
    else if (type == VAR_VARIANTMAP)
    {
        VariantMap map = serializables[0].attributes[index].GetVariantMap();
        Array<ShortStringHash>@ keys = map.keys;
        for (uint i = 0; i < keys.length; ++i)
        {
            Variant value = map[keys[i]];
            parent = CreateAttributeEditor(list, serializables, scene.GetVarName(keys[i]) + " (Var)", value.type, null, index, i);
            // Add the variant key to the parent
            parent.vars["Key"] = keys[i].value;
        }
    }

    return parent;
}

uint GetAttributeEditorCount(Array<Serializable@>@ serializables)
{
    uint count = 0;

    if (!serializables.empty)
    {
        /// \todo When multi-editing, this only counts the editor count of the first serializable
        for (uint i = 0; i < serializables[0].numAttributes; ++i)
        {
            AttributeInfo info = serializables[0].attributeInfos[i];
            if (!showNonEditableAttribute && info.mode & AM_NOEDIT != 0)
                continue;
            if (info.type == VAR_RESOURCEREFLIST)
                count += serializables[0].attributes[i].GetResourceRefList().length;
            else if (info.type == VAR_VARIANTVECTOR && GetVectorStruct(serializables, i) !is null)
                count += serializables[0].attributes[i].GetVariantVector().length;
            else if (info.type == VAR_VARIANTMAP)
                count += serializables[0].attributes[i].GetVariantMap().length;
            else
                ++count;
        }
    }

    return count;
}

UIElement@ GetAttributeEditorParent(UIElement@ parent, uint index, uint subIndex)
{
    return parent.GetChild("Edit" + String(index) + "_" + String(subIndex), true);
}

void LoadAttributeEditor(ListView@ list, Array<Serializable@>@ serializables, bool editable, uint index)
{
    UIElement@ parent = GetAttributeEditorParent(list, index, 0);
    if (parent is null)
        return;

    inLoadAttributeEditor = true;

    AttributeInfo info = serializables[0].attributeInfos[index];
    bool sameValue = true;
    Variant value = serializables[0].attributes[index];
    for (uint i = 1; i < serializables.length; ++i)
    {
        if (serializables[i].attributes[index] != value)
        {
            sameValue = false;
            break;
        }
    }

    // Attribute with different values from multiple-select is loaded with default/empty value and non-editable
    LoadAttributeEditor(parent, value, value.type, info.enumNames, info.defaultValue, editable && sameValue, sameValue);

    inLoadAttributeEditor = false;
}

void LoadAttributeEditor(UIElement@ parent, Variant value, VariantType type, Array<String>@ enumNames, Variant defaultValue, bool editable, bool sameValue = true)
{
    uint index = parent.vars["Index"].GetUInt();

    // Assume the first child is always a text label element or a container that containing a text label element
    UIElement@ label = parent.children[0];
    if (label.type == containerType)
        label = label.children[0];
    if (label.type == textType)
    {
        bool modified;
        if (defaultValue.type == VAR_NONE || defaultValue.type == VAR_RESOURCEREFLIST)
            modified = !value.IsZero();
        else
            modified = value != defaultValue;
        cast<Text>(label).color = (editable ? (modified ? modifiedTextColor : normalTextColor) : nonEditableTextColor);
    }
    
    if (type == VAR_FLOAT || type == VAR_STRING || type == VAR_BUFFER)
        SetEditable(SetValue(parent.children[1], value.ToString(), sameValue), editable);
    else if (type == VAR_BOOL)
        SetEditable(SetValue(parent.children[1], value.GetBool(), sameValue), editable);
    else if (type == VAR_INT)
    {
        if (enumNames is null || enumNames.empty)
            SetEditable(SetValue(parent.children[1], value.ToString(), sameValue), editable);
        else
            SetEditable(SetValue(parent.children[1], value.GetInt(), sameValue), editable);
    }
    else if (type == VAR_RESOURCEREF)
    {
        SetEditable(SetValue(parent.children[1].children[0], cache.GetResourceName(value.GetResourceRef().id), sameValue), editable);
        SetEditable(parent.children[1].children[1], editable);  // If editable then can pick
        SetEditable(parent.children[1].children[2], sameValue); // If same value then can open
    }
    else if (type == VAR_RESOURCEREFLIST)
    {
        UIElement@ list = parent.parent;
        ResourceRefList refList = value.GetResourceRefList();
        for (uint subIndex = 0; subIndex < refList.length; ++subIndex)
        {
            parent = GetAttributeEditorParent(list, index, subIndex);
            if (parent is null)
                break;
            SetEditable(SetValue(parent.children[1].children[0], cache.GetResourceName(refList.ids[subIndex]), sameValue), editable);
        }
    }
    else if (type == VAR_VARIANTVECTOR)
    {
        UIElement@ list = parent.parent;
        Array<Variant>@ vector = value.GetVariantVector();
        for (uint i = 0; i < vector.length; ++i)
        {
            parent = GetAttributeEditorParent(list, index, i);
            if (parent is null)
                break;
            LoadAttributeEditor(parent, vector[i], vector[i].type, null, Variant(), editable, sameValue);
        }
    }
    else if (type == VAR_VARIANTMAP)
    {
        UIElement@ list = parent.parent;
        VariantMap map = value.GetVariantMap();
        Array<ShortStringHash>@ keys = map.keys;
        for (uint i = 0; i < keys.length; ++i)
        {
            parent = GetAttributeEditorParent(list, index, i);
            if (parent is null)
                break;
            Variant value = map[keys[i]];
            LoadAttributeEditor(parent, value, value.type, null, Variant(), editable, sameValue);
        }
    }
    else
    {
        // Convert Quaternion value to Vector3 value first
        if (type == VAR_QUATERNION)
            value = value.GetQuaternion().eulerAngles;
        Array<String> components = value.ToString().Split(' ');
        for (uint i = 0; i < components.length; ++i)
            SetEditable(SetValue(parent.children[i + 1], components[i], sameValue), editable);
    }
}

void StoreAttributeEditor(UIElement@ parent, Array<Serializable@>@ serializables, uint index, uint subIndex)
{
    AttributeInfo info = serializables[0].attributeInfos[index];

    if (info.type == VAR_RESOURCEREFLIST)
    {
        for (uint i = 0; i < serializables.length; ++i)
        {
            ResourceRefList refList = serializables[i].attributes[index].GetResourceRefList();
            Variant newValue = GetEditorValue(parent, VAR_RESOURCEREF, null);
            ResourceRef ref = newValue.GetResourceRef();
            refList.ids[subIndex] = ref.id;
            serializables[i].attributes[index] = Variant(refList);
        }
    }
    else if (info.type == VAR_VARIANTVECTOR)
    {
        for (uint i = 0; i < serializables.length; ++i)
        {
            Array<Variant>@ vector = serializables[i].attributes[index].GetVariantVector();
            Variant newValue = GetEditorValue(parent, vector[subIndex].type, null);
            vector[subIndex] = newValue;
            serializables[i].attributes[index] = Variant(vector);
        }
    }
    else if (info.type == VAR_VARIANTMAP)
    {
        for (uint i = 0; i < serializables.length; ++i)
        {
            VariantMap map = serializables[0].attributes[index].GetVariantMap();
            ShortStringHash key(parent.vars["Key"].GetUInt());
            Variant newValue = GetEditorValue(parent, map[key].type, null);
            map[key] = newValue;
            serializables[i].attributes[index] = Variant(map);
        }
    }
    else
    {
        Variant newValue = GetEditorValue(parent, info.type, info.enumNames);
        for (uint i = 0; i < serializables.length; ++i) {
            serializables[i].attributes[index] = newValue;
        }
    }
}

Variant GetEditorValue(UIElement@ parent, VariantType type, Array<String>@ enumNames)
{
    if (type == VAR_STRING)
    {
        LineEdit@ attrEdit = parent.children[1];
        return Variant(attrEdit.text.Trimmed());
    }
    if (type == VAR_BOOL)
    {
        CheckBox@ attrEdit = parent.children[1];
        return Variant(attrEdit.checked);
    }
    if (type == VAR_FLOAT)
    {
        LineEdit@ attrEdit = parent.children[1];
        return Variant(attrEdit.text.ToFloat());
    }
    if (type == VAR_VECTOR2)
    {
        LineEdit@ attrEditX = parent.children[1];
        LineEdit@ attrEditY = parent.children[2];
        Vector2 vec(attrEditX.text.ToFloat(), attrEditY.text.ToFloat());
        return Variant(vec);
    }
    if (type == VAR_VECTOR3)
    {
        LineEdit@ attrEditX = parent.children[1];
        LineEdit@ attrEditY = parent.children[2];
        LineEdit@ attrEditZ = parent.children[3];
        Vector3 vec(attrEditX.text.ToFloat(), attrEditY.text.ToFloat(), attrEditZ.text.ToFloat());
        return Variant(vec);
    }
    if (type == VAR_VECTOR4)
    {
        LineEdit@ attrEditX = parent.children[1];
        LineEdit@ attrEditY = parent.children[2];
        LineEdit@ attrEditZ = parent.children[3];
        LineEdit@ attrEditW = parent.children[4];
        Vector4 vec(attrEditX.text.ToFloat(), attrEditY.text.ToFloat(), attrEditZ.text.ToFloat(), attrEditW.text.ToFloat());
        return Variant(vec);
    }
    if (type == VAR_COLOR)
    {
        LineEdit@ attrEditR = parent.children[1];
        LineEdit@ attrEditG = parent.children[2];
        LineEdit@ attrEditB = parent.children[3];
        LineEdit@ attrEditA = parent.children[4];
        Color col(attrEditR.text.ToFloat(), attrEditG.text.ToFloat(), attrEditB.text.ToFloat(), attrEditA.text.ToFloat());
        return Variant(col);
    }
    if (type == VAR_QUATERNION)
    {
        LineEdit@ attrEditX = parent.children[1];
        LineEdit@ attrEditY = parent.children[2];
        LineEdit@ attrEditZ = parent.children[3];
        Vector3 vec(attrEditX.text.ToFloat(), attrEditY.text.ToFloat(), attrEditZ.text.ToFloat());
        return Variant(Quaternion(vec));
    }
    if (type == VAR_INT)
    {
        if (enumNames is null || enumNames.empty)
        {
            LineEdit@ attrEdit = parent.children[1];
            return Variant(attrEdit.text.ToInt());
        }
        else
        {
            DropDownList@ attrEdit = parent.children[1];
            return Variant(attrEdit.selection);
        }
    }
    if (type == VAR_RESOURCEREF)
    {
        LineEdit@ attrEdit = parent.children[0];
        ResourceRef ref;
        ref.id = StringHash(attrEdit.text.Trimmed());
        ref.type = ShortStringHash(attrEdit.vars["Type"].GetUInt());
        return Variant(ref);
    }
    if (type == VAR_INTVECTOR2)
    {
        LineEdit@ attrEditX = parent.children[1];
        LineEdit@ attrEditY = parent.children[2];
        IntVector2 vec(attrEditX.text.ToInt(), attrEditY.text.ToInt());
        return Variant(vec);
    }
    if (type == VAR_INTRECT)
    {
        LineEdit@ attrEditLeft = parent.children[1];
        LineEdit@ attrEditTop = parent.children[2];
        LineEdit@ attrEditRight = parent.children[3];
        LineEdit@ attrEditBottom = parent.children[3];
        IntRect vec(attrEditLeft.text.ToInt(), attrEditTop.text.ToInt(),
                       attrEditRight.text.ToInt(), attrEditBottom.text.ToInt());
        return Variant(vec);
    }
    return Variant();
}

void UpdateAttributes(Array<Serializable@>@ serializables, ListView@ list, bool fullUpdate)
{
    // If attributes have changed structurally, do a full update
    uint count = GetAttributeEditorCount(serializables);
    if (fullUpdate == false)
    {
        if (list.contentElement.numChildren != count)
            fullUpdate = true;
    }

    // Remember the old scroll position so that a full update does not feel as jarring
    IntVector2 oldViewPos = list.viewPosition;

    if (fullUpdate)
    {
        list.RemoveAllItems();

        // Resize the node editor according to the number of variables, up to a certain maximum
        if (list.name == "NodeAttributeList")
        {
            uint maxAttrs = Clamp(count, MIN_NODE_ATTRIBUTES, MAX_NODE_ATTRIBUTES);
            list.SetFixedHeight(maxAttrs * ATTR_HEIGHT + 2);
        }
    }

    if (serializables.empty)
        return;

    // If there are many serializables, they must share same attribute structure
    for (uint i = 0; i < serializables[0].numAttributes; ++i)
    {
        AttributeInfo info = serializables[0].attributeInfos[i];
        if (!showNonEditableAttribute && info.mode & AM_NOEDIT != 0)
            continue;

        if (fullUpdate)
            CreateAttributeEditor(list, serializables, i);

        LoadAttributeEditor(list, serializables, info.mode & AM_NOEDIT == 0, i);
    }
    
    if (fullUpdate)
        list.viewPosition = oldViewPos;
}

void EditAttribute(StringHash eventType, VariantMap& eventData)
{
    // Changing elements programmatically may cause events to be sent. Stop possible infinite loop in that case.
    if (inLoadAttributeEditor)
    {
        return;
    }

    UIElement@ attrEdit = eventData["Element"].GetUIElement();
    UIElement@ parent = attrEdit.parent;
    Array<Serializable@>@ serializables = GetAttributeEditorTargets(attrEdit);
    if (serializables.empty)
        return;

    uint index = attrEdit.vars["Index"].GetUInt();
    uint subIndex = attrEdit.vars["SubIndex"].GetUInt();
    bool intermediateEdit = eventType == textChangedEventType;

    StoreAttributeEditor(parent, serializables, index, subIndex);
    for (uint i = 0; i < serializables.length; ++i)
        serializables[i].ApplyAttributes();

    // Do the editor logic after attribute has been edited.
    PostEditAttribute(serializables, index);

    // If not an intermediate edit, reload the editor fields with validated values
    // (attributes may have interactions; therefore we load everything, not just the value being edited)
    if (!intermediateEdit)
        UpdateAttributes(false);
}

// Resource picker functionality

class ResourcePicker
{
    String resourceType;
    String lastPath;
    uint lastFilter;
    Array<String> filters;

    ResourcePicker(const String&in resourceType_, const String&in filter_)
    {
        resourceType = resourceType_;
        filters.Push(filter_);
        filters.Push("*.*");
        lastFilter = 0;
    }

    ResourcePicker(const String&in resourceType_, const Array<String>@ filters_)
    {
        resourceType = resourceType_;
        filters = filters_;
        filters.Push("*.*");
        lastFilter = 0;
    }
};

Array<ResourcePicker@> resourcePickers;
Array<Serializable@> resourceTargets;
uint resourcePickIndex = 0;
uint resourcePickSubIndex = 0;
ResourcePicker@ resourcePicker = null;

void InitResourcePicker()
{
    // Fill resource picker data
    Array<String> imageFilters = {"*.png", "*.jpg"};
    Array<String> textureFilters = {"*.dds", "*.png", "*.jpg", "*.bmp", "*.ktx", "*.pvr"};
    Array<String> soundFilters = {"*.wav","*.ogg"};
    Array<String> materialFilters = {"*.xml", "*.material"};
    resourcePickers.Push(ResourcePicker("Animation", "*.ani"));
    resourcePickers.Push(ResourcePicker("Image", imageFilters));
    resourcePickers.Push(ResourcePicker("Model", "*.mdl"));
    resourcePickers.Push(ResourcePicker("Material", materialFilters));
    resourcePickers.Push(ResourcePicker("Texture2D", textureFilters));
    resourcePickers.Push(ResourcePicker("TextureCube", "*.xml"));
    resourcePickers.Push(ResourcePicker("ScriptFile", "*.as"));
    resourcePickers.Push(ResourcePicker("XMLFile", "*.xml"));
    resourcePickers.Push(ResourcePicker("Sound", soundFilters));
    sceneResourcePath = AddTrailingSlash(fileSystem.programDir + "Data");
}
{'fraction_non_alphanumeric': 0.11737360947815863, 'fraction_numerical': 0.005932893189834494, 'mean_word_length': 3.299066874027994, 'pattern_counts': {'":': 0, '<': 116, '<?xml version=': 0, '>': 86, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 6}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4895144', 'n_tokens_mistral': 16512, 'n_tokens_neox': 16195, 'n_words': 3687}