|
| 1 | +""" |
| 2 | +* SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. |
| 3 | +* SPDX-License-Identifier: Apache-2.0 |
| 4 | +* |
| 5 | +* Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 | +* you may not use this file except in compliance with the License. |
| 7 | +* You may obtain a copy of the License at |
| 8 | +* |
| 9 | +* https://www.apache.org/licenses/LICENSE-2.0 |
| 10 | +* |
| 11 | +* Unless required by applicable law or agreed to in writing, software |
| 12 | +* distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | +* See the License for the specific language governing permissions and |
| 15 | +* limitations under the License. |
| 16 | +""" |
| 17 | + |
| 18 | +from __future__ import annotations |
| 19 | + |
| 20 | +__all__ = ["RemixBackdropDelegate", "trigger_backdrop_rename", "clear_edit_triggers"] |
| 21 | + |
| 22 | +import weakref |
| 23 | +from functools import partial |
| 24 | +from typing import Callable |
| 25 | + |
| 26 | +import omni.ui as ui |
| 27 | +from omni.kit.graph.delegate.modern import HEADER, HIGHLIGHT_THICKNESS, PORT_VISIBLE_MIN |
| 28 | +from omni.kit.graph.delegate.modern.backdrop_delegate import BackdropDelegate, color_to_hex, darker_color, hex_to_color |
| 29 | +from omni.kit.widget.graph.abstract_graph_node_delegate import GraphNodeDescription |
| 30 | +from pxr import Sdf |
| 31 | + |
| 32 | +# Registry of inline edit trigger callbacks, keyed by prim path. |
| 33 | +# Uses weak references to UI widgets to allow garbage collection. |
| 34 | +_edit_triggers: dict[Sdf.Path, tuple[weakref.ref, Callable]] = {} |
| 35 | + |
| 36 | + |
| 37 | +def trigger_backdrop_rename(prim_path: Sdf.Path) -> bool: |
| 38 | + """Trigger inline rename for a backdrop at the given path. Returns True if successful.""" |
| 39 | + if prim_path not in _edit_triggers: |
| 40 | + return False |
| 41 | + |
| 42 | + widget_ref, trigger_fn = _edit_triggers[prim_path] |
| 43 | + if widget_ref() is None: |
| 44 | + # Widget was garbage collected, clean up the stale entry |
| 45 | + del _edit_triggers[prim_path] |
| 46 | + return False |
| 47 | + |
| 48 | + trigger_fn() |
| 49 | + return True |
| 50 | + |
| 51 | + |
| 52 | +def clear_edit_triggers(): |
| 53 | + """Clear all edit triggers. Call on extension shutdown to prevent memory leaks.""" |
| 54 | + _edit_triggers.clear() |
| 55 | + |
| 56 | + |
| 57 | +class RemixBackdropDelegate(BackdropDelegate): |
| 58 | + """ |
| 59 | + Custom BackdropDelegate that fixes the inline rename field styling. |
| 60 | + The original BackdropDelegate creates a StringField with no background, |
| 61 | + making it unreadable when overlapping the label. |
| 62 | + """ |
| 63 | + |
| 64 | + def node_header(self, model, node_desc: GraphNodeDescription): |
| 65 | + """Override to add proper background styling to the rename StringField.""" |
| 66 | + border_default = 0xFFD8B74B |
| 67 | + |
| 68 | + def set_color(model, node, item_model): |
| 69 | + sub_models = item_model.get_item_children() |
| 70 | + rgb = ( |
| 71 | + item_model.get_item_value_model(sub_models[0]).as_float, |
| 72 | + item_model.get_item_value_model(sub_models[1]).as_float, |
| 73 | + item_model.get_item_value_model(sub_models[2]).as_float, |
| 74 | + ) |
| 75 | + model[node].display_color = rgb |
| 76 | + model.selection = [] |
| 77 | + model._item_changed(None) # noqa: SLF001, PLW0212 |
| 78 | + |
| 79 | + header = ui.VStack() |
| 80 | + with header: |
| 81 | + node = node_desc.node |
| 82 | + node_name = model[node].name |
| 83 | + node_category = str(model[node].type) |
| 84 | + |
| 85 | + display_color = model[node].display_color |
| 86 | + if display_color: |
| 87 | + style = { |
| 88 | + f"Graph.Node.Category::{node_category}": {"background_color": color_to_hex(display_color)}, |
| 89 | + f"Graph.Node.Secondary::{node_category}": {"background_color": darker_color(display_color)}, |
| 90 | + } |
| 91 | + field_bg = darker_color(display_color) |
| 92 | + else: |
| 93 | + style = {} |
| 94 | + field_bg = 0xFF303030 |
| 95 | + |
| 96 | + ui.Spacer(height=HIGHLIGHT_THICKNESS) |
| 97 | + with ui.ZStack(): |
| 98 | + self.build_tooltip([node_name, node_category]) |
| 99 | + with ui.VStack(): |
| 100 | + with ui.ZStack(height=HEADER["sec_height"]): |
| 101 | + ui.Rectangle(style=style, name=node_category, style_type_name_override="Graph.Node.Secondary") |
| 102 | + with ui.VStack(): |
| 103 | + ui.Spacer() |
| 104 | + with ui.HStack(height=14): |
| 105 | + ui.Spacer(width=3) |
| 106 | + color_widget = ui.ColorWidget(height=14, width=14) |
| 107 | + |
| 108 | + sub_models = color_widget.model.get_item_children() |
| 109 | + border_color = model[node].display_color or hex_to_color(border_default) |
| 110 | + |
| 111 | + color_widget.model.get_item_value_model(sub_models[0]).as_float = border_color[0] |
| 112 | + color_widget.model.get_item_value_model(sub_models[1]).as_float = border_color[1] |
| 113 | + color_widget.model.get_item_value_model(sub_models[2]).as_float = border_color[2] |
| 114 | + color_widget.model.add_end_edit_fn(lambda m, i: set_color(model, node, m)) |
| 115 | + |
| 116 | + with ui.ZStack(): |
| 117 | + label = ui.Label( |
| 118 | + node_name, |
| 119 | + visible_min=PORT_VISIBLE_MIN, |
| 120 | + style_type_name_override="Graph.Node.Label", |
| 121 | + style={"margin_width": HEADER["margin_to_left"]}, |
| 122 | + ) |
| 123 | + |
| 124 | + # Styled rename field - key fix: add background color |
| 125 | + label_field = ui.StringField( |
| 126 | + style={ |
| 127 | + "background_color": field_bg, |
| 128 | + "border_radius": 2, |
| 129 | + "padding": 2, |
| 130 | + } |
| 131 | + ) |
| 132 | + label_field.visible = False |
| 133 | + |
| 134 | + def show_edit_field(field, label_widget, name, *args): |
| 135 | + field.model.set_value(name) |
| 136 | + field.visible = True |
| 137 | + label_widget.visible = False |
| 138 | + field.focus_keyboard() |
| 139 | + |
| 140 | + trigger_fn = partial(show_edit_field, label_field, label, node_name) |
| 141 | + label.set_mouse_double_clicked_fn(trigger_fn) |
| 142 | + |
| 143 | + # Register trigger for context menu access |
| 144 | + # (with weak ref to detect widget destruction) |
| 145 | + prim_path = node.GetPath() |
| 146 | + _edit_triggers[prim_path] = (weakref.ref(label_field), trigger_fn) |
| 147 | + |
| 148 | + def label_edited(field, label_widget, *args): |
| 149 | + field.visible = False |
| 150 | + label_widget.visible = True |
| 151 | + model[node].name = field.model.as_string |
| 152 | + model._item_changed(None) # noqa: SLF001, PLW0212 |
| 153 | + |
| 154 | + label_field.model.add_end_edit_fn(partial(label_edited, label_field, label)) |
| 155 | + ui.Spacer() |
| 156 | + ui.Spacer() |
| 157 | + ui.Rectangle( |
| 158 | + height=HEADER["height"], |
| 159 | + style=style, |
| 160 | + name=node_category, |
| 161 | + style_type_name_override="Graph.Node.Category", |
| 162 | + ) |
| 163 | + ui.Spacer(height=HIGHLIGHT_THICKNESS) |
| 164 | + return header |
0 commit comments