from queue import Empty
from re import T
from typing import Any
import weakref
import asyncio
import os, functools, sys
from abc import ABC, abstractmethod
from enum import Enum
from functools import partial

import carb
import omni.ui as ui
import omni.ext
import omni.usd
import omni.timeline
import omni.kit.commands
import carb.settings

from omni.hydra.scene_api import *

from pxr import Gf, Tf, Vt, Usd, Sdf, UsdGeom
from pathlib import Path
from collections import defaultdict

from omni.kit.property.usd.usd_attribute_model import (
    FloatModel,
    UsdBase,
    UsdAttributeModel,
    GfVecAttributeModel,
    GfVecAttributeSingleChannelModel,
    GfMatrixAttributeModel,
    GfQuatAttributeModel,
    GfQuatEulerAttributeModel,
)

from .transform_model import VecAttributeModel
from omni.kit.widget.highlight_label import HighlightLabel
from omni.kit.property.usd.usd_property_widget_builder import UsdPropertiesWidgetBuilder
from omni.kit.context_menu import *
from .transform_commands import *
from . import xform_op_utils

# Settings constants from omni.kit.window.toolbar
TRANSFORM_OP_SETTING = "/app/transform/operation"
TRANSFORM_OP_MOVE = "move"
TRANSFORM_OP_ROTATE = "rotate"
TRANSFORM_OP_SCALE = "scale"
TRANSFORM_MOVE_MODE_SETTING = "/app/transform/moveMode"
TRANSFORM_ROTATE_MODE_SETTING = "/app/transform/rotateMode"
TRANSFORM_MODE_GLOBAL = "global"
TRANSFORM_MODE_LOCAL = "local"

LABEL_PADDING = 128
ICON_PATH = ""
quat_view_button_style = {
    "Button:hovered": {"background_color": 0xFF575757},
    "Button": {"background_color": 0xFF333333, "padding": 0, "stack_direction": ui.Direction.RIGHT_TO_LEFT},
    "Button.Label": {"color": 0xFF9E9E9E, "alignment": ui.Alignment.LEFT_CENTER},
    "Button.Tooltip": {"color": 0xFF9E9E9E},
    "Button.Image": {"color": 0xFFFFCC99, "alignment": ui.Alignment.CENTER},
}
euler_view_button_style = {
    "Button:hovered": {"background_color": 0xFF23211F},
    "Button": {"background_color": 0xFF23211F, "padding": 0, "stack_direction": ui.Direction.RIGHT_TO_LEFT},
    "Button.Label": {"color": 0xFFA07D4F, "alignment": ui.Alignment.LEFT_CENTER},
    "Button.Tooltip": {"color": 0xFF9E9E9E},
    "Button.Image": {"color": 0xFFFFCC99, "alignment": ui.Alignment.CENTER},
}


class USDXformOpWidget:
    def __init__(
        self, prim_paths, collapsable_frame, stage, attr_path, op_name, is_valid_op, op_order_attr_path, op_order_index,
        label_kwargs=None,
    ):
        self._prim_paths = prim_paths
        self._collapsable_frame = collapsable_frame
        self._stage = stage
        self._attr_path = attr_path
        self._op_name = op_name
        self._is_valid_op = is_valid_op
        self._op_order_attr_path = op_order_attr_path
        self._op_order_index = op_order_index
        self._model = None
        self._right_click_menu = None
        self._label_kwargs = label_kwargs if label_kwargs is not None else {}
        self.rebuild()

    def __del__(self):
        self._prim_paths = None
        self._collapsable_frame = None
        self._stage = None
        self._attr_path = None
        self._op_name = None
        self._is_valid_op = False
        self._op_order_attr_path = None
        self._op_order_index = -1
        if self._model is not None:
            if isinstance(self._model, list):
                for model in self._model:
                    model.clean()
            else:
                self._model.clean()
        self._model = None
        self._right_click_menu = None

    def rebuild(self):
        pass

    def _create_inverse_widgets(self):
        # Not create inverse widgets for non-op attribute.
        if self._op_name is None:
            return
        is_inverse_op = False if self._op_name is None else xform_op_utils.is_inverse_op(self._op_name)
        if is_inverse_op:
            ui.Label("¯¹")

    ''' I don't think this is used
    def _inverse_op(self):
        if self._op_order_attr_path is not None and self._op_order_index > -1:
            order_attr = self._stage.GetObjectAtPath(self._op_order_attr_path)
            if order_attr:
                with Sdf.ChangeBlock():
                    op_order = order_attr.Get()
                    if self._op_order_index < len(op_order):
                        op_name = op_order.__getitem__(self._op_order_index)
                        if op_name == self._op_name:
                            is_inverse_op = xform_op_utils.is_inverse_op(op_name)
                            inverse_op_name = xform_op_utils.get_inverse_op_Name(op_name, not is_inverse_op)
                            self._op_name = inverse_op_name
                            op_order.__setitem__(self._op_order_index, inverse_op_name)
                            order_attr.Set(op_order)
                            # self._window().rebuild_window()
                            self._collapsable_frame.rebuild()
    '''
    def _add_key(self):
        if self._attr_path is None or self._stage is None:
            return
        attr = self._stage.GetObjectAtPath(self._attr_path)
        if attr:
            timeline = omni.timeline.get_timeline_interface()
            current_time = timeline.get_current_time()
            current_time_code = Usd.TimeCode(
                omni.usd.get_frame_time_code(current_time, self._stage.GetTimeCodesPerSecond())
            )
            if omni.usd.attr_has_timesample_on_key(attr, current_time_code):
                return
            if attr:
                omni.usd.copy_timesamples_from_weaker_layer(self._stage, attr)
                curr_value = attr.Get(current_time_code)
                if not curr_value:
                    type_name = attr.GetTypeName()
                    value_type = type(type_name.defaultValue)
                    curr_value = value_type(attr_default_value)
                new_target = self._stage.GetEditTargetForLocalLayer(self._stage.GetEditTarget().GetLayer())
                with Usd.EditContext(self._stage, new_target):
                    attr.Set(curr_value, current_time_code)


    def _delete_op_only(self):
        if self._op_order_attr_path is not None and self._op_order_index > -1:
            omni.kit.commands.execute(
                "RemoveXformOp",
                op_order_attr_path=self._op_order_attr_path,
                op_name=self._op_name,
                op_order_index=self._op_order_index,
            )

    def _delete_op_and_attribute(self):
        if self._op_order_attr_path is not None and self._op_order_index > -1:
            omni.kit.commands.execute(
                "RemoveXformOpAndAttrbute",
                op_order_attr_path=self._op_order_attr_path,
                op_name=self._op_name,
                op_order_index=self._op_order_index,
            )
            omni.kit.window.property.get_window()._window.frame.rebuild()

    def _delete_non_op_attribute(self):
        if self._stage is not None and self._op_name is None and self._attr_path is not None:
            omni.kit.commands.execute("RemoveProperty", prop_path=self._attr_path.pathString)
            omni.kit.window.property.get_window()._window.frame.rebuild()

    def _add_non_op_attribute_to_op(self):
        if self._stage is not None and self._op_name is None and self._attr_path is not None:
            omni.kit.commands.execute("EnableXformOp", op_attr_path=self._attr_path)

    # This is to toggle the value widgets for quaternion or euler angle
    def _on_display_orient_as_rotate(self):
        self._display_orient_as_rotate = not self._display_orient_as_rotate
        if self._settings:
            self._settings.set("/persistent/app/uiSettings/DisplayOrientAsRotate", self._display_orient_as_rotate)
        if self._display_orient_as_rotate is True:
            self._quat_view.visible = False
            self._euler_view.visible = True
        else:
            self._quat_view.visible = True
            self._euler_view.visible = False

    # The orient label(button) has a different style for two modes
    def _toggle_orient_button_style(self, button):
        # About to switch to raw mode
        if self._display_orient_as_rotate is True:
            button.set_style(euler_view_button_style)
        else:
            button.set_style(quat_view_button_style)

    def _on_orient_button_clicked(self, mouse_button, widget):
        if mouse_button != 0:
            return
        self._on_display_orient_as_rotate()
        self._toggle_orient_button_style(widget)

    def _show_right_click_menu(self, button):
        if button != 1:
            return

        if self._right_click_menu is None:
            self._right_click_menu = ui.Menu("Right Menu")
        self._right_click_menu.clear()
        with self._right_click_menu:
            # ResetXformStack
            if xform_op_utils.is_reset_xform_stack_op(self._op_name):
                text = "Disable" if self._is_valid_op else "Disable Invalid Op"
                ui.MenuItem(text, triggered_fn=lambda: self._delete_op_only())
            # valid/invalid Op
            elif self._op_name is not None and self._attr_path is not None:
                # ui.MenuItem("Inverse", triggered_fn=lambda: self._inverse_op())
                text_op_only = "Disable" if self._is_valid_op else "Disable Invalid Op"
                text_op_attr = "Delete" if self._is_valid_op else "Delete Invalid Op"
                ui.MenuItem(text_op_only, triggered_fn=lambda: self._delete_op_only())
                ui.MenuItem(text_op_attr, triggered_fn=lambda: self._delete_op_and_attribute())
            # non-op attribute
            elif self._op_name is None and self._attr_path is not None:
                ui.MenuItem("Delete", triggered_fn=lambda: self._delete_non_op_attribute())
                ui.MenuItem("Enable", triggered_fn=lambda: self._add_non_op_attribute_to_op())
            # no-attribute op
            elif self._op_name is not None and self._attr_path is None:
                ui.MenuItem("Disable Invalid Op", triggered_fn=lambda: self._delete_op_only())
        self._right_click_menu.show()

    def _create_multi_float_drag_matrix_with_labels(self, model, comp_count, min, max, step, labels):  #
        RECT_WIDTH = 13
        SPACING = 4
        with ui.ZStack():
            with ui.HStack():
                ui.Spacer(width=RECT_WIDTH)
                value_widget = ui.MultiFloatDragField(
                    model, name="multivalue", min=min, max=max, step=step, h_spacing=RECT_WIDTH + SPACING, v_spacing=2
                )
            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.VStack():
                for i in range(comp_count):
                    with ui.HStack():
                        for i in range(comp_count):
                            ui.Spacer(width=RECT_WIDTH)
                            mixed_overlay.append(UsdPropertiesWidgetBuilder._create_mixed_text_overlay())
        UsdPropertiesWidgetBuilder._create_control_state(model, value_widget, mixed_overlay)

