├── 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 | 
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 |
--------------------------------------------------------------------------------