Пример #1
0
    def add_element(self, field=None, field_layout=None):
        """Adds a new field to the Array.

        :param field: Optional field to add. If omitted, a copy of the last element will be added.
        """
        if field is None:
            if not self.fields:
                raise RuntimeError('No default field set in the ArrayField.')
            field = copy.deepcopy(self.fields[-1])
            self.fields.append(field)
        if field_layout:
            element_widget = QtWidgets.QWidget()
            field_layout.insertWidget(field_layout.count() - 1, element_widget)
            hbox = QtWidgets.QHBoxLayout(element_widget)
            hbox.setContentsMargins(0, 0, 0, 0)

            field_widget = field.widget()
            hbox.addWidget(field_widget)

            action = QtWidgets.QAction('Remove', field_layout)
            action.triggered.connect(
                partial(self.remove_element, field_layout, element_widget))

            icon = QtGui.QIcon(QtGui.QPixmap(':/smallTrash.png'))
            action.setIcon(icon)
            action.setToolTip('Remove')
            action.setStatusTip('Remove')
            delete_button = QtWidgets.QToolButton()
            delete_button.setDefaultAction(action)
            hbox.addWidget(delete_button)
Пример #2
0
class TestCaptureStream(object):
    """Allows the output of the tests to be displayed in a QTextEdit."""
    success_color = QtGui.QColor(92, 184, 92)
    fail_color = QtGui.QColor(240, 173, 78)
    error_color = QtGui.QColor(217, 83, 79)
    skip_color = QtGui.QColor(88, 165, 204)
    normal_color = QtGui.QColor(200, 200, 200)

    def __init__(self, text_edit):
        self.text_edit = text_edit

    def write(self, text):
        """Write text into the QTextEdit."""
        # Color the output
        if text.startswith('ok'):
            self.text_edit.setTextColor(TestCaptureStream.success_color)
        elif text.startswith('FAIL'):
            self.text_edit.setTextColor(TestCaptureStream.fail_color)
        elif text.startswith('ERROR'):
            self.text_edit.setTextColor(TestCaptureStream.error_color)
        elif text.startswith('skipped'):
            self.text_edit.setTextColor(TestCaptureStream.skip_color)

        self.text_edit.insertPlainText(text)
        self.text_edit.setTextColor(TestCaptureStream.normal_color)

    def flush(self):
        pass
Пример #3
0
    def image(cls, size=32):
        """Get the image QPixmap of the Component.

        :param size: Desired dimension of the image.
        :return: The QPixmap of the icon image.
        """
        return QtGui.QPixmap(cls.image_path()).scaled(size, size)
Пример #4
0
 def mouseMoveEvent(self, event):
     contains = self.grab_rect().contains(event.pos())
     if contains and not self.over_grab_hotspot:
         QtWidgets.qApp.setOverrideCursor(QtGui.QCursor(QtCore.Qt.OpenHandCursor))
         self.over_grab_hotspot = True
     elif not contains and self.over_grab_hotspot:
         restore_cursor()
         self.over_grab_hotspot = False
Пример #5
0
    def paintEvent(self, event):
        """Override the paintEvent to draw the grab hotspot.

        :param event:
        """
        super(ComponentWidget, self).paintEvent(event)
        painter = QtGui.QPainter(self)
        painter.setPen(QtCore.Qt.NoPen)
        painter.setBrush(self.color)
        painter.drawRect(self.grab_rect())