class TransformWatchModel(ui.AbstractValueModel):
    def __init__(self, component_index, stage):
        super(TransformWatchModel, self).__init__()
        self._usd_context = omni.usd.get_context()
        self._component_index = component_index
        self._value = 0.0
        self._selection = self._usd_context.get_selection()
        self._prim_paths = self._selection.get_selected_prim_paths()
        self._on_usd_changed()
        self.usd_watcher = omni.usd.get_watcher()
        self._subscription = self.usd_watcher.subscribe_to_change_info_path(
            self._prim_paths[0], self._on_usd_changed
        )

    def clean(self):
        """Should be called when the extension is unloaded or reloaded"""
        self._usd_context = None
        self._selection = None
        self._prim_paths = None
        self._subscription.unsubscribe()
        self._subscription = None

    def _on_usd_changed(self, path=None): #pragma no cover
        wgs_coords = get_wgs84_coords("", self._prim_paths[0])
        if len(wgs_coords) > 0:
            self.set_value(wgs_coords[self._component_index])

    def get_value_as_float(self) -> float:
        '''
        Returns the value as a float
        '''
        return self._value or 0.0

    def get_value_as_string(self) -> str:
        '''
        Returns the float value as a string
        '''
        if self._value is None:
            return ""

        # General format. This prints the number as a fixed-point
        # number, unless the number is too large, in which case it
        # switches to 'e' exponent notation.
        return "{0:g}".format(self._value)

    def set_value(self, new_value: Any) -> None:
        '''
        Attempts to cast new_value to a float and set it to the
        internal value
        '''
        try:
            value = float(new_value)
        except ValueError:
            value = 0.0
        if value != self._value:
            self._value = value
            self._value_changed()

class USDXformOpTranslateWidget(USDXformOpWidget):
    @classmethod
    def display_name(cls, attr_name: str) -> str:
        suffix = xform_op_utils.get_op_name_suffix(attr_name)
        label_name = "Translate"
        if suffix:
            label_name = label_name + ":" + suffix
        return label_name

    def rebuild(self):
        attr = self._stage.GetObjectAtPath(self._attr_path)
        attr_name = attr.GetName()
        if not attr_name.startswith("xformOp:translate"):
            carb.log_warn(f"Object {self._path} is not an xformOp:translate attribute ")
            return
        is_inverse_op = False if self._op_name is None else xform_op_utils.is_inverse_op(self._op_name)
        suffix = xform_op_utils.get_op_name_suffix(attr_name)
        if is_inverse_op and suffix == "pivot":
            return
        label_name = self.display_name(attr_name)
        # label_tooltip = get_attribute_type_tooltip(attr) + " " + attr.GetName()
        metadata = attr.GetAllMetadata()
        type_name = Sdf.ValueTypeNames.Find(metadata.get(Sdf.PrimSpec.TypeNameKey))
        label_tooltip = UsdPropertiesWidgetBuilder._generate_tooltip_string(attr.GetName(), metadata)

        self._model = []
        for i in range(3):
            self._model.append(
                GfVecAttributeSingleChannelModel(
                    self._stage,
                    [path.AppendProperty(attr_name) for path in self._prim_paths],
                    i,
                    False,
                    metadata,
                    False,
                )
            )
        vec_model = GfVecAttributeModel(
            self._stage,
            [path.AppendProperty(attr_name) for path in self._prim_paths],
            3,
            type_name.type,
            False,
            metadata,
        )

        if len(self._prim_paths) == 1:
            setattr(vec_model,'transform_widget', self)
        label = None
        with ui.HStack():
            with ui.HStack(width=LABEL_PADDING):
                if len(self._prim_paths) == 1:
                    with ui.ZStack():
                        label_tooltip = label_tooltip + "\nRight click it to disable or delete it."
                        # This rectangle is act as a hover hint helper
                        ui.Rectangle(
                            style={":hovered": {"background_color": 0xFF444444}, "background_color": 0xFF333333},
                            mouse_pressed_fn=(lambda x, y, b, m: self._show_right_click_menu(b)),
                        )
                        label = HighlightLabel(label_name, name="title", tooltip=label_tooltip, **self._label_kwargs)
                else:
                    label = HighlightLabel(label_name, name="title", tooltip=label_tooltip, **self._label_kwargs)
                ui.Spacer(width=2)
                self._create_inverse_widgets()
            with ui.HStack():
                range_min, range_max = UsdPropertiesWidgetBuilder._get_attr_value_range(metadata)
                kwargs = {"min": range_min, "max": range_max, "step": 1.0}
                UsdPropertiesWidgetBuilder._create_float_drag_per_channel_with_labels_and_control(
                    models = self._model,
                    metadata = metadata,
                    labels = [("X", 0xFF5555AA), ("Y", 0xFF76A371), ("Z", 0xFFA07D4F)],
                    kwargs = kwargs
                )

                #The vec_model is useful when we set the key for all three components together
                self._model.append(vec_model)
                UsdPropertiesWidgetBuilder._create_attribute_context_menu(label, vec_model)

        wgs_coord = get_wgs84_coords("", self._prim_paths[0].pathString)
        if len(wgs_coord) > 0: #pragma no cover OM-112507
            with ui.HStack():
                with ui.HStack(width=LABEL_PADDING):
                    label = ui.Label("WGS84", name="title", tooltip=label_tooltip)
                    ui.Spacer(width=2)
            with ui.HStack():
                with ui.VStack():
                        all_axis = ["Lat", "Lon", "Alt"]
                        colors = {"Lat": 0xFF5555AA, "Lon": 0xFF76A371, "Alt": 0xFFA07D4F}
                        component_index = 0
                        for axis in all_axis:
                            with ui.HStack():
                                with ui.HStack(width=LABEL_PADDING):
                                    label = ui.Label("", name="title", tooltip=label_tooltip)
                                with ui.ZStack(width=20):
                                    ui.Rectangle(
                                        width=20,
                                        height=20,
                                        style={
                                            "background_color": colors[axis],
                                            "border_radius": 3,
                                            "corner_flag": ui.CornerFlag.LEFT,
                                        },
                                    )
                                    ui.Label(axis, name="wgs84_label", alignment=ui.Alignment.CENTER)
                                # NOTE: Updating the WGS84 values does not update the transform translation,
                                # so make the input fields read-only for now.
                                # See https://nvidia-omniverse.atlassian.net/browse/OM-64348
                                ui.FloatDrag(TransformWatchModel(component_index, self._stage), enabled=False)
                                component_index = component_index + 1
        super().rebuild()

