def __init__(self): super(DockableWindow, self).__init__() self.shown = False # Build our *.ui files into qt/resources, so the subclass can load its layout. qt_helpers.compile_all_layouts() # How do we make our window handle global hotkeys? undo = Qt.QAction('Undo', self) undo.setShortcut(Qt.Qt.CTRL + Qt.Qt.Key_Z) undo.triggered.connect(lambda: pm.undo()) self.addAction(undo) redo = Qt.QAction('Redo', self) redo.setShortcut(Qt.Qt.CTRL + Qt.Qt.Key_Y) redo.triggered.connect(lambda: pm.redo(redo=True)) self.addAction(redo) style = '' # Maya's checkbox style makes the checkbox invisible when it's deselected, # which makes it impossible to tell that there's even a checkbox there to # click. Adjust the background color to fix this. style += 'QTreeView::indicator:unchecked { background-color: #000; }' # Make tree and list view items slightly larger by default. style += 'QTreeView::item { height: 20px; }' style += 'QListView::item { height: 26px; }' self.setStyleSheet(self.styleSheet() + style)
def __init__(self): super(KeyframeNamingWindow, self).__init__() self.callback_ids = om.MCallbackIdArray() self._reregister_callback_queued = False self.frames_in_list = [] self._currently_refreshing = False self._currently_setting_selection = False self._listening_to_singleton = None self._listening_to_anim_curve = None self.time_change_listener = maya_helpers.TimeChangeListener(self._time_changed) from .qt_generated import keyframe_naming reload(keyframe_naming) self._ui = keyframe_naming.Ui_keyframe_naming() self._ui.setupUi(self) self._ui.removeFrame.clicked.connect(self.delete_selected_frame) self._ui.renameFrame.clicked.connect(self.rename_selected_frame) self._ui.addFrame.clicked.connect(self.add_new_frame) self._ui.frameList.itemDelegate().commitData.connect(self.frame_name_edited) self._ui.frameList.itemDelegate().closeEditor.connect(self.name_editor_closed) self._ui.frameList.itemSelectionChanged.connect(self.selected_frame_changed) self._ui.frameList.itemClicked.connect(self.selected_frame_changed) self._ui.frameList.setContextMenuPolicy(Qt.Qt.CustomContextMenu) def context_menu(pos): # Activate the context menu for the selected item. item = self._ui.frameList.itemAt(pos) if item is None: return keyframe_context_menu = self._create_keyframe_context_menu(item) action = keyframe_context_menu.exec_(self._ui.frameList.mapToGlobal(pos)) self._ui.frameList.customContextMenuRequested.connect(context_menu) # Create the menu. Why can't this be done in the designer? menu_bar = Qt.QMenuBar() self.layout().setMenuBar(menu_bar) edit_menu = menu_bar.addMenu('Edit') menu_select_naming_node = Qt.QAction('Select zKeyframeNaming node', self) menu_select_naming_node.setStatusTip('Select the zKeyframeNaming node, to edit keyframes in the graph editor') menu_select_naming_node.triggered.connect(select_naming_node) edit_menu.addAction(menu_select_naming_node) add_arnold_attribute = Qt.QAction('Add Arnold attribute', self) add_arnold_attribute.setStatusTip('Add a custom Arnold attribute to export the current frame name to rendered EXR files') add_arnold_attribute.triggered.connect(connect_to_arnold) edit_menu.addAction(add_arnold_attribute) self.installEventFilter(self) self._ui.frameList.installEventFilter(self)
def refresh(self): if not self.shown: return # Don't refresh while editing. if self._ui.frameList.state() == Qt.QAbstractItemView.EditingState: return self._currently_refreshing = True try: all_keys = get_all_keys() all_names = get_all_names() self._ui.frameList.clear() self.frames_in_list = [] # Add keys in chronological order. for frame in sorted(all_keys.keys()): idx = all_keys[frame] name = all_names.get(idx, '') item = Qt.QListWidgetItem(name) item.frame = frame item.setFlags(item.flags() | Qt.Qt.ItemIsEditable) self._ui.frameList.addItem(item) self.frames_in_list.append(frame) self.set_selected_frame_from_current_time() finally: self._currently_refreshing = False
def startDrag(self, supportedActions): """ This mess is only needed because apparently there's no way to simply tell QT that you don't want a big picture of the text you're dragging covering up the drag so you can't see where you're dropping. QT is not a good API. """ drag = Qt.QDrag(self) data = self.model().mimeData(self.selectedIndexes()) drag.setMimeData(data) drag.exec_(supportedActions, Qt.Qt.MoveAction) super(NoDragPreviewMixin, self).startDrag(supportedActions)
def _create_keyframe_context_menu(self, item): keyframe_context_menu = Qt.QMenu() time_slider_start = keyframe_context_menu.addAction('Set time slider start') time_slider_start.triggered.connect(lambda: pm.playbackOptions(min=item.frame)) time_slider_end = keyframe_context_menu.addAction('Set time slider end') time_slider_end.triggered.connect(lambda: pm.playbackOptions(max=item.frame)) time_render_start = keyframe_context_menu.addAction('Set render start') time_render_start.triggered.connect(lambda: pm.PyNode('defaultRenderGlobals').startFrame.set(item.frame)) time_render_end = keyframe_context_menu.addAction('Set render end') time_render_end.triggered.connect(lambda: pm.PyNode('defaultRenderGlobals').endFrame.set(item.frame)) return keyframe_context_menu
def add_controllers_recursively(node, parent, level=0): # Controllers don't actually support cycles (they seem to, but crash when you # try to use them), but the DG connections can still have cycles. Check for # this to avoid infinite recursion. if node in seen_nodes: log.warning('Controller cycle: %s', node) return seen_nodes.add(node) item = Qt.QTreeWidgetItem(parent) item.controller_node = node self.controllers_to_items[node] = item item.controller_object = get_controller_object(node) name = item.controller_object.nodeName( ) if item.controller_object else item.controller_node.nodeName() item.setText(0, name) item.setFlags(Qt.Qt.ItemIsEnabled | Qt.Qt.ItemIsSelectable | Qt.Qt.ItemIsDragEnabled | Qt.Qt.ItemIsDropEnabled) if parent is self.ui.controllerTree: self.ui.controllerTree.addTopLevelItem(item) else: parent.addChild(item) # By default, expand everything except the first level. This way, top-level # controllers are closed to keep the list small, but you don't have to expand # controllers one by one to find things. It would be nicer to support shift-clicking # the expansion button like Maya does, but this seems to be a pain with QT. # # Expand the node if it's new or if it was expanded previously. expand_by_default = level != 0 if old_nodes.get(node, expand_by_default): self.ui.controllerTree.expandItem(item) # If this controller was selected, reselect it. if node in selected_controllers: self.ui.controllerTree.setCurrentItem(item) # Add the controller's children. children = node.children.listConnections(s=True, d=False) for child in children: add_controllers_recursively(child, item, level + 1)
def populate_transform_list(self): self.currently_refreshing = True try: # Remember which indexes were selected. old_selection = [ idx.row() for idx in self.ui.transformList.selectedIndexes() ] if self.current_node is None: new_transform_list = [] else: curve = CreateCurve(self.current_node) new_transform_list = curve.get_transforms() self.ui.transformList.clear() self.current_transform_list = new_transform_list for transform_attr in new_transform_list: # The input connection usually goes to worldMatrix[0]. Only show the attribute if # it's connected to something else. name = transform_attr.name() if transform_attr.attrName(longName=True) == 'worldMatrix': # Work around a PyMEL consistency: node.nodeName() returns just the node name, # but attr.nodeName() returns the disambiguated name. We just want the node # name. name = transform_attr.node().nodeName() else: name = '%s.%s' % (transform_attr.node().nodeName(), transform_attr.attrName(longName=True)) item = Qt.QListWidgetItem(name) item.transform_attr = transform_attr self.ui.transformList.addItem(item) # Reselect the old selection. These might not point at the same things, but it prevents # losing the selection whenever we refresh. self.select_transform_rows(old_selection) # Make sure the "remove from curve" button state is enabled after we change the list. self.refresh_after_transform_selection_changed() finally: self.currently_refreshing = False
def dragMoveEvent(self, event): # In QT 4, QAbstractItemViewPrivate::position normally gives a margin of 2 pixels # on the top and bottom for AboveItem and BelowItem, which is much too small and makes # dragging painful. In QT 5 it gives a fraction of the heigh, which is much more usable. # Maya uses QT 5, and its built-in UIs like the shape editor have QT 5's behavior, but # for some reason QTreeView, etc. still have QT 4's behavior. # # Work around this by looking at the event position. If it's in a position where an # above or below drag should be happening, snap the drag position to the boundary so # it's treated as an above or below drag. pos = event.pos() index = self.indexAt(event.pos()) if index.isValid(): rect = self.visualRect(index) margin = round(float(rect.height()) / 5.5) margin = min(max(margin, 2), 12) if pos.y() < margin + rect.top() : # Move the drag position to the top of the item, forcing AboveItem. pos.setY(rect.top()) elif pos.y() > rect.bottom() - margin: # Move the drag position to the bottom of the item, forcing BelowItem. pos.setY(rect.bottom()) elif rect.contains(pos, True): # Move the drag position to the center of the item, forcing OnItem. pos.setY((rect.bottom() + rect.top()) / 2) # Create a new, equivalent QDragMoveEvent with our adjusted position. # # The QT docs say not to construct these. But, it doesn't offer any alternative, # there's no setPos, and this works fine. event2 = Qt.QDragMoveEvent( pos, event.dropAction(), event.mimeData(), event.mouseButtons(), event.keyboardModifiers(), event.type()) super(DragFromMayaMixin, self).dragMoveEvent(event2)
def refresh(self): """ Update the UI to reflect the node's current values. """ if not self.shown or self.weight_node is None: return # This is a little tricky. If the selected node is deleted, _weight_nodes_changed # will be called and queue refresh_weight_node_list. However, other callbacks will # also happen and cause refresh to be called first, so we'll try to refresh a deleted # node before we update to notice that it's gone. Check for this before refreshing # the rest of the UI. if not self.weight_node.exists(): self.refresh_weight_node_list() if self.weight_node is None: return self._currently_refreshing = True try: solo = self.weight_node.attr('solo').get() if solo != 0: selection = 0 if solo == 1 else 1 main_weight = 1 self.ui.selectionBar.setEnabled(False) self.ui.keySelection.setEnabled(False) self.ui.mainWeightBar.setEnabled(False) self.ui.keyMainWeight.setEnabled(False) else: selection = self.weight_node.attr('selection').get() main_weight = self.weight_node.attr('mainWeight').get() self.ui.selectionBar.setEnabled(True) self.ui.keySelection.setEnabled(True) self.ui.mainWeightBar.setEnabled(True) self.ui.keyMainWeight.setEnabled(True) self.ui.selectionBar.setValue(int(selection * 1000)) self.ui.mainWeightBar.setValue(int(main_weight * 1000)) palette = Qt.QPalette() color = Qt.QColor(0, 255 * (1 - selection), 0, 255) palette.setColor(Qt.QPalette.Background, color) self.ui.shape1WeightColor.setAutoFillBackground(True) self.ui.shape1WeightColor.setPalette(palette) palette = Qt.QPalette() color = Qt.QColor(0, 255 * selection, 0, 255) palette.setColor(Qt.QPalette.Background, color) self.ui.shape2WeightColor.setAutoFillBackground(True) self.ui.shape2WeightColor.setPalette(palette) solo = self.weight_node.attr('solo').get() self.ui.soloShape1.setChecked(solo == 1) self.ui.soloShape2.setChecked(solo == 2) self._refresh_dropdowns() finally: self._currently_refreshing = False
class DragFromMayaMixin(object): """ This adds support for dragging Maya nodes and attributes into a widget. """ # Note that the arguments for these actually depend on the base class type, and can be eg. a # QListWidgetItem. # source, target (or None for DropIndicatorPosition.OnViewport), dropIndicatorPosition dragged_internally = Qt.Signal(Qt.QTreeWidgetItem, Qt.QAbstractItemView.DropIndicatorPosition) # nodes, target (or None for DropIndicatorPosition.OnViewport), dropIndicatorPosition dragged_from_maya = Qt.Signal(basestring, Qt.QTreeWidgetItem, Qt.QAbstractItemView.DropIndicatorPosition) def __init__(self, parent): super(DragFromMayaMixin, self).__init__(parent) def checkDragFromMaya(self, nodes): """ Given a list of nodes/attributes being dragged into the widget, return a list of which ones should be accepted. If the returned list is empty, the drag will be ignored. """ return pm.ls(nodes, type='transform') def dropEvent(self, event): # QTreeWidget and QListWidget::dropEvent have crazy handling for drops from ourself. They just # move items around on their own assuming the model will have exactly the same behavior, and # then change MoveAction to CopyAction, confusing what's actually happening. # # This doesn't happen if the event is already accepted. QListModeViewBase::dropOn, etc. will # return false and the dropEvent won't do this. It may still do other things, like stop auto-scrolling. # So, do our event checks first, accept the event if we process it, then run the base implementation # last. if 'application/x-maya-data' in event.mimeData().formats(): if event.dropAction() == Qt.Qt.DropAction.CopyAction: event.accept() nodes = event.mimeData().text().rstrip().split() nodes = [pm.PyNode(node) for node in nodes] nodes = self.checkDragFromMaya(nodes) if nodes: target = self.itemAt(event.pos()) self.dragged_from_maya.emit(nodes, target, self.dropIndicatorPosition()) # event.source() crashes if the drag comes from a Maya object, so don't check the other cases. elif event.source() is self: # Always accept drops from ourself (including for move events) to prevent the crazy base class behavior. event.accept() if event.dropAction() == Qt.Qt.DropAction.CopyAction: selected_indexes = self.selectedIndexes() target = self.itemAt(event.pos()) target_index = self.indexAt(event.pos()) # For QTreeWidget, if any index in the selection is a child of the drop position, so the drop # is invalid. Note that we still need to accept the event, or the base class will do nasty # things. if not _any_is_descendant(selected_indexes, target_index): indicator_position = self.dropIndicatorPosition() self.dragged_internally.emit(target, self.dropIndicatorPosition()) super(DragFromMayaMixin, self).dropEvent(event) def dragMoveEvent(self, event): # In QT 4, QAbstractItemViewPrivate::position normally gives a margin of 2 pixels # on the top and bottom for AboveItem and BelowItem, which is much too small and makes # dragging painful. In QT 5 it gives a fraction of the heigh, which is much more usable. # Maya uses QT 5, and its built-in UIs like the shape editor have QT 5's behavior, but # for some reason QTreeView, etc. still have QT 4's behavior. # # Work around this by looking at the event position. If it's in a position where an # above or below drag should be happening, snap the drag position to the boundary so # it's treated as an above or below drag. pos = event.pos() index = self.indexAt(event.pos()) if index.isValid(): rect = self.visualRect(index) margin = round(float(rect.height()) / 5.5) margin = min(max(margin, 2), 12) if pos.y() < margin + rect.top() : # Move the drag position to the top of the item, forcing AboveItem. pos.setY(rect.top()) elif pos.y() > rect.bottom() - margin: # Move the drag position to the bottom of the item, forcing BelowItem. pos.setY(rect.bottom()) elif rect.contains(pos, True): # Move the drag position to the center of the item, forcing OnItem. pos.setY((rect.bottom() + rect.top()) / 2) # Create a new, equivalent QDragMoveEvent with our adjusted position. # # The QT docs say not to construct these. But, it doesn't offer any alternative, # there's no setPos, and this works fine. event2 = Qt.QDragMoveEvent( pos, event.dropAction(), event.mimeData(), event.mouseButtons(), event.keyboardModifiers(), event.type()) super(DragFromMayaMixin, self).dragMoveEvent(event2) def mouseMoveEvent(self, event): # Match Maya's behavior and only drag on MMB-drag, since Qt's LMB-dragging is # broken with multiple selection. buttons = event.buttons() middle_pressed = bool(buttons & Qt.Qt.MiddleButton) self.setDragEnabled(middle_pressed) super(DragFromMayaMixin, self).mouseMoveEvent(event) def dragEnterEvent(self, event): super(DragFromMayaMixin, self).dragEnterEvent(event) # For Maya nodes, check if the nodes are an accepted type. if 'application/x-maya-data' in event.mimeData().formats(): if not event.mimeData().hasText(): event.ignore() return nodes = event.mimeData().text().rstrip().split() nodes = self.checkDragFromMaya(nodes) if not nodes: event.ignore() def mimeTypes(self): """ Add Maya nodes to the MIME types accepted for drag and drop. """ result = super(DragFromMayaMixin, self).mimeTypes() result.append('application/x-maya-data') return result
def add_menu_item(menu, text, func): action = Qt.QAction(text, menu) menu.addAction(action) action.triggered.connect(func) return action
def add_menu(text): menu = Qt.QMenu(text, self.menubar) self.menubar.addAction(menu.menuAction()) return menu
def __init__(self): super(CreateCurveEditor, self).__init__() self.currently_refreshing = False self.node_listener = maya_callbacks.NodeChangeListener( 'zCreateCurve', self.refresh_all) self.callbacks = maya_callbacks.MayaCallbackList() self.callbacks.add( self.refresh_on_selection_changed, lambda func: om.MEventMessage.addEventCallback( 'SelectionChanged', func)) for delete_key in (Qt.Qt.Key_Backspace, Qt.Qt.Key_Delete): shortcut = Qt.QShortcut(Qt.QKeySequence(delete_key), self, None, None, Qt.Qt.WidgetWithChildrenShortcut) shortcut.activated.connect( self.remove_selected_transforms_from_curve) from zMayaTools.qt_generated import zCreateCurve reload(zCreateCurve) # Set up the UI. self.ui = zCreateCurve.Ui_zCreateCurve() self.ui.setupUi(self) # Set up the menu bar. self.menubar = Qt.QMenuBar() self.layout().setMenuBar(self.menubar) menubar = self.menubar # If we create menus with addMenu and addAction, the parent is set, but the object is # still deleted, and we get "internal C++ object already deleted" errors later. This seems # like a QT or PySide bug. def add_menu(text): menu = Qt.QMenu(text, self.menubar) self.menubar.addAction(menu.menuAction()) return menu def add_menu_item(menu, text, func): action = Qt.QAction(text, menu) menu.addAction(action) action.triggered.connect(func) return action menu = add_menu('Node') self.menu_create_node = add_menu_item(menu, 'Create Node', self.create_and_select_node) self.menu_delete_node = add_menu_item(menu, 'Delete Node', self.delete_node) self.menu_select_node = add_menu_item(menu, 'Select Node', self.select_current_node) self.menu_select_curve = add_menu_item(menu, 'Select Output Curve', self.select_current_curve) self.ui.selectNodeButton.clicked.connect(self.select_current_node) self.ui.createNodeButton.clicked.connect(self.create_and_select_node) self.ui.addToCurveButton.clicked.connect( self.append_selected_transforms_to_curve) self.ui.removeFromCurveButton.clicked.connect( self.remove_selected_transforms_from_curve) self.ui.nodeDropdown.currentTextChanged.connect( self.current_node_changed) self.ui.transformList.setSelectionMode( Qt.QAbstractItemView.ExtendedSelection) self.ui.transformList.viewport().setAcceptDrops(True) self.ui.transformList.setDropIndicatorShown(True) self.ui.transformList.setDragDropMode(Qt.QAbstractItemView.DragDrop) self.ui.transformList.checkDragFromMaya = lambda nodes: nodes # allow dropping anything self.ui.transformList.dragged_internally.connect( self.dragged_internally) self.ui.transformList.dragged_from_maya.connect(self.dragged_from_maya) self.ui.transformList.itemSelectionChanged.connect( self.transform_selection_changed) self.ui.transformList.itemDoubleClicked.connect( self.double_clicked_transform)
def __init__(self): super(KeyingWindow, self).__init__() # How do we make our window handle global hotkeys? undo = Qt.QAction('Undo', self) undo.setShortcut(Qt.Qt.CTRL + Qt.Qt.Key_Z) undo.triggered.connect(lambda: pm.undo()) self.addAction(undo) redo = Qt.QAction('Redo', self) redo.setShortcut(Qt.Qt.CTRL + Qt.Qt.Key_Y) redo.triggered.connect(lambda: pm.redo(redo=True)) self.addAction(redo) self.weight_node = None self.shown = False self.callback_ids = om.MCallbackIdArray() self._currently_refreshing = False style = r''' /* Maya's checkbox style makes the checkbox invisible when it's deselected, * which makes it impossible to tell that there's even a checkbox there to * click. Adjust the background color to fix this. */ QTreeView::indicator:unchecked { background-color: #000; } ''' self.setStyleSheet(style) self.time_change_listener = maya_helpers.TimeChangeListener(self._time_changed, pause_during_playback=False) # Make sure zMouthController has been generated. qt_helpers.compile_all_layouts() from zMayaTools.qt_widgets import draggable_progress_bar reload(draggable_progress_bar) from zMayaTools.qt_generated import zMouthController reload(zMouthController) self.ui = zMouthController.Ui_zMouthController() self.ui.setupUi(self) self.ui.selectionBar.setMinimum(0) self.ui.selectionBar.setMaximum(1000) self.ui.mainWeightBar.setMinimum(0) self.ui.mainWeightBar.setMaximum(1000) self.ui.selectNodeButton.clicked.connect(self.select_current_node) self.ui.shapeSelection1.currentIndexChanged.connect(self.shape1Changed) self.ui.shapeSelection2.currentIndexChanged.connect(self.shape2Changed) self.ui.selectedNodeDropdown.currentIndexChanged.connect(self.selectedNodeChanged) self.ui.setKeyShape1.clicked.connect(self.clicked_key_shape_1) self.ui.setKeyShape2.clicked.connect(self.clicked_key_shape_2) self.ui.keySelection.clicked.connect(self.clicked_key_selection) self.ui.keyMainWeight.clicked.connect(self.clicked_key_main_weight) self.ui.soloShape1.clicked.connect(self.solo_shape1) self.ui.soloShape2.clicked.connect(self.solo_shape2) self.ui.selectionBar.mouse_movement.connect(self.set_selection_bar_value) self.ui.mainWeightBar.mouse_movement.connect(self.set_main_weight) self.ui.selectionBar.set_undo_chunk_around_dragging('Dragging selection') self.ui.mainWeightBar.set_undo_chunk_around_dragging('Dragging weight') # This will call selectedNodeChanged, and trigger the rest of the refresh. self.refresh_weight_node_list()