├── .gitignore
├── README.md
├── python
└── scriptsmenu
│ ├── __init__.py
│ ├── action.py
│ ├── launchformari.py
│ ├── launchformaya.py
│ ├── launchfornuke.py
│ ├── scriptsmenu.py
│ ├── vendor
│ ├── Qt.py
│ └── __init__.py
│ └── version.py
└── samples
├── example_scriptmenu.py
├── resources
└── script_a.png
├── sample_configuration_a.json
├── sample_configuration_b.json
├── script_a.py
├── script_b.py
└── script_c.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### Python template
3 | # Byte-compiled / optimized / DLL files
4 | __pycache__/
5 | *.py[cod]
6 | *$py.class
7 |
8 | # C extensions
9 | *.so
10 |
11 | # Distribution / packaging
12 | .Python
13 | env/
14 | build/
15 | develop-eggs/
16 | dist/
17 | downloads/
18 | eggs/
19 | .eggs/
20 | lib/
21 | lib64/
22 | parts/
23 | sdist/
24 | var/
25 | wheels/
26 | *.egg-info/
27 | .installed.cfg
28 | *.egg
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *,cover
49 | .hypothesis/
50 |
51 | # Translations
52 | *.mo
53 | *.pot
54 |
55 | # Django stuff:
56 | *.log
57 | local_settings.py
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # Jupyter Notebook
73 | .ipynb_checkpoints
74 |
75 | # pyenv
76 | .python-version
77 |
78 | # celery beat schedule file
79 | celerybeat-schedule
80 |
81 | # SageMath parsed files
82 | *.sage.py
83 |
84 | # dotenv
85 | .env
86 |
87 | # virtualenv
88 | .venv
89 | venv/
90 | ENV/
91 |
92 | # Spyder project settings
93 | .spyderproject
94 |
95 | # Rope project settings
96 | .ropeproject
97 |
98 | # Idea project settings
99 | /.idea
100 |
101 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # scriptsmenu
2 |
3 | ### Searchable scripts menu with search field in Qt
4 |
5 | Scriptsmenu will help you to easily organize your scripts into a
6 | customizable menu that users can quickly browse and search.
7 |
8 |
9 |
10 | #### Features
11 | - Built with [Qt.py](https://github.com/mottosso/Qt.py)
12 | - Searchable menu for your scripts and tools (using _tags_)
13 | - Update your scripts menu without restarting application
14 | - Supports use of [relative paths for scripts](#relative_paths)
15 |
16 |
17 |
18 | #### Installation
19 |
20 | To install download this package and place it on your `PYTHONPATH`.
21 |
22 |
23 |
24 | #### Usage
25 |
26 | To build a simple menu of searchable scripts
27 |
28 | ```python
29 | from scriptsmenu import ScriptsMenu
30 |
31 | menu = ScriptsMenu(title="Scripts",
32 | parent=None)
33 | menu.add_script(parent=menu,
34 | title="Script A",
35 | command="print('A')",
36 | sourcetype='python',
37 | tags=["foobar", "nugget"])
38 | menu.add_script(parent=menu,
39 | title="Script B",
40 | command="print('B')",
41 | sourcetype='python',
42 | tags=["gold", "silver", "bronze"])
43 | menu.show()
44 | ```
45 |
46 | ##### Example usage in Autodesk Maya
47 |
48 | To parent the scripts menu to an application you'll need a parent Qt widget from the host application.
49 | You can pass this parent as parent to the `ScriptMenu(parent=parent)`.
50 |
51 | Additionally if you want to alter the behavior when clicking a menu item with specific modifier buttons held (e.g. Control + Shift) you can register a callback. See the _Register callback_ example under _Advanced_ below.
52 |
53 | An example for Autodesk Maya can be found in `launchformaya.py`
54 |
55 | To show the menu in Maya:
56 |
57 | ```python
58 | import scriptsmenu.launchformaya as launchformaya
59 |
60 | menu = launchformaya.main(title="My Scripts")
61 |
62 | # continue to populate the menu here
63 | ```
64 |
65 | This will automatically parent it to Maya's main menu bar.
66 |
67 | To show the menu at Maya launch you can add code to your `userSetup.py`. This code will need to be executed deferred to ensure it runs when Maya main menu bar already exist. For example:
68 |
69 | ```python
70 | import maya.utils
71 | import scriptsmenu.launchformaya as launchformaya
72 |
73 | def build_menu():
74 | menu = launchformaya.main(title="My Scripts")
75 |
76 | maya.utils.executeDeferred(build_menu)
77 | ```
78 |
79 | An example for The Foundry Nuke can be found in `launchfornuke.py`
80 |
81 | To show the menu in Nuke:
82 |
83 | ```python
84 | import scriptsmenu.launchfornuke as launchfornuke
85 |
86 | menu = launchfornuke.main(title="My Scripts")
87 |
88 | menu.add_script(parent=menu,
89 | title="Script A",
90 | command="print('A')",
91 | sourcetype='python',
92 | tags=["foobar", "nugget"])
93 |
94 | menu.add_script(parent=menu,
95 | title="Script B",
96 | command="print('B')",
97 | sourcetype='python',
98 | tags=["gold", "silver", "bronze"])
99 |
100 | ```
101 | An example for The Foundry Mari can be found in `launchformari.py`
102 |
103 | To show the menu in Mari:
104 |
105 | ```python
106 | import scriptsmenu.launchformari as launchformari
107 |
108 | menu = launchformari.main(title="My Scripts")
109 |
110 | menu.add_script(parent=menu,
111 | title="Script A",
112 | command="print('A')",
113 | sourcetype='python',
114 | tags=["foobar", "nugget"])
115 |
116 | menu.add_script(parent=menu,
117 | title="Script B",
118 | command="print('B')",
119 | sourcetype='python',
120 | tags=["gold", "silver", "bronze"])
121 |
122 | ```
123 | ##### Configuration
124 |
125 | The menu can be reconstructed with help of a `.json` configuration file.
126 | The configuration of the menu is a list of dictionaries. The loader recognizes three types;
127 |
128 | * `menu`, a submenu for the main menu with its own actions
129 | * this is indicated with the key `"items"`
130 | * `action`, a script to run
131 | * `separator`, this is an aesthetical option but can help with separating certain actions which belong
132 | to the same group.
133 |
134 | The order the items appear in the list dictates the order in which is will be created.
135 |
136 | ```json
137 | [
138 | {
139 | "type": "action",
140 | "title": "Run Sanity Check",
141 | "command": "$SCRIPTSFOLDER\\general\\sanity_check.py",
142 | "sourcetype": "file",
143 | "tags": ["general","checks","pipeline"],
144 | "tooltip": "Run the sanity check to ensure pipeline friendly content"
145 | },
146 | {
147 | "type": "separator"
148 | },
149 | {
150 | "type": "menu",
151 | "title": "Animation",
152 | "items":[
153 | {
154 | "type": "action",
155 | "title": "Blendshapes UI",
156 | "command": "$SCRIPTSFOLDER\\animation\\blendshapes_ui.py",
157 | "sourcetype": "file",
158 | "tags": ["animation","blendshapes","UI"],
159 | "tooltip": "Open the Blendshapes UI"
160 | }
161 | ]
162 | }
163 | ]
164 | ```
165 |
166 |
167 |
168 | ### Advanced
169 |
170 |
171 | #### Relative paths
172 |
173 | To use relative paths in your scripts and icons you can use environment variables. Ensure the
174 | environment variable is set correctly and use it in the paths, like `$YOUR_ENV_VARIABLE`.
175 |
176 | A relative path for example could be set as `$SCRIPTS/relative/path/to/script.py`
177 | An example of this can be found in the samples folder of this package.
178 |
179 | #### Register callback
180 |
181 | You can override the callback behavior per modifier state. For example when you want special
182 | behavior when a menu item is clicked with _Control + Shift_ held at the same time.
183 |
184 | ```python
185 | from Qt import QtCore
186 | from scriptsmenu import ScriptsMenu
187 |
188 | def callback(action):
189 | """This will print a message prior to running the action"""
190 | print("Triggered with Control + Shift")
191 | action.run_command()
192 |
193 | # Control + Shift
194 | modifier = QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier
195 |
196 | menu = ScriptsMenu()
197 | menu.register_callback(modifier, callback)
198 | ```
199 |
200 | #### Update menu
201 |
202 | The ScriptsMenu has a signal called "updated" which can be connected to a function which
203 | rebuilds the menu
204 |
205 | ```python
206 | # This example is tested in Autodesk Maya
207 | import scriptsmenu.launchformaya as launchformaya
208 |
209 | # Here we create our own menu without any scripts
210 | menu = launchformaya.main(title="Custom Menu")
211 |
212 | # Set the update button visible, which is hidden by default
213 | menu.set_update_visible(True)
214 |
215 | # Add a custom script to the menu
216 | menu.add_script(parent=menu, title="Before", command='print("C")', sourcetype="python")
217 |
218 | # Create update function
219 | def update(menu):
220 | menu.clear_menu()
221 | menu.add_script(parent=menu, title="After", command='print("C")', sourcetype="python")
222 |
223 | # Link the update function to the update signal
224 | menu.updated.connect(update)
225 | ```
--------------------------------------------------------------------------------
/python/scriptsmenu/__init__.py:
--------------------------------------------------------------------------------
1 | from .scriptsmenu import ScriptsMenu
2 | from . import version
3 |
4 | __all__ = ["ScriptsMenu"]
5 | __version__ = version.version
6 |
--------------------------------------------------------------------------------
/python/scriptsmenu/action.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from .vendor.Qt import QtWidgets
4 |
5 |
6 | class Action(QtWidgets.QAction):
7 | """Custom Action widget"""
8 |
9 | def __init__(self, parent=None):
10 |
11 | QtWidgets.QAction.__init__(self, parent)
12 |
13 | self._root = None
14 | self._tags = list()
15 | self._command = None
16 | self._sourcetype = None
17 | self._iconfile = None
18 | self._label = None
19 |
20 | self._COMMAND = """import imp
21 | f, filepath, descr = imp.find_module('{module_name}', ['{dirname}'])
22 | module = imp.load_module('{module_name}', f, filepath, descr)
23 | module.{module_name}()"""
24 |
25 | @property
26 | def root(self):
27 | return self._root
28 |
29 | @root.setter
30 | def root(self, value):
31 | self._root = value
32 |
33 | @property
34 | def tags(self):
35 | return self._tags
36 |
37 | @tags.setter
38 | def tags(self, value):
39 | self._tags = value
40 |
41 | @property
42 | def command(self):
43 | return self._command
44 |
45 | @command.setter
46 | def command(self, value):
47 | """
48 | Store the command in the QAction
49 |
50 | Args:
51 | value (str): the full command which will be executed when clicked
52 |
53 | Return:
54 | None
55 | """
56 | self._command = value
57 |
58 | @property
59 | def sourcetype(self):
60 | return self._sourcetype
61 |
62 | @sourcetype.setter
63 | def sourcetype(self, value):
64 | """
65 | Set the command type to get the correct execution of the command given
66 |
67 | Args:
68 | value (str): the name of the command type
69 |
70 | Returns:
71 | None
72 |
73 | """
74 | self._sourcetype = value
75 |
76 | @property
77 | def iconfile(self):
78 | return self._iconfile
79 |
80 | @iconfile.setter
81 | def iconfile(self, value):
82 | """Store the path to the image file which needs to be displayed
83 |
84 | Args:
85 | value (str): the path to the image
86 |
87 | Returns:
88 | None
89 | """
90 | self._iconfile = value
91 |
92 | @property
93 | def label(self):
94 | return self._label
95 |
96 | @label.setter
97 | def label(self, value):
98 | """
99 | Set the abbreviation which will be used as overlay text in the shelf
100 |
101 | Args:
102 | value (str): an abbreviation of the name
103 |
104 | Returns:
105 | None
106 |
107 | """
108 | self._label = value
109 |
110 | def run_command(self):
111 | """
112 | Run the command of the instance or copy the command to the active shelf
113 | based on the current modifiers.
114 |
115 | If callbacks have been registered with fouind modifier integer the
116 | function will trigger all callbacks. When a callback function returns a
117 | non zero integer it will not execute the action's command
118 |
119 | """
120 |
121 | # get the current application and its linked keyboard modifiers
122 | app = QtWidgets.QApplication.instance()
123 | modifiers = app.keyboardModifiers()
124 |
125 | # If the menu has a callback registered for the current modifier
126 | # we run the callback instead of the action itself.
127 | registered = self._root.registered_callbacks
128 | callbacks = registered.get(int(modifiers), [])
129 | for callback in callbacks:
130 | signal = callback(self)
131 | if signal != 0:
132 | # Exit function on non-zero return code
133 | return
134 |
135 | exec(self.process_command())
136 |
137 | def process_command(self):
138 | """
139 | Check if the command is a file which needs to be launched and if it
140 | has a relative path, if so return the full path by expanding
141 | environment variables. Wrap any mel command in a executable string
142 | for Python and return the string if the source type is
143 |
144 | Add your own source type and required process to ensure callback
145 | is stored correctly.
146 |
147 | An example of a process is the sourcetype is MEL
148 | (Maya Embedded Language) as Python cannot run it on its own so it
149 | needs to be wrapped in a string in which we explicitly import mel and
150 | run it as a mel.eval. The string is then parsed to python as
151 | exec("command").
152 |
153 | Returns:
154 | str: a clean command which can be used
155 |
156 | """
157 | if self._sourcetype == "python":
158 | return self._command
159 |
160 | if self._sourcetype == "mel":
161 | # Escape single quotes
162 | conversion = self._command.replace("'", "\\'")
163 | return "import maya; maya.mel.eval('{}')".format(conversion)
164 |
165 | if self._sourcetype == "file":
166 | if os.path.isabs(self._command):
167 | filepath = self._command
168 | else:
169 | filepath = os.path.normpath(os.path.expandvars(self._command))
170 |
171 | return self._wrap_filepath(filepath)
172 |
173 | def has_tag(self, tag):
174 | """Check whether the tag matches with the action's tags.
175 |
176 | A partial match will also return True, for example tag `a` will match
177 | correctly with the `ape` tag on the Action.
178 |
179 | Args:
180 | tag (str): The tag
181 |
182 | Returns
183 | bool: Whether the action is tagged with given tag
184 |
185 | """
186 |
187 | for tagitem in self.tags:
188 | if tag not in tagitem:
189 | continue
190 | return True
191 |
192 | return False
193 |
194 | def _wrap_filepath(self, file_path):
195 | """Create a wrapped string for the python command
196 |
197 | Args:
198 | file_path (str): the filepath of a script
199 |
200 | Returns:
201 | str: the wrapped command
202 | """
203 |
204 | dirname = os.path.dirname(r"{}".format(file_path))
205 | dirpath = dirname.replace("\\", "/")
206 | module_name = os.path.splitext(os.path.basename(file_path))[0]
207 |
208 | return self._COMMAND.format(module_name=module_name, dirname=dirpath)
209 |
--------------------------------------------------------------------------------
/python/scriptsmenu/launchformari.py:
--------------------------------------------------------------------------------
1 |
2 | # Import third-party modules
3 | from vendor.Qt import QtWidgets
4 |
5 | # Import local modules
6 | import scriptsmenu
7 |
8 |
9 | def _mari_main_window():
10 | """Get Mari main window.
11 |
12 | Returns:
13 | MriMainWindow: Mari's main window.
14 |
15 | """
16 | for obj in QtWidgets.QApplication.topLevelWidgets():
17 | if obj.metaObject().className() == 'MriMainWindow':
18 | return obj
19 | raise RuntimeError('Could not find Mari MainWindow instance')
20 |
21 |
22 | def _mari_main_menubar():
23 | """Get Mari main menu bar.
24 |
25 | Returns:
26 | Retrieve the main menubar of the Mari window.
27 |
28 | """
29 | mari_window = _mari_main_window()
30 | menubar = [
31 | i for i in mari_window.children() if isinstance(i, QtWidgets.QMenuBar)
32 | ]
33 | assert len(menubar) == 1, "Error, could not find menu bar!"
34 | return menubar[0]
35 |
36 |
37 | def main(title="Scripts"):
38 | """Build the main scripts menu in Mari.
39 |
40 | Args:
41 | title (str): Name of the menu in the application.
42 |
43 | Returns:
44 | scriptsmenu.ScriptsMenu: Instance object.
45 |
46 | """
47 | mari_main_bar = _mari_main_menubar()
48 | for mari_bar in mari_main_bar.children():
49 | if isinstance(mari_bar, scriptsmenu.ScriptsMenu):
50 | if mari_bar.title() == title:
51 | menu = mari_bar
52 | return menu
53 | menu = scriptsmenu.ScriptsMenu(title=title, parent=mari_main_bar)
54 | return menu
55 |
--------------------------------------------------------------------------------
/python/scriptsmenu/launchformaya.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | import maya.cmds as cmds
4 | import maya.mel as mel
5 |
6 | import scriptsmenu
7 | from .vendor.Qt import QtCore, QtWidgets
8 |
9 | log = logging.getLogger(__name__)
10 |
11 |
12 | def register_repeat_last(action):
13 | """Register the action in repeatLast to ensure the RepeatLast hotkey works
14 |
15 | Args:
16 | action (action.Action): Action wigdet instance
17 |
18 | Returns:
19 | int: 0
20 |
21 | """
22 | command = action.process_command()
23 | command = command.replace("\n", "; ")
24 | # Register command to Maya (mel)
25 | cmds.repeatLast(addCommand='python("{}")'.format(command),
26 | addCommandLabel=action.label)
27 |
28 | return 0
29 |
30 |
31 | def to_shelf(action):
32 | """Copy clicked menu item to the currently active Maya shelf
33 | Args:
34 | action (action.Action): the action instance which is clicked
35 |
36 | Returns:
37 | int: 1
38 |
39 | """
40 |
41 | shelftoplevel = mel.eval("$gShelfTopLevel = $gShelfTopLevel;")
42 | current_active_shelf = cmds.tabLayout(shelftoplevel,
43 | query=True,
44 | selectTab=True)
45 |
46 | cmds.shelfButton(command=action.process_command(),
47 | sourceType="python",
48 | parent=current_active_shelf,
49 | image=action.iconfile or "pythonFamily.png",
50 | annotation=action.statusTip(),
51 | imageOverlayLabel=action.label or "")
52 |
53 | return 1
54 |
55 |
56 | def _maya_main_window():
57 | """Return Maya's main window"""
58 | for obj in QtWidgets.QApplication.topLevelWidgets():
59 | if obj.objectName() == 'MayaWindow':
60 | return obj
61 | raise RuntimeError('Could not find MayaWindow instance')
62 |
63 |
64 | def _maya_main_menubar():
65 | """Retrieve the main menubar of the Maya window"""
66 | mayawindow = _maya_main_window()
67 | menubar = [i for i in mayawindow.children()
68 | if isinstance(i, QtWidgets.QMenuBar)]
69 |
70 | assert len(menubar) == 1, "Error, could not find menu bar!"
71 |
72 | return menubar[0]
73 |
74 |
75 | def find_scripts_menu(title, parent):
76 | """
77 | Check if the menu exists with the given title in the parent
78 |
79 | Args:
80 | title (str): the title name of the scripts menu
81 |
82 | parent (QtWidgets.QMenuBar): the menubar to check
83 |
84 | Returns:
85 | QtWidgets.QMenu or None
86 |
87 | """
88 |
89 | menu = None
90 | search = [i for i in parent.children() if
91 | isinstance(i, scriptsmenu.ScriptsMenu)
92 | and i.title() == title]
93 |
94 | if search:
95 | assert len(search) < 2, ("Multiple instances of menu '{}' "
96 | "in menu bar".format(title))
97 | menu = search[0]
98 |
99 | return menu
100 |
101 |
102 | def main(title="Scripts", parent=None, objectName=None):
103 | """Build the main scripts menu in Maya
104 |
105 | Args:
106 | title (str): name of the menu in the application
107 |
108 | parent (QtWidgets.QtMenuBar): the parent object for the menu
109 |
110 | objectName (str): custom objectName for scripts menu
111 |
112 | Returns:
113 | scriptsmenu.ScriptsMenu instance
114 |
115 | """
116 |
117 | mayamainbar = parent or _maya_main_menubar()
118 | try:
119 | # check menu already exists
120 | menu = find_scripts_menu(title, mayamainbar)
121 | if not menu:
122 | log.info("Attempting to build menu ...")
123 | object_name = objectName or title.lower()
124 | menu = scriptsmenu.ScriptsMenu(title=title,
125 | parent=mayamainbar,
126 | objectName=object_name)
127 | except Exception as e:
128 | log.error(e)
129 | return
130 |
131 | # Register control + shift callback to add to shelf (maya behavior)
132 | modifiers = QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier
133 | menu.register_callback(int(modifiers), to_shelf)
134 |
135 | menu.register_callback(0, register_repeat_last)
136 |
137 | return menu
138 |
--------------------------------------------------------------------------------
/python/scriptsmenu/launchfornuke.py:
--------------------------------------------------------------------------------
1 | import scriptsmenu
2 | from .vendor.Qt import QtWidgets
3 |
4 |
5 | def _nuke_main_window():
6 | """Return Nuke's main window"""
7 | for obj in QtWidgets.QApplication.topLevelWidgets():
8 | if (obj.inherits('QMainWindow') and
9 | obj.metaObject().className() == 'Foundry::UI::DockMainWindow'):
10 | return obj
11 | raise RuntimeError('Could not find Nuke MainWindow instance')
12 |
13 |
14 | def _nuke_main_menubar():
15 | """Retrieve the main menubar of the Nuke window"""
16 | nuke_window = _nuke_main_window()
17 | menubar = [i for i in nuke_window.children()
18 | if isinstance(i, QtWidgets.QMenuBar)]
19 |
20 | assert len(menubar) == 1, "Error, could not find menu bar!"
21 | return menubar[0]
22 |
23 |
24 | def main(title="Scripts"):
25 | nuke_main_bar = _nuke_main_menubar()
26 | for nuke_bar in nuke_main_bar.children():
27 | if isinstance(nuke_bar, scriptsmenu.ScriptsMenu):
28 | if nuke_bar.title() == title:
29 | menu = nuke_bar
30 | return menu
31 |
32 | menu = scriptsmenu.ScriptsMenu(title=title, parent=nuke_main_bar)
33 | return menu
34 |
--------------------------------------------------------------------------------
/python/scriptsmenu/scriptsmenu.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 | import logging
4 | from collections import defaultdict
5 |
6 | from .vendor.Qt import QtWidgets, QtCore
7 | from . import action
8 |
9 | log = logging.getLogger(__name__)
10 |
11 |
12 | class ScriptsMenu(QtWidgets.QMenu):
13 | """A Qt menu that displays a list of searchable actions"""
14 |
15 | updated = QtCore.Signal(QtWidgets.QMenu)
16 |
17 | def __init__(self, *args, **kwargs):
18 | """Initialize Scripts menu
19 |
20 | Args:
21 | title (str): the name of the root menu which will be created
22 |
23 | parent (QtWidgets.QObject) : the QObject to parent the menu to
24 |
25 | Returns:
26 | None
27 |
28 | """
29 | QtWidgets.QMenu.__init__(self, *args, **kwargs)
30 |
31 | self.searchbar = None
32 | self.update_action = None
33 |
34 | self._script_actions = []
35 | self._callbacks = defaultdict(list)
36 |
37 | # Automatically add it to the parent menu
38 | parent = kwargs.get("parent", None)
39 | if parent:
40 | parent.addMenu(self)
41 |
42 | objectname = kwargs.get("objectName", "scripts")
43 | title = kwargs.get("title", "Scripts")
44 | self.setObjectName(objectname)
45 | self.setTitle(title)
46 |
47 | # add default items in the menu
48 | self.create_default_items()
49 |
50 | def on_update(self):
51 | self.updated.emit(self)
52 |
53 | @property
54 | def registered_callbacks(self):
55 | return self._callbacks.copy()
56 |
57 | def create_default_items(self):
58 | """Add a search bar to the top of the menu given"""
59 |
60 | # create widget and link function
61 | searchbar = QtWidgets.QLineEdit()
62 | searchbar.setFixedWidth(120)
63 | searchbar.setPlaceholderText("Search ...")
64 | searchbar.textChanged.connect(self._update_search)
65 | self.searchbar = searchbar
66 |
67 | # create widget holder
68 | searchbar_action = QtWidgets.QWidgetAction(self)
69 |
70 | # add widget to widget holder
71 | searchbar_action.setDefaultWidget(self.searchbar)
72 | searchbar_action.setObjectName("Searchbar")
73 |
74 | # add update button and link function
75 | update_action = QtWidgets.QAction(self)
76 | update_action.setObjectName("Update Scripts")
77 | update_action.setText("Update Scripts")
78 | update_action.setVisible(False)
79 | update_action.triggered.connect(self.on_update)
80 | self.update_action = update_action
81 |
82 | # add action to menu
83 | self.addAction(searchbar_action)
84 | self.addAction(update_action)
85 |
86 | # add separator object
87 | separator = self.addSeparator()
88 | separator.setObjectName("separator")
89 |
90 | def add_menu(self, title, parent=None):
91 | """Create a sub menu for a parent widget
92 |
93 | Args:
94 | parent(QtWidgets.QWidget): the object to parent the menu to
95 |
96 | title(str): the title of the menu
97 |
98 | Returns:
99 | QtWidget.QMenu instance
100 | """
101 |
102 | if not parent:
103 | parent = self
104 |
105 | menu = QtWidgets.QMenu(parent, title)
106 | menu.setTitle(title)
107 | menu.setObjectName(title)
108 | menu.setTearOffEnabled(True)
109 | parent.addMenu(menu)
110 |
111 | return menu
112 |
113 | def add_script(self, parent, title, command, sourcetype, icon=None,
114 | tags=None, label=None, tooltip=None):
115 | """Create an action item which runs a script when clicked
116 |
117 | Args:
118 | parent (QtWidget.QWidget): The widget to parent the item to
119 |
120 | title (str): The text which will be displayed in the menu
121 |
122 | command (str): The command which needs to be run when the item is
123 | clicked.
124 |
125 | sourcetype (str): The type of command, the way the command is
126 | processed is based on the source type.
127 |
128 | icon (str): The file path of an icon to display with the menu item
129 |
130 | tags (list, tuple): Keywords which describe the action
131 |
132 | label (str): A short description of the script which will be displayed
133 | when hovering over the menu item
134 |
135 | tooltip (str): A tip for the user about the usage fo the tool
136 |
137 | Returns:
138 | QtWidget.QAction instance
139 |
140 | """
141 |
142 | assert tags is None or isinstance(tags, (list, tuple))
143 | # Ensure tags is a list
144 | tags = list() if tags is None else list(tags)
145 | tags.append(title.lower())
146 |
147 | assert icon is None or isinstance(icon, str), (
148 | "Invalid data type for icon, supported : None, string")
149 |
150 | # create new action
151 | script_action = action.Action(parent)
152 | script_action.setText(title)
153 | script_action.setObjectName(title)
154 | script_action.tags = tags
155 |
156 | # link action to root for callback library
157 | script_action.root = self
158 |
159 | # Set up the command
160 | script_action.sourcetype = sourcetype
161 | script_action.command = command
162 |
163 | try:
164 | script_action.process_command()
165 | except RuntimeError as e:
166 | raise RuntimeError("Script action can't be "
167 | "processed: {}".format(e))
168 |
169 | if icon:
170 | iconfile = os.path.expandvars(icon)
171 | script_action.iconfile = iconfile
172 | script_action_icon = QtWidgets.QIcon(iconfile)
173 | script_action.setIcon(script_action_icon)
174 |
175 | if label:
176 | script_action.label = label
177 |
178 | if tooltip:
179 | script_action.setStatusTip(tooltip)
180 |
181 | script_action.triggered.connect(script_action.run_command)
182 | parent.addAction(script_action)
183 |
184 | # Add to our searchable actions
185 | self._script_actions.append(script_action)
186 |
187 | return script_action
188 |
189 | def build_from_configuration(self, parent, configuration):
190 | """Process the configurations and store the configuration
191 |
192 | This creates all submenus from a configuration.json file.
193 |
194 | When the configuration holds the key `main` all scripts under `main` will
195 | be added to the main menu first before adding the rest
196 |
197 | Args:
198 | parent (ScriptsMenu): script menu instance
199 | configuration (list): A ScriptsMenu configuration list
200 |
201 | Returns:
202 | None
203 |
204 | """
205 |
206 | for item in configuration:
207 | assert isinstance(item, dict), "Configuration is wrong!"
208 |
209 | # skip items which have no `type` key
210 | item_type = item.get('type', None)
211 | if not item_type:
212 | log.warning("Missing 'type' from configuration item")
213 | continue
214 |
215 | # add separator
216 | # Special behavior for separators
217 | if item_type == "separator":
218 | parent.addSeparator()
219 |
220 | # add submenu
221 | # items should hold a collection of submenu items (dict)
222 | elif item_type == "menu":
223 | assert "items" in item, "Menu is missing 'items' key"
224 | menu = self.add_menu(parent=parent, title=item["title"])
225 | self.build_from_configuration(menu, item["items"])
226 |
227 | # add script
228 | elif item_type == "action":
229 | # filter out `type` from the item dict
230 | config = {key: value for key, value in
231 | item.items() if key != "type"}
232 |
233 | self.add_script(parent=parent, **config)
234 |
235 | def set_update_visible(self, state):
236 | self.update_action.setVisible(state)
237 |
238 | def clear_menu(self):
239 | """Clear all menu items which are not default
240 |
241 | Returns:
242 | None
243 |
244 | """
245 |
246 | # TODO: Set up a more robust implementation for this
247 | # Delete all except the first three actions
248 | for _action in self.actions()[3:]:
249 | self.removeAction(_action)
250 |
251 | def register_callback(self, modifiers, callback):
252 | self._callbacks[modifiers].append(callback)
253 |
254 | def _update_search(self, search):
255 | """Hide all the samples which do not match the user's import
256 |
257 | Returns:
258 | None
259 |
260 | """
261 |
262 | if not search:
263 | for action in self._script_actions:
264 | action.setVisible(True)
265 | else:
266 | for action in self._script_actions:
267 | action.setVisible(action.has_tag(search.lower()))
268 |
269 | # Set visibility for all submenus
270 | for action in self.actions():
271 | if not action.menu():
272 | continue
273 |
274 | menu = action.menu()
275 | visible = any(action.isVisible() for action in menu.actions())
276 | action.setVisible(visible)
277 |
278 |
279 | def load_configuration(path):
280 | """Load the configuration from a file
281 |
282 | Read out the JSON file which will dictate the structure of the scripts menu
283 |
284 | Args:
285 | path (str): file path of the .JSON file
286 |
287 | Returns:
288 | dict
289 |
290 | """
291 |
292 | if not os.path.isfile(path):
293 | raise AttributeError("Given configuration is not "
294 | "a file!\n'{}'".format(path))
295 |
296 | extension = os.path.splitext(path)[-1]
297 | if extension != ".json":
298 | raise AttributeError("Given configuration file has unsupported "
299 | "file type, provide a .json file")
300 |
301 | # retrieve and store config
302 | with open(path, "r") as f:
303 | configuration = json.load(f)
304 |
305 | return configuration
306 |
307 |
308 | def application(configuration, parent):
309 | import sys
310 | app = QtWidgets.QApplication(sys.argv)
311 |
312 | scriptsmenu = ScriptsMenu(configuration, parent)
313 | scriptsmenu.show()
314 |
315 | sys.exit(app.exec_())
316 |
--------------------------------------------------------------------------------
/python/scriptsmenu/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 - PySide,
7 | PySide2, PyQt4 and PyQt5.
8 |
9 | 1. Build for one, run with all
10 | 2. Explicit is better than implicit
11 | 3. Support co-existence
12 |
13 | Default resolution order:
14 | - PySide2
15 | - PyQt5
16 | - PySide
17 | - PyQt4
18 |
19 | Usage:
20 | >> import sys
21 | >> from Qt import QtWidgets
22 | >> app = QtWidgets.QApplication(sys.argv)
23 | >> button = QtWidgets.QPushButton("Hello World")
24 | >> button.show()
25 | >> app.exec_()
26 |
27 | All members of PySide2 are mapped from other bindings, should they exist.
28 | If no equivalent member exist, it is excluded from Qt.py and inaccessible.
29 | The idea is to highlight members that exist across all supported binding,
30 | and guarantee that code that runs on one binding runs on all others.
31 |
32 | For more details, visit https://github.com/mottosso/Qt.py
33 |
34 | LICENSE
35 |
36 | See end of file for license (MIT, BSD) information.
37 |
38 | """
39 |
40 | import os
41 | import sys
42 | import types
43 | import shutil
44 | import importlib
45 |
46 |
47 | __version__ = "1.2.3"
48 |
49 | # Enable support for `from Qt import *`
50 | __all__ = []
51 |
52 | # Flags from environment variables
53 | QT_VERBOSE = bool(os.getenv("QT_VERBOSE"))
54 | QT_PREFERRED_BINDING = os.getenv("QT_PREFERRED_BINDING", "")
55 | QT_SIP_API_HINT = os.getenv("QT_SIP_API_HINT")
56 |
57 | # Reference to Qt.py
58 | Qt = sys.modules[__name__]
59 | Qt.QtCompat = types.ModuleType("QtCompat")
60 |
61 | try:
62 | long
63 | except NameError:
64 | # Python 3 compatibility
65 | long = int
66 |
67 |
68 | """Common members of all bindings
69 |
70 | This is where each member of Qt.py is explicitly defined.
71 | It is based on a "lowest common denominator" of all bindings;
72 | including members found in each of the 4 bindings.
73 |
74 | The "_common_members" dictionary is generated using the
75 | build_membership.sh script.
76 |
77 | """
78 |
79 | _common_members = {
80 | "QtCore": [
81 | "QAbstractAnimation",
82 | "QAbstractEventDispatcher",
83 | "QAbstractItemModel",
84 | "QAbstractListModel",
85 | "QAbstractState",
86 | "QAbstractTableModel",
87 | "QAbstractTransition",
88 | "QAnimationGroup",
89 | "QBasicTimer",
90 | "QBitArray",
91 | "QBuffer",
92 | "QByteArray",
93 | "QByteArrayMatcher",
94 | "QChildEvent",
95 | "QCoreApplication",
96 | "QCryptographicHash",
97 | "QDataStream",
98 | "QDate",
99 | "QDateTime",
100 | "QDir",
101 | "QDirIterator",
102 | "QDynamicPropertyChangeEvent",
103 | "QEasingCurve",
104 | "QElapsedTimer",
105 | "QEvent",
106 | "QEventLoop",
107 | "QEventTransition",
108 | "QFile",
109 | "QFileInfo",
110 | "QFileSystemWatcher",
111 | "QFinalState",
112 | "QGenericArgument",
113 | "QGenericReturnArgument",
114 | "QHistoryState",
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 | "QRegExp",
146 | "QResource",
147 | "QRunnable",
148 | "QSemaphore",
149 | "QSequentialAnimationGroup",
150 | "QSettings",
151 | "QSignalMapper",
152 | "QSignalTransition",
153 | "QSize",
154 | "QSizeF",
155 | "QSocketNotifier",
156 | "QState",
157 | "QStateMachine",
158 | "QSysInfo",
159 | "QSystemSemaphore",
160 | "QT_TRANSLATE_NOOP",
161 | "QT_TR_NOOP",
162 | "QT_TR_NOOP_UTF8",
163 | "QTemporaryFile",
164 | "QTextBoundaryFinder",
165 | "QTextCodec",
166 | "QTextDecoder",
167 | "QTextEncoder",
168 | "QTextStream",
169 | "QTextStreamManipulator",
170 | "QThread",
171 | "QThreadPool",
172 | "QTime",
173 | "QTimeLine",
174 | "QTimer",
175 | "QTimerEvent",
176 | "QTranslator",
177 | "QUrl",
178 | "QVariantAnimation",
179 | "QWaitCondition",
180 | "QWriteLocker",
181 | "QXmlStreamAttribute",
182 | "QXmlStreamAttributes",
183 | "QXmlStreamEntityDeclaration",
184 | "QXmlStreamEntityResolver",
185 | "QXmlStreamNamespaceDeclaration",
186 | "QXmlStreamNotationDeclaration",
187 | "QXmlStreamReader",
188 | "QXmlStreamWriter",
189 | "Qt",
190 | "QtCriticalMsg",
191 | "QtDebugMsg",
192 | "QtFatalMsg",
193 | "QtMsgType",
194 | "QtSystemMsg",
195 | "QtWarningMsg",
196 | "qAbs",
197 | "qAddPostRoutine",
198 | "qChecksum",
199 | "qCritical",
200 | "qDebug",
201 | "qFatal",
202 | "qFuzzyCompare",
203 | "qIsFinite",
204 | "qIsInf",
205 | "qIsNaN",
206 | "qIsNull",
207 | "qRegisterResourceData",
208 | "qUnregisterResourceData",
209 | "qVersion",
210 | "qWarning",
211 | "qrand",
212 | "qsrand"
213 | ],
214 | "QtGui": [
215 | "QAbstractTextDocumentLayout",
216 | "QActionEvent",
217 | "QBitmap",
218 | "QBrush",
219 | "QClipboard",
220 | "QCloseEvent",
221 | "QColor",
222 | "QConicalGradient",
223 | "QContextMenuEvent",
224 | "QCursor",
225 | "QDesktopServices",
226 | "QDoubleValidator",
227 | "QDrag",
228 | "QDragEnterEvent",
229 | "QDragLeaveEvent",
230 | "QDragMoveEvent",
231 | "QDropEvent",
232 | "QFileOpenEvent",
233 | "QFocusEvent",
234 | "QFont",
235 | "QFontDatabase",
236 | "QFontInfo",
237 | "QFontMetrics",
238 | "QFontMetricsF",
239 | "QGradient",
240 | "QHelpEvent",
241 | "QHideEvent",
242 | "QHoverEvent",
243 | "QIcon",
244 | "QIconDragEvent",
245 | "QIconEngine",
246 | "QImage",
247 | "QImageIOHandler",
248 | "QImageReader",
249 | "QImageWriter",
250 | "QInputEvent",
251 | "QInputMethodEvent",
252 | "QIntValidator",
253 | "QKeyEvent",
254 | "QKeySequence",
255 | "QLinearGradient",
256 | "QMatrix2x2",
257 | "QMatrix2x3",
258 | "QMatrix2x4",
259 | "QMatrix3x2",
260 | "QMatrix3x3",
261 | "QMatrix3x4",
262 | "QMatrix4x2",
263 | "QMatrix4x3",
264 | "QMatrix4x4",
265 | "QMouseEvent",
266 | "QMoveEvent",
267 | "QMovie",
268 | "QPaintDevice",
269 | "QPaintEngine",
270 | "QPaintEngineState",
271 | "QPaintEvent",
272 | "QPainter",
273 | "QPainterPath",
274 | "QPainterPathStroker",
275 | "QPalette",
276 | "QPen",
277 | "QPicture",
278 | "QPictureIO",
279 | "QPixmap",
280 | "QPixmapCache",
281 | "QPolygon",
282 | "QPolygonF",
283 | "QQuaternion",
284 | "QRadialGradient",
285 | "QRegExpValidator",
286 | "QRegion",
287 | "QResizeEvent",
288 | "QSessionManager",
289 | "QShortcutEvent",
290 | "QShowEvent",
291 | "QStandardItem",
292 | "QStandardItemModel",
293 | "QStatusTipEvent",
294 | "QSyntaxHighlighter",
295 | "QTabletEvent",
296 | "QTextBlock",
297 | "QTextBlockFormat",
298 | "QTextBlockGroup",
299 | "QTextBlockUserData",
300 | "QTextCharFormat",
301 | "QTextCursor",
302 | "QTextDocument",
303 | "QTextDocumentFragment",
304 | "QTextFormat",
305 | "QTextFragment",
306 | "QTextFrame",
307 | "QTextFrameFormat",
308 | "QTextImageFormat",
309 | "QTextInlineObject",
310 | "QTextItem",
311 | "QTextLayout",
312 | "QTextLength",
313 | "QTextLine",
314 | "QTextList",
315 | "QTextListFormat",
316 | "QTextObject",
317 | "QTextObjectInterface",
318 | "QTextOption",
319 | "QTextTable",
320 | "QTextTableCell",
321 | "QTextTableCellFormat",
322 | "QTextTableFormat",
323 | "QTouchEvent",
324 | "QTransform",
325 | "QValidator",
326 | "QVector2D",
327 | "QVector3D",
328 | "QVector4D",
329 | "QWhatsThisClickedEvent",
330 | "QWheelEvent",
331 | "QWindowStateChangeEvent",
332 | "qAlpha",
333 | "qBlue",
334 | "qGray",
335 | "qGreen",
336 | "qIsGray",
337 | "qRed",
338 | "qRgb",
339 | "qRgba"
340 | ],
341 | "QtHelp": [
342 | "QHelpContentItem",
343 | "QHelpContentModel",
344 | "QHelpContentWidget",
345 | "QHelpEngine",
346 | "QHelpEngineCore",
347 | "QHelpIndexModel",
348 | "QHelpIndexWidget",
349 | "QHelpSearchEngine",
350 | "QHelpSearchQuery",
351 | "QHelpSearchQueryWidget",
352 | "QHelpSearchResultWidget"
353 | ],
354 | "QtMultimedia": [
355 | "QAbstractVideoBuffer",
356 | "QAbstractVideoSurface",
357 | "QAudio",
358 | "QAudioDeviceInfo",
359 | "QAudioFormat",
360 | "QAudioInput",
361 | "QAudioOutput",
362 | "QVideoFrame",
363 | "QVideoSurfaceFormat"
364 | ],
365 | "QtNetwork": [
366 | "QAbstractNetworkCache",
367 | "QAbstractSocket",
368 | "QAuthenticator",
369 | "QHostAddress",
370 | "QHostInfo",
371 | "QLocalServer",
372 | "QLocalSocket",
373 | "QNetworkAccessManager",
374 | "QNetworkAddressEntry",
375 | "QNetworkCacheMetaData",
376 | "QNetworkConfiguration",
377 | "QNetworkConfigurationManager",
378 | "QNetworkCookie",
379 | "QNetworkCookieJar",
380 | "QNetworkDiskCache",
381 | "QNetworkInterface",
382 | "QNetworkProxy",
383 | "QNetworkProxyFactory",
384 | "QNetworkProxyQuery",
385 | "QNetworkReply",
386 | "QNetworkRequest",
387 | "QNetworkSession",
388 | "QSsl",
389 | "QTcpServer",
390 | "QTcpSocket",
391 | "QUdpSocket"
392 | ],
393 | "QtOpenGL": [
394 | "QGL",
395 | "QGLContext",
396 | "QGLFormat",
397 | "QGLWidget"
398 | ],
399 | "QtPrintSupport": [
400 | "QAbstractPrintDialog",
401 | "QPageSetupDialog",
402 | "QPrintDialog",
403 | "QPrintEngine",
404 | "QPrintPreviewDialog",
405 | "QPrintPreviewWidget",
406 | "QPrinter",
407 | "QPrinterInfo"
408 | ],
409 | "QtSql": [
410 | "QSql",
411 | "QSqlDatabase",
412 | "QSqlDriver",
413 | "QSqlDriverCreatorBase",
414 | "QSqlError",
415 | "QSqlField",
416 | "QSqlIndex",
417 | "QSqlQuery",
418 | "QSqlQueryModel",
419 | "QSqlRecord",
420 | "QSqlRelation",
421 | "QSqlRelationalDelegate",
422 | "QSqlRelationalTableModel",
423 | "QSqlResult",
424 | "QSqlTableModel"
425 | ],
426 | "QtSvg": [
427 | "QGraphicsSvgItem",
428 | "QSvgGenerator",
429 | "QSvgRenderer",
430 | "QSvgWidget"
431 | ],
432 | "QtTest": [
433 | "QTest"
434 | ],
435 | "QtWidgets": [
436 | "QAbstractButton",
437 | "QAbstractGraphicsShapeItem",
438 | "QAbstractItemDelegate",
439 | "QAbstractItemView",
440 | "QAbstractScrollArea",
441 | "QAbstractSlider",
442 | "QAbstractSpinBox",
443 | "QAction",
444 | "QActionGroup",
445 | "QApplication",
446 | "QBoxLayout",
447 | "QButtonGroup",
448 | "QCalendarWidget",
449 | "QCheckBox",
450 | "QColorDialog",
451 | "QColumnView",
452 | "QComboBox",
453 | "QCommandLinkButton",
454 | "QCommonStyle",
455 | "QCompleter",
456 | "QDataWidgetMapper",
457 | "QDateEdit",
458 | "QDateTimeEdit",
459 | "QDesktopWidget",
460 | "QDial",
461 | "QDialog",
462 | "QDialogButtonBox",
463 | "QDirModel",
464 | "QDockWidget",
465 | "QDoubleSpinBox",
466 | "QErrorMessage",
467 | "QFileDialog",
468 | "QFileIconProvider",
469 | "QFileSystemModel",
470 | "QFocusFrame",
471 | "QFontComboBox",
472 | "QFontDialog",
473 | "QFormLayout",
474 | "QFrame",
475 | "QGesture",
476 | "QGestureEvent",
477 | "QGestureRecognizer",
478 | "QGraphicsAnchor",
479 | "QGraphicsAnchorLayout",
480 | "QGraphicsBlurEffect",
481 | "QGraphicsColorizeEffect",
482 | "QGraphicsDropShadowEffect",
483 | "QGraphicsEffect",
484 | "QGraphicsEllipseItem",
485 | "QGraphicsGridLayout",
486 | "QGraphicsItem",
487 | "QGraphicsItemGroup",
488 | "QGraphicsLayout",
489 | "QGraphicsLayoutItem",
490 | "QGraphicsLineItem",
491 | "QGraphicsLinearLayout",
492 | "QGraphicsObject",
493 | "QGraphicsOpacityEffect",
494 | "QGraphicsPathItem",
495 | "QGraphicsPixmapItem",
496 | "QGraphicsPolygonItem",
497 | "QGraphicsProxyWidget",
498 | "QGraphicsRectItem",
499 | "QGraphicsRotation",
500 | "QGraphicsScale",
501 | "QGraphicsScene",
502 | "QGraphicsSceneContextMenuEvent",
503 | "QGraphicsSceneDragDropEvent",
504 | "QGraphicsSceneEvent",
505 | "QGraphicsSceneHelpEvent",
506 | "QGraphicsSceneHoverEvent",
507 | "QGraphicsSceneMouseEvent",
508 | "QGraphicsSceneMoveEvent",
509 | "QGraphicsSceneResizeEvent",
510 | "QGraphicsSceneWheelEvent",
511 | "QGraphicsSimpleTextItem",
512 | "QGraphicsTextItem",
513 | "QGraphicsTransform",
514 | "QGraphicsView",
515 | "QGraphicsWidget",
516 | "QGridLayout",
517 | "QGroupBox",
518 | "QHBoxLayout",
519 | "QHeaderView",
520 | "QInputDialog",
521 | "QItemDelegate",
522 | "QItemEditorCreatorBase",
523 | "QItemEditorFactory",
524 | "QKeyEventTransition",
525 | "QLCDNumber",
526 | "QLabel",
527 | "QLayout",
528 | "QLayoutItem",
529 | "QLineEdit",
530 | "QListView",
531 | "QListWidget",
532 | "QListWidgetItem",
533 | "QMainWindow",
534 | "QMdiArea",
535 | "QMdiSubWindow",
536 | "QMenu",
537 | "QMenuBar",
538 | "QMessageBox",
539 | "QMouseEventTransition",
540 | "QPanGesture",
541 | "QPinchGesture",
542 | "QPlainTextDocumentLayout",
543 | "QPlainTextEdit",
544 | "QProgressBar",
545 | "QProgressDialog",
546 | "QPushButton",
547 | "QRadioButton",
548 | "QRubberBand",
549 | "QScrollArea",
550 | "QScrollBar",
551 | "QShortcut",
552 | "QSizeGrip",
553 | "QSizePolicy",
554 | "QSlider",
555 | "QSpacerItem",
556 | "QSpinBox",
557 | "QSplashScreen",
558 | "QSplitter",
559 | "QSplitterHandle",
560 | "QStackedLayout",
561 | "QStackedWidget",
562 | "QStatusBar",
563 | "QStyle",
564 | "QStyleFactory",
565 | "QStyleHintReturn",
566 | "QStyleHintReturnMask",
567 | "QStyleHintReturnVariant",
568 | "QStyleOption",
569 | "QStyleOptionButton",
570 | "QStyleOptionComboBox",
571 | "QStyleOptionComplex",
572 | "QStyleOptionDockWidget",
573 | "QStyleOptionFocusRect",
574 | "QStyleOptionFrame",
575 | "QStyleOptionGraphicsItem",
576 | "QStyleOptionGroupBox",
577 | "QStyleOptionHeader",
578 | "QStyleOptionMenuItem",
579 | "QStyleOptionProgressBar",
580 | "QStyleOptionRubberBand",
581 | "QStyleOptionSizeGrip",
582 | "QStyleOptionSlider",
583 | "QStyleOptionSpinBox",
584 | "QStyleOptionTab",
585 | "QStyleOptionTabBarBase",
586 | "QStyleOptionTabWidgetFrame",
587 | "QStyleOptionTitleBar",
588 | "QStyleOptionToolBar",
589 | "QStyleOptionToolBox",
590 | "QStyleOptionToolButton",
591 | "QStyleOptionViewItem",
592 | "QStylePainter",
593 | "QStyledItemDelegate",
594 | "QSwipeGesture",
595 | "QSystemTrayIcon",
596 | "QTabBar",
597 | "QTabWidget",
598 | "QTableView",
599 | "QTableWidget",
600 | "QTableWidgetItem",
601 | "QTableWidgetSelectionRange",
602 | "QTapAndHoldGesture",
603 | "QTapGesture",
604 | "QTextBrowser",
605 | "QTextEdit",
606 | "QTimeEdit",
607 | "QToolBar",
608 | "QToolBox",
609 | "QToolButton",
610 | "QToolTip",
611 | "QTreeView",
612 | "QTreeWidget",
613 | "QTreeWidgetItem",
614 | "QTreeWidgetItemIterator",
615 | "QUndoCommand",
616 | "QUndoGroup",
617 | "QUndoStack",
618 | "QUndoView",
619 | "QVBoxLayout",
620 | "QWhatsThis",
621 | "QWidget",
622 | "QWidgetAction",
623 | "QWidgetItem",
624 | "QWizard",
625 | "QWizardPage"
626 | ],
627 | "QtX11Extras": [
628 | "QX11Info"
629 | ],
630 | "QtXml": [
631 | "QDomAttr",
632 | "QDomCDATASection",
633 | "QDomCharacterData",
634 | "QDomComment",
635 | "QDomDocument",
636 | "QDomDocumentFragment",
637 | "QDomDocumentType",
638 | "QDomElement",
639 | "QDomEntity",
640 | "QDomEntityReference",
641 | "QDomImplementation",
642 | "QDomNamedNodeMap",
643 | "QDomNode",
644 | "QDomNodeList",
645 | "QDomNotation",
646 | "QDomProcessingInstruction",
647 | "QDomText",
648 | "QXmlAttributes",
649 | "QXmlContentHandler",
650 | "QXmlDTDHandler",
651 | "QXmlDeclHandler",
652 | "QXmlDefaultHandler",
653 | "QXmlEntityResolver",
654 | "QXmlErrorHandler",
655 | "QXmlInputSource",
656 | "QXmlLexicalHandler",
657 | "QXmlLocator",
658 | "QXmlNamespaceSupport",
659 | "QXmlParseException",
660 | "QXmlReader",
661 | "QXmlSimpleReader"
662 | ],
663 | "QtXmlPatterns": [
664 | "QAbstractMessageHandler",
665 | "QAbstractUriResolver",
666 | "QAbstractXmlNodeModel",
667 | "QAbstractXmlReceiver",
668 | "QSourceLocation",
669 | "QXmlFormatter",
670 | "QXmlItem",
671 | "QXmlName",
672 | "QXmlNamePool",
673 | "QXmlNodeModelIndex",
674 | "QXmlQuery",
675 | "QXmlResultItems",
676 | "QXmlSchema",
677 | "QXmlSchemaValidator",
678 | "QXmlSerializer"
679 | ]
680 | }
681 |
682 | """ Missing members
683 |
684 | This mapping describes members that have been deprecated
685 | in one or more bindings and have been left out of the
686 | _common_members mapping.
687 |
688 | The member can provide an extra details string to be
689 | included in exceptions and warnings.
690 | """
691 |
692 | _missing_members = {
693 | "QtGui": {
694 | "QMatrix": "Deprecated in PyQt5",
695 | },
696 | }
697 |
698 |
699 | def _qInstallMessageHandler(handler):
700 | """Install a message handler that works in all bindings
701 |
702 | Args:
703 | handler: A function that takes 3 arguments, or None
704 | """
705 | def messageOutputHandler(*args):
706 | # In Qt4 bindings, message handlers are passed 2 arguments
707 | # In Qt5 bindings, message handlers are passed 3 arguments
708 | # The first argument is a QtMsgType
709 | # The last argument is the message to be printed
710 | # The Middle argument (if passed) is a QMessageLogContext
711 | if len(args) == 3:
712 | msgType, logContext, msg = args
713 | elif len(args) == 2:
714 | msgType, msg = args
715 | logContext = None
716 | else:
717 | raise TypeError(
718 | "handler expected 2 or 3 arguments, got {0}".format(len(args)))
719 |
720 | if isinstance(msg, bytes):
721 | # In python 3, some bindings pass a bytestring, which cannot be
722 | # used elsewhere. Decoding a python 2 or 3 bytestring object will
723 | # consistently return a unicode object.
724 | msg = msg.decode()
725 |
726 | handler(msgType, logContext, msg)
727 |
728 | passObject = messageOutputHandler if handler else handler
729 | if Qt.IsPySide or Qt.IsPyQt4:
730 | return Qt._QtCore.qInstallMsgHandler(passObject)
731 | elif Qt.IsPySide2 or Qt.IsPyQt5:
732 | return Qt._QtCore.qInstallMessageHandler(passObject)
733 |
734 |
735 | def _getcpppointer(object):
736 | if hasattr(Qt, "_shiboken2"):
737 | return getattr(Qt, "_shiboken2").getCppPointer(object)[0]
738 | elif hasattr(Qt, "_shiboken"):
739 | return getattr(Qt, "_shiboken").getCppPointer(object)[0]
740 | elif hasattr(Qt, "_sip"):
741 | return getattr(Qt, "_sip").unwrapinstance(object)
742 | raise AttributeError("'module' has no attribute 'getCppPointer'")
743 |
744 |
745 | def _wrapinstance(ptr, base=None):
746 | """Enable implicit cast of pointer to most suitable class
747 |
748 | This behaviour is available in sip per default.
749 |
750 | Based on http://nathanhorne.com/pyqtpyside-wrap-instance
751 |
752 | Usage:
753 | This mechanism kicks in under these circumstances.
754 | 1. Qt.py is using PySide 1 or 2.
755 | 2. A `base` argument is not provided.
756 |
757 | See :func:`QtCompat.wrapInstance()`
758 |
759 | Arguments:
760 | ptr (long): Pointer to QObject in memory
761 | base (QObject, optional): Base class to wrap with. Defaults to QObject,
762 | which should handle anything.
763 |
764 | """
765 |
766 | assert isinstance(ptr, long), "Argument 'ptr' must be of type "
767 | assert (base is None) or issubclass(base, Qt.QtCore.QObject), (
768 | "Argument 'base' must be of type ")
769 |
770 | if Qt.IsPyQt4 or Qt.IsPyQt5:
771 | func = getattr(Qt, "_sip").wrapinstance
772 | elif Qt.IsPySide2:
773 | func = getattr(Qt, "_shiboken2").wrapInstance
774 | elif Qt.IsPySide:
775 | func = getattr(Qt, "_shiboken").wrapInstance
776 | else:
777 | raise AttributeError("'module' has no attribute 'wrapInstance'")
778 |
779 | if base is None:
780 | q_object = func(long(ptr), Qt.QtCore.QObject)
781 | meta_object = q_object.metaObject()
782 | class_name = meta_object.className()
783 | super_class_name = meta_object.superClass().className()
784 |
785 | if hasattr(Qt.QtWidgets, class_name):
786 | base = getattr(Qt.QtWidgets, class_name)
787 |
788 | elif hasattr(Qt.QtWidgets, super_class_name):
789 | base = getattr(Qt.QtWidgets, super_class_name)
790 |
791 | else:
792 | base = Qt.QtCore.QObject
793 |
794 | return func(long(ptr), base)
795 |
796 |
797 | def _isvalid(object):
798 | """Check if the object is valid to use in Python runtime.
799 |
800 | Usage:
801 | See :func:`QtCompat.isValid()`
802 |
803 | Arguments:
804 | object (QObject): QObject to check the validity of.
805 |
806 | """
807 |
808 | assert isinstance(object, Qt.QtCore.QObject)
809 |
810 | if hasattr(Qt, "_shiboken2"):
811 | return getattr(Qt, "_shiboken2").isValid(object)
812 |
813 | elif hasattr(Qt, "_shiboken"):
814 | return getattr(Qt, "_shiboken").isValid(object)
815 |
816 | elif hasattr(Qt, "_sip"):
817 | return not getattr(Qt, "_sip").isdeleted(object)
818 |
819 | else:
820 | raise AttributeError("'module' has no attribute isValid")
821 |
822 |
823 | def _translate(context, sourceText, *args):
824 | # In Qt4 bindings, translate can be passed 2 or 3 arguments
825 | # In Qt5 bindings, translate can be passed 2 arguments
826 | # The first argument is disambiguation[str]
827 | # The last argument is n[int]
828 | # The middle argument can be encoding[QtCore.QCoreApplication.Encoding]
829 | if len(args) == 3:
830 | disambiguation, encoding, n = args
831 | elif len(args) == 2:
832 | disambiguation, n = args
833 | encoding = None
834 | else:
835 | raise TypeError(
836 | "Expected 4 or 5 arguments, got {0}.".format(len(args) + 2))
837 |
838 | if hasattr(Qt.QtCore, "QCoreApplication"):
839 | app = getattr(Qt.QtCore, "QCoreApplication")
840 | else:
841 | raise NotImplementedError(
842 | "Missing QCoreApplication implementation for {binding}".format(
843 | binding=Qt.__binding__,
844 | )
845 | )
846 | if Qt.__binding__ in ("PySide2", "PyQt5"):
847 | sanitized_args = [context, sourceText, disambiguation, n]
848 | else:
849 | sanitized_args = [
850 | context,
851 | sourceText,
852 | disambiguation,
853 | encoding or app.CodecForTr,
854 | n
855 | ]
856 | return app.translate(*sanitized_args)
857 |
858 |
859 | def _loadUi(uifile, baseinstance=None):
860 | """Dynamically load a user interface from the given `uifile`
861 |
862 | This function calls `uic.loadUi` if using PyQt bindings,
863 | else it implements a comparable binding for PySide.
864 |
865 | Documentation:
866 | http://pyqt.sourceforge.net/Docs/PyQt5/designer.html#PyQt5.uic.loadUi
867 |
868 | Arguments:
869 | uifile (str): Absolute path to Qt Designer file.
870 | baseinstance (QWidget): Instantiated QWidget or subclass thereof
871 |
872 | Return:
873 | baseinstance if `baseinstance` is not `None`. Otherwise
874 | return the newly created instance of the user interface.
875 |
876 | """
877 | if hasattr(Qt, "_uic"):
878 | return Qt._uic.loadUi(uifile, baseinstance)
879 |
880 | elif hasattr(Qt, "_QtUiTools"):
881 | # Implement `PyQt5.uic.loadUi` for PySide(2)
882 |
883 | class _UiLoader(Qt._QtUiTools.QUiLoader):
884 | """Create the user interface in a base instance.
885 |
886 | Unlike `Qt._QtUiTools.QUiLoader` itself this class does not
887 | create a new instance of the top-level widget, but creates the user
888 | interface in an existing instance of the top-level class if needed.
889 |
890 | This mimics the behaviour of `PyQt5.uic.loadUi`.
891 |
892 | """
893 |
894 | def __init__(self, baseinstance):
895 | super(_UiLoader, self).__init__(baseinstance)
896 | self.baseinstance = baseinstance
897 | self.custom_widgets = {}
898 |
899 | def _loadCustomWidgets(self, etree):
900 | """
901 | Workaround to pyside-77 bug.
902 |
903 | From QUiLoader doc we should use registerCustomWidget method.
904 | But this causes a segfault on some platforms.
905 |
906 | Instead we fetch from customwidgets DOM node the python class
907 | objects. Then we can directly use them in createWidget method.
908 | """
909 |
910 | def headerToModule(header):
911 | """
912 | Translate a header file to python module path
913 | foo/bar.h => foo.bar
914 | """
915 | # Remove header extension
916 | module = os.path.splitext(header)[0]
917 |
918 | # Replace os separator by python module separator
919 | return module.replace("/", ".").replace("\\", ".")
920 |
921 | custom_widgets = etree.find("customwidgets")
922 |
923 | if custom_widgets is None:
924 | return
925 |
926 | for custom_widget in custom_widgets:
927 | class_name = custom_widget.find("class").text
928 | header = custom_widget.find("header").text
929 | module = importlib.import_module(headerToModule(header))
930 | self.custom_widgets[class_name] = getattr(module,
931 | class_name)
932 |
933 | def load(self, uifile, *args, **kwargs):
934 | from xml.etree.ElementTree import ElementTree
935 |
936 | # For whatever reason, if this doesn't happen then
937 | # reading an invalid or non-existing .ui file throws
938 | # a RuntimeError.
939 | etree = ElementTree()
940 | etree.parse(uifile)
941 | self._loadCustomWidgets(etree)
942 |
943 | widget = Qt._QtUiTools.QUiLoader.load(
944 | self, uifile, *args, **kwargs)
945 |
946 | # Workaround for PySide 1.0.9, see issue #208
947 | widget.parentWidget()
948 |
949 | return widget
950 |
951 | def createWidget(self, class_name, parent=None, name=""):
952 | """Called for each widget defined in ui file
953 |
954 | Overridden here to populate `baseinstance` instead.
955 |
956 | """
957 |
958 | if parent is None and self.baseinstance:
959 | # Supposed to create the top-level widget,
960 | # return the base instance instead
961 | return self.baseinstance
962 |
963 | # For some reason, Line is not in the list of available
964 | # widgets, but works fine, so we have to special case it here.
965 | if class_name in self.availableWidgets() + ["Line"]:
966 | # Create a new widget for child widgets
967 | widget = Qt._QtUiTools.QUiLoader.createWidget(self,
968 | class_name,
969 | parent,
970 | name)
971 | elif class_name in self.custom_widgets:
972 | widget = self.custom_widgets[class_name](parent)
973 | else:
974 | raise Exception("Custom widget '%s' not supported"
975 | % class_name)
976 |
977 | if self.baseinstance:
978 | # Set an attribute for the new child widget on the base
979 | # instance, just like PyQt5.uic.loadUi does.
980 | setattr(self.baseinstance, name, widget)
981 |
982 | return widget
983 |
984 | widget = _UiLoader(baseinstance).load(uifile)
985 | Qt.QtCore.QMetaObject.connectSlotsByName(widget)
986 |
987 | return widget
988 |
989 | else:
990 | raise NotImplementedError("No implementation available for loadUi")
991 |
992 |
993 | """Misplaced members
994 |
995 | These members from the original submodule are misplaced relative PySide2
996 |
997 | """
998 | _misplaced_members = {
999 | "PySide2": {
1000 | "QtCore.QStringListModel": "QtCore.QStringListModel",
1001 | "QtGui.QStringListModel": "QtCore.QStringListModel",
1002 | "QtCore.Property": "QtCore.Property",
1003 | "QtCore.Signal": "QtCore.Signal",
1004 | "QtCore.Slot": "QtCore.Slot",
1005 | "QtCore.QAbstractProxyModel": "QtCore.QAbstractProxyModel",
1006 | "QtCore.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel",
1007 | "QtCore.QItemSelection": "QtCore.QItemSelection",
1008 | "QtCore.QItemSelectionModel": "QtCore.QItemSelectionModel",
1009 | "QtCore.QItemSelectionRange": "QtCore.QItemSelectionRange",
1010 | "QtUiTools.QUiLoader": ["QtCompat.loadUi", _loadUi],
1011 | "shiboken2.wrapInstance": ["QtCompat.wrapInstance", _wrapinstance],
1012 | "shiboken2.getCppPointer": ["QtCompat.getCppPointer", _getcpppointer],
1013 | "shiboken2.isValid": ["QtCompat.isValid", _isvalid],
1014 | "QtWidgets.qApp": "QtWidgets.QApplication.instance()",
1015 | "QtCore.QCoreApplication.translate": [
1016 | "QtCompat.translate", _translate
1017 | ],
1018 | "QtWidgets.QApplication.translate": [
1019 | "QtCompat.translate", _translate
1020 | ],
1021 | "QtCore.qInstallMessageHandler": [
1022 | "QtCompat.qInstallMessageHandler", _qInstallMessageHandler
1023 | ],
1024 | "QtWidgets.QStyleOptionViewItem": "QtCompat.QStyleOptionViewItemV4",
1025 | },
1026 | "PyQt5": {
1027 | "QtCore.pyqtProperty": "QtCore.Property",
1028 | "QtCore.pyqtSignal": "QtCore.Signal",
1029 | "QtCore.pyqtSlot": "QtCore.Slot",
1030 | "QtCore.QAbstractProxyModel": "QtCore.QAbstractProxyModel",
1031 | "QtCore.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel",
1032 | "QtCore.QStringListModel": "QtCore.QStringListModel",
1033 | "QtCore.QItemSelection": "QtCore.QItemSelection",
1034 | "QtCore.QItemSelectionModel": "QtCore.QItemSelectionModel",
1035 | "QtCore.QItemSelectionRange": "QtCore.QItemSelectionRange",
1036 | "uic.loadUi": ["QtCompat.loadUi", _loadUi],
1037 | "sip.wrapinstance": ["QtCompat.wrapInstance", _wrapinstance],
1038 | "sip.unwrapinstance": ["QtCompat.getCppPointer", _getcpppointer],
1039 | "sip.isdeleted": ["QtCompat.isValid", _isvalid],
1040 | "QtWidgets.qApp": "QtWidgets.QApplication.instance()",
1041 | "QtCore.QCoreApplication.translate": [
1042 | "QtCompat.translate", _translate
1043 | ],
1044 | "QtWidgets.QApplication.translate": [
1045 | "QtCompat.translate", _translate
1046 | ],
1047 | "QtCore.qInstallMessageHandler": [
1048 | "QtCompat.qInstallMessageHandler", _qInstallMessageHandler
1049 | ],
1050 | "QtWidgets.QStyleOptionViewItem": "QtCompat.QStyleOptionViewItemV4",
1051 | },
1052 | "PySide": {
1053 | "QtGui.QAbstractProxyModel": "QtCore.QAbstractProxyModel",
1054 | "QtGui.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel",
1055 | "QtGui.QStringListModel": "QtCore.QStringListModel",
1056 | "QtGui.QItemSelection": "QtCore.QItemSelection",
1057 | "QtGui.QItemSelectionModel": "QtCore.QItemSelectionModel",
1058 | "QtCore.Property": "QtCore.Property",
1059 | "QtCore.Signal": "QtCore.Signal",
1060 | "QtCore.Slot": "QtCore.Slot",
1061 | "QtGui.QItemSelectionRange": "QtCore.QItemSelectionRange",
1062 | "QtGui.QAbstractPrintDialog": "QtPrintSupport.QAbstractPrintDialog",
1063 | "QtGui.QPageSetupDialog": "QtPrintSupport.QPageSetupDialog",
1064 | "QtGui.QPrintDialog": "QtPrintSupport.QPrintDialog",
1065 | "QtGui.QPrintEngine": "QtPrintSupport.QPrintEngine",
1066 | "QtGui.QPrintPreviewDialog": "QtPrintSupport.QPrintPreviewDialog",
1067 | "QtGui.QPrintPreviewWidget": "QtPrintSupport.QPrintPreviewWidget",
1068 | "QtGui.QPrinter": "QtPrintSupport.QPrinter",
1069 | "QtGui.QPrinterInfo": "QtPrintSupport.QPrinterInfo",
1070 | "QtUiTools.QUiLoader": ["QtCompat.loadUi", _loadUi],
1071 | "shiboken.wrapInstance": ["QtCompat.wrapInstance", _wrapinstance],
1072 | "shiboken.unwrapInstance": ["QtCompat.getCppPointer", _getcpppointer],
1073 | "shiboken.isValid": ["QtCompat.isValid", _isvalid],
1074 | "QtGui.qApp": "QtWidgets.QApplication.instance()",
1075 | "QtCore.QCoreApplication.translate": [
1076 | "QtCompat.translate", _translate
1077 | ],
1078 | "QtGui.QApplication.translate": [
1079 | "QtCompat.translate", _translate
1080 | ],
1081 | "QtCore.qInstallMsgHandler": [
1082 | "QtCompat.qInstallMessageHandler", _qInstallMessageHandler
1083 | ],
1084 | "QtGui.QStyleOptionViewItemV4": "QtCompat.QStyleOptionViewItemV4",
1085 | },
1086 | "PyQt4": {
1087 | "QtGui.QAbstractProxyModel": "QtCore.QAbstractProxyModel",
1088 | "QtGui.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel",
1089 | "QtGui.QItemSelection": "QtCore.QItemSelection",
1090 | "QtGui.QStringListModel": "QtCore.QStringListModel",
1091 | "QtGui.QItemSelectionModel": "QtCore.QItemSelectionModel",
1092 | "QtCore.pyqtProperty": "QtCore.Property",
1093 | "QtCore.pyqtSignal": "QtCore.Signal",
1094 | "QtCore.pyqtSlot": "QtCore.Slot",
1095 | "QtGui.QItemSelectionRange": "QtCore.QItemSelectionRange",
1096 | "QtGui.QAbstractPrintDialog": "QtPrintSupport.QAbstractPrintDialog",
1097 | "QtGui.QPageSetupDialog": "QtPrintSupport.QPageSetupDialog",
1098 | "QtGui.QPrintDialog": "QtPrintSupport.QPrintDialog",
1099 | "QtGui.QPrintEngine": "QtPrintSupport.QPrintEngine",
1100 | "QtGui.QPrintPreviewDialog": "QtPrintSupport.QPrintPreviewDialog",
1101 | "QtGui.QPrintPreviewWidget": "QtPrintSupport.QPrintPreviewWidget",
1102 | "QtGui.QPrinter": "QtPrintSupport.QPrinter",
1103 | "QtGui.QPrinterInfo": "QtPrintSupport.QPrinterInfo",
1104 | # "QtCore.pyqtSignature": "QtCore.Slot",
1105 | "uic.loadUi": ["QtCompat.loadUi", _loadUi],
1106 | "sip.wrapinstance": ["QtCompat.wrapInstance", _wrapinstance],
1107 | "sip.unwrapinstance": ["QtCompat.getCppPointer", _getcpppointer],
1108 | "sip.isdeleted": ["QtCompat.isValid", _isvalid],
1109 | "QtCore.QString": "str",
1110 | "QtGui.qApp": "QtWidgets.QApplication.instance()",
1111 | "QtCore.QCoreApplication.translate": [
1112 | "QtCompat.translate", _translate
1113 | ],
1114 | "QtGui.QApplication.translate": [
1115 | "QtCompat.translate", _translate
1116 | ],
1117 | "QtCore.qInstallMsgHandler": [
1118 | "QtCompat.qInstallMessageHandler", _qInstallMessageHandler
1119 | ],
1120 | "QtGui.QStyleOptionViewItemV4": "QtCompat.QStyleOptionViewItemV4",
1121 | }
1122 | }
1123 |
1124 | """ Compatibility Members
1125 |
1126 | This dictionary is used to build Qt.QtCompat objects that provide a consistent
1127 | interface for obsolete members, and differences in binding return values.
1128 |
1129 | {
1130 | "binding": {
1131 | "classname": {
1132 | "targetname": "binding_namespace",
1133 | }
1134 | }
1135 | }
1136 | """
1137 | _compatibility_members = {
1138 | "PySide2": {
1139 | "QWidget": {
1140 | "grab": "QtWidgets.QWidget.grab",
1141 | },
1142 | "QHeaderView": {
1143 | "sectionsClickable": "QtWidgets.QHeaderView.sectionsClickable",
1144 | "setSectionsClickable":
1145 | "QtWidgets.QHeaderView.setSectionsClickable",
1146 | "sectionResizeMode": "QtWidgets.QHeaderView.sectionResizeMode",
1147 | "setSectionResizeMode":
1148 | "QtWidgets.QHeaderView.setSectionResizeMode",
1149 | "sectionsMovable": "QtWidgets.QHeaderView.sectionsMovable",
1150 | "setSectionsMovable": "QtWidgets.QHeaderView.setSectionsMovable",
1151 | },
1152 | "QFileDialog": {
1153 | "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName",
1154 | "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames",
1155 | "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName",
1156 | },
1157 | },
1158 | "PyQt5": {
1159 | "QWidget": {
1160 | "grab": "QtWidgets.QWidget.grab",
1161 | },
1162 | "QHeaderView": {
1163 | "sectionsClickable": "QtWidgets.QHeaderView.sectionsClickable",
1164 | "setSectionsClickable":
1165 | "QtWidgets.QHeaderView.setSectionsClickable",
1166 | "sectionResizeMode": "QtWidgets.QHeaderView.sectionResizeMode",
1167 | "setSectionResizeMode":
1168 | "QtWidgets.QHeaderView.setSectionResizeMode",
1169 | "sectionsMovable": "QtWidgets.QHeaderView.sectionsMovable",
1170 | "setSectionsMovable": "QtWidgets.QHeaderView.setSectionsMovable",
1171 | },
1172 | "QFileDialog": {
1173 | "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName",
1174 | "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames",
1175 | "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName",
1176 | },
1177 | },
1178 | "PySide": {
1179 | "QWidget": {
1180 | "grab": "QtWidgets.QPixmap.grabWidget",
1181 | },
1182 | "QHeaderView": {
1183 | "sectionsClickable": "QtWidgets.QHeaderView.isClickable",
1184 | "setSectionsClickable": "QtWidgets.QHeaderView.setClickable",
1185 | "sectionResizeMode": "QtWidgets.QHeaderView.resizeMode",
1186 | "setSectionResizeMode": "QtWidgets.QHeaderView.setResizeMode",
1187 | "sectionsMovable": "QtWidgets.QHeaderView.isMovable",
1188 | "setSectionsMovable": "QtWidgets.QHeaderView.setMovable",
1189 | },
1190 | "QFileDialog": {
1191 | "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName",
1192 | "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames",
1193 | "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName",
1194 | },
1195 | },
1196 | "PyQt4": {
1197 | "QWidget": {
1198 | "grab": "QtWidgets.QPixmap.grabWidget",
1199 | },
1200 | "QHeaderView": {
1201 | "sectionsClickable": "QtWidgets.QHeaderView.isClickable",
1202 | "setSectionsClickable": "QtWidgets.QHeaderView.setClickable",
1203 | "sectionResizeMode": "QtWidgets.QHeaderView.resizeMode",
1204 | "setSectionResizeMode": "QtWidgets.QHeaderView.setResizeMode",
1205 | "sectionsMovable": "QtWidgets.QHeaderView.isMovable",
1206 | "setSectionsMovable": "QtWidgets.QHeaderView.setMovable",
1207 | },
1208 | "QFileDialog": {
1209 | "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName",
1210 | "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames",
1211 | "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName",
1212 | },
1213 | },
1214 | }
1215 |
1216 |
1217 | def _apply_site_config():
1218 | try:
1219 | import QtSiteConfig
1220 | except ImportError:
1221 | # If no QtSiteConfig module found, no modifications
1222 | # to _common_members are needed.
1223 | pass
1224 | else:
1225 | # Provide the ability to modify the dicts used to build Qt.py
1226 | if hasattr(QtSiteConfig, 'update_members'):
1227 | QtSiteConfig.update_members(_common_members)
1228 |
1229 | if hasattr(QtSiteConfig, 'update_misplaced_members'):
1230 | QtSiteConfig.update_misplaced_members(members=_misplaced_members)
1231 |
1232 | if hasattr(QtSiteConfig, 'update_compatibility_members'):
1233 | QtSiteConfig.update_compatibility_members(
1234 | members=_compatibility_members)
1235 |
1236 |
1237 | def _new_module(name):
1238 | return types.ModuleType(__name__ + "." + name)
1239 |
1240 |
1241 | def _import_sub_module(module, name):
1242 | """import_sub_module will mimic the function of importlib.import_module"""
1243 | module = __import__(module.__name__ + "." + name)
1244 | for level in name.split("."):
1245 | module = getattr(module, level)
1246 | return module
1247 |
1248 |
1249 | def _setup(module, extras):
1250 | """Install common submodules"""
1251 |
1252 | Qt.__binding__ = module.__name__
1253 |
1254 | for name in list(_common_members) + extras:
1255 | try:
1256 | submodule = _import_sub_module(
1257 | module, name)
1258 | except ImportError:
1259 | try:
1260 | # For extra modules like sip and shiboken that may not be
1261 | # children of the binding.
1262 | submodule = __import__(name)
1263 | except ImportError:
1264 | continue
1265 |
1266 | setattr(Qt, "_" + name, submodule)
1267 |
1268 | if name not in extras:
1269 | # Store reference to original binding,
1270 | # but don't store speciality modules
1271 | # such as uic or QtUiTools
1272 | setattr(Qt, name, _new_module(name))
1273 |
1274 |
1275 | def _reassign_misplaced_members(binding):
1276 | """Apply misplaced members from `binding` to Qt.py
1277 |
1278 | Arguments:
1279 | binding (dict): Misplaced members
1280 |
1281 | """
1282 |
1283 | for src, dst in _misplaced_members[binding].items():
1284 | dst_value = None
1285 |
1286 | src_parts = src.split(".")
1287 | src_module = src_parts[0]
1288 | src_member = None
1289 | if len(src_parts) > 1:
1290 | src_member = src_parts[1:]
1291 |
1292 | if isinstance(dst, (list, tuple)):
1293 | dst, dst_value = dst
1294 |
1295 | dst_parts = dst.split(".")
1296 | dst_module = dst_parts[0]
1297 | dst_member = None
1298 | if len(dst_parts) > 1:
1299 | dst_member = dst_parts[1]
1300 |
1301 | # Get the member we want to store in the namesapce.
1302 | if not dst_value:
1303 | try:
1304 | _part = getattr(Qt, "_" + src_module)
1305 | while src_member:
1306 | member = src_member.pop(0)
1307 | _part = getattr(_part, member)
1308 | dst_value = _part
1309 | except AttributeError:
1310 | # If the member we want to store in the namespace does not
1311 | # exist, there is no need to continue. This can happen if a
1312 | # request was made to rename a member that didn't exist, for
1313 | # example if QtWidgets isn't available on the target platform.
1314 | _log("Misplaced member has no source: {0}".format(src))
1315 | continue
1316 |
1317 | try:
1318 | src_object = getattr(Qt, dst_module)
1319 | except AttributeError:
1320 | if dst_module not in _common_members:
1321 | # Only create the Qt parent module if its listed in
1322 | # _common_members. Without this check, if you remove QtCore
1323 | # from _common_members, the default _misplaced_members will add
1324 | # Qt.QtCore so it can add Signal, Slot, etc.
1325 | msg = 'Not creating missing member module "{m}" for "{c}"'
1326 | _log(msg.format(m=dst_module, c=dst_member))
1327 | continue
1328 | # If the dst is valid but the Qt parent module does not exist
1329 | # then go ahead and create a new module to contain the member.
1330 | setattr(Qt, dst_module, _new_module(dst_module))
1331 | src_object = getattr(Qt, dst_module)
1332 | # Enable direct import of the new module
1333 | sys.modules[__name__ + "." + dst_module] = src_object
1334 |
1335 | if not dst_value:
1336 | dst_value = getattr(Qt, "_" + src_module)
1337 | if src_member:
1338 | dst_value = getattr(dst_value, src_member)
1339 |
1340 | setattr(
1341 | src_object,
1342 | dst_member or dst_module,
1343 | dst_value
1344 | )
1345 |
1346 |
1347 | def _build_compatibility_members(binding, decorators=None):
1348 | """Apply `binding` to QtCompat
1349 |
1350 | Arguments:
1351 | binding (str): Top level binding in _compatibility_members.
1352 | decorators (dict, optional): Provides the ability to decorate the
1353 | original Qt methods when needed by a binding. This can be used
1354 | to change the returned value to a standard value. The key should
1355 | be the classname, the value is a dict where the keys are the
1356 | target method names, and the values are the decorator functions.
1357 |
1358 | """
1359 |
1360 | decorators = decorators or dict()
1361 |
1362 | # Allow optional site-level customization of the compatibility members.
1363 | # This method does not need to be implemented in QtSiteConfig.
1364 | try:
1365 | import QtSiteConfig
1366 | except ImportError:
1367 | pass
1368 | else:
1369 | if hasattr(QtSiteConfig, 'update_compatibility_decorators'):
1370 | QtSiteConfig.update_compatibility_decorators(binding, decorators)
1371 |
1372 | _QtCompat = type("QtCompat", (object,), {})
1373 |
1374 | for classname, bindings in _compatibility_members[binding].items():
1375 | attrs = {}
1376 | for target, binding in bindings.items():
1377 | namespaces = binding.split('.')
1378 | try:
1379 | src_object = getattr(Qt, "_" + namespaces[0])
1380 | except AttributeError as e:
1381 | _log("QtCompat: AttributeError: %s" % e)
1382 | # Skip reassignment of non-existing members.
1383 | # This can happen if a request was made to
1384 | # rename a member that didn't exist, for example
1385 | # if QtWidgets isn't available on the target platform.
1386 | continue
1387 |
1388 | # Walk down any remaining namespace getting the object assuming
1389 | # that if the first namespace exists the rest will exist.
1390 | for namespace in namespaces[1:]:
1391 | src_object = getattr(src_object, namespace)
1392 |
1393 | # decorate the Qt method if a decorator was provided.
1394 | if target in decorators.get(classname, []):
1395 | # staticmethod must be called on the decorated method to
1396 | # prevent a TypeError being raised when the decorated method
1397 | # is called.
1398 | src_object = staticmethod(
1399 | decorators[classname][target](src_object))
1400 |
1401 | attrs[target] = src_object
1402 |
1403 | # Create the QtCompat class and install it into the namespace
1404 | compat_class = type(classname, (_QtCompat,), attrs)
1405 | setattr(Qt.QtCompat, classname, compat_class)
1406 |
1407 |
1408 | def _pyside2():
1409 | """Initialise PySide2
1410 |
1411 | These functions serve to test the existence of a binding
1412 | along with set it up in such a way that it aligns with
1413 | the final step; adding members from the original binding
1414 | to Qt.py
1415 |
1416 | """
1417 |
1418 | import PySide2 as module
1419 | extras = ["QtUiTools"]
1420 | try:
1421 | try:
1422 | # Before merge of PySide and shiboken
1423 | import shiboken2
1424 | except ImportError:
1425 | # After merge of PySide and shiboken, May 2017
1426 | from PySide2 import shiboken2
1427 | extras.append("shiboken2")
1428 | except ImportError:
1429 | pass
1430 |
1431 | _setup(module, extras)
1432 | Qt.__binding_version__ = module.__version__
1433 |
1434 | if hasattr(Qt, "_shiboken2"):
1435 | Qt.QtCompat.wrapInstance = _wrapinstance
1436 | Qt.QtCompat.getCppPointer = _getcpppointer
1437 | Qt.QtCompat.delete = shiboken2.delete
1438 |
1439 | if hasattr(Qt, "_QtUiTools"):
1440 | Qt.QtCompat.loadUi = _loadUi
1441 |
1442 | if hasattr(Qt, "_QtCore"):
1443 | Qt.__qt_version__ = Qt._QtCore.qVersion()
1444 | Qt.QtCompat.dataChanged = (
1445 | lambda self, topleft, bottomright, roles=None:
1446 | self.dataChanged.emit(topleft, bottomright, roles or [])
1447 | )
1448 |
1449 | if hasattr(Qt, "_QtWidgets"):
1450 | Qt.QtCompat.setSectionResizeMode = \
1451 | Qt._QtWidgets.QHeaderView.setSectionResizeMode
1452 |
1453 | _reassign_misplaced_members("PySide2")
1454 | _build_compatibility_members("PySide2")
1455 |
1456 |
1457 | def _pyside():
1458 | """Initialise PySide"""
1459 |
1460 | import PySide as module
1461 | extras = ["QtUiTools"]
1462 | try:
1463 | try:
1464 | # Before merge of PySide and shiboken
1465 | import shiboken
1466 | except ImportError:
1467 | # After merge of PySide and shiboken, May 2017
1468 | from PySide import shiboken
1469 | extras.append("shiboken")
1470 | except ImportError:
1471 | pass
1472 |
1473 | _setup(module, extras)
1474 | Qt.__binding_version__ = module.__version__
1475 |
1476 | if hasattr(Qt, "_shiboken"):
1477 | Qt.QtCompat.wrapInstance = _wrapinstance
1478 | Qt.QtCompat.getCppPointer = _getcpppointer
1479 | Qt.QtCompat.delete = shiboken.delete
1480 |
1481 | if hasattr(Qt, "_QtUiTools"):
1482 | Qt.QtCompat.loadUi = _loadUi
1483 |
1484 | if hasattr(Qt, "_QtGui"):
1485 | setattr(Qt, "QtWidgets", _new_module("QtWidgets"))
1486 | setattr(Qt, "_QtWidgets", Qt._QtGui)
1487 | if hasattr(Qt._QtGui, "QX11Info"):
1488 | setattr(Qt, "QtX11Extras", _new_module("QtX11Extras"))
1489 | Qt.QtX11Extras.QX11Info = Qt._QtGui.QX11Info
1490 |
1491 | Qt.QtCompat.setSectionResizeMode = Qt._QtGui.QHeaderView.setResizeMode
1492 |
1493 | if hasattr(Qt, "_QtCore"):
1494 | Qt.__qt_version__ = Qt._QtCore.qVersion()
1495 | Qt.QtCompat.dataChanged = (
1496 | lambda self, topleft, bottomright, roles=None:
1497 | self.dataChanged.emit(topleft, bottomright)
1498 | )
1499 |
1500 | _reassign_misplaced_members("PySide")
1501 | _build_compatibility_members("PySide")
1502 |
1503 |
1504 | def _pyqt5():
1505 | """Initialise PyQt5"""
1506 |
1507 | import PyQt5 as module
1508 | extras = ["uic"]
1509 |
1510 | try:
1511 | import sip
1512 | extras += ["sip"]
1513 | except ImportError:
1514 |
1515 | # Relevant to PyQt5 5.11 and above
1516 | try:
1517 | from PyQt5 import sip
1518 | extras += ["sip"]
1519 | except ImportError:
1520 | sip = None
1521 |
1522 | _setup(module, extras)
1523 | if hasattr(Qt, "_sip"):
1524 | Qt.QtCompat.wrapInstance = _wrapinstance
1525 | Qt.QtCompat.getCppPointer = _getcpppointer
1526 | Qt.QtCompat.delete = sip.delete
1527 |
1528 | if hasattr(Qt, "_uic"):
1529 | Qt.QtCompat.loadUi = _loadUi
1530 |
1531 | if hasattr(Qt, "_QtCore"):
1532 | Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR
1533 | Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR
1534 | Qt.QtCompat.dataChanged = (
1535 | lambda self, topleft, bottomright, roles=None:
1536 | self.dataChanged.emit(topleft, bottomright, roles or [])
1537 | )
1538 |
1539 | if hasattr(Qt, "_QtWidgets"):
1540 | Qt.QtCompat.setSectionResizeMode = \
1541 | Qt._QtWidgets.QHeaderView.setSectionResizeMode
1542 |
1543 | _reassign_misplaced_members("PyQt5")
1544 | _build_compatibility_members('PyQt5')
1545 |
1546 |
1547 | def _pyqt4():
1548 | """Initialise PyQt4"""
1549 |
1550 | import sip
1551 |
1552 | # Validation of envivornment variable. Prevents an error if
1553 | # the variable is invalid since it's just a hint.
1554 | try:
1555 | hint = int(QT_SIP_API_HINT)
1556 | except TypeError:
1557 | hint = None # Variable was None, i.e. not set.
1558 | except ValueError:
1559 | raise ImportError("QT_SIP_API_HINT=%s must be a 1 or 2")
1560 |
1561 | for api in ("QString",
1562 | "QVariant",
1563 | "QDate",
1564 | "QDateTime",
1565 | "QTextStream",
1566 | "QTime",
1567 | "QUrl"):
1568 | try:
1569 | sip.setapi(api, hint or 2)
1570 | except AttributeError:
1571 | raise ImportError("PyQt4 < 4.6 isn't supported by Qt.py")
1572 | except ValueError:
1573 | actual = sip.getapi(api)
1574 | if not hint:
1575 | raise ImportError("API version already set to %d" % actual)
1576 | else:
1577 | # Having provided a hint indicates a soft constraint, one
1578 | # that doesn't throw an exception.
1579 | sys.stderr.write(
1580 | "Warning: API '%s' has already been set to %d.\n"
1581 | % (api, actual)
1582 | )
1583 |
1584 | import PyQt4 as module
1585 | extras = ["uic"]
1586 | try:
1587 | import sip
1588 | extras.append(sip.__name__)
1589 | except ImportError:
1590 | sip = None
1591 |
1592 | _setup(module, extras)
1593 | if hasattr(Qt, "_sip"):
1594 | Qt.QtCompat.wrapInstance = _wrapinstance
1595 | Qt.QtCompat.getCppPointer = _getcpppointer
1596 | Qt.QtCompat.delete = sip.delete
1597 |
1598 | if hasattr(Qt, "_uic"):
1599 | Qt.QtCompat.loadUi = _loadUi
1600 |
1601 | if hasattr(Qt, "_QtGui"):
1602 | setattr(Qt, "QtWidgets", _new_module("QtWidgets"))
1603 | setattr(Qt, "_QtWidgets", Qt._QtGui)
1604 | if hasattr(Qt._QtGui, "QX11Info"):
1605 | setattr(Qt, "QtX11Extras", _new_module("QtX11Extras"))
1606 | Qt.QtX11Extras.QX11Info = Qt._QtGui.QX11Info
1607 |
1608 | Qt.QtCompat.setSectionResizeMode = \
1609 | Qt._QtGui.QHeaderView.setResizeMode
1610 |
1611 | if hasattr(Qt, "_QtCore"):
1612 | Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR
1613 | Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR
1614 | Qt.QtCompat.dataChanged = (
1615 | lambda self, topleft, bottomright, roles=None:
1616 | self.dataChanged.emit(topleft, bottomright)
1617 | )
1618 |
1619 | _reassign_misplaced_members("PyQt4")
1620 |
1621 | # QFileDialog QtCompat decorator
1622 | def _standardizeQFileDialog(some_function):
1623 | """Decorator that makes PyQt4 return conform to other bindings"""
1624 | def wrapper(*args, **kwargs):
1625 | ret = (some_function(*args, **kwargs))
1626 |
1627 | # PyQt4 only returns the selected filename, force it to a
1628 | # standard return of the selected filename, and a empty string
1629 | # for the selected filter
1630 | return ret, ''
1631 |
1632 | wrapper.__doc__ = some_function.__doc__
1633 | wrapper.__name__ = some_function.__name__
1634 |
1635 | return wrapper
1636 |
1637 | decorators = {
1638 | "QFileDialog": {
1639 | "getOpenFileName": _standardizeQFileDialog,
1640 | "getOpenFileNames": _standardizeQFileDialog,
1641 | "getSaveFileName": _standardizeQFileDialog,
1642 | }
1643 | }
1644 | _build_compatibility_members('PyQt4', decorators)
1645 |
1646 |
1647 | def _none():
1648 | """Internal option (used in installer)"""
1649 |
1650 | Mock = type("Mock", (), {"__getattr__": lambda Qt, attr: None})
1651 |
1652 | Qt.__binding__ = "None"
1653 | Qt.__qt_version__ = "0.0.0"
1654 | Qt.__binding_version__ = "0.0.0"
1655 | Qt.QtCompat.loadUi = lambda uifile, baseinstance=None: None
1656 | Qt.QtCompat.setSectionResizeMode = lambda *args, **kwargs: None
1657 |
1658 | for submodule in _common_members.keys():
1659 | setattr(Qt, submodule, Mock())
1660 | setattr(Qt, "_" + submodule, Mock())
1661 |
1662 |
1663 | def _log(text):
1664 | if QT_VERBOSE:
1665 | sys.stdout.write(text + "\n")
1666 |
1667 |
1668 | def _convert(lines):
1669 | """Convert compiled .ui file from PySide2 to Qt.py
1670 |
1671 | Arguments:
1672 | lines (list): Each line of of .ui file
1673 |
1674 | Usage:
1675 | >> with open("myui.py") as f:
1676 | .. lines = _convert(f.readlines())
1677 |
1678 | """
1679 |
1680 | def parse(line):
1681 | line = line.replace("from PySide2 import", "from Qt import QtCompat,")
1682 | line = line.replace("QtWidgets.QApplication.translate",
1683 | "QtCompat.translate")
1684 | if "QtCore.SIGNAL" in line:
1685 | raise NotImplementedError("QtCore.SIGNAL is missing from PyQt5 "
1686 | "and so Qt.py does not support it: you "
1687 | "should avoid defining signals inside "
1688 | "your ui files.")
1689 | return line
1690 |
1691 | parsed = list()
1692 | for line in lines:
1693 | line = parse(line)
1694 | parsed.append(line)
1695 |
1696 | return parsed
1697 |
1698 |
1699 | def _cli(args):
1700 | """Qt.py command-line interface"""
1701 | import argparse
1702 |
1703 | parser = argparse.ArgumentParser()
1704 | parser.add_argument("--convert",
1705 | help="Path to compiled Python module, e.g. my_ui.py")
1706 | parser.add_argument("--compile",
1707 | help="Accept raw .ui file and compile with native "
1708 | "PySide2 compiler.")
1709 | parser.add_argument("--stdout",
1710 | help="Write to stdout instead of file",
1711 | action="store_true")
1712 | parser.add_argument("--stdin",
1713 | help="Read from stdin instead of file",
1714 | action="store_true")
1715 |
1716 | args = parser.parse_args(args)
1717 |
1718 | if args.stdout:
1719 | raise NotImplementedError("--stdout")
1720 |
1721 | if args.stdin:
1722 | raise NotImplementedError("--stdin")
1723 |
1724 | if args.compile:
1725 | raise NotImplementedError("--compile")
1726 |
1727 | if args.convert:
1728 | sys.stdout.write("#\n"
1729 | "# WARNING: --convert is an ALPHA feature.\n#\n"
1730 | "# See https://github.com/mottosso/Qt.py/pull/132\n"
1731 | "# for details.\n"
1732 | "#\n")
1733 |
1734 | #
1735 | # ------> Read
1736 | #
1737 | with open(args.convert) as f:
1738 | lines = _convert(f.readlines())
1739 |
1740 | backup = "%s_backup%s" % os.path.splitext(args.convert)
1741 | sys.stdout.write("Creating \"%s\"..\n" % backup)
1742 | shutil.copy(args.convert, backup)
1743 |
1744 | #
1745 | # <------ Write
1746 | #
1747 | with open(args.convert, "w") as f:
1748 | f.write("".join(lines))
1749 |
1750 | sys.stdout.write("Successfully converted \"%s\"\n" % args.convert)
1751 |
1752 |
1753 | class MissingMember(object):
1754 | """
1755 | A placeholder type for a missing Qt object not
1756 | included in Qt.py
1757 |
1758 | Args:
1759 | name (str): The name of the missing type
1760 | details (str): An optional custom error message
1761 | """
1762 | ERR_TMPL = ("{} is not a common object across PySide2 "
1763 | "and the other Qt bindings. It is not included "
1764 | "as a common member in the Qt.py layer")
1765 |
1766 | def __init__(self, name, details=''):
1767 | self.__name = name
1768 | self.__err = self.ERR_TMPL.format(name)
1769 |
1770 | if details:
1771 | self.__err = "{}: {}".format(self.__err, details)
1772 |
1773 | def __repr__(self):
1774 | return "<{}: {}>".format(self.__class__.__name__, self.__name)
1775 |
1776 | def __getattr__(self, name):
1777 | raise NotImplementedError(self.__err)
1778 |
1779 | def __call__(self, *a, **kw):
1780 | raise NotImplementedError(self.__err)
1781 |
1782 |
1783 | def _install():
1784 | # Default order (customise order and content via QT_PREFERRED_BINDING)
1785 | default_order = ("PySide2", "PyQt5", "PySide", "PyQt4")
1786 | preferred_order = list(
1787 | b for b in QT_PREFERRED_BINDING.split(os.pathsep) if b
1788 | )
1789 |
1790 | order = preferred_order or default_order
1791 |
1792 | available = {
1793 | "PySide2": _pyside2,
1794 | "PyQt5": _pyqt5,
1795 | "PySide": _pyside,
1796 | "PyQt4": _pyqt4,
1797 | "None": _none
1798 | }
1799 |
1800 | _log("Order: '%s'" % "', '".join(order))
1801 |
1802 | # Allow site-level customization of the available modules.
1803 | _apply_site_config()
1804 |
1805 | found_binding = False
1806 | for name in order:
1807 | _log("Trying %s" % name)
1808 |
1809 | try:
1810 | available[name]()
1811 | found_binding = True
1812 | break
1813 |
1814 | except ImportError as e:
1815 | _log("ImportError: %s" % e)
1816 |
1817 | except KeyError:
1818 | _log("ImportError: Preferred binding '%s' not found." % name)
1819 |
1820 | if not found_binding:
1821 | # If not binding were found, throw this error
1822 | raise ImportError("No Qt binding were found.")
1823 |
1824 | # Install individual members
1825 | for name, members in _common_members.items():
1826 | try:
1827 | their_submodule = getattr(Qt, "_%s" % name)
1828 | except AttributeError:
1829 | continue
1830 |
1831 | our_submodule = getattr(Qt, name)
1832 |
1833 | # Enable import *
1834 | __all__.append(name)
1835 |
1836 | # Enable direct import of submodule,
1837 | # e.g. import Qt.QtCore
1838 | sys.modules[__name__ + "." + name] = our_submodule
1839 |
1840 | for member in members:
1841 | # Accept that a submodule may miss certain members.
1842 | try:
1843 | their_member = getattr(their_submodule, member)
1844 | except AttributeError:
1845 | _log("'%s.%s' was missing." % (name, member))
1846 | continue
1847 |
1848 | setattr(our_submodule, member, their_member)
1849 |
1850 | # Install missing member placeholders
1851 | for name, members in _missing_members.items():
1852 | our_submodule = getattr(Qt, name)
1853 |
1854 | for member in members:
1855 |
1856 | # If the submodule already has this member installed,
1857 | # either by the common members, or the site config,
1858 | # then skip installing this one over it.
1859 | if hasattr(our_submodule, member):
1860 | continue
1861 |
1862 | placeholder = MissingMember("{}.{}".format(name, member),
1863 | details=members[member])
1864 | setattr(our_submodule, member, placeholder)
1865 |
1866 | # Enable direct import of QtCompat
1867 | sys.modules['Qt.QtCompat'] = Qt.QtCompat
1868 |
1869 | # Backwards compatibility
1870 | if hasattr(Qt.QtCompat, 'loadUi'):
1871 | Qt.QtCompat.load_ui = Qt.QtCompat.loadUi
1872 |
1873 |
1874 | _install()
1875 |
1876 | # Setup Binding Enum states
1877 | Qt.IsPySide2 = Qt.__binding__ == 'PySide2'
1878 | Qt.IsPyQt5 = Qt.__binding__ == 'PyQt5'
1879 | Qt.IsPySide = Qt.__binding__ == 'PySide'
1880 | Qt.IsPyQt4 = Qt.__binding__ == 'PyQt4'
1881 |
1882 | """Augment QtCompat
1883 |
1884 | QtCompat contains wrappers and added functionality
1885 | to the original bindings, such as the CLI interface
1886 | and otherwise incompatible members between bindings,
1887 | such as `QHeaderView.setSectionResizeMode`.
1888 |
1889 | """
1890 |
1891 | Qt.QtCompat._cli = _cli
1892 | Qt.QtCompat._convert = _convert
1893 |
1894 | # Enable command-line interface
1895 | if __name__ == "__main__":
1896 | _cli(sys.argv[1:])
1897 |
1898 |
1899 | # The MIT License (MIT)
1900 | #
1901 | # Copyright (c) 2016-2017 Marcus Ottosson
1902 | #
1903 | # Permission is hereby granted, free of charge, to any person obtaining a copy
1904 | # of this software and associated documentation files (the "Software"), to deal
1905 | # in the Software without restriction, including without limitation the rights
1906 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1907 | # copies of the Software, and to permit persons to whom the Software is
1908 | # furnished to do so, subject to the following conditions:
1909 | #
1910 | # The above copyright notice and this permission notice shall be included in
1911 | # all copies or substantial portions of the Software.
1912 | #
1913 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1914 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1915 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1916 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1917 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1918 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1919 | # SOFTWARE.
1920 | #
1921 | # In PySide(2), loadUi does not exist, so we implement it
1922 | #
1923 | # `_UiLoader` is adapted from the qtpy project, which was further influenced
1924 | # by qt-helpers which was released under a 3-clause BSD license which in turn
1925 | # is based on a solution at:
1926 | #
1927 | # - https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8
1928 | #
1929 | # The License for this code is as follows:
1930 | #
1931 | # qt-helpers - a common front-end to various Qt modules
1932 | #
1933 | # Copyright (c) 2015, Chris Beaumont and Thomas Robitaille
1934 | #
1935 | # All rights reserved.
1936 | #
1937 | # Redistribution and use in source and binary forms, with or without
1938 | # modification, are permitted provided that the following conditions are
1939 | # met:
1940 | #
1941 | # * Redistributions of source code must retain the above copyright
1942 | # notice, this list of conditions and the following disclaimer.
1943 | # * Redistributions in binary form must reproduce the above copyright
1944 | # notice, this list of conditions and the following disclaimer in the
1945 | # documentation and/or other materials provided with the
1946 | # distribution.
1947 | # * Neither the name of the Glue project nor the names of its contributors
1948 | # may be used to endorse or promote products derived from this software
1949 | # without specific prior written permission.
1950 | #
1951 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
1952 | # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
1953 | # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
1954 | # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
1955 | # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
1956 | # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
1957 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
1958 | # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
1959 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
1960 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
1961 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1962 | #
1963 | # Which itself was based on the solution at
1964 | #
1965 | # https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8
1966 | #
1967 | # which was released under the MIT license:
1968 | #
1969 | # Copyright (c) 2011 Sebastian Wiesner
1970 | # Modifications by Charl Botha
1971 | #
1972 | # Permission is hereby granted, free of charge, to any person obtaining a
1973 | # copy of this software and associated documentation files
1974 | # (the "Software"),to deal in the Software without restriction,
1975 | # including without limitation
1976 | # the rights to use, copy, modify, merge, publish, distribute, sublicense,
1977 | # and/or sell copies of the Software, and to permit persons to whom the
1978 | # Software is furnished to do so, subject to the following conditions:
1979 | #
1980 | # The above copyright notice and this permission notice shall be included
1981 | # in all copies or substantial portions of the Software.
1982 | #
1983 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1984 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
1985 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
1986 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
1987 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
1988 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
1989 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1990 |
--------------------------------------------------------------------------------
/python/scriptsmenu/vendor/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Colorbleed/scriptsmenu/b13c9a61412788b23d06a4e671eff5debba1f006/python/scriptsmenu/vendor/__init__.py
--------------------------------------------------------------------------------
/python/scriptsmenu/version.py:
--------------------------------------------------------------------------------
1 | VERSION_MAJOR = 1
2 | VERSION_MINOR = 5
3 | VERSION_PATCH = 2
4 |
5 |
6 | version = '{}.{}.{}'.format(VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH)
7 | __version__ = version
8 |
9 | __all__ = ['version', '__version__']
10 |
--------------------------------------------------------------------------------
/samples/example_scriptmenu.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | from Qt import QtWidgets, QtGui
5 |
6 | from scriptsmenu import ScriptsMenu
7 | from scriptsmenu.scriptsmenu import (
8 | load_configuration,
9 | build_from_configuration
10 | )
11 |
12 | # set the example evironment variable
13 | os.environ["SCRIPTMENU"] = os.path.dirname(__file__)
14 | config = os.path.expandvars(r"$SCRIPTMENU/sample_configuration_a.json")
15 | config = load_configuration(config) # parse the .json file
16 |
17 | app = QtWidgets.QApplication(sys.argv)
18 |
19 | menu = ScriptsMenu(title="Scripts", parent=None)
20 |
21 | # populate the menu using the configuration JSON file.
22 | menu.build_from_configuration(menu, config)
23 |
24 | menu.exec_(QtGui.QCursor.pos())
25 |
26 | app.exec_()
27 |
--------------------------------------------------------------------------------
/samples/resources/script_a.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Colorbleed/scriptsmenu/b13c9a61412788b23d06a4e671eff5debba1f006/samples/resources/script_a.png
--------------------------------------------------------------------------------
/samples/sample_configuration_a.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "title": "Animation",
4 | "type": "menu",
5 | "items": [
6 | {
7 | "title": "Script A",
8 | "tooltip": "Script A",
9 | "command": "$SCRIPTMENU/script_a.py",
10 | "sourcetype": "file",
11 | "tags": [
12 | "test",
13 | "script",
14 | "cluster"
15 | ],
16 | "icon": "$SCRIPTMENU/resources/script_a.png",
17 | "label": "SCR A"
18 | },
19 | {
20 | "title": "Script B",
21 | "tooltip": "Run script B",
22 | "command": "$SCRIPTMENU/script_b.py",
23 | "sourcetype": "file",
24 | "tags": [
25 | "test",
26 | "script",
27 | "curves"
28 | ]
29 | },
30 | {
31 | "title": "Script C",
32 | "tooltip": "Run script C",
33 | "command": "$SCRIPTMENU/script_c.py",
34 | "sourcetype": "file",
35 | "tags": [
36 | "test",
37 | "script",
38 | "joints"
39 | ],
40 | "icon": ""
41 | }
42 | ]
43 | },
44 | {
45 | "title": "Modeling",
46 | "type": "menu",
47 | "items": [
48 | {
49 | "type": "action",
50 | "title": "Script A",
51 | "tooltip": "Run script A",
52 | "command": "$SCRIPTMENU/script_a.py",
53 | "sourcetype": "file",
54 | "tags": [
55 | "test",
56 | "script",
57 | "model",
58 | "blendshapes"
59 | ]
60 | },
61 | {
62 | "type": "action",
63 | "title": "Script B",
64 | "tooltip": "Run script B",
65 | "command": "$SCRIPTMENU/script_b.py",
66 | "sourcetype": "file",
67 | "tags": [
68 | "test",
69 | "script",
70 | "normals",
71 | "model"
72 | ]
73 | },
74 | {
75 | "type": "action",
76 | "title": "Script C",
77 | "tooltip": "Run script C",
78 | "command": "$SCRIPTMENU/script_c.py",
79 | "sourcetype": "file",
80 | "tags": [
81 | "math",
82 | "power",
83 | "sum"
84 | ]
85 | }
86 | ]
87 | },
88 | {
89 | "type": "menu",
90 | "title": "MEL",
91 | "items": [
92 | {
93 | "title": "Create cube",
94 | "tooltip": "Launch character rigging tool",
95 | "command": "polyCube -w 1 -h 1 -d 1;",
96 | "sourcetype": "mel",
97 | "tags": [
98 | "test",
99 | "script",
100 | "mel"
101 | ]
102 | }
103 | ]
104 | }
105 | ]
--------------------------------------------------------------------------------
/samples/sample_configuration_b.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "title":"Launcher",
4 | "type": "menu",
5 | "items": [
6 | {
7 | "type": "action",
8 | "title": "Echo command",
9 | "tooltip": "Test print",
10 | "command": "print('I am a tool launcher!')",
11 | "sourcetype": "python",
12 | "tags": [
13 | "test",
14 | "script",
15 | "echo",
16 | "command"
17 | ]
18 | },
19 | {
20 | "type": "action",
21 | "title": "Echo second command",
22 | "tooltip": "Test print",
23 | "command": "print('I am a tool killer!')",
24 | "sourcetype": "python",
25 | "tags": [
26 | "test",
27 | "script",
28 | "echo",
29 | "command"
30 | ]
31 | },
32 | {
33 | "type": "action",
34 | "title": "Launch script A",
35 | "tooltip": "Test launcher",
36 | "command": "$SCRIPTMENU/script_a.py",
37 | "sourcetype": "file",
38 | "tags": [
39 | "test",
40 | "script",
41 | "launch"
42 | ]
43 | }
44 | ]
45 | }
46 | ]
--------------------------------------------------------------------------------
/samples/script_a.py:
--------------------------------------------------------------------------------
1 | print("Running script A, pretty slick right")
2 |
--------------------------------------------------------------------------------
/samples/script_b.py:
--------------------------------------------------------------------------------
1 | print("Running script B, also nice but watch till "
2 | "you add an actual script here")
3 |
--------------------------------------------------------------------------------
/samples/script_c.py:
--------------------------------------------------------------------------------
1 | print("Running script C, example of an actual script")
2 |
3 |
4 | def sum_and_power(a, b, power):
5 | return pow((a + b), power)
6 |
7 |
8 | result = sum_and_power(5, 20, 4)
9 | print result
10 |
--------------------------------------------------------------------------------