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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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