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