Update
BIN
2023/icons/animation_retargeting_tool.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
2023/icons/dwpicker.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
2023/icons/ik_fk_switcher.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
2023/icons/mgpicker.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
722
2023/scripts/animation_tools/animation_retargeting_tool.py
Normal 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()
|
||||||
62
2023/scripts/animation_tools/dwpicker/README.md
Normal 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)
|
||||||
208
2023/scripts/animation_tools/dwpicker/__init__.py
Normal 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)
|
||||||
113
2023/scripts/animation_tools/dwpicker/align.py
Normal 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
|
||||||
|
}
|
||||||
5
2023/scripts/animation_tools/dwpicker/appinfos.py
Normal 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'
|
||||||
29
2023/scripts/animation_tools/dwpicker/arrayutils.py
Normal 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)
|
||||||
22
2023/scripts/animation_tools/dwpicker/clipboard.py
Normal 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 {}
|
||||||
272
2023/scripts/animation_tools/dwpicker/colorwheel.py
Normal 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
|
||||||
159
2023/scripts/animation_tools/dwpicker/commands.py
Normal 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)
|
||||||
145
2023/scripts/animation_tools/dwpicker/compatibility.py
Normal 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]
|
||||||
820
2023/scripts/animation_tools/dwpicker/designer/attributes.py
Normal 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)
|
||||||
451
2023/scripts/animation_tools/dwpicker/designer/canvas.py
Normal 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()
|
||||||
17
2023/scripts/animation_tools/dwpicker/designer/display.py
Normal 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)
|
||||||
613
2023/scripts/animation_tools/dwpicker/designer/editor.py
Normal 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()
|
||||||
114
2023/scripts/animation_tools/dwpicker/designer/highlighter.py
Normal 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
|
||||||
162
2023/scripts/animation_tools/dwpicker/designer/layer.py
Normal 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]))
|
||||||
289
2023/scripts/animation_tools/dwpicker/designer/menu.py
Normal 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()
|
||||||
703
2023/scripts/animation_tools/dwpicker/designer/patheditor.py
Normal 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()
|
||||||
374
2023/scripts/animation_tools/dwpicker/designer/stackeditor.py
Normal 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
|
||||||
531
2023/scripts/animation_tools/dwpicker/dialog.py
Normal 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()}
|
||||||
144
2023/scripts/animation_tools/dwpicker/document.py
Normal 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
|
||||||
391
2023/scripts/animation_tools/dwpicker/geometry.py
Normal 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]
|
||||||
40
2023/scripts/animation_tools/dwpicker/hotkeys.py
Normal 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)
|
||||||
194
2023/scripts/animation_tools/dwpicker/hotkeyseditor.py
Normal 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)
|
||||||
BIN
2023/scripts/animation_tools/dwpicker/icons/addbg.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/addbutton.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/addshape.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/addtext.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/align_bottom.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/align_h_center.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/align_left.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/align_right.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/align_top.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/align_v_center.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/arrange_h.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/arrange_v.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/center.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/copy.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/copy_settings.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/delete.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/delete2.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/dock.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/dreamwallpicker.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/edit.png
Normal file
|
After Width: | Height: | Size: 784 B |
BIN
2023/scripts/animation_tools/dwpicker/icons/edit2.png
Normal file
|
After Width: | Height: | Size: 731 B |
BIN
2023/scripts/animation_tools/dwpicker/icons/frame.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/h_symmetry.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/hierarchy.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/isolate.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/link.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 22 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/manager-delete.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/manager-edit.png
Normal file
|
After Width: | Height: | Size: 963 B |
BIN
2023/scripts/animation_tools/dwpicker/icons/manager-export.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/manager-import.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/manager-new.png
Normal file
|
After Width: | Height: | Size: 998 B |
BIN
2023/scripts/animation_tools/dwpicker/icons/mini-open.png
Normal file
|
After Width: | Height: | Size: 737 B |
BIN
2023/scripts/animation_tools/dwpicker/icons/movedown.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/moveup.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/new.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/onbottom.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/ontop.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/open.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/panels.png
Normal file
|
After Width: | Height: | Size: 435 B |
BIN
2023/scripts/animation_tools/dwpicker/icons/paste.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/paste_settings.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/picker.png
Normal file
|
After Width: | Height: | Size: 567 B |
BIN
2023/scripts/animation_tools/dwpicker/icons/play.png
Normal file
|
After Width: | Height: | Size: 1020 B |
BIN
2023/scripts/animation_tools/dwpicker/icons/polygon.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/redo.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/reload.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/rotation.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/save.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/search.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/snap.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/tabs.png
Normal file
|
After Width: | Height: | Size: 663 B |
BIN
2023/scripts/animation_tools/dwpicker/icons/tangent.png
Normal file
|
After Width: | Height: | Size: 746 B |
BIN
2023/scripts/animation_tools/dwpicker/icons/tangentbreak.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/touch.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/undo.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
2023/scripts/animation_tools/dwpicker/icons/unlink.png
Normal file
|
After Width: | Height: | Size: 478 B |
BIN
2023/scripts/animation_tools/dwpicker/icons/v_symmetry.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1 @@
|
|||||||
|
from .converter import convert
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
85
2023/scripts/animation_tools/dwpicker/interactionmanager.py
Normal 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))
|
||||||
73
2023/scripts/animation_tools/dwpicker/interactive.py
Normal 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
|
||||||
89
2023/scripts/animation_tools/dwpicker/languages.py
Normal 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,
|
||||||
|
}
|
||||||
870
2023/scripts/animation_tools/dwpicker/main.py
Normal 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)
|
||||||
71
2023/scripts/animation_tools/dwpicker/namespace.py
Normal 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]
|
||||||
176
2023/scripts/animation_tools/dwpicker/optionvar.py
Normal 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)])
|
||||||
347
2023/scripts/animation_tools/dwpicker/painting.py
Normal 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)
|
||||||
80
2023/scripts/animation_tools/dwpicker/path.py
Normal 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)
|
||||||
810
2023/scripts/animation_tools/dwpicker/picker.py
Normal 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()
|
||||||