├── .gitignore ├── Houdini.keymap2.overrides ├── LICENSE ├── README.md ├── hou_file_manager.json ├── python_panels └── hou_file_manager.pypanel ├── radialmenu └── kev_quick_menu.radialmenu ├── scripts └── python │ └── hou_file_manager │ ├── __init__.py │ ├── browser.py │ ├── constants.py │ ├── hou_tree_model.py │ ├── matchers.py │ ├── treemodel.py │ └── utils.py └── toolbar └── hou_file_manager.shelf /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | -------------------------------------------------------------------------------- /Houdini.keymap2.overrides: -------------------------------------------------------------------------------- 1 | { 2 | "name":"Houdini", 3 | "version":2, 4 | "houdini.version":"20.5.445", 5 | "symbol":"", 6 | "contexts":[ 7 | { 8 | "symbol":"h.pane.wsheet", 9 | "bindings":[ 10 | { 11 | "action":"h.pane.wsheet.radial:kev_quick_menu", 12 | "keys":["G" 13 | ] 14 | } 15 | ] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Kevin Ma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Houdini File Path Manager 2 | A GUI tool and central place for managing all Houdini file paths (textures, images, caches, geometries) of node parameters. 3 | ![hou_file_manager_gui_01](https://github.com/user-attachments/assets/72080231-e58c-43fc-b32f-6c56a8f03f2f) 4 | 5 | ## What's New 6 | * v0.2.2 7 | * Added icons to the radial menu. 8 | * Added new radial menu itesm and re-organized layout of the menu. 9 | * v0.2.1 10 | * The file paths can be changed by replacing partial strings using Python str.replace() or Python Regex re.sub() functions. 11 | * Users can choose to only update parameters or also copy/move files to the new paths. 12 | * Installation can be automated using Houdini's native Package tools. 13 | * v0.1.10 14 | * UI for filtering nodes and filtering image/geometry parameters on them, so users can copy/move the files to new locations and update these parameters. 15 | * Convenient UI for choosing image/geometry files for filtered parameters on filtered nodes. 16 | * Tutorials 17 | * Youtube: https://youtu.be/LoOPm2v3AoQ 18 | * NOTE: Installation can be automated now! The Youtube video is only showing the manual method. So for the recommended installation method, please refer to the steps in this document below. 19 | 20 | ## Functionalities: 21 | * Refresh button for refreshing the Node View when Houdini scene is changed. 22 | * Node View selection will be cleared once Refresh button is clicked. 23 | * Parameter View will be cleared as well. 24 | * Search in a path for nodes with file parmaters (image or geometry). 25 | * Use the scene browser button to choose a node. Then the search will be conducted recursively under the node. 26 | * Multiple search filters are supported: 27 | * Node Name: Houdini multi name patterns, like *, ^ and combinations. 28 | * For example, 29 | * `*` for any names. 30 | * `pri*` for any names start with `pri`. 31 | * `* ^pri*` for any names NOT start with `pri`. 32 | * `*tmp` for any names end with `tmp`. 33 | * `* ^*tmp` for any names NOT end with `tmp`. 34 | * `pri* ^*tmp` for any names start with `pri` but NOT end with `tmp`. 35 | * `*shader*` for any names with `shader` in it. 36 | * `* ^*shader*` for any names without `shader` in it. 37 | * Node Type: Houdini single name patterns. 38 | * Parameter Name: Houdini multi name patterns, like *, ^ and combinations. 39 | * Refer to above Node Name multi name patterns. 40 | * Parameter File Type: `Image` or `Geometry`. 41 | * Node View 42 | * It only shows the nodes based on the search results. 43 | * The nodes with file parametes that match the filters will be highlighted in red color. 44 | * Users can select nodes in the Node View, and so their parameters that match the filters will be shown in the Parameter View. 45 | * (NOTE: This won't affect Houdini current node selection in Network View, unless double click on a node in Node View.) 46 | * Left Mouse Button (LMB) click to select single node. 47 | * LMB click and drag to select multiple nodes. 48 | * Ctrl + LMB to toggle selection of individual node. 49 | * Shift + LMB click on the start and end node to select a range of nodes. 50 | * Double-clicking on a node will set current selected node to it in the Network Editor. 51 | * (Note: it will affect Houdini current node selection in the Network View.) 52 | * Parameter View 53 | * The file parmaeters will be shown in the Parameter View for the selected nodes in the Node View. 54 | * Not the actual selected nodes in Network Editor. 55 | * Each file parameter item in the Parameter View has: 56 | * A `File Choose` button to choose a file for it (the dialog has image preview on), and 57 | * A `Preview` button to preview the image in MPlay minimal mode. 58 | * The Raw Value of the file parameter in the Parameter View can be edited in place by double-clicking on it. 59 | * Tools UI 60 | * Files in the Parameter View can be batch processed, and the Raw Value file paths of the parmaeters will be updated to the new paths. Currently supported actions are: 61 | * `Copy` : To copy the files specified in the parameters to a destination directory, and then update the parameter file paths to the new paths. But if the files specified in the parameters don't exist or the copying action failed, nothing will be copied and parameters won't be updated either. 62 | * `Move` : To move the files specified in the parameters to a destination directory, and then update the parameter file paths to the new paths. But if the files specified in the parameters don't exist or the moving action failed, nothing will be moved and parameters won't be updated either. 63 | * `Repath` : To change the directory paths of the files specified in the parameters to a new desination directory. It just simply changes the file path values of the parameters, and won't check if the file paths are really pointing to real files or not. 64 | * `` sequence file paths are supported. 65 | * Time dependent sequence paths with `$F` or `${F}` are supported. The `$F` or `${F}` can have zero paddings, such as `$F4`, `$F6`, `${F4}` etc. 66 | 67 | 68 | ## Installation 69 | 1. Go to [Releases](https://github.com/vfxkevin/hou_file_manager/releases) and download the **source code zip file** from the latest release. 70 | * For example, release `v0.1.x` is downloaded. 71 | 2. Two methods to install the package: 72 | * **Method A (RECOMMENDED)** - Automated installation using Package Archive 73 | * In Houdini, launch Package Browser 74 | ![launch_package_browser](https://github.com/user-attachments/assets/8c1b9d61-441b-464e-8198-2356fbefb2db) 75 | * In File menu, run Install Package Archive 76 | ![package_browser_install_archive](https://github.com/user-attachments/assets/87b9abf6-8535-4956-935d-db23772a491c) 77 | * Select the zip file in the previous step, and install to a sub-folder inside Houdini Packages folder. 78 | ![install_package_archive](https://github.com/user-attachments/assets/a35f4b93-9720-4d9e-9e96-2867643d1da3) 79 | * Ensure the package is installed and loaded 80 | ![package_loaded](https://github.com/user-attachments/assets/83e0e8a1-002a-4d5f-974b-cb9488e7ac6b) 81 | * **Method B** - Manual installtion (if customised installation needed, otherwise skip to step 3) 82 | * Unzip it to a location. 83 | * In our example, say, it is `C:/Users/username/Documents/hou_file_manager-0.1.1` 84 | * NOTE: 85 | * This is a Windows path example. Please replace `username` with your actual user name. 86 | * The actual package directory path is `C:/Users/username/Documents/hou_file_manager-0.1.1/hou_file_manager-0.1.1` (the double hou_file_manager-0.1.1) because the way it was zipped. 87 | * Move the `hou_file_manager.json` file into the `packages` directory inside the `Houdini user preference directory` ($HOUDINI_USER_PREF_DIR). 88 | * On Windows, the `Houdini user preference directory` is `C:/Users/username/Documents/houdini20.5` (for Houdini 20.5) 89 | * NOTE: Replace the `username` with the actual name on your computer for the actual path. 90 | * Create a `packages` directory inside the `Houdini user preference directory` if there is none. 91 | * Modify the `hou_file_manager.json` file, so that the `HOU_FILE_MANAGER` env variable points to the above unzipped directory. 92 | * In our example, it would be like: 93 | ``` 94 | "env": [ 95 | { 96 | "HOU_FILE_MANAGER": "C:/Users/username/Documents/hou_file_manager-0.1.1/hou_file_manager-0.1.1" 97 | } 98 | ] 99 | ``` 100 | * Please use back slashes in the path string. 101 | * Please replace the username with your actual user name as well. 102 | 3. Must re-start Houdini to load the tools. 103 | 104 | 105 | ## Launch Hou File Manager Tool 106 | There are 3 methods to launch the GUI: 107 | * Load the `File Manager` shelf and use the first `File Manager` button to launch it. 108 | * ![file_manager_shelf](https://github.com/user-attachments/assets/3d872471-af7b-479d-8a10-9386d30c448f) 109 | * Add a `New Pane Tab` of `Hou File Manager` to the main desktop. 110 | * ![hou_file_manager_pane_tab](https://github.com/user-attachments/assets/67130c8c-2be0-4c0d-91f1-efdc1c55eea4) 111 | * In Network Editor, press hotkey `m` to load out quick radial menu, and choose `File Manager`. 112 | * ![network_editor_radial_menu](https://github.com/user-attachments/assets/9c3f813d-1e7f-4c24-a063-31749b3733d0) 113 | 114 | ## Other Tools 115 | * These tools (on the shelf and raidial menu) are just shortcut scripts to create frequently used nodes: 116 | * Out Null 117 | * Create `Null` nodes (named OUT) and connect them to the outputs of the corresponding selected nodes. 118 | * Obj Merge 119 | * Create `Object Merge` nodes, put them on the side of the corresponding selected nodes, and set the `Object` parameter paths to the corresponding selected nodes. 120 | * Merge 121 | * Create a `Merge` node and connect all the selected nodes to the input of the Merge node. 122 | * Switch 123 | * Create a `Switch` node and connect all the selected nodes to the input of the Switch node. 124 | * Geometry 125 | * Create a `Geometry` node. But it will only work at `/obj` level. 126 | * File Cache 127 | * Create `File Cache` nodes and connect them to the outputs of the corresponding selected nodes. 128 | * Transform 129 | * Create `Transnform` nodes and connect them to the outputs of the correspoding selected nodes. 130 | 131 | ## TODOs 132 | * Logging UI. 133 | * Preview geometry file(s). 134 | -------------------------------------------------------------------------------- /hou_file_manager.json: -------------------------------------------------------------------------------- 1 | { 2 | "hpath": "$HOU_FILE_MANAGER", 3 | "load_package_once": true, 4 | "enable": "houdini_version >= '19.0' and houdini_version < '21.0'", 5 | "version": "20.5", 6 | "env": [ 7 | { 8 | "HOU_FILE_MANAGER": "$HOUDINI_PACKAGE_PATH/hou_file_manager" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /python_panels/hou_file_manager.pypanel: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /radialmenu/kev_quick_menu.radialmenu: -------------------------------------------------------------------------------- 1 | { 2 | "fileversion":"20.5.487", 3 | "name":"kev_quick_menu", 4 | "categories":"Standard", 5 | "pane":"network", 6 | "label":"Kev Quick Menu", 7 | "type":"script_submenu", 8 | "script":"###### Kevin Quick Radial Menu ######\nRED = hou.Color((1, 0, 0))\nGREEN = hou.Color((0, 1, 0))\nBLUE = hou.Color((0, 0, 1))\n\nNODE_TYPES_WITHOUT_INPUTS = ['line','circle', 'sphere', 'labs::sphere_generator', 'labs::cylinder_generator', 'box', 'tube', 'torus']\nNODE_TYPES_CONNECT_ONE_TO_ONE = ['curve', 'add', 'xform', 'filecache', 'name', 'matchsize', 'resample', 'carve', 'measure',\n 'scatter', 'remesh', 'orientalongcurve', 'polybevel', 'polyfill', 'polyextrude', 'polycut', 'revolve', 'reverse', \n 'divide', 'mirror', 'subdivide', 'sweep', 'attribwrangle', 'attribcreate', 'attribnoise', 'attribpromote', 'attribdelete',\n 'attribvop', 'attribtransfer', 'attribpaint', 'attribadjustfloat', 'attribadjustcolor', 'attribadjustvector', \n 'attribcombine', 'groupcreate', 'grouprange', 'groupcombine', 'copyxform', 'copytopoints', 'copytocurves', \n 'uvunwrap', 'uvlayout', 'delete', 'labs::physical_ambient_occlusion', 'labs::measure_curvature', 'sceneimport', 'materiallibrary',\n 'assignmaterial', 'light', 'karmaphysicalsky', 'domelight', 'labs::karma', 'split', 'convertline', 'grouppromote', 'facet',\n 'primitive', 'boolean', 'fuse']\nNODE_TYPES_CONNECT_MANY_TO_ONE = ['merge', 'switch', 'switchif']\n\n## Menu entries\nentries = {}\n\n## Function dict\nfunc_dict = {}\n\n## Gather info\npane = kwargs['pane']\nparent = pane.pwd()\ncur_position = pane.cursorPosition()\nselected = hou.selectedNodes()\n\n###### Generic util functions ######\ndef make_func(node_type, no_input=True, many_to_one=False):\n def wrapper(**kwargs):\n if no_input:\n create_node(parent, node_type, position=cur_position)\n elif many_to_one:\n create_n_connect_to_output_for_many_to_1(parent, node_type)\n else:\n create_n_connect_to_output_for_1_to_1(parent, node_type)\n return wrapper\n \ndef create_node(parent, node_type, node_name=None, color=None, position=None, set_current=True, clear_all_selected=True):\n try:\n node = parent.createNode(node_type)\n except Exception:\n return None\n if node_name:\n node.setName(node_name, unique_name=True)\n if color:\n node.setColor(color)\n if position:\n node.setPosition(position)\n node.setCurrent(True, clear_all_selected)\n return node\n \ndef connect_to_output(n, new_node, set_display=False, set_render=False):\n # Connect only first output connections to the null node's output first.\n out_con = n.outputConnections()\n for con in out_con:\n con_node = con.outputNode()\n con_index = con.inputIndex()\n if con.outputIndex() == 0:\n con_node.setInput(con_index, new_node)\n # Then connect the input of null to output of selected node.\n new_node.setInput(0, n)\n new_node.moveToGoodPosition(move_inputs=False)\n if set_display:\n new_node.setDisplayFlag(True)\n if set_render:\n try:\n new_node.setRenderFlag(True)\n except Exception:\n pass\n \ndef create_n_connect_to_output_for_1_to_1(parent, type):\n if not selected:\n create_node(parent, type, position=cur_position)\n return\n \n for n in selected:\n attr_create = create_node(parent, type)\n if not attr_create:\n continue\n connect_to_output(n, attr_create)\n \ndef create_n_connect_to_output_for_many_to_1(parent, type):\n node = create_node(parent, type, position=cur_position)\n if not node:\n return None\n for n in selected:\n node.setNextInput(n)\n return node\n\ndef create_null(connection_type, name, color):\n if not selected:\n create_node(parent, 'null', name, color, cur_position)\n return\n \n for n in selected:\n null = create_node(parent, 'null', name, color)\n if not null:\n continue\n if connection_type == 'out':\n connect_to_output(n, null, True, True)\n elif connection_type == 'in':\n in_con = n.inputConnections()\n for con in in_con:\n con_node = con.inputNode()\n con_index = con.outputIndex()\n if con.inputIndex() == 0:\n null.setInput(0, con_node, con_index)\n n.setInput(0, null)\n null.moveToGoodPosition(relative_to_inputs=False, move_outputs=False)\n\n###### Build function dict ######\nfor node_type in NODE_TYPES_WITHOUT_INPUTS:\n func_dict[node_type] = make_func(node_type, True)\n \n\nfor node_type in NODE_TYPES_CONNECT_ONE_TO_ONE:\n func_dict[node_type] = make_func(node_type, False, False)\n \n\nfor node_type in NODE_TYPES_CONNECT_MANY_TO_ONE:\n func_dict[node_type] = make_func(node_type, False, True)\n \nfunc_dict['out_null'] = lambda **kwargs: create_null('out', 'OUT', RED)\n \nfunc_dict['in_null'] = lambda **kwargs: create_null('in', 'IN', GREEN)\n \ndef obj_merge(**kwargs):\n if not selected:\n create_node(parent, 'object_merge', position=cur_position)\n return\n \n for n in selected:\n obj_merge = create_node(parent, 'object_merge', color=GREEN)\n if not obj_merge:\n continue\n n_pos = n.position()\n n_path = n.path()\n offset = hou.Vector2((1.5, 0))\n obj_merge.setPosition(n_pos + offset)\n parm = obj_merge.parm('objpath1')\n if parm:\n parm.set(n_path)\nfunc_dict['obj_merge'] = obj_merge\n \ndef file_manager(**kwargs):\n desktop = hou.ui.curDesktop()\n desktop.createFloatingPaneTab(\n hou.paneTabType.PythonPanel, \n python_panel_interface='hou_file_manager'\n )\nfunc_dict['file_manager'] = file_manager\n \ndef create_geo(**kwargs):\n if parent.type().name() == 'obj':\n geo = create_node(parent, 'geo', position=cur_position)\nfunc_dict['create_geo'] = create_geo\n \ndef create_lop(**kwargs):\n if parent.type().name() == 'obj':\n lop = create_node(parent, 'lopnet', position=cur_position)\nfunc_dict['create_lop'] = create_lop\n \ndef create_cam(**kwargs):\n if parent.type().name() == 'obj':\n node_type = 'cam'\n elif parent.type().name() == 'lopnet':\n node_type = 'camera'\n cam = create_node(parent, node_type, position=cur_position)\nfunc_dict['create_cam'] = create_cam\n \ndef grid(**kwargs):\n n = create_node(parent, 'grid', position=cur_position)\n n.parm('sizex').set(1)\n n.parm('sizey').set(1)\n n.parm('rows').set(2)\n n.parm('cols').set(2)\nfunc_dict['grid'] = grid\n\n###### Build the radial menu ######\nif pane.type() == hou.paneTabType.NetworkEditor:\n \n def make_submenu_creation(**kwargs):\n menu = {\n 'n': {\n 'type' : 'script_action',\n 'label' : 'Create Geometry',\n 'icon' : 'OBJ_geo',\n 'script' : func_dict['create_geo'],\n },\n 's': {\n 'type' : 'script_action',\n 'label' : 'Line',\n 'icon' : 'SOP_line',\n 'script' : func_dict['line'],\n },\n 'ne': {\n 'type' : 'script_action',\n 'label' : 'File Cache',\n 'icon' : 'SOP_filecache',\n 'script' : func_dict['filecache'],\n },\n 'sw': {\n 'type' : 'script_action',\n 'label' : 'Curve',\n 'icon' : 'SOP_curve',\n 'script' : func_dict['curve'],\n },\n 'e': {\n 'type' : 'script_action',\n 'label' : 'Camera',\n 'icon' : 'LOP_camera',\n 'script' : func_dict['create_cam'],\n },\n 'w': {\n 'type' : 'script_action',\n 'label' : 'Add',\n 'icon' : 'SOP_add',\n 'script' : func_dict['add'],\n },\n 'nw': {\n 'type' : 'script_submenu',\n 'label' : 'More',\n 'script' : make_submenu_creation_more,\n },\n }\n radialmenu.setRadialMenu(menu)\n \n def make_submenu_creation_more(**kwargs):\n menu = {\n 'nw': {\n 'type' : 'script_action',\n 'label' : 'Sphere',\n 'icon' : 'SOP_sphere',\n 'script' : func_dict['sphere'],\n },\n 'n': {\n 'type' : 'script_action',\n 'label' : 'Labs Sphere',\n 'icon' : 'SOP_sphere',\n 'script' : func_dict['labs::sphere_generator'],\n },\n 'w': {\n 'type' : 'script_action',\n 'label' : 'Box',\n 'icon' : 'SOP_box',\n 'script' : func_dict['box'],\n },\n 'sw': {\n 'type' : 'script_action',\n 'label' : 'Tube',\n 'icon' : 'SOP_tube',\n 'script' : func_dict['labs::cylinder_generator'],\n },\n 's': {\n 'type' : 'script_action',\n 'label' : 'Torus',\n 'icon' : 'SOP_torus',\n 'script' : func_dict['torus'],\n },\n 'e': {\n 'type' : 'script_action',\n 'label' : 'Grid',\n 'icon' : 'SOP_grid',\n 'script' : func_dict['grid'],\n },\n 'ne': {\n 'type' : 'script_action',\n 'label' : 'Circle',\n 'icon' : 'SOP_circle',\n 'script' : func_dict['circle'],\n },\n }\n radialmenu.setRadialMenu(menu)\n \n def make_submenu_modify(**kwargs):\n menu = {\n 'n': {\n 'type' : 'script_action',\n 'label' : 'Transform',\n 'icon' : 'SOP_xform',\n 'script' : func_dict['xform'],\n },\n 'nw': {\n 'type' : 'script_action',\n 'label' : 'Mirror',\n 'icon' : 'SOP_mirror',\n 'script' : func_dict['mirror'],\n },\n 'w': {\n 'type' : 'script_submenu',\n 'label' : 'More',\n 'script' : make_submenu_modify_more,\n },\n 'sw': {\n 'type' : 'script_action',\n 'label' : 'Fuse',\n 'icon' : 'SOP_fuse',\n 'script' : func_dict['fuse'],\n },\n 'ne': {\n 'type' : 'script_action',\n 'label' : 'Match Size',\n 'icon' : 'SOP_matchsize',\n 'script' : func_dict['matchsize'],\n },\n 's': {\n 'type' : 'script_action',\n 'label' : 'Delete',\n 'icon' : 'SOP_delete',\n 'script' : func_dict['delete'],\n },\n 'se': {\n 'type' : 'script_action',\n 'label' : 'Boolean',\n 'icon' : 'SOP_boolean',\n 'script' : func_dict['boolean'],\n },\n }\n radialmenu.setRadialMenu(menu)\n \n def make_submenu_modify_more(**kwargs):\n menu = {\n 'w': {\n 'type' : 'script_action',\n 'label' : 'Resample',\n 'icon' : 'SOP_resample',\n 'script' : func_dict['resample'],\n },\n 'nw': {\n 'type' : 'script_action',\n 'label' : 'Primitive Properties',\n 'icon' : 'SOP_primitive',\n 'script' : func_dict['primitive'],\n },\n 'n': {\n 'type' : 'script_action',\n 'label' : 'Reverse',\n 'icon' : 'SOP_reverse',\n 'script' : func_dict['reverse'],\n },\n 'sw': {\n 'type' : 'script_action',\n 'label' : 'Carve',\n 'icon' : 'SOP_carve',\n 'script' : func_dict['carve'],\n },\n 's': {\n 'type' : 'script_action',\n 'label' : 'Orient Along Curve',\n 'icon' : 'SOP_orientalongcurve',\n 'script' : func_dict['orientalongcurve'],\n },\n 'ne': {\n 'type' : 'script_action',\n 'label' : 'Scatter',\n 'icon' : 'SOP_scatter',\n 'script' : func_dict['scatter'],\n },\n 'se': {\n 'type' : 'script_action',\n 'label' : 'Measure',\n 'icon' : 'SOP_measure',\n 'script' : func_dict['measure'],\n },\n }\n radialmenu.setRadialMenu(menu)\n \n def make_submenu_attrs(**kwargs):\n menu = {\n 'n': {\n 'type' : 'script_action',\n 'label' : 'Attr Wrangle',\n 'icon' : 'SOP_attribwrangle',\n 'script' : func_dict['attribwrangle'],\n },\n 'nw': {\n 'type' : 'script_action',\n 'label' : 'Name',\n 'icon' : 'SOP_name',\n 'script' : func_dict['name'],\n },\n 'w': {\n 'type' : 'script_action',\n 'label' : 'Attr Create',\n 'icon' : 'SOP_attribcreate',\n 'script' : func_dict['attribcreate'],\n },\n 'sw': {\n 'type' : 'script_submenu',\n 'label' : 'More',\n 'script' : make_submenu_attr_more,\n },\n 's': {\n 'type' : 'script_action',\n 'label' : 'Attr Delete',\n 'icon' : 'SOP_attribdelete',\n 'script' : func_dict['attribdelete'],\n },\n 'se': {\n 'type' : 'script_action',\n 'label' : 'Attr Promote',\n 'icon' : 'SOP_attribpromote',\n 'script' : func_dict['attribpromote'],\n },\n 'e': {\n 'type' : 'script_action',\n 'label' : 'Attr VOP',\n 'icon' : 'SOP_attribvop',\n 'script' : func_dict['attribvop'],\n },\n }\n radialmenu.setRadialMenu(menu)\n \n def make_submenu_attr_more(**kwargs):\n menu = {\n 'n': {\n 'type' : 'script_action',\n 'label' : 'Attr Transfer',\n 'icon' : 'SOP_attribtransfer',\n 'script' : func_dict['attribtransfer'],\n },\n 'nw': {\n 'type' : 'script_action',\n 'label' : 'Attr Paint',\n 'icon' : 'SOP_attribpaint',\n 'script' : func_dict['attribpaint'],\n },\n 'w': {\n 'type' : 'script_action',\n 'label' : 'Attr Adjust Float',\n 'icon' : 'SOP_attribadjustfloat',\n 'script' : func_dict['attribadjustfloat'],\n },\n 'sw': {\n 'type' : 'script_action',\n 'label' : 'Attr Adjust Color',\n 'icon' : 'SOP_attribadjustcolor',\n 'script' : func_dict['attribadjustcolor'],\n },\n 's': {\n 'type' : 'script_action',\n 'label' : 'Attr Adjust Vector',\n 'icon' : 'SOP_attribadjustvector',\n 'script' : func_dict['attribadjustvector'],\n },\n 'e': {\n 'type' : 'script_action',\n 'label' : 'Attr Combine',\n 'icon' : 'SOP_attribcombine',\n 'script' : func_dict['attribcombine'],\n },\n 'se': {\n 'type' : 'script_action',\n 'label' : 'Attr Noise',\n 'icon' : 'SOP_attribnoise',\n 'script' : func_dict['attribnoise'],\n },\n }\n radialmenu.setRadialMenu(menu)\n \n def make_submenu_group(**kwargs):\n menu = {\n 's': {\n 'type' : 'script_action',\n 'label' : 'Group Create',\n 'icon' : 'SOP_groupcreate',\n 'script' : func_dict['groupcreate'],\n },\n 'se': {\n 'type' : 'script_action',\n 'label' : 'Group Promote',\n 'icon' : 'SOP_grouppromote',\n 'script' : func_dict['grouppromote'],\n },\n 'e': {\n 'type' : 'script_action',\n 'label' : 'Group By Range',\n 'icon' : 'SOP_grouprange',\n 'script' : func_dict['grouprange'],\n },\n 'w': {\n 'type' : 'script_action',\n 'label' : 'Group Combine',\n 'icon' : 'SOP_groupcombine',\n 'script' : func_dict['groupcombine'],\n },\n }\n radialmenu.setRadialMenu(menu)\n \n def make_submenu_copy(**kwargs):\n menu = {\n 'n': {\n 'type' : 'script_action',\n 'label' : 'Copy to Curves',\n 'icon' : 'SOP_copytocurves',\n 'script' : func_dict['copytocurves'],\n },\n 'e': {\n 'type' : 'script_action',\n 'label' : 'Copy n Transform',\n 'icon' : 'SOP_copyxform',\n 'script' : func_dict['copyxform'],\n },\n 's': {\n 'type' : 'script_action',\n 'label' : 'Copy to Points',\n 'icon' : 'SOP_copytopoints',\n 'script' : func_dict['copytopoints'],\n },\n }\n radialmenu.setRadialMenu(menu)\n \n def make_submenu_org(**kwargs):\n menu = {\n 'n': {\n 'type' : 'script_action',\n 'label' : 'Merge',\n 'icon' : 'SOP_merge',\n 'script' : func_dict['merge'],\n },\n 'nw': {\n 'type' : 'script_action',\n 'label' : 'Split',\n 'icon' : 'SOP_split',\n 'script' : func_dict['split'],\n },\n 'ne': {\n 'type' : 'script_action',\n 'label' : 'IN NULL',\n 'icon' : 'SOP_null',\n 'script' : func_dict['in_null'],\n },\n 'e': {\n 'type' : 'script_action',\n 'label' : 'OUT NULL',\n 'icon' : 'SOP_null',\n 'script' : func_dict['out_null'],\n },\n 'se': {\n 'type' : 'script_action',\n 'label' : 'Object Merge',\n 'icon' : 'SOP_object_merge',\n 'script' : func_dict['obj_merge'],\n },\n 's': {\n 'type' : 'script_action',\n 'label' : 'Switch',\n 'icon' : 'SOP_switch',\n 'script' : func_dict['switch'],\n },\n 'sw': {\n 'type' : 'script_action',\n 'label' : 'Switch If',\n 'icon' : 'SOP_switchif',\n 'script' : func_dict['switchif'],\n },\n }\n radialmenu.setRadialMenu(menu)\n \n def make_submenu_lookdev(**kwargs):\n menu = {\n 'n': {\n 'type' :'script_action',\n 'label' : 'File Manager',\n 'icon' : 'SOP_subnet',\n 'script' : file_manager,\n },\n 'ne': {\n 'type' : 'script_submenu',\n 'label' : 'More',\n 'script' : make_submenu_lookdev_more,\n },\n 'e': {\n 'type' :'script_action',\n 'label' : 'LOP Network',\n 'icon' : 'NETWORKS_lop',\n 'script' : func_dict['create_lop'],\n },\n 'se': {\n 'type' : 'script_action',\n 'label' : 'LOP: Karma',\n 'icon' : 'LOP_karma',\n 'script' : func_dict['labs::karma'],\n },\n 'w': {\n 'type' : 'script_action',\n 'label' : 'LOP: Material Library',\n 'icon' : 'LOP_materiallibrary',\n 'script' : func_dict['materiallibrary'],\n },\n 'nw': {\n 'type' : 'script_action',\n 'label' : 'LOP: Scene Import',\n 'icon' : 'LOP_sceneimport',\n 'script' : func_dict['sceneimport'],\n },\n 's': {\n 'type' : 'script_action',\n 'label' : 'LOP: Assign Material',\n 'icon' : 'LOP_assignmaterial',\n 'script' : func_dict['assignmaterial'],\n },\n }\n radialmenu.setRadialMenu(menu)\n \n def make_submenu_lookdev_more(**kwargs):\n menu = {\n 'n': {\n 'type' : 'script_action',\n 'label' : 'LOP: Physical Sky',\n 'icon' : 'LOP_karmaphysicalsky',\n 'script' : func_dict['karmaphysicalsky'],\n },\n 'ne': {\n 'type' : 'script_action',\n 'label' : 'LOP: Light',\n 'icon' : 'LOP_light',\n 'script' : func_dict['light'],\n },\n 'e': {\n 'type' : 'script_action',\n 'label' : 'LOP: Dome Light',\n 'icon' : 'LOP_domelight',\n 'script' : func_dict['domelight'],\n },\n 'w': {\n 'type' : 'script_action',\n 'label' : 'Labs Physical AO',\n 'script' : func_dict['labs::physical_ambient_occlusion'],\n },\n 'nw': {\n 'type' : 'script_action',\n 'label' : 'Labs Measure Curvature',\n 'icon' : 'VOP_curvature',\n 'script' : func_dict['labs::measure_curvature'],\n },\n }\n radialmenu.setRadialMenu(menu)\n \n def make_submenu_poly_tools(**kwargs):\n menu = {\n 'n': {\n 'type' : 'script_submenu',\n 'label' : 'More',\n 'script' : make_submenu_poly_tools_more,\n },\n 'nw': {\n 'type' : 'script_action',\n 'label' : 'Poly Extrude',\n 'icon' : 'SOP_polyextrude-2.0',\n 'script' : func_dict['polyextrude'],\n },\n 'ne': {\n 'type' : 'script_action',\n 'label' : 'Revolve',\n 'icon' : 'SOP_revolve',\n 'script' : func_dict['revolve'],\n },\n 'e': {\n 'type' : 'script_action',\n 'label' : 'Subdivide',\n 'icon' : 'SOP_subdivide',\n 'script' : func_dict['subdivide'],\n },\n 'w': {\n 'type' : 'script_action',\n 'label' : 'Poly Bevel',\n 'icon' : 'SOP_polybevel-3.0',\n 'script' : func_dict['polybevel'],\n },\n 'sw': {\n 'type' : 'script_action',\n 'label' : 'Poly Fill',\n 'icon' : 'SOP_polyfill',\n 'script' : func_dict['polyfill'],\n },\n \n 'se': {\n 'type' : 'script_action',\n 'label' : 'Sweep',\n 'icon' : 'SOP_sweep',\n 'script' : func_dict['sweep'],\n },\n }\n radialmenu.setRadialMenu(menu)\n \n def make_submenu_poly_tools_more(**kwargs):\n menu = {\n 'n': {\n 'type' : 'script_action',\n 'label' : 'Convert Line',\n 'icon' : 'SOP_convertline',\n 'script' : func_dict['convertline'],\n },\n 'ne': {\n 'type' : 'script_action',\n 'label' : 'Poly Cut',\n 'icon' : 'SOP_polycut',\n 'script' : func_dict['polycut'],\n },\n 'nw': {\n 'type' : 'script_action',\n 'label' : 'Divide',\n 'icon' : 'SOP_divide',\n 'script' : func_dict['divide'],\n },\n 'w': {\n 'type' : 'script_action',\n 'label' : 'Remesh',\n 'icon' : 'SOP_remesh',\n 'script' : func_dict['remesh'],\n },\n 'sw': {\n 'type' : 'script_action',\n 'label' : 'Facet',\n 'icon' : 'SOP_facet',\n 'script' : func_dict['facet'],\n },\n 'se': {\n 'type' : 'script_action',\n 'label' : 'UV Layout',\n 'icon' : 'SOP_uvlayout',\n 'script' : func_dict['uvlayout'],\n },\n 'e': {\n 'type' : 'script_action',\n 'label' : 'UV Unwrap',\n 'icon' : 'SOP_uvunwrap',\n 'script' : func_dict['uvunwrap'],\n },\n }\n radialmenu.setRadialMenu(menu)\n \n entries = {\n 'n': {\n 'type' : 'script_submenu',\n 'label' : 'Poly Tools',\n 'script' : make_submenu_poly_tools,\n },\n 'ne': {\n 'type' : 'script_submenu',\n 'label' : 'Lookdev',\n 'script' : make_submenu_lookdev,\n },\n 'e': {\n 'type' : 'script_submenu',\n 'label' : 'Org',\n 'script' : make_submenu_org,\n },\n 'se': {\n 'type' : 'script_submenu',\n 'label' : 'Copy',\n 'script' : make_submenu_copy,\n },\n 's': {\n 'type' : 'script_submenu',\n 'label' : 'Group',\n 'script' : make_submenu_group,\n },\n 'sw': {\n 'type' :'script_submenu',\n 'label' : 'Attr',\n 'script' : make_submenu_attrs,\n },\n 'w': {\n 'type' :'script_submenu',\n 'label' : 'Modify',\n 'script' : make_submenu_modify,\n },\n 'nw': {\n 'type' : 'script_submenu',\n 'label' : 'Create',\n 'script' : make_submenu_creation,\n } \n }\n\n###### finally set the radial menu ######\nradialmenu.setRadialMenu(entries)" 9 | } 10 | -------------------------------------------------------------------------------- /scripts/python/hou_file_manager/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vfxkevin/hou_file_manager/9fd3ab5ffe2ebda1fce62e965116763e02f48b18/scripts/python/hou_file_manager/__init__.py -------------------------------------------------------------------------------- /scripts/python/hou_file_manager/browser.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright: (C) 2024 Kevin Ma Yi 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the “Software”), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import os 24 | import re 25 | import subprocess 26 | from functools import partial 27 | 28 | from PySide2.QtWidgets import (QWidget, QFrame, QGroupBox, QTableWidget, 29 | QTableWidgetItem) 30 | from PySide2.QtWidgets import (QAbstractItemView, QListView, QTreeView, 31 | QHeaderView) 32 | from PySide2.QtWidgets import (QPushButton, QLineEdit, QLabel, 33 | QRadioButton, QCheckBox) 34 | from PySide2.QtWidgets import QVBoxLayout, QHBoxLayout, QScrollArea 35 | from PySide2.QtWidgets import QTabWidget, QSplitter, QButtonGroup 36 | from PySide2.QtWidgets import QDialog 37 | from PySide2.QtCore import QModelIndex 38 | from PySide2.QtCore import Qt 39 | from PySide2.QtGui import QIntValidator 40 | 41 | import hou 42 | import nodesearch 43 | 44 | from . import constants as const 45 | from . import matchers 46 | from . import utils 47 | from .hou_tree_model import HouParmTreeModel, HouNodeTreeModel 48 | 49 | 50 | class NodeParmFilterList(QWidget): 51 | def __init__(self): 52 | super().__init__() 53 | 54 | self._layout = QVBoxLayout() 55 | self.setLayout(self._layout) 56 | 57 | self._filter_rows = [] 58 | 59 | def add_filter_row(self, filter_type): 60 | pass 61 | 62 | def remove_filter_row(self, filter_row_obj): 63 | pass 64 | 65 | def matches_all(self): 66 | return None 67 | 68 | def matchers(self): 69 | matchers = [] 70 | for filter in self._filter_rows: 71 | matcher = filter.matcher() 72 | if not matcher: 73 | raise Exception('Filter ({}) does not have a matcher.' 74 | .format(filter)) 75 | matchers.append(matcher) 76 | 77 | return matchers 78 | 79 | 80 | class FilePathManagerBrowser(QFrame): 81 | def __init__(self, parent=None): 82 | super().__init__(parent) 83 | 84 | # Models for the two views 85 | self._node_tree_model = None 86 | self._parm_tree_model = None 87 | 88 | # --------------- top section --------------- 89 | top_section_layout = self.build_top_section() 90 | 91 | # --------------- centre section --------------- 92 | centre_section_layout = self.build_center_section() 93 | 94 | # --------------- root layout --------------- 95 | root_layout = QVBoxLayout() 96 | root_layout.addLayout(top_section_layout, stretch=0) 97 | root_layout.addLayout(centre_section_layout, stretch=1) 98 | 99 | # set the root layout 100 | self.setLayout(root_layout) 101 | 102 | def set_up_node_tree_model(self, path_list): 103 | 104 | self._node_tree_model = HouNodeTreeModel(path_list) 105 | self.ui_node_tree_view.setModel(self._node_tree_model) 106 | self.ui_node_tree_view.selectionModel().selectionChanged.connect( 107 | self.on_node_tree_view_selection_changed) 108 | 109 | # Configure tree view 110 | self.ui_node_tree_view.expandAll() 111 | self.ui_node_tree_view.resizeColumnToContents(0) 112 | 113 | def set_up_parm_tree_model(self, parm_list): 114 | 115 | # Update the parm tree view 116 | self._parm_tree_model = HouParmTreeModel(parm_list) 117 | self.ui_parm_tree_view.setModel(self._parm_tree_model) 118 | self._parm_tree_model.dataChanged.connect( 119 | self.on_parm_tree_data_changed) 120 | 121 | def node_tree_view_config_post_model_setup(self): 122 | 123 | # Configure tree view 124 | self.ui_parm_tree_view.resizeColumnToContents(0) 125 | self.ui_parm_tree_view.setColumnWidth(1, 50) 126 | header = self.ui_parm_tree_view.header() 127 | header.setSectionResizeMode(0, QHeaderView.Interactive) 128 | header.setSectionResizeMode(1, QHeaderView.Fixed) 129 | header.setSectionsMovable(False) 130 | 131 | # Add file chooser button to all column 1 items. 132 | parm_root_index = self.ui_parm_tree_view.rootIndex() 133 | parm_row_count = self._parm_tree_model.rowCount(parm_root_index) 134 | for row in range(parm_row_count): 135 | # multiple buttons 136 | buttons_widget = QWidget() 137 | buttons_layout = QHBoxLayout() 138 | buttons_layout.setContentsMargins(0, 0, 0, 0) 139 | file_chooser_button = hou.qt.FileChooserButton() 140 | # get orig data 141 | index = self._parm_tree_model.index(row, 0) 142 | parm = (self._parm_tree_model.get_item(index) 143 | .get_raw_data().get_orig_data()) 144 | if matchers.parm_is_file_type(parm, 'image'): 145 | file_chooser_button.setFileChooserFilter(hou.fileType.Image) 146 | file_chooser_button.setFileChooserIsImageChooser(True) 147 | elif matchers.parm_is_file_type(parm, 'geometry'): 148 | file_chooser_button.setFileChooserFilter(hou.fileType.Geometry) 149 | preview_button = QPushButton('P') 150 | buttons_layout.addWidget(file_chooser_button) 151 | buttons_layout.addWidget(preview_button) 152 | buttons_widget.setLayout(buttons_layout) 153 | 154 | # add the widget to the item 155 | self.ui_parm_tree_view.setIndexWidget( 156 | self._parm_tree_model.index(row,1), buttons_widget) 157 | 158 | # add callbacks 159 | file_cb = partial(self.update_parm_model, row) 160 | file_chooser_button.fileSelected.connect(file_cb) 161 | 162 | preview_cb = partial(self.on_preview_file, row) 163 | preview_button.clicked.connect(preview_cb) 164 | 165 | def build_top_section(self): 166 | 167 | # create widgets 168 | self.ui_refresh_button = QPushButton('Refresh') 169 | self.ui_refresh_button.clicked.connect(self.on_refresh) 170 | 171 | # choose root node section 172 | root_path_layout = QHBoxLayout() 173 | root_path_label = QLabel('Search In Path:') 174 | self.ui_root_path_text = hou.qt.SearchLineEdit() 175 | self.ui_root_path_text.editingFinished.connect(self.on_refresh) 176 | choose_root_button = hou.qt.NodeChooserButton() 177 | choose_root_button.nodeSelected.connect(self.on_root_node_selected) 178 | root_path_layout.addWidget(root_path_label) 179 | root_path_layout.addWidget(self.ui_root_path_text) 180 | root_path_layout.addWidget(choose_root_button) 181 | 182 | # filter layout 183 | filter_grp_box = QGroupBox('Filters') 184 | filter_layout = QVBoxLayout() 185 | 186 | # node filter layout 187 | node_filter_layout = QHBoxLayout() 188 | node_filter_layout.setSpacing(20) 189 | 190 | # Node name filter 191 | node_name_filter_layout = QHBoxLayout() 192 | node_name_filter_layout.setSpacing(5) 193 | node_name_label = QLabel('Node Name:') 194 | self.ui_node_name_filter_text = QLineEdit('*') 195 | self.ui_node_name_filter_text.editingFinished.connect(self.on_refresh) 196 | node_name_filter_layout.addWidget(node_name_label) 197 | node_name_filter_layout.addWidget(self.ui_node_name_filter_text) 198 | 199 | # Node type filter 200 | node_type_filter_layout = QHBoxLayout() 201 | node_type_filter_layout.setSpacing(5) 202 | node_type_label = QLabel('Node Type:') 203 | self.ui_node_type_category_combo = hou.qt.ComboBox() 204 | 205 | cate = list(hou.nodeTypeCategories().items()) 206 | for c in (hou.managerNodeTypeCategory(), hou.rootNodeTypeCategory()): 207 | cate.remove((c.name(), c)) 208 | cate.insert(0, ("*", None)) 209 | cate.sort() 210 | for c in cate: 211 | self.ui_node_type_category_combo.addItem(c[0], c[1]) 212 | self.ui_node_type_category_combo.setMinimumWidth(80) 213 | self.ui_node_type_category_combo.currentTextChanged.connect( 214 | self.on_node_type_category_changed) 215 | 216 | self.ui_node_type_combo = hou.qt.ComboBox() 217 | self.ui_node_type_combo.setEditable(True) 218 | self.ui_node_type_combo.addItem('*') 219 | self.ui_node_type_combo.setMinimumWidth(200) 220 | self.ui_node_type_combo.currentTextChanged.connect(self.on_refresh) 221 | node_type_filter_layout.addWidget(node_type_label) 222 | node_type_filter_layout.addWidget(self.ui_node_type_category_combo) 223 | node_type_filter_layout.addWidget(self.ui_node_type_combo, stretch=1) 224 | 225 | # add to node filter layout 226 | node_filter_layout.addLayout(node_name_filter_layout, stretch=1) 227 | node_filter_layout.addLayout(node_type_filter_layout, stretch=1) 228 | 229 | # parm filter layout 230 | parm_filter_layout = QHBoxLayout() 231 | parm_filter_layout.setSpacing(20) 232 | 233 | # Parm name filter 234 | parm_name_filter_layout = QHBoxLayout() 235 | parm_name_filter_layout.setSpacing(5) 236 | parm_name_label = QLabel('Parm Name:') 237 | self.ui_parm_name_filter_text = QLineEdit('*') 238 | self.ui_parm_name_filter_text.editingFinished.connect(self.on_refresh) 239 | parm_name_filter_layout.addWidget(parm_name_label) 240 | parm_name_filter_layout.addWidget(self.ui_parm_name_filter_text) 241 | 242 | # Parm File type filter 243 | parm_file_type_filter_layout = QHBoxLayout() 244 | parm_file_type_filter_layout.setSpacing(5) 245 | file_type_label = QLabel('Parm File Type:') 246 | parm_file_type_filter_layout.addWidget(file_type_label) 247 | self.ui_file_type_combo = hou.qt.ComboBox() 248 | self.ui_file_type_combo.addItem('Image') 249 | self.ui_file_type_combo.addItem('Geometry') 250 | self.ui_file_type_combo.currentTextChanged.connect(self.on_refresh) 251 | parm_file_type_filter_layout.addWidget(self.ui_file_type_combo) 252 | parm_file_type_filter_layout.addStretch() 253 | 254 | # add to parm filter layout 255 | parm_filter_layout.addLayout(parm_name_filter_layout, stretch=1) 256 | parm_filter_layout.addLayout(parm_file_type_filter_layout, stretch=1) 257 | 258 | # add to filter layout 259 | filter_layout.addLayout(node_filter_layout) 260 | filter_layout.addLayout(parm_filter_layout) 261 | 262 | # set layout 263 | filter_grp_box.setLayout(filter_layout) 264 | 265 | # top section layout 266 | top_section_layout = QVBoxLayout() 267 | top_section_layout.addWidget(self.ui_refresh_button) 268 | top_section_layout.addLayout(root_path_layout) 269 | top_section_layout.addWidget(filter_grp_box) 270 | 271 | return top_section_layout 272 | 273 | def build_node_view_widget(self): 274 | # Top widget and layout 275 | node_view_top_widget = QWidget() 276 | node_view_layout = QHBoxLayout() 277 | 278 | # The tree view 279 | self.ui_node_tree_view = QTreeView() 280 | self.ui_node_tree_view.setAlternatingRowColors(True) 281 | self.ui_node_tree_view.setSelectionMode( 282 | QAbstractItemView.ExtendedSelection) 283 | self.ui_node_tree_view.setToolTip( 284 | 'Node View:\n' 285 | '* Select nodes to show parameters details:\n' 286 | ' - This will NOT affect node selection in Houdini!\n' 287 | ' - Only highlighted nodes can be selected!\n' 288 | ' - Single-click the highlighted item to select single item.\n' 289 | ' - Click and drag to select multiple items.\n' 290 | ' - Use Ctrl + click to toggle selection of the items.\n' 291 | ' - Use Shift + click the start and end items for a range.\n' 292 | '* Double-click on an item to select it, set it to current \n' 293 | ' and show it in the Network View.\n' 294 | ' - This WILL affect node selection in Houdini!') 295 | self.ui_node_tree_view.doubleClicked.connect( 296 | self.on_node_tree_view_double_clicked) 297 | 298 | # Add tree view 299 | node_view_layout.addWidget(self.ui_node_tree_view) 300 | node_view_top_widget.setLayout(node_view_layout) 301 | 302 | return node_view_top_widget 303 | 304 | def build_parm_view_widget(self): 305 | # Top widget and layout 306 | parm_view_top_widget = QWidget() 307 | parm_view_layout = QHBoxLayout() 308 | 309 | # The tree view 310 | self.ui_parm_tree_view = QTreeView() 311 | self.ui_parm_tree_view.setAlternatingRowColors(True) 312 | self.ui_parm_tree_view.setSelectionMode( 313 | QAbstractItemView.ExtendedSelection) 314 | self.ui_parm_tree_view.setToolTip( 315 | 'Parameter View:\n' 316 | '* To select parameter(s) for "Batch Processing" (right panel):\n' 317 | ' - Single-click the highlighted item to select single item.\n' 318 | ' - Click and drag to select multiple items.\n' 319 | ' - Use Ctrl + click to toggle selection of the items.\n' 320 | ' - Use Shift + click the start and end items for a range.\n' 321 | '* Use the file chooser buttons in the "Tools" column \n' 322 | ' to browse and choose files.\n' 323 | '* Double-click on an item of the "Raw Value" column to \n' 324 | ' edit them directly in place.') 325 | 326 | # Add to layout 327 | parm_view_layout.addWidget(self.ui_parm_tree_view) 328 | parm_view_top_widget.setLayout(parm_view_layout) 329 | 330 | return parm_view_top_widget 331 | 332 | def build_tools_n_log_widget(self): 333 | # create tab widget 334 | tools_n_log_top_widget = QTabWidget() 335 | 336 | # create the scroll area 337 | tools_scroll_area = QScrollArea() 338 | tools_scroll_area.setWidgetResizable(True) 339 | 340 | # Create widget and layout 341 | tools_widget = QWidget() 342 | tools_layout = QVBoxLayout() 343 | 344 | # create a GroupBox for batch process files 345 | self.ui_batch_process_grp_box = QGroupBox('Batch Processing') 346 | batch_process_grp_box_layout = QVBoxLayout() 347 | 348 | # radio group 349 | selection_hlayout = QHBoxLayout() 350 | repath_label = QLabel("Repath :") 351 | selection_option_button_grp = QButtonGroup() 352 | self.ui_selected_parms_option = QRadioButton('Selected parm(s)') 353 | self.ui_selected_parms_option.setChecked(True) 354 | self.ui_all_parms_option = QRadioButton('All listed parm(s)') 355 | selection_option_button_grp.addButton(self.ui_selected_parms_option) 356 | selection_option_button_grp.addButton(self.ui_all_parms_option) 357 | selection_hlayout.addWidget(repath_label) 358 | selection_hlayout.addWidget(self.ui_selected_parms_option) 359 | selection_hlayout.addWidget(self.ui_all_parms_option) 360 | selection_hlayout.setStretch(0, 1) 361 | selection_hlayout.setStretch(1, 1) 362 | selection_hlayout.setStretch(2, 1) 363 | 364 | string_replace_grp_box = QGroupBox('String Replace') 365 | string_replace_grp_box_layout = QVBoxLayout() 366 | 367 | pattern_label = QLabel("Pattern string:") 368 | pattern_hlayout = QHBoxLayout() 369 | self.ui_pattern_str = QLineEdit('') 370 | pattern_path_browse = hou.qt.FileChooserButton() 371 | pattern_path_browse.setFileChooserFilter(hou.fileType.Directory) 372 | pattern_path_browse.setFileChooserTitle('Choose source directory') 373 | pattern_path_browse.fileSelected.connect(self.on_pattern_path_browse) 374 | pattern_hlayout.addWidget(self.ui_pattern_str) 375 | pattern_hlayout.addWidget(pattern_path_browse) 376 | 377 | replacement_label = QLabel('Replacement string:') 378 | replacement_hlayout = QHBoxLayout() 379 | self.ui_replacement_str = QLineEdit('$HIP/tex/') 380 | replacement_path_browse = hou.qt.FileChooserButton() 381 | replacement_path_browse.setFileChooserFilter(hou.fileType.Directory) 382 | replacement_path_browse.setFileChooserTitle( 383 | 'Choose destination directory') 384 | replacement_path_browse.fileSelected.connect( 385 | self.on_replacement_path_browse) 386 | replacement_hlayout.addWidget(self.ui_replacement_str) 387 | replacement_hlayout.addWidget(replacement_path_browse) 388 | 389 | syntax_layout = QHBoxLayout() 390 | syntax_label = QLabel('Function to use :') 391 | self.ui_syntax_combo_box = hou.qt.ComboBox() 392 | for func_tuple in const.STR_REPLACE_FUNCS: 393 | self.ui_syntax_combo_box.addItem(func_tuple[0]) 394 | # syntax_count_label = QLabel('Count :') 395 | # self.ui_syntax_count = QLineEdit('-1') 396 | # vali = QIntValidator() 397 | # vali.setBottom(-1) 398 | # self.ui_syntax_count.setValidator(vali) 399 | # self.ui_syntax_count.setToolTip( 400 | # 'Default -1 to replace/substitute all occurrences.\n' 401 | # 'Use 0 for not doing anything.\n' 402 | # 'Value greater than 0 to replace/substitute corresponding ' 403 | # 'number of occurrences.' 404 | # ) 405 | syntax_layout.addWidget(syntax_label) 406 | syntax_layout.addWidget(self.ui_syntax_combo_box) 407 | # syntax_layout.addWidget(syntax_count_label) 408 | # syntax_layout.addWidget(self.ui_syntax_count) 409 | syntax_layout.setStretch(1, 1) 410 | 411 | string_replace_grp_box_layout.addWidget(pattern_label) 412 | string_replace_grp_box_layout.addLayout(pattern_hlayout) 413 | string_replace_grp_box_layout.addWidget(replacement_label) 414 | string_replace_grp_box_layout.addLayout(replacement_hlayout) 415 | string_replace_grp_box_layout.addLayout(syntax_layout) 416 | string_replace_grp_box.setLayout(string_replace_grp_box_layout) 417 | 418 | file_action_hlayout = QHBoxLayout() 419 | file_action_label = QLabel('File action :') 420 | 421 | self.ui_batch_process_action_combo = hou.qt.ComboBox() 422 | for file_action in const.FILE_ACTIONS: 423 | self.ui_batch_process_action_combo.addItem(file_action) 424 | file_action_hlayout.addWidget(file_action_label) 425 | file_action_hlayout.addWidget(self.ui_batch_process_action_combo) 426 | file_action_hlayout.setStretch(1, 1) 427 | 428 | buttons_hlayout = QHBoxLayout() 429 | preview_it = QPushButton('Dryrun') 430 | 431 | preview_it.setFixedHeight(40) 432 | preview_it.clicked.connect(self.on_action_dryrun) 433 | run_it = QPushButton('Run') 434 | run_it.setFixedHeight(40) 435 | run_it.clicked.connect(self.on_action_run) 436 | buttons_hlayout.addWidget(preview_it) 437 | buttons_hlayout.addWidget(run_it) 438 | buttons_hlayout.setStretch(0, 3) 439 | buttons_hlayout.setStretch(1, 7) 440 | 441 | note_label = QLabel( 442 | 'NOTE: \n' 443 | ' 1. The repath string replace will be applied to the RAW value ' 444 | 'of the Parameter, not the expanded value.\n' 445 | ' 2. File action can apply to sequence files with or $F ' 446 | '(or ${F}) in the filenames.\n' 447 | ' 3. The intermediate-level directories will be created when ' 448 | 'copying/moving the files to a non-existent directory.\n' 449 | ' 4. Dryrun button is to only display the changes will be made ' 450 | 'to the parameters, but not copy/move files or set parameter ' 451 | 'values.' 452 | ) 453 | note_label.setWordWrap(True) 454 | 455 | batch_process_grp_box_layout.addLayout(selection_hlayout) 456 | batch_process_grp_box_layout.addWidget(string_replace_grp_box) 457 | batch_process_grp_box_layout.addLayout(file_action_hlayout) 458 | batch_process_grp_box_layout.addStretch() 459 | batch_process_grp_box_layout.addLayout(buttons_hlayout) 460 | batch_process_grp_box_layout.addWidget(note_label) 461 | self.ui_batch_process_grp_box.setLayout( 462 | batch_process_grp_box_layout 463 | ) 464 | 465 | # Add the GroupBox to the layout 466 | tools_layout.addWidget(self.ui_batch_process_grp_box) 467 | 468 | # Set the layout and widget 469 | tools_widget.setLayout(tools_layout) 470 | 471 | # add to scroll area 472 | tools_scroll_area.setWidget(tools_widget) 473 | 474 | # create log widget 475 | log_scroll_area = QScrollArea() 476 | 477 | # add widgets to tab widget 478 | tools_n_log_top_widget.addTab(tools_scroll_area, "Tools") 479 | tools_n_log_top_widget.addTab(log_scroll_area, 'Logs') 480 | 481 | return tools_n_log_top_widget 482 | 483 | def build_center_section(self): 484 | 485 | # ==== centre section top widgets ==== 486 | node_view_top_widget = self.build_node_view_widget() 487 | 488 | parm_view_top_widget = self.build_parm_view_widget() 489 | 490 | tools_n_log_top_widget = self.build_tools_n_log_widget() 491 | 492 | # ==== centre section layout ==== 493 | centre_section_layout = QHBoxLayout() 494 | splitter = QSplitter() 495 | splitter.addWidget(node_view_top_widget) 496 | splitter.addWidget(parm_view_top_widget) 497 | splitter.addWidget(tools_n_log_top_widget) 498 | splitter.setStretchFactor(0, 2) 499 | splitter.setStretchFactor(1, 4) 500 | splitter.setStretchFactor(2, 3) 501 | centre_section_layout.addWidget(splitter) 502 | 503 | return centre_section_layout 504 | 505 | def on_reset(self): 506 | self.ui_root_path_text.setText('') 507 | self.on_refresh() 508 | 509 | def on_refresh(self): 510 | """ The main callback for refreshing the UIs.""" 511 | 512 | # Get the root node 513 | root_path = self.ui_root_path_text.text() 514 | if not root_path: 515 | self.set_up_node_tree_model([]) 516 | self.set_up_parm_tree_model([]) 517 | return 518 | 519 | root_node = hou.node(root_path) 520 | if not root_node: 521 | self.set_up_node_tree_model([]) 522 | self.set_up_parm_tree_model([]) 523 | return 524 | 525 | # Get the filters 526 | filter_matchers = [] 527 | node_name_filter_txt = self.ui_node_name_filter_text.text() 528 | if not node_name_filter_txt: 529 | node_name_filter_txt = '*' 530 | filter_matchers.append(nodesearch.Name(node_name_filter_txt)) 531 | 532 | node_type_filter_txt = self.ui_node_type_combo.lineEdit().text() 533 | filter_matchers.append(nodesearch.NodeType(node_type_filter_txt)) 534 | 535 | parm_name_filter_txt = self.ui_parm_name_filter_text.text() 536 | if not parm_name_filter_txt: 537 | parm_name_filter_txt = '*' 538 | parm_file_type_filter_txt = self.ui_file_type_combo.currentText() 539 | filter_matchers.append(matchers.ParmNameAndFileType( 540 | parm_name_filter_txt, parm_file_type_filter_txt)) 541 | 542 | final_matcher_grp = nodesearch.Group(filter_matchers, intersect=True) 543 | 544 | nodes = list(final_matcher_grp.nodes(root_node, ignore_case=True, 545 | recursive=True, 546 | recurse_in_locked_nodes=False)) 547 | 548 | # Get node path list 549 | path_list = [n.path() for n in nodes] 550 | 551 | # Set up the two models. 552 | self.set_up_node_tree_model(path_list) 553 | self.set_up_parm_tree_model([]) 554 | 555 | def on_root_node_selected(self, op_node): 556 | self.ui_root_path_text.setText(op_node.path()) 557 | self.on_refresh() 558 | 559 | def on_node_tree_view_double_clicked(self, model_index: QModelIndex): 560 | 561 | node = self._node_tree_model.get_hou_object(model_index) 562 | if not node: 563 | return 564 | 565 | node.setCurrent(True, clear_all_selected=True) 566 | 567 | def on_node_tree_view_selection_changed(self, selected, deselected): 568 | # get parm name pattern 569 | parm_name_pattern = self.ui_parm_name_filter_text.text() 570 | if not parm_name_pattern: 571 | parm_name_pattern = '*' 572 | 573 | # get parm file type 574 | parm_file_type = self.ui_file_type_combo.currentText().lower() 575 | 576 | # Get the selected nodes from node tree view 577 | # Use selectedRows(0) because we just need one id per row. 578 | id_list = self.ui_node_tree_view.selectionModel().selectedRows(0) 579 | 580 | # Find all the filtered parms 581 | parm_list = [] 582 | for index in id_list: 583 | 584 | node = (self._node_tree_model.get_item(index) 585 | .get_raw_data().get_orig_data()) 586 | 587 | for parm in node.globParms(parm_name_pattern, ignore_case=True): 588 | 589 | # Re-use the function from matchers 590 | if matchers.parm_is_file_type(parm, parm_file_type, 591 | match_invisible=False): 592 | parm_list.append(parm.path()) 593 | 594 | self.set_up_parm_tree_model(parm_list) 595 | self.node_tree_view_config_post_model_setup() 596 | 597 | def update_parm_model(self, row_id, path): 598 | if not path: 599 | return 600 | 601 | index = self._parm_tree_model.index(row_id, 2) 602 | self._parm_tree_model.setData(index, path, Qt.EditRole) 603 | 604 | def on_parm_tree_data_changed(self, top_left: QModelIndex, 605 | bottom_right: QModelIndex, roles): 606 | parm = (self._parm_tree_model.get_item(top_left) 607 | .get_raw_data().get_orig_data()) 608 | 609 | def on_action_dryrun(self): 610 | results = self.run_it(dryrun=True) 611 | if not results: 612 | return 613 | 614 | dialog = QDialog(self) 615 | layout = QVBoxLayout() 616 | 617 | table = QTableWidget() 618 | table.setColumnCount(2) 619 | table.setRowCount(len(results)) 620 | table.setHorizontalHeaderLabels(['Original Raw Path', 'New Raw Path']) 621 | header = table.horizontalHeader() 622 | header.setSectionResizeMode(0, QHeaderView.ResizeToContents) 623 | header.setSectionResizeMode(1, QHeaderView.Stretch) 624 | for index, pair in enumerate(results): 625 | table.setItem(index, 0, QTableWidgetItem(pair[0])) 626 | table.setItem(index, 1, QTableWidgetItem(pair[1])) 627 | 628 | layout.addWidget(table) 629 | dialog.setLayout(layout) 630 | dialog.setGeometry(400, 200, 800,500) 631 | dialog.setModal(True) 632 | dialog.exec_() 633 | 634 | def on_action_run(self): 635 | results = self.run_it(dryrun=False) 636 | 637 | def run_it(self, dryrun=True): 638 | 639 | # Just return if no tree model 640 | if not self._parm_tree_model: 641 | return 642 | 643 | # Each item of the list is a tuple of 644 | # (id_of_column_0, id_of_column_2) 645 | id_list = [] 646 | if self.ui_selected_parms_option.isChecked(): 647 | col_0_list = self.ui_parm_tree_view.selectionModel().selectedRows(0) 648 | col_2_list = self.ui_parm_tree_view.selectionModel().selectedRows(2) 649 | id_list = list(zip(col_0_list, col_2_list)) 650 | if not id_list: 651 | return 652 | 653 | elif self.ui_all_parms_option.isChecked(): 654 | root_index = self.ui_parm_tree_view.rootIndex() 655 | root_item = self._parm_tree_model.get_item(root_index) 656 | row_num = len(root_item.children()) 657 | for row_id in range(row_num): 658 | id_list.append((self._parm_tree_model.index(row_id, 0), 659 | self._parm_tree_model.index(row_id, 2))) 660 | 661 | # replace function 662 | syntax_combo_index = self.ui_syntax_combo_box.currentIndex() 663 | if syntax_combo_index >= len(const.STR_REPLACE_FUNCS): 664 | hou.ui.displayMessage('The str replace function is not supported!\n' 665 | 'Supported are : {}' 666 | .format(const.STR_REPLACE_FUNCS)) 667 | return 668 | replace_func = const.STR_REPLACE_FUNCS[syntax_combo_index][1] 669 | 670 | # pattern string 671 | pattern_string = self.ui_pattern_str.text() 672 | if not pattern_string: 673 | hou.ui.displayMessage('Pattern string is empty. Exiting.') 674 | return 675 | 676 | # replacement string 677 | replacement_string = self.ui_replacement_str.text() 678 | if not replacement_string: 679 | hou.ui.displayMessage('Replacement string is empty. Exiting.') 680 | return 681 | 682 | # Action 683 | file_action = self.ui_batch_process_action_combo.currentText() 684 | if file_action not in const.FILE_ACTIONS: 685 | hou.ui.displayMessage('The action is not supported!\n' 686 | 'Supported actions are : {}' 687 | .format(const.FILE_ACTIONS)) 688 | return 689 | 690 | 691 | 692 | # count 693 | # count = int(self.ui_syntax_count.text()) 694 | 695 | # results with original and new raw path pairs 696 | results = [] 697 | 698 | # Process 699 | for id_pair in id_list: 700 | 701 | # get parm from id 702 | parm = (self._parm_tree_model.get_item(id_pair[0]) 703 | .get_raw_data().get_orig_data()) 704 | 705 | if not parm.rawValue(): 706 | continue 707 | 708 | # replace string 709 | original_raw_path = parm.rawValue() 710 | new_raw_path = replace_func(pattern_string, replacement_string, 711 | original_raw_path) 712 | 713 | # process parameter files 714 | if file_action == const.FILE_ACTION_NONE: 715 | success = True 716 | else: 717 | success = utils.process_parm_files(parm, file_action, 718 | new_raw_path, 719 | dryrun=dryrun) 720 | 721 | if success: 722 | # add to results 723 | results.append((original_raw_path, new_raw_path)) 724 | if not dryrun: 725 | # Then set model data, the views will update automatically. 726 | self._parm_tree_model.setData(id_pair[1], new_raw_path, 727 | Qt.EditRole) 728 | 729 | return results 730 | 731 | def on_preview_file(self, row_id): 732 | 733 | index = self._parm_tree_model.index(row_id, 2) 734 | parm = (self._parm_tree_model.get_item(index).get_raw_data() 735 | .get_orig_data()) 736 | file_path = parm.eval() 737 | raw_value = parm.rawValue() 738 | if not file_path: 739 | return 740 | 741 | if not os.path.isfile(file_path): 742 | hou.ui.displayMessage('The file does not exist:\n' 743 | ' {}\n' 744 | '(Raw value: {})' 745 | .format(file_path, raw_value)) 746 | return 747 | 748 | subprocess.Popen(['mplay', '-minimal', file_path]) 749 | 750 | def on_node_type_category_changed(self): 751 | self.ui_node_type_combo.clear() 752 | self.ui_node_type_combo.addItem('*') 753 | 754 | cur_cate = self.ui_node_type_category_combo.itemData( 755 | self.ui_node_type_category_combo.currentIndex()) 756 | 757 | if not cur_cate: 758 | return 759 | 760 | items = nodesearch.node_types(cur_cate) 761 | self.ui_node_type_combo.addItems(items) 762 | 763 | def on_pattern_path_browse(self, dir_path): 764 | self.ui_pattern_str.setText(dir_path) 765 | 766 | def on_replacement_path_browse(self, dir_path): 767 | self.ui_replacement_str.setText(dir_path) 768 | 769 | -------------------------------------------------------------------------------- /scripts/python/hou_file_manager/constants.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright: (C) 2024 Kevin Ma Yi 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the “Software”), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import hou 24 | import re 25 | 26 | PATH_DELIMITER = '/' 27 | 28 | # Headers 29 | DEFAULT_TREE_HEADERS = ['Name', 'Value'] 30 | NODE_TREE_HEADERS = ['Node View'] 31 | FILE_PARM_LIST_HEADERS = ['Parameter View', 'Tools', 32 | 'Raw Value (Double click to edit)'] 33 | 34 | PARM_TREE_VIEW_EDITABLE_COLUMN = 2 35 | 36 | # Getter attr 37 | PARM_GET_ATTRS = ['name', '', 'rawValue'] 38 | NODE_GET_ATTRS = ['name'] 39 | 40 | # Setter attr 41 | PARM_SET_ATTRS = ['', '', 'set'] 42 | NODE_SET_ATTRS = [''] 43 | 44 | # File actions 45 | FILE_ACTION_NONE = 'None -> ONLY update parameter, NOT making changes to files' 46 | FILE_ACTION_COPY = 'Copy -> Update parameter AND copy files to new paths' 47 | FILE_ACTION_MOVE = 'Move -> Update parameter AND move files to new paths' 48 | FILE_ACTIONS = [FILE_ACTION_NONE, FILE_ACTION_COPY, FILE_ACTION_MOVE] 49 | 50 | # String replace functions 51 | PYTHON_STR_REPLACE = 'Python: str.replace()' 52 | STR_REPLACE = (PYTHON_STR_REPLACE, 53 | lambda pattern, repl, string: 54 | string.replace(pattern, repl) 55 | ) 56 | PYTHON_RE_SUBSTITUTE = 'Python: re.sub()' 57 | REGEX_SUBSTITUTE = (PYTHON_RE_SUBSTITUTE, 58 | lambda pattern, repl, string: 59 | re.sub(pattern, repl, string) 60 | ) 61 | STR_REPLACE_FUNCS = [STR_REPLACE, REGEX_SUBSTITUTE] 62 | 63 | # Colors 64 | BG_RED = (100, 0, 0) 65 | BG_GREEN = (0, 100, 0) 66 | 67 | # icons 68 | ICON_SIZE = hou.ui.scaledSize(16) 69 | ADD_ICON = hou.qt.createIcon("BUTTONS_list_add", ICON_SIZE, ICON_SIZE) 70 | 71 | SESSION_VAR = 'GLOBAL_BROWSER_UI_HOU_FILE_MANAGER' 72 | 73 | 74 | -------------------------------------------------------------------------------- /scripts/python/hou_file_manager/hou_tree_model.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright: (C) 2024 Kevin Ma Yi 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the “Software”), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from PySide2.QtCore import QAbstractItemModel 24 | from PySide2.QtCore import QModelIndex 25 | 26 | from PySide2.QtCore import Qt 27 | 28 | import hou 29 | 30 | from . import constants as const 31 | from .treemodel import (BaseTreeModel, TreeItem, TreeItemDataGenericList, BaseTreeItemData) 32 | 33 | 34 | class TreeItemDataObject(BaseTreeItemData): 35 | def __init__(self, orig_data, property_get_attrs, property_set_attrs, 36 | bg_color=None): 37 | super().__init__(orig_data) 38 | 39 | self._property_get_attrs = property_get_attrs 40 | self._property_set_attrs = property_set_attrs 41 | self._bg_color = bg_color 42 | 43 | if isinstance(orig_data, hou.OpNode): 44 | orig_data.addEventCallback((hou.nodeEventType.BeingDeleted, ), 45 | self.data_deleted) 46 | 47 | def len(self): 48 | return len(self._property_get_attrs) 49 | 50 | def get(self, column: int = 0): 51 | if column < 0 or column >= len(self._property_get_attrs): 52 | return None 53 | 54 | attr = self._property_get_attrs[column] 55 | if not hasattr(self._orig_data, attr): 56 | return None 57 | 58 | return getattr(self._orig_data, attr)() 59 | 60 | def set_data(self, column: int, value): 61 | if column < 0 or column >= len(self._property_set_attrs): 62 | return False 63 | 64 | attr = self._property_set_attrs[column] 65 | if not hasattr(self._orig_data, attr): 66 | return False 67 | 68 | getattr(self._orig_data, attr)(value) 69 | return True 70 | 71 | def get_bg_color(self): 72 | return self._bg_color 73 | 74 | def get_icon(self): 75 | if not isinstance(self._orig_data, hou.OpNode): 76 | return None 77 | 78 | icon_name = self._orig_data.type().icon() 79 | return hou.qt.createIcon(icon_name) 80 | 81 | def data_deleted(self, node, event_type, **kwargs): 82 | if not hasattr(hou.session, const.SESSION_VAR): 83 | print('No Hou File Manager UI in hou.session to refresh.') 84 | return 85 | 86 | browser = getattr(hou.session, const.SESSION_VAR) 87 | if not browser: 88 | print('Browser not found from session var.') 89 | return 90 | 91 | browser.on_reset() 92 | 93 | 94 | class HouNodeTreeModel(BaseTreeModel): 95 | def __init__(self, path_list: list, parent=None): 96 | 97 | self._property_get_attrs = const.NODE_GET_ATTRS 98 | self._property_set_attrs = const.NODE_SET_ATTRS 99 | 100 | # Headers 101 | headers = const.NODE_TREE_HEADERS 102 | 103 | # root item 104 | hou_root_node = hou.node(const.PATH_DELIMITER) 105 | root_item = TreeItem( 106 | TreeItemDataObject(hou_root_node, 107 | self._property_get_attrs, 108 | self._property_set_attrs) 109 | ) 110 | 111 | super().__init__(path_list, headers, root_item, parent) 112 | 113 | def flags(self, index: QModelIndex) -> Qt.ItemFlags: 114 | if not index.isValid(): 115 | return Qt.NoItemFlags 116 | 117 | flags = QAbstractItemModel.flags(self, index) 118 | 119 | # Get tree item 120 | item = self.get_item(index) 121 | 122 | bg_color = item.get_raw_data().get_bg_color() 123 | if not bg_color: 124 | flags &= ~Qt.ItemIsSelectable 125 | 126 | return flags 127 | 128 | def set_up_model_data(self, data: list): 129 | """ Data is a string list of paths of Hou Nodes.""" 130 | 131 | # Must sort the path list 132 | data.sort() 133 | 134 | for path in data: 135 | self.add_path_to_tree(path, self._root_item,'', 136 | self._property_get_attrs, 137 | self._property_set_attrs) 138 | 139 | def get_hou_object(self, index: QModelIndex): 140 | if not index.isValid(): 141 | return None 142 | 143 | # Get tree item 144 | item = self.get_item(index) 145 | 146 | return item.tree_item_data().get_orig_data() 147 | 148 | def add_path_to_tree(self, path, target_tree_item, current_path, 149 | property_get_attrs, property_set_attrs): 150 | parts = path.strip(const.PATH_DELIMITER).split(const.PATH_DELIMITER) 151 | 152 | current = parts[0] 153 | the_rest = parts[1:] 154 | 155 | child = target_tree_item.get_child_by_column_data(current) 156 | current_path = '{}{}{}'.format(current_path, 157 | const.PATH_DELIMITER, 158 | current) 159 | 160 | if not child: 161 | # get the hou node 162 | hou_node = hou.node(current_path) 163 | # create a tree item 164 | if not the_rest: 165 | bg_color = const.BG_RED 166 | else: 167 | bg_color = None 168 | child = TreeItem( 169 | TreeItemDataObject(hou_node, 170 | property_get_attrs, 171 | property_set_attrs, 172 | bg_color) 173 | ) 174 | target_tree_item.append_child(child) 175 | 176 | if the_rest: 177 | # continue adding the rest 178 | self.add_path_to_tree(const.PATH_DELIMITER.join(the_rest), 179 | child, 180 | current_path, 181 | property_get_attrs, 182 | property_set_attrs) 183 | 184 | 185 | class HouParmTreeModel(BaseTreeModel): 186 | def __init__(self, node_list: list, parent=None): 187 | 188 | self._property_get_attrs = const.PARM_GET_ATTRS 189 | self._property_set_attrs = const.PARM_SET_ATTRS 190 | 191 | headers = const.FILE_PARM_LIST_HEADERS 192 | 193 | root_item = TreeItem(TreeItemDataGenericList(['', '', ''])) 194 | 195 | super().__init__(node_list, headers, root_item, parent) 196 | 197 | def flags(self, index: QModelIndex) -> Qt.ItemFlags: 198 | if not index.isValid(): 199 | return Qt.NoItemFlag 200 | 201 | flags = QAbstractItemModel.flags(self, index) 202 | col = index.column() 203 | if col == const.PARM_TREE_VIEW_EDITABLE_COLUMN: 204 | flags |= Qt.ItemIsEditable 205 | return flags 206 | 207 | def set_up_model_data(self, data: list): 208 | """Data is a string list of parm paths.""" 209 | 210 | for parm_path in data: 211 | parm = hou.parm(parm_path) 212 | # add the node as a child to the root 213 | child = TreeItem( 214 | TreeItemDataObject(parm, 215 | self._property_get_attrs, 216 | self._property_set_attrs) 217 | ) 218 | self._root_item.append_child(child) 219 | 220 | # then add file parameters as children to the node 221 | 222 | def get_hou_object(self, index: QModelIndex): 223 | if not index.isValid(): 224 | return None 225 | 226 | # Get tree item 227 | item = self.get_item(index) 228 | 229 | return item.tree_item_data().get_orig_data() 230 | 231 | 232 | 233 | -------------------------------------------------------------------------------- /scripts/python/hou_file_manager/matchers.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright: (C) 2024 Kevin Ma Yi 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the “Software”), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import hou 24 | from nodesearch.matchers import Matcher 25 | 26 | FILE_TYPE_DICT = {'image': hou.fileType.Image, 27 | 'geometry': hou.fileType.Geometry} 28 | 29 | 30 | def parm_is_file_type(parm, file_type, match_invisible=False): 31 | # No need to match when the parm is invisible and we don't want to 32 | # match invisible parms. 33 | if not match_invisible and not parm.isVisible(): 34 | return False 35 | 36 | # Get parmTemplate of the parm. 37 | pt = parm.parmTemplate() 38 | 39 | if not isinstance(pt, hou.StringParmTemplate): 40 | return False 41 | 42 | if not pt.stringType() == hou.stringParmType.FileReference: 43 | return False 44 | 45 | # It will return True once any parm meets the condition. 46 | # No need to continue checking all the rest of the parms. 47 | if pt.fileType().name().lower() == file_type: 48 | return True 49 | 50 | 51 | class ParmNameAndFileType(Matcher): 52 | """ 53 | Matches nodes if any of the node's parameters match the name pattern 54 | and file_type. 55 | """ 56 | 57 | def __init__(self, name_pattern, file_type, match_invisible=False): 58 | 59 | self.name_pattern = name_pattern 60 | # file_type is string and converted to lower case. 61 | self.file_type = file_type.lower() 62 | self.match_invisible = match_invisible 63 | 64 | def __repr__(self): 65 | 66 | return ("<{} {} match_invisible={}>" 67 | .format(type(self).__name__, self.file_type, 68 | self.match_invisible)) 69 | 70 | def matches(self, node, ignore_case=False): 71 | 72 | if self.file_type not in FILE_TYPE_DICT.keys(): 73 | raise Exception('The file type is not supported: {}' 74 | .format(self.file_type)) 75 | 76 | for parm in node.globParms(self.name_pattern, ignore_case=ignore_case, 77 | search_label=True, single_pattern=False): 78 | if parm_is_file_type(parm, self.file_type, self.match_invisible): 79 | return True 80 | 81 | return False 82 | 83 | -------------------------------------------------------------------------------- /scripts/python/hou_file_manager/treemodel.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright: (C) 2024 Kevin Ma Yi 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the “Software”), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from PySide2.QtCore import QAbstractItemModel 24 | from PySide2.QtCore import QModelIndex 25 | from PySide2.QtCore import Qt 26 | from PySide2.QtGui import QBrush, QColor 27 | 28 | from . import constants as const 29 | 30 | 31 | class BaseTreeItemData: 32 | def __init__(self, orig_data, tree_item=None, icon=None, bg_color=None): 33 | self._orig_data = orig_data 34 | self._icon = icon 35 | self._bg_color = bg_color 36 | self.tree_item = tree_item 37 | 38 | def len(self): 39 | raise NotImplementedError 40 | 41 | def get(self, column: int = 0): 42 | raise NotImplementedError 43 | 44 | def set_data(self, column: int, value): 45 | raise NotImplementedError 46 | 47 | def get_orig_data(self): 48 | return self._orig_data 49 | 50 | def get_icon(self): 51 | return self._icon 52 | 53 | def set_icon(self, icon): 54 | self._icon = icon 55 | 56 | def get_bg_color(self): 57 | return self._bg_color 58 | 59 | def set_bg_color(self, bg_color): 60 | self._bg_color = bg_color 61 | 62 | 63 | class TreeItemDataGenericList(BaseTreeItemData): 64 | def __init__(self, orig_data: list): 65 | super().__init__(orig_data) 66 | 67 | def len(self): 68 | return len(self._orig_data) 69 | 70 | def get(self, column: int = 0): 71 | return self._orig_data[column] 72 | 73 | def set_data(self, column: int, value): 74 | if column < 0 or column >= len(self._orig_data): 75 | return False 76 | 77 | self._orig_data[column] = value 78 | return True 79 | 80 | def get_icon(self): 81 | return None 82 | 83 | 84 | class TreeItem: 85 | def __init__(self, data: BaseTreeItemData, parent: 'TreeItem' = None): 86 | self._data = data 87 | self._parent = parent 88 | self._children = [] 89 | self._data.tree_item = self 90 | 91 | def append_child(self, item: 'TreeItem'): 92 | self._children.append(item) 93 | item.set_parent(self) 94 | 95 | def get_child(self, index: int): 96 | if index < 0 or index >= len(self._children): 97 | return None 98 | return self._children[index] 99 | 100 | def get_child_by_column_data(self, data: str, column: int = 0): 101 | if not data: 102 | return None 103 | 104 | for child in self._children: 105 | child_data = child.data(column) 106 | 107 | if not child_data: 108 | continue 109 | 110 | if data == child_data: 111 | return child 112 | 113 | def children(self): 114 | return self._children 115 | 116 | def remove_child(self, item: 'TreeItem'): 117 | self.children().remove(item) 118 | 119 | def remove_children(self, position: int, count: int) -> bool: 120 | if position < 0 or position + count > len(self.children()): 121 | return False 122 | 123 | for row in range(count): 124 | self.children().pop(position) 125 | 126 | def parent(self): 127 | return self._parent 128 | 129 | def set_parent(self, parent: 'TreeItem'): 130 | self._parent = parent 131 | 132 | def get_row_id(self): 133 | if self._parent: 134 | return self._parent.children().index(self) 135 | 136 | return 0 137 | 138 | def data(self, column: int = 0): 139 | if column < 0 or column >= self._data.len(): 140 | return None 141 | 142 | return self._data.get(column) 143 | 144 | def set_data(self, column: int, value): 145 | if column < 0 or column >= self._data.len(): 146 | return False 147 | 148 | return self._data.set_data(column, value) 149 | 150 | def tree_item_data(self): 151 | return self._data 152 | 153 | def column_count(self): 154 | return self._data.len() 155 | 156 | def get_raw_data(self): 157 | """ To return the instance of a subclass of BaseTreeItemData.""" 158 | return self._data 159 | 160 | 161 | class BaseTreeModel(QAbstractItemModel): 162 | def __init__(self, input_data: list, headers: list, root_item: TreeItem, 163 | parent=None): 164 | super().__init__(parent) 165 | 166 | # Initialize headers and root_item 167 | self._headers = headers 168 | self._root_item = root_item 169 | 170 | # finally set up data for the model 171 | self.set_up_model_data(input_data) 172 | 173 | def get_item(self, index: QModelIndex = QModelIndex()) -> TreeItem: 174 | if index.isValid(): 175 | item = index.internalPointer() 176 | if item: 177 | return item 178 | 179 | return self._root_item 180 | 181 | def headerData(self, section: int, orientation: Qt.Orientation, 182 | role: int = Qt.DisplayRole): 183 | if orientation == Qt.Horizontal and role == Qt.DisplayRole: 184 | return self._headers[section] 185 | 186 | return None 187 | 188 | def index(self, row: int, column: int, 189 | parent: QModelIndex = QModelIndex()) -> QModelIndex: 190 | 191 | if not self.hasIndex(row, column, parent): 192 | return QModelIndex() 193 | 194 | result_id = QModelIndex() 195 | 196 | parent_item = self.get_item(parent) 197 | if not parent_item: 198 | return result_id 199 | 200 | child = parent_item.get_child(row) 201 | if child: 202 | result_id = self.createIndex(row, column, child) 203 | 204 | return result_id 205 | 206 | def parent(self, index: QModelIndex = QModelIndex()) -> QModelIndex: 207 | 208 | if not index.isValid(): 209 | return QModelIndex() 210 | 211 | input_item = self.get_item(index) 212 | if input_item: 213 | parent_item = input_item.parent() 214 | else: 215 | parent_item = None 216 | 217 | if not parent_item or parent_item == self._root_item: 218 | return QModelIndex() 219 | 220 | return self.createIndex(parent_item.get_row_id(), 0, parent_item) 221 | 222 | def rowCount(self, parent: QModelIndex) -> int: 223 | 224 | if parent.isValid() and parent.column() > 0: 225 | return 0 226 | 227 | parent_item = self.get_item(parent) 228 | if not parent_item: 229 | return 0 230 | 231 | children = parent_item.children() 232 | return len(children) 233 | 234 | def columnCount(self, parent: QModelIndex = None) -> int: 235 | return self._root_item.column_count() 236 | 237 | def data(self, index: QModelIndex, role: Qt.ItemDataRole = Qt.DisplayRole): 238 | if not index.isValid(): 239 | return None 240 | 241 | # Get tree item 242 | item = self.get_item(index) 243 | 244 | if role == Qt.DisplayRole or role == Qt.EditRole: 245 | return item.data(index.column()) 246 | 247 | elif role == Qt.DecorationRole: 248 | return item.get_raw_data().get_icon() 249 | 250 | elif role == Qt.BackgroundRole: 251 | # get the original data which is the Hou Node. 252 | bg_color = item.get_raw_data().get_bg_color() 253 | if bg_color: 254 | brush = QBrush(QColor(*bg_color)) 255 | return brush 256 | 257 | return None 258 | 259 | def setData(self, index: QModelIndex, value, role: int) -> bool: 260 | if role != Qt.EditRole: 261 | return False 262 | 263 | item = self.get_item(index) 264 | result = item.set_data(index.column(), value) 265 | 266 | if result: 267 | self.dataChanged.emit(index, index, [Qt.DisplayRole, Qt.EditRole]) 268 | 269 | return result 270 | 271 | def removeRows(self, position: int, rows: int, 272 | parent: QModelIndex = QModelIndex()) -> bool: 273 | 274 | parent_item = self.get_item(parent) 275 | if not parent_item: 276 | return False 277 | 278 | self.beginRemoveRows(parent, position, position + rows -1) 279 | success = parent_item.remove_children(position, rows) 280 | self.endRemoveRows() 281 | 282 | return success 283 | 284 | def set_up_model_data(self, data: list): 285 | raise NotImplementedError 286 | 287 | 288 | -------------------------------------------------------------------------------- /scripts/python/hou_file_manager/utils.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright: (C) 2024 Kevin Ma Yi 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the “Software”), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import os 24 | import shutil 25 | import glob 26 | import re 27 | import hou 28 | from . import constants as const 29 | 30 | 31 | def process_parm_files(parm, file_action, new_raw_path, dryrun=True): 32 | src_dest_file_pairs = [] 33 | 34 | original_raw_path = parm.rawValue() 35 | original_expanded_path = parm.eval() 36 | original_raw_basename = os.path.basename(original_raw_path) 37 | original_expanded_dirname = os.path.dirname(original_expanded_path) 38 | new_expanded_path = hou.text.expandString(new_raw_path) 39 | new_raw_basename = os.path.basename(new_raw_path) 40 | new_expanded_dirname = os.path.dirname(new_expanded_path) 41 | 42 | if not original_raw_path: 43 | return False 44 | 45 | is_sequence_style = False 46 | pattern_re = None 47 | 48 | # Houdini doesn't support time-dependent UDIM texture files. 49 | # We will check if the file path contains first. 50 | if '' in original_raw_path: 51 | pattern_re = re.compile('') 52 | is_sequence_style = True 53 | elif parm.isTimeDependent(): 54 | # Backtick or () are not supported. 55 | if ('`' in original_raw_basename or '(' in original_raw_basename 56 | or ')' in original_raw_basename): 57 | print('Ignored, because backticks and () are not supported at ' 58 | 'the moment.') 59 | return False 60 | 61 | # Checking for $F4 or ${F4} like substrings. 62 | pattern_re = re.compile('\$\{*F[0-9]*\}*') 63 | result = pattern_re.search(original_raw_basename) 64 | if not result: 65 | print('Ignored, because even it is time dependent but not $F ' 66 | 'or ${F} like string can not be found.') 67 | return False 68 | is_sequence_style = True 69 | 70 | # Based on if it is sequence style 71 | if is_sequence_style: 72 | # Get the full basename regex pattern 73 | orig_raw_basename_parts = pattern_re.split(original_raw_basename) 74 | orig_expanded_full_basename_pattern_re = re.compile( 75 | '^' + re.escape(orig_raw_basename_parts[0]) + 76 | '([0-9]+)' + re.escape(orig_raw_basename_parts[1]) + '$' 77 | ) 78 | 79 | # Split the new basename using the pattern 80 | new_raw_basename_parts = pattern_re.split(new_raw_basename) 81 | 82 | for f in os.listdir(original_expanded_dirname): 83 | match = orig_expanded_full_basename_pattern_re.match(f) 84 | if not match: 85 | continue 86 | 87 | # get the frame number from f 88 | seq_number = match.groups()[0] 89 | 90 | # construct new filename 91 | new_f = (new_raw_basename_parts[0] + seq_number + 92 | new_raw_basename_parts[1]) 93 | 94 | src = os.path.join(original_expanded_dirname, f) 95 | dest = os.path.join(new_expanded_dirname, new_f) 96 | 97 | src_dest_file_pairs.append((src, dest)) 98 | else: 99 | # Then it is a single file. Eval it which will expand the path. 100 | src_dest_file_pairs.append((original_expanded_path, new_expanded_path)) 101 | 102 | # if nothing to process then return 103 | if not src_dest_file_pairs: 104 | print('No files to apply the file actions. Exiting.') 105 | return False 106 | 107 | # for s_file in source_files: 108 | for src_file, dest_file in src_dest_file_pairs: 109 | if not os.path.isfile(src_file): 110 | print('The source file does not exist: \n {}'.format(src_file)) 111 | continue 112 | 113 | # Check if target file already exists. 114 | if os.path.isfile(dest_file): 115 | print('The file with same name as source file already ' 116 | 'exists in destination directory:\n' 117 | ' {}\n' 118 | 'Exiting.' 119 | .format(dest_file)) 120 | continue 121 | 122 | # Then take file action. 123 | if file_action == const.FILE_ACTION_COPY: 124 | print('Copying source file:\n' 125 | ' {}\n' 126 | ' to destination file:\n' 127 | ' {}' 128 | .format(src_file, dest_file)) 129 | if not dryrun: 130 | os.makedirs(os.path.dirname(dest_file), exist_ok=True) 131 | shutil.copy(src_file, dest_file) 132 | elif file_action == const.FILE_ACTION_MOVE: 133 | print('Moving source file:\n' 134 | ' {}\n' 135 | ' to destination file:\n' 136 | ' {}' 137 | .format(src_file, dest_file)) 138 | if not dryrun: 139 | os.makedirs(os.path.dirname(dest_file), exist_ok=True) 140 | shutil.move(src_file, dest_file) 141 | else: 142 | print('The file action is not supported: \n' 143 | ' {}\n' 144 | 'Exiting.' 145 | .format(file_action)) 146 | 147 | print('All files have been processed. Done.') 148 | return True 149 | 150 | 151 | -------------------------------------------------------------------------------- /toolbar/hou_file_manager.shelf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 37 | 38 | 39 | 40 | 61 | 62 | 63 | 64 | 85 | 86 | 87 | 88 | 94 | 95 | 96 | 97 | 119 | 120 | 121 | 122 | 124 | 125 | 126 | 127 | 141 | 142 | 143 | 144 | 158 | 159 | 160 | --------------------------------------------------------------------------------