From d7bac8b8bd25db74548525f7f2e0aa15de1af0ce Mon Sep 17 00:00:00 2001 From: Ivan Titov Date: Tue, 23 Mar 2021 15:11:27 +0500 Subject: [PATCH] v1.6 (#81) ## Templates #### Make HDA by Template - New *Inherit parameters* option - Pick node color and shape - Pick icon from disk - Preview for current icon - Fixed #66 ## Show User Data - New *Pin to node* mode - New *Word-Wrap* mode - Zooming with `Ctrl+Wheel` and `Ctrl+Plus/Minus` - Display node shape for *nodeshape* key - Fixed #70 #### Prettify - Redesigned and generalized - Support for XML - Support for INI-like - Support for comma-separated sequence ## Find Icon - Default icon size increased from 64x64 to 90x90 - Change icon size with slider in real-time - Slight UI acceleration --- OPmenu.xml | 8 +- python2.7libs/houdini_tdk/__init__.py | 10 +- python2.7libs/houdini_tdk/filter_field.py | 22 +- .../houdini_tdk/fuzzy_filter_proxy_model.py | 101 +++++ python2.7libs/houdini_tdk/generate_code.py | 2 - .../{find_icon.py => icon_list.py} | 191 ++++---- python2.7libs/houdini_tdk/input_field.py | 42 ++ .../houdini_tdk/make_hda_by_template.py | 372 +++++++++++++-- python2.7libs/houdini_tdk/node_shape.py | 231 ++++++++++ .../houdini_tdk/node_shape_delegate.py | 77 ++++ .../houdini_tdk/node_shape_list_dialog.py | 122 +++++ .../houdini_tdk/node_shape_list_model.py | 82 ++++ .../houdini_tdk/node_shape_list_view.py | 79 ++++ .../houdini_tdk/node_shape_preview.py | 83 ++++ .../houdini_tdk/show_node_user_data.py | 244 ---------- python2.7libs/houdini_tdk/show_user_data.py | 423 ++++++++++++++++++ python2.7libs/houdini_tdk/slider.py | 4 +- toolbar/houdini_tdk.shelf | 4 +- 18 files changed, 1682 insertions(+), 415 deletions(-) create mode 100644 python2.7libs/houdini_tdk/fuzzy_filter_proxy_model.py rename python2.7libs/houdini_tdk/{find_icon.py => icon_list.py} (74%) create mode 100644 python2.7libs/houdini_tdk/input_field.py create mode 100644 python2.7libs/houdini_tdk/node_shape.py create mode 100644 python2.7libs/houdini_tdk/node_shape_delegate.py create mode 100644 python2.7libs/houdini_tdk/node_shape_list_dialog.py create mode 100644 python2.7libs/houdini_tdk/node_shape_list_model.py create mode 100644 python2.7libs/houdini_tdk/node_shape_list_view.py create mode 100644 python2.7libs/houdini_tdk/node_shape_preview.py delete mode 100644 python2.7libs/houdini_tdk/show_node_user_data.py create mode 100644 python2.7libs/houdini_tdk/show_user_data.py diff --git a/OPmenu.xml b/OPmenu.xml index e54349b..5675e94 100644 --- a/OPmenu.xml +++ b/OPmenu.xml @@ -42,7 +42,7 @@ tdk.showGenerateCode(**kwargs) import houdini_tdk reload(houdini_tdk) -houdini_tdk.showNodeUserData(cached=kwargs['shiftclick'], **kwargs) +houdini_tdk.showNodeUserData(**kwargs) @@ -53,7 +53,7 @@ houdini_tdk.showNodeUserData(cached=kwargs['shiftclick'], **kwargs) node = kwargs['node'] - return node.type().name().startswith('tdk::template') + return node.type().definition() is not None @@ -72,11 +72,11 @@ tdk.showMakeHDAByTemplateDialog(**kwargs) -from houdini_tdk.utils import openFileLocation +import houdini_tdk as tdk node = kwargs['node'] path = node.type().definition().libraryFilePath() -openFileLocation(path) +tdk.openFileLocation(path) diff --git a/python2.7libs/houdini_tdk/__init__.py b/python2.7libs/houdini_tdk/__init__.py index 24912ed..a076890 100644 --- a/python2.7libs/houdini_tdk/__init__.py +++ b/python2.7libs/houdini_tdk/__init__.py @@ -16,9 +16,11 @@ along with this program. If not, see . """ -from .find_icon import FindIconDialog, findIcon -from .new_hda_version import NewVersionDialog, showNewVersionDialog -from .show_node_user_data import UserDataWindow, showNodeUserData -from .make_hda_by_template import MakeHDAByTemplateDialog, showMakeHDAByTemplateDialog +from .icon_list import IconListDialog, findIcon +from .node_shape_list_dialog import NodeShapeListDialog, findNodeShape from .generate_code import showGenerateCode from .hda_doctor import HDADoctorWindow +from .make_hda_by_template import MakeHDAByTemplateDialog, showMakeHDAByTemplateDialog +from .new_hda_version import NewVersionDialog, showNewVersionDialog +from .show_user_data import UserDataWindow, showNodeUserData +from .utils import openFileLocation diff --git a/python2.7libs/houdini_tdk/filter_field.py b/python2.7libs/houdini_tdk/filter_field.py index 477b4ee..5018468 100644 --- a/python2.7libs/houdini_tdk/filter_field.py +++ b/python2.7libs/houdini_tdk/filter_field.py @@ -1,6 +1,6 @@ """ Tool Development Kit for SideFX Houdini -Copyright (C) 2020 Ivan Titov +Copyright (C) 2021 Ivan Titov This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -27,27 +27,21 @@ from PySide2.QtGui import * from PySide2.QtCore import * +from .input_field import InputField -class FilterField(QLineEdit): + +class FilterField(InputField): # Signals - accepted = Signal() + accepted = Signal(str) def __init__(self): super(FilterField, self).__init__() - self.setPlaceholderText('Type to Filter...') def keyPressEvent(self, event): key = event.key() - if key == Qt.Key_Escape: - self.clear() - elif key == Qt.Key_Enter or key == Qt.Key_Return: - self.accepted.emit() + + if key == Qt.Key_Enter or key == Qt.Key_Return: + self.accepted.emit(self.text()) else: super(FilterField, self).keyPressEvent(event) - - def mousePressEvent(self, event): - if event.button() == Qt.MiddleButton and \ - event.modifiers() == Qt.ControlModifier: - self.clear() - super(FilterField, self).mousePressEvent(event) diff --git a/python2.7libs/houdini_tdk/fuzzy_filter_proxy_model.py b/python2.7libs/houdini_tdk/fuzzy_filter_proxy_model.py new file mode 100644 index 0000000..90d4613 --- /dev/null +++ b/python2.7libs/houdini_tdk/fuzzy_filter_proxy_model.py @@ -0,0 +1,101 @@ +""" +Tool Development Kit for SideFX Houdini +Copyright (C) 2021 Ivan Titov + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from __future__ import print_function + +try: + from PyQt5.QtWidgets import * + from PyQt5.QtGui import * + from PyQt5.QtCore import * + + Signal = pyqtSignal +except ImportError: + from PySide2.QtWidgets import * + from PySide2.QtGui import * + from PySide2.QtCore import * + + +def fuzzyMatch(pattern, text): + if pattern == text: + return True, 999999 + + try: + pattern_start = text.index(pattern) + pattern_length = len(pattern) + return True, pattern_length * pattern_length + (1 - pattern_start / 500.0) + except ValueError: + pass + + weight = 0 + count = 0 + index = 0 + for char in text: + try: + if char == pattern[index]: + count += 1 + index += 1 + elif count != 0: + weight += count * count + count = 0 + except IndexError: + pass + + weight += count * count + if index < len(pattern): + return False, weight + + return True, weight + (1 - text.index(pattern[0]) / 500.0) + + +class FuzzyFilterProxyModel(QSortFilterProxyModel): + def __init__(self, parent=None, accept_text_role=Qt.UserRole, comp_text_role=Qt.DisplayRole): + super(FuzzyFilterProxyModel, self).__init__(parent) + + self._accept_text_role = accept_text_role + self.comp_text_role = comp_text_role + + self.setDynamicSortFilter(True) + self.setFilterCaseSensitivity(Qt.CaseInsensitive) + self.sort(0, Qt.DescendingOrder) + + self._pattern = '' + + def setFilterPattern(self, pattern): + self._pattern = pattern.lower() + self.invalidate() + + def filterAcceptsRow(self, source_row, source_parent): + if not self._pattern: + return True + + source_model = self.sourceModel() + text = source_model.data(source_model.index(source_row, 0, source_parent), self._accept_text_role) + matches, _ = fuzzyMatch(self._pattern, text.lower()) + return matches + + def lessThan(self, source_left, source_right): + if not self._pattern: + return source_left.row() < source_right.row() + + text1 = source_left.data(self.comp_text_role) + _, weight1 = fuzzyMatch(self._pattern, text1.lower()) + + text2 = source_right.data(self.comp_text_role) + _, weight2 = fuzzyMatch(self._pattern, text2.lower()) + + return weight1 < weight2 diff --git a/python2.7libs/houdini_tdk/generate_code.py b/python2.7libs/houdini_tdk/generate_code.py index d19e291..9361e8d 100644 --- a/python2.7libs/houdini_tdk/generate_code.py +++ b/python2.7libs/houdini_tdk/generate_code.py @@ -23,8 +23,6 @@ import os import tempfile -import os - try: from PyQt5.QtWidgets import * from PyQt5.QtGui import * diff --git a/python2.7libs/houdini_tdk/find_icon.py b/python2.7libs/houdini_tdk/icon_list.py similarity index 74% rename from python2.7libs/houdini_tdk/find_icon.py rename to python2.7libs/houdini_tdk/icon_list.py index 92a41b5..1298ba2 100644 --- a/python2.7libs/houdini_tdk/find_icon.py +++ b/python2.7libs/houdini_tdk/icon_list.py @@ -30,114 +30,51 @@ import hou from .filter_field import FilterField +from .slider import Slider +from .fuzzy_filter_proxy_model import FuzzyFilterProxyModel -class IconCache: - # Icons - DEFAULT_ICON = hou.qt.Icon('MISC_tier_one', 48, 48) - - # Data - data = {} - - @staticmethod - def icon(name): - if name not in IconCache.data: - try: - IconCache.data[name] = hou.qt.Icon(name, 48, 48) - except hou.OperationFailed: - IconCache.data[name] = IconCache.DEFAULT_ICON - return IconCache.data[name] - - -def fuzzyMatch(pattern, text): - if pattern == text: - return True, 999999 - +def standardIconExists(name): try: - pattern_start = text.index(pattern) - pattern_length = len(pattern) - return True, pattern_length * pattern_length + (1 - pattern_start / 500.0) - except ValueError: - pass - - weight = 0 - count = 0 - index = 0 - for char in text: - try: - if char == pattern[index]: - count += 1 - index += 1 - elif count != 0: - weight += count * count - count = 0 - except IndexError: - pass - - weight += count * count - if index < len(pattern): - return False, weight - - return True, weight + (1 - text.index(pattern[0]) / 500.0) - - -class FuzzyFilterProxyModel(QSortFilterProxyModel): - def __init__(self, parent=None): - super(FuzzyFilterProxyModel, self).__init__(parent) - - self.setDynamicSortFilter(True) - self.setFilterCaseSensitivity(Qt.CaseInsensitive) - self.sort(0, Qt.DescendingOrder) - - self._pattern = '' - - def setFilterPattern(self, pattern): - self._pattern = pattern.lower() - self.invalidate() - - def filterAcceptsRow(self, source_row, source_parent): - if not self._pattern: - return True - - source_model = self.sourceModel() - text = source_model.data(source_model.index(source_row, 0, source_parent), - Qt.UserRole) - matches, _ = fuzzyMatch(self._pattern, text.lower()) - return matches - - def lessThan(self, source_left, source_right): - if not self._pattern: - return source_left.row() < source_right.row() - - text1 = source_left.data(Qt.DisplayRole) - _, weight1 = fuzzyMatch(self._pattern, text1.lower()) - - text2 = source_right.data(Qt.DisplayRole) - _, weight2 = fuzzyMatch(self._pattern, text2.lower()) - - return weight1 < weight2 + hou.qt.Icon(name, 16, 16) + return True + except hou.OperationFailed: + return False class IconListModel(QAbstractListModel): def __init__(self, parent=None): super(IconListModel, self).__init__(parent) + self._icon_size = 64 + # Data ICON_INDEX_FILE = hou.expandString('$HFS/houdini/config/Icons/SVGIcons.index') self.__data = tuple(sorted(hou.loadIndexDataFromFile(ICON_INDEX_FILE).keys())) + def iconSize(self): + return self._icon_size + + def setIconSize(self, size): + self._icon_size = size + self.dataChanged.emit(self.index(0, 0), self.index(len(self.__data), 0), [Qt.DecorationRole]) + def rowCount(self, parent): return len(self.__data) def data(self, index, role): + if not index.isValid(): + return + icon_name = self.__data[index.row()] + if role == Qt.DisplayRole: label = icon_name.replace('.svg', '') # VOP_wood.svg -> VOP_wood if '_' in label: label = ' '.join(label.split('_')[1:]).title() # VOP_wood -> Wood return label elif role == Qt.DecorationRole: - return IconCache.icon(icon_name) + return hou.qt.Icon(icon_name, self._icon_size, self._icon_size) elif role == Qt.UserRole or role == Qt.ToolTipRole: return icon_name @@ -156,8 +93,10 @@ class IconListView(QListView): def __init__(self): super(IconListView, self).__init__() self.setViewMode(QListView.IconMode) - self.setIconSize(QSize(48, 48)) + self.setUniformItemSizes(True) + self.setBatchSize(60) + self.setLayoutMode(QListView.Batched) self.setResizeMode(QListView.Adjust) self.setDragDropMode(QAbstractItemView.NoDragDrop) @@ -208,7 +147,8 @@ def _selectedImage(self): indexes = self.selectedIndexes() if len(indexes) == 1: name = indexes[0].data(Qt.UserRole) - return hou.qt.Icon(name, 48, 48).pixmap(48, 48) + icon_size = self.model().iconSize() + return hou.qt.Icon(name, icon_size, icon_size).pixmap(icon_size, icon_size) def copySelectedIcon(self): image = self._selectedImage() @@ -262,8 +202,14 @@ def _createContextMenu(self): self._menu.addAction(self._save_image_action) def _updateContextMenu(self): - selected_indices = self.selectedIndexes() - if len(selected_indices) == 1: + selection_size = len(self.selectedIndexes()) + + if selection_size == 0: + self._menu.setEnabled(False) + else: + self._menu.setEnabled(True) + + if selection_size == 1: self._copy_image_action.setEnabled(True) self._save_image_action.setEnabled(True) else: @@ -287,11 +233,11 @@ def __emitItemDoubleClicked(self): self.itemDoubleClicked.emit(index) -class FindIconDialog(QDialog): +class IconListDialog(QDialog): def __init__(self, parent=None): - super(FindIconDialog, self).__init__(parent, Qt.Window) + super(IconListDialog, self).__init__(parent, Qt.Window) - self.setWindowTitle('TDK: Find Icon') + self.setWindowTitle('TDK: Icons') self.setWindowIcon(hou.qt.Icon('MISC_m', 32, 32)) self.resize(820, 500) @@ -300,22 +246,37 @@ def __init__(self, parent=None): main_layout.setContentsMargins(4, 4, 4, 4) main_layout.setSpacing(4) - # Filter - self.filter_field = FilterField() - main_layout.addWidget(self.filter_field) + top_layout = QHBoxLayout() + top_layout.setContentsMargins(0, 0, 0, 0) + top_layout.setSpacing(4) + main_layout.addLayout(top_layout) # Icon List self.icon_list_model = IconListModel(self) self.filter_proxy_model = FuzzyFilterProxyModel(self) self.filter_proxy_model.setSourceModel(self.icon_list_model) - self.filter_field.textChanged.connect(self.filter_proxy_model.setFilterPattern) self.icon_list_view = IconListView() self.icon_list_view.setModel(self.filter_proxy_model) self.icon_list_view.itemDoubleClicked.connect(self.accept) + self.icon_list_view.viewport().installEventFilter(self) main_layout.addWidget(self.icon_list_view) + # Filter + self.filter_field = FilterField() + self.filter_field.textChanged.connect(self.filter_proxy_model.setFilterPattern) + top_layout.addWidget(self.filter_field) + + # Scale + self.slider = Slider(Qt.Horizontal) + self.slider.setFixedWidth(120) + self.slider.setDefaultValue(64) + self.slider.setRange(48, 128) + self.slider.valueChanged.connect(self.setIconSize) + self.slider.setValue(64) + top_layout.addWidget(self.slider) + # Buttons buttons_layout = QHBoxLayout() main_layout.addLayout(buttons_layout) @@ -333,12 +294,42 @@ def __init__(self, parent=None): self.cancel_button.clicked.connect(self.reject) buttons_layout.addWidget(self.cancel_button) + def setIconSize(self, size): + size = min(max(size, 48), 128) + if size != self.icon_list_model.iconSize(): + self.slider.setToolTip('Size: ' + str(size)) + self.slider.blockSignals(True) + self.slider.setValue(size) + self.slider.blockSignals(False) + self.icon_list_model.setIconSize(size) + self.icon_list_view.setIconSize(QSize(size, size)) + + def zoomIn(self, amount=4): + self.setIconSize(self.icon_list_model.iconSize() + amount) + + def zoomOut(self, amount=4): + self.setIconSize(self.icon_list_model.iconSize() - amount) + + def eventFilter(self, watched, event): + if watched == self.icon_list_view.viewport() and event.type() == QEvent.Wheel: + if event.modifiers() == Qt.ControlModifier: + if event.delta() > 0: + self.zoomIn() + else: + self.zoomOut() + return True + return False + def keyPressEvent(self, event): if event.matches(QKeySequence.Find) or event.key() == Qt.Key_F3: self.filter_field.setFocus() self.filter_field.selectAll() + elif event.matches(QKeySequence.ZoomIn): + self.zoomIn() + elif event.matches(QKeySequence.ZoomOut): + self.zoomOut() else: - super(FindIconDialog, self).keyPressEvent(event) + super(IconListDialog, self).keyPressEvent(event) def enableDialogMode(self): self.icon_list_view.setSelectionMode(QAbstractItemView.SingleSelection) @@ -347,8 +338,8 @@ def enableDialogMode(self): self.cancel_button.setVisible(True) @classmethod - def getIconName(cls, parent=hou.qt.mainWindow(), title='Find Icon', name=None): - window = FindIconDialog(parent) + def getIconName(cls, parent=hou.qt.mainWindow(), title='Icons', name=None): + window = IconListDialog(parent) window.setWindowTitle('TDK: ' + title) window.enableDialogMode() @@ -361,9 +352,9 @@ def getIconName(cls, parent=hou.qt.mainWindow(), title='Find Icon', name=None): break if window.exec_() and window.icon_list_view.currentIndex().isValid(): - return window.icon_list_view.currentIndex().data(Qt.UserRole) + return window.icon_list_view.currentIndex().data(Qt.UserRole).replace('.svg', '') def findIcon(**kwargs): - window = FindIconDialog(hou.qt.mainWindow()) + window = IconListDialog(hou.qt.mainWindow()) window.show() diff --git a/python2.7libs/houdini_tdk/input_field.py b/python2.7libs/houdini_tdk/input_field.py new file mode 100644 index 0000000..1b49114 --- /dev/null +++ b/python2.7libs/houdini_tdk/input_field.py @@ -0,0 +1,42 @@ +""" +Tool Development Kit for SideFX Houdini +Copyright (C) 2021 Ivan Titov + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +try: + from PyQt5.QtWidgets import * + from PyQt5.QtGui import * + from PyQt5.QtCore import * + + Signal = pyqtSignal +except ImportError: + from PySide2.QtWidgets import * + from PySide2.QtGui import * + from PySide2.QtCore import * + + +class InputField(QLineEdit): + def keyPressEvent(self, event): + if event.matches(QKeySequence.Cancel): + self.clear() + else: + super(InputField, self).keyPressEvent(event) + + def mousePressEvent(self, event): + if event.button() == Qt.MiddleButton and event.modifiers() == Qt.ControlModifier: + self.clear() + else: + super(InputField, self).mousePressEvent(event) diff --git a/python2.7libs/houdini_tdk/make_hda_by_template.py b/python2.7libs/houdini_tdk/make_hda_by_template.py index 7ad5555..9ab9dce 100644 --- a/python2.7libs/houdini_tdk/make_hda_by_template.py +++ b/python2.7libs/houdini_tdk/make_hda_by_template.py @@ -19,6 +19,7 @@ from __future__ import print_function import os +from lxml import etree try: from PyQt5.QtWidgets import * @@ -33,17 +34,26 @@ import hou -from .find_icon import FindIconDialog +from .icon_list import IconListDialog, standardIconExists +from .node_shape_list_dialog import NodeShapeListDialog +from .node_shape_preview import NodeShapePreview from .notification import notify +from .node_shape import NodeShape +from .input_field import InputField -def makeNewHDAFromTemplateNode(template_node, label, name=None, namespace=None, icon=None, - sections=None, version='1.0', location='$HOUDINI_USER_PREF_DIR/otls', - inherit_subnetwork=True): - template_node_type = template_node.type() - if template_node_type.name() != 'tdk::template': - raise TypeError +def qColorFromHoudiniColor(color): + return QColor.fromRgbF(*color.rgb()) + + +def houdiniColorFromQColor(color): + return hou.Color(color.getRgbF()[:3]) + +def makeNewHDAFromTemplateNode(template_node, label, name=None, namespace=None, icon=None, + tab_sections=None, version='1.0', location='$HOUDINI_USER_PREF_DIR/otls', + inherit_subnetwork=True, inherit_parm_template_group=True, color=None, + shape=None): location = hou.expandString(location) if not os.path.exists(location) or not os.path.isdir(location): raise IOError @@ -65,6 +75,7 @@ def makeNewHDAFromTemplateNode(template_node, label, name=None, namespace=None, else: new_type_name += '1.0' + template_node_type = template_node.type() template_def = template_node_type.definition() new_hda_file_name = new_type_name.replace(':', '_').replace('.', '_') + '.hda' @@ -72,24 +83,66 @@ def makeNewHDAFromTemplateNode(template_node, label, name=None, namespace=None, template_def.copyToHDAFile(new_hda_file_path, new_type_name) new_def = hou.hda.definitionsInFile(new_hda_file_path)[0] + extra_file_options = new_def.extraFileOptions() if inherit_subnetwork: new_def.updateFromNode(template_node) + if inherit_parm_template_group: + parm_template_group = template_node.parmTemplateGroup() + hou.hda.installFile(new_hda_file_path) + new_def.setParmTemplateGroup(parm_template_group) + hou.hda.uninstallFile(new_hda_file_path) + new_def.setDescription(label) if icon: new_def.setIcon(icon) - tools = new_def.sections()['Tools.shelf'] - content = tools.contents() - sections = sections or 'Digital Assets' - try: - content = content[:content.index('') + len('')] + \ - sections + content[content.index(''):] - tools.setContents(content) - except ValueError: - pass + if tab_sections and tab_sections.strip(): + sections = (section.strip() for section in tab_sections.split(',')) + try: + tools = new_def.sections()['Tools.shelf'] + content = tools.contents() + parser = etree.XMLParser(remove_blank_text=True, resolve_entities=False, strip_cdata=False) + root = etree.fromstring(content.encode('utf-8'), parser) + + tool = root.find('tool') + for submenu in tool.findall('toolSubmenu'): + tool.remove(submenu) + + for section in sections: + submenu = etree.Element('toolSubmenu') + submenu.text = section + tool.append(submenu) + + tools.setContents(etree.tostring(root, encoding='utf-8', pretty_print=True)) + except KeyError: + pass + + if new_def.hasSection('PreFirstCreate') and extra_file_options.get('PreFirstCreate/IsPython'): + pre_first_create_section = new_def.sections()['PreFirstCreate'] + else: + pre_first_create_section = new_def.addSection('PreFirstCreate') + new_def.setExtraFileOption('PreFirstCreate/IsExpr', False) + new_def.setExtraFileOption('PreFirstCreate/IsScript', True) + new_def.setExtraFileOption('PreFirstCreate/IsPython', True) + + if color is not None: + set_default_color_code = "# Generated by TDK (https://github.com/anvdev/Houdini_TDK)\n" \ + "kwargs['type'].setDefaultColor(hou.Color({}))\n\n".format(color.getRgbF()[:3]) + + content = pre_first_create_section.contents() + content += set_default_color_code + pre_first_create_section.setContents(content) + + if shape is not None: + set_default_shape_code = "# Generated by TDK(https://github.com/anvdev/Houdini_TDK)\n" \ + "kwargs['type'].setDefaultShape('{}')\n\n".format(shape) + + content = pre_first_create_section.contents() + content += set_default_shape_code + pre_first_create_section.setContents(content) return new_def @@ -102,23 +155,96 @@ def __init__(self): layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(4) - self.edit = QLineEdit() + self.edit = InputField() layout.addWidget(self.edit) - self.pick_icon_button = QPushButton() - self.pick_icon_button.setToolTip('Pick icon') - self.pick_icon_button.setFixedSize(24, 24) - self.pick_icon_button.setIcon(hou.qt.Icon('OBJ_hlight', 16, 16)) - self.pick_icon_button.clicked.connect(self._pickIcon) - layout.addWidget(self.pick_icon_button) + self.icon_preview = QLabel() + self.icon_preview.setToolTip('Icon preview') + self.icon_preview.setFixedSize(24, 24) + self.icon_preview.setAlignment(Qt.AlignCenter) + layout.addWidget(self.icon_preview) + self.edit.textChanged.connect(self.updateIconPreview) + + self.pick_icon_from_disk_button = QPushButton() + self.pick_icon_from_disk_button.setToolTip('Pick icon from disk') + self.pick_icon_from_disk_button.setFixedSize(24, 24) + self.pick_icon_from_disk_button.setIcon(hou.qt.Icon('BUTTONS_chooser_file', 16, 16)) + self.pick_icon_from_disk_button.clicked.connect(self._pickIconFromDisk) + layout.addWidget(self.pick_icon_from_disk_button) + + self.pick_icon_from_houdini_button = QPushButton() + self.pick_icon_from_houdini_button.setToolTip('Pick icon from Houdini') + self.pick_icon_from_houdini_button.setFixedSize(24, 24) + self.pick_icon_from_houdini_button.setIcon(hou.qt.Icon('OBJ_hlight', 16, 16)) + self.pick_icon_from_houdini_button.clicked.connect(self._pickIconFromHoudini) + layout.addWidget(self.pick_icon_from_houdini_button) def text(self): return self.edit.text() - def _pickIcon(self): - icon = FindIconDialog.getIconName(self, 'Pick Icon', self.edit.text()) - if icon: - self.edit.setText(icon.replace('.svg', '')) + def updateIconPreview(self): + icon_file_name = self.edit.text() + + if not icon_file_name: + self.icon_preview.clear() + return + + if os.path.isfile(icon_file_name): # Todo: Limit allowed file size + _, ext = os.path.splitext(icon_file_name) + if ext in ('.jpg', '.jpeg', '.png', '.bmp', '.tga', '.tif', '.tiff'): + image = QImage(icon_file_name) + self.icon_preview.setPixmap(QPixmap.fromImage(image).scaled(24, 24, Qt.KeepAspectRatio)) + else: # Fallback to Houdini loading + with hou.undos.disabler(): + try: + comp_net = hou.node('/img/').createNode('img') + file_node = comp_net.createNode('file') + file_node.parm('filename1').set(icon_file_name) + + # Todo: Support alpha channel + image_data = file_node.allPixelsAsString(depth=hou.imageDepth.Int8) + image = QImage(image_data, file_node.xRes(), file_node.yRes(), QImage.Format_RGB888) + + self.icon_preview.setPixmap(QPixmap.fromImage(image).scaled(24, 24, Qt.KeepAspectRatio)) + except hou.OperationFailed: + self.icon_preview.clear() + finally: + comp_net.destroy() + else: + try: + icon = hou.qt.Icon(icon_file_name, 24, 24) + self.icon_preview.setPixmap(icon.pixmap(24, 24)) + except hou.OperationFailed: + self.icon_preview.clear() + + def setText(self, text): + self.edit.setText(text) + self.updateIconPreview() + + def _pickIconFromDisk(self): + path = self.edit.text() + if os.path.isdir(path): + initial_dir = path + elif os.path.isfile(path): + initial_dir = os.path.dirname(path) + else: + initial_dir = os.path.dirname(hou.hipFile.path()) + icon_file_name, _ = QFileDialog.getOpenFileName(self, 'Pick Icon', initial_dir, + filter='Images (*.pic *.pic.Z *.picZ *.pic.gz *.picgz *.rat ' + '*.tbf *.dsm *.picnc *.piclc *.rgb *.rgba *.sgi *.tif ' + '*.tif3 *.tif16 *.tif32 *.tiff *.yuv *.pix *.als *.cin ' + '*.kdk *.exr *.psd *.psb *.si *.tga *.vst *.vtg *.rla ' + '*.rla16 *.rlb *.rlb16 *.hdr *.ptx *.ptex *.ies *.dds ' + '*.qtl *.pic *.pic.Z *.pic.gz *.jpg *.jpeg *.bmp *.png ' + '*.svg *.);;' + 'All (*.*)') + if icon_file_name: + self.edit.setText(icon_file_name) + + def _pickIconFromHoudini(self): + icon_file_name = IconListDialog.getIconName(self, 'Pick Icon', self.edit.text()) + if icon_file_name: + self.edit.setText(icon_file_name) class LocationField(QWidget): @@ -129,7 +255,7 @@ def __init__(self, content=''): layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(4) - self.edit = QLineEdit(content) + self.edit = InputField(content) layout.addWidget(self.edit) self.pick_location_button = QPushButton() @@ -152,12 +278,131 @@ def _pickLocation(self): self.edit.setText(path) +class ColorField(QWidget): + def __init__(self, node): + super(ColorField, self).__init__() + + self.node_color = qColorFromHoudiniColor(node.color()) + self.default_node_type_color = qColorFromHoudiniColor(node.type().defaultColor()) + + layout = QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(4) + + self.edit = InputField() + self.edit.setText(self.node_color.name()) + self.edit.installEventFilter(self) + layout.addWidget(self.edit) + + self.pick_color_button = hou.qt.ColorSwatchButton() + self.pick_color_button.setToolTip('Pick color') + self.pick_color_button.setFixedSize(52, 24) + self.pick_color_button.setColor(self.node_color) + self.pick_color_button.colorChanged.connect(self._onColorPicked) + layout.addWidget(self.pick_color_button) + self.edit.textChanged.connect(self._onColorNameChanged) + + def eventFilter(self, watched, event): + if watched == self.edit: + if event.type() == QEvent.MouseButtonPress: + button = event.button() + modifiers = event.modifiers() + if button == Qt.LeftButton and modifiers == Qt.NoModifier: + self.edit.selectAll() + return True + elif button == Qt.MiddleButton and modifiers == Qt.ControlModifier: + self.edit.setText(self.node_color.name()) + return True + return False + + def text(self): + return self.edit.text() + + def setText(self, text): + self.edit.setText(text) + # self._onColorNameChanged(text) + + def color(self): + color_name = self.edit.text() + if QColor.isValidColor(color_name): + color = QColor(color_name) + if color != self.default_node_type_color: + return color + + def _onColorNameChanged(self, name): + self.pick_color_button.setColor(QColor(name)) + + def _onColorPicked(self, color): + self.edit.blockSignals(True) + self.edit.setText(color.name()) + self.edit.blockSignals(False) + + +class NodeShapeField(QWidget): + def __init__(self, node): + super(NodeShapeField, self).__init__() + + self.node_shape = node.userData('nodeshape') or '' + self.default_node_type_shape = node.type().defaultShape() + + layout = QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(4) + + self.edit = InputField(self.node_shape) + self.edit.installEventFilter(self) + layout.addWidget(self.edit) + + self.shape_preview = NodeShapePreview() + self.shape_preview.setToolTip('Shape preview') + self.shape_preview.setFixedSize(52, 24) + layout.addWidget(self.shape_preview) + self.edit.textChanged.connect(self.shape_preview.setShape) + if self.node_shape: + self.shape_preview.setShape(self.node_shape) + + self.pick_shape_button = QPushButton() + self.pick_shape_button.setToolTip('Pick shape') + self.pick_shape_button.setFixedSize(24, 24) + self.pick_shape_button.setIcon(hou.qt.Icon('NETVIEW_shape_palette', 16, 16)) + self.pick_shape_button.clicked.connect(self._pickShape) + layout.addWidget(self.pick_shape_button) + + def eventFilter(self, watched, event): + if watched == self.edit: + if event.type() == QEvent.MouseButtonPress: + button = event.button() + modifiers = event.modifiers() + if button == Qt.LeftButton and modifiers == Qt.NoModifier: + self.edit.selectAll() + return True + return False + + def text(self): + return self.edit.text() + + def setText(self, text): + self.edit.setText(text) + # self.shape_preview.setShape(text) + + def shape(self): + name = self.edit.text() + if NodeShape.isValidShape(name): + return name + + def _pickShape(self): + shape_name = NodeShapeListDialog.getShapeName(self, 'Pick Node Shape', self.edit.text()) + if shape_name: + self.edit.setText(shape_name) + + class MakeHDAByTemplateDialog(QDialog): def __init__(self, node, parent=None): super(MakeHDAByTemplateDialog, self).__init__(parent) # Data self.node = node + self.__user_template_used = node.type().name() != 'tdk::template' self.setWindowTitle('TDK: HDA by Template') self.setWindowIcon(hou.qt.Icon('NODEFLAGS_template', 32, 32)) @@ -175,32 +420,42 @@ def __init__(self, node, parent=None): self.location_field = LocationField('$HOUDINI_USER_PREF_DIR/otls') form_layout.addRow('Location', self.location_field) - self.label_field = QLineEdit() + self.label_field = InputField() self.label_field.textChanged.connect(self._onLabelChanged) form_layout.addRow('Label', self.label_field) - self.name_field = QLineEdit() + self.name_field = InputField() self.name_field.textChanged.connect(self._onNameChanged) form_layout.addRow('Name', self.name_field) - self.author_field = QLineEdit() + self.author_field = InputField() self.author_field.textChanged.connect(self._onAuthorChanged) form_layout.addRow('Author', self.author_field) - self.sections = QLineEdit() + self.sections = InputField() self.sections.textChanged.connect(self._onSectionsChanged) form_layout.addRow('Sections', self.sections) self.icon_field = IconField() form_layout.addRow('Icon', self.icon_field) - self.version_field = QLineEdit('1.0') + self.version_field = InputField('1.0') form_layout.addRow('Version', self.version_field) + self.color_field = ColorField(node) + form_layout.addRow('Color', self.color_field) + + self.shape_field = NodeShapeField(node) + form_layout.addRow('Shape', self.shape_field) + self.inherit_subnetwork_toggle = QCheckBox('Inherit subnetwork') self.inherit_subnetwork_toggle.setChecked(True) form_layout.addWidget(self.inherit_subnetwork_toggle) + self.inherit_parm_template_group_toggle = QCheckBox('Inherit parameters') + self.inherit_parm_template_group_toggle.setChecked(True) + form_layout.addWidget(self.inherit_parm_template_group_toggle) + self.install_toggle = QCheckBox('Install new HDA') self.install_toggle.setChecked(True) form_layout.addWidget(self.install_toggle) @@ -234,9 +489,29 @@ def __init__(self, node, parent=None): self.__author_changed = False self.__sections_changed = False - user_name = hou.userName() - self.author_field.setText(user_name) - self._onAuthorChanged(user_name) + # Fill Fields + _, namespace, name, version = node.type().nameComponents() + + if self.__user_template_used: + label = name + else: + label = node.name() + label = label.replace('_', ' ').title() + self.label_field.setText(label) + self._onLabelChanged(label) + + if self.__user_template_used: + author = namespace or hou.userName() + else: + author = hou.userName() + author = author.replace('_', ' ').title() + self.author_field.setText(author) + self._onAuthorChanged(author) + + if self.__user_template_used: + icon_name = node.type().icon() + if standardIconExists(icon_name): + self.icon_field.setText(icon_name) def _onLabelChanged(self, label): self.__label_changed = True @@ -272,6 +547,8 @@ def _onSectionsChanged(self, sections): def _onOk(self): if self.node: + color = self.color_field.color() + shape = self.shape_field.shape() definition = makeNewHDAFromTemplateNode(self.node, self.label_field.text(), self.name_field.text(), @@ -280,12 +557,23 @@ def _onOk(self): self.sections.text(), self.version_field.text(), self.location_field.path(), - self.inherit_subnetwork_toggle.isChecked()) + self.inherit_subnetwork_toggle.isChecked(), + self.inherit_parm_template_group_toggle.isChecked(), + color, + shape) + if self.install_toggle.isChecked(): hou.hda.installFile(definition.libraryFilePath()) if self.replace_node_toggle.isChecked(): - self.node = self.node.changeNodeType(definition.nodeTypeName(), - keep_network_contents=False) + self.node = self.node.changeNodeType(definition.nodeTypeName(), keep_network_contents=False) + self.node.setCurrent(True, True) + + if color: + definition.nodeType().setDefaultColor(houdiniColorFromQColor(color)) + + if shape: + definition.nodeType().setDefaultShape(shape) + if self.open_type_properties_toggle.isChecked(): if self.replace_node_toggle.isChecked(): hou.ui.openTypePropertiesDialog(self.node) @@ -299,14 +587,16 @@ def showMakeHDAByTemplateDialog(**kwargs): nodes = kwargs['node'], else: nodes = hou.selectedNodes() + if not nodes: notify('No node selected', hou.severityType.Error) return elif len(nodes) > 1: notify('Too much nodes selected', hou.severityType.Error) return - elif not nodes[0].type().name().startswith('tdk::template'): - notify('Node is not TDK Template', hou.severityType.Error) + elif nodes[0].type().definition() is None: + notify('Node cannot be user as a template', hou.severityType.Error) return + window = MakeHDAByTemplateDialog(nodes[0], hou.qt.mainWindow()) window.show() diff --git a/python2.7libs/houdini_tdk/node_shape.py b/python2.7libs/houdini_tdk/node_shape.py new file mode 100644 index 0000000..bd18e42 --- /dev/null +++ b/python2.7libs/houdini_tdk/node_shape.py @@ -0,0 +1,231 @@ +""" +Tool Development Kit for SideFX Houdini +Copyright (C) 2021 Ivan Titov + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from __future__ import print_function + +import json +import os + +try: + from PyQt5.QtWidgets import * + from PyQt5.QtGui import * + from PyQt5.QtCore import * + + Signal = pyqtSignal +except ImportError: + from PySide2.QtWidgets import * + from PySide2.QtGui import * + from PySide2.QtCore import * + +import hou + + +class BoundingRectF(QRectF): + def __init__(self, *args): + super(BoundingRectF, self).__init__(*args) + + self.__initial = True + + def normalize(self): + if self.width() < 0: + left = self.left() + self.setLeft(self.right()) + self.setRight(left) + + if self.height() < 0: + top = self.top() + self.setTop(self.bottom()) + self.setBottom(top) + + def addPosition(self, x, y): + if self.__initial: + self.moveTo(x, y) + self.__initial = False + return + + if x < self.left(): + self.setLeft(x) + elif x > self.right(): + self.setRight(x) + + if y < self.top(): + self.setTop(y) + elif y > self.bottom(): + self.setBottom(y) + + def addPoint(self, point): + self.addPosition(point.x(), point.y()) + + def addPositions(self, positions): + if not positions: + return + + if self.__initial: + left = top = float('+inf') + right = bottom = float('-inf') + self.__initial = False + else: + left = self.left() + top = self.top() + right = self.right() + bottom = self.bottom() + + for x, y in positions: + left = min(x, left) + top = min(y, top) + right = max(x, right) + bottom = max(y, bottom) + + self.setCoords(left, top, right, bottom) + + @staticmethod + def fromPositions(positions): + new = BoundingRectF() + new.addPositions(positions) + return new + + def addPoints(self, points): + self.addPositions((point.x(), point.y()) for point in points) + + @staticmethod + def fromPoints(points): + new = BoundingRectF() + new.addPoints(points) + return new + + +EXCLUDED_SHAPES = ('vop', 'task', 'shop', 'cop2', 'subnet_input') + + +class NodeShape(object): + + def __init__(self): + self.__valid = False + self.__name = None + self.__points = () + + def isValid(self): + return self.__valid + + def __bool__(self): + return self.__valid + + def name(self): + return self.__name + + def __copy__(self): + new = NodeShape() + new.__name = self.__name + new.__points = self.__points + new.__valid = self.__valid + return new + + def copy(self): + return self.__copy__() + + def transformToRect(self, rect, aspect_ratio_mode=Qt.KeepAspectRatio): + target_rect = QRectF(rect) + bounding_rect = BoundingRectF.fromPoints(self.__points) + + target_rect.setSize(bounding_rect.size().scaled(rect.size(), aspect_ratio_mode)) + + # Todo: Alignment + target_rect.moveCenter(rect.center()) + + bounding_polygon = QPolygonF(bounding_rect) + bounding_polygon.takeLast() + + target_polygon = QPolygonF(target_rect) + target_polygon.takeLast() + + return QTransform.quadToQuad(bounding_polygon, target_polygon) + + def fitInRect(self, rect, aspect_ratio_mode=Qt.KeepAspectRatio): + transform = self.transformToRect(rect, aspect_ratio_mode) + self.__points = tuple(transform.map(point) for point in self.__points) + + def fittedInRect(self, rect, aspect_ratio_mode=Qt.KeepAspectRatio): + new = self.copy() + new.fitInRect(rect, aspect_ratio_mode) + return new + + def painterPath(self): + path = QPainterPath(self.__points[0]) + + for point in self.__points: + path.lineTo(point) + path.closeSubpath() + + return path + + @staticmethod + def fromFile(file_path, allow_excluded=False): + shape = NodeShape() + + try: + with open(file_path) as file: + shape_data = json.load(file) + except IOError: + return shape + + if 'name' in shape_data: + shape.__name = shape_data['name'] + else: + shape.__name, _ = os.path.splitext(os.path.basename(file_path)) + + if not allow_excluded and shape.__name in EXCLUDED_SHAPES: + return shape + + if not shape_data or 'outline' not in shape_data: + return shape + + shape.__points = tuple(QPointF(x, -y) for x, y in shape_data['outline']) + + shape.__valid = True + return shape + + @staticmethod + def fromName(name, allow_excluded=False): + if not name: + return NodeShape() + + name = name.replace(' ', '_').lower() + + if allow_excluded and name in EXCLUDED_SHAPES: + return NodeShape() + + file_name = name + '.json' + shape_files = hou.findFilesWithExtension('json', 'config/NodeShapes') + for file_path in shape_files: + if file_name in file_path.lower(): + return NodeShape.fromFile(file_path) + return NodeShape() + + @staticmethod + def isValidShape(name): + name = name.replace(' ', '_').lower() + + if name in EXCLUDED_SHAPES: + return False + + file_name = name + '.json' + shape_files = hou.findFilesWithExtension('json', 'config/NodeShapes') + for file_path in shape_files: + if file_name in file_path.lower(): + return True + return False diff --git a/python2.7libs/houdini_tdk/node_shape_delegate.py b/python2.7libs/houdini_tdk/node_shape_delegate.py new file mode 100644 index 0000000..8e0b110 --- /dev/null +++ b/python2.7libs/houdini_tdk/node_shape_delegate.py @@ -0,0 +1,77 @@ +""" +Tool Development Kit for SideFX Houdini +Copyright (C) 2021 Ivan Titov + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from __future__ import print_function + +try: + from PyQt5.QtWidgets import * + from PyQt5.QtGui import * + from PyQt5.QtCore import * + + Signal = pyqtSignal +except ImportError: + from PySide2.QtWidgets import * + from PySide2.QtGui import * + from PySide2.QtCore import * + +from houdini_tdk.node_shape_list_model import NodeShapeListModel + +qInstallMessageHandler(lambda *args: None) + + +class NodeShapeDelegate(QStyledItemDelegate): + def sizeHint(self, option, index): + grid_width = self.parent().gridSize().width() + if grid_width > 100: + ratio = grid_width / 105.0 + width = 100 * ratio + else: + width = 100 + return QSize(width, 70) + + def paint(self, painter, option, index): + painter.save() + + painter.eraseRect(option.rect) + + painter.setRenderHint(QPainter.Antialiasing) + painter.setRenderHint(QPainter.HighQualityAntialiasing) + + pen_width = painter.pen().width() + inner_rect = option.rect.adjusted(pen_width, pen_width, -pen_width, -pen_width) + spacing = pen_width * 4 + inner_rect_spaced = inner_rect.adjusted(spacing, spacing, -spacing, -spacing) + + if option.state & QStyle.State_Selected: + painter.drawRect(inner_rect) + + if inner_rect_spaced.width() > 30: + metrics = painter.fontMetrics() + text_height = metrics.height() + text = metrics.elidedText(index.data(Qt.DisplayRole), Qt.ElideRight, inner_rect_spaced.width()) + painter.drawText(inner_rect, Qt.AlignHCenter | Qt.AlignBottom, text) + else: + text_height = 0 + + if inner_rect_spaced.width() > 10: + shape = index.data(NodeShapeListModel.ShapeRole) + painter.setBrush(painter.pen().color().darker()) + icon_rect = inner_rect_spaced.adjusted(0, 0, 0, -text_height) + painter.drawPath(shape.fittedInRect(icon_rect).painterPath()) + + painter.restore() diff --git a/python2.7libs/houdini_tdk/node_shape_list_dialog.py b/python2.7libs/houdini_tdk/node_shape_list_dialog.py new file mode 100644 index 0000000..b54a6c3 --- /dev/null +++ b/python2.7libs/houdini_tdk/node_shape_list_dialog.py @@ -0,0 +1,122 @@ +""" +Tool Development Kit for SideFX Houdini +Copyright (C) 2021 Ivan Titov + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from __future__ import print_function + +try: + from PyQt5.QtWidgets import * + from PyQt5.QtGui import * + from PyQt5.QtCore import * + + Signal = pyqtSignal +except ImportError: + from PySide2.QtWidgets import * + from PySide2.QtGui import * + from PySide2.QtCore import * + +import hou + +from .filter_field import FilterField +from .node_shape_list_model import NodeShapeListModel +from .node_shape_list_view import NodeShapeListView +from .node_shape_delegate import NodeShapeDelegate +from .fuzzy_filter_proxy_model import FuzzyFilterProxyModel + + +class NodeShapeListDialog(QDialog): + def __init__(self, parent=None): + super(NodeShapeListDialog, self).__init__(parent, Qt.Window) + + self.setWindowTitle('TDK: Node Shapes') + self.setWindowIcon(hou.qt.Icon('NETVIEW_shape_palette', 32, 32)) + self.resize(820, 500) + + # Layout + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(4, 4, 4, 4) + main_layout.setSpacing(4) + + # Filter + self.filter_field = FilterField() + main_layout.addWidget(self.filter_field) + + # Node Shape List + self.shape_list_model = NodeShapeListModel(self) + self.shape_list_model.updateNodeShapeList() + + self.filter_proxy_model = FuzzyFilterProxyModel(self, Qt.DisplayRole) + self.filter_proxy_model.setSourceModel(self.shape_list_model) + self.filter_field.textChanged.connect(self.filter_proxy_model.setFilterPattern) + + self.shape_list_view = NodeShapeListView() + self.shape_list_view.setModel(self.filter_proxy_model) + self.shape_list_view.setItemDelegate(NodeShapeDelegate(self.shape_list_view)) + self.shape_list_view.itemDoubleClicked.connect(self.accept) + main_layout.addWidget(self.shape_list_view) + + # Buttons + buttons_layout = QHBoxLayout() + main_layout.addLayout(buttons_layout) + + spacer = QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Ignored) + buttons_layout.addSpacerItem(spacer) + + self.ok_button = QPushButton('OK') + self.ok_button.setVisible(False) + self.ok_button.clicked.connect(self.accept) + buttons_layout.addWidget(self.ok_button) + + self.cancel_button = QPushButton('Cancel') + self.cancel_button.setVisible(False) + self.cancel_button.clicked.connect(self.reject) + buttons_layout.addWidget(self.cancel_button) + + def keyPressEvent(self, event): + if event.matches(QKeySequence.Find) or event.key() == Qt.Key_F3: + self.filter_field.setFocus() + self.filter_field.selectAll() + else: + super(NodeShapeListDialog, self).keyPressEvent(event) + + def enableDialogMode(self): + self.shape_list_view.setSelectionMode(QAbstractItemView.SingleSelection) + self.shape_list_view.enableDoubleClickedSignal() + self.ok_button.setVisible(True) + self.cancel_button.setVisible(True) + + @classmethod + def getShapeName(cls, parent=hou.qt.mainWindow(), title='Node Shapes', name=None): + window = NodeShapeListDialog(parent) + window.setWindowTitle('TDK: ' + title) + window.enableDialogMode() + + if name: + model = window.shape_list_view.model() + for row in range(model.rowCount()): + index = model.index(row, 0) + if index.data(NodeShapeListModel.ShapeNameRole) == name: + window.shape_list_view.setCurrentIndex(index) + break + + if window.exec_() and window.shape_list_view.currentIndex().isValid(): + return window.shape_list_view.currentIndex().data(NodeShapeListModel.ShapeNameRole) + + +def findNodeShape(**kwargs): + window = NodeShapeListDialog(hou.qt.mainWindow()) + window.show() diff --git a/python2.7libs/houdini_tdk/node_shape_list_model.py b/python2.7libs/houdini_tdk/node_shape_list_model.py new file mode 100644 index 0000000..f7b1123 --- /dev/null +++ b/python2.7libs/houdini_tdk/node_shape_list_model.py @@ -0,0 +1,82 @@ +""" +Tool Development Kit for SideFX Houdini +Copyright (C) 2021 Ivan Titov + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from __future__ import print_function + +try: + from PyQt5.QtWidgets import * + from PyQt5.QtGui import * + from PyQt5.QtCore import * + + Signal = pyqtSignal +except ImportError: + from PySide2.QtWidgets import * + from PySide2.QtGui import * + from PySide2.QtCore import * + +import hou + +from .node_shape import NodeShape + + +class NodeShapeListModel(QAbstractListModel): + # Roles + ShapeNameRole = Qt.UserRole + 1 + ShapeRole = Qt.UserRole + 2 + + def __init__(self, parent=None): + super(NodeShapeListModel, self).__init__(parent) + + self.shapes = () + + def updateNodeShapeList(self): + self.beginResetModel() + + shapes = [] + shape_files = hou.findFilesWithExtension('json', 'config/NodeShapes') + for file_path in shape_files: + shape = NodeShape.fromFile(file_path) + if shape.isValid(): + shapes.append(shape) + + self.shapes = tuple(shapes) + self.endResetModel() + + def rowCount(self, parent): + return len(self.shapes) + + def index(self, row, column, parent): + if not self.hasIndex(row, column, parent): + return QModelIndex() + + return self.createIndex(row, column, self.shapes[row]) + + def data(self, index, role): + if not index.isValid(): + return + + shape = index.internalPointer() + + if role == Qt.DisplayRole: + return shape.name().replace('_', ' ').title() + elif role == Qt.ToolTipRole: + return shape.name() + elif role == NodeShapeListModel.ShapeNameRole: + return shape.name() + elif role == NodeShapeListModel.ShapeRole: + return shape diff --git a/python2.7libs/houdini_tdk/node_shape_list_view.py b/python2.7libs/houdini_tdk/node_shape_list_view.py new file mode 100644 index 0000000..b27b276 --- /dev/null +++ b/python2.7libs/houdini_tdk/node_shape_list_view.py @@ -0,0 +1,79 @@ +""" +Tool Development Kit for SideFX Houdini +Copyright (C) 2021 Ivan Titov + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from __future__ import print_function + +try: + from PyQt5.QtWidgets import * + from PyQt5.QtGui import * + from PyQt5.QtCore import * + + Signal = pyqtSignal +except ImportError: + from PySide2.QtWidgets import * + from PySide2.QtGui import * + from PySide2.QtCore import * + + +class NodeShapeListView(QListView): + # Signals + itemDoubleClicked = Signal(QModelIndex) + + def __init__(self): + super(NodeShapeListView, self).__init__() + self.viewport().setContentsMargins(15, 15, 15, 15) + self.setViewMode(QListView.IconMode) + + self.setUniformItemSizes(True) + + self.setResizeMode(QListView.Adjust) + self.setDragDropMode(QAbstractItemView.NoDragDrop) + self.setSelectionMode(QAbstractItemView.ExtendedSelection) + + self.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel) + self.verticalScrollBar().setSingleStep(30) + + self.setGridSize(QSize(100, 88)) + + # Item Double Clicked + self._item_double_clicked_signal_enabled = False + self.doubleClicked.connect(self.__emitItemDoubleClicked) + + def resizeEvent(self, event): + super(NodeShapeListView, self).resizeEvent(event) + + # Update grid size + grid_size = self.gridSize() + column_count = 7 + spacing = 5 + grid_size.setWidth(self.viewport().width() / column_count - spacing) + self.setGridSize(grid_size) + + def doubleClickedSignalEnabled(self): + return self._item_double_clicked_signal_enabled + + def enableDoubleClickedSignal(self, enable=True): + self._item_double_clicked_signal_enabled = enable + + def __emitItemDoubleClicked(self): + if not self._item_double_clicked_signal_enabled: + return + + index = self.currentIndex() + if index.isValid(): + self.itemDoubleClicked.emit(index) diff --git a/python2.7libs/houdini_tdk/node_shape_preview.py b/python2.7libs/houdini_tdk/node_shape_preview.py new file mode 100644 index 0000000..0e0dfec --- /dev/null +++ b/python2.7libs/houdini_tdk/node_shape_preview.py @@ -0,0 +1,83 @@ +""" +Tool Development Kit for SideFX Houdini +Copyright (C) 2021 Ivan Titov + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from __future__ import print_function + +try: + from PyQt5.QtWidgets import * + from PyQt5.QtGui import * + from PyQt5.QtCore import * + + Signal = pyqtSignal +except ImportError: + from PySide2.QtWidgets import * + from PySide2.QtGui import * + from PySide2.QtCore import * + +from .node_shape import NodeShape + + +class NodeShapePreview(QWidget): + def __init__(self, parent=None): + super(NodeShapePreview, self).__init__(parent) + + self._shape = None + self._path = None + + def recacheShape(self, spacing=1): + rect = self.rect().adjusted(spacing, spacing, -spacing, -spacing) + self._path = self._shape.fittedInRect(rect).painterPath() + self.repaint() + + def setShape(self, shape_name): + if not shape_name: + self._shape = None + self.repaint() + return + + shape = NodeShape.fromName(shape_name) + + if not shape.isValid(): + self._shape = None + self.repaint() + return + + self._shape = shape.copy() + self.recacheShape() + self.repaint() + + def paintEvent(self, event): + if not self._shape: + return + + if not self._path: + self._path = self._shape.painterPath() + + p = QPainter(self) + + p.eraseRect(event.rect()) + + rect_width = event.rect().width() + if rect_width < 800: + p.setRenderHint(QPainter.Antialiasing) + if rect_width < 400: + p.setRenderHint(QPainter.HighQualityAntialiasing) + + p.setBrush(p.pen().color().darker()) + p.drawPath(self._path) + p.end() diff --git a/python2.7libs/houdini_tdk/show_node_user_data.py b/python2.7libs/houdini_tdk/show_node_user_data.py deleted file mode 100644 index c24891e..0000000 --- a/python2.7libs/houdini_tdk/show_node_user_data.py +++ /dev/null @@ -1,244 +0,0 @@ -""" -Tool Development Kit for SideFX Houdini -Copyright (C) 2021 Ivan Titov - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" - -import json - -try: - from PyQt5.QtWidgets import * - from PyQt5.QtGui import * - from PyQt5.QtCore import * - - Signal = pyqtSignal -except ImportError: - from PySide2.QtWidgets import * - from PySide2.QtGui import * - from PySide2.QtCore import * - -import hou - -from notification import notify - - -class UserDataItem: - def __init__(self, key, data, cached): - self.key = key - self.data = data - self.cached = cached - - -class UserDataModel(QAbstractListModel): - DEFAULT_ICON = hou.qt.Icon('DATATYPES_file', 16, 16) - CACHED_DATA_ICON = hou.qt.Icon('NETVIEW_time_dependent_badge', 16, 16) - - def __init__(self, parent=None): - super(UserDataModel, self).__init__(parent) - - # Data - self.__data = [] - - def updateDataFromNode(self, node): - self.beginResetModel() - self.__data = [] - if node is not None: - for key, data in node.userDataDict().items(): - self.__data.append(UserDataItem(key, data, False)) - - for key, data in node.cachedUserDataDict().items(): - self.__data.append(UserDataItem(key, data, True)) - self.endResetModel() - - def indexByKey(self, key): - for index, data in enumerate(self.__data): - if data.key == key: - return self.index(index, 0) - - return QModelIndex() - - def rowCount(self, parent): - return len(self.__data) - - def data(self, index, role): - item = self.__data[index.row()] - if role == Qt.DisplayRole: - return item.key - elif role == Qt.DecorationRole: - if item.cached: - return UserDataModel.CACHED_DATA_ICON - else: - return UserDataModel.DEFAULT_ICON - elif role == Qt.UserRole: - return item.data - - -class UserDataListView(QListView): - def __init__(self): - super(UserDataListView, self).__init__() - - self.setAlternatingRowColors(True) - - -class UserDataWindow(QWidget): - def __init__(self, parent=None): - super(UserDataWindow, self).__init__(parent, Qt.Window) - - self.setWindowIcon(hou.qt.Icon('TOP_jsondata', 32, 32)) - - # Layout - main_layout = QVBoxLayout(self) - main_layout.setContentsMargins(4, 4, 4, 4) - main_layout.setSpacing(4) - - splitter = QSplitter(Qt.Horizontal) - main_layout.addWidget(splitter) - - # Key List - self.user_data_model = UserDataModel() - - self.user_data_list = UserDataListView() - self.user_data_list.setModel(self.user_data_model) - self.user_data_list.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - selection_model = self.user_data_list.selectionModel() - selection_model.currentChanged.connect(self._readData) - splitter.addWidget(self.user_data_list) - - # Data View - self.user_data_view = QTextEdit() - splitter.addWidget(self.user_data_view) - - splitter.setStretchFactor(0, 1) - splitter.setStretchFactor(1, 3) - - # Data - self._current_key = None - self._node = None - - # Update - bottom_layout = QHBoxLayout() - bottom_layout.setContentsMargins(0, 0, 0, 0) - bottom_layout.setSpacing(4) - main_layout.addLayout(bottom_layout) - - update_button = QPushButton() - update_button.setToolTip('Update from Node') - update_button.setFixedSize(24, 24) - update_button.setIcon(hou.qt.Icon('NETVIEW_reload', 16, 16)) - update_button.clicked.connect(self.updateData) - bottom_layout.addWidget(update_button) - - self.update_timer = QTimer(self) - self.update_timer.setInterval(500) - self.update_timer.timeout.connect(self.updateData) - - self.auto_update_toggle = QCheckBox('Auto Update') - self.auto_update_toggle.setFixedWidth(100) - self.auto_update_toggle.setChecked(True) - self.auto_update_toggle.toggled.connect(self._switchTimer) - bottom_layout.addWidget(self.auto_update_toggle, 0) - - bottom_layout.addSpacerItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Ignored)) - - self.prettify_json_button = QPushButton() - self.prettify_json_button.setToolTip('Prettify JSON') - self.prettify_json_button.setFixedSize(24, 24) - self.prettify_json_button.setIcon(hou.qt.Icon('TOP_jsondata', 16, 16)) - self.prettify_json_button.clicked.connect(self._prettifyJSON) - bottom_layout.addWidget(self.prettify_json_button) - - def _updatePrettifyJSONButtonVisibility(self): - text = self.user_data_view.toPlainText() - try: - data = json.loads(text) - - if text == json.dumps(data, indent=4): - raise ValueError - - self.prettify_json_button.setVisible(True) - except ValueError: - self.prettify_json_button.setVisible(False) - - def _prettifyJSON(self): - text = self.user_data_view.toPlainText() - try: - data = json.loads(text) - text = json.dumps(data, indent=4) - self.user_data_view.setPlainText(text) - except ValueError: - return - - def _readData(self): - selection_model = self.user_data_list.selectionModel() - index = selection_model.currentIndex() - - if not index.isValid(): - return - - self._current_key = index.data(Qt.DisplayRole) - value = index.data(Qt.UserRole) - self.user_data_view.setText(value) - self._updatePrettifyJSONButtonVisibility() - - def _switchTimer(self): - if self.update_timer.isActive(): - self.update_timer.stop() - else: - self.update_timer.start() - - # def _removeCallbacks(self): - # if self.node: - # node.removeEventCallback((hou.nodeEventType.CustomDataChanged,), self.updateData) - - def __del__(self): - # self._removeCallbacks() - self.update_timer.stop() - - def updateData(self, auto=True): - if auto and not self.auto_update_toggle.isChecked(): - return - - if self._node: - self.user_data_model.updateDataFromNode(self._node) - - new_index = self.user_data_model.indexByKey(self._current_key) - if new_index.isValid(): - self.user_data_list.setCurrentIndex(new_index) - - def setCurrentNode(self, node): - if self._node: - self._removeCallbacks() - - self._node = node - # node.addEventCallback((hou.nodeEventType.CustomDataChanged,), self.updateData) - self.update_timer.start() - self.updateData(False) - self.setWindowTitle('TDK: Node User Data: ' + node.path()) - - -def showNodeUserData(node=None, **kwargs): - if node is None: - nodes = hou.selectedNodes() - if not nodes: - notify('No node selected', hou.severityType.Error) - return - elif len(nodes) > 1: - notify('Too much nodes selected', hou.severityType.Error) - return - node = nodes[0] - - window = UserDataWindow(hou.qt.mainWindow()) - window.setCurrentNode(node) - window.show() diff --git a/python2.7libs/houdini_tdk/show_user_data.py b/python2.7libs/houdini_tdk/show_user_data.py new file mode 100644 index 0000000..25a2d33 --- /dev/null +++ b/python2.7libs/houdini_tdk/show_user_data.py @@ -0,0 +1,423 @@ +""" +Tool Development Kit for SideFX Houdini +Copyright (C) 2021 Ivan Titov + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +import json +import re +from operator import attrgetter + +try: + from PyQt5.QtWidgets import * + from PyQt5.QtGui import * + from PyQt5.QtCore import * + + Signal = pyqtSignal +except ImportError: + from PySide2.QtWidgets import * + from PySide2.QtGui import * + from PySide2.QtCore import * +from lxml import etree + +import hou + +from .node_shape_preview import NodeShapePreview +from .notification import notify + + +class UserDataItem: + def __init__(self, key, data, cached): + self.key = key + self.data = data + self.cached = cached + + +class FilterEmptyProxyModel(QSortFilterProxyModel): + def __init__(self, parent=None): + super(FilterEmptyProxyModel, self).__init__(parent) + + self._enabled = False + + def setEnabled(self, enable): + self._enabled = enable + self.invalidate() + + def filterAcceptsRow(self, source_row, source_parent): + if not self._enabled: + return True + + source_model = self.sourceModel() + index = source_model.index(source_row, 0, source_parent) + data = source_model.data(index, Qt.UserRole) + return bool(data) + + +class UserDataModel(QAbstractListModel): + DEFAULT_ICON = hou.qt.Icon('DATATYPES_file', 16, 16) + CACHED_DATA_ICON = hou.qt.Icon('NETVIEW_time_dependent_badge', 16, 16) + + def __init__(self, parent=None): + super(UserDataModel, self).__init__(parent) + + # Data + self.__data = [] + + def updateDataFromNode(self, node): + self.beginResetModel() + + self.__data = [] + if node is not None: + persistent_items = [] + for key, data in node.userDataDict().items(): + persistent_items.append(UserDataItem(key, data, False)) + persistent_items.sort(key=attrgetter('key')) + self.__data.extend(persistent_items) + + cached_items = [] + for key, data in node.cachedUserDataDict().items(): + cached_items.append(UserDataItem(key, data, True)) + cached_items.sort(key=attrgetter('key')) + self.__data.extend(cached_items) + + self.endResetModel() + + def indexByKey(self, key): + for index, data in enumerate(self.__data): + if data.key == key: + return self.index(index, 0) + + return QModelIndex() + + def rowCount(self, parent): + return len(self.__data) + + def data(self, index, role): + item = self.__data[index.row()] + if role == Qt.DisplayRole: + return item.key + elif role == Qt.DecorationRole: + if item.cached: + return UserDataModel.CACHED_DATA_ICON + else: + return UserDataModel.DEFAULT_ICON + elif role == Qt.UserRole: + return item.data + + +class UserDataListView(QListView): + def __init__(self): + super(UserDataListView, self).__init__() + + self.setAlternatingRowColors(True) + + +def prettify(text): + # JSON + try: + data = json.loads(text) + return json.dumps(data, indent=4) + except ValueError: + pass + + # XML + try: + parser = etree.XMLParser(remove_blank_text=True, resolve_entities=False, strip_cdata=False) + data = etree.XML(text, parser) + data = etree.tostring(data, encoding='unicode', pretty_print=True) + return data + except etree.XMLSyntaxError: + pass + + # INI-like + # \n allowed, delimiter is ; + ini_regex = re.compile(r'([\w\d\"]+)[\s\n]*:?=[\s\n]*([\w\d\S]+)\n*;') + if ini_regex.match(text): + return '\n'.join(r.expand(r'\1 = \2;') for r in ini_regex.finditer(text)) + + # \n is delimiter + ini_wo_semicolon_regex = re.compile(r'([\w\d\"]+)[\s]*:?=[\s]*([\w\d\S]+)(?:\n|$)') + if ini_wo_semicolon_regex.match(text): + return '\n'.join(r.expand(r'\1 = \2') for r in ini_wo_semicolon_regex.finditer(text)) + + # Todo: CSV + + # Comma-separated sequence + comma_separated_seq_regex = re.compile(r'(?:\s*)(.+?)(?:\s*)(?:,|$)') + if comma_separated_seq_regex.match(text): + return ',\n'.join(comma_separated_seq_regex.findall(text)) + + return text + + +class UserDataWindow(QWidget): + def __init__(self, parent=None): + super(UserDataWindow, self).__init__(parent, Qt.Window) + + # Data + self._node = None + + # State + self._pinned = True + self._auto_update = True + self._word_wrap = True + self._prettify = False + self._current_key = None + + # Window + self.updateWindowTitle() + self.setWindowIcon(hou.qt.Icon('TOP_jsondata', 32, 32)) + + # Layout + main_layout = QHBoxLayout(self) + main_layout.setContentsMargins(4, 4, 4, 4) + main_layout.setSpacing(4) + + splitter = QSplitter(Qt.Horizontal) + main_layout.addWidget(splitter) + + # Key List + self.user_data_model = UserDataModel() + + self.user_data_filter_model = FilterEmptyProxyModel() + self.user_data_filter_model.setSourceModel(self.user_data_model) + + self.user_data_list = UserDataListView() + self.user_data_list.setModel(self.user_data_filter_model) + self.user_data_list.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + selection_model = self.user_data_list.selectionModel() + selection_model.currentChanged.connect(self._readData) + splitter.addWidget(self.user_data_list) + + # Data View + self.user_data_view = QTextEdit() + self.shape_preview = NodeShapePreview() + self.user_data_view.viewport().installEventFilter(self) + self.user_data_view.setPlaceholderText('Key has no data') + self.user_data_view.installEventFilter(self) + splitter.addWidget(self.user_data_view) + + splitter.setStretchFactor(0, 1) + splitter.setStretchFactor(1, 3) + + # Update + options_layout = QVBoxLayout() + options_layout.setContentsMargins(0, 0, 0, 0) + options_layout.setSpacing(4) + main_layout.addLayout(options_layout) + + self.update_timer = QTimer(self) + self.update_timer.setInterval(500) + self.update_timer.timeout.connect(self.updateData) + + self.pin_toggle = QPushButton() + self.pin_toggle.setCheckable(True) + self.pin_toggle.setChecked(True) + self.pin_toggle.setToolTip('Pin to node') + self.pin_toggle.setFixedSize(24, 24) + self.pin_toggle.setIcon(hou.qt.Icon('BUTTONS_pinned', 16, 16)) + self.pin_toggle.toggled.connect(self.__toggleUpdateNodeTimer) + options_layout.addWidget(self.pin_toggle) + + self.current_node_timer = QTimer(self) + self.current_node_timer.setInterval(300) + self.current_node_timer.timeout.connect(self.updateCurrentNode) + + self.hide_empty_toggle = QPushButton() + self.hide_empty_toggle.setCheckable(True) + self.hide_empty_toggle.setChecked(False) + self.hide_empty_toggle.setToolTip('Hide Empty') + self.hide_empty_toggle.setFixedSize(24, 24) + self.hide_empty_toggle.setIcon(hou.qt.Icon('NETVIEW_hidden_flag', 16, 16)) + self.hide_empty_toggle.toggled.connect(self.user_data_filter_model.setEnabled) + options_layout.addWidget(self.hide_empty_toggle) + + self.auto_update_toggle = QPushButton() + self.auto_update_toggle.setCheckable(True) + self.auto_update_toggle.setChecked(True) + self.auto_update_toggle.setToolTip('Auto Update') + self.auto_update_toggle.setFixedSize(24, 24) + self.auto_update_toggle.setIcon(hou.qt.Icon('NETVIEW_reload', 16, 16)) + self.auto_update_toggle.toggled.connect(self.__toggleUpdateTimer) + options_layout.addWidget(self.auto_update_toggle) + + self.word_wrap_toggle = QPushButton() + self.word_wrap_toggle.setCheckable(True) + self.word_wrap_toggle.setChecked(True) + self.word_wrap_toggle.setToolTip('Word-Wrap') + self.word_wrap_toggle.setFixedSize(24, 24) + self.word_wrap_toggle.setIcon(hou.qt.Icon('BUTTONS_decrease_indent', 16, 16)) + self.word_wrap_toggle.toggled.connect(self.setWordWrapEnabled) + options_layout.addWidget(self.word_wrap_toggle) + + self.prettify_toggle = QPushButton() + self.prettify_toggle.setCheckable(True) + self.prettify_toggle.setChecked(False) + self.prettify_toggle.setToolTip('Prettify') + self.prettify_toggle.setFixedSize(24, 24) + self.prettify_toggle.setIcon(hou.qt.Icon('TOP_jsondata', 16, 16)) + self.prettify_toggle.toggled.connect(self.setPrettifyEnabled) + options_layout.addWidget(self.prettify_toggle) + + options_layout.addSpacerItem(QSpacerItem(0, 0, QSizePolicy.Preferred, QSizePolicy.Expanding)) + + def updateWindowTitle(self): + title = 'TDK: Node User Data' + if self._node: + title += ': ' + self._node.path() + self.setWindowTitle(title) + + def setWordWrapEnabled(self, enable): + if enable: + self.user_data_view.setWordWrapMode(QTextOption.WrapAtWordBoundaryOrAnywhere) + else: + self.user_data_view.setWordWrapMode(QTextOption.NoWrap) + + def setPrettifyEnabled(self, enable): + self._prettify = enable + self.updateData() + + def _readData(self): + selection_model = self.user_data_list.selectionModel() + index = selection_model.currentIndex() + + if not index.isValid(): + return + + key = index.data(Qt.DisplayRole) + data = index.data(Qt.UserRole) + + if key == 'nodeshape': + self.shape_preview.setShape(data) + + if self._prettify: + data = prettify(data) + + if self.user_data_view.toPlainText() != data: + cursor = self.user_data_view.textCursor() + if cursor.hasSelection() and key == self._current_key: + selection_start = cursor.selectionStart() + selection_end = cursor.selectionEnd() + reselect = True + else: + selection_start = None + selection_end = None + reselect = False + + self.user_data_view.setPlainText(data) + + self._current_key = key + + if reselect: + cursor = self.user_data_view.textCursor() + cursor.movePosition(QTextCursor.Start, QTextCursor.MoveAnchor) + cursor.movePosition(QTextCursor.Right, QTextCursor.MoveAnchor, selection_start) + selection_length = abs(selection_start - selection_end) + if selection_start > selection_end: + cursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, selection_length) + else: + cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, selection_length) + self.user_data_view.setTextCursor(cursor) + + def updateData(self): + if self._node: + self.user_data_model.updateDataFromNode(self._node) + + new_index = self.user_data_model.indexByKey(self._current_key) + new_index = self.user_data_filter_model.mapFromSource(new_index) + if new_index.isValid(): + self.user_data_list.setCurrentIndex(new_index) + + def setCurrentNode(self, node): + self._node = node + self.updateData() + self.__toggleUpdateTimer() + self.updateWindowTitle() + + def updateCurrentNode(self): + if self.pin_toggle.isChecked(): + return + + selected_nodes = hou.selectedNodes() + if len(selected_nodes) != 1: + return + + self.setCurrentNode(selected_nodes[0]) + + def __toggleUpdateTimer(self): + if self.auto_update_toggle.isChecked(): + self.update_timer.start() + else: + self.update_timer.stop() + + def __toggleUpdateNodeTimer(self): + if self.pin_toggle.isChecked(): + self.current_node_timer.stop() + else: + self.current_node_timer.start() + + def keyPressEvent(self, event): + if event.matches(QKeySequence.Refresh): + self.updateData() + elif event.matches(QKeySequence.ZoomIn): + self.user_data_view.zoomIn(2) + elif event.matches(QKeySequence.ZoomOut): + self.user_data_view.zoomOut(2) + else: + super(UserDataWindow, self).keyPressEvent(event) + + def eventFilter(self, watched, event): + if watched == self.user_data_view and event.type() == QEvent.Wheel: + if event.modifiers() == Qt.ControlModifier: + if event.delta() > 0: + self.user_data_view.zoomIn(2) + else: + self.user_data_view.zoomOut(2) + return True + elif self._current_key == 'nodeshape' and watched == self.user_data_view.viewport(): + if event.type() == QEvent.Resize: + self.shape_preview.setFixedSize(event.size()) + self.shape_preview.recacheShape(20) + elif event.type() == QEvent.Paint: + pixmap = QPixmap(event.rect().size()) + self.shape_preview.render(pixmap, QPoint()) + painter = QPainter(watched) + painter.drawPixmap(event.rect().topLeft(), pixmap) + return False + + def __del__(self): + self.update_timer.stop() + self.current_node_timer.stop() + + def hideEvent(self, event): + self.update_timer.stop() + self.current_node_timer.stop() + super(UserDataWindow, self).hideEvent(event) + + +def showNodeUserData(node=None, **kwargs): + if node is None: + nodes = hou.selectedNodes() + if not nodes: + notify('No node selected', hou.severityType.Error) + return + elif len(nodes) > 1: + notify('Too much nodes selected', hou.severityType.Error) + return + node = nodes[0] + + window = UserDataWindow(hou.qt.mainWindow()) + window.setCurrentNode(node) + window.show() diff --git a/python2.7libs/houdini_tdk/slider.py b/python2.7libs/houdini_tdk/slider.py index 292be8c..045462e 100644 --- a/python2.7libs/houdini_tdk/slider.py +++ b/python2.7libs/houdini_tdk/slider.py @@ -44,12 +44,10 @@ def revertToDefault(self): self.setValue(self._default_value) def setDefaultValue(self, value): - if reset and self.value() == self._default_value: - self.setValue(value) self._default_value = value def mousePressEvent(self, event): - if event.button() == Qt.MiddleButton: + if event.button() == Qt.MiddleButton: # Todo: Revert to default hou.ui.openValueLadder(self.value(), self.setValue, data_type=hou.valueLadderDataType.Int) self._value_ladder_active = True diff --git a/toolbar/houdini_tdk.shelf b/toolbar/houdini_tdk.shelf index 9191f61..745a00c 100644 --- a/toolbar/houdini_tdk.shelf +++ b/toolbar/houdini_tdk.shelf @@ -11,8 +11,6 @@ - - @@ -121,7 +119,7 @@ tdk.showMakeHDAFromTemplateDialog(**kwargs)