├── .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 |
--------------------------------------------------------------------------------