Пример #6
0
    def mousePressEvent(self, event):
        child = self.childAt(event.pos())
        if not child:
            return
        if not isinstance(child, ComponentWidget):
            return
        pos = child.mapFromParent(event.pos())
        if not child.grab_rect().contains(pos):
            return

        # Create the drag object with the component data we are moving
        mime_data = QtCore.QMimeData()
        mime_data.setText(json.dumps(child.comp.data()))
        drag = QtGui.QDrag(self)
        drag.setMimeData(mime_data)
        hotspot = event.pos() - child.pos()
        drag.setHotSpot(hotspot)

        # Resize the indicator so it has the same height as the ComponentWidget we are dragging
        self.drop_indicator.setFixedHeight(child.height())
        QtWidgets.qApp.setOverrideCursor(
            QtGui.QCursor(QtCore.Qt.ClosedHandCursor))
        index = self.get_component_index_at_position(event.pos())
        component_widget_index = self.queue_layout.indexOf(child)
        self.queue_layout.takeAt(component_widget_index)
        child.hide()
        self.place_indicator(index)
        if drag.exec_(QtCore.Qt.MoveAction) == QtCore.Qt.MoveAction:
            # The drag reorder was accepted so do the actual move
            drop_indicator_index = self.queue_layout.indexOf(
                self.drop_indicator)
            self.hide_indicator()
            self.queue_layout.insertWidget(drop_indicator_index, child)
            child.show()
            self.queue.clear()
            components = self.get_ordered_component_widgets()
            for widget in components:
                self.queue.add(widget.comp)
        else:
            self.queue_layout.insertWidget(component_widget_index, child)
            child.show()
        restore_cursor()
Пример #7
0
    def __init__(self, *args, **kwargs):
        super(MayaTestRunnerDialog, self).__init__(*args, **kwargs)
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
        self.setWindowTitle('CMT Unit Test Runner')
        self.resize(1000, 600)
        self.rollback_importer = RollbackImporter()

        menubar = self.menuBar()
        menu = menubar.addMenu('Settings')
        action = menu.addAction('Buffer Output')
        action.setToolTip('Only display output during a failed test.')
        action.setCheckable(True)
        action.setChecked(mayaunittest.Settings.buffer_output)
        action.toggled.connect(mayaunittest.set_buffer_output)
        action = menu.addAction('New Scene Between Test')
        action.setToolTip('Creates a new scene file after each test.')
        action.setCheckable(True)
        action.setChecked(mayaunittest.Settings.file_new)
        action.toggled.connect(mayaunittest.set_file_new)
        menu = menubar.addMenu('Help')
        action = menu.addAction('Documentation')
        action.triggered.connect(documentation)

        toolbar = self.addToolBar('Tools')
        action = toolbar.addAction('Run All Tests')
        action.setIcon(QtGui.QIcon(QtGui.QPixmap(os.path.join(ICON_DIR, 'cmt_run_all_tests.png'))))
        action.triggered.connect(self.run_all_tests)
        action.setToolTip('Run all tests.')

        action = toolbar.addAction('Run Selected Tests')
        action.setIcon(QtGui.QIcon(QtGui.QPixmap(os.path.join(ICON_DIR, 'cmt_run_selected_tests.png'))))
        action.setToolTip('Run all selected tests.')
        action.triggered.connect(self.run_selected_tests)

        action = toolbar.addAction('Run Failed Tests')
        action.setIcon(QtGui.QIcon(QtGui.QPixmap(os.path.join(ICON_DIR, 'cmt_run_failed_tests.png'))))
        action.setToolTip('Run all failed tests.')
        action.triggered.connect(self.run_failed_tests)

        widget = QtWidgets.QWidget()
        self.setCentralWidget(widget)
        vbox = QtWidgets.QVBoxLayout(widget)

        splitter = QtWidgets.QSplitter(orientation=QtCore.Qt.Horizontal)
        self.test_view = QtWidgets.QTreeView()
        self.test_view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
        splitter.addWidget(self.test_view)
        self.output_console = QtWidgets.QTextEdit()
        self.output_console.setReadOnly(True)
        splitter.addWidget(self.output_console)
        vbox.addWidget(splitter)
        splitter.setStretchFactor(1, 4)
        self.stream = TestCaptureStream(self.output_console)

        test_suite = mayaunittest.get_tests()
        root_node = TestNode(test_suite)
        self.model = TestTreeModel(root_node, self)
        self.test_view.setModel(self.model)
        self.expand_tree(root_node)
Пример #8
0
 def set_color(self):
     """Open a dialog to set the override RGB color of the selected nodes."""
     nodes = cmds.ls(sl=True) or []
     if nodes:
         color = cmds.getAttr('{0}.overrideColorRGB'.format(nodes[0]))[0]
         color = QtGui.QColor(color[0] * 255, color[1] * 255,
                              color[2] * 255)
         color = QtWidgets.QColorDialog.getColor(color, self,
                                                 'Set Curve Color')
         if color.isValid():
             color = [color.redF(), color.greenF(), color.blueF()]
             for node in nodes:
                 cmds.setAttr('{0}.overrideEnabled'.format(node), True)
                 cmds.setAttr('{0}.overrideRGBColors'.format(node), True)
                 cmds.setAttr('{0}.overrideColorRGB'.format(node), *color)