class USDXformOpRotateWidget(USDXformOpWidget):
    ROTATE_AXIS_ORDER_MAP = {
        "xformOp:rotateXYZ": ["X", "Y", "Z"],
        "xformOp:rotateXZY": ["X", "Z", "Y"],
        "xformOp:rotateYXZ": ["Y", "X", "Z"],
        "xformOp:rotateYZX": ["Y", "Z", "X"],
        "xformOp:rotateZXY": ["Z", "X", "Y"],
        "xformOp:rotateZYX": ["Z", "Y", "X"],
    }

    ROTATE_ORDERS = ["XYZ", "XZY", "YXZ", "YZX", "ZXY", "ZYX"]

    @classmethod
    def display_name(cls, attr_name: str) -> str:
        suffix = xform_op_utils.get_op_name_suffix(attr_name)
        label_name = "Rotate"
        if suffix:
            label_name = label_name + ":" + suffix
        return label_name

    def __init__(self, prim_paths, collapsable_frame, stage, attr_path, op_name, is_valid_op, op_order_attr_path, op_order_index, label_kwargs=None):
        self._rotate_order_drop_down_menu = None
        super().__init__(prim_paths, collapsable_frame, stage, attr_path, op_name, is_valid_op, op_order_attr_path, op_order_index, label_kwargs)

    def __del__(self):
        super().__del__()
        self._rotate_order_drop_down_menu = None

    def get_rotation_order_index(order):
        return USDXformOpRotateWidget.ROTATE_ORDERS.index(order)

    def _change_rotation_order(self, desired_order_index):
        attr = self._stage.GetObjectAtPath(self._attr_path)
        attr_name = attr.GetName()
        if not attr_name.startswith("xformOp:rotate"):
            carb.log_warn(f"Object {self._path} is not an xformOp:rotate attribute ")
            return
        is_inverse_op = False if self._op_name is None else xform_op_utils.is_inverse_op(self._op_name)
        suffix = xform_op_utils.get_op_name_suffix(attr_name)
        current_order_index = 5
        rotate_names = attr.SplitName()
        if len(rotate_names) > 1:
            rotate_order = rotate_names[1].split("rotate", 1)[1]
            current_order_index = USDXformOpRotateWidget.get_rotation_order_index(rotate_order)
        if current_order_index == desired_order_index:
            return

        # desired_order = "XYZ"
        desired_order = USDXformOpRotateWidget.ROTATE_ORDERS[desired_order_index]
        desired_attr_name = "xformOp:rotate" + desired_order
        if suffix is not None:
            desired_attr_name += ":" + suffix
        desired_op_name = desired_attr_name if not is_inverse_op else "!invert!" + desired_attr_name
        omni.kit.commands.execute(
            "ChangeRotationOp",
            src_op_attr_path=self._attr_path,
            op_name=self._op_name,
            dst_op_attr_name=desired_attr_name,
            is_inverse_op=is_inverse_op,
            auto_target_layer = True
        )

        # after we finish the rotation change "close" the rotation drop-down menu
        if self._rotate_order_drop_down_menu:
            self._rotate_order_drop_down_menu.visible = False

    # A callback to generate a drop-down-menu-like popup window i.e. self._rotate_order_drop_down_menu
    # it is used to select & update the rotation order, e.g. XYZ, XZY, ZYX etc.
    def _on_mouse_click(self,button,parent, order):
        #only suitable for mouse left click
        if button != 0:
            return
        #Use a popup window to act as a drop-down menu
        self._rotate_order_drop_down_menu = ui.Window("RotationOrder", width=60, height=130, position_x=parent.screen_position_x, position_y=parent.screen_position_y+parent.computed_height,flags = ui.WINDOW_FLAGS_POPUP | ui.WINDOW_FLAGS_NO_TITLE_BAR | ui.WINDOW_FLAGS_NO_RESIZE | ui.WINDOW_FLAGS_NO_SCROLLBAR)
        rotate_check_box_style = {
            "": {"background_color": 0x0, "image_url": f"{ICON_PATH}/radio_off.svg", "image_width":32, "image_height":32},
            ":checked": {"image_url": f"{ICON_PATH}/radio_on.svg"}
        }
        with self._rotate_order_drop_down_menu.frame:
            collection = ui.RadioCollection()
            with ui.VStack():
                for index in range(len(USDXformOpRotateWidget.ROTATE_ORDERS)):
                    rotate_order = USDXformOpRotateWidget.ROTATE_ORDERS[index]
                    #OM-94393 let the radio button decide the height and style
                    with ui.HStack(mouse_pressed_fn=(lambda x, y, b, m, index=index: self._change_rotation_order(index)), height=0):
                        ui.RadioButton(style=rotate_check_box_style, radio_collection=collection, aligment=ui.Alignment.LEFT, height=20)
                        ui.Label(rotate_order)
                    if rotate_order == order:
                        collection.model.set_value(index)

    def rebuild(self):
        attr = self._stage.GetObjectAtPath(self._attr_path)
        # if attr.GetResolveInfo().ValueIsBlocked():
        #    return
        attr_name = attr.GetName()
        if not attr_name.startswith("xformOp:rotate"):
            carb.log_warn(f"Object {self._path} is not an xformOp:rotate attribute ")
            return
        # is_inverse_op = False if self._op_name is None else xform_op_utils.is_inverse_op(self._op_name)
        label_name = self.display_name(attr_name)
        rotate_names = attr.SplitName()
        rotate_order = "ZYX"
        if len(rotate_names) > 1:
            rotate_order = rotate_names[1].split("rotate", 1)[1]
        # label_tooltip = get_attribute_type_tooltip(attr) + " " + attr.GetName()

        metadata = attr.GetAllMetadata()
        type_name = Sdf.ValueTypeNames.Find(metadata.get(Sdf.PrimSpec.TypeNameKey))
        self._model = []
        for i in range(3):
            self._model.append(
                GfVecAttributeSingleChannelModel(
                    self._stage,
                    [path.AppendProperty(attr_name) for path in self._prim_paths],
                    i,
                    False,
                    metadata,
                    False,
                )
            )
        vec_model = GfVecAttributeModel(
            self._stage,
            [path.AppendProperty(attr_name) for path in self._prim_paths],
            3,
            type_name.type,
            False,
            metadata,
        )
        if len(self._prim_paths) == 1:
            setattr(vec_model,'transform_widget', self)
        label_tooltip = UsdPropertiesWidgetBuilder._generate_tooltip_string(attr.GetName(), metadata)

        label = None
        with ui.HStack():
            if len(self._prim_paths) == 1:
                with ui.ZStack(width=LABEL_PADDING):
                    label_tooltip = label_tooltip + "\nRight click it to disable or delete it. \nLeft click it to change the rotate order, default is XYZ."
                    # This rectangle is act as a hover hint helper
                    ui.Rectangle(
                        style={":hovered": {"background_color": 0xFF444444}, "background_color": 0xFF333333},
                        mouse_pressed_fn=(lambda x, y, b, m: self._show_right_click_menu(b)),
                    )

                    # We use a label + a triangle to hack a combo box that are closer to the design
                    label_h_stack = ui.HStack(width=LABEL_PADDING)
                    label_h_stack.set_mouse_pressed_fn(lambda x, y, b, m, order=rotate_order, parent_widget=label_h_stack: self._on_mouse_click(b,parent_widget,order))
                    with label_h_stack:
                        label = HighlightLabel(
                            label_name,
                            name="title",
                            tooltip=label_tooltip,
                            width = 35,
                            **self._label_kwargs,
                        )

                        self._create_inverse_widgets()
                        ui.Spacer(width=5)
                        # use this triangle to simulate a combo box
                        with ui.VStack():
                            ui.Spacer(height=10)
                            ui.Triangle(name="default",width=8,height=6,style={"background_color":0xFF9E9E9E},alignment=ui.Alignment.CENTER_BOTTOM)
            else:
                with ui.HStack(width=LABEL_PADDING):
                    label = HighlightLabel(label_name, name="title", tooltip=label_tooltip, **self._label_kwargs)
                    ui.Spacer()
                    self._create_inverse_widgets()
            with ui.HStack():
                range_min, range_max = UsdPropertiesWidgetBuilder._get_attr_value_range(metadata)
                kwargs = {"min": range_min, "max": range_max, "step": 1.0}
                UsdPropertiesWidgetBuilder._create_float_drag_per_channel_with_labels_and_control(
                    models = self._model,
                    metadata = metadata,
                    labels = [("X", 0xFF5555AA), ("Y", 0xFF76A371), ("Z", 0xFFA07D4F)],
                    kwargs = kwargs
                )

                #The vec_model is useful when we set the key for all three components together
                self._model.append(vec_model)
                UsdPropertiesWidgetBuilder._create_attribute_context_menu(label, vec_model)


