{
  "version": 1,
  "kind": "shelf_items",
  "shelf": "tdToolKit Library",
  "tab": "Widgets",
  "items": [
    {
      "kind": "widget",
      "id": "wid_rig_eval",
      "label": "Evaluation",
      "icon": "",
      "tooltip": "DG / Serial / Parallel, Cached Playback and GPU Override: the current state lit, click to change",
      "command": "\"\"\"Evaluation: DG / Serial / Parallel with the current mode lit, a Cached\nPlayback light (lit while on, green once the whole playback range is cached,\namber when safe mode has switched it off) and a GPU Override light. Click any\nto change it. The \"why is my rig slow\" row.\"\"\"\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\nMODES = (\n    (\"DG\", \"off\", \"DG: the classic dependency graph, one node at a time\"),\n    (\"Serial\", \"serial\", \"Serial: the evaluation graph, one node at a time (for finding cycles)\"),\n    (\"Parallel\", \"parallel\", \"Parallel: the evaluation graph across every core\"),\n)\nGREEN = \"#4c9a63\"           # a cache that holds the whole playback range\nGPU_GREEN = \"#55b46a\"       # GPU Override, lit\n# GPU Override, exactly as Preferences > Animation drives it (read out of\n# Maya's own createPrefWndUI.mel and initHUDScriptsEvaluationManager.mel):\n# the checkbox writes the ``gpuOverride`` option var and calls\n# ``turnOnOpenCLEvaluatorActive`` / ``turnOffOpenCLEvaluatorActive``, which\n# load the ``GPUBuiltInDeformer`` plug-in and toggle the *active* state of the\n# ``deformer`` evaluator -- ``evaluator -q -name deformer``, which is not the\n# same as ``evaluator -q -enable``. Driving ``-enable`` instead leaves the\n# preference untouched, which is what this widget used to do.\nGPU_EVALUATOR = \"deformer\"\nGPU_OPTION_VAR = \"gpuOverride\"\nGPU_PROCS = \"initHUDScriptsEvaluationManager.mel\"\n\n\ndef mode():\n    return (cmds.evaluationManager(query=True, mode=True) or [\"off\"])[0]\n\n\ndef cache_on():\n    return bool(cmds.evaluator(name=\"cache\", query=True, enable=True))\n\n\ndef gpu_supported():\n    return GPU_EVALUATOR in (cmds.evaluator(query=True) or [])\n\n\ndef _flag(value):\n    \"\"\"``evaluator -q`` answers with a bare value in some Mayas and a one-item\n    list in others.\"\"\"\n    if isinstance(value, (list, tuple)):\n        value = value[0] if value else False\n    return bool(value)\n\n\ndef gpu_on():\n    \"\"\"Whether GPU Override is running, as Maya's own ``isEvaluatorActive`` asks.\"\"\"\n    if not gpu_supported():\n        return False\n    try:\n        return _flag(cmds.evaluator(query=True, name=GPU_EVALUATOR))\n    except Exception:\n        return False\n\n\ndef gpu_preference():\n    \"\"\"What the Preferences checkbox is showing (it may lead the live state).\"\"\"\n    try:\n        return bool(cmds.optionVar(query=GPU_OPTION_VAR))\n    except Exception:\n        return False\n\n\ndef set_gpu(on):\n    \"\"\"Turn GPU Override on or off the way the preference checkbox does: the\n    option var it reads, then Maya's own proc, which loads the plug-in on\n    first use and rebuilds the graph.\"\"\"\n    try:\n        if not maya_utils.mel.eval(\"exists turnOnOpenCLEvaluatorActive\"):\n            maya_utils.mel.eval('source \"%s\"' % GPU_PROCS)\n    except Exception:\n        pass\n    cmds.optionVar(intValue=(GPU_OPTION_VAR, int(bool(on))))\n    maya_utils.mel.eval(\"turnOnOpenCLEvaluatorActive()\" if on else \"turnOffOpenCLEvaluatorActive()\")\n\n\ndef cache_fill():\n    \"\"\"How much of the playback range is cached, 0..1; None when Maya cannot say.\"\"\"\n    try:\n        frames = cmds.cacheEvaluator(query=True, cachedFrames=True) or []\n    except Exception:\n        return None\n    # Maya 2025 answers ``[[1, 5, 30]]``: one entry per cached range, its\n    # start and end the last two numbers. A flat ``[5, 30]`` is read too.\n    ranges = []\n    if frames and not isinstance(frames[0], (list, tuple)):\n        frames = [frames]\n    for entry in frames:\n        entry = list(entry)\n        if len(entry) >= 2:\n            ranges.append((float(entry[-2]), float(entry[-1])))\n    lo, hi = cmds.playbackOptions(query=True, minTime=True), cmds.playbackOptions(query=True, maxTime=True)\n    span = hi - lo + 1\n    if span <= 0:\n        return None\n    covered = 0.0\n    for start, end in ranges:\n        a, b = max(start, lo), min(end, hi)\n        if b >= a:\n            covered += b - a + 1\n    return min(1.0, covered / span)\n\n\ndef safe_mode_tripped():\n    try:\n        return bool(cmds.cacheEvaluator(query=True, safeModeMessages=True))\n    except Exception:\n        return False\n\n\ndef paint_cache(painter, box, on, ink, faint, fill):\n    x, y, w, h = box.left(), box.top(), box.width(), box.height()\n    # A drum (a cache), filling from the bottom as the range fills.\n    top = QtCore.QRectF(x + w * 0.15, y, w * 0.7, h * 0.3)\n    painter.setPen(QtGui.QPen(ink, 1.5, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap))\n    painter.setBrush(Qt.BrushStyle.NoBrush)\n    painter.drawLine(QtCore.QPointF(x + w * 0.15, y + h * 0.15), QtCore.QPointF(x + w * 0.15, y + h * 0.85))\n    painter.drawLine(QtCore.QPointF(x + w * 0.85, y + h * 0.15), QtCore.QPointF(x + w * 0.85, y + h * 0.85))\n    painter.drawArc(QtCore.QRectF(x + w * 0.15, y + h * 0.7, w * 0.7, h * 0.3), 180 * 16, 180 * 16)\n    if fill:\n        level = y + h * 0.85 - (h * 0.6) * fill\n        painter.setPen(Qt.PenStyle.NoPen)\n        painter.setBrush(faint)\n        painter.drawRect(QtCore.QRectF(x + w * 0.15, level, w * 0.7, y + h * 0.85 - level))\n    painter.setPen(QtGui.QPen(ink, 1.5))\n    painter.setBrush(faint if on else Qt.BrushStyle.NoBrush)\n    painter.drawEllipse(top)\n\n\ndef build(parent, item):\n    palette = palette_of(parent)\n    size = row_height_of(parent)\n    row = Row(parent, \"tdShelfEvaluation\")\n    state = {\"fill\": 0.0}\n    modes = {}\n    for label, value, what in MODES:\n        light = row.add(Light(row, size, palette, text=label))\n        light.setProperty(\"tdMode\", value)\n        light.setToolTip(what + \"\\nClick to switch to it.\")\n        light.clicked.connect(lambda _checked=False, v=value: set_mode(v))\n        modes[value] = light\n    cache = row.add(Light(row, size, palette, icon=lambda p, b, on, ink, faint: paint_cache(p, b, on, ink, faint, state[\"fill\"])))\n    cache.setObjectName(\"tdShelfCacheLight\")\n    gpu = row.add(Light(row, size, palette, text=\"GPU\", lit=GPU_GREEN, lit_fallback=GPU_GREEN))\n    gpu.setObjectName(\"tdShelfGpuLight\")\n    accent = resolve_color(\"@colors.accent\", palette, \"#5285a6\")\n    amber = resolve_color(\"@colors.warning\", palette, \"#c8a24b\")\n    row.modes, row.cache, row.gpu = modes, cache, gpu\n\n    def refresh():\n        current = mode()\n        for value, light in modes.items():\n            light.set_state(value == current)\n        on = cache_on()\n        fill = cache_fill() if on else None\n        state[\"fill\"] = fill or 0.0\n        tripped = on and safe_mode_tripped()\n        cache.set_lit_color(amber if tripped else (GREEN if fill is not None and fill >= 0.999 else accent))\n        cache.set_state(on)\n        if not on:\n            cache.setToolTip(\"Cached Playback: off.\\nClick to turn it on.\")\n        elif tripped:\n            cache.setToolTip(\"Cached Playback: on, but safe mode has switched it off for this scene.\\nClick to turn it off.\")\n        elif fill is None:\n            cache.setToolTip(\"Cached Playback: on.\\nClick to turn it off.\")\n        else:\n            cache.setToolTip(\"Cached Playback: on, %d%% of the playback range cached.\\nClick to turn it off.\" % round(fill * 100))\n        # Maya greys its own GPU Override checkbox out in DG mode, where the\n        # evaluator has nothing to run in; this follows.\n        supported, running = gpu_supported(), gpu_on()\n        gpu.setEnabled(supported and current != \"off\")\n        gpu.set_state(running)\n        if not supported:\n            gpu.setToolTip(\"GPU Override: not available in this Maya (no deformer evaluator is registered).\")\n        elif current == \"off\":\n            gpu.setToolTip(\"GPU Override: %s, but DG mode does not use it.\\nSwitch to Serial or Parallel first.\"\n                           % (\"on\" if running else \"off\"))\n        else:\n            tip = \"GPU Override: %s.\\nDeformers run on the GPU. Click to turn it %s (the same as Preferences > Animation).\" \\\n                  % (\"on\" if running else \"off\", \"off\" if running else \"on\")\n            if gpu_preference() != running:\n                tip += \"\\nThe preference says %s: the evaluator has not caught up.\" % (\"on\" if gpu_preference() else \"off\")\n            gpu.setToolTip(tip)\n\n    def set_mode(value):\n        with maya_utils.undo_paused():\n            cmds.evaluationManager(mode=value)\n        refresh()\n\n    def toggle_cache():\n        with maya_utils.undo_paused():\n            cmds.evaluator(name=\"cache\", enable=not cache_on())\n        refresh()\n\n    def toggle_gpu():\n        # Maya's proc re-triggers an evaluation (``currentTime``), which is an\n        # unnamed undo step, so the whole thing is kept off the queue.\n        if gpu_supported():\n            with maya_utils.undo_paused():\n                set_gpu(not gpu_on())\n        refresh()\n\n    cache.clicked.connect(lambda *_: toggle_cache())\n    gpu.clicked.connect(lambda *_: toggle_gpu())\n    listen(row, refresh, events=(\"customEvaluatorChanged\", \"cachingEvaluationModeChanged\", \"cachingPreferencesChanged\",\n                                 \"cachingSafeModeChanged\", \"playbackRangeChanged\", \"SceneOpened\", \"NewSceneOpened\"),\n           poll_ms=1000)                          # the cache fills during playback, and says nothing when it does\n    refresh()\n    return row\n",
      "language": "python",
      "extra": {
        "entry": "build"
      }
    }
  ]
}