Пример #9
0
    def widget(self):
        """Get the QWidget of the Field."""
        widget = QtWidgets.QWidget()
        hbox = QtWidgets.QHBoxLayout(widget)
        hbox.setContentsMargins(0, 0, 0, 0)
        validator = QtGui.QDoubleValidator(-999999.0, 999999.0, self.precision)
        widget_x = QtWidgets.QLineEdit(str(self._value[0]))
        widget_x.setToolTip(self.help_text)
        widget_x.setValidator(validator)
        widget_x.textChanged.connect(self.set_value_x)
        hbox.addWidget(widget_x)

        widget_y = QtWidgets.QLineEdit(str(self._value[1]))
        widget_y.setToolTip(self.help_text)
        widget_y.setValidator(validator)
        widget_y.textChanged.connect(self.set_value_y)
        hbox.addWidget(widget_y)

        widget_z = QtWidgets.QLineEdit(str(self._value[2]))
        widget_z.setToolTip(self.help_text)
        widget_z.setValidator(validator)
        widget_z.textChanged.connect(self.set_value_z)
        hbox.addWidget(widget_z)
        return widget
Пример #10
0
 def paintEvent(self, event):
     painter = QtGui.QPainter(self)
     color = QtGui.QColor(72, 170, 181)
     painter.setPen(color)
     painter.drawRect(0, 0, self.width() - 1, self.height() - 1)
Пример #11
0
    def __init__(self, comp, queue, parent=None):
        super(ComponentWidget, self).__init__(parent)
        self.setMouseTracking(True)
        self.queue_layout = parent.queue_layout
        self.queue = queue
        self.comp = comp
        vbox = QtWidgets.QVBoxLayout(self)
        self.setFrameStyle(QtWidgets.QFrame.StyledPanel)
        self.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
                           QtWidgets.QSizePolicy.Maximum)
        self.over_grab_hotspot = False
        self.color = ComponentWidget.normal_color

        # Header
        hbox = QtWidgets.QHBoxLayout()
        self.header_layout = hbox
        hbox.setContentsMargins(16, 4, 4, 4)
        vbox.addLayout(hbox)

        # Expand toggle
        expand_action = QtWidgets.QAction('Toggle', self)
        expand_action.setCheckable(True)
        icon = QtGui.QIcon(QtGui.QPixmap(':/arrowDown.png'))
        expand_action.setIcon(icon)
        expand_action.setToolTip('Toggle details')
        expand_action.setStatusTip('Toggle details')
        button = QtWidgets.QToolButton()
        button.setDefaultAction(expand_action)
        hbox.addWidget(button)

        # Enable checkbox
        enabled = QtWidgets.QCheckBox()
        enabled.setToolTip('Enable/Disable Component')
        hbox.addWidget(enabled)

        # Breakpoint
        self.break_point_action = QtWidgets.QAction('Breakpoint', self)
        self.break_point_action.setCheckable(True)
        self.break_point_action.setIcon(self.break_point_disabled_icon)
        self.break_point_action.setToolTip('Set break point at component.')
        self.break_point_action.setStatusTip('Set break point at component.')
        self.break_point_action.toggled.connect(self.set_break_point)
        button = QtWidgets.QToolButton()
        button.setDefaultAction(self.break_point_action)
        hbox.addWidget(button)

        # Execute button
        action = QtWidgets.QAction('Execute', self)
        icon = QtGui.QIcon(QtGui.QPixmap(':/timeplay.png'))
        action.setIcon(icon)
        action.setToolTip('Execute the component')
        action.setStatusTip('Execute the component')
        action.triggered.connect(
            partial(self.execute_component,
                    on_error=parent.on_component_execution_error))
        button = QtWidgets.QToolButton()
        button.setDefaultAction(action)
        hbox.addWidget(button)

        # Image label
        label = QtWidgets.QLabel()
        label.setPixmap(comp.image(size=24))
        label.setToolTip(comp.__class__.__doc__)
        hbox.addWidget(label)

        # Name label
        label = QtWidgets.QLabel(comp.name().split('.')[-1])
        label.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
                            QtWidgets.QSizePolicy.Fixed)
        label.setToolTip(comp.__class__.__doc__)
        font = QtGui.QFont()
        font.setPointSize(14)
        label.setFont(font)
        hbox.addWidget(label)
        hbox.addStretch()

        if comp.help_url():
            action = QtWidgets.QAction('Help', self)
            icon = QtGui.QIcon(QtGui.QPixmap(':/help.png'))
            action.setIcon(icon)
            action.setToolTip('Open help documentation.')
            action.setStatusTip('Open help documentation.')
            action.triggered.connect(partial(webbrowser.open, comp.help_url()))
            button = QtWidgets.QToolButton()
            button.setDefaultAction(action)
            hbox.addWidget(button)

        action = QtWidgets.QAction('Delete', self)
        icon = QtGui.QIcon(QtGui.QPixmap(':/smallTrash.png'))
        action.setIcon(icon)
        message = 'Delete Component'
        action.setToolTip(message)
        action.setStatusTip(message)
        action.triggered.connect(partial(self.remove, prompt=True))
        button = QtWidgets.QToolButton()
        button.setDefaultAction(action)
        hbox.addWidget(button)

        content_vbox = QtWidgets.QVBoxLayout()
        content_vbox.setContentsMargins(16, 0, 0, 0)
        vbox.addLayout(content_vbox)

        content_widget = comp.widget()
        content_vbox.addWidget(content_widget)
        enabled.toggled.connect(content_widget.setEnabled)
        enabled.setChecked(comp.enabled)
        enabled.toggled.connect(comp.set_enabled)
        expand_action.toggled.connect(content_widget.setVisible)
        content_widget.setVisible(False)
