├── .gitignore ├── CONTRIBUTORS.md ├── README.md ├── quicklauncher.py ├── setup.py └── tests ├── __init__.py └── test_api.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | *.py[co] 3 | 4 | # Packages 5 | *.egg 6 | *.egg-info 7 | dist/ 8 | build/ 9 | eggs/ 10 | parts/ 11 | var/ 12 | sdist/ 13 | develop-eggs/ 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | #Translations 24 | *.mo 25 | 26 | #Virtualenv 27 | venv/ 28 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributions to `quicklauncher` 2 | 3 | ## Creator & Maintainer 4 | 5 | * Cesar Saez 6 | 7 | 8 | ## Contributors 9 | 10 | * Marcus Ottosson (mottosso) 11 | - Added TAB-key for activation of window 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | QuickLauncher 2 | ============= 3 | 4 | A minimal Qt based menu to quickly find and execute Maya commands and user scripts (licensed under MIT license). 5 | 6 | Quicklauncher relies on PySide/PySide2/PySide6 and works on Autodesk Maya 2014 or greater. (last tested on Maya 2025) 7 | 8 | ![quicklauncher](https://cloud.githubusercontent.com/assets/2292742/20506707/8b19023c-b034-11e6-8598-a480924f8740.gif) 9 | 10 | 11 | ## Installation 12 | 1. get the [latest release](https://github.com/csaez/quicklauncher/releases) 13 | 2. copy `quicklauncher.py` to your maya script directory. e.g. `%UserProfile%\Documents\maya\2026\scripts` on windows 14 | 15 | ## Usage 16 | 17 | ```python 18 | import quicklauncher 19 | quicklauncher.show() 20 | ``` 21 | 22 | You can also select the folder in which `quicklauncher` will look for user scripts (repo). 23 | 24 | ```python 25 | import quicklauncher 26 | quicklauncher.select_repo() 27 | ``` 28 | 29 | 30 | 31 | ## Examples of advanced usage 32 | 33 | import quicklauncher as ql 34 | 35 | ``` python 36 | # show menu 37 | ql.show() 38 | 39 | # repository dialog 40 | ql.select_repo() 41 | 42 | # ... or go deeper using the API 43 | ql.get_repo() 44 | ql.set_repo(repository_path) 45 | 46 | ql.get_scripts() # {script_name: script_fullpath, ...} 47 | ql.list_scripts() # [script_name, ...] 48 | ql.run_script(script_name) 49 | 50 | ql.get_commands() # {cmd_name: cmd_object, ...} 51 | ql.list_commands() # [cmd_name, ...] 52 | ql.run_cmd(cmd_name) 53 | ``` 54 | 55 | 56 | > **TIP:** You can refresh the list of available scripts without restarting Maya by simply 57 | > reloading the python module in the script editor (or add a little python script that does 58 | > this to your repo so it's available from the menu itself). 59 | > 60 | > ```python 61 | > import quicklauncher 62 | > reload(quicklauncher) 63 | > ``` 64 | 65 | ## Contributing 66 | 67 | - [Check for open issues](https://github.com/csaez/quicklauncher/issues) or open a fresh issue to 68 | start a discussion around a feature idea or a bug. 69 | 70 | - Fork the [quicklauncher repository on Github](https://github.com/csaez/quicklauncher) to start 71 | making your changes (make sure to isolate your changes in a local branch when possible). 72 | 73 | - Write a test which shows that the bug was fixed or that the feature works as expected. 74 | 75 | - Send a pull request and bug me until it gets merged. :) 76 | 77 | 78 | Make sure to add yourself to `CONTRIBUTORS.md`! 79 | -------------------------------------------------------------------------------- /quicklauncher.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | # Copyright (c) 2014 Cesar Saez 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | import os 24 | import sys 25 | import inspect 26 | from importlib import import_module 27 | from maya import cmds, mel 28 | 29 | # qt bindings 30 | try: 31 | from PySide6 import QtCore, QtGui, QtWidgets 32 | except ImportError: 33 | try: 34 | from PySide2 import QtCore, QtGui, QtWidgets 35 | except ImportError: 36 | from PySide import QtCore, QtGui 37 | QtWidgets = QtGui 38 | 39 | 40 | __all__ = ['show', 'get_main_window', 'get_repo', 'set_repo', 'list_scripts', 41 | 'get_scripts', 'list_commands', 'get_commands', 'run_script', 42 | 'run_cmd'] 43 | 44 | 45 | def get_repo(): 46 | settings = QtCore.QSettings("csaez", "quicklauncher") 47 | repo = settings.value("repo") 48 | if not repo: 49 | repo = os.path.join(os.path.expanduser("~"), "quicklauncher") 50 | set_repo(repo) 51 | return repo 52 | 53 | 54 | def set_repo(repo_path): 55 | if not os.path.exists(repo_path): 56 | os.mkdir(repo_path) 57 | settings = QtCore.QSettings("csaez", "quicklauncher") 58 | settings.setValue("repo", repo_path) 59 | 60 | 61 | def get_scripts(): # {name: path, ...} 62 | items = [os.path.join(path, f) 63 | for (path, _, files) in os.walk(get_repo()) 64 | for f in files if f.endswith(".py") or f.endswith(".mel")] 65 | return {os.path.basename(x): x for x in items} 66 | 67 | 68 | def list_scripts(): 69 | return list(get_scripts().keys()) 70 | 71 | 72 | def get_commands(): # {name: cmd, ...} 73 | items = [(name.lower(), value) 74 | for (name, value) in inspect.getmembers(cmds, callable)] 75 | return dict(items) 76 | 77 | 78 | def list_commands(): 79 | return list(get_commands().keys()) 80 | 81 | 82 | def run_script(script_name): 83 | # validate 84 | script_path = get_scripts().get(script_name) 85 | if not script_path: 86 | return False 87 | # delegate to lang runner 88 | if script_name.endswith(".py"): 89 | return _run_python(script_path) 90 | if script_name.endswith(".mel"): 91 | return _run_mel(script_path) 92 | return False 93 | 94 | def _run_mel(script_path): 95 | filepath = os.path.join(get_repo(), script_path) 96 | if not os.path.exists(filepath): 97 | return False 98 | with open(filepath, "r") as fp: 99 | mel.eval(fp.read()) 100 | return True 101 | 102 | def _run_python(script_path): 103 | # add to pythonpath and execute 104 | sys.path.insert(0, os.path.dirname(script_path)) 105 | module_name = os.path.split(script_path)[-1].replace(".py", "") 106 | import_module(module_name) 107 | # cleanup 108 | del sys.modules[module_name] 109 | sys.path = sys.path[1:] 110 | return True 111 | 112 | def run_cmd(cmd_name): 113 | cmd = get_commands().get(cmd_name.lower()) 114 | if cmd is not None: 115 | cmd() 116 | return True 117 | return False 118 | 119 | 120 | # GUI 121 | class QuickLauncherMenu(QtWidgets.QMenu): 122 | 123 | """A menu to find and execute Maya commands and user scripts.""" 124 | 125 | def __init__(self, *args, **kwds): 126 | super(QuickLauncherMenu, self).__init__(*args, **kwds) 127 | self.init_gui() 128 | 129 | def init_gui(self): 130 | # set search field 131 | self.lineEdit = QtWidgets.QLineEdit(self) 132 | action = QtWidgets.QWidgetAction(self) 133 | action.setDefaultWidget(self.lineEdit) 134 | self.addAction(action) 135 | self.lineEdit.setFocus() 136 | # completion 137 | items = list_scripts() 138 | items.extend(list_commands()) 139 | completer = QtWidgets.QCompleter(items, self) 140 | completer.setCompletionMode(QtWidgets.QCompleter.PopupCompletion) 141 | completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) 142 | self.lineEdit.setCompleter(completer) 143 | # connect signals 144 | self.lineEdit.returnPressed.connect(self.accept) 145 | self.setMinimumWidth(300) 146 | 147 | 148 | def accept(self): 149 | text = self.lineEdit.text() 150 | completion = self.lineEdit.completer().currentCompletion() 151 | self.close() 152 | return run_cmd(text) or \ 153 | run_cmd(completion) or \ 154 | run_script(text) or \ 155 | run_script(completion) 156 | 157 | 158 | def select_repo(): 159 | repo = QtWidgets.QFileDialog.getExistingDirectory( 160 | caption="Select repository directory", 161 | parent=get_main_window(), 162 | dir=get_repo()) 163 | if repo: 164 | set_repo(repo) 165 | 166 | 167 | def get_main_window(widget=None): 168 | widget = widget or QtWidgets.QApplication.activeWindow() 169 | if widget is None: 170 | return 171 | parent = widget.parent() 172 | if parent is None: 173 | return widget 174 | return get_main_window(parent) 175 | 176 | 177 | def show(): 178 | maya_window = get_main_window() 179 | # look for existing instances of QuickLauncher 180 | ql = get_instance(maya_window, QuickLauncherMenu) 181 | if ql is None: 182 | # create a new instance 183 | ql = QuickLauncherMenu(maya_window) 184 | # clear out, move and show 185 | ql.lineEdit.setText("") 186 | position_window(ql) 187 | ql.exec_() 188 | 189 | 190 | def position_window(window): 191 | """Position window to mouse cursor""" 192 | pos = QtGui.QCursor.pos() 193 | window.move(pos.x(), pos.y()) 194 | 195 | 196 | def get_instance(parent, gui_class): 197 | for children in parent.children(): 198 | if isinstance(children, gui_class): 199 | return children 200 | return None 201 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='quicklauncher', 5 | description='quicklauncher is a simple menu to launch Maya commands and user scripts.', 6 | version='3.3.0', 7 | license='The MIT License', 8 | author='Cesar Saez', 9 | author_email='cesarte@gmail.com', 10 | url='https://www.github.com/csaez/quicklauncher', 11 | py_modules=["quicklauncher"], 12 | ) 13 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csaez/quicklauncher/754fcbff6f21d80f1caa0318e5792e9e465985f9/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from collections import Iterable 4 | import quicklauncher as ql 5 | 6 | 7 | _REPO = ql.get_repo() 8 | 9 | 10 | class MiscCase(unittest.TestCase): 11 | def test_show(self): 12 | self.assertIsNotNone(ql.show) 13 | 14 | def test_list_scripts(self): 15 | self.assertIsInstance(ql.list_scripts(), Iterable) 16 | self.assertIsInstance(ql.get_scripts(), dict) 17 | self.assertEqual(len(ql.list_scripts()), len(ql.get_scripts())) 18 | 19 | def test_list_commands(self): 20 | self.assertIsInstance(ql.list_commands(), Iterable) 21 | self.assertIsInstance(ql.get_commands(), dict) 22 | self.assertEqual(len(ql.list_commands()), 23 | len(ql.get_commands())) 24 | 25 | def test_run_cmd(self): 26 | self.assertTrue(ql.run_cmd("ls")) 27 | 28 | 29 | class RepoCase(unittest.TestCase): 30 | def setUp(self): 31 | self.test_path = os.path.join(os.path.expanduser("~"), "testing") 32 | ql.set_repo(self.test_path) 33 | 34 | def tearDown(self): 35 | ql.set_repo(_REPO) 36 | os.rmdir(self.test_path) 37 | 38 | def test_repo(self): 39 | test_path = os.path.join(os.path.expanduser("~"), "testing") 40 | self.assertEqual(ql.get_repo(), test_path) 41 | self.assertTrue(os.path.isdir(ql.get_repo())) 42 | self.assertEqual(ql.get_repo(), test_path) 43 | 44 | 45 | class ScriptCase(unittest.TestCase): 46 | def setUp(self): 47 | self.test_path = os.path.join(os.path.expanduser("~"), "testing") 48 | ql.set_repo(self.test_path) 49 | 50 | with open(os.path.join(ql.get_repo(), "testsuite.py"), "w") as f: 51 | code = 'print("im in the root dir")' 52 | f.write(code) 53 | subdir = os.path.join(ql.get_repo(), "test") 54 | if not os.path.exists(subdir): 55 | os.mkdir(subdir) 56 | with open(os.path.join(subdir, "testsuite.py"), "w") as f: 57 | code = 'print("im in a subdir")' 58 | f.write(code) 59 | 60 | def tearDown(self): 61 | for x in ("testsuite.py", "testsuite.pyc"): 62 | path = os.path.join(ql.get_repo(), x) 63 | if os.path.exists(path): 64 | os.remove(path) 65 | subdir = os.path.join(ql.get_repo(), "test") 66 | for x in ("testsuite.py", "testsuite.pyc"): 67 | path = os.path.join(subdir, x) 68 | if os.path.exists(path): 69 | os.remove(path) 70 | os.rmdir(subdir) 71 | 72 | ql.set_repo(_REPO) 73 | os.rmdir(self.test_path) 74 | 75 | def test_run_script(self): 76 | self.assertTrue(ql.run_script("testsuite.py")) 77 | self.assertTrue(ql.run_script("test/testsuite.py")) 78 | 79 | 80 | if __name__ == "__main__": 81 | unittest.main(verbosity=3, exit=False) 82 | --------------------------------------------------------------------------------