{
  "version": 1,
  "kind": "shelf_items",
  "shelf": "tdToolKit Library",
  "tab": "Widgets",
  "items": [
    {
      "kind": "widget",
      "id": "wid_rig_range",
      "label": "Frame Range Presets",
      "icon": "",
      "tooltip": "Full, Render, Keys and \u00b110: click to set the playback range; the one it equals is lit",
      "command": "\"\"\"Frame range presets: Full, Render, Keys and +-10, the one matching the\nplayback range lit. Follows the Range Slider, the current frame and the\nselection, so a range set by hand lights the preset it happens to equal.\"\"\"\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\nPRESETS = (\n    (\"Full\", None, \"the whole animation (the Range Slider's outer ends)\"),\n    (\"Render\", None, \"the render range (Render Settings)\"),\n    (\"Keys\", None, \"first to last key on the selection (every key with nothing selected)\"),\n    (\"\\u00b110\", 10, \"ten frames either side of the current frame\"),\n    (\"\\u00b120\", 20, \"twenty frames either side of the current frame\"),\n    (\"\\u00b150\", 50, \"fifty frames either side of the current frame\"),\n    (\"\\u00b1100\", 100, \"a hundred frames either side of the current frame\"),\n)\nSPANS = {name: span for name, span, _what in PRESETS if span}\n\n\ndef preset_range(name):\n    \"\"\"The (start, end) a preset stands for now, or None when it has none.\"\"\"\n    if name == \"Full\":\n        return cmds.playbackOptions(query=True, animationStartTime=True), cmds.playbackOptions(query=True, animationEndTime=True)\n    if name == \"Render\":\n        return cmds.getAttr(\"defaultRenderGlobals.startFrame\"), cmds.getAttr(\"defaultRenderGlobals.endFrame\")\n    if name == \"Keys\":\n        nodes = cmds.ls(selection=True) or cmds.ls(type=\"animCurve\") or []\n        times = cmds.keyframe(nodes, query=True, timeChange=True) if nodes else None\n        if not times:\n            return None\n        return min(times), max(times)\n    span = SPANS.get(name)\n    if not span:\n        return None\n    now = cmds.currentTime(query=True)\n    return now - span, now + span\n\n\ndef current_range():\n    return cmds.playbackOptions(query=True, minTime=True), cmds.playbackOptions(query=True, maxTime=True)\n\n\ndef apply_range(lo, hi):\n    with maya_utils.undo_paused():                # playbackOptions is an unnamed undo step\n        if lo < cmds.playbackOptions(query=True, animationStartTime=True):\n            cmds.playbackOptions(animationStartTime=lo)\n        if hi > cmds.playbackOptions(query=True, animationEndTime=True):\n            cmds.playbackOptions(animationEndTime=hi)\n        cmds.playbackOptions(minTime=lo, maxTime=hi)\n\n\ndef build(parent, item):\n    palette = palette_of(parent)\n    size = row_height_of(parent)\n    row = Row(parent, \"tdShelfRangePresets\")\n    lights = {}\n    for name, _span, what in PRESETS:\n        light = row.add(Light(row, size, palette, text=name))\n        light.setProperty(\"tdPreset\", name)\n        light.clicked.connect(lambda _checked=False, n=name: choose(n))\n        lights[name] = (light, what)\n\n    def refresh():\n        lo, hi = current_range()\n        for name, (light, what) in lights.items():\n            rng = preset_range(name)\n            on = rng is not None and abs(rng[0] - lo) < 1e-6 and abs(rng[1] - hi) < 1e-6\n            light.setEnabled(rng is not None)\n            light.set_state(on)\n            if rng is None:\n                light.setToolTip(\"%s: %s\\nNo keys to take a range from.\" % (name, what))\n            else:\n                light.setToolTip(\"%s: %s\\n%g to %g%s\" % (name, what, rng[0], rng[1],\n                                 \" (the playback range now)\" if on else \"\\nClick to make it the playback range.\"))\n\n    def choose(name):\n        rng = preset_range(name)\n        if rng is not None:\n            apply_range(*rng)\n        refresh()\n\n    listen(row, refresh, events=(\"playbackRangeChanged\", \"timeChanged\", \"SelectionChanged\",\n                                 \"SceneOpened\", \"NewSceneOpened\", \"Undo\", \"Redo\"))\n    refresh()\n    return row\n",
      "language": "python",
      "extra": {
        "entry": "build"
      }
    }
  ]
}