Пример #12
0
class ComponentWidget(QtWidgets.QFrame):
    """The widget used to display a Component in the ComponentQueue."""
    normal_color = QtGui.QColor(72, 170, 181)
    error_color = QtGui.QColor(217, 83, 79)
    break_point_disabled_icon = QtGui.QIcon(QtGui.QPixmap(':/stopClip.png'))
    break_point_enabled_icon = QtGui.QIcon(QtGui.QPixmap(':/timestop.png'))

    def __init__(self, comp, queue, parent=None):
        super(ComponentWidget, self).__init__(parent)
        self.setMouseTracking(True)
        self.queue_layout = parent.queue_layout
        self.queue = queue
        self.comp = comp
        vbox = QtWidgets.QVBoxLayout(self)
        self.setFrameStyle(QtWidgets.QFrame.StyledPanel)
        self.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
                           QtWidgets.QSizePolicy.Maximum)
        self.over_grab_hotspot = False
        self.color = ComponentWidget.normal_color

        # Header
        hbox = QtWidgets.QHBoxLayout()
        self.header_layout = hbox
        hbox.setContentsMargins(16, 4, 4, 4)
        vbox.addLayout(hbox)

        # Expand toggle
        expand_action = QtWidgets.QAction('Toggle', self)
        expand_action.setCheckable(True)
        icon = QtGui.QIcon(QtGui.QPixmap(':/arrowDown.png'))
        expand_action.setIcon(icon)
        expand_action.setToolTip('Toggle details')
        expand_action.setStatusTip('Toggle details')
        button = QtWidgets.QToolButton()
        button.setDefaultAction(expand_action)
        hbox.addWidget(button)

        # Enable checkbox
        enabled = QtWidgets.QCheckBox()
        enabled.setToolTip('Enable/Disable Component')
        hbox.addWidget(enabled)

        # Breakpoint
        self.break_point_action = QtWidgets.QAction('Breakpoint', self)
        self.break_point_action.setCheckable(True)
        self.break_point_action.setIcon(self.break_point_disabled_icon)
        self.break_point_action.setToolTip('Set break point at component.')
        self.break_point_action.setStatusTip('Set break point at component.')
        self.break_point_action.toggled.connect(self.set_break_point)
        button = QtWidgets.QToolButton()
        button.setDefaultAction(self.break_point_action)
        hbox.addWidget(button)

        # Execute button
        action = QtWidgets.QAction('Execute', self)
        icon = QtGui.QIcon(QtGui.QPixmap(':/timeplay.png'))
        action.setIcon(icon)
        action.setToolTip('Execute the component')
        action.setStatusTip('Execute the component')
        action.triggered.connect(
            partial(self.execute_component,
                    on_error=parent.on_component_execution_error))
        button = QtWidgets.QToolButton()
        button.setDefaultAction(action)
        hbox.addWidget(button)

        # Image label
        label = QtWidgets.QLabel()
        label.setPixmap(comp.image(size=24))
        label.setToolTip(comp.__class__.__doc__)
        hbox.addWidget(label)

        # Name label
        label = QtWidgets.QLabel(comp.name().split('.')[-1])
        label.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
                            QtWidgets.QSizePolicy.Fixed)
        label.setToolTip(comp.__class__.__doc__)
        font = QtGui.QFont()
        font.setPointSize(14)
        label.setFont(font)
        hbox.addWidget(label)
        hbox.addStretch()

        if comp.help_url():
            action = QtWidgets.QAction('Help', self)
            icon = QtGui.QIcon(QtGui.QPixmap(':/help.png'))
            action.setIcon(icon)
            action.setToolTip('Open help documentation.')
            action.setStatusTip('Open help documentation.')
            action.triggered.connect(partial(webbrowser.open, comp.help_url()))
            button = QtWidgets.QToolButton()
            button.setDefaultAction(action)
            hbox.addWidget(button)

        action = QtWidgets.QAction('Delete', self)
        icon = QtGui.QIcon(QtGui.QPixmap(':/smallTrash.png'))
        action.setIcon(icon)
        message = 'Delete Component'
        action.setToolTip(message)
        action.setStatusTip(message)
        action.triggered.connect(partial(self.remove, prompt=True))
        button = QtWidgets.QToolButton()
        button.setDefaultAction(action)
        hbox.addWidget(button)

        content_vbox = QtWidgets.QVBoxLayout()
        content_vbox.setContentsMargins(16, 0, 0, 0)
        vbox.addLayout(content_vbox)

        content_widget = comp.widget()
        content_vbox.addWidget(content_widget)
        enabled.toggled.connect(content_widget.setEnabled)
        enabled.setChecked(comp.enabled)
        enabled.toggled.connect(comp.set_enabled)
        expand_action.toggled.connect(content_widget.setVisible)
        content_widget.setVisible(False)

        # content_widget = QtWidgets.QWidget()
        # content_layout = QtWidgets.QVBoxLayout(content_widget)
        # content_layout.setContentsMargins(16, 8, 0, 0)
        # vbox.addWidget(content_widget)
        # comp.draw(content_layout)
        # enabled.toggled.connect(content_widget.setEnabled)
        # enabled.setChecked(comp.enabled)
        # enabled.toggled.connect(comp.set_enabled)
        # expand_action.toggled.connect(content_widget.setVisible)
        # content_widget.setVisible(False)
        # content_layout.addStretch()

    def execute_component(self, on_error):
        self.set_color(ComponentWidget.normal_color)
        self.comp.capture_execute(on_error=on_error)

    def set_break_point(self, value):
        self.comp.break_point = value
        icon = self.break_point_enabled_icon if value else self.break_point_disabled_icon
        self.break_point_action.setIcon(icon)

    def grab_rect(self):
        """Get the rectangle describing the grab hotspot."""
        return QtCore.QRect(0, 0, 16, self.height() - 1)

    def set_color(self, color):
        """Set the color of status bar on the widget.

        :param color: The new color.
        """
        self.color = color
        self.update()

    def paintEvent(self, event):
        """Override the paintEvent to draw the grab hotspot.

        :param event:
        """
        super(ComponentWidget, self).paintEvent(event)
        painter = QtGui.QPainter(self)
        painter.setPen(QtCore.Qt.NoPen)
        painter.setBrush(self.color)
        painter.drawRect(self.grab_rect())

    def mouseMoveEvent(self, event):
        contains = self.grab_rect().contains(event.pos())
        if contains and not self.over_grab_hotspot:
            QtWidgets.qApp.setOverrideCursor(
                QtGui.QCursor(QtCore.Qt.OpenHandCursor))
            self.over_grab_hotspot = True
        elif not contains and self.over_grab_hotspot:
            restore_cursor()
            self.over_grab_hotspot = False

    def leaveEvent(self, event):
        if self.over_grab_hotspot:
            restore_cursor()
            self.over_grab_hotspot = False

    def move(self, new_index):
        """Move the component to the specified index in the queue.

        :param new_index: Index to move to.
        """
        index = self.index()
        # Reorder the Component in the Queue
        self.queue.remove(index)

        if new_index and new_index > self.queue.length():
            # When we are moving a component to the bottom, the index may get greater than the max allowed
            new_index = self.queue.length()

        self.queue.insert(new_index, self.comp)
        # Reorder the ComponentWidget in the layout
        self.queue_layout.takeAt(index)
        self.queue_layout.insertWidget(new_index, self)

    def index(self):
        """Get the index of the Component. """
        return self.queue.index(self.comp)

    def remove(self, prompt=False):
        """Remove this Component from the queue.

        :param prompt: True to display a message box confirming the removal of the Component.
        """
        if prompt:
            msg_box = QtWidgets.QMessageBox()
            msg_box.setIcon(QtWidgets.QMessageBox.Question)
            msg_box.setText('Are you sure you want to remove this component?')
            msg_box.setStandardButtons(QtWidgets.QMessageBox.Yes
                                       | QtWidgets.QMessageBox.Cancel)
            if msg_box.exec_() != QtWidgets.QMessageBox.Yes:
                return
        index = self.queue.index(self.comp)
        self.queue.remove(index)
        self.queue_layout.takeAt(index)
        self.deleteLater()