#this class is used for rotateX, rotateY, rotateZ, but it's not working properly, and produce error when displayed.
# https://omniverse-jirasw.nvidia.com/browse/OMFP-2491
#disabling coverage for this code
class USDXformOpRotateScalarWidget(USDXformOpWidget):  # pragma: no cover
    @classmethod
    def display_name(cls, attr_name: str) -> str:
        suffix = xform_op_utils.get_op_name_suffix(attr_name)
        label_name = "Rotate"
        if suffix:
            label_name = label_name + ":" + suffix
        return label_name

    def rebuild(self):
        attr = self._stage.GetObjectAtPath(self._attr_path)
        # if attr.GetResolveInfo().ValueIsBlocked():
        #    return
        attr_name = attr.GetName()
        if not attr_name.startswith("xformOp:rotate"):
            carb.log_warn(f"Object {self._path} is not an xformOp:rotate attribute ")
            return
        # is_inverse_op = False if self._op_name is None else xform_op_utils.is_inverse_op(self._op_name)
        label_name = self.display_name(attr_name)
        rotate_names = attr.SplitName()
        rotate_order = "X"
        if len(rotate_names) > 1:
            rotate_order = rotate_names[1].split("rotate", 1)[1]
        # label_tooltip = get_attribute_type_tooltip(attr) + " " + attr.GetName()

        metadata = attr.GetAllMetadata()
        type_name = Sdf.ValueTypeNames.Find(metadata.get(Sdf.PrimSpec.TypeNameKey))
        self._model = UsdAttributeModel(
            self._stage, [path.AppendProperty(attr_name) for path in self._prim_paths], False, metadata
        )

        label_tooltip = UsdPropertiesWidgetBuilder._generate_tooltip_string(attr.GetName(), metadata)
        with ui.HStack():
            with ui.HStack(width=LABEL_PADDING):
                if len(self._prim_paths) == 1:
                    with ui.ZStack():
                        label_tooltip = label_tooltip + "\nRight click it to disable or delete it. "
                        # This rectangle is act as a hover hint helper
                        ui.Rectangle(
                            style={":hovered": {"background_color": 0xFF444444}, "background_color": 0xFF333333},
                            mouse_pressed_fn=(lambda x, y, b, m: self._show_right_click_menu(b)),
                        )
                        HighlightLabel(label_name, name="title", tooltip=label_tooltip, **self._label_kwargs)
                else:
                    HighlightLabel(label_name, name="title", tooltip=label_tooltip, **self._label_kwargs)
                self._create_inverse_widgets()
                ui.Spacer(width=2)
            stack_names = {"X":"rotate_scalar_stack_x", "Y":"rotate_scalar_stack_y", "Z":"rotate_scalar_stack_z"}
            rotate_scalar_stack = ui.HStack(identifier = stack_names[rotate_order])
            with rotate_scalar_stack:
                colors = {"X": 0xFF5555AA, "Y": 0xFF76A371, "Z": 0xFFA07D4F}
                with ui.ZStack():
                    with ui.HStack():
                        range_min, range_max = UsdPropertiesWidgetBuilder._get_attr_value_range(metadata)
                        widget_kwargs = {"model": self._model, "step": 1}
                        if range_min < range_max:
                            widget_kwargs["min"] = range_min
                            widget_kwargs["max"] = range_max
                        UsdPropertiesWidgetBuilder._create_drag_or_slider(ui.FloatDrag, ui.FloatSlider, **widget_kwargs)
                        UsdPropertiesWidgetBuilder._create_control_state(self._model)
                    with ui.HStack():
                        with ui.ZStack(width=14):
                            ui.Rectangle(
                                name="vector_label",
                                style={
                                    "background_color": colors[rotate_order],
                                    "border_radius": 3,
                                    "corner_flag": ui.CornerFlag.LEFT,
                                },
                            )
                            ui.Label(rotate_order, name="vector_label", alignment=ui.Alignment.CENTER)


class USDXformOpScaleWidget(USDXformOpWidget):
    @classmethod
    def display_name(cls, attr_name: str) -> str:
        suffix = xform_op_utils.get_op_name_suffix(attr_name)
        label_name = "Scale"
        if suffix:
            label_name = label_name + ":" + suffix
        return label_name

    def __init__(
        self, prim_paths, collapsable_frame, stage, attr_path, op_name, is_valid_op, op_order_attr_path, op_order_index,
        parent_widget, label_kwargs=None
    ):
        self._parent_widget = parent_widget
        super().__init__(
            prim_paths, collapsable_frame, stage, attr_path, op_name, is_valid_op, op_order_attr_path, op_order_index,
            label_kwargs
        )

    def rebuild(self):

        # The scale widget uses a modified version of the single channel model that supports
        # scaling each component of the vector uniformly.
        # If link_channels is False, the single channel model behaves as usual.
        class GfVecLinkableModel(GfVecAttributeSingleChannelModel):

            def __init__(
                self,
                stage: Usd.Stage,
                attribute_paths: List[Sdf.Path],
                channel_index: int,
                self_refresh: bool,
                metadata: dict,
                change_on_edit_end=False,
                link_channels=False,
                **kwargs
            ):
                self._link_channels = link_channels
                super().__init__(stage, attribute_paths, channel_index, self_refresh, metadata, change_on_edit_end, **kwargs)

            def set_value(self, value):
                if self._link_channels:
                    vec_value = copy.copy(self._value)
                    for i in range(3):
                        vec_value[i] = value
                    if UsdBase.set_value(self, vec_value, -1):
                        self._value_changed()
                else:
                    super().set_value(value)

            def set_default(self, comp=-1):
                if self._link_channels:
                    value = self._default_value[self._channel_index]
                    vec_value = copy.copy(self._value)
                    for i in range(3):
                        vec_value[i] = value
                    if UsdBase.set_value(self, vec_value, -1):
                        self._value_changed()
                else:
                    super().set_default(comp)


        attr = self._stage.GetObjectAtPath(self._attr_path)
        attr_name = attr.GetName()
        if not attr_name.startswith("xformOp:scale"):
            carb.log_warn(f"Object {self._path} is not an xformOp:scale attribute ")
            return
        # is_inverse_op = False if self._op_name is None else xform_op_utils.is_inverse_op(self._op_name)
        label_name = self.display_name(attr_name)
        # label_tooltip = get_attribute_type_tooltip(attr) + " " + attr.GetName()

        metadata = attr.GetAllMetadata()
        type_name = Sdf.ValueTypeNames.Find(metadata.get(Sdf.PrimSpec.TypeNameKey))

        self._model = []
        for i in range(3):
            self._model.append(
                GfVecLinkableModel(
                    self._stage,
                    [path.AppendProperty(attr_name) for path in self._prim_paths],
                    i,
                    False,
                    metadata,
                    False,
                    self._parent_widget._link_scale
                )
            )
        vec_model = GfVecAttributeModel(
            self._stage,
            [path.AppendProperty(attr_name) for path in self._prim_paths],
            3,
            type_name.type,
            False,
            metadata,
        )
        if len(self._prim_paths) == 1:
            setattr(vec_model,'transform_widget', self)
        label_tooltip = UsdPropertiesWidgetBuilder._generate_tooltip_string(attr.GetName(), metadata)
        label = None
        with ui.HStack():
            with ui.HStack(width=LABEL_PADDING):
                if len(self._prim_paths) == 1:
                    with ui.ZStack():
                        label_tooltip = label_tooltip + "\nRight click it for more options."
                        # This rectangle is act as a hover hint helper
                        ui.Rectangle(
                            style={":hovered": {"background_color": 0xFF444444}, "background_color": 0xFF333333},
                            mouse_pressed_fn=(lambda x, y, b, m: self._show_right_click_menu(b)),
                        )
                        label = HighlightLabel(label_name, name="title", tooltip=label_tooltip, **self._label_kwargs)
                else:
                    label = HighlightLabel(label_name, name="title", tooltip=label_tooltip, **self._label_kwargs)
                self._create_inverse_widgets()
                if self._parent_widget._link_scale:
                    button_style = {
                        "color": 0xFF34B5FF,
                        "background_color": 0x0,
                        "Button.Image": {"image_url": f"{ICON_PATH}/link_on.svg"},
                        ":hovered": {"color": 0xFFACDCF9}
                    }
                else:
                    button_style = {
                        "color": 0x66FFFFFF,
                        "background_color": 0x0,
                        "Button.Image": {"image_url": f"{ICON_PATH}/link_off.svg"},
                        ":hovered": {"color": 0xFFFFFFFF}
                    }
                with ui.ZStack(content_clipping = True, width=25, height=25):
                    ui.Button("",
                              style = button_style,
                              clicked_fn = self._parent_widget._toggle_link_scale,
                              identifier = "toggle_link_scale",
                              tooltip = "Toggle link scale")
                ui.Spacer(width=70)
            scale_stack = ui.HStack(identifier = "scale_t_stack")
            with scale_stack:
                range_min, range_max = UsdPropertiesWidgetBuilder._get_attr_value_range(metadata)
                kwargs = {"min": range_min, "max": range_max, "step": 1.0}
                UsdPropertiesWidgetBuilder._create_float_drag_per_channel_with_labels_and_control(
                    models = self._model,
                    metadata = metadata,
                    labels = [("X", 0xFF5555AA), ("Y", 0xFF76A371), ("Z", 0xFFA07D4F)],
                    kwargs = kwargs
                )

                #The vec_model is useful when we set the key for all three components together
                self._model.append(vec_model)
                UsdPropertiesWidgetBuilder._create_attribute_context_menu(label, vec_model)


