This commit is contained in:
2025-04-17 04:52:48 +08:00
commit 9985b73dc1
3708 changed files with 2387532 additions and 0 deletions

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 DreamWall
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,56 @@
# Dreamwall Picker
适用于 Autodesk Maya 2017或更高版本的动画选择器
作者Lionel Brouyère, Olivier Evers
> 该工具是 Hotbox DesignerLionel Brouyère的一个分支。
> 一个跨 DCC 的菜单、标记菜单和热盒设计器。
> https://github.com/luckylyk/hotbox_designer
### 功能
- 简单快速的选择器创建。
- 导入 2022 年之前完成的 AnimSchool 选择器。
- 将选择器存储在 Maya 场景中。
- 高级选择器编辑器。
- 执行 AnimSchool 选择器的所有功能以及更多功能...
<center><img src="https://raw.githubusercontent.com/DreamWall-Animation/dwpicker/main/screenshots/picker.gif" alt="drawing" align="center" width="250"/> <img src="https://s10.gifyu.com/images/createbuttons.gif" alt="drawing" align="center" width="400"/>
<img src="https://raw.githubusercontent.com/DreamWall-Animation/dwpicker/main/screenshots/editor.gif" alt="drawing" align="center" width="370"/>
### 安装
将名为 "dwpicker" 的文件夹(不是 dwpicker-main放入 Maya 脚本文件夹中
| 操作系统 | 路径 |
| ------ | ------ |
| Linux | ~/<用户名>/maya/scripts |
| Windows | \Users\<用户名>\Documents\maya\scripts |
| Mac OS X | ~<用户名>/Library/Preferences/Autodesk/maya/scripts |
### 如何运行
```python
import dwpicker
dwpicker.show()
```
### 常见问题
#### 它能在 Maya 2025 上运行吗?
可以!(自版本 0.11.2 起)
#### 我的绑定包含多个命名空间或嵌套命名空间。
此功能当前不支持。选择器旨在提供单级命名空间的灵活性,允许一个选择器在场景中为同一绑定的多个实例服务。切换选择器的命名空间很简单。然而,尽管我们努力保持这种灵活性,但我们尚未找到支持嵌套命名空间的简单方法。虽然有潜在的解决方案,但它们看起来都相当复杂,难以为用户理解和实现。也许将来会有一个绝妙的主意出现,但目前,这个功能不在我们的计划中。我们欢迎您提出任何建议!
#### 为什么我不能使用相对路径来存储我的图像文件?
当您打开选择器时,它会将文件直接导入场景中,失去原始路径引用。我们选择导入数据而不是直接引用它们,因为许多动画师更喜欢为他们的特定镜头需求自定义选择器(例如,为特定镜头添加道具或约束按钮)。这种方法使得使用相对路径变得复杂。
#### 如何在我将选择器分享给其他人时保留图像,而他们的文件存储在其他地方?
尽管不支持相对路径,但与其他 Maya 路径属性类似您可以在路径中包含环境变量。设置自定义环境变量可能很复杂。因此我们建议使用一个默认变量DWPICKER_PROJECT_DIRECTORY可在选择器首选项窗口中使用。
如果您将 DWPICKER_PROJECT_DIRECTORY 配置为 `c:/my_pickers`,并且您有一个图像路径为:
`c:/my_pickers/my_character/background.png`,可以这样输入以使路径动态化:`$DWPICKER_PROJECT_DIRECTORY/my_character/background.png`
当您从 UI 中选择文件时,它会自动创建包含变量的路径。
### 支持
最好在 GitHub 页面上发布问题。\
如果您没有 GitHub 帐户,可以发送邮件至 `brouyere |a| dreamwall.be`。\
请在邮件主题中以 ***[dwpicker]*** 开头。(请注意,使用这种方式回复的延迟可能会更长)。

View File

View File

@ -0,0 +1,134 @@
import os
import sys
from maya import cmds
if int(cmds.about(majorVersion=True)) >= 2025:
print('>> PySide6 Maya version found. PySide2 remap activated.')
sys.path.append('{}/qt_remapping'.format(os.path.dirname(__file__)))
from dwpicker.main import DwPicker, WINDOW_CONTROL_NAME
from dwpicker.optionvar import ensure_optionvars_exists
from dwpicker.qtutils import remove_workspace_control
from dwpicker.updatechecker import warn_if_update_available
_dwpicker = None
def show(editable=True, pickers=None, ignore_scene_pickers=False):
"""
This is the dwpicker default startup function.
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:
This is loading the picker empty, ignoring the scene content.
"""
ensure_optionvars_exists()
global _dwpicker
if not _dwpicker:
warn_if_update_available()
_dwpicker = DwPicker()
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.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()
def toggle():
if not _dwpicker:
return show()
_dwpicker.setVisible(not _dwpicker.isVisible())
def close():
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()

View File

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

View File

@ -0,0 +1,4 @@
VERSION = 0, 11, 2 # Version, Feature, Hotfix.
RELEASE_DATE = 'june 7th 2024'
DW_WEBSITE = 'https://fr.dreamwall.be/'
DW_GITHUB = 'https://github.com/DreamWall-Animation'

View File

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

View File

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

View File

@ -0,0 +1,272 @@
import math
from PySide2 import QtWidgets, QtGui, QtCore
from dwpicker.qtutils import get_cursor
from dwpicker.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.repaint()
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.repaint()
def degree_to_color(degree):
if degree is None:
return None
degree = degree / 360.0
r, g, b = 255.0, 255.0, 255.0
contain_red = (
(degree >= 0.0 and degree <= 0.33)
or (degree >= 0.66 and degree <= 1.0))
if contain_red:
if degree >= 0.66 and degree <= 0.83:
factor = degree - 0.66
r = round(255 * (factor / .16))
if (degree > 0.0 and degree < 0.16) or (degree > 0.83 and degree < 1.0):
r = 255
elif degree >= 0.16 and degree <= 0.33:
factor = degree - 0.16
r = 255 - round(255 * (factor / .16))
else:
r = 0
r = min(r, 255)
r = max(r, 0)
# GREEN
if degree >= 0.0 and degree <= 0.66:
if degree <= 0.16:
g = round(255.0 * (degree / .16))
elif degree < 0.5:
g = 255
if degree >= 0.5:
factor = degree - 0.5
g = 255 - round(255.0 * (factor / .16))
else:
g = 0
g = min(g, 255.0)
g = max(g, 0)
# BLUE
if degree >= 0.33 and degree <= 1.0:
if degree <= 0.5:
factor = degree - 0.33
b = round(255 * (factor / .16))
elif degree < 0.83:
b = 255.0
if degree >= 0.83 and degree <= 1.0:
factor = degree - 0.83
b = 255.0 - round(255.0 * (factor / .16))
else:
b = 0
b = min(b, 255)
b = max(b, 0)
return r, g, b

View File

@ -0,0 +1,122 @@
from copy import deepcopy
from PySide2 import QtWidgets, QtCore
from dwpicker.templates import COMMAND
from dwpicker.qtutils import icon
from dwpicker.dialog import CommandEditorDialog
class CommandsEditor(QtWidgets.QWidget):
valueSet = QtCore.Signal(object)
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]['action.commands']:
self.call_add_command(command)
def call_create_command(self):
command = deepcopy(COMMAND)
dialog = CommandEditorDialog(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 = CommandItemWidget(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 = CommandEditorDialog(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 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.delete = QtWidgets.QPushButton(icon('delete2.png'), '')
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.label)
layout.addStretch()
layout.addWidget(self.edit)
layout.addWidget(self.delete)
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())

View File

@ -0,0 +1,82 @@
"""
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.
"""
from dwpicker.appinfos import VERSION
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)
return picker_data
def update_shape_actions_for_v0_11_0(shape):
"""
With release 0.11.0 comes a new configurable action system.
"""
if 'action.namespace' in shape:
del shape['action.namespace']
if 'action.type' in shape:
del shape['action.type']
shape['action.commands'] = []
if shape['action.left.command']:
shape['action.commands'].append({
'enabled': shape['action.left'],
'button': 'left',
'language': shape['action.left.language'],
'command': shape['action.left.command'],
'alt': False,
'ctrl': False,
'shift': False,
'deferred': False,
'force_compact_undo': False})
if shape['action.right.command']:
shape['action.commands'].append({
'enabled': shape['action.right'],
'button': 'left',
'language': shape['action.right.language'],
'command': shape['action.right.command'],
'alt': False,
'ctrl': False,
'shift': False,
'deferred': False,
'force_compact_undo': False})
keys_to_clear = (
'action.left', 'action.left.language',
'action.left.command', 'action.right', 'action.right.language',
'action.right.command')
for key in keys_to_clear:
del shape[key]

View File