Пример #13
0
class TestNode(shortcuts.BaseTreeNode):
    """A node representing a Test, TestCase, or TestSuite for display in a QTreeView."""
    success_icon = QtGui.QPixmap(os.path.join(ICON_DIR, 'cmt_test_success.png'))
    fail_icon = QtGui.QPixmap(os.path.join(ICON_DIR, 'cmt_test_fail.png'))
    error_icon = QtGui.QPixmap(os.path.join(ICON_DIR, 'cmt_test_error.png'))
    skip_icon = QtGui.QPixmap(os.path.join(ICON_DIR, 'cmt_test_skip.png'))

    def __init__(self, test, parent=None):
        super(TestNode, self).__init__(parent)
        self.test = test
        self.tool_tip = str(test)
        self.status = TestStatus.not_run
        if isinstance(self.test, unittest.TestSuite):
            for test_ in self.test:
                if isinstance(test_, unittest.TestCase) or test_.countTestCases():
                    self.add_child(TestNode(test_, self))

    def name(self):
        """Get the name to print in the view."""
        if isinstance(self.test, unittest.TestCase):
            return self.test._testMethodName
        elif isinstance(self.child(0).test, unittest.TestCase):
            return self.child(0).test.__class__.__name__
        else:
            return self.child(0).child(0).test.__class__.__module__

    def path(self):
        """Gets the import path of the test.  Used for finding the test by name."""
        if self.parent() and self.parent().parent():
            return '{0}.{1}'.format(self.parent().path(), self.name())
        else:
            return self.name()

    def get_status(self):
        """Get the status of the TestNode.

        Nodes with children like the TestSuites, will get their status based on the status of the leaf nodes (the
        TestCases).
        @return: A status value from TestStatus.
        """
        if not self.children:
            return self.status
        result = TestStatus.not_run
        for child in self.children:
            child_status = child.get_status()
            if child_status == TestStatus.error:
                # Error status has highest priority so propagate that up to the parent
                return child_status
            elif child_status == TestStatus.fail:
                result = child_status
            elif child_status == TestStatus.success and result != TestStatus.fail:
                result = child_status
            elif child_status == TestStatus.skipped and result != TestStatus.fail:
                result = child_status
        return result

    def get_icon(self):
        """Get the status icon to display with the Test."""
        status = self.get_status()
        return [None,
                TestNode.success_icon,
                TestNode.fail_icon,
                TestNode.error_icon,
                TestNode.skip_icon][status]
