{
  "version": 1,
  "kind": "shelf_items",
  "shelf": "tdToolKit Library",
  "tab": "Widgets",
  "items": [
    {
      "kind": "widget",
      "id": "wid_rig_snaps",
      "label": "Snaps and Live",
      "icon": "",
      "tooltip": "Grid, curve and point snapping, and the live surface",
      "command": "\"\"\"Snaps: grid, curve and point snapping as one light each, lit while on,\nand a Live light that shows when a surface is live (its name in the tooltip)\nand clears it on click; with nothing live, a click makes the selection live.\"\"\"\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 paint_grid(painter, box, on, ink, faint):\n    x, y, w, h = box.left(), box.top(), box.width(), box.height()\n    painter.setPen(QtGui.QPen(faint, 1.2))\n    for t in (0.0, 0.5, 1.0):\n        painter.drawLine(QtCore.QPointF(x, y + h * t), QtCore.QPointF(x + w, y + h * t))\n        painter.drawLine(QtCore.QPointF(x + w * t, y), QtCore.QPointF(x + w * t, y + h))\n    painter.setPen(Qt.PenStyle.NoPen)\n    painter.setBrush(ink)\n    painter.drawEllipse(QtCore.QPointF(x + w * 0.5, y + h * 0.5), 2.4, 2.4)\n\n\ndef paint_curve(painter, box, on, ink, faint):\n    x, y, w, h = box.left(), box.top(), box.width(), box.height()\n    path = QtGui.QPainterPath(QtCore.QPointF(x, y + h * 0.9))\n    path.cubicTo(QtCore.QPointF(x + w * 0.2, y - h * 0.2), QtCore.QPointF(x + w * 0.8, y + h * 1.2),\n                 QtCore.QPointF(x + w, y + h * 0.1))\n    painter.setPen(QtGui.QPen(faint, 1.8, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap))\n    painter.setBrush(Qt.BrushStyle.NoBrush)\n    painter.drawPath(path)\n    painter.setPen(Qt.PenStyle.NoPen)\n    painter.setBrush(ink)\n    painter.drawEllipse(path.pointAtPercent(0.5), 2.4, 2.4)\n\n\ndef paint_point(painter, box, on, ink, faint):\n    x, y, w, h = box.left(), box.top(), box.width(), box.height()\n    c = box.center()\n    diamond = QtGui.QPolygonF([QtCore.QPointF(c.x(), y), QtCore.QPointF(x + w, c.y()),\n                               QtCore.QPointF(c.x(), y + h), QtCore.QPointF(x, c.y())])\n    painter.setPen(QtGui.QPen(faint, 1.4, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin))\n    painter.setBrush(Qt.BrushStyle.NoBrush)\n    painter.drawPolygon(diamond)\n    painter.setPen(Qt.PenStyle.NoPen)\n    painter.setBrush(ink)\n    painter.drawEllipse(c, 2.4, 2.4)\n\n\ndef paint_live(painter, box, on, ink, faint):\n    x, y, w, h = box.left(), box.top(), box.width(), box.height()\n    # A surface seen edge-on with a pin standing on it: what \"live\" means.\n    sheet = QtGui.QPainterPath(QtCore.QPointF(x, y + h * 0.85))\n    sheet.quadTo(QtCore.QPointF(x + w * 0.5, y + h * 0.5), QtCore.QPointF(x + w, y + h * 0.85))\n    painter.setPen(QtGui.QPen(ink, 1.6, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap))\n    painter.setBrush(Qt.BrushStyle.NoBrush)\n    painter.drawPath(sheet)\n    painter.setPen(QtGui.QPen(ink, 1.8, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap))\n    painter.drawLine(QtCore.QPointF(x + w * 0.5, y + h * 0.66), QtCore.QPointF(x + w * 0.5, y + h * 0.22))\n    painter.setPen(Qt.PenStyle.NoPen)\n    painter.setBrush(ink if on else faint)\n    painter.drawEllipse(QtCore.QPointF(x + w * 0.5, y + h * 0.16), w * 0.16, w * 0.16)\n\n\nSNAPS = ((\"grid\", \"Snap to grids\", paint_grid), (\"curve\", \"Snap to curves\", paint_curve),\n         (\"point\", \"Snap to points\", paint_point))\n\n\ndef live_objects():\n    return cmds.ls(live=True) or []\n\n\ndef build(parent, item):\n    palette = palette_of(parent)\n    size = row_height_of(parent)\n    row = Row(parent, \"tdShelfSnaps\")\n    snaps = {}\n    for kind, what, icon in SNAPS:\n        light = row.add(Light(row, size, palette, icon=icon))\n        light.setProperty(\"tdSnap\", kind)\n        light.clicked.connect(lambda _checked=False, k=kind: toggle_snap(k))\n        snaps[kind] = (light, what)\n    live = row.add(Light(row, size, palette, icon=paint_live, lit=\"@colors.warning\", lit_fallback=\"#c8a24b\"))\n    live.setObjectName(\"tdShelfLiveLight\")\n    row.snaps, row.live = snaps, live\n\n    def refresh():\n        for kind, (light, what) in snaps.items():\n            on = bool(cmds.snapMode(query=True, **{kind: True}))\n            light.set_state(on)\n            light.setToolTip(\"%s: %s.\\nClick to turn it %s.\" % (what, \"on\" if on else \"off\", \"off\" if on else \"on\"))\n        names = live_objects()\n        live.set_state(bool(names))\n        if names:\n            live.setToolTip(\"Live surface: %s\\nClick to make nothing live.\" % \", \".join(names))\n        else:\n            live.setToolTip(\"Live surface: none.\\nClick to make the selection live.\")\n\n    def toggle_snap(kind):\n        with maya_utils.undo_paused():\n            cmds.snapMode(**{kind: not bool(cmds.snapMode(query=True, **{kind: True}))})\n        refresh()\n\n    def toggle_live():\n        if live_objects():\n            cmds.makeLive(none=True)\n        elif cmds.ls(selection=True):\n            cmds.makeLive()\n        refresh()\n\n    live.clicked.connect(lambda *_: toggle_live())\n    listen(row, refresh, events=(\"snapModeChanged\", \"SelectionChanged\", \"Undo\", \"Redo\", \"SceneOpened\", \"NewSceneOpened\"),\n           poll_ms=1500)                            # nothing announces makeLive\n    refresh()\n    return row\n",
      "language": "python",
      "extra": {
        "entry": "build"
      }
    }
  ]
}