@ -0,0 +1,577 @@
import maya.cmds as cmds
from functools import partial
from PySide2 import QtCore, QtWidgets
from dwpicker.qtutils import VALIGNS, HALIGNS
from dwpicker.commands import CommandsEditor
from dwpicker.designer.layer import VisibilityLayersEditor
from dwpicker.widgets import (
BoolCombo, BrowseEdit, ColorEdit, IntEdit, FloatEdit, LayerEdit,
TextEdit, Title, WidgetToggler)
LEFT_CELL_WIDTH = 80
SHAPE_TYPES = 'square', 'round', 'rounded_rect'
class AttributeEditor(QtWidgets.QWidget):
generalOptionSet = QtCore.Signal(str, object)
imageModified = QtCore.Signal()
optionSet = QtCore.Signal(str, object)
rectModified = QtCore.Signal(str, float)
removeLayer = QtCore.Signal(str)
selectLayerContent = QtCore.Signal(str)
def __init__(self, parent=None):
super(AttributeEditor, self).__init__(parent)
self.widget = QtWidgets.QWidget()
self.generals = GeneralSettings()
self.generals.optionModified.connect(self.generalOptionSet.emit)
self.generals.layers.removeLayer.connect(self.removeLayer.emit)
mtd = self.selectLayerContent.emit
self.generals.layers.selectLayerContent.connect(mtd)
self.generals_toggler = WidgetToggler('Picker options', self.generals)
self.shape = ShapeSettings()
self.shape.optionSet.connect(self.optionSet.emit)
self.shape.rectModified.connect(self.rectModified.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()
self.action.optionSet.connect(self.optionSet.emit)
self.action_toggler = WidgetToggler('Action', self.action)
self.layout = QtWidgets.QVBoxLayout(self.widget)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0)
self.layout.addWidget(self.generals_toggler)
self.layout.addWidget(self.generals)
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.main_layout = QtWidgets.QVBoxLayout(self)
self.main_layout.setContentsMargins(0, 0, 0, 0)
self.main_layout.addWidget(self.scroll_area)
self.setFixedWidth(self.sizeHint().width() * 1.075)
def set_generals(self, options):
self.blockSignals(True)
self.generals.set_options(options)
self.blockSignals(False)
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):
optionModified = QtCore.Signal(str, object)
def __init__(self, parent=None):
super(GeneralSettings, self).__init__(parent)
self.name = TextEdit()
self.name.valueSet.connect(self.name_changed)
self.zoom_locked = BoolCombo(False)
self.zoom_locked.valueSet.connect(self.zoom_changed)
self.layers = VisibilityLayersEditor()
form_layout = QtWidgets.QFormLayout()
form_layout.setSpacing(0)
form_layout.setContentsMargins(0, 0, 0, 0)
form_layout.setHorizontalSpacing(5)
form_layout.addRow('Name', self.name)
form_layout.addRow('Zoom-locked', self.zoom_locked)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addLayout(form_layout)
layout.addWidget(Title('Visibility Layers'))
layout.addWidget(self.layers)
def set_shapes(self, shapes):
self.layers.set_shapes(shapes)
def set_options(self, options):
self.name.setText(options['name'])
self.zoom_locked.setCurrentText(str(options['zoom_locked']))
def name_changed(self, value):
self.optionModified.emit('name', value)
def zoom_changed(self, state):
self.optionModified.emit('zoom_locked', state)
class ShapeSettings(QtWidgets.QWidget):
optionSet = QtCore.Signal(str, object)
rectModified = QtCore.Signal(str, float)
def __init__(self, parent=None):
super(ShapeSettings, self).__init__(parent)
self.shape = QtWidgets.QComboBox()
self.shape.addItems(SHAPE_TYPES)
self.shape.currentIndexChanged.connect(self.shape_changed)
self.layer = LayerEdit()
method = partial(self.optionSet.emit, 'visibility_layer')
self.layer.valueSet.connect(method)
self.left = IntEdit(minimum=0)
method = partial(self.rectModified.emit, 'shape.left')
self.left.valueSet.connect(method)
self.top = IntEdit(minimum=0)
method = partial(self.rectModified.emit, 'shape.right')
self.top.valueSet.connect(method)
self.width = IntEdit(minimum=0)
method = partial(self.rectModified.emit, 'shape.width')
self.width.valueSet.connect(method)
self.height = IntEdit(minimum=0)
method = partial(self.rectModified.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)
self.layout = QtWidgets.QFormLayout(self)
self.layout.setSpacing(0)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setHorizontalSpacing(5)
self.layout.addRow('Shape', self.shape)
self.layout.addRow('Visibility layer', self.layer)
self.layout.addItem(QtWidgets.QSpacerItem(0, 8))
self.layout.addRow(Title('Dimensions'))
self.layout.addRow('left', self.left)
self.layout.addRow('top', self.top)
self.layout.addRow('width', self.width)
self.layout.addRow('height', self.height)
self.layout.addRow('roundness x', self.cornersx)
self.layout.addRow('roundness y', self.cornersy)
for label in self.findChildren(QtWidgets.QLabel):
if not isinstance(label, Title):
label.setFixedWidth(LEFT_CELL_WIDTH)
def shape_changed(self, _):
self.optionSet.emit('shape', self.shape.currentText())
def set_options(self, options):
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.setCurrentText(value)
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.width = FloatEdit()
method = partial(self.optionSet.emit, 'image.width')
self.width.valueSet.connect(method)
self.height = FloatEdit()
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('Width', self.width)
self.layout.addRow('Height', self.height)
for label in self.findChildren(QtWidgets.QLabel):
if not isinstance(label, Title):
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.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 = FloatEdit(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 = FloatEdit(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.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)
for label in self.findChildren(QtWidgets.QLabel):
if not isinstance(label, Title):
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, 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._remove_targets = QtWidgets.QPushButton('Remove')
self._replace_targets = QtWidgets.QPushButton('Replace')
self._targets_layout = QtWidgets.QHBoxLayout()
self._targets_layout.addWidget(self._add_targets)
self._targets_layout.addWidget(self._remove_targets)
self._targets_layout.addWidget(self._replace_targets)
self._add_targets.clicked.connect(self.call_add_targets)
self._remove_targets.clicked.connect(self.call_remove_targets)
self._replace_targets.clicked.connect(self.call_replace_targets)
self._commands = CommandsEditor()
method = partial(self.optionSet.emit, 'action.commands')
self._commands.valueSet.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.addRow('Add Selected', self._targets_layout)
self.layout = QtWidgets.QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0)
self.layout.addLayout(form)
self.layout.addWidget(Title('Scripts'))
self.layout.addWidget(self._commands)
for label in self.findChildren(QtWidgets.QLabel):
if not isinstance(label, Title):
label.setFixedWidth(LEFT_CELL_WIDTH)
def targets(self):
targets = str(self._targets.text())
try:
return [t.strip(" ") for t in targets.split(',')]
except ValueError:
return []
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.setFocus()
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.setFocus()
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.setFocus()
self.targets_changed()
def targets_changed(self):
if not self._targets.text():
self.optionSet.emit('action.targets', [])
return
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)
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)
for label in self.findChildren(QtWidgets.QLabel):
if not isinstance(label, Title):
label.setFixedWidth(LEFT_CELL_WIDTH)
def valign_changed(self):
self.optionSet.emit('text.valign', self.valignement.currentText())
def halign_changed(self):
self.optionSet.emit('text.halign', self.halignement.currentText())
def set_options(self, options):
values = list({option['text.content'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.text.setText(value)
values = list({option['text.size'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.size.setText(value)
values = list({option['text.bold'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.bold.setCurrentText(value)
values = list({option['text.italic'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.italic.setCurrentText(value)
values = list({option['text.color'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.color.set_color(value)
values = list({option['text.halign'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.halignement.setCurrentText(value)
values = list({option['text.valign'] for option in options})
value = str(values[0]) if len(values) == 1 else None
self.valignement.setCurrentText(value)

View File

@ -0,0 +1,191 @@
from PySide2 import QtCore, QtGui, QtWidgets
from dwpicker.interactive import Manipulator, SelectionSquare
from dwpicker.geometry import Transform, get_combined_rects
from dwpicker.painting import draw_editor, draw_shape
from dwpicker.qtutils import get_cursor
from dwpicker.selection import Selection, get_selection_mode
class ShapeEditArea(QtWidgets.QWidget):
selectedShapesChanged = QtCore.Signal()
increaseUndoStackRequested = QtCore.Signal()
centerMoved = QtCore.Signal(int, int)
callContextMenu = QtCore.Signal(QtCore.QPoint)
def __init__(self, options, parent=None):
super(ShapeEditArea, self).__init__(parent)
self.setFixedSize(750, 550)
self.setMouseTracking(True)
self.options = options
self.selection = Selection()
self.selection_square = SelectionSquare()
self.manipulator = Manipulator()
self.transform = Transform()
self.shapes = []
self.clicked_shape = None
self.clicked = False
self.selecting = False
self.handeling = False
self.manipulator_moved = False
self.increase_undo_on_release = False
self.lock_background_shape = True
self.ctrl_pressed = False
self.shit_pressed = False
def set_lock_background_shape(self, state):
self.lock_background_shape = state
def get_hovered_shape(self, cursor):
for shape in reversed(self.list_shapes()):
if shape.rect.contains(cursor):
return shape
def list_shapes(self):
if self.lock_background_shape:
return [
shape for shape in self.shapes
if not shape.is_background()]
return self.shapes
def mousePressEvent(self, event):
self.setFocus(QtCore.Qt.MouseFocusReason) # This is not automatic
if event.button() != QtCore.Qt.LeftButton:
return
cursor = get_cursor(self)
self.clicked = True
hovered_shape = self.get_hovered_shape(cursor)
self.transform.direction = self.manipulator.get_direction(cursor)
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.QRect(self.manipulator.rect)
self.transform.set_reference_point(cursor)
self.handeling = bool(hovered_shape) or self.transform.direction
self.selecting = not self.handeling and self.clicked
self.repaint()
def mouseMoveEvent(self, _):
cursor = get_cursor(self)
if self.handeling:
rect = self.manipulator.rect
if self.transform.direction:
self.transform.resize((s.rect for s in self.selection), cursor)
self.manipulator.update_geometries()
elif rect is not None:
self.transform.move((s.rect for s in self.selection), cursor)
self.manipulator.update_geometries()
for shape in self.selection:
shape.synchronize_rect()
shape.synchronize_image()
self.manipulator_moved = True
self.increase_undo_on_release = True
self.selectedShapesChanged.emit()
elif self.selecting:
self.selection_square.handle(cursor)
for shape in self.list_shapes():
shape.hovered = self.selection_square.intersects(shape.rect)
else:
for shape in self.list_shapes():
shape.hovered = shape.rect.contains(cursor)
self.repaint()
def mouseReleaseEvent(self, event):
context_menu_condition = (
event.button() == QtCore.Qt.RightButton and
not self.clicked and
not self.handeling and
not self.selecting)
if context_menu_condition:
return self.callContextMenu.emit(event.pos())
if event.button() != QtCore.Qt.LeftButton:
return
if self.increase_undo_on_release:
self.increaseUndoStackRequested.emit()
self.increase_undo_on_release = False
if self.selecting:
self.select_shapes()
self.selection_square.release()
self.clicked = False
self.handeling = False
self.selecting = False
self.repaint()
def select_shapes(self):
shapes = [
s for s in self.list_shapes()
if s.rect.intersects(self.selection_square.rect)]
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.repaint()
def update_selection(self):
rect = get_combined_rects([shape.rect for shape in self.selection])
self.manipulator.set_rect(rect)
self.selectedShapesChanged.emit()
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 paint(self, painter):
painter.setRenderHint(QtGui.QPainter.Antialiasing)
draw_editor(painter, self.rect(), snap=self.transform.snap)
for shape in self.shapes:
draw_shape(painter, shape)
self.manipulator.draw(painter, get_cursor(self))
self.selection_square.draw(painter)

View File

@ -0,0 +1,557 @@
from functools import partial
from math import ceil
from PySide2 import QtWidgets, QtCore
from maya import cmds
from dwpicker import clipboard
from dwpicker.align import align_shapes, arrange_horizontal, arrange_vertical
from dwpicker.arrayutils import (
move_elements_to_array_end, move_elements_to_array_begin,
move_up_array_elements, move_down_array_elements)
from dwpicker.dialog import SearchAndReplaceDialog, warning, SettingsPaster
from dwpicker.interactive import Shape, get_shape_rect_from_options
from dwpicker.geometry import get_combined_rects, rect_symmetry
from dwpicker.optionvar import BG_LOCKED, TRIGGER_REPLACE_ON_MIRROR
from dwpicker.picker import frame_shapes
from dwpicker.qtutils import set_shortcut, get_cursor
from dwpicker.templates import BUTTON, TEXT, BACKGROUND
from dwpicker.designer.editarea import ShapeEditArea
from dwpicker.designer.menu import MenuWidget
from dwpicker.designer.attributes import AttributeEditor
DIRECTION_OFFSETS = {
'Left': (-1, 0), 'Right': (1, 0), 'Up': (0, -1), 'Down': (0, 1)}
class PickerEditor(QtWidgets.QWidget):
pickerDataModified = QtCore.Signal(object)
def __init__(self, picker_data, undo_manager, parent=None):
super(PickerEditor, self).__init__(parent, QtCore.Qt.Window)
title = "Picker editor - " + picker_data['general']['name']
self.setWindowTitle(title)
self.options = picker_data['general']
self.undo_manager = undo_manager
self.shape_editor = ShapeEditArea(self.options)
self.shape_editor.callContextMenu.connect(self.call_context_menu)
bg_locked = bool(cmds.optionVar(query=BG_LOCKED))
self.shape_editor.set_lock_background_shape(bg_locked)
self.set_picker_data(picker_data)
self.shape_editor.selectedShapesChanged.connect(self.selection_changed)
method = self.set_data_modified
self.shape_editor.increaseUndoStackRequested.connect(method)
self.scrollarea = QtWidgets.QScrollArea()
alignment = QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter
self.scrollarea.setFocusPolicy(QtCore.Qt.NoFocus)
policy = QtWidgets.QSizePolicy.Expanding
self.scrollarea.setSizePolicy(policy, policy)
# HACK: Stupid hack to force scroll area to fix layout size.
self.scrollarea.sizeHint = lambda: QtCore.QSize(10000, 10000)
self.scrollarea.setAlignment(alignment)
self.scrollarea.setWidget(self.shape_editor)
self.menu = MenuWidget()
self.menu.copyRequested.connect(self.copy)
self.menu.copySettingsRequested.connect(self.copy_settings)
self.menu.deleteRequested.connect(self.delete_selection)
self.menu.frameShapes.connect(self.frame_shapes)
self.menu.pasteRequested.connect(self.paste)
self.menu.pasteSettingsRequested.connect(self.paste_settings)
self.menu.sizeChanged.connect(self.editor_size_changed)
self.menu.snapValuesChanged.connect(self.snap_value_changed)
self.menu.useSnapToggled.connect(self.use_snap)
method = self.shape_editor.set_lock_background_shape
self.menu.lockBackgroundShapeToggled.connect(method)
width, height = self.options['width'], self.options['height']
self.menu.set_size_values(width, height)
self.menu.undoRequested.connect(self.undo)
self.menu.redoRequested.connect(self.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)
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_editor, self.undo)
set_shortcut("Ctrl+Y", self.shape_editor, self.redo)
set_shortcut("Ctrl+C", self.shape_editor, self.copy)
set_shortcut("Ctrl+V", self.shape_editor, self.paste)
set_shortcut("Ctrl+R", self.shape_editor, self.search_and_replace)
set_shortcut("del", self.shape_editor, self.delete_selection)
set_shortcut("Ctrl+D", self.shape_editor, self.deselect_all)
set_shortcut("Ctrl+A", self.shape_editor, self.select_all)
set_shortcut("Ctrl+I", self.shape_editor, self.invert_selection)
for direction in ['Left', 'Right', 'Up', 'Down']:
method = partial(self.move_selection, direction)
shortcut = set_shortcut(direction, self.shape_editor, method)
shortcut.setAutoRepeat(True)
self.attribute_editor = AttributeEditor()
self.attribute_editor.set_generals(self.options)
self.attribute_editor.generals.set_shapes(self.shape_editor.shapes)
self.attribute_editor.generalOptionSet.connect(self.generals_modified)
self.attribute_editor.optionSet.connect(self.option_set)
self.attribute_editor.rectModified.connect(self.rect_modified)
self.attribute_editor.imageModified.connect(self.image_modified)
self.attribute_editor.removeLayer.connect(self.remove_layer)
self.attribute_editor.selectLayerContent.connect(self.select_layer)
self.hlayout = QtWidgets.QHBoxLayout()
self.hlayout.setSizeConstraint(QtWidgets.QLayout.SetMaximumSize)
self.hlayout.setContentsMargins(0, 0, 0, 0)
self.hlayout.addStretch(1)
self.hlayout.addWidget(self.scrollarea)
self.hlayout.addStretch(1)
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 copy(self):
clipboard.set([
s.options.copy() for s in self.shape_editor.selection])
def copy_settings(self):
if len(self.shape_editor.selection) != 1:
return warning('Copy settings', 'Please select only one shape')
shape = self.shape_editor.selection[0]
clipboard.set_settings(shape.options.copy())
def sizeHint(self):
return QtCore.QSize(1300, 750)
def paste(self):
clipboad_copy = [s.copy() for s in clipboard.get()]
shape_datas = self.picker_data()['shapes'][:] + clipboad_copy
picker_data = {
'general': self.options,
'shapes': shape_datas}
self.set_picker_data(picker_data)
self.undo_manager.set_data_modified(picker_data)
self.pickerDataModified.emit(picker_data)
# select new shapes
shapes = self.shape_editor.shapes[-len(clipboard.get()):]
self.shape_editor.selection.replace(shapes)
self.shape_editor.update_selection()
self.shape_editor.repaint()
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_editor.selection:
shape.options.update(settings)
shape.rect = get_shape_rect_from_options(shape.options)
shape.synchronize_image()
self.set_data_modified()
self.selection_changed()
self.shape_editor.update_selection()
self.shape_editor.repaint()
def undo(self):
result = self.undo_manager.undo()
if result is False:
return
self.update_undo_manager()
def redo(self):
self.undo_manager.redo()
self.update_undo_manager()
def update_undo_manager(self):
data = self.undo_manager.data
self.set_picker_data(data)
self.pickerDataModified.emit(self.picker_data())
self.attribute_editor.generals.set_shapes(self.shape_editor.shapes)
def deselect_all(self):
self.shape_editor.selection.clear()
self.shape_editor.update_selection()
self.shape_editor.repaint()
def select_all(self):
shapes = self.shape_editor.list_shapes()
self.shape_editor.selection.add(shapes)
self.shape_editor.update_selection()
self.shape_editor.repaint()
def invert_selection(self):
self.shape_editor.selection.invert(self.shape_editor.shapes)
if self.menu.lock_bg.isChecked():
shapes = [
s for s in self.shape_editor.selection
if not s.is_background()]
self.shape_editor.selection.set(shapes)
self.shape_editor.update_selection()
self.shape_editor.repaint()
def set_data_modified(self):
self.undo_manager.set_data_modified(self.picker_data())
self.pickerDataModified.emit(self.picker_data())
def use_snap(self, state):
snap = self.menu.snap_values() if state else None
self.shape_editor.transform.snap = snap
self.shape_editor.repaint()
def snap_value_changed(self):
self.shape_editor.transform.snap = self.menu.snap_values()
self.set_data_modified()
self.shape_editor.repaint()
def generals_modified(self, key, value):
self.options[key] = value
if key == 'name':
title = "Picker editor - " + self.options['name']
self.setWindowTitle(title)
self.pickerDataModified.emit(self.picker_data())
def option_set(self, option, value):
for shape in self.shape_editor.selection:
shape.options[option] = value
self.shape_editor.repaint()
self.set_data_modified()
if option == 'visibility_layer':
self.attribute_editor.generals.set_shapes(self.shape_editor.shapes)
def editor_size_changed(self):
size = self.menu.get_size()
self.shape_editor.setFixedSize(size)
self.options['width'] = size.width()
self.options['height'] = size.height()
self.set_data_modified()
def rect_modified(self, option, value):
shapes = self.shape_editor.selection
for shape in shapes:
shape.options[option] = value
if option == 'shape.height':
shape.rect.setHeight(value)
shape.synchronize_image()
continue
elif option == 'shape.width':
shape.rect.setWidth(value)
shape.synchronize_image()
continue
width = shape.rect.width()
height = shape.rect.height()
if option == 'shape.left':
shape.rect.setLeft(value)
else:
shape.rect.setTop(value)
shape.rect.setWidth(width)
shape.rect.setHeight(height)
shape.synchronize_image()
self.update_manipulator_rect()
self.set_data_modified()
def selection_changed(self):
shapes = self.shape_editor.selection
options = [shape.options for shape in shapes]
self.attribute_editor.set_options(options)
def frame_shapes(self):
shapes = self.shape_editor.shapes
width = self.options['width']
height = self.options['height']
frame_shapes(shapes)
width = int(ceil(max(s.rect.right() for s in shapes)))
height = int(ceil(max(s.rect.bottom() for s in shapes)))
self.shape_editor.repaint()
self.update_manipulator_rect()
# This mark data as changed, no need to repeat.
self.menu.set_size_values(width, height)
def create_shape(
self, template, before=False, position=None, targets=None):
options = template.copy()
shape = Shape(options)
if not position:
shape.rect.moveCenter(self.shape_editor.rect().center())
else:
shape.rect.moveTopLeft(position)
if targets:
shape.set_targets(targets)
shape.synchronize_rect()
if before is True:
self.shape_editor.shapes.insert(0, shape)
else:
self.shape_editor.shapes.append(shape)
self.shape_editor.repaint()
self.set_data_modified()
def update_targets(self, shape):
# shape = self.shape_editor.selection[0]
shape.set_targets(cmds.ls(selection=True))
self.shape_editor.repaint()
self.set_data_modified()
def image_modified(self):
for shape in self.shape_editor.selection:
shape.synchronize_image()
self.shape_editor.repaint()
def set_selection_move_down(self):
array = self.shape_editor.shapes
elements = self.shape_editor.selection
move_down_array_elements(array, elements)
self.shape_editor.repaint()
self.set_data_modified()
def set_selection_move_up(self):
array = self.shape_editor.shapes
elements = self.shape_editor.selection
move_up_array_elements(array, elements)
self.shape_editor.repaint()
self.set_data_modified()
def set_selection_on_top(self):
array = self.shape_editor.shapes
elements = self.shape_editor.selection
self.shape_editor.shapes = move_elements_to_array_end(array, elements)
self.shape_editor.repaint()
self.set_data_modified()
def set_selection_on_bottom(self):
array = self.shape_editor.shapes
elements = self.shape_editor.selection
shapes = move_elements_to_array_begin(array, elements)
self.shape_editor.shapes = shapes
self.shape_editor.repaint()
self.set_data_modified()
def delete_selection(self):
for shape in reversed(self.shape_editor.selection.shapes):
self.shape_editor.shapes.remove(shape)
self.shape_editor.selection.remove(shape)
self.update_manipulator_rect()
self.set_data_modified()
def update_manipulator_rect(self):
rects = [shape.rect for shape in self.shape_editor.selection]
rect = get_combined_rects(rects)
self.shape_editor.manipulator.set_rect(rect)
self.shape_editor.repaint()
def picker_data(self):
return {
'general': self.options,
'shapes': [shape.options for shape in self.shape_editor.shapes]}
def set_picker_data(self, picker_data, reset_stacks=False):
self.options = picker_data['general']
self.shape_editor.options = self.options
shapes = [Shape(options) for options in picker_data['shapes']]
self.shape_editor.shapes = shapes
self.shape_editor.manipulator.set_rect(None)
self.shape_editor.repaint()
if reset_stacks is True:
self.undo_manager.reset_stacks()
def do_symmetry(self, horizontal=True):
shapes = self.shape_editor.selection.shapes
for shape in shapes:
rect_symmetry(
rect=shape.rect,
point=self.shape_editor.manipulator.rect.center(),
horizontal=horizontal)
shape.synchronize_rect()
self.shape_editor.repaint()
if not cmds.optionVar(query=TRIGGER_REPLACE_ON_MIRROR):
self.set_data_modified()
return
if not self.search_and_replace():
self.set_data_modified()
def search_and_replace(self):
dialog = SearchAndReplaceDialog()
if not dialog.exec_():
return False
if dialog.filter == 0: # Search on all shapes.
shapes = self.shape_editor.shapes
else:
shapes = self.shape_editor.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.set_data_modified()
self.shape_editor.repaint()
return True
def move_selection(self, direction):
offset = DIRECTION_OFFSETS[direction]
rects = (s.rect for s in self.shape_editor.selection)
rects = (s.rect for s in self.shape_editor.selection)
rect = self.shape_editor.manipulator.rect
reference_rect = QtCore.QRect(rect)
self.shape_editor.transform.set_rect(rect)
self.shape_editor.transform.reference_rect = reference_rect
self.shape_editor.transform.shift(rects, offset)
self.shape_editor.manipulator.update_geometries()
for shape in self.shape_editor.selection:
shape.synchronize_rect()
self.shape_editor.repaint()
self.shape_editor.selectedShapesChanged.emit()
self.pickerDataModified.emit(self.picker_data())
def align_selection(self, direction):
if not self.shape_editor.selection:
return
align_shapes(self.shape_editor.selection, direction)
rects = [s.rect for s in self.shape_editor.selection]
self.shape_editor.manipulator.set_rect(get_combined_rects(rects))
self.shape_editor.manipulator.update_geometries()
self.shape_editor.repaint()
self.shape_editor.selectedShapesChanged.emit()
self.pickerDataModified.emit(self.picker_data())
def arrange_selection(self, direction):
if not self.shape_editor.selection:
return
if direction == 'horizontal':
arrange_horizontal(self.shape_editor.selection)
else:
arrange_vertical(self.shape_editor.selection)
rects = [s.rect for s in self.shape_editor.selection]
self.shape_editor.manipulator.set_rect(get_combined_rects(rects))
self.shape_editor.manipulator.update_geometries()
self.shape_editor.repaint()
self.shape_editor.selectedShapesChanged.emit()
self.pickerDataModified.emit(self.picker_data())
def call_context_menu(self, position):
targets = cmds.ls(selection=True)
button = QtWidgets.QAction('Add selection button', self)
method = partial(
self.create_shape, BUTTON.copy(),
position=position, targets=targets)
button.triggered.connect(method)
template = BUTTON.copy()
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)
cursor = get_cursor(self.shape_editor)
shape = self.shape_editor.get_hovered_shape(cursor)
method = partial(self.update_targets, shape)
text = 'Update targets'
button3 = QtWidgets.QAction(text, self)
button3.setEnabled(bool(shape))
button3.triggered.connect(method)
menu = QtWidgets.QMenu()
menu.addAction(button)
menu.addAction(button2)
menu.addAction(button3)
menu.addSection('Visibility Layers')
layers = sorted(list({
s.visibility_layer()
for s in self.shape_editor.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_editor.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_editor.selection.shapes))
menu.addAction(create_layer)
menu.exec_(self.shape_editor.mapToGlobal(position))
def set_visibility_layer(self, layer=''):
for shape in self.shape_editor.selection:
shape.options['visibility_layer'] = layer
self.layers_modified()
def layers_modified(self):
self.set_data_modified()
self.attribute_editor.generals.set_shapes(self.shape_editor.shapes)
self.selection_changed()
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_editor.selection:
shape.options['visibility_layer'] = text
self.layers_modified()
def select_layer(self, layer):
shapes = [
shape for shape in self.shape_editor.shapes
if shape.visibility_layer() == layer]
self.shape_editor.selection.set(shapes)
self.shape_editor.update_selection()
self.shape_editor.repaint()
self.selection_changed()
def remove_layer(self, layer):
for shape in self.shape_editor.shapes:
if shape.visibility_layer() == layer:
shape.options['visibility_layer'] = None
self.layers_modified()

View File

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

View File

@ -0,0 +1,101 @@
from PySide2 import QtWidgets, QtCore
class VisibilityLayersEditor(QtWidgets.QWidget):
removeLayer = QtCore.Signal(str)
selectLayerContent = QtCore.Signal(str)
def __init__(self, parent=None):
super(VisibilityLayersEditor, self).__init__(parent)
self.model = VisbilityLayersModel()
self.table = QtWidgets.QTableView()
self.table.horizontalHeader().setSectionResizeMode(
QtWidgets.QHeaderView.ResizeToContents)
self.table.verticalHeader().setSectionResizeMode(
QtWidgets.QHeaderView.ResizeToContents)
self.table.setShowGrid(False)
self.table.setAlternatingRowColors(True)
self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
self.table.setSelectionMode(
QtWidgets.QAbstractItemView.SingleSelection)
self.table.setModel(self.model)
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
return self.model.layers_data[indexes[0].row()][0]
def call_remove_layer(self):
layer = self.selected_layer()
if not layer:
return
self.removeLayer.emit(layer)
def call_select_layer(self):
layer = self.selected_layer()
if not layer:
return
self.selectLayerContent.emit(layer)
def set_shapes(self, shapes):
self.model.set_shapes(shapes)
class VisbilityLayersModel(QtCore.QAbstractTableModel):
HEADERS = 'name', 'shapes'
def __init__(self, parent=None):
super(VisbilityLayersModel, self).__init__(parent)
self.layers_data = []
def rowCount(self, _):
return len(self.layers_data)
def columnCount(self, _):
return len(self.HEADERS)
def set_shapes(self, shapes):
self.layoutAboutToBeChanged.emit()
data = {}
for shape in shapes:
if not shape.visibility_layer():
continue
data[shape.visibility_layer()] = data.setdefault(
shape.visibility_layer(), 0) + 1
self.layers_data = sorted(data.items())
self.layoutChanged.emit()
def headerData(self, section, orientation, role):
if orientation == QtCore.Qt.Vertical or role != QtCore.Qt.DisplayRole:
return
return self.HEADERS[section]
def data(self, index, role):
if not index.isValid():
return
if role == QtCore.Qt.TextAlignmentRole:
if index.column() == 1:
return QtCore.Qt.AlignCenter
if role != QtCore.Qt.DisplayRole:
return
if index.column() == 0:
return self.layers_data[index.row()][0]
if index.column() == 1:
return str(self.layers_data[index.row()][1])

View File

@ -0,0 +1,270 @@
from functools import partial
from maya import cmds
from PySide2 import QtGui, QtWidgets, QtCore
from dwpicker.optionvar import (
BG_LOCKED, SNAP_ITEMS, SNAP_GRID_X, SNAP_GRID_Y, save_optionvar)
from dwpicker.qtutils import icon
class MenuWidget(QtWidgets.QWidget):
addBackgroundRequested = QtCore.Signal()
addButtonRequested = QtCore.Signal()
addTextRequested = QtCore.Signal()
arrangeRequested = QtCore.Signal(str)
centerValuesChanged = QtCore.Signal(int, int)
copyRequested = QtCore.Signal()
copySettingsRequested = QtCore.Signal()
deleteRequested = QtCore.Signal()
editCenterToggled = QtCore.Signal(bool)
frameShapes = QtCore.Signal()
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()
sizeChanged = QtCore.Signal()
snapValuesChanged = QtCore.Signal()
symmetryRequested = QtCore.Signal(bool)
undoRequested = QtCore.Signal()
useSnapToggled = QtCore.Signal(bool)
alignRequested = QtCore.Signal(str)
def __init__(self, parent=None):
super(MenuWidget, self).__init__(parent=parent)
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)
validator = QtGui.QIntValidator()
self.picker_width = QtWidgets.QLineEdit('600')
self.picker_width.setFixedWidth(35)
self.picker_width.setValidator(validator)
self.picker_width.textEdited.connect(self.size_changed)
self.picker_height = QtWidgets.QLineEdit('300')
self.picker_height.setFixedWidth(35)
self.picker_height.setValidator(validator)
self.picker_height.textEdited.connect(self.size_changed)
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('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)
self.frame_shapes = QtWidgets.QAction(icon('frame.png'), '', self)
self.frame_shapes.setToolTip('Frame buttons')
self.frame_shapes.triggered.connect(self.frameShapes.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)
method = partial(self.symmetryRequested.emit, True)
self.hsymmetry.triggered.connect(method)
self.vsymmetry = QtWidgets.QAction(icon('v_symmetry.png'), '', self)
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)
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)
method = partial(self.alignRequested.emit, 'right')
self.align_right = QtWidgets.QAction(icon('align_right.png'), '', self)
self.align_right.triggered.connect(method)
method = partial(self.alignRequested.emit, 'top')
self.align_top = QtWidgets.QAction(icon('align_top.png'), '', self)
self.align_top.triggered.connect(method)
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)
file_ = 'align_bottom.png'
method = partial(self.alignRequested.emit, 'bottom')
self.align_bottom = QtWidgets.QAction(icon(file_), '', self)
self.align_bottom.triggered.connect(method)
file_ = 'arrange_h.png'
method = partial(self.arrangeRequested.emit, 'horizontal')
self.arrange_horizontal = QtWidgets.QAction(icon(file_), '', self)
self.arrange_horizontal.triggered.connect(method)
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.toolbar = QtWidgets.QToolBar()
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.addSeparator()
self.toolbar.addAction(self.snap)
self.toolbar.addWidget(self.snapx)
self.toolbar.addWidget(self.snapy)
self.toolbar.addSeparator()
self.toolbar.addWidget(QtWidgets.QLabel('size'))
self.toolbar.addWidget(self.picker_width)
self.toolbar.addWidget(self.picker_height)
self.toolbar.addSeparator()
self.toolbar.addAction(self.addbutton)
self.toolbar.addAction(self.addtext)
self.toolbar.addAction(self.addbg)
self.toolbar.addSeparator()
self.toolbar.addAction(self.frame_shapes)
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 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)))
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()))
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()
def set_size_values(self, width, height):
self.picker_width.setText(str(width))
self.picker_height.setText(str(height))
self.sizeChanged.emit()
def get_size(self):
width = int(self.picker_width.text()) if self.picker_width.text() else 1
height = int(self.picker_height.text()) if self.picker_height.text() else 1
return QtCore.QSize(width, height)

View File

@ -0,0 +1,445 @@
from functools import partial
import os
from PySide2 import QtWidgets, QtCore, QtGui
from maya import cmds
from dwpicker.designer.highlighter import get_highlighter
from dwpicker.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 dwpicker.languages import MEL, PYTHON
from dwpicker.path import get_image_directory
from dwpicker.namespace import selected_namespace
from dwpicker.templates import BUTTON
SEARCH_AND_REPLACE_FIELDS = 'Targets', 'Label', 'Image path', 'Command'
SHAPES_FILTERS = 'All shapes', 'Selected shapes'
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):
filename = QtWidgets.QFileDialog.getOpenFileName(
parent, "Repath image...",
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('B')
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.setFixedHeight(100)
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 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()}

View File

@ -0,0 +1,549 @@
import math
from PySide2 import QtCore
POINT_RADIUS = 8
POINT_OFFSET = 4
DIRECTIONS = [
'top_left',
'bottom_left',
'top_right',
'bottom_right',
'left',
'right',
'top',
'bottom']
class ViewportMapper():
"""
Used to translate/map between:
- abstract/data/units coordinates
- viewport/display/pixels coordinates
"""
def __init__(self):
self.zoom = 1
self.origin = QtCore.QPointF(0, 0)
# We need the viewport size to be able to center the view or to
# automatically set zoom from selection:
self.viewsize = QtCore.QSize(300, 300)
def to_viewport(self, value):
return value * self.zoom
def to_units(self, pixels):
return pixels / self.zoom
def to_viewport_coords(self, units_point):
return QtCore.QPointF(
self.to_viewport(units_point.x()) - self.origin.x(),
self.to_viewport(units_point.y()) - self.origin.y())
def to_units_coords(self, pixels_point):
return QtCore.QPointF(
self.to_units(pixels_point.x() + self.origin.x()),
self.to_units(pixels_point.y() + self.origin.y()))
def to_viewport_rect(self, units_rect):
return QtCore.QRectF(
(units_rect.left() * self.zoom) - self.origin.x(),
(units_rect.top() * self.zoom) - self.origin.y(),
units_rect.width() * self.zoom,
units_rect.height() * self.zoom)
def to_units_rect(self, pixels_rect):
top_left = self.to_units_coords(pixels_rect.topLeft())
width = self.to_units(pixels_rect.width())
height = self.to_units(pixels_rect.height())
return QtCore.QRectF(top_left.x(), top_left.y(), width, height)
def zoomin(self, factor=10.0):
self.zoom += self.zoom * factor
self.zoom = min(self.zoom, 5.0)
def zoomout(self, factor=10.0):
self.zoom -= self.zoom * factor
self.zoom = max(self.zoom, .1)
def center_on_point(self, units_center):
"""Given current zoom and viewport size, set the origin point."""
self.origin = QtCore.QPointF(
units_center.x() * self.zoom - self.viewsize.width() / 2,
units_center.y() * self.zoom - self.viewsize.height() / 2)
def focus(self, units_rect):
self.zoom = min([
float(self.viewsize.width()) / units_rect.width(),
float(self.viewsize.height()) / units_rect.height()])
if self.zoom > 1:
self.zoom *= 0.7 # lower zoom to add some breathing space
self.zoom = max(self.zoom, .1)
self.center_on_point(units_rect.center())
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 relative(value, in_min, in_max, out_min, out_max):
"""
this function resolve simple equation and return the unknown value
in between two values.
a, a" = in_min, out_min
b, b " = out_max, out_max
c = value
? is the unknown processed by function.
a --------- c --------- b
a" --------------- ? ---------------- b"
"""
factor = float((value - in_min)) / (in_max - in_min)
width = out_max - out_min
return out_min + (width * (factor))
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.QRect(left, top, width, height)
def resize_rect_with_reference(rect, in_reference_rect, out_reference_rect):
"""
__________________________________ B
| ________________ A |
| | | |
| |_______________| |
| |
|________________________________|
__________________________ C
| ? |
| |
|________________________|
A = rect given
B = in_reference_rect
C = out_reference_rect
the function process the fourth rect,
it scale the A rect using the B, C scales as reference
"""
left = relative(
value=rect.left(),
in_min=in_reference_rect.left(),
in_max=in_reference_rect.right(),
out_min=out_reference_rect.left(),
out_max=out_reference_rect.right())
top = relative(
value=rect.top(),
in_min=in_reference_rect.top(),
in_max=in_reference_rect.bottom(),
out_min=out_reference_rect.top(),
out_max=out_reference_rect.bottom())
right = relative(
value=rect.right(),
in_min=in_reference_rect.left(),
in_max=in_reference_rect.right(),
out_min=out_reference_rect.left(),
out_max=out_reference_rect.right())
bottom = relative(
value=rect.bottom(),
in_min=in_reference_rect.top(),
in_max=in_reference_rect.bottom(),
out_min=out_reference_rect.top(),
out_max=out_reference_rect.bottom())
rect.setCoords(left, top, right, bottom)
def resize_rect_with_direction(rect, cursor, direction, force_square=False):
if direction == 'top_left':
if cursor.x() < rect.right() and cursor.y() < rect.bottom():
rect.setTopLeft(cursor)
if force_square:
left = rect.right() - rect.height()
rect.setLeft(left)
elif direction == 'bottom_left':
if cursor.x() < rect.right() and cursor.y() > rect.top():
rect.setBottomLeft(cursor)
if force_square:
rect.setHeight(rect.width())
elif direction == 'top_right':
if cursor.x() > rect.left() and cursor.y() < rect.bottom():
rect.setTopRight(cursor)
if force_square:
rect.setWidth(rect.height())
elif direction == 'bottom_right':
if cursor.x() > rect.left() and cursor.y() > rect.top():
rect.setBottomRight(cursor)
if force_square:
rect.setHeight(rect.width())
elif direction == 'left':
if cursor.x() < rect.right():
rect.setLeft(cursor.x())
if force_square:
rect.setHeight(rect.width())
elif direction == 'right':
if cursor.x() > rect.left():
rect.setRight(cursor.x())
if force_square:
rect.setHeight(rect.width())
elif direction == 'top':
if cursor.y() < rect.bottom():
rect.setTop(cursor.y())
if force_square:
rect.setWidth(rect.height())
elif direction == 'bottom':
if cursor.y() > rect.top():
rect.setBottom(cursor.y())
if force_square:
rect.setWidth(rect.height())
class Transform:
def __init__(self):
self.snap = None
self.direction = None
self.rect = None
self.mode = None
self.square = False
self.reference_x = None
self.reference_y = None
self.reference_rect = None
def set_rect(self, rect):
if not isinstance(rect, QtCore.QRect):
raise ValueError()
self.rect = rect
if rect is None:
self.reference_x = None
self.reference_y = None
return
def set_reference_point(self, cursor):
self.reference_x = cursor.x() - self.rect.left()
self.reference_y = cursor.y() - self.rect.top()
def resize(self, rects, cursor):
if self.snap is not None:
x, y = snap(cursor.x(), cursor.y(), self.snap)
cursor.setX(x)
cursor.setY(y)
resize_rect_with_direction(
self.rect, cursor, self.direction, force_square=self.square)
self.apply_relative_transformation(rects)
def apply_relative_transformation(self, rects):
for rect in rects:
resize_rect_with_reference(
rect, self.reference_rect, self.rect)
self.reference_rect = QtCore.QRect(
self.rect.topLeft(), self.rect.size())
def move(self, rects, cursor):
x = cursor.x() - self.reference_x
y = cursor.y() - self.reference_y
if self.snap is not None:
x, y = snap(x, y, self.snap)
self.apply_topleft(rects, x, y)
def shift(self, rects, offset):
x, y = offset
if self.snap is not None:
x *= self.snap[0]
y *= self.snap[1]
x = self.rect.left() + x
y = self.rect.top() + y
if self.snap:
x, y = snap(x, y, self.snap)
self.apply_topleft(rects, x, y)
def apply_topleft(self, rects, x, y):
width = self.rect.width()
height = self.rect.height()
self.rect.setTopLeft(QtCore.QPoint(x, y))
self.rect.setWidth(width)
self.rect.setHeight(height)
self.apply_relative_transformation(rects)
def snap(x, y, snap):
x = snap[0] * round(x / snap[0])
y = snap[1] * round(y / snap[1])
return x, y
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.QRect(l, t, r-l, b-t)
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 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)]
if __name__ == "__main__":
assert split_range(0, 10, 11) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 784 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1020 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1 @@
from dwpicker.ingest.animschool.converter import convert

View File

@ -0,0 +1,122 @@
import json
import os
from PySide2 import QtGui
from dwpicker.templates import PICKER, BUTTON, BACKGROUND
from dwpicker.ingest.animschool.parser import parse_animschool_picker, save_png
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)
with open(dst, "w") as f:
json.dump(picker, f, indent=2)
def convert(filepath, directory=None):
directory = directory or os.path.dirname(filepath)
title, buttons, png_data = parse_animschool_picker(filepath)
picker_filename = os.path.splitext(os.path.basename(filepath))[0]
png_path = unique_filename(directory, picker_filename, 'png')
png_path = png_path if png_data else None
dst = unique_filename(directory, picker_filename, 'json')
if png_path:
save_png(png_data, png_path)
build_picker_from_pkr(title, buttons, png_path, dst)
return dst
def unique_filename(directory, filename, extension):
filepath = os.path.join(directory, filename) + '.' + extension
i = 0
while os.path.exists(filepath):
filepath = '{base}.{index}.{extension}'.format(
base=os.path.join(directory, filename),
index=str(i).zfill(3),
extension=extension)
i += 1
return filepath

View File

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

View File

@ -0,0 +1,213 @@
from PySide2 import QtCore, QtGui
from dwpicker.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, proportional_rect)
from dwpicker.languages import execute_code
from dwpicker.painting import (
draw_selection_square, draw_manipulator, get_hovered_path)
from dwpicker.path import expand_path
from dwpicker.selection import select_targets
EXCECUTION_WARNING = """\
Code execution failed for shape: "{name}"
{error}.
"""
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, rect):
if not rect or not self.rect:
return False
return self.rect.intersects(rect)
def draw(self, painter):
if self.rect is None:
return
draw_selection_square(painter, self.rect)
class Manipulator():
def __init__(self):
self._rect = None
self._is_hovered = False
self._tl_corner_rect = None
self._bl_corner_rect = None
self._tr_corner_rect = None
self._br_corner_rect = None
self._l_side_rect = None
self._r_side_rect = None
self._t_side_rect = None
self._b_side_rect = None
self.hovered_path = None
@property
def rect(self):
return self._rect
def handler_rects(self):
return [
self._tl_corner_rect, self._bl_corner_rect, self._tr_corner_rect,
self._br_corner_rect, self._l_side_rect, self._r_side_rect,
self._t_side_rect, self._b_side_rect]
def get_direction(self, cursor):
if self.rect is None:
return None
for i, rect in enumerate(self.handler_rects()):
if rect.contains(cursor):
return DIRECTIONS[i]
def hovered_rects(self, cursor):
rects = []
for rect in self.handler_rects() + [self.rect]:
if not rect:
continue
if rect.contains(cursor):
rects.append(rect)
return rects
def set_rect(self, rect):
self._rect = rect
self.update_geometries()
def update_geometries(self):
rect = self.rect
self._tl_corner_rect = get_topleft_rect(rect) if rect else None
self._bl_corner_rect = get_bottomleft_rect(rect) if rect else None
self._tr_corner_rect = get_topright_rect(rect) if rect else None
self._br_corner_rect = get_bottomright_rect(rect) if rect else None
self._l_side_rect = get_left_side_rect(rect) if rect else None
self._r_side_rect = get_right_side_rect(rect) if rect else None
self._t_side_rect = get_top_side_rect(rect) if rect else None
self._b_side_rect = get_bottom_side_rect(rect) if rect else None
self.hovered_path = get_hovered_path(rect) if rect else None
def draw(self, painter, cursor):
if self.rect is not None and all(self.handler_rects()):
draw_manipulator(painter, self, cursor)
def get_shape_rect_from_options(options):
return QtCore.QRectF(
options['shape.left'],
options['shape.top'],
options['shape.width'],
options['shape.height'])
class Shape():
def __init__(self, options):
self.hovered = False
self.clicked = False
self.selected = False
self.options = options
self.rect = get_shape_rect_from_options(options)
self.pixmap = None
self.image_rect = None
self.synchronize_image()
def set_hovered(self, cursor):
self.hovered = self.rect.contains(cursor)
def set_clicked(self, cursor):
self.clicked = self.rect.contains(cursor)
def release(self, cursor):
self.clicked = False
self.hovered = self.rect.contains(cursor)
def synchronize_rect(self):
self.options['shape.left'] = self.rect.left()
self.options['shape.top'] = self.rect.top()
self.options['shape.width'] = self.rect.width()
self.options['shape.height'] = self.rect.height()
def content_rect(self):
if self.options['shape'] == 'round':
return proportional_rect(self.rect, 70)
return self.rect
def execute(self, button, shift=False, ctrl=False):
commands = _find_commands(
self.options['action.commands'],
button, shift=shift, ctrl=ctrl)
for command in commands:
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(EXCECUTION_WARNING.format(
name=self.options['text.content'], error=e))
print(traceback.format_exc())
def select(self, selection_mode='replace'):
select_targets([self], selection_mode=selection_mode)
def targets(self):
return self.options['action.targets']
def set_targets(self, targets):
self.options['action.targets'] = targets
def is_interactive(self):
return bool(
[c for c in self.options['action.commands'] if c['enabled']])
def is_background(self):
return not any([
bool(self.targets()),
bool(self.options['action.commands'])])
def visibility_layer(self):
return self.options['visibility_layer']
def synchronize_image(self):
path = expand_path(self.options['image.path'])
self.pixmap = QtGui.QPixmap(path)
if self.options['image.fit'] is True:
self.image_rect = None
return
self.image_rect = QtCore.QRectF(
self.rect.left(),
self.rect.top(),
self.options['image.width'],
self.options['image.height'])
self.image_rect.moveCenter(self.rect.center())
def _find_commands(commands, button, ctrl=False, shift=False):
result = []
for command in commands:
conditions = (
command['button'] == button and
command['ctrl'] == ctrl and
command['shift'] == shift)
if conditions:
result.append(command)
return result

View File

@ -0,0 +1,52 @@
PYTHON = 'python'
MEL = 'mel'
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;
"""
def execute_code(language, code, deferred=False, compact_undo=False):
return EXECUTORS[language](code, deferred, compact_undo)
def execute_python(code, deferred=False, compact_undo=False):
if compact_undo:
code = STACK_UNDO_PYTHON.format(code=code)
if deferred:
code = DEFERRED_PYTHON.format(code=code)
exec(code, globals())
def execute_mel(code, 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)
mel.eval(code.replace(u'\u2029', '\n'))
EXECUTORS = {
PYTHON: execute_python,
MEL: execute_mel,
}

View File

@ -0,0 +1,865 @@
# -*- coding: utf-8 -*-
import os
import json
import webbrowser
from copy import deepcopy
from functools import partial
from PySide2 import QtWidgets, QtCore, QtGui
from maya import cmds
import maya.OpenMaya as om
from dwpicker.appinfos import VERSION, RELEASE_DATE, DW_GITHUB, DW_WEBSITE
from dwpicker.compatibility import ensure_retro_compatibility
from dwpicker.designer.editor import PickerEditor
from dwpicker.dialog import CommandEditorDialog
from dwpicker.dialog import (
warning, question, get_image_path, NamespaceDialog)
from dwpicker.ingest import animschool
from dwpicker.interactive import Shape
from dwpicker.hotkeys import get_hotkeys_config
from dwpicker.namespace import (
switch_namespace, selected_namespace, detect_picker_namespace,
pickers_namespaces)
from dwpicker.optionvar import (
AUTO_FOCUS_BEHAVIOR, AUTO_SWITCH_TAB, CHECK_IMAGES_PATHS,
AUTO_SET_NAMESPACE, DISABLE_IMPORT_CALLBACKS,
DISPLAY_QUICK_OPTIONS, INSERT_TAB_AFTER_CURRENT, LAST_OPEN_DIRECTORY,
LAST_IMPORT_DIRECTORY, LAST_COMMAND_LANGUAGE, LAST_SAVE_DIRECTORY,
NAMESPACE_TOOLBAR, USE_ICON_FOR_UNSAVED_TAB, WARN_ON_TAB_CLOSED,
save_optionvar, append_recent_filename, save_opened_filenames)
from dwpicker.path import get_import_directory, get_open_directory
from dwpicker.picker import PickerView, list_targets
from dwpicker.preference import PreferencesWindow
from dwpicker.qtutils import set_shortcut, icon, maya_main_window, DockableBase
from dwpicker.quick import QuickOptions
from dwpicker.references import ensure_images_path_exists
from dwpicker.scenedata import (
load_local_picker_data, store_local_picker_data,
clean_stray_picker_holder_nodes)
from dwpicker.templates import BUTTON, PICKER, BACKGROUND, COMMAND
from dwpicker.undo import UndoManager
ABOUT = """\
DreamWall Picker
Licence MIT
Version: {version}
Release date: {release}
Authors: Lionel Brouyère, Olivier Evers
Contributor(s): Herizoran
Features:
Animation picker widget.
Quick picker creation.
Advanced picker editin.
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 ?"""
def build_multiple_shapes(targets, override):
shapes = [BUTTON.copy() for _ in range(len(targets))]
for shape, target in zip(shapes, targets):
if override:
shape.update(override)
shape['action.targets'] = [target]
return [Shape(shape) for shape in shapes]
class DwPicker(DockableBase, QtWidgets.QWidget):
def __init__(self):
super(DwPicker, self).__init__(control_name=WINDOW_CONTROL_NAME)
self.setWindowTitle(WINDOW_TITLE)
self.shortcuts = {}
self.editable = True
self.callbacks = []
self.stored_focus = None
self.editors = []
self.generals = []
self.undo_managers = []
self.pickers = []
self.filenames = []
self.modified_states = []
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.namespace_label = QtWidgets.QLabel("Namespace: ")
self.namespace_combo = QtWidgets.QComboBox()
self.namespace_combo.setFixedWidth(235)
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.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)
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.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),
}
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 update_namespaces(self, *_):
namespaces = sorted(list(set(
(cmds.namespaceInfo(listOnlyNamespaces=True, recurse=True)) +
(pickers_namespaces(self.pickers)))))
self.namespace_combo.blockSignals(True)
self.namespace_combo.clear()
self.namespace_combo.addItem("*Root*")
self.namespace_combo.addItems(namespaces)
self.namespace_combo.blockSignals(False)
def tab_index_changed(self, index):
if not self.pickers:
return
picker = self.pickers[index]
if not picker:
return
namespace = detect_picker_namespace(picker.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):
lists = (
self.editors,
self.generals,
self.pickers,
self.filenames,
self.modified_states)
for l in lists:
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([fn for fn in self.filenames if fn])
if not any(self.modified_states):
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),
button=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(self.filenames)
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.shapes)
if nodes[-1] in targets:
return
for i, picker in enumerate(self.pickers):
if nodes[-1] in list_targets(picker.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):
picker = 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.picker_data(i) 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),
button=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.modified_states[index] 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.generals.pop(index)
self.modified_states.pop(index)
self.undo_managers.pop(index)
self.filenames.pop(index)
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()
for i in range(self.tab.count()):
self.set_modified_state(i, self.modified_states[i])
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 create_picker(self, data):
picker = PickerView()
picker.editable = self.editable
picker.register_callbacks()
picker.addButtonRequested.connect(self.add_button)
picker.updateButtonRequested.connect(self.update_button)
picker.deleteButtonRequested.connect(self.delete_buttons)
if self.editable:
method = partial(self.data_changed_from_picker, picker)
picker.dataChanged.connect(method)
shapes = [Shape(s) for s in data['shapes']]
picker.set_shapes(shapes)
picker.reset()
picker.zoom_locked = data['general']['zoom_locked']
return picker
def add_picker(self, data, filename=None, modified_state=False):
picker = self.create_picker(data)
insert = cmds.optionVar(query=INSERT_TAB_AFTER_CURRENT)
if not insert or self.tab.currentIndex() == self.tab.count() - 1:
self.generals.append(data['general'])
self.pickers.append(picker)
self.editors.append(None)
self.undo_managers.append(UndoManager(data))
self.filenames.append(filename)
self.modified_states.append(modified_state)
self.tab.addTab(picker, data['general']['name'])
self.tab.setCurrentIndex(self.tab.count() - 1)
else:
index = self.tab.currentIndex() + 1
self.generals.insert(index, data['general'])
self.pickers.insert(index, picker)
self.editors.insert(index, None)
self.undo_managers.insert(index, UndoManager(data))
self.filenames.insert(index, filename)
self.modified_states.insert(index, modified_state)
self.tab.insertTab(index, picker, data['general']['name'])
self.tab.setCurrentIndex(index)
picker.reset()
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.filenames[index]
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
undo_manager = self.undo_managers[index]
undo_manager.undo()
self.data_changed_from_undo_manager(index)
def call_redo(self):
index = self.tab.currentIndex()
if index < 0:
return
undo_manager = self.undo_managers[index]
undo_manager.redo()
self.data_changed_from_undo_manager(index)
def save_picker(self, index, filename):
self.filenames[index] = filename
save_optionvar(LAST_SAVE_DIRECTORY, os.path.dirname(filename))
append_recent_filename(filename)
with open(filename, 'w') as f:
json.dump(self.picker_data(index), 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': PICKER.copy(),
'shapes': []})
self.store_local_pickers_data()
def picker_data(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 {
'version': VERSION,
'general': self.generals[index],
'shapes': [shape.options for shape in picker.shapes]}
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:
data = self.picker_data()
undo_manager = self.undo_managers[index]
editor = PickerEditor(
picker_data=data,
undo_manager=undo_manager,
parent=self)
picker = self.pickers[index]
method = partial(self.data_changed_from_editor, picker=picker)
editor.pickerDataModified.connect(method)
self.editors[index] = editor
self.editors[index].show()
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 set_modified_state(self, index, state):
"""
Update the tab icon. Add a "save" icon if tab contains unsaved
modifications.
"""
if not self.filenames[index]:
return
self.modified_states[index] = 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.generals[index]['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_about(self):
QtWidgets.QMessageBox.about(self, 'About', ABOUT)
def sizeHint(self):
return QtCore.QSize(500, 800)
def add_button(self, x, y, button_type):
targets = cmds.ls(selection=True)
if not targets and button_type <= 1:
return warning("Warning", "No targets selected")
if button_type == 1:
overrides = self.quick_options.values
shapes = build_multiple_shapes(targets, overrides)
if not shapes:
return
picker = self.tab.currentWidget()
picker.drag_shapes = shapes
return
data = BUTTON.copy()
data['shape.left'] = x
data['shape.top'] = y
data.update(self.quick_options.values)
if button_type == 0:
data['action.targets'] = targets
else:
text, result = (
QtWidgets.QInputDialog.getText(self, 'Button text', 'text'))
if not result:
return
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)
data['action.commands'] = [command]
width = max([data['shape.width'], len(data['text.content']) * 7])
data['shape.width'] = width
self.add_shape_to_current_picker(Shape(data))
def update_button(self, shape):
picker = self.tab.currentWidget()
shape.set_targets(cmds.ls(selection=True))
self.data_changed_from_picker(picker)
def delete_buttons(self):
picker = self.tab.currentWidget()
selected_shapes = [s for s in picker.shapes if s.selected]
for shape in selected_shapes:
picker.shapes.remove(shape)
self.data_changed_from_picker(picker)
def add_shape_to_current_picker(self, shape, prepend=False):
picker = self.tab.currentWidget()
if prepend:
picker.shapes.insert(0, shape)
else:
picker.shapes.append(shape)
self.data_changed_from_picker(picker)
def data_changed_from_picker(self, picker):
index = self.tab.indexOf(picker)
data = self.picker_data(index)
if self.editors[index]:
self.editors[index].set_picker_data(data)
self.set_modified_state(index, True)
picker.repaint()
self.undo_managers[index].set_data_modified(data)
self.store_local_pickers_data()
def data_changed_from_editor(self, data, picker):
index = self.tab.indexOf(picker)
self.generals[index] = data['general']
shapes = [Shape(s) for s in data['shapes']]
picker.set_shapes(shapes)
picker.zoom_locked = data['general']['zoom_locked']
self.set_title(index, data['general']['name'])
self.set_modified_state(index, True)
self.store_local_pickers_data()
def data_changed_from_undo_manager(self, index):
data = self.undo_managers[index].data
if self.editors[index]:
self.editors[index].set_picker_data(data)
self.data_changed_from_editor(data, self.pickers[index])
def change_title(self, index=None):
if not self.editable:
return
index = self.tab.currentIndex() if type(index) is not int else index
if index < 0:
return
title, operate = QtWidgets.QInputDialog.getText(
None, 'Change picker title', 'New title')
if not operate:
return
self.set_title(index, title)
self.data_changed_from_picker(self.tab.widget(index))
def set_title(self, index, title):
self.generals[index]['name'] = title
use_icon = cmds.optionVar(query=USE_ICON_FOR_UNSAVED_TAB)
if not use_icon and self.modified_states[index]:
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):
picker = self.tab.currentWidget()
for shape in picker.shapes:
if not shape.targets():
continue
targets = [switch_namespace(t, namespace) for t in shape.targets()]
shape.options['action.targets'] = targets
self.data_changed_from_picker(picker)
def add_background(self):
filename = get_image_path(self)
if not filename:
return
shape = BACKGROUND.copy()
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
shape = Shape(shape)
self.add_shape_to_current_picker(shape, prepend=True)
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)
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.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.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.tools)
self.help.addAction(self.dw)
self.help.addSeparator()
self.help.addAction(self.about)
self.addMenu(self.file)
self.addMenu(self.edit)
self.addMenu(self.help)
def set_editable(self, state):
self.undo.setEnabled(state)
self.redo.setEnabled(state)
self.change_title.setEnabled(state)
self.advanced_edit.setEnabled(state)
self.add_background.setEnabled(state)

View File

@ -0,0 +1,65 @@
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.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]
@contextmanager
def maya_namespace(
namespace='', create_if_missing=True, restore_current_namespace=True):
"""Context manager to temporarily set a namespace"""
initial_namespace = ':' + cmds.namespaceInfo(currentNamespace=True)
if not namespace.startswith(':'):
namespace = ':' + namespace
try:
if not cmds.namespace(absoluteName=True, exists=namespace):
if create_if_missing:
cmds.namespace(setNamespace=':')
namespace = cmds.namespace(addNamespace=namespace)
else:
cmds.namespace(initial_namespace)
raise ValueError(namespace + " doesn't exist.")
cmds.namespace(setNamespace=namespace)
yield namespace
finally:
if restore_current_namespace:
cmds.namespace(setNamespace=initial_namespace)
def switch_namespace(name, namespace):
basename = name.split("|")[-1]
name = basename if ":" not in basename else basename.split(":")[-1]
if not namespace:
return name
return namespace + ":" + name
def selected_namespace():
selection = cmds.ls(selection=True)
if not selection:
return ":"
node = selection[0]
basename = node.split("|")[-1]
if ":" not in node:
return None
return basename.split(":")[0]

View File

@ -0,0 +1,159 @@
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_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'
OVERRIDE_PROD_PICKER_DIRECTORY_ENV = 'dwpicker_override_picker_directory_env'
INSERT_TAB_AFTER_CURRENT = 'dwpicker_insert_tab_after_current'
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'
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'
OPTIONVARS = {
AUTO_FOCUS_BEHAVIOR: AUTO_FOCUS_BEHAVIORS[-1],
AUTO_SWITCH_TAB: 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: int(cmds.about(majorVersion=True) != '2023'),
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'),
DEFAULT_WIDTH: 30,
DISABLE_IMPORT_CALLBACKS: 1,
DISPLAY_QUICK_OPTIONS: 1,
OVERRIDE_PROD_PICKER_DIRECTORY_ENV: 0,
INSERT_TAB_AFTER_CURRENT: 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,
SETTINGS_GROUP_TO_COPY: 'bordercolor;text;image;bgcolor;shape;borderwidth;border',
SETTINGS_TO_COPY: (
'bgcolor.clicked;bgcolor.hovered;bgcolor.normal;bgcolor.transparency;'
'border;bordercolor.clicked;bordercolor.hovered;bordercolor.normal;'
'bordercolor.transparency;borderwidth.clicked;borderwidth.hovered;'
'borderwidth.normal;image.fit;image.height;image.width;shape;'
'shape.cornersx;shape.cornersy;shape.height;shape.left;'
'shape.top;shape.width;text.bold;text.color;text.halign;text.italic;'
'text.size;text.valign'),
SNAP_ITEMS: 0,
SNAP_GRID_X: 10,
SNAP_GRID_Y: 10,
SYNCHRONYZE_SELECTION: 1,
TRIGGER_REPLACE_ON_MIRROR: 0,
USE_BASE64_DATA_ENCODING: 0,
USE_ICON_FOR_UNSAVED_TAB: 1,
USE_PROD_PICKER_DIR_AS_DEFAULT: 0,
WARN_ON_TAB_CLOSED: 0,
ZOOM_BUTTON: ZOOM_BUTTONS[2],
ZOOM_SENSITIVITY: 50
}
TYPES = {
int: 'intValue',
float: 'floatValue',
str: 'stringValue'}
# Ensure backward compatibility.
if sys.version_info[0] == 2:
TYPES[unicode] = 'stringValue'
def ensure_optionvars_exists():
for optionvar, default_value in OPTIONVARS.items():
if cmds.optionVar(exists=optionvar):
continue
save_optionvar(optionvar, default_value)
def save_optionvar(optionvar, value):
kwargs = {TYPES.get(type(value)): [optionvar, value]}
cmds.optionVar(**kwargs)
def save_opened_filenames(filenames):
save_optionvar(OPENED_FILES, ";".join(filenames))
def append_recent_filename(filename):
filename = os.path.normpath(filename)
stored_filenames = cmds.optionVar(query=RECENT_FILES)
if not stored_filenames:
cmds.optionVar(stringValue=[RECENT_FILES, filename + ';'])
return
# Just reorder list if the filename is already in the recent filenames.
stored_filenames = stored_filenames.split(';')
for stored_filename in stored_filenames:
if os.path.normpath(stored_filename) == filename:
stored_filenames.remove(stored_filename)
stored_filenames.insert(0, filename)
cmds.optionVar(
stringValue=[RECENT_FILES, ';'.join(stored_filenames)])
return
# Append to list if new filename.
if len(stored_filenames) >= 10:
stored_filenames = stored_filenames[:9]
stored_filenames.insert(0, filename)
cmds.optionVar(stringValue=[RECENT_FILES, ';'.join(stored_filenames)])

View File

@ -0,0 +1,152 @@
from PySide2 import QtCore, QtGui
from maya import cmds
from dwpicker.optionvar import ZOOM_SENSITIVITY
from dwpicker.qtutils import VALIGNS, HALIGNS
from dwpicker.geometry import grow_rect, ViewportMapper
SELECTION_COLOR = '#3388FF'
MANIPULATOR_BORDER = 5
def factor_sensitivity(factor):
sensitivity = cmds.optionVar(query=ZOOM_SENSITIVITY) / 50.0
return factor * sensitivity
def draw_editor(painter, rect, snap=None, viewportmapper=None):
viewportmapper = viewportmapper or ViewportMapper()
rect = viewportmapper.to_viewport_rect(rect)
# draw border
pen = QtGui.QPen(QtGui.QColor('#333333'))
pen.setStyle(QtCore.Qt.DashDotLine)
pen.setWidthF(viewportmapper.to_viewport(3))
brush = QtGui.QBrush(QtGui.QColor(255, 255, 255, 25))
painter.setPen(pen)
painter.setBrush(brush)
painter.drawRect(rect)
if snap is None:
return
# draw snap grid
snap = viewportmapper.to_viewport(snap[0]), viewportmapper.to_viewport(snap[1])
pen = QtGui.QPen(QtGui.QColor('red'))
painter.setPen(pen)
x = 0
y = 0
while y < rect.bottom():
painter.drawPoint(x, y)
x += snap[0]
if x > rect.right():
x = 0
y += snap[1]
def draw_shape(painter, shape, viewportmapper=None):
viewportmapper = viewportmapper or ViewportMapper()
options = shape.options
content_rect = shape.content_rect()
if shape.clicked or shape.selected:
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)
pen.setWidthF(viewportmapper.to_viewport(bordersize))
painter.setPen(pen)
painter.setBrush(QtGui.QBrush(backgroundcolor))
rect = viewportmapper.to_viewport_rect(shape.rect)
if options['shape'] == 'square':
painter.drawRect(rect)
elif options['shape'] == 'round':
painter.drawEllipse(rect)
else: # 'rounded_rect'
x = viewportmapper.to_viewport(options['shape.cornersx'])
y = viewportmapper.to_viewport(options['shape.cornersy'])
painter.drawRoundedRect(rect, x, y)
if shape.pixmap is not None:
rect = shape.image_rect or content_rect
rect = viewportmapper.to_viewport_rect(rect)
painter.drawPixmap(rect.toRect(), shape.pixmap)
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 = round(viewportmapper.to_viewport(options['text.size']))
font.setPixelSize(size)
painter.setFont(font)
text = options['text.content']
content_rect = viewportmapper.to_viewport_rect(content_rect)
painter.drawText(content_rect, flags, text)
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_manipulator(painter, manipulator, cursor, viewportmapper=None):
viewportmapper = viewportmapper or ViewportMapper()
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)
painter.drawPath(manipulator.hovered_path)
pen = QtGui.QPen(QtGui.QColor('black'))
brush = QtGui.QBrush(QtGui.QColor('white'))
painter.setBrush(brush)
for rect in manipulator.handler_rects():
rect = viewportmapper.to_viewport_rect(rect)
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

View File

@ -0,0 +1,78 @@
import os
from maya import cmds
from dwpicker.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):
return unix_path(cmds.optionVar(query=CUSTOM_PROD_PICKER_DIRECTORY))
return unix_path(os.getenv('DWPICKER_PROJECT_DIRECTORY'))
def expand_path(path):
backup = None
if cmds.optionVar(query=OVERRIDE_PROD_PICKER_DIRECTORY_ENV):
root = unix_path(cmds.optionVar(query=CUSTOM_PROD_PICKER_DIRECTORY))
backup = os.getenv('DWPICKER_PROJECT_DIRECTORY')
os.environ['DWPICKER_PROJECT_DIRECTORY'] = root
result = os.path.expandvars(path)
if backup:
os.environ['DWPICKER_PROJECT_DIRECTORY'] = backup
return result
def get_open_directory():
if cmds.optionVar(query=USE_PROD_PICKER_DIR_AS_DEFAULT):
directory = get_picker_project_directory()
if directory:
return directory
return cmds.optionVar(query=LAST_OPEN_DIRECTORY)
def get_import_directory():
if cmds.optionVar(query=USE_PROD_PICKER_DIR_AS_DEFAULT):
directory = get_picker_project_directory()
if directory:
return directory
return cmds.optionVar(query=LAST_IMPORT_DIRECTORY)
def get_image_directory():
if cmds.optionVar(query=USE_PROD_PICKER_DIR_AS_DEFAULT):
directory = get_picker_project_directory()
if directory:
return directory
return cmds.optionVar(query=LAST_IMAGE_DIRECTORY_USED)

View File

@ -0,0 +1,465 @@
from functools import partial
from maya import cmds
import maya.OpenMaya as om
from PySide2 import QtWidgets, QtGui, QtCore
from dwpicker.interactive import SelectionSquare
from dwpicker.dialog import warning
from dwpicker.geometry import split_line, get_combined_rects
from dwpicker.optionvar import (
SYNCHRONYZE_SELECTION, ZOOM_BUTTON, ZOOM_SENSITIVITY)
from dwpicker.painting import ViewportMapper, draw_shape
from dwpicker.qtutils import get_cursor
from dwpicker.selection import (
select_targets, select_shapes_from_selection, get_selection_mode,
NameclashError)
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()
def frame_shapes(shapes):
offset_x = min(shape.rect.left() for shape in shapes)
offset_y = min(shape.rect.top() for shape in shapes)
offset = -min([offset_x, 0]), -min([offset_y, 0])
for shape in shapes:
shape.rect.moveLeft(shape.rect.left() + offset[0])
shape.rect.moveTop(shape.rect.top() + offset[1])
shape.synchronize_rect()
shape.synchronize_image()
def set_shapes_hovered(shapes, cursor, hidden_layers=None, selection_rect=None):
"""
It set hovered the shape if his rect contains the cursor.
"""
if not shapes:
return
cursor = cursor.toPoint()
selection_rect = selection_rect or QtCore.QRect(cursor, cursor)
selection_shapes = [s for s in shapes if s.targets()]
selection_shapes_intersect_selection = [
s for s in selection_shapes
if s.rect.contains(cursor) or
s.rect.intersects(selection_rect)]
selection_shapes_hovered = [
s for s in selection_shapes_intersect_selection if
not s.visibility_layer() or
not hidden_layers or
s.visibility_layer() not in hidden_layers]
targets = list_targets(selection_shapes_hovered)
for s in selection_shapes:
state = next((False for t in s.targets() if t not in targets), True)
s.hovered = state
def detect_hovered_shape(shapes, cursor):
if not shapes:
return
for shape in reversed(shapes):
if not (shape.is_interactive() or shape.targets()):
continue
if shape.rect.contains(cursor):
return shape
def list_targets(shapes):
return {t for s in shapes for t in s.targets()}
class PickerView(QtWidgets.QWidget):
dataChanged = QtCore.Signal()
addButtonRequested = QtCore.Signal(int, int, int)
updateButtonRequested = QtCore.Signal(object)
deleteButtonRequested = QtCore.Signal()
def __init__(self, editable=True, parent=None):
super(PickerView, self).__init__(parent)
self.callbacks = []
self.editable = editable
self.mode_manager = ModeManager()
self.viewportmapper = ViewportMapper()
self.selection_square = SelectionSquare()
self.layers_menu = VisibilityLayersMenu()
self.setMouseTracking(True)
self.shapes = None
self.clicked_shape = None
self.context_menu = None
self.drag_shapes = []
self.zoom_locked = False
def register_callbacks(self):
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
select_shapes_from_selection(self.shapes)
self.repaint()
def set_shapes(self, shapes):
self.shapes = shapes
self.mode_manager.shapes = shapes
self.layers_menu.set_shapes(shapes)
self.repaint()
def visible_shapes(self):
return [
s for s in self.shapes if
not s.visibility_layer()
or s.visibility_layer() not in self.layers_menu.hidden_layers]
def reset(self):
shapes = self.visible_shapes()
shapes_rects = [s.rect for s in shapes if s.selected]
if not shapes_rects:
shapes_rects = [s.rect for s in shapes]
if not shapes_rects:
self.repaint()
return
self.viewportmapper.viewsize = self.size()
rect = get_combined_rects(shapes_rects)
self.viewportmapper.focus(rect)
self.repaint()
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.repaint()
def mousePressEvent(self, event):
self.setFocus(QtCore.Qt.MouseFocusReason)
self.shapes.extend(self.drag_shapes)
cursor = self.viewportmapper.to_units_coords(event.pos()).toPoint()
self.clicked_shape = detect_hovered_shape(self.shapes, cursor)
hsh = any(s.hovered for s in self.shapes)
self.mode_manager.update(
event,
pressed=True,
has_shape_hovered=hsh,
dragging=bool(self.drag_shapes))
def mouseReleaseEvent(self, event):
shift = self.mode_manager.shift_pressed
ctrl = self.mode_manager.ctrl_pressed
selection_mode = get_selection_mode(shift=shift, ctrl=ctrl)
cursor = self.viewportmapper.to_units_coords(event.pos()).toPoint()
zoom = self.mode_manager.zoom_button_pressed
interact = (
self.clicked_shape and
self.clicked_shape is detect_hovered_shape(self.shapes, cursor) and
self.clicked_shape.is_interactive())
if zoom and self.mode_manager.alt_pressed:
self.release(event)
return
if self.mode_manager.mode == ModeManager.DRAGGING:
self.drag_shapes = []
self.dataChanged.emit()
elif self.mode_manager.mode == ModeManager.SELECTION and not interact:
try:
select_targets(self.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.mode_manager.right_click_pressed:
self.call_context_menu()
elif self.clicked_shape is detect_hovered_shape(self.shapes, cursor):
show_context = (
self.mode_manager.right_click_pressed and
not self.clicked_shape.is_interactive())
left_clicked = self.mode_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.mode_manager.left_click_pressed
else 'right')
self.clicked_shape.execute(
button=button,
ctrl=self.mode_manager.ctrl_pressed,
shift=self.mode_manager.shift_pressed)
self.release(event)
def release(self, event):
self.mode_manager.update(event, pressed=False)
self.selection_square.release()
self.clicked_shape = None
self.repaint()
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.repaint()
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):
selection_rect = self.selection_square.rect
if selection_rect:
selection_rect = self.viewportmapper.to_units_rect(selection_rect)
selection_rect = selection_rect.toRect()
set_shapes_hovered(
self.shapes,
self.viewportmapper.to_units_coords(event.pos()),
self.layers_menu.hidden_layers,
selection_rect)
if self.mode_manager.mode == ModeManager.DRAGGING:
point1 = self.viewportmapper.to_units_coords(
self.mode_manager.anchor)
point2 = self.viewportmapper.to_units_coords(event.pos())
align_shapes_on_line(self.drag_shapes, point1, point2)
elif self.mode_manager.mode == ModeManager.SELECTION:
if not self.selection_square.handeling:
self.selection_square.clicked(event.pos())
self.selection_square.handle(event.pos())
return self.repaint()
elif self.mode_manager.mode == ModeManager.ZOOMING:
if self.zoom_locked:
return self.repaint()
offset = self.mode_manager.mouse_offset(event.pos())
if offset is not None and self.mode_manager.zoom_anchor:
sensitivity = float(cmds.optionVar(query=ZOOM_SENSITIVITY))
factor = (offset.x() + offset.y()) / sensitivity
self.zoom(factor, self.mode_manager.zoom_anchor)
elif self.mode_manager.mode == ModeManager.NAVIGATION:
if self.zoom_locked:
return self.repaint()
offset = self.mode_manager.mouse_offset(event.pos())
if offset is not None:
self.viewportmapper.origin = (
self.viewportmapper.origin - offset)
self.repaint()
def call_context_menu(self):
if not self.editable:
return
self.context_menu = PickerMenu()
position = get_cursor(self)
method = partial(self.add_button, position, button_type=0)
self.context_menu.add_single.triggered.connect(method)
self.context_menu.add_single.setEnabled(bool(cmds.ls(selection=True)))
method = partial(self.add_button, position, button_type=1)
self.context_menu.add_multiple.triggered.connect(method)
state = len(cmds.ls(selection=True)) > 1
self.context_menu.add_multiple.setEnabled(state)
method = partial(self.add_button, position, button_type=2)
self.context_menu.add_command.triggered.connect(method)
method = partial(self.updateButtonRequested.emit, self.clicked_shape)
self.context_menu.update_button.triggered.connect(method)
state = bool(self.clicked_shape) and bool(cmds.ls(selection=True))
self.context_menu.update_button.setEnabled(state)
method = self.deleteButtonRequested.emit
self.context_menu.delete_selected.triggered.connect(method)
if self.layers_menu.displayed:
self.context_menu.addMenu(self.layers_menu)
self.context_menu.exec_(QtGui.QCursor.pos())
def add_button(self, position, button_type=0):
"""
Button types:
0 = Single button from selection.
1 = Multiple buttons from selection.
2 = Command button.
"""
position = self.viewportmapper.to_units_coords(position).toPoint()
self.addButtonRequested.emit(position.x(), position.y(), button_type)
def paintEvent(self, event):
try:
painter = QtGui.QPainter()
painter.begin(self)
painter.setRenderHints(QtGui.QPainter.Antialiasing)
if not self.shapes:
return
hidden_layers = self.layers_menu.hidden_layers
for shape in self.shapes:
visible = (
not shape.visibility_layer() or
not shape.visibility_layer() in hidden_layers)
if not visible:
continue
draw_shape(painter, shape, self.viewportmapper)
self.selection_square.draw(painter)
except BaseException:
pass # avoid crash
# TODO: log the error
finally:
painter.end()
class PickerMenu(QtWidgets.QMenu):
def __init__(self, parent=None):
super(PickerMenu, self).__init__(parent)
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)
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 ModeManager:
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 ModeManager.DRAGGING
elif self.zoom_button_pressed and self.alt_pressed:
return ModeManager.ZOOMING
elif self.middle_click_pressed:
return ModeManager.NAVIGATION
elif self.left_click_pressed:
return ModeManager.SELECTION
self.mouse_ghost = None
return ModeManager.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))
class VisibilityLayersMenu(QtWidgets.QMenu):
def __init__(self, parent=None):
super(VisibilityLayersMenu, self).__init__('Visibility layers', parent)
self.hidden_layers = []
self.displayed = False
def set_shapes(self, shapes):
layers = sorted(
{s.visibility_layer() for s in shapes if s.visibility_layer()})
self.clear()
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)
self.displayed = bool(layers)
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)

View File

@ -0,0 +1,279 @@
import os
from PySide2 import QtWidgets, QtCore
from maya import cmds
from dwpicker.hotkeyseditor import HotkeysEditor
from dwpicker.optionvar import (
save_optionvar,
AUTO_COLLAPSE_IMG_PATH_FROM_ENV, AUTO_FOCUS_BEHAVIOR, AUTO_SET_NAMESPACE,
AUTO_FOCUS_BEHAVIORS, AUTO_SWITCH_TAB, CHECK_IMAGES_PATHS,
CUSTOM_PROD_PICKER_DIRECTORY, CHECK_FOR_UPDATE, DISPLAY_QUICK_OPTIONS,
DISABLE_IMPORT_CALLBACKS, OVERRIDE_PROD_PICKER_DIRECTORY_ENV,
INSERT_TAB_AFTER_CURRENT, NAMESPACE_TOOLBAR, SYNCHRONYZE_SELECTION,
TRIGGER_REPLACE_ON_MIRROR, USE_BASE64_DATA_ENCODING,
USE_PROD_PICKER_DIR_AS_DEFAULT, USE_ICON_FOR_UNSAVED_TAB,
WARN_ON_TAB_CLOSED, ZOOM_SENSITIVITY, ZOOM_BUTTON, ZOOM_BUTTONS)
from dwpicker.path import unix_path
MAX_SENSITIVITY = 500
AUTO_FOCUSES = {
'Disable': AUTO_FOCUS_BEHAVIORS[0],
'Bilateral': AUTO_FOCUS_BEHAVIORS[1],
'From picker to Maya only': AUTO_FOCUS_BEHAVIORS[2]}
class PreferencesWindow(QtWidgets.QWidget):
need_update_callbacks = QtCore.Signal()
hotkey_changed = QtCore.Signal()
def __init__(self, callback=None, parent=None):
super(PreferencesWindow, self).__init__(parent, QtCore.Qt.Tool)
self.setWindowTitle("Preferences")
self.general_preferences = GeneralPreferences(callback)
self.general_preferences.disable_import_callbacks.released.connect(
self.need_update_callbacks.emit)
self.hotkeys_editor = HotkeysEditor()
self.hotkeys_editor.hotkey_changed.connect(self.hotkey_changed.emit)
tab = QtWidgets.QTabWidget()
tab.addTab(self.general_preferences, 'General')
tab.addTab(self.hotkeys_editor, 'Hotkeys')
layout = QtWidgets.QHBoxLayout(self)
layout.addWidget(tab)
class GeneralPreferences(QtWidgets.QWidget):
def __init__(self, callback=None, parent=None):
super(GeneralPreferences, self).__init__(parent)
self.callback = callback
text = "Display namespace toolbar."
self.namespace_toolbar = QtWidgets.QCheckBox(text)
self.quick_options = QtWidgets.QCheckBox("Display quick options.")
text = "Auto switch tab with selection."
self.autoswitch_tab = QtWidgets.QCheckBox(text)
text = "Auto switch namespace."
self.autoswitch_namespace = QtWidgets.QCheckBox(text)
self.sychronize = QtWidgets.QCheckBox("Synchronize picker selection.")
text = "Missing images warning."
self.check_images_paths = QtWidgets.QCheckBox(text)
text = "Disable callback at import time. (Use with Studio Library)"
self.disable_import_callbacks = QtWidgets.QCheckBox(text)
text = "Use icon to mark unsaved tab."
self.unsaved_tab_icon = QtWidgets.QCheckBox(text)
text = "Insert new tab after current tab."
self.insert_after_current = QtWidgets.QCheckBox(text)
text = "Warning before closing a tab."
self.warn_on_tab_close = QtWidgets.QCheckBox(text)
self.ui_group = QtWidgets.QGroupBox("Ui")
self.ui_layout = QtWidgets.QVBoxLayout(self.ui_group)
self.ui_layout.addWidget(self.namespace_toolbar)
self.ui_layout.addWidget(self.quick_options)
self.ui_layout.addWidget(self.disable_import_callbacks)
self.ui_layout.addWidget(self.autoswitch_namespace)
self.ui_layout.addWidget(self.autoswitch_tab)
self.ui_layout.addWidget(self.sychronize)
self.ui_layout.addWidget(self.check_images_paths)
self.ui_layout.addWidget(self.unsaved_tab_icon)
self.ui_layout.addWidget(self.insert_after_current)
self.ui_layout.addWidget(self.warn_on_tab_close)
notfound = "environment variable not found"
text = '$DWPICKER_PROJECT_DIRECTORY:{}'.format(
os.getenv("DWPICKER_PROJECT_DIRECTORY", notfound))
self.project_dir_env = QtWidgets.QLineEdit(text)
self.project_dir_env.setReadOnly(True)
text = (
"Auto-collapse path with environment "
"variable $DWPICKER_PROJECT_DIRECTORY")
self.auto_collapse_path = QtWidgets.QCheckBox(text)
text = "Override $DWPICKER_PROJECT_DIRECTORY"
self.override_variable = QtWidgets.QCheckBox(text)
self.custom_prod_path = QtWidgets.QLineEdit()
text = "Force file dialog to use this directory"
self.force_file_dialog_directory = QtWidgets.QCheckBox(text)
custom_path_layout = QtWidgets.QHBoxLayout()
custom_path_layout.setContentsMargins(0, 0, 0, 0)
custom_path_layout.addWidget(self.override_variable)
custom_path_layout.addWidget(self.custom_prod_path)
self.env_group = QtWidgets.QGroupBox("Environment Variables")
self.env_layout = QtWidgets.QVBoxLayout(self.env_group)
self.env_layout.addWidget(self.project_dir_env)
self.env_layout.addWidget(self.auto_collapse_path)
self.env_layout.addLayout(custom_path_layout)
self.env_layout.addWidget(self.force_file_dialog_directory)
text = "Encode in-scene data as base64."
self.use_base64_encoding = QtWidgets.QCheckBox(text)
self.data_group = QtWidgets.QGroupBox("Data")
self.data_layout = QtWidgets.QVBoxLayout(self.data_group)
self.data_layout.addWidget(self.use_base64_encoding)
self.auto_focus = QtWidgets.QComboBox()
self.auto_focus.addItems(list(AUTO_FOCUSES))
self.focus_group = QtWidgets.QGroupBox("Auto-focus")
self.focus_layout = QtWidgets.QFormLayout(self.focus_group)
self.focus_layout.addRow("Behavior", self.auto_focus)
msg = "Prompt search and replace after mirror."
self.search_on_mirror = QtWidgets.QCheckBox(msg)
self.advanced_group = QtWidgets.QGroupBox("Advanced editor")
self.advanced_layout = QtWidgets.QVBoxLayout(self.advanced_group)
self.advanced_layout.addWidget(self.search_on_mirror)
self.zoom_sensitivity = QtWidgets.QSlider(QtCore.Qt.Horizontal)
self.zoom_sensitivity.setMaximum(MAX_SENSITIVITY)
self.zoom_sensitivity.setMinimum(1)
self.zoom_sensitivity.setSingleStep(1)
self.zoom_button = QtWidgets.QComboBox()
for item in ZOOM_BUTTONS:
self.zoom_button.addItem(item)
self.zoom_group = QtWidgets.QGroupBox("Zoom options")
self.zoom_layout = QtWidgets.QFormLayout(self.zoom_group)
self.zoom_layout.addRow("Sensitivity", self.zoom_sensitivity)
self.zoom_layout.addRow("Mouse button", self.zoom_button)
msg = "Check for new version at startup."
self.check_for_update = QtWidgets.QCheckBox(msg)
self.update_group = QtWidgets.QGroupBox("Update check")
self.update_layout = QtWidgets.QVBoxLayout(self.update_group)
self.update_layout.addWidget(self.check_for_update)
central_widget = QtWidgets.QWidget()
self.sublayout = QtWidgets.QVBoxLayout(central_widget)
self.sublayout.addWidget(self.ui_group)
self.sublayout.addWidget(self.env_group)
self.sublayout.addWidget(self.data_group)
self.sublayout.addWidget(self.focus_group)
self.sublayout.addWidget(self.advanced_group)
self.sublayout.addWidget(self.zoom_group)
self.sublayout.addWidget(self.update_group)
scroll = QtWidgets.QScrollArea()
scroll.setWidgetResizable(True)
scroll.setWidget(central_widget)
scroll.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
scroll.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(scroll)
self.load_ui_states()
self.auto_collapse_path.released.connect(self.save_ui_states)
self.autoswitch_tab.released.connect(self.save_ui_states)
self.autoswitch_namespace.released.connect(self.save_ui_states)
self.auto_focus.currentIndexChanged.connect(self.save_ui_states)
self.check_for_update.released.connect(self.save_ui_states)
self.check_images_paths.released.connect(self.save_ui_states)
self.custom_prod_path.textEdited.connect(self.save_ui_states)
self.disable_import_callbacks.released.connect(self.save_ui_states)
self.force_file_dialog_directory.released.connect(self.save_ui_states)
self.override_variable.released.connect(self.save_ui_states)
self.insert_after_current.released.connect(self.save_ui_states)
self.quick_options.released.connect(self.save_ui_states)
self.namespace_toolbar.released.connect(self.save_ui_states)
self.use_base64_encoding.released.connect(self.save_ui_states)
self.unsaved_tab_icon.released.connect(self.save_ui_states)
self.sychronize.released.connect(self.save_ui_states)
self.search_on_mirror.released.connect(self.save_ui_states)
self.warn_on_tab_close.released.connect(self.save_ui_states)
self.zoom_sensitivity.valueChanged.connect(self.save_ui_states)
self.zoom_button.currentIndexChanged.connect(self.save_ui_states)
def sizeHint(self):
return QtCore.QSize(520, 600)
def load_ui_states(self):
state = bool(cmds.optionVar(query=AUTO_COLLAPSE_IMG_PATH_FROM_ENV))
self.auto_collapse_path.setChecked(state)
value = cmds.optionVar(query=AUTO_FOCUS_BEHAVIOR)
text = {v: k for k, v in AUTO_FOCUSES.items()}[value]
self.auto_focus.setCurrentText(text)
state = bool(cmds.optionVar(query=AUTO_SET_NAMESPACE))
self.autoswitch_namespace.setChecked(state)
state = bool(cmds.optionVar(query=AUTO_SWITCH_TAB))
self.autoswitch_tab.setChecked(state)
state = bool(cmds.optionVar(query=DISABLE_IMPORT_CALLBACKS))
self.disable_import_callbacks.setChecked(state)
value = cmds.optionVar(query=CUSTOM_PROD_PICKER_DIRECTORY)
self.custom_prod_path.setText(value)
state = bool(cmds.optionVar(query=CHECK_IMAGES_PATHS))
self.check_images_paths.setChecked(state)
state = bool(cmds.optionVar(query=CHECK_FOR_UPDATE))
self.check_for_update.setChecked(state)
state = bool(cmds.optionVar(query=USE_PROD_PICKER_DIR_AS_DEFAULT))
self.force_file_dialog_directory.setChecked(state)
state = bool(cmds.optionVar(query=OVERRIDE_PROD_PICKER_DIRECTORY_ENV))
self.override_variable.setChecked(state)
self.custom_prod_path.setEnabled(state)
state = bool(cmds.optionVar(query=NAMESPACE_TOOLBAR))
self.namespace_toolbar.setChecked(state)
state = bool(cmds.optionVar(query=DISPLAY_QUICK_OPTIONS))
self.quick_options.setChecked(state)
state = bool(cmds.optionVar(query=SYNCHRONYZE_SELECTION))
self.sychronize.setChecked(state)
state = bool(cmds.optionVar(query=USE_BASE64_DATA_ENCODING))
self.use_base64_encoding.setChecked(state)
state = bool(cmds.optionVar(query=USE_ICON_FOR_UNSAVED_TAB))
self.unsaved_tab_icon.setChecked(state)
state = bool(cmds.optionVar(query=WARN_ON_TAB_CLOSED))
self.warn_on_tab_close.setChecked(state)
state = bool(cmds.optionVar(query=INSERT_TAB_AFTER_CURRENT))
self.insert_after_current.setChecked(state)
state = bool(cmds.optionVar(query=TRIGGER_REPLACE_ON_MIRROR))
self.search_on_mirror.setChecked(state)
value = MAX_SENSITIVITY - cmds.optionVar(query=ZOOM_SENSITIVITY)
self.zoom_sensitivity.setSliderPosition(value)
value = cmds.optionVar(query=ZOOM_BUTTON)
self.zoom_button.setCurrentText(value)
def save_ui_states(self, *_):
value = int(self.auto_collapse_path.isChecked())
save_optionvar(AUTO_COLLAPSE_IMG_PATH_FROM_ENV, value)
value = AUTO_FOCUSES[self.auto_focus.currentText()]
save_optionvar(AUTO_FOCUS_BEHAVIOR, value)
value = int(self.autoswitch_namespace.isChecked())
save_optionvar(AUTO_SET_NAMESPACE, value)
value = int(self.autoswitch_tab.isChecked())
save_optionvar(AUTO_SWITCH_TAB, value)
value = int(self.check_images_paths.isChecked())
save_optionvar(CHECK_IMAGES_PATHS, value)
value = int(self.check_for_update.isChecked())
save_optionvar(CHECK_FOR_UPDATE, value)
value = unix_path(self.custom_prod_path.text())
save_optionvar(CUSTOM_PROD_PICKER_DIRECTORY, value)
value = int(self.insert_after_current.isChecked())
save_optionvar(INSERT_TAB_AFTER_CURRENT, value)
value = int(self.disable_import_callbacks.isChecked())
save_optionvar(DISABLE_IMPORT_CALLBACKS, value)
value = int(self.force_file_dialog_directory.isChecked())
save_optionvar(USE_PROD_PICKER_DIR_AS_DEFAULT, value)
value = self.override_variable.isChecked()
save_optionvar(OVERRIDE_PROD_PICKER_DIRECTORY_ENV, int(value))
self.custom_prod_path.setEnabled(value)
value = int(self.quick_options.isChecked())
save_optionvar(DISPLAY_QUICK_OPTIONS, value)
value = int(self.namespace_toolbar.isChecked())
save_optionvar(NAMESPACE_TOOLBAR, value)
value = int(self.use_base64_encoding.isChecked())
save_optionvar(USE_BASE64_DATA_ENCODING, value)
value = int(self.unsaved_tab_icon.isChecked())
save_optionvar(USE_ICON_FOR_UNSAVED_TAB, value)
value = int(self.search_on_mirror.isChecked())
save_optionvar(TRIGGER_REPLACE_ON_MIRROR, value)
value = int(self.warn_on_tab_close.isChecked())
save_optionvar(WARN_ON_TAB_CLOSED, value)
save_optionvar(ZOOM_BUTTON, self.zoom_button.currentText())
value = MAX_SENSITIVITY - int(self.zoom_sensitivity.value()) + 1
save_optionvar(ZOOM_SENSITIVITY, value)
if self.callback:
self.callback()

View File

@ -0,0 +1,14 @@
from PySide6 import QtCore, QtGui, QtWidgets
from PySide6 import __version__
QtWidgets.QShortcut = QtGui.QShortcut
QtWidgets.QAction = QtGui.QAction
QtGui.QMouseEvent.pos = QtGui.QMouseEvent.position
QtGui.QMouseEvent.globalPos = QtGui.QMouseEvent.globalPosition
QtGui.QWheelEvent.pos = QtGui.QWheelEvent.position
QtCore.Qt.BackgroundColorRole = QtCore.Qt.BackgroundRole

View File

@ -0,0 +1 @@
from shiboken6 import wrapInstance

View File

@ -0,0 +1,109 @@
import inspect
import os
import sys
from PySide2 import QtGui, QtWidgets, QtCore
from maya import cmds
import maya.OpenMayaUI as omui
from maya.app.general.mayaMixin import MayaQWidgetDockableMixin
import shiboken2
# Ensure backward compatibility.
if sys.version_info[0] == 3:
long = int
VALIGNS = {
'top': QtCore.Qt.AlignTop,
'center': QtCore.Qt.AlignVCenter,
'bottom': QtCore.Qt.AlignBottom}
HALIGNS = {
'left': QtCore.Qt.AlignLeft,
'center': QtCore.Qt.AlignHCenter,
'right': QtCore.Qt.AlignRight}
HERE = os.path.dirname(__file__)
ERROR_IMPORT_MSG = ('''
ERROR: Dwpicker: DwPicker is not found in Python paths.
- Please use sys.path.append('<dwpicker forlder>') and relaunch it.
- Or add '<picker folder>' to environment variable PYTHONPATH''')
RESTORE_CMD = ("""
try:
import {0}
{0}.{1}.restore()
except ImportError:
print("{2}")
""")
mixin_windows = {}
if sys.version_info[0] != 2:
long = int
def icon(filename):
return QtGui.QIcon(os.path.join(HERE, 'icons', filename))
def get_cursor(widget):
return widget.mapFromGlobal(QtGui.QCursor.pos())
def set_shortcut(keysequence, parent, method, context=None):
shortcut = QtWidgets.QShortcut(QtGui.QKeySequence(keysequence), parent)
shortcut.setContext(context or QtCore.Qt.WidgetWithChildrenShortcut)
shortcut.activated.connect(method)
return shortcut
def remove_workspace_control(control_name):
workspace_control_name = control_name + "WorkspaceControl"
cmds.deleteUI(workspace_control_name, control=True)
def maya_main_window():
ptr = omui.MQtUtil.mainWindow()
if ptr is not None:
return shiboken2.wrapInstance(long(ptr), QtWidgets.QWidget)
class DockableBase(MayaQWidgetDockableMixin):
"""
Code from https://kainev.com/qt-for-maya-dockable-windows
Thanks for this !
Convenience class for creating dockable Maya windows.
"""
def __init__(self, control_name, **kwargs):
super(DockableBase, self).__init__(**kwargs)
self.setObjectName(control_name)
def show(self, dockable=True, *_, **kwargs):
"""
Show UI with generated uiScript argument
"""
modulename = inspect.getmodule(self).__name__
classname = self.__class__.__name__
command = RESTORE_CMD.format(modulename, classname, ERROR_IMPORT_MSG)
super(DockableBase, self).show(
dockable=dockable, uiScript=command, **kwargs)
@classmethod
def restore(cls):
"""
Internal method to restore the UI when Maya is opened.
"""
# Create UI instance
instance = cls()
# Get the empty WorkspaceControl created by Maya
workspace_control = omui.MQtUtil.getCurrentParent()
# Grab the pointer to our instance as a Maya object
mixinPtr = omui.MQtUtil.findControl(instance.objectName())
# Add our UI to the WorkspaceControl
omui.MQtUtil.addWidgetToMayaLayout(
long(mixinPtr), long(workspace_control))
# Store reference to UI
global mixin_windows
mixin_windows[instance.objectName()] = instance

View File

@ -0,0 +1,120 @@
from PySide2 import QtWidgets, QtGui, QtCore
from maya import cmds
from dwpicker.colorwheel import ColorDialog
from dwpicker.optionvar import (
save_optionvar, DEFAULT_LABEL, DEFAULT_HEIGHT, DEFAULT_WIDTH,
DEFAULT_TEXT_COLOR, DEFAULT_BG_COLOR)
class QuickOptions(QtWidgets.QWidget):
def __init__(self, parent=None):
super(QuickOptions, self).__init__(parent=parent)
self.bg_color = ColorButton()
self.bg_color.colorChanged.connect(self.save_ui_states)
self.text_color = ColorButton()
self.text_color.colorChanged.connect(self.save_ui_states)
validator = QtGui.QIntValidator()
self.width = QtWidgets.QLineEdit()
self.width.returnPressed.connect(self.save_ui_states)
self.width.setValidator(validator)
self.width.setFixedWidth(50)
self.height = QtWidgets.QLineEdit()
self.height.returnPressed.connect(self.save_ui_states)
self.height.setValidator(validator)
self.height.setFixedWidth(50)
self.label = QtWidgets.QLineEdit()
self.label.returnPressed.connect(self.save_ui_states)
self.layout = QtWidgets.QHBoxLayout(self)
self.layout.setSpacing(0)
self.layout.addWidget(QtWidgets.QLabel('Bg-color: '))
self.layout.addWidget(self.bg_color)
self.layout.addSpacing(12)
self.layout.addWidget(QtWidgets.QLabel('Text-color: '))
self.layout.addWidget(self.text_color)
self.layout.addSpacing(12)
self.layout.addWidget(QtWidgets.QLabel('Size: '))
self.layout.addWidget(self.width)
self.layout.addWidget(self.height)
self.layout.addSpacing(12)
self.layout.addWidget(QtWidgets.QLabel('Label: '))
self.layout.addWidget(self.label)
self.load_ui_states()
def save_ui_states(self, *_):
values = self.values
save_optionvar(DEFAULT_BG_COLOR, values['bgcolor.normal'])
save_optionvar(DEFAULT_TEXT_COLOR, values['text.color'])
save_optionvar(DEFAULT_WIDTH, values['shape.width'])
save_optionvar(DEFAULT_HEIGHT, values['shape.height'])
save_optionvar(DEFAULT_LABEL, values['text.content'])
def load_ui_states(self):
self.values = {
'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)}
@property
def values(self):
return {
'bgcolor.normal': self.bg_color.name,
'text.color': self.text_color.name,
'shape.width': int(self.width.text()) if self.width.text() else 10,
'shape.height': int(self.height.text()) if self.height.text() else 10,
'text.content': self.label.text()}
@values.setter
def values(self, values):
self.bg_color.name = values['bgcolor.normal']
self.text_color.name = values['text.color']
self.width.setText(str(values['shape.width']))
self.height.setText(str(values['shape.height']))
self.label.setText(str(values['text.content']))
class ColorButton(QtWidgets.QAbstractButton):
colorChanged = QtCore.Signal()
def __init__(self, parent=None):
super(ColorButton, self).__init__(parent=parent)
self.setFixedSize(20, 20)
self.color = QtGui.QColor(QtCore.Qt.black)
self.released.connect(self.pick_color)
def pick_color(self):
dialog = ColorDialog(self.name)
if not dialog.exec_():
return
self.name = dialog.colorname()
self.colorChanged.emit()
self.repaint()
@property
def name(self):
return self.color.name()
@name.setter
def name(self, value):
self.color.setNamedColor(value)
def paintEvent(self, _):
try:
painter = QtGui.QPainter()
painter.begin(self)
painter.setBrush(QtGui.QBrush(self.color))
if self.rect().contains(QtGui.QCursor.pos()):
color = QtCore.Qt.transparent
else:
color = QtCore.Qt.gray
painter.setPen(QtGui.QPen(color))
painter.drawRect(self.rect())
except BaseException:
pass # avoid crash
# TODO: log the error
finally:
painter.end()

View File

@ -0,0 +1,42 @@
import os
from dwpicker.dialog import MissingImages
from dwpicker.path import expand_path
IMAGE_MISSING_WARNING = (
'\nImage is not found.\nWould you like to set a new path ?')
def ensure_images_path_exists(pickers):
"""
As images are stored as path in the picker, this function ensure the paths
exists. If not, it proposes to set a new path. If more than an image is not
found, it will automatically look up into directories given in previous
repath to find the images.
"""
missing_images = list_missing_images(pickers)
if not missing_images:
return
dialog = MissingImages(missing_images)
if not dialog.exec_():
return
for picker_data in pickers:
for shape in picker_data['shapes']:
path = expand_path(shape['image.path'])
if path in missing_images:
new_path = dialog.output(path)
if not new_path:
continue
shape['image.path'] = new_path
return pickers
def list_missing_images(pickers_data):
return sorted(list(set([
shape['image.path']
for picker_data in pickers_data
for shape in picker_data['shapes'] if
shape['image.path'] and not
os.path.exists(expand_path(shape['image.path']))])))

View File

@ -0,0 +1,89 @@
import sys
import json
import base64
from maya import cmds
from dwpicker.compatibility import ensure_retro_compatibility
from dwpicker.optionvar import USE_BASE64_DATA_ENCODING
from dwpicker.namespace import maya_namespace
PICKER_HOLDER_NODE = '_dwpicker_data'
PICKER_HOLDER_ATTRIBUTE = '_dwpicker_data'
LS_EXP = ["*." + PICKER_HOLDER_ATTRIBUTE, "*:*." + PICKER_HOLDER_ATTRIBUTE]
def get_picker_holder_node():
if cmds.objExists(PICKER_HOLDER_NODE):
return PICKER_HOLDER_NODE
return create_picker_holder_node()
def create_picker_holder_node():
with maya_namespace(":"):
node = cmds.createNode('script', name=PICKER_HOLDER_NODE)
cmds.setAttr(node + '.nodeState', 1)
cmds.addAttr(node, longName=PICKER_HOLDER_ATTRIBUTE, dataType='string')
return node
def store_local_picker_data(pickers):
data = encode_data(pickers)
node = get_picker_holder_node()
cmds.setAttr(node + '.' + PICKER_HOLDER_ATTRIBUTE, data, type='string')
clean_stray_picker_holder_nodes()
def load_local_picker_data():
nodes = list_picker_holder_nodes()
pickers = []
for node in nodes:
data = cmds.getAttr(node + '.' + PICKER_HOLDER_ATTRIBUTE)
data = decode_data(data)
pickers.extend(ensure_retro_compatibility(p) for p in data)
return pickers
def encode_data(pickers):
data = json.dumps(pickers)
if not cmds.optionVar(query=USE_BASE64_DATA_ENCODING):
return data
# Ensure backward compatibility.
if sys.version_info[0] == 2:
return base64.b64encode(bytes(data))
return base64.b64encode(bytes(data, "utf-8"))
def decode_data(data):
try:
return json.loads(data)
except ValueError: # Happe if data encoded is encoded as base 64 string.
return json.loads(base64.b64decode(data))
def list_picker_holder_nodes():
"""
Look up in the scene all the nodes holding an attribute named
"_dwpicker_holder" which are not set on the "_dwpicker_holder" node.
This mignt happed if a node node is imported (creating a namespace or a
incrementation).
"""
return [node.split(".")[0] for node in cmds.ls(LS_EXP)]
def clean_stray_picker_holder_nodes():
"""
If the scene contains multiple picker holder nodes, we remove them
automatically to avoid repeated pickers.
"""
for node in list_picker_holder_nodes():
if node == PICKER_HOLDER_NODE:
continue
try:
cmds.delete(node)
except BaseException:
# Node is locked or in reference and cannot be removed.
# As we cant remove it, we reset his data to avoid double pickers.
cmds.setAttr(
node + "." + PICKER_HOLDER_ATTRIBUTE, "", dataType="string")

View File

@ -0,0 +1,127 @@
from maya import cmds
class NameclashError(BaseException):
def __init__(self, nodes=None):
self.clashes = [node for node in nodes or [] if len(cmds.ls(node)) > 1]
message = 'Some nodes exists more than once:\n'
nodes = '\n - '.join(self.clashes)
super(NameclashError, self).__init__(message + nodes)
def select_targets(shapes, selection_mode='replace'):
shapes = [s for s in shapes if s.targets()]
hovered = [s for s in shapes if s.hovered]
targets = [t for s in hovered for t in s.targets() if cmds.objExists(t)]
if selection_mode in ('add', 'replace', 'invert'):
try:
return cmds.select(list(targets), add=selection_mode == 'add')
except ValueError:
raise NameclashError(targets)
elif selection_mode == 'remove':
selection = [n for n in cmds.ls(sl=True) if n not in targets]
try:
return cmds.select(selection)
except ValueError:
raise NameclashError(targets)
# Invert selection
selected = [s for s in shapes if s.selected]
to_select = [s for s in shapes if s in hovered and s not in selected]
# List targets unaffected by selection
targets = {
t for s in selected for t in s.targets()
if cmds.objExists(t) and not s.hovered}
# List targets in reversed selection
invert_t = {t for s in to_select for t in s.targets() if cmds.objExists(t)}
targets.union(invert_t)
try:
cmds.select(targets)
except ValueError:
raise NameclashError(targets)
return
def select_shapes_from_selection(shapes):
selection = cmds.ls(sl=True)
for shape in shapes:
if not shape.targets():
shape.selected = False
continue
for target in shape.targets():
if target not in selection:
shape.selected = False
break
else:
shape.selected = True
class Selection():
def __init__(self):
self.shapes = []
self.mode = 'replace'
def set(self, shapes):
if self.mode == 'add':
if shapes is None:
return
return self.add(shapes)
elif self.mode == 'replace':
if shapes is None:
return self.clear()
return self.replace(shapes)
elif self.mode == 'invert':
if shapes is None:
return
return self.invert(shapes)
elif self.mode == 'remove':
if shapes is None:
return
for shape in shapes:
if shape in self.shapes:
self.remove(shape)
def replace(self, shapes):
self.shapes = shapes
def add(self, shapes):
self.shapes.extend([s for s in shapes if s not in self])
def remove(self, shape):
self.shapes.remove(shape)
def invert(self, shapes):
for shape in shapes:
if shape not in self.shapes:
self.add([shape])
else:
self.remove(shape)
def clear(self):
self.shapes = []
def __len__(self):
return len(self.shapes)
def __bool__(self):
return bool(self.shapes)
__nonzero__ = __bool__
def __getitem__(self, i):
return self.shapes[i]
def __iter__(self):
return self.shapes.__iter__()
def get_selection_mode(ctrl, shift):
if not ctrl and not shift:
return 'replace'
elif ctrl and shift:
return 'invert'
elif shift:
return 'add'
return 'remove'

View File

@ -0,0 +1,130 @@
from dwpicker.appinfos import VERSION
BUTTON = {
'visibility_layer': None,
'shape': 'square', # or round
'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': [],
'image.path': '',
'image.fit': True,
'image.height': 32,
'image.width': 32}
TEXT = {
'visibility_layer': None,
'shape': 'square', # or round
'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': [],
'image.path': '',
'image.fit': False,
'image.height': 32,
'image.width': 32}
BACKGROUND = {
'visibility_layer': None,
'shape': 'square', # or round or rounded_rect
'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': [],
'image.path': '',
'image.fit': False,
'image.height': 32,
'image.width': 32}
COMMAND = {
'enabled': True,
'button': 'left', # right
'language': 'python', # or mel
'command': '',
'ctrl': False,
'shift': False,
'deferred': False,
'force_compact_undo': False,
}
PICKER = {
'name': 'Untitled',
'version': VERSION,
'zoom_locked': False,
'width': 900,
'height': 600,
}

View File

@ -0,0 +1,49 @@
from copy import deepcopy
class UndoManager():
def __init__(self, data):
self._current_state = data
self._modified = False
self._undo_stack = []
self._redo_stack = []
@property
def data(self):
return deepcopy(self._current_state)
def undo(self):
if not self._undo_stack:
print('No undostack.')
return False
self._redo_stack.append(deepcopy(self._current_state))
self._current_state = deepcopy(self._undo_stack[-1])
del self._undo_stack[-1]
return True
def redo(self):
if not self._redo_stack:
return False
self._undo_stack.append(deepcopy(self._current_state))
self._current_state = deepcopy(self._redo_stack[-1])
del self._redo_stack[-1]
return True
def set_data_modified(self, data):
self._redo_stack = []
self._undo_stack.append(deepcopy(self._current_state))
self._current_state = deepcopy(data)
self._modified = True
def set_data_saved(self):
self._modified = False
@property
def data_saved(self):
return not self._modified
def reset_stacks(self):
self._undo_stack = []
self._redo_stack = []

View File

@ -0,0 +1,35 @@
import re
import webbrowser
try:
from urllib.request import urlopen
except ImportError:
from urllib2 import urlopen # python2
from maya import cmds
from dwpicker.dialog import UpdateAvailableDialog
from dwpicker.appinfos import VERSION
from dwpicker.optionvar import CHECK_FOR_UPDATE
APPINFOS_URL = (
'https://raw.githubusercontent.com/DreamWall-Animation/dwpicker/main/'
'dwpicker/appinfos.py')
LATEST_RELEASE_URL = (
'https://github.com/DreamWall-Animation/dwpicker/releases/latest')
VERSION_PATTERN = r'\d(\.|,).\d(\.|,).\d'
def warn_if_update_available():
if not cmds.optionVar(query=CHECK_FOR_UPDATE):
return
try:
appinfos = urlopen(APPINFOS_URL).read().decode()
latest_version_str = re.search(VERSION_PATTERN, appinfos)[0]
latest_version = tuple(
int(n) for n in latest_version_str.replace(',', '.').split('.'))
if VERSION < latest_version:
if UpdateAvailableDialog(latest_version_str).exec_():
webbrowser.open(LATEST_RELEASE_URL)
except BaseException:
print('DwPicker: could not check for new version')

View File

@ -0,0 +1,291 @@
from PySide2 import QtGui, QtCore, QtWidgets
from dwpicker.colorwheel import ColorDialog
from dwpicker.dialog import get_image_path
from dwpicker.path import format_path
from dwpicker.qtutils import icon
# don't use style sheet like that, find better design
TOGGLER_STYLESHEET = (
'background: rgb(0, 0, 0, 75); text-align: left; font: bold')
class BoolCombo(QtWidgets.QComboBox):
valueSet = QtCore.Signal(bool)
def __init__(self, state=True, parent=None):
super(BoolCombo, self).__init__(parent)
self.addItem('True')
self.addItem('False')
self.setCurrentText(str(state))
self.currentIndexChanged.connect(self.current_index_changed)
def state(self):
return self.currentText() == 'True'
def current_index_changed(self):
self.valueSet.emit(self.state())
class BrowseEdit(QtWidgets.QWidget):
valueSet = QtCore.Signal(str)
def __init__(self, parent=None):
super(BrowseEdit, self).__init__(parent)
self.text = QtWidgets.QLineEdit()
self.text.returnPressed.connect(self.apply)
self.text.focusOutEvent = self.text_focus_out_event
self.button = QtWidgets.QPushButton('B')
self.button.setFixedSize(21, 21)
self.button.released.connect(self.browse)
self.layout = QtWidgets.QHBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0)
self.layout.addWidget(self.text)
self.layout.addWidget(self.button)
self._value = self.value()
def text_focus_out_event(self, _):
self.apply()
def browse(self):
filename = get_image_path(self)
format_path(filename)
if not filename:
return
self.text.setText(filename)
self.apply()
def apply(self):
text = format_path(self.text.text())
self.text.setText(text)
self.valueSet.emit(text)
def value(self):
value = format(self.text.text())
return value if value != '' else None
def set_value(self, value):
self.text.setText(value)
class WidgetToggler(QtWidgets.QPushButton):
def __init__(self, label, widget, parent=None):
super(WidgetToggler, self).__init__(parent)
self.setStyleSheet(TOGGLER_STYLESHEET)
self.setText(' v ' + label)
self.widget = widget
self.setCheckable(True)
self.setChecked(True)
self.toggled.connect(self._call_toggled)
def _call_toggled(self, state):
if state is True:
self.widget.show()
self.setText(self.text().replace('>', 'v'))
else:
self.widget.hide()
self.setText(self.text().replace('v', '>'))
class ColorEdit(QtWidgets.QWidget):
valueSet = QtCore.Signal(str)
def __init__(self, parent=None):
super(ColorEdit, self).__init__(parent)
self.pixmap = QtWidgets.QLabel()
self.pixmap.setFixedSize(21, 21)
color = QtWidgets.QApplication.palette().color(
QtGui.QPalette.Base)
self.pixmap.setPixmap(_color_pixmap(color, self.pixmap.size()))
self.text = QtWidgets.QLineEdit()
self.text.returnPressed.connect(self.apply)
self.text.focusInEvent = self.focusInEvent
self.text.focusOutEvent = self.focusOutEvent
self.button = QtWidgets.QPushButton(icon('picker.png'), '')
self.button.setFixedSize(21, 21)
self.button.released.connect(self.pick_color)
self.layout = QtWidgets.QHBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0)
self.layout.addWidget(self.pixmap)
self.layout.addWidget(self.text)
self.layout.addWidget(self.button)
self.layout.setStretchFactor(self.pixmap, 1)
self.layout.setStretchFactor(self.text, 5)
self.layout.setStretchFactor(self.button, 1)
self._value = self.value()
def focusInEvent(self, event):
self._value = self.value()
return super(ColorEdit, self).focusInEvent(event)
def focusOutEvent(self, event):
self.apply()
return super(ColorEdit, self).focusOutEvent(event)
def showEvent(self, event):
super(ColorEdit, self).showEvent(event)
self.pixmap.setFixedSize(21, 21)
def pick_color(self):
color = self.text.text() or None
dialog = ColorDialog(color)
if dialog.exec_():
self.text.setText(dialog.colorname())
self.pixmap.setPixmap(
_color_pixmap(dialog.colorname(), self.pixmap.size()))
self.apply()
def apply(self):
if self._value != self.value():
self.valueSet.emit(self.value())
self._value = self.value()
def value(self):
value = self.text.text()
return value if value != '' else None
def set_color(self, color=None):
self.text.setText(color)
color = color or QtWidgets.QApplication.palette().color(
QtGui.QPalette.Base)
self.pixmap.setPixmap(_color_pixmap(color, self.pixmap.size()))
def _color_pixmap(colorname, qsize):
pixmap = QtGui.QPixmap(qsize)
painter = QtGui.QPainter(pixmap)
painter.setBrush(QtGui.QColor(colorname))
painter.setPen(QtCore.Qt.black)
painter.drawRect(0, 0, qsize.width(), qsize.height())
painter.end()
return pixmap
class LineEdit(QtWidgets.QLineEdit):
valueSet = QtCore.Signal(float)
VALIDATOR_CLS = QtGui.QDoubleValidator
def __init__(self, minimum=None, maximum=None, parent=None):
super(LineEdit, self).__init__(parent)
self.validator = self.VALIDATOR_CLS() if self.VALIDATOR_CLS else None
if minimum is not None:
self.validator.setBottom(minimum)
if maximum is not None:
self.validator.setTop(maximum)
self.setValidator(self.validator)
self._value = self.value()
self.returnPressed.connect(self.apply)
def focusInEvent(self, event):
self._value = self.value()
return super(LineEdit, self).focusInEvent(event)
def focusOutEvent(self, event):
self.apply()
return super(LineEdit, self).focusOutEvent(event)
def apply(self):
if self._value != self.value():
self.valueSet.emit(self.value())
self._value = self.value()
def value(self):
if self.text() == '':
return None
return float(self.text().replace(',', '.'))
class TextEdit(LineEdit):
VALIDATOR_CLS = None
valueSet = QtCore.Signal(str)
def value(self):
if self.text() == '':
return None
return self.text()
class FloatEdit(LineEdit):
valueSet = QtCore.Signal(float)
VALIDATOR_CLS = QtGui.QDoubleValidator
def value(self):
if self.text() == '':
return None
return float(self.text().replace(',', '.'))
class IntEdit(LineEdit):
valueSet = QtCore.Signal(int)
VALIDATOR_CLS = QtGui.QIntValidator
def value(self):
if self.text() == '':
return None
return int(float(self.text()))
class Title(QtWidgets.QLabel):
def __init__(self, title, parent=None):
super(Title, self).__init__(parent)
self.setFixedHeight(20)
self.setStyleSheet('background: rgb(0, 0, 0, 25)')
self.setText('<b>&nbsp;&nbsp;&nbsp;' + title)
class TouchEdit(QtWidgets.QLineEdit):
def keyPressEvent(self, event):
self.setText(QtGui.QKeySequence(event.key()).toString().lower())
self.textEdited.emit(self.text())
class CommandButton(QtWidgets.QWidget):
released = QtCore.Signal()
playReleased = QtCore.Signal()
def __init__(self, label, parent=None):
super(CommandButton, self).__init__(parent)
self.mainbutton = QtWidgets.QPushButton(label)
self.mainbutton.released.connect(self.released.emit)
self.playbutton = QtWidgets.QPushButton(icon('play.png'), '')
self.playbutton.released.connect(self.playReleased.emit)
self.playbutton.setFixedSize(22, 22)
self.layout = QtWidgets.QHBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(2)
self.layout.addWidget(self.mainbutton)
self.layout.addWidget(self.playbutton)
class LayerEdit(QtWidgets.QWidget):
valueSet = QtCore.Signal(object)
def __init__(self, parent=None):
super(LayerEdit, self).__init__(parent)
self.layer = QtWidgets.QLineEdit()
self.layer.setReadOnly(True)
self.reset = QtWidgets.QPushButton('x')
self.reset.released.connect(self.do_reset)
self.layout = QtWidgets.QHBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0)
self.layout.addWidget(self.layer)
self.layout.addWidget(self.reset)
def set_layer(self, text):
self.layer.setText(text or '')
def do_reset(self):
if not self.layer.text():
return
self.layer.setText('')
self.valueSet.emit(None)

Binary file not shown.

After

Width:  |  Height:  |  Size: 916 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 KiB

View File

@ -0,0 +1,35 @@
"""
The multiple nested namespaces is currently not supported by the namespace
switch system. This can cause issue for picker having to support sub namespace
on part of the rig.
This piece of code allow to set manually a namespace through the selected
shapes"""
import dwpicker
namespace = 'write:nested:namespace:here'
picker = dwpicker.current()
## Edit Shapes from picker view selection
# selection = [s for s in picker.shapes if s.selected]
# for shape in selection:
# targets = [namespace + ':' + t.split(':')[-1] for t in shape.targets()]
# shape.options['action.targets'] = targets
# dwpicker._dwpicker.data_changed_from_picker(picker)
## Edit Shapes from advanced editor selection
index = dwpicker._dwpicker.pickers.index(picker)
editor = dwpicker._dwpicker.editors[index]
if editor is None:
raise RuntimeWarning("Please open current picker's avanced editor")
selection = editor.shape_editor.selection
for shape in selection:
targets = [namespace + ':' + t.split(':')[-1] for t in shape.targets()]
shape.options['action.targets'] = targets
editor.set_data_modified()

View File

@ -0,0 +1,24 @@
"""
A developper hack to reload the Dreamwall picker without having to restart
Maya each time.
"""
# If the picker is not in a known PYTHONPATH.
import sys
sys.path.insert(0, "<dwpicker path>")
# Code to clean modules and relaunch a Dreamwall picker with updated code.
try:
# Important step to not let some callbacks left behind.
dwpicker.close()
except:
pass
for module in list(sys.modules):
if "dwpicker" in module:
print("deleted: " + module)
del sys.modules[module]
import dwpicker
dwpicker.show()

View File

@ -0,0 +1,13 @@
# This code as to be ran in a Maya able to use 'dwpicker' package.
import os
from dwpicker.ingest import animschool
SOURCE_DIRECTORY = "" # Replace those variables before conversion
DESTINATION_DIRECTORY = ""
for f in os.listdir(SOURCE_DIRECTORY):
if not f.lower().endswith(".pkr"):
continue
filepath = os.path.join(SOURCE_DIRECTORY, f)
animschool.convert(filepath, DESTINATION_DIRECTORY)

View File

@ -0,0 +1,33 @@
import dwpicker
from dwpicker.scenedata import load_local_picker_data, store_local_picker_data
from dwpicker.templates import BUTTON
def add_button(index, options, refresh_ui=True):
"""
This works with pick closed as well.
@param int index: the tab position of the dwpicker.
@param dict options:
This is a dictionnary of the shape options. List of possible options
are can be found here dwpicker.templates.BUTTON
(too much very many long to be documented here ;) )
@param bool refresh_ui:
this update the ui. Can be disabled for loop purpose.
"""
pickers = load_local_picker_data()
button = BUTTON.copy()
button.update(options)
pickers[index]['shapes'].append(button)
store_local_picker_data(pickers)
if refresh_ui:
dwpicker.refresh()
options = {
'text.content': 'Button',
'shape.left': 250,
'shape.top': 150,
'shape.width': 120.0,
'shape.height': 25.0,
}
add_button(0, options)

View File

@ -0,0 +1,7 @@
import dwpicker
from dwpicker.picker import detect_picker_namespace
picker = dwpicker.current()
if picker:
namespace = detect_picker_namespace(picker.shapes)

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