# Copyright (c) 2020, NVIDIA CORPORATION.  All rights reserved.
#
# NVIDIA CORPORATION and its licensors retain all intellectual property
# and proprietary rights in and to this software, related documentation
# and any modifications thereto.  Any use, reproduction, disclosure or
# distribution of this software and related documentation without an express
# license agreement from NVIDIA CORPORATION is strictly prohibited.
#

import asyncio
import fnmatch
import weakref
from functools import lru_cache
from typing import Any, Callable, List

import carb
import carb.settings
import omni.client
import omni.kit.ui
import omni.ui as ui
import omni.usd
from omni.kit.window.property.templates import HORIZONTAL_SPACING, LABEL_HEIGHT, LABEL_WIDTH, LABEL_WIDTH_LIGHT
from omni.kit.window.file_importer import get_file_importer
from pxr import Ar, Sdf, Usd

from .asset_filepicker import show_asset_file_picker
from .attribute_context_menu import AttributeContextMenu, AttributeContextMenuEvent
from .control_state_manager import ControlStateManager
from .relationship import RelationshipEditWidget
from .usd_attribute_model import (
    GfVecAttributeModel,
    GfVecAttributeSingleChannelModel,
    MdlEnumAttributeModel,
    SdfAssetPathArrayAttributeItemModel,
    SdfAssetPathAttributeModel,
    SdfTimeCodeModel,
    TfTokenAttributeModel,
    UsdAttributeModel,
)
from .usd_object_model import MetadataObjectModel
from .widgets import ICON_PATH


@lru_cache()
def _get_plus_glyph():
    return omni.kit.ui.get_custom_glyph_code("${glyphs}/menu_context.svg")


def get_ui_style():
    return carb.settings.get_settings().get_as_string("/persistent/app/window/uiStyle") or "NvidiaDark"


def get_model_cls(cls, args, key="model_cls"):
    if args:
        return args.get(key, cls)
    return cls

def get_model_kwargs(args, key="model_kwargs"):
    if args:
        return args.get(key, {})
    return {}

