├── .github └── workflows │ └── python-publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── pyproject.toml ├── requirements.txt └── unreal_script_editor ├── __init__.py ├── codeEditor ├── .gitignore ├── README.md ├── __init__.py ├── codeEditor.py ├── highlighter │ ├── __init__.py │ ├── jsonHighlight.py │ └── pyHighlight.py └── main.py ├── config.txt ├── icons ├── clearAll.png ├── clearHistory.png ├── clearInput.png ├── execute.png └── executeAll.png ├── main.py ├── outputTextWidget.py ├── startup.py └── ui ├── __init__.py ├── output_text_widget.py ├── output_text_widget.ui ├── script_editor.py └── script_editor.ui /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.x' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 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 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Xingyu Lei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Unreal Python Editor [![PyPI](https://img.shields.io/pypi/v/unreal-script-editor?color=blue)](https://pypi.org/project/unreal-script-editor/) ![](https://img.shields.io/github/stars/leixingyu/unrealScriptEditor?label=GitHub%E2%AD%90) 3 | 4 | A Qt widget that's the Unreal equivalent of the "Maya Script Editor". 5 | This repo hosts the Python module, for the Unreal plugin, see [this repo](https://github.com/hannesdelbeke/unreal-plugin-python-script-editor) ![](https://img.shields.io/github/stars/hannesdelbeke/unreal-plugin-python-script-editor?label=%E2%AD%90) 6 | 7 | 8 | ## About The Project 9 | 10 | ui 11 | 12 | With the rapid advancement of Unreal Engine and the Python support in Unreal 13 | Engine, more and more people are jumping into Unreal Python scripting. 14 | Hence the creation of this tool! 15 | 16 | ## Getting Started 17 | 18 | ### Prerequisites 19 | 20 | The tool needs the following library to be installed: 21 | 22 | - v0.0.2 uses [Qt.py](https://pypi.org/project/Qt.py/), and [PySide2](https://pypi.org/project/PySide2/) or [PyQt5](https://pypi.org/project/PyQt5/) 23 | - v0.0.3+ uses PySide6 24 | 25 | 26 | ### Add as Menu Button 27 | 28 | The tool is meant to be launched from a menu bar button like such: 29 | 30 | menu 31 | 32 | You can set up this very easily by adding `startup.py` as a startup script, 33 | under _Project Settings - Plugins - Python_ 34 | 35 | - download & extract the project zip file 36 | - find the `startup.py` location, and add it to the startup scripts: e.g. `C:\Downloads\unrealScriptEditor\startup.py` 37 | 38 | 39 | menu 40 | 41 | ### Simple Launch Script 42 | 43 | **If** you just want to launch the tool in Unreal's python console without adding it to menu, 44 | or if you want to customize the location where the tool is being launched; 45 | refer to the following command: 46 | 47 | - the tool has to be in a path that Unreal will search for! 48 | 49 | ```python 50 | from unreal_script_editor import main 51 | global editor 52 | editor = main.show() 53 | ``` 54 | 55 | ### Install as module 56 | install with pip 57 | ```bash 58 | pip install unreal-script-editor 59 | ``` 60 | 61 | Install the module from the repo 62 | ```bash 63 | python -m pip install git+https://github.com/hannesdelbeke/unreal-script-editor 64 | ``` 65 | 66 | ## Features 67 | 68 | - [x] Unreal "native" [stylesheet](https://github.com/leixingyu/unrealStylesheet) 69 | - [x] Save and load python files and temporary scripts 70 | - [x] Code editor short-cut support and Highlighter 71 | - [ ] Auto-completion 72 | 73 | ## Support 74 | 75 | This tool is still in development, if there's any issue please submit your bug 76 | [here](https://github.com/leixingyu/unrealScriptEditor/issues) 77 | or contact [techartlei@gmail.com]() 78 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "setuptools-scm"] 3 | #build-backend = "setuptools.build_meta" 4 | 5 | 6 | [tool.setuptools] 7 | packages = ["unreal_script_editor"] 8 | 9 | 10 | [project] 11 | name = "unreal-script-editor" 12 | authors = [ 13 | {name = "Lei Xingyu"}, 14 | {name = "Hannes Delbeke"}, 15 | ] 16 | description = "A Qt Python script editor, styled in Unreal stylesheet" 17 | readme = "README.md" 18 | requires-python = ">=3.4" 19 | keywords = ["unreal", "scripter"] 20 | license = { file = "LICENSE" } 21 | classifiers = [ 22 | "Programming Language :: Python :: 3.7", 23 | ] 24 | #dynamic = ["dependencies"] 25 | dependencies = ['importlib-metadata; python_version<"3.7"', "unreal-stylesheet", "PySide6"] 26 | #dynamic = ["version"] 27 | version = "0.0.3" 28 | 29 | #[project.optional-dependencies] 30 | #yaml = ["pyyaml"] 31 | 32 | #[project.scripts] 33 | #my-script = "my_package.module:function" 34 | 35 | [project.urls] 36 | Homepage = "https://github.com/leixingyu/unrealScriptEditor" 37 | Source = "https://github.com/leixingyu/unrealScriptEditor" 38 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | unreal-stylesheet 2 | PySide6 3 | -------------------------------------------------------------------------------- /unreal_script_editor/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unreal Script Editor Widget 3 | """ 4 | 5 | # import attributes to expose to the developer 6 | # >>> import unreal_script_editor 7 | # >>> from unreal_script_editor.MY_ATTR 8 | 9 | from unreal_script_editor.main import show -------------------------------------------------------------------------------- /unreal_script_editor/codeEditor/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc -------------------------------------------------------------------------------- /unreal_script_editor/codeEditor/README.md: -------------------------------------------------------------------------------- 1 |
2 |

Qt Code Editor

3 | 4 |

5 | A PyQt5/PySide2 version of code editor with line counter and syntax highlighting 6 |

7 |
8 | 9 | ## About The Project 10 | 11 |
12 | 13 |
14 | code-editor 15 |
16 | 17 |
18 | 19 | A central repo to store useful stuff related to creating a code editor based on PyQt. 20 | It currently consists of the main code editor subclassing from `QPlainTextEdit` with 21 | a side widget for line counter. It currently supports syntax highlighting for 22 | `.json` and `.py` files, they are `QSyntaxHighlighter` class which can be found 23 | in the **highlighter** folder. 24 | 25 | ## Getting Started 26 | 27 | ### Prerequisites 28 | 29 | - [Qt](https://github.com/mottosso/Qt.py): a module that supports different 30 | python qt bindings 31 | 32 | or alternatively, change the code below to whatever qt binding you have on your machine. 33 | ```python 34 | from PySide6 import QtWidgets, QtCore, QtGui 35 | from PySide6 import _loadUi 36 | ``` 37 | 38 | ### Launch 39 | 40 | 1. Unzip the **qt-code-editor** package and run `main.py` directly 41 | 42 | 2. Or include the following snippet to your ui code to create a 43 | code editor with syntax highlighting. 44 | ```python 45 | import codeEditor 46 | from highlighter.pyHighlight import PythonHighlighter 47 | 48 | editor = codeEditor.CodeEditor() 49 | highlighter = PythonHighlighter(editor.document()) 50 | ``` 51 | 52 | ### Reference 53 | 54 | [Qt Documentation - Code Editor Example](https://doc.qt.io/qt-5/qtwidgets-widgets-codeeditor-example.html) 55 | 56 | [Qt for Python 6.2.2 - Code Editor Example](https://doc.qt.io/qtforpython/examples/example_widgets__codeeditor.html) 57 | 58 | [Python.org - Python syntax highlighting](https://wiki.python.org/moin/PyQt/Python%20syntax%20highlighting) 59 | 60 | -------------------------------------------------------------------------------- /unreal_script_editor/codeEditor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leixingyu/unrealScriptEditor/b0d1509ee4e4b93d7a3976fae3666e999155cb85/unreal_script_editor/codeEditor/__init__.py -------------------------------------------------------------------------------- /unreal_script_editor/codeEditor/codeEditor.py: -------------------------------------------------------------------------------- 1 | """ 2 | https://doc.qt.io/qt-5/qtwidgets-widgets-codeeditor-example.html#the-linenumberarea-class 3 | https://doc.qt.io/qtforpython/examples/example_widgets__codeeditor.html 4 | """ 5 | 6 | from PySide6 import QtCore, QtGui, QtWidgets 7 | 8 | 9 | class LineNumberArea(QtWidgets.QWidget): 10 | def __init__(self, editor): 11 | super(LineNumberArea, self).__init__(editor) 12 | self._code_editor = editor 13 | 14 | def sizeHint(self): 15 | return QtCore.QSize(self._code_editor.line_number_area_width(), 0) 16 | 17 | def paintEvent(self, event): 18 | self._code_editor.lineNumberAreaPaintEvent(event) 19 | 20 | class CodeTextEdit(QtWidgets.QPlainTextEdit): 21 | is_first = False 22 | pressed_keys = list() 23 | 24 | indented = QtCore.Signal(object) 25 | unindented = QtCore.Signal(object) 26 | commented = QtCore.Signal(object) 27 | uncommented = QtCore.Signal(object) 28 | 29 | def __init__(self): 30 | super(CodeTextEdit, self).__init__() 31 | 32 | self.indented.connect(self.do_indent) 33 | self.unindented.connect(self.undo_indent) 34 | self.commented.connect(self.do_comment) 35 | self.uncommented.connect(self.undo_comment) 36 | 37 | def clear_selection(self): 38 | """ 39 | Clear text selection on cursor 40 | """ 41 | pos = self.textCursor().selectionEnd() 42 | self.textCursor().movePosition(pos) 43 | 44 | def get_selection_range(self): 45 | """ 46 | Get text selection line range from cursor 47 | Note: currently only support continuous selection 48 | :return: (int, int). start line number and end line number 49 | """ 50 | cursor = self.textCursor() 51 | if not cursor.hasSelection(): 52 | return 0, 0 53 | 54 | start_pos = cursor.selectionStart() 55 | end_pos = cursor.selectionEnd() 56 | 57 | cursor.setPosition(start_pos) 58 | start_line = cursor.blockNumber() 59 | cursor.setPosition(end_pos) 60 | end_line = cursor.blockNumber() 61 | 62 | return start_line, end_line 63 | 64 | def remove_line_start(self, string, line_number): 65 | """ 66 | Remove certain string occurrence on line start 67 | :param string: str. string pattern to remove 68 | :param line_number: int. line number 69 | """ 70 | cursor = QtGui.QTextCursor( 71 | self.document().findBlockByLineNumber(line_number)) 72 | cursor.select(QtGui.QTextCursor.LineUnderCursor) 73 | text = cursor.selectedText() 74 | if text.startswith(string): 75 | cursor.removeSelectedText() 76 | cursor.insertText(text.split(string, 1)[-1]) 77 | 78 | def insert_line_start(self, string, line_number): 79 | """ 80 | Insert certain string pattern on line start 81 | :param string: str. string pattern to insert 82 | :param line_number: int. line number 83 | """ 84 | cursor = QtGui.QTextCursor( 85 | self.document().findBlockByLineNumber(line_number)) 86 | self.setTextCursor(cursor) 87 | self.textCursor().insertText(string) 88 | 89 | def keyPressEvent(self, event): 90 | """ 91 | Extend the key press event to create key shortcuts 92 | """ 93 | self.is_first = True 94 | self.pressed_keys.append(event.key()) 95 | start_line, end_line = self.get_selection_range() 96 | 97 | # indent event 98 | if event.key() == QtCore.Qt.Key_Tab and \ 99 | (end_line - start_line): 100 | lines = range(start_line, end_line+1) 101 | self.indented.emit(lines) 102 | return 103 | 104 | # un-indent event 105 | elif event.key() == QtCore.Qt.Key_Backtab: 106 | lines = range(start_line, end_line+1) 107 | self.unindented.emit(lines) 108 | return 109 | 110 | super(CodeTextEdit, self).keyPressEvent(event) 111 | 112 | def keyReleaseEvent(self, event): 113 | """ 114 | Extend the key release event to catch key combos 115 | """ 116 | if self.is_first: 117 | self.process_multi_keys(self.pressed_keys) 118 | 119 | self.is_first = False 120 | self.pressed_keys.pop() 121 | super(CodeTextEdit, self).keyReleaseEvent(event) 122 | 123 | def process_multi_keys(self, keys): 124 | """ 125 | Placeholder for processing multiple key combo events 126 | :param keys: [QtCore.Qt.Key]. key combos 127 | """ 128 | # toggle comments indent event 129 | if keys == [QtCore.Qt.Key_Control, QtCore.Qt.Key_Slash]: 130 | pass 131 | 132 | def do_indent(self, lines): 133 | """ 134 | Indent lines 135 | :param lines: [int]. line numbers 136 | """ 137 | for line in lines: 138 | self.insert_line_start('\t', line) 139 | 140 | def undo_indent(self, lines): 141 | """ 142 | Un-indent lines 143 | :param lines: [int]. line numbers 144 | """ 145 | for line in lines: 146 | self.remove_line_start('\t', line) 147 | 148 | def do_comment(self, lines): 149 | """ 150 | Comment out lines 151 | :param lines: [int]. line numbers 152 | """ 153 | for line in lines: 154 | pass 155 | 156 | def undo_comment(self, lines): 157 | """ 158 | Un-comment lines 159 | :param lines: [int]. line numbers 160 | """ 161 | for line in lines: 162 | pass 163 | 164 | 165 | class CodeEditor(CodeTextEdit): 166 | def __init__(self): 167 | super(CodeEditor, self).__init__() 168 | self.setLineWrapMode(QtWidgets.QPlainTextEdit.NoWrap) 169 | 170 | self.line_number_area = LineNumberArea(self) 171 | 172 | self.font = QtGui.QFont() 173 | self.font.setFamily("Consolas") 174 | self.font.setStyleHint(QtGui.QFont.Monospace) 175 | self.font.setPointSize(10) 176 | self.setFont(self.font) 177 | 178 | self.tab_size = 4 179 | self.setTabStopDistance(self.tab_size * self.fontMetrics().horizontalAdvance(' ')) 180 | 181 | self.blockCountChanged.connect(self.update_line_number_area_width) 182 | self.updateRequest.connect(self.update_line_number_area) 183 | 184 | self.update_line_number_area_width(0) 185 | 186 | def line_number_area_width(self): 187 | digits = 1 188 | max_num = max(1, self.blockCount()) 189 | while max_num >= 10: 190 | max_num *= 0.1 191 | digits += 1 192 | 193 | space = 20 + self.fontMetrics().horizontalAdvance('9') * digits 194 | return space 195 | 196 | def resizeEvent(self, e): 197 | super(CodeEditor, self).resizeEvent(e) 198 | cr = self.contentsRect() 199 | width = self.line_number_area_width() 200 | rect = QtCore.QRect(cr.left(), cr.top(), width, cr.height()) 201 | self.line_number_area.setGeometry(rect) 202 | 203 | def lineNumberAreaPaintEvent(self, event): 204 | BACKGROUND_COLOR = QtGui.QColor(21, 21, 21) 205 | LINENUMBER_COLOR = QtGui.QColor(200, 200, 200) 206 | 207 | painter = QtGui.QPainter(self.line_number_area) 208 | painter.fillRect(event.rect(), BACKGROUND_COLOR) 209 | block = self.firstVisibleBlock() 210 | block_number = block.blockNumber() 211 | offset = self.contentOffset() 212 | top = self.blockBoundingGeometry(block).translated(offset).top() 213 | bottom = top + self.blockBoundingRect(block).height() 214 | 215 | while block.isValid() and top <= event.rect().bottom(): 216 | if block.isVisible() and bottom >= event.rect().top(): 217 | number = str(block_number + 1) 218 | painter.setPen(LINENUMBER_COLOR) 219 | width = self.line_number_area.width() - 10 220 | height = self.fontMetrics().height() 221 | painter.drawText( 222 | 0, 223 | int(top), 224 | int(width), 225 | int(height), 226 | QtCore.Qt.AlignRight, 227 | number 228 | ) 229 | 230 | block = block.next() 231 | top = bottom 232 | bottom = top + self.blockBoundingRect(block).height() 233 | block_number += 1 234 | 235 | def update_line_number_area_width(self, new_block_count): 236 | self.setViewportMargins(self.line_number_area_width(), 0, 0, 0) 237 | 238 | def update_line_number_area(self, rect, dy): 239 | if dy: 240 | self.line_number_area.scroll(0, dy) 241 | else: 242 | width = self.line_number_area.width() 243 | self.line_number_area.update(0, rect.y(), width, rect.height()) 244 | 245 | if rect.contains(self.viewport().rect()): 246 | self.update_line_number_area_width(0) 247 | -------------------------------------------------------------------------------- /unreal_script_editor/codeEditor/highlighter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leixingyu/unrealScriptEditor/b0d1509ee4e4b93d7a3976fae3666e999155cb85/unreal_script_editor/codeEditor/highlighter/__init__.py -------------------------------------------------------------------------------- /unreal_script_editor/codeEditor/highlighter/jsonHighlight.py: -------------------------------------------------------------------------------- 1 | from PySide6 import QtWidgets, QtCore, QtGui 2 | 3 | 4 | class HighlightRule(object): 5 | def __init__(self, pattern, cformat): 6 | self.pattern = pattern 7 | self.format = cformat 8 | 9 | 10 | class JsonHighlighter(QtGui.QSyntaxHighlighter): 11 | def __init__(self, parent=None): 12 | """ 13 | Initialize rules with expression pattern and text format 14 | """ 15 | super(JsonHighlighter, self).__init__(parent) 16 | 17 | self.rules = list() 18 | 19 | # cformat = QtGui.QTextCharFormat() 20 | # cformat.setForeground(QtCore.Qt.darkMagenta) 21 | # cformat.setFontWeight(QtGui.QFont.Bold) 22 | 23 | # for pattern in [r"\{", r"\}", r"\[", r"\]"]: 24 | # rule = HighlightRule() 25 | # rule.pattern = QtCore.QRegExp(pattern) 26 | # rule.format = cformat 27 | # self.rules.append(rule) 28 | 29 | # numeric value 30 | cformat = QtGui.QTextCharFormat() 31 | cformat.setForeground(QtCore.Qt.darkBlue) 32 | cformat.setFontWeight(QtGui.QFont.Bold) 33 | pattern = QtCore.QRegExp("([-0-9.]+)(?!([^\"]*\"[\\s]*\\:))") 34 | 35 | rule = HighlightRule(pattern, cformat) 36 | self.rules.append(rule) 37 | 38 | # key 39 | cformat = QtGui.QTextCharFormat() 40 | pattern = QtCore.QRegExp("(\"[^\"]*\")\\s*\\:") 41 | # cformat.setForeground(QtCore.Qt.darkMagenta) 42 | cformat.setFontWeight(QtGui.QFont.Bold) 43 | 44 | rule = HighlightRule(pattern, cformat) 45 | self.rules.append(rule) 46 | 47 | # value 48 | cformat = QtGui.QTextCharFormat() 49 | pattern = QtCore.QRegExp(":+(?:[: []*)(\"[^\"]*\")") 50 | cformat.setForeground(QtCore.Qt.darkGreen) 51 | 52 | rule = HighlightRule(pattern, cformat) 53 | self.rules.append(rule) 54 | 55 | def highlightBlock(self, text): 56 | """ 57 | Override: implementing virtual method of highlighting the text block 58 | """ 59 | for rule in self.rules: 60 | # create a regular expression from the retrieved pattern 61 | expression = QtCore.QRegExp(rule.pattern) 62 | 63 | # check what index that expression occurs at with the ENTIRE text 64 | index = expression.indexIn(text) 65 | while index >= 0: 66 | # get the length of how long the expression is 67 | # set format from the start to the length with the text format 68 | length = expression.matchedLength() 69 | self.setFormat(index, length, rule.format) 70 | 71 | # set index to where the expression ends in the text 72 | index = expression.indexIn(text, index + length) 73 | -------------------------------------------------------------------------------- /unreal_script_editor/codeEditor/highlighter/pyHighlight.py: -------------------------------------------------------------------------------- 1 | """ 2 | https://wiki.python.org/moin/PyQt/Python%20syntax%20highlighting 3 | """ 4 | 5 | 6 | from PySide6 import QtGui 7 | from PySide6.QtCore import QRegularExpression 8 | 9 | 10 | def format(color, style=''): 11 | """ 12 | Return a QTextCharFormat with the given attributes. 13 | """ 14 | _format = QtGui.QTextCharFormat() 15 | _format.setForeground(color) 16 | if 'bold' in style: 17 | _format.setFontWeight(QtGui.QFont.Bold) 18 | if 'italic' in style: 19 | _format.setFontItalic(True) 20 | 21 | return _format 22 | 23 | 24 | # Syntax styles that can be shared by all languages 25 | STYLES = { 26 | 'keyword': format(QtGui.QColor('#cc7832'), 'bold'), 27 | # 'operator': format('red'), 28 | # 'brace': format('darkGray'), 29 | 'defclass': format(QtGui.QColor('#cc7832')), 30 | 'string': format(QtGui.QColor(255, 255, 0)), 31 | 'string2': format(QtGui.QColor('#829755'), 'italic'), 32 | 'comment': format(QtGui.QColor('#47802c')), 33 | 'self': format(QtGui.QColor('#94558d')), 34 | 'numbers': format(QtGui.QColor('#6897bb')), 35 | } 36 | 37 | 38 | class PythonHighlighter(QtGui.QSyntaxHighlighter): 39 | """ 40 | Syntax highlighter for the Python language. 41 | """ 42 | # Python keywords 43 | keywords = [ 44 | 'and', 'assert', 'break', 'class', 'continue', 'def', 45 | 'del', 'elif', 'else', 'except', 'exec', 'finally', 46 | 'for', 'from', 'global', 'if', 'import', 'in', 47 | 'is', 'lambda', 'not', 'or', 'pass', 'print', 48 | 'raise', 'return', 'try', 'while', 'yield', 49 | 'None', 'True', 'False', 50 | ] 51 | 52 | # Python operators 53 | operators = [ 54 | '=', 55 | # Comparison 56 | '==', '!=', '<', '<=', '>', '>=', 57 | # Arithmetic 58 | '\+', '-', '\*', '/', '//', '\%', '\*\*', 59 | # In-place 60 | '\+=', '-=', '\*=', '/=', '\%=', 61 | # Bitwise 62 | '\^', '\|', '\&', '\~', '>>', '<<', 63 | ] 64 | 65 | # Python braces 66 | braces = [ 67 | '\{', '\}', '\(', '\)', '\[', '\]', 68 | ] 69 | 70 | def __init__(self, parent=None): 71 | super(PythonHighlighter, self).__init__(parent) 72 | 73 | # Multi-line strings (expression, flag, style) 74 | 75 | self.tri_single = (QRegularExpression("'''"), 1, STYLES['string2']) 76 | self.tri_double = (QRegularExpression('"""'), 2, STYLES['string2']) 77 | 78 | rules = [] 79 | 80 | # Keyword, operator, and brace rules 81 | rules += [(r'\b%s\b' % w, 0, STYLES['keyword']) 82 | for w in PythonHighlighter.keywords] 83 | # rules += [(r'%s' % o, 0, STYLES['operator']) 84 | # for o in PythonHighlighter.operators] 85 | # rules += [(r'%s' % b, 0, STYLES['brace']) 86 | # for b in PythonHighlighter.braces] 87 | 88 | # All other rules 89 | rules += [ 90 | # 'self' 91 | (r'\bself\b', 0, STYLES['self']), 92 | 93 | # 'def' followed by an identifier 94 | (r'\bdef\b\s*(\w+)', 1, STYLES['defclass']), 95 | # 'class' followed by an identifier 96 | (r'\bclass\b\s*(\w+)', 1, STYLES['defclass']), 97 | 98 | # Numeric literals 99 | (r'\b[+-]?[0-9]+[lL]?\b', 0, STYLES['numbers']), 100 | (r'\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b', 0, STYLES['numbers']), 101 | (r'\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b', 0, STYLES['numbers']), 102 | 103 | # Double-quoted string, possibly containing escape sequences 104 | (r'"[^"\\]*(\\.[^"\\]*)*"', 0, STYLES['string']), 105 | # Single-quoted string, possibly containing escape sequences 106 | (r"'[^'\\]*(\\.[^'\\]*)*'", 0, STYLES['string']), 107 | 108 | # From '#' until a newline 109 | (r'#[^\n]*', 0, STYLES['comment']), 110 | ] 111 | 112 | # Build a QRegExp for each pattern 113 | self.rules = [(QRegularExpression(pat), index, fmt) 114 | for (pat, index, fmt) in rules] 115 | 116 | def highlightBlock(self, text): 117 | """ 118 | Apply syntax highlighting to the given block of text. 119 | """ 120 | self.tripleQuoutesWithinStrings = [] 121 | # Do other syntax formatting 122 | for expression, nth, format in self.rules: 123 | match = expression.match(text, 0) 124 | while match.hasMatch(): 125 | index = match.capturedStart(nth) 126 | if index >= 0: 127 | # if there is a string we check 128 | # if there are some triple quotes within the string 129 | # they will be ignored if they are matched again 130 | if expression.pattern() in [r'"[^"\\]*(\\.[^"\\]*)*"', r"'[^'\\]*(\\.[^'\\]*)*'"]: 131 | inner_match = self.tri_single[0].match(text, index + 1) 132 | innerIndex = inner_match.capturedStart() if inner_match.hasMatch() else -1 133 | if innerIndex == -1: 134 | inner_match = self.tri_double[0].match(text, index + 1) 135 | innerIndex = inner_match.capturedStart() if inner_match.hasMatch() else -1 136 | 137 | if innerIndex != -1: 138 | tripleQuoteIndexes = range(innerIndex, innerIndex + 3) 139 | self.tripleQuoutesWithinStrings.extend(tripleQuoteIndexes) 140 | 141 | while index >= 0: 142 | # skipping triple quotes within strings 143 | if index in self.tripleQuoutesWithinStrings: 144 | index += 1 145 | match = expression.match(text, index) 146 | if match.hasMatch(): 147 | index = match.capturedStart(nth) 148 | continue 149 | 150 | # We actually want the index of the nth match 151 | length = match.capturedLength(nth) 152 | self.setFormat(index, length, format) 153 | match = expression.match(text, index + length) 154 | if match.hasMatch(): 155 | index = match.capturedStart(nth) 156 | else: 157 | index = -1 158 | 159 | self.setCurrentBlockState(0) 160 | 161 | # Do multi-line strings 162 | in_multiline = self.match_multiline(text, *self.tri_single) 163 | if not in_multiline: 164 | in_multiline = self.match_multiline(text, *self.tri_double) 165 | 166 | def match_multiline(self, text, delimiter, in_state, style): 167 | """ 168 | Do highlighting of multi-line strings. ``delimiter`` should be a 169 | ``QRegularExpression`` for triple-single-quotes or triple-double-quotes, and 170 | ``in_state`` should be a unique integer to represent the corresponding 171 | state changes when inside those strings. Returns True if we're still 172 | inside a multi-line string when this function is finished. 173 | """ 174 | # If inside triple-single quotes, start at 0 175 | if self.previousBlockState() == in_state: 176 | start = 0 177 | add = 0 178 | # Otherwise, look for the delimiter on this line 179 | else: 180 | match = delimiter.match(text) 181 | start = match.capturedStart() 182 | 183 | # skipping triple quotes within strings 184 | if start in self.tripleQuoutesWithinStrings: 185 | return False 186 | # Move past this match 187 | add = match.capturedLength() 188 | 189 | # As long as there's a delimiter match on this line... 190 | while start >= 0: 191 | match = delimiter.match(text, start + add) 192 | end = match.capturedStart() 193 | 194 | # Look for the ending delimiter 195 | if end >= add: 196 | length = end - start + add + match.capturedLength() 197 | self.setCurrentBlockState(0) 198 | # No; multi-line string 199 | else: 200 | self.setCurrentBlockState(in_state) 201 | length = len(text) - start + add 202 | # Apply formatting 203 | self.setFormat(start, length, style) 204 | # Look for the next match 205 | match = delimiter.match(text, start + length) 206 | start = match.capturedStart() if match.hasMatch() else -1 207 | 208 | # Return True if still inside a multi-line string, False otherwise 209 | return self.currentBlockState() == in_state 210 | -------------------------------------------------------------------------------- /unreal_script_editor/codeEditor/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from PySide6 import QtWidgets 4 | 5 | import codeEditor 6 | from highlighter.pyHighlight import PythonHighlighter 7 | 8 | 9 | if __name__ == "__main__": 10 | app = QtWidgets.QApplication(sys.argv) 11 | 12 | editor = codeEditor.CodeEditor() 13 | editor.setWindowTitle("Code Editor Example") 14 | highlighter = PythonHighlighter(editor.document()) 15 | editor.show() 16 | 17 | sys.exit(app.exec_()) 18 | -------------------------------------------------------------------------------- /unreal_script_editor/config.txt: -------------------------------------------------------------------------------- 1 | [{'index': 0, 'label': 'Python', 'active': True, 'command': 'print("hello")'}] -------------------------------------------------------------------------------- /unreal_script_editor/icons/clearAll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leixingyu/unrealScriptEditor/b0d1509ee4e4b93d7a3976fae3666e999155cb85/unreal_script_editor/icons/clearAll.png -------------------------------------------------------------------------------- /unreal_script_editor/icons/clearHistory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leixingyu/unrealScriptEditor/b0d1509ee4e4b93d7a3976fae3666e999155cb85/unreal_script_editor/icons/clearHistory.png -------------------------------------------------------------------------------- /unreal_script_editor/icons/clearInput.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leixingyu/unrealScriptEditor/b0d1509ee4e4b93d7a3976fae3666e999155cb85/unreal_script_editor/icons/clearInput.png -------------------------------------------------------------------------------- /unreal_script_editor/icons/execute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leixingyu/unrealScriptEditor/b0d1509ee4e4b93d7a3976fae3666e999155cb85/unreal_script_editor/icons/execute.png -------------------------------------------------------------------------------- /unreal_script_editor/icons/executeAll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leixingyu/unrealScriptEditor/b0d1509ee4e4b93d7a3976fae3666e999155cb85/unreal_script_editor/icons/executeAll.png -------------------------------------------------------------------------------- /unreal_script_editor/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | code for the main script editor window 3 | """ 4 | 5 | import ast 6 | import logging 7 | import os 8 | import sys 9 | import traceback 10 | from collections import namedtuple 11 | 12 | try: 13 | import unreal 14 | RUNNING_IN_UNREAL = True 15 | except ImportError: 16 | RUNNING_IN_UNREAL = False 17 | 18 | from PySide6 import QtWidgets, QtCore, QtGui 19 | 20 | from unreal_script_editor import outputTextWidget 21 | from unreal_script_editor.codeEditor import codeEditor 22 | from unreal_script_editor.codeEditor.highlighter import pyHighlight 23 | 24 | from unreal_script_editor.ui.script_editor import Ui_MainWindow 25 | 26 | 27 | LOGGER = logging.getLogger(__name__) 28 | 29 | APP = None 30 | WINDOW = None 31 | 32 | MODULE_PATH = os.path.dirname(os.path.abspath(__file__)) 33 | MODULE_NAME = os.path.basename(MODULE_PATH) 34 | UI_PATH = os.path.join(MODULE_PATH, 'ui', 'script_editor.ui') 35 | CONFIG_PATH = os.path.join(MODULE_PATH, 'config.txt') 36 | 37 | ICONS_PATH = os.path.join(MODULE_PATH, 'icons') 38 | QtCore.QDir.addSearchPath("ICONS", ICONS_PATH) 39 | 40 | 41 | class TabConfig(namedtuple('TabConfig', ['index', 'label', 'active', 'command'])): 42 | """ 43 | Dataclass to store python script information in the tabs 44 | 45 | :param index: int. script tab index within the tab widget 46 | :param label: str. script tab title label 47 | :param active: bool. whether this tab is set to active (current) 48 | only one tab is allowed to be active 49 | :param command: str. script in the tab 50 | """ 51 | __slots__ = () 52 | 53 | 54 | class ScriptEditorWindow(QtWidgets.QMainWindow, Ui_MainWindow): 55 | """ 56 | Script Editor main window 57 | """ 58 | 59 | def __init__(self, parent=None): 60 | """ 61 | Initialization 62 | """ 63 | super(ScriptEditorWindow, self).__init__(parent) 64 | self.setupUi(self) # Set up the UI 65 | 66 | splitter = QtWidgets.QSplitter() 67 | splitter.setOrientation(QtCore.Qt.Vertical) 68 | self.centralWidget().layout().addWidget(splitter) 69 | self.ui_log_edit = outputTextWidget.OutputTextWidget() 70 | splitter.addWidget(self.ui_log_edit) 71 | splitter.addWidget(self.ui_tab_widget) 72 | 73 | self.ui_tabs = list() 74 | self.ui_tab_highlighters = list() 75 | 76 | self.register_traceback() 77 | self.load_configs() 78 | 79 | # 80 | self.ui_run_all_btn.clicked.connect(self.execute) 81 | self.ui_run_sel_btn.clicked.connect(self.execute_sel) 82 | self.ui_clear_log_btn.clicked.connect(self.clear_log) 83 | self.ui_clear_script_btn.clicked.connect(self.clear_script) 84 | self.ui_clear_both_btn.clicked.connect(self.clear_all) 85 | 86 | self.ui_save_action.triggered.connect(self.save_script) 87 | self.ui_open_action.triggered.connect(self.open_script) 88 | 89 | self.ui_tab_widget.tabBarClicked.connect(self.add_tab) 90 | self.ui_tab_widget.tabCloseRequested.connect(self.remove_tab) 91 | 92 | # region Overrides 93 | def closeEvent(self, event): 94 | """ 95 | Override: close the tool automatically saves out the script configs 96 | """ 97 | self.save_configs() 98 | super(ScriptEditorWindow, self).closeEvent(event) 99 | 100 | def register_traceback(self): 101 | """ 102 | Link Unreal traceback 103 | """ 104 | def custom_traceback(exc_type, exc_value, exc_traceback=None): 105 | message = 'Error: {}: {}\n'.format(exc_type, exc_value) 106 | if exc_traceback: 107 | format_exception = traceback.format_tb(exc_traceback) 108 | for line in format_exception: 109 | message += line 110 | self.ui_log_edit.update_logger(message, 'error') 111 | 112 | sys.excepthook = custom_traceback 113 | # endregion 114 | 115 | # region Config 116 | def save_configs(self): 117 | """ 118 | Save all current python tabs' config 119 | """ 120 | configs = list() 121 | active_index = self.ui_tab_widget.currentIndex() 122 | 123 | for i in range(self.ui_tab_widget.count()-1): 124 | self.ui_tab_widget.setCurrentIndex(i) 125 | script_tab = self.ui_tab_widget.currentWidget() 126 | label = self.ui_tab_widget.tabText(i) 127 | active = active_index == i 128 | 129 | config = TabConfig(i, label, active, script_tab.toPlainText()) 130 | configs.append(config) 131 | 132 | # go back to the previous active tab 133 | self.ui_tab_widget.setCurrentIndex(active_index) 134 | 135 | with open(CONFIG_PATH, 'w') as f: 136 | string = [config._asdict() for config in configs] 137 | f.write(str(string)) 138 | 139 | def load_configs(self): 140 | """ 141 | During startup, load python script config file and initialize tab gui 142 | """ 143 | if not os.path.exists(CONFIG_PATH): 144 | self.load_tabs() 145 | return 146 | 147 | with open(CONFIG_PATH, 'r') as f: 148 | tab_configs = list() 149 | tab_config_dicts = ast.literal_eval(f.read()) 150 | for tab_config_dict in tab_config_dicts: 151 | tab_config = TabConfig(**tab_config_dict) 152 | tab_configs.append(tab_config) 153 | 154 | self.load_tabs(tab_configs) 155 | 156 | def load_tabs(self, tab_configs=None): 157 | """ 158 | Initialize python script tab gui from config object 159 | 160 | :param tab_configs: TabConfig. dataclass object storing python tab info 161 | """ 162 | if not tab_configs: 163 | tab_configs = [TabConfig(0, 'Python', True, '')] 164 | 165 | active_index = 0 166 | for tab_config in tab_configs: 167 | self.insert_tab(tab_config.index, tab_config.command, tab_config.label) 168 | if tab_config.active: 169 | active_index = tab_config.index 170 | 171 | self.ui_tab_widget.setCurrentIndex(active_index) 172 | 173 | def insert_tab(self, index, command, label): 174 | """ 175 | Insert a python tab into the tab widget 176 | 177 | :param index: int. tab index to insert 178 | :param command: str. python script command to add to the inserted tab 179 | :param label: str. title/label of the tab inserted 180 | """ 181 | script_edit = codeEditor.CodeEditor() 182 | script_edit.setPlainText(command) 183 | highlight = pyHighlight.PythonHighlighter( 184 | script_edit.document()) 185 | 186 | self.ui_tab_widget.insertTab(index, script_edit, label) 187 | self.ui_tab_highlighters.append(highlight) 188 | self.ui_tabs.append(script_edit) 189 | 190 | self.ui_tab_widget.setCurrentIndex(index) 191 | # endregion 192 | 193 | # region Execution 194 | def execute(self): 195 | """ 196 | Send all command in script area for maya to execute 197 | """ 198 | command = self.ui_tab_widget.currentWidget().toPlainText() 199 | if RUNNING_IN_UNREAL: 200 | output = unreal.PythonScriptLibrary.execute_python_command_ex( 201 | python_command=command, 202 | execution_mode=unreal.PythonCommandExecutionMode.EXECUTE_FILE, 203 | file_execution_scope=unreal.PythonFileExecutionScope.PUBLIC 204 | ) 205 | 206 | if not output: 207 | return 208 | 209 | self.ui_log_edit.update_logger( 210 | "# Command executed: \n" 211 | "{}\n" 212 | "# Command execution ended".format(command) 213 | ) 214 | self.send_formatted_output(output) 215 | else: 216 | # todo this wont get any output, fix it 217 | output = None 218 | exec(command) 219 | 220 | def execute_sel(self): 221 | """ 222 | Send selected command in script area for maya to execute 223 | """ 224 | command = self.ui_tab_widget.currentWidget().textCursor().selection().toPlainText() 225 | if RUNNING_IN_UNREAL: 226 | output = unreal.PythonScriptLibrary.execute_python_command_ex( 227 | python_command=command, 228 | execution_mode=unreal.PythonCommandExecutionMode.EXECUTE_FILE, 229 | file_execution_scope=unreal.PythonFileExecutionScope.PUBLIC 230 | ) 231 | 232 | if not output: 233 | return 234 | 235 | self.ui_log_edit.update_logger( 236 | "# Command executed: \n" 237 | "{}\n" 238 | "# Command execution ended".format(command) 239 | ) 240 | self.send_formatted_output(output) 241 | else: 242 | # todo this wont get any output, fix it 243 | output = None 244 | exec(command) 245 | 246 | def send_formatted_output(self, output): 247 | """ 248 | Update ui field with messages 249 | """ 250 | if not output: 251 | return 252 | 253 | result, log_entries = output 254 | for entry in log_entries: 255 | if entry.type != unreal.PythonLogOutputType.INFO: 256 | self.ui_log_edit.update_logger(entry.output, 'error') 257 | else: 258 | self.ui_log_edit.update_logger(entry.output, 'info') 259 | 260 | def clear_log(self): 261 | """ 262 | Clear history logging area 263 | """ 264 | self.ui_log_edit.clear() 265 | 266 | def clear_script(self): 267 | self.ui_tab_widget.currentWidget().setPlainText('') 268 | 269 | def clear_all(self): 270 | self.clear_script() 271 | self.clear_log() 272 | # endregion 273 | 274 | # region Tab Operation 275 | def add_tab(self, index): 276 | """ 277 | Add a python tab when 'Add' tab button is clicked 278 | """ 279 | if index == self.ui_tab_widget.count() - 1: 280 | self.insert_tab(index, '', 'Python') 281 | 282 | def remove_tab(self, index): 283 | """ 284 | Remove a python tab 285 | 286 | :param index: int. removal tab index 287 | """ 288 | msg_box = QtWidgets.QMessageBox( 289 | QtWidgets.QMessageBox.Question, 290 | '', 291 | 'Delete the Current Tab?', 292 | QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No 293 | ) 294 | 295 | usr_choice = msg_box.exec() 296 | if usr_choice == QtWidgets.QMessageBox.Yes: 297 | if index != self.ui_tab_widget.count() - 1: 298 | self.ui_tab_widget.removeTab(index) 299 | self.ui_tab_widget.setCurrentIndex(index-1) 300 | # endregion 301 | 302 | # region File IO 303 | def open_script(self): 304 | """ 305 | Open python file to script edit area 306 | """ 307 | path = QtWidgets.QFileDialog.getOpenFileName( 308 | None, 309 | "Open Script", 310 | MODULE_PATH, 311 | filter="*.py")[0] 312 | 313 | if not path: 314 | return 315 | 316 | with open(path, 'r') as f: 317 | file_name = os.path.basename(path) 318 | output = f.read() 319 | index = self.ui_tab_widget.count() - 1 320 | self.insert_tab(index, output, file_name) 321 | 322 | def save_script(self): 323 | """ 324 | Save script edit area as a python file 325 | """ 326 | path = QtWidgets.QFileDialog.getSaveFileName( 327 | None, 328 | "Save Script As...", 329 | MODULE_PATH, 330 | filter="*.py")[0] 331 | 332 | if not path: 333 | return 334 | 335 | command = self.ui_tab_widget.currentWidget().toPlainText() 336 | with open(path, 'w') as f: 337 | f.write(command) 338 | # endregion 339 | 340 | 341 | def show(): 342 | global APP 343 | global WINDOW 344 | 345 | APP = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv) 346 | 347 | try: 348 | import unreal_stylesheet 349 | unreal_stylesheet.setup() 350 | except ImportError: 351 | LOGGER.warning("unreal_stylesheet module not found, " 352 | "please run `pip install unreal-stylesheet`") 353 | 354 | # handles existing instance 355 | WINDOW = WINDOW or ScriptEditorWindow() 356 | WINDOW.show() 357 | 358 | if RUNNING_IN_UNREAL: 359 | unreal.parent_external_window_to_slate(int(WINDOW.winId())) 360 | 361 | return WINDOW 362 | 363 | 364 | if __name__ == "__main__": 365 | APP = QtWidgets.QApplication.instance() 366 | w = show() 367 | APP.exec() 368 | -------------------------------------------------------------------------------- /unreal_script_editor/outputTextWidget.py: -------------------------------------------------------------------------------- 1 | """ 2 | code for the main output text widget 3 | """ 4 | 5 | import os 6 | 7 | from PySide6 import QtWidgets, QtCore, QtGui 8 | from unreal_script_editor.ui.output_text_widget import Ui_Form 9 | 10 | 11 | MODULE_PATH = os.path.dirname(os.path.abspath(__file__)) 12 | MODULE_NAME = os.path.basename(MODULE_PATH) 13 | UI_PATH = os.path.join(MODULE_PATH, 'ui', 'output_text_widget.ui') 14 | 15 | # display text formatting 16 | ERROR_FORMAT = QtGui.QTextCharFormat() 17 | ERROR_FORMAT.setForeground(QtGui.QBrush(QtCore.Qt.red)) 18 | 19 | WARNING_FORMAT = QtGui.QTextCharFormat() 20 | WARNING_FORMAT.setForeground(QtGui.QBrush(QtCore.Qt.yellow)) 21 | 22 | INFO_FORMAT = QtGui.QTextCharFormat() 23 | INFO_FORMAT.setForeground(QtGui.QBrush(QtGui.QColor('#6897bb'))) 24 | 25 | REGULAR_FORMAT = QtGui.QTextCharFormat() 26 | REGULAR_FORMAT.setForeground(QtGui.QBrush(QtGui.QColor(200, 200, 200))) 27 | 28 | 29 | class OutputTextWidget(QtWidgets.QWidget, Ui_Form): 30 | """ 31 | Text Widget to display output information from Unreal command execution 32 | """ 33 | 34 | def __init__(self, parent=None): 35 | """ 36 | Initialization 37 | """ 38 | super(OutputTextWidget, self).__init__(parent) 39 | self.setupUi(self) # Set up the UI 40 | 41 | def clear(self): 42 | """ 43 | Clear the all text 44 | """ 45 | self.ui_log_edit.clear() 46 | 47 | def update_logger(self, message, mtype=None): 48 | """ 49 | Append plain message to display text widget 50 | 51 | :param message: str. message 52 | :param mtype: str. message type, this determines the message format/style 53 | """ 54 | if mtype == 'info': 55 | self.ui_log_edit.setCurrentCharFormat(INFO_FORMAT) 56 | elif mtype == 'warning': 57 | self.ui_log_edit.setCurrentCharFormat(WARNING_FORMAT) 58 | elif mtype == 'error': 59 | self.ui_log_edit.setCurrentCharFormat(ERROR_FORMAT) 60 | else: 61 | self.ui_log_edit.setCurrentCharFormat(REGULAR_FORMAT) 62 | 63 | self.ui_log_edit.insertPlainText(message) 64 | self.ui_log_edit.insertPlainText('\n') 65 | 66 | scroll = self.ui_log_edit.verticalScrollBar() 67 | scroll.setValue(scroll.maximum()) 68 | 69 | def update_logger_html(self, html): 70 | """ 71 | Append html message to display text widget 72 | 73 | :param html: str. message as html 74 | """ 75 | self.ui_log_edit.insertHtml(html) 76 | self.ui_log_edit.insertHtml('
') 77 | 78 | scroll = self.ui_log_edit.verticalScrollBar() 79 | scroll.setValue(scroll.maximum()) 80 | -------------------------------------------------------------------------------- /unreal_script_editor/startup.py: -------------------------------------------------------------------------------- 1 | """ 2 | this startup code is used to add a script editor button to the unreal tool bar. 3 | ignore this file when using this script editor widget outside Unreal 4 | """ 5 | 6 | import os 7 | import sys 8 | 9 | import unreal 10 | 11 | 12 | MODULE_PATH = os.path.dirname(os.path.abspath(__file__)) 13 | UPPER_PATH = os.path.join(MODULE_PATH, '..') 14 | 15 | sys.path.append(UPPER_PATH) 16 | 17 | 18 | def create_script_editor_button(): 19 | """ 20 | Start up script to add script editor button to tool bar 21 | """ 22 | 23 | section_name = 'Plugins' 24 | se_command = ( 25 | 'from unreal_script_editor import main;' 26 | 'global editor;' 27 | 'editor = main.show()' 28 | ) 29 | 30 | menus = unreal.ToolMenus.get() 31 | level_menu_bar = menus.find_menu('LevelEditor.LevelEditorToolBar.PlayToolBar') 32 | level_menu_bar.add_section(section_name=section_name, label=section_name) 33 | 34 | entry = unreal.ToolMenuEntry(type=unreal.MultiBlockType.TOOL_BAR_BUTTON) 35 | entry.set_label('Script Editor') 36 | entry.set_tool_tip('Unreal Python Script Editor') 37 | entry.set_icon('EditorStyle', 'DebugConsole.Icon') 38 | entry.set_string_command( 39 | type=unreal.ToolMenuStringCommandType.PYTHON, 40 | custom_type=unreal.Name(''), 41 | string=se_command 42 | ) 43 | level_menu_bar.add_menu_entry(section_name, entry) 44 | menus.refresh_all_widgets() 45 | 46 | 47 | if __name__ == "__main__": 48 | create_script_editor_button() 49 | -------------------------------------------------------------------------------- /unreal_script_editor/ui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leixingyu/unrealScriptEditor/b0d1509ee4e4b93d7a3976fae3666e999155cb85/unreal_script_editor/ui/__init__.py -------------------------------------------------------------------------------- /unreal_script_editor/ui/output_text_widget.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'output_text_widget.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.7.1 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, 15 | QFont, QFontDatabase, QGradient, QIcon, 16 | QImage, QKeySequence, QLinearGradient, QPainter, 17 | QPalette, QPixmap, QRadialGradient, QTransform) 18 | from PySide6.QtWidgets import (QApplication, QSizePolicy, QTextEdit, QVBoxLayout, 19 | QWidget) 20 | 21 | class Ui_Form(object): 22 | def setupUi(self, Form): 23 | if not Form.objectName(): 24 | Form.setObjectName(u"Form") 25 | Form.resize(544, 274) 26 | self.verticalLayout = QVBoxLayout(Form) 27 | self.verticalLayout.setSpacing(0) 28 | self.verticalLayout.setObjectName(u"verticalLayout") 29 | self.verticalLayout.setContentsMargins(0, 0, 0, 0) 30 | self.ui_log_edit = QTextEdit(Form) 31 | self.ui_log_edit.setObjectName(u"ui_log_edit") 32 | font = QFont() 33 | font.setFamilies([u"MS Sans Serif"]) 34 | self.ui_log_edit.setFont(font) 35 | self.ui_log_edit.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) 36 | self.ui_log_edit.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn) 37 | self.ui_log_edit.setLineWrapMode(QTextEdit.NoWrap) 38 | self.ui_log_edit.setReadOnly(True) 39 | 40 | self.verticalLayout.addWidget(self.ui_log_edit) 41 | 42 | 43 | self.retranslateUi(Form) 44 | 45 | QMetaObject.connectSlotsByName(Form) 46 | # setupUi 47 | 48 | def retranslateUi(self, Form): 49 | Form.setWindowTitle(QCoreApplication.translate("Form", u"Form", None)) 50 | # retranslateUi 51 | 52 | -------------------------------------------------------------------------------- /unreal_script_editor/ui/output_text_widget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 544 10 | 274 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 0 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 0 31 | 32 | 33 | 34 | 35 | 36 | MS Sans Serif 37 | 38 | 39 | 40 | Qt::ScrollBarAlwaysOn 41 | 42 | 43 | Qt::ScrollBarAlwaysOn 44 | 45 | 46 | QTextEdit::NoWrap 47 | 48 | 49 | true 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /unreal_script_editor/ui/script_editor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'script_editor.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.7.1 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient, 15 | QCursor, QFont, QFontDatabase, QGradient, 16 | QIcon, QImage, QKeySequence, QLinearGradient, 17 | QPainter, QPalette, QPixmap, QRadialGradient, 18 | QTransform) 19 | from PySide6.QtWidgets import (QApplication, QFrame, QGroupBox, QHBoxLayout, 20 | QMainWindow, QMenu, QMenuBar, QPushButton, 21 | QSizePolicy, QSpacerItem, QTabWidget, QVBoxLayout, 22 | QWidget) 23 | 24 | class Ui_MainWindow(object): 25 | def setupUi(self, MainWindow): 26 | if not MainWindow.objectName(): 27 | MainWindow.setObjectName(u"MainWindow") 28 | MainWindow.resize(567, 569) 29 | self.ui_save_action = QAction(MainWindow) 30 | self.ui_save_action.setObjectName(u"ui_save_action") 31 | font = QFont() 32 | font.setFamilies([u"Bahnschrift"]) 33 | font.setPointSize(10) 34 | self.ui_save_action.setFont(font) 35 | self.ui_open_action = QAction(MainWindow) 36 | self.ui_open_action.setObjectName(u"ui_open_action") 37 | self.ui_open_action.setFont(font) 38 | self.centralwidget = QWidget(MainWindow) 39 | self.centralwidget.setObjectName(u"centralwidget") 40 | self.verticalLayout = QVBoxLayout(self.centralwidget) 41 | self.verticalLayout.setObjectName(u"verticalLayout") 42 | self.verticalLayout.setContentsMargins(10, 10, 10, 10) 43 | self.groupBox = QGroupBox(self.centralwidget) 44 | self.groupBox.setObjectName(u"groupBox") 45 | self.verticalLayout_2 = QVBoxLayout(self.groupBox) 46 | self.verticalLayout_2.setObjectName(u"verticalLayout_2") 47 | self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) 48 | self.horizontalLayout_2 = QHBoxLayout() 49 | self.horizontalLayout_2.setSpacing(0) 50 | self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") 51 | self.horizontalLayout_2.setContentsMargins(-1, 0, -1, -1) 52 | self.ui_run_all_btn = QPushButton(self.groupBox) 53 | self.ui_run_all_btn.setObjectName(u"ui_run_all_btn") 54 | icon = QIcon() 55 | icon.addFile(u"ICONS:/executeAll.png", QSize(), QIcon.Normal, QIcon.Off) 56 | self.ui_run_all_btn.setIcon(icon) 57 | self.ui_run_all_btn.setIconSize(QSize(25, 25)) 58 | self.ui_run_all_btn.setFlat(True) 59 | 60 | self.horizontalLayout_2.addWidget(self.ui_run_all_btn) 61 | 62 | self.ui_run_sel_btn = QPushButton(self.groupBox) 63 | self.ui_run_sel_btn.setObjectName(u"ui_run_sel_btn") 64 | icon1 = QIcon() 65 | icon1.addFile(u"ICONS:/execute.png", QSize(), QIcon.Normal, QIcon.Off) 66 | self.ui_run_sel_btn.setIcon(icon1) 67 | self.ui_run_sel_btn.setIconSize(QSize(25, 25)) 68 | self.ui_run_sel_btn.setFlat(True) 69 | 70 | self.horizontalLayout_2.addWidget(self.ui_run_sel_btn) 71 | 72 | self.line_3 = QFrame(self.groupBox) 73 | self.line_3.setObjectName(u"line_3") 74 | self.line_3.setFrameShape(QFrame.Shape.VLine) 75 | self.line_3.setFrameShadow(QFrame.Shadow.Sunken) 76 | 77 | self.horizontalLayout_2.addWidget(self.line_3) 78 | 79 | self.ui_clear_log_btn = QPushButton(self.groupBox) 80 | self.ui_clear_log_btn.setObjectName(u"ui_clear_log_btn") 81 | icon2 = QIcon() 82 | icon2.addFile(u"ICONS:/clearHistory.png", QSize(), QIcon.Normal, QIcon.Off) 83 | self.ui_clear_log_btn.setIcon(icon2) 84 | self.ui_clear_log_btn.setIconSize(QSize(25, 25)) 85 | self.ui_clear_log_btn.setFlat(True) 86 | 87 | self.horizontalLayout_2.addWidget(self.ui_clear_log_btn) 88 | 89 | self.ui_clear_script_btn = QPushButton(self.groupBox) 90 | self.ui_clear_script_btn.setObjectName(u"ui_clear_script_btn") 91 | icon3 = QIcon() 92 | icon3.addFile(u"ICONS:/clearInput.png", QSize(), QIcon.Normal, QIcon.Off) 93 | self.ui_clear_script_btn.setIcon(icon3) 94 | self.ui_clear_script_btn.setIconSize(QSize(25, 25)) 95 | self.ui_clear_script_btn.setFlat(True) 96 | 97 | self.horizontalLayout_2.addWidget(self.ui_clear_script_btn) 98 | 99 | self.ui_clear_both_btn = QPushButton(self.groupBox) 100 | self.ui_clear_both_btn.setObjectName(u"ui_clear_both_btn") 101 | icon4 = QIcon() 102 | icon4.addFile(u"ICONS:/clearAll.png", QSize(), QIcon.Normal, QIcon.Off) 103 | self.ui_clear_both_btn.setIcon(icon4) 104 | self.ui_clear_both_btn.setIconSize(QSize(25, 25)) 105 | self.ui_clear_both_btn.setFlat(True) 106 | 107 | self.horizontalLayout_2.addWidget(self.ui_clear_both_btn) 108 | 109 | self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) 110 | 111 | self.horizontalLayout_2.addItem(self.horizontalSpacer) 112 | 113 | 114 | self.verticalLayout_2.addLayout(self.horizontalLayout_2) 115 | 116 | 117 | self.verticalLayout.addWidget(self.groupBox) 118 | 119 | self.ui_tab_widget = QTabWidget(self.centralwidget) 120 | self.ui_tab_widget.setObjectName(u"ui_tab_widget") 121 | self.ui_tab_widget.setTabsClosable(True) 122 | self.ui_add_tab = QWidget() 123 | self.ui_add_tab.setObjectName(u"ui_add_tab") 124 | self.ui_tab_widget.addTab(self.ui_add_tab, "") 125 | 126 | self.verticalLayout.addWidget(self.ui_tab_widget) 127 | 128 | MainWindow.setCentralWidget(self.centralwidget) 129 | self.menubar = QMenuBar(MainWindow) 130 | self.menubar.setObjectName(u"menubar") 131 | self.menubar.setGeometry(QRect(0, 0, 567, 26)) 132 | self.menuFile = QMenu(self.menubar) 133 | self.menuFile.setObjectName(u"menuFile") 134 | self.menuFile.setFont(font) 135 | MainWindow.setMenuBar(self.menubar) 136 | 137 | self.menubar.addAction(self.menuFile.menuAction()) 138 | self.menuFile.addAction(self.ui_save_action) 139 | self.menuFile.addAction(self.ui_open_action) 140 | 141 | self.retranslateUi(MainWindow) 142 | 143 | self.ui_tab_widget.setCurrentIndex(0) 144 | 145 | 146 | QMetaObject.connectSlotsByName(MainWindow) 147 | # setupUi 148 | 149 | def retranslateUi(self, MainWindow): 150 | MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"Unreal Script Editor", None)) 151 | self.ui_save_action.setText(QCoreApplication.translate("MainWindow", u"Save as...", None)) 152 | self.ui_open_action.setText(QCoreApplication.translate("MainWindow", u"Open", None)) 153 | self.groupBox.setTitle("") 154 | #if QT_CONFIG(tooltip) 155 | self.ui_run_all_btn.setToolTip(QCoreApplication.translate("MainWindow", u"execute all commands", None)) 156 | #endif // QT_CONFIG(tooltip) 157 | self.ui_run_all_btn.setText("") 158 | #if QT_CONFIG(tooltip) 159 | self.ui_run_sel_btn.setToolTip(QCoreApplication.translate("MainWindow", u"execute selected commands", None)) 160 | #endif // QT_CONFIG(tooltip) 161 | self.ui_run_sel_btn.setText("") 162 | #if QT_CONFIG(tooltip) 163 | self.ui_clear_log_btn.setToolTip(QCoreApplication.translate("MainWindow", u"clear unreal log", None)) 164 | #endif // QT_CONFIG(tooltip) 165 | self.ui_clear_log_btn.setText("") 166 | #if QT_CONFIG(tooltip) 167 | self.ui_clear_script_btn.setToolTip(QCoreApplication.translate("MainWindow", u"clear input script", None)) 168 | #endif // QT_CONFIG(tooltip) 169 | self.ui_clear_script_btn.setText("") 170 | #if QT_CONFIG(tooltip) 171 | self.ui_clear_both_btn.setToolTip(QCoreApplication.translate("MainWindow", u"clear all", None)) 172 | #endif // QT_CONFIG(tooltip) 173 | self.ui_clear_both_btn.setText("") 174 | self.ui_tab_widget.setTabText(self.ui_tab_widget.indexOf(self.ui_add_tab), QCoreApplication.translate("MainWindow", u"+", None)) 175 | self.menuFile.setTitle(QCoreApplication.translate("MainWindow", u"File", None)) 176 | # retranslateUi 177 | 178 | 179 | 180 | def show(): 181 | import sys 182 | 183 | app = QApplication.instance() or QApplication(sys.argv) 184 | 185 | main_window = QMainWindow() 186 | ui = Ui_MainWindow() 187 | ui.setupUi(main_window) 188 | main_window.show() 189 | 190 | app.exec_() 191 | # if RUNNING_IN_UNREAL: 192 | # unreal.parent_external_window_to_slate(int(WINDOW.winId())) 193 | 194 | return main_window 195 | 196 | # if __name__ == "__main__": 197 | # show() -------------------------------------------------------------------------------- /unreal_script_editor/ui/script_editor.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 567 10 | 569 11 | 12 | 13 | 14 | Unreal Script Editor 15 | 16 | 17 | 18 | 19 | 10 20 | 21 | 22 | 10 23 | 24 | 25 | 10 26 | 27 | 28 | 10 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 0 38 | 39 | 40 | 0 41 | 42 | 43 | 0 44 | 45 | 46 | 0 47 | 48 | 49 | 50 | 51 | 0 52 | 53 | 54 | 0 55 | 56 | 57 | 58 | 59 | execute all commands 60 | 61 | 62 | 63 | 64 | 65 | 66 | ICONS:/executeAll.pngICONS:/executeAll.png 67 | 68 | 69 | 70 | 25 71 | 25 72 | 73 | 74 | 75 | true 76 | 77 | 78 | 79 | 80 | 81 | 82 | execute selected commands 83 | 84 | 85 | 86 | 87 | 88 | 89 | ICONS:/execute.pngICONS:/execute.png 90 | 91 | 92 | 93 | 25 94 | 25 95 | 96 | 97 | 98 | true 99 | 100 | 101 | 102 | 103 | 104 | 105 | Qt::Vertical 106 | 107 | 108 | 109 | 110 | 111 | 112 | clear unreal log 113 | 114 | 115 | 116 | 117 | 118 | 119 | ICONS:/clearHistory.pngICONS:/clearHistory.png 120 | 121 | 122 | 123 | 25 124 | 25 125 | 126 | 127 | 128 | true 129 | 130 | 131 | 132 | 133 | 134 | 135 | clear input script 136 | 137 | 138 | 139 | 140 | 141 | 142 | ICONS:/clearInput.pngICONS:/clearInput.png 143 | 144 | 145 | 146 | 25 147 | 25 148 | 149 | 150 | 151 | true 152 | 153 | 154 | 155 | 156 | 157 | 158 | clear all 159 | 160 | 161 | 162 | 163 | 164 | 165 | ICONS:/clearAll.pngICONS:/clearAll.png 166 | 167 | 168 | 169 | 25 170 | 25 171 | 172 | 173 | 174 | true 175 | 176 | 177 | 178 | 179 | 180 | 181 | Qt::Horizontal 182 | 183 | 184 | 185 | 40 186 | 20 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 0 200 | 201 | 202 | true 203 | 204 | 205 | 206 | + 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 0 217 | 0 218 | 567 219 | 26 220 | 221 | 222 | 223 | 224 | 225 | Bahnschrift 226 | 10 227 | 228 | 229 | 230 | File 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | Save as... 240 | 241 | 242 | 243 | Bahnschrift 244 | 10 245 | 246 | 247 | 248 | 249 | 250 | Open 251 | 252 | 253 | 254 | Bahnschrift 255 | 10 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | --------------------------------------------------------------------------------