This commit is contained in:
2025-11-23 23:31:18 +08:00
parent d60cdc52fd
commit 9f7667a475
710 changed files with 252869 additions and 6 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
2023/icons/dwpicker.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
2023/icons/mgpicker.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -0,0 +1,722 @@
'''
Name: animation_retargeting_tool
Description: Transfer animation data between rigs or transfer raw mocap from a skeleton to a custom rig.
Author: Joar Engberg 2021
Installation:
Add animation_retargeting_tool.py to your Maya scripts folder (Username\Documents\maya\scripts).
To start the tool within Maya, run these this lines of code from the Maya script editor or add them to a shelf button:
import animation_retargeting_tool
animation_retargeting_tool.start()
'''
from collections import OrderedDict
import os
import sys
import webbrowser
import maya.mel
import maya.cmds as cmds
from functools import partial
import maya.OpenMayaUI as omui
maya_version = int(cmds.about(version=True))
if maya_version < 2025:
from shiboken2 import wrapInstance
from PySide2 import QtCore, QtGui, QtWidgets
else:
from shiboken6 import wrapInstance
from PySide6 import QtCore, QtGui, QtWidgets
def maya_main_window():
# Return the Maya main window as QMainWindow
main_window = omui.MQtUtil.mainWindow()
if sys.version_info.major >= 3:
return wrapInstance(int(main_window), QtWidgets.QWidget)
else:
return wrapInstance(long(main_window), QtWidgets.QWidget) # type: ignore
class RetargetingTool(QtWidgets.QDialog):
'''
Retargeting tool class
'''
WINDOW_TITLE = "Animation Retargeting Tool"
def __init__(self):
super(RetargetingTool, self).__init__(maya_main_window())
self.script_job_ids = []
self.connection_ui_widgets = []
self.color_counter = 0
self.maya_color_index = OrderedDict([(13, "red"), (18, "cyan"), (14, "lime"), (17, "yellow")])
self.cached_connect_nodes = []
self.setWindowTitle(self.WINDOW_TITLE)
self.setWindowFlags(self.windowFlags() ^ QtCore.Qt.WindowContextHelpButtonHint)
self.resize(400, 300)
self.create_ui_widgets()
self.create_ui_layout()
self.create_ui_connections()
self.create_script_jobs()
if cmds.about(macOS=True):
self.setWindowFlags(QtCore.Qt.Tool)
def create_ui_widgets(self):
self.refresh_button = QtWidgets.QPushButton(QtGui.QIcon(":refresh.png"), "")
self.simple_conn_button = QtWidgets.QPushButton("Create Connection")
self.ik_conn_button = QtWidgets.QPushButton("Create IK Connection")
self.bake_button = QtWidgets.QPushButton("Bake Animation")
self.bake_button.setStyleSheet("background-color: lightgreen; color: black")
self.batch_bake_button = QtWidgets.QPushButton("Batch Bake And Export ...")
self.help_button = QtWidgets.QPushButton("?")
self.help_button.setFixedWidth(25)
self.rot_checkbox = QtWidgets.QCheckBox("Rotation")
self.pos_checkbox = QtWidgets.QCheckBox("Translation")
self.mo_checkbox = QtWidgets.QCheckBox("Maintain Offset")
self.snap_checkbox = QtWidgets.QCheckBox("Align To Position")
def create_ui_layout(self):
horizontal_layout_1 = QtWidgets.QHBoxLayout()
horizontal_layout_1.addWidget(self.pos_checkbox)
horizontal_layout_1.addWidget(self.rot_checkbox)
horizontal_layout_1.addWidget(self.snap_checkbox)
horizontal_layout_1.addStretch()
horizontal_layout_1.addWidget(self.help_button)
horizontal_layout_2 = QtWidgets.QHBoxLayout()
horizontal_layout_2.addWidget(self.simple_conn_button)
horizontal_layout_2.addWidget(self.ik_conn_button)
horizontal_layout_3 = QtWidgets.QHBoxLayout()
horizontal_layout_3.addWidget(self.batch_bake_button)
horizontal_layout_3.addWidget(self.bake_button)
connection_list_widget = QtWidgets.QWidget()
self.connection_layout = QtWidgets.QVBoxLayout(connection_list_widget)
self.connection_layout.setContentsMargins(2, 2, 2, 2)
self.connection_layout.setSpacing(3)
self.connection_layout.setAlignment(QtCore.Qt.AlignTop)
list_scroll_area = QtWidgets.QScrollArea()
list_scroll_area.setWidgetResizable(True)
list_scroll_area.setWidget(connection_list_widget)
separator_line = QtWidgets.QFrame(parent=None)
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.setContentsMargins(10, 10, 10, 10)
main_layout.addWidget(list_scroll_area)
main_layout.addLayout(horizontal_layout_1)
main_layout.addLayout(horizontal_layout_2)
main_layout.addWidget(separator_line)
main_layout.addLayout(horizontal_layout_3)
def create_ui_connections(self):
self.simple_conn_button.clicked.connect(self.create_connection_node)
self.ik_conn_button.clicked.connect(self.create_ik_connection_node)
self.refresh_button.clicked.connect(self.refresh_ui_list)
self.bake_button.clicked.connect(self.bake_animation_confirm)
self.batch_bake_button.clicked.connect(self.open_batch_window)
self.help_button.clicked.connect(self.help_dialog)
self.rot_checkbox.setChecked(True)
self.pos_checkbox.setChecked(True)
self.snap_checkbox.setChecked(True)
def create_script_jobs(self):
self.script_job_ids.append(cmds.scriptJob(event=["SelectionChanged", partial(self.refresh_ui_list)]))
self.script_job_ids.append(cmds.scriptJob(event=["NameChanged", partial(self.refresh_ui_list)]))
def kill_script_jobs(self):
for id in self.script_job_ids:
if cmds.scriptJob(exists=id):
cmds.scriptJob(kill=id)
else:
pass
def refresh_ui_list(self):
self.clear_list()
connect_nodes_in_scene = RetargetingTool.get_connect_nodes()
self.cached_connect_nodes = connect_nodes_in_scene
for node in connect_nodes_in_scene:
connection_ui_item = ListItemWidget(parent_instance=self, connection_node=node)
self.connection_layout.addWidget(connection_ui_item)
self.connection_ui_widgets.append(connection_ui_item)
def clear_list(self):
self.connection_ui_widgets = []
while self.connection_layout.count() > 0:
connection_ui_item = self.connection_layout.takeAt(0)
if connection_ui_item.widget():
connection_ui_item.widget().deleteLater()
def showEvent(self, event):
self.refresh_ui_list()
def closeEvent(self, event):
self.kill_script_jobs()
self.clear_list()
def create_connection_node(self):
try:
selected_joint = cmds.ls(selection=True)[0]
selected_ctrl = cmds.ls(selection=True)[1]
except:
return cmds.warning("No selections!")
if self.snap_checkbox.isChecked() == True:
cmds.matchTransform(selected_ctrl, selected_joint, pos=True)
else:
pass
if self.rot_checkbox.isChecked() == True and self.pos_checkbox.isChecked() == False:
suffix = "_ROT"
elif self.pos_checkbox.isChecked() == True and self.rot_checkbox.isChecked() == False:
suffix = "_TRAN"
else:
suffix = "_TRAN_ROT"
locator = self.create_ctrl_sphere(selected_joint+suffix)
# Add message attr
cmds.addAttr(locator, longName="ConnectNode", attributeType="message")
cmds.addAttr(selected_ctrl, longName="ConnectedCtrl", attributeType="message")
cmds.connectAttr(locator+".ConnectNode",selected_ctrl+".ConnectedCtrl")
cmds.parent(locator, selected_joint)
cmds.xform(locator, rotation=(0, 0, 0))
cmds.xform(locator, translation=(0, 0, 0))
# Select the type of constraint based on the ui checkboxes
if self.rot_checkbox.isChecked() == True and self.pos_checkbox.isChecked() == True:
cmds.parentConstraint(locator, selected_ctrl, maintainOffset=True)
elif self.rot_checkbox.isChecked() == True and self.pos_checkbox.isChecked() == False:
cmds.orientConstraint(locator, selected_ctrl, maintainOffset=True)
elif self.pos_checkbox.isChecked() == True and self.rot_checkbox.isChecked() == False:
cmds.pointConstraint(locator, selected_ctrl, maintainOffset=True)
else:
cmds.warning("Select translation and/or rotation!")
cmds.delete(locator)
cmds.deleteAttr(selected_ctrl, at="ConnectedCtrl")
self.refresh_ui_list()
def create_ik_connection_node(self):
try:
selected_joint = cmds.ls(selection=True)[0]
selected_ctrl = cmds.ls(selection=True)[1]
except:
return cmds.warning("No selections!")
self.rot_checkbox.setChecked(True)
self.pos_checkbox.setChecked(True)
if self.snap_checkbox.isChecked() == True:
cmds.matchTransform(selected_ctrl, selected_joint, pos=True)
else:
pass
tran_locator = self.create_ctrl_sphere(selected_joint+"_TRAN")
cmds.parent(tran_locator, selected_joint)
cmds.xform(tran_locator, rotation=(0, 0, 0))
cmds.xform(tran_locator, translation=(0, 0, 0))
rot_locator = self.create_ctrl_locator(selected_joint+"_ROT")
# Add message attributes and connect them
cmds.addAttr(tran_locator, longName="ConnectNode", attributeType="message")
cmds.addAttr(rot_locator, longName="ConnectNode", attributeType="message")
cmds.addAttr(selected_ctrl, longName="ConnectedCtrl", attributeType="message")
cmds.connectAttr(tran_locator+".ConnectNode",selected_ctrl+".ConnectedCtrl")
cmds.parent(rot_locator, tran_locator)
cmds.xform(rot_locator, rotation=(0, 0, 0))
cmds.xform(rot_locator, translation=(0, 0, 0))
joint_parent = cmds.listRelatives(selected_joint, parent=True)[0]
cmds.parent(tran_locator, joint_parent)
cmds.makeIdentity(tran_locator, apply=True, translate=True)
cmds.orientConstraint(selected_joint, tran_locator, maintainOffset=False)
cmds.parentConstraint(rot_locator, selected_ctrl, maintainOffset=True)
# Lock and hide attributes
cmds.setAttr(rot_locator+".tx", lock=True, keyable=False)
cmds.setAttr(rot_locator+".ty", lock=True, keyable=False)
cmds.setAttr(rot_locator+".tz", lock=True, keyable=False)
cmds.setAttr(tran_locator+".rx", lock=True, keyable=False)
cmds.setAttr(tran_locator+".ry", lock=True, keyable=False)
cmds.setAttr(tran_locator+".rz", lock=True, keyable=False)
self.refresh_ui_list()
def scale_ctrl_shape(self, controller, size):
cmds.select(self.get_cvs(controller), replace=True)
cmds.scale(size, size, size)
cmds.select(clear=True)
def get_cvs(self, object):
children = cmds.listRelatives(object, type="shape", children=True)
ctrl_vertices = []
for c in children:
spans = int(cmds.getAttr(c+".spans")) + 1
vertices = "{shape}.cv[0:{count}]".format(shape=c, count=spans)
ctrl_vertices.append(vertices)
return ctrl_vertices
def create_ctrl_locator(self, ctrl_shape_name):
curves = []
curves.append(cmds.curve(degree=1, p=[(0, 0, 1), (0, 0, -1)], k=[0,1]))
curves.append(cmds.curve(degree=1, p=[(1, 0, 0), (-1, 0, 0)], k=[0,1]))
curves.append(cmds.curve(degree=1, p=[(0, 1, 0), (0, -1, 0)], k=[0,1]))
locator = self.combine_shapes(curves, ctrl_shape_name)
cmds.setAttr(locator+".overrideEnabled", 1)
cmds.setAttr(locator+".overrideColor", list(self.maya_color_index.keys())[self.color_counter])
return locator
def create_ctrl_sphere(self, ctrl_shape_name):
circles = []
for n in range(0, 5):
circles.append(cmds.circle(normal=(0,0,0), center=(0,0,0))[0])
cmds.rotate(0, 45, 0, circles[0])
cmds.rotate(0, -45, 0, circles[1])
cmds.rotate(0, -90, 0, circles[2])
cmds.rotate(90, 0, 0, circles[3])
sphere = self.combine_shapes(circles, ctrl_shape_name)
cmds.setAttr(sphere+".overrideEnabled", 1)
cmds.setAttr(sphere+".overrideColor", list(self.maya_color_index.keys())[self.color_counter])
self.scale_ctrl_shape(sphere, 0.5)
return sphere
def combine_shapes(self, shapes, ctrl_shape_name):
shape_nodes = cmds.listRelatives(shapes, shapes=True)
output_node = cmds.group(empty=True, name=ctrl_shape_name)
cmds.makeIdentity(shapes, apply=True, translate=True, rotate=True, scale=True)
cmds.parent(shape_nodes, output_node, shape=True, relative=True)
cmds.delete(shape_nodes, constructionHistory=True)
cmds.delete(shapes)
return output_node
def bake_animation_confirm(self):
confirm = cmds.confirmDialog(title="Confirm", message="Baking the animation will delete all the connection nodes. Do you wish to proceed?", button=["Yes","No"], defaultButton="Yes", cancelButton="No")
if confirm == "Yes":
progress_dialog = QtWidgets.QProgressDialog("Baking animation", None, 0, -1, self)
progress_dialog.setWindowFlags(progress_dialog.windowFlags() ^ QtCore.Qt.WindowCloseButtonHint)
progress_dialog.setWindowFlags(progress_dialog.windowFlags() ^ QtCore.Qt.WindowContextHelpButtonHint)
progress_dialog.setWindowTitle("Progress...")
progress_dialog.setWindowModality(QtCore.Qt.WindowModal)
progress_dialog.show()
QtCore.QCoreApplication.processEvents()
# Bake animation
self.bake_animation()
progress_dialog.close()
if confirm == "No":
pass
self.refresh_ui_list()
def help_dialog(self):
confirm = cmds.confirmDialog(
title="How to use",
message="To create a connection simply select the driver and then the driven and click 'Create connection'. For IK hands and IK feet controllers you can use 'Create IK Connection' for more complex retargeting. \n \nAs an example: if you want to transfer animation from a skeleton to a rig, first select the animated joint and then select the controller before you create a connection.",
button=["How to use the retargeting tool (Youtube)", "How to use the batch exporter (Youtube)", "Cancel"],
defaultButton="Cancel",
cancelButton="Cancel",
dismissString="Cancel")
if confirm == "How to use the retargeting tool (Youtube)":
webbrowser.open_new("https://youtu.be/x2-agPVfinc")
elif confirm == "How to use the batch exporter (Youtube)":
webbrowser.open_new("https://youtu.be/KOURUtN36ko")
def open_batch_window(self):
try:
self.settings_window.close()
self.settings_window.deleteLater()
except:
pass
self.settings_window = BatchExport()
self.settings_window.show()
@classmethod
def bake_animation(cls):
if len(cls.get_connected_ctrls()) == 0:
cmds.warning("No connections found in scene!")
if len(cls.get_connected_ctrls()) != 0:
time_min = cmds.playbackOptions(query=True, min=True)
time_max = cmds.playbackOptions(query=True, max=True)
# Bake the animation
cmds.refresh(suspend=True)
cmds.bakeResults(cls.get_connected_ctrls(), t=(time_min, time_max), sb=1, at=["rx","ry","rz","tx","ty","tz"], hi="none")
cmds.refresh(suspend=False)
# Delete the connect nodes
for node in cls.get_connect_nodes():
try:
cmds.delete(node)
except:
pass
# Remove the message attribute from the controllers
for ctrl in cls.get_connected_ctrls():
try:
cmds.deleteAttr(ctrl, attribute="ConnectedCtrl")
except:
pass
@classmethod
def get_connect_nodes(cls):
connect_nodes_in_scene = []
for i in cmds.ls():
if cmds.attributeQuery("ConnectNode", node=i, exists=True) == True:
connect_nodes_in_scene.append(i)
else:
pass
return connect_nodes_in_scene
@classmethod
def get_connected_ctrls(cls):
connected_ctrls_in_scene = []
for i in cmds.ls():
if cmds.attributeQuery("ConnectedCtrl", node=i, exists=True) == True:
connected_ctrls_in_scene.append(i)
else:
pass
return connected_ctrls_in_scene
class ListItemWidget(QtWidgets.QWidget):
'''
UI list item class.
When a new List Item is created it gets added to the connection_list_widget in the RetargetingTool class.
'''
def __init__(self, connection_node, parent_instance):
super(ListItemWidget, self).__init__()
self.connection_node = connection_node
self.main = parent_instance
self.setFixedHeight(26)
self.create_ui_widgets()
self.create_ui_layout()
self.create_ui_connections()
# If there is already connection nodes in the scene update the color counter
try:
current_override = cmds.getAttr(self.connection_node+".overrideColor")
self.main.color_counter = self.main.maya_color_index.keys().index(current_override)
except:
pass
def create_ui_widgets(self):
self.color_button = QtWidgets.QPushButton()
self.color_button.setFixedSize(20, 20)
self.color_button.setStyleSheet("background-color:" + self.get_current_color())
self.sel_button = QtWidgets.QPushButton()
self.sel_button.setStyleSheet("background-color: #707070")
self.sel_button.setText("Select")
self.sel_button.setFixedWidth(80)
self.del_button = QtWidgets.QPushButton()
self.del_button.setStyleSheet("background-color: #707070")
self.del_button.setText("Delete")
self.del_button.setFixedWidth(80)
self.transform_name_label = QtWidgets.QLabel(self.connection_node)
self.transform_name_label.setAlignment(QtCore.Qt.AlignCenter)
self.transform_name_label.setStyleSheet("color: darkgray")
for selected in cmds.ls(selection=True):
if selected == self.connection_node:
self.transform_name_label.setStyleSheet("color: white")
def create_ui_layout(self):
main_layout = QtWidgets.QHBoxLayout(self)
main_layout.setContentsMargins(5, 5, 20, 0)
main_layout.addWidget(self.color_button)
main_layout.addWidget(self.transform_name_label)
main_layout.addWidget(self.sel_button)
main_layout.addWidget(self.del_button)
def create_ui_connections(self):
self.sel_button.clicked.connect(self.select_connection_node)
self.del_button.clicked.connect(self.delete_connection_node)
self.color_button.clicked.connect(self.set_color)
def select_connection_node(self):
cmds.select(self.connection_node)
for widget in self.main.connection_ui_widgets:
widget.transform_name_label.setStyleSheet("color: darkgray")
self.transform_name_label.setStyleSheet("color: white")
def delete_connection_node(self):
try:
for attr in cmds.listConnections(self.connection_node, destination=True):
if cmds.attributeQuery("ConnectedCtrl", node=attr, exists=True):
cmds.deleteAttr(attr, at="ConnectedCtrl")
except:
pass
cmds.delete(self.connection_node)
self.main.refresh_ui_list()
def set_color(self):
# Set the color on the connection node and button
connection_nodes = self.main.cached_connect_nodes
color = list(self.main.maya_color_index.keys())
if self.main.color_counter < 3:
self.main.color_counter += 1
else:
self.main.color_counter = 0
for node in connection_nodes:
cmds.setAttr(node+".overrideEnabled", 1)
cmds.setAttr(node+".overrideColor", color[self.main.color_counter])
for widget in self.main.connection_ui_widgets:
widget.color_button.setStyleSheet("background-color:"+self.get_current_color())
def get_current_color(self):
current_color_index = cmds.getAttr(self.connection_node+".overrideColor")
color_name = self.main.maya_color_index.get(current_color_index, "grey")
return color_name
class BatchExport(QtWidgets.QDialog):
'''
Batch exporter class
'''
WINDOW_TITLE = "Batch Exporter"
def __init__(self):
super(BatchExport, self).__init__(maya_main_window())
self.setWindowTitle(self.WINDOW_TITLE)
self.setWindowFlags(self.windowFlags() ^ QtCore.Qt.WindowContextHelpButtonHint)
self.resize(400, 250)
self.animation_clip_paths = []
self.output_folder = ""
if cmds.about(macOS=True):
self.setWindowFlags(QtCore.Qt.Tool)
self.create_ui()
self.create_connections()
def create_ui(self):
self.file_list_widget = QtWidgets.QListWidget()
self.remove_selected_button = QtWidgets.QPushButton("Remove Selected")
self.remove_selected_button.setFixedHeight(24)
self.load_anim_button = QtWidgets.QPushButton("Load Animations")
self.load_anim_button.setFixedHeight(24)
self.export_button = QtWidgets.QPushButton("Batch Export Animations")
self.export_button.setStyleSheet("background-color: lightgreen; color: black")
self.connection_file_line = QtWidgets.QLineEdit()
self.connection_file_line.setToolTip("Enter the file path to the connection rig file. A file which contains a rig with connections.")
self.connection_filepath_button = QtWidgets.QPushButton()
self.connection_filepath_button.setIcon(QtGui.QIcon(":fileOpen.png"))
self.connection_filepath_button.setFixedSize(24, 24)
self.export_selected_label = QtWidgets.QLabel("Export Selected (Optional):")
self.export_selected_line = QtWidgets.QLineEdit()
self.export_selected_line.setToolTip("Enter the name(s) of the nodes that should be exported. Leave blank to export all.")
self.export_selected_button = QtWidgets.QPushButton()
self.export_selected_button.setIcon(QtGui.QIcon(":addClip.png"))
self.export_selected_button.setFixedSize(24, 24)
self.output_filepath_button = QtWidgets.QPushButton()
self.output_filepath_button.setIcon(QtGui.QIcon(":fileOpen.png"))
self.file_type_combo = QtWidgets.QComboBox()
self.file_type_combo.addItems([".fbx", ".ma"])
horizontal_layout_1 = QtWidgets.QHBoxLayout()
horizontal_layout_1.addWidget(QtWidgets.QLabel("Connection Rig File:"))
horizontal_layout_1.addWidget(self.connection_file_line)
horizontal_layout_1.addWidget(self.connection_filepath_button)
horizontal_layout_2 = QtWidgets.QHBoxLayout()
horizontal_layout_2.addWidget(self.load_anim_button)
horizontal_layout_2.addWidget(self.remove_selected_button)
horizontal_layout_3 = QtWidgets.QHBoxLayout()
horizontal_layout_3.addWidget(QtWidgets.QLabel("Output File Type:"))
horizontal_layout_3.addWidget(self.file_type_combo)
horizontal_layout_3.addWidget(self.export_button)
horizontal_layout_4 = QtWidgets.QHBoxLayout()
horizontal_layout_4.addWidget(self.export_selected_label)
horizontal_layout_4.addWidget(self.export_selected_line)
horizontal_layout_4.addWidget(self.export_selected_button)
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.addWidget(self.file_list_widget)
main_layout.addLayout(horizontal_layout_2)
main_layout.addLayout(horizontal_layout_1)
main_layout.addLayout(horizontal_layout_4)
main_layout.addLayout(horizontal_layout_3)
def create_connections(self):
self.connection_filepath_button.clicked.connect(self.connection_filepath_dialog)
self.load_anim_button.clicked.connect(self.animation_filepath_dialog)
self.export_button.clicked.connect(self.batch_action)
self.export_selected_button.clicked.connect(self.add_selected_action)
self.remove_selected_button.clicked.connect(self.remove_selected_item)
def connection_filepath_dialog(self):
file_path = QtWidgets.QFileDialog.getOpenFileName(self, "Select Connection Rig File", "", "Maya ACSII (*.ma);;All files (*.*)")
if file_path[0]:
self.connection_file_line.setText(file_path[0])
def output_filepath_dialog(self):
folder_path = QtWidgets.QFileDialog.getExistingDirectory(self, "Select export folder path", "")
if folder_path:
self.output_folder = folder_path
return True
else:
return False
def animation_filepath_dialog(self):
file_paths = QtWidgets.QFileDialog.getOpenFileNames(self, "Select Animation Clips", "", "FBX (*.fbx);;All files (*.*)")
file_path_list = file_paths[0]
if file_path_list[0]:
for i in file_path_list:
self.file_list_widget.addItem(i)
for i in range(0, self.file_list_widget.count()):
self.file_list_widget.item(i).setTextColor(QtGui.QColor("white"))
def add_selected_action(self):
selection = cmds.ls(selection=True)
if len(selection) > 1:
text_string = "["
for i in selection:
text_string += '"{}", '.format(i)
text_string = text_string[:-2]
text_string += "]"
elif selection[0]:
text_string = "{}".format(selection[0])
else:
pass
self.export_selected_line.setText(text_string)
def remove_selected_item(self):
try:
selected_items = self.file_list_widget.selectedItems()
for item in selected_items:
self.file_list_widget.takeItem(self.file_list_widget.row(item))
except:
pass
def batch_action(self):
if self.connection_file_line.text() == "":
cmds.warning("Connection file textfield is empty. Add a connection rig file to be able to export. This file should contain the rig and connections to a skeleton.")
elif self.file_list_widget.count() == 0:
cmds.warning("Animation clip list is empty. Add animation clips to the list to be able to export!")
else:
confirm_dialog = self.output_filepath_dialog()
if confirm_dialog == True:
self.bake_export()
else:
pass
def bake_export(self):
self.animation_clip_paths = []
for i in range(self.file_list_widget.count()):
self.animation_clip_paths.append(self.file_list_widget.item(i).text())
number_of_operations = len(self.animation_clip_paths) * 3
current_operation = 0
progress_dialog = QtWidgets.QProgressDialog("Preparing", "Cancel", 0, number_of_operations, self)
progress_dialog.setWindowFlags(progress_dialog.windowFlags() ^ QtCore.Qt.WindowCloseButtonHint)
progress_dialog.setWindowFlags(progress_dialog.windowFlags() ^ QtCore.Qt.WindowContextHelpButtonHint)
progress_dialog.setValue(0)
progress_dialog.setWindowTitle("Progress...")
progress_dialog.setWindowModality(QtCore.Qt.WindowModal)
progress_dialog.show()
QtCore.QCoreApplication.processEvents()
export_result = []
for i, path in enumerate(self.animation_clip_paths):
# Import connection file and animation clip
progress_dialog.setLabelText("Baking and exporting {} of {}".format(i + 1, len(self.animation_clip_paths)))
self.file_list_widget.item(i).setTextColor(QtGui.QColor("yellow"))
cmds.file(new=True, force=True)
cmds.file(self.connection_file_line.text(), open=True)
maya.mel.eval('FBXImportMode -v "exmerge";')
maya.mel.eval('FBXImport -file "{}";'.format(path))
current_operation += 1
progress_dialog.setValue(current_operation)
# Bake animation
RetargetingTool.bake_animation()
current_operation += 1
progress_dialog.setValue(current_operation)
# Export animation
output_path = self.output_folder + "/" + os.path.splitext(os.path.basename(path))[0]
if self.file_type_combo.currentText() == ".fbx":
output_path += ".fbx"
cmds.file(rename=output_path)
if self.export_selected_line.text() != "":
cmds.select(self.export_selected_line.text(), replace=True)
maya.mel.eval('FBXExport -f "{}" -s'.format(output_path))
else:
maya.mel.eval('FBXExport -f "{}"'.format(output_path))
elif self.file_type_combo.currentText() == ".ma":
output_path += ".ma"
cmds.file(rename=output_path)
if self.export_selected_line.text() != "":
cmds.select(self.export_selected_line.text(), replace=True)
cmds.file(exportSelected=True, type="mayaAscii")
else:
cmds.file(exportAll=True, type="mayaAscii")
current_operation += 1
progress_dialog.setValue(current_operation)
if os.path.exists(output_path):
self.file_list_widget.item(i).setTextColor(QtGui.QColor("lime"))
export_result.append("Sucessfully exported: "+output_path)
else:
self.file_list_widget.item(i).setTextColor(QtGui.QColor("red"))
export_result.append("Failed exporting: "+output_path)
print("------")
for i in export_result:
print(i)
print("------")
progress_dialog.setValue(number_of_operations)
progress_dialog.close()
def start():
global retarget_tool_ui
try:
retarget_tool_ui.close()
retarget_tool_ui.deleteLater()
except:
pass
retarget_tool_ui = RetargetingTool()
retarget_tool_ui.show()
if __name__ == "__main__":
start()

View File

