Files
Nexus/2023/scripts/animation_tools/dwpicker/designer/editor.py
2025-11-23 23:31:18 +08:00

614 lines
24 KiB
Python

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()