class UsdPropertiesWidgetBuilder:
    # We don't use Tf.Type.FindByName(name) directly because the name depends
    # on the compiler. For example the line `Sdf.ValueTypeNames.Int64.type`
    # resolves to `Tf.Type.FindByName('long')` on Linux and on Windows it's
    # `Tf.Type.FindByName('__int64')`.
    tf_half = Sdf.ValueTypeNames.Half.type
    tf_float = Sdf.ValueTypeNames.Float.type
    tf_double = Sdf.ValueTypeNames.Double.type
    tf_uchar = Sdf.ValueTypeNames.UChar.type
    tf_uint = Sdf.ValueTypeNames.UInt.type
    tf_int = Sdf.ValueTypeNames.Int.type
    tf_int64 = Sdf.ValueTypeNames.Int64.type
    tf_uint64 = Sdf.ValueTypeNames.UInt64.type
    tf_bool = Sdf.ValueTypeNames.Bool.type
    tf_string = Sdf.ValueTypeNames.String.type
    tf_gf_vec2i = Sdf.ValueTypeNames.Int2.type
    tf_gf_vec2h = Sdf.ValueTypeNames.Half2.type
    tf_gf_vec2f = Sdf.ValueTypeNames.Float2.type
    tf_gf_vec2d = Sdf.ValueTypeNames.Double2.type
    tf_gf_vec3i = Sdf.ValueTypeNames.Int3.type
    tf_gf_vec3h = Sdf.ValueTypeNames.Half3.type
    tf_gf_vec3f = Sdf.ValueTypeNames.Float3.type
    tf_gf_vec3d = Sdf.ValueTypeNames.Double3.type
    tf_gf_vec4i = Sdf.ValueTypeNames.Int4.type
    tf_gf_vec4h = Sdf.ValueTypeNames.Half4.type
    tf_gf_vec4f = Sdf.ValueTypeNames.Float4.type
    tf_gf_vec4d = Sdf.ValueTypeNames.Double4.type
    tf_tf_token = Sdf.ValueTypeNames.Token.type
    tf_sdf_asset_path = Sdf.ValueTypeNames.Asset.type
    tf_sdf_time_code = Sdf.ValueTypeNames.TimeCode.type

    # array type builders
    tf_sdf_asset_array_path = Sdf.ValueTypeNames.AssetArray.type

    @classmethod
    def init_builder_table(cls):
        cls.widget_builder_table = {
            cls.tf_half: cls._floating_point_builder,
            cls.tf_float: cls._floating_point_builder,
            cls.tf_double: cls._floating_point_builder,
            cls.tf_uchar: cls._integer_builder,
            cls.tf_uint: cls._integer_builder,
            cls.tf_int: cls._integer_builder,
            cls.tf_int64: cls._integer_builder,
            cls.tf_uint64: cls._integer_builder,
            cls.tf_bool: cls._bool_builder,
            cls.tf_string: cls._string_builder,
            cls.tf_gf_vec2i: cls._vec2_per_channel_builder,
            cls.tf_gf_vec2h: cls._vec2_per_channel_builder,
            cls.tf_gf_vec2f: cls._vec2_per_channel_builder,
            cls.tf_gf_vec2d: cls._vec2_per_channel_builder,
            cls.tf_gf_vec3i: cls._vec3_per_channel_builder,
            cls.tf_gf_vec3h: cls._vec3_per_channel_builder,
            cls.tf_gf_vec3f: cls._vec3_per_channel_builder,
            cls.tf_gf_vec3d: cls._vec3_per_channel_builder,
            cls.tf_gf_vec4i: cls._vec4_per_channel_builder,
            cls.tf_gf_vec4h: cls._vec4_per_channel_builder,
            cls.tf_gf_vec4f: cls._vec4_per_channel_builder,
            cls.tf_gf_vec4d: cls._vec4_per_channel_builder,
            cls.tf_tf_token: cls._tftoken_builder,
            cls.tf_sdf_asset_path: cls._sdf_asset_path_builder,
            cls.tf_sdf_time_code: cls._time_code_builder,
            # array type builders
            cls.tf_sdf_asset_array_path: cls._sdf_asset_path_array_builder,
        }
        cls.reset_builder_coverage_table()

    @classmethod
    def reset_builder_coverage_table(cls):
        cls.widget_builder_coverage_table = {
            cls.tf_half: False,
            cls.tf_float: False,
            cls.tf_double: False,
            cls.tf_uchar: False,
            cls.tf_uint: False,
            cls.tf_int: False,
            cls.tf_int64: False,
            cls.tf_uint64: False,
            cls.tf_bool: False,
            cls.tf_string: False,
            cls.tf_gf_vec2i: False,
            cls.tf_gf_vec2h: False,
            cls.tf_gf_vec2f: False,
            cls.tf_gf_vec2d: False,
            cls.tf_gf_vec3i: False,
            cls.tf_gf_vec3h: False,
            cls.tf_gf_vec3f: False,
            cls.tf_gf_vec3d: False,
            cls.tf_gf_vec4i: False,
            cls.tf_gf_vec4h: False,
            cls.tf_gf_vec4f: False,
            cls.tf_gf_vec4d: False,
            cls.tf_tf_token: False,
            cls.tf_sdf_asset_path: False,
            cls.tf_sdf_time_code: False,
            cls.tf_sdf_asset_array_path: False,
        }

    @classmethod
    def startup(cls):
        cls.init_builder_table()
        cls.default_range_steps = {}

    @classmethod
    def shutdown(cls):
        pass

    @classmethod
    def build(
        cls,
        stage,
        attr_name,
        metadata,
        property_type,
        prim_paths: List[Sdf.Path],
        additional_label_kwargs=None,
        additional_widget_kwargs=None,
    ):
        if property_type == Usd.Attribute:
            type_name = cls._get_type_name(metadata)
            tf_type = type_name.type
            build_func = cls.widget_builder_table.get(tf_type, cls._fallback_builder)
            cls.widget_builder_coverage_table[tf_type] = True
            return build_func(
                stage, attr_name, type_name, metadata, prim_paths, additional_label_kwargs, additional_widget_kwargs
            )
        elif property_type == Usd.Relationship:
            return cls._relationship_builder(
                stage, attr_name, metadata, prim_paths, additional_label_kwargs, additional_widget_kwargs
            )

    ###### builder funcs ######
    @classmethod
    def _relationship_builder(
        cls,
        stage,
        attr_name,
        metadata,
        prim_paths: List[Sdf.Path],
        additional_label_kwargs=None,
        additional_widget_kwargs=None,
    ):
        with ui.HStack(spacing=HORIZONTAL_SPACING):
            cls._create_label(attr_name, metadata, additional_label_kwargs)
            return RelationshipEditWidget(stage, attr_name, prim_paths, additional_widget_kwargs)

    @classmethod
    def _fallback_builder(
        cls,
        stage,
        attr_name,
        type_name,
        metadata,
        prim_paths: List[Sdf.Path],
        additional_label_kwargs=None,
        additional_widget_kwargs=None,
    ):
        with ui.HStack(spacing=HORIZONTAL_SPACING):
            model_kwargs = get_model_kwargs(additional_widget_kwargs)
            model_cls = get_model_cls(UsdAttributeModel, additional_widget_kwargs)
            model = model_cls(stage, [path.AppendProperty(attr_name) for path in prim_paths], False, metadata, **model_kwargs)
            label = cls._create_label(attr_name, metadata, additional_label_kwargs)
            kwargs = {
                "name": "models_readonly",
                "model": model,
                "enabled": False,
                "tooltip": model.get_value_as_string(),
            }
            if additional_widget_kwargs:
                kwargs.update(additional_widget_kwargs)
            with ui.ZStack():
                value_widget = ui.StringField(**kwargs)
                value_widget.identifier = f"fallback_{attr_name}"
                mixed_overlay = cls._create_mixed_text_overlay()
            cls._create_control_state(value_widget=value_widget, mixed_overlay=mixed_overlay, **kwargs, label=label)
            return model

    @classmethod
    def _floating_point_builder(
        cls,
        stage,
        attr_name,
        type_name,
        metadata,
        prim_paths: List[Sdf.Path],
        additional_label_kwargs=None,
        additional_widget_kwargs=None,
    ):
        with ui.HStack(spacing=HORIZONTAL_SPACING):
            model_kwargs = cls._get_attr_value_range_kwargs(metadata)
            model_kwargs.update(get_model_kwargs(additional_widget_kwargs))
            model_cls = get_model_cls(UsdAttributeModel, additional_widget_kwargs)
            model = model_cls(
                stage, [path.AppendProperty(attr_name) for path in prim_paths], False, metadata, **model_kwargs
            )
            # insert hard-range min/max before soft-range
            widget_kwargs = {"model": model}
            widget_kwargs.update(model_kwargs)
            soft_range_min, soft_range_max = cls._get_attr_value_soft_range(metadata, model)
            label = cls._create_label(attr_name, metadata, additional_label_kwargs)
            widget_kwargs.update(cls._get_attr_value_soft_range_kwargs(metadata, model))
            if additional_widget_kwargs:
                widget_kwargs.update(additional_widget_kwargs)
            with ui.ZStack():
                if soft_range_min < soft_range_max:
                    value_widget = cls._create_drag_or_slider(ui.FloatDrag, ui.FloatDrag, **widget_kwargs)
                    cls._setup_soft_float_dynamic_range(attr_name, metadata, value_widget.step, model, value_widget)
                else:
                    value_widget = cls._create_drag_or_slider(ui.FloatDrag, ui.FloatSlider, **widget_kwargs)
                value_widget.identifier = f"float_slider_{attr_name}"
                mixed_overlay = cls._create_mixed_text_overlay()
            cls._create_control_state(
                value_widget=value_widget, mixed_overlay=mixed_overlay, **widget_kwargs, label=label
            )
            return model

    @classmethod
    def _integer_builder(
        cls,
        stage,
        attr_name,
        type_name,
        metadata,
        prim_paths: List[Sdf.Path],
        additional_label_kwargs=None,
        additional_widget_kwargs=None,
    ):
        with ui.HStack(spacing=HORIZONTAL_SPACING):
            label = cls._create_label(attr_name, metadata, additional_label_kwargs)
            is_mdl_enum = False
            if metadata.get("renderType", None):
                sdr_metadata = metadata.get("sdrMetadata", None)
                if sdr_metadata and sdr_metadata.get("options", None):
                    is_mdl_enum = True
            if is_mdl_enum:
                model_kwargs = get_model_kwargs(additional_widget_kwargs)
                model_cls = get_model_cls(MdlEnumAttributeModel, additional_widget_kwargs)
                model = model_cls(stage, [path.AppendProperty(attr_name) for path in prim_paths], False, metadata, **model_kwargs)
                widget_kwargs = {"name": "choices", "model": model}
                if additional_widget_kwargs:
                    widget_kwargs.update(additional_widget_kwargs)
                with ui.ZStack():
                    value_widget = ui.ComboBox(model, **widget_kwargs)
                    mixed_overlay = cls._create_mixed_text_overlay()
            else:
                model_kwargs = cls._get_attr_value_range_kwargs(metadata)
                if type_name.type == cls.tf_uint:
                    model_kwargs["min"] = 0
                model_kwargs.update(get_model_kwargs(additional_widget_kwargs))
                model_cls = get_model_cls(UsdAttributeModel, additional_widget_kwargs)
                model = model_cls(
                    stage, [path.AppendProperty(attr_name) for path in prim_paths], False, metadata, **model_kwargs
                )
                # insert hard-range min/max before soft-range
                widget_kwargs = {"model": model}
                widget_kwargs.update(model_kwargs)
                widget_kwargs.update(cls._get_attr_value_soft_range_kwargs(metadata, model))
                if additional_widget_kwargs:
                    widget_kwargs.update(additional_widget_kwargs)
                with ui.ZStack():
                    value_widget = cls._create_drag_or_slider(ui.IntDrag, ui.IntSlider, **widget_kwargs)
                    mixed_overlay = cls._create_mixed_text_overlay()
            value_widget.identifier = f"integer_slider_{attr_name}"
            cls._create_control_state(
                value_widget=value_widget, mixed_overlay=mixed_overlay, **widget_kwargs, label=label
            )
            return model

    @classmethod
    def _bool_builder(
        cls,
        stage,
        attr_name,
        type_name,
        metadata,
        prim_paths: List[Sdf.Path],
        additional_label_kwargs=None,
        additional_widget_kwargs=None,
    ):
        with ui.HStack(spacing=HORIZONTAL_SPACING):
            model_kwargs = get_model_kwargs(additional_widget_kwargs)
            model_cls = get_model_cls(UsdAttributeModel, additional_widget_kwargs)
            model = model_cls(stage, [path.AppendProperty(attr_name) for path in prim_paths], False, metadata, **model_kwargs)
            settings = carb.settings.get_settings()
            left_aligned = settings.get("ext/omni.kit.window.property/checkboxAlignment") == "left"

            if not left_aligned:
                if not additional_label_kwargs:
                    additional_label_kwargs = {}
                additional_label_kwargs["width"] = 0
            label = cls._create_label(attr_name, metadata, additional_label_kwargs)
            if not left_aligned:
                ui.Spacer(width=10)
                ui.Line(style={"color": 0x338A8777}, width=ui.Fraction(1))
                ui.Spacer(width=5)
            with ui.VStack(width=10):
                ui.Spacer()
                widget_kwargs = {"width": 10, "height": 0, "name": "greenCheck", "model": model}
                if additional_widget_kwargs:
                    widget_kwargs.update(additional_widget_kwargs)
                with ui.ZStack():
                    with ui.Placer(offset_x=0, offset_y=-2):
                        value_widget = ui.CheckBox(**widget_kwargs)
                        value_widget.identifier = f"bool_{attr_name}"
                    with ui.Placer(offset_x=1, offset_y=-1):
                        mixed_overlay = ui.Rectangle(
                            height=8, width=8, name="mixed_overlay", alignment=ui.Alignment.CENTER, visible=False
                        )
                ui.Spacer()
            if left_aligned:
                ui.Spacer(width=5)
                ui.Line(style={"color": 0x338A8777}, width=ui.Fraction(1))
            cls._create_control_state(
                value_widget=value_widget, mixed_overlay=mixed_overlay, **widget_kwargs, label=label
            )
            return model

    @classmethod
    def _string_builder(
        cls,
        stage,
        attr_name,
        type_name,
        metadata,
        prim_paths: List[Sdf.Path],
        additional_label_kwargs=None,
        additional_widget_kwargs=None,
    ):
        model = cls._create_filepath_for_ui_type(
            stage, attr_name, metadata, prim_paths, additional_label_kwargs, additional_widget_kwargs
        )
        if model:
            return model

        with ui.HStack(spacing=HORIZONTAL_SPACING):
            label = cls._create_label(attr_name, metadata, additional_label_kwargs)
            model_kwargs = get_model_kwargs(additional_widget_kwargs)
            model_cls = get_model_cls(UsdAttributeModel, additional_widget_kwargs)
            model = model_cls(stage, [path.AppendProperty(attr_name) for path in prim_paths], False, metadata, **model_kwargs)
            widget_kwargs = {"name": "string"}
            if additional_widget_kwargs:
                widget_kwargs.update(additional_widget_kwargs)
            with ui.ZStack():
                value_widget = ui.StringField(model, **widget_kwargs)
                value_widget.identifier = f"string_{attr_name}"
                mixed_overlay = cls._create_mixed_text_overlay()
            cls._create_control_state(
                model=model, value_widget=value_widget, mixed_overlay=mixed_overlay, **widget_kwargs, label=label
            )
            return model

    @classmethod
    def _vec2_builder(
        cls,
        stage,
        attr_name,
        type_name,
        metadata,
        prim_paths: List[Sdf.Path],
        additional_label_kwargs=None,
        additional_widget_kwargs=None,
    ):  # pragma: no cover
        """
        The entire vector is built as one multi-drag field
        """
        with ui.HStack(spacing=HORIZONTAL_SPACING):
            model_kwargs = cls._get_attr_value_range_kwargs(metadata)
            model_kwargs.update(get_model_kwargs(additional_widget_kwargs))
            model_cls = get_model_cls(GfVecAttributeModel, additional_widget_kwargs)
            model = model_cls(
                stage,
                [path.AppendProperty(attr_name) for path in prim_paths],
                2,
                type_name.type,
                False,
                metadata,
                **model_kwargs,
            )
            label = cls._create_label(attr_name, metadata, additional_label_kwargs)
            widget_kwargs = {"model": model}
            widget_kwargs.update(cls._get_attr_value_soft_range_kwargs(metadata, model))
            if additional_widget_kwargs:
                widget_kwargs.update(additional_widget_kwargs)

            create_drag_fn = (
                cls._create_multi_int_drag_with_labels
                if type_name.type.typeName.endswith("i")
                else cls._create_multi_float_drag_with_labels
            )
            value_widget, mixed_overlay = create_drag_fn(
                labels=[("X", 0xFF5555AA), ("Y", 0xFF76A371)], comp_count=2, **widget_kwargs
            )
            value_widget.identifier = f"vec2_{attr_name}"
            cls._create_control_state(
                value_widget=value_widget, mixed_overlay=mixed_overlay, **widget_kwargs, label=label
            )
            return model

    @classmethod
    def _vec2_per_channel_builder(
        cls,
        stage,
        attr_name,
        type_name,
        metadata,
        prim_paths: List[Sdf.Path],
        additional_label_kwargs=None,
        additional_widget_kwargs=None,
    ):
        """
        The vector is split into components and each one has their own drag field and status control
        """
        custom_data = metadata.get(Sdf.PrimSpec.CustomDataKey, {})
        with ui.HStack(spacing=HORIZONTAL_SPACING):
            label = cls._create_label(attr_name, metadata, additional_label_kwargs)
            if (
                custom_data
                and "mdl" in custom_data
                and "type" in custom_data["mdl"]
                and custom_data["mdl"]["type"] == "bool2"
            ):
                return cls._create_bool_per_channel(
                    stage,
                    attr_name,
                    prim_paths,
                    2,
                    type_name,
                    type_name.type,
                    metadata,
                    label,
                    additional_widget_kwargs,
                )
            else:
                return cls._create_color_or_drag_per_channel(
                    stage,
                    attr_name,
                    prim_paths,
                    2,
                    type_name,
                    type_name.type,
                    metadata,
                    label,
                    additional_widget_kwargs,
                )

    @classmethod
    def _vec3_builder(
        cls,
        stage,
        attr_name,
        type_name,
        metadata,
        prim_paths: List[Sdf.Path],
        additional_label_kwargs=None,
        additional_widget_kwargs=None,
    ):  # pragma: no cover
        """
        The entire vector is built as one multi-drag field
        """
        with ui.HStack(spacing=HORIZONTAL_SPACING):
            label = cls._create_label(attr_name, metadata, additional_label_kwargs)
            return cls._create_color_or_multidrag(
                stage, attr_name, prim_paths, 3, type_name, type_name.type, metadata, label, additional_widget_kwargs
            )

    @classmethod
    def _vec3_per_channel_builder(
        cls,
        stage,
        attr_name,
        type_name,
        metadata,
        prim_paths: List[Sdf.Path],
        additional_label_kwargs=None,
        additional_widget_kwargs=None,
    ):
        """
        The vector is split into components and each one has their own drag field and status control
        """
        custom_data = metadata.get(Sdf.PrimSpec.CustomDataKey, {})
        with ui.HStack(spacing=HORIZONTAL_SPACING):
            label = cls._create_label(attr_name, metadata, additional_label_kwargs)
            if (
                custom_data
                and "mdl" in custom_data
                and "type" in custom_data["mdl"]
                and custom_data["mdl"]["type"] == "bool3"
            ):
                return cls._create_bool_per_channel(
                    stage,
                    attr_name,
                    prim_paths,
                    3,
                    type_name,
                    type_name.type,
                    metadata,
                    label,
                    additional_widget_kwargs,
                )
            else:
                return cls._create_color_or_drag_per_channel(
                    stage,
                    attr_name,
                    prim_paths,
                    3,
                    type_name,
                    type_name.type,
                    metadata,
                    label,
                    additional_widget_kwargs,
                )

    @classmethod
    def _vec4_builder(
        cls,
        stage,
        attr_name,
        type_name,
        metadata,
        prim_paths: List[Sdf.Path],
        additional_label_kwargs=None,
        additional_widget_kwargs=None,
    ):  # pragma: no cover
        """
        The entire vector is built as one multi-drag field
        """
        with ui.HStack(spacing=HORIZONTAL_SPACING):
            label = cls._create_label(attr_name, metadata, additional_label_kwargs)
            return cls._create_color_or_multidrag(
                stage, attr_name, prim_paths, 4, type_name, type_name.type, metadata, label, additional_widget_kwargs
            )

    @classmethod
    def _vec4_per_channel_builder(
        cls,
        stage,
        attr_name,
        type_name,
        metadata,
        prim_paths: List[Sdf.Path],
        additional_label_kwargs=None,
        additional_widget_kwargs=None,
    ):
        """
        The vector is split into components and each one has their own drag field and status control
        """
        custom_data = metadata.get(Sdf.PrimSpec.CustomDataKey, {})
        with ui.HStack(spacing=HORIZONTAL_SPACING):
            label = cls._create_label(attr_name, metadata, additional_label_kwargs)
            if (
                custom_data
                and "mdl" in custom_data
                and "type" in custom_data["mdl"]
                and custom_data["mdl"]["type"] == "bool4"
            ):
                return cls._create_bool_per_channel(
                    stage,
                    attr_name,
                    prim_paths,
                    4,
                    type_name,
                    type_name.type,
                    metadata,
                    label,
                    additional_widget_kwargs,
                )
            else:
                return cls._create_color_or_drag_per_channel(
                    stage,
                    attr_name,
                    prim_paths,
                    4,
                    type_name,
                    type_name.type,
                    metadata,
                    label,
                    additional_widget_kwargs,
                )

    @classmethod
    def _tftoken_builder(
        cls,
        stage,
        attr_name,
        type_name,
        metadata,
        prim_paths: List[Sdf.Path],
        additional_label_kwargs=None,
        additional_widget_kwargs=None,
    ):
        model = cls._create_filepath_for_ui_type(
            stage, attr_name, metadata, prim_paths, additional_label_kwargs, additional_widget_kwargs
        )
        if model:
            return model

        with ui.HStack(spacing=HORIZONTAL_SPACING):
            model = None
            label = cls._create_label(attr_name, metadata, additional_label_kwargs)
            tokens = metadata.get("allowedTokens")
            if tokens is not None and len(tokens) > 0:
                model_kwargs = get_model_kwargs(additional_widget_kwargs)
                model_cls = get_model_cls(TfTokenAttributeModel, additional_widget_kwargs)
                model = model_cls(stage, [path.AppendProperty(attr_name) for path in prim_paths], False, metadata, **model_kwargs)
                widget_kwargs = {"name": "choices"}
                if additional_widget_kwargs:
                    widget_kwargs.update(additional_widget_kwargs)
                with ui.ZStack():
                    value_widget = ui.ComboBox(model, **widget_kwargs)
                    mixed_overlay = cls._create_mixed_text_overlay()
            else:
                model_kwargs = get_model_kwargs(additional_widget_kwargs)
                model_cls = get_model_cls(
                    UsdAttributeModel, additional_widget_kwargs, key="no_allowed_tokens_model_cls"
                )
                model = model_cls(stage, [path.AppendProperty(attr_name) for path in prim_paths], False, metadata, **model_kwargs)
                widget_kwargs = {"name": "models"}
                if additional_widget_kwargs:
                    widget_kwargs.update(additional_widget_kwargs)
                with ui.ZStack():
                    value_widget = ui.StringField(model, **widget_kwargs)
                    mixed_overlay = cls._create_mixed_text_overlay()
            value_widget.identifier = f"token_{attr_name}"
            cls._create_control_state(
                model=model, value_widget=value_widget, mixed_overlay=mixed_overlay, **widget_kwargs, label=label
            )
            return model

    @classmethod
    def _build_asset_checkpoint_ui(cls, model, frame):
        try:
            from .versioning_helper import VersioningHelper

            absolute_asset_path = model.get_resolved_path()
            if VersioningHelper.is_versioning_enabled() and absolute_asset_path:
                # Use checkpoint widget in the drop down menu for more detailed information
                from omni.kit.widget.versioning.checkpoint_combobox import CheckpointCombobox

                spacer = ui.Spacer(height=5)
                stack = ui.HStack()
                with stack:
                    UsdPropertiesWidgetBuilder._create_label("Checkpoint", additional_label_kwargs={"width": 80})

                    def on_selection_changed(model_or_item, model):
                        url = model.get_resolved_path()
                        try:
                            from omni.kit.widget.versioning.checkpoints_model import CheckpointItem

                            if isinstance(model_or_item, CheckpointItem):
                                checkpoint = model_or_item.get_relative_path()
                            elif model_or_item is None:
                                checkpoint = ""
                        except Exception as e:
                            pass

                        client_url = omni.client.break_url(url)
                        new_path = omni.client.make_url(
                            scheme=client_url.scheme,
                            user=client_url.user,
                            host=client_url.host,
                            port=client_url.port,
                            path=client_url.path,
                            query=checkpoint,
                            fragment=client_url.fragment,
                        )
                        if url != new_path:
                            model.set_value(new_path)
                            frame.rebuild()

                    checkpoint_combobox = CheckpointCombobox(
                        absolute_asset_path, lambda si, m=model: on_selection_changed(si, m)
                    )

                    # reset button
                    def reset_func(model):
                        on_selection_changed(None, model)
                        frame.rebuild()

                    checkpoint = ""
                    client_url = omni.client.break_url(model.get_resolved_path())
                    if client_url.query:
                        _, checkpoint = omni.client.get_branch_and_checkpoint_from_query(client_url.query)
                    ui.Spacer(width=4)
                    ui.Image(
                        f"{ICON_PATH}/Default value.svg" if checkpoint == "" else f"{ICON_PATH}/Changed value.svg",
                        mouse_pressed_fn=lambda x, y, b, a, m=model: reset_func(m),
                        width=12,
                        height=18,
                        tooltip="Reset Checkpoint" if checkpoint else "",
                    )

                    def on_have_server_info(server: str, support_checkpoint: bool, ui_items: list):
                        if not support_checkpoint:
                            for item in ui_items:
                                item.visible = False

                    VersioningHelper.check_server_checkpoint_support(
                        VersioningHelper.extract_server_from_url(absolute_asset_path),
                        lambda s, c, i=[spacer, stack]: on_have_server_info(s, c, i),
                    )

                    return None
        except ImportError as e:
            # If the widget is not available, create a simple combo box instead
            carb.log_warn(f"Checkpoint widget in Asset is not available due to: {e}")

    @classmethod
    def _sdf_asset_path_builder(
        cls,
        stage,
        attr_name,
        type_name,
        metadata,
        prim_paths: List[Sdf.Path],
        additional_label_kwargs=None,
        additional_widget_kwargs=None,
    ):
        model_kwargs = get_model_kwargs(additional_widget_kwargs)
        model_cls = get_model_cls(SdfAssetPathAttributeModel, additional_widget_kwargs)
        model = model_cls(stage, [path.AppendProperty(attr_name) for path in prim_paths], False, metadata, **model_kwargs)

        # List of models. It's possible that the path is texture related path, which
        # will return colorSpace model also.
        return cls._create_path_widget(
            model, stage, attr_name, metadata, prim_paths, additional_label_kwargs, additional_widget_kwargs
        )

    @classmethod
    def _sdf_asset_path_array_builder(
        cls,
        stage,
        attr_name,
        type_name,
        metadata,
        prim_paths: List[Sdf.Path],
        additional_label_kwargs=None,
        additional_widget_kwargs=None,
    ):
        widget_kwargs = {"name": "models"}
        if additional_widget_kwargs:
            widget_kwargs.update(additional_widget_kwargs)

        models = []

        class SdfAssetPathDelegate(ui.AbstractItemDelegate):
            """
            Delegate is the representation layer. TreeView calls the methods
            of the delegate to create custom widgets for each item.
            """

            def __init__(self):
                super().__init__()

            def build_branch(self, model, item, column_id, level, expanded):
                """Create a branch widget that opens or closes subtree"""
                pass

            def build_widget(self, model, item, column_id, level, expanded):
                """Create a widget per column per item"""
                with ui.VStack():
                    ui.Spacer(height=2)
                    with ui.ZStack():
                        ui.Rectangle(name="backdrop")
                        frame = ui.Frame(
                            height=0,
                            spacing=5,
                            style={
                                "Frame": {"margin_width": 2, "margin_height": 2},
                                "Button": {"background_color": 0x0},
                            },
                        )
                        with frame:
                            (item_value_model, value_model) = model.get_item_value_model(item, column_id)

                            extra_widgets = []
                            with ui.HStack(spacing=HORIZONTAL_SPACING, width=ui.Percent(100)):
                                with ui.VStack(spacing=0, height=ui.Percent(100), width=0):
                                    ui.Spacer()
                                    # Create ||| grab area
                                    grab = ui.HStack(
                                        identifier=f"sdf_asset_array_{attr_name}[{item_value_model.index}].reorder_grab",
                                        height=LABEL_HEIGHT,
                                    )
                                    with grab:
                                        for i in range(3):
                                            ui.Line(
                                                width=3,
                                                alignment=ui.Alignment.H_CENTER,
                                                name="grab",
                                            )
                                    ui.Spacer()

                                # do a content_clipping for the rest of the widget so only dragging on the grab triggers
                                # reorder
                                with ui.HStack(content_clipping=1):
                                    widget_kwargs["model"] = item_value_model
                                    cls._build_path_field(
                                        item_value_model, stage, attr_name, widget_kwargs, frame, extra_widgets
                                    )

                                    def remove_entry(index: int, value_model: UsdAttributeModel):
                                        value = list(value_model.get_value())
                                        new_value = value[:index] + value[index + 1 :]
                                        value_model.set_value(new_value)

                                    remove_style = {
                                        "image_url": str(ICON_PATH.joinpath("remove.svg")),
                                        "margin": 0,
                                        "padding": 0,
                                    }
                                    ui.Spacer(width=HORIZONTAL_SPACING)
                                    ui.Button(
                                        "",
                                        width=12,
                                        height=ui.Percent(100),
                                        fill_policy=ui.FillPolicy.PRESERVE_ASPECT_FIT,
                                        clicked_fn=lambda index=item_value_model.index, value_model=value_model: remove_entry(
                                            index, value_model
                                        ),
                                        name="remove",
                                        style=remove_style,
                                        tooltip="Remove Asset",
                                        identifier=f"sdf_asset_array_{attr_name}[{item_value_model.index}].remove",
                                    )

                    ui.Spacer(height=2)

        with ui.HStack(spacing=HORIZONTAL_SPACING):
            label = cls._create_label(attr_name, metadata, additional_label_kwargs)

            with ui.VStack():
                delegate_cls = get_model_cls(SdfAssetPathDelegate, additional_widget_kwargs, key="delegate_cls")
                delegate = delegate_cls()
                model_cls = get_model_cls(SdfAssetPathArrayAttributeItemModel, additional_widget_kwargs)
                item_model = model_cls(
                    stage, [path.AppendProperty(attr_name) for path in prim_paths], False, metadata, delegate
                )

                # content_clipping so drag n drop in tree view doesn't scroll the outer frame
                tree_frame_stack = ui.HStack(spacing=HORIZONTAL_SPACING, content_clipping=1)
                with tree_frame_stack:
                    with ui.Frame(height=0):
                        tree_view = ui.TreeView(
                            item_model,
                            delegate=delegate,
                            root_visible=False,
                            header_visible=False,
                            drop_between_items=True,
                            style={
                                "TreeView:selected": {"background_color": 0x00},
                                "TreeView": {"background_color": 0xFFFFFFFF},  # reorder indicator
                            },
                        )
                        tree_view.identifier = f"sdf_asset_array_{attr_name}"

                    ui.Spacer(width=12)
                with ui.HStack(spacing=HORIZONTAL_SPACING, height=LABEL_HEIGHT):
                    extra_widgets = []
                    with ui.ZStack():

                        def assign_value_fn(model, path):
                            value = list(model.get_value())
                            value.append(Sdf.AssetPath(path))
                            model.set_value(Sdf.AssetPathArray(value))

                        button = ui.Button(
                            f"{_get_plus_glyph()} Add Asset...",
                            clicked_fn=lambda model_weak=weakref.ref(item_model.value_model), stage_weak=weakref.ref(
                                stage
                            ): show_asset_file_picker(
                                "Select Asset...",
                                assign_value_fn,
                                model_weak,
                                stage_weak,
                                multi_selection=True,
                                on_selected_fn=cls._assign_asset_path_value
                            ),
                        )
                        button.identifier = f"sdf_asset_array_{attr_name}.add_asset"
                        button.set_accept_drop_fn(
                            lambda url, model_weak=weakref.ref(item_model.value_model): cls.can_accept_file_drop(
                                url, model_weak, True
                            )
                        )

                        def on_drop_fn(event, model_weak):
                            model_weak = model_weak()
                            if not model_weak:
                                return

                            paths = event.mime_data.split("\n")
                            with omni.kit.undo.group():
                                for path in paths:
                                    path = cls._convert_asset_path(path)
                                    assign_value_fn(model_weak, path)

                        button.set_drop_fn(
                            lambda event, model_weak=weakref.ref(item_model.value_model): on_drop_fn(event, model_weak)
                        )
                        extra_widgets.append(button)
                        mixed_overlay = cls._create_mixed_text_overlay(content_clipping=1)

                    def on_model_value_changed(model: UsdAttributeModel):
                        value = model.get_value()
                        # hide treeview if mixed-editing or no node
                        tree_frame_stack.visible = not model.is_ambiguous() and bool(value)
                        button.visible = not model.is_ambiguous()

                    item_model.value_model.add_value_changed_fn(on_model_value_changed)
                    on_model_value_changed(item_model.value_model)

                    cls._create_control_state(
                        item_model.value_model,
                        value_widget=tree_view,
                        mixed_overlay=mixed_overlay,
                        extra_widgets=extra_widgets,
                        **widget_kwargs,
                        label=label,
                    )

            models.append(item_model)
            return models

    @classmethod
    def _get_alignment(cls):
        settings = carb.settings.get_settings()
        return (
            ui.Alignment.RIGHT
            if settings.get("/ext/omni.kit.window.property/labelAlignment") == "right"
            else ui.Alignment.LEFT
        )

    @classmethod
    def _create_label(cls, attr_name, metadata=None, additional_label_kwargs=None):
        alignment = cls._get_alignment()
        label_kwargs = {
            "name": "label",
            "word_wrap": True,
            "width": LABEL_WIDTH,
            "height": LABEL_HEIGHT,
            "alignment": alignment,
        }
        if get_ui_style() == "NvidiaLight":
            label_kwargs["width"] = LABEL_WIDTH_LIGHT
        if additional_label_kwargs:
            label_kwargs.update(additional_label_kwargs)
        if metadata and "tooltip" not in label_kwargs:
            label_kwargs["tooltip"] = cls._generate_tooltip_string(attr_name, metadata)
        label = ui.Label(cls._get_display_name(attr_name, metadata), **label_kwargs)
        ui.Spacer(width=5)
        return label

    @classmethod
    def _create_text_label(cls, attr_name, metadata=None, additional_label_kwargs=None):
        alignment = cls._get_alignment()
        label_kwargs = {
            "name": "label",
            "word_wrap": True,
            "width": LABEL_WIDTH,
            "height": LABEL_HEIGHT,
            "alignment": alignment,
        }

        def open_url(url):
            import webbrowser

            webbrowser.open(url)

        if get_ui_style() == "NvidiaLight":
            label_kwargs["width"] = LABEL_WIDTH_LIGHT
        if additional_label_kwargs:
            label_kwargs.update(additional_label_kwargs)
        if metadata and "tooltip" not in label_kwargs:
            label_kwargs["tooltip"] = cls._generate_tooltip_string(attr_name, metadata)
        display_name = cls._get_display_name(attr_name, metadata)

        if display_name.startswith("http"):
            label_kwargs["name"] = "url"
            label = ui.StringField(**label_kwargs)
            label.model.set_value(display_name)
            label.set_mouse_pressed_fn(lambda x, y, b, m, url=display_name: open_url(url))
        else:
            label = ui.StringField(**label_kwargs)
            label.model.set_value(display_name)

        ui.Spacer(width=5)
        return label

    @classmethod
    def _generate_tooltip_string(cls, attr_name, metadata):
        doc_string = metadata.get(Sdf.PropertySpec.DocumentationKey)
        type_name = cls._get_type_name(metadata)
        tooltip = f"{attr_name} ({type_name})" if not doc_string else f"{attr_name} ({type_name})\n\t\t{doc_string}"
        return tooltip

    @classmethod
    def _create_attribute_context_menu(cls, widget, model, comp_index=-1):
        def show_attribute_context_menu(b, widget_ref, model_ref):
            if b != 1:
                return

            if not model_ref:
                return

            if not widget_ref:
                return

            event = AttributeContextMenuEvent(
                widget_ref,
                model.get_attribute_paths(),
                model_ref._stage,
                model_ref.get_current_time_code(),
                model_ref,
                comp_index,
            )
            AttributeContextMenu.get_instance().on_mouse_event(event)

        widget.set_mouse_pressed_fn(
            lambda x, y, b, _: show_attribute_context_menu(b, weakref.proxy(widget), weakref.proxy(model))
        )

    ###### helper funcs ######

    @staticmethod
    def _get_attr_value_range(metadata):
        custom_data = metadata.get(Sdf.PrimSpec.CustomDataKey, {})
        range = custom_data.get("range", {})
        range_min = range.get("min", 0)
        range_max = range.get("max", 0)

        # TODO: IMGUI DragScalarN only support scalar range for all vector component.
        # Need to change it to support per component range.
        if hasattr(range_min, "__getitem__"):
            range_min = range_min[0]
        if hasattr(range_max, "__getitem__"):
            range_max = range_max[0]

        return range_min, range_max

    @staticmethod
    def _get_attr_value_ui_type(metadata):
        custom_data = metadata.get(Sdf.PrimSpec.CustomDataKey, {})
        return custom_data.get("uiType")

    @classmethod
    def _setup_soft_float_dynamic_range(cls, attr_name, metadata, default_step, model, widget):
        soft_range_min, soft_range_max = cls._get_attr_value_soft_range(metadata, model)
        if soft_range_min < soft_range_max:

            def on_end_edit(model, widget, attr_name, metadata, soft_range_min, soft_range_max):
                default_range = True

                value = model.get_value()
                # TODO: IMGUI DragScalarN only support scalar range for all vector component.
                # Need to change it to support per component range.
                if hasattr(value, "__getitem__"):
                    value = value[0]

                if model._soft_range_min != None and model._soft_range_min < soft_range_min:
                    soft_range_min = model._soft_range_min
                    default_range = False
                if model._soft_range_max != None and model._soft_range_max > soft_range_max:
                    soft_range_max = model._soft_range_max
                    default_range = False

                if value < soft_range_min:
                    soft_range_min = value
                    model.set_soft_range_userdata(soft_range_min, soft_range_max)
                    default_range = False

                if value > soft_range_max:
                    soft_range_max = value
                    model.set_soft_range_userdata(soft_range_min, soft_range_max)
                    default_range = False

                widget.min = soft_range_min
                widget.max = soft_range_max
                if not default_range:
                    if not attr_name in cls.default_range_steps:
                        cls.default_range_steps[attr_name] = widget.step
                    widget.step = max(0.1, (soft_range_max - soft_range_min) / 1000.0)

            def on_set_default(model, widget, attr_name, metadata):
                soft_range_min, soft_range_max = cls._get_attr_value_soft_range(metadata, model, False)
                widget.min = soft_range_min
                widget.max = soft_range_max
                if attr_name in cls.default_range_steps:
                    widget.step = cls.default_range_steps[attr_name]

            model.add_end_edit_fn(
                lambda m, w=widget, n=attr_name, md=metadata, min=soft_range_min, max=soft_range_max: on_end_edit(
                    m, w, n, md, min, max
                )
            )
            model.set_on_set_default_fn(lambda m=model, w=widget, n=attr_name, md=metadata: on_set_default(m, w, n, md))

    @classmethod
    def _get_attr_value_range_kwargs(cls, metadata):
        kwargs = {}
        min, max = cls._get_attr_value_range(metadata)

        # only set range if soft_range is valid (min < max)
        if min < max:
            kwargs["min"] = min
            kwargs["max"] = max

        return kwargs

    @staticmethod
    def _get_attr_value_soft_range(metadata, model=None, use_override=True):
        custom_data = metadata.get(Sdf.PrimSpec.CustomDataKey, {})
        soft_range = custom_data.get("soft_range", {})
        soft_range_min = soft_range.get("min", 0)
        soft_range_max = soft_range.get("max", 0)

        # TODO: IMGUI DragScalarN only support scalar range for all vector component.
        # Need to change it to support per component range.
        if hasattr(soft_range_min, "__getitem__"):
            soft_range_min = soft_range_min[0]
        if hasattr(soft_range_max, "__getitem__"):
            soft_range_max = soft_range_max[0]

        if model and use_override:
            value = model.get_value()
            if hasattr(value, "__getitem__"):
                value = value[0]

            if model._soft_range_min != None and model._soft_range_min < soft_range_min:
                soft_range_min = model._soft_range_min
            if model._soft_range_max != None and model._soft_range_max > soft_range_max:
                soft_range_max = model._soft_range_max

            if soft_range_min < soft_range_max:
                if value < soft_range_min:
                    soft_range_min = value
                    model.set_soft_range_userdata(soft_range_min, soft_range_max)
                if value > soft_range_max:
                    soft_range_max = value
                    model.set_soft_range_userdata(soft_range_min, soft_range_max)

        return soft_range_min, soft_range_max

    @classmethod
    def _get_attr_value_soft_range_kwargs(cls, metadata, model=None):
        kwargs = {}
        min, max = cls._get_attr_value_soft_range(metadata, model)

        # only set soft_range if soft_range is valid (min < max)
        if min < max:
            kwargs["min"] = min
            kwargs["max"] = max
            model.update_control_state()

        return kwargs

    @staticmethod
    def _create_drag_or_slider(drag_widget, slider_widget, **kwargs):
        if "min" in kwargs and "max" in kwargs:
            range_min = kwargs["min"]
            range_max = kwargs["max"]
            if range_max - range_min < 100:
                return slider_widget(name="value", **kwargs)
            else:
                if "step" not in kwargs:
                    kwargs["step"] = max(0.1, (range_max - range_min) / 1000.0)
        else:
            if "step" not in kwargs:
                kwargs["step"] = 0.1

        # If range is too big or no range, don't use a slider
        return drag_widget(name="value", **kwargs)

    @classmethod
    def _create_multi_float_drag_with_labels(cls, model, labels, comp_count, **kwargs):  # pragma: no cover
        return cls._create_multi_drag_with_labels(ui.MultiFloatDragField, model, labels, comp_count, **kwargs)

    @classmethod
    def _create_multi_int_drag_with_labels(cls, model, labels, comp_count, **kwargs):  # pragma: no cover
        return cls._create_multi_drag_with_labels(ui.MultiIntDragField, model, labels, comp_count, **kwargs)

    @classmethod
    def _create_multi_drag_with_labels(cls, drag_field_widget, model, labels, comp_count, **kwargs):  # pragma: no cover
        RECT_WIDTH = 13
        SPACING = 4
        with ui.ZStack():
            with ui.HStack():
                ui.Spacer(width=RECT_WIDTH)
                widget_kwargs = {"name": "multivalue", "h_spacing": RECT_WIDTH + SPACING}
                widget_kwargs.update(kwargs)
                value_widget = drag_field_widget(model, **widget_kwargs)
            with ui.HStack():
                for i in range(comp_count):
                    if i != 0:
                        ui.Spacer(width=SPACING)
                    label = labels[i]
                    with ui.ZStack(width=RECT_WIDTH + 1):
                        ui.Rectangle(name="vector_label", style={"background_color": label[1]})
                        ui.Label(label[0], name="vector_label", alignment=ui.Alignment.CENTER)
                    ui.Spacer()
            mixed_overlay = []
            with ui.HStack():
                for i in range(comp_count):
                    ui.Spacer(width=RECT_WIDTH + SPACING)
                    item_model = model.get_item_value_model(model.get_item_children(None)[i], 0)
                    mixed_overlay.append(cls._create_mixed_text_overlay(item_model, model, i))

        return value_widget, mixed_overlay

    @classmethod
    def _create_float_drag_per_channel_with_labels_and_control(cls, models, metadata, labels, **kwargs):
        return cls._create_drag_per_channel_with_labels_and_control(
            ui.FloatDrag, ui.FloatSlider, models, metadata, labels, **kwargs
        )

    @classmethod
    def _create_int_drag_per_channel_with_labels_and_control(cls, models, metadata, labels, **kwargs):
        return cls._create_drag_per_channel_with_labels_and_control(
            ui.IntDrag, ui.IntSlider, models, metadata, labels, **kwargs
        )

    @classmethod
    def _create_drag_per_channel_with_labels_and_control(
        cls, drag_field_widget, slider_field_widget, models, metadata, labels, **kwargs
    ):
        RECT_WIDTH = 13
        SPACING = 4

        hstack = ui.HStack()
        with hstack:
            for i, model in enumerate(models):
                with ui.HStack():
                    if i != 0:
                        ui.Spacer(width=SPACING)
                    label = labels[i]
                    with ui.ZStack(width=RECT_WIDTH + 1):
                        ui.Rectangle(name="vector_label", style={"background_color": label[1]})
                        ui.Label(label[0], name="vector_label", alignment=ui.Alignment.CENTER)
                    widget_kwargs = {"model": model}
                    widget_kwargs.update(cls._get_attr_value_soft_range_kwargs(metadata, model))
                    widget_kwargs.update(**kwargs)
                    soft_range_min, soft_range_max = cls._get_attr_value_soft_range(metadata, model)
                    with ui.ZStack():
                        if soft_range_min < soft_range_max:
                            value_widget = cls._create_drag_or_slider(
                                drag_field_widget, drag_field_widget, **widget_kwargs
                            )
                            cls._setup_soft_float_dynamic_range(
                                model._object_paths[-1], metadata, value_widget.step, model, value_widget
                            )
                        else:
                            value_widget = cls._create_drag_or_slider(
                                drag_field_widget, slider_field_widget, **widget_kwargs
                            )
                        mixed_overlay = cls._create_mixed_text_overlay(model, model)

                    ui.Spacer(width=SPACING)
                    widget_kwargs["widget_comp_index"] = i
                    cls._create_control_state(value_widget=value_widget, mixed_overlay=mixed_overlay, **widget_kwargs)
        return hstack

    @classmethod
    def _create_color_or_multidrag(
        cls,
        stage,
        attr_name,
        prim_paths,
        comp_count,
        type_name,
        tf_type,
        metadata,
        label,
        additional_widget_kwargs=None,
    ):  # pragma: no cover
        model_kwargs = cls._get_attr_value_range_kwargs(metadata)
        model_kwargs.update(get_model_kwargs(additional_widget_kwargs))
        extra_widgets = []
        model_cls = get_model_cls(GfVecAttributeModel, additional_widget_kwargs)
        model = model_cls(
            stage,
            [path.AppendProperty(attr_name) for path in prim_paths],
            comp_count,
            tf_type,
            False,
            metadata,
            **model_kwargs,
        )
        ui_type = ""
        if comp_count == 3 or comp_count == 4:
            ui_type = cls._get_attr_value_ui_type(metadata)

        if (
            type_name == Sdf.ValueTypeNames.Color3h
            or type_name == Sdf.ValueTypeNames.Color3f
            or type_name == Sdf.ValueTypeNames.Color3d
            or type_name == Sdf.ValueTypeNames.Color4h
            or type_name == Sdf.ValueTypeNames.Color4f
            or type_name == Sdf.ValueTypeNames.Color4d
            or ui_type == "color"
        ):
            widget_kwargs = {"min": 0.0, "max": 2.0, "model": model}
            if additional_widget_kwargs:
                widget_kwargs.update(additional_widget_kwargs)
            with ui.HStack(spacing=4):
                value_widget, mixed_overlay = cls._create_multi_float_drag_with_labels(
                    labels=[("R", 0xFF5555AA), ("G", 0xFF76A371), ("B", 0xFFA07D4F), ("A", 0xFFFFFFFF)],
                    comp_count=comp_count,
                    **widget_kwargs,
                )
                extra_widgets.append(ui.ColorWidget(model, width=65, height=0))
        else:
            widget_kwargs = {"model": model}
            widget_kwargs.update(cls._get_attr_value_soft_range_kwargs(metadata, model))
            if additional_widget_kwargs:
                widget_kwargs.update(additional_widget_kwargs)
            create_drag_fn = (
                cls._create_multi_int_drag_with_labels
                if type_name.type.typeName.endswith("i")
                else cls._create_multi_float_drag_with_labels
            )
            value_widget, mixed_overlay = create_drag_fn(
                labels=[("X", 0xFF5555AA), ("Y", 0xFF76A371), ("Z", 0xFFA07D4F), ("W", 0xFFFFFFFF)],
                comp_count=comp_count,
                **widget_kwargs,
            )
            value_widget.identifier = f"color_{attr_name}"
        cls._create_control_state(
            value_widget=value_widget,
            mixed_overlay=mixed_overlay,
            extra_widgets=extra_widgets,
            **widget_kwargs,
            label=label,
        )
        return model

    @classmethod
    def _create_bool_per_channel(
        cls,
        stage,
        attr_name,
        prim_paths,
        comp_count,
        type_name,
        tf_type,
        metadata,
        label,
        additional_widget_kwargs=None,
    ):
        model_kwargs = cls._get_attr_value_range_kwargs(metadata)
        model_kwargs.update(get_model_kwargs(additional_widget_kwargs))
        models = []

        # We need a UsdAttributeModel here for context menu and default button:
        model_cls = get_model_cls(UsdAttributeModel, additional_widget_kwargs)
        model = model_cls(
            stage,
            [path.AppendProperty(attr_name) for path in prim_paths],
            False,
            metadata,
            False,
            **model_kwargs,
        )

        def on_model_value_changed(model: UsdAttributeModel):
            model._update_value()

        model.add_value_changed_fn(on_model_value_changed)

        single_channel_model_cls = get_model_cls(
            GfVecAttributeSingleChannelModel, additional_widget_kwargs, "single_channel_model_cls"
        )
        for i in range(comp_count):
            models.append(
                single_channel_model_cls(
                    stage,
                    [path.AppendProperty(attr_name) for path in prim_paths],
                    i,
                    False,
                    metadata,
                    False,
                    **model_kwargs,
                )
            )

            widget_kwargs = {}
            if additional_widget_kwargs:
                widget_kwargs.update(additional_widget_kwargs)

        mixed_overlay = []
        with ui.ZStack():
            with ui.HStack(spacing=4, width=32):
                with ui.HStack(spacing=HORIZONTAL_SPACING, identifier=f"boolean_per_channel_{attr_name}"):
                    for i, channel_model in enumerate(models):
                        with ui.VStack(width=10):
                            ui.Spacer()
                            with ui.ZStack():
                                with ui.Placer(offset_x=0, offset_y=-2):
                                    with ui.ZStack():
                                        ui.CheckBox(width=10, height=0, name="greenCheck", model=channel_model)
                                        mixed_overlay.append(
                                            ui.Rectangle(
                                                width=12,
                                                height=12,
                                                name="mixed_overlay",
                                                alignment=ui.Alignment.CENTER,
                                                visible=False,
                                            )
                                        )
                            ui.Spacer()

        ui.Line(style={"color": 0x338A8777}, width=ui.Fraction(1))
        ui.Spacer(width=5)
        models.append(model)
        cls._create_control_state(model=model, mixed_overlay=mixed_overlay, label=label)
        model._update_value()  # trigger an initial refresh. Only needed if the model is not assigned to a widget
        return models

    @classmethod
    def _create_color_or_drag_per_channel(
        cls,
        stage,
        attr_name,
        prim_paths,
        comp_count,
        type_name,
        tf_type,
        metadata,
        label,
        additional_widget_kwargs=None,
    ):
        model_kwargs = cls._get_attr_value_range_kwargs(metadata)
        model_kwargs.update(get_model_kwargs(additional_widget_kwargs))
        extra_widgets = []
        models = []

        # We need a GfVecAttributeModel here for:
        # If attribute is a color, the color picker widget needs this model type
        # When copying value from attribute label, this model is used to fetch value.
        model_cls = get_model_cls(GfVecAttributeModel, additional_widget_kwargs)
        model = model_cls(
            stage,
            [path.AppendProperty(attr_name) for path in prim_paths],
            comp_count,
            tf_type,
            False,
            metadata,
            **model_kwargs,
        )

        single_channel_model_cls = get_model_cls(
            GfVecAttributeSingleChannelModel, additional_widget_kwargs, "single_channel_model_cls"
        )
        for i in range(comp_count):
            models.append(
                single_channel_model_cls(
                    stage,
                    [path.AppendProperty(attr_name) for path in prim_paths],
                    i,
                    False,
                    metadata,
                    False,
                    **model_kwargs,
                )
            )

        ui_type = ""
        if comp_count == 3 or comp_count == 4:
            ui_type = cls._get_attr_value_ui_type(metadata)

        if (
            type_name == Sdf.ValueTypeNames.Color3h
            or type_name == Sdf.ValueTypeNames.Color3f
            or type_name == Sdf.ValueTypeNames.Color3d
            or type_name == Sdf.ValueTypeNames.Color4h
            or type_name == Sdf.ValueTypeNames.Color4f
            or type_name == Sdf.ValueTypeNames.Color4d
            or ui_type == "color"
        ):
            widget_kwargs = {"min": 0.0, "max": 2.0}
            if additional_widget_kwargs:
                widget_kwargs.update(additional_widget_kwargs)
            with ui.HStack(spacing=4):
                labels = [("R", 0xFF5555AA), ("G", 0xFF76A371), ("B", 0xFFA07D4F), ("A", 0xFFFFFFFF)]
                value_widget = cls._create_float_drag_per_channel_with_labels_and_control(
                    models, metadata, labels, **widget_kwargs
                )
                color_picker = ui.ColorWidget(model, width=LABEL_HEIGHT, height=0)
                extra_widgets.append(color_picker)

                # append model AFTER _create_float_drag_per_channel_with_labels_and_control call
                widget_kwargs["model"] = model
                models.append(model)
                cls._create_control_state(
                    value_widget=color_picker,
                    mixed_overlay=None,
                    extra_widgets=extra_widgets,
                    **widget_kwargs,
                    label=label,
                )

        else:
            widget_kwargs = {}
            widget_kwargs.update(cls._get_attr_value_soft_range_kwargs(metadata, model))
            if additional_widget_kwargs:
                widget_kwargs.update(additional_widget_kwargs)

            labels = [("X", 0xFF5555AA), ("Y", 0xFF76A371), ("Z", 0xFFA07D4F), ("W", 0xFFFFFFFF)]

            if type_name.type.typeName.endswith("i"):
                value_widget = cls._create_int_drag_per_channel_with_labels_and_control(
                    models, metadata, labels, **widget_kwargs
                )
            else:
                value_widget = cls._create_float_drag_per_channel_with_labels_and_control(
                    models, metadata, labels, **widget_kwargs
                )

            # append model AFTER _create_float_drag_per_channel_with_labels_and_control call
            models.append(model)
            cls._create_attribute_context_menu(label, model)

        value_widget.identifier = f"drag_per_channel_{attr_name}"
        return models

    @staticmethod
    def _get_display_name(attr_name, metadata):
        if not metadata:
            return attr_name
        return metadata.get(Sdf.PropertySpec.DisplayNameKey, attr_name)

    @staticmethod
    def _get_type_name(metadata):
        type_name = metadata.get(Sdf.PrimSpec.TypeNameKey, "unknown type")
        return Sdf.ValueTypeNames.Find(type_name)

    @staticmethod
    def _create_mixed_text_overlay(widget_model=None, model=None, comp=-1, content_clipping: bool = False):
        with ui.ZStack(alignment=ui.Alignment.CENTER):
            stack = ui.ZStack(alignment=ui.Alignment.CENTER, visible=False, content_clipping=content_clipping)
            with stack:
                ui.Rectangle(alignment=ui.Alignment.CENTER, name="mixed_overlay_text")
                ui.Label("Mixed", name="mixed_overlay", alignment=ui.Alignment.CENTER)

        if widget_model and model is not None:
            hidden_on_edit = False

            def begin_edit(_):
                nonlocal hidden_on_edit
                if stack.visible:
                    stack.visible = False
                    hidden_on_edit = True

            def end_edit(_):
                nonlocal hidden_on_edit
                if hidden_on_edit:
                    if model.is_comp_ambiguous(comp):
                        stack.visible = True
                    hidden_on_edit = False

            widget_model.add_begin_edit_fn(begin_edit)
            widget_model.add_end_edit_fn(end_edit)
        return stack

    @classmethod
    def _create_control_state(cls, model, value_widget=None, mixed_overlay=None, extra_widgets=[], **kwargs):
        # Allow widgets to be displayed without the control state 
        if kwargs.get("no_control_state"):
            return

        control_state_mgr = ControlStateManager.get_instance()

        no_default = kwargs.get("no_default", False)
        widget_comp_index = kwargs.get("widget_comp_index", -1)
        original_widget_name = value_widget.name if value_widget else ""

        original_widget_state = {}
        if value_widget:
            original_widget_state["value_widget"] = value_widget.enabled
        else:
            original_widget_state["value_widget"] = True

        for index, widget in enumerate(extra_widgets):
            original_widget_state[f"extra_widgets_{index}"] = widget.enabled

        def build_fn():
            state = model.control_state

            # no_default is actually no state
            if no_default:
                state = 0

            action, icon_path, tooltip = control_state_mgr.build_control_state(
                control_state=state,
                model=model,
                value_widget=value_widget,
                extra_widgets=extra_widgets,
                mixed_overlay=mixed_overlay,
                original_widget_name=original_widget_name,
                **kwargs,
            )

            with ui.VStack():
                ui.Spacer()
                button = ui.ImageWithProvider(
                    icon_path,
                    mouse_pressed_fn=action,
                    width=12,
                    height=5 if state == 0 else 12,  # TODO let state decide
                    tooltip=tooltip,
                )
                ui.Spacer()

                attr_paths = model.get_attribute_paths()
                if attr_paths and len(attr_paths) > 0:
                    button.identifier = f"control_state_{attr_paths[0].elementString[1:]}"

        frame = ui.Frame(build_fn=build_fn, width=0)
        model.set_on_control_state_changed_fn(frame.rebuild)

        if value_widget:
            cls._create_attribute_context_menu(value_widget, model, widget_comp_index)

        label_widget = kwargs.get("label", None)
        if label_widget:
            # When right click on label, always copy entire value
            cls._create_attribute_context_menu(label_widget, model)

    @classmethod
    def _time_code_builder(
        cls,
        stage,
        attr_name,
        type_name,
        metadata,
        prim_paths: List[Sdf.Path],
        additional_label_kwargs=None,
        additional_widget_kwargs=None,
    ):
        with ui.HStack(spacing=HORIZONTAL_SPACING):
            model_kwargs = get_model_kwargs(additional_widget_kwargs)
            model_cls = get_model_cls(SdfTimeCodeModel, additional_widget_kwargs)
            model = model_cls(stage, [path.AppendProperty(attr_name) for path in prim_paths], False, metadata, **model_kwargs)
            label = cls._create_label(attr_name, metadata, additional_label_kwargs)
            range_min, range_max = cls._get_attr_value_range(metadata)
            widget_kwargs = {"model": model}
            widget_kwargs.update(cls._get_attr_value_soft_range_kwargs(metadata, model))
            if additional_widget_kwargs:
                widget_kwargs.update(additional_widget_kwargs)
            with ui.ZStack():
                value_widget = cls._create_drag_or_slider(ui.FloatDrag, ui.FloatSlider, **widget_kwargs)
                value_widget.identifier = f"timecode_{attr_name}"
                mixed_overlay = cls._create_mixed_text_overlay()
            cls._create_control_state(
                value_widget=value_widget, mixed_overlay=mixed_overlay, **widget_kwargs, label=label
            )
            return model

    @staticmethod
    def can_accept_file_drop(payload: str, model_weak, allow_multi_files=False) -> bool:
        model_weak = model_weak()
        if not model_weak:
            return False

        urls = payload.split("\n")

        if not urls:
            return False

        if not allow_multi_files and len(urls) > 1:
            carb.log_warn(f"sdf_asset_path_builder multifile drag/drop not supported")
            return False

        custom_data = model_weak.metadata.get(Sdf.PrimSpec.CustomDataKey, {})
        file_exts_dict = custom_data.get("fileExts", {})

        for url in urls:
            accepted = False
            if file_exts_dict:
                for ext in file_exts_dict:
                    if fnmatch.fnmatch(url, ext):
                        accepted = True
                        break
                if not accepted:
                    carb.log_warn(f"Dropped file {url} does not match allowed extensions {list(file_exts_dict.keys())}")
            else:
                # TODO support filtering by file extension
                if "." in url:
                    # TODO dragging from stage view also result in a drop, which is a prim path not an asset path
                    # For now just check if dot presents in the url (indicating file extension).
                    accepted = True
            if not accepted:
                return False
        return True

    @staticmethod
    def _convert_asset_path(path: str) -> str:
        path_method = carb.settings.get_settings().get("/persistent/app/material/dragDropMaterialPath") or "relative"
        if path_method.lower() == "relative":
            if not Ar.GetResolver().IsRelativePath(path):
                path = omni.usd.make_path_relative_to_current_edit_target(path)
        return path

    @classmethod
    def _build_path_field(cls, model, stage, attr_name, widget_kwargs, frame, extra_widgets):
        with ui.HStack():

            def clear_name(widget: ui.StringField):
                widget.model.set_value("")

            def name_changed(model, widget: ui.Button, type):
                val = model.get_value()
                setattr(widget, type, not bool(val == "" or val == "@@"))

            def locate_name_changed(model, widget: ui.Button, type):
                # NOTE: changing style makes image dispear

                enabled = True
                if isinstance(model, SdfAssetPathAttributeModel):
                    enabled = model.is_valid_path()

                # update widget
                widget.enabled = enabled
                widget.visible = enabled
                widget.tooltip = "Locate File" if enabled else ""

            def assign_value_fn(model, path: str, resolved_path: str=""):
                model.set_value(path, resolved_path)

            remove_style = {
                "Button.Image::remove": {"image_url": str(ICON_PATH.joinpath("remove-text.svg"))},
                "Button.Image::remove:hovered": {"image_url": str(ICON_PATH.joinpath("remove-text-hovered.svg"))},
            }

            with ui.ZStack():
                with ui.ZStack(style=remove_style):
                    value_widget = ui.StringField(**widget_kwargs)
                    with ui.HStack():
                        ui.Spacer()
                        clear_widget_stack = ui.VStack(width=0, content_clipping=True)  # vertically center the button
                        with clear_widget_stack:
                            ui.Spacer()
                            clear_widget = ui.Button(
                                "",
                                width=20,
                                height=20,
                                fill_policy=ui.FillPolicy.PRESERVE_ASPECT_FIT,
                                clicked_fn=lambda f=value_widget: clear_name(f),
                                name="remove",
                            )
                            ui.Spacer()
                    value_widget.model.add_value_changed_fn(
                        lambda m, w=clear_widget_stack: name_changed(m, w, "visible")
                    )
                    value_widget.model._value_changed()

                value_widget.set_accept_drop_fn(
                    lambda url, model_weak=weakref.ref(model): cls.can_accept_file_drop(url, model_weak)
                )
                value_widget.set_drop_fn(
                    lambda event, model_weak=weakref.ref(model), stage_weak=weakref.ref(
                        stage
                    ): cls._assign_asset_path_value(stage_weak, model_weak, event.mime_data, assign_value_fn, frame)
                )
                value_widget.identifier = f"sdf_asset_{attr_name}" + (
                    f"[{model.index}]" if hasattr(model, "index") else ""
                )
                clear_widget.identifier = f"sdf_clear_asset_{attr_name}" + (
                    f"[{model.index}]" if hasattr(model, "index") else ""
                )
                mixed_overlay = cls._create_mixed_text_overlay()
            ui.Spacer(width=3)

            style = {"image_url": str(ICON_PATH.joinpath("small_folder.png"))}
            enabled = get_file_importer() is not None
            browse_button = ui.Button(
                style=style,
                width=20,
                tooltip="Browse..." if enabled else "File importer not available",
                clicked_fn=lambda model_weak=weakref.ref(model), stage_weak=weakref.ref(
                    stage
                ): show_asset_file_picker(
                    "Select Asset...",
                    assign_value_fn,
                    model_weak,
                    stage_weak,
                    on_selected_fn=cls._assign_asset_resolved_value
                ),
                enabled=enabled,
                identifier=f"sdf_browse_asset_{attr_name}" + (f"[{model.index}]" if hasattr(model, "index") else ""),
            )
            extra_widgets.append(browse_button)

            extra_widgets.append(ui.Spacer(width=3))

            # Button to jump to the file in Content Window
            def locate_file(model):
                async def locate_file_async(model):
                    # omni.kit.window.content_browser is optional dependency
                    try:
                        import omni.client
                        import omni.kit.app
                        import os

                        await omni.kit.app.get_app().next_update_async()

                        url = ""
                        if isinstance(model, SdfAssetPathAttributeModel):
                            url = model.get_resolved_path()
                            if url:
                                url = f"{omni.usd.correct_filename_case(os.path.dirname(url))}/{os.path.basename(url)}"
                        elif isinstance(model, ui.AbstractValueModel):
                            url = model.get_value_as_string()

                        if not url:
                            return

                        client_url = omni.client.break_url(url)
                        if client_url:
                            url = omni.client.make_url(
                                scheme=client_url.scheme,
                                user=client_url.user,
                                host=client_url.host,
                                port=client_url.port,
                                path=client_url.path,
                            )

                        import omni.kit.window.content_browser

                        instance = omni.kit.window.content_browser.get_instance()
                        instance.navigate_to(url)
                    except Exception as e:
                        carb.log_warn(f"Failed to locate file: {e}")

                asyncio.ensure_future(locate_file_async(model))

            style["image_url"] = str(ICON_PATH.joinpath("find.png"))
            enabled = True
            if isinstance(model, SdfAssetPathAttributeModel):
                enabled = model.is_valid_path()

            locate_widget = ui.Button(
                style=style,
                width=20,
                enabled=enabled,
                visible=enabled,
                tooltip="Locate File" if enabled else "",
                clicked_fn=lambda model=model: locate_file(model),
                identifier=f"sdf_locate_asset_{attr_name}" + (f"[{model.index}]" if hasattr(model, "index") else ""),
            )
            value_widget.model.add_value_changed_fn(lambda m, w=locate_widget: locate_name_changed(m, w, "enabled"))

            return value_widget, mixed_overlay

    @classmethod
    def _assign_asset_path_value(
        cls, stage_weak, model_weak, payload: str, assign_value_fn: Callable[[Any, str], None], frame=None
    ):
        stage_weak = stage_weak()
        if not stage_weak:
            return

        model_weak = model_weak()
        if not model_weak:
            return

        urls = payload.split("\n")
        with omni.kit.undo.group():
            for path in urls:
                path = cls._convert_asset_path(path)
                assign_value_fn(model_weak, path.replace("\\", "/"))

        if frame:
            frame.rebuild()

    @classmethod
    def _assign_asset_resolved_value(
        cls, stage_weak, model_weak, payload: str, assign_value_fn: Callable[[Any, str], None], frame=None
    ):
        model = model_weak()
        if not model:
            return

        urls = payload.split("\n")
        with omni.kit.undo.group():
            for path in urls:
                assign_value_fn(model, cls._convert_asset_path(path).replace("\\", "/"), path)

        if frame:
            frame.rebuild()

    @classmethod
    def _create_path_widget(
        cls, model, stage, attr_name, metadata, prim_paths, additional_label_kwargs, additional_widget_kwargs
    ):
        if "colorSpace" in metadata:
            colorspace_model = MetadataObjectModel(
                stage,
                [path.AppendProperty(attr_name) for path in prim_paths],
                False,
                metadata,
                key="colorSpace",
                default="auto",
                options=["auto", "raw", "sRGB"],
            )
        else:
            colorspace_model = None

        def build_frame(
            cls, model, colorspace_model, frame, stage, attr_name, metadata, prim_paths,
            additional_label_kwargs, additional_widget_kwargs
        ):
            extra_widgets = []

            with ui.VStack():
                with ui.HStack(spacing=HORIZONTAL_SPACING):
                    label = cls._create_label(attr_name, metadata, additional_label_kwargs)

                    widget_kwargs = {"name": "models", "model": model}
                    if additional_widget_kwargs:
                        widget_kwargs.update(additional_widget_kwargs)

                    value_widget, mixed_overlay = cls._build_path_field(
                        model, stage, attr_name, widget_kwargs, frame, extra_widgets
                    )

                    cls._create_control_state(
                        value_widget=value_widget,
                        mixed_overlay=mixed_overlay,
                        extra_widgets=extra_widgets,
                        **widget_kwargs,
                        label=label,
                    )
                if isinstance(model, SdfAssetPathAttributeModel):
                    cls._build_asset_checkpoint_ui(model, frame)

                if colorspace_model:
                    ui.Spacer(height=5)
                    with ui.HStack(spacing=HORIZONTAL_SPACING):
                        label = cls._create_label(
                            cls._get_display_name(attr_name, metadata) + " Color Space", {}, additional_label_kwargs
                        )
                        with ui.ZStack():
                            value_widget = ui.ComboBox(colorspace_model, name="choices")
                            value_widget.identifier = f"colorspace_{attr_name}"
                            mixed_overlay = cls._create_mixed_text_overlay()
                            widget_kwargs = {"no_default": True}
                        cls._create_control_state(
                            colorspace_model, value_widget, mixed_overlay, **widget_kwargs, label=label
                        )

        frame = ui.Frame(width=omni.ui.Percent(100))
        frame.set_build_fn(
            lambda: build_frame(
                cls,
                model,
                colorspace_model,
                frame,
                stage,
                attr_name,
                metadata,
                prim_paths,
                additional_label_kwargs,
                additional_widget_kwargs,
            )
        )

        if colorspace_model:
            # Returns colorspace model also so it could receive updates.
            return [model, colorspace_model]
        else:
            return [model]

    @classmethod
    def _create_filepath_for_ui_type(
        cls, stage, attr_name, metadata, prim_paths: List[Sdf.Path], additional_label_kwargs, additional_widget_kwargs
    ):
        model = None
        ui_type = cls._get_attr_value_ui_type(metadata)
        if ui_type == "filePath":
            model_kwargs = get_model_kwargs(additional_widget_kwargs)
            model_cls = get_model_cls(UsdAttributeModel, additional_widget_kwargs, key="no_allowed_tokens_model_cls")
            model = model_cls(stage, [path.AppendProperty(attr_name) for path in prim_paths], False, metadata, **model_kwargs)
            widget_kwargs = {"name": "models"}
            if additional_widget_kwargs:
                widget_kwargs.update(additional_widget_kwargs)
            cls._create_path_widget(
                model, stage, attr_name, metadata, prim_paths, additional_label_kwargs, widget_kwargs
            )
        return model