@@ -0,0 +1,62 @@
# DreamWall Picker
Animation picker for Autodesk Maya 2017 (or higher)
## 作者
- Lionel Brouyère
- Olivier Evers
> This tool is a fork of Hotbox Designer (Lionel Brouyère).
> A menus, markmenu and hotbox designer cross DCC.
> https://github.com/luckylyk/hotbox_designer
## 功能特性
- 简单快速的 picker 创建
- 导入 2022 年之前的 AnimSchool pickers
- 在 Maya 场景中存储 picker
- 高级 picker 编辑器
- 实现 AnimSchool picker 的所有功能,甚至更多...
## 使用方法
### 在 Python 中启动
```python
import animation_tools.dwpicker
animation_tools.dwpicker.show()
```
### 带参数启动
```python
import animation_tools.dwpicker
# 只读模式
animation_tools.dwpicker.show(editable=False)
# 加载指定的 picker 文件
animation_tools.dwpicker.show(pickers=['/path/to/picker.json'])
# 忽略场景中的 pickers
animation_tools.dwpicker.show(ignore_scene_pickers=True)
```
## 模块修改说明
此模块已从原始的 dwpicker 包修改为 animation_tools.dwpicker 子模块:
1. **主模块导入**dwpicker/*.py`from dwpicker.xxx``from .xxx`
2. **一级子模块导入**designer/*.py
- 引用父模块:`from dwpicker.xxx``from ..xxx`
- 引用同级:`from .canvas` → 保持不变
3. **二级子模块导入**ingest/animschool/*.py
- 引用根模块:`from dwpicker.xxx``from ...xxx`(三个点)
- 引用同级:`from .parser` → 保持不变
4. **跨模块导入**`from dwpicker import xxx``from .. import xxx`
5. **代码执行模板**`import dwpicker``import animation_tools.dwpicker as dwpicker`
6. 保持所有原始功能不变
## 官方文档
更多信息请访问 [Official Documentation](https://dreamwall-animation.github.io/dwpicker)

View File

@@ -0,0 +1,208 @@
from maya import cmds
from .main import DwPicker, WINDOW_CONTROL_NAME
from .optionvar import ensure_optionvars_exists
from .namespace import detect_picker_namespace
from .qtutils import remove_workspace_control
from .updatechecker import warn_if_update_available
_dwpicker = None
def show(
editable=True,
pickers=None,
ignore_scene_pickers=False,
replace_namespace_function=None,
list_namespaces_function=None):
"""
This is the dwpicker default startup function.
kwargs:
editable: bool
This allow users to do local edit on their picker. This is NOT
affecting the original file.
pickers: list[str]
Path to pickers to open. If scene contains already pickers,
they are going to be ignored.
ignore_scene_pickers: bool
This is loading the picker empty, ignoring the scene content.
replace_namespace_function: callable
Function used when on each target when a namespace switch is
triggered. Function must follow this templace:
def function(target: str, namespace: str)
-> new_target_name: str
list_namespaces_function: callable
Function used when the picker is listing the scene existing
namespace. The default behavior list all the scene namespaces, but
in some studios, lot of namespace are not relevant to list, this
by this way you can customize how it does works.
def function():
-> List[str]
return:
DwPicker: window
"""
ensure_optionvars_exists()
global _dwpicker
if not _dwpicker:
warn_if_update_available()
_dwpicker = DwPicker(
replace_namespace_function=replace_namespace_function,
list_namespaces_function=list_namespaces_function)
try:
_dwpicker.show(dockable=True)
except RuntimeError:
# Workspace control already exists, UI restore as probably failed.
remove_workspace_control(WINDOW_CONTROL_NAME)
_dwpicker.show()
_dwpicker.set_editable(editable)
if not ignore_scene_pickers and not pickers:
_dwpicker.load_saved_pickers()
if not pickers:
return _dwpicker
_dwpicker.clear()
for filename in pickers:
try:
print(filename)
_dwpicker.add_picker_from_file(filename)
except BaseException:
import traceback
print("Not able to load: {}".format(filename))
print(traceback.format_exc())
_dwpicker.store_local_pickers_data()
return _dwpicker
def toggle():
"""
Switch the DwPicker visibility.
"""
if not _dwpicker:
return show()
_dwpicker.setVisible(not _dwpicker.isVisible())
def close():
"""
Close properly the DwPicker.
It unregister all DwPicker remaining callbacks.
"""
global _dwpicker
if not _dwpicker:
return
_dwpicker.unregister_callbacks()
for i in range(_dwpicker.tab.count()):
picker = _dwpicker.tab.widget(i)
picker.unregister_callbacks()
_dwpicker.close()
_dwpicker = None
class disable():
"""
This context manager temporarily disable the picker callbacks.
This is usefull to decorate code which change the maya selection multiple
times. This can lead constant refresh of the picker and lead performance
issue. This should fix it.
"""
def __enter__(self):
if _dwpicker is None:
return
_dwpicker.unregister_callbacks()
for i in range(_dwpicker.tab.count()):
picker = _dwpicker.tab.widget(i)
picker.unregister_callbacks()
def __exit__(self, *_):
if _dwpicker is None:
return
_dwpicker.register_callbacks()
for i in range(_dwpicker.tab.count()):
picker = _dwpicker.tab.widget(i)
picker.register_callbacks()
def current():
"""
Get the current picker widget visible in the main tab widget.
"""
if not _dwpicker:
return
return _dwpicker.tab.currentWidget()
def refresh():
"""
Trigger this function to refresh ui if the picker datas has been changed
manually inside the scene.
"""
if not _dwpicker:
return
def open_picker_file(filepath):
"""
Add programmatically a picker to the main UI.
"""
if not _dwpicker:
return cmds.warning('Please open picker first.')
_dwpicker.add_picker_from_file(filepath)
_dwpicker.store_local_pickers_data()
def current_namespace():
"""
Returns the namespace of the current displayed picker.
"""
picker = current()
if not picker:
return ':'
return detect_picker_namespace(picker.document.shapes)
def set_layer_visible(layername, visible=True):
if not _dwpicker:
return cmds.warning('Please open picker first.')
picker = current()
if not picker:
return
if visible:
if layername in picker.layers_menu.hidden_layers:
picker.layers_menu.hidden_layers.remove(layername)
picker.update()
return
if layername not in picker.layers_menu.hidden_layers:
picker.layers_menu.hidden_layers.append(layername)
picker.update()
def toggle_layer_visibility(layername):
if not _dwpicker:
return cmds.warning('Please open picker first.')
picker = current()
if not picker:
return
if layername in picker.layers_menu.hidden_layers:
picker.layers_menu.hidden_layers.remove(layername)
else:
picker.layers_menu.hidden_layers.append(layername)
picker.update()
def get_shape(shape_id):
picker = current()
if not picker:
return
return picker.document.shapes_by_id.get(shape_id)

View File

@@ -0,0 +1,113 @@
from .pyside import QtCore
from .geometry import split_line
def align_shapes(shapes, direction):
_direction_matches[direction](shapes)
def align_left(shapes):
left = min(s.bounding_rect().left() for s in shapes)
for shape in shapes:
shape_left = left + (shape.rect.left() - shape.bounding_rect().left())
shape.rect.moveLeft(shape_left)
shape.synchronize_rect()
shape.update_path()
def align_h_center(shapes):
x = sum(s.bounding_rect().center().x() for s in shapes) / len(shapes)
for shape in shapes:
offset = shape.bounding_rect().center().x() - shape.rect.center().x()
shape_x = x - offset
shape.rect.moveCenter(QtCore.QPointF(shape_x, shape.rect.center().y()))
shape.synchronize_rect()
shape.update_path()
def align_right(shapes):
right = max(s.bounding_rect().right() for s in shapes)
for shape in shapes:
offset = right - shape.bounding_rect().right()
shape.rect.moveLeft(shape.rect.left() + offset)
shape.synchronize_rect()
shape.update_path()
def align_top(shapes):
top = min(s.bounding_rect().top() for s in shapes)
for shape in shapes:
shape_top = top + (shape.rect.top() - shape.bounding_rect().top())
shape.rect.moveTop(shape_top)
shape.synchronize_rect()
shape.update_path()
def align_v_center(shapes):
y = sum(s.bounding_rect().center().y() for s in shapes) / len(shapes)
for shape in shapes:
offset = shape.bounding_rect().center().y() - shape.rect.center().y()
shape_y = y - offset
shape.rect.moveCenter(QtCore.QPointF(shape.rect.center().x(), shape_y))
shape.synchronize_rect()
shape.update_path()
def align_bottom(shapes):
bottom = max(s.bounding_rect().bottom() for s in shapes)
for shape in shapes:
offset = shape.rect.bottom() - shape.bounding_rect().bottom()
shape_bottom = bottom + offset
shape.rect.moveBottom(shape_bottom)
shape.synchronize_rect()
shape.update_path()
def arrange_horizontal(shapes):
if len(shapes) < 3:
return
shapes = sorted(shapes, key=lambda s: s.bounding_rect().center().x())
centers = split_line(
point1=shapes[0].bounding_rect().center(),
point2=shapes[-1].bounding_rect().center(),
step_number=len(shapes))
for shape, center in zip(shapes, centers):
offset = shape.bounding_rect().center().x() - shape.rect.center().x()
point = QtCore.QPointF(center.x() - offset, shape.rect.center().y())
shape.rect.moveCenter(point)
shape.synchronize_rect()
shape.update_path()
def arrange_vertical(shapes):
if len(shapes) < 3:
return
shapes = sorted(shapes, key=lambda s: s.bounding_rect().center().y())
centers = split_line(
point1=shapes[0].bounding_rect().center(),
point2=shapes[-1].bounding_rect().center(),
step_number=len(shapes))
for shape, center in zip(shapes, centers):
offset = shape.bounding_rect().center().y() - shape.rect.center().y()
point = QtCore.QPointF(shape.rect.center().x(), center.y() - offset)
shape.rect.moveCenter(point)
shape.synchronize_rect()
shape.update_path()
def align_shapes_on_line(shapes, point1, point2):
centers = split_line(point1, point2, len(shapes))
for center, shape in zip(centers, shapes):
shape.rect.moveCenter(center)
shape.synchronize_rect()
shape.update_path()
_direction_matches = {
'left': align_left,
'h_center': align_h_center,
'right': align_right,
'top': align_top,
'v_center': align_v_center,
'bottom': align_bottom
}

View File

@@ -0,0 +1,5 @@
VERSION = 1, 0, 4 # Version, Feature, Hotfix.
RELEASE_DATE = 'april 4 2025'
DW_WEBSITE = 'https://fr.dreamwall.be/'
DW_GITHUB = 'https://github.com/DreamWall-Animation'
PICKER_DOCUMENTATION = 'https://dreamwall-animation.github.io/dwpicker'

View File

@@ -0,0 +1,29 @@
def move_elements_to_array_end(array, elements):
return [e for e in array if e not in elements] + [e for e in elements]
def move_elements_to_array_begin(array, elements):
return [e for e in elements] + [e for e in array if e not in elements]
def move_up_array_elements(array, elements):
for element in reversed(array):
if element not in elements:
continue
index = array.index(element)
if index == len(array):
continue
array.insert(index + 2, element)
array.pop(index)
def move_down_array_elements(array, elements):
for shape in array:
if shape not in elements:
continue
index = array.index(shape)
if index == 0:
continue
array.pop(index)
array.insert(index - 1, shape)

View File

@@ -0,0 +1,22 @@
_clipboard_data = None
_clipboard_settings_data = None
def set(data):
global _clipboard_data
_clipboard_data = data
def get():
return _clipboard_data
def set_settings(settings):
global _clipboard_settings_data
_clipboard_settings_data = settings
def get_settings():
return _clipboard_settings_data or {}

View File

@@ -0,0 +1,272 @@
import math
from .pyside import QtWidgets, QtGui, QtCore
from .qtutils import get_cursor
from .geometry import (
get_relative_point, get_point_on_line, get_absolute_angle_c)
CONICAL_GRADIENT = (
(0.0, (0, 255, 255)),
(0.16, (0, 0, 255)),
(0.33, (255, 0, 255)),
(0.5, (255, 0, 0)),
(0.66, (255, 255, 0)),
(0.83, (0, 255, 0)),
(1.0, (0, 255, 255)))
TRANSPARENT = 0, 0, 0, 0
BLACK = 'black'
WHITE = 'white'
class ColorDialog(QtWidgets.QDialog):
def __init__(self, hexacolor, parent=None):
super(ColorDialog, self).__init__(parent)
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
self.colorwheel = ColorWheel()
self.colorwheel.set_current_color(QtGui.QColor(hexacolor))
self.ok = QtWidgets.QPushButton('ok')
self.ok.released.connect(self.accept)
self.layout = QtWidgets.QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0)
self.layout.addWidget(self.colorwheel)
self.layout.addWidget(self.ok)
def colorname(self):
return self.colorwheel.current_color().name()
def exec_(self):
point = get_cursor(self)
point.setX(point.x() - 50)
point.setY(point.y() - 75)
self.move(point)
return super(ColorDialog, self).exec_()
class ColorWheel(QtWidgets.QWidget):
currentColorChanged = QtCore.Signal(QtGui.QColor)
def __init__(self, parent=None):
super(ColorWheel, self).__init__(parent)
self._is_clicked = False
self._rect = QtCore.QRectF(25, 25, 50, 50)
self._current_color = QtGui.QColor(WHITE)
self._color_point = QtCore.QPoint(150, 50)
self._current_tool = None
self._angle = 180
self.setFixedSize(100, 100)
self.initUI()
def initUI(self):
self._conicalGradient = QtGui.QConicalGradient(
self.width() / 2, self.height() / 2, 180)
for pos, (r, g, b) in CONICAL_GRADIENT:
self._conicalGradient.setColorAt(pos, QtGui.QColor(r, g, b))
top = self._rect.top()
bottom = self._rect.top() + self._rect.height()
self._vertical_gradient = QtGui.QLinearGradient(0, top, 0, bottom)
self._vertical_gradient.setColorAt(0.0, QtGui.QColor(*TRANSPARENT))
self._vertical_gradient.setColorAt(1.0, QtGui.QColor(BLACK))
left = self._rect.left()
right = self._rect.left() + self._rect.width()
self._horizontal_gradient = QtGui.QLinearGradient(left, 0, right, 0)
self._horizontal_gradient.setColorAt(0.0, QtGui.QColor(WHITE))
def paintEvent(self, _):
try:
painter = QtGui.QPainter()
painter.begin(self)
self.paint(painter)
except BaseException:
pass # avoid crash
# TODO: log the error
finally:
painter.end()
def mousePressEvent(self, event):
tool = 'rect' if self._rect.contains(event.pos()) else 'wheel'
self._current_tool = tool
self.mouse_update(event)
def mouseMoveEvent(self, event):
self._is_clicked = True
self.mouse_update(event)
def mouse_update(self, event):
if self._current_tool == 'rect':
self.color_point = event.pos()
else:
center = self._get_center()
a = QtCore.QPoint(event.pos().x(), center.y())
self._angle = get_absolute_angle_c(a=a, b=event.pos(), c=center)
self.update()
self.currentColorChanged.emit(self.current_color())
def mouseReleaseEvent(self, event):
self._is_clicked = False
def paint(self, painter):
painter.setRenderHint(QtGui.QPainter.Antialiasing)
pen = QtGui.QPen(QtGui.QColor(0, 0, 0, 0))
pen.setWidth(0)
pen.setJoinStyle(QtCore.Qt.MiterJoin)
painter.setBrush(self._conicalGradient)
painter.setPen(pen)
painter.drawRoundedRect(
6, 6, (self.width() - 12), (self.height() - 12),
self.width(), self.height())
painter.setBrush(self.palette().color(QtGui.QPalette.Window))
painter.drawRoundedRect(
12.5, 12.5, (self.width() - 25), (self.height() - 25),
self.width(), self.height())
self._horizontal_gradient.setColorAt(
1.0, self._get_current_wheel_color())
painter.setBrush(self._horizontal_gradient)
painter.drawRect(self._rect)
painter.setBrush(self._vertical_gradient)
painter.drawRect(self._rect)
pen.setColor(QtGui.QColor(BLACK))
pen.setWidth(3)
painter.setPen(pen)
angle = math.radians(self._angle)
painter.drawLine(
get_point_on_line(angle, 37),
get_point_on_line(angle, 46))
pen.setWidth(5)
pen.setCapStyle(QtCore.Qt.RoundCap)
painter.setPen(pen)
painter.drawPoint(self._color_point)
@property
def color_point(self):
return self._color_point
@color_point.setter
def color_point(self, point):
if point.x() < self._rect.left():
x = self._rect.left()
elif point.x() > self._rect.left() + self._rect.width():
x = self._rect.left() + self._rect.width()
else:
x = point.x()
if point.y() < self._rect.top():
y = self._rect.top()
elif point.y() > self._rect.top() + self._rect.height():
y = self._rect.top() + self._rect.height()
else:
y = point.y()
self._color_point = QtCore.QPoint(x, y)
def _get_current_wheel_color(self):
degree = 360 - self._angle
return QtGui.QColor(*degree_to_color(degree))
def _get_center(self):
return QtCore.QPoint(self.width() / 2, self.height() / 2)
def current_color(self):
point = get_relative_point(self._rect, self.color_point)
x_factor = 1.0 - (float(point.x()) / self._rect.width())
y_factor = 1.0 - (float(point.y()) / self._rect.height())
r, g, b, _ = self._get_current_wheel_color().getRgb()
# fade to white
differences = 255.0 - r, 255.0 - g, 255.0 - b
r += round(differences[0] * x_factor)
g += round(differences[1] * x_factor)
b += round(differences[2] * x_factor)
# fade to black
r = round(r * y_factor)
g = round(g * y_factor)
b = round(b * y_factor)
return QtGui.QColor(r, g, b)
def set_current_color(self, color):
[r, g, b] = color.getRgb()[:3]
self._angle = 360.0 - (QtGui.QColor(r, g, b).getHslF()[0] * 360.0)
self._angle = self._angle if self._angle != 720.0 else 0
x = ((((
sorted([r, g, b], reverse=True)[0] -
sorted([r, g, b])[0]) / 255.0) * self._rect.width()) +
self._rect.left())
y = ((((
255 - (sorted([r, g, b], reverse=True)[0])) / 255.0) *
self._rect.height()) + self._rect.top())
self._current_color = color
self._color_point = QtCore.QPoint(x, y)
self.update()
def degree_to_color(degree):
if degree is None:
return None
degree = degree / 360.0
r, g, b = 255.0, 255.0, 255.0
contain_red = (
(degree >= 0.0 and degree <= 0.33)
or (degree >= 0.66 and degree <= 1.0))
if contain_red:
if degree >= 0.66 and degree <= 0.83:
factor = degree - 0.66
r = round(255 * (factor / .16))
if (degree > 0.0 and degree < 0.16) or (degree > 0.83 and degree < 1.0):
r = 255
elif degree >= 0.16 and degree <= 0.33:
factor = degree - 0.16
r = 255 - round(255 * (factor / .16))
else:
r = 0
r = min(r, 255)
r = max(r, 0)
# GREEN
if degree >= 0.0 and degree <= 0.66:
if degree <= 0.16:
g = round(255.0 * (degree / .16))
elif degree < 0.5:
g = 255
if degree >= 0.5:
factor = degree - 0.5
g = 255 - round(255.0 * (factor / .16))
else:
g = 0
g = min(g, 255.0)
g = max(g, 0)
# BLUE
if degree >= 0.33 and degree <= 1.0:
if degree <= 0.5:
factor = degree - 0.33
b = round(255 * (factor / .16))
elif degree < 0.83:
b = 255.0
if degree >= 0.83 and degree <= 1.0:
factor = degree - 0.83
b = 255.0 - round(255.0 * (factor / .16))
else:
b = 0
b = min(b, 255)
b = max(b, 0)
return r, g, b

View File

@@ -0,0 +1,159 @@
from copy import deepcopy
from .pyside import QtWidgets, QtCore
from .templates import COMMAND, MENU_COMMAND
from .qtutils import icon
from .dialog import CommandEditorDialog, MenuCommandEditorDialog
class CommandItemWidget(QtWidgets.QWidget):
editRequested = QtCore.Signal(object)
deletedRequested = QtCore.Signal(object)
def __init__(self, command, parent=None):
super(CommandItemWidget, self).__init__(parent)
self.command = command
self.label = QtWidgets.QLabel(self.get_label())
self.edit = QtWidgets.QPushButton(icon('edit2.png'), '')
self.edit.released.connect(lambda: self.editRequested.emit(self))
self.edit.setFixedSize(25, 25)
self.delete = QtWidgets.QPushButton(icon('delete2.png'), '')
self.delete.setFixedSize(25, 25)
self.delete.released.connect(lambda: self.deletedRequested.emit(self))
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addWidget(self.edit)
layout.addWidget(self.delete)
layout.addSpacing(10)
layout.addWidget(self.label)
def get_label(self):
language = '<a style="color: #FFFF00"><i>({0})</i></a>'.format(
self.command['language'])
touchs = [self.command['button'] + 'Click']
touchs.extend([m for m in ('ctrl', 'shift') if self.command[m]])
return '{} {}'.format('+'.join(touchs), language)
def update_label(self):
self.label.setText(self.get_label())
class MenuCommandItemWidget(CommandItemWidget):
def get_label(self):
language = '<a style="color: #FFFF00"><i>({0})</i></a>'.format(
self.command['language'])
return '{} {}'.format(self.command['caption'], language)
class CommandsEditor(QtWidgets.QWidget):
valueSet = QtCore.Signal(object)
edit_dialog_constructor = CommandEditorDialog
picker_command_key = 'action.commands'
template = COMMAND
item_constructor = CommandItemWidget
def __init__(self, parent=None):
super(CommandsEditor, self).__init__(parent)
self.warning = QtWidgets.QLabel('Select only one shape')
self.commands = QtWidgets.QListWidget()
self.commands.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
self.commands.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
self.commands.setHorizontalScrollBarPolicy(
QtCore.Qt.ScrollBarAlwaysOff)
self.add_command = QtWidgets.QPushButton('Add command')
self.add_command.released.connect(self.call_create_command)
self.add_command.setEnabled(False)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addWidget(self.warning)
layout.addWidget(self.commands)
layout.addWidget(self.add_command)
def set_options(self, options):
self.commands.clear()
if len(options) != 1:
self.warning.setVisible(True)
self.add_command.setEnabled(False)
return
self.warning.setVisible(False)
self.add_command.setEnabled(True)
for command in options[0][self.picker_command_key]:
self.call_add_command(command)
def call_create_command(self):
command = deepcopy(self.template)
dialog = self.edit_dialog_constructor(command)
if not dialog.exec_():
return
self.call_add_command(dialog.command_data())
self.valueSet.emit(self.commands_data())
def call_add_command(self, command=None):
widget = self.item_constructor(command)
widget.editRequested.connect(self.edit_command)
widget.deletedRequested.connect(self.delete_command)
item = QtWidgets.QListWidgetItem()
item.widget = widget
item.setSizeHint(
QtCore.QSize(
self.commands.width() -
self.commands.verticalScrollBar().width(),
widget.sizeHint().height()))
self.commands.addItem(item)
self.commands.setItemWidget(item, widget)
def edit_command(self, widget):
for r in range(self.commands.count()):
item = self.commands.item(r)
if item.widget != widget:
continue
dialog = self.edit_dialog_constructor(item.widget.command)
if not dialog.exec_():
return
widget.command = dialog.command_data()
widget.update_label()
self.valueSet.emit(self.commands_data())
def delete_command(self, widget):
for r in range(self.commands.count()):
item = self.commands.item(r)
if item.widget != widget:
continue
self.commands.takeItem(r)
self.valueSet.emit(self.commands_data())
return
def commands_data(self):
return [
self.commands.item(r).widget.command
for r in range(self.commands.count())]
class MenuCommandsEditor(CommandsEditor):
edit_dialog_constructor = MenuCommandEditorDialog
picker_command_key = 'action.menu_commands'
template = MENU_COMMAND
item_constructor = MenuCommandItemWidget
class GlobalCommandsEditor(CommandsEditor):
edit_dialog_constructor = MenuCommandEditorDialog
picker_command_key = 'menu_commands'
template = MENU_COMMAND
item_constructor = MenuCommandItemWidget
def __init__(self, parent=None):
super(GlobalCommandsEditor, self).__init__(parent)
self.warning.hide()
self.add_command.setEnabled(True)
def set_options(self, options):
self.commands.clear()
for command in options[self.picker_command_key]:
self.call_add_command(command)

View File

@@ -0,0 +1,145 @@
"""
This module contain a function to ingest picker done with older version.
If the structure changed, it can convert automatically the data to the new
version.
"""
import uuid
from .appinfos import VERSION
from .stack import count_panels
from .shapepath import get_relative_path
def ensure_retro_compatibility(picker_data):
"""
This function ensure retro compatibility.
"""
# If a new release involve a data structure change in the picker, implement
# the way to update the data here using this pattern:
#
# if version < (youre version number):
# picker_data = your code update
version = picker_data['general'].get('version') or (0, 0, 0)
picker_data['general']['version'] = VERSION
if tuple(version) < (0, 3, 0):
# Add new options added to version 0, 3, 0.
picker_data['general']['zoom_locked'] = False
if tuple(version) < (0, 4, 0):
picker_data['general'].pop('centerx')
picker_data['general'].pop('centery')
if tuple(version) < (0, 10, 0):
for shape in picker_data['shapes']:
shape['visibility_layer'] = None
if tuple(version) < (0, 11, 0):
for shape in picker_data['shapes']:
update_shape_actions_for_v0_11_0(shape)
if tuple(version) < (0, 11, 3):
for shape in picker_data['shapes']:
shape['background'] = not (
any(cmd['enabled'] for cmd in shape['action.commands']) or
shape['action.targets'])
if tuple(version) < (0, 12, 0):
for shape in picker_data['shapes']:
shape['action.menu_commands'] = []
if tuple(version) < (0, 12, 1):
picker_data['general'].pop('width')
picker_data['general'].pop('height')
if tuple(version) < (0, 14, 0):
for shape in picker_data['shapes']:
shape['shape.path'] = []
if tuple(version) < (0, 14, 1):
picker_data['general']['menu_commands'] = []
if tuple(version) < (0, 15, 0):
picker_data['general']['panels'] = [[1.0, [1.0]]]
picker_data['general']['panels.orientation'] = 'vertical'
zoom_locked = picker_data['general']['zoom_locked']
picker_data['general']['panels.zoom_locked'] = [zoom_locked]
del picker_data['general']['zoom_locked']
for shape in picker_data['shapes']:
shape['panel'] = 0
shape['shape.space'] = 'world'
shape['shape.anchor'] = 'top_left'
if tuple(version) < (0, 15, 2):
picker_data['general']['hidden_layers'] = []
if tuple(version) < (0, 15, 3):
picker_data['general']['panels.as_sub_tab'] = False
picker_data['general']['panels.colors'] = [None]
picker_data['general']['panels.names'] = ['Panel 1']
ensure_general_options_sanity(picker_data['general'])
if tuple(version) < (1, 0, 0):
for shape in picker_data['shapes']:
shape['id'] = str(uuid.uuid4())
point = shape['shape.left'], shape['shape.top']
shape['shape.path'] = get_relative_path(point, shape['shape.path'])
shape['shape.ignored_by_focus'] = False
shape['image.ratio'] = False
shape['children'] = []
return picker_data
def ensure_general_options_sanity(options):
split_count = count_panels(options['panels'])
while split_count > len(options['panels.zoom_locked']):
options['panels.zoom_locked'].append(False)
while split_count > len(options['panels.colors']):
options['panels.colors'].append(None)
while split_count > len(options['panels.names']):
name = 'Panel ' + str(len(options["panels.names"]) + 1)
options['panels.names'].append(name)
def update_shape_actions_for_v0_11_0(shape):
"""
With release 0.11.0 comes a new configurable action system.
"""
if 'action.namespace' in shape:
del shape['action.namespace']
if 'action.type' in shape:
del shape['action.type']
shape['action.commands'] = []
if shape['action.left.command']:
shape['action.commands'].append({
'enabled': shape['action.left'],
'button': 'left',
'language': shape['action.left.language'],
'command': shape['action.left.command'],
'alt': False,
'ctrl': False,
'shift': False,
'deferred': False,
'force_compact_undo': False})
if shape['action.right.command']:
shape['action.commands'].append({
'enabled': shape['action.right'],
'button': 'left',
'language': shape['action.right.language'],
'command': shape['action.right.command'],
'alt': False,
'ctrl': False,
'shift': False,
'deferred': False,
'force_compact_undo': False})
keys_to_clear = (
'action.left', 'action.left.language',
'action.left.command', 'action.right', 'action.right.language',
'action.right.command')
for key in keys_to_clear:
del shape[key]

View File

@@ -0,0 +1,820 @@
import maya.cmds as cmds
from functools import partial
from ..pyside import QtCore, QtWidgets
from ..commands import (
CommandsEditor, MenuCommandsEditor, GlobalCommandsEditor)
from ..qtutils import VALIGNS, HALIGNS
from .stackeditor import StackEditor
from .layer import VisibilityLayersEditor
from .patheditor import PathEditor
from ..stack import ORIENTATIONS
from ..widgets import (
BoolCombo, BrowseEdit, ColorEdit, ChildrenWidget, IntEdit, FloatEdit,
LayerEdit, TextEdit, Title, WidgetToggler, ZoomsLockedEditor)
LEFT_CELL_WIDTH = 90
SHAPE_TYPES = 'square', 'round', 'rounded_rect', 'custom'
SPACES = 'world', 'screen'
ANCHORS = 'top_left', 'top_right', 'bottom_left', 'bottom_right'
class AttributeEditor(QtWidgets.QWidget):
imageModified = QtCore.Signal()
optionSet = QtCore.Signal(str, object)
optionsSet = QtCore.Signal(dict, bool) # all options, affect rect
selectLayerContent = QtCore.Signal(str)
panelDoubleClicked = QtCore.Signal(int)
def __init__(self, document, display_options, parent=None):
super(AttributeEditor, self).__init__(parent)
self.document = document
self.generals = GeneralSettings(self.document, display_options)
self.generals.panelDoubleClicked.connect(self.panel_double_clicked)
mtd = self.selectLayerContent.emit
self.generals.layers.selectLayerContent.connect(mtd)
self.shape = ShapeSettings()
self.shape.optionSet.connect(self.optionSet.emit)
self.shape.optionsSet.connect(self.optionsSet.emit)
self.shape_toggler = WidgetToggler('Shape', self.shape)
self.image = ImageSettings()
self.image.optionSet.connect(self.image_modified)
self.image_toggler = WidgetToggler('Image', self.image)
self.appearence = AppearenceSettings()
self.appearence.optionSet.connect(self.optionSet.emit)
self.appearence_toggler = WidgetToggler('Appearence', self.appearence)
self.text = TextSettings()
self.text.optionSet.connect(self.optionSet.emit)
self.text_toggler = WidgetToggler('Text', self.text)
self.action = ActionSettings(document, display_options)
self.action.optionSet.connect(self.optionSet.emit)
self.action_toggler = WidgetToggler('Action', self.action)
self.widget = QtWidgets.QWidget()
self.layout = QtWidgets.QVBoxLayout(self.widget)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0)
self.layout.addWidget(self.shape_toggler)
self.layout.addWidget(self.shape)
self.layout.addWidget(self.image_toggler)
self.layout.addWidget(self.image)
self.layout.addWidget(self.appearence_toggler)
self.layout.addWidget(self.appearence)
self.layout.addWidget(self.text_toggler)
self.layout.addWidget(self.text)
self.layout.addWidget(self.action_toggler)
self.layout.addWidget(self.action)
self.layout.addStretch(1)
self.scroll_area = QtWidgets.QScrollArea()
self.scroll_area.setWidget(self.widget)
self.tab = QtWidgets.QTabWidget()
self.tab.addTab(self.scroll_area, 'Shapes')
self.tab.addTab(self.generals, 'Picker')
self.main_layout = QtWidgets.QVBoxLayout(self)
self.main_layout.setContentsMargins(0, 0, 0, 0)
self.main_layout.addWidget(self.tab)
self.setFixedWidth(self.sizeHint().width() * 1.05)
def panel_double_clicked(self, panel):
self.panelDoubleClicked.emit(panel - 1)
def set_options(self, options):
self.blockSignals(True)
self.shape.set_options(options)
self.image.set_options(options)
self.appearence.set_options(options)
self.text.set_options(options)
self.action.set_options(options)
self.blockSignals(False)
def image_modified(self, option, value):
self.optionSet.emit(option, value)
self.imageModified.emit()
class GeneralSettings(QtWidgets.QWidget):
panelDoubleClicked = QtCore.Signal(int)
def __init__(self, document, display_options, parent=None):
super(GeneralSettings, self).__init__(parent)
self.document = document
self.display_options = display_options
self.document.general_option_changed.connect(self.update_options)
self.document.data_changed.connect(self.update_options)
self.name = TextEdit()
self.name.valueSet.connect(partial(self.set_general, 'name'))
self.zoom_locked = ZoomsLockedEditor(self.document)
self.orientation = QtWidgets.QComboBox()
self.orientation.addItems(list(ORIENTATIONS))
method = partial(self.set_general, 'panels.orientation')
self.orientation.currentTextChanged.connect(method)
self.as_sub_tab = BoolCombo()
method = partial(self.set_general, 'panels.as_sub_tab')
self.as_sub_tab.valueSet.connect(method)
self.stack = StackEditor()
self.stack.setMinimumHeight(100)
self.stack.panelsChanged.connect(self.stack_changed)
self.stack.panelSelected.connect(self.panel_selected)
self.stack.panelDoubleClicked.connect(self.panelDoubleClicked.emit)
self.layers = VisibilityLayersEditor(self.document)
self.commands = GlobalCommandsEditor()
method = partial(self.set_general, 'menu_commands')
self.commands.valueSet.connect(method)
form_layout = QtWidgets.QFormLayout()
form_layout.setSpacing(0)
form_layout.setContentsMargins(0, 0, 0, 0)
form_layout.setHorizontalSpacing(5)
form_layout.addRow('Picker Name', self.name)
form_layout_2 = QtWidgets.QFormLayout()
form_layout_2.setSpacing(0)
form_layout_2.setContentsMargins(0, 0, 0, 0)
form_layout_2.setHorizontalSpacing(5)
form_layout_2.addRow('Columns orientation', self.orientation)
form_layout_2.addRow('Display panels as tab', self.as_sub_tab)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addLayout(form_layout)
layout.addItem(QtWidgets.QSpacerItem(0, 8))
layout.addWidget(Title('Picker Panels'))
layout.addLayout(form_layout_2)
layout.addWidget(self.stack)
layout.addItem(QtWidgets.QSpacerItem(0, 8))
layout.addWidget(Title('Panels Zoom Locked'))
layout.addWidget(self.zoom_locked)
layout.addItem(QtWidgets.QSpacerItem(0, 8))
layout.addWidget(Title('Visibility Layers'))
layout.addWidget(self.layers)
layout.addItem(QtWidgets.QSpacerItem(0, 8))
layout.addWidget(Title('Global Right Click Commands'))
layout.addWidget(self.commands)
layout.addStretch()
self.update_options()
def panel_selected(self, index):
self.display_options.current_panel = index - 1
self.display_options.options_changed.emit()
def stack_changed(self):
self.set_general('panels', self.stack.data)
def set_general(self, key, value):
self.document.data['general'][key] = value
self.document.general_option_changed.emit('attribute_editor', key)
self.document.record_undo()
if key == 'panels.orientation':
self.stack.set_orientation(value)
def update_options(self, *_):
self.block_signals(True)
options = self.document.data['general']
self.stack.set_data(options['panels'])
self.stack.set_orientation(options['panels.orientation'])
self.as_sub_tab.setCurrentText(str(options['panels.as_sub_tab']))
self.orientation.setCurrentText(options['panels.orientation'])
self.name.setText(options['name'])
self.commands.set_options(options)
self.block_signals(False)
def block_signals(self, state):
widgets = (
self.stack,
self.as_sub_tab,
self.orientation,
self.name,
self.commands)
for widget in widgets:
widget.blockSignals(state)
class ShapeSettings(QtWidgets.QWidget):
optionSet = QtCore.Signal(str, object)
optionsSet = QtCore.Signal(dict, bool) # all options, affect rect
def __init__(self, parent=None):
super(ShapeSettings, self).__init__(parent)
self.id_ = QtWidgets.QLineEdit()
self.id_.setReadOnly(True)
self.shape = QtWidgets.QComboBox()
self.shape.addItems(SHAPE_TYPES)
self.shape.currentIndexChanged.connect(self.shape_changed)
self.path_editor = PathEditor(self)
self.path_editor.pathEdited.connect(self.path_edited)
self.path_editor.setVisible(False)
self.path_editor.setEnabled(False)
self.panel = QtWidgets.QLineEdit()
self.panel.setReadOnly(True)
self.layer = LayerEdit()
method = partial(self.optionSet.emit, 'visibility_layer')
self.layer.valueSet.connect(method)
self.background = BoolCombo()
method = partial(self.optionSet.emit, 'background')
self.background.valueSet.connect(method)
self.ignored_by_focus = BoolCombo()
method = partial(self.optionSet.emit, 'shape.ignored_by_focus')
self.ignored_by_focus.valueSet.connect(method)
self.space = QtWidgets.QComboBox()
self.space.addItems(SPACES)
self.space.currentIndexChanged.connect(self.space_changed)
self.anchor = QtWidgets.QComboBox()
self.anchor.addItems(ANCHORS)
method = partial(self.optionSet.emit, 'shape.anchor')
self.anchor.currentTextChanged.connect(method)
self.left = IntEdit()
method = partial(self.optionSet.emit, 'shape.left')
self.left.valueSet.connect(method)
self.top = IntEdit()
method = partial(self.optionSet.emit, 'shape.top')
self.top.valueSet.connect(method)
self.width = IntEdit(minimum=0)
method = partial(self.optionSet.emit, 'shape.width')
self.width.valueSet.connect(method)
self.height = IntEdit(minimum=0)
method = partial(self.optionSet.emit, 'shape.height')
self.height.valueSet.connect(method)
self.cornersx = IntEdit(minimum=0)
method = partial(self.optionSet.emit, 'shape.cornersx')
self.cornersx.valueSet.connect(method)
self.cornersy = IntEdit(minimum=0)
method = partial(self.optionSet.emit, 'shape.cornersy')
self.cornersy.valueSet.connect(method)
layout1 = QtWidgets.QFormLayout()
layout1.setSpacing(0)
layout1.setContentsMargins(0, 0, 0, 0)
layout1.setHorizontalSpacing(5)
layout1.addRow('Id', self.id_)
layout1.addItem(QtWidgets.QSpacerItem(0, 8))
layout1.addRow(Title('Display'))
layout1.addRow('Panel number', self.panel)
layout1.addRow('Visibility layer', self.layer)
layout1.addRow('Background', self.background)
layout1.addRow('Ignored by focus', self.ignored_by_focus)
layout1.addRow('Shape', self.shape)
layout2 = QtWidgets.QVBoxLayout()
layout2.setSpacing(0)
layout2.setContentsMargins(0, 0, 0, 0)
layout2.addWidget(self.path_editor)
layout3 = QtWidgets.QFormLayout()
layout3.addItem(QtWidgets.QSpacerItem(0, 8))
layout3.addRow(Title('Space'))
layout3.addRow('space', self.space)
layout3.addRow('anchor', self.anchor)
layout3.addItem(QtWidgets.QSpacerItem(0, 8))
layout3.addRow(Title('Dimensions'))
layout3.addRow('left', self.left)
layout3.addRow('top', self.top)
layout3.addRow('width', self.width)
layout3.addRow('height', self.height)
layout3.addRow('roundness x', self.cornersx)
layout3.addRow('roundness y', self.cornersy)
layout3.addItem(QtWidgets.QSpacerItem(0, 8))
layout = QtWidgets.QVBoxLayout(self)
layout.setSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
layout.addLayout(layout1)
layout.addLayout(layout2)
layout.addLayout(layout3)
for label in self.findChildren(QtWidgets.QLabel):
alignment = QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter
if not isinstance(label, Title):
label.setAlignment(alignment)
label.setFixedWidth(LEFT_CELL_WIDTH)
def path_edited(self):
if self.shape.currentText() != 'custom':
return
self.optionSet.emit('shape.path', self.path_editor.path())
def shape_changed(self, _):
self.path_editor.setEnabled(self.shape.currentText() == 'custom')
self.path_editor.setVisible(self.shape.currentText() == 'custom')
self.height.setEnabled(self.shape.currentText() != 'custom')
self.width.setEnabled(self.shape.currentText() != 'custom')
self.optionSet.emit('shape', self.shape.currentText())
self.path_editor.canvas.focus()
def space_changed(self, index):
self.anchor.setEnabled(bool(index))
self.optionSet.emit('shape.space', self.space.currentText())
def set_options(self, options):
values = list({option['id'] for option in options})
value = str(values[0]) if len(values) == 1 else '...'
self.id_.setText(value)
values = list({option['background'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.background.setCurrentText(value)
values = list({option['shape.ignored_by_focus'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.ignored_by_focus.setCurrentText(value)
values = list({option['panel'] for option in options})
value = values[0] if len(values) == 1 else '' if not values else '...'
self.panel.setText(str(value + 1 if isinstance(value, int) else value))
values = list({option['shape.space'] for option in options})
value = values[0] if len(values) == 1 else '' if not values else '...'
self.space.setCurrentText(value)
values = list({option['shape.anchor'] for option in options})
value = values[0] if len(values) == 1 else '' if not values else '...'
self.anchor.setCurrentText(value)
self.anchor.setEnabled(self.space.currentText() == 'screen')
values = list({option['visibility_layer'] for option in options})
value = values[0] if len(values) == 1 else '' if not values else '...'
self.layer.set_layer(value)
values = list({option['shape'] for option in options})
value = values[0] if len(values) == 1 else '...'
self.shape.blockSignals(True)
self.shape.setCurrentText(value)
self.shape.blockSignals(False)
self.height.setEnabled('custom' not in values)
self.width.setEnabled('custom' not in values)
if len(options) == 1:
self.path_editor.setEnabled(options[0]['shape'] == 'custom')
self.path_editor.setVisible(options[0]['shape'] == 'custom')
self.path_editor.set_options(options[0])
else:
self.path_editor.setEnabled(False)
self.path_editor.setVisible(False)
self.path_editor.set_options(None)
values = list({int(round((option['shape.left']))) for option in options})
value = str(values[0]) if len(values) == 1 else None
self.left.setText(value)
values = list({option['shape.top'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.top.setText(value)
values = list({option['shape.width'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.width.setText(value)
values = list({option['shape.height'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.height.setText(value)
values = list({option['shape.cornersx'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.cornersx.setText(value)
values = list({option['shape.cornersy'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.cornersy.setText(value)
class ImageSettings(QtWidgets.QWidget):
optionSet = QtCore.Signal(str, object)
def __init__(self, parent=None):
super(ImageSettings, self).__init__(parent)
self.path = BrowseEdit()
self.path.valueSet.connect(partial(self.optionSet.emit, 'image.path'))
self.fit = BoolCombo(True)
self.fit.valueSet.connect(partial(self.optionSet.emit, 'image.fit'))
self.ratio = BoolCombo(True)
method = partial(self.optionSet.emit, 'image.ratio')
self.ratio.valueSet.connect(method)
self.width = FloatEdit(minimum=0)
method = partial(self.optionSet.emit, 'image.width')
self.width.valueSet.connect(method)
self.height = FloatEdit(minimum=0)
method = partial(self.optionSet.emit, 'image.height')
self.height.valueSet.connect(method)
self.layout = QtWidgets.QFormLayout(self)
self.layout.setSpacing(0)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setHorizontalSpacing(5)
self.layout.addRow('Path', self.path)
self.layout.addRow('Fit to shape', self.fit)
self.layout.addRow('Preserve ratio', self.ratio)
self.layout.addRow('Width', self.width)
self.layout.addRow('Height', self.height)
self.layout.addItem(QtWidgets.QSpacerItem(0, 8))
for label in self.findChildren(QtWidgets.QLabel):
alignment = QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter
if not isinstance(label, Title):
label.setAlignment(alignment)
label.setFixedWidth(LEFT_CELL_WIDTH)
def set_options(self, options):
values = list({option['image.path'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.path.set_value(value)
values = list({option['image.fit'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.fit.setCurrentText(value)
values = list({option['image.ratio'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.ratio.setCurrentText(value)
values = list({option['image.width'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.width.setText(value)
values = list({option['image.height'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.height.setText(value)
class AppearenceSettings(QtWidgets.QWidget):
optionSet = QtCore.Signal(str, object)
def __init__(self, parent=None):
super(AppearenceSettings, self).__init__(parent)
self.border = BoolCombo(True)
method = partial(self.optionSet.emit, 'border')
self.border.valueSet.connect(method)
self.borderwidth_normal = FloatEdit(minimum=0.0)
method = partial(self.optionSet.emit, 'borderwidth.normal')
self.borderwidth_normal.valueSet.connect(method)
self.borderwidth_hovered = FloatEdit(minimum=0.0)
method = partial(self.optionSet.emit, 'borderwidth.hovered')
self.borderwidth_hovered.valueSet.connect(method)
self.borderwidth_clicked = FloatEdit(minimum=0.0)
method = partial(self.optionSet.emit, 'borderwidth.clicked')
self.borderwidth_clicked.valueSet.connect(method)
self.bordercolor_normal = ColorEdit()
method = partial(self.optionSet.emit, 'bordercolor.normal')
self.bordercolor_normal.valueSet.connect(method)
self.bordercolor_hovered = ColorEdit()
method = partial(self.optionSet.emit, 'bordercolor.hovered')
self.bordercolor_hovered.valueSet.connect(method)
self.bordercolor_clicked = ColorEdit()
method = partial(self.optionSet.emit, 'bordercolor.clicked')
self.bordercolor_clicked.valueSet.connect(method)
self.bordercolor_transparency = IntEdit(minimum=0, maximum=255)
method = partial(self.optionSet.emit, 'bordercolor.transparency')
self.bordercolor_transparency.valueSet.connect(method)
self.backgroundcolor_normal = ColorEdit()
method = partial(self.optionSet.emit, 'bgcolor.normal')
self.backgroundcolor_normal.valueSet.connect(method)
self.backgroundcolor_hovered = ColorEdit()
method = partial(self.optionSet.emit, 'bgcolor.hovered')
self.backgroundcolor_hovered.valueSet.connect(method)
self.backgroundcolor_clicked = ColorEdit()
method = partial(self.optionSet.emit, 'bgcolor.clicked')
self.backgroundcolor_clicked.valueSet.connect(method)
self.backgroundcolor_transparency = IntEdit(minimum=0, maximum=255)
method = partial(self.optionSet.emit, 'bgcolor.transparency')
self.backgroundcolor_transparency.valueSet.connect(method)
self.layout = QtWidgets.QFormLayout(self)
self.layout.setSpacing(0)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setHorizontalSpacing(5)
self.layout.addRow('border visible', self.border)
self.layout.addItem(QtWidgets.QSpacerItem(0, 8))
self.layout.addRow(Title('Border width (pxf)'))
self.layout.addRow('normal', self.borderwidth_normal)
self.layout.addRow('hovered', self.borderwidth_hovered)
self.layout.addRow('clicked', self.borderwidth_clicked)
self.layout.addItem(QtWidgets.QSpacerItem(0, 8))
self.layout.addRow(Title('Border color'))
self.layout.addRow('normal', self.bordercolor_normal)
self.layout.addRow('hovered', self.bordercolor_hovered)
self.layout.addRow('clicked', self.bordercolor_clicked)
self.layout.addRow('transparency', self.bordercolor_transparency)
self.layout.addItem(QtWidgets.QSpacerItem(0, 8))
self.layout.addRow(Title('Background color'))
self.layout.addRow('normal', self.backgroundcolor_normal)
self.layout.addRow('hovered', self.backgroundcolor_hovered)
self.layout.addRow('clicked', self.backgroundcolor_clicked)
self.layout.addRow('transparency', self.backgroundcolor_transparency)
self.layout.addItem(QtWidgets.QSpacerItem(0, 8))
for label in self.findChildren(QtWidgets.QLabel):
alignment = QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter
if not isinstance(label, Title):
label.setAlignment(alignment)
label.setFixedWidth(LEFT_CELL_WIDTH)
def set_options(self, options):
values = list({option['border'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.border.setCurrentText(value)
values = list({option['borderwidth.normal'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.borderwidth_normal.setText(value)
values = list({option['borderwidth.hovered'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.borderwidth_hovered.setText(value)
values = list({option['borderwidth.clicked'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.borderwidth_clicked.setText(value)
values = list({option['bordercolor.normal'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.bordercolor_normal.set_color(value)
values = list({option['bordercolor.hovered'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.bordercolor_hovered.set_color(value)
values = list({option['bordercolor.clicked'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.bordercolor_clicked.set_color(value)
values = list({option['bordercolor.transparency'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.bordercolor_transparency.setText(value)
values = list({option['bgcolor.normal'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.backgroundcolor_normal.set_color(value)
values = list({option['bgcolor.hovered'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.backgroundcolor_hovered.set_color(value)
values = list({option['bgcolor.clicked'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.backgroundcolor_clicked.set_color(value)
values = list({option['bgcolor.transparency'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.backgroundcolor_transparency.setText(value)
class ActionSettings(QtWidgets.QWidget):
optionSet = QtCore.Signal(str, object)
def __init__(self, document, display_options, parent=None):
super(ActionSettings, self).__init__(parent)
self._targets = QtWidgets.QLineEdit()
self._targets.returnPressed.connect(self.targets_changed)
self._add_targets = QtWidgets.QPushButton('Add')
self._add_targets.released.connect(self.call_add_targets)
self._add_targets.setToolTip('Add selected objects')
self._add_targets.setFocusPolicy(QtCore.Qt.NoFocus)
self._remove_targets = QtWidgets.QPushButton('Remove')
self._remove_targets.released.connect(self.call_remove_targets)
self._remove_targets.setToolTip('Remove selected objects')
self._remove_targets.setFocusPolicy(QtCore.Qt.NoFocus)
self._replace_targets = QtWidgets.QPushButton('Replace')
self._replace_targets.clicked.connect(self.call_replace_targets)
self._replace_targets.setToolTip('Replace target by current selection')
self._replace_targets.setFocusPolicy(QtCore.Qt.NoFocus)
self._clear_targets = QtWidgets.QPushButton('Clear')
self._clear_targets.clicked.connect(self.call_clear_targets)
self._clear_targets.setToolTip('Clear shape targets')
self._clear_targets.setFocusPolicy(QtCore.Qt.NoFocus)
_targets = QtWidgets.QWidget()
self._targets_layout = QtWidgets.QHBoxLayout(_targets)
self._targets_layout.setContentsMargins(0, 0, 0, 0)
self._targets_layout.setSpacing(2)
self._targets_layout.addStretch()
self._targets_layout.addWidget(self._add_targets)
self._targets_layout.addWidget(self._remove_targets)
self._targets_layout.addWidget(self._replace_targets)
self._targets_layout.addWidget(self._clear_targets)
self._commands = CommandsEditor()
method = partial(self.optionSet.emit, 'action.commands')
self._commands.valueSet.connect(method)
self._menu = MenuCommandsEditor()
method = partial(self.optionSet.emit, 'action.menu_commands')
self._menu.valueSet.connect(method)
self.select_one_shape_label = QtWidgets.QLabel('Select only one shape')
self.children = ChildrenWidget(document, display_options)
method = partial(self.optionSet.emit, 'children')
self.children.children_changed.connect(method)
form = QtWidgets.QFormLayout()
form.setSpacing(0)
form.setContentsMargins(0, 0, 0, 0)
form.setHorizontalSpacing(5)
form.addRow(Title('Selection'))
form.addRow('Targets', self._targets)
form.addWidget(_targets)
self.layout = QtWidgets.QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0)
self.layout.addLayout(form)
self.layout.addItem(QtWidgets.QSpacerItem(0, 8))
for label in self.findChildren(QtWidgets.QLabel):
alignment = QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter
if not isinstance(label, Title):
label.setAlignment(alignment)
label.setFixedWidth(LEFT_CELL_WIDTH)
self.layout.addWidget(Title('Scripts'))
self.layout.addWidget(self._commands)
self.layout.addItem(QtWidgets.QSpacerItem(0, 8))
self.layout.addWidget(Title('Right Click Menu'))
self.layout.addWidget(self._menu)
self.layout.addItem(QtWidgets.QSpacerItem(0, 8))
self.layout.addWidget(Title('Hierarchy'))
self.layout.addWidget(self.select_one_shape_label)
self.layout.addWidget(self.children)
self.layout.addItem(QtWidgets.QSpacerItem(0, 8))
def targets(self):
targets = str(self._targets.text())
try:
return [t.strip(" ") for t in targets.split(',')]
except ValueError:
return []
def call_clear_targets(self):
self._targets.setText('')
self.targets_changed()
def call_add_targets(self):
selection = cmds.ls(selection=True, flatten=True)
if not selection:
return
targets = self.targets()
edits = [item for item in selection if item not in targets]
targets = targets if targets != [''] else []
self._targets.setText(', '.join(targets + edits))
self.targets_changed()
def call_remove_targets(self):
selection = cmds.ls(selection=True, flatten=True)
if not selection:
return
targets = [item for item in self.targets() if item not in selection]
self._targets.setText(', '.join(targets))
self.targets_changed()
def call_replace_targets(self):
selection = cmds.ls(selection=True, flatten=True)
if not selection:
return
self._targets.setText(', '.join(selection))
self.targets_changed()
def targets_changed(self):
values = [t.strip(" ") for t in self._targets.text().split(",")]
self.optionSet.emit('action.targets', values)
def set_options(self, options):
values = list({o for opt in options for o in opt['action.targets']})
self._targets.setText(", ".join(sorted(values)))
self._commands.set_options(options)
self._menu.set_options(options)
self.select_one_shape_label.setVisible(len(options) != 1)
self.children.clear()
if len(options) == 1:
self.children.set_children(options[0]['children'])
class TextSettings(QtWidgets.QWidget):
optionSet = QtCore.Signal(str, object)
def __init__(self, parent=None):
super(TextSettings, self).__init__(parent)
self.text = TextEdit()
method = partial(self.optionSet.emit, 'text.content')
self.text.valueSet.connect(method)
self.size = FloatEdit(minimum=0.0)
self.size.valueSet.connect(partial(self.optionSet.emit, 'text.size'))
self.bold = BoolCombo()
self.bold.valueSet.connect(partial(self.optionSet.emit, 'text.bold'))
self.italic = BoolCombo()
method = partial(self.optionSet.emit, 'text.italic')
self.italic.valueSet.connect(method)
self.color = ColorEdit()
self.color.valueSet.connect(partial(self.optionSet.emit, 'text.color'))
self.halignement = QtWidgets.QComboBox()
self.halignement.addItems(HALIGNS.keys())
self.halignement.currentIndexChanged.connect(self.halign_changed)
self.valignement = QtWidgets.QComboBox()
self.valignement.addItems(VALIGNS.keys())
self.valignement.currentIndexChanged.connect(self.valign_changed)
self.layout = QtWidgets.QFormLayout(self)
self.layout.setSpacing(0)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setHorizontalSpacing(5)
self.layout.addRow('Content', self.text)
self.layout.addItem(QtWidgets.QSpacerItem(0, 8))
self.layout.addRow(Title('Options'))
self.layout.addRow('Size', self.size)
self.layout.addRow('Bold', self.bold)
self.layout.addRow('Italic', self.italic)
self.layout.addRow('Color', self.color)
self.layout.addItem(QtWidgets.QSpacerItem(0, 8))
self.layout.addRow(Title('Alignement'))
self.layout.addRow('Horizontal', self.halignement)
self.layout.addRow('Vertical', self.valignement)
self.layout.addItem(QtWidgets.QSpacerItem(0, 8))
for label in self.findChildren(QtWidgets.QLabel):
alignment = QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter
if not isinstance(label, Title):
label.setAlignment(alignment)
label.setFixedWidth(LEFT_CELL_WIDTH)
def valign_changed(self):
self.optionSet.emit('text.valign', self.valignement.currentText())
def halign_changed(self):
self.optionSet.emit('text.halign', self.halignement.currentText())
def set_options(self, options):
values = list({option['text.content'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.text.setText(value)
values = list({option['text.size'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.size.setText(value)
values = list({option['text.bold'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.bold.setCurrentText(value)
values = list({option['text.italic'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.italic.setCurrentText(value)
values = list({option['text.color'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.color.set_color(value)
values = list({option['text.halign'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.halignement.setCurrentText(value)
values = list({option['text.valign'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.valignement.setCurrentText(value)

View File

@@ -0,0 +1,451 @@
from functools import partial
from ..pyside import QtCore, QtGui, QtWidgets
from maya import cmds
from ..align import align_shapes_on_line
from ..interactive import Manipulator, SelectionSquare
from ..interactionmanager import InteractionManager
from ..optionvar import SNAP_GRID_X, SNAP_GRID_Y, SNAP_ITEMS
from ..geometry import get_shapes_bounding_rects, get_connection_path
from ..painting import (
draw_editor_canvas, draw_shape, draw_manipulator, draw_selection_square,
draw_parenting_shapes, draw_current_panel, draw_shape_as_child_background,
draw_connections)
from ..qtutils import get_cursor
from ..selection import Selection, get_selection_mode
from ..shape import cursor_in_shape
from ..transform import Transform
from ..viewport import ViewportMapper
def load_saved_snap():
if not cmds.optionVar(query=SNAP_ITEMS):
return
return (
cmds.optionVar(query=SNAP_GRID_X),
cmds.optionVar(query=SNAP_GRID_Y))
class ShapeEditCanvas(QtWidgets.QWidget):
selectedShapesChanged = QtCore.Signal()
callContextMenu = QtCore.Signal(QtCore.QPoint)
def __init__(self, document, display_options, parent=None):
super(ShapeEditCanvas, self).__init__(parent)
self.setMouseTracking(True)
self.display_options = display_options
method = partial(self.update_selection, False)
self.display_options.options_changed.connect(method)
self.display_options.options_changed.connect(self.update)
self.document = document
method = partial(self.update_selection, False)
self.document.data_changed.connect(method)
self.drag_shapes = []
self.viewportmapper = ViewportMapper()
self.viewportmapper.viewsize = self.size()
self.interaction_manager = InteractionManager()
self.selection = Selection(self.document)
self.selection_square = SelectionSquare()
self.manipulator = Manipulator(self.viewportmapper)
self.transform = Transform(load_saved_snap())
self.parenting_shapes = None
self.clicked_shape = None
self.increase_undo_on_release = False
self.lock_background_shape = True
self.ctrl_pressed = False
self.shit_pressed = False
def wheelEvent(self, event):
# To center the zoom on the mouse, we save a reference mouse position
# and compare the offset after zoom computation.
factor = .25 if event.angleDelta().y() > 0 else -.25
self.zoom(factor, event.pos())
self.update()
def select_panel_shapes(self, panel):
panel_shapes = [
s for s in self.document.shapes if
s.options['panel'] == panel]
if panel_shapes:
self.selection.set(panel_shapes)
self.update_selection()
def focus(self):
shapes = self.selection.shapes or self.visible_shapes()
if not shapes:
self.update()
return
self.viewportmapper.viewsize = self.size()
rect = get_shapes_bounding_rects(shapes)
self.viewportmapper.focus(rect)
self.update()
def zoom(self, factor, reference):
abspoint = self.viewportmapper.to_units_coords(reference)
if factor > 0:
self.viewportmapper.zoomin(abs(factor))
else:
self.viewportmapper.zoomout(abs(factor))
relcursor = self.viewportmapper.to_viewport_coords(abspoint)
vector = relcursor - reference
self.viewportmapper.origin = self.viewportmapper.origin + vector
def set_lock_background_shape(self, state):
self.lock_background_shape = state
def get_hovered_shape(self, cursor, skip_backgrounds=False):
for shape in reversed(self.list_shapes(skip_backgrounds)):
if cursor_in_shape(shape, cursor):
return shape
def list_shapes(self, skip_background=False):
if self.lock_background_shape or skip_background:
return [
shape for shape in self.visible_shapes()
if not shape.is_background()]
return self.visible_shapes()
def leaveEvent(self, _):
for shape in self.list_shapes():
shape.hovered = False
self.update()
def mousePressEvent(self, event):
skip = (
event.button() == QtCore.Qt.RightButton and
self.interaction_manager.left_click_pressed)
if skip:
return
self.setFocus(QtCore.Qt.MouseFocusReason) # This is not automatic
if self.drag_shapes and event.button() == QtCore.Qt.LeftButton:
pos = self.viewportmapper.to_units_coords(event.pos())
align_shapes_on_line(self.drag_shapes, pos, pos)
self.interaction_manager.update(
event,
pressed=True,
has_shape_hovered=False,
dragging=bool(self.drag_shapes))
self.update()
return
cursor = self.viewportmapper.to_units_coords(get_cursor(self))
hovered_shape = self.get_hovered_shape(cursor)
self.transform.direction = self.manipulator.get_direction(event.pos())
parenting = (
self.interaction_manager.alt_pressed and not
self.interaction_manager.ctrl_pressed and not
self.interaction_manager.shift_pressed and
hovered_shape and not hovered_shape.options['background'])
if parenting:
self.parenting_shapes = [hovered_shape, None]
self.interaction_manager.update(
event,
pressed=True,
has_shape_hovered=True,
dragging=True)
self.update()
return
if event.button() != QtCore.Qt.LeftButton:
self.interaction_manager.update(
event,
pressed=True,
has_shape_hovered=False,
dragging=False)
self.update()
return
conditions = (
hovered_shape and
hovered_shape not in self.selection and
not self.transform.direction)
if conditions:
self.selection.set([hovered_shape])
self.update_selection()
elif not hovered_shape and not self.transform.direction:
self.selection.set([])
self.update_selection()
self.selection_square.clicked(cursor)
if self.manipulator.rect is not None:
self.transform.set_rect(self.manipulator.rect)
self.transform.reference_rect = QtCore.QRectF(self.manipulator.rect)
self.transform.set_reference_point(cursor)
self.update()
self.interaction_manager.update(
event,
pressed=True,
has_shape_hovered=bool(hovered_shape),
dragging=bool(hovered_shape) or self.transform.direction)
def resizeEvent(self, event):
self.viewportmapper.viewsize = self.size()
size = (event.size() - event.oldSize()) / 2
offset = QtCore.QPointF(size.width(), size.height())
self.viewportmapper.origin -= offset
self.update()
def mouseMoveEvent(self, event):
cursor = self.viewportmapper.to_units_coords(get_cursor(self)).toPoint()
if self.interaction_manager.mode == InteractionManager.DRAGGING:
if self.parenting_shapes:
self.parenting_shapes[1] = self.get_hovered_shape(
cursor, skip_backgrounds=True)
self.update()
return
if self.drag_shapes:
point1 = self.viewportmapper.to_units_coords(
self.interaction_manager.anchor)
point2 = self.viewportmapper.to_units_coords(event.pos())
align_shapes_on_line(self.drag_shapes, point1, point2)
self.increase_undo_on_release = True
return self.update()
rect = self.manipulator.rect
if self.transform.direction:
self.transform.resize(self.selection.shapes, cursor)
elif rect is not None:
self.transform.move(shapes=self.selection, cursor=cursor)
for shape in self.selection:
shape.synchronize_rect()
shape.update_path()
shape.synchronize_image()
self.increase_undo_on_release = True
self.selectedShapesChanged.emit()
elif self.interaction_manager.mode == InteractionManager.SELECTION:
self.selection_square.handle(cursor)
for shape in self.list_shapes():
shape.hovered = self.selection_square.intersects(shape)
elif self.interaction_manager.mode == InteractionManager.NAVIGATION:
offset = self.interaction_manager.mouse_offset(event.pos())
if offset is not None:
self.viewportmapper.origin = (
self.viewportmapper.origin - offset)
else:
for shape in self.list_shapes():
shape.hovered = cursor_in_shape(shape, cursor)
self.update()
def mouseReleaseEvent(self, event):
if event.button() == QtCore.Qt.RightButton:
self.interaction_manager.update(event, pressed=False)
return self.callContextMenu.emit(event.pos())
if event.button() != QtCore.Qt.LeftButton:
self.interaction_manager.update(event, pressed=False)
return
if self.parenting_shapes:
self.parent_shapes()
self.interaction_manager.update(event, pressed=False)
return
if self.drag_shapes:
self.add_drag_shapes()
if self.increase_undo_on_release:
self.document.record_undo()
self.document.shapes_changed.emit()
self.increase_undo_on_release = False
if self.interaction_manager.mode == InteractionManager.SELECTION:
self.select_shapes()
elif self.interaction_manager.mode == InteractionManager.DRAGGING:
self.update_selection(False)
self.interaction_manager.update(event, pressed=False)
self.selection_square.release()
self.update()
def parent_shapes(self):
skip = (
not self.parenting_shapes[1] or
self.parenting_shapes[0].options['id'] ==
self.parenting_shapes[1].options['id'])
if skip:
self.parenting_shapes = None
self.update()
return
children = set(self.parenting_shapes[1].options['children'])
children.add(self.parenting_shapes[0].options['id'])
self.parenting_shapes[1].options['children'] = sorted(children)
self.document.shapes_changed.emit()
self.document.record_undo()
self.parenting_shapes = None
def visible_shapes(self):
conditions = (
not self.display_options.isolate or
self.display_options.current_panel < 0)
if conditions:
return self.document.shapes[:]
return [
s for s in self.document.shapes if
s.options['panel'] == self.display_options.current_panel]
def add_drag_shapes(self):
shapes_data = [s.options for s in self.drag_shapes]
for data in shapes_data:
data['panel'] = max((self.display_options.current_panel, 0))
shapes = self.document.add_shapes(shapes_data, hierarchize=True)
self.document.shapes_changed.emit()
self.drag_shapes = []
self.selection.replace(shapes)
self.update_selection()
def select_shapes(self):
shapes = [
s for s in self.list_shapes()
if self.selection_square.intersects(s)]
if shapes:
self.selection.set(shapes)
self.update_selection()
def keyPressEvent(self, event):
self.key_event(event, True)
def keyReleaseEvent(self, event):
self.key_event(event, False)
def key_event(self, event, pressed):
if event.key() == QtCore.Qt.Key_Shift:
self.transform.square = pressed
self.shit_pressed = pressed
if event.key() == QtCore.Qt.Key_Control:
self.ctrl_pressed = pressed
self.selection.mode = get_selection_mode(
shift=self.shit_pressed,
ctrl=self.ctrl_pressed)
self.update()
def update_selection(self, changed=True):
shapes = [s for s in self.selection if s in self.visible_shapes()]
if shapes:
rect = get_shapes_bounding_rects(shapes)
else:
rect = None
self.manipulator.set_rect(rect)
if changed:
self.selectedShapesChanged.emit()
def paintEvent(self, _):
try:
painter = QtGui.QPainter()
painter.begin(self)
self.paint(painter)
except BaseException:
import traceback
print(traceback.format_exc())
pass # avoid crash
# TODO: log the error
finally:
painter.end()
def paint(self, painter):
painter.setRenderHint(QtGui.QPainter.Antialiasing)
visible_shapes = self.visible_shapes()
# Draw background and marks.
draw_editor_canvas(
painter, self.rect(),
snap=self.transform.snap,
viewportmapper=self.viewportmapper)
# Get the visible shapes.
current_panel_shapes = []
for shape in self.document.shapes:
if shape.options['panel'] == self.display_options.current_panel:
current_panel_shapes.append(shape)
# Draw the current select panel boundaries.
if current_panel_shapes:
rect = get_shapes_bounding_rects(current_panel_shapes)
draw_current_panel(painter, rect, self.viewportmapper)
# Draw highlighted selected children.
for id_ in self.display_options.highlighted_children_ids:
shape = self.document.shapes_by_id.get(id_)
if shape in visible_shapes:
draw_shape_as_child_background(
painter, shape, viewportmapper=self.viewportmapper)
if self.interaction_manager.left_click_pressed:
visible_shapes.extend(self.drag_shapes)
cutter = QtGui.QPainterPath()
cutter.setFillRule(QtCore.Qt.WindingFill)
for shape in visible_shapes:
qpath = draw_shape(
painter, shape,
draw_selected_state=False,
viewportmapper=self.viewportmapper)
screen_space = shape.options['shape.space'] == 'screen'
if not shape.options['background'] or screen_space:
cutter.addPath(qpath)
connections_path = QtGui.QPainterPath()
if self.display_options.display_hierarchy:
for shape in visible_shapes:
for child in shape.options['children']:
child = self.document.shapes_by_id.get(child)
if child is None:
continue
start_point = shape.bounding_rect().center()
end_point = child.bounding_rect().center()
path = get_connection_path(
start_point, end_point, self.viewportmapper)
connections_path.addPath(path)
connections_path = connections_path.subtracted(cutter)
draw_connections(painter, connections_path)
if self.parenting_shapes:
draw_parenting_shapes(
painter=painter,
child=self.parenting_shapes[0],
potential_parent=self.parenting_shapes[1],
cursor=get_cursor(self),
viewportmapper=self.viewportmapper)
conditions = (
self.manipulator.rect is not None and
all(self.manipulator.viewport_handlers()))
if conditions:
draw_manipulator(
painter, self.manipulator,
get_cursor(self), self.viewportmapper)
if self.selection_square.rect:
draw_selection_square(
painter, self.selection_square.rect, self.viewportmapper)
def delete_selection(self):
self.document.remove_shapes(self.selection.shapes)
self.selection.clear()
self.document.shapes_changed.emit()
self.manipulator.set_rect(None)
self.document.record_undo()

View File

@@ -0,0 +1,17 @@
from ..pyside import QtCore
from maya import cmds
from ..optionvar import (
ISOLATE_CURRENT_PANEL_SHAPES, DISPLAY_HIERARCHY_IN_CANVAS)
class DisplayOptions(QtCore.QObject):
options_changed = QtCore.Signal()
def __init__(self):
super(DisplayOptions, self).__init__()
self.isolate = cmds.optionVar(query=ISOLATE_CURRENT_PANEL_SHAPES)
self.current_panel = -1
self.highlighted_children_ids = []
state = cmds.optionVar(query=DISPLAY_HIERARCHY_IN_CANVAS)
self.display_hierarchy = bool(state)

View File

@@ -0,0 +1,613 @@
from functools import partial
from copy import deepcopy
from ..pyside import QtWidgets, QtCore, QtGui
from maya import cmds
from .. import clipboard
from ..align import align_shapes, arrange_horizontal, arrange_vertical
from ..arrayutils import (
move_elements_to_array_end, move_elements_to_array_begin,
move_up_array_elements, move_down_array_elements)
from ..dialog import (
SearchAndReplaceDialog, warning, SettingsPaster, get_image_path)
from ..geometry import (
rect_symmetry, path_symmetry, get_shapes_bounding_rects,
rect_top_left_symmetry)
from ..optionvar import BG_LOCKED, TRIGGER_REPLACE_ON_MIRROR
from ..path import format_path
from ..qtutils import set_shortcut, get_cursor
from ..shape import Shape, get_shape_rect_from_options
from ..shapelibrary import ShapeLibraryMenu
from ..stack import count_panels
from ..templates import BUTTON, TEXT, BACKGROUND, SHAPE_BUTTON
from .canvas import ShapeEditCanvas
from .display import DisplayOptions
from .menu import MenuWidget
from .attributes import AttributeEditor
DIRECTION_OFFSETS = {
'Left': (-1, 0), 'Right': (1, 0), 'Up': (0, -1), 'Down': (0, 1)}
class PickerEditor(QtWidgets.QWidget):
def __init__(self, document, parent=None):
super(PickerEditor, self).__init__(parent, QtCore.Qt.Window)
title = "Picker editor - " + document.data['general']['name']
self.setWindowTitle(title)
self.document = document
self.document.shapes_changed.connect(self.update)
self.document.general_option_changed.connect(self.generals_modified)
self.document.data_changed.connect(self.update)
self.document.data_changed.connect(self.selection_changed)
self.display_options = DisplayOptions()
self.shape_canvas = ShapeEditCanvas(
self.document, self.display_options)
self.shape_canvas.callContextMenu.connect(self.call_context_menu)
bg_locked = bool(cmds.optionVar(query=BG_LOCKED))
self.shape_canvas.set_lock_background_shape(bg_locked)
self.shape_canvas.selectedShapesChanged.connect(self.selection_changed)
self.shape_library_menu = ShapeLibraryMenu(self)
self.shape_library_menu.path_selected.connect(
self.create_library_shape)
self.menu = MenuWidget(self.display_options)
self.menu.copyRequested.connect(self.copy)
self.menu.copySettingsRequested.connect(self.copy_settings)
self.menu.deleteRequested.connect(self.shape_canvas.delete_selection)
self.menu.pasteRequested.connect(self.paste)
self.menu.pasteSettingsRequested.connect(self.paste_settings)
self.menu.snapValuesChanged.connect(self.snap_value_changed)
self.menu.buttonLibraryRequested.connect(self.call_library)
self.menu.useSnapToggled.connect(self.use_snap)
method = self.shape_canvas.set_lock_background_shape
self.menu.lockBackgroundShapeToggled.connect(method)
self.menu.undoRequested.connect(self.document.undo)
self.menu.redoRequested.connect(self.document.redo)
method = partial(self.create_shape, BUTTON)
self.menu.addButtonRequested.connect(method)
method = partial(self.create_shape, TEXT)
self.menu.addTextRequested.connect(method)
method = partial(self.create_shape, BACKGROUND, before=True, image=True)
self.menu.addBackgroundRequested.connect(method)
method = self.set_selection_move_down
self.menu.moveDownRequested.connect(method)
method = self.set_selection_move_up
self.menu.moveUpRequested.connect(method)
method = self.set_selection_on_top
self.menu.onTopRequested.connect(method)
method = self.set_selection_on_bottom
self.menu.onBottomRequested.connect(method)
self.menu.symmetryRequested.connect(self.do_symmetry)
self.menu.searchAndReplaceRequested.connect(self.search_and_replace)
self.menu.alignRequested.connect(self.align_selection)
self.menu.arrangeRequested.connect(self.arrange_selection)
self.menu.load_ui_states()
set_shortcut("Ctrl+Z", self.shape_canvas, self.document.undo)
set_shortcut("Ctrl+Y", self.shape_canvas, self.document.redo)
set_shortcut("Ctrl+C", self.shape_canvas, self.copy)
set_shortcut("Ctrl+V", self.shape_canvas, self.paste)
set_shortcut("Ctrl+R", self.shape_canvas, self.search_and_replace)
set_shortcut("del", self.shape_canvas, self.shape_canvas.delete_selection)
set_shortcut("Ctrl+D", self.shape_canvas, self.deselect_all)
set_shortcut("Ctrl+A", self.shape_canvas, self.select_all)
set_shortcut("Ctrl+I", self.shape_canvas, self.invert_selection)
set_shortcut("U", self.shape_canvas, self.update_targets_on_selection)
set_shortcut("F", self.shape_canvas, self.shape_canvas.focus)
for direction in ['Left', 'Right', 'Up', 'Down']:
method = partial(self.move_selection, direction)
shortcut = set_shortcut(direction, self.shape_canvas, method)
shortcut.setAutoRepeat(True)
self.attribute_editor = AttributeEditor(document, self.display_options)
self.attribute_editor.optionSet.connect(self.option_set)
self.attribute_editor.optionsSet.connect(self.options_set)
self.attribute_editor.imageModified.connect(self.image_modified)
self.attribute_editor.selectLayerContent.connect(self.select_layer)
self.attribute_editor.panelDoubleClicked.connect(
self.shape_canvas.select_panel_shapes)
self.hlayout = QtWidgets.QHBoxLayout()
self.hlayout.setSizeConstraint(QtWidgets.QLayout.SetMaximumSize)
self.hlayout.setContentsMargins(0, 0, 0, 0)
self.hlayout.addWidget(self.shape_canvas)
self.hlayout.addWidget(self.attribute_editor)
self.vlayout = QtWidgets.QVBoxLayout(self)
self.vlayout.setContentsMargins(0, 0, 0, 0)
self.vlayout.setSpacing(0)
self.vlayout.addWidget(self.menu)
self.vlayout.addLayout(self.hlayout)
def call_library(self, point):
self.shape_library_menu.move(point)
self.shape_library_menu.show()
def panels_changed(self, panels):
self.document.data['general']['panels'] = panels
def panels_resized(self, panels):
self.document.data['general']['panels'] = panels
def copy(self):
clipboard.set([
deepcopy(s.options) for s in self.shape_canvas.selection])
def copy_settings(self):
if len(self.shape_canvas.selection) != 1:
return warning('Copy settings', 'Please select only one shape')
shape = self.shape_canvas.selection[0]
clipboard.set_settings(deepcopy(shape.options))
def sizeHint(self):
return QtCore.QSize(1300, 750)
def paste(self):
clipboad_copy = [deepcopy(s) for s in clipboard.get()]
shapes = self.document.add_shapes(clipboad_copy)
self.shape_canvas.selection.replace(shapes)
self.shape_canvas.update_selection()
self.document.record_undo()
self.document.shapes_changed.emit()
def paste_settings(self):
dialog = SettingsPaster()
if not dialog.exec_():
return
settings = clipboard.get_settings()
settings = {k: v for k, v in settings.items() if k in dialog.settings}
for shape in self.shape_canvas.selection:
shape.options.update(deepcopy(settings))
shape.rect = get_shape_rect_from_options(shape.options)
shape.synchronize_image()
shape.update_path()
self.document.record_undo()
self.document.shapes_changed.emit()
self.selection_changed()
self.shape_canvas.update_selection()
self.shape_canvas.update()
def deselect_all(self):
self.shape_canvas.selection.clear()
self.shape_canvas.update_selection()
self.shape_canvas.update()
def select_all(self):
shapes = self.shape_canvas.list_shapes()
self.shape_canvas.selection.add(shapes)
self.shape_canvas.update_selection()
self.shape_canvas.update()
def invert_selection(self):
self.shape_canvas.selection.invert(self.shape_canvas.shapes)
if self.menu.lock_bg.isChecked():
shapes = [
s for s in self.shape_canvas.selection
if not s.is_background()]
self.shape_canvas.selection.set(shapes)
self.shape_canvas.update_selection()
self.shape_canvas.update()
def use_snap(self, state):
snap = self.menu.snap_values() if state else None
self.shape_canvas.transform.snap = snap
self.shape_canvas.update()
def snap_value_changed(self):
self.shape_canvas.transform.snap = self.menu.snap_values()
self.shape_canvas.update()
def generals_modified(self, _, key):
if key == 'name':
title = "Picker editor - " + self.document.data['general']['name']
self.setWindowTitle(title)
def options_set(self, options, rect_update):
for shape in self.shape_canvas.selection:
shape.options.update(options)
if rect_update:
shape.rect = QtCore.QRectF(
options['shape.left'],
options['shape.top'],
options['shape.width'],
options['shape.height'])
shape.update_path()
self.shape_canvas.update()
self.update_manipulator_rect()
self.document.record_undo()
self.document.shapes_changed.emit()
def option_set(self, option, value):
update_geometries = False
update_selection = False
rect_options = (
'shape.top', 'shape.width', 'shape.height', 'shape.left')
for shape in self.shape_canvas.selection:
shape.options[option] = value
if option in ('shape.path', 'shape'):
if value == 'custom' and not shape.options['shape.path']:
update_selection = True
shape.update_path()
shape.synchronize_image()
update_geometries = True
if option in rect_options:
shape.rect = QtCore.QRectF(
shape.options['shape.left'],
shape.options['shape.top'],
shape.options['shape.width'],
shape.options['shape.height'])
shape.update_path()
shape.synchronize_image()
update_geometries = True
if update_selection:
self.selection_changed()
if update_geometries:
self.update_manipulator_rect()
if option == 'visibility_layer':
self.layers_modified()
else:
self.document.shapes_changed.emit()
self.document.record_undo()
self.shape_canvas.update()
def selection_changed(self):
shapes = self.shape_canvas.selection
options = [shape.options for shape in shapes]
self.attribute_editor.set_options(options)
def create_shapes(self, targets, use_clipboard_data=False):
shapes = []
for target in targets:
template = deepcopy(BUTTON)
if use_clipboard_data:
template.update(deepcopy(clipboard.get_settings()))
template['action.targets'] = [target]
shapes.append(Shape(template))
self.shape_canvas.drag_shapes = shapes
def create_library_shape(self, path):
options = deepcopy(SHAPE_BUTTON)
options['shape.path'] = deepcopy(path)
self.create_shape(options)
def create_shape(
self, template, before=False, position=None, targets=None,
image=False):
options = deepcopy(template)
panel = self.shape_canvas.display_options.current_panel
options['panel'] = max((panel, 0))
if image:
filename = get_image_path(self, "Select background image.")
if filename:
filename = format_path(filename)
options['image.path'] = filename
qimage = QtGui.QImage(filename)
options['image.width'] = qimage.size().width()
options['image.height'] = qimage.size().height()
options['shape.width'] = qimage.size().width()
options['shape.height'] = qimage.size().height()
options['bgcolor.transparency'] = 255
shape = Shape(options)
if not position:
center = self.shape_canvas.rect().center()
center = self.shape_canvas.viewportmapper.to_units_coords(center)
if not options['shape.path']:
shape.rect.moveCenter(center)
else:
shape.rect.moveTopLeft(center - shape.bounding_rect().center())
else:
tl = self.shape_canvas.viewportmapper.to_units_coords(position)
shape.rect.moveTopLeft(tl)
if targets:
shape.set_targets(targets)
shape.synchronize_rect()
shape.update_path()
shapes = self.document.add_shapes([shape.options], prepend=before)
self.document.shapes_changed.emit()
self.document.record_undo()
self.shape_canvas.selection.replace(shapes)
self.selection_changed()
self.update_manipulator_rect()
def update_targets_on_selection(self):
if not self.shape_canvas.selection:
return
targets = cmds.ls(selection=True)
for shape in self.shape_canvas.selection:
shape.set_targets(targets)
self.shape_canvas.update()
self.document.shapes_changed.emit()
self.document.record_undo()
def update_targets(self, shape):
shape.set_targets(cmds.ls(selection=True))
self.shape_canvas.update()
self.document.shapes_changed.emit()
self.document.record_undo()
def image_modified(self):
for shape in self.shape_canvas.selection:
shape.synchronize_image()
self.shape_canvas.update()
def set_selection_move_on_stack(self, function, inplace=True):
selected_ids = [s.options['id'] for s in self.shape_canvas.selection]
all_ids = list(self.document.shapes_by_id)
result = function(all_ids, selected_ids)
if inplace:
result = all_ids
data = [self.document.shapes_by_id[id_].options for id_ in result]
self.document.set_shapes_data(data)
self.document.record_undo()
self.document.shapes_changed.emit()
self.shape_canvas.update()
def set_selection_move_down(self):
self.set_selection_move_on_stack(move_down_array_elements, True)
def set_selection_move_up(self):
self.set_selection_move_on_stack(move_up_array_elements, True)
def set_selection_on_top(self):
self.set_selection_move_on_stack(move_elements_to_array_end, False)
def set_selection_on_bottom(self):
self.set_selection_move_on_stack(move_elements_to_array_begin, False)
def update_manipulator_rect(self):
rect = get_shapes_bounding_rects(self.shape_canvas.selection)
self.shape_canvas.manipulator.set_rect(rect)
self.shape_canvas.update()
def do_symmetry(self, horizontal=True):
shapes = self.shape_canvas.selection.shapes
for shape in shapes:
if shape.options['shape'] == 'custom':
path_symmetry(
path=shape.options['shape.path'],
horizontal=horizontal)
rect_top_left_symmetry(
rect=shape.rect,
point=self.shape_canvas.manipulator.rect.center(),
horizontal=horizontal)
shape.synchronize_rect()
shape.update_path()
else:
rect_symmetry(
rect=shape.rect,
point=self.shape_canvas.manipulator.rect.center(),
horizontal=horizontal)
shape.synchronize_rect()
self.shape_canvas.update()
self.document.shapes_changed.emit()
if not cmds.optionVar(query=TRIGGER_REPLACE_ON_MIRROR):
self.document.record_undo()
return
if not self.search_and_replace():
self.document.record_undo()
self.attribute_editor.update()
self.update_manipulator_rect()
def search_and_replace(self):
dialog = SearchAndReplaceDialog()
if not dialog.exec_():
return False
if dialog.filter == 0: # Search on all shapes.
shapes = self.shape_canvas.shapes
else:
shapes = self.shape_canvas.selection
pattern = dialog.search.text()
replace = dialog.replace.text()
for s in shapes:
if not dialog.field: # Targets
if not s.targets():
continue
targets = [t.replace(pattern, replace) for t in s.targets()]
s.options['action.targets'] = targets
continue
if dialog.field <= 2:
key = ('text.content', 'image.path')[dialog.field - 1]
result = s.options[key].replace(pattern, replace)
s.options[key] = result
else: # Command code
for command in s.options['action.commands']:
result = command['command'].replace(pattern, replace)
command['command'] = result
self.document.shape_changed.emit()
self.document.record_undo()
self.shape_canvas.update()
return True
def move_selection(self, direction):
offset = DIRECTION_OFFSETS[direction]
rect = self.shape_canvas.manipulator.rect
reference_rect = QtCore.QRectF(rect)
self.shape_canvas.transform.set_rect(rect)
self.shape_canvas.transform.reference_rect = reference_rect
self.shape_canvas.transform.shift(
self.shape_canvas.selection.shapes, offset)
for shape in self.shape_canvas.selection:
shape.synchronize_rect()
shape.update_path()
self.shape_canvas.update()
self.selection_changed()
self.document.record_undo()
self.document.shapes_changed.emit()
def align_selection(self, direction):
if not self.shape_canvas.selection:
return
align_shapes(self.shape_canvas.selection, direction)
rect = get_shapes_bounding_rects(self.shape_canvas.selection)
self.shape_canvas.manipulator.set_rect(rect)
self.shape_canvas.update()
self.selection_changed()
self.document.record_undo()
self.document.shapes_changed.emit()
def arrange_selection(self, direction):
if not self.shape_canvas.selection:
return
if direction == 'horizontal':
arrange_horizontal(self.shape_canvas.selection)
else:
arrange_vertical(self.shape_canvas.selection)
rect = get_shapes_bounding_rects(self.shape_canvas.selection)
self.shape_canvas.manipulator.set_rect(rect)
self.shape_canvas.update()
self.selection_changed()
self.document.record_undo()
self.document.shapes_changed.emit()
def call_context_menu(self, position):
targets = cmds.ls(selection=True)
button = QtWidgets.QAction('Add selection button', self)
method = partial(
self.create_shape, deepcopy(BUTTON),
position=position, targets=targets)
button.triggered.connect(method)
template = deepcopy(BUTTON)
template.update(clipboard.get_settings())
method = partial(
self.create_shape, template,
position=position, targets=targets)
text = 'Add selection button (using settings clipboard)'
button2 = QtWidgets.QAction(text, self)
button2.triggered.connect(method)
button3 = QtWidgets.QAction('Add selection multiple buttons', self)
button3.triggered.connect(partial(self.create_shapes, targets))
button3.setEnabled(len(targets) > 1)
text = 'Add selection multiple buttons (using settings clipboard)'
button4 = QtWidgets.QAction(text, self)
button4.triggered.connect(partial(self.create_shapes, targets, True))
button4.setEnabled(len(targets) > 1)
cursor = get_cursor(self.shape_canvas)
cursor = self.shape_canvas.viewportmapper.to_units_coords(cursor)
hovered_shape = self.shape_canvas.get_hovered_shape(cursor)
method = partial(self.update_targets, hovered_shape)
text = 'Update targets'
button5 = QtWidgets.QAction(text, self)
button5.setEnabled(bool(hovered_shape))
button5.triggered.connect(method)
button6 = QtWidgets.QAction('Clear children', self)
button6.setEnabled(bool(self.shape_canvas.selection or hovered_shape))
method = partial(self.clear_children, hovered_shape)
button6.triggered.connect(method)
menu = QtWidgets.QMenu()
menu.addAction(button)
menu.addAction(button2)
menu.addAction(button3)
menu.addAction(button4)
menu.addAction(button5)
menu.addSection('Hierarchy')
menu.addAction(button6)
menu.addSection('Visibility Layers')
layers = sorted(list({
s.visibility_layer()
for s in self.document.shapes
if s.visibility_layer()}))
add_selection = QtWidgets.QMenu('Assign to layer', self)
add_selection.setEnabled(bool(layers))
menu.addMenu(add_selection)
for layer in layers:
action = QtWidgets.QAction(layer, self)
action.triggered.connect(partial(self.set_visibility_layer, layer))
add_selection.addAction(action)
remove_selection = QtWidgets.QAction('Remove assigned layer', self)
remove_selection.setEnabled(bool(self.shape_canvas.selection.shapes))
remove_selection.triggered.connect(self.set_visibility_layer)
menu.addAction(remove_selection)
create_layer = QtWidgets.QAction('Create layer from selection', self)
create_layer.triggered.connect(self.create_visibility_layer)
create_layer.setEnabled(bool(self.shape_canvas.selection.shapes))
menu.addAction(create_layer)
menu.addSeparator()
assign_to_panel = QtWidgets.QMenu('Assign to panel', self)
for i in range(count_panels(self.document.data['general']['panels'])):
action = QtWidgets.QAction(str(i + 1), self)
action.triggered.connect(partial(self.assign_to_panel, i))
assign_to_panel.addAction(action)
menu.addMenu(assign_to_panel)
menu.exec_(self.shape_canvas.mapToGlobal(position))
def clear_children(self, hovered_shape):
if hovered_shape and hovered_shape not in self.shape_canvas.selection:
shapes = [hovered_shape]
else:
shapes = self.shape_canvas.selection
for shape in shapes:
shape.options['children'] = []
self.document.shapes_changed.emit()
self.document.record_undo()
def set_visibility_layer(self, layer=''):
for shape in self.shape_canvas.selection:
shape.options['visibility_layer'] = layer
self.layers_modified()
def assign_to_panel(self, panel):
for shape in self.shape_canvas.selection:
shape.options['panel'] = panel
self.document.shapes_changed.emit()
self.document.record_undo()
self.document.sync_shapes_caches()
self.shape_canvas.update_selection(False)
def layers_modified(self):
self.selection_changed()
model = self.attribute_editor.generals.layers.model
model.layoutAboutToBeChanged.emit()
self.document.sync_shapes_caches()
self.document.record_undo()
self.document.shapes_changed.emit()
model.layoutChanged.emit()
def create_visibility_layer(self):
text, result = QtWidgets.QInputDialog.getText(
self, 'Create visibility layer', 'Layer name')
if not text or not result:
return
for shape in self.shape_canvas.selection:
shape.options['visibility_layer'] = text
self.layers_modified()
def select_layer(self, layer):
self.shape_canvas.selection.set(self.document.shapes_by_layer[layer])
self.shape_canvas.update_selection()
self.shape_canvas.update()
self.selection_changed()

View File

@@ -0,0 +1,114 @@
import keyword
from ..pyside import QtGui, QtCore
from ..languages import PYTHON, MEL
MELKEYWORDS = [
'if', 'else', 'int', 'float', 'double', 'string', 'array'
'var', 'return', 'case', 'then', 'continue', 'break', 'global', 'proc']
TEXT_STYLES = {
'keyword': {
'color': 'white',
'bold': True,
'italic': False},
'number': {
'color': 'cyan',
'bold': False,
'italic': False},
'comment': {
'color': (0.7, 0.5, 0.5),
'bold': False,
'italic': False},
'function': {
'color': '#ff0571',
'bold': False,
'italic': True},
'string': {
'color': 'yellow',
'bold': False,
'italic': False},
'boolean': {
'color': '#a18852',
'bold': True,
'italic': False}}
PATTERNS = {
PYTHON: {
'keyword': r'\b|'.join(keyword.kwlist),
'number': r'\b[+-]?[0-9]+[lL]?\b',
'comment': r'#[^\n]*',
'function': r'\b[A-Za-z0-9_]+(?=\()',
'string': r'".*"|\'.*\'',
'boolean': r'\bTrue\b|\bFalse\b'},
MEL: {
'keyword': r'\b|'.join(MELKEYWORDS),
'number': r'\b[+-]?[0-9]+[lL]?\b',
'comment': r'//[^\n]*',
'function': r'\b[A-Za-z0-9_]+(?=\()',
'string': r'".*"|\'.*\'',
'boolean': r'\btrue\b|\bfalse\b'}
}
class Highlighter(QtGui.QSyntaxHighlighter):
PATTERNS = []
def __init__(self, parent=None):
super(Highlighter, self).__init__(parent)
self.rules = []
for name, properties in TEXT_STYLES.items():
if name not in self.PATTERNS:
continue
text_format = create_textcharformat(
color=properties['color'],
bold=properties['bold'],
italic=properties['italic'])
rule = QtCore.QRegularExpression(self.PATTERNS[name]), text_format
self.rules.append(rule)
def highlightBlock(self, text):
for pattern, format_ in self.rules:
expression = QtCore.QRegularExpression(pattern)
iterator = expression.globalMatch(text)
while iterator.hasNext():
match = iterator.next()
index = match.capturedStart()
length = match.capturedLength()
self.setFormat(index, length, format_)
class PythonHighlighter(Highlighter):
PATTERNS = PATTERNS[PYTHON]
class MelHighlighter(Highlighter):
PATTERNS = PATTERNS[MEL]
HIGHLIGHTERS = {
PYTHON: PythonHighlighter,
MEL: MelHighlighter}
def get_highlighter(language):
return HIGHLIGHTERS.get(language, Highlighter)
def create_textcharformat(color, bold=False, italic=False):
char_format = QtGui.QTextCharFormat()
qcolor = QtGui.QColor()
if isinstance(color, str):
qcolor.setNamedColor(color)
else:
r, g, b = color
qcolor.setRgbF(r, g, b)
char_format.setForeground(qcolor)
if bold:
char_format.setFontWeight(QtGui.QFont.Bold)
if italic:
char_format.setFontItalic(True)
return char_format

View File

@@ -0,0 +1,162 @@
from functools import partial
from ..pyside import QtWidgets, QtCore, QtGui
from ..widgets import V, CheckWidget
class VisibilityLayersEditor(QtWidgets.QWidget):
selectLayerContent = QtCore.Signal(str)
def __init__(self, document, parent=None):
super(VisibilityLayersEditor, self).__init__(parent)
self.document = document
self.model = VisbilityLayersModel(document)
self.table = QtWidgets.QTableView()
self.table.setModel(self.model)
self.table.setItemDelegateForColumn(0, CheckDelegate())
self.table.setEditTriggers(QtWidgets.QAbstractItemView.AllEditTriggers)
self.table.verticalHeader().hide()
self.table.horizontalHeader().setSectionResizeMode(
QtWidgets.QHeaderView.ResizeToContents)
self.table.verticalHeader().setSectionResizeMode(
QtWidgets.QHeaderView.ResizeToContents)
self.table.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
self.table.setFixedHeight(100)
self.select_content = QtWidgets.QPushButton('Select layer content')
self.select_content.released.connect(self.call_select_layer)
self.remove_layer = QtWidgets.QPushButton('Remove selected layer')
self.remove_layer.released.connect(self.call_remove_layer)
layout = QtWidgets.QVBoxLayout(self)
layout.setSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self.table)
layout.addWidget(self.select_content)
layout.addWidget(self.remove_layer)
def selected_layer(self):
indexes = self.table.selectedIndexes()
if not indexes:
return
layer = sorted(list(self.document.shapes_by_layer))[indexes[0].row()]
return layer
def call_remove_layer(self):
layer = self.selected_layer()
if not layer:
return
for shape in self.document.shapes_by_layer[layer]:
if shape.visibility_layer() == layer:
shape.options['visibility_layer'] = None
self.model.layoutAboutToBeChanged.emit()
self.document.sync_shapes_caches()
self.document.shapes_changed.emit()
self.document.record_undo()
self.model.layoutChanged.emit()
def call_select_layer(self):
layer = self.selected_layer()
if not layer:
return
self.selectLayerContent.emit(layer)
class CheckDelegate(QtWidgets.QItemDelegate):
def mouseReleaseEvent(self, event):
if event.button() != QtCore.Qt.LeftButton:
return
self.update()
def createEditor(self, parent, _, index):
model = index.model()
hidden_layers = model.document.data['general']['hidden_layers']
layer = model.data(index)
state = layer in hidden_layers
model.set_hidden_layer(layer, not state)
checker = CheckWidget(not state, parent)
checker.toggled.connect(partial(model.set_hidden_layer, layer))
return checker
def paint(self, painter, option, index):
model = index.model()
hidden_layers = model.document.data['general']['hidden_layers']
state = model.data(index) in hidden_layers
center = option.rect.center()
painter.setBrush(QtCore.Qt.NoBrush)
rect = QtCore.QRectF(center.x() - 10, center.y() - 10, 20, 20)
if not state:
return
font = QtGui.QFont()
font.setPixelSize(20)
option = QtGui.QTextOption()
option.setAlignment(QtCore.Qt.AlignCenter)
painter.drawText(rect, V, option)
class VisbilityLayersModel(QtCore.QAbstractTableModel):
HEADERS = 'hide', 'name', 'shapes'
def __init__(self, document, parent=None):
super(VisbilityLayersModel, self).__init__(parent)
self.document = document
self.document.changed.connect(self.layoutChanged.emit)
def rowCount(self, _):
return len(self.document.shapes_by_layer)
def columnCount(self, _):
return len(self.HEADERS)
def headerData(self, section, orientation, role):
if orientation == QtCore.Qt.Vertical or role != QtCore.Qt.DisplayRole:
return
return self.HEADERS[section]
def flags(self, index):
flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
if index.column() == 0:
flags |= QtCore.Qt.ItemIsEditable
return flags
def set_hidden_layer(self, layer, state):
self.layoutAboutToBeChanged.emit()
hidden_layers = self.document.data['general']['hidden_layers']
if state and layer not in hidden_layers:
hidden_layers.append(layer)
elif not state and layer in hidden_layers:
hidden_layers.remove(layer)
else:
self.layoutChanged.emit()
return
self.document.record_undo()
self.document.general_option_changed.emit('editor', 'hidden_layers')
self.layoutChanged.emit()
def data(self, index, role=QtCore.Qt.UserRole):
if not index.isValid():
return
if role == QtCore.Qt.TextAlignmentRole:
if index.column() == 2:
return QtCore.Qt.AlignCenter
layers = sorted(list(self.document.shapes_by_layer))
if role == QtCore.Qt.UserRole:
return layers[index.row()]
if role != QtCore.Qt.DisplayRole:
return
if index.column() == 1:
return layers[index.row()]
if index.column() == 2:
layer = layers[index.row()]
return str(len(self.document.shapes_by_layer[layer]))

View File

@@ -0,0 +1,289 @@
from functools import partial
from maya import cmds
from ..pyside import QtGui, QtWidgets, QtCore
from ..optionvar import (
BG_LOCKED, DISPLAY_HIERARCHY_IN_CANVAS, ISOLATE_CURRENT_PANEL_SHAPES,
SNAP_ITEMS, SNAP_GRID_X, SNAP_GRID_Y, save_optionvar)
from ..qtutils import icon
class MenuWidget(QtWidgets.QWidget):
addBackgroundRequested = QtCore.Signal()
addButtonRequested = QtCore.Signal()
addTextRequested = QtCore.Signal()
alignRequested = QtCore.Signal(str)
arrangeRequested = QtCore.Signal(str)
buttonLibraryRequested = QtCore.Signal(QtCore.QPoint)
centerValuesChanged = QtCore.Signal(int, int)
copyRequested = QtCore.Signal()
copySettingsRequested = QtCore.Signal()
deleteRequested = QtCore.Signal()
editCenterToggled = QtCore.Signal(bool)
lockBackgroundShapeToggled = QtCore.Signal(bool)
moveDownRequested = QtCore.Signal()
moveUpRequested = QtCore.Signal()
onBottomRequested = QtCore.Signal()
onTopRequested = QtCore.Signal()
pasteRequested = QtCore.Signal()
pasteSettingsRequested = QtCore.Signal()
redoRequested = QtCore.Signal()
searchAndReplaceRequested = QtCore.Signal()
snapValuesChanged = QtCore.Signal()
symmetryRequested = QtCore.Signal(bool)
undoRequested = QtCore.Signal()
useSnapToggled = QtCore.Signal(bool)
def __init__(self, display_options, parent=None):
super(MenuWidget, self).__init__(parent=parent)
self.display_options = display_options
self.delete = QtWidgets.QAction(icon('delete.png'), '', self)
self.delete.setToolTip('Delete selection')
self.delete.triggered.connect(self.deleteRequested.emit)
self.copy = QtWidgets.QAction(icon('copy.png'), '', self)
self.copy.setToolTip('Copy selection')
self.copy.triggered.connect(self.copyRequested.emit)
self.paste = QtWidgets.QAction(icon('paste.png'), '', self)
self.paste.setToolTip('Paste')
self.paste.triggered.connect(self.pasteRequested.emit)
self.undo = QtWidgets.QAction(icon('undo.png'), '', self)
self.undo.setToolTip('Undo')
self.undo.triggered.connect(self.undoRequested.emit)
self.redo = QtWidgets.QAction(icon('redo.png'), '', self)
self.redo.setToolTip('Redo')
self.redo.triggered.connect(self.redoRequested.emit)
icon_ = icon('copy_settings.png')
self.copy_settings = QtWidgets.QAction(icon_, '', self)
self.copy_settings.setToolTip('Copy settings')
self.copy_settings.triggered.connect(self.copySettingsRequested.emit)
icon_ = icon('paste_settings.png')
self.paste_settings = QtWidgets.QAction(icon_, '', self)
self.paste_settings.setToolTip('Paste settings')
self.paste_settings.triggered.connect(self.pasteSettingsRequested.emit)
self.search = QtWidgets.QAction(icon('search.png'), '', self)
self.search.triggered.connect(self.searchAndReplaceRequested.emit)
self.search.setToolTip('Search and replace')
icon_ = icon('lock-non-interactive.png')
self.lock_bg = QtWidgets.QAction(icon_, '', self)
self.lock_bg.setToolTip('Lock background items')
self.lock_bg.setCheckable(True)
self.lock_bg.triggered.connect(self.save_ui_states)
self.lock_bg.toggled.connect(self.lockBackgroundShapeToggled.emit)
self.isolate = QtWidgets.QAction(icon('isolate.png'), '', self)
self.isolate.setToolTip('Isolate current panel shapes')
self.isolate.setCheckable(True)
self.isolate.toggled.connect(self.isolate_panel)
self.hierarchy = QtWidgets.QAction(icon('hierarchy.png'), '', self)
self.hierarchy.setToolTip('Display hierarchy')
self.hierarchy.setCheckable(True)
state = bool(cmds.optionVar(query=DISPLAY_HIERARCHY_IN_CANVAS))
self.hierarchy.setChecked(state)
self.hierarchy.toggled.connect(self.toggle_hierarchy_display)
self.snap = QtWidgets.QAction(icon('snap.png'), '', self)
self.snap.setToolTip('Snap grid enable')
self.snap.setCheckable(True)
self.snap.triggered.connect(self.snap_toggled)
validator = QtGui.QIntValidator(5, 150)
self.snapx = QtWidgets.QLineEdit('10')
self.snapx.setFixedWidth(35)
self.snapx.setValidator(validator)
self.snapx.setEnabled(False)
self.snapx.textEdited.connect(self.snap_value_changed)
self.snapy = QtWidgets.QLineEdit('10')
self.snapy.setFixedWidth(35)
self.snapy.setValidator(validator)
self.snapy.setEnabled(False)
self.snapy.textEdited.connect(self.snap_value_changed)
self.snap.toggled.connect(self.snapx.setEnabled)
self.snap.toggled.connect(self.snapy.setEnabled)
icon_ = icon('addshape.png')
self.call_library = QtWidgets.QAction(icon_, '', self)
self.call_library.setToolTip('Add button')
self.call_library.triggered.connect(self._call_library)
icon_ = icon('addbutton.png')
self.addbutton = QtWidgets.QAction(icon_, '', self)
self.addbutton.setToolTip('Add button')
self.addbutton.triggered.connect(self.addButtonRequested.emit)
self.addtext = QtWidgets.QAction(icon('addtext.png'), '', self)
self.addtext.setToolTip('Add text')
self.addtext.triggered.connect(self.addTextRequested.emit)
self.addbg = QtWidgets.QAction(icon('addbg.png'), '', self)
self.addbg.setToolTip('Add background shape')
self.addbg.triggered.connect(self.addBackgroundRequested.emit)
icon_ = icon('onbottom.png')
self.onbottom = QtWidgets.QAction(icon_, '', self)
self.onbottom.setToolTip('Set selected shapes on bottom')
self.onbottom.triggered.connect(self.onBottomRequested.emit)
icon_ = icon('movedown.png')
self.movedown = QtWidgets.QAction(icon_, '', self)
self.movedown.setToolTip('Move down selected shapes')
self.movedown.triggered.connect(self.moveDownRequested.emit)
self.moveup = QtWidgets.QAction(icon('moveup.png'), '', self)
self.moveup.setToolTip('Move up selected shapes')
self.moveup.triggered.connect(self.moveUpRequested.emit)
self.ontop = QtWidgets.QAction(icon('ontop.png'), '', self)
self.ontop.setToolTip('Set selected shapes on top')
self.ontop.triggered.connect(self.onTopRequested.emit)
self.hsymmetry = QtWidgets.QAction(icon('h_symmetry.png'), '', self)
self.hsymmetry.setToolTip('Mirror a shape horizontally')
method = partial(self.symmetryRequested.emit, True)
self.hsymmetry.triggered.connect(method)
self.vsymmetry = QtWidgets.QAction(icon('v_symmetry.png'), '', self)
self.vsymmetry.setToolTip('Mirror a shape vertically')
method = partial(self.symmetryRequested.emit, False)
self.vsymmetry.triggered.connect(method)
method = partial(self.alignRequested.emit, 'left')
self.align_left = QtWidgets.QAction(icon('align_left.png'), '', self)
self.align_left.triggered.connect(method)
self.align_left.setToolTip('Align to left')
file_ = 'align_h_center.png'
method = partial(self.alignRequested.emit, 'h_center')
self.align_h_center = QtWidgets.QAction(icon(file_), '', self)
self.align_h_center.triggered.connect(method)
self.align_h_center.setToolTip('Align to center horizontally')
method = partial(self.alignRequested.emit, 'right')
self.align_right = QtWidgets.QAction(icon('align_right.png'), '', self)
self.align_right.triggered.connect(method)
self.align_right.setToolTip('Align to right')
method = partial(self.alignRequested.emit, 'top')
self.align_top = QtWidgets.QAction(icon('align_top.png'), '', self)
self.align_top.triggered.connect(method)
self.align_top.setToolTip('Align to top')
file_ = 'align_v_center.png'
self.align_v_center = QtWidgets.QAction(icon(file_), '', self)
method = partial(self.alignRequested.emit, 'v_center')
self.align_v_center.triggered.connect(method)
self.align_v_center.setToolTip('Align to center vertically')
file_ = 'align_bottom.png'
method = partial(self.alignRequested.emit, 'bottom')
self.align_bottom = QtWidgets.QAction(icon(file_), '', self)
self.align_bottom.triggered.connect(method)
self.align_bottom.setToolTip('Align to bottom')
file_ = 'arrange_h.png'
method = partial(self.arrangeRequested.emit, 'horizontal')
self.arrange_horizontal = QtWidgets.QAction(icon(file_), '', self)
self.arrange_horizontal.triggered.connect(method)
self.arrange_horizontal.setToolTip('Distribute horizontally')
file_ = 'arrange_v.png'
method = partial(self.arrangeRequested.emit, 'vertical')
self.arrange_vertical = QtWidgets.QAction(icon(file_), '', self)
self.arrange_vertical.triggered.connect(method)
self.arrange_vertical.setToolTip('Distribute vertically')
self.toolbar = QtWidgets.QToolBar()
self.toolbar.setIconSize(QtCore.QSize(24, 24))
self.toolbar.addAction(self.delete)
self.toolbar.addAction(self.copy)
self.toolbar.addAction(self.paste)
self.toolbar.addAction(self.copy_settings)
self.toolbar.addAction(self.paste_settings)
self.toolbar.addSeparator()
self.toolbar.addAction(self.undo)
self.toolbar.addAction(self.redo)
self.toolbar.addSeparator()
self.toolbar.addAction(self.search)
self.toolbar.addSeparator()
self.toolbar.addAction(self.lock_bg)
self.toolbar.addAction(self.isolate)
self.toolbar.addAction(self.hierarchy)
self.toolbar.addSeparator()
self.toolbar.addAction(self.snap)
self.toolbar.addWidget(self.snapx)
self.toolbar.addWidget(self.snapy)
self.toolbar.addSeparator()
self.toolbar.addAction(self.call_library)
self.toolbar.addAction(self.addbutton)
self.toolbar.addAction(self.addtext)
self.toolbar.addAction(self.addbg)
self.toolbar.addSeparator()
self.toolbar.addAction(self.hsymmetry)
self.toolbar.addAction(self.vsymmetry)
self.toolbar.addSeparator()
self.toolbar.addAction(self.onbottom)
self.toolbar.addAction(self.movedown)
self.toolbar.addAction(self.moveup)
self.toolbar.addAction(self.ontop)
self.toolbar.addSeparator()
self.toolbar.addAction(self.align_left)
self.toolbar.addAction(self.align_h_center)
self.toolbar.addAction(self.align_right)
self.toolbar.addAction(self.align_top)
self.toolbar.addAction(self.align_v_center)
self.toolbar.addAction(self.align_bottom)
self.toolbar.addAction(self.arrange_horizontal)
self.toolbar.addAction(self.arrange_vertical)
self.layout = QtWidgets.QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 10, 0)
self.layout.addWidget(self.toolbar)
self.load_ui_states()
def toggle_hierarchy_display(self, state):
save_optionvar(DISPLAY_HIERARCHY_IN_CANVAS, int(state))
self.display_options.display_hierarchy = state
self.display_options.options_changed.emit()
def isolate_panel(self, state):
self.display_options.isolate = state
self.display_options.options_changed.emit()
def _call_library(self):
rect = self.toolbar.actionGeometry(self.call_library)
point = self.toolbar.mapToGlobal(rect.bottomLeft())
self.buttonLibraryRequested.emit(point)
def load_ui_states(self):
self.snap.setChecked(cmds.optionVar(query=SNAP_ITEMS))
value = str(cmds.optionVar(query=SNAP_GRID_X))
self.snapx.setText(value)
value = str(cmds.optionVar(query=SNAP_GRID_Y))
self.snapy.setText(value)
self.lock_bg.setChecked(bool(cmds.optionVar(query=BG_LOCKED)))
value = bool(cmds.optionVar(query=ISOLATE_CURRENT_PANEL_SHAPES))
self.isolate.setChecked(value)
def save_ui_states(self):
save_optionvar(BG_LOCKED, int(self.lock_bg.isChecked()))
save_optionvar(SNAP_ITEMS, int(self.snap.isChecked()))
save_optionvar(SNAP_GRID_X, int(self.snapx.text()))
save_optionvar(SNAP_GRID_Y, int(self.snapy.text()))
value = int(self.isolate.isChecked())
save_optionvar(ISOLATE_CURRENT_PANEL_SHAPES, value)
def size_changed(self, *_):
self.sizeChanged.emit()
def edit_center_toggled(self):
self.editCenterToggled.emit(self.editcenter.isChecked())
def snap_toggled(self):
self.useSnapToggled.emit(self.snap.isChecked())
self.save_ui_states()
def snap_values(self):
x = int(self.snapx.text()) if self.snapx.text() else 1
y = int(self.snapy.text()) if self.snapy.text() else 1
x = x if x > 0 else 1
y = y if y > 0 else 1
return x, y
def snap_value_changed(self, _):
self.snapValuesChanged.emit()
self.save_ui_states()

View File

@@ -0,0 +1,703 @@
import os
import json
from copy import deepcopy
from functools import partial
from ..pyside import QtWidgets, QtCore, QtGui
from maya import cmds
from ..geometry import (
distance, get_global_rect, grow_rect, path_symmetry)
from ..qtutils import icon
from ..interactionmanager import InteractionManager
from ..interactive import SelectionSquare, Manipulator
from ..optionvar import (
LAST_OPEN_DIRECTORY, SHAPE_PATH_ROTATION_STEP_ANGLE, save_optionvar)
from ..painting import (
draw_selection_square, draw_manipulator, draw_tangents,
draw_world_coordinates)
from ..path import get_open_directory
from ..qtutils import get_cursor
from ..selection import get_selection_mode
from ..shapepath import (
offset_tangent, offset_path, auto_tangent, create_polygon_path,
rotate_path)
from ..transform import (
Transform, resize_path_with_reference, resize_rect_with_direction)
from ..viewport import ViewportMapper
class PathEditor(QtWidgets.QWidget):
pathEdited = QtCore.Signal()
def __init__(self, parent=None):
super(PathEditor, self).__init__(parent)
self.setWindowTitle('Shape path editor')
self.buffer_path = None
self.rotate_center = None
self.canvas = PathEditorCanvas()
self.canvas.pathEdited.connect(self.pathEdited.emit)
export_path = QtWidgets.QAction(icon('save.png'), 'Export path', self)
export_path.triggered.connect(self.export_path)
import_path = QtWidgets.QAction(icon('open.png'), 'Import path', self)
import_path.triggered.connect(self.import_path)
delete = QtWidgets.QAction(icon('delete.png'), 'Delete vertex', self)
delete.triggered.connect(self.canvas.delete)
smooth_tangent = QtWidgets.QAction(
icon('tangent.png'), 'Smooth tangents', self)
smooth_tangent.triggered.connect(self.canvas.smooth_tangents)
break_tangent = QtWidgets.QAction(
icon('tangentbreak.png'), 'Break tangents', self)
break_tangent.triggered.connect(self.canvas.break_tangents)
hsymmetry = QtWidgets.QAction(
icon('h_symmetry.png'), 'Mirror horizontally', self)
hsymmetry.triggered.connect(partial(self.canvas.symmetry, True))
vsymmetry = QtWidgets.QAction(
icon('v_symmetry.png'), 'Mirror vertically', self)
vsymmetry.triggered.connect(partial(self.canvas.symmetry, False))
center_path = QtWidgets.QAction(
icon('frame.png'), 'Center path', self)
center_path.triggered.connect(partial(self.canvas.center_path))
center_path_button = QtWidgets.QToolButton()
center_path_button.setDefaultAction(center_path)
self.angle = QtWidgets.QSlider(QtCore.Qt.Horizontal)
self.angle.setMinimum(0)
self.angle.setMaximum(360)
self.angle.sliderPressed.connect(self.start_rotate)
self.angle.sliderReleased.connect(self.end_rotate)
self.angle.valueChanged.connect(self.rotate)
self.angle_step = QtWidgets.QSpinBox()
self.angle_step.setToolTip('Step')
self.angle_step.setMinimum(0)
self.angle_step.setMaximum(90)
value = cmds.optionVar(query=SHAPE_PATH_ROTATION_STEP_ANGLE)
self.angle_step.setValue(value)
function = partial(save_optionvar, SHAPE_PATH_ROTATION_STEP_ANGLE)
self.angle_step.valueChanged.connect(function)
polygon = QtWidgets.QAction(
icon('polygon.png'), 'Create Polygon', self)
polygon.triggered.connect(self.create_polygon)
toggle = QtWidgets.QAction(icon('dock.png'), 'Dock/Undock', self)
toggle.triggered.connect(self.toggle_flag)
self.toolbar = QtWidgets.QToolBar()
self.toolbar.setIconSize(QtCore.QSize(18, 18))
self.toolbar.addAction(import_path)
self.toolbar.addAction(export_path)
self.toolbar.addSeparator()
self.toolbar.addAction(polygon)
self.toolbar.addAction(delete)
self.toolbar.addSeparator()
self.toolbar.addAction(smooth_tangent)
self.toolbar.addAction(break_tangent)
self.toolbar.addSeparator()
self.toolbar.addAction(hsymmetry)
self.toolbar.addAction(vsymmetry)
toolbar3 = QtWidgets.QHBoxLayout()
toolbar3.setContentsMargins(0, 0, 0, 0)
toolbar3.addWidget(center_path_button)
toolbar3.addStretch()
toolbar3.addWidget(QtWidgets.QLabel('Rotate: '))
toolbar3.addWidget(self.angle)
toolbar3.addWidget(self.angle_step)
self.toolbar2 = QtWidgets.QToolBar()
self.toolbar2.setIconSize(QtCore.QSize(18, 18))
self.toolbar2.addAction(toggle)
toolbars = QtWidgets.QHBoxLayout()
toolbars.setContentsMargins(0, 0, 0, 0)
toolbars.addWidget(self.toolbar)
toolbars.addStretch()
toolbars.addWidget(self.toolbar2)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addLayout(toolbars)
layout.addWidget(self.canvas)
layout.addLayout(toolbar3)
def export_path(self):
directory = get_open_directory()
filename = QtWidgets.QFileDialog.getSaveFileName(
self, 'Export shape', directory, '*.dws')
if not filename[0]:
return
save_optionvar(LAST_OPEN_DIRECTORY, os.path.dirname(filename[0]))
with open(filename[0], 'w') as f:
json.dump(self.canvas.path, f, indent=2)
def import_path(self):
directory = get_open_directory()
filename = QtWidgets.QFileDialog.getOpenFileName(
self, 'Import shape', directory, '*.dws')
if not filename[0]:
return
save_optionvar(LAST_OPEN_DIRECTORY, os.path.dirname(filename[0]))
with open(filename[0], 'r') as f:
path = json.load(f)
self.canvas.set_path(path)
self.pathEdited.emit()
def start_rotate(self):
self.buffer_path = deepcopy(self.canvas.path)
if not self.canvas.selection:
self.rotate_center = (0, 0)
elif len(self.canvas.selection) == 1:
index = self.canvas.selection[0]
self.rotate_center = self.buffer_path[index]['point']
else:
point = self.canvas.manipulator.rect.center()
self.rotate_center = point.toTuple()
def end_rotate(self):
self.buffer_path = None
self.rotate_center = None
self.pathEdited.emit()
self.angle.blockSignals(True)
self.angle.setValue(0)
self.angle.blockSignals(False)
def rotate(self, value):
if self.buffer_path is None:
self.start_rotate()
step_size = self.angle_step.value()
value = round(value / step_size) * step_size
if 1 < len(self.canvas.selection):
path = deepcopy(self.buffer_path)
points = [self.buffer_path[i] for i in self.canvas.selection]
rotated_path = rotate_path(points, value, self.rotate_center)
for i, point in zip(self.canvas.selection, rotated_path):
path[i] = point
else:
path = rotate_path(self.buffer_path, value, self.rotate_center)
if path is None:
return
self.canvas.path = path
if self.canvas.selection:
points = [
QtCore.QPointF(*path[i]['point'])
for i in self.canvas.selection]
self.canvas.update_manipulator_rect(points)
self.canvas.update()
def create_polygon(self):
edges, result = QtWidgets.QInputDialog.getInt(
self, 'Polygon', 'Number of edges', value=3, minValue=3,
maxValue=25)
if not result:
return
path = create_polygon_path(radius=45, n=edges)
self.canvas.set_path(path)
self.pathEdited.emit()
def toggle_flag(self):
point = self.mapToGlobal(self.rect().topLeft())
state = not self.windowFlags() & QtCore.Qt.Tool
self.setWindowFlag(QtCore.Qt.Tool, state)
self.show()
if state:
self.resize(400, 400)
self.move(point)
self.canvas.focus()
def set_options(self, options):
if options is None:
self.canvas.set_path(None)
return
self.canvas.set_path(options['shape.path'] or [])
def path(self):
return self.canvas.path
def path_rect(self):
return get_global_rect(
[QtCore.QPointF(*p['point']) for p in self.canvas.path])
class PathEditorCanvas(QtWidgets.QWidget):
pathEdited = QtCore.Signal()
def __init__(self, parent=None):
super(PathEditorCanvas, self).__init__(parent)
self.viewportmapper = ViewportMapper()
self.viewportmapper.viewsize = self.size()
self.selection_square = SelectionSquare()
self.manipulator = Manipulator(self.viewportmapper)
self.transform = Transform()
self.selection = PointSelection()
self.interaction_manager = InteractionManager()
self.setMouseTracking(True)
self.path = []
def sizeHint(self):
return QtCore.QSize(300, 200)
def mousePressEvent(self, event):
if not self.path:
return
cursor = self.viewportmapper.to_units_coords(get_cursor(self))
self.transform.direction = self.manipulator.get_direction(event.pos())
self.current_action = self.get_action()
if self.current_action and self.current_action[0] == 'move point':
self.selection.set([self.current_action[1]])
self.update_manipulator_rect()
if self.manipulator.rect is not None:
self.transform.set_rect(self.manipulator.rect)
self.transform.reference_rect = QtCore.QRectF(self.manipulator.rect)
self.transform.set_reference_point(cursor)
has_shape_hovered = bool(self.current_action)
self.interaction_manager.update(
event, pressed=True,
has_shape_hovered=has_shape_hovered,
dragging=has_shape_hovered)
self.selection_square.clicked(cursor)
self.update()
def get_action(self):
if not self.path:
return
cursor = self.viewportmapper.to_units_coords(get_cursor(self))
if self.manipulator.rect and self.manipulator.rect.contains(cursor):
return 'move points', None
direction = self.manipulator.get_direction(get_cursor(self))
if direction:
return 'resize points', direction
tolerance = self.viewportmapper.to_units(5)
for i, data in enumerate(self.path):
point = QtCore.QPointF(*data['point'])
if distance(point, cursor) < tolerance:
return 'move point', i
if data['tangent_in']:
point = QtCore.QPointF(*data['tangent_in'])
if distance(point, cursor) < tolerance:
return 'move in', i
if data['tangent_out']:
point = QtCore.QPointF(*data['tangent_out'])
if distance(point, cursor) < tolerance:
return 'move out', i
index = is_point_on_path_edge(self.path, cursor, tolerance)
if index is not None:
return 'create point', index
def mouseMoveEvent(self, event):
if not self.path:
return
cursor = self.viewportmapper.to_units_coords(get_cursor(self)).toPoint()
if self.interaction_manager.mode == InteractionManager.NAVIGATION:
offset = self.interaction_manager.mouse_offset(event.pos())
if offset is not None:
origin = self.viewportmapper.origin - offset
self.viewportmapper.origin = origin
elif self.interaction_manager.mode == InteractionManager.SELECTION:
self.selection_square.handle(cursor)
elif self.interaction_manager.mode == InteractionManager.DRAGGING:
if not self.current_action:
return self.update()
offset = self.interaction_manager.mouse_offset(event.pos())
if not offset:
return self.update()
offset = QtCore.QPointF(
self.viewportmapper.to_units(offset.x()),
self.viewportmapper.to_units(offset.y()))
if self.current_action[0] == 'move points':
offset_path(self.path, offset, self.selection)
self.update_manipulator_rect()
elif self.current_action[0] == 'resize points':
resize_rect_with_direction(
self.transform.rect, cursor,
self.transform.direction)
path = (
[self.path[i] for i in self.selection] if
self.selection else self.path)
resize_path_with_reference(
path,
self.transform.reference_rect,
self.transform.rect)
rect = self.transform.rect
self.transform.reference_rect.setTopLeft(rect.topLeft())
self.transform.reference_rect.setSize(rect.size())
self.manipulator.set_rect(self.transform.rect)
elif self.current_action[0] == 'move point':
offset_path(self.path, offset, [self.current_action[1]])
elif self.current_action and self.current_action[0] == 'move in':
move_tangent(
point=self.path[self.current_action[1]],
tangent_in_moved=True,
offset=offset,
lock=not self.interaction_manager.ctrl_pressed)
elif self.current_action[0] == 'move out':
move_tangent(
point=self.path[self.current_action[1]],
tangent_in_moved=False,
offset=offset,
lock=not self.interaction_manager.ctrl_pressed)
elif self.current_action[0] == 'create point':
self.interaction_manager.mouse_offset(event.pos())
point = {
'point': [cursor.x(), cursor.y()],
'tangent_in': None,
'tangent_out': None}
index = self.current_action[1] + 1
self.path.insert(index, point)
self.autotangent(index)
self.current_action = 'move point', index
self.selection.set([index])
self.update_manipulator_rect()
self.update()
def move_point(self, i, offset):
self.path[i]['point'][0] += offset.x()
self.path[i]['point'][1] += offset.y()
point = self.path[i]['tangent_in']
if point:
point[0] += offset.x()
point[1] += offset.y()
point = self.path[i]['tangent_out']
if point:
point[0] += offset.x()
point[1] += offset.y()
def mouseReleaseEvent(self, event):
if not self.path:
return
if self.current_action:
self.pathEdited.emit()
if self.interaction_manager.mode == InteractionManager.SELECTION:
self.select()
self.selection_square.release()
self.interaction_manager.update(
event, pressed=False, has_shape_hovered=False, dragging=False)
self.update()
def select(self):
shift = self.interaction_manager.shift_pressed
ctrl = self.interaction_manager.ctrl_pressed
self.selection.mode = get_selection_mode(shift=shift, ctrl=ctrl)
rect = self.selection_square.rect
points = []
indexes = []
for i, p in enumerate(self.path):
point = QtCore.QPointF(*p['point'])
if rect.contains(point):
indexes.append(i)
points.append(point)
self.selection.set(indexes)
self.update_manipulator_rect()
def update_manipulator_rect(self, points=None):
if points is None:
points = [
QtCore.QPointF(*self.path[i]['point'])
for i in self.selection]
if len(points) < 2:
self.manipulator.set_rect(None)
return
rect = get_global_rect(points)
rect.setHeight(max(rect.height(), .5))
rect.setWidth(max(rect.width(), .5))
self.manipulator.set_rect(rect)
def wheelEvent(self, event):
# To center the zoom on the mouse, we save a reference mouse position
# and compare the offset after zoom computation.
factor = .25 if event.angleDelta().y() > 0 else -.25
self.zoom(factor, event.pos())
self.update()
def resizeEvent(self, event):
self.viewportmapper.viewsize = self.size()
size = (event.size() - event.oldSize()) / 2
offset = QtCore.QPointF(size.width(), size.height())
self.viewportmapper.origin -= offset
self.update()
def zoom(self, factor, reference):
abspoint = self.viewportmapper.to_units_coords(reference)
if factor > 0:
self.viewportmapper.zoomin(abs(factor))
else:
self.viewportmapper.zoomout(abs(factor))
relcursor = self.viewportmapper.to_viewport_coords(abspoint)
vector = relcursor - reference
self.viewportmapper.origin = self.viewportmapper.origin + vector
def paintEvent(self, event):
if not self.path:
return
try:
painter = QtGui.QPainter(self)
painter.setPen(QtGui.QPen())
color = QtGui.QColor('black')
color.setAlpha(50)
painter.setBrush(color)
rect = QtCore.QRect(
0, 0, self.rect().width() - 1, self.rect().height() - 1)
painter.drawRect(rect)
draw_world_coordinates(
painter, self.rect(), QtGui.QColor('#282828'),
self.viewportmapper)
painter.setBrush(QtGui.QBrush())
draw_shape_path(
painter, self.path, self.selection, self.viewportmapper)
draw_tangents(painter, self.path, self.viewportmapper)
if self.selection_square.rect:
draw_selection_square(
painter, self.selection_square.rect, self.viewportmapper)
conditions = (
self.manipulator.rect is not None and
all(self.manipulator.viewport_handlers()))
if conditions:
draw_manipulator(
painter, self.manipulator,
get_cursor(self), self.viewportmapper)
finally:
painter.end()
def center_path(self):
qpath = path_to_qpath(self.path, ViewportMapper())
center = qpath.boundingRect().center()
offset_path(self.path, -center)
self.pathEdited.emit()
self.update_manipulator_rect()
self.update()
def delete(self):
if len(self.path) - len(self.selection) < 3:
return QtWidgets.QMessageBox.critical(
self, 'Error', 'Shape must at least contains 3 control points')
for i in sorted(self.selection, reverse=True):
del self.path[i]
self.selection.clear()
self.update_manipulator_rect()
self.pathEdited.emit()
self.update()
def break_tangents(self):
for i in self.selection:
self.path[i]['tangent_in'] = None
self.path[i]['tangent_out'] = None
self.pathEdited.emit()
self.update()
def smooth_tangents(self):
if not self.selection:
return
for i in self.selection:
self.autotangent(i)
self.update()
self.pathEdited.emit()
def autotangent(self, i):
point = self.path[i]['point']
next_index = i + 1 if i < (len(self.path) - 1) else 0
next_point = self.path[next_index]['point']
previous_point = self.path[i - 1]['point']
tan_in, tan_out = auto_tangent(point, previous_point, next_point)
self.path[i]['tangent_in'] = tan_in
self.path[i]['tangent_out'] = tan_out
def set_path(self, path):
self.path = path
self.selection.clear()
self.manipulator.set_rect(None)
self.focus()
def focus(self):
if not self.path:
self.update()
return
points = [QtCore.QPointF(*p['point']) for p in self.path]
rect = get_global_rect(points)
self.viewportmapper.focus(grow_rect(rect, 15))
self.update()
def symmetry(self, horizontal=True):
path = (
[self.path[i] for i in self.selection] if
self.selection else self.path)
if self.manipulator.rect:
center = self.manipulator.rect.center()
else:
points = [QtCore.QPointF(*p['point']) for p in self.path]
rect = get_global_rect(points)
center = rect.center()
path_symmetry(path, center, horizontal=horizontal)
self.pathEdited.emit()
self.update()
class PointSelection():
def __init__(self):
self.elements = []
self.mode = 'replace'
def set(self, elements):
if self.mode == 'add':
if elements is None:
return
return self.add(elements)
elif self.mode == 'replace':
if elements is None:
return self.clear()
return self.replace(elements)
elif self.mode == 'invert':
if elements is None:
return
return self.invert(elements)
elif self.mode == 'remove':
if elements is None:
return
for element in elements:
if element in self.elements:
self.remove(element)
def replace(self, elements):
self.elements = elements
def add(self, elements):
self.elements.extend([e for e in elements if e not in self])
def remove(self, element):
self.elements.remove(element)
def invert(self, elements):
for element in elements:
if element not in self.elements:
self.add([element])
else:
self.remove(element)
def clear(self):
self.elements = []
def __len__(self):
return len(self.elements)
def __bool__(self):
return bool(self.elements)
__nonzero__ = __bool__
def __getitem__(self, i):
return self.elements[i]
def __iter__(self):
return self.elements.__iter__()
def path_to_qpath(path, viewportmapper):
painter_path = QtGui.QPainterPath()
start = QtCore.QPointF(*path[0]['point'])
painter_path.moveTo(viewportmapper.to_viewport_coords(start))
for i in range(len(path)):
point = path[i]
point2 = path[i + 1 if i + 1 < len(path) else 0]
c1 = QtCore.QPointF(*(point['tangent_out'] or point['point']))
c2 = QtCore.QPointF(*(point2['tangent_in'] or point2['point']))
end = QtCore.QPointF(*point2['point'])
painter_path.cubicTo(
viewportmapper.to_viewport_coords(c1),
viewportmapper.to_viewport_coords(c2),
viewportmapper.to_viewport_coords(end))
return painter_path
def draw_shape_path(painter, path, selection, viewportmapper):
painter.setPen(QtCore.Qt.gray)
painter.drawPath(path_to_qpath(path, viewportmapper))
rect = QtCore.QRectF(0, 0, 5, 5)
for i, point in enumerate(path):
center = QtCore.QPointF(*point['point'])
rect.moveCenter(viewportmapper.to_viewport_coords(center))
painter.setBrush(QtCore.Qt.white if i in selection else QtCore.Qt.NoBrush)
painter.drawRect(rect)
def is_point_on_path_edge(path, cursor, tolerance=3):
stroker = QtGui.QPainterPathStroker()
stroker.setWidth(tolerance * 2)
for i in range(len(path)):
point = path[i]
painter_path = QtGui.QPainterPath()
painter_path.moveTo(QtCore.QPointF(*point['point']))
point2 = path[i + 1 if i + 1 < len(path) else 0]
c1 = QtCore.QPointF(*(point['tangent_out'] or point['point']))
c2 = QtCore.QPointF(*(point2['tangent_in'] or point2['point']))
end = QtCore.QPointF(*point2['point'])
painter_path.cubicTo(c1, c2, end)
stroke = stroker.createStroke(painter_path)
if stroke.contains(cursor):
return i
return None
def move_tangent(point, tangent_in_moved, offset, lock):
center_point = point['point']
tangent_in = point['tangent_in' if tangent_in_moved else 'tangent_out']
tangent_out = point['tangent_out' if tangent_in_moved else 'tangent_in']
offset = offset.x(), offset.y()
tangent_in, tangent_out = offset_tangent(
tangent_in, tangent_out, center_point, offset, lock)
point['tangent_in'if tangent_in_moved else 'tangent_out'] = tangent_in
point['tangent_out'if tangent_in_moved else 'tangent_in'] = tangent_out
if __name__ == '__main__':
try:
se.close()
except:
pass
from ..qtutils import maya_main_window
se = PathEditor(maya_main_window())
se.setWindowFlags(QtCore.Qt.Window)
se.show()

View File

@@ -0,0 +1,374 @@
from decimal import Decimal, getcontext
from ..pyside import QtWidgets, QtGui, QtCore
HANDLER_WIDTH = 16
HANDLER_HEIGHT = 16
class StackEditor(QtWidgets.QWidget):
panelsChanged = QtCore.Signal(object)
panelSelected = QtCore.Signal(int)
panelDoubleClicked = QtCore.Signal(int)
def __init__(self, parent=None):
super(StackEditor, self).__init__(parent)
self.data = [[1., [1.]]]
self.orientation = 'vertical'
self.stack_rects = get_stack_rects(
self.data, self.rect(), self.orientation)
self.setMouseTracking(True)
self.clicked_action = None
self.selected_index = None
self.panels_are_changed = False
self.panel_is_selected = None
def set_orientation(self, orientation):
self.orientation = orientation
self.stack_rects = get_stack_rects(
self.data, self.rect(), self.orientation)
self.update()
def set_data(self, data):
self.data = data
self.update()
def sizeHint(self):
return QtCore.QSize(300, 210)
def resizeEvent(self, event):
self.stack_rects = get_stack_rects(
self.data, self.rect(), self.orientation)
def mousePressEvent(self, event):
if event.button() != QtCore.Qt.LeftButton:
return
self.clicked_action = self.get_action(event.pos())
def mouseDoubleClickEvent(self, event):
if event.button() != QtCore.Qt.LeftButton:
return
clicked_action = self.get_action(event.pos())
if clicked_action[0] == 'select':
panel = self.panel_number(clicked_action[1])
self.panelSelected.emit(panel)
self.panelDoubleClicked.emit(panel)
self.selected_index = clicked_action[1]
self.update()
def panel_number(self, index):
k = 1
for i, (_, rows) in enumerate(self.data):
for j in range(len(rows)):
if [i, j] == index:
return k
k += 1
def mouseReleaseEvent(self, event):
if event.button() != QtCore.Qt.LeftButton or not self.clicked_action:
return
index = self.clicked_action[1]
if self.clicked_action[0] == 'delete':
delete_panel(self.data, index)
self.stack_rects = get_stack_rects(
self.data, self.rect(), self.orientation)
self.panelsChanged.emit(self.data)
elif self.clicked_action[0] == 'select':
index = self.clicked_action[1]
if index == self.selected_index:
self.selected_index = None
self.panelSelected.emit(-1)
else:
self.selected_index = index
self.panelSelected.emit(self.panel_number(self.selected_index))
else:
self.check_buffer_states()
self.update()
self.clicked_action = None
def check_buffer_states(self):
if self.panel_is_selected is not None:
self.panelSelected.emit(self.panel_is_selected)
if self.panels_are_changed:
self.panelsChanged.emit(self.data)
self.panel_is_selected = None
self.panels_are_changed = False
def get_action(self, cursor):
for i, column in enumerate(self.stack_rects):
for j, rect in enumerate(column):
if not rect.contains(cursor):
continue
if get_close_handler_rect(rect).contains(cursor):
if i + j:
return 'delete', [i, j]
hrect = get_horizontal_handler_rect(rect)
vrect = get_vertical_handler_rect(rect)
if self.orientation == 'horizontal':
hrect, vrect = vrect, hrect
if hrect.contains(cursor):
if j == len(column) - 1:
return 'create vertical', [i, j]
return 'move vertical', [i, j]
if vrect.contains(cursor):
if i == len(self.data) - 1:
return 'create horizontal', [i, j]
return 'move horizontal', [i, j]
return 'select', [i, j]
def mouseMoveEvent(self, event):
if not self.clicked_action:
return
vertical = self.orientation == 'vertical'
if self.clicked_action[0] == 'create vertical':
index = self.clicked_action[1]
col = self.data[index[0]][1]
col[-1] -= .1
col.append(.1)
self.clicked_action = 'move vertical', index
self.selected_index = [index[0], index[1] + 1]
self.panel_is_selected = self.panel_number(self.selected_index)
self.panels_are_changed = True
elif self.clicked_action[0] == 'create horizontal':
index = self.clicked_action[1]
self.data[-1][0] -= .1
self.data.append([.1, [1.]])
self.clicked_action = 'move horizontal', index
self.selected_index = [index[0] + 1, 0]
self.panel_is_selected = self.panel_number(self.selected_index)
self.panels_are_changed = True
elif self.clicked_action[0] == 'move vertical' and vertical:
index = self.clicked_action[1]
y = event.pos().y() / self.height()
move_vertical(self.data, index, y)
self.panels_are_changed = True
elif self.clicked_action[0] == 'move vertical':
index = self.clicked_action[1]
x = event.pos().x() / self.width()
move_vertical(self.data, index, x)
self.panels_are_changed = True
elif self.clicked_action[0] == 'move horizontal' and vertical:
index = self.clicked_action[1]
x = event.pos().x() / self.width()
move_horizontal(self.data, index, x)
self.panels_are_changed = True
elif self.clicked_action[0] == 'move horizontal':
index = self.clicked_action[1]
y = event.pos().y() / self.height()
move_horizontal(self.data, index, y)
self.panels_are_changed = True
self.stack_rects = get_stack_rects(
self.data, self.rect(), self.orientation)
self.update()
def paintEvent(self, _):
painter = QtGui.QPainter(self)
k = 1
original_pen = painter.pen()
original_brush = painter.brush()
for i, column in enumerate(self.stack_rects):
for j, rect in enumerate(column):
if [i, j] == self.selected_index:
pen = QtGui.QPen(QtGui.QColor('yellow'))
pen.setWidth(5)
painter.setPen(pen)
brush = QtGui.QBrush(original_brush)
color = brush.color()
color.setAlpha(50)
brush.setColor(color)
brush.setStyle(QtCore.Qt.FDiagPattern)
painter.setBrush(brush)
else:
pen = original_pen
painter.setPen(pen)
painter.setBrush(original_brush)
painter.drawRect(rect)
painter.setPen(QtCore.Qt.NoPen)
painter.setBrush(pen.color())
handler_rect = get_horizontal_handler_rect(rect)
painter.drawPath(up_arrow(handler_rect))
handler_rect = get_vertical_handler_rect(rect)
painter.drawPath(left_arrow(handler_rect))
painter.setPen(original_pen)
painter.setBrush(original_brush)
font = QtGui.QFont()
font.setPointSize(15)
font.setBold(True)
painter.setFont(font)
painter.drawText(rect, QtCore.Qt.AlignCenter, str(k))
k += 1
if i + j == 0:
continue
font = QtGui.QFont()
font.setBold(True)
painter.setFont(font)
painter.drawText(
get_close_handler_rect(rect), QtCore.Qt.AlignCenter, 'X')
painter.end()
def get_stack_rects(data, rect, orientation):
if orientation == 'vertical':
return get_vertical_stack_rects(data, rect)
return get_horizontal_stack_rects(data, rect)
def get_horizontal_stack_rects(data, rect):
result = []
y = 0
for height, rows in data:
column = []
x = 0
for width in rows:
panel_rect = QtCore.QRectF(
x * rect.width(),
y * rect.height(),
(width * rect.width()) - 1,
(height * rect.height()) - 1)
column.append(panel_rect)
x += width
y += height
result.append(column)
return result
def get_vertical_stack_rects(data, rect):
result = []
x = 0
for width, rows in data:
column = []
y = 0
for height in rows:
panel_rect = QtCore.QRectF(
x * rect.width(),
y * rect.height(),
(width * rect.width()) - 1,
(height * rect.height()) - 1)
column.append(panel_rect)
y += height
x += width
result.append(column)
return result
def get_vertical_handler_rect(rect):
return QtCore.QRectF(
rect.right() - HANDLER_WIDTH,
rect.center().y() - (HANDLER_HEIGHT / 2),
HANDLER_WIDTH, HANDLER_HEIGHT)
def get_horizontal_handler_rect(rect):
return QtCore.QRectF(
rect.center().x() - (HANDLER_WIDTH / 2),
rect.bottom() - HANDLER_HEIGHT,
HANDLER_WIDTH, HANDLER_HEIGHT)
def get_close_handler_rect(rect):
return QtCore.QRectF(
rect.right() - HANDLER_WIDTH,
rect.top(), HANDLER_HEIGHT, HANDLER_WIDTH)
def delete_panel(data, index):
column = data[index[0]][1]
if len(column) > 1:
data[index[0]][1] = delete_value(column, index[1])
return
values = delete_value([c[0] for c in data], index[0])
del data[index[0]]
for i, value in enumerate(values):
data[i][0] = value
def delete_value(values, index):
getcontext().prec = 50
decimal_values = [Decimal(v) for v in values]
values = [
Decimal(v) for i, v in enumerate(decimal_values) if i != index]
return [
float((v / sum(values)).quantize(Decimal('1.00000')))
for v in values]
def move_vertical(data, index, y):
column = data[index[0]][1]
ratios = to_ratios(column)
if index[1] == 0:
y = max((.1, y))
else:
y = max((ratios[index[1] - 1] + .1, y))
y = min((y, ratios[index[1] + 1] - .1))
ratios[index[1]] = y
data[index[0]][1] = to_weights(ratios)
def move_horizontal(data, index, x):
ratios = to_ratios(c[0] for c in data)
if index[0] == 0:
x = max((.1, x))
else:
x = max((ratios[index[0] - 1] + .1, x))
x = min((x, ratios[index[0] + 1] - .1))
ratios[index[0]] = x
for i, col in enumerate(to_weights(ratios)):
data[i][0] = col
def up_arrow(rect):
path = QtGui.QPainterPath(rect.bottomLeft())
path.lineTo(rect.bottomRight())
point = QtCore.QPointF(rect.center().x(), rect.top())
path.lineTo(point)
path.lineTo(rect.bottomLeft())
return path
def left_arrow(rect):
path = QtGui.QPainterPath(rect.topRight())
path.lineTo(rect.bottomRight())
point = QtCore.QPointF(rect.left(), rect.center().y())
path.lineTo(point)
path.lineTo(rect.topRight())
return path
def to_ratios(weights):
"""
Convert weight list to ratios.
input: [0.2, 0.3, 0.4, 0.1]
output: [0.2, 0.5, 0.9, 1.0]
"""
total = 0.0
result = []
for weight in weights:
total += weight
result.append(total)
return result
def to_weights(ratios):
"""
Convert ratio list to weights.
input: [0.2, 0.5, 0.9, 1.0]
output: [0.2, 0.3, 0.4, 0.1]
"""
result = []
result.extend(ratio - sum(result) for ratio in ratios)
return result

View File

@@ -0,0 +1,531 @@
from functools import partial
import os
from .pyside import QtWidgets, QtCore, QtGui
from maya import cmds
from .designer.highlighter import get_highlighter
from .optionvar import (
save_optionvar, CHECK_FOR_UPDATE,
SEARCH_FIELD_INDEX, LAST_IMAGE_DIRECTORY_USED, SETTINGS_GROUP_TO_COPY,
SHAPES_FILTER_INDEX, SETTINGS_TO_COPY)
from .languages import MEL, PYTHON
from .path import get_image_directory
from .qtutils import icon
from .namespace import selected_namespace
from .templates import BUTTON
SEARCH_AND_REPLACE_FIELDS = 'Targets', 'Label', 'Image path', 'Command'
SHAPES_FILTERS = 'All shapes', 'Selected shapes'
COMMAND_PLACEHOLDER = """\
PYTHON:
__targets__: List[str] (variable available by default in the script)
__shape__: dict (clicked shape data as dict. Dict is editable).
example to toggle the background color:
current_color = __shape__['bgcolor.normal']
__shape__['bgcolor.normal'] = (
"black" if current_color == 'white' else "white")
MEL:
var $targets[] is availables by default.
"""
def warning(title, message, parent=None):
return QtWidgets.QMessageBox.warning(
parent,
title,
message,
QtWidgets.QMessageBox.Ok,
QtWidgets.QMessageBox.Ok)
def question(title, message, buttons=None, parent=None):
buttons = buttons or QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel
result = QtWidgets.QMessageBox.question(
parent, title, message, buttons, QtWidgets.QMessageBox.Ok)
return result == QtWidgets.QMessageBox.Ok
def get_image_path(parent=None, dialog_title="Repath image..."):
filename = QtWidgets.QFileDialog.getOpenFileName(
parent, dialog_title, get_image_directory(),
filter="Images (*.jpg *.gif *.png *.tga)")[0]
if not filename:
return None
directory = os.path.dirname(filename)
save_optionvar(LAST_IMAGE_DIRECTORY_USED, directory)
return filename
class NamespaceDialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super(NamespaceDialog, self).__init__(parent=parent)
self.setWindowTitle('Select namespace ...')
self.namespace_combo = QtWidgets.QComboBox()
self.namespace_combo.setEditable(True)
namespaces = [':'] + cmds.namespaceInfo(
listOnlyNamespaces=True, recurse=True)
self.namespace_combo.addItems(namespaces)
self.namespace_combo.setCurrentText(selected_namespace())
self.detect_selection = QtWidgets.QPushButton('Detect from selection')
self.detect_selection.released.connect(self.call_detect_selection)
self.ok = QtWidgets.QPushButton('Ok')
self.ok.released.connect(self.accept)
self.cancel = QtWidgets.QPushButton('Cancel')
self.cancel.released.connect(self.reject)
self.button_layout = QtWidgets.QHBoxLayout()
self.button_layout.setContentsMargins(0, 0, 0, 0)
self.button_layout.addStretch(1)
self.button_layout.addWidget(self.detect_selection)
self.button_layout.addSpacing(16)
self.button_layout.addWidget(self.ok)
self.button_layout.addWidget(self.cancel)
self.layout = QtWidgets.QVBoxLayout(self)
self.layout.addWidget(self.namespace_combo)
self.layout.addLayout(self.button_layout)
@property
def namespace(self):
return self.namespace_combo.currentText()
def call_detect_selection(self):
self.namespace_combo.setCurrentText(selected_namespace())
class SettingsPaster(QtWidgets.QDialog):
def __init__(self, parent=None):
super(SettingsPaster, self).__init__(parent)
self.setWindowTitle('Paste settings')
self.groups = {}
self.categories = {}
enable_settings = cmds.optionVar(query=SETTINGS_TO_COPY).split(';')
for setting in sorted(BUTTON.keys()):
text = ' '.join(setting.split('.')[1:]).capitalize()
checkbox = QtWidgets.QCheckBox(text or setting.capitalize())
checkbox.setting = setting
checkbox.setChecked(setting in enable_settings)
checkbox.stateChanged.connect(self.updated)
name = setting.split('.')[0]
self.categories.setdefault(name, []).append(checkbox)
enable_groups = cmds.optionVar(query=SETTINGS_GROUP_TO_COPY).split(';')
groups_layout = QtWidgets.QVBoxLayout()
self.group_layouts = QtWidgets.QHBoxLayout()
checkboxes_count = 0
for category, checkboxes in self.categories.items():
if checkboxes_count > 12:
checkboxes_count = 0
groups_layout.addStretch(1)
self.group_layouts.addLayout(groups_layout)
groups_layout = QtWidgets.QVBoxLayout()
group = QtWidgets.QGroupBox(category)
group.setCheckable(True)
group.setChecked(category in enable_groups)
group.toggled.connect(self.updated)
group_layout = QtWidgets.QVBoxLayout(group)
for checkbox in checkboxes:
group_layout.addWidget(checkbox)
self.groups[category] = group
groups_layout.addWidget(group)
checkboxes_count += len(checkboxes)
groups_layout.addStretch(1)
self.group_layouts.addLayout(groups_layout)
self.paste = QtWidgets.QPushButton('Paste')
self.paste.released.connect(self.accept)
self.cancel = QtWidgets.QPushButton('Cancel')
self.cancel.released.connect(self.reject)
self.buttons_layout = QtWidgets.QHBoxLayout()
self.buttons_layout.addStretch(1)
self.buttons_layout.addWidget(self.paste)
self.buttons_layout.addWidget(self.cancel)
self.layout = QtWidgets.QVBoxLayout(self)
self.layout.addLayout(self.group_layouts)
self.layout.addLayout(self.buttons_layout)
@property
def settings(self):
return [
cb.setting for category, checkboxes in self.categories.items()
for cb in checkboxes if cb.isChecked() and
self.groups[category].isChecked()]
def updated(self, *_):
cat = ';'.join([c for c, g in self.groups.items() if g.isChecked()])
save_optionvar(SETTINGS_GROUP_TO_COPY, cat)
save_optionvar(SETTINGS_TO_COPY, ';'.join(self.settings))
class SearchAndReplaceDialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super(SearchAndReplaceDialog, self).__init__(parent=parent)
self.setWindowTitle('Search and replace in shapes')
self.sizeHint = lambda: QtCore.QSize(320, 80)
self.filters = QtWidgets.QComboBox()
self.filters.addItems(SHAPES_FILTERS)
self.filters.setCurrentIndex(cmds.optionVar(query=SHAPES_FILTER_INDEX))
function = partial(save_optionvar, SHAPES_FILTER_INDEX)
self.filters.currentIndexChanged.connect(function)
self.fields = QtWidgets.QComboBox()
self.fields.addItems(SEARCH_AND_REPLACE_FIELDS)
self.fields.setCurrentIndex(cmds.optionVar(query=SEARCH_FIELD_INDEX))
function = partial(save_optionvar, SEARCH_FIELD_INDEX)
self.fields.currentIndexChanged.connect(function)
self.search = QtWidgets.QLineEdit()
self.replace = QtWidgets.QLineEdit()
self.ok = QtWidgets.QPushButton('Replace')
self.ok.released.connect(self.accept)
self.cancel = QtWidgets.QPushButton('Cancel')
self.cancel.released.connect(self.reject)
self.options = QtWidgets.QFormLayout()
self.options.setContentsMargins(0, 0, 0, 0)
self.options.addRow('Apply on: ', self.filters)
self.options.addRow('Field to search: ', self.fields)
self.options.addRow('Search: ', self.search)
self.options.addRow('Replace by: ', self.replace)
self.button_layout = QtWidgets.QHBoxLayout()
self.button_layout.setContentsMargins(0, 0, 0, 0)
self.button_layout.addStretch(1)
self.button_layout.addWidget(self.ok)
self.button_layout.addWidget(self.cancel)
self.layout = QtWidgets.QVBoxLayout(self)
self.layout.addLayout(self.options)
self.layout.addLayout(self.button_layout)
@property
def field(self):
'''
0 = Targets
1 = Label
2 = Command
3 = Image path
'''
return self.fields.currentIndex()
@property
def filter(self):
'''
0 = Apply on all shapes
1 = Apply on selected shapes
'''
return self.filters.currentIndex()
class MissingImages(QtWidgets.QDialog):
def __init__(self, paths, parent=None):
super(MissingImages, self).__init__(parent)
self.setWindowTitle('Missing images')
self.model = PathModel(paths)
self.paths = QtWidgets.QTableView()
self.paths.setAlternatingRowColors(True)
self.paths.setShowGrid(False)
self.paths.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
mode = QtWidgets.QHeaderView.ResizeToContents
self.paths.verticalHeader().resizeSections(mode)
self.paths.verticalHeader().hide()
self.paths.horizontalHeader().show()
self.paths.horizontalHeader().resizeSections(mode)
self.paths.horizontalHeader().setStretchLastSection(True)
mode = QtWidgets.QAbstractItemView.ScrollPerPixel
self.paths.setHorizontalScrollMode(mode)
self.paths.setVerticalScrollMode(mode)
self.paths.setModel(self.model)
self.browse = QtWidgets.QPushButton(icon('mini-open.png'), '')
self.browse.setFixedWidth(30)
self.browse.released.connect(self.call_browse)
self.update = QtWidgets.QPushButton('Update')
self.update.released.connect(self.accept)
self.skip = QtWidgets.QPushButton('Skip')
self.skip.released.connect(self.reject)
self.validators = QtWidgets.QHBoxLayout()
self.validators.addStretch(1)
self.validators.addWidget(self.browse)
self.validators.addWidget(self.update)
self.validators.addWidget(self.skip)
self.layout = QtWidgets.QVBoxLayout(self)
self.layout.addWidget(self.paths)
self.layout.addLayout(self.validators)
def output(self, path):
for p, output in zip(self.model.paths, self.model.outputs):
if p == path:
return output
@property
def outputs(self):
return self.model.outputs
def resizeEvent(self, _):
mode = QtWidgets.QHeaderView.ResizeToContents
self.paths.verticalHeader().resizeSections(mode)
self.paths.horizontalHeader().resizeSections(mode)
def call_browse(self):
directory = QtWidgets.QFileDialog.getExistingDirectory(
self, "Select image folder")
if not directory:
return
filenames = os.listdir(directory)
self.model.layoutAboutToBeChanged.emit()
for i, path in enumerate(self.model.paths):
filename = os.path.basename(path)
if filename in filenames:
filepath = os.path.join(directory, filename)
self.model.outputs[i] = filepath
self.model.layoutChanged.emit()
class PathModel(QtCore.QAbstractTableModel):
HEADERS = 'filename', 'directory'
def __init__(self, paths, parent=None):
super(PathModel, self).__init__(parent)
self.paths = paths
self.outputs = paths[:]
def rowCount(self, *_):
return len(self.paths)
def columnCount(self, *_):
return 2
def flags(self, index):
flags = super(PathModel, self).flags(index)
if index.column() == 1:
flags |= QtCore.Qt.ItemIsEditable
return flags
def headerData(self, position, orientation, role):
if orientation != QtCore.Qt.Horizontal:
return
if role != QtCore.Qt.DisplayRole:
return
return self.HEADERS[position]
def data(self, index, role):
if not index.isValid():
return
row, col = index.row(), index.column()
if role == QtCore.Qt.DisplayRole:
if col == 0:
return os.path.basename(self.outputs[row])
if col == 1:
return os.path.dirname(self.outputs[row])
elif role == QtCore.Qt.BackgroundColorRole:
if not os.path.exists(self.outputs[row]):
return QtGui.QColor(QtCore.Qt.darkRed)
class UpdateAvailableDialog(QtWidgets.QDialog):
def __init__(self, version, parent=None):
super(UpdateAvailableDialog, self).__init__(parent=parent)
self.setWindowTitle('Update available')
# Widgets
text = '\n New DreamWall Picker version "{0}" is available ! \n'
label = QtWidgets.QLabel(text.format(version))
ok_btn = QtWidgets.QPushButton('Open GitHub page')
ok_btn.released.connect(self.accept)
cancel_btn = QtWidgets.QPushButton('Close')
cancel_btn.released.connect(self.reject)
self.check_cb = QtWidgets.QCheckBox('Check for update at startup')
self.check_cb.stateChanged.connect(
self.change_check_for_update_preference)
self.check_cb.setChecked(cmds.optionVar(query=CHECK_FOR_UPDATE))
# Layouts
button_layout = QtWidgets.QHBoxLayout()
button_layout.addStretch(1)
button_layout.addWidget(ok_btn)
button_layout.addWidget(cancel_btn)
cb_layout = QtWidgets.QHBoxLayout()
cb_layout.addStretch(1)
cb_layout.addWidget(self.check_cb)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(label)
layout.addLayout(cb_layout)
layout.addLayout(button_layout)
def change_check_for_update_preference(self):
save_optionvar(CHECK_FOR_UPDATE, int(self.check_cb.isChecked()))
class CommandEditorDialog(QtWidgets.QDialog):
def __init__(self, command, parent=None):
super(CommandEditorDialog, self).__init__(parent)
self.setWindowTitle('Edit/Create command')
self.languages = QtWidgets.QComboBox()
self.languages.addItems([MEL, PYTHON])
self.languages.setCurrentText(command['language'])
self.languages.currentIndexChanged.connect(self.language_changed)
self.button = QtWidgets.QComboBox()
self.button.addItems(['left', 'right'])
self.button.setCurrentText(command['button'])
self.enabled = QtWidgets.QCheckBox('Enabled')
self.enabled.setChecked(command['enabled'])
self.ctrl = QtWidgets.QCheckBox('Ctrl')
self.ctrl.setChecked(command['ctrl'])
self.shift = QtWidgets.QCheckBox('Shift')
self.shift.setChecked(command['shift'])
self.eval_deferred = QtWidgets.QCheckBox('Eval deferred (python only)')
self.eval_deferred.setChecked(command['deferred'])
self.unique_undo = QtWidgets.QCheckBox('Unique undo')
self.unique_undo.setChecked(command['force_compact_undo'])
self.command = QtWidgets.QTextEdit()
self.command.setPlaceholderText(COMMAND_PLACEHOLDER)
self.command.setPlainText(command['command'])
self.ok = QtWidgets.QPushButton('Ok')
self.ok.released.connect(self.accept)
self.cancel = QtWidgets.QPushButton('Cancel')
self.cancel.released.connect(self.reject)
form = QtWidgets.QFormLayout()
form.setSpacing(0)
form.addRow('Language', self.languages)
form.addRow('Mouse button', self.button)
modifiers_group = QtWidgets.QGroupBox('Modifiers')
modifiers_layout = QtWidgets.QVBoxLayout(modifiers_group)
modifiers_layout.addWidget(self.ctrl)
modifiers_layout.addWidget(self.shift)
options_group = QtWidgets.QGroupBox('Options')
options_layout = QtWidgets.QVBoxLayout(options_group)
options_layout.addWidget(self.eval_deferred)
options_layout.addWidget(self.unique_undo)
options_layout.addLayout(form)
code = QtWidgets.QGroupBox('Code')
code_layout = QtWidgets.QVBoxLayout(code)
code_layout.setSpacing(0)
code_layout.addWidget(self.command)
buttons_layout = QtWidgets.QHBoxLayout()
buttons_layout.addStretch(1)
buttons_layout.addWidget(self.ok)
buttons_layout.addWidget(self.cancel)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(options_group)
layout.addWidget(modifiers_group)
layout.addWidget(code)
layout.addLayout(buttons_layout)
self.language_changed()
def sizeHint(self):
return QtCore.QSize(400, 550)
def language_changed(self, *_):
language = self.languages.currentText()
highlighter = get_highlighter(language)
highlighter(self.command.document())
def command_data(self):
return {
'enabled': self.enabled.isChecked(),
'button': self.button.currentText(),
'language': self.languages.currentText(),
'command': self.command.toPlainText(),
'ctrl': self.ctrl.isChecked(),
'shift': self.shift.isChecked(),
'deferred': self.eval_deferred.isChecked(),
'force_compact_undo': self.unique_undo.isChecked()}
class MenuCommandEditorDialog(QtWidgets.QDialog):
def __init__(self, command, parent=None):
super(MenuCommandEditorDialog, self).__init__(parent)
self.setWindowTitle('Edit/Create command')
self.languages = QtWidgets.QComboBox()
self.languages.addItems([MEL, PYTHON])
self.languages.setCurrentText(command['language'])
self.languages.currentIndexChanged.connect(self.language_changed)
self.eval_deferred = QtWidgets.QCheckBox('Eval deferred (python only)')
self.eval_deferred.setChecked(command['deferred'])
self.unique_undo = QtWidgets.QCheckBox('Unique undo')
self.unique_undo.setChecked(command['force_compact_undo'])
self.caption = QtWidgets.QLineEdit()
self.caption.setText(command['caption'])
self.command = QtWidgets.QTextEdit()
self.command.setPlaceholderText(COMMAND_PLACEHOLDER)
self.command.setPlainText(command['command'])
self.ok = QtWidgets.QPushButton('Ok')
self.ok.released.connect(self.accept)
self.cancel = QtWidgets.QPushButton('Cancel')
self.cancel.released.connect(self.reject)
form = QtWidgets.QFormLayout()
form.setSpacing(0)
form.addRow('Caption', self.caption)
form.addRow('Language', self.languages)
options_group = QtWidgets.QGroupBox('Options')
options_layout = QtWidgets.QVBoxLayout(options_group)
options_layout.addWidget(self.eval_deferred)
options_layout.addWidget(self.unique_undo)
options_layout.addLayout(form)
code = QtWidgets.QGroupBox('Code')
code_layout = QtWidgets.QVBoxLayout(code)
code_layout.setSpacing(0)
code_layout.addWidget(self.command)
buttons_layout = QtWidgets.QHBoxLayout()
buttons_layout.addStretch(1)
buttons_layout.addWidget(self.ok)
buttons_layout.addWidget(self.cancel)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(options_group)
layout.addWidget(code)
layout.addLayout(buttons_layout)
self.language_changed()
def sizeHint(self):
return QtCore.QSize(400, 550)
def language_changed(self, *_):
language = self.languages.currentText()
highlighter = get_highlighter(language)
highlighter(self.command.document())
def command_data(self):
return {
'caption': self.caption.text(),
'language': self.languages.currentText(),
'command': self.command.toPlainText(),
'deferred': self.eval_deferred.isChecked(),
'force_compact_undo': self.unique_undo.isChecked()}

View File

@@ -0,0 +1,144 @@
import uuid
from copy import deepcopy
from collections import defaultdict
from .pyside import QtCore
from .shape import Shape
from .templates import PICKER
from .undo import UndoManager
from .stack import count_panels
class PickerDocument(QtCore.QObject):
shapes_changed = QtCore.Signal()
# origin: str ["editor"|"picker"], key: str
general_option_changed = QtCore.Signal(str, str)
data_changed = QtCore.Signal()
changed = QtCore.Signal()
def __init__(self, data):
super(PickerDocument, self).__init__()
self.data = data
self.filename = None
self.modified_state = False
self.undo_manager = UndoManager(self.data)
self.shapes = []
self.shapes_by_panel = {}
self.shapes_by_id = {}
self.shapes_by_layer = {}
self.generate_shapes()
self.shapes_changed.connect(self.emit_change)
self.general_option_changed.connect(self.emit_change)
self.data_changed.connect(self.emit_change)
self.shapes_changed.connect(self.emit_change)
def emit_change(self, *_):
"""
Signal allways emitted when any data of the model changed.
"""
self.changed.emit()
@staticmethod
def create():
data = {
'general': deepcopy(PICKER),
'shapes': []}
return PickerDocument(data)
def record_undo(self):
self.undo_manager.set_data_modified(self.data)
self.modified_state = True
def undo(self):
if self.undo_manager.undo():
self.data = self.undo_manager.data
self.generate_shapes()
self.data_changed.emit()
self.modified_state = True
def redo(self):
if self.undo_manager.redo():
self.data = self.undo_manager.data
self.generate_shapes()
self.data_changed.emit()
self.modified_state = True
def panel_count(self):
return count_panels(self.data['general']['panels'])
def set_shapes_data(self, data):
self.data['shapes'] = data
self.generate_shapes()
def generate_shapes(self):
self.shapes = [Shape(options) for options in self.data['shapes']]
self.sync_shapes_caches()
def sync_shapes_caches(self):
self.shapes_by_panel = defaultdict(list)
self.shapes_by_id = {}
self.shapes_by_layer = defaultdict(list)
for shape in self.shapes:
self.shapes_by_panel[shape.options['panel']].append(shape)
self.shapes_by_id[shape.options['id']] = shape
layer = shape.options['visibility_layer']
if layer:
self.shapes_by_layer[layer].append(shape)
def add_shapes(self, shapes_data, prepend=False, hierarchize=False):
for options in shapes_data:
options['id'] = str(uuid.uuid4())
options['children'] = []
shapes = []
parent_shape = None
for options in shapes_data:
shape = Shape(options)
shapes.append(shape)
if parent_shape and hierarchize:
parent_shape.options['children'].append(shape.options['id'])
parent_shape = shape
if prepend:
for shape in reversed(shapes):
self.shapes.insert(0, shape)
self.data['shapes'].insert(0, shape.options)
else:
self.shapes.extend(shapes)
self.data['shapes'].extend(shapes_data)
self.sync_shapes_caches()
return shapes
def remove_shapes(self, shapes):
removed_ids = [shape.options['id'] for shape in shapes]
self.data['shapes'] = [
s for s in self.data['shapes'] if s['id'] not in removed_ids]
self.generate_shapes()
def all_children(self, id_):
if id_ not in self.shapes_by_id:
return []
shape = self.shapes_by_id[id_]
result = []
to_visit = [id_]
visited = set()
while to_visit:
current_id = to_visit.pop(0)
if current_id in visited:
continue
visited.add(current_id)
shape = self.shapes_by_id.get(current_id)
if shape:
result.append(shape)
children = shape.options.get('children', [])
to_visit.extend(c for c in children if c not in visited)
return result

View File

@@ -0,0 +1,391 @@
import math
from .pyside import QtCore, QtGui
POINT_RADIUS = 8
POINT_OFFSET = 4
DIRECTIONS = [
'top_left',
'bottom_left',
'top_right',
'bottom_right',
'left',
'right',
'top',
'bottom']
def get_topleft_rect(rect):
"""
this function return a manipulator rect for the transform
handler.
*__________________________
| |
| |
|________________________|
"""
if rect is None:
return None
point = rect.topLeft()
return QtCore.QRectF(
point.x() - (POINT_RADIUS / 2.0) - POINT_OFFSET,
point.y() - (POINT_RADIUS / 2.0) - POINT_OFFSET,
POINT_RADIUS, POINT_RADIUS)
def get_bottomleft_rect(rect):
"""
this function return a manipulator rect for the transform
handler.
__________________________
| |
| |
|________________________|
*
"""
if rect is None:
return None
point = rect.bottomLeft()
return QtCore.QRectF(
point.x() - (POINT_RADIUS / 2.0) - POINT_OFFSET,
point.y() + (POINT_RADIUS / 2.0) - POINT_OFFSET,
POINT_RADIUS, POINT_RADIUS)
def get_topright_rect(rect):
"""
this function return a manipulator rect for the transform
handler.
__________________________*
| |
| |
|________________________|
"""
if rect is None:
return None
point = rect.topRight()
return QtCore.QRectF(
point.x() + (POINT_RADIUS / 2.0) - POINT_OFFSET,
point.y() - (POINT_RADIUS / 2.0) - POINT_OFFSET,
POINT_RADIUS, POINT_RADIUS)
def get_bottomright_rect(rect):
"""
this function return a manipulator rect for the transform
handler.
__________________________
| |
| |
|________________________|
*
"""
if rect is None:
return None
point = rect.bottomRight()
return QtCore.QRectF(
point.x() + (POINT_RADIUS / 2.0) - POINT_OFFSET,
point.y() + (POINT_RADIUS / 2.0) - POINT_OFFSET,
POINT_RADIUS, POINT_RADIUS)
def get_left_side_rect(rect):
"""
this function return a manipulator rect for the transform
handler.
__________________________
| |
*| |
|________________________|
"""
if rect is None:
return None
top = rect.top() + (rect.height() / 2.0)
return QtCore.QRectF(
rect.left() - (POINT_RADIUS / 2.0) - POINT_OFFSET,
top - (POINT_RADIUS / 2.0),
POINT_RADIUS, POINT_RADIUS)
def get_right_side_rect(rect):
"""
this function return a manipulator rect for the transform
handler.
__________________________
| |
| |*
|________________________|
"""
if rect is None:
return None
top = rect.top() + (rect.height() / 2.0)
return QtCore.QRectF(
rect.right() + (POINT_RADIUS / 2.0) - POINT_OFFSET,
top - (POINT_RADIUS / 2.0),
POINT_RADIUS, POINT_RADIUS)
def get_top_side_rect(rect):
"""
this function return a manipulator rect for the transform
handler.
_____________*____________
| |
| |
|________________________|
"""
if rect is None:
return None
return QtCore.QRectF(
rect.left() + (rect.width() / 2.0) - (POINT_RADIUS / 2.0),
rect.top() - (POINT_RADIUS / 2.0) - POINT_OFFSET,
POINT_RADIUS, POINT_RADIUS)
def get_bottom_side_rect(rect):
"""
this function return a manipulator rect for the transform
handler.
__________________________
| |
| |
|________________________|
*
"""
if rect is None:
return None
return QtCore.QRectF(
rect.left() + (rect.width() / 2.0) - (POINT_RADIUS / 2.0),
rect.bottom() + (POINT_RADIUS / 2.0) - POINT_OFFSET,
POINT_RADIUS, POINT_RADIUS)
def grow_rect(rect, value):
if rect is None:
return None
return QtCore.QRectF(
rect.left() - value,
rect.top() - value,
rect.width() + (value * 2),
rect.height() + (value * 2))
def distance(a, b):
""" return distance between two points """
x = (b.x() - a.x())**2
y = (b.y() - a.y())**2
return math.sqrt(abs(x + y))
def get_relative_point(rect, point):
x = point.x() - rect.left()
y = point.y() - rect.top()
return QtCore.QPoint(x, y)
def get_quarter(a, b, c):
quarter = None
if b.y() <= a.y() and b.x() < c.x():
quarter = 0
elif b.y() < a.y() and b.x() >= c.x():
quarter = 1
elif b.y() >= a.y() and b.x() > c.x():
quarter = 2
elif b.y() >= a.y() and b.x() <= c.x():
quarter = 3
return quarter
def get_point_on_line(angle, ray):
x = 50 + ray * math.cos(float(angle))
y = 50 + ray * math.sin(float(angle))
return QtCore.QPoint(x, y)
def get_angle_c(a, b, c):
return math.degrees(math.atan(distance(a, b) / distance(a, c)))
def get_absolute_angle_c(a, b, c):
quarter = get_quarter(a, b, c)
try:
angle_c = get_angle_c(a, b, c)
except ZeroDivisionError:
return 360 - (90 * quarter)
if quarter == 0:
return round(180.0 + angle_c, 1)
elif quarter == 1:
return round(270.0 + (90 - angle_c), 1)
elif quarter == 2:
return round(angle_c, 1)
elif quarter == 3:
return math.fabs(round(90.0 + (90 - angle_c), 1))
def proportional_rect(rect, percent=None):
""" return a scaled rect with a percentage """
factor = float(percent) / 100
width = rect.width() * factor
height = rect.height() * factor
left = rect.left() + round((rect.width() - width) / 2)
top = rect.top() + round((rect.height() - height) / 2)
return QtCore.QRectF(left, top, width, height)
def resize_rect_with_ratio(rect, reference_rect_output):
ratio = rect.width() / rect.height()
width = reference_rect_output.width()
height = reference_rect_output.width() / ratio
if reference_rect_output.height() < height:
width = reference_rect_output.height() * ratio
height = reference_rect_output.height()
rect = QtCore.QRectF(0, 0, width, height)
rect.moveCenter(reference_rect_output.center())
return rect
def get_shapes_bounding_rects(shapes):
rects = [
shape.rect if shape.options['shape'] != 'custom' else
shape.path.boundingRect()
for shape in shapes]
return get_combined_rects(rects)
def get_combined_rects(rects):
"""
this function analyse list of rects and return
a rect with the smaller top and left and highest right and bottom
__________________________________ ?
| | A |
| | |
|______________| ___________| B
| | |
|_____________________|__________|
"""
if not rects:
return None
l = min(rect.left() for rect in rects)
t = min(rect.top() for rect in rects)
r = max(rect.right() for rect in rects)
b = max(rect.bottom() for rect in rects)
return QtCore.QRectF(l, t, r-l, b-t)
def get_global_rect(points):
left = min(p.x() for p in points)
top = min(p.y() for p in points)
width = max(p.x() for p in points) - left
height = max(p.y() for p in points) - top
return QtCore.QRectF(left, top, width, height)
def rect_symmetry(rect, point, horizontal=True):
"""
______ rect ______ result
| | | |
|______| |______|
. point
Compute symmetry for a rect from a given point and axis
"""
center = rect.center()
if horizontal:
dist = (center.x() - point.x()) * 2
vector = QtCore.QPoint(dist, 0)
else:
dist = (center.y() - point.y()) * 2
vector = QtCore.QPoint(0, dist)
center = rect.center() - vector
rect.moveCenter(center)
return rect
def rect_top_left_symmetry(rect, point, horizontal=True):
topleft = rect.topLeft()
if horizontal:
dist = (topleft.x() - point.x()) * 2
vector = QtCore.QPoint(dist, 0)
else:
dist = (topleft.y() - point.y()) * 2
vector = QtCore.QPoint(0, dist)
topleft = rect.topLeft() - vector
rect.moveTopLeft(topleft)
return rect
def path_symmetry(path, center=None, horizontal=True):
center = center or QtCore.QPointF(0, 0)
for point in path:
for key in ['point', 'tangent_in', 'tangent_out']:
if point[key] is None:
continue
if horizontal:
point[key][0] = center.x() - (point[key][0] - center.x())
else:
point[key][1] = center.y() - (point[key][1] - center.y())
def split_line(point1, point2, step_number):
"""
split a line on given number of points.
"""
if step_number <= 1:
return [point2]
x_values = split_range(point1.x(), point2.x(), step_number)
y_values = split_range(point1.y(), point2.y(), step_number)
return [QtCore.QPoint(x, y) for x, y in zip(x_values, y_values)]
def split_range(input_, output, step_number):
difference = output - input_
step = difference / float(step_number - 1)
return [int(input_ + (step * i)) for i in range(step_number)]
def angle_at(path, percent):
halfway_point = path.pointAtPercent(percent)
tangent = path.percentAtLength(path.length() / 2)
dx = path.pointAtPercent(tangent - 0.01).x() - halfway_point.x()
dy = path.pointAtPercent(tangent - 0.01).y() - halfway_point.y()
angle_radians = math.atan2(dy, dx)
angle_degrees = math.degrees(angle_radians)
return angle_degrees
def get_connection_path(
start_point, end_point, viewportmapper=None):
start_point = viewportmapper.to_viewport_coords(start_point)
end_point = viewportmapper.to_viewport_coords(end_point)
path = QtGui.QPainterPath(start_point)
path.lineTo(end_point)
path = QtGui.QPainterPathStroker().createStroke(path)
line = QtGui.QPainterPath(start_point)
line.lineTo(end_point)
degrees = angle_at(line, 0.5)
center = line.pointAtPercent(0.5)
offset = 3 + viewportmapper.zoom
triangle = QtGui.QPolygonF([
QtCore.QPointF(center.x() - offset, center.y() - offset),
QtCore.QPointF(center.x() + offset, center.y()),
QtCore.QPointF(center.x() - offset, center.y() + offset),
QtCore.QPointF(center.x() - offset, center.y() - offset)])
transform = QtGui.QTransform()
transform.translate(center.x(), center.y())
transform.rotate(degrees)
transform.translate(-center.x(), -center.y())
triangle = transform.map(triangle)
path.addPolygon(triangle)
return path
if __name__ == "__main__":
assert split_range(0, 10, 11) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

View File

@@ -0,0 +1,40 @@
from maya import cmds
from .optionvar import save_optionvar, DEFAULT_HOTKEYS, OPTIONVARS
def get_hotkeys_config():
# For config retro compatibility, we always ensure that default value is
# set in case of new shortcut added in the system. We also ensure that old
# shortcut is going to be removed from the config.
default = build_config_from_string(OPTIONVARS[DEFAULT_HOTKEYS])
saved = build_config_from_string(cmds.optionVar(query=DEFAULT_HOTKEYS))
for key in default.keys():
if key in saved:
default[key] = saved[key]
return default
def build_config_from_string(value):
config = {}
for entry in value.split(';'):
function_name = entry.split('=')[0]
enabled = bool(int(entry.split('=')[-1].split(',')[-1]))
key_sequence = entry.split('=')[-1].split(',')[0]
config[function_name] = {
'enabled': enabled if key_sequence != 'None' else False,
'key_sequence': None if key_sequence == 'None' else key_sequence}
return config
def set_hotkey_config(function, key_sequence, enabled):
config = get_hotkeys_config()
config[function] = {'enabled': enabled, 'key_sequence': key_sequence}
save_hotkey_config(config)
def save_hotkey_config(config):
value = ';'.join([
'{0}={1},{2}'.format(function, data['key_sequence'], int(data['enabled']))
for function, data in config.items()])
save_optionvar(DEFAULT_HOTKEYS, value)

View File

@@ -0,0 +1,194 @@
from .pyside import QtWidgets, QtCore, QtGui
from .hotkeys import get_hotkeys_config, save_hotkey_config
class HotkeysEditor(QtWidgets.QWidget):
hotkey_changed = QtCore.Signal()
def __init__(self, parent=None):
super(HotkeysEditor, self).__init__(parent)
self.model = HotkeysTableModel()
self.model.hotkey_changed.connect(self.hotkey_changed.emit)
self.table = QtWidgets.QTableView()
self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
self.table.setModel(self.model)
self.table.selectionModel().selectionChanged.connect(
self.selection_changed)
self.hotkey_editor = HotkeyEditor()
self.hotkey_editor.hotkey_edited.connect(self.update_hotkeys)
self.clear = QtWidgets.QPushButton('Clear')
self.clear.released.connect(self.do_clear)
hotkey_layout = QtWidgets.QVBoxLayout()
hotkey_layout.setContentsMargins(0, 0, 0, 0)
hotkey_layout.addWidget(self.hotkey_editor)
hotkey_layout.addWidget(self.clear)
hotkey_layout.addStretch(1)
layout = QtWidgets.QHBoxLayout(self)
layout.addWidget(self.table)
layout.addLayout(hotkey_layout)
def do_clear(self):
self.hotkey_editor.clear_values()
self.update_hotkeys()
self.hotkey_changed.emit()
def update_hotkeys(self):
self.model.set_keysequence(
self.hotkey_editor.function_name,
self.hotkey_editor.key_sequence())
def selection_changed(self, *_):
indexes = self.table.selectionModel().selectedIndexes()
if not indexes:
self.hotkey_editor.clear()
return
row = indexes[0].row()
function_name = sorted(list(self.model.config))[row]
data = self.model.config[function_name]
self.hotkey_editor.set_key_sequence(
function_name, data['key_sequence'])
class HotkeyEditor(QtWidgets.QWidget):
hotkey_edited = QtCore.Signal()
def __init__(self, parent=None):
super(HotkeyEditor, self).__init__(parent)
self.function_name = None
self.function_name_label = QtWidgets.QLabel()
self.alt = QtWidgets.QCheckBox('Alt')
self.alt.released.connect(self.emit_hotkey_edited)
self.ctrl = QtWidgets.QCheckBox('Ctrl')
self.ctrl.released.connect(self.emit_hotkey_edited)
self.shift = QtWidgets.QCheckBox('Shift')
self.shift.released.connect(self.emit_hotkey_edited)
self.string = KeyField()
self.string.changed.connect(self.hotkey_edited.emit)
modifiers = QtWidgets.QHBoxLayout()
modifiers.addWidget(self.alt)
modifiers.addWidget(self.ctrl)
modifiers.addWidget(self.shift)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.function_name_label)
layout.addLayout(modifiers)
layout.addWidget(self.string)
def clear(self):
self.function_name = None
self.clear_values()
self.function_name_label.setText('')
def clear_values(self):
self.ctrl.setChecked(False)
self.alt.setChecked(False)
self.shift.setChecked(False)
self.string.setText('')
def emit_hotkey_edited(self, *_):
self.hotkey_edited.emit()
def key_sequence(self):
if not self.string.text():
return None
sequence = []
if self.ctrl.isChecked():
sequence.append('CTRL')
if self.alt.isChecked():
sequence.append('ALT')
if self.shift.isChecked():
sequence.append('SHIFT')
sequence.append(self.string.text())
return '+'.join(sequence)
def set_key_sequence(self, function_name, key_sequence):
self.function_name = function_name
self.function_name_label.setText(function_name.title())
if key_sequence is None:
self.ctrl.setChecked(False)
self.alt.setChecked(False)
self.shift.setChecked(False)
self.string.setText('')
return
self.ctrl.setChecked('ctrl' in key_sequence.lower())
self.alt.setChecked('alt' in key_sequence.lower())
self.shift.setChecked('shift' in key_sequence.lower())
self.string.setText(key_sequence.split('+')[-1])
class KeyField(QtWidgets.QLineEdit):
changed = QtCore.Signal()
def __init__(self, parent=None):
super(KeyField, self).__init__(parent)
self.setReadOnly(True)
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Shift:
return
self.setText(QtGui.QKeySequence(event.key()).toString())
self.changed.emit()
class HotkeysTableModel(QtCore.QAbstractTableModel):
HEADERS = 'Function', 'Key sequence'
hotkey_changed = QtCore.Signal()
def __init__(self, parent=None):
super(HotkeysTableModel, self).__init__(parent)
self.config = get_hotkeys_config()
def rowCount(self, *_):
return len(self.config)
def columnCount(self, *_):
return len(self.HEADERS)
def set_keysequence(self, function_name, key_sequence):
self.layoutAboutToBeChanged.emit()
self.config[function_name]['key_sequence'] = key_sequence
if key_sequence is None:
self.config[function_name]['enabled'] = False
save_hotkey_config(self.config)
self.layoutChanged.emit()
self.hotkey_changed.emit()
def flags(self, index):
flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
if index.column() == 0:
flags |= QtCore.Qt.ItemIsUserCheckable
return flags
def headerData(self, section, orientation, role):
if orientation == QtCore.Qt.Vertical or role != QtCore.Qt.DisplayRole:
return
return self.HEADERS[section]
def setData(self, index, value, role):
if role != QtCore.Qt.CheckStateRole or index.column() != 0:
return
function = sorted(list(self.config))[index.row()]
self.config[function]['enabled'] = value
save_hotkey_config(self.config)
self.hotkey_changed.emit()
return True
def data(self, index, role):
if not index.isValid():
return
function = sorted(list(self.config))[index.row()]
data = self.config[function]
if role == QtCore.Qt.DisplayRole:
if index.column() == 0:
return function.title()
else:
return data['key_sequence']
if role == QtCore.Qt.CheckStateRole and index.column() == 0:
return (
QtCore.Qt.Checked if data['enabled'] else QtCore.Qt.Unchecked)

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 784 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 737 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1020 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 746 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1 @@
from .converter import convert

View File

@@ -0,0 +1,271 @@
import json
import os
from ...pyside import QtGui
from ...compatibility import ensure_retro_compatibility
from .parser import parse_animschool_picker, save_png
PICKER = {
'name': 'Untitled',
'version': (0, 15, 3),
'panels.as_sub_tab': False,
'panels.orientation': 'vertical',
'panels.zoom_locked': [False],
'panels.colors': [None],
'panels.names': ['Panel 1'],
'menu_commands': [],
'hidden_layers': [],
'panels': [[1.0, [1.0]]]
}
BUTTON = {
'background': False,
'visibility_layer': None,
'shape.ignored_by_focus': False,
'panel': 0,
'shape': 'square', # or round or rounded_rect or custom
'shape.space': 'world', # or screen
'shape.anchor': 'top_left', # or bottom_left, top_right, bottom_right
'shape.path' : [],
'shape.left': 0.0,
'shape.top': 0.0,
'shape.width': 120.0,
'shape.height': 25.0,
'shape.cornersx': 4,
'shape.cornersy': 4,
'border': True,
'borderwidth.normal': 1.0,
'borderwidth.hovered': 1.25,
'borderwidth.clicked': 2,
'bordercolor.normal': '#000000',
'bordercolor.hovered': '#393939',
'bordercolor.clicked': '#FFFFFF',
'bordercolor.transparency': 0,
'bgcolor.normal': '#888888',
'bgcolor.hovered': '#AAAAAA',
'bgcolor.clicked': '#DDDDDD',
'bgcolor.transparency': 0,
'text.content': 'Button',
'text.size': 12,
'text.bold': False,
'text.italic': False,
'text.color': '#FFFFFF',
'text.valign': 'center', # or 'top' or bottom
'text.halign': 'center', # or 'left' or 'right'
'action.targets': [],
'action.commands': [],
'action.menu_commands': [],
'image.path': '',
'image.fit': True,
'image.ratio': True,
'image.height': 32,
'image.width': 32
}
TEXT = {
'background': False,
'visibility_layer': None,
'shape.ignored_by_focus': False,
'panel': 0,
'shape': 'square', # or round or rounded_rect or custom
'shape.space': 'world', # or screen
'shape.anchor': 'top_left', # or bottom_left, top_right, bottom_right
'shape.path' : [],
'shape.left': 0.0,
'shape.top': 0.0,
'shape.width': 200.0,
'shape.height': 50.0,
'shape.cornersx': 4,
'shape.cornersy': 4,
'border': False,
'borderwidth.normal': 0,
'borderwidth.hovered': 0,
'borderwidth.clicked': 0,
'bordercolor.normal': '#000000',
'bordercolor.hovered': '#393939',
'bordercolor.clicked': '#FFFFFF',
'bordercolor.transparency': 0,
'bgcolor.normal': '#888888',
'bgcolor.hovered': '#AAAAAA',
'bgcolor.clicked': '#DDDDDD',
'bgcolor.transparency': 255,
'text.content': 'Text',
'text.size': 16,
'text.bold': True,
'text.italic': False,
'text.color': '#FFFFFF',
'text.valign': 'top', # or 'top' or bottom
'text.halign': 'left', # or 'left' or 'right'
'action.targets': [],
'action.commands': [],
'action.menu_commands': [],
'image.path': '',
'image.fit': False,
'image.ratio': True,
'image.height': 32,
'image.width': 32,
}
BACKGROUND = {
'background': True,
'visibility_layer': None,
'shape.ignored_by_focus': True,
'panel': 0,
'shape': 'square', # or round or rounded_rect or custom
'shape.space': 'world', # or screen
'shape.anchor': 'top_left', # or bottom_left, top_right, bottom_right
'shape.path' : [],
'shape.left': 0.0,
'shape.top': 0.0,
'shape.width': 400.0,
'shape.height': 400.0,
'shape.cornersx': 4,
'shape.cornersy': 4,
'border': False,
'borderwidth.normal': 0,
'borderwidth.hovered': 0,
'borderwidth.clicked': 0,
'bordercolor.normal': '#888888',
'bordercolor.hovered': '#888888',
'bordercolor.clicked': '#888888',
'bordercolor.transparency': 0,
'bgcolor.normal': '#888888',
'bgcolor.hovered': '#888888',
'bgcolor.clicked': '#888888',
'bgcolor.transparency': 0,
'text.content': '',
'text.size': 12,
'text.bold': False,
'text.italic': False,
'text.color': '#FFFFFF',
'text.valign': 'center', # or 'top' or bottom
'text.halign': 'center', # or 'left' or 'right'
'action.targets': [],
'action.commands': [],
'action.menu_commands': [],
'image.path': '',
'image.fit': True,
'image.ratio': False,
'image.height': 32,
'image.width': 32,
}
def rgb_to_hex(r, g, b):
return '#{r:02x}{g:02x}{b:02x}'.format(r=r, g=g, b=b)
def _label_width(text):
width = 0
for letter in text:
if letter == " ":
width += 3
elif letter.isupper():
width += 7
else:
width += 6
return width
def convert_to_picker_button(button):
if len(button['label']):
button['w'] = max((button['w'], _label_width(button['label'])))
delta = {
'text.content': button['label'],
'shape.left': button['x'] - (button['w'] // 2),
'shape.top': button['y'] - (button['h'] // 2),
'shape.width': button['w'],
'shape.height': button['h']}
if button['action'] == 'select':
delta['action.targets'] = button['targets']
if len(button['targets']) > 1:
delta['shape'] = 'rounded_square' if button['label'] else 'round'
delta['shape.cornersx'] = delta['shape.width'] / 10
delta['shape.cornersy'] = delta['shape.height'] / 10
else:
delta['action.left.language'] = button['lang']
delta['action.left.command'] = button['targets'][0]
delta['bgcolor.normal'] = rgb_to_hex(*button['bgcolor'])
delta['text.color'] = rgb_to_hex(*button['txtcolor'])
delta['border'] = button['action'] == 'command'
delta['border'] = button['action'] == 'command'
picker_button = BUTTON.copy()
picker_button.update(delta)
return picker_button
def frame_picker_buttons(picker):
shapes = picker['shapes']
offset_x = min(shape['shape.left'] for shape in shapes)
offset_y = min(shape['shape.top'] for shape in shapes)
offset = -min([offset_x, 0]), -min([offset_y, 0])
for shape in shapes:
shape['shape.left'] += offset[0]
shape['shape.top'] += offset[1]
def fit_picker_to_content(picker):
shapes = picker['shapes']
width = max(s['shape.left'] + s['shape.width'] for s in shapes)
height = max(s['shape.top'] + s['shape.height'] for s in shapes)
picker['general']['width'] = int(width)
picker['general']['height'] = int(height)
def image_to_background_shape(imagepath):
shape = BACKGROUND.copy()
shape['image.path'] = imagepath
image = QtGui.QImage(imagepath)
shape['image.width'] = image.size().width()
shape['image.height'] = image.size().height()
shape['shape.width'] = image.size().width()
shape['shape.height'] = image.size().height()
shape['bgcolor.transparency'] = 255
return shape
def build_picker_from_pkr(title, buttons, imagepath, dst):
picker = {
'general': PICKER.copy(),
'shapes': [convert_to_picker_button(b) for b in buttons]}
picker['general']['name'] = title
if imagepath:
picker['shapes'].insert(0, image_to_background_shape(imagepath))
frame_picker_buttons(picker)
fit_picker_to_content(picker)
ensure_retro_compatibility(picker)
with open(dst, "w") as f:
json.dump(picker, f, indent=2)
def convert(filepath, directory=None):
directory = directory or os.path.dirname(filepath)
title, buttons, png_data = parse_animschool_picker(filepath)
picker_filename = os.path.splitext(os.path.basename(filepath))[0]
png_path = unique_filename(directory, picker_filename, 'png')
png_path = png_path if png_data else None
dst = unique_filename(directory, picker_filename, 'json')
if png_path:
save_png(png_data, png_path)
build_picker_from_pkr(title, buttons, png_path, dst)
return dst
def unique_filename(directory, filename, extension):
filepath = os.path.join(directory, filename) + '.' + extension
i = 0
while os.path.exists(filepath):
filepath = '{base}.{index}.{extension}'.format(
base=os.path.join(directory, filename),
index=str(i).zfill(3),
extension=extension)
i += 1
return filepath

View File

@@ -0,0 +1,275 @@
"""
Module to parse and extract data from AnimSchool picker file.
This works for Animschool until 2021 release.
PKR file structure description:
-- header --
4 bytes (singed int): Picker Version.
4 bytes (singed int): Title number (x) of bytes length.
x bytes (hex text): Title.
-- PNG data --
...
--- buttons ---
4 bytes (singed int): Number of buttons
-- Button array --
for _ in range(number_of_buttons)
- 4 bytes (singed int): Button id as signed int.
- 4 bytes (singed int): Center position X.
- 4 bytes (singed int): Center position Y.
- 4 bytes (singed int):
Size for old AnimSchool versions (4 and older)
This is still there but unused in 2021 version.
- 4 bytes (singed int): Width.
- 4 bytes (singed int): Height.
- 4 bytes (bool): Button type.
True = Command button.
False = Selection button.
- 4 bytes (bool): Languages used for command button.
True = Python.
False = Mel.
- 4 bytes (hex __RRGGBB): Background color.
- 4 bytes (hex __RRGGBB): Text color.
- 4 bytes (singed int): Label number (x) of bytes length.
- x bytes (hexa text): Label.
- 4 bytes (singed int): Number (x) of targets.
This is automatically 1 for command button
for _ in range(number_of_targets):
- 4 bytes (singed int): Target name number (x) of bytes length.
- x bytes (hexa text): Target name.
The script export pkr data in 3 different objects:
PNG data:
This is a one to one of the png binari data encapsulated in the pkr
file.
Title:
As simple string
Buttons:
Translate the binari buttons as readable python dict!
{
"id": int,
"x": int,
"y": int,
"w": int,
"h": int,
"action": str: "select" | "command",
"lang": str: "mel" | "python",
"bgcolor": [r:int, g:int, b:int],
"txtcolor": [r:int, g:int, b:int],
"label": str,
"targets": List[str]
}
"""
from binascii import hexlify, unhexlify
import json
import os
PNG_HEADER = b'89504e470d0a1a0a'
PNG_FOOTER = b'ae426082'
def split_data(content, number_of_bytes=4):
if isinstance(number_of_bytes, bytes):
number_of_bytes = int(number_of_bytes, 16)
return content[:number_of_bytes * 2], content[number_of_bytes * 2:]
def bytes_to_string(stringdata):
return ''.join(
b.decode('cp1252')
for b in unhexlify(stringdata).split(b'\x00'))
def bytes_to_int(i):
if i[:4] == b'00' * 2:
return int(i, 16)
elif i[:4] == b'ff' * 2:
return -65535 + int(i[-4:], 16)
raise Exception('Count not interpret data as int')
def print_(data, max_bytes=64):
string = repr(data)[2:-1][:max_bytes * 2]
beautified = ''
for i in range(len(string)):
beautified += string[i].upper()
if i % 2:
beautified += ' '
if (i + 1) % 16 == 0 and i != 0:
beautified += '\n'
print(beautified)
def bytes_to_rgb(data):
data = int(data, 16)
b = data & 255
g = (data >> 8) & 255
r = (data >> 16) & 255
return r, g, b
def extract_string(data):
string_size, data = split_data(data)
string, data = split_data(data, string_size)
string = bytes_to_string(string)
return string, data
def extract_png_data(data):
png_len_size, data = split_data(data)
png_len_size = bytes_to_int(png_len_size)
if not png_len_size:
return None, data
png_len, data = split_data(data, png_len_size)
png_len = int(bytes_to_string(png_len)) # lol
if png_len == 0:
_, data = split_data(data, 4) # remove some leftover data
return None, data
_, data = split_data(data, 4)
png_end = int((data.find(PNG_FOOTER) + len(PNG_FOOTER)) / 2)
return split_data(data, png_end)
def extract_button_targets(data):
number_of_targets, data = split_data(data)
targets = []
number_of_targets = int(number_of_targets, 16)
for _ in range(number_of_targets):
target_name, data = extract_string(data)
targets.append(target_name)
return targets, data
def extract_button_data(data, version=5, verbose=True):
button_id, data = split_data(data)
button_id = bytes_to_int(button_id)
if verbose:
print('Button #{button_id}'.format(button_id=button_id))
x, data = split_data(data)
x = bytes_to_int(x)
y, data = split_data(data)
y = bytes_to_int(y)
old_height, data = split_data(data)
if version > 4:
width, data = split_data(data)
width = bytes_to_int(width)
height, data = split_data(data)
height = bytes_to_int(height)
else:
width, height = bytes_to_int(old_height), bytes_to_int(old_height)
action, data = split_data(data)
action = bytes_to_int(action)
assert action in [0, 1]
action = 'command' if action else 'select'
lang, data = split_data(data)
lang = bytes_to_int(lang)
assert lang in [0, 1]
lang = 'python' if lang else 'mel'
bgcolor, data = split_data(data)
bgcolor = bytes_to_rgb(bgcolor)
txtcolor, data = split_data(data)
txtcolor = bytes_to_rgb(txtcolor)
label_size, data = split_data(data)
if label_size == b'ff' * 4:
label = ''
else:
label, data = split_data(data, label_size)
label = bytes_to_string(label)
targets, data = extract_button_targets(data)
button = dict(
id=button_id, x=x, y=y, w=width, h=height, action=action,
lang=lang, bgcolor=bgcolor, txtcolor=txtcolor, label=label,
targets=targets)
return button, data
def parse_animschool_picker(picker_path, verbose=False):
with open(picker_path, 'rb') as file:
data = hexlify(file.read())
# Get version
version, data = split_data(data)
version = bytes_to_int(version)
print("this picker is build with AnimSchool v" + str(version))
# Get title
title, data = extract_string(data)
if verbose:
print('Title: "{title}"'.format(title=title))
# Extract PNG
png_data, data = extract_png_data(data)
if verbose and png_data:
print('PNG data found')
# Get number of buttons
number_of_buttons, data = split_data(data)
number_of_buttons = int(number_of_buttons, 16)
if verbose:
print('Number of buttons: "{num}"'.format(num=number_of_buttons))
# Parse buttons one by one:
buttons = []
while data:
button, data = extract_button_data(data, version, verbose)
buttons.append(button)
if len(buttons) != number_of_buttons:
raise Exception('Parsing buttons went wrong.')
return title, buttons, png_data
def extract_to_files(pkr_path, verbose=False):
"""
Extract data and image to .json and .png (if any) next to the .pkr
"""
title, buttons, png_data = parse_animschool_picker(pkr_path, verbose)
# Save to json
with open(pkr_path + '.json', 'w') as f:
json.dump([title, buttons], f, indent=4)
# Write PNG to file:
png_path = pkr_path + '.png'
if png_data and not os.path.exists(png_path):
save_png(png_data, png_path)
return title, buttons, png_data
def save_png(png_data, dst):
print('Saving PNG to "{dst}"'.format(dst=dst))
with open(dst, 'wb') as f:
f.write(unhexlify(png_data))
if __name__ == '__main__':
import sys
arg = sys.argv[-1]
if arg == 'dir':
# Extract json and png for all .pkr files in current dir:
import glob
for pkr_path in glob.glob('./*.pkr'):
print(os.path.basename(pkr_path))
try:
extract_to_files(pkr_path)
except BaseException:
print('Failed to parse {pkr_path}'.format(pkr_path=pkr_path))
elif arg.endswith('.pkr') and os.path.exists(arg):
# Extract given path to json and png:
import pprint
print('Parsing {arg}'.format(arg=arg))
title, buttons, png_data = extract_to_files(arg, verbose=True)
print(title)
pprint.pprint(buttons)

View File

@@ -0,0 +1,85 @@
from .pyside import QtWidgets, QtCore
from maya import cmds
from .optionvar import ZOOM_BUTTON
class InteractionManager:
FLY_OVER = 'fly_over'
SELECTION = 'selection'
NAVIGATION = 'navigation'
DRAGGING = 'dragging'
ZOOMING = 'zooming'
def __init__(self):
self.shapes = []
self.left_click_pressed = False
self.right_click_pressed = False
self.middle_click_pressed = False
self.mouse_ghost = None
self.has_shape_hovered = False
self.dragging = False
self.anchor = None
self.zoom_anchor = None
@property
def ctrl_pressed(self):
modifiers = QtWidgets.QApplication.keyboardModifiers()
return modifiers == (modifiers | QtCore.Qt.ControlModifier)
@property
def shift_pressed(self):
modifiers = QtWidgets.QApplication.keyboardModifiers()
return modifiers == (modifiers | QtCore.Qt.ShiftModifier)
@property
def alt_pressed(self):
modifiers = QtWidgets.QApplication.keyboardModifiers()
return modifiers == (modifiers | QtCore.Qt.AltModifier)
def update(
self,
event,
pressed=False,
has_shape_hovered=False,
dragging=False):
self.dragging = dragging
self.has_shape_hovered = has_shape_hovered
self.update_mouse(event, pressed)
def update_mouse(self, event, pressed):
if event.button() == QtCore.Qt.LeftButton:
self.left_click_pressed = pressed
self.anchor = event.pos() if self.dragging else None
elif event.button() == QtCore.Qt.RightButton:
self.right_click_pressed = pressed
elif event.button() == QtCore.Qt.MiddleButton:
self.middle_click_pressed = pressed
if self.zoom_button_pressed:
self.zoom_anchor = event.pos() if pressed else None
@property
def mode(self):
if self.dragging:
return InteractionManager.DRAGGING
elif self.zoom_button_pressed and self.alt_pressed:
return InteractionManager.ZOOMING
elif self.middle_click_pressed:
return InteractionManager.NAVIGATION
elif self.left_click_pressed:
return InteractionManager.SELECTION
self.mouse_ghost = None
return InteractionManager.FLY_OVER
def mouse_offset(self, position):
result = position - self.mouse_ghost if self.mouse_ghost else None
self.mouse_ghost = position
return result or None
@property
def zoom_button_pressed(self):
button = cmds.optionVar(query=ZOOM_BUTTON)
return any((
button == 'left' and self.left_click_pressed,
button == 'middle' and self.middle_click_pressed,
button == 'right' and self.right_click_pressed))

View File

@@ -0,0 +1,73 @@
from .pyside import QtCore
from .geometry import (
DIRECTIONS, get_topleft_rect, get_bottomleft_rect, get_topright_rect,
get_bottomright_rect, get_left_side_rect, get_right_side_rect,
get_top_side_rect, get_bottom_side_rect)
from .shape import rect_intersects_shape
class SelectionSquare():
def __init__(self):
self.rect = None
self.handeling = False
def clicked(self, cursor):
self.handeling = True
self.rect = QtCore.QRectF(cursor, cursor)
def handle(self, cursor):
self.rect.setBottomRight(cursor)
def release(self):
self.handeling = False
self.rect = None
def intersects(self, shape):
if not shape or not self.rect:
return False
return rect_intersects_shape(shape, self.rect)
class Manipulator():
def __init__(self, viewportmapper=None):
self._rect = None
self.viewportmapper = viewportmapper
self._is_hovered = False
@property
def rect(self):
return self._rect
def viewport_handlers(self):
rect = self.viewportmapper.to_viewport_rect(self.rect)
return [
get_topleft_rect(rect) if rect else None,
get_bottomleft_rect(rect) if rect else None,
get_topright_rect(rect) if rect else None,
get_bottomright_rect(rect) if rect else None,
get_left_side_rect(rect) if rect else None,
get_right_side_rect(rect) if rect else None,
get_top_side_rect(rect) if rect else None,
get_bottom_side_rect(rect) if rect else None]
def get_direction(self, viewport_cursor):
if self.rect is None:
return None
for i, rect in enumerate(self.viewport_handlers()):
if rect.contains(viewport_cursor):
return DIRECTIONS[i]
def hovered_rects(self, cursor):
rects = []
for rect in self.viewport_handlers() + [self.rect]:
if not rect:
continue
if rect.contains(cursor):
rects.append(rect)
return rects
def set_rect(self, rect):
self._rect = rect

View File

@@ -0,0 +1,89 @@
from copy import deepcopy
PYTHON = 'python'
MEL = 'mel'
PYTHON_TARGETS_VARIABLE = """\
import animation_tools.dwpicker as dwpicker
__targets__ = [{targets}]
if dwpicker.get_shape('{shape_id}'):
__shape__ = dwpicker.get_shape('{shape_id}').options
else:
__shape__ = None
{code}
"""
MEL_TARGETS_VARIABLE = """\
string $targets[] = {{{targets}}};
{code}
"""
DEFERRED_PYTHON = """\
from maya import cmds
cmds.evalDeferred(\"\"\"{code}\"\"\", lowestPriority=True)
"""
DEFERRED_MEL = """\
evalDeferred "{code}" -lowestPriority;"""
STACK_UNDO_PYTHON = """\
from maya import cmds
cmds.undoInfo(openChunk=True)
{code}
cmds.undoInfo(closeChunk=True)
"""
STACK_UNDO_MEL = """\
undoInfo -openChunk;
{code}
undoInfo -closeChunk;
"""
EXECUTION_WARNING = """\
Code execution failed for {object}: "{name}"
{error}.
"""
def execute_code(
language, code, shape=None, deferred=False, compact_undo=False):
return EXECUTORS[language](code, shape, deferred, compact_undo)
def execute_python(
code, shape=None, deferred=False, compact_undo=False):
if compact_undo:
code = STACK_UNDO_PYTHON.format(code=code)
if deferred:
code = DEFERRED_PYTHON.format(code=code)
targets = (shape.targets() or []) if shape else []
targets = ', '.join(('"{}"'.format(target) for target in targets))
shape_id = shape.options['id'] if shape else None
code = PYTHON_TARGETS_VARIABLE.format(
targets=targets, shape_id=shape_id, code=code)
exec(code, globals())
def execute_mel(code, shape=None, deferred=False, compact_undo=False):
from maya import mel
if compact_undo:
code = STACK_UNDO_MEL.format(code=code)
if deferred:
print('Eval deferred not supported for mel command.')
# code = DEFERRED_MEL.format(code=code)
targets = (shape.targets() or []) if shape else []
if targets:
targets = ', '.join(
'"{}"'.format(target) for target in shape.targets())
code = MEL_TARGETS_VARIABLE.format(targets=targets, code=code)
mel.eval(code.replace(u'\u2029', '\n'))
EXECUTORS = {
PYTHON: execute_python,
MEL: execute_mel,
}

View File

@@ -0,0 +1,870 @@
# -*- coding: utf-8 -*-
import os
import json
import webbrowser
from copy import deepcopy
from functools import partial
from .pyside import QtWidgets, QtCore, QtGui
from maya import cmds
import maya.OpenMaya as om
from .appinfos import (
VERSION, RELEASE_DATE, DW_GITHUB, DW_WEBSITE, PICKER_DOCUMENTATION)
from .compatibility import ensure_retro_compatibility
from .document import PickerDocument
from .designer.editor import PickerEditor
from .dialog import (
question, get_image_path, NamespaceDialog)
from .ingest import animschool
from .hotkeys import get_hotkeys_config
from .namespace import (
switch_namespace, selected_namespace, detect_picker_namespace,
pickers_namespaces)
from .optionvar import (
AUTO_FOCUS_BEHAVIOR, AUTO_SWITCH_TAB, AUTO_RESIZE_NAMESPACE_COMBO,
CHECK_IMAGES_PATHS, AUTO_SET_NAMESPACE, DISABLE_IMPORT_CALLBACKS,
DISPLAY_HIERARCHY_IN_PICKER, DISPLAY_QUICK_OPTIONS,
INSERT_TAB_AFTER_CURRENT, LAST_OPEN_DIRECTORY, LAST_IMPORT_DIRECTORY,
LAST_SAVE_DIRECTORY, NAMESPACE_TOOLBAR, USE_ICON_FOR_UNSAVED_TAB,
WARN_ON_TAB_CLOSED, save_optionvar, append_recent_filename,
save_opened_filenames)
from .path import get_import_directory, get_open_directory, format_path
from .picker import PickerStackedView, list_targets
from .preference import PreferencesWindow
from .qtutils import set_shortcut, icon, maya_main_window, DockableBase
from .quick import QuickOptions
from .references import ensure_images_path_exists
from .scenedata import (
load_local_picker_data, store_local_picker_data,
clean_stray_picker_holder_nodes)
from .templates import PICKER, BACKGROUND
ABOUT = """\
DreamWall Picker
Licence MIT
Version: {version}
Release date: {release}
Authors: Lionel Brouyère, Olivier Evers
Contributor(s):
Herizo Ran, fabiencollet, c-morten, kalemas (Konstantin Maslyuk),
Markus Ng, Jerome Drese, Renaud Lessard Larouche
Features:
Animation picker widget.
Quick picker creation.
Advanced picker editing.
Read AnimSchoolPicker files (december 2021 version and latest)
Free and open source, today and forever.
This tool is a fork of Hotbox Designer (Lionel Brouyère).
A menus, markmenu and hotbox designer cross DCC.
https://github.com/luckylyk/hotbox_designer
""".format(
version=".".join(str(n) for n in VERSION),
release=RELEASE_DATE)
WINDOW_TITLE = "DreamWall - Picker"
WINDOW_CONTROL_NAME = "dwPickerWindow"
CLOSE_CALLBACK_COMMAND = "import dwpicker;dwpicker._dwpicker.close_event()"
CLOSE_TAB_WARNING = """\
Close the tab will remove completely the picker data from the scene.
Are you sure to continue ?"""
class DwPicker(DockableBase, QtWidgets.QWidget):
def __init__(
self,
replace_namespace_function=None,
list_namespaces_function=None):
super(DwPicker, self).__init__(control_name=WINDOW_CONTROL_NAME)
self.setWindowTitle(WINDOW_TITLE)
self.shortcuts = {}
self.replace_namespace_custom_function = replace_namespace_function
self.list_namespaces_function = list_namespaces_function
self.editable = True
self.callbacks = []
self.stored_focus = None
self.editors = []
self.pickers = []
self.preferences_window = PreferencesWindow(
callback=self.load_ui_states, parent=maya_main_window())
self.preferences_window.need_update_callbacks.connect(
self.reload_callbacks)
self.preferences_window.hotkey_changed.connect(self.register_shortcuts)
self.sub_panels_view = QtWidgets.QToolButton()
self.sub_panels_view.setCheckable(True)
self.sub_panels_view.setChecked(True)
self.sub_panels_view.setIcon(icon("panels.png"))
self.sub_panels_view.setFixedSize(17, 17)
self.sub_panels_view.released.connect(self.update_panels_display_mode)
self.sub_tabs_view = QtWidgets.QToolButton()
self.sub_tabs_view.setCheckable(True)
self.sub_tabs_view.setIcon(icon("tabs.png"))
self.sub_tabs_view.setFixedSize(17, 17)
self.sub_tabs_view.released.connect(self.update_panels_display_mode)
self.panel_buttons = QtWidgets.QButtonGroup()
self.panel_buttons.addButton(self.sub_panels_view, 0)
self.panel_buttons.addButton(self.sub_tabs_view, 1)
self.namespace_label = QtWidgets.QLabel("Namespace: ")
self.namespace_combo = QtWidgets.QComboBox()
self.namespace_combo.setMinimumWidth(200)
method = self.change_namespace_combo
self.namespace_combo.currentIndexChanged.connect(method)
self.namespace_refresh = QtWidgets.QPushButton("")
self.namespace_refresh.setIcon(icon("reload.png"))
self.namespace_refresh.setFixedSize(17, 17)
self.namespace_refresh.setIconSize(QtCore.QSize(15, 15))
self.namespace_refresh.released.connect(self.update_namespaces)
self.namespace_picker = QtWidgets.QPushButton("")
self.namespace_picker.setIcon(icon("picker.png"))
self.namespace_picker.setFixedSize(17, 17)
self.namespace_picker.setIconSize(QtCore.QSize(15, 15))
self.namespace_picker.released.connect(self.pick_namespace)
self.namespace_widget = QtWidgets.QWidget()
self.namespace_layout = QtWidgets.QHBoxLayout(self.namespace_widget)
self.namespace_layout.setContentsMargins(10, 2, 2, 2)
self.namespace_layout.setSpacing(0)
self.namespace_layout.addWidget(self.namespace_label)
self.namespace_layout.addSpacing(4)
self.namespace_layout.addWidget(self.namespace_combo)
self.namespace_layout.addSpacing(2)
self.namespace_layout.addWidget(self.namespace_refresh)
self.namespace_layout.addWidget(self.namespace_picker)
self.namespace_layout.addStretch(1)
self.namespace_layout.addWidget(self.sub_panels_view)
self.namespace_layout.addWidget(self.sub_tabs_view)
self.tab = QtWidgets.QTabWidget()
self.tab.setTabsClosable(True)
self.tab.setMovable(True)
self.tab.tabBar().tabMoved.connect(self.tab_moved)
self.tab.tabBar().tabBarDoubleClicked.connect(self.change_title)
self.tab.currentChanged.connect(self.tab_index_changed)
method = partial(self.close_tab, store=True)
self.tab.tabCloseRequested.connect(method)
self.quick_options = QuickOptions()
self.menubar = DwPickerMenu(parent=self)
self.menubar.new.triggered.connect(self.call_new)
self.menubar.open.triggered.connect(self.call_open)
self.menubar.save.triggered.connect(self.call_save)
self.menubar.save_as.triggered.connect(self.call_save_as)
self.menubar.exit.triggered.connect(self.close)
self.menubar.import_.triggered.connect(self.call_import)
self.menubar.undo.triggered.connect(self.call_undo)
self.menubar.redo.triggered.connect(self.call_redo)
self.menubar.advanced_edit.triggered.connect(self.call_edit)
self.menubar.preferences.triggered.connect(self.call_preferences)
self.menubar.change_title.triggered.connect(self.change_title)
self.menubar.toggle_display.triggered.connect(self.toggle_display_mode)
method = self.toggle_hierarchy_display
self.menubar.toggle_hierarchy_display.triggered.connect(method)
method = self.change_namespace_dialog
self.menubar.change_namespace.triggered.connect(method)
self.menubar.add_background.triggered.connect(self.add_background)
self.menubar.tools.triggered.connect(self.call_tools)
self.menubar.documentation.triggered.connect(self.call_documentation)
self.menubar.dw.triggered.connect(self.call_dreamwall)
self.menubar.about.triggered.connect(self.call_about)
self.layout = QtWidgets.QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0)
self.layout.setMenuBar(self.menubar)
self.layout.addWidget(self.namespace_widget)
self.layout.addWidget(self.tab)
self.layout.addWidget(self.quick_options)
self.load_ui_states()
self.register_shortcuts()
def register_shortcuts(self):
# Unregister all shortcuts before create new ones
function_names_actions = {
'focus': (self.reset, None),
'new': (self.call_new, self.menubar.new),
'open': (self.call_open, self.menubar.open),
'save': (self.call_save, self.menubar.save),
'close': (self.close, self.menubar.exit),
'undo': (self.call_undo, self.menubar.undo),
'redo': (self.call_redo, self.menubar.redo),
'edit': (self.call_edit, self.menubar.advanced_edit),
'next_tab': (self.call_next_tab, None),
'previous_tab': (self.call_previous_tab, None),
'toggle_display': (
self.toggle_display_mode, self.menubar.toggle_display),
'display_hierarchy': (
self.toggle_hierarchy_display,
self.menubar.toggle_hierarchy_display)
}
for function_name, sc in self.shortcuts.items():
sc.activated.disconnect(function_names_actions[function_name][0])
seq = QtGui.QKeySequence()
action = function_names_actions[function_name][1]
if not action:
continue
action.setShortcut(seq)
self.shortcuts = {}
shortcut_context = QtCore.Qt.WidgetWithChildrenShortcut
for function_name, data in get_hotkeys_config().items():
if not data['enabled']:
continue
method = function_names_actions[function_name][0]
ks = data['key_sequence']
if ks is None:
continue
sc = set_shortcut(ks, self, method, shortcut_context)
self.shortcuts[function_name] = sc
# HACK: Need to implement twice the shortcut to display key
# sequence in the menu and keep it active when the view is docked.
action = function_names_actions[function_name][1]
if action is None:
continue
action.setShortcut(ks)
action.setShortcutContext(shortcut_context)
def show(self, *args, **kwargs):
super(DwPicker, self).show(
closeCallback=CLOSE_CALLBACK_COMMAND, *args, **kwargs)
self.register_callbacks()
def close_event(self):
self.preferences_window.close()
def list_scene_namespaces(self):
if self.list_namespaces_function:
ns = self.list_namespaces_function()
else:
ns = cmds.namespaceInfo(listOnlyNamespaces=True, recurse=True)
ns = ns or []
namespaces = ns + pickers_namespaces(self.pickers)
return sorted(list(set(namespaces)))
def update_namespaces(self, *_):
self.namespace_combo.blockSignals(True)
self.namespace_combo.clear()
self.namespace_combo.addItem("*Root*")
namespaces = self.list_scene_namespaces()
self.namespace_combo.addItems(namespaces)
self.namespace_combo.blockSignals(False)
# Auto update namespace combo to namespace size.
if not cmds.optionVar(query=AUTO_RESIZE_NAMESPACE_COMBO):
self.namespace_combo.setSizePolicy(
QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.Minimum)
self.namespace_combo.setMinimumWidth(200)
return
max_width = 0
for i in range(self.namespace_combo.count()):
t = self.namespace_combo.itemText(i)
width = self.namespace_combo.fontMetrics().horizontalAdvance(t)
max_width = max(max_width, width)
width = max_width + 20 # padding
self.namespace_combo.setFixedWidth(max((200, width)))
def toggle_display_mode(self):
index = int(not bool(self.panel_buttons.checkedId()))
self.panel_buttons.button(index).setChecked(True)
self.update_panels_display_mode()
self.setFocus()
def toggle_hierarchy_display(self):
state = not bool(cmds.optionVar(query=DISPLAY_HIERARCHY_IN_PICKER))
save_optionvar(DISPLAY_HIERARCHY_IN_PICKER, int(state))
self.update()
self.setFocus()
def update_panels_display_mode(self, *_):
state = bool(self.panel_buttons.checkedId())
picker = self.tab.currentWidget()
if picker is None:
return
focused_panel = picker.reset()
picker.as_sub_tab = state
picker.create_pickers()
picker.create_panels(panel=focused_panel)
QtCore.QTimer.singleShot(10, partial(picker.reset, force_all=True))
picker.update()
def tab_index_changed(self, index):
if not self.pickers:
return
picker = self.pickers[index]
index = int(picker.as_sub_tab)
self.panel_buttons.button(index).setChecked(True)
if not picker:
return
namespace = detect_picker_namespace(picker.document.shapes)
self.namespace_combo.blockSignals(True)
if self.namespace_combo.findText(namespace) == -1 and namespace:
self.namespace_combo.addItem(namespace)
if namespace:
self.namespace_combo.setCurrentText(namespace)
else:
self.namespace_combo.setCurrentIndex(0)
self.namespace_combo.blockSignals(False)
def tab_moved(self, newindex, oldindex):
for l in (self.editors, self.pickers):
l.insert(newindex, l.pop(oldindex))
self.store_local_pickers_data()
def leaveEvent(self, _):
mode = cmds.optionVar(query=AUTO_FOCUS_BEHAVIOR)
if mode == 'off':
return
cmds.setFocus("MayaWindow")
def enterEvent(self, _):
mode = cmds.optionVar(query=AUTO_FOCUS_BEHAVIOR)
if mode == 'bilateral':
cmds.setFocus(self.objectName())
def dockCloseEventTriggered(self):
save_opened_filenames([
p.document.filename for p in self.pickers
if p.document.filename])
modified = [p.document.modified_state for p in self.pickers]
if not any(modified):
return super(DwPicker, self).dockCloseEventTriggered()
msg = (
'Some picker have unsaved modification. \n'
'Would you like to save them ?')
result = QtWidgets.QMessageBox.question(
None, 'Save ?', msg,
buttons=(
QtWidgets.QMessageBox.SaveAll |
QtWidgets.QMessageBox.Close),
defaultButton=QtWidgets.QMessageBox.SaveAll)
if result == QtWidgets.QMessageBox.Close:
return
for i in range(self.tab.count()-1, -1, -1):
self.save_tab(i)
save_opened_filenames([p.document.filename for p in self.pickers])
return super(DwPicker, self).dockCloseEventTriggered()
def reload_callbacks(self):
self.unregister_callbacks()
self.register_callbacks()
def register_callbacks(self):
self.unregister_callbacks()
callbacks = {
om.MSceneMessage.kBeforeNew: [
self.close_tabs, self.update_namespaces],
om.MSceneMessage.kAfterOpen: [
self.load_saved_pickers, self.update_namespaces],
om.MSceneMessage.kAfterCreateReference: [
self.load_saved_pickers, self.update_namespaces]}
if not cmds.optionVar(query=DISABLE_IMPORT_CALLBACKS):
callbacks[om.MSceneMessage.kAfterImport] = [
self.load_saved_pickers, self.update_namespaces]
for event, methods in callbacks.items():
for method in methods:
callback = om.MSceneMessage.addCallback(event, method)
self.callbacks.append(callback)
method = self.auto_switch_tab
cb = om.MEventMessage.addEventCallback('SelectionChanged', method)
self.callbacks.append(cb)
method = self.auto_switch_namespace
cb = om.MEventMessage.addEventCallback('SelectionChanged', method)
self.callbacks.append(cb)
for picker in self.pickers:
picker.register_callbacks()
def unregister_callbacks(self):
for cb in self.callbacks:
om.MMessage.removeCallback(cb)
self.callbacks.remove(cb)
for picker in self.pickers:
picker.unregister_callbacks()
def auto_switch_namespace(self, *_, **__):
if not cmds.optionVar(query=AUTO_SET_NAMESPACE):
return
self.pick_namespace()
def auto_switch_tab(self, *_, **__):
if not cmds.optionVar(query=AUTO_SWITCH_TAB):
return
nodes = cmds.ls(selection=True)
if not nodes:
return
picker = self.tab.currentWidget()
if not picker:
return
targets = list_targets(picker.document.shapes)
if nodes[-1] in targets:
return
for i, picker in enumerate(self.pickers):
if nodes[-1] in list_targets(picker.document.shapes):
self.tab.setCurrentIndex(i)
return
def load_saved_pickers(self, *_, **__):
self.clear()
pickers = load_local_picker_data()
if cmds.optionVar(query=CHECK_IMAGES_PATHS):
ensure_images_path_exists(pickers)
for picker in pickers:
self.add_picker(picker)
clean_stray_picker_holder_nodes()
def store_local_pickers_data(self):
if not self.editable:
return
if not self.tab.count():
store_local_picker_data([])
return
pickers = [self.document(i).data for i in range(self.tab.count())]
store_local_picker_data(pickers)
def save_tab(self, index):
msg = (
'Picker contain unsaved modification !\n'
'Woud you like to continue ?')
result = QtWidgets.QMessageBox.question(
None, 'Save ?', msg,
buttons=(
QtWidgets.QMessageBox.Save |
QtWidgets.QMessageBox.Yes |
QtWidgets.QMessageBox.Cancel),
defaultButton=QtWidgets.QMessageBox.Cancel)
if result == QtWidgets.QMessageBox.Cancel:
return False
elif result == QtWidgets.QMessageBox.Save and not self.call_save(index):
return False
return True
def close_tabs(self, *_):
for i in range(self.tab.count()-1, -1, -1):
self.close_tab(i)
self.store_local_pickers_data()
def clear(self):
for i in range(self.tab.count()-1, -1, -1):
self.close_tab(i, force=True)
def close_tab(self, index, force=False, store=False):
if self.document(index).modified_state and force is False:
if not self.save_tab(index):
return
elif (cmds.optionVar(query=WARN_ON_TAB_CLOSED) and
not question('Warning', CLOSE_TAB_WARNING)):
return
editor = self.editors.pop(index)
if editor:
editor.close()
picker = self.pickers.pop(index)
picker.unregister_callbacks()
picker.close()
self.tab.removeTab(index)
if store:
self.store_local_pickers_data()
def load_ui_states(self):
value = bool(cmds.optionVar(query=DISPLAY_QUICK_OPTIONS))
self.quick_options.setVisible(value)
value = bool(cmds.optionVar(query=NAMESPACE_TOOLBAR))
self.namespace_widget.setVisible(value)
self.update_namespaces()
self.update_modified_states()
def add_picker_from_file(self, filename):
with open(filename, "r") as f:
data = ensure_retro_compatibility(json.load(f))
ensure_images_path_exists([data])
self.add_picker(data, filename=filename)
append_recent_filename(filename)
def reset(self):
picker = self.tab.currentWidget()
if picker:
picker.reset()
def general_changed(self, origin, option):
if option == 'name' and origin != 'main_window':
self.update_names()
if option == 'panels.as_sub_tab' and origin != 'main_window':
index = int(self.document().data['general']['panels.as_sub_tab'])
self.panel_buttons.button(index).setChecked(True)
def update_names(self):
for i in range(self.tab.count()):
self.set_title(i, self.document(i).data['general']['name'])
def create_picker(self, data):
document = PickerDocument(data)
document.changed.connect(self.store_local_pickers_data)
document.general_option_changed.connect(self.general_changed)
document.data_changed.connect(self.update_names)
document.changed.connect(self.update_modified_states)
picker = PickerStackedView(document, self.editable)
picker.register_callbacks()
return picker
def add_picker(self, data, filename=None, modified_state=False):
picker = self.create_picker(data)
picker.document.filename = filename
picker.document.modified_state = modified_state
insert = cmds.optionVar(query=INSERT_TAB_AFTER_CURRENT)
if not insert or self.tab.currentIndex() == self.tab.count() - 1:
self.pickers.append(picker)
self.editors.append(None)
self.tab.addTab(picker, data['general']['name'])
self.tab.setCurrentIndex(self.tab.count() - 1)
else:
index = self.tab.currentIndex() + 1
self.pickers.insert(index, picker)
self.editors.insert(index, None)
self.tab.insertTab(index, picker, data['general']['name'])
self.tab.setCurrentIndex(index)
picker.reset(force_all=True)
def call_open(self):
filenames = QtWidgets.QFileDialog.getOpenFileNames(
None, "Open a picker...",
get_open_directory(),
filter="Dreamwall Picker (*.json)")[0]
if not filenames:
return
save_optionvar(LAST_OPEN_DIRECTORY, os.path.dirname(filenames[0]))
for filename in filenames:
self.add_picker_from_file(filename)
self.store_local_pickers_data()
def call_preferences(self):
self.preferences_window.show()
def call_save(self, index=None):
index = self.tab.currentIndex() if type(index) is not int else index
filename = self.document(index).filename
if not filename:
return self.call_save_as(index=index)
return self.save_picker(index, filename)
def call_save_as(self, index=None):
index = self.tab.currentIndex() if type(index) is not int else index
filename = QtWidgets.QFileDialog.getSaveFileName(
None, "Save a picker ...",
cmds.optionVar(query=LAST_SAVE_DIRECTORY),
filter="Dreamwall Picker (*.json)")[0]
if not filename:
return False
if os.path.exists(filename):
msg = '{} already, exists. Do you want to erase it ?'
if not question('File exist', msg.format(filename)):
return False
self.save_picker(index, filename)
def call_undo(self):
index = self.tab.currentIndex()
if index < 0:
return
self.document(index).undo()
self.document(index).changed.emit()
def call_redo(self):
index = self.tab.currentIndex()
if index < 0:
return
self.document(index).redo()
self.document(index).changed.emit()
def save_picker(self, index, filename):
self.document(index).filename = filename
save_optionvar(LAST_SAVE_DIRECTORY, os.path.dirname(filename))
append_recent_filename(filename)
with open(filename, 'w') as f:
json.dump(self.document(index).data, f, indent=2)
self.set_modified_state(index, False)
return True
def call_import(self):
sources = QtWidgets.QFileDialog.getOpenFileNames(
None, "Import a picker...",
get_import_directory(),
filter="Anim School Picker (*.pkr)")[0]
if not sources:
return
dst = QtWidgets.QFileDialog.getExistingDirectory(
None,
"Conversion destination",
os.path.dirname(sources[0]),
options=QtWidgets.QFileDialog.ShowDirsOnly)
if not dst:
return
save_optionvar(LAST_IMPORT_DIRECTORY, os.path.dirname(sources[0]))
for src in sources:
filename = animschool.convert(src, dst)
self.add_picker_from_file(filename)
def call_new(self):
self.add_picker({
'general': deepcopy(PICKER),
'shapes': []})
self.store_local_pickers_data()
def document(self, index=None):
index = self.tab.currentIndex() if type(index) is not int else index
if index < 0:
return None
picker = self.tab.widget(index)
return picker.document
def call_edit(self):
index = self.tab.currentIndex()
if index < 0:
QtWidgets.QMessageBox.warning(self, "Warning", "No picker set")
return
if self.editors[index] is None:
document = self.document()
editor = PickerEditor(
document,
parent=self)
self.editors[index] = editor
self.editors[index].show()
self.editors[index].shape_canvas.focus()
def call_next_tab(self):
index = self.tab.currentIndex() + 1
if index == self.tab.count():
index = 0
self.tab.setCurrentIndex(index)
def call_previous_tab(self):
index = self.tab.currentIndex() - 1
if index < 0:
index = self.tab.count() - 1
self.tab.setCurrentIndex(index)
def set_editable(self, state):
self.editable = state
self.menubar.set_editable(state)
for picker in self.pickers:
picker.editable = state
def update_modified_states(self):
for index, picker in enumerate(self.pickers):
state = picker.document.modified_state
use_icon = cmds.optionVar(query=USE_ICON_FOR_UNSAVED_TAB)
icon_ = icon('save.png') if state and use_icon else QtGui.QIcon()
self.tab.setTabIcon(index, icon_)
title = self.document(index).data['general']['name']
title = "*" + title if state and not use_icon else title
self.tab.setTabText(index, title)
def set_modified_state(self, index, state):
"""
Update the tab icon. Add a "save" icon if tab contains unsaved
modifications.
"""
if not self.document(index).filename:
return
self.document(index).modified_state = state
use_icon = cmds.optionVar(query=USE_ICON_FOR_UNSAVED_TAB)
icon_ = icon('save.png') if state and use_icon else QtGui.QIcon()
self.tab.setTabIcon(index, icon_)
title = self.document(index).data['general']['name']
title = "*" + title if state and not use_icon else title
self.tab.setTabText(index, title)
def call_tools(self):
webbrowser.open(DW_GITHUB)
def call_dreamwall(self):
webbrowser.open(DW_WEBSITE)
def call_documentation(self):
webbrowser.open(PICKER_DOCUMENTATION)
def call_about(self):
QtWidgets.QMessageBox.about(self, 'About', ABOUT)
def sizeHint(self):
return QtCore.QSize(500, 800)
def change_title(self, index=None):
if not self.editable:
return
index = (
self.tab.currentIndex() if not isinstance(index, int) else index)
if index < 0:
return
title, operate = QtWidgets.QInputDialog.getText(
None, 'Change picker title', 'New title',
text=self.document(index).data['general']['name'])
if not operate:
return
self.set_title(index, title)
index = (
self.tab.currentIndex() if not isinstance(index, int) else index)
if index < 0:
return
document = self.document(index)
document.data['general']['name'] = title
document.general_option_changed.emit('main_window', 'name')
self.document(index).record_undo()
self.set_title(index, title)
def set_title(self, index=None, title=''):
use_icon = cmds.optionVar(query=USE_ICON_FOR_UNSAVED_TAB)
if not use_icon and self.document(index).modified_state:
title = "*" + title
self.tab.setTabText(index, title)
def change_namespace_dialog(self):
dialog = NamespaceDialog()
if not dialog.exec_():
return
namespace = dialog.namespace
self.change_namespace(namespace)
def change_namespace_combo(self):
index = self.namespace_combo.currentIndex()
text = self.namespace_combo.currentText()
namespace = text if index else ":"
self.change_namespace(namespace)
def pick_namespace(self):
namespace = selected_namespace()
self.namespace_combo.setCurrentText(namespace)
def change_namespace(self, namespace):
document = self.document()
if not document:
return
switch_namespace_function = (
self.replace_namespace_custom_function or switch_namespace)
for shape in document.shapes:
if not shape.targets():
continue
targets = [
switch_namespace_function(t, namespace)
for t in shape.targets()]
shape.options['action.targets'] = targets
document.record_undo()
document.shapes_changed.emit()
def add_background(self):
filename = get_image_path(self)
if not filename:
return
filename = format_path(filename)
shape = deepcopy(BACKGROUND)
shape['image.path'] = filename
image = QtGui.QImage(filename)
shape['image.width'] = image.size().width()
shape['image.height'] = image.size().height()
shape['shape.width'] = image.size().width()
shape['shape.height'] = image.size().height()
shape['bgcolor.transparency'] = 255
self.document().add_shapes([shape], prepend=True)
self.document().record_undo()
self.document().shapes_changed.emit()
class DwPickerMenu(QtWidgets.QMenuBar):
def __init__(self, parent=None):
super(DwPickerMenu, self).__init__(parent)
self.new = QtWidgets.QAction('&New', parent)
self.open = QtWidgets.QAction('&Open', parent)
self.import_ = QtWidgets.QAction('&Import', parent)
self.save = QtWidgets.QAction('&Save', parent)
self.save_as = QtWidgets.QAction('&Save as', parent)
self.exit = QtWidgets.QAction('Exit', parent)
self.undo = QtWidgets.QAction('Undo', parent)
self.redo = QtWidgets.QAction('Redo', parent)
self.advanced_edit = QtWidgets.QAction('Advanced &editing', parent)
self.preferences = QtWidgets.QAction('Preferences', parent)
text = 'Toggle panel display mode'
self.toggle_display = QtWidgets.QAction(text, parent)
text = 'Toggle hierarchy display'
self.toggle_hierarchy_display = QtWidgets.QAction(text, parent)
self.change_title = QtWidgets.QAction('Change picker title', parent)
self.change_namespace = QtWidgets.QAction('Change namespace', parent)
self.add_background = QtWidgets.QAction('Add background item', parent)
self.documentation = QtWidgets.QAction('Documentation', parent)
self.tools = QtWidgets.QAction('Other DreamWall &tools', parent)
self.dw = QtWidgets.QAction('&About DreamWall', parent)
self.about = QtWidgets.QAction('&About DwPicker', parent)
self.file = QtWidgets.QMenu('&File', parent)
self.file.addAction(self.new)
self.file.addAction(self.open)
self.file.addAction(self.import_)
self.file.addSeparator()
self.file.addAction(self.save)
self.file.addAction(self.save_as)
self.file.addSeparator()
self.file.addAction(self.exit)
self.edit = QtWidgets.QMenu('&Edit', parent)
self.edit.addAction(self.undo)
self.edit.addAction(self.redo)
self.edit.addSeparator()
self.edit.addAction(self.advanced_edit)
self.edit.addAction(self.preferences)
self.edit.addSeparator()
self.edit.addAction(self.toggle_display)
self.edit.addAction(self.toggle_hierarchy_display)
self.edit.addSeparator()
self.edit.addAction(self.change_title)
self.edit.addSeparator()
self.edit.addAction(self.change_namespace)
self.edit.addAction(self.add_background)
self.help = QtWidgets.QMenu('&Help', parent)
self.help.addAction(self.documentation)
self.help.addSeparator()
self.help.addAction(self.tools)
self.help.addAction(self.dw)
self.help.addSeparator()
self.help.addAction(self.about)
self.addMenu(self.file)
self.addMenu(self.edit)
self.addMenu(self.help)
def set_editable(self, state):
self.undo.setEnabled(state)
self.redo.setEnabled(state)
self.change_title.setEnabled(state)
self.advanced_edit.setEnabled(state)
self.add_background.setEnabled(state)

View File

@@ -0,0 +1,71 @@
from contextlib import contextmanager
from maya import cmds
def detect_picker_namespace(shapes):
targets = {target for shape in shapes for target in shape.targets()}
namespaces = {ns for ns in [node_namespace(t) for t in targets] if ns}
if len(namespaces) != 1:
return None
return list(namespaces)[0]
def pickers_namespaces(pickers):
targets = {
t for p in pickers for s in p.document.shapes for t in s.targets()}
namespaces = {ns for ns in [node_namespace(t) for t in targets] if ns}
return sorted(list(namespaces))
def node_namespace(node):
basename = node.split("|")[-1]
if ":" not in node:
return None
return basename.split(":")[0]
def node_full_namespace(node):
basename = node.split('|')[-1]
return (basename.rsplit(':', 1)[:-1] or [None])[-1]
@contextmanager
def maya_namespace(
namespace='', create_if_missing=True, restore_current_namespace=True):
"""Context manager to temporarily set a namespace"""
initial_namespace = ':' + cmds.namespaceInfo(currentNamespace=True)
if not namespace.startswith(':'):
namespace = ':' + namespace
try:
if not cmds.namespace(absoluteName=True, exists=namespace):
if create_if_missing:
cmds.namespace(setNamespace=':')
namespace = cmds.namespace(addNamespace=namespace)
else:
cmds.namespace(initial_namespace)
raise ValueError(namespace + " doesn't exist.")
cmds.namespace(setNamespace=namespace)
yield namespace
finally:
if restore_current_namespace:
cmds.namespace(setNamespace=initial_namespace)
def switch_namespace(name, namespace):
basename = name.split("|")[-1]
name = basename if ":" not in basename else basename.split(":")[-1]
if not namespace:
return name
return namespace + ":" + name
def selected_namespace():
selection = cmds.ls(selection=True)
if not selection:
return ":"
node = selection[0]
basename = node.split("|")[-1]
if ":" not in node:
return None
return basename.split(":")[0]

View File

@@ -0,0 +1,176 @@
import os
import sys
from maya import cmds
AUTO_FOCUS_BEHAVIORS = ['off', 'bilateral', 'pickertomaya']
ZOOM_BUTTONS = ["left", "middle", "right"]
AUTO_FOCUS_BEHAVIOR = 'dwpicker_auto_focus_behavior'
AUTO_COLLAPSE_IMG_PATH_FROM_ENV = 'dwpicker_auto_collapse_image_path_from_env'
AUTO_SET_NAMESPACE = 'dwpicker_auto_set_namespace'
AUTO_RESIZE_NAMESPACE_COMBO = 'dwpicker_auto_resize_namespace_combo'
AUTO_SWITCH_TAB = 'dwpicker_auto_switch_tab'
BG_LOCKED = 'dwpicker_designer_background_items_locked'
CHECK_IMAGES_PATHS = 'dwpicker_check_images_paths'
CHECK_FOR_UPDATE = 'dwpicker_check_for_update'
CUSTOM_PROD_PICKER_DIRECTORY = 'dwpicker_custom_prod_picker_directory'
DEFAULT_BG_COLOR = 'dwpicker_default_background_color'
DEFAULT_HOTKEYS = 'dwpicker_default_hotkeys'
DEFAULT_LABEL = 'dwpicker_default_label_color'
DEFAULT_HEIGHT = 'dwpicker_default_height'
DEFAULT_TEXT_COLOR = 'dwpicker_default_text_color'
DEFAULT_WIDTH = 'dwpicker_default_width'
DISABLE_IMPORT_CALLBACKS = 'dwpicker_disable_import_callbacks'
DISPLAY_QUICK_OPTIONS = 'dwpicker_display_quick_options'
DISPLAY_HIERARCHY_IN_CANVAS = 'dwpicker_display_hierarchy_in_canvas'
DISPLAY_HIERARCHY_IN_PICKER = 'dwpicker_display_hierarchy_in_picker'
OVERRIDE_PROD_PICKER_DIRECTORY_ENV = 'dwpicker_override_picker_directory_env'
INSERT_TAB_AFTER_CURRENT = 'dwpicker_insert_tab_after_current'
ISOLATE_CURRENT_PANEL_SHAPES = 'dwpicker_isolate_current_panel_shapes'
LAST_COMMAND_LANGUAGE = 'dwpicker_last_command_language_used'
LAST_IMAGE_DIRECTORY_USED = 'dwpicker_last_directory_used'
LAST_IMPORT_DIRECTORY = 'dwpicker_last_file_import_directory'
LAST_OPEN_DIRECTORY = 'dwpicker_last_file_open_directory'
LAST_SAVE_DIRECTORY = 'dwpicker_last_file_save_directory'
OPENED_FILES = 'dwpicker_opened_files'
NAMESPACE_TOOLBAR = 'dwpicker_display_dwtoolbar'
RECENT_FILES = 'dwpicker_recent_files'
SEARCH_FIELD_INDEX = 'dwpicker_designer_search_field_index'
SETTINGS_GROUP_TO_COPY = 'dwpicker_settings_group_to_copy'
SETTINGS_TO_COPY = 'dwpicker_settings_to_copy'
SHAPES_FILTER_INDEX = 'dwpicker_designer_shape_filter_index'
SHAPE_PATH_ROTATION_STEP_ANGLE = 'dwpicker_shape_path_rotation_step_angle'
SNAP_ITEMS = 'dwpicker_designer_snap_items'
SNAP_GRID_X = 'dwpicker_designer_snap_x'
SNAP_GRID_Y = 'dwpicker_designer_snap_y'
SYNCHRONYZE_SELECTION = 'dwpicker_synchronize_selection'
TRIGGER_REPLACE_ON_MIRROR = 'dwpicker_trigger_search_and_replace_on_mirror'
USE_BASE64_DATA_ENCODING = 'dwpicker_use_base64_data_encoding'
USE_ICON_FOR_UNSAVED_TAB = 'dwpicker_use_icon_for_unsaved_tab'
USE_PROD_PICKER_DIR_AS_DEFAULT = 'dwpicker_user_prod_picker_dir_for_import'
ZOOM_BUTTON = 'dwpicker_picker_zoom_mouse_button'
WARN_ON_TAB_CLOSED = 'dwpicker_warn_on_tab_closed'
ZOOM_SENSITIVITY = 'dwpicker_zoom_sensitivity'
try:
check_for_update = int(cmds.about(majorVersion=True) != '2023')
# cmds.about command for Maya prio 2022 does not have majorVersion argument.
except TypeError:
check_for_update = 0
OPTIONVARS = {
AUTO_FOCUS_BEHAVIOR: AUTO_FOCUS_BEHAVIORS[-1],
AUTO_SWITCH_TAB: 0,
AUTO_RESIZE_NAMESPACE_COMBO: 0,
AUTO_SET_NAMESPACE: 0,
AUTO_COLLAPSE_IMG_PATH_FROM_ENV: 1,
BG_LOCKED: 1,
CHECK_IMAGES_PATHS: 1,
# We disable this default feature for maya 2023. It seems that the github
# request can cause a maya crash due to an incompatibility with the python
# with this specific version of Maya.
CHECK_FOR_UPDATE: check_for_update,
CUSTOM_PROD_PICKER_DIRECTORY: '',
DEFAULT_BG_COLOR: '#777777',
DEFAULT_HEIGHT: 20,
DEFAULT_LABEL: '',
DEFAULT_TEXT_COLOR: '#000000',
DEFAULT_HOTKEYS: (
'focus=F,1;new=CTRL+N,1;open=CTRL+O,1;save=CTRL+S,1;close=CTRL+Q,1;'
'undo=CTRL+Z,1;redo=CTRL+Y,1;edit=CTRL+E,1;next_tab=None,0;'
'previous_tab=None,0;toggle_display=T,1;display_hierarchy=Y,1'),
DISPLAY_HIERARCHY_IN_CANVAS: 1,
DEFAULT_WIDTH: 30,
DISABLE_IMPORT_CALLBACKS: 1,
DISPLAY_HIERARCHY_IN_PICKER: 1,
DISPLAY_QUICK_OPTIONS: 1,
OVERRIDE_PROD_PICKER_DIRECTORY_ENV: 0,
INSERT_TAB_AFTER_CURRENT: 0,
ISOLATE_CURRENT_PANEL_SHAPES: 0,
LAST_OPEN_DIRECTORY: os.path.expanduser("~"),
LAST_SAVE_DIRECTORY: os.path.expanduser("~"),
LAST_IMPORT_DIRECTORY: os.path.expanduser("~"),
LAST_COMMAND_LANGUAGE: 0, # 0 = python, 1 = mel
LAST_IMAGE_DIRECTORY_USED: os.path.expanduser("~"),
NAMESPACE_TOOLBAR: 0,
OPENED_FILES: '',
RECENT_FILES: '',
SEARCH_FIELD_INDEX: 0,
SHAPES_FILTER_INDEX: 0,
SHAPE_PATH_ROTATION_STEP_ANGLE: 15,
SETTINGS_GROUP_TO_COPY: 'bordercolor;text;image;bgcolor;shape;borderwidth;border',
SETTINGS_TO_COPY: (
'bgcolor.clicked;bgcolor.hovered;bgcolor.normal;bgcolor.transparency;'
'border;bordercolor.clicked;bordercolor.hovered;bordercolor.normal;'
'bordercolor.transparency;borderwidth.clicked;borderwidth.hovered;'
'borderwidth.normal;image.fit;image.height;image.width;shape;'
'shape.cornersx;shape.cornersy;shape.height;shape.left;'
'shape.top;shape.width;text.bold;text.color;text.halign;text.italic;'
'text.size;text.valign'),
SNAP_ITEMS: 0,
SNAP_GRID_X: 10,
SNAP_GRID_Y: 10,
SYNCHRONYZE_SELECTION: 1,
TRIGGER_REPLACE_ON_MIRROR: 0,
USE_BASE64_DATA_ENCODING: 0,
USE_ICON_FOR_UNSAVED_TAB: 1,
USE_PROD_PICKER_DIR_AS_DEFAULT: 0,
WARN_ON_TAB_CLOSED: 0,
ZOOM_BUTTON: ZOOM_BUTTONS[2],
ZOOM_SENSITIVITY: 50
}
TYPES = {
int: 'intValue',
float: 'floatValue',
str: 'stringValue'}
# Ensure backward compatibility.
if sys.version_info[0] == 2:
TYPES[unicode] = 'stringValue'
def ensure_optionvars_exists():
for optionvar, default_value in OPTIONVARS.items():
if cmds.optionVar(exists=optionvar):
continue
save_optionvar(optionvar, default_value)
def save_optionvar(optionvar, value):
kwargs = {TYPES.get(type(value)): [optionvar, value]}
cmds.optionVar(**kwargs)
def save_opened_filenames(filenames):
save_optionvar(OPENED_FILES, ";".join(filenames))
def append_recent_filename(filename):
filename = os.path.normpath(filename)
stored_filenames = cmds.optionVar(query=RECENT_FILES)
if not stored_filenames:
cmds.optionVar(stringValue=[RECENT_FILES, filename + ';'])
return
# Just reorder list if the filename is already in the recent filenames.
stored_filenames = stored_filenames.split(';')
for stored_filename in stored_filenames:
if os.path.normpath(stored_filename) == filename:
stored_filenames.remove(stored_filename)
stored_filenames.insert(0, filename)
cmds.optionVar(
stringValue=[RECENT_FILES, ';'.join(stored_filenames)])
return
# Append to list if new filename.
if len(stored_filenames) >= 10:
stored_filenames = stored_filenames[:9]
stored_filenames.insert(0, filename)
cmds.optionVar(stringValue=[RECENT_FILES, ';'.join(stored_filenames)])

View File

@@ -0,0 +1,347 @@
from .pyside import QtCore, QtGui
from maya import cmds
from .optionvar import ZOOM_SENSITIVITY
from .qtutils import VALIGNS, HALIGNS
from .geometry import grow_rect, get_connection_path
from .shape import to_shape_space_rect, to_shape_space
from .viewport import ViewportMapper
SELECTION_COLOR = '#3388FF'
PANEL_COLOR = '#00FFFF'
FOCUS_COLOR = '#FFFFFF'
MANIPULATOR_BORDER = 5
CONNECTION_COLOR = '#666666'
def factor_sensitivity(factor):
sensitivity = cmds.optionVar(query=ZOOM_SENSITIVITY) / 50.0
return factor * sensitivity
def draw_world_coordinates(painter, rect, color, viewportmapper):
center = viewportmapper.to_viewport_coords(QtCore.QPoint(0, 0))
top_center = QtCore.QPointF(center.x(), rect.top())
bottom_center = QtCore.QPointF(center.x(), rect.bottom())
left_center = QtCore.QPointF(rect.left(), center.y())
right_center = QtCore.QPointF(rect.right(), center.y())
color.setAlpha(100)
pen = QtGui.QPen(color)
pen.setWidthF(2)
painter.setPen(pen)
painter.drawLine(top_center, bottom_center)
painter.drawLine(left_center, right_center)
def draw_parenting_shapes(
painter, child, potential_parent, cursor, viewportmapper):
draw_shape_as_child_background(
painter, child, 'yellow',
alpha=150, padding=3, pen_width=5,
viewportmapper=viewportmapper)
if potential_parent:
draw_shape_as_child_background(
painter, potential_parent, 'white', alpha=255, padding=3,
pen_width=5,
viewportmapper=viewportmapper)
start_point = potential_parent.bounding_rect().center()
end_point = child.bounding_rect().center()
path = get_connection_path(start_point, end_point, viewportmapper)
draw_connections(painter, path, 'white')
return
end_point = child.bounding_rect().center()
start_point = viewportmapper.to_units_coords(cursor)
path = get_connection_path(
start_point, end_point, viewportmapper=viewportmapper)
pen = QtGui.QPen('yellow')
pen.setWidthF(2)
pen.setJoinStyle(QtCore.Qt.MiterJoin)
painter.setPen(pen)
painter.setBrush(QtGui.QColor(CONNECTION_COLOR))
painter.drawPath(path)
def draw_connections(painter, path, color=None):
pen = QtGui.QPen(color or CONNECTION_COLOR)
pen.setWidthF(1.5)
pen.setJoinStyle(QtCore.Qt.MiterJoin)
painter.setPen(pen)
painter.setBrush(QtGui.QColor(CONNECTION_COLOR))
painter.drawPath(path)
def draw_editor_canvas(painter, rect, snap=None, viewportmapper=None):
viewportmapper = viewportmapper or ViewportMapper()
color = QtGui.QColor('#333333')
pen = QtGui.QPen(color)
pen.setWidthF(2)
brush = QtGui.QBrush(QtGui.QColor(255, 255, 255, 25))
painter.setPen(pen)
painter.setBrush(brush)
painter.drawRect(rect)
draw_world_coordinates(painter, rect, color, viewportmapper)
center = viewportmapper.to_viewport_coords(QtCore.QPoint(0, 0))
text = QtGui.QStaticText('bottom_right')
x = center.x() - text.size().width() - 4
y = center.y() - text.size().height() - 4
point = QtCore.QPointF(x, y)
painter.drawStaticText(point, text)
text = QtGui.QStaticText('bottom_left')
y = center.y() - text.size().height() - 4
point = QtCore.QPointF(center.x() + 4, y)
painter.drawStaticText(point, text)
text = QtGui.QStaticText('top_right')
x = center.x() - text.size().width() - 4
point = QtCore.QPointF(x, center.y() + 4)
painter.drawStaticText(point, text)
text = QtGui.QStaticText('top_left')
point = QtCore.QPointF(center.x() + 4, center.y() + 4)
painter.drawStaticText(point, text)
if snap is None:
return
if viewportmapper.zoom < 0.5:
snap = snap[0] * 2, snap[1] * 2
pen = QtGui.QPen(QtGui.QColor('red'))
pen.setWidth(
1 if viewportmapper.zoom < 1 else 2 if
viewportmapper.zoom < 3 else 3)
painter.setPen(pen)
rect = viewportmapper.to_units_rect(rect)
x_start = ((rect.left() // snap[0]) * snap[0])
if x_start < rect.left():
x_start += snap[0]
y_start = ((rect.top() // snap[1]) * snap[1])
if y_start < rect.top():
y_start += snap[1]
x = x_start
while x <= rect.right():
if x >= rect.left():
y = y_start
while y <= rect.bottom():
if y >= rect.top():
point = QtCore.QPoint(*(x, y))
painter.drawPoint(viewportmapper.to_viewport_coords(point))
y += snap[1]
x += snap[0]
def draw_shape_as_child_background(
painter, shape, color=None, padding=5, pen_width=1.5, alpha=30,
viewportmapper=None):
rect = viewportmapper.to_viewport_rect(shape.bounding_rect())
rect = grow_rect(rect, padding)
color = QtGui.QColor(color or 'yellow')
color.setAlpha(alpha)
pen = QtGui.QPen(color)
pen.setWidthF(pen_width)
pen.setStyle(QtCore.Qt.DashLine)
painter.setPen(pen)
brush = QtGui.QBrush(color)
brush.setStyle(QtCore.Qt.BDiagPattern)
painter.setBrush(brush)
painter.drawRect(rect)
def draw_shape(
painter, shape, force_world_space=True,
draw_selected_state=True, viewportmapper=None):
viewportmapper = viewportmapper or ViewportMapper()
options = shape.options
content_rect = shape.content_rect()
if shape.clicked or (shape.selected and draw_selected_state):
bordercolor = QtGui.QColor(options['bordercolor.clicked'])
backgroundcolor = QtGui.QColor(options['bgcolor.clicked'])
bordersize = options['borderwidth.clicked']
elif shape.hovered:
bordercolor = QtGui.QColor(options['bordercolor.hovered'])
backgroundcolor = QtGui.QColor(options['bgcolor.hovered'])
bordersize = options['borderwidth.hovered']
else:
bordercolor = QtGui.QColor(options['bordercolor.normal'])
backgroundcolor = QtGui.QColor(options['bgcolor.normal'])
bordersize = options['borderwidth.normal']
textcolor = QtGui.QColor(options['text.color'])
alpha = options['bordercolor.transparency'] if options['border'] else 255
bordercolor.setAlpha(255 - alpha)
backgroundcolor.setAlpha(255 - options['bgcolor.transparency'])
pen = QtGui.QPen(bordercolor)
pen.setStyle(QtCore.Qt.SolidLine)
w = to_shape_space(bordersize, shape, force_world_space, viewportmapper)
pen.setWidthF(w)
painter.setPen(pen)
painter.setBrush(QtGui.QBrush(backgroundcolor))
rect = to_shape_space_rect(
shape.rect, shape, force_world_space, viewportmapper)
r = draw_shape_shape(
painter, rect, shape, force_world_space, viewportmapper)
painter.setPen(QtGui.QPen(textcolor))
painter.setBrush(QtGui.QBrush(textcolor))
option = QtGui.QTextOption()
flags = VALIGNS[options['text.valign']] | HALIGNS[options['text.halign']]
option.setAlignment(flags)
font = QtGui.QFont()
font.setBold(options['text.bold'])
font.setItalic(options['text.italic'])
size = to_shape_space(
options['text.size'], shape, force_world_space, viewportmapper)
font.setPixelSize(round(size))
painter.setFont(font)
text = options['text.content']
content_rect = to_shape_space_rect(
content_rect, shape, force_world_space, viewportmapper)
painter.drawText(content_rect, flags, text)
return r
def draw_shape_shape(painter, rect, shape, force_world_space, viewportmapper):
options = shape.options
content_rect = shape.content_rect()
qpath = QtGui.QPainterPath()
if options['shape'] == 'square':
painter.drawRect(rect)
qpath.addRect(rect)
elif options['shape'] == 'round':
painter.drawEllipse(rect)
qpath.addEllipse(rect)
elif options['shape'] == 'rounded_rect':
x = to_shape_space(
options['shape.cornersx'], shape, force_world_space,
viewportmapper)
y = to_shape_space(
options['shape.cornersy'], shape, force_world_space,
viewportmapper)
painter.drawRoundedRect(rect, x, y)
qpath.addRoundedRect(rect, x, y)
else:
qpath = shape.get_painter_path(force_world_space, viewportmapper)
painter.drawPath(qpath)
qpath = qpath
if shape.pixmap is not None:
painter.setClipPath(qpath)
transformed_rect = shape.image_rect or content_rect
transformed_rect = to_shape_space_rect(
transformed_rect, shape, force_world_space, viewportmapper)
painter.drawPixmap(transformed_rect.toRect(), shape.pixmap)
painter.setClipping(False)
return qpath
def draw_selection_square(painter, rect, viewportmapper=None):
viewportmapper = viewportmapper or ViewportMapper()
rect = viewportmapper.to_viewport_rect(rect)
bordercolor = QtGui.QColor(SELECTION_COLOR)
backgroundcolor = QtGui.QColor(SELECTION_COLOR)
backgroundcolor.setAlpha(85)
painter.setPen(QtGui.QPen(bordercolor))
painter.setBrush(QtGui.QBrush(backgroundcolor))
painter.drawRect(rect)
def draw_picker_focus(painter, rect):
color = QtGui.QColor(FOCUS_COLOR)
color.setAlpha(10)
pen = QtGui.QPen(color)
pen.setWidthF(4)
painter.setPen(pen)
painter.setBrush(QtCore.Qt.NoBrush)
painter.drawRect(rect)
painter.setBrush(QtGui.QBrush())
def draw_current_panel(painter, rect, viewportmapper=None):
viewportmapper = viewportmapper or ViewportMapper()
rect = viewportmapper.to_viewport_rect(rect)
color = QtGui.QColor(PANEL_COLOR)
color.setAlpha(30)
pen = QtGui.QPen(color)
pen.setWidthF(1.5)
pen.setStyle(QtCore.Qt.DashLine)
painter.setPen(pen)
brush = QtGui.QBrush(color)
brush.setStyle(QtCore.Qt.BDiagPattern)
painter.setBrush(brush)
painter.drawRect(rect)
def draw_manipulator(painter, manipulator, cursor, viewportmapper=None):
viewportmapper = viewportmapper or ViewportMapper()
cursor = viewportmapper.to_units_coords(cursor).toPoint()
hovered = manipulator.hovered_rects(cursor)
if manipulator.rect in hovered:
pen = QtGui.QPen(QtGui.QColor(0, 0, 0, 0))
brush = QtGui.QBrush(QtGui.QColor(125, 125, 125))
brush.setStyle(QtCore.Qt.FDiagPattern)
painter.setPen(pen)
painter.setBrush(brush)
rect = viewportmapper.to_viewport_rect(manipulator.rect)
painter.drawPath(get_hovered_path(rect))
pen = QtGui.QPen(QtGui.QColor('black'))
brush = QtGui.QBrush(QtGui.QColor('white'))
painter.setBrush(brush)
for rect in manipulator.viewport_handlers():
pen.setWidth(3 if rect in hovered else 1)
painter.setPen(pen)
painter.drawEllipse(rect)
pen.setWidth(1)
pen.setStyle(QtCore.Qt.DashLine) # if not moving else QtCore.Qt.SolidLine)
painter.setPen(pen)
painter.setBrush(QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)))
rect = viewportmapper.to_viewport_rect(manipulator.rect)
painter.drawRect(rect)
def get_hovered_path(rect, viewportmapper=None):
viewportmapper = viewportmapper or ViewportMapper()
rect = viewportmapper.to_viewport_rect(rect)
manipulator_rect = grow_rect(
rect, viewportmapper.to_viewport(MANIPULATOR_BORDER))
path = QtGui.QPainterPath()
path.addRect(rect)
path.addRect(manipulator_rect)
return path
def draw_tangents(painter, path, viewportmapper):
rect = QtCore.QRectF(0, 0, 6, 6)
painter.setBrush(QtCore.Qt.yellow)
painter.setPen(QtCore.Qt.yellow)
for point in path:
center = QtCore.QPointF(*point['point'])
center = viewportmapper.to_viewport_coords(center)
if point['tangent_in'] is not None:
tangent_in = QtCore.QPointF(*point['tangent_in'])
tangent_in = viewportmapper.to_viewport_coords(tangent_in)
rect.moveCenter(tangent_in)
painter.drawRect(rect)
painter.drawLine(tangent_in, center)
if point['tangent_out'] is not None:
tangent_out = QtCore.QPointF(*point['tangent_out'])
tangent_out = viewportmapper.to_viewport_coords(tangent_out)
rect.moveCenter(tangent_out)
painter.drawRect(rect)
painter.drawLine(tangent_out, center)

View File

@@ -0,0 +1,80 @@
import os
from maya import cmds
from .optionvar import (
AUTO_COLLAPSE_IMG_PATH_FROM_ENV, CUSTOM_PROD_PICKER_DIRECTORY,
LAST_IMPORT_DIRECTORY, LAST_IMAGE_DIRECTORY_USED, LAST_OPEN_DIRECTORY,
OVERRIDE_PROD_PICKER_DIRECTORY_ENV, USE_PROD_PICKER_DIR_AS_DEFAULT)
def unix_path(path, isroot=False):
path = path.replace('\\', '/')
condition = (
os.name == 'nt' and
isroot and
path.startswith('/') and
not path.startswith('//'))
if condition:
path = '/' + path
path = path.rstrip(r'\/')
return path
def format_path(path):
if path is None:
return
path = unix_path(path)
if not cmds.optionVar(query=AUTO_COLLAPSE_IMG_PATH_FROM_ENV):
return path
root = get_picker_project_directory()
if not root or not path.lower().startswith(root.lower()):
return path
return '$DWPICKER_PROJECT_DIRECTORY/{}'.format(
path[len(root):].lstrip('/'))
def get_picker_project_directory():
if cmds.optionVar(query=OVERRIDE_PROD_PICKER_DIRECTORY_ENV):
path = cmds.optionVar(query=CUSTOM_PROD_PICKER_DIRECTORY)
return unix_path(path) if path else None
path = os.getenv('DWPICKER_PROJECT_DIRECTORY')
return unix_path(path) if path else None
def expand_path(path):
backup = None
if cmds.optionVar(query=OVERRIDE_PROD_PICKER_DIRECTORY_ENV):
root = unix_path(cmds.optionVar(query=CUSTOM_PROD_PICKER_DIRECTORY))
backup = os.getenv('DWPICKER_PROJECT_DIRECTORY')
os.environ['DWPICKER_PROJECT_DIRECTORY'] = root
result = os.path.expandvars(path)
if backup:
os.environ['DWPICKER_PROJECT_DIRECTORY'] = backup
return result
def get_open_directory():
if cmds.optionVar(query=USE_PROD_PICKER_DIR_AS_DEFAULT):
directory = get_picker_project_directory()
if directory:
return directory
return cmds.optionVar(query=LAST_OPEN_DIRECTORY)
def get_import_directory():
if cmds.optionVar(query=USE_PROD_PICKER_DIR_AS_DEFAULT):
directory = get_picker_project_directory()
if directory:
return directory
return cmds.optionVar(query=LAST_IMPORT_DIRECTORY)
def get_image_directory():
if cmds.optionVar(query=USE_PROD_PICKER_DIR_AS_DEFAULT):
directory = get_picker_project_directory()
if directory:
return directory
return cmds.optionVar(query=LAST_IMAGE_DIRECTORY_USED)

View File

@@ -0,0 +1,810 @@
from copy import deepcopy
from functools import partial
from maya import cmds
import maya.OpenMaya as om
from .pyside import QtWidgets, QtGui, QtCore
from .align import align_shapes_on_line
from .compatibility import ensure_general_options_sanity
from .document import PickerDocument
from .dialog import warning, CommandEditorDialog
from .interactive import SelectionSquare
from .interactionmanager import InteractionManager
from .geometry import get_combined_rects, get_connection_path
from .languages import execute_code
from .optionvar import (
save_optionvar, DEFAULT_BG_COLOR, DEFAULT_TEXT_COLOR, DEFAULT_WIDTH,
DEFAULT_HEIGHT, DEFAULT_LABEL, DISPLAY_HIERARCHY_IN_PICKER,
LAST_COMMAND_LANGUAGE, SYNCHRONYZE_SELECTION, ZOOM_SENSITIVITY)
from .painting import (
draw_shape, draw_selection_square, draw_picker_focus, draw_connections)
from .qtutils import get_cursor, clear_layout
from .shape import (
build_multiple_shapes, cursor_in_shape, rect_intersects_shape)
from .stack import create_stack_splitters, count_panels
from .selection import (
select_targets, select_shapes_from_selection, get_selection_mode,
NameclashError)
from .templates import BUTTON, COMMAND
from .viewport import ViewportMapper
SPLITTER_STYLE = """\
QSplitter::handle {
background-color: rgba(0, 0, 0, 50);
border: 1px solid #444;
width: 2px;
height: 2px;
}
"""
def set_shapes_hovered(
shapes,
world_cursor,
viewport_cursor,
selection_rect,
viewport_selection_rect,
viewportmapper=None):
"""
It set hovered the shape if his rect contains the cursor.
"""
if not shapes:
return
world_cursor = world_cursor.toPoint()
shapes = [s for s in shapes if not s.is_background()]
selection_shapes_intersect_selection = [
s for s in shapes
if cursor_in_shape(s, world_cursor, viewport_cursor, False, viewportmapper)
or rect_intersects_shape(
shape=s,
unit_rect=selection_rect,
viewport_rect=viewport_selection_rect,
force_world_space=False,
viewportmapper=viewportmapper)]
targets = list_targets(selection_shapes_intersect_selection)
for s in shapes:
if s.targets():
# Set all buttons hovered from his targets contents.
# I the physically hovered buttons contains targets, this will
# highlight all buttons containing similare targets.
state = next((False for t in s.targets() if t not in targets), True)
elif not s.is_background():
# Simple highlighting method for the interactive buttons.
state = s in selection_shapes_intersect_selection
else:
state = False
s.hovered = state
def detect_hovered_shape(shapes, world_cursor, screen_cursor, viewportmapper):
if not shapes:
return
for shape in reversed(shapes):
hovered = cursor_in_shape(
shape=shape,
world_cursor=world_cursor,
viewpoert_cursor=screen_cursor,
force_world_space=False,
viewportmapper=viewportmapper)
if hovered and not shape.is_background():
return shape
def list_targets(shapes):
return {t for s in shapes for t in s.targets()}
class PickerStackedView(QtWidgets.QWidget):
def __init__(self, document=None, editable=True, parent=None):
super(PickerStackedView, self).__init__(parent)
self.document = document or PickerDocument.create()
mtd = self.general_option_changed
self.document.general_option_changed.connect(mtd)
self.document.data_changed.connect(self.full_refresh)
self.editable = editable
self.pickers = []
self.widget = None
self.last_selected_tab = None
self.layers_menu = VisibilityLayersMenu(document)
self.layers_menu.visibilities_changed.connect(self.update)
self.as_sub_tab = document.data['general']['panels.as_sub_tab']
self.layout = QtWidgets.QHBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.setStyleSheet(SPLITTER_STYLE)
self.create_pickers()
self.create_panels()
def register_callbacks(self):
for picker in self.pickers:
picker.register_callbacks()
def unregister_callbacks(self):
for picker in self.pickers:
picker.unregister_callbacks()
def reset(self, force_all=False):
if not force_all and not isinstance(self.widget, QtWidgets.QTabWidget):
for picker in self.pickers:
if picker.rect().contains(get_cursor(picker)):
picker.reset()
return picker.panel
elif not force_all:
picker = self.pickers[self.widget.currentIndex()]
picker.reset()
return
# If no picker hovered, focus all.
if self.document.data['general']['panels.as_sub_tab']:
viewsize = self.pickers[0].viewportmapper.viewsize
else:
viewsize = None
for picker in self.pickers:
picker.reset(viewsize)
def create_pickers(self):
self.unregister_callbacks()
self.pickers = [
PickerPanelView(
self.document, self.editable, i, self.layers_menu, self)
for i in range(self.document.panel_count())]
for picker in self.pickers:
picker.size_event_triggered.connect(self.picker_resized)
self.register_callbacks()
def picker_resized(self, event):
data = self.document.data
if not data['general']['panels.as_sub_tab']:
return
for i, picker in enumerate(self.pickers):
if i == self.widget.currentIndex():
continue
picker.adjust_center(event.size(), event.oldSize())
def copy_pickers(self):
self.pickers = [p.copy() for p in self.pickers]
for picker in self.pickers:
picker.size_event_triggered.connect(self.picker_resized)
def create_panels(self, panel=None):
data = self.document.data
if not self.as_sub_tab:
panels = data['general']['panels']
orientation = data['general']['panels.orientation']
self.widget = create_stack_splitters(
panels, self.pickers, orientation)
else:
self.widget = QtWidgets.QTabWidget()
names = data['general']['panels.names']
for picker, name in zip(self.pickers, names):
self.widget.addTab(picker, name)
# Check "if panel is not None" (0 is a valid value,
# so "if panel" would be incorrect)
if panel is not None:
self.widget.setCurrentIndex(panel)
self.last_selected_tab = panel
elif self.last_selected_tab:
self.widget.setCurrentIndex(self.last_selected_tab)
self.widget.currentChanged.connect(self.on_tab_changed)
clear_layout(self.layout)
self.layout.addWidget(self.widget)
def on_tab_changed(self, index):
self.last_selected_tab = index
def full_refresh(self):
panels = self.document.data['general']['panels']
if count_panels(panels) != len(self.pickers):
self.create_pickers()
self.create_panels()
def general_option_changed(self, _, option):
value = self.document.data['general'][option]
panels = self.document.data['general']['panels']
reset = False
if option == 'panels.as_sub_tab':
state = self.document.data['general']['panels.as_sub_tab']
self.as_sub_tab = state
if option in ('panels', 'panels.orientation', 'panels.as_sub_tab'):
ensure_general_options_sanity(self.document.data['general'])
if count_panels(panels) != len(self.pickers):
self.create_pickers()
reset = True
else:
self.copy_pickers()
reset = option in ('panels.orientation', 'panels.as_sub_tab')
self.create_panels()
if option == 'panels.names' and self.as_sub_tab:
for i, name in enumerate(value):
self.widget.setTabText(i, name)
if option == 'hidden_layers':
self.layers_menu.hidden_layers = value
if reset:
QtCore.QTimer.singleShot(0, partial(self.reset, force_all=True))
self.update()
def set_auto_center(self, state):
for picker in self.pickers:
picker.auto_center = state
class PickerPanelView(QtWidgets.QWidget):
size_event_triggered = QtCore.Signal(object)
def __init__(
self, document, editable=True, panel=0, layers_menu=None,
parent=None):
super(PickerPanelView, self).__init__(parent)
self._shown = False
self.document = document
self.document.shapes_changed.connect(self.update)
self.callbacks = []
self.panel = panel
self.auto_center = True
self.editable = editable
self.interaction_manager = InteractionManager()
self.viewportmapper = ViewportMapper()
self.selection_square = SelectionSquare()
self.layers_menu = layers_menu
self.setMouseTracking(True)
self.clicked_shape = None
self.drag_shapes = []
def copy(self):
self.unregister_callbacks()
picker = PickerPanelView(
self.document, self.editable, self.panel, self.layers_menu)
picker.register_callbacks()
picker.viewportmapper = self.viewportmapper
picker.register_callbacks()
picker.auto_center = self.auto_center
self.deleteLater()
return picker
def showEvent(self, event):
if self._shown:
return super(PickerPanelView, self).showEvent(event)
self._shown = True
self.reset(self.size(), selection_only=False)
@property
def zoom_locked(self):
return self.document.data['general']['panels.zoom_locked'][self.panel]
def register_callbacks(self):
self.unregister_callbacks()
method = self.sync_with_maya_selection
cb = om.MEventMessage.addEventCallback('SelectionChanged', method)
self.callbacks.append(cb)
def unregister_callbacks(self):
for callback in self.callbacks:
om.MMessage.removeCallback(callback)
self.callbacks.remove(callback)
def sync_with_maya_selection(self, *_):
if not cmds.optionVar(query=SYNCHRONYZE_SELECTION):
return
shapes = self.document.shapes_by_panel[self.panel]
select_shapes_from_selection(shapes)
self.update()
def visible_shapes(self):
return [
s for s in self.document.shapes_by_panel[self.panel] if
not s.visibility_layer()
or s.visibility_layer() not in self.layers_menu.hidden_layers]
def reset(self, viewsize=None, selection_only=True):
shapes = [
s for s in self.visible_shapes() if
s.options['shape.space'] == 'world' and not
s.options['shape.ignored_by_focus']]
shapes_rects = [
s.bounding_rect() for s in shapes if
not selection_only or s.selected]
if not shapes_rects:
shapes_rects = [s.bounding_rect() for s in shapes]
if not shapes_rects:
self.update()
return
self.viewportmapper.viewsize = viewsize or self.size()
rect = get_combined_rects(shapes_rects)
if self.zoom_locked:
self.viewportmapper.zoom = 1
x = rect.center().x() - (self.size().width() / 2)
y = rect.center().y() - (self.size().height() / 2)
self.viewportmapper.origin = QtCore.QPointF(x, y)
else:
self.viewportmapper.focus(rect)
self.update()
def resizeEvent(self, event):
if not self.auto_center or event.oldSize() == QtCore.QSize(-1, -1):
return
self.adjust_center(event.size(), event.oldSize())
self.size_event_triggered.emit(event)
def adjust_center(self, size, old_size):
self.viewportmapper.viewsize = self.size()
size = (size - old_size) / 2
offset = QtCore.QPointF(size.width(), size.height())
self.viewportmapper.origin -= offset
self.update()
def enterEvent(self, _):
self.update()
def leaveEvent(self, _):
for shape in self.visible_shapes():
shape.hovered = False
self.update()
def mousePressEvent(self, event):
self.setFocus(QtCore.Qt.MouseFocusReason)
if self.drag_shapes and event.button() == QtCore.Qt.LeftButton:
pos = self.viewportmapper.to_units_coords(event.pos())
align_shapes_on_line(self.drag_shapes, pos, pos)
world_cursor = self.viewportmapper.to_units_coords(event.pos())
shapes = self.visible_shapes()
self.clicked_shape = detect_hovered_shape(
shapes=shapes,
world_cursor=world_cursor.toPoint(),
screen_cursor=event.pos(),
viewportmapper=self.viewportmapper)
shapes = self.document.shapes_by_panel[self.panel]
hsh = any(s.hovered for s in shapes)
self.interaction_manager.update(
event,
pressed=True,
has_shape_hovered=hsh,
dragging=bool(self.drag_shapes))
def mouseDoubleClickEvent(self, event):
world_cursor = self.viewportmapper.to_units_coords(event.pos())
shapes = self.visible_shapes()
clicked_shape = detect_hovered_shape(
shapes=shapes,
world_cursor=world_cursor.toPoint(),
screen_cursor=event.pos(),
viewportmapper=self.viewportmapper)
if not clicked_shape or event.button() != QtCore.Qt.LeftButton:
return
shift = self.interaction_manager.shift_pressed
ctrl = self.interaction_manager.ctrl_pressed
selection_mode = get_selection_mode(shift=shift, ctrl=ctrl)
shapes = self.document.all_children(clicked_shape.options['id'])
for shape in shapes:
shape.hovered = True
select_targets(self.visible_shapes(), selection_mode=selection_mode)
def mouseReleaseEvent(self, event):
shift = self.interaction_manager.shift_pressed
ctrl = self.interaction_manager.ctrl_pressed
selection_mode = get_selection_mode(shift=shift, ctrl=ctrl)
world_cursor = self.viewportmapper.to_units_coords(event.pos())
zoom = self.interaction_manager.zoom_button_pressed
shapes = self.visible_shapes()
hovered_shape = detect_hovered_shape(
shapes=shapes,
world_cursor=world_cursor.toPoint(),
screen_cursor=event.pos(),
viewportmapper=self.viewportmapper)
interact = (
self.clicked_shape and
self.clicked_shape is hovered_shape and
self.clicked_shape.is_interactive())
if zoom and self.interaction_manager.alt_pressed:
self.release(event)
return
if self.interaction_manager.mode == InteractionManager.DRAGGING:
self.add_drag_shapes()
self.release(event)
return
elif self.interaction_manager.mode == InteractionManager.SELECTION and not interact:
try:
select_targets(shapes, selection_mode=selection_mode)
except NameclashError as e:
warning('Selection Error', str(e), parent=self)
self.release(event)
return
if not self.clicked_shape:
if self.interaction_manager.right_click_pressed:
self.call_context_menu()
elif self.clicked_shape is hovered_shape:
show_context = (
self.interaction_manager.right_click_pressed and
not self.clicked_shape.has_right_click_command())
left_clicked = self.interaction_manager.left_click_pressed
if show_context:
self.call_context_menu()
elif left_clicked and self.clicked_shape.targets():
self.clicked_shape.select(selection_mode)
if interact:
button = (
'left' if self.interaction_manager.left_click_pressed
else 'right')
self.clicked_shape.execute(
button=button,
ctrl=self.interaction_manager.ctrl_pressed,
shift=self.interaction_manager.shift_pressed)
self.release(event)
def add_drag_shapes(self):
shapes_data = [s.options for s in self.drag_shapes]
self.document.add_shapes(shapes_data, hierarchize=True)
self.document.shapes_changed.emit()
self.document.record_undo()
self.drag_shapes = []
def release(self, event):
self.interaction_manager.update(event, pressed=False)
self.selection_square.release()
self.clicked_shape = None
self.update()
def wheelEvent(self, event):
# To center the zoom on the mouse, we save a reference mouse position
# and compare the offset after zoom computation.
if self.zoom_locked:
return
factor = .25 if event.angleDelta().y() > 0 else -.25
self.zoom(factor, event.pos())
self.update()
def zoom(self, factor, reference):
abspoint = self.viewportmapper.to_units_coords(reference)
if factor > 0:
self.viewportmapper.zoomin(abs(factor))
else:
self.viewportmapper.zoomout(abs(factor))
relcursor = self.viewportmapper.to_viewport_coords(abspoint)
vector = relcursor - reference
self.viewportmapper.origin = self.viewportmapper.origin + vector
def mouseMoveEvent(self, event):
world_cursor=self.viewportmapper.to_units_coords(event.pos())
selection_rect = (
self.selection_square.rect or
QtCore.QRectF(world_cursor, world_cursor))
unit_selection_rect = self.viewportmapper.to_units_rect(selection_rect)
unit_selection_rect = unit_selection_rect.toRect()
set_shapes_hovered(
shapes=self.visible_shapes(),
world_cursor=world_cursor,
viewport_cursor=event.pos(),
selection_rect=unit_selection_rect,
viewport_selection_rect=selection_rect,
viewportmapper=self.viewportmapper)
if self.interaction_manager.mode == InteractionManager.DRAGGING:
point1 = self.viewportmapper.to_units_coords(
self.interaction_manager.anchor)
point2 = self.viewportmapper.to_units_coords(event.pos())
align_shapes_on_line(self.drag_shapes, point1, point2)
return self.update()
elif self.interaction_manager.mode == InteractionManager.SELECTION:
if not self.selection_square.handeling:
self.selection_square.clicked(event.pos())
self.selection_square.handle(event.pos())
return self.update()
elif self.interaction_manager.mode == InteractionManager.ZOOMING:
if self.zoom_locked:
return self.update()
offset = self.interaction_manager.mouse_offset(event.pos())
if offset is not None and self.interaction_manager.zoom_anchor:
sensitivity = float(cmds.optionVar(query=ZOOM_SENSITIVITY))
factor = (offset.x() + offset.y()) / sensitivity
self.zoom(factor, self.interaction_manager.zoom_anchor)
return self.update()
elif self.interaction_manager.mode == InteractionManager.NAVIGATION:
offset = self.interaction_manager.mouse_offset(event.pos())
if offset is not None:
self.viewportmapper.origin = (
self.viewportmapper.origin - offset)
return self.update()
self.update()
def call_context_menu(self):
screen_cursor = get_cursor(self)
world_cursor = self.viewportmapper.to_units_coords(screen_cursor)
shape = detect_hovered_shape(
self.visible_shapes(), world_cursor, screen_cursor,
self.viewportmapper)
global_commands = self.document.data['general']['menu_commands']
context_menu = PickerMenu(global_commands, shape, self.editable)
method = partial(self.add_button, world_cursor, button_type=0)
context_menu.add_single.triggered.connect(method)
context_menu.add_single.setEnabled(bool(cmds.ls(selection=True)))
method = partial(self.add_button, world_cursor, button_type=1)
context_menu.add_multiple.triggered.connect(method)
state = len(cmds.ls(selection=True)) > 1
context_menu.add_multiple.setEnabled(state)
method = partial(self.add_button, world_cursor, button_type=2)
context_menu.add_command.triggered.connect(method)
method = partial(self.update_button, self.clicked_shape)
context_menu.update_button.triggered.connect(method)
state = bool(self.clicked_shape) and bool(cmds.ls(selection=True))
context_menu.update_button.setEnabled(state)
context_menu.delete_selected.triggered.connect(self.delete_buttons)
if self.layers_menu.displayed:
context_menu.addSeparator()
context_menu.addMenu(self.layers_menu)
action = context_menu.exec_(QtGui.QCursor.pos())
if isinstance(action, CommandAction):
if not shape:
self.execute_menu_command(action.command)
return
shape.execute(command=action.command)
def execute_menu_command(self, command):
try:
execute_code(
language=command['language'],
code=command['command'],
deferred=command['deferred'],
compact_undo=command['force_compact_undo'])
except Exception as e:
import traceback
print(traceback.format_exc())
def update_button(self, shape):
shape.set_targets(cmds.ls(selection=True))
self.document.record_undo()
def delete_buttons(self):
selected_shapes = [s for s in self.document.shapes if s.selected]
self.document.remove_shapes(selected_shapes)
self.document.record_undo()
self.document.shapes_changed.emit()
def get_quick_options(self):
return {
'bgcolor.normal': cmds.optionVar(query=DEFAULT_BG_COLOR),
'text.color': cmds.optionVar(query=DEFAULT_TEXT_COLOR),
'shape.width': cmds.optionVar(query=DEFAULT_WIDTH),
'shape.height': cmds.optionVar(query=DEFAULT_HEIGHT),
'text.content': cmds.optionVar(query=DEFAULT_LABEL)}
def add_button(self, position, button_type=0):
"""
Button types:
0 = Single button from selection.
1 = Multiple buttons from selection.
2 = Command button.
"""
targets = cmds.ls(selection=True)
if not targets and button_type <= 1:
return warning("Warning", "No targets selected")
if button_type == 1:
overrides = self.get_quick_options()
overrides['panel'] = self.panel
shapes = build_multiple_shapes(targets, overrides)
if not shapes:
return
self.drag_shapes = shapes
return
shape_data = deepcopy(BUTTON)
shape_data['panel'] = self.panel
shape_data['shape.left'] = position.x()
shape_data['shape.top'] = position.y()
shape_data.update(self.get_quick_options())
if button_type == 0:
shape_data['action.targets'] = targets
else:
text, result = (
QtWidgets.QInputDialog.getText(self, 'Button text', 'text'))
if not result:
return
shape_data['text.content'] = text
command = deepcopy(COMMAND)
languages = ['python', 'mel']
language = languages[cmds.optionVar(query=LAST_COMMAND_LANGUAGE)]
command['language'] = language
dialog = CommandEditorDialog(command)
if not dialog.exec_():
return
command = dialog.command_data()
index = languages.index(command['language'])
save_optionvar(LAST_COMMAND_LANGUAGE, index)
shape_data['action.commands'] = [command]
width = max([
shape_data['shape.width'],
len(shape_data['text.content']) * 7])
shape_data['shape.width'] = width
self.document.add_shapes([shape_data])
self.document.record_undo()
self.document.shapes_changed.emit()
def paintEvent(self, _):
try:
painter = QtGui.QPainter()
painter.begin(self)
# Color background.
color = self.document.data['general']['panels.colors'][self.panel]
if color:
painter.setPen(QtCore.Qt.NoPen)
painter.setBrush(QtGui.QColor(color))
painter.drawRect(self.rect())
# Color border focus.
if self.rect().contains(get_cursor(self)):
draw_picker_focus(painter, self.rect())
# List renderable shapes.
painter.setRenderHints(QtGui.QPainter.Antialiasing)
hidden_layers = self.layers_menu.hidden_layers
shapes = [
shape for shape in self.document.shapes_by_panel[self.panel] if
not shape.visibility_layer() or
shape.visibility_layer() not in hidden_layers]
if self.interaction_manager.left_click_pressed:
shapes.extend(self.drag_shapes)
# Draw shapes and create a mask for arrows shapes.
cutter = QtGui.QPainterPath()
cutter.setFillRule(QtCore.Qt.WindingFill)
for shape in shapes:
qpath = draw_shape(
painter, shape,
force_world_space=False,
viewportmapper=self.viewportmapper)
screen_space = shape.options['shape.space'] == 'screen'
if not shape.options['background'] or screen_space:
cutter.addPath(qpath)
# Draw hierarchy connections.
connections_path = QtGui.QPainterPath()
if cmds.optionVar(query=DISPLAY_HIERARCHY_IN_PICKER):
for shape in shapes:
if shape.options['shape.space'] == 'screen':
continue
for child in shape.options['children']:
child = self.document.shapes_by_id.get(child)
hidden = (
child and
child.visibility_layer() and
child.visibility_layer() in hidden_layers)
screen_space = child.options['shape.space'] == 'screen'
panel = child.options['panel'] != shape.options['panel']
if hidden or screen_space or panel:
continue
start_point = shape.bounding_rect().center()
end_point = child.bounding_rect().center()
path = get_connection_path(
start_point, end_point, self.viewportmapper)
connections_path.addPath(path)
connections_path = connections_path.subtracted(cutter)
draw_connections(painter, connections_path)
# Draw Selection square/
if self.selection_square.rect:
draw_selection_square(
painter, self.selection_square.rect)
except BaseException as e:
import traceback
print(traceback.format_exc())
print(str(e))
pass # avoid crash
# TODO: log the error
finally:
painter.end()
class CommandAction(QtWidgets.QAction):
def __init__(self, command, parent=None):
super(CommandAction, self).__init__(command['caption'], parent)
self.command = command
class PickerMenu(QtWidgets.QMenu):
def __init__(
self,
global_commands=None,
shape=None,
editable=True,
parent=None):
super(PickerMenu, self).__init__(parent)
if shape and shape.options['action.menu_commands']:
for command in shape.options['action.menu_commands']:
self.addAction(CommandAction(command, self))
if not global_commands:
self.addSeparator()
if global_commands:
for command in global_commands:
self.addAction(CommandAction(command, self))
self.addSeparator()
self.add_single = QtWidgets.QAction('Add single button', self)
self.add_multiple = QtWidgets.QAction('Add multiple buttons', self)
self.update_button = QtWidgets.QAction('Update button', self)
self.add_command = QtWidgets.QAction('Add command', self)
text = 'Delete selected button(s)'
self.delete_selected = QtWidgets.QAction(text, self)
if editable:
self.addAction(self.add_single)
self.addAction(self.add_multiple)
self.addAction(self.update_button)
self.addSeparator()
self.addAction(self.add_command)
self.addSeparator()
self.addAction(self.delete_selected)
class VisibilityLayersMenu(QtWidgets.QMenu):
visibilities_changed = QtCore.Signal()
def __init__(self, document, parent=None):
super(VisibilityLayersMenu, self).__init__('Visibility layers', parent)
self.document = document
self.document.shapes_changed.connect(self.update_actions)
self.hidden_layers = document.data['general']['hidden_layers'][:]
self.update_actions()
@property
def displayed(self):
return bool(self.document.shapes_by_layer)
def update_actions(self):
self.clear()
layers = list(self.document.shapes_by_layer)
action = QtWidgets.QAction('Show all')
for layer in layers:
action = QtWidgets.QAction(layer, self)
action.setCheckable(True)
action.setChecked(layer not in self.hidden_layers)
action.toggled.connect(partial(self.set_hidden_layer, layer))
self.addAction(action)
def set_hidden_layer(self, layer, state):
if state is False and layer not in self.hidden_layers:
self.hidden_layers.append(layer)
if state is True and layer in self.hidden_layers:
self.hidden_layers.remove(layer)
self.visibilities_changed.emit()

Some files were not shown because too many files have changed in this diff Show More