├── .gitignore ├── LICENSE ├── README.md ├── RELEASES.md └── capture_gui ├── __init__.py ├── accordion.py ├── app.py ├── colorpicker.py ├── lib.py ├── plugin.py ├── plugins ├── cameraplugin.py ├── codecplugin.py ├── defaultoptionsplugin.py ├── displayplugin.py ├── genericplugin.py ├── ioplugin.py ├── panzoomplugin.py ├── rendererplugin.py ├── resolutionplugin.py ├── timeplugin.py └── viewportplugin.py ├── presets.py ├── resources ├── config.png ├── import.png ├── reset.png └── save.png ├── tokens.py ├── vendor ├── Qt.py └── __init__.py └── version.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.pyc 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Roy Nieterau 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Playblasting in Maya done with GUI 2 | 3 | A visual interface for 4 | [maya-capture](https://github.com/abstractfactory/maya-capture). 5 | 6 | 7 | 8 | > WARNING: Preview release 9 | 10 |
11 | 12 | ### Features 13 | 14 | - Set up your playblasts visually (with direct feedback). 15 | - Produce consistent predictable playblasts. 16 | - Callbacks to allow custom encoding prior to opening viewer. 17 | - Avoid unwanted overscan; playblast what you render. 18 | 19 |
20 | 21 | ### Installation 22 | 23 | To install, download this package and [capture](https://github.com/abstractfactory/maya-capture) 24 | and place both in a directory where Maya can find them. 25 | 26 |
27 | 28 | ### Usage 29 | 30 | To show the interface in Maya run: 31 | 32 | ```python 33 | import capture_gui 34 | capture_gui.main() 35 | ``` 36 | 37 |
38 | 39 | ### Advanced 40 | 41 | #### Callbacks 42 | Register a pre-view callback to allow a custom conversion or overlays on the 43 | resulting footage in your pipeline (e.g. through FFMPEG) 44 | 45 | ```python 46 | import capture_gui 47 | 48 | # Use Qt.py to be both compatible with PySide and PySide2 (Maya 2017+) 49 | from capture_gui.vendor.Qt import QtCore 50 | 51 | def callback(options): 52 | """Implement your callback here""" 53 | 54 | print("Callback before launching viewer..") 55 | 56 | # Debug print all options for example purposes 57 | import pprint 58 | pprint.pprint(options) 59 | 60 | filename = options['filename'] 61 | print("Finished callback for video {0}".format(filename)) 62 | 63 | 64 | app = capture_gui.main(show=False) 65 | 66 | # Use QtCore.Qt.DirectConnection to ensure the viewer waits to launch until 67 | # your callback has finished. This is especially important when using your 68 | # callback to perform an extra encoding pass over the resulting file. 69 | app.viewer_start.connect(callback, QtCore.Qt.DirectConnection) 70 | 71 | # Show the app manually 72 | app.show() 73 | ``` 74 | 75 | #### Register preset paths 76 | 77 | Register a preset path that will be used by the capture gui to load default presets from. 78 | 79 | ```python 80 | import capture_gui.presets 81 | import capture_gui 82 | 83 | path = "path/to/directory" 84 | capture_gui.presets.register_path(path) 85 | 86 | # After registering capture gui will automatically load 87 | # the presets found in all registered preset paths 88 | capture_gui.main() 89 | ``` 90 | 91 | #### Register tokens and translators 92 | 93 | Register a token and translator that will be used to translate any tokens 94 | in the given filename. 95 | 96 | ```python 97 | import capture.tokens 98 | import capture_gui 99 | 100 | # this is an example function which retrieves the name of the current user 101 | def get_user_name(): 102 | import getpass 103 | return getpass.getuser() 104 | 105 | # register the token and pass the function which should be called 106 | # when this token is present. 107 | # The label is for the right mouse button menu's readability. 108 | capture.tokens.register_token("", 109 | lambda options : get_user_name(), 110 | label="Insert current user's name") 111 | ``` 112 | 113 | ### Known issues 114 | 115 | ##### Viewport Plugin _show_ menu close button sometimes appears off screen when torn off 116 | 117 | Tearing off the _show_ menu in the Viewport Plugin results in a menu 118 | with an off screen title bar when torn off near the top edge of the 119 | screen. This makes it hard to close the menu. To fix this either close 120 | the capture GUI (to close the menu) or make a new torn off version of 121 | the _show_ menu at a lower position on screen (this will close the 122 | previous torn off menu). 123 | -------------------------------------------------------------------------------- /RELEASES.md: -------------------------------------------------------------------------------- 1 | # Maya Capture GUI RELEASES 2 | 3 | ## 10 / 05 / 2017 - v1.5.0 4 | __Additions__ 5 | - Implemented lighting menu, pick type of display lighting for playblast 6 | - Added Two Sided Ligthing and Shadows as checkboxes to the widget 7 | 8 | __Changes__ 9 | - Removed redundant attributes 10 | - Restructured UI for Viewport Plugin 11 | 12 | ## 10 / 05 / 2017 - v1.4.0 13 | __Additions__ 14 | - File state check reverted to previous 15 | - Playblast background color can be overridden 16 | - Playblast takes over custom gradient or solid color 17 | - Playblast takes scene's background color setting if override is off 18 | 19 | __Changes__ 20 | - Updated docstring 21 | - toggle_override is renamed to on_toggle_override 22 | - get_color_value is renamed to show_color_dialog 23 | 24 | 25 | ## 04 / 05 / 2017 - v1.3.1 26 | - Added token support for filename, example: __ 27 | - Solved issue #0026 28 | + Added "Raw frame number" checkbox, when using custom frames the user can enable to use the actual frame numbers in the file name. Example: playblast.0012.png 29 | 30 | ## 03 / 05 / 2017 - v1.3.0 31 | - Changed mode name in Time Plugin, old presets incompatible with current version 32 | - Removed unused keyword argument 33 | 34 | ## 03 / 05 / 2017 - v1.2.0 35 | - Extended README with example of adding presets before launching the 36 | tool 37 | - Solved issue 0008 38 | + Playback of images, frame padding ( #### ) solved 39 | + View when finished works regardless of Save being checked 40 | - Solved issue 0019 41 | + Non-chronological time range is not possible anymore 42 | - Solved issue 0020 43 | + Added custom frame range, similar to print pages in Word 44 | 45 | ## 02 / 05 / 2017 - v1.1.0 46 | - Solved issue 0014 47 | - Added plugin validation function 48 | - Added app validation function to validate listed plugins 49 | 50 | ## 24 / 04 / 2017 - v1.0.2 51 | Fixed issue with storing presets and recent playblasts 52 | Fixed issue with changing presets in selection box 53 | 54 | ## 24 / 04 / 2017 - v1.0.1 55 | 56 | Resolved issue #11 57 | Resolved issue #09 58 | 59 | - Update Save options: 60 | + Choose to save to a location or keep it in the temp folder 61 | + Use default path : workspace/images 62 | + Use custom path: directory / file name 63 | 64 | - Added menu for previous playblasts 65 | - Added checkbox to control whether to open the playblast after capture 66 | 67 | ## 21 / 04 / 2017 - v1.0.0 68 | 69 | - Time plugin updated when start and end frame are changed in widget 70 | - Added "Save to default location" option -------------------------------------------------------------------------------- /capture_gui/__init__.py: -------------------------------------------------------------------------------- 1 | from .vendor.Qt import QtWidgets 2 | from . import app 3 | from . import lib 4 | 5 | 6 | def main(show=True): 7 | """Convenience method to run the Application inside Maya. 8 | 9 | Args: 10 | show (bool): Whether to directly show the instantiated application. 11 | Defaults to True. Set this to False if you want to manage the 12 | application (like callbacks) prior to showing the interface. 13 | 14 | Returns: 15 | capture_gui.app.App: The pyblish gui application instance. 16 | 17 | """ 18 | # get main maya window to parent widget to 19 | parent = lib.get_maya_main_window() 20 | instance = parent.findChild(QtWidgets.QWidget, app.App.object_name) 21 | if instance: 22 | instance.close() 23 | 24 | # launch app 25 | window = app.App(title="Capture GUI", parent=parent) 26 | if show: 27 | window.show() 28 | 29 | return window 30 | -------------------------------------------------------------------------------- /capture_gui/accordion.py: -------------------------------------------------------------------------------- 1 | from .vendor.Qt import QtCore, QtWidgets, QtGui 2 | 3 | 4 | class AccordionItem(QtWidgets.QGroupBox): 5 | trigger = QtCore.Signal(bool) 6 | 7 | def __init__(self, accordion, title, widget): 8 | QtWidgets.QGroupBox.__init__(self, parent=accordion) 9 | 10 | # create the layout 11 | layout = QtWidgets.QVBoxLayout() 12 | layout.setContentsMargins(6, 12, 6, 6) 13 | layout.setSpacing(0) 14 | layout.addWidget(widget) 15 | 16 | self._accordianWidget = accordion 17 | self._rolloutStyle = 2 18 | self._dragDropMode = 0 19 | 20 | self.setAcceptDrops(True) 21 | self.setLayout(layout) 22 | self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) 23 | self.customContextMenuRequested.connect(self.showMenu) 24 | 25 | # create custom properties 26 | self._widget = widget 27 | self._collapsed = False 28 | self._collapsible = True 29 | self._clicked = False 30 | self._customData = {} 31 | 32 | # set common properties 33 | self.setTitle(title) 34 | 35 | def accordionWidget(self): 36 | """ 37 | \remarks grabs the parent item for the accordian widget 38 | \return 39 | """ 40 | return self._accordianWidget 41 | 42 | def customData(self, key, default=None): 43 | """ 44 | \remarks return a custom pointer to information stored with this item 45 | \param key 46 | \param default default value to return if the key was not found 47 | \return data 48 | """ 49 | return self._customData.get(str(key), default) 50 | 51 | def dragEnterEvent(self, event): 52 | if not self._dragDropMode: 53 | return 54 | 55 | source = event.source() 56 | if source != self and source.parent() == self.parent() and isinstance( 57 | source, AccordionItem): 58 | event.acceptProposedAction() 59 | 60 | def dragDropRect(self): 61 | return QtCore.QRect(25, 7, 10, 6) 62 | 63 | def dragDropMode(self): 64 | return self._dragDropMode 65 | 66 | def dragMoveEvent(self, event): 67 | if not self._dragDropMode: 68 | return 69 | 70 | source = event.source() 71 | if source != self and source.parent() == self.parent() and isinstance( 72 | source, AccordionItem): 73 | event.acceptProposedAction() 74 | 75 | def dropEvent(self, event): 76 | widget = event.source() 77 | layout = self.parent().layout() 78 | layout.insertWidget(layout.indexOf(self), widget) 79 | self._accordianWidget.emitItemsReordered() 80 | 81 | def expandCollapseRect(self): 82 | return QtCore.QRect(0, 0, self.width(), 20) 83 | 84 | def enterEvent(self, event): 85 | self.accordionWidget().leaveEvent(event) 86 | event.accept() 87 | 88 | def leaveEvent(self, event): 89 | self.accordionWidget().enterEvent(event) 90 | event.accept() 91 | 92 | def mouseReleaseEvent(self, event): 93 | if self._clicked and self.expandCollapseRect().contains(event.pos()): 94 | self.toggleCollapsed() 95 | event.accept() 96 | else: 97 | event.ignore() 98 | 99 | self._clicked = False 100 | 101 | def mouseMoveEvent(self, event): 102 | event.ignore() 103 | 104 | def mousePressEvent(self, event): 105 | # handle an internal move 106 | 107 | # start a drag event 108 | if event.button() == QtCore.Qt.LeftButton and self.dragDropRect().contains( 109 | event.pos()): 110 | # create the pixmap 111 | pixmap = QtGui.QPixmap.grabWidget(self, self.rect()) 112 | 113 | # create the mimedata 114 | mimeData = QtCore.QMimeData() 115 | mimeData.setText('ItemTitle::%s' % (self.title())) 116 | 117 | # create the drag 118 | drag = QtGui.QDrag(self) 119 | drag.setMimeData(mimeData) 120 | drag.setPixmap(pixmap) 121 | drag.setHotSpot(event.pos()) 122 | 123 | if not drag.exec_(): 124 | self._accordianWidget.emitItemDragFailed(self) 125 | 126 | event.accept() 127 | 128 | # determine if the expand/collapse should occur 129 | elif event.button() == QtCore.Qt.LeftButton and self.expandCollapseRect().contains( 130 | event.pos()): 131 | self._clicked = True 132 | event.accept() 133 | 134 | else: 135 | event.ignore() 136 | 137 | def isCollapsed(self): 138 | return self._collapsed 139 | 140 | def isCollapsible(self): 141 | return self._collapsible 142 | 143 | def __drawTriangle(self, painter, x, y): 144 | 145 | brush = QtGui.QBrush(QtGui.QColor(255, 255, 255, 160), 146 | QtCore.Qt.SolidPattern) 147 | if not self.isCollapsed(): 148 | tl, tr, tp = QtCore.QPoint(x + 9, y + 8), QtCore.QPoint(x + 19, 149 | y + 8), QtCore.QPoint( 150 | x + 14, y + 13.0) 151 | points = [tl, tr, tp] 152 | triangle = QtGui.QPolygon(points) 153 | else: 154 | tl, tr, tp = QtCore.QPoint(x + 11, y + 6), QtCore.QPoint(x + 16, 155 | y + 11), QtCore.QPoint( 156 | x + 11, y + 16.0) 157 | points = [tl, tr, tp] 158 | triangle = QtGui.QPolygon(points) 159 | 160 | currentBrush = painter.brush() 161 | painter.setBrush(brush) 162 | painter.drawPolygon(triangle) 163 | painter.setBrush(currentBrush) 164 | 165 | def paintEvent(self, event): 166 | painter = QtGui.QPainter() 167 | painter.begin(self) 168 | painter.setRenderHint(painter.Antialiasing) 169 | font = painter.font() 170 | font.setBold(True) 171 | painter.setFont(font) 172 | 173 | x = self.rect().x() 174 | y = self.rect().y() 175 | w = self.rect().width() - 1 176 | h = self.rect().height() - 1 177 | r = 8 178 | 179 | # draw a rounded style 180 | if self._rolloutStyle == 2: 181 | # draw the text 182 | painter.drawText(x + 33, y + 3, w, 16, 183 | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop, 184 | self.title()) 185 | 186 | # draw the triangle 187 | self.__drawTriangle(painter, x, y) 188 | 189 | # draw the borders 190 | pen = QtGui.QPen(self.palette().color(QtGui.QPalette.Light)) 191 | pen.setWidthF(0.6) 192 | painter.setPen(pen) 193 | 194 | painter.drawRoundedRect(x + 1, y + 1, w - 1, h - 1, r, r) 195 | 196 | pen.setColor(self.palette().color(QtGui.QPalette.Shadow)) 197 | painter.setPen(pen) 198 | 199 | painter.drawRoundedRect(x, y, w - 1, h - 1, r, r) 200 | 201 | # draw a square style 202 | if self._rolloutStyle == 3: 203 | # draw the text 204 | painter.drawText(x + 33, y + 3, w, 16, 205 | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop, 206 | self.title()) 207 | 208 | self.__drawTriangle(painter, x, y) 209 | 210 | # draw the borders 211 | pen = QtGui.QPen(self.palette().color(QtGui.QPalette.Light)) 212 | pen.setWidthF(0.6) 213 | painter.setPen(pen) 214 | 215 | painter.drawRect(x + 1, y + 1, w - 1, h - 1) 216 | 217 | pen.setColor(self.palette().color(QtGui.QPalette.Shadow)) 218 | painter.setPen(pen) 219 | 220 | painter.drawRect(x, y, w - 1, h - 1) 221 | 222 | # draw a Maya style 223 | if self._rolloutStyle == 4: 224 | # draw the text 225 | painter.drawText(x + 33, y + 3, w, 16, 226 | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop, 227 | self.title()) 228 | 229 | painter.setRenderHint(QtGui.QPainter.Antialiasing, False) 230 | 231 | self.__drawTriangle(painter, x, y) 232 | 233 | # draw the borders - top 234 | headerHeight = 20 235 | 236 | headerRect = QtCore.QRect(x + 1, y + 1, w - 1, headerHeight) 237 | headerRectShadow = QtCore.QRect(x - 1, y - 1, w + 1, 238 | headerHeight + 2) 239 | 240 | # Highlight 241 | pen = QtGui.QPen(self.palette().color(QtGui.QPalette.Light)) 242 | pen.setWidthF(0.4) 243 | painter.setPen(pen) 244 | 245 | painter.drawRect(headerRect) 246 | painter.fillRect(headerRect, QtGui.QColor(255, 255, 255, 18)) 247 | 248 | # Shadow 249 | pen.setColor(self.palette().color(QtGui.QPalette.Dark)) 250 | painter.setPen(pen) 251 | painter.drawRect(headerRectShadow) 252 | 253 | if not self.isCollapsed(): 254 | # draw the lover border 255 | pen = QtGui.QPen(self.palette().color(QtGui.QPalette.Dark)) 256 | pen.setWidthF(0.8) 257 | painter.setPen(pen) 258 | 259 | offSet = headerHeight + 3 260 | bodyRect = QtCore.QRect(x, y + offSet, w, h - offSet) 261 | bodyRectShadow = QtCore.QRect(x + 1, y + offSet, w + 1, 262 | h - offSet + 1) 263 | painter.drawRect(bodyRect) 264 | 265 | pen.setColor(self.palette().color(QtGui.QPalette.Light)) 266 | pen.setWidthF(0.4) 267 | painter.setPen(pen) 268 | 269 | painter.drawRect(bodyRectShadow) 270 | 271 | # draw a boxed style 272 | elif self._rolloutStyle == 1: 273 | if self.isCollapsed(): 274 | arect = QtCore.QRect(x + 1, y + 9, w - 1, 4) 275 | brect = QtCore.QRect(x, y + 8, w - 1, 4) 276 | text = '+' 277 | else: 278 | arect = QtCore.QRect(x + 1, y + 9, w - 1, h - 9) 279 | brect = QtCore.QRect(x, y + 8, w - 1, h - 9) 280 | text = '-' 281 | 282 | # draw the borders 283 | pen = QtGui.QPen(self.palette().color(QtGui.QPalette.Light)) 284 | pen.setWidthF(0.6) 285 | painter.setPen(pen) 286 | 287 | painter.drawRect(arect) 288 | 289 | pen.setColor(self.palette().color(QtGui.QPalette.Shadow)) 290 | painter.setPen(pen) 291 | 292 | painter.drawRect(brect) 293 | 294 | painter.setRenderHint(painter.Antialiasing, False) 295 | painter.setBrush( 296 | self.palette().color(QtGui.QPalette.Window).darker(120)) 297 | painter.drawRect(x + 10, y + 1, w - 20, 16) 298 | painter.drawText(x + 16, y + 1, 299 | w - 32, 16, 300 | QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, 301 | text) 302 | painter.drawText(x + 10, y + 1, 303 | w - 20, 16, 304 | QtCore.Qt.AlignCenter, 305 | self.title()) 306 | 307 | if self.dragDropMode(): 308 | rect = self.dragDropRect() 309 | 310 | # draw the lines 311 | l = rect.left() 312 | r = rect.right() 313 | cy = rect.center().y() 314 | 315 | for y in (cy - 3, cy, cy + 3): 316 | painter.drawLine(l, y, r, y) 317 | 318 | painter.end() 319 | 320 | def setCollapsed(self, state=True): 321 | if self.isCollapsible(): 322 | accord = self.accordionWidget() 323 | accord.setUpdatesEnabled(False) 324 | 325 | self._collapsed = state 326 | 327 | if state: 328 | self.setMinimumHeight(22) 329 | self.setMaximumHeight(22) 330 | self.widget().setVisible(False) 331 | else: 332 | self.setMinimumHeight(0) 333 | self.setMaximumHeight(1000000) 334 | self.widget().setVisible(True) 335 | 336 | self._accordianWidget.emitItemCollapsed(self) 337 | accord.setUpdatesEnabled(True) 338 | 339 | def setCollapsible(self, state=True): 340 | self._collapsible = state 341 | 342 | def setCustomData(self, key, value): 343 | """ 344 | \remarks set a custom pointer to information stored on this item 345 | \param key 346 | \param value 347 | """ 348 | self._customData[str(key)] = value 349 | 350 | def setDragDropMode(self, mode): 351 | self._dragDropMode = mode 352 | 353 | def setRolloutStyle(self, style): 354 | self._rolloutStyle = style 355 | 356 | def showMenu(self): 357 | if QtCore.QRect(0, 0, self.width(), 20).contains( 358 | self.mapFromGlobal(QtGui.QCursor.pos())): 359 | self._accordianWidget.emitItemMenuRequested(self) 360 | 361 | def rolloutStyle(self): 362 | return self._rolloutStyle 363 | 364 | def toggleCollapsed(self): 365 | # enable signaling here 366 | collapse_state = not self.isCollapsed() 367 | self.setCollapsed(collapse_state) 368 | return collapse_state 369 | 370 | def widget(self): 371 | return self._widget 372 | 373 | 374 | class AccordionWidget(QtWidgets.QScrollArea): 375 | """Accordion style widget. 376 | 377 | A collapsible accordion widget like Maya's attribute editor. 378 | 379 | This is a modified version bsed on Blur's Accordion Widget to 380 | include a Maya style. 381 | 382 | """ 383 | itemCollapsed = QtCore.Signal(AccordionItem) 384 | itemMenuRequested = QtCore.Signal(AccordionItem) 385 | itemDragFailed = QtCore.Signal(AccordionItem) 386 | itemsReordered = QtCore.Signal() 387 | 388 | Boxed = 1 389 | Rounded = 2 390 | Square = 3 391 | Maya = 4 392 | 393 | NoDragDrop = 0 394 | InternalMove = 1 395 | 396 | def __init__(self, parent): 397 | 398 | QtWidgets.QScrollArea.__init__(self, parent) 399 | 400 | self.setFrameShape(QtWidgets.QScrollArea.NoFrame) 401 | self.setAutoFillBackground(False) 402 | self.setWidgetResizable(True) 403 | self.setMouseTracking(True) 404 | self.verticalScrollBar().setMaximumWidth(10) 405 | 406 | widget = QtWidgets.QWidget(self) 407 | 408 | # define custom properties 409 | self._rolloutStyle = AccordionWidget.Rounded 410 | self._dragDropMode = AccordionWidget.NoDragDrop 411 | self._scrolling = False 412 | self._scrollInitY = 0 413 | self._scrollInitVal = 0 414 | self._itemClass = AccordionItem 415 | 416 | layout = QtWidgets.QVBoxLayout() 417 | layout.setContentsMargins(2, 2, 2, 6) 418 | layout.setSpacing(2) 419 | layout.addStretch(1) 420 | 421 | widget.setLayout(layout) 422 | 423 | self.setWidget(widget) 424 | 425 | def setSpacing(self, spaceInt): 426 | self.widget().layout().setSpacing(spaceInt) 427 | 428 | def addItem(self, title, widget, collapsed=False): 429 | self.setUpdatesEnabled(False) 430 | item = self._itemClass(self, title, widget) 431 | item.setRolloutStyle(self.rolloutStyle()) 432 | item.setDragDropMode(self.dragDropMode()) 433 | layout = self.widget().layout() 434 | layout.insertWidget(layout.count() - 1, item) 435 | layout.setStretchFactor(item, 0) 436 | 437 | if collapsed: 438 | item.setCollapsed(collapsed) 439 | 440 | self.setUpdatesEnabled(True) 441 | 442 | return item 443 | 444 | def clear(self): 445 | self.setUpdatesEnabled(False) 446 | layout = self.widget().layout() 447 | while layout.count() > 1: 448 | item = layout.itemAt(0) 449 | 450 | # remove the item from the layout 451 | w = item.widget() 452 | layout.removeItem(item) 453 | 454 | # close the widget and delete it 455 | w.close() 456 | w.deleteLater() 457 | 458 | self.setUpdatesEnabled(True) 459 | 460 | def eventFilter(self, object, event): 461 | if event.type() == QtCore.QEvent.MouseButtonPress: 462 | self.mousePressEvent(event) 463 | return True 464 | 465 | elif event.type() == QtCore.QEvent.MouseMove: 466 | self.mouseMoveEvent(event) 467 | return True 468 | 469 | elif event.type() == QtCore.QEvent.MouseButtonRelease: 470 | self.mouseReleaseEvent(event) 471 | return True 472 | 473 | return False 474 | 475 | def canScroll(self): 476 | return self.verticalScrollBar().maximum() > 0 477 | 478 | def count(self): 479 | return self.widget().layout().count() - 1 480 | 481 | def dragDropMode(self): 482 | return self._dragDropMode 483 | 484 | def indexOf(self, widget): 485 | """ 486 | \remarks Searches for widget(not including child layouts). 487 | Returns the index of widget, or -1 if widget is not found 488 | \return 489 | """ 490 | layout = self.widget().layout() 491 | for index in range(layout.count()): 492 | if layout.itemAt(index).widget().widget() == widget: 493 | return index 494 | return -1 495 | 496 | def isBoxedMode(self): 497 | return self._rolloutStyle == AccordionWidget.Maya 498 | 499 | def itemClass(self): 500 | return self._itemClass 501 | 502 | def itemAt(self, index): 503 | layout = self.widget().layout() 504 | if 0 <= index and index < layout.count() - 1: 505 | return layout.itemAt(index).widget() 506 | return None 507 | 508 | def emitItemCollapsed(self, item): 509 | if not self.signalsBlocked(): 510 | self.itemCollapsed.emit(item) 511 | 512 | def emitItemDragFailed(self, item): 513 | if not self.signalsBlocked(): 514 | self.itemDragFailed.emit(item) 515 | 516 | def emitItemMenuRequested(self, item): 517 | if not self.signalsBlocked(): 518 | self.itemMenuRequested.emit(item) 519 | 520 | def emitItemsReordered(self): 521 | if not self.signalsBlocked(): 522 | self.itemsReordered.emit() 523 | 524 | def enterEvent(self, event): 525 | if self.canScroll(): 526 | QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.OpenHandCursor) 527 | 528 | def leaveEvent(self, event): 529 | if self.canScroll(): 530 | QtWidgets.QApplication.restoreOverrideCursor() 531 | 532 | def mouseMoveEvent(self, event): 533 | if self._scrolling: 534 | sbar = self.verticalScrollBar() 535 | smax = sbar.maximum() 536 | 537 | # calculate the distance moved for the moust point 538 | dy = event.globalY() - self._scrollInitY 539 | 540 | # calculate the percentage that is of the scroll bar 541 | dval = smax * (dy / float(sbar.height())) 542 | 543 | # calculate the new value 544 | sbar.setValue(self._scrollInitVal - dval) 545 | 546 | event.accept() 547 | 548 | def mousePressEvent(self, event): 549 | # handle a scroll event 550 | if event.button() == QtCore.Qt.LeftButton and self.canScroll(): 551 | self._scrolling = True 552 | self._scrollInitY = event.globalY() 553 | self._scrollInitVal = self.verticalScrollBar().value() 554 | 555 | QtWidgets.QApplication.setOverrideCursor( 556 | QtCore.Qt.ClosedHandCursor) 557 | 558 | event.accept() 559 | 560 | def mouseReleaseEvent(self, event): 561 | if self._scrolling: 562 | QtWidgets.QApplication.restoreOverrideCursor() 563 | 564 | self._scrolling = False 565 | self._scrollInitY = 0 566 | self._scrollInitVal = 0 567 | event.accept() 568 | 569 | def moveItemDown(self, index): 570 | layout = self.widget().layout() 571 | if (layout.count() - 1) > (index + 1): 572 | widget = layout.takeAt(index).widget() 573 | layout.insertWidget(index + 1, widget) 574 | 575 | def moveItemUp(self, index): 576 | if index > 0: 577 | layout = self.widget().layout() 578 | widget = layout.takeAt(index).widget() 579 | layout.insertWidget(index - 1, widget) 580 | 581 | def setBoxedMode(self, state): 582 | if state: 583 | self._rolloutStyle = AccordionWidget.Boxed 584 | else: 585 | self._rolloutStyle = AccordionWidget.Rounded 586 | 587 | def setDragDropMode(self, dragDropMode): 588 | self._dragDropMode = dragDropMode 589 | 590 | for item in self.findChildren(AccordionItem): 591 | item.setDragDropMode(self._dragDropMode) 592 | 593 | def setItemClass(self, itemClass): 594 | self._itemClass = itemClass 595 | 596 | def setRolloutStyle(self, rolloutStyle): 597 | self._rolloutStyle = rolloutStyle 598 | 599 | for item in self.findChildren(AccordionItem): 600 | item.setRolloutStyle(self._rolloutStyle) 601 | 602 | def rolloutStyle(self): 603 | return self._rolloutStyle 604 | 605 | def takeAt(self, index): 606 | self.setUpdatesEnabled(False) 607 | layout = self.widget().layout() 608 | widget = None 609 | if 0 <= index and index < layout.count() - 1: 610 | item = layout.itemAt(index) 611 | widget = item.widget() 612 | 613 | layout.removeItem(item) 614 | widget.close() 615 | self.setUpdatesEnabled(True) 616 | return widget 617 | 618 | def widgetAt(self, index): 619 | item = self.itemAt(index) 620 | if item: 621 | return item.widget() 622 | return None 623 | 624 | pyBoxedMode = QtCore.Property('bool', isBoxedMode, setBoxedMode) 625 | -------------------------------------------------------------------------------- /capture_gui/app.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import tempfile 5 | 6 | import capture 7 | import maya.cmds as cmds 8 | 9 | from .vendor.Qt import QtCore, QtWidgets, QtGui 10 | from . import lib 11 | from . import plugin 12 | from . import presets 13 | from . import version 14 | from . import tokens 15 | from .accordion import AccordionWidget 16 | 17 | log = logging.getLogger("Capture Gui") 18 | 19 | 20 | class ClickLabel(QtWidgets.QLabel): 21 | """A QLabel that emits a clicked signal when clicked upon.""" 22 | clicked = QtCore.Signal() 23 | 24 | def mouseReleaseEvent(self, event): 25 | self.clicked.emit() 26 | return super(ClickLabel, self).mouseReleaseEvent(event) 27 | 28 | 29 | class PreviewWidget(QtWidgets.QWidget): 30 | """The playblast image preview widget. 31 | 32 | Upon refresh it will retrieve the options through the function set as 33 | `options_getter` and make a call to `capture.capture()` for a single 34 | frame (playblasted) snapshot. The result is displayed as image. 35 | """ 36 | 37 | preview_width = 320 38 | preview_height = 180 39 | 40 | def __init__(self, options_getter, validator, parent=None): 41 | QtWidgets.QWidget.__init__(self, parent=parent) 42 | 43 | # Add attributes 44 | self.options_getter = options_getter 45 | self.validator = validator 46 | self.preview = ClickLabel() 47 | self.preview.setFixedWidth(self.preview_width) 48 | self.preview.setFixedHeight(self.preview_height) 49 | 50 | tip = "Click to force a refresh" 51 | self.preview.setToolTip(tip) 52 | self.preview.setStatusTip(tip) 53 | 54 | # region Build 55 | self.layout = QtWidgets.QVBoxLayout() 56 | self.layout.setAlignment(QtCore.Qt.AlignHCenter) 57 | self.layout.setContentsMargins(0, 0, 0, 0) 58 | 59 | self.setLayout(self.layout) 60 | self.layout.addWidget(self.preview) 61 | # endregion Build 62 | 63 | # Connect widgets to functions 64 | self.preview.clicked.connect(self.refresh) 65 | 66 | def refresh(self): 67 | """Refresh the playblast preview""" 68 | 69 | frame = cmds.currentTime(query=True) 70 | 71 | # When playblasting outside of an undo queue it seems that undoing 72 | # actually triggers a reset to frame 0. As such we sneak in the current 73 | # time into the undo queue to enforce correct undoing. 74 | cmds.currentTime(frame, update=True) 75 | 76 | # check if plugin outputs are correct 77 | valid = self.validator() 78 | if not valid: 79 | return 80 | 81 | with lib.no_undo(): 82 | options = self.options_getter() 83 | tempdir = tempfile.mkdtemp() 84 | 85 | # override settings that are constants for the preview 86 | options = options.copy() 87 | options['filename'] = None 88 | options['complete_filename'] = os.path.join(tempdir, "temp.jpg") 89 | options['width'] = self.preview_width 90 | options['height'] = self.preview_height 91 | options['viewer'] = False 92 | options['frame'] = frame 93 | options['off_screen'] = True 94 | options['format'] = "image" 95 | options['compression'] = "jpg" 96 | options['sound'] = None 97 | 98 | fname = capture.capture(**options) 99 | if not fname: 100 | log.warning("Preview failed") 101 | return 102 | 103 | image = QtGui.QPixmap(fname) 104 | self.preview.setPixmap(image) 105 | os.remove(fname) 106 | 107 | def showEvent(self, event): 108 | """Initialize when shown""" 109 | self.refresh() 110 | event.accept() 111 | 112 | 113 | class PresetWidget(QtWidgets.QWidget): 114 | """Preset Widget 115 | 116 | Allows the user to set preferences and create presets to load before 117 | capturing. 118 | 119 | """ 120 | 121 | preset_loaded = QtCore.Signal(dict) 122 | config_opened = QtCore.Signal() 123 | 124 | id = "Presets" 125 | label = "Presets" 126 | 127 | def __init__(self, inputs_getter, parent=None): 128 | QtWidgets.QWidget.__init__(self, parent=parent) 129 | 130 | self.inputs_getter = inputs_getter 131 | 132 | layout = QtWidgets.QHBoxLayout(self) 133 | layout.setAlignment(QtCore.Qt.AlignCenter) 134 | layout.setContentsMargins(0, 0, 0, 0) 135 | 136 | presets = QtWidgets.QComboBox() 137 | presets.setFixedWidth(220) 138 | presets.addItem("*") 139 | 140 | # Icons 141 | icon_path = os.path.join(os.path.dirname(__file__), "resources") 142 | save_icon = os.path.join(icon_path, "save.png") 143 | load_icon = os.path.join(icon_path, "import.png") 144 | config_icon = os.path.join(icon_path, "config.png") 145 | 146 | # Create buttons 147 | save = QtWidgets.QPushButton() 148 | save.setIcon(QtGui.QIcon(save_icon)) 149 | save.setFixedWidth(30) 150 | save.setToolTip("Save Preset") 151 | save.setStatusTip("Save Preset") 152 | 153 | load = QtWidgets.QPushButton() 154 | load.setIcon(QtGui.QIcon(load_icon)) 155 | load.setFixedWidth(30) 156 | load.setToolTip("Load Preset") 157 | load.setStatusTip("Load Preset") 158 | 159 | config = QtWidgets.QPushButton() 160 | config.setIcon(QtGui.QIcon(config_icon)) 161 | config.setFixedWidth(30) 162 | config.setToolTip("Preset configuration") 163 | config.setStatusTip("Preset configuration") 164 | 165 | layout.addWidget(presets) 166 | layout.addWidget(save) 167 | layout.addWidget(load) 168 | layout.addWidget(config) 169 | 170 | # Make available for all methods 171 | self.presets = presets 172 | self.config = config 173 | self.load = load 174 | self.save = save 175 | 176 | # Signals 177 | self.save.clicked.connect(self.on_save_preset) 178 | self.load.clicked.connect(self.import_preset) 179 | self.config.clicked.connect(self.config_opened) 180 | self.presets.currentIndexChanged.connect(self.load_active_preset) 181 | 182 | self._process_presets() 183 | 184 | def _process_presets(self): 185 | """Adds all preset files from preset paths to the Preset widget. 186 | 187 | Returns: 188 | None 189 | 190 | """ 191 | for presetfile in presets.discover(): 192 | self.add_preset(presetfile) 193 | 194 | def import_preset(self): 195 | """Load preset files to override output values""" 196 | 197 | path = self._default_browse_path() 198 | filters = "Text file (*.json)" 199 | dialog = QtWidgets.QFileDialog 200 | filename, _ = dialog.getOpenFileName(self, "Open preference file", 201 | path, filters) 202 | if not filename: 203 | return 204 | 205 | # create new entry in combobox 206 | self.add_preset(filename) 207 | 208 | # read file 209 | return self.load_active_preset() 210 | 211 | def load_active_preset(self): 212 | """Load the active preset. 213 | 214 | Returns: 215 | dict: The preset inputs. 216 | 217 | """ 218 | current_index = self.presets.currentIndex() 219 | filename = self.presets.itemData(current_index) 220 | if not filename: 221 | return {} 222 | 223 | preset = lib.load_json(filename) 224 | 225 | # Emit preset load signal 226 | log.debug("Emitting preset_loaded: {0}".format(filename)) 227 | self.preset_loaded.emit(preset) 228 | 229 | # Ensure we preserve the index after loading the changes 230 | # for all the plugin widgets 231 | self.presets.blockSignals(True) 232 | self.presets.setCurrentIndex(current_index) 233 | self.presets.blockSignals(False) 234 | 235 | return preset 236 | 237 | def add_preset(self, filename): 238 | """Add the filename to the preset list. 239 | 240 | This also sets the index to the filename. 241 | 242 | Returns: 243 | None 244 | 245 | """ 246 | 247 | filename = os.path.normpath(filename) 248 | if not os.path.exists(filename): 249 | log.warning("Preset file does not exist: {0}".format(filename)) 250 | return 251 | 252 | label = os.path.splitext(os.path.basename(filename))[0] 253 | item_count = self.presets.count() 254 | 255 | paths = [self.presets.itemData(i) for i in range(item_count)] 256 | if filename in paths: 257 | log.info("Preset is already in the " 258 | "presets list: {0}".format(filename)) 259 | item_index = paths.index(filename) 260 | else: 261 | self.presets.addItem(label, userData=filename) 262 | item_index = item_count 263 | 264 | self.presets.blockSignals(True) 265 | self.presets.setCurrentIndex(item_index) 266 | self.presets.blockSignals(False) 267 | 268 | return item_index 269 | 270 | def _default_browse_path(self): 271 | """Return the current browse path for save/load preset. 272 | 273 | If a preset is currently loaded it will use that specific path 274 | otherwise it will go to the last registered preset path. 275 | 276 | Returns: 277 | str: Path to use as default browse location. 278 | 279 | """ 280 | 281 | current_index = self.presets.currentIndex() 282 | path = self.presets.itemData(current_index) 283 | 284 | if not path: 285 | # Fallback to last registered preset path 286 | paths = presets.preset_paths() 287 | if paths: 288 | path = paths[-1] 289 | 290 | return path 291 | 292 | def save_preset(self, inputs): 293 | """Save inputs to a file""" 294 | 295 | path = self._default_browse_path() 296 | filters = "Text file (*.json)" 297 | filename, _ = QtWidgets.QFileDialog.getSaveFileName(self, 298 | "Save preferences", 299 | path, 300 | filters) 301 | if not filename: 302 | return 303 | 304 | with open(filename, "w") as f: 305 | json.dump(inputs, f, sort_keys=True, 306 | indent=4, separators=(',', ': ')) 307 | 308 | self.add_preset(filename) 309 | 310 | return filename 311 | 312 | def get_presets(self): 313 | """Return all currently listed presets""" 314 | configurations = [self.presets.itemText(i) for 315 | i in range(self.presets.count())] 316 | 317 | return configurations 318 | 319 | def on_save_preset(self): 320 | """Save the inputs of all the plugins in a preset.""" 321 | 322 | inputs = self.inputs_getter(as_preset=True) 323 | self.save_preset(inputs) 324 | 325 | def apply_inputs(self, settings): 326 | 327 | path = settings.get("selected", None) 328 | index = self.presets.findData(path) 329 | if index == -1: 330 | # If the last loaded preset still exists but wasn't on the 331 | # "discovered preset paths" then add it. 332 | if os.path.exists(path): 333 | log.info("Adding previously selected preset explicitly: %s", 334 | path) 335 | self.add_preset(path) 336 | return 337 | else: 338 | log.warning("Previously selected preset is not available: %s", 339 | path) 340 | index = 0 341 | 342 | self.presets.setCurrentIndex(index) 343 | 344 | def get_inputs(self, as_preset=False): 345 | 346 | if as_preset: 347 | # Don't save the current preset into the preset because 348 | # that would just be recursive and make no sense 349 | return {} 350 | else: 351 | current_index = self.presets.currentIndex() 352 | selected = self.presets.itemData(current_index) 353 | return {"selected": selected} 354 | 355 | 356 | class App(QtWidgets.QWidget): 357 | """The main application in which the widgets are placed""" 358 | 359 | # Signals 360 | options_changed = QtCore.Signal(dict) 361 | playblast_start = QtCore.Signal(dict) 362 | playblast_finished = QtCore.Signal(dict) 363 | viewer_start = QtCore.Signal(dict) 364 | 365 | # Attributes 366 | object_name = "CaptureGUI" 367 | application_sections = ["config", "app"] 368 | 369 | def __init__(self, title, parent=None): 370 | QtWidgets.QWidget.__init__(self, parent=parent) 371 | 372 | # Settings 373 | # Remove pointer for memory when closed 374 | self.setAttribute(QtCore.Qt.WA_DeleteOnClose) 375 | self.settingfile = self._ensure_config_exist() 376 | self.plugins = {"app": list(), 377 | "config": list()} 378 | 379 | self._config_dialog = None 380 | self._build_configuration_dialog() 381 | 382 | # region Set Attributes 383 | title_version = "{} v{}".format(title, version.version) 384 | self.setObjectName(self.object_name) 385 | self.setWindowTitle(title_version) 386 | self.setMinimumWidth(380) 387 | 388 | # Set dialog window flags so the widget can be correctly parented 389 | # to Maya main window 390 | self.setWindowFlags(self.windowFlags() | QtCore.Qt.Dialog) 391 | self.setProperty("saveWindowPref", True) 392 | # endregion Set Attributes 393 | 394 | self.layout = QtWidgets.QVBoxLayout() 395 | self.layout.setContentsMargins(0, 0, 0, 0) 396 | self.setLayout(self.layout) 397 | 398 | # Add accordion widget (Maya attribute editor style) 399 | self.widgetlibrary = AccordionWidget(self) 400 | self.widgetlibrary.setRolloutStyle(AccordionWidget.Maya) 401 | 402 | # Add separate widgets 403 | self.widgetlibrary.addItem("Preview", 404 | PreviewWidget(self.get_outputs, 405 | self.validate, 406 | parent=self), 407 | collapsed=True) 408 | 409 | self.presetwidget = PresetWidget(inputs_getter=self.get_inputs, 410 | parent=self) 411 | self.widgetlibrary.addItem("Presets", self.presetwidget) 412 | 413 | # add plug-in widgets 414 | for widget in plugin.discover(): 415 | self.add_plugin(widget) 416 | 417 | self.layout.addWidget(self.widgetlibrary) 418 | 419 | # add standard buttons 420 | self.apply_button = QtWidgets.QPushButton("Capture") 421 | self.layout.addWidget(self.apply_button) 422 | 423 | # default actions 424 | self.apply_button.clicked.connect(self.apply) 425 | 426 | # signals and slots 427 | self.presetwidget.config_opened.connect(self.show_config) 428 | self.presetwidget.preset_loaded.connect(self.apply_inputs) 429 | 430 | self.apply_inputs(self._read_widget_configuration()) 431 | 432 | def apply(self): 433 | """Run capture action with current settings""" 434 | 435 | valid = self.validate() 436 | if not valid: 437 | return 438 | 439 | options = self.get_outputs() 440 | filename = options.get("filename", None) 441 | 442 | self.playblast_start.emit(options) 443 | 444 | # The filename can be `None` when the 445 | # playblast will *not* be saved. 446 | if filename is not None: 447 | # Format the tokens in the filename 448 | filename = tokens.format_tokens(filename, options) 449 | 450 | # expand environment variables 451 | filename = os.path.expandvars(filename) 452 | 453 | # Make relative paths absolute to the "images" file rule by default 454 | if not os.path.isabs(filename): 455 | root = lib.get_project_rule("images") 456 | filename = os.path.join(root, filename) 457 | 458 | # normalize (to remove double slashes and alike) 459 | filename = os.path.normpath(filename) 460 | 461 | options["filename"] = filename 462 | 463 | # Perform capture and store returned filename with extension 464 | options["filename"] = lib.capture_scene(options) 465 | 466 | self.playblast_finished.emit(options) 467 | filename = options["filename"] # get filename after callbacks 468 | 469 | # Show viewer 470 | viewer = options.get("viewer", False) 471 | if viewer: 472 | if filename and os.path.exists(filename): 473 | self.viewer_start.emit(options) 474 | lib.open_file(filename) 475 | else: 476 | raise RuntimeError("Can't open playblast because file " 477 | "doesn't exist: {0}".format(filename)) 478 | 479 | return filename 480 | 481 | def apply_inputs(self, inputs): 482 | """Apply all the settings of the widgets. 483 | 484 | Arguments: 485 | inputs (dict): input values per plug-in widget 486 | 487 | Returns: 488 | None 489 | 490 | """ 491 | if not inputs: 492 | return 493 | 494 | widgets = self._get_plugin_widgets() 495 | widgets.append(self.presetwidget) 496 | for widget in widgets: 497 | widget_inputs = inputs.get(widget.id, None) 498 | if not widget_inputs: 499 | continue 500 | widget.apply_inputs(widget_inputs) 501 | 502 | def show_config(self): 503 | """Show the advanced configuration""" 504 | # calculate center of main widget 505 | geometry = self.geometry() 506 | self._config_dialog.move(QtCore.QPoint(geometry.x()+30, 507 | geometry.y())) 508 | self._config_dialog.show() 509 | 510 | def add_plugin(self, plugin): 511 | """Add an options widget plug-in to the UI""" 512 | 513 | if plugin.section not in self.application_sections: 514 | log.warning("{}'s section is invalid: " 515 | "{}".format(plugin.label, plugin.section)) 516 | return 517 | 518 | widget = plugin(parent=self) 519 | widget.initialize() 520 | widget.options_changed.connect(self.on_widget_settings_changed) 521 | self.playblast_finished.connect(widget.on_playblast_finished) 522 | 523 | # Add to plug-ins in its section 524 | self.plugins[widget.section].append(widget) 525 | 526 | # Implement additional settings depending on section 527 | if widget.section == "app": 528 | if not widget.hidden: 529 | item = self.widgetlibrary.addItem(widget.label, widget) 530 | # connect label change behaviour 531 | widget.label_changed.connect(item.setTitle) 532 | 533 | # Add the plugin in a QGroupBox to the configuration dialog 534 | if widget.section == "config": 535 | layout = self._config_dialog.layout() 536 | # create group box 537 | group_widget = QtWidgets.QGroupBox(widget.label) 538 | group_layout = QtWidgets.QVBoxLayout(group_widget) 539 | group_layout.addWidget(widget) 540 | 541 | layout.addWidget(group_widget) 542 | 543 | def validate(self): 544 | """Validate whether the outputs of the widgets are good. 545 | 546 | Returns: 547 | bool: Whether it's valid to capture the current settings. 548 | 549 | """ 550 | 551 | errors = list() 552 | for widget in self._get_plugin_widgets(): 553 | widget_errors = widget.validate() 554 | if widget_errors: 555 | errors.extend(widget_errors) 556 | 557 | if errors: 558 | message_title = "%s Validation Error(s)" % len(errors) 559 | message = "\n".join(errors) 560 | QtWidgets.QMessageBox.critical(self, 561 | message_title, 562 | message, 563 | QtWidgets.QMessageBox.Ok) 564 | return False 565 | 566 | return True 567 | 568 | def get_outputs(self): 569 | """Return settings for a capture as currently set in the Application. 570 | 571 | Returns: 572 | dict: Current output settings 573 | 574 | """ 575 | 576 | # Get settings from widgets 577 | outputs = dict() 578 | for widget in self._get_plugin_widgets(): 579 | widget_outputs = widget.get_outputs() 580 | if not widget_outputs: 581 | continue 582 | 583 | for key, value in widget_outputs.items(): 584 | 585 | # We merge dictionaries by updating them so we have 586 | # the "mixed" values of both settings 587 | if isinstance(value, dict) and key in outputs: 588 | outputs[key].update(value) 589 | else: 590 | outputs[key] = value 591 | 592 | return outputs 593 | 594 | def get_inputs(self, as_preset=False): 595 | """Return the inputs per plug-in widgets by `plugin.id`. 596 | 597 | Returns: 598 | dict: The inputs per widget 599 | 600 | """ 601 | 602 | inputs = dict() 603 | # Here we collect all the widgets from which we want to store the 604 | # current inputs. This will be restored in the next session 605 | # The preset widget is added to make sure the user starts with the 606 | # previously selected preset configuration 607 | config_widgets = self._get_plugin_widgets() 608 | config_widgets.append(self.presetwidget) 609 | for widget in config_widgets: 610 | widget_inputs = widget.get_inputs(as_preset=as_preset) 611 | if not isinstance(widget_inputs, dict): 612 | log.debug("Widget inputs are not a dictionary " 613 | "'{}': {}".format(widget.id, widget_inputs)) 614 | return 615 | 616 | if not widget_inputs: 617 | continue 618 | 619 | inputs[widget.id] = widget_inputs 620 | 621 | return inputs 622 | 623 | def on_widget_settings_changed(self): 624 | """Set current preset to '*' on settings change""" 625 | 626 | self.options_changed.emit(self.get_outputs) 627 | self.presetwidget.presets.setCurrentIndex(0) 628 | 629 | def _build_configuration_dialog(self): 630 | """Build a configuration to store configuration widgets in""" 631 | 632 | dialog = QtWidgets.QDialog(self) 633 | dialog.setWindowTitle("Capture - Preset Configuration") 634 | QtWidgets.QVBoxLayout(dialog) 635 | 636 | self._config_dialog = dialog 637 | 638 | def _ensure_config_exist(self): 639 | """Create the configuration file if it does not exist yet. 640 | 641 | Returns: 642 | unicode: filepath of the configuration file 643 | 644 | """ 645 | 646 | userdir = os.path.expanduser("~") 647 | capturegui_dir = os.path.join(userdir, "CaptureGUI") 648 | capturegui_inputs = os.path.join(capturegui_dir, "capturegui.json") 649 | if not os.path.exists(capturegui_dir): 650 | os.makedirs(capturegui_dir) 651 | 652 | if not os.path.isfile(capturegui_inputs): 653 | config = open(capturegui_inputs, "w") 654 | config.close() 655 | 656 | return capturegui_inputs 657 | 658 | def _store_widget_configuration(self): 659 | """Store all used widget settings in the local json file""" 660 | 661 | inputs = self.get_inputs(as_preset=False) 662 | path = self.settingfile 663 | 664 | with open(path, "w") as f: 665 | log.debug("Writing JSON file: {0}".format(path)) 666 | json.dump(inputs, f, sort_keys=True, 667 | indent=4, separators=(',', ': ')) 668 | 669 | def _read_widget_configuration(self): 670 | """Read the stored widget inputs""" 671 | 672 | inputs = {} 673 | path = self.settingfile 674 | 675 | if not os.path.isfile(path) or os.stat(path).st_size == 0: 676 | return inputs 677 | 678 | with open(path, "r") as f: 679 | log.debug("Reading JSON file: {0}".format(path)) 680 | try: 681 | inputs = json.load(f) 682 | except ValueError as error: 683 | log.error(str(error)) 684 | 685 | return inputs 686 | 687 | def _get_plugin_widgets(self): 688 | """List all plug-in widgets. 689 | 690 | Returns: 691 | list: The plug-in widgets in *all* sections 692 | 693 | """ 694 | 695 | widgets = list() 696 | for section in self.plugins.values(): 697 | widgets.extend(section) 698 | 699 | return widgets 700 | 701 | # override close event to ensure the input are stored 702 | 703 | def closeEvent(self, event): 704 | """Store current configuration upon closing the application.""" 705 | 706 | self._store_widget_configuration() 707 | for section_widgets in self.plugins.values(): 708 | for widget in section_widgets: 709 | widget.uninitialize() 710 | 711 | event.accept() 712 | -------------------------------------------------------------------------------- /capture_gui/colorpicker.py: -------------------------------------------------------------------------------- 1 | from capture_gui.vendor.Qt import QtCore, QtWidgets, QtGui 2 | 3 | 4 | class ColorPicker(QtWidgets.QPushButton): 5 | """Custom color pick button to store and retrieve color values""" 6 | 7 | valueChanged = QtCore.Signal() 8 | 9 | def __init__(self): 10 | QtWidgets.QPushButton.__init__(self) 11 | 12 | self.clicked.connect(self.show_color_dialog) 13 | self._color = None 14 | 15 | self.color = [1, 1, 1] 16 | 17 | # region properties 18 | @property 19 | def color(self): 20 | return self._color 21 | 22 | @color.setter 23 | def color(self, values): 24 | """Set the color value and update the stylesheet 25 | 26 | Arguments: 27 | values (list): the color values; red, green, blue 28 | 29 | Returns: 30 | None 31 | 32 | """ 33 | self._color = values 34 | self.valueChanged.emit() 35 | 36 | values = [int(x*255) for x in values] 37 | self.setStyleSheet("background: rgb({},{},{})".format(*values)) 38 | 39 | # endregion properties 40 | 41 | def show_color_dialog(self): 42 | """Display a color picker to change color. 43 | 44 | When a color has been chosen this updates the color of the button 45 | and its current value 46 | 47 | :return: the red, green and blue values 48 | :rtype: list 49 | """ 50 | current = QtGui.QColor() 51 | current.setRgbF(*self._color) 52 | colors = QtWidgets.QColorDialog.getColor(current) 53 | if not colors: 54 | return 55 | self.color = [colors.redF(), colors.greenF(), colors.blueF()] 56 | -------------------------------------------------------------------------------- /capture_gui/lib.py: -------------------------------------------------------------------------------- 1 | # TODO: fetch Maya main window without shiboken that also doesn't crash 2 | 3 | import sys 4 | import logging 5 | import json 6 | import os 7 | import glob 8 | import subprocess 9 | import contextlib 10 | from collections import OrderedDict 11 | 12 | import datetime 13 | import maya.cmds as cmds 14 | import maya.mel as mel 15 | import maya.OpenMayaUI as omui 16 | import capture 17 | 18 | from .vendor.Qt import QtWidgets 19 | try: 20 | # PySide1 21 | import shiboken 22 | except ImportError: 23 | # PySide2 24 | import shiboken2 as shiboken 25 | 26 | log = logging.getLogger(__name__) 27 | 28 | # region Object types 29 | OBJECT_TYPES = OrderedDict() 30 | OBJECT_TYPES['NURBS Curves'] = 'nurbsCurves' 31 | OBJECT_TYPES['NURBS Surfaces'] = 'nurbsSurfaces' 32 | OBJECT_TYPES['NURBS CVs'] = 'controlVertices' 33 | OBJECT_TYPES['NURBS Hulls'] = 'hulls' 34 | OBJECT_TYPES['Polygons'] = 'polymeshes' 35 | OBJECT_TYPES['Subdiv Surfaces'] = 'subdivSurfaces' 36 | OBJECT_TYPES['Planes'] = 'planes' 37 | OBJECT_TYPES['Lights'] = 'lights' 38 | OBJECT_TYPES['Cameras'] = 'cameras' 39 | OBJECT_TYPES['Image Planes'] = 'imagePlane' 40 | OBJECT_TYPES['Joints'] = 'joints' 41 | OBJECT_TYPES['IK Handles'] = 'ikHandles' 42 | OBJECT_TYPES['Deformers'] = 'deformers' 43 | OBJECT_TYPES['Dynamics'] = 'dynamics' 44 | OBJECT_TYPES['Particle Instancers'] = 'particleInstancers' 45 | OBJECT_TYPES['Fluids'] = 'fluids' 46 | OBJECT_TYPES['Hair Systems'] = 'hairSystems' 47 | OBJECT_TYPES['Follicles'] = 'follicles' 48 | OBJECT_TYPES['nCloths'] = 'nCloths' 49 | OBJECT_TYPES['nParticles'] = 'nParticles' 50 | OBJECT_TYPES['nRigids'] = 'nRigids' 51 | OBJECT_TYPES['Dynamic Constraints'] = 'dynamicConstraints' 52 | OBJECT_TYPES['Locators'] = 'locators' 53 | OBJECT_TYPES['Dimensions'] = 'dimensions' 54 | OBJECT_TYPES['Pivots'] = 'pivots' 55 | OBJECT_TYPES['Handles'] = 'handles' 56 | OBJECT_TYPES['Textures Placements'] = 'textures' 57 | OBJECT_TYPES['Strokes'] = 'strokes' 58 | OBJECT_TYPES['Motion Trails'] = 'motionTrails' 59 | OBJECT_TYPES['Plugin Shapes'] = 'pluginShapes' 60 | OBJECT_TYPES['Clip Ghosts'] = 'clipGhosts' 61 | OBJECT_TYPES['Grease Pencil'] = 'greasePencils' 62 | OBJECT_TYPES['Manipulators'] = 'manipulators' 63 | OBJECT_TYPES['Grid'] = 'grid' 64 | OBJECT_TYPES['HUD'] = 'hud' 65 | # endregion Object types 66 | 67 | 68 | def get_show_object_types(): 69 | 70 | results = OrderedDict() 71 | 72 | # Add the plug-in shapes 73 | plugin_shapes = get_plugin_shapes() 74 | results.update(plugin_shapes) 75 | 76 | # We add default shapes last so plug-in shapes could 77 | # never potentially overwrite any built-ins. 78 | results.update(OBJECT_TYPES) 79 | 80 | return results 81 | 82 | 83 | def get_current_scenename(): 84 | path = cmds.file(query=True, sceneName=True) 85 | if path: 86 | return os.path.splitext(os.path.basename(path))[0] 87 | return None 88 | 89 | 90 | def get_current_camera(): 91 | """Returns the currently active camera. 92 | 93 | Searched in the order of: 94 | 1. Active Panel 95 | 2. Selected Camera Shape 96 | 3. Selected Camera Transform 97 | 98 | Returns: 99 | str: name of active camera transform 100 | 101 | """ 102 | 103 | # Get camera from active modelPanel (if any) 104 | panel = cmds.getPanel(withFocus=True) 105 | if cmds.getPanel(typeOf=panel) == "modelPanel": 106 | cam = cmds.modelEditor(panel, query=True, camera=True) 107 | # In some cases above returns the shape, but most often it returns the 108 | # transform. Still we need to make sure we return the transform. 109 | if cam: 110 | if cmds.nodeType(cam) == "transform": 111 | return cam 112 | # camera shape is a shape type 113 | elif cmds.objectType(cam, isAType="shape"): 114 | parent = cmds.listRelatives(cam, parent=True, fullPath=True) 115 | if parent: 116 | return parent[0] 117 | 118 | # Check if a camShape is selected (if so use that) 119 | cam_shapes = cmds.ls(selection=True, type="camera") 120 | if cam_shapes: 121 | return cmds.listRelatives(cam_shapes, 122 | parent=True, 123 | fullPath=True)[0] 124 | 125 | # Check if a transform of a camShape is selected 126 | # (return cam transform if any) 127 | transforms = cmds.ls(selection=True, type="transform") 128 | if transforms: 129 | cam_shapes = cmds.listRelatives(transforms, shapes=True, type="camera") 130 | if cam_shapes: 131 | return cmds.listRelatives(cam_shapes, 132 | parent=True, 133 | fullPath=True)[0] 134 | 135 | 136 | def get_active_editor(): 137 | """Return the active editor panel to playblast with""" 138 | # fixes `cmds.playblast` undo bug 139 | cmds.currentTime(cmds.currentTime(query=True)) 140 | panel = cmds.playblast(activeEditor=True) 141 | return panel.split("|")[-1] 142 | 143 | 144 | def get_current_frame(): 145 | return cmds.currentTime(query=True) 146 | 147 | 148 | def get_time_slider_range(highlighted=True, 149 | withinHighlighted=True, 150 | highlightedOnly=False): 151 | """Return the time range from Maya's time slider. 152 | 153 | Arguments: 154 | highlighted (bool): When True if will return a selected frame range 155 | (if there's any selection of more than one frame!) otherwise it 156 | will return min and max playback time. 157 | withinHighlighted (bool): By default Maya returns the highlighted range 158 | end as a plus one value. When this is True this will be fixed by 159 | removing one from the last number. 160 | 161 | Returns: 162 | list: List of two floats of start and end frame numbers. 163 | 164 | """ 165 | if highlighted is True: 166 | gPlaybackSlider = mel.eval("global string $gPlayBackSlider; " 167 | "$gPlayBackSlider = $gPlayBackSlider;") 168 | if cmds.timeControl(gPlaybackSlider, query=True, rangeVisible=True): 169 | highlightedRange = cmds.timeControl(gPlaybackSlider, 170 | query=True, 171 | rangeArray=True) 172 | if withinHighlighted: 173 | highlightedRange[-1] -= 1 174 | return highlightedRange 175 | if not highlightedOnly: 176 | return [cmds.playbackOptions(query=True, minTime=True), 177 | cmds.playbackOptions(query=True, maxTime=True)] 178 | 179 | 180 | def get_current_renderlayer(): 181 | return cmds.editRenderLayerGlobals(query=True, currentRenderLayer=True) 182 | 183 | 184 | def get_plugin_shapes(): 185 | """Get all currently available plugin shapes 186 | 187 | Returns: 188 | dict: plugin shapes by their menu label and script name 189 | 190 | """ 191 | filters = cmds.pluginDisplayFilter(query=True, listFilters=True) 192 | labels = [cmds.pluginDisplayFilter(f, query=True, label=True) for f in 193 | filters] 194 | return OrderedDict(zip(labels, filters)) 195 | 196 | 197 | def open_file(filepath): 198 | """Open file using OS default settings""" 199 | if sys.platform.startswith('darwin'): 200 | subprocess.call(('open', filepath)) 201 | elif os.name == 'nt': 202 | os.startfile(filepath) 203 | elif os.name == 'posix': 204 | subprocess.call(('xdg-open', filepath)) 205 | else: 206 | raise NotImplementedError("OS not supported: {0}".format(os.name)) 207 | 208 | 209 | def load_json(filepath): 210 | """open and read json, return read values""" 211 | with open(filepath, "r") as f: 212 | return json.load(f) 213 | 214 | 215 | def _fix_playblast_output_path(filepath): 216 | """Workaround a bug in maya.cmds.playblast to return correct filepath. 217 | 218 | When the `viewer` argument is set to False and maya.cmds.playblast does not 219 | automatically open the playblasted file the returned filepath does not have 220 | the file's extension added correctly. 221 | 222 | To workaround this we just glob.glob() for any file extensions and assume 223 | the latest modified file is the correct file and return it. 224 | 225 | """ 226 | # Catch cancelled playblast 227 | if filepath is None: 228 | log.warning("Playblast did not result in output path. " 229 | "Playblast is probably interrupted.") 230 | return 231 | 232 | # Fix: playblast not returning correct filename (with extension) 233 | # Lets assume the most recently modified file is the correct one. 234 | if not os.path.exists(filepath): 235 | directory = os.path.dirname(filepath) 236 | filename = os.path.basename(filepath) 237 | # check if the filepath is has frame based filename 238 | # example : capture.####.png 239 | parts = filename.split(".") 240 | if len(parts) == 3: 241 | query = os.path.join(directory, "{}.*.{}".format(parts[0], 242 | parts[-1])) 243 | files = glob.glob(query) 244 | else: 245 | files = glob.glob("{}.*".format(filepath)) 246 | 247 | if not files: 248 | raise RuntimeError("Couldn't find playblast from: " 249 | "{0}".format(filepath)) 250 | filepath = max(files, key=os.path.getmtime) 251 | 252 | return filepath 253 | 254 | 255 | def capture_scene(options): 256 | """Capture using scene settings. 257 | 258 | Uses the view settings from "panel". 259 | 260 | This ensures playblast is done as quicktime H.264 100% quality. 261 | It forces showOrnaments to be off and does not render off screen. 262 | 263 | Arguments: 264 | options (dict): a collection of output options 265 | 266 | Returns: 267 | str: Full path to playblast file. 268 | 269 | """ 270 | 271 | filename = options.get("filename", "%TEMP%") 272 | log.info("Capturing to: {0}".format(filename)) 273 | 274 | options = options.copy() 275 | 276 | # Force viewer to False in call to capture because we have our own 277 | # viewer opening call to allow a signal to trigger between playblast 278 | # and viewer 279 | options['viewer'] = False 280 | 281 | # Remove panel key since it's internal value to capture_gui 282 | options.pop("panel", None) 283 | 284 | path = capture.capture(**options) 285 | path = _fix_playblast_output_path(path) 286 | 287 | return path 288 | 289 | 290 | def browse(path=None): 291 | """Open a pop-up browser for the user""" 292 | 293 | # Acquire path from user input if none defined 294 | if path is None: 295 | 296 | scene_path = cmds.file(query=True, sceneName=True) 297 | 298 | # use scene file name as default name 299 | default_filename = os.path.splitext(os.path.basename(scene_path))[0] 300 | if not default_filename: 301 | # Scene wasn't saved yet so found no valid name for playblast. 302 | default_filename = "playblast" 303 | 304 | # Default to images rule 305 | default_root = os.path.normpath(get_project_rule("images")) 306 | default_path = os.path.join(default_root, default_filename) 307 | path = cmds.fileDialog2(fileMode=0, 308 | dialogStyle=2, 309 | startingDirectory=default_path) 310 | 311 | if not path: 312 | return 313 | 314 | if isinstance(path, (tuple, list)): 315 | path = path[0] 316 | 317 | if path.endswith(".*"): 318 | path = path[:-2] 319 | 320 | # Bug-Fix/Workaround: 321 | # Fix for playblasts that result in nesting of the 322 | # extension (eg. '.mov.mov.mov') which happens if the format 323 | # is defined in the filename used for saving. 324 | extension = os.path.splitext(path)[-1] 325 | if extension: 326 | path = path[:-len(extension)] 327 | 328 | return path 329 | 330 | 331 | def default_output(): 332 | """Return filename based on current scene name. 333 | 334 | Returns: 335 | str: A relative filename 336 | 337 | """ 338 | 339 | scene = get_current_scenename() or "playblast" 340 | 341 | # get current datetime 342 | timestamp = datetime.datetime.today() 343 | str_timestamp = timestamp.strftime("%Y-%m-%d_%H-%M-%S") 344 | filename = "{}_{}".format(scene, str_timestamp) 345 | 346 | return filename 347 | 348 | 349 | def get_project_rule(rule): 350 | """Get the full path of the rule of the project""" 351 | 352 | workspace = cmds.workspace(query=True, rootDirectory=True) 353 | folder = cmds.workspace(fileRuleEntry=rule) 354 | if not folder: 355 | log.warning("File Rule Entry '{}' has no value, please check if the " 356 | "rule name is typed correctly".format(rule)) 357 | 358 | return os.path.join(workspace, folder) 359 | 360 | 361 | def list_formats(): 362 | # Workaround for Maya playblast bug where undo would 363 | # move the currentTime to frame one. 364 | cmds.currentTime(cmds.currentTime(query=True)) 365 | return cmds.playblast(query=True, format=True) 366 | 367 | 368 | def list_compressions(format='avi'): 369 | # Workaround for Maya playblast bug where undo would 370 | # move the currentTime to frame one. 371 | cmds.currentTime(cmds.currentTime(query=True)) 372 | 373 | cmd = 'playblast -format "{0}" -query -compression'.format(format) 374 | return mel.eval(cmd) 375 | 376 | 377 | @contextlib.contextmanager 378 | def no_undo(): 379 | """Disable undo during the context""" 380 | try: 381 | cmds.undoInfo(stateWithoutFlush=False) 382 | yield 383 | finally: 384 | cmds.undoInfo(stateWithoutFlush=True) 385 | 386 | 387 | def get_maya_main_window(): 388 | """Get the main Maya window as a QtGui.QMainWindow instance 389 | 390 | Returns: 391 | QtGui.QMainWindow: instance of the top level Maya windows 392 | 393 | """ 394 | ptr = omui.MQtUtil.mainWindow() 395 | if ptr is not None: 396 | return shiboken.wrapInstance(long(ptr), QtWidgets.QWidget) 397 | -------------------------------------------------------------------------------- /capture_gui/plugin.py: -------------------------------------------------------------------------------- 1 | """Plug-in system 2 | 3 | Works similar to how OSs look for executables; i.e. a number of 4 | absolute paths are searched for a given match. The predicate for 5 | executables is whether or not an extension matches a number of 6 | options, such as ".exe" or ".bat". 7 | 8 | In this system, the predicate is whether or not a fname ends with ".py" 9 | 10 | """ 11 | 12 | # Standard library 13 | import os 14 | import sys 15 | import types 16 | import logging 17 | import inspect 18 | 19 | from .vendor.Qt import QtCore, QtWidgets 20 | 21 | log = logging.getLogger(__name__) 22 | 23 | _registered_paths = list() 24 | _registered_plugins = dict() 25 | 26 | 27 | class classproperty(object): 28 | def __init__(self, getter): 29 | self.getter = getter 30 | 31 | def __get__(self, instance, owner): 32 | return self.getter(owner) 33 | 34 | 35 | class Plugin(QtWidgets.QWidget): 36 | """Base class for Option plug-in Widgets. 37 | 38 | This is a regular Qt widget that can be added to the capture interface 39 | as an additional component, like a plugin. 40 | 41 | The plug-ins are sorted in the interface by their `order` attribute and 42 | will be displayed in the main interface when `section` is set to "app" 43 | and displayed in the additional settings pop-up when set to "config". 44 | 45 | When `hidden` is set to True the widget will not be shown in the interface. 46 | This could be useful as a plug-in that supplies solely default values to 47 | the capture GUI command. 48 | 49 | """ 50 | 51 | label = "" 52 | section = "app" # "config" or "app" 53 | hidden = False 54 | options_changed = QtCore.Signal() 55 | label_changed = QtCore.Signal(str) 56 | order = 0 57 | highlight = "border: 1px solid red;" 58 | validate_state = True 59 | 60 | def on_playblast_finished(self, options): 61 | pass 62 | 63 | def validate(self): 64 | """ 65 | Ensure outputs of the widget are possible, when errors are raised it 66 | will return a message with what has caused the error 67 | :return: 68 | """ 69 | errors = [] 70 | return errors 71 | 72 | def get_outputs(self): 73 | """Return the options as set in this plug-in widget. 74 | 75 | This is used to identify the settings to be used for the playblast. 76 | As such the values should be returned in a way that a call to 77 | `capture.capture()` would understand as arguments. 78 | 79 | Args: 80 | panel (str): The active modelPanel of the user. This is passed so 81 | values could potentially be parsed from the active panel 82 | 83 | Returns: 84 | dict: The options for this plug-in. (formatted `capture` style) 85 | 86 | """ 87 | return dict() 88 | 89 | def get_inputs(self, as_preset): 90 | """Return widget's child settings. 91 | 92 | This should provide a dictionary of input settings of the plug-in 93 | that results in a dictionary that can be supplied to `apply_input()` 94 | This is used to save the settings of the preset to a widget. 95 | 96 | :param as_preset: 97 | :param as_presets: Toggle to mute certain input values of the widget 98 | :type as_presets: bool 99 | 100 | Returns: 101 | dict: The currently set inputs of this widget. 102 | 103 | """ 104 | return dict() 105 | 106 | def apply_inputs(self, settings): 107 | """Apply a dictionary of settings to the widget. 108 | 109 | This should update the widget's inputs to the settings provided in 110 | the dictionary. This is used to apply settings from a preset. 111 | 112 | Returns: 113 | None 114 | 115 | """ 116 | pass 117 | 118 | def initialize(self): 119 | """ 120 | This method is used to register any callbacks 121 | :return: 122 | """ 123 | pass 124 | 125 | def uninitialize(self): 126 | """ 127 | Unregister any callback created when deleting the widget 128 | 129 | A general explation: 130 | 131 | The deletion method is an attribute that lives inside the object to be 132 | deleted, and that is the problem: 133 | Destruction seems not to care about the order of destruction, 134 | and the __dict__ that also holds the onDestroy bound method 135 | gets destructed before it is called. 136 | 137 | Another solution is to use a weakref 138 | 139 | :return: None 140 | """ 141 | pass 142 | 143 | def __str__(self): 144 | return self.label or type(self).__name__ 145 | 146 | def __repr__(self): 147 | return u"%s.%s(%r)" % (__name__, type(self).__name__, self.__str__()) 148 | 149 | id = classproperty(lambda cls: cls.__name__) 150 | 151 | 152 | def register_plugin_path(path): 153 | """Plug-ins are looked up at run-time from directories registered here 154 | 155 | To register a new directory, run this command along with the absolute 156 | path to where you"re plug-ins are located. 157 | 158 | Example: 159 | >>> import os 160 | >>> my_plugins = "/server/plugins" 161 | >>> register_plugin_path(my_plugins) 162 | '/server/plugins' 163 | 164 | Returns: 165 | Actual path added, including any post-processing 166 | 167 | """ 168 | 169 | if path in _registered_paths: 170 | return log.warning("Path already registered: {0}".format(path)) 171 | 172 | _registered_paths.append(path) 173 | 174 | return path 175 | 176 | 177 | def deregister_plugin_path(path): 178 | """Remove a _registered_paths path 179 | 180 | Raises: 181 | KeyError if `path` isn't registered 182 | 183 | """ 184 | 185 | _registered_paths.remove(path) 186 | 187 | 188 | def deregister_all_plugin_paths(): 189 | """Mainly used in tests""" 190 | _registered_paths[:] = [] 191 | 192 | 193 | def registered_plugin_paths(): 194 | """Return paths added via registration 195 | 196 | ..note:: This returns a copy of the registered paths 197 | and can therefore not be modified directly. 198 | 199 | """ 200 | 201 | return list(_registered_paths) 202 | 203 | 204 | def registered_plugins(): 205 | """Return plug-ins added via :func:`register_plugin` 206 | 207 | .. note:: This returns a copy of the registered plug-ins 208 | and can therefore not be modified directly 209 | 210 | """ 211 | 212 | return _registered_plugins.values() 213 | 214 | 215 | def register_plugin(plugin): 216 | """Register a new plug-in 217 | 218 | Arguments: 219 | plugin (Plugin): Plug-in to register 220 | 221 | Raises: 222 | TypeError if `plugin` is not callable 223 | 224 | """ 225 | 226 | if not hasattr(plugin, "__call__"): 227 | raise TypeError("Plug-in must be callable " 228 | "returning an instance of a class") 229 | 230 | if not plugin_is_valid(plugin): 231 | raise TypeError("Plug-in invalid: %s", plugin) 232 | 233 | _registered_plugins[plugin.__name__] = plugin 234 | 235 | 236 | def plugin_paths(): 237 | """Collect paths from all sources. 238 | 239 | This function looks at the three potential sources of paths 240 | and returns a list with all of them together. 241 | 242 | The sources are: 243 | 244 | - Registered paths using :func:`register_plugin_path` 245 | 246 | Returns: 247 | list of paths in which plugins may be locat 248 | 249 | """ 250 | 251 | paths = list() 252 | 253 | for path in registered_plugin_paths(): 254 | if path in paths: 255 | continue 256 | paths.append(path) 257 | 258 | return paths 259 | 260 | 261 | def discover(paths=None): 262 | """Find and return available plug-ins 263 | 264 | This function looks for files within paths registered via 265 | :func:`register_plugin_path`. 266 | 267 | Arguments: 268 | paths (list, optional): Paths to discover plug-ins from. 269 | If no paths are provided, all paths are searched. 270 | 271 | """ 272 | 273 | plugins = dict() 274 | 275 | # Include plug-ins from registered paths 276 | for path in paths or plugin_paths(): 277 | path = os.path.normpath(path) 278 | 279 | if not os.path.isdir(path): 280 | continue 281 | 282 | for fname in os.listdir(path): 283 | if fname.startswith("_"): 284 | continue 285 | 286 | abspath = os.path.join(path, fname) 287 | 288 | if not os.path.isfile(abspath): 289 | continue 290 | 291 | mod_name, mod_ext = os.path.splitext(fname) 292 | 293 | if not mod_ext == ".py": 294 | continue 295 | 296 | module = types.ModuleType(mod_name) 297 | module.__file__ = abspath 298 | 299 | try: 300 | execfile(abspath, module.__dict__) 301 | 302 | # Store reference to original module, to avoid 303 | # garbage collection from collecting it's global 304 | # imports, such as `import os`. 305 | sys.modules[mod_name] = module 306 | 307 | except Exception as err: 308 | log.debug("Skipped: \"%s\" (%s)", mod_name, err) 309 | continue 310 | 311 | for plugin in plugins_from_module(module): 312 | if plugin.id in plugins: 313 | log.debug("Duplicate plug-in found: %s", plugin) 314 | continue 315 | 316 | plugins[plugin.id] = plugin 317 | 318 | # Include plug-ins from registration. 319 | # Directly registered plug-ins take precedence. 320 | for name, plugin in _registered_plugins.items(): 321 | if name in plugins: 322 | log.debug("Duplicate plug-in found: %s", plugin) 323 | continue 324 | plugins[name] = plugin 325 | 326 | plugins = list(plugins.values()) 327 | sort(plugins) # In-place 328 | 329 | return plugins 330 | 331 | 332 | def plugins_from_module(module): 333 | """Return plug-ins from module 334 | 335 | Arguments: 336 | module (types.ModuleType): Imported module from which to 337 | parse valid plug-ins. 338 | 339 | Returns: 340 | List of plug-ins, or empty list if none is found. 341 | 342 | """ 343 | 344 | plugins = list() 345 | 346 | for name in dir(module): 347 | if name.startswith("_"): 348 | continue 349 | 350 | # It could be anything at this point 351 | obj = getattr(module, name) 352 | 353 | if not inspect.isclass(obj): 354 | continue 355 | 356 | if not issubclass(obj, Plugin): 357 | continue 358 | 359 | if not plugin_is_valid(obj): 360 | log.debug("Plug-in invalid: %s", obj) 361 | continue 362 | 363 | plugins.append(obj) 364 | 365 | return plugins 366 | 367 | 368 | def plugin_is_valid(plugin): 369 | """Determine whether or not plug-in `plugin` is valid 370 | 371 | Arguments: 372 | plugin (Plugin): Plug-in to assess 373 | 374 | """ 375 | 376 | if not plugin: 377 | return False 378 | 379 | return True 380 | 381 | 382 | def sort(plugins): 383 | """Sort `plugins` in-place 384 | 385 | Their order is determined by their `order` attribute. 386 | 387 | Arguments: 388 | plugins (list): Plug-ins to sort 389 | 390 | """ 391 | 392 | if not isinstance(plugins, list): 393 | raise TypeError("plugins must be of type list") 394 | 395 | plugins.sort(key=lambda p: p.order) 396 | return plugins 397 | 398 | 399 | # Register default paths 400 | default_plugins_path = os.path.join(os.path.dirname(__file__), "plugins") 401 | register_plugin_path(default_plugins_path) 402 | -------------------------------------------------------------------------------- /capture_gui/plugins/cameraplugin.py: -------------------------------------------------------------------------------- 1 | import maya.cmds as cmds 2 | from capture_gui.vendor.Qt import QtCore, QtWidgets 3 | 4 | import capture_gui.lib as lib 5 | import capture_gui.plugin 6 | 7 | 8 | class CameraPlugin(capture_gui.plugin.Plugin): 9 | """Camera widget. 10 | 11 | Allows to select a camera. 12 | 13 | """ 14 | id = "Camera" 15 | section = "app" 16 | order = 10 17 | 18 | def __init__(self, parent=None): 19 | super(CameraPlugin, self).__init__(parent=parent) 20 | 21 | self._layout = QtWidgets.QHBoxLayout() 22 | self._layout.setContentsMargins(5, 0, 5, 0) 23 | self.setLayout(self._layout) 24 | 25 | self.cameras = QtWidgets.QComboBox() 26 | self.cameras.setMinimumWidth(200) 27 | 28 | self.get_active = QtWidgets.QPushButton("Get active") 29 | self.get_active.setToolTip("Set camera from currently active view") 30 | self.refresh = QtWidgets.QPushButton("Refresh") 31 | self.refresh.setToolTip("Refresh the list of cameras") 32 | 33 | self._layout.addWidget(self.cameras) 34 | self._layout.addWidget(self.get_active) 35 | self._layout.addWidget(self.refresh) 36 | 37 | # Signals 38 | self.connections() 39 | 40 | # Force update of the label 41 | self.set_active_cam() 42 | self.on_update_label() 43 | 44 | def connections(self): 45 | self.get_active.clicked.connect(self.set_active_cam) 46 | self.refresh.clicked.connect(self.on_refresh) 47 | 48 | self.cameras.currentIndexChanged.connect(self.on_update_label) 49 | self.cameras.currentIndexChanged.connect(self.validate) 50 | 51 | def set_active_cam(self): 52 | cam = lib.get_current_camera() 53 | self.on_refresh(camera=cam) 54 | 55 | def select_camera(self, cam): 56 | if cam: 57 | # Ensure long name 58 | cameras = cmds.ls(cam, long=True) 59 | if not cameras: 60 | return 61 | cam = cameras[0] 62 | 63 | # Find the index in the list 64 | for i in range(self.cameras.count()): 65 | value = str(self.cameras.itemText(i)) 66 | if value == cam: 67 | self.cameras.setCurrentIndex(i) 68 | return 69 | 70 | def validate(self): 71 | 72 | errors = [] 73 | camera = self.cameras.currentText() 74 | if not cmds.objExists(camera): 75 | errors.append("{} : Selected camera '{}' " 76 | "does not exist!".format(self.id, camera)) 77 | self.cameras.setStyleSheet(self.highlight) 78 | else: 79 | self.cameras.setStyleSheet("") 80 | 81 | return errors 82 | 83 | def get_outputs(self): 84 | """Return currently selected camera from combobox.""" 85 | 86 | idx = self.cameras.currentIndex() 87 | camera = str(self.cameras.itemText(idx)) if idx != -1 else None 88 | 89 | return {"camera": camera} 90 | 91 | def on_refresh(self, camera=None): 92 | """Refresh the camera list with all current cameras in scene. 93 | 94 | A currentIndexChanged signal is only emitted for the cameras combobox 95 | when the camera is different at the end of the refresh. 96 | 97 | Args: 98 | camera (str): When name of camera is passed it will try to select 99 | the camera with this name after the refresh. 100 | 101 | Returns: 102 | None 103 | 104 | """ 105 | 106 | cam = self.get_outputs()['camera'] 107 | 108 | # Get original selection 109 | if camera is None: 110 | index = self.cameras.currentIndex() 111 | if index != -1: 112 | camera = self.cameras.currentText() 113 | 114 | self.cameras.blockSignals(True) 115 | 116 | # Update the list with available cameras 117 | self.cameras.clear() 118 | 119 | cam_shapes = cmds.ls(type="camera") 120 | cam_transforms = cmds.listRelatives(cam_shapes, 121 | parent=True, 122 | fullPath=True) 123 | self.cameras.addItems(cam_transforms) 124 | 125 | # If original selection, try to reselect 126 | self.select_camera(camera) 127 | 128 | self.cameras.blockSignals(False) 129 | 130 | # If camera changed emit signal 131 | if cam != self.get_outputs()['camera']: 132 | idx = self.cameras.currentIndex() 133 | self.cameras.currentIndexChanged.emit(idx) 134 | 135 | def on_update_label(self): 136 | 137 | cam = self.cameras.currentText() 138 | cam = cam.rsplit("|", 1)[-1] # ensure short name 139 | self.label = "Camera ({0})".format(cam) 140 | 141 | self.label_changed.emit(self.label) 142 | -------------------------------------------------------------------------------- /capture_gui/plugins/codecplugin.py: -------------------------------------------------------------------------------- 1 | from capture_gui.vendor.Qt import QtCore, QtWidgets 2 | 3 | import capture_gui.lib as lib 4 | import capture_gui.plugin 5 | 6 | 7 | class CodecPlugin(capture_gui.plugin.Plugin): 8 | """Codec widget. 9 | 10 | Allows to set format, compression and quality. 11 | 12 | """ 13 | id = "Codec" 14 | label = "Codec" 15 | section = "config" 16 | order = 50 17 | 18 | def __init__(self, parent=None): 19 | super(CodecPlugin, self).__init__(parent=parent) 20 | 21 | self._layout = QtWidgets.QHBoxLayout() 22 | self._layout.setContentsMargins(0, 0, 0, 0) 23 | self.setLayout(self._layout) 24 | 25 | self.format = QtWidgets.QComboBox() 26 | self.compression = QtWidgets.QComboBox() 27 | self.quality = QtWidgets.QSpinBox() 28 | self.quality.setMinimum(0) 29 | self.quality.setMaximum(100) 30 | self.quality.setValue(100) 31 | self.quality.setToolTip("Compression quality percentage") 32 | 33 | self._layout.addWidget(self.format) 34 | self._layout.addWidget(self.compression) 35 | self._layout.addWidget(self.quality) 36 | 37 | self.format.currentIndexChanged.connect(self.on_format_changed) 38 | 39 | self.refresh() 40 | 41 | # Default to format 'qt' 42 | index = self.format.findText("qt") 43 | if index != -1: 44 | self.format.setCurrentIndex(index) 45 | 46 | # Default to compression 'H.264' 47 | index = self.compression.findText("H.264") 48 | if index != -1: 49 | self.compression.setCurrentIndex(index) 50 | 51 | self.connections() 52 | 53 | def connections(self): 54 | self.compression.currentIndexChanged.connect(self.options_changed) 55 | self.format.currentIndexChanged.connect(self.options_changed) 56 | self.quality.valueChanged.connect(self.options_changed) 57 | 58 | def refresh(self): 59 | formats = sorted(lib.list_formats()) 60 | self.format.clear() 61 | self.format.addItems(formats) 62 | 63 | def on_format_changed(self): 64 | """Refresh the available compressions.""" 65 | 66 | format = self.format.currentText() 67 | compressions = lib.list_compressions(format) 68 | self.compression.clear() 69 | self.compression.addItems(compressions) 70 | 71 | def get_outputs(self): 72 | """Get the plugin outputs that matches `capture.capture` arguments 73 | 74 | Returns: 75 | dict: Plugin outputs 76 | 77 | """ 78 | 79 | return {"format": self.format.currentText(), 80 | "compression": self.compression.currentText(), 81 | "quality": self.quality.value()} 82 | 83 | def get_inputs(self, as_preset): 84 | # a bit redundant but it will work when iterating over widgets 85 | # so we don't have to write an exception 86 | return self.get_outputs() 87 | 88 | def apply_inputs(self, settings): 89 | codec_format = settings.get("format", 0) 90 | compr = settings.get("compression", 4) 91 | quality = settings.get("quality", 100) 92 | 93 | self.format.setCurrentIndex(self.format.findText(codec_format)) 94 | self.compression.setCurrentIndex(self.compression.findText(compr)) 95 | self.quality.setValue(int(quality)) 96 | -------------------------------------------------------------------------------- /capture_gui/plugins/defaultoptionsplugin.py: -------------------------------------------------------------------------------- 1 | import capture 2 | import capture_gui.plugin 3 | 4 | 5 | class DefaultOptionsPlugin(capture_gui.plugin.Plugin): 6 | """Invisible Plugin that supplies some default values to the gui. 7 | 8 | This enures: 9 | - no HUD is present in playblasts 10 | - no overscan (`overscan` set to 1.0) 11 | - no title safe, action safe, gate mask, etc. 12 | - active sound is included in video playblasts 13 | 14 | """ 15 | order = -1 16 | hidden = True 17 | 18 | def get_outputs(self): 19 | """Get the plugin outputs that matches `capture.capture` arguments 20 | 21 | Returns: 22 | dict: Plugin outputs 23 | 24 | """ 25 | 26 | outputs = dict() 27 | 28 | # use active sound track 29 | scene = capture.parse_active_scene() 30 | outputs['sound'] = scene['sound'] 31 | 32 | # override default settings 33 | outputs['show_ornaments'] = True # never show HUD or overlays 34 | 35 | # override camera options 36 | outputs['camera_options'] = dict() 37 | outputs['camera_options']['overscan'] = 1.0 38 | outputs['camera_options']['displayFieldChart'] = False 39 | outputs['camera_options']['displayFilmGate'] = False 40 | outputs['camera_options']['displayFilmOrigin'] = False 41 | outputs['camera_options']['displayFilmPivot'] = False 42 | outputs['camera_options']['displayGateMask'] = False 43 | outputs['camera_options']['displayResolution'] = False 44 | outputs['camera_options']['displaySafeAction'] = False 45 | outputs['camera_options']['displaySafeTitle'] = False 46 | 47 | return outputs 48 | -------------------------------------------------------------------------------- /capture_gui/plugins/displayplugin.py: -------------------------------------------------------------------------------- 1 | import maya.cmds as cmds 2 | 3 | from capture_gui.vendor.Qt import QtCore, QtWidgets 4 | import capture_gui.plugin 5 | import capture_gui.colorpicker as colorpicker 6 | 7 | 8 | # region GLOBALS 9 | 10 | BACKGROUND_DEFAULT = [0.6309999823570251, 11 | 0.6309999823570251, 12 | 0.6309999823570251] 13 | 14 | TOP_DEFAULT = [0.5350000262260437, 15 | 0.6169999837875366, 16 | 0.7020000219345093] 17 | 18 | BOTTOM_DEFAULT = [0.052000001072883606, 19 | 0.052000001072883606, 20 | 0.052000001072883606] 21 | 22 | COLORS = {"background": BACKGROUND_DEFAULT, 23 | "backgroundTop": TOP_DEFAULT, 24 | "backgroundBottom": BOTTOM_DEFAULT} 25 | 26 | LABELS = {"background": "Background", 27 | "backgroundTop": "Top", 28 | "backgroundBottom": "Bottom"} 29 | # endregion GLOBALS 30 | 31 | 32 | class DisplayPlugin(capture_gui.plugin.Plugin): 33 | """Plugin to apply viewport visibilities and settings""" 34 | 35 | id = "Display Options" 36 | label = "Display Options" 37 | section = "config" 38 | order = 70 39 | 40 | def __init__(self, parent=None): 41 | super(DisplayPlugin, self).__init__(parent=parent) 42 | 43 | self._colors = dict() 44 | 45 | self._layout = QtWidgets.QVBoxLayout() 46 | self._layout.setContentsMargins(0, 0, 0, 0) 47 | self.setLayout(self._layout) 48 | 49 | self.override = QtWidgets.QCheckBox("Override Display Options") 50 | 51 | self.display_type = QtWidgets.QComboBox() 52 | self.display_type.addItems(["Solid", "Gradient"]) 53 | 54 | # create color columns 55 | self._color_layout = QtWidgets.QHBoxLayout() 56 | for label, default in COLORS.items(): 57 | self.add_color_picker(self._color_layout, label, default) 58 | 59 | # populate layout 60 | self._layout.addWidget(self.override) 61 | self._layout.addWidget(self.display_type) 62 | self._layout.addLayout(self._color_layout) 63 | 64 | # ensure widgets are in the correct enable state 65 | self.on_toggle_override() 66 | 67 | self.connections() 68 | 69 | def connections(self): 70 | self.override.toggled.connect(self.on_toggle_override) 71 | self.override.toggled.connect(self.options_changed) 72 | self.display_type.currentIndexChanged.connect(self.options_changed) 73 | 74 | def add_color_picker(self, layout, label, default): 75 | """Create a column with a label and a button to select a color 76 | 77 | Arguments: 78 | layout (QtWidgets.QLayout): Layout to add color picker to 79 | label (str): system name for the color type, e.g. : backgroundTop 80 | default (list): The default color values to start with 81 | 82 | Returns: 83 | colorpicker.ColorPicker: a color picker instance 84 | 85 | """ 86 | 87 | column = QtWidgets.QVBoxLayout() 88 | label_widget = QtWidgets.QLabel(LABELS[label]) 89 | 90 | color_picker = colorpicker.ColorPicker() 91 | color_picker.color = default 92 | 93 | column.addWidget(label_widget) 94 | column.addWidget(color_picker) 95 | 96 | column.setAlignment(label_widget, QtCore.Qt.AlignCenter) 97 | 98 | layout.addLayout(column) 99 | 100 | # connect signal 101 | color_picker.valueChanged.connect(self.options_changed) 102 | 103 | # store widget 104 | self._colors[label] = color_picker 105 | 106 | return color_picker 107 | 108 | def on_toggle_override(self): 109 | """Callback when override is toggled. 110 | 111 | Enable or disable the color pickers and background type widgets bases 112 | on the current state of the override checkbox 113 | 114 | Returns: 115 | None 116 | 117 | """ 118 | state = self.override.isChecked() 119 | self.display_type.setEnabled(state) 120 | for widget in self._colors.values(): 121 | widget.setEnabled(state) 122 | 123 | def display_gradient(self): 124 | """Return whether the background should be displayed as gradient. 125 | 126 | When True the colors will use the top and bottom color to define the 127 | gradient otherwise the background color will be used as solid color. 128 | 129 | Returns: 130 | bool: Whether background is gradient 131 | 132 | """ 133 | return self.display_type.currentText() == "Gradient" 134 | 135 | def apply_inputs(self, settings): 136 | """Apply the saved inputs from the inputs configuration 137 | 138 | Arguments: 139 | settings (dict): The input settings to apply. 140 | 141 | """ 142 | 143 | for label, widget in self._colors.items(): 144 | default = COLORS.get(label, [0, 0, 0]) # fallback default to black 145 | value = settings.get(label, default) 146 | widget.color = value 147 | 148 | override = settings.get("override_display", False) 149 | self.override.setChecked(override) 150 | 151 | def get_inputs(self, as_preset): 152 | inputs = {"override_display": self.override.isChecked()} 153 | for label, widget in self._colors.items(): 154 | inputs[label] = widget.color 155 | 156 | return inputs 157 | 158 | def get_outputs(self): 159 | """Get the plugin outputs that matches `capture.capture` arguments 160 | 161 | Returns: 162 | dict: Plugin outputs 163 | 164 | """ 165 | 166 | outputs = {} 167 | if self.override.isChecked(): 168 | outputs["displayGradient"] = self.display_gradient() 169 | for label, widget in self._colors.items(): 170 | outputs[label] = widget.color 171 | else: 172 | # Parse active color settings 173 | outputs["displayGradient"] = cmds.displayPref(query=True, 174 | displayGradient=True) 175 | for key in COLORS.keys(): 176 | color = cmds.displayRGBColor(key, query=True) 177 | outputs[key] = color 178 | 179 | return {"display_options": outputs} 180 | -------------------------------------------------------------------------------- /capture_gui/plugins/genericplugin.py: -------------------------------------------------------------------------------- 1 | import maya.cmds as mc 2 | from capture_gui.vendor.Qt import QtCore, QtWidgets 3 | 4 | import capture_gui.plugin 5 | import capture_gui.lib 6 | 7 | 8 | class GenericPlugin(capture_gui.plugin.Plugin): 9 | """Widget for generic options""" 10 | id = "Generic" 11 | label = "Generic" 12 | section = "config" 13 | order = 100 14 | 15 | def __init__(self, parent=None): 16 | super(GenericPlugin, self).__init__(parent=parent) 17 | 18 | layout = QtWidgets.QVBoxLayout(self) 19 | layout.setContentsMargins(0, 0, 0, 0) 20 | self.setLayout(layout) 21 | 22 | isolate_view = QtWidgets.QCheckBox( 23 | "Use isolate view from active panel") 24 | off_screen = QtWidgets.QCheckBox("Render offscreen") 25 | 26 | layout.addWidget(isolate_view) 27 | layout.addWidget(off_screen) 28 | 29 | isolate_view.stateChanged.connect(self.options_changed) 30 | off_screen.stateChanged.connect(self.options_changed) 31 | 32 | self.widgets = { 33 | "off_screen": off_screen, 34 | "isolate_view": isolate_view 35 | } 36 | 37 | self.apply_inputs(self.get_defaults()) 38 | 39 | def get_defaults(self): 40 | return { 41 | "off_screen": True, 42 | "isolate_view": False 43 | } 44 | 45 | def get_inputs(self, as_preset): 46 | """Return the widget options 47 | 48 | Returns: 49 | dict: The input settings of the widgets. 50 | 51 | """ 52 | 53 | inputs = dict() 54 | for key, widget in self.widgets.items(): 55 | state = widget.isChecked() 56 | inputs[key] = state 57 | 58 | return inputs 59 | 60 | def apply_inputs(self, inputs): 61 | """Apply the saved inputs from the inputs configuration 62 | 63 | Arguments: 64 | inputs (dict): The input settings to apply. 65 | 66 | """ 67 | 68 | for key, widget in self.widgets.items(): 69 | state = inputs.get(key, None) 70 | if state is not None: 71 | widget.setChecked(state) 72 | 73 | return inputs 74 | 75 | def get_outputs(self): 76 | """Returns all the options from the widget 77 | 78 | Returns: dictionary with the settings 79 | 80 | """ 81 | 82 | inputs = self.get_inputs(as_preset=False) 83 | outputs = dict() 84 | outputs['off_screen'] = inputs['off_screen'] 85 | 86 | import capture_gui.lib 87 | 88 | # Get isolate view members of the active panel 89 | if inputs['isolate_view']: 90 | panel = capture_gui.lib.get_active_editor() 91 | filter_set = mc.modelEditor(panel, query=True, viewObjects=True) 92 | isolate = mc.sets(filter_set, query=True) if filter_set else None 93 | outputs['isolate'] = isolate 94 | 95 | return outputs 96 | -------------------------------------------------------------------------------- /capture_gui/plugins/ioplugin.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | from functools import partial 4 | 5 | from capture_gui.vendor.Qt import QtCore, QtWidgets 6 | from capture_gui import plugin, lib 7 | from capture_gui import tokens 8 | 9 | log = logging.getLogger("IO") 10 | 11 | 12 | class IoAction(QtWidgets.QAction): 13 | 14 | def __init__(self, parent, filepath): 15 | super(IoAction, self).__init__(parent) 16 | 17 | action_label = os.path.basename(filepath) 18 | 19 | self.setText(action_label) 20 | self.setData(filepath) 21 | 22 | # check if file exists and disable when false 23 | self.setEnabled(os.path.isfile(filepath)) 24 | 25 | # get icon from file 26 | info = QtCore.QFileInfo(filepath) 27 | icon_provider = QtWidgets.QFileIconProvider() 28 | self.setIcon(icon_provider.icon(info)) 29 | 30 | self.triggered.connect(self.open_object_data) 31 | 32 | def open_object_data(self): 33 | lib.open_file(self.data()) 34 | 35 | 36 | class IoPlugin(plugin.Plugin): 37 | """Codec widget. 38 | 39 | Allows to set format, compression and quality. 40 | 41 | """ 42 | id = "IO" 43 | label = "Save" 44 | section = "app" 45 | order = 40 46 | max_recent_playblasts = 5 47 | 48 | def __init__(self, parent=None): 49 | super(IoPlugin, self).__init__(parent=parent) 50 | 51 | self.recent_playblasts = list() 52 | 53 | self._layout = QtWidgets.QVBoxLayout() 54 | self._layout.setContentsMargins(0, 0, 0, 0) 55 | self.setLayout(self._layout) 56 | 57 | # region Checkboxes 58 | self.save_file = QtWidgets.QCheckBox(text="Save") 59 | self.open_viewer = QtWidgets.QCheckBox(text="View when finished") 60 | self.raw_frame_numbers = QtWidgets.QCheckBox(text="Raw frame numbers") 61 | 62 | checkbox_hlayout = QtWidgets.QHBoxLayout() 63 | checkbox_hlayout.setContentsMargins(5, 0, 5, 0) 64 | checkbox_hlayout.addWidget(self.save_file) 65 | checkbox_hlayout.addWidget(self.open_viewer) 66 | checkbox_hlayout.addWidget(self.raw_frame_numbers) 67 | checkbox_hlayout.addStretch(True) 68 | # endregion Checkboxes 69 | 70 | # region Path 71 | self.path_widget = QtWidgets.QWidget() 72 | 73 | self.browse = QtWidgets.QPushButton("Browse") 74 | self.file_path = QtWidgets.QLineEdit() 75 | self.file_path.setPlaceholderText("(not set; using scene name)") 76 | tip = "Right click in the text field to insert tokens" 77 | self.file_path.setToolTip(tip) 78 | self.file_path.setStatusTip(tip) 79 | self.file_path.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) 80 | self.file_path.customContextMenuRequested.connect(self.show_token_menu) 81 | 82 | path_hlayout = QtWidgets.QHBoxLayout() 83 | path_hlayout.setContentsMargins(0, 0, 0, 0) 84 | path_label = QtWidgets.QLabel("Path:") 85 | path_label.setFixedWidth(30) 86 | 87 | path_hlayout.addWidget(path_label) 88 | path_hlayout.addWidget(self.file_path) 89 | path_hlayout.addWidget(self.browse) 90 | self.path_widget.setLayout(path_hlayout) 91 | # endregion Path 92 | 93 | # region Recent Playblast 94 | self.play_recent = QtWidgets.QPushButton("Play recent playblast") 95 | self.recent_menu = QtWidgets.QMenu() 96 | self.play_recent.setMenu(self.recent_menu) 97 | # endregion Recent Playblast 98 | 99 | self._layout.addLayout(checkbox_hlayout) 100 | self._layout.addWidget(self.path_widget) 101 | self._layout.addWidget(self.play_recent) 102 | 103 | # Signals / connections 104 | self.browse.clicked.connect(self.show_browse_dialog) 105 | self.file_path.textChanged.connect(self.options_changed) 106 | self.save_file.stateChanged.connect(self.options_changed) 107 | self.raw_frame_numbers.stateChanged.connect(self.options_changed) 108 | self.save_file.stateChanged.connect(self.on_save_changed) 109 | 110 | # Ensure state is up-to-date with current settings 111 | self.on_save_changed() 112 | 113 | def on_save_changed(self): 114 | """Update the visibility of the path field""" 115 | 116 | state = self.save_file.isChecked() 117 | if state: 118 | self.path_widget.show() 119 | else: 120 | self.path_widget.hide() 121 | 122 | def show_browse_dialog(self): 123 | """Set the filepath using a browser dialog. 124 | 125 | :return: None 126 | """ 127 | 128 | path = lib.browse() 129 | if not path: 130 | return 131 | 132 | # Maya's browser return Linux based file paths to ensure Windows is 133 | # supported we use normpath 134 | path = os.path.normpath(path) 135 | 136 | self.file_path.setText(path) 137 | 138 | def add_playblast(self, item): 139 | """ 140 | Add an item to the previous playblast menu 141 | 142 | :param item: full path to a playblast file 143 | :type item: str 144 | 145 | :return: None 146 | """ 147 | 148 | # If item already in the recent playblasts remove it so we are 149 | # sure to add it as the new first most-recent 150 | try: 151 | self.recent_playblasts.remove(item) 152 | except ValueError: 153 | pass 154 | 155 | # Add as first in the recent playblasts 156 | self.recent_playblasts.insert(0, item) 157 | 158 | # Ensure the playblast list is never longer than maximum amount 159 | # by removing the older entries that are at the end of the list 160 | if len(self.recent_playblasts) > self.max_recent_playblasts: 161 | del self.recent_playblasts[self.max_recent_playblasts:] 162 | 163 | # Rebuild the actions menu 164 | self.recent_menu.clear() 165 | for playblast in self.recent_playblasts: 166 | action = IoAction(parent=self, filepath=playblast) 167 | self.recent_menu.addAction(action) 168 | 169 | def on_playblast_finished(self, options): 170 | """Take action after the play blast is done""" 171 | playblast_file = options['filename'] 172 | if not playblast_file: 173 | return 174 | self.add_playblast(playblast_file) 175 | 176 | def get_outputs(self): 177 | """Get the plugin outputs that matches `capture.capture` arguments 178 | 179 | Returns: 180 | dict: Plugin outputs 181 | 182 | """ 183 | 184 | output = {"filename": None, 185 | "raw_frame_numbers": self.raw_frame_numbers.isChecked(), 186 | "viewer": self.open_viewer.isChecked()} 187 | 188 | save = self.save_file.isChecked() 189 | if not save: 190 | return output 191 | 192 | # get path, if nothing is set fall back to default 193 | # project/images/playblast 194 | path = self.file_path.text() 195 | if not path: 196 | path = lib.default_output() 197 | 198 | output["filename"] = path 199 | 200 | return output 201 | 202 | def get_inputs(self, as_preset): 203 | inputs = {"name": self.file_path.text(), 204 | "save_file": self.save_file.isChecked(), 205 | "open_finished": self.open_viewer.isChecked(), 206 | "recent_playblasts": self.recent_playblasts, 207 | "raw_frame_numbers": self.raw_frame_numbers.isChecked()} 208 | 209 | if as_preset: 210 | inputs["recent_playblasts"] = [] 211 | 212 | return inputs 213 | 214 | def apply_inputs(self, settings): 215 | 216 | directory = settings.get("name", None) 217 | save_file = settings.get("save_file", True) 218 | open_finished = settings.get("open_finished", True) 219 | raw_frame_numbers = settings.get("raw_frame_numbers", False) 220 | previous_playblasts = settings.get("recent_playblasts", []) 221 | 222 | self.save_file.setChecked(save_file) 223 | self.open_viewer.setChecked(open_finished) 224 | self.raw_frame_numbers.setChecked(raw_frame_numbers) 225 | 226 | for playblast in reversed(previous_playblasts): 227 | self.add_playblast(playblast) 228 | 229 | self.file_path.setText(directory) 230 | 231 | def token_menu(self): 232 | """ 233 | Build the token menu based on the registered tokens 234 | 235 | :returns: Menu 236 | :rtype: QtWidgets.QMenu 237 | """ 238 | menu = QtWidgets.QMenu(self) 239 | registered_tokens = tokens.list_tokens() 240 | 241 | for token, value in registered_tokens.items(): 242 | label = "{} \t{}".format(token, value['label']) 243 | action = QtWidgets.QAction(label, menu) 244 | fn = partial(self.file_path.insert, token) 245 | action.triggered.connect(fn) 246 | menu.addAction(action) 247 | 248 | return menu 249 | 250 | def show_token_menu(self, pos): 251 | """Show custom manu on position of widget""" 252 | menu = self.token_menu() 253 | globalpos = QtCore.QPoint(self.file_path.mapToGlobal(pos)) 254 | menu.exec_(globalpos) 255 | -------------------------------------------------------------------------------- /capture_gui/plugins/panzoomplugin.py: -------------------------------------------------------------------------------- 1 | from capture_gui.vendor.Qt import QtCore, QtWidgets 2 | import capture_gui.plugin 3 | 4 | 5 | class PanZoomPlugin(capture_gui.plugin.Plugin): 6 | """Pan/Zoom widget. 7 | 8 | Allows to toggle whether you want to playblast with the camera's pan/zoom 9 | state or disable it during the playblast. When "Use pan/zoom from camera" 10 | is *not* checked it will force disable pan/zoom. 11 | 12 | """ 13 | id = "PanZoom" 14 | label = "Pan/Zoom" 15 | section = "config" 16 | order = 110 17 | 18 | def __init__(self, parent=None): 19 | super(PanZoomPlugin, self).__init__(parent=parent) 20 | 21 | self._layout = QtWidgets.QHBoxLayout() 22 | self._layout.setContentsMargins(5, 0, 5, 0) 23 | self.setLayout(self._layout) 24 | 25 | self.pan_zoom = QtWidgets.QCheckBox("Use pan/zoom from camera") 26 | self.pan_zoom.setChecked(True) 27 | 28 | self._layout.addWidget(self.pan_zoom) 29 | 30 | self.pan_zoom.stateChanged.connect(self.options_changed) 31 | 32 | def get_outputs(self): 33 | 34 | if not self.pan_zoom.isChecked(): 35 | return {"camera_options": { 36 | "panZoomEnabled": 1, 37 | "horizontalPan": 0.0, 38 | "verticalPan": 0.0, 39 | "zoom": 1.0} 40 | } 41 | else: 42 | return {} 43 | 44 | def apply_inputs(self, settings): 45 | self.pan_zoom.setChecked(settings.get("pan_zoom", True)) 46 | 47 | def get_inputs(self, as_preset): 48 | return {"pan_zoom": self.pan_zoom.isChecked()} 49 | -------------------------------------------------------------------------------- /capture_gui/plugins/rendererplugin.py: -------------------------------------------------------------------------------- 1 | import maya.cmds as cmds 2 | 3 | from capture_gui.vendor.Qt import QtCore, QtWidgets 4 | import capture_gui.lib as lib 5 | import capture_gui.plugin 6 | 7 | 8 | class RendererPlugin(capture_gui.plugin.Plugin): 9 | """Renderer plugin to control the used playblast renderer for viewport""" 10 | 11 | id = "Renderer" 12 | label = "Renderer" 13 | section = "config" 14 | order = 60 15 | 16 | def __init__(self, parent=None): 17 | super(RendererPlugin, self).__init__(parent=parent) 18 | 19 | layout = QtWidgets.QVBoxLayout(self) 20 | layout.setContentsMargins(0, 0, 0, 0) 21 | self.setLayout(layout) 22 | 23 | # Get active renderers for viewport 24 | self._renderers = self.get_renderers() 25 | 26 | # Create list of renderers 27 | self.renderers = QtWidgets.QComboBox() 28 | self.renderers.addItems(self._renderers.keys()) 29 | 30 | layout.addWidget(self.renderers) 31 | 32 | self.apply_inputs(self.get_defaults()) 33 | 34 | # Signals 35 | self.renderers.currentIndexChanged.connect(self.options_changed) 36 | 37 | def get_current_renderer(self): 38 | """Get current renderer by internal name (non-UI) 39 | 40 | Returns: 41 | str: Name of renderer. 42 | 43 | """ 44 | renderer_ui = self.renderers.currentText() 45 | renderer = self._renderers.get(renderer_ui, None) 46 | if renderer is None: 47 | raise RuntimeError("No valid renderer: {0}".format(renderer_ui)) 48 | 49 | return renderer 50 | 51 | def get_renderers(self): 52 | """Collect all available renderers for playblast""" 53 | active_editor = lib.get_active_editor() 54 | renderers_ui = cmds.modelEditor(active_editor, 55 | query=True, 56 | rendererListUI=True) 57 | renderers_id = cmds.modelEditor(active_editor, 58 | query=True, 59 | rendererList=True) 60 | 61 | renderers = dict(zip(renderers_ui, renderers_id)) 62 | renderers.pop("Stub Renderer") 63 | 64 | return renderers 65 | 66 | def get_defaults(self): 67 | return {"rendererName": "vp2Renderer"} 68 | 69 | def get_inputs(self, as_preset): 70 | return {"rendererName": self.get_current_renderer()} 71 | 72 | def get_outputs(self): 73 | """Get the plugin outputs that matches `capture.capture` arguments 74 | 75 | Returns: 76 | dict: Plugin outputs 77 | 78 | """ 79 | return { 80 | "viewport_options": { 81 | "rendererName": self.get_current_renderer() 82 | } 83 | } 84 | 85 | def apply_inputs(self, inputs): 86 | """Apply previous settings or settings from a preset 87 | 88 | Args: 89 | inputs (dict): Plugin input settings 90 | 91 | Returns: 92 | None 93 | 94 | """ 95 | 96 | reverse_lookup = {value: key for key, value in self._renderers.items()} 97 | renderer = inputs.get("rendererName", "vp2Renderer") 98 | renderer_ui = reverse_lookup.get(renderer) 99 | 100 | if renderer_ui: 101 | index = self.renderers.findText(renderer_ui) 102 | self.renderers.setCurrentIndex(index) 103 | else: 104 | self.renderers.setCurrentIndex(1) 105 | -------------------------------------------------------------------------------- /capture_gui/plugins/resolutionplugin.py: -------------------------------------------------------------------------------- 1 | import math 2 | from functools import partial 3 | 4 | import maya.cmds as cmds 5 | from capture_gui.vendor.Qt import QtCore, QtWidgets 6 | 7 | import capture_gui.lib as lib 8 | import capture_gui.plugin 9 | 10 | 11 | class ResolutionPlugin(capture_gui.plugin.Plugin): 12 | """Resolution widget. 13 | 14 | Allows to set scale based on set of options. 15 | 16 | """ 17 | id = "Resolution" 18 | section = "app" 19 | order = 20 20 | 21 | resolution_changed = QtCore.Signal() 22 | 23 | ScaleWindow = "From Window" 24 | ScaleRenderSettings = "From Render Settings" 25 | ScaleCustom = "Custom" 26 | 27 | def __init__(self, parent=None): 28 | super(ResolutionPlugin, self).__init__(parent=parent) 29 | 30 | self._layout = QtWidgets.QVBoxLayout() 31 | self._layout.setContentsMargins(0, 0, 0, 0) 32 | self.setLayout(self._layout) 33 | 34 | # Scale 35 | self.mode = QtWidgets.QComboBox() 36 | self.mode.addItems([self.ScaleWindow, 37 | self.ScaleRenderSettings, 38 | self.ScaleCustom]) 39 | self.mode.setCurrentIndex(1) # Default: From render settings 40 | 41 | # Custom width/height 42 | self.resolution = QtWidgets.QWidget() 43 | self.resolution.setContentsMargins(0, 0, 0, 0) 44 | resolution_layout = QtWidgets.QHBoxLayout() 45 | resolution_layout.setContentsMargins(0, 0, 0, 0) 46 | resolution_layout.setSpacing(6) 47 | 48 | self.resolution.setLayout(resolution_layout) 49 | width_label = QtWidgets.QLabel("Width") 50 | width_label.setFixedWidth(40) 51 | self.width = QtWidgets.QSpinBox() 52 | self.width.setMinimum(0) 53 | self.width.setMaximum(99999) 54 | self.width.setValue(1920) 55 | heigth_label = QtWidgets.QLabel("Height") 56 | heigth_label.setFixedWidth(40) 57 | self.height = QtWidgets.QSpinBox() 58 | self.height.setMinimum(0) 59 | self.height.setMaximum(99999) 60 | self.height.setValue(1080) 61 | 62 | resolution_layout.addWidget(width_label) 63 | resolution_layout.addWidget(self.width) 64 | resolution_layout.addWidget(heigth_label) 65 | resolution_layout.addWidget(self.height) 66 | 67 | self.scale_result = QtWidgets.QLineEdit() 68 | self.scale_result.setReadOnly(True) 69 | 70 | # Percentage 71 | self.percent_label = QtWidgets.QLabel("Scale") 72 | self.percent = QtWidgets.QDoubleSpinBox() 73 | self.percent.setMinimum(0.01) 74 | self.percent.setValue(1.0) # default value 75 | self.percent.setSingleStep(0.05) 76 | 77 | self.percent_presets = QtWidgets.QHBoxLayout() 78 | self.percent_presets.setSpacing(4) 79 | for value in [0.25, 0.5, 0.75, 1.0, 2.0]: 80 | btn = QtWidgets.QPushButton(str(value)) 81 | self.percent_presets.addWidget(btn) 82 | btn.setFixedWidth(35) 83 | btn.clicked.connect(partial(self.percent.setValue, value)) 84 | 85 | self.percent_layout = QtWidgets.QHBoxLayout() 86 | self.percent_layout.addWidget(self.percent_label) 87 | self.percent_layout.addWidget(self.percent) 88 | self.percent_layout.addLayout(self.percent_presets) 89 | 90 | # Resulting scale display 91 | self._layout.addWidget(self.mode) 92 | self._layout.addWidget(self.resolution) 93 | self._layout.addLayout(self.percent_layout) 94 | self._layout.addWidget(self.scale_result) 95 | 96 | # refresh states 97 | self.on_mode_changed() 98 | self.on_resolution_changed() 99 | 100 | # connect signals 101 | self.mode.currentIndexChanged.connect(self.on_mode_changed) 102 | self.mode.currentIndexChanged.connect(self.on_resolution_changed) 103 | self.percent.valueChanged.connect(self.on_resolution_changed) 104 | self.width.valueChanged.connect(self.on_resolution_changed) 105 | self.height.valueChanged.connect(self.on_resolution_changed) 106 | 107 | # Connect options changed 108 | self.mode.currentIndexChanged.connect(self.options_changed) 109 | self.percent.valueChanged.connect(self.options_changed) 110 | self.width.valueChanged.connect(self.options_changed) 111 | self.height.valueChanged.connect(self.options_changed) 112 | 113 | def on_mode_changed(self): 114 | """Update the width/height enabled state when mode changes""" 115 | 116 | if self.mode.currentText() != self.ScaleCustom: 117 | self.width.setEnabled(False) 118 | self.height.setEnabled(False) 119 | self.resolution.hide() 120 | else: 121 | self.width.setEnabled(True) 122 | self.height.setEnabled(True) 123 | self.resolution.show() 124 | 125 | def _get_output_resolution(self): 126 | 127 | options = self.get_outputs() 128 | return int(options["width"]), int(options["height"]) 129 | 130 | def on_resolution_changed(self): 131 | """Update the resulting resolution label""" 132 | 133 | width, height = self._get_output_resolution() 134 | label = "Result: {0}x{1}".format(width, height) 135 | 136 | self.scale_result.setText(label) 137 | 138 | # Update label 139 | self.label = "Resolution ({0}x{1})".format(width, height) 140 | self.label_changed.emit(self.label) 141 | 142 | def get_outputs(self): 143 | """Return width x height defined by the combination of settings 144 | 145 | Returns: 146 | dict: width and height key values 147 | 148 | """ 149 | mode = self.mode.currentText() 150 | panel = lib.get_active_editor() 151 | 152 | if mode == self.ScaleCustom: 153 | width = self.width.value() 154 | height = self.height.value() 155 | 156 | elif mode == self.ScaleRenderSettings: 157 | # width height from render resolution 158 | width = cmds.getAttr("defaultResolution.width") 159 | height = cmds.getAttr("defaultResolution.height") 160 | 161 | elif mode == self.ScaleWindow: 162 | # width height from active view panel size 163 | if not panel: 164 | # No panel would be passed when updating in the UI as such 165 | # the resulting resolution can't be previewed. But this should 166 | # never happen when starting the capture. 167 | width = 0 168 | height = 0 169 | else: 170 | width = cmds.control(panel, query=True, width=True) 171 | height = cmds.control(panel, query=True, height=True) 172 | else: 173 | raise NotImplementedError("Unsupported scale mode: " 174 | "{0}".format(mode)) 175 | 176 | scale = [width, height] 177 | percentage = self.percent.value() 178 | scale = [math.floor(x * percentage) for x in scale] 179 | 180 | return {"width": scale[0], "height": scale[1]} 181 | 182 | def get_inputs(self, as_preset): 183 | return {"mode": self.mode.currentText(), 184 | "width": self.width.value(), 185 | "height": self.height.value(), 186 | "percent": self.percent.value()} 187 | 188 | def apply_inputs(self, settings): 189 | # get value else fall back to default values 190 | mode = settings.get("mode", self.ScaleRenderSettings) 191 | width = int(settings.get("width", 1920)) 192 | height = int(settings.get("height", 1080)) 193 | percent = float(settings.get("percent", 1.0)) 194 | 195 | # set values 196 | self.mode.setCurrentIndex(self.mode.findText(mode)) 197 | self.width.setValue(width) 198 | self.height.setValue(height) 199 | self.percent.setValue(percent) 200 | -------------------------------------------------------------------------------- /capture_gui/plugins/timeplugin.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | import re 4 | 5 | import maya.OpenMaya as om 6 | from capture_gui.vendor.Qt import QtCore, QtWidgets 7 | 8 | import capture_gui.lib 9 | import capture_gui.plugin 10 | 11 | log = logging.getLogger("Time Range") 12 | 13 | 14 | def parse_frames(string): 15 | """Parse the resulting frames list from a frame list string. 16 | 17 | Examples 18 | >>> parse_frames("0-3;30") 19 | [0, 1, 2, 3, 30] 20 | >>> parse_frames("0,2,4,-10") 21 | [0, 2, 4, -10] 22 | >>> parse_frames("-10--5,-2") 23 | [-10, -9, -8, -7, -6, -5, -2] 24 | 25 | Args: 26 | string (str): The string to parse for frames. 27 | 28 | Returns: 29 | list: A list of frames 30 | 31 | """ 32 | 33 | result = list() 34 | if not string.strip(): 35 | raise ValueError("Can't parse an empty frame string.") 36 | 37 | if not re.match("^[-0-9,; ]*$", string): 38 | raise ValueError("Invalid symbols in frame string: {}".format(string)) 39 | 40 | for raw in re.split(";|,", string): 41 | 42 | # Skip empty elements 43 | value = raw.strip().replace(" ", "") 44 | if not value: 45 | continue 46 | 47 | # Check for sequences (1-20) including negatives (-10--8) 48 | sequence = re.search("(-?[0-9]+)-(-?[0-9]+)", value) 49 | 50 | # Sequence 51 | if sequence: 52 | start, end = sequence.groups() 53 | frames = range(int(start), int(end) + 1) 54 | result.extend(frames) 55 | 56 | # Single frame 57 | else: 58 | try: 59 | frame = int(value) 60 | except ValueError: 61 | raise ValueError("Invalid frame description: " 62 | "'{0}'".format(value)) 63 | 64 | result.append(frame) 65 | 66 | if not result: 67 | # This happens when only spaces are entered with a separator like `,` or `;` 68 | raise ValueError("Unable to parse any frames from string: {}".format(string)) 69 | 70 | return result 71 | 72 | 73 | class TimePlugin(capture_gui.plugin.Plugin): 74 | """Widget for time based options""" 75 | 76 | id = "Time Range" 77 | section = "app" 78 | order = 30 79 | 80 | RangeTimeSlider = "Time Slider" 81 | RangeStartEnd = "Start/End" 82 | CurrentFrame = "Current Frame" 83 | CustomFrames = "Custom Frames" 84 | 85 | def __init__(self, parent=None): 86 | super(TimePlugin, self).__init__(parent=parent) 87 | 88 | self._event_callbacks = list() 89 | 90 | self._layout = QtWidgets.QHBoxLayout() 91 | self._layout.setContentsMargins(5, 0, 5, 0) 92 | self.setLayout(self._layout) 93 | 94 | self.mode = QtWidgets.QComboBox() 95 | self.mode.addItems([self.RangeTimeSlider, 96 | self.RangeStartEnd, 97 | self.CurrentFrame, 98 | self.CustomFrames]) 99 | 100 | frame_input_height = 20 101 | self.start = QtWidgets.QSpinBox() 102 | self.start.setRange(-sys.maxint, sys.maxint) 103 | self.start.setFixedHeight(frame_input_height) 104 | self.end = QtWidgets.QSpinBox() 105 | self.end.setRange(-sys.maxint, sys.maxint) 106 | self.end.setFixedHeight(frame_input_height) 107 | 108 | # unique frames field 109 | self.custom_frames = QtWidgets.QLineEdit() 110 | self.custom_frames.setFixedHeight(frame_input_height) 111 | self.custom_frames.setPlaceholderText("Example: 1-20,25;50;75,100-150") 112 | self.custom_frames.setVisible(False) 113 | 114 | self._layout.addWidget(self.mode) 115 | self._layout.addWidget(self.start) 116 | self._layout.addWidget(self.end) 117 | self._layout.addWidget(self.custom_frames) 118 | 119 | # Connect callbacks to ensure start is never higher then end 120 | # and the end is never lower than start 121 | self.end.valueChanged.connect(self._ensure_start) 122 | self.start.valueChanged.connect(self._ensure_end) 123 | 124 | self.on_mode_changed() # force enabled state refresh 125 | 126 | self.mode.currentIndexChanged.connect(self.on_mode_changed) 127 | self.start.valueChanged.connect(self.on_mode_changed) 128 | self.end.valueChanged.connect(self.on_mode_changed) 129 | self.custom_frames.textChanged.connect(self.on_mode_changed) 130 | 131 | def _ensure_start(self, value): 132 | self.start.setValue(min(self.start.value(), value)) 133 | 134 | def _ensure_end(self, value): 135 | self.end.setValue(max(self.end.value(), value)) 136 | 137 | def on_mode_changed(self, emit=True): 138 | """Update the GUI when the user updated the time range or settings. 139 | 140 | Arguments: 141 | emit (bool): Whether to emit the options changed signal 142 | 143 | Returns: 144 | None 145 | 146 | """ 147 | 148 | mode = self.mode.currentText() 149 | if mode == self.RangeTimeSlider: 150 | start, end = capture_gui.lib.get_time_slider_range() 151 | self.start.setEnabled(False) 152 | self.end.setEnabled(False) 153 | self.start.setVisible(True) 154 | self.end.setVisible(True) 155 | self.custom_frames.setVisible(False) 156 | mode_values = int(start), int(end) 157 | elif mode == self.RangeStartEnd: 158 | self.start.setEnabled(True) 159 | self.end.setEnabled(True) 160 | self.start.setVisible(True) 161 | self.end.setVisible(True) 162 | self.custom_frames.setVisible(False) 163 | mode_values = self.start.value(), self.end.value() 164 | elif mode == self.CustomFrames: 165 | self.start.setVisible(False) 166 | self.end.setVisible(False) 167 | self.custom_frames.setVisible(True) 168 | mode_values = "({})".format(self.custom_frames.text()) 169 | 170 | # ensure validation state for custom frames 171 | self.validate() 172 | 173 | else: 174 | self.start.setEnabled(False) 175 | self.end.setEnabled(False) 176 | self.start.setVisible(True) 177 | self.end.setVisible(True) 178 | self.custom_frames.setVisible(False) 179 | currentframe = int(capture_gui.lib.get_current_frame()) 180 | mode_values = "({})".format(currentframe) 181 | 182 | # Update label 183 | self.label = "Time Range {}".format(mode_values) 184 | self.label_changed.emit(self.label) 185 | 186 | if emit: 187 | self.options_changed.emit() 188 | 189 | def validate(self): 190 | errors = [] 191 | 192 | if self.mode.currentText() == self.CustomFrames: 193 | 194 | # Reset 195 | self.custom_frames.setStyleSheet("") 196 | 197 | try: 198 | parse_frames(self.custom_frames.text()) 199 | except ValueError as exc: 200 | errors.append("{} : Invalid frame description: " 201 | "{}".format(self.id, exc)) 202 | self.custom_frames.setStyleSheet(self.highlight) 203 | 204 | return errors 205 | 206 | def get_outputs(self, panel=""): 207 | """Get the plugin outputs that matches `capture.capture` arguments 208 | 209 | Returns: 210 | dict: Plugin outputs 211 | 212 | """ 213 | 214 | mode = self.mode.currentText() 215 | frames = None 216 | 217 | if mode == self.RangeTimeSlider: 218 | start, end = capture_gui.lib.get_time_slider_range() 219 | 220 | elif mode == self.RangeStartEnd: 221 | start = self.start.value() 222 | end = self.end.value() 223 | 224 | elif mode == self.CurrentFrame: 225 | frame = capture_gui.lib.get_current_frame() 226 | start = frame 227 | end = frame 228 | 229 | elif mode == self.CustomFrames: 230 | frames = parse_frames(self.custom_frames.text()) 231 | start = None 232 | end = None 233 | else: 234 | raise NotImplementedError("Unsupported time range mode: " 235 | "{0}".format(mode)) 236 | 237 | return {"start_frame": start, 238 | "end_frame": end, 239 | "frame": frames} 240 | 241 | def get_inputs(self, as_preset): 242 | return {"time": self.mode.currentText(), 243 | "start_frame": self.start.value(), 244 | "end_frame": self.end.value(), 245 | "frame": self.custom_frames.text()} 246 | 247 | def apply_inputs(self, settings): 248 | # get values 249 | mode = self.mode.findText(settings.get("time", self.RangeTimeSlider)) 250 | startframe = settings.get("start_frame", 1) 251 | endframe = settings.get("end_frame", 120) 252 | custom_frames = settings.get("frame", None) 253 | 254 | # set values 255 | self.mode.setCurrentIndex(mode) 256 | self.start.setValue(int(startframe)) 257 | self.end.setValue(int(endframe)) 258 | if custom_frames is not None: 259 | self.custom_frames.setText(custom_frames) 260 | 261 | def initialize(self): 262 | self._register_callbacks() 263 | 264 | def uninitialize(self): 265 | self._remove_callbacks() 266 | 267 | def _register_callbacks(self): 268 | """Register maya time and playback range change callbacks. 269 | 270 | Register callbacks to ensure Capture GUI reacts to changes in 271 | the Maya GUI in regards to time slider and current frame 272 | 273 | """ 274 | 275 | callback = lambda x: self.on_mode_changed(emit=False) 276 | 277 | # this avoid overriding the ids on re-run 278 | currentframe = om.MEventMessage.addEventCallback("timeChanged", 279 | callback) 280 | timerange = om.MEventMessage.addEventCallback("playbackRangeChanged", 281 | callback) 282 | 283 | self._event_callbacks.append(currentframe) 284 | self._event_callbacks.append(timerange) 285 | 286 | def _remove_callbacks(self): 287 | """Remove callbacks when closing widget""" 288 | for callback in self._event_callbacks: 289 | try: 290 | om.MEventMessage.removeCallback(callback) 291 | except RuntimeError, error: 292 | log.error("Encounter error : {}".format(error)) 293 | -------------------------------------------------------------------------------- /capture_gui/plugins/viewportplugin.py: -------------------------------------------------------------------------------- 1 | from capture_gui.vendor.Qt import QtCore, QtWidgets 2 | import capture_gui.plugin 3 | import capture_gui.lib as lib 4 | import capture 5 | 6 | 7 | class ViewportPlugin(capture_gui.plugin.Plugin): 8 | """Plugin to apply viewport visibilities and settings""" 9 | 10 | id = "Viewport Options" 11 | label = "Viewport Options" 12 | section = "config" 13 | order = 70 14 | 15 | def __init__(self, parent=None): 16 | super(ViewportPlugin, self).__init__(parent=parent) 17 | 18 | # set inherited attributes 19 | self.setObjectName(self.label) 20 | 21 | # custom atttributes 22 | self.show_type_actions = list() 23 | 24 | # get information 25 | self.show_types = lib.get_show_object_types() 26 | 27 | # set main layout for widget 28 | self._layout = QtWidgets.QVBoxLayout() 29 | self._layout.setContentsMargins(0, 0, 0, 0) 30 | self.setLayout(self._layout) 31 | 32 | # build 33 | # region Menus 34 | menus_vlayout = QtWidgets.QHBoxLayout() 35 | 36 | # Display Lights 37 | self.display_light_menu = self._build_light_menu() 38 | self.display_light_menu.setFixedHeight(20) 39 | 40 | # Show 41 | self.show_types_button = QtWidgets.QPushButton("Show") 42 | self.show_types_button.setFixedHeight(20) 43 | self.show_types_menu = self._build_show_menu() 44 | self.show_types_button.setMenu(self.show_types_menu) 45 | 46 | # fill layout 47 | menus_vlayout.addWidget(self.display_light_menu) 48 | menus_vlayout.addWidget(self.show_types_button) 49 | 50 | # endregion Menus 51 | 52 | # region Checkboxes 53 | checkbox_layout = QtWidgets.QGridLayout() 54 | self.high_quality = QtWidgets.QCheckBox() 55 | self.high_quality.setText("Force Viewport 2.0 + AA") 56 | self.override_viewport = QtWidgets.QCheckBox("Override viewport " 57 | "settings") 58 | self.override_viewport.setChecked(True) 59 | 60 | # two sided lighting 61 | self.two_sided_ligthing = QtWidgets.QCheckBox("Two Sided Ligthing") 62 | self.two_sided_ligthing.setChecked(False) 63 | 64 | # show 65 | self.shadows = QtWidgets.QCheckBox("Shadows") 66 | self.shadows.setChecked(False) 67 | 68 | checkbox_layout.addWidget(self.override_viewport, 0, 0) 69 | checkbox_layout.addWidget(self.high_quality, 0, 1) 70 | checkbox_layout.addWidget(self.two_sided_ligthing, 1, 0) 71 | checkbox_layout.addWidget(self.shadows, 1, 1) 72 | # endregion Checkboxes 73 | 74 | self._layout.addLayout(checkbox_layout) 75 | self._layout.addLayout(menus_vlayout) 76 | 77 | self.connections() 78 | 79 | def connections(self): 80 | 81 | self.high_quality.stateChanged.connect(self.options_changed) 82 | self.override_viewport.stateChanged.connect(self.options_changed) 83 | self.override_viewport.stateChanged.connect(self.on_toggle_override) 84 | 85 | self.two_sided_ligthing.stateChanged.connect(self.options_changed) 86 | self.shadows.stateChanged.connect(self.options_changed) 87 | 88 | self.display_light_menu.currentIndexChanged.connect( 89 | self.options_changed 90 | ) 91 | 92 | def _build_show_menu(self): 93 | """Build the menu to select which object types are shown in the output. 94 | 95 | Returns: 96 | QtGui.QMenu: The visibilities "show" menu. 97 | 98 | """ 99 | 100 | menu = QtWidgets.QMenu(self) 101 | menu.setObjectName("ShowShapesMenu") 102 | menu.setWindowTitle("Show") 103 | menu.setFixedWidth(180) 104 | menu.setTearOffEnabled(True) 105 | 106 | # Show all check 107 | toggle_all = QtWidgets.QAction(menu, text="All") 108 | toggle_none = QtWidgets.QAction(menu, text="None") 109 | menu.addAction(toggle_all) 110 | menu.addAction(toggle_none) 111 | menu.addSeparator() 112 | 113 | # add plugin shapes if any 114 | for shape in self.show_types: 115 | action = QtWidgets.QAction(menu, text=shape) 116 | action.setCheckable(True) 117 | # emit signal when the state is changed of the checkbox 118 | action.toggled.connect(self.options_changed) 119 | menu.addAction(action) 120 | self.show_type_actions.append(action) 121 | 122 | # connect signals 123 | toggle_all.triggered.connect(self.toggle_all_visbile) 124 | toggle_none.triggered.connect(self.toggle_all_hide) 125 | 126 | return menu 127 | 128 | def _build_light_menu(self): 129 | """Build lighting menu. 130 | 131 | Create the menu items for the different types of lighting for 132 | in the viewport 133 | 134 | Returns: 135 | None 136 | 137 | """ 138 | 139 | menu = QtWidgets.QComboBox(self) 140 | 141 | # names cane be found in 142 | display_lights = (("Use Default Lighting", "default"), 143 | ("Use All Lights", "all"), 144 | ("Use Selected Lights", "active"), 145 | ("Use Flat Lighting", "flat"), 146 | ("Use No Lights", "none")) 147 | 148 | for label, name in display_lights: 149 | menu.addItem(label, userData=name) 150 | 151 | return menu 152 | 153 | def on_toggle_override(self): 154 | """Enable or disable show menu when override is checked""" 155 | state = self.override_viewport.isChecked() 156 | self.show_types_button.setEnabled(state) 157 | self.high_quality.setEnabled(state) 158 | self.display_light_menu.setEnabled(state) 159 | self.shadows.setEnabled(state) 160 | self.two_sided_ligthing.setEnabled(state) 161 | 162 | def toggle_all_visbile(self): 163 | """Set all object types off or on depending on the state""" 164 | for action in self.show_type_actions: 165 | action.setChecked(True) 166 | 167 | def toggle_all_hide(self): 168 | """Set all object types off or on depending on the state""" 169 | for action in self.show_type_actions: 170 | action.setChecked(False) 171 | 172 | def get_show_inputs(self): 173 | """Return checked state of show menu items 174 | 175 | Returns: 176 | dict: The checked show states in the widget. 177 | 178 | """ 179 | 180 | show_inputs = {} 181 | # get all checked objects 182 | for action in self.show_type_actions: 183 | label = action.text() 184 | name = self.show_types.get(label, None) 185 | if name is None: 186 | continue 187 | show_inputs[name] = action.isChecked() 188 | 189 | return show_inputs 190 | 191 | def get_displaylights(self): 192 | """Get and parse the currently selected displayLights options. 193 | 194 | Returns: 195 | dict: The display light options 196 | 197 | """ 198 | indx = self.display_light_menu.currentIndex() 199 | return {"displayLights": self.display_light_menu.itemData(indx), 200 | "shadows": self.shadows.isChecked(), 201 | "twoSidedLighting": self.two_sided_ligthing.isChecked()} 202 | 203 | def get_inputs(self, as_preset): 204 | """Return the widget options 205 | 206 | Returns: 207 | dict: The input settings of the widgets. 208 | 209 | """ 210 | inputs = {"high_quality": self.high_quality.isChecked(), 211 | "override_viewport_options": self.override_viewport.isChecked(), 212 | "displayLights": self.display_light_menu.currentIndex(), 213 | "shadows": self.shadows.isChecked(), 214 | "twoSidedLighting": self.two_sided_ligthing.isChecked()} 215 | 216 | inputs.update(self.get_show_inputs()) 217 | 218 | return inputs 219 | 220 | def apply_inputs(self, inputs): 221 | """Apply the saved inputs from the inputs configuration 222 | 223 | Arguments: 224 | settings (dict): The input settings to apply. 225 | 226 | """ 227 | 228 | # get input values directly from input given 229 | override_viewport = inputs.get("override_viewport_options", True) 230 | high_quality = inputs.get("high_quality", True) 231 | displaylight = inputs.get("displayLights", 0) # default lighting 232 | two_sided_ligthing = inputs.get("twoSidedLighting", False) 233 | shadows = inputs.get("shadows", False) 234 | 235 | self.high_quality.setChecked(high_quality) 236 | self.override_viewport.setChecked(override_viewport) 237 | self.show_types_button.setEnabled(override_viewport) 238 | 239 | # display light menu 240 | self.display_light_menu.setCurrentIndex(displaylight) 241 | self.shadows.setChecked(shadows) 242 | self.two_sided_ligthing.setChecked(two_sided_ligthing) 243 | 244 | for action in self.show_type_actions: 245 | system_name = self.show_types[action.text()] 246 | state = inputs.get(system_name, True) 247 | action.setChecked(state) 248 | 249 | def get_outputs(self): 250 | """Get the plugin outputs that matches `capture.capture` arguments 251 | 252 | Returns: 253 | dict: Plugin outputs 254 | 255 | """ 256 | outputs = dict() 257 | 258 | high_quality = self.high_quality.isChecked() 259 | override_viewport_options = self.override_viewport.isChecked() 260 | 261 | if override_viewport_options: 262 | outputs['viewport2_options'] = dict() 263 | outputs['viewport_options'] = dict() 264 | 265 | if high_quality: 266 | # force viewport 2.0 and AA 267 | outputs['viewport_options']['rendererName'] = 'vp2Renderer' 268 | outputs['viewport2_options']['multiSampleEnable'] = True 269 | outputs['viewport2_options']['multiSampleCount'] = 8 270 | 271 | show_per_type = self.get_show_inputs() 272 | display_lights = self.get_displaylights() 273 | outputs['viewport_options'].update(show_per_type) 274 | outputs['viewport_options'].update(display_lights) 275 | else: 276 | # TODO: When this fails we should give the user a warning 277 | # Use settings from the active viewport 278 | outputs = capture.parse_active_view() 279 | 280 | # Remove the display options and camera attributes 281 | outputs.pop("display_options", None) 282 | outputs.pop("camera", None) 283 | 284 | # Remove the current renderer because there's already 285 | # renderer plug-in handling that 286 | outputs["viewport_options"].pop("rendererName", None) 287 | 288 | # Remove all camera options except depth of field 289 | dof = outputs["camera_options"]["depthOfField"] 290 | outputs["camera_options"] = {"depthOfField": dof} 291 | 292 | return outputs 293 | -------------------------------------------------------------------------------- /capture_gui/presets.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | import logging 4 | 5 | _registered_paths = [] 6 | log = logging.getLogger("Presets") 7 | 8 | 9 | def discover(paths=None): 10 | """Get the full list of files found in the registered folders 11 | 12 | Args: 13 | paths (list, Optional): directories which host preset files or None. 14 | When None (default) it will list from the registered preset paths. 15 | 16 | Returns: 17 | list: valid .json preset file paths. 18 | 19 | """ 20 | 21 | presets = [] 22 | for path in paths or preset_paths(): 23 | path = os.path.normpath(path) 24 | if not os.path.isdir(path): 25 | continue 26 | 27 | # check for json files 28 | glob_query = os.path.abspath(os.path.join(path, "*.json")) 29 | filenames = glob.glob(glob_query) 30 | for filename in filenames: 31 | # skip private files 32 | if filename.startswith("_"): 33 | continue 34 | 35 | # check for file size 36 | if not check_file_size(filename): 37 | log.warning("Filesize is smaller than 1 byte for file '%s'", 38 | filename) 39 | continue 40 | 41 | if filename not in presets: 42 | presets.append(filename) 43 | 44 | return presets 45 | 46 | 47 | def check_file_size(filepath): 48 | """Check if filesize of the given file is bigger than 1.0 byte 49 | 50 | Args: 51 | filepath (str): full filepath of the file to check 52 | 53 | Returns: 54 | bool: Whether bigger than 1 byte. 55 | 56 | """ 57 | 58 | file_stats = os.stat(filepath) 59 | if file_stats.st_size < 1: 60 | return False 61 | return True 62 | 63 | 64 | def preset_paths(): 65 | """Return existing registered preset paths 66 | 67 | Returns: 68 | list: List of full paths. 69 | 70 | """ 71 | 72 | paths = list() 73 | for path in _registered_paths: 74 | # filter duplicates 75 | if path in paths: 76 | continue 77 | 78 | if not os.path.exists(path): 79 | continue 80 | 81 | paths.append(path) 82 | 83 | return paths 84 | 85 | 86 | def register_preset_path(path): 87 | """Add filepath to registered presets 88 | 89 | :param path: the directory of the preset file(s) 90 | :type path: str 91 | 92 | :return: 93 | """ 94 | if path in _registered_paths: 95 | return log.warning("Path already registered: %s", path) 96 | 97 | _registered_paths.append(path) 98 | 99 | return path 100 | 101 | 102 | # Register default user folder 103 | user_folder = os.path.expanduser("~") 104 | capture_gui_presets = os.path.join(user_folder, "CaptureGUI", "presets") 105 | register_preset_path(capture_gui_presets) 106 | -------------------------------------------------------------------------------- /capture_gui/resources/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigRoy/maya-capture-gui/c02c2385c485db9666b50f00582a60b784338f38/capture_gui/resources/config.png -------------------------------------------------------------------------------- /capture_gui/resources/import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigRoy/maya-capture-gui/c02c2385c485db9666b50f00582a60b784338f38/capture_gui/resources/import.png -------------------------------------------------------------------------------- /capture_gui/resources/reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigRoy/maya-capture-gui/c02c2385c485db9666b50f00582a60b784338f38/capture_gui/resources/reset.png -------------------------------------------------------------------------------- /capture_gui/resources/save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigRoy/maya-capture-gui/c02c2385c485db9666b50f00582a60b784338f38/capture_gui/resources/save.png -------------------------------------------------------------------------------- /capture_gui/tokens.py: -------------------------------------------------------------------------------- 1 | """Token system 2 | 3 | The capture gui application will format tokens in the filename. 4 | The tokens can be registered using `register_token` 5 | 6 | """ 7 | from . import lib 8 | 9 | _registered_tokens = dict() 10 | 11 | 12 | def format_tokens(string, options): 13 | """Replace the tokens with the correlated strings 14 | 15 | Arguments: 16 | string (str): filename of the playblast with tokens. 17 | options (dict): The parsed capture options. 18 | 19 | Returns: 20 | str: The formatted filename with all tokens resolved 21 | 22 | """ 23 | 24 | if not string: 25 | return string 26 | 27 | for token, value in _registered_tokens.items(): 28 | if token in string: 29 | func = value['func'] 30 | string = string.replace(token, func(options)) 31 | 32 | return string 33 | 34 | 35 | def register_token(token, func, label=""): 36 | assert token.startswith("<") and token.endswith(">") 37 | assert callable(func) 38 | _registered_tokens[token] = {"func": func, "label": label} 39 | 40 | 41 | def list_tokens(): 42 | return _registered_tokens.copy() 43 | 44 | 45 | # register default tokens 46 | # scene based tokens 47 | def _camera_token(options): 48 | """Return short name of camera from capture options""" 49 | camera = options['camera'] 50 | camera = camera.rsplit("|", 1)[-1] # use short name 51 | camera = camera.replace(":", "_") # namespace `:` to `_` 52 | return camera 53 | 54 | 55 | register_token("", _camera_token, 56 | label="Insert camera name") 57 | register_token("", lambda options: lib.get_current_scenename() or "playblast", 58 | label="Insert current scene name") 59 | register_token("", lambda options: lib.get_current_renderlayer(), 60 | label="Insert active render layer name") 61 | 62 | # project based tokens 63 | register_token("", 64 | lambda options: lib.get_project_rule("images"), 65 | label="Insert image directory of set project") 66 | register_token("", 67 | lambda options: lib.get_project_rule("movie"), 68 | label="Insert movies directory of set project") 69 | -------------------------------------------------------------------------------- /capture_gui/vendor/Qt.py: -------------------------------------------------------------------------------- 1 | """The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2017 Marcus Ottosson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | Documentation 24 | 25 | Map all bindings to PySide2 26 | 27 | Project goals: 28 | Qt.py was born in the film and visual effects industry to address 29 | the growing need for the development of software capable of running 30 | with more than one flavour of the Qt bindings for Python - PySide, 31 | PySide2, PyQt4 and PyQt5. 32 | 33 | 1. Build for one, run with all 34 | 2. Explicit is better than implicit 35 | 3. Support co-existence 36 | 37 | Default resolution order: 38 | - PySide2 39 | - PyQt5 40 | - PySide 41 | - PyQt4 42 | 43 | Usage: 44 | >> import sys 45 | >> from Qt import QtWidgets 46 | >> app = QtWidgets.QApplication(sys.argv) 47 | >> button = QtWidgets.QPushButton("Hello World") 48 | >> button.show() 49 | >> app.exec_() 50 | 51 | All members of PySide2 are mapped from other bindings, should they exist. 52 | If no equivalent member exist, it is excluded from Qt.py and inaccessible. 53 | The idea is to highlight members that exist across all supported binding, 54 | and guarantee that code that runs on one binding runs on all others. 55 | 56 | For more details, visit https://github.com/mottosso/Qt.py 57 | 58 | """ 59 | 60 | import os 61 | import sys 62 | import types 63 | import shutil 64 | import importlib 65 | 66 | __version__ = "1.0.0.b3" 67 | 68 | # Enable support for `from Qt import *` 69 | __all__ = [] 70 | 71 | # Flags from environment variables 72 | QT_VERBOSE = bool(os.getenv("QT_VERBOSE")) 73 | QT_PREFERRED_BINDING = os.getenv("QT_PREFERRED_BINDING", "") 74 | QT_SIP_API_HINT = os.getenv("QT_SIP_API_HINT") 75 | 76 | # Reference to Qt.py 77 | Qt = sys.modules[__name__] 78 | Qt.QtCompat = types.ModuleType("QtCompat") 79 | 80 | """Common members of all bindings 81 | 82 | This is where each member of Qt.py is explicitly defined. 83 | It is based on a "lowest commond denominator" of all bindings; 84 | including members found in each of the 4 bindings. 85 | 86 | Find or add excluded members in build_membership.py 87 | 88 | """ 89 | 90 | _common_members = { 91 | "QtGui": [ 92 | "QAbstractTextDocumentLayout", 93 | "QActionEvent", 94 | "QBitmap", 95 | "QBrush", 96 | "QClipboard", 97 | "QCloseEvent", 98 | "QColor", 99 | "QConicalGradient", 100 | "QContextMenuEvent", 101 | "QCursor", 102 | "QDoubleValidator", 103 | "QDrag", 104 | "QDragEnterEvent", 105 | "QDragLeaveEvent", 106 | "QDragMoveEvent", 107 | "QDropEvent", 108 | "QFileOpenEvent", 109 | "QFocusEvent", 110 | "QFont", 111 | "QFontDatabase", 112 | "QFontInfo", 113 | "QFontMetrics", 114 | "QFontMetricsF", 115 | "QGradient", 116 | "QHelpEvent", 117 | "QHideEvent", 118 | "QHoverEvent", 119 | "QIcon", 120 | "QIconDragEvent", 121 | "QIconEngine", 122 | "QImage", 123 | "QImageIOHandler", 124 | "QImageReader", 125 | "QImageWriter", 126 | "QInputEvent", 127 | "QInputMethodEvent", 128 | "QIntValidator", 129 | "QKeyEvent", 130 | "QKeySequence", 131 | "QLinearGradient", 132 | "QMatrix2x2", 133 | "QMatrix2x3", 134 | "QMatrix2x4", 135 | "QMatrix3x2", 136 | "QMatrix3x3", 137 | "QMatrix3x4", 138 | "QMatrix4x2", 139 | "QMatrix4x3", 140 | "QMatrix4x4", 141 | "QMouseEvent", 142 | "QMoveEvent", 143 | "QMovie", 144 | "QPaintDevice", 145 | "QPaintEngine", 146 | "QPaintEngineState", 147 | "QPaintEvent", 148 | "QPainter", 149 | "QPainterPath", 150 | "QPainterPathStroker", 151 | "QPalette", 152 | "QPen", 153 | "QPicture", 154 | "QPictureIO", 155 | "QPixmap", 156 | "QPixmapCache", 157 | "QPolygon", 158 | "QPolygonF", 159 | "QQuaternion", 160 | "QRadialGradient", 161 | "QRegExpValidator", 162 | "QRegion", 163 | "QResizeEvent", 164 | "QSessionManager", 165 | "QShortcutEvent", 166 | "QShowEvent", 167 | "QStandardItem", 168 | "QStandardItemModel", 169 | "QStatusTipEvent", 170 | "QSyntaxHighlighter", 171 | "QTabletEvent", 172 | "QTextBlock", 173 | "QTextBlockFormat", 174 | "QTextBlockGroup", 175 | "QTextBlockUserData", 176 | "QTextCharFormat", 177 | "QTextCursor", 178 | "QTextDocument", 179 | "QTextDocumentFragment", 180 | "QTextFormat", 181 | "QTextFragment", 182 | "QTextFrame", 183 | "QTextFrameFormat", 184 | "QTextImageFormat", 185 | "QTextInlineObject", 186 | "QTextItem", 187 | "QTextLayout", 188 | "QTextLength", 189 | "QTextLine", 190 | "QTextList", 191 | "QTextListFormat", 192 | "QTextObject", 193 | "QTextObjectInterface", 194 | "QTextOption", 195 | "QTextTable", 196 | "QTextTableCell", 197 | "QTextTableCellFormat", 198 | "QTextTableFormat", 199 | "QTransform", 200 | "QValidator", 201 | "QVector2D", 202 | "QVector3D", 203 | "QVector4D", 204 | "QWhatsThisClickedEvent", 205 | "QWheelEvent", 206 | "QWindowStateChangeEvent", 207 | "qAlpha", 208 | "qBlue", 209 | "qGray", 210 | "qGreen", 211 | "qIsGray", 212 | "qRed", 213 | "qRgb", 214 | "qRgb", 215 | ], 216 | "QtWidgets": [ 217 | "QAbstractButton", 218 | "QAbstractGraphicsShapeItem", 219 | "QAbstractItemDelegate", 220 | "QAbstractItemView", 221 | "QAbstractScrollArea", 222 | "QAbstractSlider", 223 | "QAbstractSpinBox", 224 | "QAction", 225 | "QActionGroup", 226 | "QApplication", 227 | "QBoxLayout", 228 | "QButtonGroup", 229 | "QCalendarWidget", 230 | "QCheckBox", 231 | "QColorDialog", 232 | "QColumnView", 233 | "QComboBox", 234 | "QCommandLinkButton", 235 | "QCommonStyle", 236 | "QCompleter", 237 | "QDataWidgetMapper", 238 | "QDateEdit", 239 | "QDateTimeEdit", 240 | "QDesktopWidget", 241 | "QDial", 242 | "QDialog", 243 | "QDialogButtonBox", 244 | "QDirModel", 245 | "QDockWidget", 246 | "QDoubleSpinBox", 247 | "QErrorMessage", 248 | "QFileDialog", 249 | "QFileIconProvider", 250 | "QFileSystemModel", 251 | "QFocusFrame", 252 | "QFontComboBox", 253 | "QFontDialog", 254 | "QFormLayout", 255 | "QFrame", 256 | "QGesture", 257 | "QGestureEvent", 258 | "QGestureRecognizer", 259 | "QGraphicsAnchor", 260 | "QGraphicsAnchorLayout", 261 | "QGraphicsBlurEffect", 262 | "QGraphicsColorizeEffect", 263 | "QGraphicsDropShadowEffect", 264 | "QGraphicsEffect", 265 | "QGraphicsEllipseItem", 266 | "QGraphicsGridLayout", 267 | "QGraphicsItem", 268 | "QGraphicsItemGroup", 269 | "QGraphicsLayout", 270 | "QGraphicsLayoutItem", 271 | "QGraphicsLineItem", 272 | "QGraphicsLinearLayout", 273 | "QGraphicsObject", 274 | "QGraphicsOpacityEffect", 275 | "QGraphicsPathItem", 276 | "QGraphicsPixmapItem", 277 | "QGraphicsPolygonItem", 278 | "QGraphicsProxyWidget", 279 | "QGraphicsRectItem", 280 | "QGraphicsRotation", 281 | "QGraphicsScale", 282 | "QGraphicsScene", 283 | "QGraphicsSceneContextMenuEvent", 284 | "QGraphicsSceneDragDropEvent", 285 | "QGraphicsSceneEvent", 286 | "QGraphicsSceneHelpEvent", 287 | "QGraphicsSceneHoverEvent", 288 | "QGraphicsSceneMouseEvent", 289 | "QGraphicsSceneMoveEvent", 290 | "QGraphicsSceneResizeEvent", 291 | "QGraphicsSceneWheelEvent", 292 | "QGraphicsSimpleTextItem", 293 | "QGraphicsTextItem", 294 | "QGraphicsTransform", 295 | "QGraphicsView", 296 | "QGraphicsWidget", 297 | "QGridLayout", 298 | "QGroupBox", 299 | "QHBoxLayout", 300 | "QHeaderView", 301 | "QInputDialog", 302 | "QItemDelegate", 303 | "QItemEditorCreatorBase", 304 | "QItemEditorFactory", 305 | "QKeyEventTransition", 306 | "QLCDNumber", 307 | "QLabel", 308 | "QLayout", 309 | "QLayoutItem", 310 | "QLineEdit", 311 | "QListView", 312 | "QListWidget", 313 | "QListWidgetItem", 314 | "QMainWindow", 315 | "QMdiArea", 316 | "QMdiSubWindow", 317 | "QMenu", 318 | "QMenuBar", 319 | "QMessageBox", 320 | "QMouseEventTransition", 321 | "QPanGesture", 322 | "QPinchGesture", 323 | "QPlainTextDocumentLayout", 324 | "QPlainTextEdit", 325 | "QProgressBar", 326 | "QProgressDialog", 327 | "QPushButton", 328 | "QRadioButton", 329 | "QRubberBand", 330 | "QScrollArea", 331 | "QScrollBar", 332 | "QShortcut", 333 | "QSizeGrip", 334 | "QSizePolicy", 335 | "QSlider", 336 | "QSpacerItem", 337 | "QSpinBox", 338 | "QSplashScreen", 339 | "QSplitter", 340 | "QSplitterHandle", 341 | "QStackedLayout", 342 | "QStackedWidget", 343 | "QStatusBar", 344 | "QStyle", 345 | "QStyleFactory", 346 | "QStyleHintReturn", 347 | "QStyleHintReturnMask", 348 | "QStyleHintReturnVariant", 349 | "QStyleOption", 350 | "QStyleOptionButton", 351 | "QStyleOptionComboBox", 352 | "QStyleOptionComplex", 353 | "QStyleOptionDockWidget", 354 | "QStyleOptionFocusRect", 355 | "QStyleOptionFrame", 356 | "QStyleOptionGraphicsItem", 357 | "QStyleOptionGroupBox", 358 | "QStyleOptionHeader", 359 | "QStyleOptionMenuItem", 360 | "QStyleOptionProgressBar", 361 | "QStyleOptionRubberBand", 362 | "QStyleOptionSizeGrip", 363 | "QStyleOptionSlider", 364 | "QStyleOptionSpinBox", 365 | "QStyleOptionTab", 366 | "QStyleOptionTabBarBase", 367 | "QStyleOptionTabWidgetFrame", 368 | "QStyleOptionTitleBar", 369 | "QStyleOptionToolBar", 370 | "QStyleOptionToolBox", 371 | "QStyleOptionToolButton", 372 | "QStyleOptionViewItem", 373 | "QStylePainter", 374 | "QStyledItemDelegate", 375 | "QSwipeGesture", 376 | "QSystemTrayIcon", 377 | "QTabBar", 378 | "QTabWidget", 379 | "QTableView", 380 | "QTableWidget", 381 | "QTableWidgetItem", 382 | "QTableWidgetSelectionRange", 383 | "QTapAndHoldGesture", 384 | "QTapGesture", 385 | "QTextBrowser", 386 | "QTextEdit", 387 | "QTimeEdit", 388 | "QToolBar", 389 | "QToolBox", 390 | "QToolButton", 391 | "QToolTip", 392 | "QTreeView", 393 | "QTreeWidget", 394 | "QTreeWidgetItem", 395 | "QTreeWidgetItemIterator", 396 | "QUndoCommand", 397 | "QUndoGroup", 398 | "QUndoStack", 399 | "QUndoView", 400 | "QVBoxLayout", 401 | "QWhatsThis", 402 | "QWidget", 403 | "QWidgetAction", 404 | "QWidgetItem", 405 | "QWizard", 406 | "QWizardPage", 407 | ], 408 | "QtCore": [ 409 | "QAbstractAnimation", 410 | "QAbstractEventDispatcher", 411 | "QAbstractItemModel", 412 | "QAbstractListModel", 413 | "QAbstractState", 414 | "QAbstractTableModel", 415 | "QAbstractTransition", 416 | "QAnimationGroup", 417 | "QBasicTimer", 418 | "QBitArray", 419 | "QBuffer", 420 | "QByteArray", 421 | "QByteArrayMatcher", 422 | "QChildEvent", 423 | "QCoreApplication", 424 | "QCryptographicHash", 425 | "QDataStream", 426 | "QDate", 427 | "QDateTime", 428 | "QDir", 429 | "QDirIterator", 430 | "QDynamicPropertyChangeEvent", 431 | "QEasingCurve", 432 | "QElapsedTimer", 433 | "QEvent", 434 | "QEventLoop", 435 | "QEventTransition", 436 | "QFile", 437 | "QFileInfo", 438 | "QFileSystemWatcher", 439 | "QFinalState", 440 | "QGenericArgument", 441 | "QGenericReturnArgument", 442 | "QHistoryState", 443 | "QIODevice", 444 | "QLibraryInfo", 445 | "QLine", 446 | "QLineF", 447 | "QLocale", 448 | "QMargins", 449 | "QMetaClassInfo", 450 | "QMetaEnum", 451 | "QMetaMethod", 452 | "QMetaObject", 453 | "QMetaProperty", 454 | "QMimeData", 455 | "QModelIndex", 456 | "QMutex", 457 | "QMutexLocker", 458 | "QObject", 459 | "QParallelAnimationGroup", 460 | "QPauseAnimation", 461 | "QPersistentModelIndex", 462 | "QPluginLoader", 463 | "QPoint", 464 | "QPointF", 465 | "QProcess", 466 | "QProcessEnvironment", 467 | "QPropertyAnimation", 468 | "QReadLocker", 469 | "QReadWriteLock", 470 | "QRect", 471 | "QRectF", 472 | "QRegExp", 473 | "QResource", 474 | "QRunnable", 475 | "QSemaphore", 476 | "QSequentialAnimationGroup", 477 | "QSettings", 478 | "QSignalMapper", 479 | "QSignalTransition", 480 | "QSize", 481 | "QSizeF", 482 | "QSocketNotifier", 483 | "QState", 484 | "QStateMachine", 485 | "QSysInfo", 486 | "QSystemSemaphore", 487 | "QTemporaryFile", 488 | "QTextBoundaryFinder", 489 | "QTextCodec", 490 | "QTextDecoder", 491 | "QTextEncoder", 492 | "QTextStream", 493 | "QTextStreamManipulator", 494 | "QThread", 495 | "QThreadPool", 496 | "QTime", 497 | "QTimeLine", 498 | "QTimer", 499 | "QTimerEvent", 500 | "QTranslator", 501 | "QUrl", 502 | "QVariantAnimation", 503 | "QWaitCondition", 504 | "QWriteLocker", 505 | "QXmlStreamAttribute", 506 | "QXmlStreamAttributes", 507 | "QXmlStreamEntityDeclaration", 508 | "QXmlStreamEntityResolver", 509 | "QXmlStreamNamespaceDeclaration", 510 | "QXmlStreamNotationDeclaration", 511 | "QXmlStreamReader", 512 | "QXmlStreamWriter", 513 | "Qt", 514 | "QtCriticalMsg", 515 | "QtDebugMsg", 516 | "QtFatalMsg", 517 | "QtMsgType", 518 | "QtSystemMsg", 519 | "QtWarningMsg", 520 | "qAbs", 521 | "qAddPostRoutine", 522 | "qChecksum", 523 | "qCritical", 524 | "qDebug", 525 | "qFatal", 526 | "qFuzzyCompare", 527 | "qIsFinite", 528 | "qIsInf", 529 | "qIsNaN", 530 | "qIsNull", 531 | "qRegisterResourceData", 532 | "qUnregisterResourceData", 533 | "qVersion", 534 | "qWarning", 535 | "qrand", 536 | "qsrand", 537 | ], 538 | "QtXml": [ 539 | "QDomAttr", 540 | "QDomCDATASection", 541 | "QDomCharacterData", 542 | "QDomComment", 543 | "QDomDocument", 544 | "QDomDocumentFragment", 545 | "QDomDocumentType", 546 | "QDomElement", 547 | "QDomEntity", 548 | "QDomEntityReference", 549 | "QDomImplementation", 550 | "QDomNamedNodeMap", 551 | "QDomNode", 552 | "QDomNodeList", 553 | "QDomNotation", 554 | "QDomProcessingInstruction", 555 | "QDomText", 556 | "QXmlAttributes", 557 | "QXmlContentHandler", 558 | "QXmlDTDHandler", 559 | "QXmlDeclHandler", 560 | "QXmlDefaultHandler", 561 | "QXmlEntityResolver", 562 | "QXmlErrorHandler", 563 | "QXmlInputSource", 564 | "QXmlLexicalHandler", 565 | "QXmlLocator", 566 | "QXmlNamespaceSupport", 567 | "QXmlParseException", 568 | "QXmlReader", 569 | "QXmlSimpleReader" 570 | ], 571 | "QtHelp": [ 572 | "QHelpContentItem", 573 | "QHelpContentModel", 574 | "QHelpContentWidget", 575 | "QHelpEngine", 576 | "QHelpEngineCore", 577 | "QHelpIndexModel", 578 | "QHelpIndexWidget", 579 | "QHelpSearchEngine", 580 | "QHelpSearchQuery", 581 | "QHelpSearchQueryWidget", 582 | "QHelpSearchResultWidget" 583 | ], 584 | "QtNetwork": [ 585 | "QAbstractNetworkCache", 586 | "QAbstractSocket", 587 | "QAuthenticator", 588 | "QHostAddress", 589 | "QHostInfo", 590 | "QLocalServer", 591 | "QLocalSocket", 592 | "QNetworkAccessManager", 593 | "QNetworkAddressEntry", 594 | "QNetworkCacheMetaData", 595 | "QNetworkConfiguration", 596 | "QNetworkConfigurationManager", 597 | "QNetworkCookie", 598 | "QNetworkCookieJar", 599 | "QNetworkDiskCache", 600 | "QNetworkInterface", 601 | "QNetworkProxy", 602 | "QNetworkProxyFactory", 603 | "QNetworkProxyQuery", 604 | "QNetworkReply", 605 | "QNetworkRequest", 606 | "QNetworkSession", 607 | "QSsl", 608 | "QTcpServer", 609 | "QTcpSocket", 610 | "QUdpSocket" 611 | ], 612 | "QtOpenGL": [ 613 | "QGL", 614 | "QGLContext", 615 | "QGLFormat", 616 | "QGLWidget" 617 | ] 618 | } 619 | 620 | 621 | def _new_module(name): 622 | return types.ModuleType(__name__ + "." + name) 623 | 624 | 625 | def _setup(module, extras): 626 | """Install common submodules""" 627 | 628 | Qt.__binding__ = module.__name__ 629 | 630 | for name in list(_common_members) + extras: 631 | try: 632 | # print("Trying %s" % name) 633 | submodule = importlib.import_module( 634 | module.__name__ + "." + name) 635 | except ImportError: 636 | # print("Failed %s" % name) 637 | continue 638 | 639 | setattr(Qt, "_" + name, submodule) 640 | 641 | if name not in extras: 642 | # Store reference to original binding, 643 | # but don't store speciality modules 644 | # such as uic or QtUiTools 645 | setattr(Qt, name, _new_module(name)) 646 | 647 | 648 | def _pyside2(): 649 | """Initialise PySide2 650 | 651 | These functions serve to test the existence of a binding 652 | along with set it up in such a way that it aligns with 653 | the final step; adding members from the original binding 654 | to Qt.py 655 | 656 | """ 657 | 658 | import PySide2 as module 659 | _setup(module, ["QtUiTools"]) 660 | 661 | Qt.__binding_version__ = module.__version__ 662 | 663 | if hasattr(Qt, "_QtUiTools"): 664 | Qt.QtCompat.loadUi = lambda fname: \ 665 | Qt._QtUiTools.QUiLoader().load(fname) 666 | 667 | if hasattr(Qt, "_QtGui") and hasattr(Qt, "_QtCore"): 668 | Qt.QtCore.QStringListModel = Qt._QtGui.QStringListModel 669 | 670 | if hasattr(Qt, "_QtWidgets"): 671 | Qt.QtCompat.setSectionResizeMode = \ 672 | Qt._QtWidgets.QHeaderView.setSectionResizeMode 673 | 674 | if hasattr(Qt, "_QtCore"): 675 | Qt.__qt_version__ = Qt._QtCore.qVersion() 676 | Qt.QtCompat.translate = Qt._QtCore.QCoreApplication.translate 677 | 678 | Qt.QtCore.Property = Qt._QtCore.Property 679 | Qt.QtCore.Signal = Qt._QtCore.Signal 680 | Qt.QtCore.Slot = Qt._QtCore.Slot 681 | 682 | Qt.QtCore.QAbstractProxyModel = Qt._QtCore.QAbstractProxyModel 683 | Qt.QtCore.QSortFilterProxyModel = Qt._QtCore.QSortFilterProxyModel 684 | Qt.QtCore.QItemSelection = Qt._QtCore.QItemSelection 685 | Qt.QtCore.QItemSelectionRange = Qt._QtCore.QItemSelectionRange 686 | Qt.QtCore.QItemSelectionModel = Qt._QtCore.QItemSelectionModel 687 | 688 | 689 | def _pyside(): 690 | """Initialise PySide""" 691 | 692 | import PySide as module 693 | _setup(module, ["QtUiTools"]) 694 | 695 | Qt.__binding_version__ = module.__version__ 696 | 697 | if hasattr(Qt, "_QtUiTools"): 698 | Qt.QtCompat.loadUi = lambda fname: \ 699 | Qt._QtUiTools.QUiLoader().load(fname) 700 | 701 | if hasattr(Qt, "_QtGui"): 702 | setattr(Qt, "QtWidgets", _new_module("QtWidgets")) 703 | setattr(Qt, "_QtWidgets", Qt._QtGui) 704 | 705 | Qt.QtCompat.setSectionResizeMode = Qt._QtGui.QHeaderView.setResizeMode 706 | 707 | if hasattr(Qt, "_QtCore"): 708 | Qt.QtCore.QAbstractProxyModel = Qt._QtGui.QAbstractProxyModel 709 | Qt.QtCore.QSortFilterProxyModel = Qt._QtGui.QSortFilterProxyModel 710 | Qt.QtCore.QStringListModel = Qt._QtGui.QStringListModel 711 | Qt.QtCore.QItemSelection = Qt._QtGui.QItemSelection 712 | Qt.QtCore.QItemSelectionRange = Qt._QtGui.QItemSelectionRange 713 | Qt.QtCore.QItemSelectionModel = Qt._QtGui.QItemSelectionModel 714 | 715 | if hasattr(Qt, "_QtCore"): 716 | Qt.__qt_version__ = Qt._QtCore.qVersion() 717 | 718 | Qt.QtCore.Property = Qt._QtCore.Property 719 | Qt.QtCore.Signal = Qt._QtCore.Signal 720 | Qt.QtCore.Slot = Qt._QtCore.Slot 721 | 722 | QCoreApplication = Qt._QtCore.QCoreApplication 723 | Qt.QtCompat.translate = ( 724 | lambda context, sourceText, disambiguation, n: 725 | QCoreApplication.translate( 726 | context, 727 | sourceText, 728 | disambiguation, 729 | QCoreApplication.CodecForTr, 730 | n 731 | ) 732 | ) 733 | 734 | 735 | def _pyqt5(): 736 | """Initialise PyQt5""" 737 | 738 | import PyQt5 as module 739 | _setup(module, ["uic"]) 740 | 741 | if hasattr(Qt, "_uic"): 742 | Qt.QtCompat.loadUi = lambda fname: Qt._uic.loadUi(fname) 743 | 744 | if hasattr(Qt, "_QtWidgets"): 745 | Qt.QtCompat.setSectionResizeMode = \ 746 | Qt._QtWidgets.QHeaderView.setSectionResizeMode 747 | 748 | if hasattr(Qt, "_QtCore"): 749 | Qt.QtCompat.translate = Qt._QtCore.QCoreApplication.translate 750 | 751 | Qt.QtCore.Property = Qt._QtCore.pyqtProperty 752 | Qt.QtCore.Signal = Qt._QtCore.pyqtSignal 753 | Qt.QtCore.Slot = Qt._QtCore.pyqtSlot 754 | 755 | Qt.QtCore.QAbstractProxyModel = Qt._QtCore.QAbstractProxyModel 756 | Qt.QtCore.QSortFilterProxyModel = Qt._QtCore.QSortFilterProxyModel 757 | Qt.QtCore.QStringListModel = Qt._QtCore.QStringListModel 758 | Qt.QtCore.QItemSelection = Qt._QtCore.QItemSelection 759 | Qt.QtCore.QItemSelectionModel = Qt._QtCore.QItemSelectionModel 760 | Qt.QtCore.QItemSelectionRange = Qt._QtCore.QItemSelectionRange 761 | 762 | Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR 763 | Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR 764 | 765 | 766 | def _pyqt4(): 767 | """Initialise PyQt4""" 768 | 769 | import sip 770 | 771 | # Validation of envivornment variable. Prevents an error if 772 | # the variable is invalid since it's just a hint. 773 | try: 774 | hint = int(QT_SIP_API_HINT) 775 | except TypeError: 776 | hint = None # Variable was None, i.e. not set. 777 | except ValueError: 778 | raise ImportError("QT_SIP_API_HINT=%s must be a 1 or 2") 779 | 780 | for api in ("QString", 781 | "QVariant", 782 | "QDate", 783 | "QDateTime", 784 | "QTextStream", 785 | "QTime", 786 | "QUrl"): 787 | try: 788 | sip.setapi(api, hint or 2) 789 | except AttributeError: 790 | raise ImportError("PyQt4 < 4.6 isn't supported by Qt.py") 791 | except ValueError: 792 | actual = sip.getapi(api) 793 | if not hint: 794 | raise ImportError("API version already set to %d" % actual) 795 | else: 796 | # Having provided a hint indicates a soft constraint, one 797 | # that doesn't throw an exception. 798 | sys.stderr.write( 799 | "Warning: API '%s' has already been set to %d.\n" 800 | % (api, actual) 801 | ) 802 | 803 | import PyQt4 as module 804 | _setup(module, ["uic"]) 805 | 806 | if hasattr(Qt, "_uic"): 807 | Qt.QtCompat.loadUi = lambda fname: Qt._uic.loadUi(fname) 808 | 809 | if hasattr(Qt, "_QtGui"): 810 | setattr(Qt, "QtWidgets", _new_module("QtWidgets")) 811 | setattr(Qt, "_QtWidgets", Qt._QtGui) 812 | 813 | Qt.QtCompat.setSectionResizeMode = \ 814 | Qt._QtGui.QHeaderView.setResizeMode 815 | 816 | if hasattr(Qt, "_QtCore"): 817 | Qt.QtCore.QAbstractProxyModel = Qt._QtGui.QAbstractProxyModel 818 | Qt.QtCore.QSortFilterProxyModel = Qt._QtGui.QSortFilterProxyModel 819 | Qt.QtCore.QItemSelection = Qt._QtGui.QItemSelection 820 | Qt.QtCore.QStringListModel = Qt._QtGui.QStringListModel 821 | Qt.QtCore.QItemSelectionModel = Qt._QtGui.QItemSelectionModel 822 | Qt.QtCore.QItemSelectionRange = Qt._QtGui.QItemSelectionRange 823 | 824 | if hasattr(Qt, "_QtCore"): 825 | Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR 826 | Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR 827 | 828 | Qt.QtCore.Property = Qt._QtCore.pyqtProperty 829 | Qt.QtCore.Signal = Qt._QtCore.pyqtSignal 830 | Qt.QtCore.Slot = Qt._QtCore.pyqtSlot 831 | 832 | QCoreApplication = Qt._QtCore.QCoreApplication 833 | Qt.QtCompat.translate = ( 834 | lambda context, sourceText, disambiguation, n: 835 | QCoreApplication.translate( 836 | context, 837 | sourceText, 838 | disambiguation, 839 | QCoreApplication.CodecForTr, 840 | n) 841 | ) 842 | 843 | 844 | def _none(): 845 | """Internal option (used in installer)""" 846 | 847 | Mock = type("Mock", (), {"__getattr__": lambda Qt, attr: None}) 848 | 849 | Qt.__binding__ = "None" 850 | Qt.__qt_version__ = "0.0.0" 851 | Qt.__binding_version__ = "0.0.0" 852 | Qt.QtCompat.loadUi = lambda fname: None 853 | Qt.QtCompat.setSectionResizeMode = lambda *args, **kwargs: None 854 | 855 | for submodule in _common_members.keys(): 856 | setattr(Qt, submodule, Mock()) 857 | setattr(Qt, "_" + submodule, Mock()) 858 | 859 | 860 | def _log(text): 861 | if QT_VERBOSE: 862 | sys.stdout.write(text + "\n") 863 | 864 | 865 | def _convert(lines): 866 | """Convert compiled .ui file from PySide2 to Qt.py 867 | 868 | Arguments: 869 | lines (list): Each line of of .ui file 870 | 871 | Usage: 872 | >> with open("myui.py") as f: 873 | .. lines = _convert(f.readlines()) 874 | 875 | """ 876 | 877 | def parse(line): 878 | line = line.replace("from PySide2 import", "from Qt import") 879 | line = line.replace("QtWidgets.QApplication.translate", 880 | "Qt.QtCompat.translate") 881 | return line 882 | 883 | parsed = list() 884 | for line in lines: 885 | line = parse(line) 886 | parsed.append(line) 887 | 888 | return parsed 889 | 890 | 891 | def _cli(args): 892 | """Qt.py command-line interface""" 893 | import argparse 894 | 895 | parser = argparse.ArgumentParser() 896 | parser.add_argument("--convert", 897 | help="Path to compiled Python module, e.g. my_ui.py") 898 | parser.add_argument("--compile", 899 | help="Accept raw .ui file and compile with native " 900 | "PySide2 compiler.") 901 | parser.add_argument("--stdout", 902 | help="Write to stdout instead of file", 903 | action="store_true") 904 | parser.add_argument("--stdin", 905 | help="Read from stdin instead of file", 906 | action="store_true") 907 | 908 | args = parser.parse_args(args) 909 | 910 | if args.stdout: 911 | raise NotImplementedError("--stdout") 912 | 913 | if args.stdin: 914 | raise NotImplementedError("--stdin") 915 | 916 | if args.compile: 917 | raise NotImplementedError("--compile") 918 | 919 | if args.convert: 920 | sys.stdout.write("#\n" 921 | "# WARNING: --convert is an ALPHA feature.\n#\n" 922 | "# See https://github.com/mottosso/Qt.py/pull/132\n" 923 | "# for details.\n" 924 | "#\n") 925 | 926 | # 927 | # ------> Read 928 | # 929 | with open(args.convert) as f: 930 | lines = _convert(f.readlines()) 931 | 932 | backup = "%s_backup%s" % os.path.splitext(args.convert) 933 | sys.stdout.write("Creating \"%s\"..\n" % backup) 934 | shutil.copy(args.convert, backup) 935 | 936 | # 937 | # <------ Write 938 | # 939 | with open(args.convert, "w") as f: 940 | f.write("".join(lines)) 941 | 942 | sys.stdout.write("Successfully converted \"%s\"\n" % args.convert) 943 | 944 | 945 | def _install(): 946 | # Default order (customise order and content via QT_PREFERRED_BINDING) 947 | default_order = ("PySide2", "PyQt5", "PySide", "PyQt4") 948 | preferred_order = list( 949 | b for b in QT_PREFERRED_BINDING.split(os.pathsep) if b 950 | ) 951 | 952 | order = preferred_order or default_order 953 | 954 | available = { 955 | "PySide2": _pyside2, 956 | "PyQt5": _pyqt5, 957 | "PySide": _pyside, 958 | "PyQt4": _pyqt4, 959 | "None": _none 960 | } 961 | 962 | _log("Order: '%s'" % "', '".join(order)) 963 | 964 | found_binding = False 965 | for name in order: 966 | _log("Trying %s" % name) 967 | 968 | try: 969 | available[name]() 970 | found_binding = True 971 | break 972 | 973 | except ImportError as e: 974 | _log("ImportError: %s" % e) 975 | 976 | except KeyError: 977 | _log("ImportError: Preferred binding '%s' not found." % name) 978 | 979 | if not found_binding: 980 | # If not binding were found, throw this error 981 | raise ImportError("No Qt binding were found.") 982 | 983 | # Install individual members 984 | for name, members in _common_members.items(): 985 | try: 986 | their_submodule = getattr(Qt, "_%s" % name) 987 | except AttributeError: 988 | continue 989 | 990 | our_submodule = getattr(Qt, name) 991 | 992 | # Enable import * 993 | __all__.append(name) 994 | 995 | # Enable direct import of submodule, 996 | # e.g. import Qt.QtCore 997 | sys.modules[__name__ + "." + name] = our_submodule 998 | 999 | for member in members: 1000 | # Accept that a submodule may miss certain members. 1001 | try: 1002 | their_member = getattr(their_submodule, member) 1003 | except AttributeError: 1004 | _log("'%s.%s' was missing." % (name, member)) 1005 | continue 1006 | 1007 | setattr(our_submodule, member, their_member) 1008 | 1009 | # Backwards compatibility 1010 | Qt.QtCompat.load_ui = Qt.QtCompat.loadUi 1011 | 1012 | 1013 | _install() 1014 | 1015 | 1016 | """Augment QtCompat 1017 | 1018 | QtCompat contains wrappers and added functionality 1019 | to the original bindings, such as the CLI interface 1020 | and otherwise incompatible members between bindings, 1021 | such as `QHeaderView.setSectionResizeMode`. 1022 | 1023 | """ 1024 | 1025 | Qt.QtCompat._cli = _cli 1026 | Qt.QtCompat._convert = _convert 1027 | 1028 | # Enable command-line interface 1029 | if __name__ == "__main__": 1030 | _cli(sys.argv[1:]) 1031 | -------------------------------------------------------------------------------- /capture_gui/vendor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BigRoy/maya-capture-gui/c02c2385c485db9666b50f00582a60b784338f38/capture_gui/vendor/__init__.py -------------------------------------------------------------------------------- /capture_gui/version.py: -------------------------------------------------------------------------------- 1 | VERSION_MAJOR = 1 2 | VERSION_MINOR = 5 3 | VERSION_PATCH = 0 4 | 5 | 6 | version = '{}.{}.{}'.format(VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH) 7 | __version__ = version 8 | 9 | __all__ = ['version', 'version_info', '__version__'] 10 | --------------------------------------------------------------------------------