├── .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 [](https://pypi.org/project/unreal-script-editor/) 
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) 
6 |
7 |
8 | ## About The Project
9 |
10 |
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 |
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 |
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 |

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