Пример #14
0
    def __init__(self, parent=None):
        super(ControlWindow, self).__init__(parent)
        self.setWindowTitle('CMT Control Creator')
        self.resize(300, 500)
        vbox = QtWidgets.QVBoxLayout(self)

        size = 20
        label_width = 60
        icon_left = QtGui.QIcon(
            QtGui.QPixmap(':/nudgeLeft.png').scaled(size, size))
        icon_right = QtGui.QIcon(
            QtGui.QPixmap(':/nudgeRight.png').scaled(size, size))
        validator = QtGui.QDoubleValidator(-180.0, 180.0, 2)
        grid = QtWidgets.QGridLayout()
        vbox.addLayout(grid)

        # Rotate X
        label = QtWidgets.QLabel('Rotate X')
        label.setMaximumWidth(label_width)
        grid.addWidget(label, 0, 0, QtCore.Qt.AlignRight)
        b = QtWidgets.QPushButton(icon_left, '')
        b.released.connect(partial(self.rotate_x, direction=-1))
        grid.addWidget(b, 0, 1)
        self.offset_x = QtWidgets.QLineEdit('45.0')
        self.offset_x.setValidator(validator)
        grid.addWidget(self.offset_x, 0, 2)
        b = QtWidgets.QPushButton(icon_right, '')
        b.released.connect(partial(self.rotate_x, direction=1))
        grid.addWidget(b, 0, 3)

        # Rotate Y
        label = QtWidgets.QLabel('Rotate Y')
        label.setMaximumWidth(label_width)
        grid.addWidget(label, 1, 0, QtCore.Qt.AlignRight)
        b = QtWidgets.QPushButton(icon_left, '')
        b.released.connect(partial(self.rotate_y, direction=-1))
        grid.addWidget(b, 1, 1)
        self.offset_y = QtWidgets.QLineEdit('45.0')
        self.offset_y.setValidator(validator)
        grid.addWidget(self.offset_y, 1, 2)
        b = QtWidgets.QPushButton(icon_right, '')
        b.released.connect(partial(self.rotate_y, direction=1))
        grid.addWidget(b, 1, 3)

        # Rotate Z
        label = QtWidgets.QLabel('Rotate Z')
        label.setMaximumWidth(label_width)
        grid.addWidget(label, 2, 0, QtCore.Qt.AlignRight)
        b = QtWidgets.QPushButton(icon_left, '')
        b.released.connect(partial(self.rotate_z, direction=-1))
        grid.addWidget(b, 2, 1)
        self.offset_z = QtWidgets.QLineEdit('45.0')
        self.offset_z.setValidator(validator)
        grid.addWidget(self.offset_z, 2, 2)
        b = QtWidgets.QPushButton(icon_right, '')
        b.released.connect(partial(self.rotate_z, direction=1))
        grid.addWidget(b, 2, 3)
        grid.setColumnStretch(2, 2)

        hbox = QtWidgets.QHBoxLayout()
        vbox.addLayout(hbox)
        b = QtWidgets.QPushButton('Export Selected')
        b.released.connect(self.dump_controls)
        hbox.addWidget(b)

        b = QtWidgets.QPushButton('Set Color')
        b.released.connect(self.set_color)
        hbox.addWidget(b)

        hbox = QtWidgets.QHBoxLayout()
        vbox.addLayout(hbox)
        b = QtWidgets.QPushButton('Create Selected')
        b.released.connect(self.create_selected)
        hbox.addWidget(b)

        b = QtWidgets.QPushButton('Remove Selected')
        b.released.connect(self.remove_selected)
        hbox.addWidget(b)

        hbox = QtWidgets.QHBoxLayout()
        vbox.addLayout(hbox)
        self.stack_count = QtWidgets.QSpinBox()
        self.stack_count.setValue(2)
        hbox.addWidget(self.stack_count)

        b = QtWidgets.QPushButton('Create Transform Stack')
        b.released.connect(self.create_transform_stack)
        b.setToolTip('Creates a transform stack above each selected node.')
        hbox.addWidget(b)

        self.control_list = QtWidgets.QListWidget()
        self.control_list.setSelectionMode(
            QtWidgets.QAbstractItemView.ExtendedSelection)
        vbox.addWidget(self.control_list)

        self.populate_controls()