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

316 lines
11 KiB
Python

from ngSkinTools2 import signal
from ngSkinTools2.api import influence_names
from ngSkinTools2.api.influenceMapping import InfluenceInfo
from ngSkinTools2.api.layers import Layer
from ngSkinTools2.api.log import getLogger
from ngSkinTools2.api.pyside import QtCore, QtGui, QtWidgets
from ngSkinTools2.api.target_info import list_influences
from ngSkinTools2.ui import actions, qt
from ngSkinTools2.ui.layout import scale_multiplier
from ngSkinTools2.ui.options import Config, config
log = getLogger("influencesView")
_ = Layer # only imported for type reference
def build_used_influences_action(parent):
def toggle():
config.influences_show_used_influences_only.set(not config.influences_show_used_influences_only())
result = actions.define_action(
parent,
"Used Influences Only",
callback=toggle,
tooltip="If enabled, influences view will only show influences that have weights on current layer",
)
@signal.on(config.influences_show_used_influences_only.changed, qtParent=parent)
def update():
result.setChecked(config.influences_show_used_influences_only())
result.setCheckable(True)
update()
return result
def build_set_influences_sorted_action(parent):
from ngSkinTools2.ui import actions
def toggle():
new_value = Config.InfluencesSortDescending
if config.influences_sort() == new_value:
new_value = Config.InfluencesSortUnsorted
config.influences_sort.set(new_value)
result = actions.define_action(
parent,
"Show influences sorted",
callback=toggle,
tooltip="Sort influences by name",
)
@signal.on(config.influences_show_used_influences_only.changed, qtParent=parent)
def update():
result.setChecked(config.influences_sort() == Config.InfluencesSortDescending)
result.setCheckable(True)
update()
return result
icon_mask = QtGui.QIcon(":/blendColors.svg")
icon_dq = QtGui.QIcon(":/rotate_M.png")
icon_joint = QtGui.QIcon(":/joint.svg")
icon_joint_disabled = qt.image_icon("joint_disabled.png")
icon_transform = QtGui.QIcon(":/cube.png")
icon_transform_disabled = qt.image_icon("cube_disabled.png")
def build_view(parent, actions, session, filter):
"""
:param parent: ui parent
:type actions: ngSkinTools2.ui.actions.Actions
:type session: ngSkinTools2.ui.session.Session
:type filter: InfluenceNameFilter
"""
icon_locked = QtGui.QIcon(":/Lock_ON.png")
icon_unlocked = QtGui.QIcon(":/Lock_OFF_grey.png")
id_role = QtCore.Qt.UserRole + 1
item_size_hint = QtCore.QSize(25 * scale_multiplier, 25 * scale_multiplier)
def get_item_id(item):
if item is None:
return None
return item.data(0, id_role)
tree_items = {}
def build_items(view, items, layer):
# type: (QtWidgets.QTreeWidget, list[InfluenceInfo], Layer) -> None
is_group_layer = layer is not None and layer.num_children != 0
def rebuild_buttons(item, item_id, buttons):
bar = QtWidgets.QToolBar(parent=parent)
bar.setMovable(False)
bar.setIconSize(QtCore.QSize(13 * scale_multiplier, 13 * scale_multiplier))
def add_or_remove(input_list, items, should_add):
if should_add:
return list(input_list) + list(items)
return [i for i in input_list if i not in items]
def lock_unlock_handler(lock):
def handler():
targets = layer.paint_targets
if item_id not in targets:
targets = (item_id,)
layer.locked_influences = add_or_remove(layer.locked_influences, targets, lock)
log.info("updated locked influences to %r", layer.locked_influences)
session.events.influencesListUpdated.emit()
return handler
if "unlocked" in buttons:
a = bar.addAction(icon_unlocked, "Toggle locked/unlocked")
qt.on(a.triggered)(lock_unlock_handler(True))
if "locked" in buttons:
a = bar.addAction(icon_locked, "Toggle locked/unlocked")
qt.on(a.triggered)(lock_unlock_handler(False))
view.setItemWidget(item, 1, bar)
selected_ids = []
if session.state.currentLayer.layer:
selected_ids = session.state.currentLayer.layer.paint_targets
current_id = None if not selected_ids else selected_ids[0]
with qt.signals_blocked(view):
tree_items.clear()
tree_root = view.invisibleRootItem()
item_index = 0
for item_id, displayName, icon, buttons in wanted_tree_items(
items=items,
include_dq_item=session.state.skin_cluster_dq_channel_used,
is_group_layer=is_group_layer,
layer=layer,
config=config,
filter=filter,
):
if item_index >= tree_root.childCount():
item = QtWidgets.QTreeWidgetItem([displayName])
else:
item = tree_root.child(item_index)
item.setText(0, displayName)
item.setData(0, id_role, item_id)
item.setIcon(0, icon)
item.setSizeHint(0, item_size_hint)
tree_root.addChild(item)
tree_items[item_id] = item
if item_id == current_id:
view.setCurrentItem(item, 0, QtCore.QItemSelectionModel.NoUpdate)
item.setSelected(item_id in selected_ids)
rebuild_buttons(item, item_id, buttons)
item_index += 1
while item_index < tree_root.childCount():
tree_root.removeChild(tree_root.child(item_index))
view = QtWidgets.QTreeWidget(parent)
view.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
view.setUniformRowHeights(True)
view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
view.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
actions.addInfluencesActions(view)
view.addAction(actions.separator(parent, "View Options"))
view.addAction(actions.show_used_influences_only)
view.addAction(actions.set_influences_sorted)
view.setIndentation(10 * scale_multiplier)
view.header().setStretchLastSection(False)
view.header().setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
view.setHeaderLabels(["Influences", ""])
view.header().setSectionResizeMode(1, QtWidgets.QHeaderView.Fixed)
view.setColumnWidth(1, 25 * scale_multiplier)
# view.setHeaderHidden(True)
def refresh_items():
items = list_influences(session.state.currentLayer.selectedSkinCluster)
def sort_func(a):
"""
:type a: InfluenceInfo
"""
return a.name
# items = sorted(items, key=sort_func)
build_items(view, items, session.state.currentLayer.layer)
@signal.on(
filter.changed,
config.influences_show_used_influences_only.changed,
config.influences_sort.changed,
session.events.influencesListUpdated,
)
def filter_changed():
refresh_items()
@signal.on(session.events.currentLayerChanged, qtParent=view)
def current_layer_changed():
if not session.state.currentLayer.layer:
build_items(view, [], None)
else:
log.info("current layer changed to %s", session.state.currentLayer.layer)
refresh_items()
current_influence_changed()
@signal.on(session.events.currentInfluenceChanged, qtParent=view)
def current_influence_changed():
if session.state.currentLayer.layer is None:
return
log.info("current influence changed - updating item selection")
with qt.signals_blocked(view):
targets = session.state.currentLayer.layer.paint_targets
first = True
for tree_item in tree_items.values():
selected = get_item_id(tree_item) in targets
if selected and first:
view.setCurrentItem(tree_item, 0, QtCore.QItemSelectionModel.NoUpdate)
first = False
tree_item.setSelected(selected)
@qt.on(view.currentItemChanged)
def current_item_changed(curr, prev):
if curr is None:
return
if session.state.selectedSkinCluster is None:
return
if not session.state.currentLayer.layer:
return
log.info("focused item changed: %r", get_item_id(curr))
sync_paint_targets_to_selection()
@qt.on(view.itemSelectionChanged)
def sync_paint_targets_to_selection():
log.info("syncing paint targets")
selected_ids = [get_item_id(item) for item in view.selectedItems()]
selected_ids = [i for i in selected_ids if i is not None]
current_item = view.currentItem()
if current_item and current_item.isSelected():
# move id of current item to front, if it's selected
item_id = get_item_id(current_item)
selected_ids.remove(item_id)
selected_ids = [item_id] + selected_ids
if session.state.currentLayer.layer:
session.state.currentLayer.layer.paint_targets = selected_ids
current_layer_changed()
return view
def get_icon(influence, is_joint):
if influence.used:
return icon_joint if is_joint else icon_transform
return icon_joint_disabled if is_joint else icon_transform_disabled
def wanted_tree_items(
layer,
config,
is_group_layer,
include_dq_item,
filter,
items,
):
"""
:type items: list[InfluenceInfo]
"""
if layer is None:
return
# calculate "used" regardless as we're displaying it visually even if "show used influences only" is toggled off
used = set((layer.get_used_influences() or []))
locked = set((layer.locked_influences or []))
for i in items:
i.used = i.logicalIndex in used
i.locked = i.logicalIndex in locked
if config.influences_show_used_influences_only() and layer is not None:
items = [i for i in items if i.used]
if is_group_layer:
items = []
yield "mask", "[Mask]", icon_mask, []
if not is_group_layer and include_dq_item:
yield "dq", "[DQ Weights]", icon_dq, []
names = influence_names.unique_names([i.path_name() for i in items])
for i, name in zip(items, names):
i.unique_name = name
if config.influences_sort() == Config.InfluencesSortDescending:
items = list(sorted(items, key=lambda i: i.unique_name))
for i in items:
is_joint = i.path is not None
if filter.is_match(i.path_name()):
yield i.logicalIndex, i.unique_name, get_icon(i, is_joint), ["locked" if i.locked else "unlocked"]