class USDXformOpOrientWidget(USDXformOpWidget):
    @classmethod
    def display_name(cls, attr_name: str) -> str:
        suffix = xform_op_utils.get_op_name_suffix(attr_name)
        label_name = "Orient"
        if suffix:
            label_name = label_name + ":" + suffix
        return label_name

    def __init__(
        self, prim_paths, collapsable_frame, stage, attr_path, op_name, is_valid_op, op_order_attr_path, op_order_index,
        label_kwargs=None,
    ):
        self._settings = carb.settings.get_settings()
        if self._settings:
            if self._settings.get("/persistent/app/uiSettings/DisplayOrientAsRotate") is None:
                self._settings.set_default_bool("/persistent/app/uiSettings/DisplayOrientAsRotate", True)
            self._display_orient_as_rotate = self._settings.get_as_bool(
                "/persistent/app/uiSettings/DisplayOrientAsRotate"
            )
        super().__init__(
            prim_paths, collapsable_frame, stage, attr_path, op_name, is_valid_op, op_order_attr_path, op_order_index,
            label_kwargs,
        )

    def _build_orient_widgets(self, attr, attr_name, type_name, label_name, label_tooltip, metadata):
        self._model = GfQuatAttributeModel(
            self._stage, [path.AppendProperty(attr_name) for path in self._prim_paths], type_name.type, False, metadata
        )
        with ui.HStack():
            kwargs = {"min": -1, "max": 1, "step": 0.01}
            value_widget, mixed_overlay = UsdPropertiesWidgetBuilder._create_multi_float_drag_with_labels(
                self._model,
                comp_count=4,
                labels=[("W", 0xFFAA5555), ("X", 0xFF5555AA), ("Y", 0xFF76A371), ("Z", 0xFFA07D4F)],
                **kwargs,
            )
            value_widget.identifier = f"orient_{attr_name}"
            UsdPropertiesWidgetBuilder._create_control_state(self._model, value_widget, mixed_overlay)

    def _build_euler_widgets(self, attr, attr_name, type_name, label_name, label_tooltip, metadata):
        self._euler_model = GfQuatEulerAttributeModel(
            self._stage, [path.AppendProperty(attr_name) for path in self._prim_paths], type_name.type, False, metadata
        )
        with ui.HStack():
            kwargs = {"min": -360, "max": 360, "step": 1}
            value_widget, mixed_overlay = UsdPropertiesWidgetBuilder._create_multi_float_drag_with_labels(
                self._euler_model,
                comp_count=3,
                labels=[("X", 0xFF5555AA), ("Y", 0xFF76A371), ("Z", 0xFFA07D4F)],
                **kwargs,
            )
            value_widget.identifier = f"euler_{attr_name}"
            UsdPropertiesWidgetBuilder._create_control_state(self._euler_model, value_widget, mixed_overlay)

    def _build_header(self, attr, attr_name, type_name, label_name, label_tooltip, metadata):
        with ui.ZStack(width=LABEL_PADDING):
            if len(self._prim_paths) == 1:
                label_tooltip = label_tooltip + "\nDisplay Orient As Euler Angles. \nRight click to disable or delete it. \nLeft click to switch between Orient & Euler Angles."
                # A black rect background indicates the Orient is not original data format
                _rect = ui.Rectangle(
                    style={
                        "Rectangle:hovered": {"background_color": 0xFF444444},
                        "Rectangle": {"background_color": 0xFF333333},
                    },
                    tooltip=label_tooltip,
                )
                # _rect = ui.Rectangle(tooltip=label_tooltip)
                _rect.set_mouse_pressed_fn(lambda x, y, b, m: self._show_right_click_menu(b))

            # TODO: Replace this hack when OM-24290 is fixed
            with ui.ZStack():
                _button = ui.Button(
                    label_name,
                    name="title",
                    width=50,
                    tooltip=label_tooltip if len(self._prim_paths) > 1 else "",
                    style=euler_view_button_style if self._display_orient_as_rotate else quat_view_button_style,
                )
                _button.set_mouse_pressed_fn(
                    lambda x, y, b, m, widget=_button: self._on_orient_button_clicked(b, widget)
                )
                self._create_inverse_widgets()
                with ui.HStack():
                    ui.Spacer(width=35)
                    ui.Image(f"{ICON_PATH}/orient_button.svg", width=10, alignment=ui.Alignment.CENTER)

    def rebuild(self):
        # attr & attr_name
        attr = self._stage.GetObjectAtPath(self._attr_path)
        attr_name = attr.GetName()
        if not attr_name.startswith("xformOp:orient"):
            carb.log_warn(f"Object {self._path} is not an xformOp:orient attribute ")
            return

        # metadata
        metadata = attr.GetAllMetadata()

        # type_name
        type_name = Sdf.ValueTypeNames.Find(metadata.get(Sdf.PrimSpec.TypeNameKey))

        # label_name
        label_name = self.display_name(attr_name)

        # label_tooltip
        label_tooltip = UsdPropertiesWidgetBuilder._generate_tooltip_string(attr.GetName(), metadata)

        with ui.HStack():
            self._build_header(attr, attr_name, type_name, label_name, label_tooltip, metadata)
            # Create two views, Quaternion view & Euler Angle view
            # self._display_orient_as_rotate True:  Euler Angle view,  False: Quaternion view
            with ui.ZStack():
                # Quaternion view is default on
                self._quat_view = ui.Frame(visible=False if self._display_orient_as_rotate else True)
                with self._quat_view:
                    self._build_orient_widgets(attr, attr_name, type_name, label_name, label_tooltip, metadata)
                # Euler view is default off
                self._euler_view = ui.Frame(visible=True if self._display_orient_as_rotate else False)
                with self._euler_view:
                    self._build_euler_widgets(attr, attr_name, type_name, label_name, label_tooltip, metadata)


class USDXformOpTransformWidget(USDXformOpWidget):
    @classmethod
    def display_name(cls, attr_name: str) -> str:
        suffix = xform_op_utils.get_op_name_suffix(attr_name)
        label_name = "Transform"
        if suffix:
            label_name = label_name + ":" + suffix
        return label_name

    def rebuild(self):
        attr = self._stage.GetObjectAtPath(self._attr_path)
        attr_name = attr.GetName()
        if not attr_name.startswith("xformOp:transform"):
            carb.log_warn(f"Object {self._path} is not an xformOp:transform attribute ")
            return
        # is_inverse_op = False if self._op_name is None else xform_op_utils.is_inverse_op(self._op_name)
        label_name = self.display_name(attr_name)
        # label_tooltip = get_attribute_type_tooltip(attr) + " " + attr.GetName()

        metadata = attr.GetAllMetadata()
        type_name = Sdf.ValueTypeNames.Find(metadata.get(Sdf.PrimSpec.TypeNameKey))
        self._model = GfMatrixAttributeModel(
            self._stage,
            [path.AppendProperty(attr_name) for path in self._prim_paths],
            4,
            type_name.type,
            False,
            metadata,
        )
        label_tooltip = UsdPropertiesWidgetBuilder._generate_tooltip_string(attr.GetName(), metadata)
        with ui.HStack():
            with ui.HStack(width=LABEL_PADDING):
                if len(self._prim_paths) == 1:
                    with ui.ZStack():
                        label_tooltip = label_tooltip + "\nRight click it to disable or delete it."
                        # This rectangle is act as a hover hint helper
                        ui.Rectangle(
                            style={":hovered": {"background_color": 0xFF444444}, "background_color": 0xFF333333},
                            mouse_pressed_fn=(lambda x, y, b, m: self._show_right_click_menu(b)),
                        )
                        HighlightLabel(label_name, name="title", tooltip=label_tooltip, **self._label_kwargs)
                else:
                    HighlightLabel(label_name, name="title", tooltip=label_tooltip, **self._label_kwargs)
                self._create_inverse_widgets()
                ui.Spacer(width=2)
            with ui.HStack():
                range_min, range_max = UsdPropertiesWidgetBuilder._get_attr_value_range(metadata)
                # TODO: make step default to 1.0 before the adaptive stepping solution is available
                step = 1.0
                self._create_multi_float_drag_matrix_with_labels(
                    self._model,
                    4,
                    range_min,
                    range_max,
                    step,
                    [("X", 0xFF5555AA), ("Y", 0xFF76A371), ("Z", 0xFFA07D4F), ("W", 0xFFAA5555)],
                )
        wgs_coord = get_wgs84_coords("", self._prim_paths[0].pathString)
        if len(wgs_coord) > 0: #pragma no cover OM-112507
            with ui.HStack():
                with ui.HStack(width=LABEL_PADDING):
                    label = ui.Label("WGS84", name="title", tooltip=label_tooltip)
                    ui.Spacer(width=2)
            with ui.HStack():
                with ui.VStack():
                        all_axis = ["Lat", "Lon", "Alt"]
                        colors = {"Lat": 0xFF5555AA, "Lon": 0xFF76A371, "Alt": 0xFFA07D4F}
                        component_index = 0
                        for axis in all_axis:
                            with ui.HStack():
                                with ui.HStack(width=LABEL_PADDING):
                                    label = ui.Label("", name="title", tooltip=label_tooltip)
                                with ui.ZStack(width=20):
                                    ui.Rectangle(
                                        width=20,
                                        height=20,
                                        style={
                                            "background_color": colors[axis],
                                            "border_radius": 3,
                                            "corner_flag": ui.CornerFlag.LEFT,
                                        },
                                    )
                                    ui.Label(axis, name="wgs84_label", alignment=ui.Alignment.CENTER)
                                # NOTE: Updating the WGS84 values does not update the transform translation,
                                # so make the input fields read-only for now.
                                # See https://nvidia-omniverse.atlassian.net/browse/OM-64348
                                ui.FloatDrag(TransformWatchModel(component_index, self._stage), enabled=False)
                                component_index = component_index + 1


class USDResetXformStackWidget(USDXformOpWidget):
    def rebuild(self):
        is_reset_xform_stack_op = xform_op_utils.is_reset_xform_stack_op(self._op_name)
        if not is_reset_xform_stack_op:
            return
        label_name = "ResetXformStack"
        label_tooltip = "Invalid all above xformOp and parent transform. Calulate world transform from the op below."
        self._model = None
        with ui.HStack():
            with ui.HStack(width=LABEL_PADDING):
                ui.Label(
                    label_name,
                    name="title",
                    tooltip=label_tooltip,
                    mouse_pressed_fn=(lambda x, y, b, m: self._show_right_click_menu(b)),
                )
                ui.Spacer(width=5)
            with ui.HStack():
                with ui.VStack():
                    ui.Spacer()
                    color = 0xFF888888 if self._is_valid_op else 0xFF444444
                    ui.Line(
                        name="ResetXformStack",
                        height=2,
                        style={"color": color, "border_width": 0.5, "alignment": ui.Alignment.BOTTOM},
                        alignment=ui.Alignment.BOTTOM,
                    )
                    ui.Spacer()


