├── python ├── robust_skin_weights_transfer_inpaint │ ├── __init__.py │ ├── vendor │ │ ├── __init__.py │ │ └── Qt.py │ ├── setup.py │ ├── ui.py │ └── logic.py └── userSetup.py ├── MayaTransferInpaintWeights.mod ├── LICENSE ├── pyproject.toml ├── README.md └── .gitignore /python/robust_skin_weights_transfer_inpaint/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/robust_skin_weights_transfer_inpaint/vendor/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MayaTransferInpaintWeights.mod: -------------------------------------------------------------------------------- 1 | + MayaTransferInpaintWeights any MayaTransferInpaintWeights 2 | PYTHONPATH +:= python 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 yamahigashi 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 | -------------------------------------------------------------------------------- /python/userSetup.py: -------------------------------------------------------------------------------- 1 | # # -*- coding: utf-8 -*- 2 | import sys 3 | import textwrap 4 | 5 | from maya import ( 6 | cmds, 7 | mel, 8 | ) 9 | 10 | 11 | if sys.version_info > (3, 0): 12 | from typing import TYPE_CHECKING 13 | if TYPE_CHECKING: 14 | from typing import ( 15 | Optional, # noqa: F401 16 | Dict, # noqa: F401 17 | List, # noqa: F401 18 | Tuple, # noqa: F401 19 | Pattern, # noqa: F401 20 | Callable, # noqa: F401 21 | Any, # noqa: F401 22 | Text, # noqa: F401 23 | Generator, # noqa: F401 24 | Union, # noqa: F401 25 | Iterable # noqa: F401 26 | ) 27 | 28 | 29 | ############################################################################## 30 | def __register_menu(): 31 | """Setup menu""" 32 | 33 | from textwrap import dedent 34 | cmds.evalDeferred(dedent( 35 | """ 36 | try: 37 | import robust_skin_weights_transfer_inpaint.setup as setup 38 | setup.register_menu() 39 | except: 40 | import traceback 41 | traceback.print_exc() 42 | """ 43 | )) 44 | 45 | 46 | if __name__ == '__main__': 47 | try: 48 | __register_menu() 49 | 50 | except Exception: 51 | import traceback 52 | traceback.print_exc() 53 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.flake8] 2 | max-line-length = 120 3 | max-complexity = 11 4 | 5 | [tool.black] 6 | line-length = 120 7 | target_version = ['py310'] 8 | include = '\.pyi?$' 9 | exclude = ''' 10 | 11 | ( 12 | /( 13 | \.eggs # exclude a few common directories in the 14 | | \.git # root of the project 15 | | \.hg 16 | | \.mypy_cache 17 | | \.tox 18 | | \.venv 19 | | _build 20 | | buck-out 21 | | build 22 | | dist 23 | )/ 24 | | foo.py # also separately exclude a file named foo.py in 25 | # the root of the project 26 | ) 27 | ''' 28 | 29 | 30 | [tool.pylint."FORMAT"] 31 | max-line-length=120 32 | 33 | [tool.pylint."MASTER"] 34 | init-hook='import sys;import os;from pylint.config import find_default_config_files;[sys.path.append(p) for p in os.getenv("PYTHONPATH").split(";")];[sys.path.append(os.path.join(os.path.dirname(p), "python") for p in find_default_config_files()];sys.path.append(os.path.join(os.path.dirname(p), "vendor_python") for p in find_default_config_files()' 35 | 36 | [tool.pylint."MESSAGES CONTROL"] 37 | disable=[ 38 | "consider-using-f-string", 39 | "invalid-name", 40 | "missing-function-docstring", 41 | "import-outside-toplevel", 42 | "consider-using-from-import", 43 | "wrong-import-order", 44 | ] 45 | 46 | 47 | [tool.pyright] 48 | extraPaths = [ 49 | "python", 50 | "vendor_python", 51 | "lib", 52 | "src", 53 | ] 54 | exclude = [ 55 | "**/node_modules", 56 | "**/__pycache__", 57 | ] 58 | reportUnusedImport = false 59 | reportMissingImports = true 60 | reportMissingTypeStubs = false 61 | reportMissingModuleSource = false 62 | reportTypeCommentUsage = false 63 | pythonVersion = "3.7" 64 | pythonPlatform = "Windows" 65 | typeCheckingMode = "basic" 66 | 67 | 68 | [tool.ruff] 69 | # Exclude a variety of commonly ignored directories. 70 | exclude = [ 71 | ".bzr", 72 | ".direnv", 73 | ".eggs", 74 | ".git", 75 | ".git-rewrite", 76 | ".hg", 77 | ".mypy_cache", 78 | ".nox", 79 | ".pants.d", 80 | ".pytype", 81 | ".ruff_cache", 82 | ".svn", 83 | ".tox", 84 | ".venv", 85 | "__pypackages__", 86 | "_build", 87 | "buck-out", 88 | "build", 89 | "dist", 90 | "node_modules", 91 | "venv", 92 | ] 93 | 94 | # Same as Black. 95 | line-length = 120 96 | indent-width = 4 97 | 98 | target-version = "py310" 99 | 100 | [tool.ruff.lint] 101 | # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. 102 | # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or 103 | # McCabe complexity (`C901`) by default. 104 | select = ["E4", "E7", "E9", "F"] 105 | ignore = [] 106 | 107 | # Allow fix for all enabled rules (when `--fix`) is provided. 108 | fixable = ["ALL"] 109 | unfixable = [] 110 | 111 | # Allow unused variables when underscore-prefixed. 112 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" 113 | 114 | [tool.ruff.format] 115 | # Like Black, use double quotes for strings. 116 | quote-style = "double" 117 | 118 | # Like Black, indent with spaces, rather than tabs. 119 | indent-style = "space" 120 | 121 | # Like Black, respect magic trailing commas. 122 | skip-magic-trailing-comma = false 123 | 124 | # Like Black, automatically detect the appropriate line ending. 125 | line-ending = "auto" 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Maya Transfer and Inpaint Skin Weights Plugin 2 | Unofficial implementation of Robust Skin Weights Transfer via Weight Inpainting, out of Epic Games, as Autodesk Maya plugin. 3 | The official implementation has been published and is now available! You can access it here: [[rin-23/RobustSkinWeightsTransferCode](https://github.com/rin-23/RobustSkinWeightsTransferCode)] 4 | 5 | ## Description 6 | This Autodesk Maya plugin introduces a two-stage skin weight transfer process, enhancing precision and artist control in the rigging of diverse garments. By dividing the process, it ensures better results through initial weight transfer for high-confidence areas, followed by artist-guided interpolation for the rest, boosting both efficiency and quality in character design. 7 | 8 | 9 | ## Installation 10 | - Download the [zip](https://github.com/yamahigashi/MayaTransferInpaintWeights/releases/download/v0.0.1/MayaTransferInpaintWeights.zip) file from the [Releases page](https://github.com/yamahigashi/MayaTransferInpaintWeights/releases). 11 | - Unzip the downloaded file. 12 | - Place the unzipped files in a folder that is recognized by the `MAYA_MODULE_PATH`, using one of the following methods: 13 | 14 | ``` 15 | a. Place it in the `MyDocuments\maya\modules` folder within your Documents. 16 | b. Place it in any location and register that location in the system's environment variables. 17 | ``` 18 | 19 | If you are not familiar with handling environment variables, method a. is recommended. Here's a detailed explanation for method a.: 20 | 21 | - Open the My Documents folder. 22 | - If there is no `modules` folder inside the maya folder, create one. 23 | - Place the unzipped files in this newly created folder. 24 | 25 | 26 | 27 | ## Dependencies 28 | - numpy 29 | - scipy 30 | 31 | ```bat 32 | mayapy -m pip install numpy 33 | mayapy -m pip install scipy 34 | ``` 35 | 36 | 37 | ## Usage 38 | 39 | ### startup 40 | 1. Open Autodesk Maya. 41 | 2. Launch the tool, Go to the `Main Menu and select Window > Skin Weight Transfer Inpaint`. 42 | 3. If instructions for installing `numpy` and `scipy` appear, please follow the dialog instructions, open the command prompt, and execute the specified commands. 43 | 4. If already installed, a window will appear. 44 | 45 | ### Operation 46 | The operation within the window is categorized into the following two stages: 47 | 48 | 1. Classify vertices between src/dst into high precision and low precision, i.e., vertices that require inpainting. 49 | 2. Perform inpainting on vertices with low precision or on currently selected vertices. 50 | 51 | ![image](https://github.com/yamahigashi/MayaTransferInpaintWeights/assets/523673/532fb6ef-5289-4939-9bc4-5bc540a30722) 52 | 53 | 54 | ## Citation 55 | If you use this unofficial implementation in your work, please cite the original paper as follows: 56 | ```bibtex 57 | @inproceedings{abdrashitov2023robust, 58 | author = {Abdrashitov, Rinat and Raichstat, Kim and Monsen, Jared and Hill, David}, 59 | title = {Robust Skin Weights Transfer via Weight Inpainting}, 60 | year = {2023}, 61 | isbn = {9798400703140}, 62 | publisher = {Association for Computing Machinery}, 63 | address = {New York, NY, USA}, 64 | url = {https://doi.org/10.1145/3610543.3626180}, 65 | doi = {10.1145/3610543.3626180}, 66 | booktitle = {SIGGRAPH Asia 2023 Technical Communications}, 67 | articleno = {25}, 68 | numpages = {4}, 69 | location = {, Sydney, NSW, Australia, }, 70 | series = {SA '23} 71 | } 72 | 73 | ``` 74 | -------------------------------------------------------------------------------- /.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/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /python/robust_skin_weights_transfer_inpaint/setup.py: -------------------------------------------------------------------------------- 1 | # # -*- coding: utf-8 -*- 2 | import os 3 | import sys 4 | import textwrap 5 | 6 | from maya import ( 7 | cmds, 8 | mel, 9 | ) 10 | 11 | 12 | if sys.version_info > (3, 0): 13 | from typing import TYPE_CHECKING 14 | if TYPE_CHECKING: 15 | from typing import ( 16 | Optional, # noqa: F401 17 | Dict, # noqa: F401 18 | List, # noqa: F401 19 | Tuple, # noqa: F401 20 | Pattern, # noqa: F401 21 | Callable, # noqa: F401 22 | Any, # noqa: F401 23 | Text, # noqa: F401 24 | Generator, # noqa: F401 25 | Union, # noqa: F401 26 | Iterable # noqa: F401 27 | ) 28 | 29 | 30 | ############################################################################## 31 | def register_menu(): 32 | # type: () -> None 33 | """Setup menu""" 34 | 35 | if cmds.about(batch=True): 36 | return 37 | 38 | if not is_default_window_menu_registered(): 39 | register_default_window_menu() 40 | 41 | cmds.setParent("MayaWindow|mainWindowMenu", menu=True) 42 | 43 | cmds.menuItem(divider=True) 44 | item = cmds.menuItem( 45 | "open_skin_weight_transfer_inpaint_window", 46 | label="Skin Weight Transfer Inpaint", 47 | annotation="Show Skin Weight Transfer Inpaint Window", 48 | echoCommand=True, 49 | command=textwrap.dedent( 50 | """ 51 | import robust_skin_weights_transfer_inpaint.setup as setup 52 | if setup.check_dependencies(): 53 | import robust_skin_weights_transfer_inpaint.ui as ui 54 | ui.show() 55 | """) 56 | ) 57 | print("TransferInpaintWeights: register menu item as {}".format(item)) 58 | 59 | 60 | def is_default_window_menu_registered(): 61 | # type: () -> bool 62 | """Check if default Window menu is registered""" 63 | if not cmds.menu("MayaWindow|mainWindowMenu", exists=True): 64 | return False 65 | 66 | kids = cmds.menu("MayaWindow|mainWindowMenu", query=True, itemArray=True) 67 | if not kids: 68 | return False 69 | 70 | if len(kids) == 0: 71 | return False 72 | 73 | return True 74 | 75 | 76 | def register_default_window_menu(): 77 | cmd = ''' 78 | buildViewMenu MayaWindow|mainWindowMenu; 79 | setParent -menu "MayaWindow|mainWindowMenu"; 80 | ''' 81 | 82 | mel.eval(cmd) 83 | 84 | 85 | def deregister_menu(): 86 | # type: () -> None 87 | """Remove menu""" 88 | 89 | if cmds.about(batch=True): 90 | return 91 | 92 | try: 93 | path = "MayaWindow|mainWindowMenu|open_skin_weight_transfer_inpaint_window" 94 | cmds.deleteUI(path, menuItem=True) 95 | 96 | except Exception as e: 97 | import traceback 98 | traceback.print_exc() 99 | print(e) 100 | 101 | 102 | def check_python_version(): 103 | if sys.version_info < (3, 6): 104 | print("Python 3.6 or higher is required.") 105 | sys.exit(1) 106 | 107 | 108 | def check_dependencies(): 109 | executable = os.path.join(os.path.dirname(sys.executable), "mayapy") 110 | is_valid = True 111 | 112 | try: 113 | import numpy 114 | 115 | except ImportError: 116 | is_valid = False 117 | title = "Missing dependency Please install numpy first" 118 | command = '"{}" -m pip install numpy'.format(executable) 119 | message = "Please install numpy first" \ 120 | " by running the following command:\n\n" 121 | 122 | show_error(title, message, command) 123 | 124 | try: 125 | import scipy 126 | except ImportError: 127 | is_valid = False 128 | title = "Missing dependency Please install scipy first" 129 | command = '"{}" -m pip install scipy'.format(executable) 130 | message = "Please install scipy first" \ 131 | " by running the following command in a terminal:\n\n" 132 | 133 | show_error(title, message, command) 134 | 135 | return is_valid 136 | 137 | 138 | def show_error(title, message, command): 139 | import maya.cmds as cmds 140 | cmds.promptDialog( 141 | title=title, 142 | message=message, 143 | text=command, 144 | button="OK", 145 | defaultButton="OK") 146 | -------------------------------------------------------------------------------- /python/robust_skin_weights_transfer_inpaint/ui.py: -------------------------------------------------------------------------------- 1 | """This module contains the UI for the application.""" 2 | import sys 3 | 4 | from maya import cmds 5 | from maya.app.general.mayaMixin import MayaQWidgetBaseMixin 6 | 7 | from .vendor import Qt 8 | from Qt.QtWidgets import ( # type: ignore 9 | QApplication, 10 | QWidget, 11 | QLineEdit, 12 | QPushButton, 13 | QLabel, 14 | QSlider, 15 | QHBoxLayout, 16 | QVBoxLayout, 17 | QRadioButton, 18 | QGroupBox, 19 | QSpacerItem, 20 | QSizePolicy, 21 | ) 22 | 23 | from Qt.QtCore import ( 24 | Qt, 25 | QSize, 26 | ) 27 | 28 | from robust_skin_weights_transfer_inpaint import logic 29 | 30 | 31 | if sys.version_info > (3, 0): 32 | from typing import TYPE_CHECKING 33 | if TYPE_CHECKING: 34 | from typing import ( 35 | Optional, # noqa: F401 36 | Dict, # noqa: F401 37 | List, # noqa: F401 38 | Tuple, # noqa: F401 39 | Pattern, # noqa: F401 40 | Callable, # noqa: F401 41 | Any, # noqa: F401 42 | Text, # noqa: F401 43 | Generator, # noqa: F401 44 | Union # noqa: F401 45 | ) 46 | 47 | 48 | ######################################################################################################################## 49 | 50 | 51 | class FloatSlider(QWidget): 52 | def __init__(self, label, minimum=0.0, maximum=1.0, interval=0.05, step=0.001, initial_value=0.05): 53 | # type: (str, float, float, float, float, float) -> None 54 | 55 | super(FloatSlider, self).__init__() 56 | 57 | self.sizePolicy().setHorizontalPolicy(QSizePolicy.MinimumExpanding) 58 | self.sizePolicy().setVerticalPolicy(QSizePolicy.MinimumExpanding) 59 | self.sizeHint() 60 | 61 | self.minimum = minimum 62 | self.maximum = maximum 63 | self.interval = interval 64 | self.step = step 65 | self.value_multiplier = 1.0 / step 66 | 67 | self.initUI(label, initial_value) 68 | 69 | def initUI(self, label, initial_value): 70 | # Create the slider and the label 71 | self.label = QLabel(label, self) 72 | self.label.setFixedWidth(50) 73 | self.label.setAlignment(Qt.AlignRight) 74 | self.value_display = QLabel("{:.2f}".format(initial_value), self) 75 | self.slider = QSlider(Qt.Horizontal, self) 76 | 77 | # Set the range and step of the slider 78 | self.slider.setMinimum(self.minimum * self.value_multiplier) 79 | self.slider.setMaximum(self.maximum * self.value_multiplier) 80 | self.slider.setTickInterval(self.interval * self.value_multiplier) 81 | self.slider.setSingleStep(self.step * self.value_multiplier) 82 | self.slider.setValue(initial_value * self.value_multiplier) 83 | 84 | # Connect the valueChanged signal to the slot 85 | self.slider.valueChanged.connect(self.updateValueDisplay) 86 | 87 | # Create the layout and add the widgets 88 | layout = QHBoxLayout() 89 | layout.addWidget(self.label) 90 | layout.addWidget(self.slider) 91 | layout.addWidget(self.value_display) 92 | self.setLayout(layout) 93 | 94 | def sizeHint(self): 95 | # type: () -> QSize 96 | """Return the size hint of the widget.""" 97 | return QSize(300, 30) 98 | 99 | def updateValueDisplay(self, value): 100 | # Calculate the float value based on the slider's integer value 101 | float_value = value / self.value_multiplier 102 | self.value_display.setText("{:.2f}".format(float_value)) 103 | 104 | def value(self): 105 | # Get the current float value of the slider 106 | return self.slider.value() / self.value_multiplier 107 | 108 | def setValue(self, float_value): 109 | # Set the value of the slider using a float 110 | self.slider.setValue(int(float_value * self.value_multiplier)) 111 | 112 | 113 | class WeightTransferInpaintMainWidget(MayaQWidgetBaseMixin, QWidget): 114 | 115 | def __init__(self, parent=None): 116 | # type: (QWidget|None) -> None 117 | super(WeightTransferInpaintMainWidget, self).__init__(parent) 118 | 119 | self.unconfident_vertices = [] # type: List[int] 120 | self.initUI() 121 | 122 | def initUI(self): 123 | # type: () -> None 124 | 125 | # grid = QGridLayout() 126 | 127 | # ----------------------------------------------- 128 | # source and destination meshes 129 | self.meshes_group_box = QGroupBox("Meshes", self) 130 | self.src_label = QLabel("src:", self) 131 | self.src_label.setAlignment(Qt.AlignRight) 132 | self.src_label.setFixedWidth(60) 133 | self.src_line_edit = QLineEdit(self) 134 | self.src_line_edit.setReadOnly(True) 135 | self.src_line_edit.setFocusPolicy(Qt.NoFocus) 136 | self.src_button = QPushButton("set", self) 137 | self.src_button.clicked.connect(self.insertText) 138 | 139 | self.dst_label = QLabel("dst:", self) 140 | self.dst_label.setAlignment(Qt.AlignRight) 141 | self.dst_label.setFixedWidth(60) 142 | self.dst_line_edit = QLineEdit(self) 143 | self.dst_line_edit.setReadOnly(True) 144 | self.dst_line_edit.setFocusPolicy(Qt.NoFocus) 145 | self.dst_button = QPushButton("set", self) 146 | self.dst_button.clicked.connect(self.insertText) 147 | 148 | # ----------------------------------------------- 149 | # search settings 150 | self.search_group_box = QGroupBox("Search Settings", self) 151 | self.mode_label = QLabel("mode:", self) 152 | self.mode_label.setAlignment(Qt.AlignRight) 153 | self.mode_label.setFixedWidth(60) 154 | self.dist_slider = FloatSlider("distance:", minimum=0.000001, maximum=1.0, interval=0.1, step=0.005, initial_value=0.05) 155 | self.angle_slider = FloatSlider("angle:", minimum=0.0, maximum=180.0, interval=1.0, step=0.5, initial_value=25.0) 156 | 157 | self.radio_accurate = QRadioButton("Accurate (Slow)", self) 158 | self.radio_fast = QRadioButton("Inaccurate (Fast)", self) 159 | self.radio_fast.setChecked(True) 160 | self.radio_accurate.toggled.connect(self.radioButtonToggled) 161 | self.radio_fast.toggled.connect(self.radioButtonToggled) 162 | 163 | # ----------------------------------------------- 164 | self.vertex_count_label = QLabel("vertex count:", self) 165 | self.vertex_count_label.setAlignment(Qt.AlignRight) 166 | self.vertex_count_label.setFixedWidth(70) 167 | self.confident_count_display = QLabel("0", self) 168 | self.unconfident_count_display = QLabel("0", self) 169 | 170 | # ----------------------------------------------- 171 | self.search_button1 = QPushButton("search", self) 172 | self.search_button1.clicked.connect(self.searchButtonClicked) 173 | 174 | self.select_button1 = QPushButton("select", self) 175 | self.select_button1.clicked.connect(self.selectButtonClicked) 176 | 177 | self.transfer_button1 = QPushButton("transfer", self) 178 | self.transfer_button1.clicked.connect(self.transferButtonClicked) 179 | 180 | self.inpaint_button1 = QPushButton("inpaint", self) 181 | self.inpaint_button1.clicked.connect(self.inpaintButtonClicked) 182 | 183 | # ----------------------------------------------- 184 | # Create layouts 185 | src_layout = QHBoxLayout() 186 | src_layout.addWidget(self.src_label) 187 | src_layout.addWidget(self.src_line_edit) 188 | src_layout.addWidget(self.src_button) 189 | 190 | dst_layout = QHBoxLayout() 191 | dst_layout.addWidget(self.dst_label) 192 | dst_layout.addWidget(self.dst_line_edit) 193 | dst_layout.addWidget(self.dst_button) 194 | 195 | meshes_group_box_layout = QVBoxLayout() 196 | meshes_group_box_layout.addLayout(src_layout) 197 | meshes_group_box_layout.addLayout(dst_layout) 198 | self.meshes_group_box.setLayout(meshes_group_box_layout) 199 | 200 | dist_layout = QHBoxLayout() 201 | dist_layout.addWidget(self.dist_slider) 202 | 203 | angle_layout = QHBoxLayout() 204 | angle_layout.addWidget(self.angle_slider) 205 | 206 | mode_layout = QHBoxLayout() 207 | mode_layout.addWidget(self.mode_label) 208 | mode_layout.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Minimum)) 209 | mode_layout.addWidget(self.radio_accurate) 210 | mode_layout.addWidget(self.radio_fast) 211 | mode_layout.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Minimum)) 212 | 213 | search_group_box_layout = QVBoxLayout() 214 | search_group_box_layout.addLayout(dist_layout) 215 | search_group_box_layout.addLayout(angle_layout) 216 | search_group_box_layout.addLayout(mode_layout) 217 | self.search_group_box.setLayout(search_group_box_layout) 218 | 219 | count_layout = QHBoxLayout() 220 | count_layout.addWidget(self.vertex_count_label) 221 | count_layout.addWidget(QLabel("confident:")) 222 | count_layout.addWidget(self.confident_count_display) 223 | count_layout.addWidget(QLabel("unconfident:")) 224 | count_layout.addWidget(self.unconfident_count_display) 225 | 226 | buttons_layout = QHBoxLayout() 227 | buttons_layout.addWidget(self.search_button1) 228 | buttons_layout.addWidget(self.select_button1) 229 | buttons_layout.addWidget(self.transfer_button1) 230 | buttons_layout.addWidget(self.inpaint_button1) 231 | 232 | # Main layout 233 | main_layout = QVBoxLayout() 234 | main_layout.addWidget(self.meshes_group_box) 235 | main_layout.addWidget(self.search_group_box) 236 | main_layout.addLayout(count_layout) 237 | spacer = QSpacerItem(0, 0, QSizePolicy.Minimum, QSizePolicy.Expanding) 238 | main_layout.addItem(spacer) 239 | main_layout.addLayout(buttons_layout) 240 | 241 | self.setLayout(main_layout) 242 | 243 | # Set window properties 244 | self.setGeometry(300, 300, 350, 250) 245 | self.setWindowTitle("Transfer and Inpainting Skinning Weights") 246 | self.show() 247 | 248 | def insertText(self): 249 | # type: () -> None 250 | """Insert text into the line edit.""" 251 | 252 | selection = cmds.ls(sl=True, objectsOnly=True) 253 | if not selection: 254 | cmds.warning("Nothing is selected") 255 | return 256 | 257 | sel = selection[0] 258 | 259 | sender = self.sender() 260 | if sender == self.src_button: 261 | 262 | if self.src_line_edit.text() != sel: 263 | self.clear() 264 | 265 | self.src_line_edit.setText(sel) 266 | 267 | elif sender == self.dst_button: 268 | 269 | if self.dst_line_edit.text() != sel: 270 | self.clear() 271 | 272 | self.dst_line_edit.setText(sel) 273 | 274 | def clear(self): 275 | # type: () -> None 276 | self.unconfident_vertices = [] 277 | self.confident_count_display.setText("0") 278 | self.unconfident_count_display.setText("0") 279 | 280 | def updateValueDisplay(self): 281 | # type: () -> None 282 | """Update the value display label.""" 283 | 284 | sender = self.sender() 285 | if sender == self.dist_slider: 286 | self.dist_value_display.setText(str(self.dist_slider.value())) 287 | 288 | elif sender == self.angle_slider: 289 | self.angle_value_display.setText(str(self.angle_slider.value())) 290 | 291 | def radioButtonToggled(self): 292 | if self.radio_accurate.isChecked(): 293 | pass 294 | elif self.radio_fast.isChecked(): 295 | pass 296 | 297 | def getSearchMode(self): 298 | # type: () -> str 299 | """Return the search mode.""" 300 | 301 | if self.radio_accurate.isChecked(): 302 | return "accurate" 303 | elif self.radio_fast.isChecked(): 304 | return "fast" 305 | else: 306 | raise RuntimeError("No search mode selected") 307 | 308 | 309 | def searchButtonClicked(self): 310 | # type: () -> None 311 | """Search for vertices to transfer weights from.""" 312 | 313 | src = self.src_line_edit.text() 314 | dst = self.dst_line_edit.text() 315 | dist = self.dist_slider.value() 316 | angle = self.angle_slider.value() 317 | mode = self.getSearchMode() 318 | 319 | use_fast_search = True if mode == "fast" else False 320 | 321 | if not src or not dst: 322 | cmds.warning("Source and destination meshes must be set") 323 | return 324 | 325 | if src == dst: 326 | cmds.warning("Source and destination meshes must be different") 327 | return 328 | 329 | if not cmds.objExists(src) or not cmds.objExists(dst): 330 | cmds.warning("Source and destination meshes must exist") 331 | return 332 | 333 | # Get the vertices to transfer weights from 334 | src_mesh, dst_mesh = logic.load_meshes(src, dst) 335 | tmp = logic.segregate_vertices_by_confidence(src_mesh, dst_mesh, dist, angle, use_fast_search) 336 | confident, self.unconfident_vertices = tmp 337 | 338 | # Update the display 339 | self.confident_count_display.setText(str(len(confident))) 340 | self.unconfident_count_display.setText(str(len(self.unconfident_vertices))) 341 | 342 | def selectButtonClicked(self): 343 | # type: () -> None 344 | """Select the vertices to transfer weights from.""" 345 | 346 | if not self.unconfident_vertices: 347 | cmds.warning("No vertices to select") 348 | return 349 | 350 | dst = self.dst_line_edit.text() 351 | cmds.select(clear=True) 352 | cmds.select(["{}.vtx[{}]".format(dst, v) for v in self.unconfident_vertices], add=True) 353 | 354 | def transferButtonClicked(self): 355 | # type: () -> None 356 | """Transfer weights from the vertices to transfer weights from.""" 357 | 358 | src = self.src_line_edit.text() 359 | dst = self.dst_line_edit.text() 360 | 361 | if not src or not dst: 362 | cmds.warning("Source and destination meshes must be set") 363 | return 364 | 365 | if src == dst: 366 | cmds.warning("Source and destination meshes must be different") 367 | return 368 | 369 | if not cmds.objExists(src) or not cmds.objExists(dst): 370 | cmds.warning("Source and destination meshes must exist") 371 | return 372 | 373 | # Transfer the weights 374 | logic.transfer_weights(src, dst, self.unconfident_vertices) 375 | 376 | def inpaintButtonClicked(self): 377 | # type: () -> None 378 | """Inpaint the vertices to transfer weights from.""" 379 | 380 | force = False 381 | 382 | dst = cmds.ls(sl=True, objectsOnly=True) 383 | if not dst: 384 | cmds.warning("Nothing is selected") 385 | return 386 | dst = dst[0] 387 | 388 | if dst != self.dst_line_edit.text(): 389 | confirm = cmds.confirmDialog( 390 | title="Confirm", 391 | message="Selected mesh does not match destination mesh. Continue?", 392 | button=["Yes", "No"], 393 | defaultButton="Yes", 394 | cancelButton="No", 395 | dismissString="No" 396 | ) 397 | if confirm == "No": 398 | return 399 | else: 400 | force = True 401 | 402 | selection = cmds.ls(sl=True, flatten=True) 403 | vertices = cmds.filterExpand(selection, selectionMask=31, expand=True) 404 | if not vertices: 405 | cmds.warning("No vertices selected") 406 | return 407 | 408 | vertices = [int(v.split("[")[-1].split("]")[0]) for v in vertices if dst in v] 409 | if not force and set(vertices) != set(self.unconfident_vertices): 410 | confirm = cmds.confirmDialog( 411 | title="Confirm", 412 | message="Selected vertices do not match unconfident vertices. Continue?", 413 | button=["Yes", "No"], 414 | defaultButton="Yes", 415 | cancelButton="No", 416 | dismissString="No" 417 | ) 418 | if confirm == "No": 419 | return 420 | 421 | try: 422 | logic.get_skincluster(dst) 423 | except RuntimeError: 424 | src = self.src_line_edit.text() 425 | logic.transfer_weights(src, dst) 426 | 427 | # Inpaint the weights 428 | cmds.progressBar(progress=0, edit=True, beginProgress=True, isInterruptable=True) 429 | logic.inpaint_weights(dst, self.unconfident_vertices) 430 | cmds.progressBar(progress=100, edit=True, endProgress=True) 431 | cmds.inViewMessage(amg="Inpainting complete", pos="topCenter", fade=True, alpha=0.9) 432 | 433 | 434 | def show(): 435 | # type: () -> None 436 | """Show the UI.""" 437 | global MAIN_WIDGET 438 | 439 | # close all previous windows 440 | all_widgets = {w.objectName(): w for w in QApplication.allWidgets()} 441 | for k, v in all_widgets.items(): 442 | if v.__class__.__name__ == "WeightTransferInpaintMainWidget": 443 | v.close() 444 | 445 | main_widget = WeightTransferInpaintMainWidget() 446 | main_widget.show() 447 | 448 | MAIN_WIDGET = main_widget 449 | 450 | 451 | if __name__ == "__main__": 452 | show() 453 | -------------------------------------------------------------------------------- /python/robust_skin_weights_transfer_inpaint/logic.py: -------------------------------------------------------------------------------- 1 | """ 2 | https://github.com/rin-23/RobustSkinWeightsTransferCode 3 | """ 4 | import sys 5 | import time 6 | 7 | from maya import ( 8 | cmds, 9 | ) 10 | 11 | from maya.api import ( 12 | OpenMaya as om, 13 | OpenMayaAnim as oma, 14 | # OpenMayaUI as omui, 15 | ) 16 | 17 | import numpy as np 18 | # import numpy.linalg as nplinalg 19 | import scipy.sparse as sp 20 | import scipy.sparse.linalg as splinalg 21 | from scipy.spatial import cKDTree 22 | 23 | if sys.version_info > (3, 0): 24 | from typing import TYPE_CHECKING 25 | if TYPE_CHECKING: 26 | from typing import ( 27 | Optional, # noqa: F401 28 | Dict, # noqa: F401 29 | List, # noqa: F401 30 | Tuple, # noqa: F401 31 | Pattern, # noqa: F401 32 | Callable, # noqa: F401 33 | Any, # noqa: F401 34 | Text, # noqa: F401 35 | Generator, # noqa: F401 36 | Union # noqa: F401 37 | ) 38 | 39 | 40 | from logging import ( 41 | getLogger, 42 | WARN, # noqa: F401 43 | DEBUG, # noqa: F401 44 | INFO, # noqa: F401 45 | ) 46 | logger = getLogger(__name__) 47 | logger.setLevel(INFO) 48 | 49 | 50 | ######################################################################################################################## 51 | 52 | def timeit(func): 53 | def wrapper(*args, **kwargs): 54 | start_time = time.time() 55 | result = func(*args, **kwargs) 56 | end_time = time.time() 57 | logger.debug("Execution time of {func.__name__}: {elapsed} seconds".format( 58 | func=func, 59 | elapsed=(end_time - start_time) 60 | )) 61 | return result 62 | return wrapper 63 | 64 | 65 | ######################################################################################################################## 66 | def as_selection_list(iterable): 67 | # type: (Union[Text, List[Text]]) -> om.MSelectionList 68 | """Converts an iterable to an MSelectionList.""" 69 | 70 | selection_list = om.MSelectionList() 71 | if isinstance(iterable, (list, tuple)): 72 | for item in iterable: 73 | selection_list.add(item) 74 | else: 75 | selection_list.add(iterable) 76 | 77 | return selection_list 78 | 79 | 80 | def as_dag_path(node): 81 | # type: (Union[Text, om.MObject, om.MDagPath]) -> om.MDagPath 82 | """Converts a node to an MDagPath.""" 83 | 84 | if isinstance(node, om.MDagPath): 85 | return node 86 | 87 | if isinstance(node, om.MObject): 88 | dag_path = om.MDagPath.getAPathTo(node) 89 | return dag_path 90 | 91 | selection_list = as_selection_list(node) 92 | dag_path = selection_list.getDagPath(0) 93 | return dag_path 94 | 95 | 96 | def as_depend_node(node): 97 | # type: (Union[Text, om.MObject, om.MDagPath]) -> om.MFnDependencyNode 98 | """Converts a node to an MFnDependencyNode.""" 99 | 100 | selection_list = as_selection_list(node) 101 | depend_node = om.MFnDependencyNode(selection_list.getDependNode(0)) 102 | 103 | return depend_node 104 | 105 | def as_mfn_mesh(node): 106 | # type: (Union[Text, om.MObject, om.MDagPath]) -> om.MFnMesh 107 | """Converts a node to an MFnMesh.""" 108 | 109 | dag_path = as_dag_path(node) 110 | mfn_mesh = om.MFnMesh(dag_path) 111 | return mfn_mesh 112 | 113 | 114 | def as_mfn_skin_cluster(node): 115 | # type: (Union[Text, om.MObject, om.MDagPath]) -> om.MFnSkinCluster 116 | """Converts a node to an MFnSkinCluster.""" 117 | 118 | dep_node = as_depend_node(node) 119 | mfn_skin_cluster = oma.MFnSkinCluster(dep_node.object()) 120 | return mfn_skin_cluster 121 | 122 | 123 | def load_meshes(source_mesh_name, target_mesh_name): 124 | # type: (str, str) -> Tuple[om.MFnMesh, om.MFnMesh] 125 | """load meshes.""" 126 | 127 | print("Loading meshes...") 128 | 129 | if cmds.objectType(source_mesh_name) == "mesh": 130 | sm = source_mesh_name 131 | else: 132 | sm = cmds.listRelatives(source_mesh_name, shapes=True)[0] 133 | 134 | if cmds.objectType(target_mesh_name) == "mesh": 135 | tm = target_mesh_name 136 | else: 137 | tm = cmds.listRelatives(target_mesh_name, shapes=True)[0] 138 | 139 | source_mesh = as_mfn_mesh(sm) 140 | target_mesh = as_mfn_mesh(tm) 141 | 142 | return source_mesh, target_mesh 143 | ######################################################################################################################## 144 | 145 | 146 | @timeit 147 | def get_vertex_positions_as_numpy_array(mesh): 148 | # type: (om.MFnMesh) -> np.ndarray 149 | """Returns numpy array of vertex positions.""" 150 | 151 | points = mesh.getPoints(om.MSpace.kWorld) # type: ignore 152 | return np.array([[p.x, p.y, p.z] for p in points]) 153 | 154 | 155 | @timeit 156 | def get_vertex_normals_as_numpy_array(mesh): 157 | # type: (om.MFnMesh) -> np.ndarray 158 | """Returns numpy array of vertex normals.""" 159 | 160 | normals = [] 161 | for i in range(mesh.numVertices): 162 | normal = mesh.getVertexNormal(i, om.MSpace.kWorld) # type: ignore 163 | normals.append([normal.x, normal.y, normal.z]) 164 | return np.array(normals) 165 | 166 | 167 | @timeit 168 | def create_vertex_data_array(mesh): 169 | # type: (om.MFnMesh) -> np.ndarray 170 | """Create a structured numpy array containing vertex index, position, and normal.""" 171 | 172 | vertex_data = np.zeros( 173 | mesh.numVertices, 174 | dtype=[ 175 | ("index", np.int64), 176 | ("position", np.float64, 3), 177 | ("normal", np.float64, 3), 178 | ("face_index", np.int64), 179 | ]) 180 | 181 | for i in range(mesh.numVertices): 182 | position = mesh.getPoint(i, om.MSpace.kWorld) # type: ignore 183 | normal = mesh.getVertexNormal(i, om.MSpace.kWorld) # type: ignore 184 | vertex_data[i] = ( 185 | i, 186 | [position.x, position.y, position.z], 187 | [normal.x, normal.y, normal.z], 188 | -1) 189 | 190 | return vertex_data 191 | 192 | 193 | @timeit 194 | def get_closest_points(source_mesh, target_vertex_data): 195 | # type: (om.MFnMesh, np.ndarray) -> np.ndarray 196 | """get closest points and return a structured numpy array similar to target_vertex_data.""" 197 | 198 | closest_points_data = np.zeros(target_vertex_data.shape, dtype=target_vertex_data.dtype) 199 | num_vertices = target_vertex_data.shape[0] 200 | 201 | if not cmds.about(batch=True): 202 | cmds.progressWindow( 203 | title="Finding closest points...", 204 | progress=0, 205 | status="Finding closest points...", 206 | isInterruptable=True, 207 | max=num_vertices, 208 | ) 209 | 210 | for i in range(num_vertices): 211 | target_pos = target_vertex_data[i]["position"] 212 | 213 | # Get closest point on source mesh 214 | try: 215 | tmp = source_mesh.getClosestPointAndNormal(om.MPoint(target_pos), om.MSpace.kWorld) # type: ignore 216 | except RuntimeError: 217 | continue 218 | 219 | closest_point, closest_normal, face_index = tmp 220 | pos = np.array([closest_point.x, closest_point.y, closest_point.z]) 221 | norm = np.array([closest_normal.x, closest_normal.y, closest_normal.z]) 222 | 223 | # Store target vertex index, closest point position, and closest point normal 224 | closest_points_data[i] = ( 225 | target_vertex_data[i]["index"], 226 | pos, 227 | norm, 228 | face_index 229 | ) 230 | 231 | if not cmds.about(batch=True): 232 | cmds.progressWindow(edit=True, step=1) 233 | 234 | if not cmds.about(batch=True): 235 | cmds.progressWindow(edit=True, endProgress=True) 236 | 237 | return closest_points_data 238 | 239 | 240 | @timeit 241 | def get_closest_points_by_kdtree(source_mesh, target_vertex_data): 242 | # type: (om.MFnMesh, np.ndarray) -> np.ndarray 243 | """get closest points and return a structured numpy array similar to target_vertex_data.""" 244 | 245 | source_vertex_data = create_vertex_data_array(source_mesh) 246 | B_positions = np.array([vertex["position"] for vertex in source_vertex_data]) 247 | A_positions = np.array([vertex["position"] for vertex in target_vertex_data]) 248 | 249 | tree = cKDTree(B_positions) 250 | _, indices = tree.query(A_positions) 251 | 252 | nearest_in_B_for_A = source_vertex_data[indices] 253 | return nearest_in_B_for_A 254 | 255 | 256 | @timeit 257 | def filter_high_confidence_matches(target_vertex_data, closest_points_data, max_distance, max_angle): 258 | # type: (np.ndarray, np.ndarray, float, float) -> List[int] 259 | """filter high confidence matches using structured arrays.""" 260 | 261 | target_positions = target_vertex_data["position"] 262 | target_normals = target_vertex_data["normal"] 263 | source_positions = closest_points_data["position"] 264 | source_normals = closest_points_data["normal"] 265 | 266 | # Calculate distances (vectorized) 267 | distances = np.linalg.norm(source_positions - target_positions, axis=1) 268 | 269 | # Calculate angles between normals (vectorized) 270 | cos_angles = np.einsum("ij,ij->i", source_normals, target_normals) 271 | cos_angles /= np.linalg.norm(source_normals, axis=1) * np.linalg.norm(target_normals, axis=1) 272 | cos_angles = np.abs(cos_angles) # Consider opposite normals by taking absolute value 273 | angles = np.arccos(np.clip(cos_angles, -1, 1)) * 180 / np.pi 274 | 275 | # Apply thresholds (vectorized) 276 | high_confidence_indices = np.where((distances <= max_distance) & (angles <= max_angle))[0] 277 | 278 | return high_confidence_indices.tolist() 279 | 280 | 281 | @timeit 282 | def copy_weights_for_confident_matches(source_mesh, target_mesh, confident_vertex_indices, closest_points_data): 283 | # type: (om.MFnMesh, om.MFnMesh, List[int], np.ndarray) -> Dict[int, np.ndarray] 284 | """copy weights for confident matches.""" 285 | 286 | source_skin_cluster_name = get_skincluster(source_mesh.name()) 287 | source_skin_cluster = as_mfn_skin_cluster(source_skin_cluster_name) 288 | deformer_bones = cmds.skinCluster(source_skin_cluster_name, query=True, influence=True) 289 | 290 | target_skin_cluster_name = get_or_create_skincluster(target_mesh.name(), deformer_bones) 291 | target_skin_cluster = as_mfn_skin_cluster(target_skin_cluster_name) 292 | 293 | known_weights = {} # type: Dict[int, np.ndarray] 294 | 295 | # copy weights 296 | for i in confident_vertex_indices: 297 | src_face_index = closest_points_data[i]["face_index"] 298 | point = om.MPoint(closest_points_data[i]["position"]) 299 | if src_face_index < 0: 300 | continue 301 | 302 | weights = get_weights_at_point(source_skin_cluster, source_mesh, src_face_index, point) 303 | 304 | if len(weights) <= 0: 305 | continue 306 | 307 | for j in range(len(weights)): 308 | cmds.setAttr( 309 | "{}.weightList[{}].weights[{}]".format( 310 | target_skin_cluster.name(), 311 | i, 312 | j, 313 | ), 314 | weights[j]) 315 | 316 | known_weights[i] = np.array(weights) 317 | 318 | return known_weights 319 | 320 | 321 | @timeit 322 | def transfer_weights(source_mesh, target_mesh, confident_vertex_indices=None): 323 | # type: (om.MFnMesh|Text, om.MFnMesh|Text, List[int]|None) -> None 324 | """transfer weights for confident matches.""" 325 | 326 | if not isinstance(source_mesh, om.MFnMesh): 327 | source_mesh = as_mfn_mesh(source_mesh) 328 | 329 | if not isinstance(target_mesh, om.MFnMesh): 330 | target_mesh = as_mfn_mesh(target_mesh) 331 | 332 | src_skin_cluster_name = get_skincluster(source_mesh.name()) 333 | src_deformer_bones = cmds.skinCluster(src_skin_cluster_name, query=True, influence=True) 334 | dst_skin_cluster_name = get_or_create_skincluster(target_mesh.name(), src_deformer_bones) 335 | dst_deformer_bones = cmds.skinCluster(src_skin_cluster_name, query=True, influence=True) 336 | 337 | if len(src_deformer_bones) != len(dst_deformer_bones): 338 | cmds.warning("The number of deformer bones is different between source and target meshes.") 339 | cmds.delete(dst_skin_cluster_name) 340 | dst_skin_cluster_name = get_or_create_skincluster(target_mesh.name(), src_deformer_bones) 341 | 342 | # TODO: 343 | # Should we only target confident_vertex_indices? 344 | cmds.copySkinWeights( 345 | sourceSkin=src_skin_cluster_name, 346 | destinationSkin=dst_skin_cluster_name, 347 | noMirror=True, 348 | surfaceAssociation="closestPoint", 349 | influenceAssociation="closestJoint", 350 | ) 351 | 352 | @timeit 353 | def get_skincluster(obj): 354 | # type: (Union[Text, List[Text]]) -> Text 355 | """get skincluster from object.""" 356 | 357 | for history in cmds.listHistory(obj) or []: # type: ignore 358 | obj_type = cmds.objectType(history) 359 | if obj_type == "skinCluster": 360 | return history 361 | 362 | raise RuntimeError("No skinCluster found on target mesh.") 363 | 364 | 365 | @timeit 366 | def get_or_create_skincluster(obj, deformers): 367 | # type: (Union[Text, List[Text]], List[Text]) -> Text 368 | try: 369 | return get_skincluster(obj) 370 | 371 | except RuntimeError: 372 | return cmds.skinCluster( 373 | obj, 374 | deformers, # type: ignore 375 | toSelectedBones=True, 376 | tsb=True, 377 | mi=1, 378 | omi=True, 379 | bm=0, 380 | sm=0, nw=1, wd=0, rui=False, n=obj + "_skinCluster")[0] # type: ignore 381 | 382 | 383 | def get_weights_at_point(skin_cluster, mesh, face_index, closest_point): 384 | # type: (oma.MFnSkinCluster, om.MFnMesh, int, om.MPoint) -> np.ndarray 385 | """Returns weights at a given point.""" 386 | 387 | it_face = om.MItMeshPolygon(mesh.dagPath()) 388 | it_face.setIndex(int(face_index)) 389 | num_vertices = it_face.polygonVertexCount() 390 | 391 | positions = it_face.getPoints(om.MSpace.kWorld) 392 | dastances = [positions[i].distanceTo(closest_point) for i in range(num_vertices)] 393 | total_distance = sum(dastances) 394 | ratios = [dastances[i] / total_distance for i in range(num_vertices)] 395 | reverse_ratios = [1.0 - ratios[i] for i in range(num_vertices)] 396 | reverse_total_normalized = sum(reverse_ratios) 397 | normalized_ratios = [reverse_ratios[i] / reverse_total_normalized for i in range(num_vertices)] 398 | 399 | n_vertices = it_face.polygonVertexCount() 400 | avg_weights = None 401 | 402 | for i in range(n_vertices): 403 | 404 | ratio = normalized_ratios[i] 405 | 406 | vertex_index = it_face.vertexIndex(i) 407 | component = om.MFnSingleIndexedComponent(om.MFnSingleIndexedComponent().create(om.MFn.kMeshVertComponent)) 408 | component.addElement(int(vertex_index)) 409 | raw_weights = skin_cluster.getWeights(mesh.dagPath(), component.object())[0] 410 | weights = np.array(raw_weights) 411 | 412 | if len(weights) <= 0: 413 | continue 414 | 415 | if avg_weights is None: 416 | avg_weights = weights * ratio 417 | 418 | else: 419 | avg_weights += (weights * ratio) 420 | 421 | return np.array(avg_weights) 422 | 423 | 424 | def add_laplacian_entry_in_place(L, tri_positions, tri_indices): 425 | # type: (sp.lil_matrix, np.ndarray, np.ndarray) -> None 426 | """add laplacian entry. 427 | 428 | CAUTION: L is modified in-place. 429 | """ 430 | 431 | i1 = tri_indices[0] 432 | i2 = tri_indices[1] 433 | i3 = tri_indices[2] 434 | 435 | v1 = tri_positions[0] 436 | v2 = tri_positions[1] 437 | v3 = tri_positions[2] 438 | 439 | # calculate cotangent 440 | cotan1 = compute_cotangent(v2, v1, v3) 441 | cotan2 = compute_cotangent(v1, v2, v3) 442 | cotan3 = compute_cotangent(v1, v3, v2) 443 | 444 | # update laplacian matrix 445 | L[i1, i2] += cotan1 # type: ignore 446 | L[i2, i1] += cotan1 # type: ignore 447 | L[i1, i1] -= cotan1 # type: ignore 448 | L[i2, i2] -= cotan1 # type: ignore 449 | 450 | L[i2, i3] += cotan2 # type: ignore 451 | L[i3, i2] += cotan2 # type: ignore 452 | L[i2, i2] -= cotan2 # type: ignore 453 | L[i3, i3] -= cotan2 # type: ignore 454 | 455 | L[i1, i3] += cotan3 # type: ignore 456 | L[i3, i1] += cotan3 # type: ignore 457 | L[i1, i1] -= cotan3 # type: ignore 458 | L[i3, i3] -= cotan3 # type: ignore 459 | 460 | 461 | def add_area_in_place(areas, tri_positions, tri_indices): 462 | # type: (np.ndarray, np.ndarray, np.ndarray) -> None 463 | """add area. 464 | 465 | CAUTION: areas is modified in-place. 466 | """ 467 | 468 | v1 = tri_positions[0] 469 | v2 = tri_positions[1] 470 | v3 = tri_positions[2] 471 | area = 0.5 * np.linalg.norm(np.cross(v2 - v1, v3 - v1)) 472 | 473 | for idx in tri_indices: 474 | areas[idx] += area 475 | 476 | 477 | def compute_laplacian_and_mass_matrix(mesh): 478 | # type: (om.MFnMesh) -> Tuple[sp.csr_array, sp.dia_matrix] 479 | """compute laplacian matrix from mesh. 480 | 481 | treat area as mass matrix. 482 | """ 483 | 484 | # initialize sparse laplacian matrix 485 | n_vertices = mesh.numVertices 486 | L = sp.lil_matrix((n_vertices, n_vertices)) 487 | areas = np.zeros(n_vertices) 488 | 489 | # for each edge and face, calculate the laplacian entry and area 490 | face_iter = om.MItMeshPolygon(mesh.dagPath()) 491 | while not face_iter.isDone(): 492 | 493 | n_tri = face_iter.numTriangles() 494 | 495 | for j in range(n_tri): 496 | 497 | tri_positions, tri_indices = face_iter.getTriangle(j) 498 | add_laplacian_entry_in_place(L, tri_positions, tri_indices) 499 | add_area_in_place(areas, tri_positions, tri_indices) 500 | 501 | face_iter.next() 502 | 503 | L_csr = L.tocsr() 504 | M_csr = sp.diags(areas) 505 | 506 | return L_csr, M_csr 507 | 508 | 509 | def compute_cotangent(v1, v2, v3): 510 | # type: (om.MPoint, om.MPoint, om.MPoint) -> float 511 | """compute cotangent from three points.""" 512 | 513 | edeg1 = v2 - v1 514 | edeg2 = v3 - v1 515 | 516 | norm1 = edeg1 ^ edeg2 517 | 518 | area = norm1.length() 519 | cotan = edeg1 * edeg2 / area 520 | 521 | return cotan 522 | 523 | 524 | def compute_mass_matrix(mesh): 525 | # type: (om.MFnMesh) -> sp.dia_matrix 526 | """Compute the mass matrix for a given mesh. 527 | 528 | This function calculates the mass matrix of a mesh by iterating over its faces. 529 | For each face, it computes the area of the triangle formed by the vertices of the face. 530 | The area is then assigned to the corresponding vertices. 531 | 532 | The mass matrix is represented as a diagonal sparse matrix, where each 533 | diagonal element corresponds to the sum of the areas of all faces 534 | connected to a vertex. 535 | 536 | Parameters: 537 | mesh (om.MFnMesh): The mesh for which the mass matrix is to be computed. 538 | 539 | Returns: 540 | sp.dia_matrix: The diagonal sparse mass matrix, where each diagonal element represents 541 | the total area associated with a vertex. 542 | """ 543 | 544 | n_vertices = mesh.numVertices 545 | areas = np.zeros(n_vertices) 546 | face_iter = om.MItMeshPolygon(mesh.dagPath()) 547 | 548 | while not face_iter.isDone(): 549 | 550 | tri_positions, tri_indices = face_iter.getTriangle(0) 551 | v1 = np.array(tri_positions[0]) 552 | v2 = np.array(tri_positions[1]) 553 | v3 = np.array(tri_positions[2]) 554 | 555 | # calculate area of the current face 556 | area = 0.5 * np.linalg.norm(np.cross(v2 - v1, v3 - v1)) 557 | 558 | # add area to the corresponding vertices 559 | for idx in tri_indices: 560 | areas[idx] += area 561 | 562 | face_iter.next() 563 | 564 | # create sparse diagonal mass matrix 565 | M = sp.diags(areas) 566 | 567 | return M 568 | 569 | 570 | def __do_inpainting(mesh, known_weights): 571 | # type: (om.MFnMesh, Dict[int, np.ndarray]) -> np.ndarray 572 | 573 | L, M = compute_laplacian_and_mass_matrix(mesh) 574 | # Q = -L + L @ sp.diags(np.reciprocal(M.diagonal())) @ L # @ operator is not supported for python 2... 575 | Q = -L + np.dot(L, sp.diags(np.reciprocal(M.diagonal())).dot(L)) 576 | 577 | S_match = np.array(list(known_weights.keys())) 578 | S_nomatch = np.array(list(set(range(mesh.numVertices)) - set(S_match))) 579 | 580 | Q_UU = sp.csr_matrix(Q[np.ix_(S_nomatch, S_nomatch)]) 581 | Q_UI = sp.csr_matrix(Q[np.ix_(S_nomatch, S_match)]) 582 | 583 | num_vertices = mesh.numVertices 584 | num_bones = len(next(iter(known_weights.values()))) 585 | 586 | W = np.zeros((num_vertices, num_bones)) 587 | for i, weights in known_weights.items(): 588 | W[i] = weights 589 | 590 | W_I = W[S_match, :] 591 | W_U = W[S_nomatch, :] 592 | 593 | for bone_idx in range(num_bones): 594 | # b = -Q_UI @ W_I[:, bone_idx] # @ operator is not supported for python 2... 595 | b = -np.dot(Q_UI, W_I[:, bone_idx]) 596 | W_U[:, bone_idx] = splinalg.spsolve(Q_UU, b) 597 | 598 | W[S_nomatch, :] = W_U 599 | 600 | # apply constraints, 601 | 602 | # each element is between 0 and 1 603 | W = np.clip(W, 0.0, 1.0) 604 | 605 | # normalize each row to sum to 1 606 | W = W / W.sum(axis=1, keepdims=True) 607 | 608 | return W 609 | 610 | 611 | def calculate_inpainting(mesh, unknown_vertex_indices): 612 | # type: (om.MFnMesh, List[int]|Tuple[int]) -> np.ndarray 613 | """Inpainting weights for unknown vertices from known vertices.""" 614 | 615 | num_vertices = mesh.numVertices 616 | known_indices = list(set(range(num_vertices)) - set(unknown_vertex_indices)) 617 | skin_cluster_name = get_skincluster(mesh.name()) 618 | skin_cluster = as_mfn_skin_cluster(skin_cluster_name) 619 | 620 | weights, num_deformers = skin_cluster.getWeights(mesh.dagPath(), om.MObject()) 621 | weights_np = np.array(weights) 622 | 623 | known_weights = {} # type: Dict[int, np.ndarray] 624 | for vertex_index in known_indices: 625 | known_weights[vertex_index] = weights_np[vertex_index * num_deformers: (vertex_index + 1) * num_deformers] 626 | 627 | return __do_inpainting(mesh, known_weights) 628 | 629 | 630 | def compute_weights_for_remaining_vertices(target_mesh, known_weights): 631 | # type: (om.MFnMesh, Dict[int, np.ndarray]) -> np.ndarray 632 | """compute weights for remaining vertices.""" 633 | 634 | try: 635 | optimized = __do_inpainting(target_mesh, known_weights) 636 | except Exception as e: 637 | import traceback 638 | traceback.print_exc() 639 | print("Error: {}".format(e)) 640 | raise 641 | 642 | return optimized 643 | 644 | 645 | def apply_weight_inpainting(target_mesh, optimized_weights, unconvinced_vertex_indices): 646 | # type: (om.MFnMesh, np.ndarray, List[int]) -> None 647 | """apply weight inpainting.""" 648 | 649 | target_skin_cluster_name = get_skincluster(target_mesh.name()) 650 | target_skin_cluster = as_mfn_skin_cluster(target_skin_cluster_name) 651 | 652 | for i in unconvinced_vertex_indices: 653 | 654 | weights = optimized_weights[i] 655 | 656 | if len(weights) <= 0: 657 | continue 658 | 659 | for j in range(len(weights)): 660 | cmds.setAttr( 661 | "{}.weightList[{}].weights[{}]".format( 662 | target_skin_cluster.name(), 663 | i, 664 | j, 665 | ), 666 | weights[j]) 667 | 668 | print("Done.") 669 | 670 | 671 | def calculate_threshold_distance(mesh, threadhold_ratio=0.05): 672 | # type: (om.MFnMesh, float) -> float 673 | """Returns dbox * 0.05 674 | 675 | dbox is the target mesh bounding box diagonal length. 676 | """ 677 | 678 | bbox = mesh.boundingBox 679 | bbox_min = bbox.min 680 | bbox_max = bbox.max 681 | bbox_diag = bbox_max - bbox_min 682 | bbox_diag_length = bbox_diag.length() 683 | 684 | threshold_distance = bbox_diag_length * threadhold_ratio 685 | 686 | return threshold_distance 687 | 688 | 689 | def segregate_vertices_by_confidence(src_mesh, dst_mesh, threshold_distance=0.05, threshold_angle=25.0, use_kdtree=False): 690 | # type: (om.MFnMesh|Text, om.MFnMesh|Text, float, float, bool) -> Tuple[List[int], List[int]] 691 | """segregate vertices by confidence.""" 692 | 693 | if not isinstance(src_mesh, om.MFnMesh): 694 | src_mesh = as_mfn_mesh(src_mesh) 695 | 696 | if not isinstance(dst_mesh, om.MFnMesh): 697 | dst_mesh = as_mfn_mesh(dst_mesh) 698 | 699 | threshold_distance = calculate_threshold_distance(dst_mesh, threshold_distance) 700 | target_vertex_data = create_vertex_data_array(dst_mesh) 701 | 702 | if use_kdtree: 703 | closest_points_data = get_closest_points_by_kdtree(src_mesh, target_vertex_data) 704 | else: 705 | closest_points_data = get_closest_points(src_mesh, target_vertex_data) 706 | 707 | confident_vertex_indices = filter_high_confidence_matches(target_vertex_data, closest_points_data, threshold_distance, threshold_angle) 708 | unconvinced_vertex_indices = list(set(range(dst_mesh.numVertices)) - set(confident_vertex_indices)) 709 | 710 | return confident_vertex_indices, unconvinced_vertex_indices 711 | 712 | 713 | def inpaint_weights(target_mesh, indices): 714 | # type: (om.MFnMesh|Text, List[int]) -> None 715 | """apply inpainting for indices.""" 716 | 717 | if not isinstance(target_mesh, om.MFnMesh): 718 | target_mesh = as_mfn_mesh(target_mesh) 719 | 720 | tmp = calculate_inpainting(target_mesh, indices) 721 | apply_weight_inpainting(target_mesh, tmp, indices) 722 | 723 | 724 | def main(): 725 | 726 | # setup 727 | source_mesh, target_mesh = load_meshes(cmds.ls(sl=True)[0], cmds.ls(sl=True)[1]) 728 | tmp = segregate_vertices_by_confidence(source_mesh, target_mesh) 729 | target_vertex_data = create_vertex_data_array(target_mesh) 730 | 731 | # confidence 732 | confident_vertex_indices = tmp[0] 733 | unconvinced_vertex_indices = tmp[1] 734 | 735 | closest_points_data = get_closest_points(source_mesh, target_vertex_data) 736 | known_weights = copy_weights_for_confident_matches(source_mesh, target_mesh, confident_vertex_indices, closest_points_data) 737 | 738 | # inpainting 739 | optimized_weights = compute_weights_for_remaining_vertices(target_mesh, known_weights) 740 | apply_weight_inpainting(target_mesh, optimized_weights, unconvinced_vertex_indices) 741 | 742 | 743 | if __name__ == "__main__": 744 | main() 745 | -------------------------------------------------------------------------------- /python/robust_skin_weights_transfer_inpaint/vendor/Qt.py: -------------------------------------------------------------------------------- 1 | """Minimal Python 2 & 3 shim around all Qt bindings 2 | 3 | DOCUMENTATION 4 | Qt.py was born in the film and visual effects industry to address 5 | the growing need for the development of software capable of running 6 | with more than one flavour of the Qt bindings for Python. 7 | 8 | Supported Binding: PySide, PySide2, PySide6, PyQt4, PyQt5 9 | 10 | 1. Build for one, run with all 11 | 2. Explicit is better than implicit 12 | 3. Support co-existence 13 | 14 | Default resolution order: 15 | - PySide6 16 | - PySide2 17 | - PyQt5 18 | - PySide 19 | - PyQt4 20 | 21 | Usage: 22 | >> import sys 23 | >> from Qt import QtWidgets 24 | >> app = QtWidgets.QApplication(sys.argv) 25 | >> button = QtWidgets.QPushButton("Hello World") 26 | >> button.show() 27 | >> app.exec_() 28 | 29 | All members of PySide2 are mapped from other bindings, should they exist. 30 | If no equivalent member exist, it is excluded from Qt.py and inaccessible. 31 | The idea is to highlight members that exist across all supported binding, 32 | and guarantee that code that runs on one binding runs on all others. 33 | 34 | For more details, visit https://github.com/mottosso/Qt.py 35 | 36 | LICENSE 37 | 38 | See end of file for license (MIT, BSD) information. 39 | 40 | """ 41 | 42 | import os 43 | import sys 44 | import types 45 | import shutil 46 | import importlib 47 | import json 48 | 49 | 50 | __version__ = "1.4.1" 51 | 52 | # Enable support for `from Qt import *` 53 | __all__ = [] 54 | 55 | # Flags from environment variables 56 | QT_VERBOSE = bool(os.getenv("QT_VERBOSE")) 57 | QT_PREFERRED_BINDING_JSON = os.getenv("QT_PREFERRED_BINDING_JSON", "") 58 | QT_PREFERRED_BINDING = os.getenv("QT_PREFERRED_BINDING", "") 59 | QT_SIP_API_HINT = os.getenv("QT_SIP_API_HINT") 60 | 61 | # Reference to Qt.py 62 | Qt = sys.modules[__name__] 63 | Qt.QtCompat = types.ModuleType("QtCompat") 64 | 65 | try: 66 | long 67 | except NameError: 68 | # Python 3 compatibility 69 | long = int 70 | 71 | 72 | """Common members of all bindings 73 | 74 | This is where each member of Qt.py is explicitly defined. 75 | It is based on a "lowest common denominator" of all bindings; 76 | including members found in each of the 4 bindings. 77 | 78 | The "_common_members" dictionary is generated using the 79 | build_membership.sh script. 80 | 81 | """ 82 | 83 | _common_members = { 84 | "QtCore": [ 85 | "QAbstractAnimation", 86 | "QAbstractEventDispatcher", 87 | "QAbstractItemModel", 88 | "QAbstractListModel", 89 | "QAbstractTableModel", 90 | "QAnimationGroup", 91 | "QBasicTimer", 92 | "QBitArray", 93 | "QBuffer", 94 | "QByteArray", 95 | "QByteArrayMatcher", 96 | "QChildEvent", 97 | "QCoreApplication", 98 | "QCryptographicHash", 99 | "QDataStream", 100 | "QDate", 101 | "QDateTime", 102 | "QDir", 103 | "QDirIterator", 104 | "QDynamicPropertyChangeEvent", 105 | "QEasingCurve", 106 | "QElapsedTimer", 107 | "QEvent", 108 | "QEventLoop", 109 | "QFile", 110 | "QFileInfo", 111 | "QFileSystemWatcher", 112 | "QGenericArgument", 113 | "QGenericReturnArgument", 114 | "QItemSelection", 115 | "QItemSelectionRange", 116 | "QIODevice", 117 | "QLibraryInfo", 118 | "QLine", 119 | "QLineF", 120 | "QLocale", 121 | "QMargins", 122 | "QMetaClassInfo", 123 | "QMetaEnum", 124 | "QMetaMethod", 125 | "QMetaObject", 126 | "QMetaProperty", 127 | "QMimeData", 128 | "QModelIndex", 129 | "QMutex", 130 | "QMutexLocker", 131 | "QObject", 132 | "QParallelAnimationGroup", 133 | "QPauseAnimation", 134 | "QPersistentModelIndex", 135 | "QPluginLoader", 136 | "QPoint", 137 | "QPointF", 138 | "QProcess", 139 | "QProcessEnvironment", 140 | "QPropertyAnimation", 141 | "QReadLocker", 142 | "QReadWriteLock", 143 | "QRect", 144 | "QRectF", 145 | "QResource", 146 | "QRunnable", 147 | "QSemaphore", 148 | "QSequentialAnimationGroup", 149 | "QSettings", 150 | "QSignalMapper", 151 | "QSize", 152 | "QSizeF", 153 | "QSocketNotifier", 154 | "QSysInfo", 155 | "QSystemSemaphore", 156 | "QT_TRANSLATE_NOOP", 157 | "QT_TR_NOOP", 158 | "QT_TR_NOOP_UTF8", 159 | "QTemporaryFile", 160 | "QTextBoundaryFinder", 161 | "QTextStream", 162 | "QTextStreamManipulator", 163 | "QThread", 164 | "QThreadPool", 165 | "QTime", 166 | "QTimeLine", 167 | "QTimer", 168 | "QTimerEvent", 169 | "QTranslator", 170 | "QUrl", 171 | "QVariantAnimation", 172 | "QWaitCondition", 173 | "QWriteLocker", 174 | "QXmlStreamAttribute", 175 | "QXmlStreamAttributes", 176 | "QXmlStreamEntityDeclaration", 177 | "QXmlStreamEntityResolver", 178 | "QXmlStreamNamespaceDeclaration", 179 | "QXmlStreamNotationDeclaration", 180 | "QXmlStreamReader", 181 | "QXmlStreamWriter", 182 | "Qt", 183 | "QtMsgType", 184 | "qAbs", 185 | "qAddPostRoutine", 186 | "qCritical", 187 | "qDebug", 188 | "qFatal", 189 | "qFuzzyCompare", 190 | "qIsFinite", 191 | "qIsInf", 192 | "qIsNaN", 193 | "qIsNull", 194 | "qRegisterResourceData", 195 | "qUnregisterResourceData", 196 | "qVersion", 197 | "qWarning", 198 | ], 199 | "QtGui": [ 200 | "QAbstractTextDocumentLayout", 201 | "QActionEvent", 202 | "QBitmap", 203 | "QBrush", 204 | "QClipboard", 205 | "QCloseEvent", 206 | "QColor", 207 | "QConicalGradient", 208 | "QContextMenuEvent", 209 | "QCursor", 210 | "QDesktopServices", 211 | "QDoubleValidator", 212 | "QDrag", 213 | "QDragEnterEvent", 214 | "QDragLeaveEvent", 215 | "QDragMoveEvent", 216 | "QDropEvent", 217 | "QFileOpenEvent", 218 | "QFocusEvent", 219 | "QFont", 220 | "QFontDatabase", 221 | "QFontInfo", 222 | "QFontMetrics", 223 | "QFontMetricsF", 224 | "QGradient", 225 | "QHelpEvent", 226 | "QHideEvent", 227 | "QHoverEvent", 228 | "QIcon", 229 | "QIconDragEvent", 230 | "QIconEngine", 231 | "QImage", 232 | "QImageIOHandler", 233 | "QImageReader", 234 | "QImageWriter", 235 | "QInputEvent", 236 | "QInputMethodEvent", 237 | "QIntValidator", 238 | "QKeyEvent", 239 | "QKeySequence", 240 | "QLinearGradient", 241 | "QMatrix2x2", 242 | "QMatrix2x3", 243 | "QMatrix2x4", 244 | "QMatrix3x2", 245 | "QMatrix3x3", 246 | "QMatrix3x4", 247 | "QMatrix4x2", 248 | "QMatrix4x3", 249 | "QMatrix4x4", 250 | "QMouseEvent", 251 | "QMoveEvent", 252 | "QMovie", 253 | "QPaintDevice", 254 | "QPaintEngine", 255 | "QPaintEngineState", 256 | "QPaintEvent", 257 | "QPainter", 258 | "QPainterPath", 259 | "QPainterPathStroker", 260 | "QPalette", 261 | "QPen", 262 | "QPicture", 263 | "QPixmap", 264 | "QPixmapCache", 265 | "QPolygon", 266 | "QPolygonF", 267 | "QQuaternion", 268 | "QRadialGradient", 269 | "QRegion", 270 | "QResizeEvent", 271 | "QSessionManager", 272 | "QShortcutEvent", 273 | "QShowEvent", 274 | "QStandardItem", 275 | "QStandardItemModel", 276 | "QStatusTipEvent", 277 | "QSyntaxHighlighter", 278 | "QTabletEvent", 279 | "QTextBlock", 280 | "QTextBlockFormat", 281 | "QTextBlockGroup", 282 | "QTextBlockUserData", 283 | "QTextCharFormat", 284 | "QTextCursor", 285 | "QTextDocument", 286 | "QTextDocumentFragment", 287 | "QTextFormat", 288 | "QTextFragment", 289 | "QTextFrame", 290 | "QTextFrameFormat", 291 | "QTextImageFormat", 292 | "QTextInlineObject", 293 | "QTextItem", 294 | "QTextLayout", 295 | "QTextLength", 296 | "QTextLine", 297 | "QTextList", 298 | "QTextListFormat", 299 | "QTextObject", 300 | "QTextObjectInterface", 301 | "QTextOption", 302 | "QTextTable", 303 | "QTextTableCell", 304 | "QTextTableCellFormat", 305 | "QTextTableFormat", 306 | "QTouchEvent", 307 | "QTransform", 308 | "QValidator", 309 | "QVector2D", 310 | "QVector3D", 311 | "QVector4D", 312 | "QWhatsThisClickedEvent", 313 | "QWheelEvent", 314 | "QWindowStateChangeEvent", 315 | "qAlpha", 316 | "qBlue", 317 | "qGray", 318 | "qGreen", 319 | "qIsGray", 320 | "qRed", 321 | "qRgb", 322 | "qRgba" 323 | ], 324 | "QtHelp": [ 325 | "QHelpContentItem", 326 | "QHelpContentModel", 327 | "QHelpContentWidget", 328 | "QHelpEngine", 329 | "QHelpEngineCore", 330 | "QHelpIndexModel", 331 | "QHelpIndexWidget", 332 | "QHelpSearchEngine", 333 | "QHelpSearchQuery", 334 | "QHelpSearchQueryWidget", 335 | "QHelpSearchResultWidget" 336 | ], 337 | "QtNetwork": [ 338 | "QAbstractNetworkCache", 339 | "QAbstractSocket", 340 | "QAuthenticator", 341 | "QHostAddress", 342 | "QHostInfo", 343 | "QLocalServer", 344 | "QLocalSocket", 345 | "QNetworkAccessManager", 346 | "QNetworkAddressEntry", 347 | "QNetworkCacheMetaData", 348 | "QNetworkCookie", 349 | "QNetworkCookieJar", 350 | "QNetworkDiskCache", 351 | "QNetworkInterface", 352 | "QNetworkProxy", 353 | "QNetworkProxyFactory", 354 | "QNetworkProxyQuery", 355 | "QNetworkReply", 356 | "QNetworkRequest", 357 | "QSsl", 358 | "QTcpServer", 359 | "QTcpSocket", 360 | "QUdpSocket" 361 | ], 362 | "QtPrintSupport": [ 363 | "QAbstractPrintDialog", 364 | "QPageSetupDialog", 365 | "QPrintDialog", 366 | "QPrintEngine", 367 | "QPrintPreviewDialog", 368 | "QPrintPreviewWidget", 369 | "QPrinter", 370 | "QPrinterInfo" 371 | ], 372 | "QtSvg": [ 373 | "QSvgGenerator", 374 | "QSvgRenderer" 375 | ], 376 | "QtTest": [ 377 | "QTest" 378 | ], 379 | "QtWidgets": [ 380 | "QAbstractButton", 381 | "QAbstractGraphicsShapeItem", 382 | "QAbstractItemDelegate", 383 | "QAbstractItemView", 384 | "QAbstractScrollArea", 385 | "QAbstractSlider", 386 | "QAbstractSpinBox", 387 | "QAction", 388 | "QApplication", 389 | "QBoxLayout", 390 | "QButtonGroup", 391 | "QCalendarWidget", 392 | "QCheckBox", 393 | "QColorDialog", 394 | "QColumnView", 395 | "QComboBox", 396 | "QCommandLinkButton", 397 | "QCommonStyle", 398 | "QCompleter", 399 | "QDataWidgetMapper", 400 | "QDateEdit", 401 | "QDateTimeEdit", 402 | "QDial", 403 | "QDialog", 404 | "QDialogButtonBox", 405 | "QDockWidget", 406 | "QDoubleSpinBox", 407 | "QErrorMessage", 408 | "QFileDialog", 409 | "QFileIconProvider", 410 | "QFileSystemModel", 411 | "QFocusFrame", 412 | "QFontComboBox", 413 | "QFontDialog", 414 | "QFormLayout", 415 | "QFrame", 416 | "QGesture", 417 | "QGestureEvent", 418 | "QGestureRecognizer", 419 | "QGraphicsAnchor", 420 | "QGraphicsAnchorLayout", 421 | "QGraphicsBlurEffect", 422 | "QGraphicsColorizeEffect", 423 | "QGraphicsDropShadowEffect", 424 | "QGraphicsEffect", 425 | "QGraphicsEllipseItem", 426 | "QGraphicsGridLayout", 427 | "QGraphicsItem", 428 | "QGraphicsItemGroup", 429 | "QGraphicsLayout", 430 | "QGraphicsLayoutItem", 431 | "QGraphicsLineItem", 432 | "QGraphicsLinearLayout", 433 | "QGraphicsObject", 434 | "QGraphicsOpacityEffect", 435 | "QGraphicsPathItem", 436 | "QGraphicsPixmapItem", 437 | "QGraphicsPolygonItem", 438 | "QGraphicsProxyWidget", 439 | "QGraphicsRectItem", 440 | "QGraphicsRotation", 441 | "QGraphicsScale", 442 | "QGraphicsScene", 443 | "QGraphicsSceneContextMenuEvent", 444 | "QGraphicsSceneDragDropEvent", 445 | "QGraphicsSceneEvent", 446 | "QGraphicsSceneHelpEvent", 447 | "QGraphicsSceneHoverEvent", 448 | "QGraphicsSceneMouseEvent", 449 | "QGraphicsSceneMoveEvent", 450 | "QGraphicsSceneResizeEvent", 451 | "QGraphicsSceneWheelEvent", 452 | "QGraphicsSimpleTextItem", 453 | "QGraphicsTextItem", 454 | "QGraphicsTransform", 455 | "QGraphicsView", 456 | "QGraphicsWidget", 457 | "QGridLayout", 458 | "QGroupBox", 459 | "QHBoxLayout", 460 | "QHeaderView", 461 | "QInputDialog", 462 | "QItemDelegate", 463 | "QItemEditorCreatorBase", 464 | "QItemEditorFactory", 465 | "QLCDNumber", 466 | "QLabel", 467 | "QLayout", 468 | "QLayoutItem", 469 | "QLineEdit", 470 | "QListView", 471 | "QListWidget", 472 | "QListWidgetItem", 473 | "QMainWindow", 474 | "QMdiArea", 475 | "QMdiSubWindow", 476 | "QMenu", 477 | "QMenuBar", 478 | "QMessageBox", 479 | "QPanGesture", 480 | "QPinchGesture", 481 | "QPlainTextDocumentLayout", 482 | "QPlainTextEdit", 483 | "QProgressBar", 484 | "QProgressDialog", 485 | "QPushButton", 486 | "QRadioButton", 487 | "QRubberBand", 488 | "QScrollArea", 489 | "QScrollBar", 490 | "QSizeGrip", 491 | "QSizePolicy", 492 | "QSlider", 493 | "QSpacerItem", 494 | "QSpinBox", 495 | "QSplashScreen", 496 | "QSplitter", 497 | "QSplitterHandle", 498 | "QStackedLayout", 499 | "QStackedWidget", 500 | "QStatusBar", 501 | "QStyle", 502 | "QStyleFactory", 503 | "QStyleHintReturn", 504 | "QStyleHintReturnMask", 505 | "QStyleHintReturnVariant", 506 | "QStyleOption", 507 | "QStyleOptionButton", 508 | "QStyleOptionComboBox", 509 | "QStyleOptionComplex", 510 | "QStyleOptionDockWidget", 511 | "QStyleOptionFocusRect", 512 | "QStyleOptionFrame", 513 | "QStyleOptionGraphicsItem", 514 | "QStyleOptionGroupBox", 515 | "QStyleOptionHeader", 516 | "QStyleOptionMenuItem", 517 | "QStyleOptionProgressBar", 518 | "QStyleOptionRubberBand", 519 | "QStyleOptionSizeGrip", 520 | "QStyleOptionSlider", 521 | "QStyleOptionSpinBox", 522 | "QStyleOptionTab", 523 | "QStyleOptionTabBarBase", 524 | "QStyleOptionTabWidgetFrame", 525 | "QStyleOptionTitleBar", 526 | "QStyleOptionToolBar", 527 | "QStyleOptionToolBox", 528 | "QStyleOptionToolButton", 529 | "QStyleOptionViewItem", 530 | "QStylePainter", 531 | "QStyledItemDelegate", 532 | "QSwipeGesture", 533 | "QSystemTrayIcon", 534 | "QTabBar", 535 | "QTabWidget", 536 | "QTableView", 537 | "QTableWidget", 538 | "QTableWidgetItem", 539 | "QTableWidgetSelectionRange", 540 | "QTapAndHoldGesture", 541 | "QTapGesture", 542 | "QTextBrowser", 543 | "QTextEdit", 544 | "QTimeEdit", 545 | "QToolBar", 546 | "QToolBox", 547 | "QToolButton", 548 | "QToolTip", 549 | "QTreeView", 550 | "QTreeWidget", 551 | "QTreeWidgetItem", 552 | "QTreeWidgetItemIterator", 553 | "QUndoView", 554 | "QVBoxLayout", 555 | "QWhatsThis", 556 | "QWidget", 557 | "QWidgetAction", 558 | "QWidgetItem", 559 | "QWizard", 560 | "QWizardPage" 561 | ], 562 | "QtXml": [ 563 | "QDomAttr", 564 | "QDomCDATASection", 565 | "QDomCharacterData", 566 | "QDomComment", 567 | "QDomDocument", 568 | "QDomDocumentFragment", 569 | "QDomDocumentType", 570 | "QDomElement", 571 | "QDomEntity", 572 | "QDomEntityReference", 573 | "QDomImplementation", 574 | "QDomNamedNodeMap", 575 | "QDomNode", 576 | "QDomNodeList", 577 | "QDomNotation", 578 | "QDomProcessingInstruction", 579 | "QDomText" 580 | ] 581 | } 582 | 583 | """ Missing members 584 | 585 | This mapping describes members that have been deprecated 586 | in one or more bindings and have been left out of the 587 | _common_members mapping. 588 | 589 | The member can provide an extra details string to be 590 | included in exceptions and warnings. 591 | """ 592 | 593 | _missing_members = { 594 | "QtGui": { 595 | "QMatrix": "Deprecated in PyQt5", 596 | }, 597 | } 598 | 599 | 600 | def _qInstallMessageHandler(handler): 601 | """Install a message handler that works in all bindings 602 | 603 | Args: 604 | handler: A function that takes 3 arguments, or None 605 | """ 606 | def messageOutputHandler(*args): 607 | # In Qt4 bindings, message handlers are passed 2 arguments 608 | # In Qt5 bindings, message handlers are passed 3 arguments 609 | # The first argument is a QtMsgType 610 | # The last argument is the message to be printed 611 | # The Middle argument (if passed) is a QMessageLogContext 612 | if len(args) == 3: 613 | msgType, logContext, msg = args 614 | elif len(args) == 2: 615 | msgType, msg = args 616 | logContext = None 617 | else: 618 | raise TypeError( 619 | "handler expected 2 or 3 arguments, got {0}".format(len(args))) 620 | 621 | if isinstance(msg, bytes): 622 | # In python 3, some bindings pass a bytestring, which cannot be 623 | # used elsewhere. Decoding a python 2 or 3 bytestring object will 624 | # consistently return a unicode object. 625 | msg = msg.decode() 626 | 627 | handler(msgType, logContext, msg) 628 | 629 | passObject = messageOutputHandler if handler else handler 630 | if Qt.IsPySide or Qt.IsPyQt4: 631 | return Qt._QtCore.qInstallMsgHandler(passObject) 632 | elif Qt.IsPySide2 or Qt.IsPyQt5 or Qt.IsPySide6: 633 | return Qt._QtCore.qInstallMessageHandler(passObject) 634 | 635 | 636 | def _getcpppointer(object): 637 | if hasattr(Qt, "_shiboken6"): 638 | return getattr(Qt, "_shiboken6").getCppPointer(object)[0] 639 | elif hasattr(Qt, "_shiboken2"): 640 | return getattr(Qt, "_shiboken2").getCppPointer(object)[0] 641 | elif hasattr(Qt, "_shiboken"): 642 | return getattr(Qt, "_shiboken").getCppPointer(object)[0] 643 | elif hasattr(Qt, "_sip"): 644 | return getattr(Qt, "_sip").unwrapinstance(object) 645 | raise AttributeError("'module' has no attribute 'getCppPointer'") 646 | 647 | 648 | def _wrapinstance(ptr, base=None): 649 | """Enable implicit cast of pointer to most suitable class 650 | 651 | This behaviour is available in sip per default. 652 | 653 | Based on http://nathanhorne.com/pyqtpyside-wrap-instance 654 | 655 | Usage: 656 | This mechanism kicks in under these circumstances. 657 | 1. Qt.py is using PySide 1 or 2. 658 | 2. A `base` argument is not provided. 659 | 660 | See :func:`QtCompat.wrapInstance()` 661 | 662 | Arguments: 663 | ptr (long): Pointer to QObject in memory 664 | base (QObject, optional): Base class to wrap with. Defaults to QObject, 665 | which should handle anything. 666 | 667 | """ 668 | 669 | assert isinstance(ptr, long), "Argument 'ptr' must be of type " 670 | assert (base is None) or issubclass(base, Qt.QtCore.QObject), ( 671 | "Argument 'base' must be of type ") 672 | 673 | if Qt.IsPyQt4 or Qt.IsPyQt5: 674 | func = getattr(Qt, "_sip").wrapinstance 675 | elif Qt.IsPySide2: 676 | func = getattr(Qt, "_shiboken2").wrapInstance 677 | elif Qt.IsPySide6: 678 | func = getattr(Qt, "_shiboken6").wrapInstance 679 | elif Qt.IsPySide: 680 | func = getattr(Qt, "_shiboken").wrapInstance 681 | else: 682 | raise AttributeError("'module' has no attribute 'wrapInstance'") 683 | 684 | if base is None: 685 | if Qt.IsPyQt4 or Qt.IsPyQt5: 686 | base = Qt.QtCore.QObject 687 | else: 688 | q_object = func(long(ptr), Qt.QtCore.QObject) 689 | meta_object = q_object.metaObject() 690 | 691 | while True: 692 | class_name = meta_object.className() 693 | 694 | try: 695 | base = getattr(Qt.QtWidgets, class_name) 696 | except AttributeError: 697 | try: 698 | base = getattr(Qt.QtCore, class_name) 699 | except AttributeError: 700 | meta_object = meta_object.superClass() 701 | continue 702 | 703 | break 704 | 705 | return func(long(ptr), base) 706 | 707 | 708 | def _isvalid(object): 709 | """Check if the object is valid to use in Python runtime. 710 | 711 | Usage: 712 | See :func:`QtCompat.isValid()` 713 | 714 | Arguments: 715 | object (QObject): QObject to check the validity of. 716 | 717 | """ 718 | if hasattr(Qt, "_shiboken6"): 719 | return getattr(Qt, "_shiboken6").isValid(object) 720 | 721 | elif hasattr(Qt, "_shiboken2"): 722 | return getattr(Qt, "_shiboken2").isValid(object) 723 | 724 | elif hasattr(Qt, "_shiboken"): 725 | return getattr(Qt, "_shiboken").isValid(object) 726 | 727 | elif hasattr(Qt, "_sip"): 728 | return not getattr(Qt, "_sip").isdeleted(object) 729 | 730 | else: 731 | raise AttributeError("'module' has no attribute isValid") 732 | 733 | 734 | def _translate(context, sourceText, *args): 735 | # In Qt4 bindings, translate can be passed 2 or 3 arguments 736 | # In Qt5 bindings, translate can be passed 2 arguments 737 | # The first argument is disambiguation[str] 738 | # The last argument is n[int] 739 | # The middle argument can be encoding[QtCore.QCoreApplication.Encoding] 740 | try: 741 | app = Qt.QtCore.QCoreApplication 742 | except AttributeError: 743 | raise NotImplementedError( 744 | "Missing QCoreApplication implementation for {}".format( 745 | Qt.__binding__ 746 | ) 747 | ) 748 | 749 | def get_arg(index): 750 | try: 751 | return args[index] 752 | except IndexError: 753 | pass 754 | 755 | n = -1 756 | encoding = None 757 | 758 | if len(args) == 3: 759 | disambiguation, encoding, n = args 760 | else: 761 | disambiguation = get_arg(0) 762 | n_or_encoding = get_arg(1) 763 | 764 | if isinstance(n_or_encoding, int): 765 | n = n_or_encoding 766 | else: 767 | encoding = n_or_encoding 768 | 769 | if Qt.__binding__ in ("PySide2", "PySide6","PyQt5"): 770 | sanitized_args = [context, sourceText, disambiguation, n] 771 | else: 772 | sanitized_args = [ 773 | context, 774 | sourceText, 775 | disambiguation, 776 | encoding or app.CodecForTr, 777 | n, 778 | ] 779 | 780 | return app.translate(*sanitized_args) 781 | 782 | 783 | def _loadUi(uifile, baseinstance=None): 784 | """Dynamically load a user interface from the given `uifile` 785 | 786 | This function calls `uic.loadUi` if using PyQt bindings, 787 | else it implements a comparable binding for PySide. 788 | 789 | Documentation: 790 | http://pyqt.sourceforge.net/Docs/PyQt5/designer.html#PyQt5.uic.loadUi 791 | 792 | Arguments: 793 | uifile (str): Absolute path to Qt Designer file. 794 | baseinstance (QWidget): Instantiated QWidget or subclass thereof 795 | 796 | Return: 797 | baseinstance if `baseinstance` is not `None`. Otherwise 798 | return the newly created instance of the user interface. 799 | 800 | """ 801 | if hasattr(Qt, "_uic"): 802 | return Qt._uic.loadUi(uifile, baseinstance) 803 | 804 | elif hasattr(Qt, "_QtUiTools"): 805 | # Implement `PyQt5.uic.loadUi` for PySide(2) 806 | 807 | class _UiLoader(Qt._QtUiTools.QUiLoader): 808 | """Create the user interface in a base instance. 809 | 810 | Unlike `Qt._QtUiTools.QUiLoader` itself this class does not 811 | create a new instance of the top-level widget, but creates the user 812 | interface in an existing instance of the top-level class if needed. 813 | 814 | This mimics the behaviour of `PyQt5.uic.loadUi`. 815 | 816 | """ 817 | 818 | def __init__(self, baseinstance): 819 | super(_UiLoader, self).__init__(baseinstance) 820 | self.baseinstance = baseinstance 821 | self.custom_widgets = {} 822 | 823 | def _loadCustomWidgets(self, etree): 824 | """ 825 | Workaround to pyside-77 bug. 826 | 827 | From QUiLoader doc we should use registerCustomWidget method. 828 | But this causes a segfault on some platforms. 829 | 830 | Instead we fetch from customwidgets DOM node the python class 831 | objects. Then we can directly use them in createWidget method. 832 | """ 833 | 834 | def headerToModule(header): 835 | """ 836 | Translate a header file to python module path 837 | foo/bar.h => foo.bar 838 | """ 839 | # Remove header extension 840 | module = os.path.splitext(header)[0] 841 | 842 | # Replace os separator by python module separator 843 | return module.replace("/", ".").replace("\\", ".") 844 | 845 | custom_widgets = etree.find("customwidgets") 846 | 847 | if custom_widgets is None: 848 | return 849 | 850 | for custom_widget in custom_widgets: 851 | class_name = custom_widget.find("class").text 852 | header = custom_widget.find("header").text 853 | 854 | try: 855 | # try to import the module using the header as defined by the user 856 | module = importlib.import_module(header) 857 | except ImportError: 858 | # try again, but use the customized conversion of a path to a module 859 | module = importlib.import_module(headerToModule(header)) 860 | 861 | self.custom_widgets[class_name] = getattr(module, 862 | class_name) 863 | 864 | def load(self, uifile, *args, **kwargs): 865 | from xml.etree.ElementTree import ElementTree 866 | 867 | # For whatever reason, if this doesn't happen then 868 | # reading an invalid or non-existing .ui file throws 869 | # a RuntimeError. 870 | etree = ElementTree() 871 | etree.parse(uifile) 872 | self._loadCustomWidgets(etree) 873 | 874 | widget = Qt._QtUiTools.QUiLoader.load( 875 | self, uifile, *args, **kwargs) 876 | 877 | # Workaround for PySide 1.0.9, see issue #208 878 | widget.parentWidget() 879 | 880 | return widget 881 | 882 | def createWidget(self, class_name, parent=None, name=""): 883 | """Called for each widget defined in ui file 884 | 885 | Overridden here to populate `baseinstance` instead. 886 | 887 | """ 888 | 889 | if parent is None and self.baseinstance: 890 | # Supposed to create the top-level widget, 891 | # return the base instance instead 892 | return self.baseinstance 893 | 894 | # For some reason, Line is not in the list of available 895 | # widgets, but works fine, so we have to special case it here. 896 | if class_name in self.availableWidgets() + ["Line"]: 897 | # Create a new widget for child widgets 898 | widget = Qt._QtUiTools.QUiLoader.createWidget(self, 899 | class_name, 900 | parent, 901 | name) 902 | elif class_name in self.custom_widgets: 903 | widget = self.custom_widgets[class_name](parent=parent) 904 | else: 905 | raise Exception("Custom widget '%s' not supported" 906 | % class_name) 907 | 908 | if self.baseinstance: 909 | # Set an attribute for the new child widget on the base 910 | # instance, just like PyQt5.uic.loadUi does. 911 | setattr(self.baseinstance, name, widget) 912 | 913 | return widget 914 | 915 | widget = _UiLoader(baseinstance).load(uifile) 916 | Qt.QtCore.QMetaObject.connectSlotsByName(widget) 917 | 918 | return widget 919 | 920 | else: 921 | raise NotImplementedError("No implementation available for loadUi") 922 | 923 | 924 | """Misplaced members 925 | 926 | These members from the original submodule are misplaced relative PySide2 927 | 928 | NOTE: For bindings where a member is not replaced, they still 929 | need to be added such that they are added to Qt.py 930 | 931 | """ 932 | _misplaced_members = { 933 | "PySide6": { 934 | "QtGui.QUndoCommand": "QtWidgets.QUndoCommand", 935 | "QtGui.QUndoGroup": "QtWidgets.QUndoGroup", 936 | "QtGui.QUndoStack": "QtWidgets.QUndoStack", 937 | "QtGui.QActionGroup": "QtWidgets.QActionGroup", 938 | "QtCore.QStringListModel": "QtCore.QStringListModel", 939 | "QtGui.QStringListModel": "QtCore.QStringListModel", 940 | "QtCore.Property": "QtCore.Property", 941 | "QtCore.Signal": "QtCore.Signal", 942 | "QtCore.Slot": "QtCore.Slot", 943 | "QtCore.QAbstractProxyModel": "QtCore.QAbstractProxyModel", 944 | "QtCore.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel", 945 | "QtCore.QItemSelection": "QtCore.QItemSelection", 946 | "QtCore.QItemSelectionModel": "QtCore.QItemSelectionModel", 947 | "QtCore.QItemSelectionRange": "QtCore.QItemSelectionRange", 948 | "QtCore.QRegularExpression": "QtCore.QRegExp", 949 | "QtStateMachine.QStateMachine": "QtCore.QStateMachine", 950 | "QtStateMachine.QState": "QtCore.QState", 951 | "QtGui.QRegularExpressionValidator": "QtGui.QRegExpValidator", 952 | "QtGui.QShortcut": "QtWidgets.QShortcut", 953 | "QtGui.QAction": "QtWidgets.QAction", 954 | "QtUiTools.QUiLoader": ["QtCompat.loadUi", _loadUi], 955 | "shiboken6.wrapInstance": ["QtCompat.wrapInstance", _wrapinstance], 956 | "shiboken6.getCppPointer": ["QtCompat.getCppPointer", _getcpppointer], 957 | "shiboken6.isValid": ["QtCompat.isValid", _isvalid], 958 | "QtWidgets.qApp": "QtWidgets.QApplication.instance()", 959 | "QtCore.QCoreApplication.translate": [ 960 | "QtCompat.translate", _translate 961 | ], 962 | "QtWidgets.QApplication.translate": [ 963 | "QtCompat.translate", _translate 964 | ], 965 | "QtCore.qInstallMessageHandler": [ 966 | "QtCompat.qInstallMessageHandler", _qInstallMessageHandler 967 | ], 968 | "QtWidgets.QStyleOptionViewItem": "QtCompat.QStyleOptionViewItemV4", 969 | }, 970 | "PySide2": { 971 | "QtWidgets.QUndoCommand": "QtWidgets.QUndoCommand", 972 | "QtWidgets.QUndoGroup": "QtWidgets.QUndoGroup", 973 | "QtWidgets.QUndoStack": "QtWidgets.QUndoStack", 974 | "QtWidgets.QActionGroup": "QtWidgets.QActionGroup", 975 | "QtCore.QStringListModel": "QtCore.QStringListModel", 976 | 977 | # Older versions of PySide2 still left this in QtGui, this accounts for those too 978 | "QtGui.QStringListModel": "QtCore.QStringListModel", 979 | 980 | "QtCore.Property": "QtCore.Property", 981 | "QtCore.Signal": "QtCore.Signal", 982 | "QtCore.Slot": "QtCore.Slot", 983 | "QtCore.QRegExp": "QtCore.QRegExp", 984 | "QtWidgets.QShortcut": "QtWidgets.QShortcut", 985 | "QtGui.QRegExpValidator": "QtGui.QRegExpValidator", 986 | "QtCore.QAbstractProxyModel": "QtCore.QAbstractProxyModel", 987 | "QtCore.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel", 988 | "QtCore.QItemSelection": "QtCore.QItemSelection", 989 | "QtCore.QItemSelectionModel": "QtCore.QItemSelectionModel", 990 | "QtCore.QItemSelectionRange": "QtCore.QItemSelectionRange", 991 | "QtUiTools.QUiLoader": ["QtCompat.loadUi", _loadUi], 992 | "shiboken2.wrapInstance": ["QtCompat.wrapInstance", _wrapinstance], 993 | "shiboken2.getCppPointer": ["QtCompat.getCppPointer", _getcpppointer], 994 | "shiboken2.isValid": ["QtCompat.isValid", _isvalid], 995 | "QtWidgets.qApp": "QtWidgets.QApplication.instance()", 996 | "QtCore.QCoreApplication.translate": [ 997 | "QtCompat.translate", _translate 998 | ], 999 | "QtWidgets.QApplication.translate": [ 1000 | "QtCompat.translate", _translate 1001 | ], 1002 | "QtCore.qInstallMessageHandler": [ 1003 | "QtCompat.qInstallMessageHandler", _qInstallMessageHandler 1004 | ], 1005 | "QtWidgets.QStyleOptionViewItem": "QtCompat.QStyleOptionViewItemV4", 1006 | }, 1007 | "PyQt5": { 1008 | "QtWidgets.QUndoCommand": "QtWidgets.QUndoCommand", 1009 | "QtWidgets.QUndoGroup": "QtWidgets.QUndoGroup", 1010 | "QtWidgets.QUndoStack": "QtWidgets.QUndoStack", 1011 | "QtWidgets.QActionGroup": "QtWidgets.QActionGroup", 1012 | "QtCore.pyqtProperty": "QtCore.Property", 1013 | "QtCore.pyqtSignal": "QtCore.Signal", 1014 | "QtCore.pyqtSlot": "QtCore.Slot", 1015 | "uic.loadUi": ["QtCompat.loadUi", _loadUi], 1016 | "sip.wrapinstance": ["QtCompat.wrapInstance", _wrapinstance], 1017 | "sip.unwrapinstance": ["QtCompat.getCppPointer", _getcpppointer], 1018 | "sip.isdeleted": ["QtCompat.isValid", _isvalid], 1019 | "QtWidgets.qApp": "QtWidgets.QApplication.instance()", 1020 | "QtGui.QRegExpValidator": "QtGui.QRegExpValidator", 1021 | "QtCore.QRegExp": "QtCore.QRegExp", 1022 | "QtCore.QCoreApplication.translate": [ 1023 | "QtCompat.translate", _translate 1024 | ], 1025 | "QtWidgets.QApplication.translate": [ 1026 | "QtCompat.translate", _translate 1027 | ], 1028 | "QtCore.qInstallMessageHandler": [ 1029 | "QtCompat.qInstallMessageHandler", _qInstallMessageHandler 1030 | ], 1031 | "QtWidgets.QShortcut": "QtWidgets.QShortcut", 1032 | "QtWidgets.QStyleOptionViewItem": "QtCompat.QStyleOptionViewItemV4", 1033 | }, 1034 | "PySide": { 1035 | "QtGui.QUndoCommand": "QtWidgets.QUndoCommand", 1036 | "QtGui.QUndoGroup": "QtWidgets.QUndoGroup", 1037 | "QtGui.QUndoStack": "QtWidgets.QUndoStack", 1038 | "QtGui.QActionGroup": "QtWidgets.QActionGroup", 1039 | "QtCore.Property": "QtCore.Property", 1040 | "QtCore.Signal": "QtCore.Signal", 1041 | "QtCore.Slot": "QtCore.Slot", 1042 | "QtGui.QAbstractProxyModel": "QtCore.QAbstractProxyModel", 1043 | "QtGui.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel", 1044 | "QtGui.QStringListModel": "QtCore.QStringListModel", 1045 | "QtGui.QItemSelection": "QtCore.QItemSelection", 1046 | "QtGui.QItemSelectionModel": "QtCore.QItemSelectionModel", 1047 | "QtGui.QItemSelectionRange": "QtCore.QItemSelectionRange", 1048 | "QtGui.QAbstractPrintDialog": "QtPrintSupport.QAbstractPrintDialog", 1049 | "QtGui.QRegExpValidator": "QtGui.QRegExpValidator", 1050 | "QtGui.QPageSetupDialog": "QtPrintSupport.QPageSetupDialog", 1051 | "QtGui.QPrintDialog": "QtPrintSupport.QPrintDialog", 1052 | "QtGui.QPrintEngine": "QtPrintSupport.QPrintEngine", 1053 | "QtGui.QPrintPreviewDialog": "QtPrintSupport.QPrintPreviewDialog", 1054 | "QtGui.QPrintPreviewWidget": "QtPrintSupport.QPrintPreviewWidget", 1055 | "QtGui.QPrinter": "QtPrintSupport.QPrinter", 1056 | "QtWidgets.QShortcut": "QtWidgets.QShortcut", 1057 | "QtGui.QPrinterInfo": "QtPrintSupport.QPrinterInfo", 1058 | "QtUiTools.QUiLoader": ["QtCompat.loadUi", _loadUi], 1059 | "shiboken.wrapInstance": ["QtCompat.wrapInstance", _wrapinstance], 1060 | "shiboken.unwrapInstance": ["QtCompat.getCppPointer", _getcpppointer], 1061 | "shiboken.isValid": ["QtCompat.isValid", _isvalid], 1062 | "QtGui.qApp": "QtWidgets.QApplication.instance()", 1063 | "QtCore.QRegExp": "QtCore.QRegExp", 1064 | "QtCore.QCoreApplication.translate": [ 1065 | "QtCompat.translate", _translate 1066 | ], 1067 | "QtGui.QApplication.translate": [ 1068 | "QtCompat.translate", _translate 1069 | ], 1070 | "QtCore.qInstallMsgHandler": [ 1071 | "QtCompat.qInstallMessageHandler", _qInstallMessageHandler 1072 | ], 1073 | "QtGui.QStyleOptionViewItemV4": "QtCompat.QStyleOptionViewItemV4", 1074 | }, 1075 | "PyQt4": { 1076 | "QtGui.QUndoCommand": "QtWidgets.QUndoCommand", 1077 | "QtGui.QUndoGroup": "QtWidgets.QUndoGroup", 1078 | "QtGui.QUndoStack": "QtWidgets.QUndoStack", 1079 | "QtGui.QActionGroup": "QtWidgets.QActionGroup", 1080 | "QtGui.QAbstractProxyModel": "QtCore.QAbstractProxyModel", 1081 | "QtGui.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel", 1082 | "QtGui.QItemSelection": "QtCore.QItemSelection", 1083 | "QtGui.QStringListModel": "QtCore.QStringListModel", 1084 | "QtGui.QItemSelectionModel": "QtCore.QItemSelectionModel", 1085 | "QtCore.pyqtProperty": "QtCore.Property", 1086 | "QtCore.pyqtSignal": "QtCore.Signal", 1087 | "QtCore.pyqtSlot": "QtCore.Slot", 1088 | "QtGui.QItemSelectionRange": "QtCore.QItemSelectionRange", 1089 | "QtGui.QAbstractPrintDialog": "QtPrintSupport.QAbstractPrintDialog", 1090 | "QtGui.QRegExpValidator": "QtGui.QRegExpValidator", 1091 | "QtGui.QPageSetupDialog": "QtPrintSupport.QPageSetupDialog", 1092 | "QtGui.QPrintDialog": "QtPrintSupport.QPrintDialog", 1093 | "QtGui.QPrintEngine": "QtPrintSupport.QPrintEngine", 1094 | "QtWidgets.QShortcut": "QtWidgets.QShortcut", 1095 | "QtGui.QPrintPreviewDialog": "QtPrintSupport.QPrintPreviewDialog", 1096 | "QtGui.QPrintPreviewWidget": "QtPrintSupport.QPrintPreviewWidget", 1097 | "QtGui.QPrinter": "QtPrintSupport.QPrinter", 1098 | "QtGui.QPrinterInfo": "QtPrintSupport.QPrinterInfo", 1099 | "uic.loadUi": ["QtCompat.loadUi", _loadUi], 1100 | "sip.wrapinstance": ["QtCompat.wrapInstance", _wrapinstance], 1101 | "sip.unwrapinstance": ["QtCompat.getCppPointer", _getcpppointer], 1102 | "sip.isdeleted": ["QtCompat.isValid", _isvalid], 1103 | "QtCore.QString": "str", 1104 | "QtGui.qApp": "QtWidgets.QApplication.instance()", 1105 | "QtCore.QRegExp": "QtCore.QRegExp", 1106 | "QtCore.QCoreApplication.translate": [ 1107 | "QtCompat.translate", _translate 1108 | ], 1109 | "QtGui.QApplication.translate": [ 1110 | "QtCompat.translate", _translate 1111 | ], 1112 | "QtCore.qInstallMsgHandler": [ 1113 | "QtCompat.qInstallMessageHandler", _qInstallMessageHandler 1114 | ], 1115 | "QtGui.QStyleOptionViewItemV4": "QtCompat.QStyleOptionViewItemV4", 1116 | } 1117 | } 1118 | 1119 | """ Compatibility Members 1120 | 1121 | This dictionary is used to build Qt.QtCompat objects that provide a consistent 1122 | interface for obsolete members, and differences in binding return values. 1123 | 1124 | { 1125 | "binding": { 1126 | "classname": { 1127 | "targetname": "binding_namespace", 1128 | } 1129 | } 1130 | } 1131 | """ 1132 | _compatibility_members = { 1133 | "PySide6": { 1134 | "QWidget": { 1135 | "grab": "QtWidgets.QWidget.grab", 1136 | }, 1137 | "QHeaderView": { 1138 | "sectionsClickable": "QtWidgets.QHeaderView.sectionsClickable", 1139 | "setSectionsClickable": 1140 | "QtWidgets.QHeaderView.setSectionsClickable", 1141 | "sectionResizeMode": "QtWidgets.QHeaderView.sectionResizeMode", 1142 | "setSectionResizeMode": 1143 | "QtWidgets.QHeaderView.setSectionResizeMode", 1144 | "sectionsMovable": "QtWidgets.QHeaderView.sectionsMovable", 1145 | "setSectionsMovable": "QtWidgets.QHeaderView.setSectionsMovable", 1146 | }, 1147 | "QFileDialog": { 1148 | "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName", 1149 | "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", 1150 | "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", 1151 | }, 1152 | "QFont":{ 1153 | "setWeight": "QtGui.QFont.setWeight", 1154 | }, 1155 | "Qt": { 1156 | "MidButton": "QtCore.Qt.MiddleButton", 1157 | }, 1158 | }, 1159 | "PySide2": { 1160 | "QWidget": { 1161 | "grab": "QtWidgets.QWidget.grab", 1162 | }, 1163 | "QHeaderView": { 1164 | "sectionsClickable": "QtWidgets.QHeaderView.sectionsClickable", 1165 | "setSectionsClickable": 1166 | "QtWidgets.QHeaderView.setSectionsClickable", 1167 | "sectionResizeMode": "QtWidgets.QHeaderView.sectionResizeMode", 1168 | "setSectionResizeMode": 1169 | "QtWidgets.QHeaderView.setSectionResizeMode", 1170 | "sectionsMovable": "QtWidgets.QHeaderView.sectionsMovable", 1171 | "setSectionsMovable": "QtWidgets.QHeaderView.setSectionsMovable", 1172 | }, 1173 | "QFileDialog": { 1174 | "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName", 1175 | "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", 1176 | "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", 1177 | }, 1178 | "QFont":{ 1179 | "setWeight": "QtGui.QFont.setWeight", 1180 | }, 1181 | "Qt": { 1182 | "MidButton": "QtCore.Qt.MiddleButton", 1183 | }, 1184 | }, 1185 | "PyQt5": { 1186 | "QWidget": { 1187 | "grab": "QtWidgets.QWidget.grab", 1188 | }, 1189 | "QHeaderView": { 1190 | "sectionsClickable": "QtWidgets.QHeaderView.sectionsClickable", 1191 | "setSectionsClickable": 1192 | "QtWidgets.QHeaderView.setSectionsClickable", 1193 | "sectionResizeMode": "QtWidgets.QHeaderView.sectionResizeMode", 1194 | "setSectionResizeMode": 1195 | "QtWidgets.QHeaderView.setSectionResizeMode", 1196 | "sectionsMovable": "QtWidgets.QHeaderView.sectionsMovable", 1197 | "setSectionsMovable": "QtWidgets.QHeaderView.setSectionsMovable", 1198 | }, 1199 | "QFileDialog": { 1200 | "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName", 1201 | "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", 1202 | "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", 1203 | }, 1204 | "QFont":{ 1205 | "setWeight": "QtGui.QFont.setWeight", 1206 | }, 1207 | "Qt": { 1208 | "MidButton": "QtCore.Qt.MiddleButton", 1209 | }, 1210 | }, 1211 | "PySide": { 1212 | "QWidget": { 1213 | "grab": "QtWidgets.QPixmap.grabWidget", 1214 | }, 1215 | "QHeaderView": { 1216 | "sectionsClickable": "QtWidgets.QHeaderView.isClickable", 1217 | "setSectionsClickable": "QtWidgets.QHeaderView.setClickable", 1218 | "sectionResizeMode": "QtWidgets.QHeaderView.resizeMode", 1219 | "setSectionResizeMode": "QtWidgets.QHeaderView.setResizeMode", 1220 | "sectionsMovable": "QtWidgets.QHeaderView.isMovable", 1221 | "setSectionsMovable": "QtWidgets.QHeaderView.setMovable", 1222 | }, 1223 | "QFileDialog": { 1224 | "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName", 1225 | "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", 1226 | "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", 1227 | }, 1228 | "QFont":{ 1229 | "setWeight": "QtGui.QFont.setWeight", 1230 | }, 1231 | "Qt": { 1232 | "MidButton": "QtCore.Qt.MiddleButton", 1233 | }, 1234 | }, 1235 | "PyQt4": { 1236 | "QWidget": { 1237 | "grab": "QtWidgets.QPixmap.grabWidget", 1238 | }, 1239 | "QHeaderView": { 1240 | "sectionsClickable": "QtWidgets.QHeaderView.isClickable", 1241 | "setSectionsClickable": "QtWidgets.QHeaderView.setClickable", 1242 | "sectionResizeMode": "QtWidgets.QHeaderView.resizeMode", 1243 | "setSectionResizeMode": "QtWidgets.QHeaderView.setResizeMode", 1244 | "sectionsMovable": "QtWidgets.QHeaderView.isMovable", 1245 | "setSectionsMovable": "QtWidgets.QHeaderView.setMovable", 1246 | }, 1247 | "QFileDialog": { 1248 | "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName", 1249 | "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", 1250 | "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", 1251 | }, 1252 | "QFont":{ 1253 | "setWeight": "QtGui.QFont.setWeight", 1254 | }, 1255 | "Qt": { 1256 | "MidButton": "QtCore.Qt.MiddleButton", 1257 | }, 1258 | }, 1259 | } 1260 | 1261 | 1262 | def _apply_site_config(): 1263 | try: 1264 | import QtSiteConfig 1265 | except ImportError: 1266 | # If no QtSiteConfig module found, no modifications 1267 | # to _common_members are needed. 1268 | pass 1269 | else: 1270 | # Provide the ability to modify the dicts used to build Qt.py 1271 | if hasattr(QtSiteConfig, 'update_members'): 1272 | QtSiteConfig.update_members(_common_members) 1273 | 1274 | if hasattr(QtSiteConfig, 'update_misplaced_members'): 1275 | QtSiteConfig.update_misplaced_members(members=_misplaced_members) 1276 | 1277 | if hasattr(QtSiteConfig, 'update_compatibility_members'): 1278 | QtSiteConfig.update_compatibility_members( 1279 | members=_compatibility_members) 1280 | 1281 | 1282 | def _new_module(name): 1283 | return types.ModuleType(__name__ + "." + name) 1284 | 1285 | 1286 | def _import_sub_module(module, name): 1287 | """import_sub_module will mimic the function of importlib.import_module""" 1288 | module = __import__(module.__name__ + "." + name) 1289 | for level in name.split("."): 1290 | module = getattr(module, level) 1291 | return module 1292 | 1293 | 1294 | def _setup(module, extras): 1295 | """Install common submodules""" 1296 | 1297 | Qt.__binding__ = module.__name__ 1298 | 1299 | def _warn_import_error(exc, module): 1300 | msg = str(exc) 1301 | if "No module named" in msg: 1302 | return 1303 | _warn("ImportError(%s): %s" % (module, msg)) 1304 | 1305 | for name in list(_common_members) + extras: 1306 | try: 1307 | submodule = _import_sub_module( 1308 | module, name) 1309 | except ImportError as e: 1310 | try: 1311 | # For extra modules like sip and shiboken that may not be 1312 | # children of the binding. 1313 | submodule = __import__(name) 1314 | except ImportError as e2: 1315 | _warn_import_error(e, name) 1316 | _warn_import_error(e2, name) 1317 | continue 1318 | 1319 | setattr(Qt, "_" + name, submodule) 1320 | 1321 | if name not in extras: 1322 | # Store reference to original binding, 1323 | # but don't store speciality modules 1324 | # such as uic or QtUiTools 1325 | setattr(Qt, name, _new_module(name)) 1326 | 1327 | 1328 | def _reassign_misplaced_members(binding): 1329 | """Apply misplaced members from `binding` to Qt.py 1330 | 1331 | Arguments: 1332 | binding (dict): Misplaced members 1333 | 1334 | """ 1335 | 1336 | 1337 | for src, dst in _misplaced_members[binding].items(): 1338 | dst_value = None 1339 | 1340 | src_parts = src.split(".") 1341 | src_module = src_parts[0] 1342 | src_member = None 1343 | if len(src_parts) > 1: 1344 | src_member = src_parts[1:] 1345 | 1346 | if isinstance(dst, (list, tuple)): 1347 | dst, dst_value = dst 1348 | 1349 | dst_parts = dst.split(".") 1350 | dst_module = dst_parts[0] 1351 | dst_member = None 1352 | if len(dst_parts) > 1: 1353 | dst_member = dst_parts[1] 1354 | 1355 | 1356 | # Get the member we want to store in the namesapce. 1357 | if not dst_value: 1358 | try: 1359 | _part = getattr(Qt, "_" + src_module) 1360 | while src_member: 1361 | member = src_member.pop(0) 1362 | _part = getattr(_part, member) 1363 | dst_value = _part 1364 | except AttributeError: 1365 | # If the member we want to store in the namespace does not 1366 | # exist, there is no need to continue. This can happen if a 1367 | # request was made to rename a member that didn't exist, for 1368 | # example if QtWidgets isn't available on the target platform. 1369 | _log("Misplaced member has no source: {0}".format(src)) 1370 | continue 1371 | 1372 | try: 1373 | src_object = getattr(Qt, dst_module) 1374 | except AttributeError: 1375 | if dst_module not in _common_members: 1376 | # Only create the Qt parent module if its listed in 1377 | # _common_members. Without this check, if you remove QtCore 1378 | # from _common_members, the default _misplaced_members will add 1379 | # Qt.QtCore so it can add Signal, Slot, etc. 1380 | msg = 'Not creating missing member module "{m}" for "{c}"' 1381 | _log(msg.format(m=dst_module, c=dst_member)) 1382 | continue 1383 | # If the dst is valid but the Qt parent module does not exist 1384 | # then go ahead and create a new module to contain the member. 1385 | setattr(Qt, dst_module, _new_module(dst_module)) 1386 | src_object = getattr(Qt, dst_module) 1387 | # Enable direct import of the new module 1388 | sys.modules[__name__ + "." + dst_module] = src_object 1389 | 1390 | if not dst_value: 1391 | dst_value = getattr(Qt, "_" + src_module) 1392 | if src_member: 1393 | dst_value = getattr(dst_value, src_member) 1394 | 1395 | setattr( 1396 | src_object, 1397 | dst_member or dst_module, 1398 | dst_value 1399 | ) 1400 | 1401 | 1402 | def _build_compatibility_members(binding, decorators=None): 1403 | """Apply `binding` to QtCompat 1404 | 1405 | Arguments: 1406 | binding (str): Top level binding in _compatibility_members. 1407 | decorators (dict, optional): Provides the ability to decorate the 1408 | original Qt methods when needed by a binding. This can be used 1409 | to change the returned value to a standard value. The key should 1410 | be the classname, the value is a dict where the keys are the 1411 | target method names, and the values are the decorator functions. 1412 | 1413 | """ 1414 | 1415 | decorators = decorators or dict() 1416 | 1417 | # Allow optional site-level customization of the compatibility members. 1418 | # This method does not need to be implemented in QtSiteConfig. 1419 | try: 1420 | import QtSiteConfig 1421 | except ImportError: 1422 | pass 1423 | else: 1424 | if hasattr(QtSiteConfig, 'update_compatibility_decorators'): 1425 | QtSiteConfig.update_compatibility_decorators(binding, decorators) 1426 | 1427 | _QtCompat = type("QtCompat", (object,), {}) 1428 | 1429 | for classname, bindings in _compatibility_members[binding].items(): 1430 | attrs = {} 1431 | for target, binding in bindings.items(): 1432 | namespaces = binding.split('.') 1433 | try: 1434 | src_object = getattr(Qt, "_" + namespaces[0]) 1435 | except AttributeError as e: 1436 | _log("QtCompat: AttributeError: %s" % e) 1437 | # Skip reassignment of non-existing members. 1438 | # This can happen if a request was made to 1439 | # rename a member that didn't exist, for example 1440 | # if QtWidgets isn't available on the target platform. 1441 | continue 1442 | 1443 | # Walk down any remaining namespace getting the object assuming 1444 | # that if the first namespace exists the rest will exist. 1445 | for namespace in namespaces[1:]: 1446 | src_object = getattr(src_object, namespace) 1447 | 1448 | # decorate the Qt method if a decorator was provided. 1449 | if target in decorators.get(classname, []): 1450 | # staticmethod must be called on the decorated method to 1451 | # prevent a TypeError being raised when the decorated method 1452 | # is called. 1453 | src_object = staticmethod( 1454 | decorators[classname][target](src_object)) 1455 | 1456 | attrs[target] = src_object 1457 | 1458 | # Create the QtCompat class and install it into the namespace 1459 | compat_class = type(classname, (_QtCompat,), attrs) 1460 | setattr(Qt.QtCompat, classname, compat_class) 1461 | 1462 | 1463 | def _pyside6(): 1464 | """Initialise PySide6 1465 | 1466 | These functions serve to test the existence of a binding 1467 | along with set it up in such a way that it aligns with 1468 | the final step; adding members from the original binding 1469 | to Qt.py 1470 | 1471 | """ 1472 | 1473 | import PySide6 as module 1474 | extras = ["QtUiTools"] 1475 | try: 1476 | import shiboken6 1477 | extras.append("shiboken6") 1478 | except ImportError as e: 1479 | print("ImportError: %s" % e) 1480 | 1481 | _setup(module, extras) 1482 | Qt.__binding_version__ = module.__version__ 1483 | 1484 | if hasattr(Qt, "_shiboken6"): 1485 | Qt.QtCompat.wrapInstance = _wrapinstance 1486 | Qt.QtCompat.getCppPointer = _getcpppointer 1487 | Qt.QtCompat.delete = shiboken6.delete 1488 | 1489 | if hasattr(Qt, "_QtUiTools"): 1490 | Qt.QtCompat.loadUi = _loadUi 1491 | 1492 | if hasattr(Qt, "_QtCore"): 1493 | Qt.__qt_version__ = Qt._QtCore.qVersion() 1494 | Qt.QtCompat.dataChanged = ( 1495 | lambda self, topleft, bottomright, roles=None: 1496 | self.dataChanged.emit(topleft, bottomright, roles or []) 1497 | ) 1498 | 1499 | if hasattr(Qt, "_QtWidgets"): 1500 | Qt.QtCompat.setSectionResizeMode = \ 1501 | Qt._QtWidgets.QHeaderView.setSectionResizeMode 1502 | 1503 | def setWeight(func): 1504 | def wrapper(self, weight): 1505 | weight = { 1506 | 100: Qt._QtGui.QFont.Thin, 1507 | 200: Qt._QtGui.QFont.ExtraLight, 1508 | 300: Qt._QtGui.QFont.Light, 1509 | 400: Qt._QtGui.QFont.Normal, 1510 | 500: Qt._QtGui.QFont.Medium, 1511 | 600: Qt._QtGui.QFont.DemiBold, 1512 | 700: Qt._QtGui.QFont.Bold, 1513 | 800: Qt._QtGui.QFont.ExtraBold, 1514 | 900: Qt._QtGui.QFont.Black, 1515 | }.get(weight, Qt._QtGui.QFont.Normal) 1516 | 1517 | return func(self, weight) 1518 | 1519 | wrapper.__doc__ = func.__doc__ 1520 | wrapper.__name__ = func.__name__ 1521 | 1522 | return wrapper 1523 | 1524 | 1525 | decorators = { 1526 | "QFont": { 1527 | "setWeight": setWeight, 1528 | } 1529 | } 1530 | 1531 | _reassign_misplaced_members("PySide6") 1532 | _build_compatibility_members("PySide6", decorators) 1533 | 1534 | 1535 | def _pyside2(): 1536 | """Initialise PySide2 1537 | 1538 | These functions serve to test the existence of a binding 1539 | along with set it up in such a way that it aligns with 1540 | the final step; adding members from the original binding 1541 | to Qt.py 1542 | 1543 | """ 1544 | 1545 | import PySide2 as module 1546 | extras = ["QtUiTools"] 1547 | try: 1548 | try: 1549 | # Before merge of PySide and shiboken 1550 | import shiboken2 1551 | except ImportError: 1552 | # After merge of PySide and shiboken, May 2017 1553 | from PySide2 import shiboken2 1554 | extras.append("shiboken2") 1555 | except ImportError: 1556 | pass 1557 | 1558 | _setup(module, extras) 1559 | Qt.__binding_version__ = module.__version__ 1560 | 1561 | if hasattr(Qt, "_shiboken2"): 1562 | Qt.QtCompat.wrapInstance = _wrapinstance 1563 | Qt.QtCompat.getCppPointer = _getcpppointer 1564 | Qt.QtCompat.delete = shiboken2.delete 1565 | 1566 | if hasattr(Qt, "_QtUiTools"): 1567 | Qt.QtCompat.loadUi = _loadUi 1568 | 1569 | if hasattr(Qt, "_QtCore"): 1570 | Qt.__qt_version__ = Qt._QtCore.qVersion() 1571 | Qt.QtCompat.dataChanged = ( 1572 | lambda self, topleft, bottomright, roles=None: 1573 | self.dataChanged.emit(topleft, bottomright, roles or []) 1574 | ) 1575 | 1576 | if hasattr(Qt, "_QtWidgets"): 1577 | Qt.QtCompat.setSectionResizeMode = \ 1578 | Qt._QtWidgets.QHeaderView.setSectionResizeMode 1579 | 1580 | _reassign_misplaced_members("PySide2") 1581 | _build_compatibility_members("PySide2") 1582 | 1583 | 1584 | def _pyside(): 1585 | """Initialise PySide""" 1586 | 1587 | import PySide as module 1588 | extras = ["QtUiTools"] 1589 | try: 1590 | try: 1591 | # Before merge of PySide and shiboken 1592 | import shiboken 1593 | except ImportError: 1594 | # After merge of PySide and shiboken, May 2017 1595 | from PySide import shiboken 1596 | extras.append("shiboken") 1597 | except ImportError: 1598 | pass 1599 | 1600 | _setup(module, extras) 1601 | Qt.__binding_version__ = module.__version__ 1602 | 1603 | if hasattr(Qt, "_shiboken"): 1604 | Qt.QtCompat.wrapInstance = _wrapinstance 1605 | Qt.QtCompat.getCppPointer = _getcpppointer 1606 | Qt.QtCompat.delete = shiboken.delete 1607 | 1608 | if hasattr(Qt, "_QtUiTools"): 1609 | Qt.QtCompat.loadUi = _loadUi 1610 | 1611 | if hasattr(Qt, "_QtGui"): 1612 | setattr(Qt, "QtWidgets", _new_module("QtWidgets")) 1613 | setattr(Qt, "_QtWidgets", Qt._QtGui) 1614 | if hasattr(Qt._QtGui, "QX11Info"): 1615 | setattr(Qt, "QtX11Extras", _new_module("QtX11Extras")) 1616 | Qt.QtX11Extras.QX11Info = Qt._QtGui.QX11Info 1617 | 1618 | Qt.QtCompat.setSectionResizeMode = Qt._QtGui.QHeaderView.setResizeMode 1619 | 1620 | if hasattr(Qt, "_QtCore"): 1621 | Qt.__qt_version__ = Qt._QtCore.qVersion() 1622 | Qt.QtCompat.dataChanged = ( 1623 | lambda self, topleft, bottomright, roles=None: 1624 | self.dataChanged.emit(topleft, bottomright) 1625 | ) 1626 | 1627 | _reassign_misplaced_members("PySide") 1628 | _build_compatibility_members("PySide") 1629 | 1630 | 1631 | def _pyqt5(): 1632 | """Initialise PyQt5""" 1633 | 1634 | import PyQt5 as module 1635 | extras = ["uic"] 1636 | 1637 | try: 1638 | # Relevant to PyQt5 5.11 and above 1639 | from PyQt5 import sip 1640 | extras += ["sip"] 1641 | except ImportError: 1642 | 1643 | try: 1644 | import sip 1645 | extras += ["sip"] 1646 | except ImportError: 1647 | sip = None 1648 | 1649 | _setup(module, extras) 1650 | if hasattr(Qt, "_sip"): 1651 | Qt.QtCompat.wrapInstance = _wrapinstance 1652 | Qt.QtCompat.getCppPointer = _getcpppointer 1653 | Qt.QtCompat.delete = sip.delete 1654 | 1655 | if hasattr(Qt, "_uic"): 1656 | Qt.QtCompat.loadUi = _loadUi 1657 | 1658 | if hasattr(Qt, "_QtCore"): 1659 | Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR 1660 | Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR 1661 | Qt.QtCompat.dataChanged = ( 1662 | lambda self, topleft, bottomright, roles=None: 1663 | self.dataChanged.emit(topleft, bottomright, roles or []) 1664 | ) 1665 | 1666 | if hasattr(Qt, "_QtWidgets"): 1667 | Qt.QtCompat.setSectionResizeMode = \ 1668 | Qt._QtWidgets.QHeaderView.setSectionResizeMode 1669 | 1670 | _reassign_misplaced_members("PyQt5") 1671 | _build_compatibility_members('PyQt5') 1672 | 1673 | 1674 | def _pyqt4(): 1675 | """Initialise PyQt4""" 1676 | 1677 | import sip 1678 | 1679 | # Validation of envivornment variable. Prevents an error if 1680 | # the variable is invalid since it's just a hint. 1681 | try: 1682 | hint = int(QT_SIP_API_HINT) 1683 | except TypeError: 1684 | hint = None # Variable was None, i.e. not set. 1685 | except ValueError: 1686 | raise ImportError("QT_SIP_API_HINT=%s must be a 1 or 2") 1687 | 1688 | for api in ("QString", 1689 | "QVariant", 1690 | "QDate", 1691 | "QDateTime", 1692 | "QTextStream", 1693 | "QTime", 1694 | "QUrl"): 1695 | try: 1696 | sip.setapi(api, hint or 2) 1697 | except AttributeError: 1698 | raise ImportError("PyQt4 < 4.6 isn't supported by Qt.py") 1699 | except ValueError: 1700 | actual = sip.getapi(api) 1701 | if not hint: 1702 | raise ImportError("API version already set to %d" % actual) 1703 | else: 1704 | # Having provided a hint indicates a soft constraint, one 1705 | # that doesn't throw an exception. 1706 | sys.stderr.write( 1707 | "Warning: API '%s' has already been set to %d.\n" 1708 | % (api, actual) 1709 | ) 1710 | 1711 | import PyQt4 as module 1712 | extras = ["uic"] 1713 | try: 1714 | import sip 1715 | extras.append(sip.__name__) 1716 | except ImportError: 1717 | sip = None 1718 | 1719 | _setup(module, extras) 1720 | if hasattr(Qt, "_sip"): 1721 | Qt.QtCompat.wrapInstance = _wrapinstance 1722 | Qt.QtCompat.getCppPointer = _getcpppointer 1723 | Qt.QtCompat.delete = sip.delete 1724 | 1725 | if hasattr(Qt, "_uic"): 1726 | Qt.QtCompat.loadUi = _loadUi 1727 | 1728 | if hasattr(Qt, "_QtGui"): 1729 | setattr(Qt, "QtWidgets", _new_module("QtWidgets")) 1730 | setattr(Qt, "_QtWidgets", Qt._QtGui) 1731 | if hasattr(Qt._QtGui, "QX11Info"): 1732 | setattr(Qt, "QtX11Extras", _new_module("QtX11Extras")) 1733 | Qt.QtX11Extras.QX11Info = Qt._QtGui.QX11Info 1734 | 1735 | Qt.QtCompat.setSectionResizeMode = \ 1736 | Qt._QtGui.QHeaderView.setResizeMode 1737 | 1738 | if hasattr(Qt, "_QtCore"): 1739 | Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR 1740 | Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR 1741 | Qt.QtCompat.dataChanged = ( 1742 | lambda self, topleft, bottomright, roles=None: 1743 | self.dataChanged.emit(topleft, bottomright) 1744 | ) 1745 | 1746 | _reassign_misplaced_members("PyQt4") 1747 | 1748 | # QFileDialog QtCompat decorator 1749 | def _standardizeQFileDialog(some_function): 1750 | """Decorator that makes PyQt4 return conform to other bindings""" 1751 | def wrapper(*args, **kwargs): 1752 | ret = (some_function(*args, **kwargs)) 1753 | 1754 | # PyQt4 only returns the selected filename, force it to a 1755 | # standard return of the selected filename, and a empty string 1756 | # for the selected filter 1757 | return ret, '' 1758 | 1759 | wrapper.__doc__ = some_function.__doc__ 1760 | wrapper.__name__ = some_function.__name__ 1761 | 1762 | return wrapper 1763 | 1764 | decorators = { 1765 | "QFileDialog": { 1766 | "getOpenFileName": _standardizeQFileDialog, 1767 | "getOpenFileNames": _standardizeQFileDialog, 1768 | "getSaveFileName": _standardizeQFileDialog, 1769 | } 1770 | } 1771 | _build_compatibility_members('PyQt4', decorators) 1772 | 1773 | 1774 | def _none(): 1775 | """Internal option (used in installer)""" 1776 | 1777 | Mock = type("Mock", (), {"__getattr__": lambda Qt, attr: None}) 1778 | 1779 | Qt.__binding__ = "None" 1780 | Qt.__qt_version__ = "0.0.0" 1781 | Qt.__binding_version__ = "0.0.0" 1782 | Qt.QtCompat.loadUi = lambda uifile, baseinstance=None: None 1783 | Qt.QtCompat.setSectionResizeMode = lambda *args, **kwargs: None 1784 | 1785 | for submodule in _common_members.keys(): 1786 | setattr(Qt, submodule, Mock()) 1787 | setattr(Qt, "_" + submodule, Mock()) 1788 | 1789 | 1790 | def _log(text): 1791 | if QT_VERBOSE: 1792 | sys.stdout.write("Qt.py [info]: %s\n" % text) 1793 | 1794 | 1795 | def _warn(text): 1796 | try: 1797 | sys.stderr.write("Qt.py [warning]: %s\n" % text) 1798 | except UnicodeDecodeError: 1799 | import locale 1800 | encoding = locale.getpreferredencoding() 1801 | sys.stderr.write("Qt.py [warning]: %s\n" % text.decode(encoding)) 1802 | 1803 | 1804 | def _convert(lines): 1805 | """Convert compiled .ui file from PySide2 to Qt.py 1806 | 1807 | Arguments: 1808 | lines (list): Each line of of .ui file 1809 | 1810 | Usage: 1811 | >> with open("myui.py") as f: 1812 | .. lines = _convert(f.readlines()) 1813 | 1814 | """ 1815 | 1816 | def parse(line): 1817 | line = line.replace("from PySide2 import", "from Qt import QtCompat,") 1818 | line = line.replace("QtWidgets.QApplication.translate", 1819 | "QtCompat.translate") 1820 | if "QtCore.SIGNAL" in line: 1821 | raise NotImplementedError("QtCore.SIGNAL is missing from PyQt5 " 1822 | "and so Qt.py does not support it: you " 1823 | "should avoid defining signals inside " 1824 | "your ui files.") 1825 | return line 1826 | 1827 | parsed = list() 1828 | for line in lines: 1829 | line = parse(line) 1830 | parsed.append(line) 1831 | 1832 | return parsed 1833 | 1834 | 1835 | def _cli(args): 1836 | """Qt.py command-line interface""" 1837 | import argparse 1838 | 1839 | parser = argparse.ArgumentParser() 1840 | parser.add_argument("--convert", 1841 | help="Path to compiled Python module, e.g. my_ui.py") 1842 | parser.add_argument("--compile", 1843 | help="Accept raw .ui file and compile with native " 1844 | "PySide2 compiler.") 1845 | parser.add_argument("--stdout", 1846 | help="Write to stdout instead of file", 1847 | action="store_true") 1848 | parser.add_argument("--stdin", 1849 | help="Read from stdin instead of file", 1850 | action="store_true") 1851 | 1852 | args = parser.parse_args(args) 1853 | 1854 | if args.stdout: 1855 | raise NotImplementedError("--stdout") 1856 | 1857 | if args.stdin: 1858 | raise NotImplementedError("--stdin") 1859 | 1860 | if args.compile: 1861 | raise NotImplementedError("--compile") 1862 | 1863 | if args.convert: 1864 | sys.stdout.write("#\n" 1865 | "# WARNING: --convert is an ALPHA feature.\n#\n" 1866 | "# See https://github.com/mottosso/Qt.py/pull/132\n" 1867 | "# for details.\n" 1868 | "#\n") 1869 | 1870 | # 1871 | # ------> Read 1872 | # 1873 | with open(args.convert) as f: 1874 | lines = _convert(f.readlines()) 1875 | 1876 | backup = "%s_backup%s" % os.path.splitext(args.convert) 1877 | sys.stdout.write("Creating \"%s\"..\n" % backup) 1878 | shutil.copy(args.convert, backup) 1879 | 1880 | # 1881 | # <------ Write 1882 | # 1883 | with open(args.convert, "w") as f: 1884 | f.write("".join(lines)) 1885 | 1886 | sys.stdout.write("Successfully converted \"%s\"\n" % args.convert) 1887 | 1888 | 1889 | class MissingMember(object): 1890 | """ 1891 | A placeholder type for a missing Qt object not 1892 | included in Qt.py 1893 | 1894 | Args: 1895 | name (str): The name of the missing type 1896 | details (str): An optional custom error message 1897 | """ 1898 | ERR_TMPL = ("{} is not a common object across PySide2 " 1899 | "and the other Qt bindings. It is not included " 1900 | "as a common member in the Qt.py layer") 1901 | 1902 | def __init__(self, name, details=''): 1903 | self.__name = name 1904 | self.__err = self.ERR_TMPL.format(name) 1905 | 1906 | if details: 1907 | self.__err = "{}: {}".format(self.__err, details) 1908 | 1909 | def __repr__(self): 1910 | return "<{}: {}>".format(self.__class__.__name__, self.__name) 1911 | 1912 | def __getattr__(self, name): 1913 | raise NotImplementedError(self.__err) 1914 | 1915 | def __call__(self, *a, **kw): 1916 | raise NotImplementedError(self.__err) 1917 | 1918 | 1919 | def _install(): 1920 | # Default order (customize order and content via QT_PREFERRED_BINDING) 1921 | default_order = ("PySide6", "PySide2", "PyQt5", "PySide", "PyQt4") 1922 | preferred_order = None 1923 | if QT_PREFERRED_BINDING_JSON: 1924 | # A per-vendor preferred binding customization was defined 1925 | # This should be a dictionary of the full Qt.py module namespace to 1926 | # apply binding settings to. The "default" key can be used to apply 1927 | # custom bindings to all modules not explicitly defined. If the json 1928 | # data is invalid this will raise a exception. 1929 | # Example: 1930 | # {"mylibrary.vendor.Qt": ["PySide2"], "default":["PyQt5","PyQt4"]} 1931 | try: 1932 | preferred_bindings = json.loads(QT_PREFERRED_BINDING_JSON) 1933 | except ValueError: 1934 | # Python 2 raises ValueError, Python 3 raises json.JSONDecodeError 1935 | # a subclass of ValueError 1936 | _warn("Failed to parse QT_PREFERRED_BINDING_JSON='%s'" 1937 | % QT_PREFERRED_BINDING_JSON) 1938 | _warn("Falling back to default preferred order") 1939 | else: 1940 | preferred_order = preferred_bindings.get(__name__) 1941 | # If no matching binding was used, optionally apply a default. 1942 | if preferred_order is None: 1943 | preferred_order = preferred_bindings.get("default", None) 1944 | if preferred_order is None: 1945 | # If a json preferred binding was not used use, respect the 1946 | # QT_PREFERRED_BINDING environment variable if defined. 1947 | preferred_order = list( 1948 | b for b in QT_PREFERRED_BINDING.split(os.pathsep) if b 1949 | ) 1950 | 1951 | order = preferred_order or default_order 1952 | 1953 | available = { 1954 | "PySide6": _pyside6, 1955 | "PySide2": _pyside2, 1956 | "PyQt5": _pyqt5, 1957 | "PySide": _pyside, 1958 | "PyQt4": _pyqt4, 1959 | "None": _none 1960 | } 1961 | 1962 | _log("Order: '%s'" % "', '".join(order)) 1963 | 1964 | # Allow site-level customization of the available modules. 1965 | _apply_site_config() 1966 | 1967 | found_binding = False 1968 | for name in order: 1969 | _log("Trying %s" % name) 1970 | 1971 | try: 1972 | available[name]() 1973 | found_binding = True 1974 | break 1975 | 1976 | except ImportError as e: 1977 | _log("ImportError: %s" % e) 1978 | 1979 | except KeyError: 1980 | _log("ImportError: Preferred binding '%s' not found." % name) 1981 | 1982 | if not found_binding: 1983 | # If not binding were found, throw this error 1984 | raise ImportError("No Qt binding were found.") 1985 | 1986 | # Install individual members 1987 | for name, members in _common_members.items(): 1988 | try: 1989 | their_submodule = getattr(Qt, "_%s" % name) 1990 | except AttributeError: 1991 | continue 1992 | 1993 | our_submodule = getattr(Qt, name) 1994 | 1995 | # Enable import * 1996 | __all__.append(name) 1997 | 1998 | # Enable direct import of submodule, 1999 | # e.g. import Qt.QtCore 2000 | sys.modules[__name__ + "." + name] = our_submodule 2001 | 2002 | for member in members: 2003 | # Accept that a submodule may miss certain members. 2004 | try: 2005 | their_member = getattr(their_submodule, member) 2006 | except AttributeError: 2007 | _log("'%s.%s' was missing." % (name, member)) 2008 | continue 2009 | 2010 | setattr(our_submodule, member, their_member) 2011 | 2012 | # Install missing member placeholders 2013 | for name, members in _missing_members.items(): 2014 | our_submodule = getattr(Qt, name) 2015 | 2016 | for member in members: 2017 | 2018 | # If the submodule already has this member installed, 2019 | # either by the common members, or the site config, 2020 | # then skip installing this one over it. 2021 | if hasattr(our_submodule, member): 2022 | continue 2023 | 2024 | placeholder = MissingMember("{}.{}".format(name, member), 2025 | details=members[member]) 2026 | setattr(our_submodule, member, placeholder) 2027 | 2028 | # Enable direct import of QtCompat 2029 | sys.modules[__name__ + ".QtCompat"] = Qt.QtCompat 2030 | 2031 | # Backwards compatibility 2032 | if hasattr(Qt.QtCompat, 'loadUi'): 2033 | Qt.QtCompat.load_ui = Qt.QtCompat.loadUi 2034 | 2035 | 2036 | _install() 2037 | 2038 | # Setup Binding Enum states 2039 | Qt.IsPySide6 = Qt.__binding__ == "PySide6" 2040 | Qt.IsPySide2 = Qt.__binding__ == 'PySide2' 2041 | Qt.IsPyQt5 = Qt.__binding__ == 'PyQt5' 2042 | Qt.IsPySide = Qt.__binding__ == 'PySide' 2043 | Qt.IsPyQt4 = Qt.__binding__ == 'PyQt4' 2044 | 2045 | """Augment QtCompat 2046 | 2047 | QtCompat contains wrappers and added functionality 2048 | to the original bindings, such as the CLI interface 2049 | and otherwise incompatible members between bindings, 2050 | such as `QHeaderView.setSectionResizeMode`. 2051 | 2052 | """ 2053 | 2054 | Qt.QtCompat._cli = _cli 2055 | Qt.QtCompat._convert = _convert 2056 | 2057 | # Enable command-line interface 2058 | if __name__ == "__main__": 2059 | _cli(sys.argv[1:]) 2060 | 2061 | 2062 | # The MIT License (MIT) 2063 | # 2064 | # Copyright (c) 2016-2017 Marcus Ottosson 2065 | # 2066 | # Permission is hereby granted, free of charge, to any person obtaining a copy 2067 | # of this software and associated documentation files (the "Software"), to deal 2068 | # in the Software without restriction, including without limitation the rights 2069 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 2070 | # copies of the Software, and to permit persons to whom the Software is 2071 | # furnished to do so, subject to the following conditions: 2072 | # 2073 | # The above copyright notice and this permission notice shall be included in 2074 | # all copies or substantial portions of the Software. 2075 | # 2076 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 2077 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 2078 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 2079 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 2080 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 2081 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 2082 | # SOFTWARE. 2083 | # 2084 | # In PySide(2), loadUi does not exist, so we implement it 2085 | # 2086 | # `_UiLoader` is adapted from the qtpy project, which was further influenced 2087 | # by qt-helpers which was released under a 3-clause BSD license which in turn 2088 | # is based on a solution at: 2089 | # 2090 | # - https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8 2091 | # 2092 | # The License for this code is as follows: 2093 | # 2094 | # qt-helpers - a common front-end to various Qt modules 2095 | # 2096 | # Copyright (c) 2015, Chris Beaumont and Thomas Robitaille 2097 | # 2098 | # All rights reserved. 2099 | # 2100 | # Redistribution and use in source and binary forms, with or without 2101 | # modification, are permitted provided that the following conditions are 2102 | # met: 2103 | # 2104 | # * Redistributions of source code must retain the above copyright 2105 | # notice, this list of conditions and the following disclaimer. 2106 | # * Redistributions in binary form must reproduce the above copyright 2107 | # notice, this list of conditions and the following disclaimer in the 2108 | # documentation and/or other materials provided with the 2109 | # distribution. 2110 | # * Neither the name of the Glue project nor the names of its contributors 2111 | # may be used to endorse or promote products derived from this software 2112 | # without specific prior written permission. 2113 | # 2114 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 2115 | # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 2116 | # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 2117 | # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 2118 | # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 2119 | # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 2120 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 2121 | # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 2122 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 2123 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 2124 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 2125 | # 2126 | # Which itself was based on the solution at 2127 | # 2128 | # https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8 2129 | # 2130 | # which was released under the MIT license: 2131 | # 2132 | # Copyright (c) 2011 Sebastian Wiesner 2133 | # Modifications by Charl Botha 2134 | # 2135 | # Permission is hereby granted, free of charge, to any person obtaining a 2136 | # copy of this software and associated documentation files 2137 | # (the "Software"),to deal in the Software without restriction, 2138 | # including without limitation 2139 | # the rights to use, copy, modify, merge, publish, distribute, sublicense, 2140 | # and/or sell copies of the Software, and to permit persons to whom the 2141 | # Software is furnished to do so, subject to the following conditions: 2142 | # 2143 | # The above copyright notice and this permission notice shall be included 2144 | # in all copies or substantial portions of the Software. 2145 | # 2146 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 2147 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 2148 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 2149 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 2150 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 2151 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 2152 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 2153 | --------------------------------------------------------------------------------