{
  "version": 1,
  "kind": "shelf_items",
  "shelf": "tdToolKit Library",
  "tab": "Widgets",
  "items": [
    {
      "kind": "widget",
      "id": "wid_102",
      "label": "Scrub",
      "icon": "",
      "tooltip": "Drag to change the current frame",
      "command": "\"\"\"A mini time slider: drag to scrub the playback range.\n\nScrubbing is kept off the undo queue, as Maya's own time slider is. The\nslider follows the current frame and the playback range however they change\n(the time slider, playback, a scene opening): it listens to Maya's\n``timeChanged``, ``playbackRangeChanged`` and scene-opened events, and takes\nthose callbacks off again with the widget.\n\nLaid out like every row on this shelf: its caption (``extra[\"caption\"]``) in\none column, then the slider, as wide as the other rows' controls.\n\"\"\"\nimport maya.api.OpenMaya as om\nfrom tdtoolkit.qt import Qt, QtCore, QtWidgets, is_valid\nfrom tdtoolkit.widgets import resolve_color\nfrom tdtoolkit import maya_utils\nfrom maya import cmds\n\nCAPTIONS = (\"Select\", \"Range\", \"Camera\", \"Time\")   # every captioned row on this shelf: the column fits the widest\nBODY_WIDTH = 190                                   # every row's controls, so the buttons after them line up\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\ndef captioned(parent, item, default):\n    \"\"\"A row laid out like every other on this shelf: the caption in a column\n    as wide as the widest of ``CAPTIONS`` (drawn as the shelf draws an inline\n    label), then ``BODY_WIDTH`` of controls. Answers the row, the layout to\n    put the controls in, and how tall a control should be.\"\"\"\n    palette = palette_of(parent)\n    height = max(20, row_height_of(parent) - 14)\n    row = QtWidgets.QWidget(parent)\n    layout = QtWidgets.QHBoxLayout(row)\n    layout.setContentsMargins(3, 0, 0, 0)\n    layout.setSpacing(0)\n    caption = QtWidgets.QLabel(str((getattr(item, \"extra\", None) or {}).get(\"caption\") or default), row)\n    color = resolve_color(\"@tools.shelf.heading\", palette)\n    if not color.isValid():\n        color = resolve_color(\"@colors.text\", palette, \"#dddddd\")\n    caption.setStyleSheet(\"color: %s; background: transparent;\" % color.name())\n    caption.setFixedWidth(max(caption.fontMetrics().horizontalAdvance(c) for c in CAPTIONS) + 8)\n    layout.addWidget(caption)\n    body = QtWidgets.QHBoxLayout()\n    body.setContentsMargins(0, 0, 0, 0)\n    body.setSpacing(4)\n    holder = QtWidgets.QWidget(row)\n    holder.setLayout(body)\n    holder.setFixedSize(BODY_WIDTH, height)\n    layout.addWidget(holder)\n    row.caption = caption\n    return row, holder, body, height\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, widget.handles = heard, handles\n    return timer\n\n\ndef build(parent, item):\n    row, holder, body, height = captioned(parent, item, \"Time\")\n    slider = QtWidgets.QSlider(Qt.Orientation.Horizontal, holder)\n    slider.setFixedHeight(height)\n    slider.setFocusPolicy(Qt.FocusPolicy.NoFocus)\n\n    def refresh():\n        lo = int(cmds.playbackOptions(query=True, minTime=True))\n        hi = int(cmds.playbackOptions(query=True, maxTime=True))\n        now = cmds.currentTime(query=True)\n        slider.blockSignals(True)\n        slider.setRange(lo, max(hi, lo + 1))\n        slider.setValue(int(now))\n        slider.blockSignals(False)\n        slider.setToolTip(\"Frame %g of %d to %d. Drag to scrub the playback range.\" % (now, lo, hi))\n\n    def scrub(value):\n        # currentTime from a script is an unnamed undo step: keep it off the queue\n        with maya_utils.undo_paused():\n            cmds.currentTime(value)\n\n    slider.valueChanged.connect(scrub)\n    body.addWidget(slider)\n    listen(row, refresh, events=(\"timeChanged\", \"playbackRangeChanged\", \"SceneOpened\", \"NewSceneOpened\"))\n    refresh()\n    return row\n",
      "language": "python",
      "extra": {
        "entry": "build",
        "caption": "Time"
      }
    }
  ]
}
