{
  "version": 1,
  "kind": "shelf_items",
  "shelf": "tdToolKit Library",
  "tab": "Widgets",
  "items": [
    {
      "kind": "widget",
      "id": "wid_rig_refs",
      "label": "References",
      "icon": "",
      "tooltip": "The scene's references by state; load / unload the chosen one, right-click for more",
      "command": "\"\"\"References: the scene's references in a drop-down colored by state (loaded,\nunloaded, file missing), a light beside it that loads or unloads the chosen\none, and a right-click menu to reload it or select everything in its\nnamespace. Follows references being created, removed, loaded and unloaded.\"\"\"\nimport os\n\nfrom maya import cmds\nimport maya.api.OpenMaya as om\nfrom tdtoolkit import maya_utils\nfrom tdtoolkit.qt import Qt, QtCore, QtGui, QtWidgets, is_valid, exec_\nfrom tdtoolkit.widgets import resolve_color\n\n\ndef palette_of(widget):\n    \"\"\"The applied theme's colors, from the shelf that holds the widget.\"\"\"\n    while widget is not None:\n        fn = getattr(widget, \"theme_palette\", None)\n        if callable(fn):\n            try:\n                return fn()\n            except Exception:\n                return {}\n        widget = widget.parentWidget()\n    return {}\n\n\ndef row_height_of(widget):\n    while widget is not None:\n        fn = getattr(widget, \"row_height\", None)\n        if callable(fn):\n            try:\n                return int(fn())\n            except Exception:\n                break\n        widget = widget.parentWidget()\n    return 40\n\n\nclass Light(QtWidgets.QToolButton):\n    \"\"\"A button lit in a color while its state is on. Draws an icon\n    (``icon(painter, box, on, ink, faint)``) or a word.\"\"\"\n\n    def __init__(self, parent, size, palette, icon=None, text=\"\", width=None,\n                 lit=\"@colors.accent\", lit_fallback=\"#5285a6\"):\n        super().__init__(parent)\n        self.setObjectName(\"tdShelfLight\")\n        self.setCheckable(True)\n        self.setCursor(Qt.CursorShape.PointingHandCursor)\n        self.setFocusPolicy(Qt.FocusPolicy.NoFocus)\n        self._icon = icon\n        self._text = text\n        self._on_fill = resolve_color(lit, palette, lit_fallback)\n        self._off_fill = resolve_color(\"@colors.base\", palette, \"#2b2b2b\")\n        self._ink_on = resolve_color(\"@colors.text\", palette, \"#eeeeee\").lighter(115)\n        self._ink_off = resolve_color(\"@colors.text_disabled\", palette, \"#8a8a8a\")\n        self._border = resolve_color(\"@colors.border\", palette, \"#444444\")\n        self._hover = False\n        font = QtGui.QFont(self.font())\n        font.setBold(True)\n        font.setPixelSize(max(9, int(size * 0.34)))\n        self.setFont(font)\n        if width is None:\n            width = size if not text else max(size, QtGui.QFontMetrics(font).horizontalAdvance(text) + 16)\n        self.setFixedSize(int(width), int(size))\n\n    def set_lit_color(self, color):\n        self._on_fill = QtGui.QColor(color)\n        self.update()\n\n    def set_text(self, text):\n        self._text = text\n        self.update()\n\n    def set_state(self, on):\n        \"\"\"The state as read from Maya: shown, never announced.\"\"\"\n        self.blockSignals(True)\n        self.setChecked(bool(on))\n        self.blockSignals(False)\n        self.update()\n\n    def enterEvent(self, event):\n        self._hover = True\n        self.update()\n        super().enterEvent(event)\n\n    def leaveEvent(self, event):\n        self._hover = False\n        self.update()\n        super().leaveEvent(event)\n\n    def paintEvent(self, event):\n        on = self.isChecked()\n        painter = QtGui.QPainter(self)\n        painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True)\n        if not self.isEnabled():\n            painter.setOpacity(0.45)\n        rect = QtCore.QRectF(self.rect()).adjusted(1.5, 1.5, -1.5, -1.5)\n        fill = QtGui.QColor(self._on_fill if on else self._off_fill)\n        if self._hover:\n            fill = fill.lighter(118)\n        if self.isDown():\n            fill = fill.darker(115)\n        painter.setPen(QtGui.QPen(self._on_fill.lighter(125) if on else self._border, 1))\n        painter.setBrush(fill)\n        painter.drawRoundedRect(rect, 5, 5)\n        if on:\n            # A lit button carries a light bar along its foot as well, so the\n            # state reads at a glance even where the accent is close to the base.\n            bar = QtCore.QRectF(rect.left() + 6, rect.bottom() - 4, rect.width() - 12, 2.5)\n            painter.setPen(Qt.PenStyle.NoPen)\n            painter.setBrush(self._ink_on)\n            painter.drawRoundedRect(bar, 1, 1)\n        ink = self._ink_on if on else self._ink_off\n        faint = QtGui.QColor(ink)\n        faint.setAlpha(150 if on else 110)\n        if self._text:\n            painter.setPen(ink)\n            painter.setFont(self.font())\n            painter.drawText(rect.adjusted(0, 0, 0, -3), Qt.AlignmentFlag.AlignCenter, self._text)\n        elif self._icon is not None:\n            inset = max(6.0, rect.height() * 0.2)\n            box = rect.adjusted(inset, inset - 1, -inset, -inset - 2)\n            self._icon(painter, box, on, ink, faint)\n        painter.end()\n\n\nclass Row(QtWidgets.QWidget):\n    \"\"\"Lights side by side, as one shelf widget.\"\"\"\n\n    def __init__(self, parent, name):\n        super().__init__(parent)\n        self.setObjectName(name)\n        self._layout = QtWidgets.QHBoxLayout(self)\n        self._layout.setContentsMargins(0, 0, 0, 0)\n        self._layout.setSpacing(3)\n\n    def add(self, widget):\n        self._layout.addWidget(widget)\n        return widget\n\n\ndef listen(widget, refresh, events=(), scene=(), poll_ms=0):\n    \"\"\"Call ``refresh`` after any of Maya's ``events`` (names for\n    ``MEventMessage``; one this Maya lacks is skipped) or ``scene`` messages\n    (``MSceneMessage`` kinds), coalesced through a 0 ms timer, and every\n    ``poll_ms`` for a state Maya announces nowhere. Everything is taken off\n    again with the widget: a shelf rebuilds its widgets often.\"\"\"\n    timer = QtCore.QTimer(widget)\n    timer.setSingleShot(True)\n    timer.setInterval(0)\n\n    def safe():\n        if is_valid(widget):\n            refresh()\n\n    timer.timeout.connect(safe)\n    handles, heard = [], []\n    for event in events:\n        try:\n            handles.append(om.MEventMessage.addEventCallback(event, lambda *_: timer.start()))\n            heard.append(event)\n        except Exception:\n            pass\n    for kind in scene:\n        handles.append(om.MSceneMessage.addCallback(kind, lambda *_: timer.start()))\n    if poll_ms:\n        poll = QtCore.QTimer(widget)\n        poll.setInterval(int(poll_ms))\n        poll.timeout.connect(safe)\n        poll.start()\n    widget.destroyed.connect(lambda *_: maya_utils.remove_callbacks(handles))\n    widget.heard = heard\n    return timer\n\n\n\n\ndef references():\n    rows = []\n    for node in cmds.ls(type=\"reference\") or []:\n        if \"sharedReferenceNode\" in node:\n            continue\n        try:\n            path = cmds.referenceQuery(node, filename=True, withoutCopyNumber=True)\n        except Exception:\n            continue                          # a reference node with no file behind it\n        try:\n            namespace = cmds.referenceQuery(node, namespace=True).lstrip(\":\")\n        except Exception:\n            namespace = \"\"\n        try:\n            loaded = bool(cmds.referenceQuery(node, isLoaded=True))\n        except Exception:\n            loaded = False\n        rows.append({\"node\": node, \"path\": path, \"namespace\": namespace or node,\n                     \"loaded\": loaded, \"missing\": not os.path.isfile(path)})\n    return rows\n\n\ndef paint_reload(painter, box, on, ink, faint):\n    x, y, w, h = box.left(), box.top(), box.width(), box.height()\n    # A circular arrow: most of a ring, with an arrow head where it ends.\n    ring = QtCore.QRectF(x + w * 0.1, y + h * 0.1, w * 0.8, h * 0.8)\n    painter.setPen(QtGui.QPen(ink, 2.0, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap))\n    painter.setBrush(Qt.BrushStyle.NoBrush)\n    painter.drawArc(ring, 40 * 16, 280 * 16)\n    tip = QtCore.QPointF(x + w * 0.9, y + h * 0.28)\n    painter.setPen(Qt.PenStyle.NoPen)\n    painter.setBrush(ink)\n    painter.drawPolygon(QtGui.QPolygonF([tip, QtCore.QPointF(tip.x() - w * 0.3, tip.y() - h * 0.02),\n                                         QtCore.QPointF(tip.x() - w * 0.02, tip.y() + h * 0.3)]))\n\n\nclass RefCombo(QtWidgets.QComboBox):\n    def __init__(self, parent, on_menu):\n        super().__init__(parent)\n        self._on_menu = on_menu\n        self.setFocusPolicy(Qt.FocusPolicy.NoFocus)\n\n    def contextMenuEvent(self, event):\n        self._on_menu(event.globalPos())\n\n    def wheelEvent(self, event):\n        event.ignore()                        # the wheel scrolls the shelf, never switches a reference\n\n\ndef build(parent, item):\n    palette = palette_of(parent)\n    size = row_height_of(parent)\n    row = Row(parent, \"tdShelfReferences\")\n    colors = {\n        \"loaded\": resolve_color(\"@colors.text\", palette, \"#dddddd\"),\n        \"unloaded\": resolve_color(\"@colors.text_dim\", palette, \"#a0a0a0\"),\n        \"missing\": resolve_color(\"@colors.error\", palette, \"#c85a4b\"),\n    }\n    combo = row.add(RefCombo(row, lambda pos: menu_at(pos)))\n    combo.setFixedHeight(size)\n    combo.setMinimumWidth(int(size * 4.5))\n    combo.setSizeAdjustPolicy(QtWidgets.QComboBox.SizeAdjustPolicy.AdjustToContents)\n    toggle = row.add(Light(row, size, palette, icon=paint_reload))\n    toggle.setObjectName(\"tdShelfReferenceLoad\")\n    row.combo, row.toggle = combo, toggle\n\n    def chosen():\n        return combo.itemData(combo.currentIndex()) if combo.count() else None\n\n    def refresh():\n        keep = chosen()\n        rows = references()\n        combo.blockSignals(True)\n        combo.clear()\n        for ref in rows:\n            state = \"missing\" if ref[\"missing\"] else (\"loaded\" if ref[\"loaded\"] else \"unloaded\")\n            combo.addItem(ref[\"namespace\"], ref)\n            index = combo.count() - 1\n            combo.setItemData(index, QtGui.QBrush(colors[state]), Qt.ItemDataRole.ForegroundRole)\n            combo.setItemData(index, \"%s\\n%s%s\" % (ref[\"path\"], state, \", file not found\" if ref[\"missing\"] else \"\"),\n                              Qt.ItemDataRole.ToolTipRole)\n            if keep and keep.get(\"node\") == ref[\"node\"]:\n                combo.setCurrentIndex(index)\n        if not rows:\n            combo.addItem(\"(no references)\", None)\n        combo.setEnabled(bool(rows))\n        combo.blockSignals(False)\n        sync_toggle()\n\n    def sync_toggle():\n        ref = chosen()\n        toggle.setEnabled(ref is not None and not ref[\"missing\"])\n        toggle.set_state(bool(ref and ref[\"loaded\"]))\n        if ref is None:\n            toggle.setToolTip(\"No reference chosen.\")\n        elif ref[\"missing\"]:\n            toggle.setToolTip(\"%s: its file is missing\\n%s\" % (ref[\"namespace\"], ref[\"path\"]))\n        else:\n            toggle.setToolTip(\"%s: %s\\n%s\\nClick to %s it.\" % (\n                ref[\"namespace\"], \"loaded\" if ref[\"loaded\"] else \"unloaded\", ref[\"path\"],\n                \"unload\" if ref[\"loaded\"] else \"load\"))\n        combo.setToolTip(combo.itemData(combo.currentIndex(), Qt.ItemDataRole.ToolTipRole) or \"The scene's references\")\n\n    def load_or_unload():\n        ref = chosen()\n        if ref is None:\n            return\n        if ref[\"loaded\"]:\n            cmds.file(unloadReference=ref[\"node\"])\n        else:\n            cmds.file(loadReference=ref[\"node\"])\n        refresh()\n\n    def menu_at(pos):\n        ref = chosen()\n        if ref is None:\n            return\n        menu = QtWidgets.QMenu(combo)\n        menu.addAction(\"Select All Nodes in %s\" % ref[\"namespace\"], lambda: select_all(ref))\n        menu.addAction(\"Reload\", lambda: (cmds.file(loadReference=ref[\"node\"]), refresh()))\n        menu.addAction(\"Unload\" if ref[\"loaded\"] else \"Load\", load_or_unload)\n        exec_(menu, pos)\n        menu.deleteLater()\n\n    def select_all(ref):\n        nodes = cmds.referenceQuery(ref[\"node\"], nodes=True, dagPath=True) or []\n        cmds.select(nodes, replace=True)\n\n    toggle.clicked.connect(lambda *_: load_or_unload())\n    combo.currentIndexChanged.connect(lambda *_: sync_toggle())\n    kinds = om.MSceneMessage\n    listen(row, refresh, events=(\"Undo\", \"Redo\"),\n           scene=(kinds.kAfterOpen, kinds.kAfterNew, kinds.kAfterImport, kinds.kAfterCreateReference,\n                  kinds.kAfterRemoveReference, kinds.kAfterLoadReference, kinds.kAfterUnloadReference))\n    refresh()\n    return row\n",
      "language": "python",
      "extra": {
        "entry": "build"
      }
    }
  ]
}