class USDNoAttributeOpWidget(USDXformOpWidget):
    def rebuild(self):
        if self._op_name is None:
            return
        label_name = self._op_name
        self._model = None
        is_valid_op_name = xform_op_utils.is_valid_op_name(self._op_name)
        op_attr_name = xform_op_utils.get_op_attr_name(self._op_name)
        label_error = (
            f"Could not find attribute {op_attr_name} for this xformOp."
            if is_valid_op_name
            else "Invalid xformOp name!"
        )
        with ui.HStack():
            with ui.HStack(width=LABEL_PADDING):
                ui.Label(label_name, name="title", mouse_pressed_fn=(lambda x, y, b, m: self._show_right_click_menu(b)))
                ui.Spacer(width=5)
            with ui.HStack():
                ui.Label(label_error, name="title")

class OperationTypes(Enum):
    ADD = 0
    MULTIPLY = 1
    INVALID = 2

class TransformWidgets:

    def __init__(self, parent_widget):
        self._settings = carb.settings.get_settings()
        self._clear_widgets()
        self._parent_widget = parent_widget
        self._offset_index = -1
        self._old_transform = ""
        self._active_fields = 0
        self._dragging = False
        self._tab_swap = 0

    def __del__(self):
        self._clear_widgets()

    def _clear_widgets(self):
        self._stage = None
        self._widgets = []
        self._models = defaultdict(list)

    def _create_xform_op_widget(
        self,
        prim_paths,
        collapsable_frame,
        stage,
        attr,
        op_name=None,
        is_valid_op=False,
        op_order_attr=None,
        op_order_index=-1,
    ):
        op_order_path = op_order_attr.GetPath() if op_order_attr is not None and op_order_attr else None
        if xform_op_utils.is_reset_xform_stack_op(op_name):
            return USDResetXformStackWidget(
                prim_paths, collapsable_frame, stage, None, op_name, is_valid_op, op_order_path, op_order_index
            )
        if not attr:
            return USDNoAttributeOpWidget(
                prim_paths, collapsable_frame, stage, None, op_name, is_valid_op, op_order_path, op_order_index
            )
        attr_name = attr.GetName()

        def match(label):
            return self._parent_widget._filter.matches(label)

        highlight = self._parent_widget._filter.name
        label_kwargs = {"highlight": highlight}

        if attr_name.startswith("xformOp:translate") and match(USDXformOpTranslateWidget.display_name(attr_name)):
            return USDXformOpTranslateWidget(
                prim_paths,
                collapsable_frame,
                stage,
                attr.GetPath(),
                op_name,
                is_valid_op,
                op_order_path,
                op_order_index,
                label_kwargs,
            )
        elif attr_name.startswith("xformOp:rotate") and match(USDXformOpRotateWidget.display_name(attr_name)):
            op_type_name = xform_op_utils.get_op_type_name(attr_name)
            if len(op_type_name.split("rotate", 1)[1]) == 3:
                return USDXformOpRotateWidget(
                    prim_paths,
                    collapsable_frame,
                    stage,
                    attr.GetPath(),
                    op_name,
                    is_valid_op,
                    op_order_path,
                    op_order_index,
                    label_kwargs,
                )
            elif len(op_type_name.split("rotate", 1)[1]) == 1:
                return USDXformOpRotateScalarWidget(
                    prim_paths,
                    collapsable_frame,
                    stage,
                    attr.GetPath(),
                    op_name,
                    is_valid_op,
                    op_order_path,
                    op_order_index,
                    label_kwargs,
                )
        elif attr_name.startswith("xformOp:orient") and match(USDXformOpOrientWidget.display_name(attr_name)):
            return USDXformOpOrientWidget(
                prim_paths,
                collapsable_frame,
                stage,
                attr.GetPath(),
                op_name,
                is_valid_op,
                op_order_path,
                op_order_index,
                label_kwargs,
            )
        elif attr_name.startswith("xformOp:scale") and match(USDXformOpScaleWidget.display_name(attr_name)):
            return USDXformOpScaleWidget(
                prim_paths,
                collapsable_frame,
                stage,
                attr.GetPath(),
                op_name,
                is_valid_op,
                op_order_path,
                op_order_index,
                self._parent_widget,
                label_kwargs,
            )
        elif attr_name.startswith("xformOp:transform") and match(USDXformOpTransformWidget.display_name(attr_name)):
            return USDXformOpTransformWidget(
                prim_paths,
                collapsable_frame,
                stage,
                attr.GetPath(),
                op_name,
                is_valid_op,
                op_order_path,
                op_order_index,
                label_kwargs,
            )
        return None

    def _get_common_xformop(self, prim_paths):

        def _get_xformop_order(stage, prim_path):
            prim = stage.GetPrimAtPath(prim_path)
            if not prim:
                return []
            order_attr = prim.GetAttribute("xformOpOrder")
            if not order_attr:
                return []
            xform_op_order = order_attr.Get()
            if not xform_op_order:
                return []
            return xform_op_order

        common_xformOp_order = [ \
            "xformOp:translate","xformOp:scale","xformOp:rotateX",  \
            "xformOp:rotateY","xformOp:rotateZ","xformOp:rotateXYZ", \
            "xformOp:rotateXZY","xformOp:rotateYXZ","xformOp:rotateYZX", \
            "xformOp:rotateZXY","xformOp:rotateZYX","xformOp:orient","xformOp:transform"
        ]
        all_empty_xformOp = True
        for prim_path in prim_paths:
            xformOp_order = _get_xformop_order(self._stage, prim_path)
            common_xformOp_order = list(set(common_xformOp_order) & set(xformOp_order))
            all_empty_xformOp &= (len(xformOp_order) == 0)

        return common_xformOp_order, all_empty_xformOp

    def _create_multi_string_with_labels(self, ui_widget, model, labels, comp_count):
        # This is similar to the multi_float_drag function above, but without USD backing and using a given single
        # field (draggable float and string, for offset mode we need to do some string parsing to look for math operations)
        RECT_WIDTH = 13
        SPACING = 4
        value_widget = []
        with ui.ZStack():
            with ui.HStack():
                ui.Spacer(width=RECT_WIDTH)
                items = model.get_item_children(self)
                for i in range(comp_count):
                    if i !=0:
                        ui.Spacer(width=RECT_WIDTH)
                    field = model.get_item_value_model(items[i], i)
                    value_widget.append(ui_widget(field))
            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()
        return value_widget

    # Two sets of fields are generated, FloatDrags are visible dragging and StringFields are visible during keyboard input
    # These functions ensure the correct fields are visible and active when single clicking, double clicking, and tab selecting
    def _on_double_click(self, transform):
        self._dragging = False
        float_widget = self._get_widget(transform)[0]
        string_widget = self._get_widget(transform)[1]
        for widget in float_widget:
            widget.visible = False
        for widget in string_widget:
            widget.visible = True
        async def focus(field):
            await omni.kit.app.get_app().next_update_async()
            field.focus_keyboard()
        string_widget[self._offset_index].focus_keyboard()
        asyncio.ensure_future(focus(string_widget[self._offset_index]))
        if self._tab_swap == 1:
            self._active_fields -= 1

    def _on_press(self):
        self._dragging = True
        self._tab_swap = 0
        if self._active_fields < 0:
            self._active_fields = 0

    def _start_field_edit(self, index, transform):
        self._offset_index = index
        self._active_fields += 1
        self._old_transform = transform
        self._settings.set(TRANSFORM_OP_SETTING, transform)
        float_widget = self._get_widget(transform)[0]
        if float_widget[index].visible == True and self._dragging == False:
            self._tab_swap += 1
            self._on_double_click(transform)

    def _swapping(self, transform):
        if transform == self._old_transform:
            return False
        else:
            return True

    async def _end_string_edit(self, transform):
        self._active_fields -= 1
        if self._swapping(transform):
            if self._get_widget(transform)[0][0].visible == False:
                for widget in self._get_widget(transform)[1]:
                    widget.visible = False
                for widget in self._get_widget(transform)[0]:
                    widget.visible = True
        # Swap back to FloatDrag fields if the user clicks off
        await asyncio.sleep(.3)
        if self._active_fields < 1:
            if self._get_widget(transform)[0][0].visible == False:
                for widget in self._get_widget(transform)[1]:
                    widget.visible = False
                for widget in self._get_widget(transform)[0]:
                    widget.visible = True


    def _get_widget(self, transform):
        if transform == TRANSFORM_OP_MOVE:
            return self._float_translate_widget, self._string_translate_widget
        elif transform == TRANSFORM_OP_ROTATE:
            return self._float_rotate_widget, self._string_rotate_widget
        else:
            return self._float_scale_widget, self._string_scale_widget

    def _get_offset_data(self, text: str):
        # See if input looks like "[op] [number]", for the four supported operations
        try:
            text = text.replace(" ", "")
            if len(text) > 0:
                operator = text[0]
                value = float(text[1:])

                if operator == '*':
                    return OperationTypes.MULTIPLY, value
                elif operator == '/':
                    return OperationTypes.MULTIPLY, 1.0 / value
                elif operator == '+':
                    return OperationTypes.ADD, value
                elif operator == '-':
                    return OperationTypes.ADD, -value
        except ValueError:
            pass

        # If we didn't find an operation, see if the input is just a number
        try:
            value = float(text)
            return OperationTypes.ADD, value
        except ValueError:
            return OperationTypes.INVALID, 0.0

    def _on_translate_offset(self, offset_text: str, index: int, stage: Usd.Stage, prim_paths: List[Sdf.Path]):
        operation, offset_value = self._get_offset_data(offset_text)

        if operation is OperationTypes.INVALID:
            carb.log_warn("Unrecognized input provided to offset field. No operation will be applied.")
        elif stage:
            with omni.kit.undo.group():
                for path in prim_paths:
                    prim = stage.GetPrimAtPath(path)
                    prim_world_xform = omni.usd.get_world_transform_matrix(prim)

                    success, scale_orient_mat, scale, rotation_mat, translation, persp_mat = prim_world_xform.Factor()
                    scale_mat = Gf.Matrix4d().SetScale(scale)

                    local = self._settings.get(TRANSFORM_MOVE_MODE_SETTING) is None or \
                        self._settings.get_as_string(TRANSFORM_MOVE_MODE_SETTING) != TRANSFORM_MODE_GLOBAL

                    if operation is OperationTypes.ADD:
                        if local:
                            offset_vec = Gf.Vec3d([offset_value if i == index else 0 for i in range(3)])
                            offset_mat = Gf.Matrix4d().SetTranslate(offset_vec)
                            translation_mat = Gf.Matrix4d().SetTranslate(translation)
                            translation = (offset_mat * rotation_mat * translation_mat).ExtractTranslation()
                        else:
                            translation[index] += offset_value
                    elif operation is OperationTypes.MULTIPLY:
                        if local:
                            offset_vec = Gf.Vec3d([offset_value if i == index else 1 for i in range(3)])
                            offset_mat = Gf.Matrix4d().SetScale(offset_vec)
                            translation_mat = Gf.Matrix4d().SetTranslate(translation)
                            # Rotate translation into local frame, apply the scale there, rotate back out
                            translation = (translation_mat * rotation_mat.GetTranspose() * offset_mat * rotation_mat).ExtractTranslation()
                        else:
                            translation[index] *= offset_value

                    translation_mat = Gf.Matrix4d().SetTranslate(translation)
                    prim_world_xform = scale_orient_mat * scale_mat * scale_orient_mat.GetTranspose() * rotation_mat * translation_mat
                    parent_world_xform = omni.usd.get_world_transform_matrix(prim.GetParent())
                    prim_local_xform = prim_world_xform * parent_world_xform.GetInverse()
                    omni.kit.commands.execute("TransformPrimCommand", path = path, new_transform_matrix = prim_local_xform)
                asyncio.ensure_future(self._end_string_edit(TRANSFORM_OP_MOVE))

    def _on_rotate_offset(self, offset_text: str, index: int, stage: Usd.Stage, prim_paths: List[Sdf.Path]):
        operation, offset_value = self._get_offset_data(offset_text)

        if operation is OperationTypes.INVALID:
            carb.log_warn("Unrecognized input provided to offset field. No operation will be applied.")
        elif stage:
            with omni.kit.undo.group():
                for path in prim_paths:
                    prim = stage.GetPrimAtPath(path)
                    prim_world_xform = omni.usd.get_world_transform_matrix(prim)

                    success, scale_orient_mat, scale, rotation_mat, translation, persp_mat = prim_world_xform.Factor()
                    scale_mat = Gf.Matrix4d().SetScale(scale)

                    local = self._settings.get(TRANSFORM_ROTATE_MODE_SETTING) is None or \
                        self._settings.get_as_string(TRANSFORM_ROTATE_MODE_SETTING) != TRANSFORM_MODE_GLOBAL

                    if operation is OperationTypes.ADD:
                        axis = Gf.Vec3d([1 if i == index else 0 for i in range(3)])
                        offset_r = Gf.Rotation(axis, offset_value)
                        offset_mat = Gf.Matrix4d().SetRotate(offset_r.GetQuat())
                        if local:
                            rotation_mat = offset_mat * rotation_mat
                        else:
                            rotation_mat = rotation_mat * offset_mat
                        translation_mat = Gf.Matrix4d().SetTranslate(translation)
                        prim_world_xform = scale_orient_mat * scale_mat * scale_orient_mat.GetTranspose() * rotation_mat * translation_mat
                        parent_world_xform = omni.usd.get_world_transform_matrix(prim.GetParent())
                        prim_local_xform = prim_world_xform * parent_world_xform.GetInverse()
                        omni.kit.commands.execute("TransformPrimCommand", path = path, new_transform_matrix = prim_local_xform)
                    elif operation is OperationTypes.MULTIPLY:
                        # rotation-multiply is unlike the other offsets because it we need to know the component of the
                        # current rotation about a particular axis in order to scale it, but a rotation's component along
                        # one axis can be different depending on the representation (e.g. different euler angle orderings).
                        # Rather than deal with that, we'll just look for the prim's current rotation values
                        # and scale the specified component.
                        scale, rotation, order, translation = omni.usd.get_local_transform_SRT(prim)
                        rotation[index] *= offset_value
                        omni.kit.commands.execute("TransformPrimSRTCommand", path = path, new_rotation_euler = rotation)
                asyncio.ensure_future(self._end_string_edit(TRANSFORM_OP_ROTATE))

    def _on_scale_offset(self, offset_text: str, index: int, stage: Usd.Stage, prim_paths: List[Sdf.Path]):
        operation, offset_value = self._get_offset_data(offset_text)

        if operation is OperationTypes.INVALID:
            carb.log_warn("Unrecognized input provided to offset field. No operation will be applied.")
        elif stage:
            with omni.kit.undo.group():
                for path in prim_paths:
                    prim = stage.GetPrimAtPath(path)
                    prim_world_xform = omni.usd.get_world_transform_matrix(prim)

                    success, scale_orient_mat, scale, rotation_mat, translation, persp_mat = prim_world_xform.Factor()

                    if operation is OperationTypes.ADD:
                        if self._parent_widget._link_scale:
                            scale = ((scale[index] + offset_value) / scale[index]) * scale
                        else:
                            scale[index] += offset_value
                    elif operation is OperationTypes.MULTIPLY:
                        if self._parent_widget._link_scale:
                            scale = offset_value * scale
                        else:
                            scale[index] *= offset_value
                    scale_mat = Gf.Matrix4d().SetScale(scale)

                    translation_mat = Gf.Matrix4d().SetTranslate(translation)
                    prim_world_xform = scale_orient_mat * scale_mat * scale_orient_mat.GetTranspose() * rotation_mat * translation_mat
                    parent_world_xform = omni.usd.get_world_transform_matrix(prim.GetParent())
                    prim_local_xform = prim_world_xform * parent_world_xform.GetInverse()
                    omni.kit.commands.execute("TransformPrimCommand", path = path, new_transform_matrix = prim_local_xform)
                asyncio.ensure_future(self._end_string_edit(TRANSFORM_OP_SCALE))

    def build_transform_frame(self, prim_paths, collapsable_frame, stage):

        self._clear_widgets()
        # Transform Frame
        if stage is None or len(prim_paths) <= 0 or prim_paths is None:
            return None, False
        prim_path = prim_paths[0]
        prim = stage.GetPrimAtPath(prim_path)
        if not prim or not prim.IsA(UsdGeom.Xformable):
            return None, False

        self._stage = stage
        xform = UsdGeom.Xformable(prim)

        # if xformOpOrder is absent or an empty array don't return
        # we may still have xformOp that are not listed in the xformOpOrder
        order_attr = prim.GetAttribute("xformOpOrder")
        if not order_attr:
            xform_op_order = []
        xform_op_order = order_attr.Get()
        if xform_op_order is None:
            xform_op_order = []

        # when select more than two prim, get common xformop to show
        if len(prim_paths) > 1:
            common_xformop, all_empty_xformop = self._get_common_xformop(prim_paths)
            for index, op_name in enumerate(common_xformop):
                attr_name = xform_op_utils.get_op_attr_name(op_name)
                attr = Usd.Attribute()
                if attr_name is not None:
                    attr = prim.GetAttribute(attr_name)
                widget = self._create_xform_op_widget(
                    prim_paths, collapsable_frame, stage, attr, op_name, True, order_attr, index
                )
                if widget is not None:
                    self._widgets.append(widget)
                    model = widget._model
                    #if it is a list the last one is the vector model
                    if isinstance(model, list):
                        for m in model:
                            if m is not None:
                                for sdf_path in m.get_attribute_paths():
                                    self._models[sdf_path].append(m)
                    elif model is not None:
                        for sdf_path in model.get_attribute_paths():
                            self._models[sdf_path].append(model)

                    if isinstance(widget, USDXformOpOrientWidget):
                        if widget._euler_model is not None:
                            for sdf_path in widget._euler_model.get_attribute_paths():
                                self._models[sdf_path].append(widget._euler_model)
            return self._models, all_empty_xformop

        attr_xform_op_order = xform.GetXformOpOrderAttr()
        xform_ops = xform.GetOrderedXformOps()
        has_reset_xform_stack = xform.GetResetXformStack()

        reset_index = -1
        if has_reset_xform_stack:
            for i in range(len(xform_op_order) - 1, -1, -1):
                if xform_op_utils.is_reset_xform_stack_op(xform_op_order.__getitem__(i)):
                    reset_index = i
                    break

        attr_xform_ops = []
        for index, op_name in enumerate(xform_op_order):
            attr_name = xform_op_utils.get_op_attr_name(op_name)
            if attr_name is not None:
                attr = prim.GetAttribute(attr_name)
                if attr:
                    attr_xform_ops.append(attr)

        attrs_all = [attr for attr in prim.GetAttributes() if not attr.IsHidden()]
        attr_not_in_order_xform_ops = list(
            attr for attr in attrs_all if (attr.GetName().startswith("xformOp:") and not attr in attr_xform_ops)
        )
        if len(xform_op_order) + len(attr_not_in_order_xform_ops) > 0:
            with ui.VStack(spacing=8, name="frame_v_stack"):
                ui.Spacer(height=0)
                for index, op_name in enumerate(xform_op_order):
                    is_valid_op = index >= reset_index and xform_op_utils.is_valid_op_name(op_name)
                    attr_name = xform_op_utils.get_op_attr_name(op_name)
                    attr = Usd.Attribute()
                    if attr_name is not None:
                        attr = prim.GetAttribute(attr_name)
                    widget = self._create_xform_op_widget(
                        prim_paths, collapsable_frame, stage, attr, op_name, is_valid_op, order_attr, index
                    )
                    if widget is not None:
                        self._widgets.append(widget)
                        # self._models.append(widget._model)
                        model = widget._model
                        if isinstance(model, list):
                            for m in model:
                                if m is not None:
                                    for sdf_path in m.get_attribute_paths():
                                        self._models[sdf_path].append(m)
                        elif model is not None:
                            for sdf_path in model.get_attribute_paths():
                                self._models[sdf_path].append(model)

                        if isinstance(widget, USDXformOpOrientWidget):
                            if widget._euler_model is not None:
                                for sdf_path in widget._euler_model.get_attribute_paths():
                                    self._models[sdf_path].append(widget._euler_model)

                if len(attr_not_in_order_xform_ops) > 0:
                    ui.Separator()
                    for attr in attr_not_in_order_xform_ops:
                        widget = self._create_xform_op_widget(prim_paths, collapsable_frame, stage, attr)
                        if widget is not None:
                            self._widgets.append(widget)

                ui.Spacer(height=0)
        return self._models, False

    def build_transform_offset_frame(self, prim_paths, collapsable_frame, stage):

        self._clear_widgets()

        translate_model = VecAttributeModel(
            default_value = "0.0",
            begin_edit_callback = functools.partial(self._start_field_edit, transform=TRANSFORM_OP_MOVE),
            end_edit_callback = functools.partial(self._on_translate_offset, stage=stage, prim_paths=prim_paths)
        )
        rotate_model = VecAttributeModel(
            default_value = "0.0",
            begin_edit_callback = functools.partial(self._start_field_edit, transform=TRANSFORM_OP_ROTATE),
            end_edit_callback = functools.partial(self._on_rotate_offset, stage=stage, prim_paths=prim_paths)
        )
        scale_model = VecAttributeModel(
            default_value = "0.0",
            begin_edit_callback = functools.partial(self._start_field_edit, transform=TRANSFORM_OP_SCALE),
            end_edit_callback = functools.partial(self._on_scale_offset, stage=stage, prim_paths=prim_paths)
        )

        def match(label):
            return self._parent_widget._filter.matches(label)

        highlight = self._parent_widget._filter.name
        label_kwargs = {"highlight": highlight}

        HEIGHT_SPACE = 8
        with ui.VStack():
            if match("Translate"):
                self._parent_widget._any_item_visible = True
                ui.Spacer(height = HEIGHT_SPACE)
                with ui.HStack():
                    with ui.HStack(width=LABEL_PADDING):
                        HighlightLabel("Translate", name="title", **label_kwargs)
                        ui.Spacer(width=2)
                    translate_stack = ui.HStack(identifier = "translate_stack")
                    with translate_stack:
                        with ui.ZStack():
                            kwargs = {"step": 1.0}
                            self._string_translate_widget = self._create_multi_string_with_labels(
                                ui_widget = ui.StringField,
                                model = translate_model,
                                labels = [("X", 0xFF5555AA), ("Y", 0xFF76A371), ("Z", 0xFFA07D4F)],
                                comp_count = 3
                            )
                            for widget in self._string_translate_widget:
                                widget.visible = False
                            self._float_translate_widget = self._create_multi_string_with_labels(
                                ui_widget = ui.FloatDrag,
                                model = translate_model,
                                labels = [("X", 0xFF5555AA), ("Y", 0xFF76A371), ("Z", 0xFFA07D4F)],
                                comp_count = 3
                            )
                    translate_stack.set_mouse_double_clicked_fn(lambda x, y, b, m: self._on_double_click(TRANSFORM_OP_MOVE))
                    translate_stack.set_mouse_pressed_fn(lambda x, y, b, m: self._on_press())

            if match("Rotate"):
                self._parent_widget._any_item_visible = True
                ui.Spacer(height = HEIGHT_SPACE)
                with ui.HStack():
                    with ui.HStack(width=LABEL_PADDING):
                        HighlightLabel("Rotate", name="title", **label_kwargs)
                        ui.Spacer(width=2)
                    rotate_stack = ui.HStack(identifier = "rotate_stack")
                    with rotate_stack:
                        with ui.ZStack():
                            kwargs = {"step": 1.0}
                            self._string_rotate_widget = self._create_multi_string_with_labels(
                                    ui_widget = ui.StringField,
                                    model = rotate_model,
                                    labels = [("X", 0xFF5555AA), ("Y", 0xFF76A371), ("Z", 0xFFA07D4F)],
                                    comp_count = 3
                            )
                            for widget in self._string_rotate_widget:
                                widget.visible = False
                            self._float_rotate_widget = self._create_multi_string_with_labels(
                                ui_widget = ui.FloatDrag,
                                model = rotate_model,
                                labels = [("X", 0xFF5555AA), ("Y", 0xFF76A371), ("Z", 0xFFA07D4F)],
                                comp_count = 3
                            )
                    rotate_stack.set_mouse_double_clicked_fn(lambda x, y, b, m: self._on_double_click(TRANSFORM_OP_ROTATE))
                    rotate_stack.set_mouse_pressed_fn(lambda x, y, b, m: self._on_press())

            if match("Scale"):
                self._parent_widget._any_item_visible = True
                ui.Spacer(height = HEIGHT_SPACE)
                with ui.HStack():
                    with ui.HStack(width=LABEL_PADDING):
                        HighlightLabel("Scale", name="title", **label_kwargs)
                        if self._parent_widget._link_scale:
                            button_style = {
                            "color": 0xFF34B5FF,
                            "background_color": 0x0,
                            "Button.Image": {"image_url": f"{ICON_PATH}/link_on.svg"},
                            ":hovered": {"color": 0xFFACDCF9}
                            }
                        else:
                            button_style = {
                            "color": 0x66FFFFFF,
                            "background_color": 0x0,
                            "Button.Image": {"image_url": f"{ICON_PATH}/link_off.svg"},
                            ":hovered": {"color": 0xFFFFFFFF}
                            }
                        with ui.ZStack(content_clipping = True, width=25, height=25):
                            ui.Button("",
                                    style = button_style,
                                    clicked_fn = self._parent_widget._toggle_link_scale,
                                    identifier = "toggle_link_offset_scale",
                                    tooltip = "Toggle link scale")
                        ui.Spacer(width=70)
                    scale_stack = ui.HStack(identifier = "scale_stack")
                    with scale_stack:
                        with ui.ZStack():
                            kwargs = {"step": 1.0}
                            self._string_scale_widget = self._create_multi_string_with_labels(
                                    ui_widget = ui.StringField,
                                    model = scale_model,
                                    labels = [("X", 0xFF5555AA), ("Y", 0xFF76A371), ("Z", 0xFFA07D4F)],
                                    comp_count = 3
                            )
                            for widget in self._string_scale_widget:
                                widget.visible = False
                            self._float_scale_widget = self._create_multi_string_with_labels(
                                ui_widget = ui.FloatDrag,
                                model = scale_model,
                                labels = [("X", 0xFF5555AA), ("Y", 0xFF76A371), ("Z", 0xFFA07D4F)],
                                comp_count = 3
                            )
                    scale_stack.set_mouse_double_clicked_fn(lambda x, y, b, m: self._on_double_click(TRANSFORM_OP_SCALE))
                    scale_stack.set_mouse_pressed_fn(lambda x, y, b, m: self._on_press())


        return None
