Files
Nexus/2023/scripts/rigging_tools/ngskintools2/ui/layersview.py
2025-11-24 08:27:50 +08:00

226 lines
8.4 KiB
Python

from ngSkinTools2 import api, signal
from ngSkinTools2.api import python_compatibility
from ngSkinTools2.api.log import getLogger
from ngSkinTools2.api.pyside import QtCore, QtWidgets
from ngSkinTools2.api.session import session
from ngSkinTools2.ui import qt
from ngSkinTools2.ui.layout import scale_multiplier
if python_compatibility.PY3:
from typing import Union
log = getLogger("layersView")
def build_view(parent, actions):
from ngSkinTools2.operations import layers
layer_icon_size = 20
visibility_icon_size = 13
icon_layer = qt.scaled_icon(":/layeredTexture.svg", layer_icon_size, layer_icon_size)
icon_layer_disabled = qt.scaled_icon(":/layerEditor.png", layer_icon_size, layer_icon_size)
icon_visible = qt.scaled_icon("eye-fill.svg", visibility_icon_size, visibility_icon_size)
icon_hidden = qt.scaled_icon("eye-slash-fill.svg", visibility_icon_size, visibility_icon_size)
layer_data_role = QtCore.Qt.UserRole + 1
def item_to_layer(item):
# type: (QtWidgets.QTreeWidgetItem) -> Union[api.Layer, None]
if item is None:
return None
return item.data(0, layer_data_role)
# noinspection PyShadowingNames
def sync_layer_parents_to_widget_items(view):
"""
after drag/drop tree reordering, just brute-force check
that rearranged items match layers parents
:return:
"""
def sync_item(tree_item, parent_layer_id):
for i in range(tree_item.childCount()):
child = tree_item.child(i)
rebuild_buttons(child)
child_layer = item_to_layer(child)
if child_layer.parent_id != parent_layer_id:
log.info("changing layer parent: %r->%r (was %r)", parent_layer_id, child_layer, child_layer.parent_id)
child_layer.parent = parent_layer_id
new_index = tree_item.childCount() - i - 1
if child_layer.index != new_index:
log.info("changing layer index: %r->%r (was %r)", child_layer, new_index, child_layer.index)
child_layer.index = new_index
sync_item(child, child_layer.id)
with qt.signals_blocked(view):
sync_item(view.invisibleRootItem(), None)
# noinspection PyPep8Naming
class LayersWidget(QtWidgets.QTreeWidget):
def dropEvent(self, event):
QtWidgets.QTreeWidget.dropEvent(self, event)
sync_layer_parents_to_widget_items(self)
view = LayersWidget(parent)
view.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
view.setUniformRowHeights(True)
view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
# enable drag/drop
view.setDragEnabled(True)
view.viewport().setAcceptDrops(True)
view.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
view.setDropIndicatorShown(True)
# add context menu
view.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
actions.addLayersActions(view)
view.setHeaderLabels(["Layers", ""])
# view.setHeaderHidden(True)
view.header().setMinimumSectionSize(1)
view.header().setStretchLastSection(False)
view.header().swapSections(0, 1)
view.header().setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
view.header().setSectionResizeMode(1, QtWidgets.QHeaderView.Fixed)
view.setColumnWidth(1, 25 * scale_multiplier)
view.setIndentation(15 * scale_multiplier)
view.setIconSize(QtCore.QSize(layer_icon_size * scale_multiplier, layer_icon_size * scale_multiplier))
tree_items = {}
def rebuild_buttons(item):
layer = item_to_layer(item)
bar = QtWidgets.QToolBar(parent=parent)
bar.setMovable(False)
bar.setIconSize(QtCore.QSize(visibility_icon_size * scale_multiplier, visibility_icon_size * scale_multiplier))
a = bar.addAction(icon_visible if layer is None or layer.enabled else icon_hidden, "Toggle enabled/disabled")
@qt.on(a.triggered)
def handler():
layer.enabled = not layer.enabled
session.events.layerListChanged.emitIfChanged()
view.setItemWidget(item, 1, bar)
def build_items(layer_infos):
"""
sync items in view with provided layer values, trying to delete as little items on the view as possible
:type layer_infos: list[api.Layer]
"""
# build map "parent id->list of children "
log.info("syncing items...")
# save selected layers IDs to restore item selection later
selected_layer_ids = {item_to_layer(item).id for item in view.selectedItems()}
log.info("selected layer IDs: %r", selected_layer_ids)
current_item_id = None if view.currentItem() is None else item_to_layer(view.currentItem()).id
hierarchy = {}
for child in layer_infos:
if child.parent_id not in hierarchy:
hierarchy[child.parent_id] = []
hierarchy[child.parent_id].append(child)
def sync(parent_tree_item, children_list):
while parent_tree_item.childCount() > len(children_list):
parent_tree_item.removeChild(parent_tree_item.child(len(children_list)))
for index, child in enumerate(reversed(children_list)):
if index >= parent_tree_item.childCount():
item = QtWidgets.QTreeWidgetItem()
item.setSizeHint(1, QtCore.QSize(1 * scale_multiplier, 25 * scale_multiplier))
item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable)
parent_tree_item.addChild(item)
else:
item = parent_tree_item.child(index)
tree_items[child.id] = item
item.setData(0, layer_data_role, child)
item.setText(0, child.name)
item.setIcon(0, icon_layer if child.enabled else icon_layer_disabled)
rebuild_buttons(item)
sync(item, hierarchy.get(child.id, []))
with qt.signals_blocked(view):
tree_items.clear()
sync(view.invisibleRootItem(), hierarchy.get(None, []))
current_item = tree_items.get(current_item_id, None)
if current_item is not None:
view.setCurrentItem(current_item, 0, QtCore.QItemSelectionModel.NoUpdate)
for i in selected_layer_ids:
item = tree_items.get(i, None)
if item is not None:
item.setSelected(True)
@signal.on(session.events.layerListChanged, qtParent=view)
def refresh_layer_list():
log.info("event handler for layer list changed")
if not session.state.layersAvailable:
build_items([])
else:
build_items(session.state.all_layers)
update_selected_items()
@signal.on(session.events.currentLayerChanged, qtParent=view)
def current_layer_changed():
log.info("event handler for currentLayerChanged")
layer = session.state.currentLayer.layer
current_item = view.currentItem()
if layer is None:
view.setCurrentItem(None)
return
prev_layer = None if current_item is None else item_to_layer(current_item)
if prev_layer is None or prev_layer.id != layer.id:
item = tree_items.get(layer.id, None)
if item is not None:
log.info("setting current item to " + item.text(0))
view.setCurrentItem(item, 0, QtCore.QItemSelectionModel.SelectCurrent | QtCore.QItemSelectionModel.ClearAndSelect)
item.setSelected(True)
@qt.on(view.currentItemChanged)
def current_item_changed(curr, _):
log.info("current item changed")
if curr is None:
return
selected_layer = item_to_layer(curr)
if layers.getCurrentLayer() == selected_layer:
return
layers.setCurrentLayer(selected_layer)
@qt.on(view.itemChanged)
def item_changed(item, column):
log.info("item changed")
layers.renameLayer(item_to_layer(item), item.text(column))
@qt.on(view.itemSelectionChanged)
def update_selected_items():
selection = [item_to_layer(item) for item in view.selectedItems()]
if selection != session.context.selected_layers(default=[]):
log.info("new selected layers: %r", selection)
session.context.selected_layers.set(selection)
refresh_layer_list()
return view