├── tests
├── __init__.py
├── test_pytestqt
│ ├── __init__.py
│ ├── test_actions.py
│ └── test_app.py
├── pre-commit
├── test_dependencies.py
├── test_qt_binding.py
├── test_syntax.py
├── in_progress
│ └── _autocomplete.py
└── test_autosavexml.py
├── PythonEditor
├── app
│ ├── __init__.py
│ └── nukefeatures
│ │ ├── __init__.py
│ │ ├── editorknob.py
│ │ ├── jupyter_nodebook.py
│ │ ├── nukedock.py
│ │ ├── nodepanels.py
│ │ └── nukeinit.py
├── models
│ ├── __init__.py
│ └── xmlmodel.py
├── ui
│ ├── __init__.py
│ ├── dialogs
│ │ ├── __init__.py
│ │ ├── preferences.py
│ │ ├── objectinspector.py
│ │ └── popups.py
│ ├── features
│ │ ├── __init__.py
│ │ ├── linenumberarea.py
│ │ └── nukepalette.py
│ ├── menubar.py
│ ├── pythoneditor.py
│ ├── tabview.py
│ ├── terminal.py
│ └── ide.py
├── core
│ ├── __init__.py
│ ├── streams.py
│ └── execute.py
├── utils
│ ├── __init__.py
│ ├── log.py
│ ├── goto.py
│ ├── signals.py
│ ├── constants.py
│ ├── convert2to3.py
│ ├── debug.py
│ ├── introspection.py
│ ├── search.py
│ ├── eventfilters.py
│ └── save.py
├── _version.py
└── __init__.py
├── requirements.txt
├── media
└── Screenshot1.png
├── .gitignore
├── scripts
├── _init_script.py
├── menu.py
└── prototypes
│ ├── explore_colour_dialog.py
│ ├── button_tabs.py
│ ├── jsonhighlight.py
│ ├── shortcut_conflict.py
│ ├── captcha.py
│ ├── autocomplete_directconnection.py
│ ├── shortcut_config.json
│ ├── browser.py
│ ├── multi_selection.py
│ ├── tab_search.py
│ ├── shortcutthief.py
│ ├── new_autocomplete.py
│ ├── shorcut_detector.py
│ ├── loaderlist.py
│ ├── code_coverage_feature.py
│ ├── redirect.py
│ ├── hover_text_tooltip.py
│ ├── _popup_label.py
│ ├── simplemanager.py
│ ├── terminal_mid.py
│ ├── tabview_demo.py
│ ├── terminal_dict.py
│ ├── qtabbar_paint.py
│ ├── terminal_events.py
│ ├── terminal_earlier.py
│ └── manager.py
├── README.md
├── .github
└── workflows
│ └── python-app.yml
└── bin
└── PythonEditorLaunch.py
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/PythonEditor/app/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/PythonEditor/models/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/PythonEditor/ui/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/tests/test_pytestqt/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/PythonEditor/core/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/PythonEditor/ui/dialogs/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/PythonEditor/utils/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/PythonEditor/ui/features/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/PythonEditor/_version.py:
--------------------------------------------------------------------------------
1 | __version__ = '0.7.1'
2 | __date__ = '2020.09.13'
3 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plasmax/PythonEditor/HEAD/requirements.txt
--------------------------------------------------------------------------------
/media/Screenshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plasmax/PythonEditor/HEAD/media/Screenshot1.png
--------------------------------------------------------------------------------
/PythonEditor/app/nukefeatures/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 | from . import nukeinit
3 |
--------------------------------------------------------------------------------
/tests/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # designed to be run with git bash
3 |
4 | /c/Users/Max/AppData/Local/Programs/Python/Python27/python.exe $PWD/tests/test_syntax.py
5 | python $PWD/tests/test_syntax.py
--------------------------------------------------------------------------------
/tests/test_dependencies.py:
--------------------------------------------------------------------------------
1 |
2 | def test_cElementTree():
3 | from xml.etree import cElementTree
4 |
5 | def test_Qt():
6 | from Qt import QtWidgets
7 |
8 | def test_six():
9 | from six import string_types
10 |
11 |
--------------------------------------------------------------------------------
/tests/test_qt_binding.py:
--------------------------------------------------------------------------------
1 | # test that we can find Qt bindings with Qt.py
2 | # raise ImportError("No Qt binding were found.")
3 |
4 | import os
5 | os.environ["QT_VERBOSE"] = "1"
6 |
7 | def test_basic_import():
8 | from PythonEditor.ui.editor import Editor
9 |
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *.pytest_cache
3 | TODO.txt
4 | _tests/
5 | _experiments/
6 | _deprecated/
7 | _old
8 | *.nk
9 | icons/
10 | test_app.bat
11 | *test_code.py
12 | QTDark.stylesheet
13 | test_code_objects.py
14 | *tost_code.py
15 | *.sublime-workspace
16 | *.sublime-project
17 | *.h
18 | *.cpp
19 | *.dot
20 | test_app_python27.bat
21 | .vscode/
22 | .pytest_cache/
23 | __pycache__
--------------------------------------------------------------------------------
/scripts/_init_script.py:
--------------------------------------------------------------------------------
1 | """
2 | Can be executed on Nuke startup to load PythonEditor.
3 | """
4 | import sys
5 | import os
6 | sys.path.append(os.path.dirname(os.path.dirname(__file__)))
7 |
8 | if ('nuke' in globals() and nuke.GUI):
9 | import PythonEditor
10 | from PythonEditor.utils.constants import NUKE_DIR
11 | PythonEditor.nuke_menu_setup(node_menu=True, nuke_menu=True)
12 |
--------------------------------------------------------------------------------
/scripts/menu.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import nuke
4 |
5 | nuke.tprint('MENU.PY')
6 |
7 | __folder__ = os.path.dirname(os.path.dirname(__file__))
8 | sys.path.append(__folder__)
9 |
10 | os.environ['PYTHONEDITOR_TEST_SUITE_VARIABLE'] = 'test'
11 | os.environ['PYTHONEDITOR_CUSTOM_DIR'] = os.path.join(
12 | os.path.expanduser('~'),
13 | 'Desktop',
14 | 'TEST' # will only work if there's a folder called TEST on the Desktop.
15 | )
16 | import PythonEditor
17 | PythonEditor.nuke_menu_setup()
18 |
--------------------------------------------------------------------------------
/scripts/prototypes/explore_colour_dialog.py:
--------------------------------------------------------------------------------
1 | # exploratory testing
2 | from PythonEditor.ui.Qt import QtWidgets, QtGui, QtCore
3 |
4 | widgets = QtWidgets.QApplication.topLevelWidgets()
5 | ide = [w for w in widgets if w.objectName() == 'IDE'][0]
6 | ide.setStyleSheet('background:rgb(45,42,46);')
7 |
8 |
9 | @QtCore.Slot(QtGui.QColor)
10 | def info(colour):
11 | rgb = {'r': colour.red(),
12 | 'g': colour.green(),
13 | 'b': colour.blue()}
14 | ide.setStyleSheet('background:rgb({r},{g},{b});'.format(**rgb))
15 |
16 |
17 | cd = QtWidgets.QColorDialog()
18 | cd.currentColorChanged.connect(info)
19 | cd.show()
20 | # cd.colorSelected.connect(info)
21 |
--------------------------------------------------------------------------------
/PythonEditor/utils/log.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | import sys
3 | import logging
4 |
5 |
6 | logger = logging.getLogger('PythonEditor')
7 |
8 | handlers = logger.handlers
9 | for handler in handlers:
10 | logger.removeHandler(handler)
11 |
12 | handler = logging.StreamHandler(sys.stdout)
13 | format_string = '[%(filename)s:%(lineno)s - %(funcName)20s() ] %(message)s'
14 | format_string = '%(asctime)s, %(levelname)-8s [%(filename)s:%(module)s:%(funcName)s:%(lineno)d] %(message)s'
15 | formatter = logging.Formatter(format_string)
16 | handler.setFormatter(formatter)
17 | logger.addHandler(handler)
18 |
19 | # logging.basicConfig(format=format_string, datefmt='%Y-%m-%d:%H:%M:%S', level=logging.DEBUG)
20 |
--------------------------------------------------------------------------------
/tests/test_pytestqt/test_actions.py:
--------------------------------------------------------------------------------
1 | from pytestqt import qtbot
2 | from PythonEditor.ui.features import actions
3 | from PythonEditor.ui.Qt import QtWidgets, QtGui
4 |
5 | def test_toggle_backslashes_in_string():
6 | test_path = "c:\\path/to\\some\\file.jpg"
7 | result = actions.toggle_backslashes_in_string(test_path)
8 | assert result == "c:/path/to/some/file.jpg"
9 |
10 |
11 | def test_toggle_backslashes(qtbot):
12 | editor = QtWidgets.QPlainTextEdit()
13 | test_path = "c:\\path/to\\some\\file.jpg"
14 | editor.setPlainText(test_path)
15 | textCursor = editor.textCursor()
16 | textCursor.setPosition(0, QtGui.QTextCursor.MoveAnchor)
17 | editor.setTextCursor(textCursor)
18 | actions.toggle_backslashes(editor)
19 | assert editor.toPlainText() == "c:/path/to/some/file.jpg"
20 |
21 |
--------------------------------------------------------------------------------
/PythonEditor/utils/goto.py:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | def goto_position(editor, pos):
5 | """
6 | Goto position in document.
7 | """
8 | cursor = editor.textCursor()
9 | editor.moveCursor(cursor.End)
10 | cursor.setPosition(pos)
11 | editor.setTextCursor(cursor)
12 |
13 | def goto_line(editor, lineno, scroll=False):
14 | """
15 | Sets the text cursor to the
16 | end of the document, then to
17 | the given lineno.
18 | """
19 | count = editor.blockCount()
20 | if lineno > count:
21 | lineno = count
22 | lineno = lineno-1
23 | pos = editor.document(
24 | ).findBlockByNumber(
25 | lineno).position()
26 |
27 | goto_position(editor, pos)
28 |
29 | if scroll:
30 | bar = editor.verticalScrollBar()
31 | bar.setValue(max(0, bar.value()-2))
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PythonEditor
2 | A Script Editor for Foundry's NUKE with more features.
3 |
4 | [](https://github.com/plasmax/PythonEditor/actions/workflows/python-app.yml)
5 |
6 | ## Installation:
7 | [Download](https://downgit.github.io/#/home?url=https://github.com/plasmax/PythonEditor/tree/master/PythonEditor).
8 | Copy the "PythonEditor" folder from the .zip file into your user .nuke folder.
9 |
10 | Create a menu.py file in the .nuke folder and add the following code to it:
11 |
12 | ```python
13 | import PythonEditor
14 | PythonEditor.nuke_menu_setup(nuke_menu=True, node_menu=True, pane_menu=True)
15 | ```
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/PythonEditor/utils/signals.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | def connect(cls, signal, slot):
4 | """
5 | Function to connect signals to slots and get the signature
6 | of the signal in the process, i.e. "2signal(PyObject)"
7 |
8 | TODO: instead of using a global temp variable,
9 | a variable on the given class may work better.
10 | """
11 | global __temp_name
12 | __temp_name = 'temp'
13 |
14 | def foo(x):
15 | try:
16 | x = str(x.name())
17 | except AttributeError:
18 | # PySide2 sends a QtCore.QMetaMethod
19 | # object to connectNotify; PySide sends
20 | # just the string name.
21 | pass
22 |
23 | global __temp_name
24 | __temp_name = x
25 |
26 | connectNotify = cls.connectNotify
27 | cls.connectNotify = foo
28 | signal.connect(slot)
29 | cls.connectNotify = connectNotify
30 | name = __temp_name
31 | del __temp_name
32 | return name, signal, slot
33 |
--------------------------------------------------------------------------------
/PythonEditor/utils/constants.py:
--------------------------------------------------------------------------------
1 | import os
2 | from xml.etree import ElementTree
3 |
4 | pyside = 'PySide'
5 | pyqt = 'PyQt4'
6 |
7 | IN_NUKE = 'nuke' in globals()
8 | if IN_NUKE:
9 | import nuke
10 | if nuke.NUKE_VERSION_MAJOR > 10:
11 | pyside = 'PySide2'
12 | pyqt = 'PyQt5'
13 |
14 | USER = os.environ.get('USERNAME')
15 |
16 | NUKE_DIR = os.path.join(
17 | os.path.expanduser('~'),
18 | '.nuke'
19 | )
20 | PYTHONEDITOR_CUSTOM_DIR = os.getenv(
21 | 'PYTHONEDITOR_CUSTOM_DIR'
22 | )
23 | if PYTHONEDITOR_CUSTOM_DIR is not None:
24 | if os.path.isdir(PYTHONEDITOR_CUSTOM_DIR):
25 | NUKE_DIR = PYTHONEDITOR_CUSTOM_DIR
26 |
27 | QT_VERSION = os.pathsep.join([pyside, pyqt])
28 |
29 | DEFAULT_FONT = (
30 | 'Courier New'
31 | if (os.name == 'nt')
32 | else 'DejaVu Sans Mono'
33 | )
34 | if os.getenv(
35 | 'PYTHONEDITOR_DEFAULT_FONT'
36 | ) is None:
37 | os.environ[
38 | 'PYTHONEDITOR_DEFAULT_FONT'
39 | ] = DEFAULT_FONT
40 |
--------------------------------------------------------------------------------
/PythonEditor/utils/convert2to3.py:
--------------------------------------------------------------------------------
1 | # convert code from python 2 to 3
2 | from __future__ import print_function
3 | import traceback
4 |
5 | import lib2to3
6 | from lib2to3.main import diff_texts
7 | from lib2to3.refactor import RefactoringTool, get_fixers_from_package
8 |
9 |
10 | def convert_to_python3(script, verbose=False):
11 | """Convert a string containing python 2
12 | syntax to python 3 syntax. The string passed
13 | must be valid python code without any python 2
14 | syntax errors.
15 |
16 | :param script: `str` python 2 code
17 | :param verbose: `bool` optionally, print a diff of the changes
18 | :rtype: `str` python 3 code
19 | """
20 | fixers = get_fixers_from_package("lib2to3.fixes")
21 | refactoring_tool = RefactoringTool(fixer_names=fixers)
22 | result = refactoring_tool.refactor_string(script, "script")
23 | new_script = str(result)
24 | if verbose:
25 | for line in diff_texts(script, new_script, ""):
26 | print(line)
27 | return new_script
--------------------------------------------------------------------------------
/PythonEditor/app/nukefeatures/editorknob.py:
--------------------------------------------------------------------------------
1 | from PythonEditor.ui.editor import Editor
2 | import nuke
3 |
4 | class EditorKnob(Editor):
5 | """
6 | TODO: needs a mechanism to switch/write to its own callback knobs
7 | and a method to execute from within node context (aka button)
8 | """
9 | def __init__(self, node=None):
10 | super(EditorKnob, self).__init__()
11 | self.text_changed_signal.connect(self.valueChanged)
12 | self.node = node
13 | self.read()
14 |
15 | def knob_check(self):
16 | if not self.node.knob('code'):
17 | self.node.addKnob(
18 | nuke.PyScript_Knob(
19 | 'code',
20 | 'Execute Code',
21 | ''
22 | )
23 | )
24 |
25 | def read(self):
26 | self.knob_check()
27 | self.setPlainText(self.node.knob('code').value())
28 |
29 | def makeUI(self):
30 | return self
31 |
32 | def valueChanged(self):
33 | self.knob_check()
34 | self.node['code'].setValue(self.toPlainText())
35 |
--------------------------------------------------------------------------------
/tests/test_pytestqt/test_app.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | from pytestqt import qtbot
5 | from pytestqt.qt_compat import qt_api
6 |
7 | from PythonEditor.ui import ide
8 | from PythonEditor.ui.features import nukepalette
9 | from PythonEditor.ui.Qt import QtWidgets, QtGui
10 |
11 |
12 | def test_app(qtbot):
13 | app = qt_api.QApplication.instance()
14 |
15 | # add the package path to sys.path
16 | FOLDER = os.path.dirname(__file__)
17 | PACKAGE_PATH = os.path.dirname(FOLDER)
18 |
19 | # set the application icon
20 | ICON_PATH = os.path.join(PACKAGE_PATH, 'icons', 'PythonEditor.png')
21 | icon = QtGui.QIcon(ICON_PATH)
22 | app.setWindowIcon(icon)
23 |
24 | # set style (makes palette work on linux)
25 | # Plastique isn't available on Windows, so try multiple styles.
26 | styles = QtWidgets.QStyleFactory.keys()
27 | for style_name in ['Plastique', 'Fusion']:
28 | if style_name not in styles:
29 | continue
30 | print('Setting style to:', style_name)
31 | style = QtWidgets.QStyleFactory.create(style_name)
32 | QtWidgets.QApplication.setStyle(style)
33 | break
34 |
35 | app.setPalette(nukepalette.getNukePalette())
36 |
37 | _ide = ide.IDE()
38 |
39 |
--------------------------------------------------------------------------------
/PythonEditor/utils/debug.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | import os
3 | import sys
4 | import inspect
5 |
6 |
7 | def print_call_stack():
8 | stack = []
9 | f = sys._getframe()
10 | while f is not None:
11 | stack.append(f)
12 | f = f.f_back
13 | s = ''
14 | for f in reversed(stack):
15 | _name = f.f_globals.get('__name__')
16 | l = '{0}{1} in {2}'.format(
17 | s,
18 | f.f_code.co_name,
19 | _name
20 | )
21 | print(l)
22 | s+=' '
23 |
24 |
25 | def debug(*args, **kwargs):
26 | """
27 | For exclusively printing to mlast user terminal.
28 | TODO: what more useful information could be included here?
29 | Would be nice to allow this to send information via email.
30 | """
31 | try:
32 | if os.getenv('USER') != 'mlast':
33 | return
34 |
35 | f = sys._getframe().f_back
36 |
37 | print('\nDEBUG:')
38 | print(*args, **kwargs)
39 | if 'print_call_stack' in kwargs:
40 | print_call_stack()
41 |
42 | _file = inspect.getfile(f)
43 | lineno = str(inspect.getlineno(f))
44 | print('sublime %s:%s' % (_file, lineno))
45 | except:
46 | pass
47 |
--------------------------------------------------------------------------------
/scripts/prototypes/button_tabs.py:
--------------------------------------------------------------------------------
1 |
2 | from PythonEditor.ui.Qt import QtWidgets, QtCore
3 | from PythonEditor.ui.editor import Editor
4 | from functools import partial
5 |
6 |
7 | class Label():
8 | def __init__(self):
9 | super(Label, self).__init__()
10 |
11 | class Tabs(QtWidgets.QTabWidget):
12 | tabs = []
13 | def __init__(self):
14 | super(Tabs, self).__init__()
15 |
16 | def new_button(self):
17 | i = self.currentIndex()+1
18 | e = Editor()
19 | self.insertTab(i, e, '')
20 | b = QtWidgets.QToolButton()
21 | b.editor = e
22 | b.clicked.connect(self.close_tab)
23 | self.tabs.append(b)
24 | b.setText('x')
25 | l = QtWidgets.QLabel('Widget')
26 | self.tabs.append(l)
27 | tb = self.tabBar()
28 | tb.setTabButton(i, QtWidgets.QTabBar.RightSide, b)
29 | tb.setTabButton(i, QtWidgets.QTabBar.LeftSide, l)
30 |
31 | def close_tab(self):
32 | tab = self.sender()
33 | self.removeTab(self.indexOf(tab.editor))
34 |
35 |
36 | t = Tabs()
37 | t.show()
38 |
39 | t.new_button()
40 | t.new_button()
41 |
42 | class VB(QtCore.QObject):
43 | s = QtCore.Signal()
44 |
45 | v = VB()
46 | v.s
47 |
48 |
49 | QtCore.Signal
--------------------------------------------------------------------------------
/scripts/prototypes/jsonhighlight.py:
--------------------------------------------------------------------------------
1 | from Qt.QtGui import QSyntaxHighlighter, QColor
2 |
3 |
4 | PURPLE = QColor.fromRgbF(0.7, 0.5, 1, 1)
5 | BLUE = QColor.fromRgbF(0, 0.5, 1, 1)
6 |
7 |
8 | class JSONHighlighter(QSyntaxHighlighter):
9 | def highlightBlock(self, s):
10 | if not s.strip():
11 | return
12 | i = 0
13 | for start, length in get_string_ranges(s):
14 | if i % 2:
15 | color = BLUE
16 | else:
17 | color = PURPLE
18 | i += 1
19 | self.setFormat(start, length, color)
20 |
21 |
22 | def get_string_ranges(t):
23 | """Get the in and out points of double-quote encased strings."""
24 |
25 | # life's too short to parse escape characters.
26 | s = t.replace('\\"', '##')
27 | assert len(s) == len(t)
28 |
29 | i = 0
30 | prev_c = ''
31 | in_str = False
32 | length = 0
33 | for i in range(len(s)):
34 | c = s[i]
35 |
36 | if in_str:
37 | length += 1
38 |
39 | if c == '\"':
40 | if in_str:
41 | # we've reached the end of the string
42 | in_str = False
43 | yield i-length+1, length-1
44 | length = 0
45 | else:
46 | in_str = True
47 |
48 | prev_c = c
49 | i += 1
50 |
--------------------------------------------------------------------------------
/.github/workflows/python-app.yml:
--------------------------------------------------------------------------------
1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
3 |
4 | name: build
5 |
6 | on:
7 | push:
8 | branches: [ master ]
9 | pull_request:
10 | branches: [ master ]
11 |
12 |
13 | jobs:
14 | build:
15 |
16 | runs-on: ubuntu-latest
17 |
18 | steps:
19 | - uses: actions/checkout@v2
20 | - name: Set up Python 3.9
21 | uses: actions/setup-python@v2
22 | with:
23 | python-version: 3.9
24 | - name: Install dependencies
25 | run: |
26 | python -m pip install --upgrade pip
27 | pip install flake8 pytest pytest-qt
28 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
29 | - name: Lint with flake8
30 | run: |
31 | # stop the build if there are Python syntax errors or undefined names
32 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --per-file-ignores="six.py:F821"
33 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
34 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
35 | working-directory: ./PythonEditor
36 | - name: Test with pytest
37 | run: |
38 | python -m pytest --rootdir /home/runner/work/PythonEditor/PythonEditor/ --ignore=tests/test_pytestqt
--------------------------------------------------------------------------------
/tests/test_syntax.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # git pre-commit hook SyntaxError checker
3 |
4 | import os
5 | import sys
6 | import json
7 |
8 |
9 | PACKAGE_NAME = 'PythonEditor' # this is the only bit that should change from project to project
10 |
11 |
12 | def get_project_dir():
13 | folder = os.path.dirname(os.path.abspath(__file__))
14 | folder_name = os.path.basename(folder)
15 |
16 | # package should be one level up from /tests/
17 | if folder_name != 'tests':
18 | raise Exception('This test is designed to be run from a "/tests/" subdirectory, not %s'%folder)
19 |
20 | package_dir = os.path.dirname(folder)
21 |
22 | project_dir = os.path.join(package_dir, PACKAGE_NAME)
23 | return project_dir
24 |
25 |
26 | def read(path):
27 | with open(path, 'r') as fd:
28 | return fd.read()
29 |
30 |
31 | def check_py(path):
32 | contents = read(path)
33 | compile(contents, path, 'exec')
34 |
35 |
36 | def check_json(path):
37 | contents = read(path)
38 | json.loads(contents)
39 |
40 |
41 | def main():
42 | project_dir = get_project_dir()
43 | for root, dirs, files in os.walk(project_dir):
44 | for filename in files:
45 | if filename.endswith('.py'):
46 | check_py(os.path.join(root, filename))
47 | if filename.endswith('.json'):
48 | check_json(os.path.join(root, filename))
49 |
50 | print('Syntax check complete, all .py and .json files compile with Python {}'.format(sys.version))
51 |
52 |
53 | if __name__ == '__main__':
54 | main()
55 |
--------------------------------------------------------------------------------
/scripts/prototypes/shortcut_conflict.py:
--------------------------------------------------------------------------------
1 | from PySide import QtGui, QtCore
2 |
3 | class Window(QtGui.QMainWindow):
4 | def __init__(self):
5 | QtGui.QMainWindow.__init__(self)
6 | widget = QtGui.QWidget(self)
7 | self.setCentralWidget(widget)
8 | layout = QtGui.QVBoxLayout(widget)
9 | self.edit = QtGui.QTextEdit(self)
10 | self.edit.setText('text')
11 | self.table = Table(self)
12 | layout.addWidget(self.edit)
13 | layout.addWidget(self.table)
14 | menu = self.menuBar().addMenu('&File')
15 | def add_action(text, shortcut):
16 | action = menu.addAction(text)
17 | action.setShortcut(shortcut)
18 | action.triggered.connect(self.handleAction)
19 | action.setShortcutContext(QtCore.Qt.WidgetShortcut)
20 | self.edit.addAction(action)
21 | add_action('&Copy', 'Ctrl+C')
22 | add_action('&Print', 'Ctrl+P')
23 |
24 | def handleAction(self):
25 | print ('Action!')
26 |
27 | class Table(QtGui.QTableWidget):
28 | def __init__(self, parent):
29 | QtGui.QTableWidget.__init__(self, parent)
30 | self.setRowCount(4)
31 | self.setColumnCount(2)
32 | self.setItem(0, 0, QtGui.QTableWidgetItem('item'))
33 |
34 | def keyPressEvent(self, event):
35 | print ('keyPressEvent: %s' % event.key())
36 | QtGui.QTableWidget.keyPressEvent(self, event)
37 |
38 | if __name__ == '__main__':
39 |
40 | import sys
41 | app = QtGui.QApplication(sys.argv)
42 | window = Window()
43 | window.show()
44 | sys.exit(app.exec_())
45 |
--------------------------------------------------------------------------------
/tests/in_progress/_autocomplete.py:
--------------------------------------------------------------------------------
1 | #!/net/homes/mlast/Desktop nuke-tg-python
2 | """ For testing independently. """
3 | from __future__ import absolute_import
4 | import sys
5 | import os
6 |
7 |
8 | sys.dont_write_bytecode = True
9 | TESTS_DIR = os.path.dirname(__file__)
10 | PACKAGE_PATH = os.path.dirname(TESTS_DIR)
11 | sys.path.append(PACKAGE_PATH)
12 |
13 | try:
14 | import nuke
15 | pyside = ('PySide' if (nuke.NUKE_VERSION_MAJOR < 11) else 'PySide2')
16 | except ImportError:
17 | pyside = 'PySide'
18 |
19 | os.environ['QT_PREFERRED_BINDING'] = pyside
20 |
21 | from PythonEditor.ui.features import nukepalette
22 | from PythonEditor.ui.features import autocompletion
23 | from PythonEditor.ui import editor
24 | from PythonEditor.ui.Qt import QtWidgets
25 |
26 |
27 | class TestEditor(editor.Editor):
28 | """
29 | An override of Editor to strip out other features.
30 | """
31 | def __init__(self):
32 | super(editor.Editor, self).__init__()
33 | self._changed = False
34 | self.autocomplete_overriding = True
35 | self.autocomplete = autocompletion.AutoCompleter(self)
36 |
37 | if __name__ == '__main__':
38 | """
39 | For testing outside of nuke.
40 | """
41 | try:
42 | app = QtWidgets.QApplication(sys.argv)
43 | except RuntimeError:
44 | # for running inside and outside of Nuke
45 | app = QtWidgets.QApplication.instance()
46 |
47 | e = TestEditor()
48 | app.setPalette(nukepalette.getNukePalette())
49 | e.show()
50 | e.resize(500, 800)
51 | plastique = QtWidgets.QStyleFactory.create('Plastique')
52 | QtWidgets.QApplication.setStyle(plastique)
53 | sys.exit(app.exec_())
54 |
--------------------------------------------------------------------------------
/scripts/prototypes/captcha.py:
--------------------------------------------------------------------------------
1 | # capture keystrokes including modifiers and turn them into human-readable format
2 | from Qt import QtCore, QtWidgets, QtGui
3 |
4 |
5 | def is_int(key):
6 | try:
7 | int(key)
8 | return True
9 | except Exception:
10 | return False
11 |
12 | def is_key(key):
13 | return ('key' in repr(key).lower())
14 |
15 | modmap = {
16 | QtCore.Qt.Key_Control: QtCore.Qt.ControlModifier,
17 | QtCore.Qt.Key_Shift: QtCore.Qt.ShiftModifier,
18 | QtCore.Qt.Key_Alt: QtCore.Qt.AltModifier,
19 | QtCore.Qt.Key.Key_Meta: QtCore.Qt.MetaModifier,
20 | }
21 | #keymap = {v:v for k,v in QtCore.Qt.__dict__.items() if is_key(v) and is_int(v)}
22 | #keymap.update(**modmap)
23 |
24 | class Captcha(QtWidgets.QLineEdit):
25 | keylist = []
26 | def keyPressEvent(self, event):
27 | if event.isAutoRepeat():
28 | return
29 | key = event.key()
30 | key = modmap.get(key, key)
31 |
32 | self.keylist.append(key)
33 | combo = 0
34 | text = ''
35 | for k in self.keylist:
36 | if k in modmap.values():
37 | combo |= k
38 | else:
39 | if text != '':
40 | text += '+'
41 | text += QtGui.QKeySequence(k).toString()
42 |
43 | stroke = QtGui.QKeySequence(combo).toString()
44 | self.setText(stroke+text)
45 |
46 | def keyReleaseEvent(self, event):
47 | if event.isAutoRepeat():
48 | return
49 | key = event.key()
50 | key = modmap.get(key, key)
51 | try:
52 | self.keylist.remove(key)
53 | except ValueError:
54 | pass
55 |
56 |
57 | if __name__ == '__main__':
58 | captcha = Captcha()
59 | captcha.show()
60 |
--------------------------------------------------------------------------------
/PythonEditor/app/nukefeatures/jupyter_nodebook.py:
--------------------------------------------------------------------------------
1 | import nuke
2 | from PythonEditor.ui import editor
3 |
4 |
5 | class PyKnobEdit(editor.Editor):
6 | """
7 | Editor that automatically updates knobs.
8 | """
9 | def __init__(self, node=None, knob=None):
10 | super(PyKnobEdit, self).__init__()
11 | self.node = node
12 |
13 | knob = node.knob('py_btn') if knob is None else knob
14 | if knob is None:
15 | return
16 | self.knob = knob
17 |
18 | self.read()
19 | self.text_changed_signal.connect(self.write)
20 |
21 | def makeUI(self):
22 | return self
23 |
24 | def read(self):
25 | self.setPlainText(self.knob.value())
26 |
27 | def write(self):
28 | self.knob.setValue(self.toPlainText())
29 |
30 |
31 | def create_jupyter_node(exec_cmd=''):
32 | noop = nuke.nodes.NoOp(name='Jupyter_Node')
33 | pybtn = nuke.PyScript_Knob('py_btn', 'Execute Code')
34 | pybtn.setFlag(nuke.STARTLINE)
35 | pytreebtn = nuke.PyScript_Knob('py_tree_btn', 'Execute Tree', exec_cmd)
36 | pyknob = nuke.PyCustom_Knob('py_edit', '', 'PyKnobEdit(node=nuke.thisNode())')
37 |
38 | for knob in pyknob, pybtn, pytreebtn:
39 | noop.addKnob(knob)
40 |
41 | userknob = noop.knob('User')
42 | userknob.setLabel('Python')
43 |
44 | exec_tree = """# exec connected jupyter nodes
45 | def node_tree(node):
46 | nodes = []
47 | while node is not None:
48 | if node.knob('py_btn'):
49 | nodes.append(node)
50 | node = node.input(0)
51 | return reversed(nodes)
52 |
53 | def exec_tree_btns():
54 | for node in node_tree(nuke.thisNode()):
55 | node.knob('py_btn').execute()
56 |
57 | exec_tree_btns()
58 | """
59 |
60 | # create_jupyter_node(exec_cmd=exec_tree)
61 |
62 |
63 |
--------------------------------------------------------------------------------
/scripts/prototypes/autocomplete_directconnection.py:
--------------------------------------------------------------------------------
1 | from Qt import QtWidgets, QtGui, QtCore
2 |
3 |
4 | class Editor(QtWidgets.QPlainTextEdit):
5 | text_entered_signal = QtCore.Signal(object)
6 | def __init__(self):
7 | super(Editor, self).__init__()
8 | self.autocomplete = AutoCompleter(self)
9 |
10 | def keyPressEvent(self, event):
11 | self.enter_text = True
12 | self.text_entered_signal.emit(event)
13 | if not self.enter_text:
14 | return
15 | #super(Editor, self).keyPressEvent(event)
16 |
17 |
18 | class AutoCompleter(QtWidgets.QListView):
19 | def __init__(self, editor):
20 | super(AutoCompleter, self).__init__()
21 |
22 | self.setWindowFlags(
23 | QtCore.Qt.WindowStaysOnTopHint
24 | | QtCore.Qt.FramelessWindowHint
25 | )
26 | self.editor = editor
27 | editor.setFocus(QtCore.Qt.MouseFocusReason)
28 | editor.text_entered_signal.connect(
29 | self.block,
30 | QtCore.Qt.DirectConnection
31 | )
32 |
33 | @QtCore.Slot(object)
34 | def block(self, event):
35 | editor = self.editor
36 | super(editor.__class__, editor).keyPressEvent(event)
37 | return
38 |
39 | if event.key() == QtCore.Qt.Key_Tab:
40 | event.accept()
41 | editor.enter_text = False
42 |
43 | rect = editor.cursorRect()
44 | pos = rect.bottomRight()
45 | pos = editor.mapToGlobal(pos)
46 | self.move(pos)
47 | self.show()
48 |
49 | def keyPressEvent(self, event):
50 | if event.key() == QtCore.Qt.Key_Escape:
51 | self.hide()
52 | super(AutoCompleter, self).keyPressEvent(event)
53 |
54 |
55 | e = Editor()
56 | e.show()
57 |
58 |
59 |
--------------------------------------------------------------------------------
/PythonEditor/app/nukefeatures/nukedock.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | def setup_dock(shortcut=None):
4 | """Register the PythonEditor interface
5 | as a Nuke docked panel.
6 | """
7 | import nuke
8 | from nukescripts import PythonPanel, registerPanel
9 |
10 | # register the panel manually, expressly for
11 | # the purpose of redefining the nested addToPane
12 | # function to take a pane parameter.
13 | class Panel(PythonPanel):
14 | # This class was previously nested within
15 | # the nukescripts.registerWidgetAsPanel function.
16 | def __init__(self, widget, name, _id):
17 | PythonPanel.__init__(self, name, _id)
18 | self.custom_knob = nuke.PyCustom_Knob(
19 | name, '',
20 | '__import__("nukescripts").panels.WidgetKnob('+widget+')'
21 | )
22 | self.addKnob(self.custom_knob)
23 |
24 | widget = '__import__("PythonEditor.ui.ide", fromlist=["IDE"]).IDE'
25 | name = 'Python Editor'
26 | _id = 'Python.Editor'
27 |
28 | def add_panel(pane=None):
29 | """Add or move existing PythonEditor
30 | panel to a new pane. This is what
31 | makes the PythonEditor a singleton.
32 |
33 | :param pane: Nuke
34 |
35 | TODO: maybe not here, but this should trigger the "in focus" signal that overrides shortcuts
36 | (me, later: the editor showEvent triggers a ShortcutFocusReason, is this what I meant?)
37 | """
38 | import PythonEditor
39 | try:
40 | panel = PythonEditor.__dock
41 | except AttributeError:
42 | panel = Panel(widget, name, _id)
43 | PythonEditor.__dock = panel
44 | return panel.addToPane(pane=pane)
45 |
46 | menu = nuke.menu('Pane')
47 | menu.addCommand(name, add_panel, shortcut=shortcut)
48 | registerPanel(_id, add_panel)
49 |
--------------------------------------------------------------------------------
/PythonEditor/ui/menubar.py:
--------------------------------------------------------------------------------
1 |
2 | import os # temporary for self.open until files.py or files/open.py, save.py, autosave.py implemented.
3 | import uuid
4 |
5 | from PythonEditor.ui.Qt import QtWidgets
6 | from PythonEditor.ui.features import actions
7 | from PythonEditor.utils import save
8 | from PythonEditor._version import __version__
9 |
10 |
11 | class MenuBar(object):
12 | """
13 | Install a menu on the given widget.
14 | """
15 | def __init__(self, widget):
16 | self.pythoneditor = widget
17 | self.tabeditor = widget.tabeditor
18 | self.tabs = widget.tabeditor.tabs
19 | self.editor = widget.tabeditor.editor
20 | self.terminal = widget.terminal
21 | self.menu_setup()
22 |
23 | def menu_setup(self):
24 | """
25 | Adds top menu bar and various menu
26 | items based on a json config file.
27 | """
28 | self.menu = QtWidgets.QMenuBar(self.pythoneditor)
29 | names = [
30 | 'File',
31 | 'Edit',
32 | 'View',
33 | 'Tools',
34 | 'Find',
35 | 'Selection',
36 | 'Preferences',
37 | 'Help',
38 | ]
39 | for name in names:
40 | self.menu.addMenu(name)
41 |
42 | for widget, action_name, attributes in actions.class_actions(self):
43 | location = attributes.get('Menu Location')
44 | if location is None:
45 | continue
46 | for action in widget.actions():
47 | if action.text() != action_name:
48 | continue
49 | break
50 | else:
51 | continue
52 |
53 | menu = self.menu
54 | if location.strip():
55 | for name in location.split('/'):
56 | item = actions.find_menu_item(menu, name)
57 | if item is None:
58 | item = menu.addMenu(name)
59 | menu = item
60 | menu.addAction(action)
61 |
62 | self.pythoneditor.layout().insertWidget(0, self.menu)
63 |
--------------------------------------------------------------------------------
/scripts/prototypes/shortcut_config.json:
--------------------------------------------------------------------------------
1 | {
2 | "tab_shortcuts": {
3 | "editortabs.new_tab": "Ctrl+Shift+N",
4 | "editortabs.close_current_tab": "Ctrl+Shift+W"
5 | },
6 | "signal_dict": {
7 | "return_handler": "Return/Enter",
8 | "tab_handler": "Tab",
9 | "wheel_zoom": "Ctrl+Mouse Wheel",
10 | "move_to_bottom": "Ctrl+Alt+End",
11 | "jump_to_start": "Home",
12 | "wrap_text": [
13 | "'",
14 | "\"",
15 | "(",
16 | ")",
17 | "[",
18 | "]",
19 | "\\{",
20 | "\\}"
21 | ],
22 | "cut_line": "Ctrl+X",
23 | "move_to_top": "Ctrl+Alt+Home",
24 | "clear_output_signal": "Ctrl+Backspace"
25 | },
26 | "editor_shortcuts": {
27 | "previous_tab": "Ctrl+Shift+Tab",
28 | "new_line_above": "Ctrl+Shift+Return",
29 | "delete_to_start_of_line": "Ctrl+Shift+Backspace",
30 | "hop_brackets": "Ctrl+M",
31 | "zoom_in": [
32 | "Ctrl+=",
33 | "Ctrl++"
34 | ],
35 | "duplicate_lines": "Ctrl+Shift+D",
36 | "search_input": "Ctrl+Shift+F",
37 | "new_line_below": "Ctrl+Alt+Return",
38 | "delete_lines": "Ctrl+Shift+K",
39 | "print_help": "Ctrl+H",
40 | "delete_to_end_of_line": "Ctrl+Shift+Delete",
41 | "zoom_out": "Ctrl+-",
42 | "move_lines_up": "Ctrl+Shift+Up",
43 | "indent": "Ctrl+]",
44 | "print_type": "Ctrl+T",
45 | "comment_toggle": "Ctrl+/",
46 | "wrap_text": [
47 | "'",
48 | "\"",
49 | "(",
50 | ")",
51 | "[",
52 | "]",
53 | "Shift+{",
54 | "Shift+}"
55 | ],
56 | "return_handler": [
57 | "Return",
58 | "Enter"
59 | ],
60 | "unindent": [
61 | "Ctrl+[",
62 | "Shift+Tab"
63 | ],
64 | "select_word": "Ctrl+D",
65 | "select_between_brackets": "Ctrl+Shift+M",
66 | "next_tab": "Ctrl+Tab",
67 | "exec_current_line": "Ctrl+G",
68 | "move_lines_down": "Ctrl+Shift+Down",
69 | "join_lines": "Ctrl+J",
70 | "select_lines": "Ctrl+L"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/PythonEditor/__init__.py:
--------------------------------------------------------------------------------
1 | """ PythonEditor by Max Last.
2 |
3 | The object hierarchy is:
4 | IDE
5 | PythonEditor
6 | TabEditor
7 | Tabs
8 | Editor
9 | AutoCompleter
10 | AutoSaveManager
11 | LineNumberArea
12 | ShortcutHandler
13 | Highlight
14 | Terminal
15 | MenuBar
16 | ObjectInspector
17 | PreferencesEditor
18 | ShortcutEditor
19 | Actions
20 | """
21 |
22 | def main():
23 | """The main entrypoint into PythonEditor.
24 | Makes sure the environment is configured correctly for Qt.py.
25 | """
26 | bindings = ['PySide2', 'PyQt5', 'PySide', 'PyQt4']
27 |
28 | # Nuke 10 segfaults when you even _look_ at PySide2.
29 | try:
30 | import nuke
31 | if nuke.NUKE_VERSION_MAJOR < 11:
32 | bindings.remove('PySide2')
33 | import os
34 | if not os.environ.get('QT_PREFERRED_BINDING'):
35 | os.environ['QT_PREFERRED_BINDING'] = os.pathsep.join(bindings)
36 | except ImportError:
37 | pass
38 |
39 | # do not create .pyc files
40 | import sys
41 | sys.dont_write_bytecode = True
42 |
43 | try:
44 | from PythonEditor.ui.features.actions import backup_pythoneditor_history
45 | backup_pythoneditor_history(in_tmp=True)
46 | except Exception:
47 | pass
48 |
49 |
50 | def _print_load_error(error):
51 | import traceback
52 | print('Sorry! There has been an error loading PythonEditor:')
53 | traceback.print_exc()
54 | print(error)
55 | print('Please contact tsalxam@gmail.com with the above error details.')
56 |
57 |
58 | def nuke_menu_setup(nuke_menu=False, node_menu=False, pane_menu=True):
59 | """ If in Nuke, set up menu.
60 |
61 | :param nuke_menu: `bool` Add menu items to the main Nuke menu.
62 | :param node_menu: `bool` Add menu item to the Node menu.
63 | :param pane_menu: `bool` Add menu item to the Pane menu.
64 | """
65 | try:
66 | import nuke
67 | except ImportError:
68 | return
69 |
70 | try:
71 | from PythonEditor.app.nukefeatures import nukeinit
72 | nukeinit.setup(nuke_menu=nuke_menu, node_menu=node_menu, pane_menu=pane_menu)
73 | except Exception as e:
74 | _print_load_error(e)
75 |
76 |
77 | try:
78 | main()
79 | except Exception as e:
80 | _print_load_error(e)
81 |
--------------------------------------------------------------------------------
/scripts/prototypes/browser.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from PythonEditor.ui.Qt import QtCore, QtWidgets, QtGui
4 | from PythonEditor.utils.constants import NUKE_DIR
5 |
6 |
7 | class FileTree(QtWidgets.QTreeView):
8 | path_signal = QtCore.Signal(str)
9 |
10 | def __init__(self, path):
11 | super(FileTree, self).__init__()
12 | self.set_model(path)
13 |
14 | def set_model(self, path):
15 | model = QtWidgets.QFileSystemModel()
16 | model.setRootPath(path)
17 | model.setNameFilterDisables(False)
18 | model.setNameFilters(['*.py',
19 | '*.txt',
20 | '*.md'])
21 |
22 | self.setModel(model)
23 | RTC = QtWidgets.QHeaderView.ResizeToContents
24 | self.header().setResizeMode(RTC)
25 | self.setRootIndex(model.index(path))
26 |
27 | def mousePressEvent(self, event):
28 | if event.button() == QtCore.Qt.LeftButton:
29 | super(FileTree, self).mousePressEvent(event)
30 | if event.button() == QtCore.Qt.RightButton:
31 | menu = QtWidgets.QMenu()
32 | menu.addAction('New', 'print "does nothing"')
33 | menu.addAction('Delete', 'print "does nothing"')
34 | cursor = QtGui.QCursor()
35 | pos = cursor.pos()
36 | menu.exec_(pos)
37 |
38 | def selectionChanged(self, selected, deselected):
39 | index_sel = selected.indexes()[0]
40 | item = self.model().filePath(index_sel)
41 | self.path_signal.emit(item)
42 |
43 |
44 | class FileBrowser(QtWidgets.QDialog):
45 | def __init__(self, path):
46 | QtWidgets.QDialog.__init__(self)
47 |
48 | self.setWindowTitle('Nuke Mini Browser')
49 | self._layout = QtWidgets.QVBoxLayout()
50 | self.setLayout(self._layout)
51 | self.resize(400, 320)
52 |
53 | self.file_tree = FileTree(path)
54 | self._layout.addWidget(self.file_tree)
55 |
56 |
57 | def make_panel(directory_path):
58 | global mini_browser
59 | mini_browser = FileBrowser(directory_path)
60 | mini_browser.show()
61 |
62 |
63 | if __name__ == '__main__':
64 | if not QtWidgets.QApplication.instance():
65 | app = QtWidgets.QApplication(sys.argv)
66 | make_panel(NUKE_DIR)
67 | sys.exit(app.exec_())
68 | else:
69 | make_panel(NUKE_DIR)
70 |
--------------------------------------------------------------------------------
/PythonEditor/ui/dialogs/preferences.py:
--------------------------------------------------------------------------------
1 | import os
2 | from PythonEditor.ui.Qt import QtWidgets
3 | from PythonEditor.ui.features import autosavexml
4 | from PythonEditor.utils import constants
5 |
6 |
7 | class PreferencesEditor(QtWidgets.QDialog):
8 | """
9 | A display widget that allows editing of
10 | preferences assigned to the editor.
11 | TODO: Implement mechanism to change external editor.
12 | """
13 | def __init__(self):
14 | super(PreferencesEditor, self).__init__()
15 | self.setObjectName('PythonEditorPreferences')
16 | self.layout = QtWidgets.QVBoxLayout(self)
17 | self.setWindowTitle('Python Editor Preferences')
18 |
19 | self.build_layout()
20 | self.connect_signals()
21 |
22 | def build_layout(self):
23 |
24 | # external editor path
25 | self.edit_path = QtWidgets.QLineEdit()
26 | self.external_editor_label = QtWidgets.QLabel('External Editor Path')
27 | self.external_editor_label.setBuddy(self.edit_path)
28 | self.layout.addWidget(self.external_editor_label)
29 | self.layout.addWidget(self.edit_path)
30 |
31 | # # change editor colours
32 | self.choose_colour_button = QtWidgets.QPushButton('Choose Colour')
33 | self.colour_dialog = QtWidgets.QColorDialog()
34 | self.choose_colour_button.clicked.connect(self.colour_dialog.show)
35 | self.layout.addWidget(self.choose_colour_button)
36 |
37 | # change editor font
38 | self.font_size = QtWidgets.QSpinBox()
39 | self.font_size.setValue(9)
40 | self.font_size_label = QtWidgets.QLabel('Choose Font Size')
41 | self.font_size_label.setBuddy(self.font_size)
42 | self.layout.addWidget(self.font_size_label)
43 | self.layout.addWidget(self.font_size)
44 |
45 | # syntax highlighter themes
46 |
47 | def connect_signals(self):
48 | self.edit_path.editingFinished.connect(self.set_editor_path)
49 |
50 | def set_editor_path(self):
51 | path = self.edit_path.text()
52 | autosavexml.set_external_editor_path(path=path)
53 |
54 | def showEvent(self, event):
55 | self.show_current_preferences()
56 | super(PreferencesEditor, self).showEvent(event)
57 |
58 | def show_current_preferences(self):
59 | self.edit_path.setText(os.environ.get('EXTERNAL_EDITOR_PATH'))
60 |
--------------------------------------------------------------------------------
/PythonEditor/ui/pythoneditor.py:
--------------------------------------------------------------------------------
1 | """ This module contains the main PythonEditor.
2 | It has all the functionality of PythonEditor
3 | except the ability to fully reload the whole package,
4 | which is kept in the container object.
5 |
6 | Example usage:
7 | from PythonEditor.ui import pythoneditor
8 |
9 | python_editor = pythoneditor.PythonEditor()
10 | python_editor.show()
11 | """
12 | from PythonEditor.ui.Qt import QtWidgets, QtCore
13 | from PythonEditor.ui import terminal
14 | from PythonEditor.ui import tabs
15 | from PythonEditor.ui import menubar
16 | from PythonEditor.ui.features import shortcuts
17 | from PythonEditor.ui.features import actions
18 | from PythonEditor.ui.features import autosavexml
19 |
20 |
21 | class PythonEditor(QtWidgets.QWidget):
22 | """ Main widget. Sets up layout
23 | and connects some signals.
24 | """
25 | def __init__(self, parent=None):
26 | super(
27 | PythonEditor,
28 | self
29 | ).__init__(parent=parent)
30 | self.setObjectName('PythonEditor')
31 | self._parent = parent
32 | if parent is not None:
33 | self.setParent(parent)
34 |
35 | layout = QtWidgets.QVBoxLayout(self)
36 | layout.setObjectName(
37 | 'PythonEditor_MainLayout'
38 | )
39 | layout.setContentsMargins(0, 0, 0, 0)
40 |
41 | self.tabeditor = tabs.TabEditor(self)
42 | self.editor = self.tabeditor.editor
43 | self.terminal = terminal.Terminal()
44 |
45 | splitter = QtWidgets.QSplitter(
46 | QtCore.Qt.Vertical
47 | )
48 | splitter.setObjectName(
49 | 'PythonEditor_MainVerticalSplitter'
50 | )
51 | splitter.addWidget(self.terminal)
52 | splitter.addWidget(self.tabeditor)
53 |
54 | layout.addWidget(splitter)
55 | self.splitter = splitter
56 |
57 | act = actions.Actions(
58 | pythoneditor=self,
59 | editor=self.editor,
60 | tabeditor=self.tabeditor,
61 | terminal=self.terminal,
62 | )
63 |
64 | self.menubar = menubar.MenuBar(self)
65 | self.shortcuthandler = shortcuts.ShortcutHandler(
66 | editor=self.editor,
67 | tabeditor=self.tabeditor,
68 | terminal=self.terminal,
69 | )
70 |
71 | # Loading the AutosaveManager will also load
72 | # all the contents of the autosave into tabs.
73 | self.filehandler = autosavexml.AutoSaveManager(self.tabeditor)
--------------------------------------------------------------------------------
/bin/PythonEditorLaunch.py:
--------------------------------------------------------------------------------
1 | #!/net/homes/mlast/bin nuke-safe-python-tg
2 | """ Launch PythonEditor as a Standalone Application.
3 | This file can also be executed from within an existing
4 | Qt QApplication to launch PythonEditor in a separate window.
5 | """
6 | from __future__ import absolute_import
7 | import time
8 | start = time.time()
9 |
10 | import os
11 | import sys
12 | import signal
13 | try:
14 | import PySide2
15 | pyside = 'PySide2'
16 | except ImportError:
17 | pyside = 'PySide'
18 |
19 |
20 | # Python's default signal handler doesn't work nicely with Qt, meaning
21 | # that ctrl-c won't work. Bypass Python's signal handling.
22 | signal.signal(signal.SIGINT, signal.SIG_DFL)
23 |
24 | # do not create .pyc files
25 | sys.dont_write_bytecode = True
26 |
27 | # add the package path to sys.path
28 | FOLDER = os.path.dirname(__file__)
29 | PACKAGE_PATH = os.path.dirname(FOLDER)
30 | if PACKAGE_PATH not in sys.path:
31 | sys.path.append(PACKAGE_PATH)
32 |
33 | # set startup env variables
34 | os.environ['QT_PREFERRED_BINDING'] = pyside
35 | try:
36 | # allow this variable to be set before launching
37 | os.environ['PYTHONEDITOR_CAPTURE_STARTUP_STREAMS']
38 | except KeyError:
39 | print('Will try and encapsulate sys.stdout immediately.')
40 | os.environ['PYTHONEDITOR_CAPTURE_STARTUP_STREAMS'] = '1'
41 | os.environ['PYTHONEDITOR_DEFAULT_FONT'] = 'Consolas'
42 |
43 | from PythonEditor.ui import ide
44 | from PythonEditor.ui.features import nukepalette
45 | from PythonEditor.ui.Qt import QtWidgets, QtGui
46 |
47 | try:
48 | app = QtWidgets.QApplication(sys.argv)
49 | except RuntimeError:
50 | # for running inside and outside of other applications
51 | app = QtWidgets.QApplication.instance()
52 |
53 | # set the application icon
54 | ICON_PATH = os.path.join(PACKAGE_PATH, 'icons', 'PythonEditor.png')
55 | icon = QtGui.QIcon(ICON_PATH)
56 | app.setWindowIcon(icon)
57 |
58 | # set style (makes palette work on linux)
59 | # Plastique isn't available on Windows, so try multiple styles.
60 | styles = QtWidgets.QStyleFactory.keys()
61 | for style_name in ['Plastique', 'Fusion']:
62 | if style_name not in styles:
63 | continue
64 | print('Setting style to:', style_name)
65 | style = QtWidgets.QStyleFactory.create(style_name)
66 | QtWidgets.QApplication.setStyle(style)
67 | break
68 |
69 | app.setPalette(nukepalette.getNukePalette())
70 |
71 | _ide = ide.IDE()
72 | _ide.showMaximized()
73 |
74 | print('PythonEditor startup time: %.04f seconds'%(time.time()-start))
75 |
76 | # run
77 | sys.exit(app.exec_())
78 |
--------------------------------------------------------------------------------
/PythonEditor/utils/introspection.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import inspect
3 | from functools import partial
4 |
5 | from ctypes import c_int64 as mutable_int
6 |
7 |
8 | def test_sys_count(func):
9 | global count
10 | count = 0
11 | def msr(*args, **kwargs):
12 | global count
13 | count += 1
14 |
15 | sys.setprofile(msr)
16 | func()
17 | sys.setprofile(None)
18 | print('Number of frame objects counted for {0}: {1}'.format(func,count))
19 |
20 |
21 | def increment(number):
22 | number.value += 1
23 |
24 |
25 | def get_called_code(func, *args, **kwargs):
26 | """
27 | Returns a string containing source code.
28 | Sets a temporary profiling function that
29 | collects source code from all callables.
30 | """
31 | def trace_source(frame, event, arguments, source=[]):
32 | try:
33 | if event == 'call':
34 | try:
35 | src = inspect.getsource(frame)
36 | file = frame.f_code.co_filename
37 | if not any(src == text for f, text, c in source):
38 | source.append((file, src, mutable_int(1)))
39 | else:
40 | for f, text, c in source:
41 | if src == text:
42 | increment(c)
43 | except IOError as e:
44 | pass
45 | # print e, frame.f_code, event, arguments, '\n'*5 # TODO: perfect place to log
46 | except Exception as e:
47 | sys.setprofile(None)
48 | # print 'Trace Source Quitting on Error:', e
49 |
50 | srccode = []
51 | prof = partial(trace_source, source=srccode)
52 |
53 | sys.setprofile(prof)
54 |
55 | try:
56 | func.__call__(*args, **kwargs)
57 | except Exception as e:
58 | sys.setprofile(None)
59 | finally:
60 | sys.setprofile(None)
61 |
62 | sys.setprofile(None)
63 |
64 | def info(filename, sourcecode, count):
65 | spacing = '\n'*3
66 | file_text = 'Filename: '
67 | count_text = 'Number of times called: '
68 | string = '{0}# {1}{2}\n# {3}{4}\n{5}'
69 | return string.format(spacing,
70 | file_text,
71 | filename,
72 | count_text,
73 | count.value,
74 | sourcecode,)
75 |
76 | source_code = ''.join([info(*a) for a in srccode])
77 | if source_code == '':
78 | print('No source code could be retrieved.')
79 | return source_code
80 |
81 |
--------------------------------------------------------------------------------
/scripts/prototypes/multi_selection.py:
--------------------------------------------------------------------------------
1 | from Qt.QtGui import *
2 | from Qt.QtCore import *
3 | from Qt.QtWidgets import *
4 | from PythonEditor.ui import editor
5 |
6 |
7 | #class MultiCursor(editor.Editor):
8 | class MultiCursor(QPlainTextEdit):
9 | def paintEvent(self, event):
10 | super(MultiCursor, self).paintEvent(event)
11 | painter = QPainter(self.viewport())
12 | offset = self.contentOffset()
13 | '''
14 | pen = QPen(Qt.yellow, 12, Qt.SolidLine)
15 | painter.setPen(pen)
16 | '''
17 | for c in self.cursors():
18 | block = c.block()
19 | l = block.layout()
20 | l.drawCursor(
21 | painter,
22 | offset, # QPointF
23 | c.position(),# int
24 | 2 # width:int
25 | )
26 |
27 | def mousePressEvent(self, event):
28 | app = QApplication
29 | mods = app.keyboardModifiers()
30 | if mods == Qt.ControlModifier:
31 | self.add_cursor(
32 | self.cursorForPosition(
33 | event.pos()
34 | )
35 | )
36 | return super(
37 | MultiCursor, self
38 | ).mousePressEvent(event)
39 |
40 | def keyPressEvent(self, event):
41 | if event.key() == Qt.Key_Escape:
42 | self._cursors=[]
43 | self.repaint()
44 | elif (
45 | event.key() in [
46 | Qt.Key_Up,
47 | Qt.Key_Down
48 | ]
49 | and event.modifiers() == Qt.ControlModifier|Qt.AltModifier
50 | ):
51 | self._cursors.append(
52 | self.textCursor()
53 | )
54 |
55 | return super(
56 | MultiCursor, self
57 | ).keyPressEvent(event)
58 |
59 | _cursors = []
60 | def cursors(self):
61 | """
62 | List of QTextCursors used to make
63 | multi-edits.
64 | """
65 | return self._cursors
66 |
67 | def add_cursor(self, cursor):
68 | self._cursors.append(
69 | cursor
70 | )
71 |
72 | def keyPressMulti(self, event):
73 | multi_keys = [
74 | Qt.Key_Up,
75 | Qt.Key_Down,
76 | Qt.Key_Left,
77 | Qt.Key_Right,
78 | ]
79 | k = event.key()
80 | if k not in multi_keys:
81 | return
82 | for c in self.cursors():
83 | c # insert key
84 |
85 |
86 | m = MultiCursor()
87 | m.show()
88 | m.setPlainText("""
89 | some boring test text
90 | some boring test text
91 | some boring test text
92 | some boring test text
93 |
94 | some boring test text
95 | """)
96 |
--------------------------------------------------------------------------------
/PythonEditor/ui/dialogs/objectinspector.py:
--------------------------------------------------------------------------------
1 | import os
2 | from __main__ import __dict__
3 |
4 | if not os.environ.get('QT_PREFERRED_BINDING'):
5 | os.environ['QT_PREFERRED_BINDING'] = 'PySide'
6 |
7 | from PythonEditor.ui.Qt import QtWidgets, QtCore, QtGui
8 |
9 |
10 | class ObjectInspector(QtWidgets.QWidget):
11 | """
12 | Inspired by the Object Inspector from
13 | the Pythonista app.
14 | """
15 | def __init__(self):
16 | super(ObjectInspector, self).__init__()
17 | self.layout = QtWidgets.QGridLayout(self)
18 |
19 | self.setMinimumWidth(900)
20 | self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
21 |
22 | self.treeview = QtWidgets.QTreeView()
23 | self.treemodel = QtGui.QStandardItemModel()
24 | self.treeview.setModel(self.treemodel)
25 | self.treeview.setUniformRowHeights(True)
26 |
27 | self.layout.addWidget(self.treeview)
28 |
29 | self.treemodel.setHorizontalHeaderLabels(['object name',
30 | 'object'])
31 |
32 | self.treeview.header().setStretchLastSection(False)
33 | mode = QtWidgets.QHeaderView.ResizeToContents
34 | self.treeview.header().setResizeMode(mode)
35 |
36 | self.load_globals()
37 | self.start_timer()
38 |
39 | def load_globals(self):
40 | """
41 | Load globals into Tree
42 | """
43 | self.treemodel.removeRows(0, self.treemodel.rowCount())
44 |
45 | self.names = __dict__.copy()
46 |
47 | rootItem = self.treemodel.invisibleRootItem()
48 | for key, value in __dict__.iteritems():
49 | # if hasattr(value, '__repr__'):
50 | try:
51 | items = [QtGui.QStandardItem(i.__repr__())
52 | for i in [key, value]]
53 | rootItem.appendRow(items)
54 | except Exception as e:
55 | print(key, value, e)
56 |
57 | def start_timer(self):
58 | """
59 | Starts timer for
60 | self.check_update_globals
61 | """
62 | self.timer = QtCore.QTimer()
63 | self.timer.setInterval(100)
64 | self.timer.timeout.connect(self.check_update_globals)
65 | self.timer.start()
66 |
67 | def check_update_globals(self):
68 | """
69 | Timer that checks len() of
70 | __main__.__dict__
71 | and updates globals on new items.
72 | """
73 | if not self.names == __dict__:
74 | self.load_globals()
75 |
76 | def open_in_external_editor(self):
77 | """
78 | Like sublime's "Go to"
79 | Open file in which object was defined.
80 | TODO: Would be excellent to jump to line.
81 | """
82 | pass
83 |
--------------------------------------------------------------------------------
/scripts/prototypes/tab_search.py:
--------------------------------------------------------------------------------
1 | # replace the Tab Choice menu on the far right with a search box
2 | from __future__ import absolute_import
3 | from __future__ import print_function
4 | from Qt import QtWidgets, QtGui, QtCore
5 | from six.moves import range
6 |
7 |
8 | class TabComboList(QtWidgets.QComboBox):
9 | def __init__(self, names=[], tabs=None):
10 | super(TabComboList, self).__init__()
11 | self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
12 | self.setInsertPolicy(self.NoInsert)
13 | self.setEditable(True)
14 | self._names = names
15 | self.addItems(names)
16 | self.tabs = tabs
17 | #self._comp = QtWidgets.QCompleter(names)
18 | #self.setCompleter(self._comp)
19 | #self.editingFinished.connect(self.close)
20 | #self._comp.highlighted.connect(self.emit_list_index)
21 | self.currentIndexChanged.connect(self.emit_list_index)
22 | self.activated.connect(self.handle_activated)
23 | #cpl = self.completer()
24 | #cpl.highlighted.connect(self.handle_activated)
25 | #self.setCompleter(cpl)
26 |
27 | def emit_list_index(self, index):
28 | print(index)
29 | print(self._names[index])
30 |
31 | def handle_activated(self, index):
32 | print('activated', index)
33 | self.tabs.tabs.setCurrentIndex(index)
34 |
35 | def keyPressEvent(self, event):
36 | if event.key() in [
37 | #QtCore.Qt.Key_Return,
38 | #QtCore.Qt.Key_Enter,
39 | QtCore.Qt.Key_Escape
40 | ]:
41 | self.close()
42 | super(TabComboList, self).keyPressEvent(event)
43 |
44 | def focusOutEvent(self, event):
45 | super(TabComboList, self).focusOutEvent(event)
46 | comp = self.completer()
47 | popup = comp.popup()
48 | if popup is not None:
49 | if not popup.isVisible():
50 | self.close()
51 |
52 |
53 | def show_tab_menu(self):
54 | names = [
55 | self.tabs.tabText(i)
56 | for i in range(self.tabs.count())
57 | ]
58 | if hasattr(self, '_tab_combo_list'):
59 | tcl = self._tab_combo_list
60 | if tcl.isVisible():
61 | tcl.close()
62 | return
63 | tcl = self._tab_combo_list = TabComboList(names, self)
64 | tcl.show()
65 | b = self.tab_list_button
66 | g = b.geometry()
67 | p = b.pos()
68 | p = g.topLeft()
69 | p = self.mapToGlobal(p)
70 |
71 | tcl.resize(200, tcl.height())
72 | tcl.move(p-QtCore.QPoint(200, 0))
73 | tcl.raise_()
74 | tcl.setFocus(QtCore.Qt.MouseFocusReason)
75 |
76 |
77 | from functools import partial
78 | self = _ide.python_editor.tabeditor
79 | self.show_tab_menu = partial(show_tab_menu, self)
80 | #tab_list_button.
81 |
--------------------------------------------------------------------------------
/PythonEditor/utils/search.py:
--------------------------------------------------------------------------------
1 |
2 | def nonconsec_find(needle, haystack, anchored=False):
3 | """checks if each character of "needle" can be
4 | found in order (but not
5 | necessarily consecutivly) in haystack.
6 | For example, "mm" can be found in "matchmove",
7 | but not "move2d"
8 | "m2" can be found in "move2d", but not "matchmove"
9 |
10 | >>> nonconsec_find("m2", "move2d")
11 | True
12 | >>> nonconsec_find("m2", "matchmove")
13 | False
14 |
15 | Anchored ensures the first letter matches
16 |
17 | >>> nonconsec_find(
18 | "atch", "matchmove", anchored = False)
19 | True
20 | >>> nonconsec_find(
21 | "atch", "matchmove", anchored = True)
22 | False
23 | >>> nonconsec_find(
24 | "match", "matchmove", anchored = True)
25 | True
26 |
27 | If needle starts with a string,
28 | non-consecutive searching is disabled:
29 |
30 | >>> nonconsec_find(
31 | " mt", "matchmove", anchored = True)
32 | False
33 | >>> nonconsec_find(
34 | " ma", "matchmove", anchored = True)
35 | True
36 | >>> nonconsec_find(
37 | " oe", "matchmove", anchored = False)
38 | False
39 | >>> nonconsec_find(
40 | " ov", "matchmove", anchored = False)
41 | True
42 | """
43 |
44 | # if "[" not in needle:
45 | # haystack = haystack.rpartition(" [")[0]
46 |
47 | if len(haystack) == 0 and len(needle) > 0:
48 | # "a" is not in ""
49 | return False
50 |
51 | elif len(needle) == 0 and len(haystack) > 0:
52 | # "" is in "blah"
53 | return True
54 |
55 | elif len(needle) == 0 and len(haystack) == 0:
56 | # ..?
57 | return True
58 |
59 | # Turn haystack into list of characters
60 | # (as strings are immutable)
61 | haystack = [hay for hay in str(haystack)]
62 |
63 | if needle.startswith(" "):
64 | # "[space]abc" does consecutive
65 | # search for "abc" in "abcdef"
66 | if anchored:
67 | if "".join(
68 | haystack).startswith(
69 | needle.lstrip(" ")):
70 | return True
71 | else:
72 | if needle.lstrip(" ") in "".join(haystack):
73 | return True
74 |
75 | if anchored:
76 | if needle[0] != haystack[0]:
77 | return False
78 | else:
79 | # First letter matches, remove it
80 | # for further matches
81 | needle = needle[1:]
82 | del haystack[0]
83 |
84 | for needle_atom in needle:
85 | try:
86 | needle_pos = haystack.index(needle_atom)
87 | except ValueError:
88 | return False
89 | else:
90 | # Dont find string in same pos or
91 | # backwards again
92 | del haystack[:needle_pos + 1]
93 | return True
94 |
--------------------------------------------------------------------------------
/scripts/prototypes/shortcutthief.py:
--------------------------------------------------------------------------------
1 | from Qt import QtWidgets, QtGui, QtCore
2 | from PythonEditor.utils import eventfilters
3 |
4 |
5 | def key_to_shortcut(key):
6 | """
7 | Convert the given QtCore.Qt.Key type
8 | to a string including currently held modifiers.
9 | """
10 | modifier_map = {
11 | QtCore.Qt.Key_Control: QtCore.Qt.ControlModifier,
12 | QtCore.Qt.Key_Shift: QtCore.Qt.ShiftModifier,
13 | QtCore.Qt.Key_Alt: QtCore.Qt.AltModifier,
14 | QtCore.Qt.Key_Meta: QtCore.Qt.MetaModifier,
15 | }
16 | held = QtWidgets.QApplication.keyboardModifiers()
17 | combo = 0
18 | for mod in modifier_map.values():
19 | if held & mod == mod:
20 | combo |= mod
21 | combo |= key
22 |
23 | #print combo
24 | return combo
25 | shortcut = QtGui.QKeySequence(combo).toString()
26 | try:
27 | shortcut = str(shortcut)
28 | except UnicodeEncodeError:
29 | shortcut = repr(shortcut)
30 | return shortcut
31 |
32 |
33 |
34 | # from PythonEditor. useful!
35 | def remove_panel(PANEL_NAME):
36 | for stack in QtWidgets.QApplication.instance().allWidgets():
37 | if not isinstance(stack, QtWidgets.QStackedWidget):
38 | continue
39 | for child in stack.children():
40 | if child.objectName() == PANEL_NAME:
41 | child.deleteLater()
42 |
43 |
44 | class ShortcutThief(eventfilters.GenericEventFilter):
45 | def event_filter(self, obj, event):
46 | if event.type() == event.Shortcut:
47 | print('mine!')
48 | key = event.key()
49 | print(key)
50 | print(key)_to_shortcut(key)
51 | if obj != self.parent():
52 | event.ignore()
53 | return True
54 | return False
55 | return False
56 |
57 |
58 | class Edits(QtWidgets.QPlainTextEdit):
59 | def focusInEvent(self, event):
60 | self.thief = ShortcutThief()
61 | self.thief.setParent(self)
62 | print(self.thief)
63 | super(Edits, self).focusInEvent(event)
64 |
65 | def focusOutEvent(self, event):
66 | self.thief.quit()
67 | super(Edits, self).focusOutEvent(event)
68 |
69 | def closeEvent(self, event):
70 | self.thief.quit()
71 | super(Edits, self).closeEvent(event)
72 |
73 |
74 |
75 |
76 | # test floating panel
77 | e = Edits()
78 | e.show()
79 |
80 | def edit_func():
81 | print('EDITOR!')
82 |
83 | a = QtWidgets.QAction(e)
84 | a.triggered.connect(edit_func)
85 | s = QtGui.QKeySequence('Ctrl+S')
86 | a.setShortcut(s)
87 | a.setShortcutContext(QtCore.Qt.WidgetShortcut)
88 | e.addAction(a)
89 |
90 | #&& # test docked panel
91 | remove_panel('edits')
92 | panel = nukescripts.registerWidgetAsPanel2(
93 | 'Edits', 'edits', 'edits', create=True
94 | )
95 | dock = nuke.getPaneFor('Viewer.1')
96 | panel.addToPane(dock)
--------------------------------------------------------------------------------
/PythonEditor/models/xmlmodel.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from PythonEditor.ui.Qt.QtCore import (
4 | Qt, QMimeData, Signal)
5 | from PythonEditor.ui.Qt.QtGui import (
6 | QStandardItem, QStandardItemModel)
7 | from PythonEditor.ui.features import autosavexml
8 | from xml.etree import cElementTree as ETree
9 |
10 |
11 | class XMLModel(QStandardItemModel):
12 | def __init__(self):
13 | super(XMLModel, self).__init__()
14 | self.load_xml()
15 | self.itemChanged.connect(self.store_data)
16 |
17 | def load_xml(self):
18 | root, elements = autosavexml.parsexml('subscript')
19 | for element in elements:
20 | item1 = QStandardItem(element.attrib['name'])
21 | item2 = QStandardItem(element.text)
22 | item3 = QStandardItem(element.attrib['uuid'])
23 | self.appendRow([item1, item2, item3])
24 |
25 | def store_data(self, item):
26 | text = item.text()
27 | element = ETree.Element('subscript')
28 | element.text = text
29 |
30 | def flags(self, index):
31 | """
32 | https://www.qtcentre.org/threads/23258-How-to-reorder-items-in-QListView
33 | You still want the root index to accepts
34 | drops, just not the items.
35 | This will work if the root index is
36 | invalid (as it usually is). However,
37 | if you use setRootIndex you may have
38 | to compare against that index instead.
39 | """
40 | if index.isValid():
41 | return (
42 | Qt.ItemIsSelectable
43 | |Qt.ItemIsEditable
44 | |Qt.ItemIsDragEnabled
45 | |Qt.ItemIsEnabled
46 | )
47 |
48 | return (
49 | Qt.ItemIsSelectable
50 | |Qt.ItemIsDragEnabled
51 | |Qt.ItemIsDropEnabled
52 | |Qt.ItemIsEnabled
53 | )
54 |
55 | def mimeTypes(self):
56 | return ['text/json']
57 |
58 | def mimeData(self, indexes):
59 | data = {}
60 | row = indexes[0].row()
61 | data['name'] = self.item(row, 0).text()
62 | data['text'] = self.item(row, 1).text()
63 | data['uuid'] = self.item(row, 2).text()
64 | data['row'] = row
65 | dragData = json.dumps(data, indent=2)
66 | mimeData = QMimeData()
67 | mimeData.setData('text/json', dragData)
68 | return mimeData
69 |
70 | def stringList(self):
71 | """ List of document names.
72 | """
73 | names = []
74 | for i in range(self.rowCount()):
75 | name = self.item(i,0).text()
76 | print(name)
77 | names.append(name)
78 | return names
79 |
80 | row_moved = Signal(int, int)
81 | def dropMimeData(self, data, action, row, column, parent=None):
82 | dropData = json.loads(bytes(data.data('text/json')))
83 | take_row = dropData['row']
84 | items = self.takeRow(take_row)
85 | if take_row < row:
86 | row -= 1
87 | elif row == -1:
88 | row = self.rowCount()
89 | print(take_row, '->', row)
90 | self.insertRow(row, items)
91 | self.row_moved.emit(take_row, row)
92 | return True
93 |
--------------------------------------------------------------------------------
/PythonEditor/ui/tabview.py:
--------------------------------------------------------------------------------
1 | """ The TabView class is designed to represent
2 | a one-dimensional array of data. It behaves like
3 | a QListView and is meant to work in sync with other
4 | views. The TabView class is data-agnostic. All methods
5 | to do with the saving of data are implemented
6 | on the model.
7 | """
8 | from PythonEditor.ui.Qt.QtWidgets import QTabBar, QListView
9 | from PythonEditor.ui.Qt.QtCore import Slot
10 | from PythonEditor.ui.Qt.QtGui import QStandardItemModel, QStandardItem
11 |
12 |
13 | def ismodel(model):
14 | """ Type check for QStandardItemModel. """
15 | return isinstance(model, QStandardItemModel)
16 |
17 |
18 | class TabView(QTabBar):
19 | def __init__(self, parent=None):
20 | super(TabView, self).__init__(parent=parent)
21 | self.setTabsClosable(True)
22 | self.setMovable(True)
23 |
24 | def clear(self):
25 | for i in reversed(range(self.count())):
26 | self.tabBar.removeTab(i)
27 |
28 | def update_from_model(self):
29 | for i in range(self._model.rowCount()):
30 | item = self._model.item(i,0)
31 | text = item.text()
32 | self.addTab(text)
33 |
34 | _model = None
35 | def setModel(self, model):
36 | if not ismodel(model):
37 | raise TypeError(
38 | "setModel(model) argument must "
39 | "be a {!r}, not '{!r}'".format(
40 | QStandardItemModel, type(model))
41 | )
42 | m = self.model()
43 | if m is not None:
44 | m.itemChanged.disconnect(
45 | self.tab_name_changed
46 | )
47 |
48 | self._model = model
49 | model.itemChanged.connect(
50 | self.tab_name_changed
51 | )
52 | self.tabMoved.connect(
53 | self.update_model_order
54 | )
55 | self.update_from_model()
56 |
57 | def model(self):
58 | return self._model
59 |
60 | _selection_model = None
61 | def selectionModel(self):
62 | return self._selection_model
63 |
64 | def setSelectionModel(self, model):
65 | self._selection_model = model
66 |
67 | model_moving = False
68 | @Slot(int, int)
69 | def update_model_order(self, _from, to):
70 | if self.model_moving:
71 | self.model_moving = False
72 | return
73 | m = self.model()
74 | m.insertRow(to, m.takeRow(_from))
75 |
76 | @Slot(QStandardItem)
77 | def tab_name_changed(self, item):
78 | index = item.index()
79 | # name column is 0
80 | if index.column() != 0:
81 | return
82 | self.setTabText(
83 | index.row(),
84 | item.text()
85 | )
86 |
87 | @Slot(int, int)
88 | def swap_tabs(self, _from, to):
89 | self.model_moving = True
90 | self.moveTab(_from, to)
91 | #from functools import partial
92 | #model_moved = partial(setattr, self, 'model_moving', False)
93 | #QTimer.singleShot(100, model_moved)
94 |
95 |
96 | class ListView(QListView):
97 | def __init__(self):
98 | super(ListView, self).__init__()
99 | self.setDragDropMode(QListView.InternalMove)
100 | # self.setMovement(QListView.Snap)
101 | # self.setAcceptsDrops(True)
102 |
103 | @Slot(int, int)
104 | def row_moved(self, _from, to):
105 | self.setCurrentIndex(self.model().index(to, 0))
106 |
107 | def setModel(self, model):
108 | super(ListView, self).setModel(model)
109 | model.row_moved.connect(self.row_moved)
110 |
111 |
--------------------------------------------------------------------------------
/PythonEditor/utils/eventfilters.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import traceback
3 | from PythonEditor.ui.Qt import QtCore, QtWidgets
4 |
5 | qApp = QtWidgets.QApplication.instance
6 | CTRL = QtCore.Qt.ControlModifier
7 |
8 |
9 | def full_stack():
10 | """
11 | Print full stack information from error within try/except block.
12 | """
13 | exc = sys.exc_info()[0]
14 | if exc is not None:
15 | f = sys.exc_info()[-1].tb_frame.f_back
16 | stack = traceback.extract_stack(f)
17 | else:
18 | stack = traceback.extract_stack()[:-1]
19 | # last one would be full_stack()
20 | trc = 'Traceback (most recent call last):\n'
21 | stackstr = trc + ''.join(traceback.format_list(stack))
22 | if exc is not None:
23 | stackstr += ' ' + traceback.format_exc().lstrip(trc)
24 | return stackstr
25 |
26 |
27 | class PySingleton(object):
28 | """
29 | Return a single instance of a class
30 | or create a new instance if none exists.
31 | """
32 | def __new__(cls, *args, **kwargs):
33 | if '_the_instance' not in cls.__dict__:
34 | cls._the_instance = object.__new__(cls)
35 | return cls._the_instance
36 |
37 |
38 | # TODO: make this a singleton? It might have to be a
39 | # singleton object that the QObject lives on.
40 | class GenericEventFilter(QtCore.QObject):
41 | """
42 | Generic EventFilter
43 | Implements safe filtering with auto-installation
44 | and autoquit with full stack trace on error.
45 |
46 | Example Usage:
47 |
48 | from PythonEditor.utils.eventfilters import GenericEventFilter
49 | class Filt(GenericEventFilter):
50 | def event_filter(self, obj, event):
51 | 1/0 # cause error
52 | return False # if we have not handled the event
53 |
54 | filt = Filt(target=QtWidgets.QApplication.instance())
55 | """
56 | def __init__(self, target=None):
57 | super(GenericEventFilter, self).__init__()
58 | self.setObjectName('GenericEventFilter')
59 |
60 | if target is None:
61 | target = QtWidgets.QApplication.instance()
62 |
63 | self.target = target
64 | self.install()
65 |
66 | def install(self):
67 | QtCore.QCoreApplication.installEventFilter(self.target, self)
68 |
69 | def eventFilter(self, obj, event):
70 | try:
71 | result = self.event_filter(obj, event)
72 | if result not in [True, False]:
73 | raise Exception('result is not True or False')
74 | return result
75 | except Exception:
76 | self.quit()
77 | print(full_stack())
78 | return True
79 | else:
80 | return False
81 |
82 | def event_filter(self, obj, event):
83 | """
84 | The method to be overridden when subclassing.
85 | """
86 | return False
87 |
88 | def quit(self):
89 | print(self.__class__, 'exiting')
90 | self.silent_quit()
91 |
92 | def remove_event_filter(self):
93 | QtCore.QCoreApplication.removeEventFilter(self.target, self)
94 |
95 | def silent_quit(self):
96 | self.remove_event_filter()
97 | self.deleteLater()
98 |
99 |
100 | class InfoFilter(GenericEventFilter):
101 | """
102 | Example Filter that prints object and event information.
103 | """
104 | def event_filter(self, obj, event):
105 | print(obj.metaObject().className(), event.type())
106 | return False
107 |
108 |
109 | if __name__ == '__main__':
110 | app = QtWidgets.QApplication(sys.argv)
111 | w = QtWidgets.QWidget()
112 | f = GenericEventFilter(target=w)
113 | w.show()
114 | app.exec_()
115 |
--------------------------------------------------------------------------------
/PythonEditor/app/nukefeatures/nodepanels.py:
--------------------------------------------------------------------------------
1 | from functools import partial
2 | import nuke
3 | from PythonEditor.ui.Qt import QtWidgets, QtCore
4 | from PythonEditor.ui import editor
5 |
6 |
7 | class PyKnobs(QtWidgets.QWidget):
8 | def __init__(self, knobs):
9 | super(PyKnobs, self).__init__()
10 | self.knobs = knobs
11 | self._knob = knobs[0]
12 | self.layout = QtWidgets.QVBoxLayout(self)
13 |
14 | self.knobChooser = QtWidgets.QComboBox()
15 | self.knobChooser.addItems([k.name() for k in knobs])
16 | self.editor = PyKnobEdit(self._knob)
17 |
18 | self.layout.addWidget(self.knobChooser)
19 | self.layout.addWidget(self.editor)
20 |
21 | self.knobChooser.currentIndexChanged.connect(self.updateKnob)
22 |
23 | def updateKnob(self, index):
24 | self.editor.knob = self.knobs[index]
25 | self.editor.getKnobValue()
26 |
27 |
28 | class PyKnobEdit(editor.Editor):
29 | """ Editor that automatically updates knobs.
30 | """
31 | def __init__(self, knob):
32 | super(PyKnobEdit, self).__init__()
33 | self._knob = knob
34 | self.text_changed_signal.connect(self.updateValue)
35 | self.editingFinished.connect(self.updateKnobChanged)
36 |
37 | def updateValue(self):
38 | if self._knob.name() != 'knobChanged':
39 | self._knob.setValue(self.toPlainText())
40 |
41 | def updateKnobChanged(self):
42 | self._knob.setValue(self.toPlainText())
43 |
44 | def hideEvent(self, event):
45 | super(PyKnobEdit, self).hideEvent(event)
46 |
47 | @property
48 | def knob(self):
49 | return self._knob
50 |
51 | @knob.setter
52 | def knob(self, knob):
53 | self._knob = knob
54 |
55 | def getKnobValue(self):
56 | self.setPlainText(self.knob.value())
57 |
58 |
59 | @QtCore.Slot(object)
60 | def addTextKnobs(node):
61 | """ Finds node panel widget and adds some
62 | extra widgets to it that act like knobs.
63 | TODO: find node panels in Properties bin.
64 | also appears that this causes segmentation faults
65 | probably because of pointers to missing or already
66 | deleted widgets.
67 | """
68 | print(node.name())
69 |
70 | np_list = [w for w in QtWidgets.QApplication.instance().allWidgets()
71 | if w.objectName() == node.name()]
72 | if len(np_list) > 0:
73 | np = np_list.pop()
74 | else:
75 | return
76 |
77 | sw = np.findChild(QtWidgets.QStackedWidget, 'qt_tabwidget_stackedwidget')
78 | print(sw)
79 | tw = sw.parent()
80 | pyk = PyKnobs([k for k in node.allKnobs() if 'py' in k.Class().lower()])
81 | tw.addTab(pyk, 'Python Knobs')
82 |
83 | # stw = QtWidgets.QTabWidget() #TODO: probably nicer to have a dropdown connected to a single textedit
84 | # tw.addTab(stw, 'Python Knobs')
85 |
86 | # for k in node.allKnobs():
87 | # if 'py' in k.Class().lower():
88 | # stw.addTab(PyKnobEdit(k), k.name())
89 |
90 |
91 | def pythonKnobEdit():
92 | if nuke.thisKnob().name() == 'showPanel': # TODO: is there a 'knob added' knobchanged?
93 | node = nuke.thisNode()
94 | global timer
95 | timer = QtCore.QTimer()
96 | timer.setSingleShot(True)
97 | timer.setInterval(10)
98 | timer.timeout.connect(partial(addTextKnobs, node))
99 | timer.start()
100 |
101 |
102 | testing = """
103 | from PythonEditor.app.nukefeatures import nodepanels
104 | reload(nodepanels)
105 |
106 | #to delete
107 | del nuke.callbacks.knobChangeds['*'][1]
108 | """
109 |
110 | knobChangeds = nuke.callbacks.knobChangeds['*']
111 | for index, info in enumerate(knobChangeds):
112 | func, _, _, _ = info
113 | if func.func_name == 'pythonKnobEdit':
114 | del knobChangeds[index]
115 |
116 | nuke.callbacks.addKnobChanged(
117 | pythonKnobEdit,
118 | args=(),
119 | kwargs={},
120 | nodeClass='*',
121 | node=None
122 | )
123 |
--------------------------------------------------------------------------------
/PythonEditor/ui/dialogs/popups.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from PythonEditor.ui.Qt import QtWidgets
4 | from PythonEditor.ui.Qt import QtGui
5 | from PythonEditor.ui.Qt import QtCore
6 |
7 | from PythonEditor.utils.goto import goto_line
8 |
9 |
10 | class CommandPalette(QtWidgets.QLineEdit):
11 | """
12 | The base class for a LineEdit widget that
13 | appears over a parent widget and can be
14 | used for entering commands, searching, etc.
15 | """
16 | location = QtCore.Qt.BottomSection
17 | location = QtCore.Qt.TopSection
18 | def __init__(self, parent=None):
19 | super(CommandPalette, self).__init__()
20 | self._parent = parent
21 | self.setWindowFlags(
22 | QtCore.Qt.WindowStaysOnTopHint
23 | | QtCore.Qt.FramelessWindowHint
24 | )
25 | self.editingFinished.connect(self.hide)
26 | font = self.font()
27 | font.setPointSize(12)
28 | font.setBold(False)
29 | self.setFont(font)
30 | self.place_over_parent()
31 |
32 | def parent(self):
33 | return self._parent
34 |
35 | def keyPressEvent(self, event):
36 | esc = QtCore.Qt.Key.Key_Escape
37 | if event.key() == esc:
38 | self.hide()
39 | super(
40 | CommandPalette, self
41 | ).keyPressEvent(event)
42 |
43 | def showEvent(self, event):
44 | self.parent().installEventFilter(self)
45 | self.setFocus(QtCore.Qt.MouseFocusReason)
46 | if event.type() != QtCore.QEvent.Show:
47 | return
48 | super(CommandPalette, self).showEvent(event)
49 | self.place_over_parent()
50 |
51 | def hideEvent(self, event):
52 | self.parent().removeEventFilter(self)
53 | if event.type() != QtCore.QEvent.Hide:
54 | return
55 | super(CommandPalette, self).hideEvent(event)
56 | self.parent().setFocus(QtCore.Qt.MouseFocusReason)
57 |
58 | def place_over_parent(self):
59 | if self.location == QtCore.Qt.TopSection:
60 | self.move_to_top()
61 | elif self.location == QtCore.Qt.BottomSection:
62 | self.move_to_bottom()
63 |
64 | def move_to_top(self):
65 | geo = self.parent().geometry()
66 | centre = geo.center()
67 | x = centre.x()-(self.width()/2)
68 | y = geo.top()-12
69 | pos = QtCore.QPoint(x, y)
70 | pos = self.parent().mapToGlobal(pos)
71 | self.move(pos)
72 |
73 | def move_to_bottom(self):
74 | geo = self.parent().geometry()
75 | centre = geo.center()
76 | x = centre.x()-(self.width()/2)
77 | y = geo.bottom()-70
78 | pos = QtCore.QPoint(x, y)
79 | pos = self.parent().mapToGlobal(pos)
80 | self.move(pos)
81 |
82 | def eventFilter(self, obj, event):
83 | if event.type() == QtCore.QEvent.Move:
84 | self.place_over_parent()
85 | elif event.type() == QtCore.QEvent.Resize:
86 | self.place_over_parent()
87 | elif event.type() == QtCore.QEvent.Hide:
88 | self.hide()
89 | return False
90 |
91 |
92 | class GotoPalette(CommandPalette):
93 | def __init__(self, editor):
94 | super(GotoPalette, self).__init__(editor)
95 | self.editor = editor
96 | self.setPlaceholderText('enter line number')
97 | self.current_line = editor.textCursor(
98 | ).block(
99 | ).blockNumber()+1
100 | # self.setText(str(self.current_line))
101 |
102 | def keyPressEvent(self, event):
103 | esc = QtCore.Qt.Key.Key_Escape
104 | if event.key() == esc:
105 | goto_line(self.editor, self.current_line)
106 | self.hide()
107 |
108 | if event.text().isalpha():
109 | return
110 |
111 | super(GotoPalette, self).keyPressEvent(event)
112 | try:
113 | lineno = int(self.text())
114 | except ValueError:
115 | return
116 | goto_line(self.editor, lineno)
117 |
--------------------------------------------------------------------------------
/scripts/prototypes/new_autocomplete.py:
--------------------------------------------------------------------------------
1 | from Qt import QtCore, QtGui, QtWidgets
2 |
3 | key_list = {}
4 | for k in dir(QtCore.Qt):
5 | try:
6 | key_list[int(getattr(QtCore.Qt, k))] = k
7 | except TypeError:
8 | print(k)
9 |
10 | class Editor(QtWidgets.QPlainTextEdit):
11 | block_key_press = False
12 | key_pressed_signal = QtCore.Signal(QtGui.QKeyEvent)
13 |
14 | def __init__(self):
15 | super(Editor, self).__init__()
16 | self._completer = Completer(self)
17 |
18 | def keyPressEvent(self, event):
19 |
20 | self.key_pressed_signal.emit(event)
21 | if self.block_key_press:
22 | return
23 |
24 | super(Editor, self).keyPressEvent(event)
25 | self.block_key_press = False
26 |
27 |
28 |
29 | class Completer(QtWidgets.QCompleter):
30 | def __init__(self, editor):
31 | super(Completer, self).__init__([])
32 |
33 | self.setCompletionMode(QtWidgets.QCompleter.PopupCompletion)
34 | self.setCaseSensitivity(QtCore.Qt.CaseSensitive)
35 |
36 | self.editor = editor
37 | self.editor.key_pressed_signal.connect(self.key_press_event)
38 | self.setParent(self.editor)
39 | self.setWidget(self.editor)
40 | self.activated.connect(self.insert_completion)
41 | print(globals().keys())
42 | self.set_list(globals().keys())
43 |
44 |
45 | def set_list(self, stringlist):
46 | """
47 | Sets the list of completions.
48 | """
49 | qslm = QtCore.QStringListModel()
50 | qslm.setStringList(stringlist)
51 | self.setModel(qslm)
52 |
53 | def word_under_cursor(self):
54 | """
55 | Returns a string with the word under the cursor.
56 | """
57 | textCursor = self.editor.textCursor()
58 | textCursor.select(QtGui.QTextCursor.LineUnderCursor)
59 | #textCursor.select(QtGui.QTextCursor.WordUnderCursor)
60 | word = textCursor.selection().toPlainText()
61 | return word
62 |
63 | def insert_completion(self, text):
64 | print(text)
65 |
66 | def show_popup(self):
67 | """
68 | Show the completer list.
69 | """
70 | cursorRect = self.editor.cursorRect()
71 | pop = self.popup()
72 | cursorRect.setWidth(pop.sizeHintForColumn(0)
73 | + pop.verticalScrollBar().sizeHint().width())
74 | self.complete(cursorRect)
75 |
76 | def key_press_event(self, event):
77 |
78 | print(k)ey_list[int(event.key())]
79 | complete_keys = [
80 | QtCore.Qt.Key_Enter,
81 | QtCore.Qt.Key_Return,
82 | QtCore.Qt.Key_Escape,
83 | QtCore.Qt.Key_Tab,
84 | QtCore.Qt.Key_Backtab,
85 | QtCore.Qt.Key_Meta,
86 | ]
87 | if event.key() in (complete_keys):
88 |
89 | if self.popup() and self.popup().isVisible():
90 | #event.ignore() # ? necessary?
91 | pass
92 | return
93 |
94 | not_alnum_or_mod = (not str(event.text()).isalnum()
95 | and event.modifiers() == QtCore.Qt.NoModifier)
96 |
97 | zero_completions = self.completionCount() == 0
98 | if not_alnum_or_mod or zero_completions:
99 | self.popup().hide()
100 | return
101 |
102 | current_word = self.word_under_cursor()+event.text()
103 | print('Current word:', current_word)
104 | self.setCompletionPrefix(current_word)
105 | self.popup().setCurrentIndex(self.completionModel().index(0, 0))
106 | self.show_popup()
107 |
108 | #if event.text() == 'f':
109 | #self.editor.block_key_press = True
110 |
111 | e = Editor()
112 | e.show()
113 |
--------------------------------------------------------------------------------
/scripts/prototypes/shorcut_detector.py:
--------------------------------------------------------------------------------
1 | # THIS is the implementation I should use
2 | from Qt import QtWidgets, QtGui, QtCore, QtTest
3 | #import nukescripts
4 | import json
5 | import time
6 | from PythonEditor.ui.features import actions
7 | reload(actions)
8 |
9 |
10 | def get_action_dict():
11 |
12 | json_path = 'C:/Repositories/PythonEditor/PythonEditor/ui/features/action_register.json'
13 |
14 | with open(json_path, 'r') as f:
15 | action_dict = json.load(f)
16 |
17 | #print json.dumps(action_dict, indent=2)
18 | return action_dict
19 |
20 |
21 | def get_shortcuts():
22 | action_dict = get_action_dict()
23 | shortcuts = []
24 | for widget_name, widget_actions in action_dict.items():
25 | for name, attributes in widget_actions.items():
26 | strokes = attributes['Shortcuts']
27 | for shortcut in strokes:
28 | shortcuts.append(str(shortcut))
29 | return shortcuts
30 |
31 | def get_actions(widget_name):
32 | action_dict = get_action_dict()
33 | actions = {}
34 | widget_actions = action_dict[widget_name]
35 | for name, attributes in widget_actions.items():
36 | strokes = attributes['Shortcuts']
37 | method = attributes['Method']
38 | for shortcut in strokes:
39 | actions[shortcut] = method
40 | return actions
41 |
42 |
43 | def key_to_shortcut(key):
44 | """
45 | Convert the given QtCore.Qt.Key type
46 | to a string including currently held modifiers.
47 | """
48 | modifier_map = {
49 | QtCore.Qt.Key_Control: QtCore.Qt.ControlModifier,
50 | QtCore.Qt.Key_Shift: QtCore.Qt.ShiftModifier,
51 | QtCore.Qt.Key_Alt: QtCore.Qt.AltModifier,
52 | QtCore.Qt.Key_Meta: QtCore.Qt.MetaModifier,
53 | }
54 | held = QtWidgets.QApplication.keyboardModifiers()
55 | #held_keys = tuple(
56 | #mod for mod in modifier_map.values()
57 | #if held & mod == mod
58 | #)
59 | #combo = 0
60 | #for mod in held_keys:
61 | #combo |= mod
62 | #combo |= key
63 |
64 | combo = 0
65 | for mod in modifier_map.values():
66 | if held & mod == mod:
67 | combo |= mod
68 | combo |= key
69 |
70 | combo = QtGui.QKeySequence(combo).toString()
71 | try:
72 | combo = str(combo)
73 | except UnicodeEncodeError:
74 | combo = repr(combo)
75 | return combo
76 |
77 |
78 |
79 |
80 |
81 | class KeyCatcher(QtWidgets.QPlainTextEdit):
82 | shortcut_signal = QtCore.Signal(unicode)
83 | def __init__(self):
84 | super(KeyCatcher, self).__init__()
85 | self.autocomplete_overriding = False
86 |
87 | def keyPressEvent(self, event):
88 | shortcut = key_to_shortcut(event.key())
89 | self.shortcut_handled = False
90 | self.shortcut_signal.emit(shortcut)
91 | if self.shortcut_handled:
92 | return
93 | super(KeyCatcher, self).keyPressEvent(event)
94 |
95 |
96 | class Shortcuts(QtCore.QObject):
97 | def __init__(self, editor):
98 | super(Shortcuts, self).__init__()
99 | self.setParent(editor)
100 | self.editor = editor
101 | self.action_manager = editor.action_manager
102 | editor.shortcut_signal.connect(
103 | self.dispatch,
104 | QtCore.Qt.DirectConnection
105 | )
106 | self.lookup = get_actions('editor')
107 |
108 | def dispatch(self, shortcut):
109 | name = self.lookup.get(shortcut)
110 | if name is None:
111 | return
112 | method = getattr(self.action_manager, name)
113 | if method is None:
114 | return
115 | method.__call__()
116 | self.editor.shortcut_handled = True
117 |
118 |
119 | #&& # test 1
120 | k = KeyCatcher()
121 | ACTIONS = actions.Actions(editor=k)
122 | k.action_manager = ACTIONS
123 | k.shortcuts = Shortcuts(k)
124 | k.show()
125 |
126 |
127 | #&& # test 2
128 | for key in QtCore.Qt.Key.values.values():
129 | print(key_to_shortcut(key))
130 | #&&
131 |
--------------------------------------------------------------------------------
/scripts/prototypes/loaderlist.py:
--------------------------------------------------------------------------------
1 | from PythonEditor.ui.Qt import QtWidgets, QtGui, QtCore
2 | from PythonEditor.ui import edittabs
3 | from PythonEditor.ui import editor
4 | from PythonEditor.ui.features import autosavexml
5 |
6 | QtWidgets.QTabBar.setTabButton
7 | class LoaderList(QtWidgets.QListView): #WIP name
8 |
9 | emit_text = QtCore.Signal(str)
10 | emit_tab = QtCore.Signal(dict)
11 | def __init__(self):
12 | super(LoaderList, self).__init__()
13 | _model = QtGui.QStandardItemModel()
14 | self._model = QtGui.QStandardItemModel()
15 | self.setModel(self._model)
16 |
17 | def __setitem__(self, name, value):
18 | item = QtGui.QStandardItem(name)
19 | item.setData(value, role=QtCore.Qt.UserRole+1)
20 | self._model.appendRow(item)
21 |
22 | def selectionChanged(self, selected, deselected):
23 | #print selected, deselected
24 | for index in selected.indexes():
25 | item = self._model.item(index.row(), index.column())
26 | #print item
27 | a = item.data(QtCore.Qt.UserRole+1)
28 | print(a)
29 | #self.emit_text.emit(text)
30 | self.emit_tab.emit(a)
31 | #if index.column() == 0:
32 | super(LoaderList, self).selectionChanged(
33 | selected, deselected)
34 |
35 | class SingleTab(QtWidgets.QWidget):
36 | def __init__(self):
37 | super(SingleTab, self).__init__()
38 | l = LoaderList()
39 | self.l = l
40 | root, subscripts = autosavexml.parsexml('subscript')
41 | for s in subscripts:
42 | name = s.attrib.get('name')
43 | a = s.attrib.copy()
44 | a['text'] = s.text
45 | l[name] = a
46 |
47 | self.t = edittabs.EditTabs()
48 | self.l.emit_tab.connect(self.receive_tab)
49 |
50 | self._layout = QtWidgets.QHBoxLayout(self)
51 | self.splitter = QtWidgets.QSplitter(self)
52 | self._layout.addWidget(self.splitter)
53 | self.setLayout(self._layout)
54 |
55 | self.splitter.addWidget(self.l)
56 | self.splitter.addWidget(self.t)
57 |
58 |
59 |
60 | @QtCore.Slot(dict)
61 | def receive_tab(self, tab):
62 | # look for temp_tab and replace it if still temp
63 | editor = self.t.new_tab(tab_name=tab['name'])
64 | #editor = QtWidgets.QPlainTextEdit()
65 | #t.insertTab(0, temp_editor, tab['name'])
66 | text = tab['text']
67 | editor.path = tab.get('path')
68 | editor.uid = tab['uuid']
69 | editor.setPlainText(text)
70 | editor.temp_tab = True
71 |
72 | s = SingleTab()
73 | s.show()
74 | """
75 | #QtCore.QModelIndex.data(1, QtCore.Qt.UserRole)
76 | l = LoaderList()
77 | l.show()
78 |
79 | root, subscripts = autosavexml.parsexml('subscript')
80 | for s in subscripts:
81 | name = s.attrib.get('name')
82 | a = s.attrib.copy()
83 | a['text'] = s.text
84 | l[name] = a
85 |
86 | #e = editor.Editor()
87 | #e.show()
88 | ##l.emit_text.connect(e.setPlainText)
89 |
90 |
91 | @QtCore.Slot(dict)
92 | def receive_tab(tab):
93 | # look for temp_tab and replace it if still temp
94 | editor = t.new_tab(tab_name=tab['name'])
95 | #editor = QtWidgets.QPlainTextEdit()
96 | #t.insertTab(0, temp_editor, tab['name'])
97 | text = tab['text']
98 | editor.path = tab.get('path')
99 | editor.uid = tab['uuid']
100 | editor.setPlainText(text)
101 | editor.temp_tab = True
102 |
103 | t = edittabs.EditTabs()
104 | t.show()
105 | l.emit_tab.connect(receive_tab)
106 | """
107 |
108 |
109 | """
110 | w = QtWidgets.QWidget()
111 | l = QtWidgets.QVBoxLayout(w)
112 | s = QtWidgets.QSplitter(w)
113 | l.addWidget(s)
114 | w.show()
115 |
116 | d1 = QtWidgets.QDial()
117 | d2 = QtWidgets.QDial()
118 | s.addWidget(d1)
119 | s.addWidget(d2)
120 | """
121 |
122 |
123 | """
124 | t = edittabs.EditTabs()
125 | t.show()
126 |
127 |
128 | root, subscripts = autosavexml.parsexml('subscript')
129 | for s in subscripts:
130 | name = s.attrib.get('name')
131 | l[name] = s.text
132 | t.insertTab(0, e, name)
133 | #t.new_tab(tab_name=name)
134 | """
--------------------------------------------------------------------------------
/scripts/prototypes/code_coverage_feature.py:
--------------------------------------------------------------------------------
1 | # code coverage feature for pythoneditor - this would be AMAZING on a "record" button!
2 |
3 | # basically, all your integration testing now also becomes code coverage testing...
4 |
5 |
6 | import sys
7 |
8 |
9 | # def displayhook(value):
10 | # print('displayhook:', value)
11 | # sys.displayhook = displayhook
12 | # sys.displayhook = sys.__displayhook__
13 |
14 |
15 | def get_class_from_frame(fr):
16 | import inspect
17 | args, _, _, value_dict = inspect.getargvalues(fr)
18 | # we check the first parameter for the frame function is
19 | # named 'self'
20 | if len(args) and args[0] == 'self':
21 | # in that case, 'self' will be referenced in value_dict
22 | instance = value_dict.get('self', None)
23 | if instance:
24 | # return its class
25 | return getattr(instance, '__class__', None)
26 | # return None otherwise
27 | return None
28 |
29 |
30 | class Trace(object):
31 | def __init__(self, module_name):
32 | self.module_name = module_name
33 | self.results = set([])
34 | self.data = {}
35 |
36 | def coverage_trace(self, frame, event, arg):
37 | module_name = frame.f_globals.get('__name__')
38 | if not module_name.startswith(self.module_name):
39 | return
40 |
41 | namespace = self.data.setdefault(module_name, {})
42 | func_name = frame.f_code.co_name
43 | if func_name in ['', '']:
44 | return
45 |
46 | klass = get_class_from_frame(frame)
47 | if klass is not None:
48 | namespace = namespace.setdefault(klass.__name__, {})
49 | namespace.setdefault(func_name, {}) # TODO: put some args in this dict
50 |
51 | def _coverage_trace(self, frame, event, arg):
52 | module_name = frame.f_globals.get('__name__')
53 | if not module_name.startswith(self.module_name):
54 | return
55 | func_name = frame.f_code.co_name
56 | klass = get_class_from_frame(frame)
57 | if klass is not None:
58 | func_name = f'{klass.__name__}.{func_name}'
59 | path = f'{module_name}.{func_name}'
60 | if path not in self.results:
61 | print(path)
62 | self.results.add(path)
63 |
64 |
65 | def start(self):
66 | self.__enter__()
67 |
68 | def stop(self):
69 | self.__exit__(0,0,0)
70 |
71 | def __enter__(self):
72 | sys.settrace(self.coverage_trace)
73 | return self
74 |
75 | def __exit__(self, exception_type, exception_value, traceback):
76 | sys.settrace(None)
77 |
78 | def save(self, path=None):
79 | import json
80 | if path is None:
81 | folder = os.path.dirname(__file__)
82 | path = os.path.join(folder, 'coverage_output.json')
83 | with open(path, 'w') as fd:
84 | json.dump(trace.data, fd, indent=4)
85 |
86 |
87 | if __name__ == '__main__':
88 | # ('b'=='a')
89 | with Trace('PythonEditor') as trace:
90 | from PythonEditor.ui.ide import IDE
91 | ide = IDE()
92 | ide.show()
93 |
94 | # from PythonEditor.ui.editor import Editor
95 | # editor = Editor()
96 | # editor.show()
97 |
98 | # trace = Trace('PythonEditor')
99 | trace.start()
100 | trace.stop()
101 |
102 | # sys.gettrace()
103 | trace.results
104 | import json
105 | folder = os.path.dirname(__file__)
106 | path = os.path.join(folder, 'coverage_output.json')
107 |
108 | #&&
109 |
110 | with open(path, 'w') as fd:
111 | json.dump(trace.data, fd, indent=4)
112 | # json.dump([trace.data, list(trace.results)], fd, indent=2)
113 |
114 | #&&
115 | # import ast
116 | # ast.parse(
117 |
118 | package_dir = os.path.dirname(os.path.dirname(__file__))
119 | folder = os.path.join(package_dir, 'PythonEditor')
120 | for root, dirs, files in os.walk(folder):
121 | base = root.replace(package_dir+os.sep, '').replace(os.sep, '.')
122 | for filename in files:
123 | route = '.'.join([base, filename.replace('.py', '')])
124 | try:
125 | namespace = trace.data[route]
126 | module = sys.modules[route]
127 | for _global in vars(module).keys():
128 | if _global not in namespace.keys():
129 | print(_global)
130 | for _global in namespace:
131 | if _global in ['', '', '']:
132 | continue
133 | item = getattr(module, _global)
134 | # module.globals(_global)
135 | # trace.data.keys()
136 | except KeyError:
137 | print(route)
138 |
139 | # for trace.data
--------------------------------------------------------------------------------
/scripts/prototypes/redirect.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import os
3 | from Queue import Queue
4 |
5 | sys.dont_write_bytecode = True
6 | TESTS_DIR = os.path.dirname(__file__)
7 | PACKAGE_PATH = os.path.dirname(TESTS_DIR)
8 | sys.path.append(PACKAGE_PATH)
9 |
10 |
11 | from PythonEditor.ui.Qt import QtWidgets, QtCore, QtGui
12 |
13 |
14 | class VirtualModule(object):
15 | pass
16 |
17 |
18 | class Loader(object):
19 | def load_module(self, name):
20 | try:
21 | from _fnpython import stderrRedirector, outputRedirector
22 | sys.outputRedirector = outputRedirector
23 | sys.stderrRedirector = stderrRedirector
24 | finally:
25 | # firmly block all imports of the module
26 | return VirtualModule()
27 |
28 |
29 | class Finder(object):
30 | _deletable = ''
31 |
32 | def find_module(self, name, path=''):
33 | if 'FnRedirect' in name:
34 | return Loader()
35 |
36 |
37 | sys.meta_path = [i for i in sys.meta_path
38 | if not hasattr(i, '_deletable')]
39 | sys.meta_path.append(Finder())
40 |
41 |
42 | class PySingleton(object):
43 | """
44 | Return a single instance of a class
45 | or create a new instance if none exists.
46 | """
47 | def __new__(cls, *args, **kwargs):
48 | if '_the_instance' not in cls.__dict__:
49 | cls._the_instance = object.__new__(cls)
50 | return cls._the_instance
51 |
52 |
53 | def call(func, *args, **kwargs):
54 | func(*args, **kwargs)
55 |
56 |
57 | class Redirect(object):
58 | def __init__(self, stream):
59 | self.stream = stream
60 | self.queue = Queue()
61 |
62 | self.func = lambda x: None
63 | self.SERedirect = lambda x: None
64 |
65 | for a in dir(stream):
66 | try:
67 | getattr(self, a)
68 | except AttributeError:
69 | attr = getattr(stream, a)
70 | setattr(self, a, attr)
71 |
72 | def write(self, text):
73 | self.stream.write(text)
74 | self.SERedirect(text)
75 |
76 | self.queue.put(text)
77 | self.func(self.queue)
78 |
79 |
80 | class SysOut(Redirect, PySingleton):
81 | pass
82 |
83 |
84 | class SysErr(Redirect, PySingleton):
85 | pass
86 |
87 |
88 | class SysIn(Redirect, PySingleton):
89 | pass
90 |
91 |
92 | class Terminal(QtWidgets.QPlainTextEdit):
93 | def __init__(self):
94 | super(Terminal, self).__init__()
95 | self.setReadOnly(True)
96 | self.setObjectName('Terminal')
97 | self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
98 |
99 | sys.stdout.func = self.get
100 | self.get(sys.stdout.queue)
101 | sys.stderr.func = self.get
102 | self.get(sys.stderr.queue)
103 |
104 | def get(self, queue):
105 | """
106 | The get method allows the terminal to pick up
107 | on output created between the stream object
108 | encapsulation and the terminal creation.
109 |
110 | This is as opposed to connecting directly to the
111 | insertPlainText method, e.g.
112 | sys.stdout.write = self.insertPlainText
113 | """
114 |
115 | while not queue.empty():
116 | text = queue.get()
117 | self.receive(text)
118 |
119 | def receive(self, text):
120 | textCursor = self.textCursor()
121 | self.moveCursor(QtGui.QTextCursor.End)
122 | self.insertPlainText(text)
123 |
124 | def showEvent(self, event):
125 | super(Terminal, self).showEvent(event)
126 | self.get(sys.stdout.queue)
127 | self.get(sys.stderr.queue)
128 |
129 |
130 | if not hasattr(sys.stdout, 'func'):
131 | sys.stdout = SysOut(sys.__stdout__)
132 | if not hasattr(sys.stderr, 'func'):
133 | sys.stderr = SysErr(sys.__stderr__)
134 | if not hasattr(sys.stdin, 'func'):
135 | sys.stdin = SysIn(sys.__stdin__)
136 |
137 |
138 |
139 | # if __name__ == '__main__':
140 | # print "this won't get printed to the terminal"
141 |
142 | # print "this should get printed to the terminal"
143 |
144 | # print sys.stdout is sys.stderr is sys.stdin
145 | # print 5, object, type, sys._getframe()
146 | # print 'bba\npp'*8
147 |
148 | # print 'pre-app warmup'
149 | # app = QtWidgets.QApplication(sys.argv)
150 | # t = Terminal()
151 | # t.show()
152 | # t.receive('some text')
153 | # print 'and let the show begin'
154 |
155 | # def printhi():
156 | # print 'anus'
157 | # raise StopIteration
158 |
159 | # timer = QtCore.QTimer()
160 | # timer.timeout.connect(printhi)
161 | # timer.setInterval(1000)
162 | # timer.start()
163 | # sys.exit(app.exec_())
164 |
--------------------------------------------------------------------------------
/PythonEditor/ui/terminal.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | import os
3 | import re
4 | import sys
5 |
6 | from PythonEditor.core import streams
7 | from PythonEditor.utils.constants import DEFAULT_FONT
8 | from PythonEditor.ui.Qt.QtGui import (QFont,
9 | QTextCursor,
10 | QCursor,
11 | QClipboard)
12 | from PythonEditor.ui.Qt.QtCore import (Qt,
13 | Signal,
14 | Slot,
15 | QTimer)
16 | from PythonEditor.ui.Qt.QtWidgets import QPlainTextEdit
17 | from PythonEditor.utils.debug import debug
18 | from PythonEditor.ui.features.actions import get_external_editor_path
19 | from PythonEditor.ui.features.actions import open_in_external_editor
20 |
21 |
22 | STARTUP = 'PYTHONEDITOR_CAPTURE_STARTUP_STREAMS'
23 |
24 | class Terminal(QPlainTextEdit):
25 | """ Output text display widget """
26 |
27 | def __init__(self):
28 | super(Terminal, self).__init__()
29 |
30 | self.setObjectName('Terminal')
31 | self.setWindowFlags(
32 | Qt.WindowStaysOnTopHint
33 | )
34 | self.setReadOnly(True)
35 | self.destroyed.connect(self.stop)
36 | font = QFont(DEFAULT_FONT)
37 | font.setPointSize(10)
38 | self.setFont(font)
39 |
40 | if os.getenv(STARTUP) == '1':
41 | self.setup()
42 | else:
43 | QTimer.singleShot(0, self.setup)
44 |
45 | @Slot(str)
46 | def receive(self, text):
47 | try:
48 | textCursor = self.textCursor()
49 | if bool(textCursor):
50 | self.moveCursor(
51 | QTextCursor.End
52 | )
53 | except Exception:
54 | pass
55 | self.insertPlainText(text)
56 |
57 | def stop(self):
58 | for stream in sys.stdout, sys.stderr:
59 | if hasattr(stream, 'reset'):
60 | stream.reset()
61 |
62 | def setup(self):
63 | """
64 | Checks for an existing stream wrapper
65 | for sys.stdout and connects to it. If
66 | not present, creates a new one.
67 | TODO:
68 | The FnRedirect sys.stdout is always active.
69 | With a singleton object on a thread,
70 | that reads off this stream, we can make it
71 | available to Python Editor even before opening
72 | the panel.
73 | """
74 | if hasattr(sys.stdout, '_signal'):
75 | self.speaker = sys.stdout._signal
76 | else:
77 | self.speaker = streams.Speaker()
78 | sys.stdout = streams.SESysStdOut(sys.stdout, self.speaker)
79 | sys.stderr = streams.SESysStdErr(sys.stderr, self.speaker)
80 |
81 | self.speaker.emitter.connect(self.receive)
82 |
83 | def contextMenuEvent(self, event):
84 | menu = self.createStandardContextMenu()
85 | path_in_line = self.path_in_line(
86 | self.line_from_event(event)
87 | )
88 | if path_in_line:
89 | def _goto():
90 | goto(path_in_line)
91 | menu.addAction('Goto {0}'.format(path_in_line), _goto)
92 | menu.addAction('Parse Last Traceback', self.parse_last_traceback)
93 | menu.exec_(QCursor().pos())
94 |
95 | def line_from_event(self, event):
96 | pos = event.pos()
97 | cursor = self.cursorForPosition(pos)
98 | cursor.select(cursor.BlockUnderCursor)
99 | selection = cursor.selection()
100 | text = selection.toPlainText().strip()
101 | return text
102 |
103 | def path_in_line(self, text):
104 | """
105 | Parse the line under the cursor
106 | to see if it contains a path to a
107 | file. If it does, return it.
108 | """
109 | pattern = re.compile(r'([\w\-\.\/\\]+)(", line )(\d+)')
110 | path = ''
111 | for fp, _, lineno in re.findall(pattern, text):
112 | return ':'.join([fp, lineno])
113 | return None
114 |
115 | def parse_last_traceback(self):
116 | tb = self.toPlainText().split('Traceback')[-1]
117 | pattern = re.compile(r'(File ")([\w\.\/]+)(", line )(\d+)')
118 | text = ''
119 | for _, fp, _, lineno in re.findall(pattern, tb):
120 | text += 'sublime '+':'.join([fp, lineno])
121 | text += '\n'
122 |
123 | print(text)
124 | QClipboard().setText(text)
125 |
126 |
127 | def goto(path):
128 | eepath = get_external_editor_path()
129 | if eepath is not None:
130 | # this assumes the external
131 | # editor can handle paths
132 | # with path:lineno
133 | print('Going to:')
134 | print(path)
135 | open_in_external_editor(path)
136 |
--------------------------------------------------------------------------------
/scripts/prototypes/hover_text_tooltip.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 | from Qt import QtWidgets, QtGui, QtCore
4 |
5 | PHC = QtCore.Qt.PointingHandCursor
6 | ARC = QtCore.Qt.ArrowCursor
7 |
8 | LUC = QtGui.QTextCursor.LineUnderCursor
9 | WUC = QtGui.QTextCursor.WordUnderCursor
10 | BUC = QtGui.QTextCursor.BlockUnderCursor
11 | class HoverText(QtWidgets.QPlainTextEdit):
12 | hovered_word = ''
13 | def __init__(self):
14 | super(HoverText, self).__init__()
15 | self.setMouseTracking(True)
16 |
17 | def mousePressEvent(self, event):
18 | super(HoverText, self).mousePressEvent(event)
19 | if self._cur_path:
20 | print(self._cur_path)
21 |
22 | def enterEvent(self, event):
23 | self.mouse_in = True
24 | print('in')
25 | super(HoverText, self).enterEvent(event)
26 |
27 | def leaveEvent(self, event):
28 | self.mouse_in = False
29 | print('out')
30 | super(HoverText, self).leaveEvent(event)
31 |
32 | def mouseMoveEvent(self, event):
33 | super(HoverText, self).mouseMoveEvent(event)
34 | if not self.mouse_in:
35 | return
36 |
37 | pos = event.pos()
38 | tc = self.cursorForPosition(pos)
39 | tc.select(BUC)
40 | sel = tc.selection()
41 | text = sel.toPlainText().strip()
42 | #if not text:
43 | #return
44 | same = (self.hovered_word == text)
45 | if not same:
46 | self.hovered_word = text
47 | #return
48 | #c = QtGui.QCursor()
49 | #c = self.cursor()
50 |
51 | #self.cursor()
52 | #self.cursor().setShape(ARC)
53 | #self.cursor().setShape(PHC)
54 | #c.setShape(PHC)
55 | ptn = '(?<=\")([^\"]+)(?=\")'
56 | pbc = re.findall(ptn, text)
57 |
58 | app = QtWidgets.QApplication
59 |
60 |
61 | pos = event.pos()
62 | tc = self.cursorForPosition(pos)
63 | tc.select(WUC)
64 | sel = tc.selection()
65 | word = sel.toPlainText().strip()
66 |
67 | if pbc:
68 | self._cur_path = pbc[0]
69 | else:
70 | self._cur_path = ''
71 |
72 | if not os.path.isfile(self._cur_path):
73 | self._cur_path = ''
74 |
75 | if word and word in self._cur_path:
76 | app.setOverrideCursor(PHC)
77 | else:
78 | app.restoreOverrideCursor()
79 | app.processEvents()
80 | #print text
81 | #else:
82 | #c.setShape(ARC)
83 | #self.setCursor(c)
84 |
85 | ht = HoverText()
86 | ht.show()
87 |
88 | text = r"""
89 | Python 2.7.9 (default, Dec 10 2014, 12:28:03) [MSC v.1500 64 bit (AMD64)]
90 | on win32
91 | Type "copyright", "credits" or "license()" for more information.
92 | >>> ================================ RESTART ================================
93 | Enter salesperson ID or 9999 to quit: 1584
94 | Traceback (most recent call last):
95 | File "C:\Repositories\PythonEditor\PythonEditor\ui\features\autocompletion.py",
96 | line 8, in
97 | while salesPersonID != 9999:
98 | NameError: name 'salesPersonID' is not defined
99 | """
100 | ht.setPlainText(text)
101 |
102 | QtGui.QCursor().shape()
103 | QtGui.QCursor().setShape(PHC)
104 | #QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.PointingHandCursor)
105 | #QtWidgets.QApplication.restoreOverrideCursor()
106 |
107 | os.path.isdir('C:')
108 |
109 |
110 | cursors = [c for c in dir(QtCore.Qt) if 'cursor' in c.lower()]
111 | for l in """
112 | ['ArrowCursor', 'BitmapCursor', 'BlankCursor', 'BusyCursor', 'ClosedHandCursor', 'CrossCursor', 'CursorMoveStyle', 'CursorShape', 'CustomCursor', 'DragCopyCursor', 'DragLinkCursor', 'DragMoveCursor', 'ForbiddenCursor', 'IBeamCursor', 'ImCursorPosition', 'LastCursor', 'NavigationModeCursorAuto', 'NavigationModeCursorForceVisible', 'OpenHandCursor', 'PointingHandCursor', 'SizeAllCursor', 'SizeBDiagCursor', 'SizeFDiagCursor', 'SizeHorCursor', 'SizeVerCursor', 'SplitHCursor', 'SplitVCursor', 'UpArrowCursor', 'WA_SetCursor', 'WaitCursor', 'WhatsThisCursor']""".split():
113 | #print l
114 | pass
115 |
116 | ['ArrowCursor',
117 | 'BitmapCursor',
118 | 'BlankCursor',
119 | 'BusyCursor',
120 | 'ClosedHandCursor',
121 | 'CrossCursor',
122 | 'CursorMoveStyle',
123 | 'CursorShape',
124 | 'CustomCursor',
125 | 'DragCopyCursor',
126 | 'DragLinkCursor',
127 | 'DragMoveCursor',
128 | 'ForbiddenCursor',
129 | 'IBeamCursor',
130 | 'ImCursorPosition',
131 | 'LastCursor',
132 | 'NavigationModeCursorAuto',
133 | 'NavigationModeCursorForceVisible',
134 | 'OpenHandCursor',
135 | 'PointingHandCursor',
136 | 'SizeAllCursor',
137 | 'SizeBDiagCursor',
138 | 'SizeFDiagCursor',
139 | 'SizeHorCursor',
140 | 'SizeVerCursor',
141 | 'SplitHCursor',
142 | 'SplitVCursor',
143 | 'UpArrowCursor',
144 | 'WA_SetCursor',
145 | 'WaitCursor',
146 | 'WhatsThisCursor']
147 |
--------------------------------------------------------------------------------
/scripts/prototypes/_popup_label.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 | from PythonEditor.ui.Qt import QtWidgets, QtGui, QtCore
3 | from PythonEditor.ui import tabs
4 | from six.moves import range
5 |
6 | class PopupBarTest(QtCore.QObject):
7 | """
8 | Test class for the popup_bar
9 | """
10 | show_popup_signal = QtCore.Signal()
11 | def __init__(self, tabs):
12 | super(PopupBarTest, self).__init__()
13 | #self.setParent(tabs)
14 |
15 | self.tabeditor = tabs
16 | self.editor = tabs.editor
17 | self.tabs = tabs.tabs
18 | self.setParent(tabs)
19 | self.show_popup_signal.connect(self.show_popup_bar)
20 |
21 | def show_popup_bar(self):
22 | #editor = _ide.python_editor.editor
23 | #layout = _ide.python_editor.tabeditor.layout()
24 |
25 | editor = self.editor
26 | layout = self.tabeditor.layout()
27 |
28 | # first remove any previous widgets
29 | name = 'Document out of sync warning'
30 | for i in range(layout.count()):
31 | item = layout.itemAt(i)
32 | if item is None:
33 | continue
34 | widget = item.widget()
35 | if widget.objectName() != name:
36 | continue
37 | layout.removeItem(item)
38 | widget.deleteLater()
39 |
40 | popup_bar = QtWidgets.QWidget()
41 | popup_bar.setObjectName('Document out of sync warning')
42 | bar_layout = QtWidgets.QHBoxLayout(popup_bar)
43 |
44 | l = QtWidgets.QLabel()
45 | l.setText('This tab is out of sync with the autosave.')
46 | new_button = QtWidgets.QPushButton('Load into New Tab')
47 | save_button = QtWidgets.QPushButton('Save This Version')
48 | update_button = QtWidgets.QPushButton('Update From Autosave')
49 | diff_button = QtWidgets.QPushButton('Show Diff')
50 |
51 | stylesheet = """
52 | QPushButton { background-color: #444; }
53 | QPushButton:hover { background-color: orange; }
54 | """
55 |
56 | for b in new_button, save_button, update_button, diff_button:
57 | #b.setFlat(True)
58 | b.setStyleSheet(stylesheet)
59 |
60 | for b in l, new_button, save_button, update_button, diff_button:
61 | bar_layout.addWidget(b)
62 |
63 | layout.insertWidget(1, popup_bar)
64 | popup_bar.setMaximumHeight(0)
65 |
66 | #print popup_bar.maximumHeight()
67 | #popup_bar.setMaximumHeight(46)
68 | def anim_popup_bar(popup_bar):
69 | anim = QtCore.QPropertyAnimation(
70 | popup_bar,
71 | 'maximumHeight'
72 | )
73 | anim.setStartValue(0)
74 | anim.setEndValue(46)
75 | anim.setDuration(400)
76 | anim.start()
77 | anim_popup_bar.anim = anim
78 |
79 | anim_popup_bar(popup_bar)
80 |
81 | tabs = _ide.python_editor.tabeditor
82 | def test_A():
83 | A = PopupBarTest(tabs)
84 | for i in range(1):
85 | A.show_popup_signal.emit()
86 | #A.show_popup_bar()
87 |
88 | #test_A()
89 | QtCore.QTimer.singleShot(300, test_A)
90 | #&&
91 |
92 | anim2 = QtCore.QPropertyAnimation(popup_bar, 'maximumHeight')
93 | #anim2.setStartValue(popup_bar.maximumHeight())
94 | anim2.setStartValue(46)
95 | anim2.setEndValue(0)
96 | anim2.setDuration(500)
97 | from functools import partial
98 | start = partial(QtCore.QTimer.singleShot, 1200, anim2.start)
99 | anim.finished.connect(start)
100 |
101 | #anim2.finished.connect(popup_bar.deleteLater)
102 |
103 | #&&
104 |
105 | anim = QtCore.QPropertyAnimation(popup_bar, 'size')
106 | anim.setStartValue(QtCore.QSize(958, 0))
107 | anim.setEndValue(QtCore.QSize(958, 46))
108 | anim.setDuration(300)
109 | anim.start()
110 |
111 | #&&
112 | item.geometry()
113 | item.setGeometry(QtCore.QRect(0,0,958,0))
114 | layout.setSpacing(0)
115 | #layout.setSizeConstraint(layout.SetFixedSize)
116 | layout.setSizeConstraint(layout.SetNoConstraint)
117 | layout.setSizeConstraint(layout.SetDefaultConstraint)
118 | #&&
119 | anim = QtCore.QPropertyAnimation(popup_bar, 'height')
120 | anim.setStartValue(0)
121 | anim.setEndValue(246)
122 | anim.setDuration(500)
123 | anim.start()
124 |
125 |
126 | #&&
127 | anim = QtCore.QPropertyAnimation(popup_bar, 'geometry')
128 | x,y,w,h = editor.rect().getRect()
129 | start_rect = QtCore.QRect(x, y, w, 70)
130 | anim.setStartValue(start_rect)
131 | end_rect = QtCore.QRect(x, y, w, 110)
132 | anim.setEndValue(end_rect)
133 | anim.setDuration(1500)
134 | anim.start()
135 |
136 |
137 |
138 | #def add_items():
139 | #anim.finished.connect(add_items)
140 | #&&
141 | #editor.resize(editor.width(), editor.height()-40)
142 | #editor.move(editor.x(), editor.y()+40)
143 |
144 | w.show()
145 | w.raise_()
146 | #&&
147 | wg = [a for a in editor.children() if isinstance(a, QtWidgets.QWidget)]
--------------------------------------------------------------------------------
/tests/test_autosavexml.py:
--------------------------------------------------------------------------------
1 | # tests autosave.
2 |
3 | import os
4 | import shutil
5 | import pytest
6 | from xml.etree import cElementTree, ElementTree
7 |
8 | from PythonEditor.ui.features import autosavexml
9 |
10 | @pytest.fixture
11 | def setup_and_teardown_autosave_file():
12 | """If the autosave file already exists, create a temporary
13 | backup, then tear down by reinstating the original file."""
14 | autosave_exists = os.path.isfile(autosavexml.AUTOSAVE_FILE)
15 | if autosave_exists:
16 | print("File exists: %s" % autosavexml.AUTOSAVE_FILE)
17 |
18 | shutil.copy2(autosavexml.AUTOSAVE_FILE,
19 | autosavexml.AUTOSAVE_FILE+'.bak')
20 | print("Backed up autosave file to: %s" % autosavexml.AUTOSAVE_FILE+'.bak')
21 | yield # <- where the test() is run.
22 | if os.path.exists(autosavexml.AUTOSAVE_FILE+'.bak'):
23 | if os.path.isfile(autosavexml.AUTOSAVE_FILE):
24 | print('Removing %s' % autosavexml.AUTOSAVE_FILE)
25 | os.remove(autosavexml.AUTOSAVE_FILE)
26 | os.rename(autosavexml.AUTOSAVE_FILE+'.bak', autosavexml.AUTOSAVE_FILE)
27 | print('Restored original %s' % autosavexml.AUTOSAVE_FILE)
28 | if (not autosave_exists) and os.path.isfile(autosavexml.AUTOSAVE_FILE):
29 | os.remove(autosavexml.AUTOSAVE_FILE)
30 |
31 |
32 | # --- critical autosave functions that are the "write/out" points of the application:
33 |
34 | def test_create_autosave_file(setup_and_teardown_autosave_file):
35 | """Test that the autosave file always exists after calling this function."""
36 | assert autosavexml.create_autosave_file()
37 | assert os.path.isfile(autosavexml.AUTOSAVE_FILE)
38 | assert os.environ["PYTHONEDITOR_AUTOSAVE_FILE"]==autosavexml.AUTOSAVE_FILE
39 |
40 |
41 | def test_create_empty_autosave(setup_and_teardown_autosave_file):
42 | """Test that an empty autosave is always created with the appropriate header."""
43 | autosavexml.create_empty_autosave()
44 | assert os.path.isfile(autosavexml.AUTOSAVE_FILE)
45 | assert os.environ["PYTHONEDITOR_AUTOSAVE_FILE"]==autosavexml.AUTOSAVE_FILE
46 | with open(autosavexml.AUTOSAVE_FILE, 'r') as fd:
47 | contents = fd.read()
48 | assert contents == ''
49 |
50 |
51 | def test_writexml(setup_and_teardown_autosave_file):
52 | """Test that the writexml function writes back sensible data."""
53 | # make sure we have an autosave file
54 | if not os.path.isfile(autosavexml.AUTOSAVE_FILE):
55 | autosavexml.create_autosave_file()
56 |
57 | # read the file, then write it back.
58 | root, elements = autosavexml.parsexml("subscript")
59 | autosavexml.writexml(root, path=autosavexml.AUTOSAVE_FILE)
60 |
61 | # does the file still exist?
62 | assert os.path.isfile(autosavexml.AUTOSAVE_FILE)
63 |
64 | # the file may have had some characters sanitized,
65 | # but the number of elements should be the same.
66 | new_root, new_elements = autosavexml.parsexml("subscript")
67 | assert len(new_elements) == len(elements)
68 |
69 | try:
70 | unicode
71 | except NameError:
72 | # no unicode function in python 3
73 | def unicode(text): return text
74 |
75 | from PythonEditor.ui.features.autosavexml import sanitize
76 |
77 | # none of the element attributes should have changed
78 | for old_element, new_element in zip(elements, new_elements):
79 | assert old_element.attrib == new_element.attrib
80 | old_text = unicode(old_element.text)
81 | new_text = unicode(new_element.text)
82 | assert sanitize(old_text) == sanitize(new_text)
83 |
84 |
85 | def test_fix_broken_xml(setup_and_teardown_autosave_file):
86 | if not os.path.isfile(autosavexml.AUTOSAVE_FILE):
87 | autosavexml.create_autosave_file()
88 | autosavexml.fix_broken_xml(path=autosavexml.AUTOSAVE_FILE)
89 | # test that the file can be parsed correctly now
90 | root, elements = autosavexml.parsexml("subscript", path=autosavexml.AUTOSAVE_FILE)
91 |
92 |
93 | def test_parsexml(setup_and_teardown_autosave_file):
94 | root, elements = autosavexml.parsexml("subscript", path=autosavexml.AUTOSAVE_FILE)
95 | assert isinstance(elements, list)
96 | assert root is not None
97 |
98 |
99 | # ---
100 |
101 |
102 | # --- TODO: test methods picked up by tracing autosave usage with sys.settrace
103 | """
104 | "PythonEditor.ui.features.autosavexml"
105 | "AutoSaveManager"
106 | "__init__
107 | "setup_save_timer
108 | "readautosave
109 | "connect_signals
110 | "check_autosave_modified
111 | "remove_existing_popups
112 | "check_document_modified
113 | "save_timer
114 | "autosave_handler
115 | "autosave
116 | "save_by_uuid
117 | "sync_tab_indices
118 | "store_current_index
119 | "remove_subscript
120 | "update_tab_index
121 | "handle_document_save"
122 | """
123 |
--------------------------------------------------------------------------------
/PythonEditor/utils/save.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | import os
3 | import subprocess
4 |
5 | from PythonEditor.ui.Qt import QtWidgets
6 | from PythonEditor.utils import constants
7 |
8 |
9 | def write_to_file(path, text):
10 | """
11 | Write text to a file.
12 | """
13 | with open(path, 'w') as f:
14 | try:
15 | f.write(text)
16 | except UnicodeEncodeError:
17 | f.write(text.encode('utf-16'))
18 |
19 |
20 | def get_save_file_name(title='Save Text As'):
21 | """
22 | Ask user where they would like to save.
23 |
24 | :return: Path user chose to save. None if user cancels.
25 | """
26 | path, _ = QtWidgets.QFileDialog.getSaveFileName(
27 | None,
28 | title,
29 | constants.NUKE_DIR,
30 | selectedFilter='*.py'
31 | )
32 |
33 | if not path:
34 | path = None
35 |
36 | return path
37 |
38 |
39 | def save(text, path=None):
40 | """
41 | Ctrl+S. Save text to the given path.
42 | If there is no path, prompt the user
43 | for one.
44 |
45 | :return: Path user chose to save. None if user cancels.
46 | """
47 | if path is None or not path.strip():
48 | path = save_as(text)
49 |
50 | if path is None:
51 | # User cancelled
52 | return
53 |
54 | write_to_file(path, text)
55 | print('Saved', path, sep=' ')
56 | return path
57 |
58 |
59 | def save_as(text):
60 | """
61 | Ask the user where they would like to save the document.
62 | """
63 | path = get_save_file_name(title='Save As')
64 | if path:
65 | save(text, path)
66 | return path
67 |
68 |
69 | def save_selected_text(editor):
70 | """
71 | Export whatever text we have selected to a file. It's only
72 | in this case that we don't want to change the autosave or
73 | set the editor status to read_only, because we may be saving
74 | only part of the document.
75 | """
76 | text = editor.textCursor().selection().toPlainText()
77 | path = get_save_file_name(title='Save Selected Text')
78 | if path:
79 | write_to_file(path, text)
80 | return path
81 |
82 |
83 | def export_selected_to_external_editor(editor):
84 | path = save_selected_text(editor)
85 |
86 | #TODO: this is a horrible hack to avoid circular imports
87 | from PythonEditor.ui.features.autosavexml import get_external_editor_path
88 | EXTERNAL_EDITOR_PATH = get_external_editor_path()
89 |
90 | if path and EXTERNAL_EDITOR_PATH:
91 | subprocess.Popen([EXTERNAL_EDITOR_PATH, path])
92 | return path
93 |
94 |
95 | def save_tab(folder, name, tabs, tab_index):
96 | """
97 | Compose a path from folder and tab name.
98 | Set the tab path property, then save.
99 | """
100 | file = name.split('.')[0] + '.py'
101 | data = tabs.tabData(tab_index)
102 | path = os.path.join(folder, file)
103 | data['path'] = path
104 | tabs.setTabData(tab_index, data)
105 | save(data['text'], path)
106 | return path
107 |
108 |
109 | def open_external_editor(path):
110 |
111 | #TODO: this is a horrible hack to avoid circular imports
112 | from PythonEditor.ui.features.autosavexml import get_external_editor_path
113 | EXTERNAL_EDITOR_PATH = get_external_editor_path()
114 |
115 | if path and EXTERNAL_EDITOR_PATH:
116 | subprocess.Popen([EXTERNAL_EDITOR_PATH, path])
117 |
118 |
119 | def export_current_tab_to_external_editor(tabs, editor):
120 |
121 | tab_index = tabs.currentIndex()
122 | name = tabs.tabText(tab_index)
123 |
124 | path, _ = QtWidgets.QFileDialog.getSaveFileName(
125 | tabs,
126 | 'Choose Directory to save current tab',
127 | os.path.join(constants.NUKE_DIR, name),
128 | selectedFilter='*.py'
129 | )
130 |
131 | if not path:
132 | return
133 |
134 | folder = os.path.dirname(path)
135 | save_tab(folder, name, tabs, tab_index)
136 | open_external_editor(path)
137 |
138 |
139 | def export_all_tabs_to_external_editor(tabs):
140 | if tabs.count() == 0:
141 | return
142 |
143 | path, _ = QtWidgets.QFileDialog.getSaveFileName(
144 | tabs,
145 | 'Choose Directory to save all tabs',
146 | os.path.join(constants.NUKE_DIR, 'tab_name_used_per_file'),
147 | selectedFilter='*.py'
148 | )
149 |
150 | if not path:
151 | return
152 |
153 | folder = os.path.dirname(path)
154 |
155 | for i in range(tabs.count()):
156 | data = tabs.tabData(i)
157 | name = tabs.tabText(i)
158 | filename = name.split('.')[0] + '.py'
159 | path = os.path.join(folder, filename)
160 | text = data['text']
161 | if not text:
162 | print('No text found for tab %s, it will not be saved' % name)
163 | continue
164 | save(text, path)
165 |
166 | open_external_editor(folder)
167 |
168 | Yes = QtWidgets.QMessageBox.Yes
169 | No = QtWidgets.QMessageBox.No
170 | answer = QtWidgets.QMessageBox.question(
171 | tabs,
172 | 'Remove all tabs?',
173 | 'Choosing Yes will remove all tabs and clear the temp file.',
174 | Yes, No
175 | )
176 |
177 | if answer == Yes:
178 | tabs.reset_tab_signal.emit()
179 |
--------------------------------------------------------------------------------
/scripts/prototypes/simplemanager.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 |
5 | def cd_up(path, level=1):
6 | for d in range(level):
7 | path = os.path.dirname(path)
8 | return path
9 |
10 |
11 | package_dir = cd_up(__file__, level=3)
12 | sys.path.insert(0, package_dir)
13 |
14 | from PythonEditor.ui.Qt import QtWidgets, QtCore
15 | from PythonEditor.ui import editor
16 | from PythonEditor.ui import browser
17 | from PythonEditor.ui import menubar
18 | from PythonEditor.utils.constants import NUKE_DIR
19 |
20 |
21 | def get_parent(widget, level=1):
22 | """
23 | Return a widget's nth parent widget.
24 | """
25 | parent = widget
26 | for p in range(level):
27 | parent = parent.parentWidget()
28 | return parent
29 |
30 |
31 | class Manager(QtWidgets.QWidget):
32 | """
33 | Manager with only one file connected at a time.
34 | """
35 | def __init__(self):
36 | super(Manager, self).__init__()
37 | self.currently_viewed_file = None
38 | self.build_layout()
39 |
40 | def build_layout(self):
41 | """
42 | Create the layout.
43 | """
44 | layout = QtWidgets.QVBoxLayout(self)
45 | layout.setContentsMargins(0, 0, 0, 0)
46 | # self.setup_menu()
47 | self.read_only = True
48 | self.menubar = menubar.MenuBar(self)
49 |
50 | left_widget = QtWidgets.QWidget()
51 | left_layout = QtWidgets.QVBoxLayout(left_widget)
52 |
53 | path_edit = QtWidgets.QLineEdit()
54 | path_edit.textChanged.connect(self.update_tree)
55 | self.path_edit = path_edit
56 |
57 | splitter = QtWidgets.QSplitter(QtCore.Qt.Horizontal)
58 | self.splitter = splitter
59 |
60 | self.xpanded = False
61 | self.setLayout(layout)
62 | self.tool_button = QtWidgets.QToolButton()
63 | self.tool_button.setText('<')
64 | self.tool_button.clicked.connect(self.xpand)
65 | self.tool_button.setMaximumWidth(20)
66 |
67 | layout.addWidget(splitter)
68 |
69 | browse = browser.FileTree(NUKE_DIR)
70 | self.browser = browse
71 | left_layout.addWidget(self.path_edit)
72 | left_layout.addWidget(self.browser)
73 |
74 | self.editor = editor.Editor(handle_shortcuts=True)
75 | self.editor.path = 'C:/Users/tsalx/Desktop/temp_editor_save.py'
76 |
77 | widgets = [left_widget,
78 | self.tool_button,
79 | self.editor]
80 | for w in widgets:
81 | splitter.addWidget(w)
82 |
83 | splitter.setSizes([200, 10, 800])
84 | self.browser.path_signal.connect(self.read)
85 | self.editor.textChanged.connect(self.write)
86 | self.editor.modificationChanged.connect(self.handle_changed)
87 |
88 | def xpand(self):
89 | """
90 | Expand or contract the QSplitter
91 | to show or hide the file browser.
92 | """
93 | if self.xpanded:
94 | symbol = '<'
95 | sizes = [200, 10, 800] # should be current sizes
96 | else:
97 | symbol = '>'
98 | sizes = [0, 10, 800] # should be current sizes
99 |
100 | self.tool_button.setText(symbol)
101 | self.splitter.setSizes(sizes)
102 | self.xpanded = not self.xpanded
103 |
104 | @QtCore.Slot(str)
105 | def update_tree(self, path):
106 | """
107 | Update the file browser when the
108 | lineedit is updated.
109 | """
110 | model = self.browser.model()
111 | root_path = model.rootPath()
112 | if root_path in path:
113 | return
114 | path = os.path.dirname(path)
115 | if not os.path.isdir(path):
116 | return
117 | path = path+os.altsep
118 | print(path)
119 | self.browser.set_model(path)
120 |
121 | @QtCore.Slot(str)
122 | def read(self, path):
123 | """
124 | Read from text file.
125 | """
126 | self.read_only = True
127 | self.path_edit.setText(path)
128 | if not os.path.isfile(path):
129 | return
130 |
131 | with open(path, 'rt') as f:
132 | text = f.read()
133 | self.editor.setPlainText(text)
134 |
135 | self.editor.path = path
136 |
137 | @QtCore.Slot()
138 | def write(self):
139 | """
140 | Write to text file.
141 | """
142 | if self.read_only:
143 | return
144 |
145 | path = self.editor.path
146 |
147 | with open(path, 'wt') as f:
148 | f.write(self.editor.toPlainText())
149 |
150 | def handle_changed(self, changed):
151 | self.read_only = not changed
152 |
153 | def showEvent(self, event):
154 | """
155 | Hack to get rid of margins automatically put in
156 | place by Nuke Dock Window.
157 | """
158 | try:
159 | for i in 2, 4:
160 | parent = get_parent(self, level=i)
161 | parent.layout().setContentsMargins(0, 0, 0, 0)
162 | except Exception:
163 | pass
164 |
165 | super(Manager, self).showEvent(event)
166 |
167 |
168 | if __name__ == '__main__':
169 |
170 | app = QtWidgets.QApplication(sys.argv)
171 | m = Manager()
172 | m.show()
173 | app.exec_()
174 |
--------------------------------------------------------------------------------
/PythonEditor/core/streams.py:
--------------------------------------------------------------------------------
1 | """ This module augments Nuke's default stdout/stderr
2 | stream redirectors with ones that use Qt's Signal/Slot mechanism.
3 | These redirectors also output to Nuke's original outputRedirector
4 | and stderrRedirector which display text in the native Script Editor.
5 | """
6 | from __future__ import print_function
7 | import sys
8 |
9 | from PythonEditor.ui.Qt import QtCore
10 | from PythonEditor.utils.debug import debug
11 |
12 |
13 | # ====================================
14 | # -- override Nuke hiero.FnRedirect --
15 | # ====================================
16 | class Loader(object):
17 | """ When the Finder object in sys.meta_path
18 | returns this object, attempt to load Nuke's default
19 | redirectors and store them in the sys module.
20 | """
21 | def load_module(self, name):
22 | try:
23 | from _fnpython import stderrRedirector, outputRedirector
24 | sys.outputRedirector = outputRedirector
25 | sys.stderrRedirector = stderrRedirector
26 | finally:
27 | class MockModule(object):
28 | pass
29 | # firmly block all imports of the module
30 | return MockModule()
31 |
32 |
33 | class Finder(object):
34 | """ Override the import system to provide
35 | a loader that bypasses the FnRedirect module.
36 | """
37 | _fnredirect_blocker = ''
38 |
39 | def find_module(self, name, path=''):
40 | if 'FnRedirect' in name:
41 | return Loader()
42 |
43 |
44 | # clear any previous instances first
45 | sys.meta_path = [
46 | i for i in sys.meta_path
47 | if not hasattr(i, '_fnredirect_blocker')
48 | ]
49 | sys.meta_path.append(Finder())
50 | # ====================================
51 | # ------- end override section -------
52 | # ====================================
53 |
54 |
55 | class PySingleton(object):
56 | """ Return a single instance of a class
57 | or create a new instance if none exists.
58 | """
59 | def __new__(cls, *args, **kwargs):
60 | if '_the_instance' not in cls.__dict__:
61 | cls._the_instance = object.__new__(cls)
62 | return cls._the_instance
63 |
64 |
65 | class Speaker(QtCore.QObject):
66 | """ Used to relay sys stdout, stderr, stdin
67 | """
68 | emitter = QtCore.Signal(str)
69 |
70 |
71 | class SERedirector(object):
72 | """ For encapsulating and replacing a stream object.
73 | """
74 | def __init__(self, stream, _signal=None):
75 | file_methods = ('fileno',
76 | 'flush',
77 | 'isatty',
78 | 'read',
79 | 'readline',
80 | 'readlines',
81 | 'seek',
82 | 'tell',
83 | 'write',
84 | 'writelines',
85 | 'xreadlines',
86 | '__iter__',
87 | 'name')
88 |
89 | if hasattr(stream, 'reset'):
90 | stream.reset()
91 |
92 | for i in file_methods:
93 | if not hasattr(self, i) and hasattr(stream, i):
94 | setattr(self, i, getattr(stream, i))
95 |
96 | if not hasattr(self, 'isatty'):
97 | self.isatty = self._isatty
98 |
99 | self.saved_stream = stream
100 | self._signal = _signal
101 |
102 | def _isatty(self):
103 | return False
104 |
105 | def reset(self):
106 | raise NotImplementedError
107 |
108 | def close(self):
109 | self.flush()
110 |
111 | def stream(self):
112 | return self.saved_stream
113 |
114 | def __del__(self):
115 | self.reset()
116 |
117 |
118 | class SESysStdOut(SERedirector, PySingleton):
119 | def reset(self):
120 | sys.stdout = self.saved_stream
121 | print('reset stream out')
122 |
123 | def write(self, text):
124 | if self._signal is not None:
125 | self._signal.emitter.emit(text)
126 |
127 | if hasattr(sys, 'outputRedirector'):
128 | sys.outputRedirector(text)
129 |
130 | try:
131 | sys.__stdout__.write(text)
132 | except IOError:
133 | pass
134 |
135 |
136 | class SESysStdErr(SERedirector, PySingleton):
137 | def reset(self):
138 | sys.stderr = self.saved_stream
139 | print('reset stream err')
140 |
141 | def write(self, text):
142 | if self._signal is not None:
143 | self._signal.emitter.emit(text)
144 |
145 | if hasattr(sys, 'stderrRedirector'):
146 | sys.stderrRedirector(text)
147 | else:
148 | try:
149 | sys.__stderr__.write(text)
150 | except IOError:
151 | pass
152 |
153 |
154 | # we need these functions to be registered in the sys module
155 | try:
156 | sys.outputRedirector = lambda x: None
157 | sys.stderrRedirector = lambda x: None
158 | except Exception:
159 | pass
160 | # in case we decide to reload the module, we need to
161 | # re-add the functions to write to Nuke's Script Editor.
162 | try:
163 | from _fnpython import stderrRedirector, outputRedirector
164 | sys.outputRedirector = outputRedirector
165 | sys.stderrRedirector = stderrRedirector
166 | except ImportError:
167 | pass
168 |
--------------------------------------------------------------------------------
/scripts/prototypes/terminal_mid.py:
--------------------------------------------------------------------------------
1 | # an attempt to connect via signals instead of a list of methods,
2 | # with the idea that signal connections might not interrupt garbage collection
3 |
4 | """
5 | This module needs to satisfy the following requirements:
6 |
7 | - [ ] Redirect stdout to all Python Editor Terminal QPlainTextEdits
8 | - [ ] Preserve unread output in Queue objects, which are read when loading the Terminal(s)
9 | - [ ] Be reloadable without losing stdout connections
10 | - [ ] Not keep references to destroyed objects
11 |
12 |
13 | # would sys.displayhook be useful in here?
14 |
15 | """
16 |
17 |
18 | import sys
19 | import os
20 | from Queue import Queue
21 |
22 | sys.dont_write_bytecode = True
23 | TESTS_DIR = os.path.dirname(__file__)
24 | PACKAGE_PATH = os.path.dirname(TESTS_DIR)
25 | sys.path.append(PACKAGE_PATH)
26 |
27 |
28 | from PythonEditor.ui.Qt import QtWidgets, QtCore, QtGui
29 |
30 |
31 | # ----- override nuke FnRedirect -----
32 | class VirtualModule(object):
33 | pass
34 |
35 |
36 | class Loader(object):
37 | def load_module(self, name):
38 | print(name)
39 | from _fnpython import stderrRedirector, outputRedirector
40 | sys.stdout.SERedirect = outputRedirector
41 | sys.stderr.SERedirect = stderrRedirector
42 | return VirtualModule()
43 |
44 |
45 | class Finder(object):
46 | def find_module(self, name, path=''):
47 | if 'FnRedirect' in name:
48 | return Loader()
49 |
50 |
51 | # sys.meta_path = [i for i in sys.meta_path
52 | # if not isinstance(i, Finder)]
53 | # sys.meta_path.append(Finder())
54 | sys.meta_path = [Finder()]
55 | # ----- end override section -----
56 |
57 |
58 | class PySingleton(object):
59 | """
60 | Return a single instance of a class
61 | or create a new instance if none exists.
62 | """
63 | def __new__(cls, *args, **kwargs):
64 | if '_the_instance' not in cls.__dict__:
65 | cls._the_instance = object.__new__(cls)
66 | return cls._the_instance
67 |
68 |
69 | class Redirect(QtCore.QObject):
70 | signal = QtCore.Signal(str, object)
71 | def __init__(self, stream):
72 | super(Redirect, self).__init__()
73 | self.stream = stream
74 | self.queue = Queue(maxsize=2000)
75 | self.SERedirect = lambda x: None
76 | # self.receivers = []
77 |
78 | for a in dir(stream):
79 | try:
80 | getattr(self, a)
81 | except AttributeError:
82 | attr = getattr(stream, a)
83 | setattr(self, a, attr)
84 |
85 | def write(self, text):
86 | queue = self.queue
87 | receivers = self.receivers('2signal')
88 | if not receivers:
89 | queue.put(text)
90 | else:
91 | # if queue.empty():
92 | # queue = None
93 | # for func in receivers:
94 | # func(text=text, queue=queue)
95 | self.signal.emit(text, queue)
96 |
97 | self.stream.write(text)
98 | self.SERedirect(text)
99 |
100 |
101 | class SysOut(Redirect, PySingleton):
102 | pass
103 |
104 |
105 | class SysErr(Redirect, PySingleton):
106 | pass
107 |
108 |
109 | class SysIn(Redirect, PySingleton):
110 | pass
111 |
112 |
113 | class Terminal(QtWidgets.QPlainTextEdit):
114 | def __init__(self):
115 | super(Terminal, self).__init__()
116 | self.setReadOnly(True)
117 | self.setObjectName('Terminal')
118 | self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
119 |
120 | # sys.stdout.receivers.append(self.get)
121 | sys.stdout.signal.connect(self.get)
122 | self.get(queue=sys.stdout.queue)
123 | sys.stderr.signal.connect(self.get)
124 | # sys.stderr.receivers.append(self.get)
125 | self.get(queue=sys.stderr.queue)
126 |
127 | @QtCore.Slot(str, object)
128 | def get(self, text=None, queue=None):
129 | """
130 | The get method allows the terminal to pick up
131 | on output created between the stream object
132 | encapsulation and the terminal creation.
133 |
134 | This is as opposed to connecting directly to the
135 | insertPlainText method, e.g.
136 | sys.stdout.write = self.insertPlainText
137 | """
138 | if queue is not None:
139 | while not queue.empty():
140 | _text = queue.get()
141 | self.receive(_text)
142 |
143 | if text is not None:
144 | self.receive(text)
145 |
146 | def receive(self, text):
147 | # textCursor = self.textCursor()
148 | self.moveCursor(QtGui.QTextCursor.End)
149 | self.insertPlainText(text)
150 |
151 | def showEvent(self, event):
152 | super(Terminal, self).showEvent(event)
153 | self.get(queue=sys.stdout.queue)
154 | self.get(queue=sys.stderr.queue)
155 |
156 |
157 |
158 | sys.stdout = sys.__stdout__
159 | sys.stderr = sys.__stderr__
160 | sys.stdin = sys.__stdin__
161 |
162 | sys.stdout = SysOut(sys.__stdout__)
163 | sys.stderr = SysErr(sys.__stderr__)
164 | sys.stdin = SysIn(sys.__stdin__)
165 |
166 | try:
167 | from _fnpython import stderrRedirector, outputRedirector
168 | sys.stdout.SERedirect = outputRedirector
169 | sys.stderr.SERedirect = stderrRedirector
170 | except ImportError:
171 | pass
172 |
173 |
--------------------------------------------------------------------------------
/PythonEditor/app/nukefeatures/nukeinit.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from os.path import dirname
3 |
4 | from PythonEditor.app.nukefeatures import nukedock
5 | from PythonEditor.utils import constants
6 | from PythonEditor.ui.Qt import QtWidgets, QtCore
7 |
8 | try:
9 | import nuke
10 | IN_NUKE_GUI_MODE = nuke.GUI
11 | except ImportError:
12 | IN_NUKE_GUI_MODE = False
13 |
14 |
15 | PANEL_NAME = 'Python.Editor'
16 |
17 | """
18 | The purpose of this command is to fully re-initiliase the
19 | PythonEditor module for two purposes:
20 | 1) to speed up development
21 | 2) to act as a "repair" command in the event PythonEditor
22 | stops working.
23 |
24 | # TODO: could this be in a function? Do I want it to be?
25 | Also, this should close the Python Editor before reloading/restoring
26 | """
27 |
28 | # FIXME: iterating through allWidgets is bad practice and causes
29 | # PySide objects to go out of scope.
30 | RELOAD_CMD = """
31 | # Remove PythonEditor Panel
32 | # ------------------------------------------
33 | from imp import reload
34 | from PythonEditor.ui.Qt import QtWidgets, QtGui, QtCore
35 |
36 | def remove_panel(PANEL_NAME):
37 | for stack in QtWidgets.QApplication.instance().allWidgets():
38 | if not isinstance(stack, QtWidgets.QStackedWidget):
39 | continue
40 | for child in stack.children():
41 | if child.objectName() == PANEL_NAME:
42 | child.deleteLater()
43 |
44 | remove_panel('Python.Editor')
45 |
46 | # Reset standard and error outputs
47 | # ------------------------------------------
48 | try:
49 | sys.stdout.reset()
50 | sys.stderr.reset()
51 | except Exception as e:
52 | print(e)
53 |
54 | # Remove all Python Editor modules
55 | # ------------------------------------------
56 | for m in list(sys.modules.keys()):
57 | if 'PythonEditor' in m:
58 | del sys.modules[m]
59 |
60 | # Reload main module
61 | # ------------------------------------------
62 | import PythonEditor
63 | reload(PythonEditor)
64 |
65 | # Rerun menu setup
66 | # ------------------------------------------
67 | from PythonEditor.app.nukefeatures import nukedock
68 | reload(nukedock)
69 | nukedock.setup_dock()
70 |
71 | from PythonEditor.app.nukefeatures import nukeinit
72 | reload(nukeinit)
73 |
74 | # Re-launch panel
75 | # ------------------------------------------
76 | PythonEditor.app.nukefeatures.nukeinit.add_to_pane()
77 | """
78 |
79 | IMPORT_CMD = (
80 | '__import__("PythonEditor")'
81 | '.app.nukefeatures.nukeinit.add_to_pane()'
82 | )
83 |
84 | ICON_PATH = 'PythonEditor.png'
85 |
86 |
87 | def setup(nuke_menu=False, node_menu=False, pane_menu=True, shortcut='\\'):
88 | """PythonEditor requires the pane menu to be setup in order to
89 | be accessible to the user (without launching the panel
90 | programmatically). The nuke_menu and node_menu exist as optional
91 | extras.
92 |
93 | TODO:
94 | Set this up automatically based on Preferences.
95 | """
96 | if not IN_NUKE_GUI_MODE:
97 | return
98 |
99 | if nuke_menu:
100 | add_nuke_menu(shortcut=shortcut)
101 |
102 | if node_menu:
103 | add_node_menu()
104 |
105 | if pane_menu:
106 | nukedock.setup_dock()
107 |
108 |
109 | def add_nuke_menu(shortcut='\\'):
110 | """Adds a "Panels" menu to the Nuke menubar.
111 | """
112 | try:
113 | package_dir = dirname(dirname(sys.modules['PythonEditor'].__file__))
114 | nuke.pluginAddPath(package_dir)
115 | except Exception as error:
116 | print(error)
117 |
118 | nuke_menu = nuke.menu('Nuke')
119 | panel_menu = nuke_menu.addMenu('Panels')
120 | panel_menu.addCommand(
121 | 'Python Editor',
122 | IMPORT_CMD,
123 | icon=ICON_PATH,
124 | shortcut=shortcut
125 | )
126 |
127 | panel_menu.addCommand(
128 | '[dev] Fully Reload Python Editor',
129 | RELOAD_CMD,
130 | icon=ICON_PATH
131 | )
132 |
133 |
134 | def add_node_menu():
135 | """Adds a menu item to the Node Menu.
136 | """
137 | node_menu = nuke.menu('Nodes')
138 | node_menu.addCommand(
139 | 'Py',
140 | IMPORT_CMD,
141 | icon=ICON_PATH
142 | )
143 |
144 |
145 | def add_to_pane():
146 | """Add or move PythonEditor to the current or default pane. Only
147 | one instance of the PythonEditor widget is allowed at a time.
148 |
149 | BUG: This now seems to disagree greatly with the "Reload Package"
150 | feature, causing many a segfault.
151 | """
152 | # nuke-specific imports in here so that PythonEditor works outside of nuke.
153 | import nuke
154 | from nukescripts.panels import __panels
155 |
156 | # is the active pane one of the ones we want to add Python Editor to?
157 | candidates = ['Viewer.1', 'Properties.1', 'DAG.1']
158 |
159 | for tab_name in candidates:
160 | dock = nuke.getPaneFor(tab_name)
161 | if dock is None:
162 | continue
163 | break
164 | else:
165 | # no "break"? use thisPane
166 | dock = nuke.thisPane()
167 |
168 | import PythonEditor
169 | try:
170 | # if the panel exists already, it's
171 | # likely the user is trying to move it.
172 | ide = PythonEditor.__dock
173 | ide.addToPane(dock)
174 | except AttributeError:
175 | nuke_panel = __panels.get(PANEL_NAME).__call__(pane=dock)
176 |
177 |
--------------------------------------------------------------------------------
/PythonEditor/core/execute.py:
--------------------------------------------------------------------------------
1 | import __main__
2 | import traceback
3 | import re
4 |
5 |
6 | FILENAME = ''
7 | VERBOSITY_LOW = 0
8 | VERBOSITY_HIGH = 1
9 |
10 | def mainexec(text, whole_text, verbosity=VERBOSITY_LOW):
11 | """
12 | Code execution in top level namespace.
13 | Reformats exceptions to remove
14 | references to this file.
15 |
16 | :param text: code to execute
17 | :param whole_text: all text in document
18 | :type text: str
19 | :type whole_text: str
20 | """
21 | if len(text.strip().split('\n')) == 1:
22 | mode = 'single'
23 | else:
24 | mode = 'exec'
25 | try:
26 | _code = compile(text, FILENAME, mode)
27 | except SyntaxError:
28 | error_line_numbers = print_syntax_traceback()
29 | return error_line_numbers
30 |
31 | namespace = __main__.__dict__.copy()
32 | print('# Result: ')
33 | try:
34 | # Ian Thompson is a golden god
35 | exec(_code, __main__.__dict__)
36 | except Exception as e:
37 | error_line_numbers = print_traceback(whole_text, e)
38 | return error_line_numbers
39 | else:
40 | if verbosity == VERBOSITY_LOW:
41 | return
42 | # try to print new assigned values
43 | if mode != 'single':
44 | return
45 |
46 | not_dicts = not all([
47 | isinstance(namespace, dict),
48 | isinstance(__main__.__dict__, dict),
49 | hasattr(namespace, 'values')
50 | ])
51 |
52 | if not_dicts:
53 | return None
54 |
55 | try:
56 | for key, value in __main__.__dict__.items():
57 | if value not in namespace.values():
58 | try:
59 | print(value)
60 | except Exception:
61 | # if the value throws an error here,
62 | # try to remove it from globals.
63 | del __main__.__dict__[key]
64 | except Exception:
65 | # if there's an error in iterating through the
66 | # interpreter globals, restore the globals one
67 | # by one until the offending value is removed.
68 | __copy = __main__.__dict__.copy()
69 | __main__.__dict__.clear()
70 | for key, value in __copy.items():
71 | try:
72 | __main__.__dict__.update({key:value})
73 | except Exception:
74 | print("Couldn't restore ", key, value, "into main globals")
75 |
76 | # TODO: do some logging here.
77 | # sometimes, this causes
78 | # SystemError: Objects/longobject.c:244:
79 | # TypeError('vars() argument must have __dict__ attribute',)
80 | # bad argument to internal function
81 | # NotImplementedError, AttributeError, TypeError, SystemError
82 | return None
83 |
84 |
85 | def print_syntax_traceback():
86 | """
87 | Print traceback without lines of
88 | the error that refer to this file.
89 | """
90 | print('# Python Editor SyntaxError')
91 | formatted_lines = traceback.format_exc().splitlines()
92 | print(formatted_lines[0])
93 | print('\n'.join(formatted_lines[3:]))
94 |
95 | error_line_numbers = []
96 | global FILENAME
97 | pattern = r'(?<="{0}",\sline\s)(\d+)'.format(FILENAME)
98 | for line in formatted_lines:
99 | result = re.search(pattern, line)
100 | if result:
101 | lineno = int(result.group())
102 | error_line_numbers.append(lineno)
103 | return error_line_numbers
104 |
105 |
106 | def print_traceback(whole_text, error):
107 | """
108 | Print traceback ignoring lines that refer to the
109 | external execution python file, using the whole
110 | text of the document. Extracts lines of code from
111 | whole_text that caused the error.
112 |
113 | :param whole_text: all text in document
114 | :param error: python exception object
115 | :type whole_text: str
116 | :type error: exceptions.Exception
117 | """
118 | text_lines = whole_text.split('\n')
119 | num_lines = len(text_lines)
120 |
121 | error_message = traceback.format_exc()
122 |
123 | global FILENAME
124 | pattern = r'(?<="{0}",\sline\s)(\d+)'.format(FILENAME)
125 |
126 | message_lines = []
127 | error_lines = error_message.splitlines()
128 | error = error_lines.pop()
129 | error_line_numbers = []
130 | exec_string = 'exec(_code, __main__.__dict__)'
131 | for line in error_lines:
132 | if (__file__ in line
133 | or exec_string in line):
134 | continue
135 |
136 | message_lines.append(line)
137 |
138 | result = re.search(pattern, line)
139 | if result:
140 | lineno = int(result.group())
141 | while lineno >= num_lines:
142 | # FIXME: this exists to patch a logical fault
143 | # When text is selected and there is no newline
144 | # afterwards, the lineno can exceed the number
145 | # of lines in the text_lines list. ideally, no
146 | # whole_text would be provided that can exceed
147 | # this limit
148 | lineno -= 1
149 | text = ' ' + text_lines[lineno].strip()
150 | message_lines.append(text)
151 | error_line_numbers.append(lineno)
152 | message_lines.append(error)
153 | error_message = '\n'.join(message_lines)
154 | print(error_message)
155 | return error_line_numbers
156 |
--------------------------------------------------------------------------------
/PythonEditor/ui/ide.py:
--------------------------------------------------------------------------------
1 | """ The IDE is a top-level container class that
2 | houses the pythoneditor interface. The point of this
3 | class is to make all subsequent modules fully reloadable from
4 | within the PythonEditor UI for development purposes.
5 |
6 | Example usage:
7 | from PythonEditor.ui import ide
8 |
9 | integrated_development_environment = ide.IDE()
10 | integrated_development_environment.show()
11 | """
12 | import os
13 | import sys
14 | import traceback
15 |
16 | if (sys.version_info.major >= 3
17 | and sys.version_info.minor >= 4):
18 | from importlib import reload as reload_module
19 | else:
20 | from imp import reload as reload_module
21 |
22 | PYTHON_EDITOR_MODULES = []
23 |
24 | # define this before importing PythonEditor modules.
25 | class Finder(object):
26 | """ Keep track of pythoneditor modules loaded
27 | so that they can be reloaded in the same order.
28 | """
29 | _pythoneditor_module_tracker = True
30 | def find_module(self, name, path=''):
31 | if 'PythonEditor' not in name:
32 | return
33 |
34 | global PYTHON_EDITOR_MODULES
35 | if PYTHON_EDITOR_MODULES is None:
36 | return
37 | if name in PYTHON_EDITOR_MODULES:
38 | return
39 | if path is None:
40 | return
41 |
42 | filename = name.split('.').pop()+'.py'
43 | for p in path:
44 | if filename in os.listdir(p):
45 | PYTHON_EDITOR_MODULES.append(name)
46 | return
47 |
48 | def add_to_meta_path():
49 | sys.meta_path = [
50 | i for i in sys.meta_path
51 | if not hasattr(i, '_pythoneditor_module_tracker')
52 | ]
53 | sys.meta_path.append(Finder())
54 | add_to_meta_path()
55 |
56 |
57 | # imports here now that we are done modifying importer
58 | from PythonEditor.ui.Qt.QtWidgets import QWidget, QHBoxLayout
59 | from PythonEditor.ui.Qt.QtCore import QTimer, Qt
60 | from PythonEditor.ui import pythoneditor
61 |
62 |
63 | class IDE(QWidget):
64 | """ Container widget that allows the whole
65 | package to be reloaded, apart from this module.
66 | """
67 | def __init__(self, parent=None):
68 | super(IDE, self).__init__(parent)
69 | self.setLayout(
70 | QHBoxLayout(self)
71 | )
72 | self.layout().setContentsMargins(
73 | 0, 0, 0, 0
74 | )
75 | self.setObjectName('IDE')
76 | self.setWindowTitle('Python Editor')
77 | self.buildUI()
78 |
79 | def buildUI(self):
80 | PE = pythoneditor.PythonEditor
81 | self.python_editor = PE(parent=self)
82 | self.layout().addWidget(self.python_editor)
83 |
84 | def reload_package(self):
85 | """ Reloads the whole package (except for
86 | this module), in an order that does not
87 | cause errors.
88 | """
89 | self.python_editor.terminal.stop()
90 | self.python_editor.deleteLater()
91 | del self.python_editor
92 |
93 | # reload modules in the order
94 | # that they were loaded in
95 | for name in PYTHON_EDITOR_MODULES:
96 | mod = sys.modules.get(name)
97 | if mod is None:
98 | continue
99 |
100 | path = mod.__file__
101 | if path.endswith('.pyc'):
102 | path = path.replace('.pyc', '.py')
103 | if not os.path.isfile(path):
104 | continue
105 | with open(path, 'r') as f:
106 | data = f.read()
107 | if '\x00' in data:
108 | msg = 'Cannot load {0} due to Null bytes. Path:\n{1}'
109 | print(msg.format(mod, path))
110 | continue
111 | try:
112 | code = compile(data, mod.__file__, 'exec')
113 | except SyntaxError:
114 | # This message only shows in terminal
115 | # if this environment variable is set:
116 | # PYTHONEDITOR_CAPTURE_STARTUP_STREAMS
117 | error = traceback.format_exc()
118 | msg = 'Could not reload due to the following error:'
119 | def print_error():
120 | print(msg)
121 | print(error)
122 | QTimer.singleShot(100, print_error)
123 | continue
124 | try:
125 | reload_module(mod)
126 | except ImportError:
127 | msg = 'could not reload {0}: {1}'
128 | print(msg.format(name, mod))
129 |
130 | QTimer.singleShot(1, self.buildUI)
131 | QTimer.singleShot(10, self.set_editor_focus)
132 |
133 | def set_editor_focus(self):
134 | """ Set the focus inside the editor.
135 | """
136 | try:
137 | retries = self.retries
138 | except AttributeError:
139 | self.retries = 0
140 |
141 | if self.retries > 4:
142 | return
143 |
144 | if not hasattr(self, 'python_editor'):
145 | QTimer.singleShot(100, self.set_editor_focus)
146 | self.retries += 1
147 | return
148 |
149 | editor = self.python_editor.tabeditor.editor
150 | if not editor.isVisible():
151 | QTimer.singleShot(100, self.set_editor_focus)
152 | self.retries += 1
153 | return
154 |
155 | # trigger the autosave.
156 | editor.focus_in_signal.emit()
157 |
158 | def showEvent(self, event):
159 | """ Hack to get rid of margins
160 | automatically put in place
161 | by Nuke Dock Window.
162 | """
163 | try:
164 | parent = self.parent()
165 | for x in range(6):
166 | parent.layout(
167 | ).setContentsMargins(
168 | 0, 0, 0, 0
169 | )
170 | parent = parent.parent()
171 | except AttributeError:
172 | pass
173 |
174 | super(IDE, self).showEvent(event)
175 |
--------------------------------------------------------------------------------
/PythonEditor/ui/features/linenumberarea.py:
--------------------------------------------------------------------------------
1 | from PythonEditor.ui.Qt import QtGui
2 | from PythonEditor.ui.Qt import QtCore
3 | from PythonEditor.ui.Qt import QtWidgets
4 | from PythonEditor.utils.constants import IN_NUKE
5 |
6 |
7 | class LineNumberArea(QtWidgets.QWidget):
8 | """
9 | Installs line numbers along
10 | left column of QPlainTextEdit.
11 | """
12 | def __init__(self, editor):
13 | super(LineNumberArea, self).__init__(editor)
14 | self.setObjectName('LineNumberArea')
15 | self.editor = editor
16 | self.setFont(editor.font())
17 | self.setParent(editor)
18 | self.setupLineNumbers()
19 |
20 | def setupLineNumbers(self):
21 |
22 | self.editor.blockCountChanged.connect(self.updateLineNumberAreaWidth)
23 | self.editor.updateRequest.connect(self.updateLineNumberArea)
24 | self.editor.cursorPositionChanged.connect(self.highlightCurrentLine)
25 | self.editor.resize_signal.connect(self.resizeLineNo, QtCore.Qt.DirectConnection)
26 |
27 | self.updateLineNumberAreaWidth(0)
28 | self.highlightCurrentLine()
29 |
30 | def sizeHint(self):
31 | return QtCore.QSize(self.lineNumberAreaWidth(), 0)
32 |
33 | def paintEvent(self, event):
34 | self.lineNumberAreaPaintEvent(event)
35 |
36 | def lineNumberAreaPaintEvent(self, event):
37 | mypainter = QtGui.QPainter(self)
38 |
39 | block = self.editor.firstVisibleBlock()
40 | blockNumber = block.blockNumber()
41 | blockGeo = self.editor.blockBoundingGeometry(block)
42 | top = blockGeo.translated(self.editor.contentOffset()).top()
43 | bottom = top + self.editor.blockBoundingRect(block).height()
44 |
45 | p = self.editor.textCursor().position()
46 | doc = self.editor.document()
47 | current_block = doc.findBlock(p).blockNumber()
48 |
49 | height = self.editor.fontMetrics().height()
50 | while block.isValid() and (top <= event.rect().bottom()):
51 | if not block.isVisible():
52 | continue
53 | if block.isVisible() and (bottom >= event.rect().top()):
54 | number = str(blockNumber + 1)
55 | colour = QtCore.Qt.darkGray
56 | font = self.font()
57 | if blockNumber == current_block:
58 | colour = QtCore.Qt.yellow
59 | font = QtGui.QFont(font)
60 | font.setBold(True)
61 | mypainter.setFont(font)
62 | mypainter.setPen(colour)
63 | mypainter.drawText(
64 | 0,
65 | top,
66 | self.width(),
67 | height,
68 | QtCore.Qt.AlignRight,
69 | number
70 | )
71 |
72 | block = block.next()
73 | top = bottom
74 | bottom = top + self.editor.blockBoundingRect(block).height()
75 | blockNumber += 1
76 |
77 | def lineNumberAreaWidth(self):
78 | digits = 1
79 | count = max(1, self.editor.blockCount())
80 | while count >= 10:
81 | count /= 10
82 | digits += 1
83 | metrics = self.editor.fontMetrics()
84 | try:
85 | space = 3 + metrics.horizontalAdvance('9') * digits
86 | except AttributeError:
87 | space = 3 + metrics.width('9') * digits
88 | space = 30 if space < 30 else space
89 | return space
90 |
91 | def updateLineNumberAreaWidth(self, _):
92 | self.editor.setViewportMargins(
93 | self.lineNumberAreaWidth(), 0, 0, 0
94 | )
95 |
96 | def updateLineNumberArea(self, rect, dy):
97 | if dy:
98 | self.scroll(0, dy)
99 | else:
100 | self.update(
101 | 0,
102 | rect.y(),
103 | self.width(),
104 | rect.height()
105 | )
106 |
107 | if rect.contains(self.editor.viewport().rect()):
108 | self.updateLineNumberAreaWidth(0)
109 |
110 | def highlightCurrentLine(self):
111 | extraSelections = []
112 |
113 | if not self.editor.isReadOnly():
114 | selection = QtWidgets.QTextEdit.ExtraSelection()
115 |
116 | if IN_NUKE:
117 | bg = QtGui.QPalette.Background
118 | colour = self.editor.palette().color(bg).darker(100)
119 | else:
120 | colour = QtGui.QColor.fromRgbF(
121 | 0.196078,
122 | 0.196078,
123 | 0.196078,
124 | 0.500000
125 | )
126 |
127 | selection.format.setBackground(colour)
128 | selection.format.setProperty(
129 | QtGui.QTextFormat.FullWidthSelection,
130 | True
131 | )
132 | selection.cursor = self.editor.textCursor()
133 | selection.cursor.clearSelection()
134 | extraSelections.append(selection)
135 |
136 | self.editor.setExtraSelections(extraSelections)
137 | self.highlight_cell_block()
138 |
139 | def resizeLineNo(self):
140 | cr = self.editor.contentsRect()
141 | rect = QtCore.QRect(
142 | cr.left(),
143 | cr.top(),
144 | self.lineNumberAreaWidth(),
145 | cr.height()
146 | )
147 | self.setGeometry(rect)
148 |
149 | def highlight_cell_block(self):
150 | """
151 | Highlight blocks that start with #&&
152 | These denote a cell block border.
153 | """
154 | extraSelections = self.editor.extraSelections()
155 | doc = self.editor.document()
156 | for i in range(doc.blockCount()):
157 | block = doc.findBlockByLineNumber(i)
158 | text = block.text()
159 | if not text.startswith('#&&'):
160 | continue
161 | selection = QtWidgets.QTextEdit.ExtraSelection()
162 | colour = QtGui.QColor.fromRgbF(1, 1, 1, 0.05)
163 | selection.format.setBackground(colour)
164 | selection.format.setProperty(
165 | QtGui.QTextFormat.FullWidthSelection,
166 | True
167 | )
168 |
169 | cursor = self.editor.textCursor()
170 | cursor.setPosition(block.position())
171 | selection.cursor = cursor
172 | selection.cursor.clearSelection()
173 | extraSelections.append(selection)
174 |
175 | self.editor.setExtraSelections(extraSelections)
176 |
--------------------------------------------------------------------------------
/scripts/prototypes/tabview_demo.py:
--------------------------------------------------------------------------------
1 | from PythonEditor.ui.Qt.QtCore import QPropertyAnimation, Qt, Slot, QTimer
2 | from PythonEditor.ui.Qt.QtGui import QStandardItem, QFont
3 | from PythonEditor.ui.Qt.QtWidgets import (
4 | QWidget,
5 | QSplitter,
6 | QSizePolicy,
7 | QGridLayout,
8 | QVBoxLayout,
9 | QHBoxLayout,
10 | QToolButton,
11 | QPushButton,
12 | QPlainTextEdit,
13 | QDataWidgetMapper,
14 | )
15 |
16 | from PythonEditor.ui.editor import Editor
17 | from PythonEditor.ui.tabview import TabView, ListView
18 | from PythonEditor.models.xmlmodel import XMLModel
19 | from PythonEditor.ui import terminal
20 |
21 | from PythonEditor.ui import menubar
22 | from PythonEditor.ui.features import shortcuts
23 | from PythonEditor.ui.features import actions
24 | from PythonEditor.ui.features import autosavexml
25 | from PythonEditor.ui.dialogs import preferences
26 | from PythonEditor.ui.dialogs import shortcuteditor
27 |
28 |
29 | class PythonEditor(QWidget):
30 | def __init__(self):
31 | """ Brings together the data model (currently XMLModel),
32 | along with several views - Editor, TabView, ListView, and QDataWidgetMapper.
33 | """
34 | super(PythonEditor, self).__init__()
35 | self.model = XMLModel()
36 |
37 | # create widgets
38 | self.tab_view = TabView()
39 |
40 | self.editor = Editor()
41 | self.editor.setFont(QFont('DejaVu Sans'))
42 |
43 | self.list_view = ListView()
44 | # self.list_view.setMaximumWidth(0)
45 | # self.list_view.resize(50, self.list_view.height())
46 |
47 | self.listButton = QToolButton()
48 | self.listButton.setArrowType(Qt.DownArrow)
49 | self.listButton.setFixedSize(24, 24)
50 |
51 | # layout
52 | self.tab_widget = QWidget(self)
53 | tab_layout = QHBoxLayout(self.tab_widget)
54 | tab_layout.setContentsMargins(0,0,0,0)
55 | tab_layout.addWidget(self.tab_view)
56 | tab_layout.addWidget(self.listButton)
57 |
58 | self.bottom_splitter = QSplitter(Qt.Horizontal)
59 | self.bottom_splitter.addWidget(self.editor)
60 | self.bottom_splitter.addWidget(self.list_view)
61 | self.bottom_splitter.setSizes([300,20])
62 | policy = QSizePolicy.Expanding
63 | self.bottom_splitter.setSizePolicy(policy, policy)
64 |
65 | self.terminal = terminal.Terminal()
66 | self.tabeditor = QWidget(self)
67 | bottom_layout = QVBoxLayout(self.tabeditor)
68 | bottom_layout.setContentsMargins(0,0,0,0)
69 | bottom_layout.addWidget(self.tab_widget)
70 | bottom_layout.addWidget(self.bottom_splitter)
71 |
72 | layout = QVBoxLayout(self)
73 | layout.setObjectName(
74 | 'PythonEditor_MainLayout'
75 | )
76 | layout.setContentsMargins(0,0,0,0)
77 | layout.addWidget(self.tabeditor)
78 | self.setLayout(layout)
79 |
80 | splitter = QSplitter(
81 | Qt.Vertical
82 | )
83 | splitter.setObjectName(
84 | 'PythonEditor_MainVerticalSplitter'
85 | )
86 | splitter.addWidget(self.terminal)
87 | splitter.addWidget(self.tabeditor)
88 |
89 | layout.addWidget(splitter)
90 | self.splitter = splitter
91 |
92 | # act = actions.Actions(
93 | # pythoneditor=self,
94 | # editor=self.editor,
95 | # tabeditor=self.tabeditor,
96 | # terminal=self.terminal,
97 | # )
98 |
99 | # sch = shortcuts.ShortcutHandler(
100 | # editor=self.editor,
101 | # tabeditor=self.tabeditor,
102 | # terminal=self.terminal,
103 | # )
104 |
105 | # self.menubar = menubar.MenuBar(self)
106 |
107 | SE = shortcuteditor.ShortcutEditor
108 | self.shortcuteditor = SE(
109 | editor=self.editor,
110 | tabeditor=self.tabeditor,
111 | terminal=self.terminal
112 | )
113 |
114 | PE = preferences.PreferencesEditor
115 | self.preferenceseditor = PE()
116 |
117 | # set model on views
118 | self.list_view.setModel(self.model)
119 | self.tab_view.setModel(self.model)
120 |
121 | # add model mapper
122 | self.mapper = QDataWidgetMapper(self)
123 | self.mapper.setModel(self.model)
124 | self.mapper.addMapping(self.editor, 1)
125 |
126 | # connect signals
127 | self.selection_model = self.list_view.selectionModel()
128 | self.selection_model.currentRowChanged.connect(self.mapper.setCurrentModelIndex)
129 | self.tab_view.currentChanged.connect(self.mapper.setCurrentIndex)
130 | self.tab_view.setSelectionModel(self.selection_model)
131 | self.editor.modificationChanged.connect(self.submit_and_refocus)
132 | self.mapper.currentIndexChanged.connect(self.tab_view.setCurrentIndex)
133 | self.mapper.currentIndexChanged.connect(self.setListIndex)
134 | self.model.row_moved.connect(self.tab_view.swap_tabs)
135 | self.listButton.clicked.connect(self.animate_listview)
136 |
137 | # get first item from model - this is where we would restore last viewed tab
138 | self.mapper.toFirst()
139 |
140 | # self.list_view.installEventFilter(self)
141 |
142 | # def eventFilter(self, obj, event):
143 | # print event.type()
144 | # return super(PythonEditor, self).eventFilter(obj, event)
145 |
146 | def animate_listview(self, start=0, end=100):
147 | anim = QPropertyAnimation(
148 | self.list_view,
149 | 'maximumWidth'
150 | )
151 | self._anim = anim
152 |
153 | def release():
154 | self.list_view.setMaximumWidth(2000)
155 | if self.list_view.maximumWidth() != 0:
156 | start, end = end, start
157 | self.listButton.setArrowType(Qt.DownArrow)
158 | else:
159 | self.listButton.setArrowType(Qt.RightArrow)
160 | anim.finished.connect(release)
161 |
162 | anim.setStartValue(start)
163 | anim.setEndValue(end)
164 | anim.setDuration(80)
165 | anim.start()
166 |
167 | @Slot()
168 | def submit_and_refocus(self):
169 | cursor = self.editor.textCursor()
170 | self.mapper.submit()
171 | self.editor.setTextCursor(cursor)
172 |
173 | @Slot(int)
174 | def setListIndex(self, row):
175 | index = self.model.index(row, 0)
176 | self.list_view.setCurrentIndex(index)
177 |
178 |
179 | if __name__ == '__main__':
180 | w = PythonEditor()
181 | w.show()
182 | w.resize(800, 400)
--------------------------------------------------------------------------------
/scripts/prototypes/terminal_dict.py:
--------------------------------------------------------------------------------
1 | """
2 | This module needs to satisfy the following requirements:
3 |
4 | - [ ] Redirect stdout to all Python Editor Terminal QPlainTextEdits
5 | - [ ] Preserve unread output in Queue objects, which are read when loading the Terminal(s)
6 | - [ ] Be reloadable without losing stdout connections
7 | (currently doesn't pick up on queue object because stdout is wiped on reload)
8 | - [ ] Not keep references to destroyed objects
9 |
10 |
11 | # would sys.displayhook be useful in here? No, displayhook is for registering
12 | objects that are also printed.
13 |
14 | """
15 |
16 |
17 | import sys
18 | import os
19 | from Queue import Queue
20 |
21 | from PythonEditor.ui.Qt import QtWidgets, QtCore, QtGui
22 |
23 |
24 | # ----- override nuke FnRedirect -----
25 | class VirtualModule(object):
26 | pass
27 |
28 |
29 | class Loader(object):
30 | def load_module(self, name):
31 | try:
32 | from _fnpython import stderrRedirector, outputRedirector
33 | sys.outputRedirector = outputRedirector
34 | sys.stderrRedirector = stderrRedirector
35 | finally:
36 | # firmly block all imports of the module
37 | return VirtualModule()
38 |
39 |
40 | class Finder(object):
41 | _deletable = ''
42 |
43 | def find_module(self, name, path=''):
44 | if 'FnRedirect' in name:
45 | return Loader()
46 |
47 |
48 | sys.meta_path = [i for i in sys.meta_path
49 | if not hasattr(i, '_deletable')]
50 | sys.meta_path.append(Finder())
51 | # ----- end override section -----
52 |
53 |
54 | class Signal(QtCore.QObject):
55 | s = QtCore.Signal(str)
56 | receivers = {
57 | '': [],
58 | '': []
59 | }
60 |
61 | def customEvent(self, event):
62 |
63 | for func in self.receivers[event.print_type]:
64 | func(text=event.text, queue=event.queue)
65 |
66 | if event.text is None:
67 | return
68 |
69 | if event.print_type == '':
70 | sys.outputRedirector(event.text)
71 | elif event.print_type == '':
72 | sys.stderrRedirector(event.text)
73 |
74 |
75 | class PrintEvent(QtCore.QEvent):
76 | def __init__(self, text=None, queue=None):
77 | self.text = text
78 | self.queue = queue
79 | super(PrintEvent, self).__init__(QtCore.QEvent.User)
80 |
81 |
82 | class PySingleton(object):
83 | """
84 | Return a single instance of a class
85 | or create a new instance if none exists.
86 | """
87 | def __new__(cls, *args, **kwargs):
88 | if '_the_instance' not in cls.__dict__:
89 | cls._the_instance = object.__new__(cls)
90 | return cls._the_instance
91 |
92 |
93 | class Redirect(object):
94 | def __init__(self, stream, queue=Queue()):
95 | self.stream = stream
96 | self.signal = Signal()
97 | self.queue = queue
98 |
99 | for a in dir(stream):
100 | try:
101 | getattr(self, a)
102 | except AttributeError:
103 | attr = getattr(stream, a)
104 | setattr(self, a, attr)
105 |
106 | def write(self, text):
107 | self.stream.write(text)
108 |
109 | queue = self.queue
110 | r = self.signal.receivers
111 | receivers = r['']+r[''] # this is ugly
112 | if bool(receivers):
113 | queue = None
114 | else:
115 | queue.put(text)
116 |
117 | app = QtWidgets.QApplication.instance()
118 | event = PrintEvent(text=text, queue=queue)
119 | event.print_type = self.stream.name
120 | app.postEvent(self.signal, event, -2)
121 |
122 |
123 | class SysOut(Redirect, PySingleton):
124 | pass
125 |
126 |
127 | class SysErr(Redirect, PySingleton):
128 | pass
129 |
130 |
131 | class SysIn(Redirect, PySingleton):
132 | pass
133 |
134 |
135 | class Terminal(QtWidgets.QPlainTextEdit):
136 | def __init__(self):
137 | super(Terminal, self).__init__()
138 | self.setReadOnly(True)
139 | self.setObjectName('Terminal')
140 | self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
141 |
142 | def get(self, text=None, queue=None, name=None):
143 | """
144 | The get method allows the terminal to pick up
145 | on output created between the stream object
146 | encapsulation and the terminal creation.
147 |
148 | This is as opposed to connecting directly to the
149 | insertPlainText method, e.g.
150 | sys.stdout.write = self.insertPlainText
151 |
152 | !Warning! Don't print anything in here, it
153 | will cause an infinite loop.
154 | """
155 |
156 | if name is not None:
157 | self.receive(name)
158 |
159 | if queue is not None:
160 | while not queue.empty():
161 | _text = queue.get()
162 | self.receive(_text)
163 |
164 | if text is not None:
165 | self.receive(text)
166 |
167 | def receive(self, text):
168 | # textCursor = self.textCursor()
169 | self.moveCursor(QtGui.QTextCursor.End)
170 | self.insertPlainText(text)
171 |
172 | def showEvent(self, event):
173 |
174 | sys.stdout.signal.receivers[''].append(self.get)
175 | sys.stderr.signal.receivers[''].append(self.get)
176 |
177 | for stream in sys.stdout, sys.stderr:
178 | self.get(queue=stream.queue)
179 |
180 | super(Terminal, self).showEvent(event)
181 |
182 | def hideEvent(self, event):
183 |
184 | sys.stdout.signal.receivers[''].remove(self.get)
185 | sys.stderr.signal.receivers[''].remove(self.get)
186 |
187 | super(Terminal, self).hideEvent(event)
188 |
189 | def closeEvent(self, event):
190 | super(Terminal, self).closeEvent(event)
191 |
192 |
193 | # before we reset stdout and err, try to recover their queues
194 | out_queue = getattr(sys.stdout, 'queue', Queue())
195 | err_queue = getattr(sys.stderr, 'queue', Queue())
196 |
197 | # reset stdout, stderr, stdin:
198 | sys.stdout = sys.__stdout__
199 | sys.stderr = sys.__stderr__
200 | sys.stdin = sys.__stdin__
201 |
202 | # override stdout, stderr, stdin
203 | sys.stdout = SysOut(sys.__stdout__, queue=out_queue)
204 | sys.stderr = SysErr(sys.__stderr__, queue=err_queue)
205 | sys.stdin = SysIn(sys.__stdin__)
206 |
207 | # we need these functions to be registered in the sys module
208 | sys.outputRedirector = lambda x: None
209 | sys.stderrRedirector = lambda x: None
210 |
211 | # in case we decide to reload the module, we need to
212 | # re-add the functions to write to Nuke's Script Editor.
213 | try:
214 | from _fnpython import stderrRedirector, outputRedirector
215 | sys.outputRedirector = outputRedirector
216 | sys.stderrRedirector = stderrRedirector
217 | except ImportError:
218 | pass
219 |
220 |
221 |
--------------------------------------------------------------------------------
/scripts/prototypes/qtabbar_paint.py:
--------------------------------------------------------------------------------
1 | # cleaner tabbar implementation for pythoneditor
2 | """
3 | TODO:
4 | [x] Close button - permanent on selected tab, available over hovered tab
5 | [x] Line edit for editing tab name
6 | [x] Tab width/height controlled without stylesheets
7 | [x] Tab tooltips with setTabToolTip()
8 | [x] Line edit should quit on esc and focus out
9 | [x] middle click to close tab
10 | [ ] Get rid of the RHS <> buttons, add custom ones
11 | [ ] Offset tab text a little to the left (probably need to paint it)
12 | [ ] Draw close button when tab is moving
13 | [ ] use QDataWidgetMapper to map tabs to XML data
14 | """
15 | try:
16 | from PySide2.QtWidgets import *
17 | from PySide2.QtCore import *
18 | from PySide2.QtGui import *
19 | except ImportError:
20 | from PySide.QtCore import *
21 | from PySide.QtGui import *
22 |
23 |
24 | class NameEdit(QLineEdit):
25 | def __init__(self, parent=None):
26 | super(NameEdit, self).__init__(parent=parent)
27 | self.setTextMargins(3,1,3,1)
28 | self.setWindowFlags(Qt.FramelessWindowHint)
29 |
30 | def keyPressEvent(self, event):
31 | QLineEdit.keyPressEvent(self, event)
32 | if event.key()==Qt.Key_Escape:
33 | self.hide()
34 |
35 |
36 | class Tabs(QTabBar):
37 | def __init__(self):
38 | super(Tabs, self).__init__()
39 | self.hovered_index = -1
40 | self.close_button_hovered = -1
41 | self.close_button_pressed = -1
42 |
43 | self.setSelectionBehaviorOnRemove(QTabBar.SelectPreviousTab)
44 | self.setMovable(True)
45 | self.setMouseTracking(True)
46 |
47 | self.line_edit = NameEdit(self)
48 | self.line_edit.editingFinished.connect(self.name_edited)
49 | self.line_edit.hide()
50 |
51 | # self.currentChanged.connect(self.set_current)
52 |
53 | # def tabRemoved(self, index):
54 | # QTabBar.tabRemoved(self, index)
55 |
56 | # def tabRect(self, index):
57 | # rect = QTabBar.tabRect(self, index)
58 | ## print 'rect', rect
59 | # rect.adjust(0,0,0,0)
60 | # return rect
61 |
62 | def tabSizeHint(self, index):
63 | size = QTabBar.tabSizeHint(self, index)
64 | size.setWidth(size.width()+50)
65 | size.setHeight(size.height()+6)
66 | return size
67 |
68 | def mouseDoubleClickEvent(self, event):
69 | if event.button() == Qt.LeftButton:
70 | index = self.tabAt(event.pos())
71 | rect = self.get_close_button_rect(index)
72 | if not rect.contains(event.pos()):
73 | self.show_line_edit(index)
74 | else:
75 | QTabBar.mouseDoubleClickEvent(self, event)
76 |
77 | def mousePressEvent(self, event):
78 | if event.button()==Qt.LeftButton:
79 | self.line_edit.hide()
80 |
81 | index = self.tabAt(event.pos())
82 | rect = self.get_close_button_rect(index)
83 | if rect.contains(event.pos()):
84 | self.close_button_pressed = index
85 | self.update()
86 | return
87 | elif event.button()==Qt.MidButton:
88 | index = self.tabAt(event.pos())
89 | self.removeTab(index)
90 | QTabBar.mousePressEvent(self, event)
91 |
92 | def mouseReleaseEvent(self, event):
93 | if event.button()==Qt.LeftButton:
94 | index = self.tabAt(event.pos())
95 | rect = self.get_close_button_rect(index)
96 | if rect.contains(event.pos()):
97 | self.close_button_pressed = -1
98 | self.update()
99 | self.removeTab(index)
100 | return
101 | QTabBar.mouseReleaseEvent(self, event)
102 |
103 | def mouseMoveEvent(self, event):
104 | QTabBar.mouseMoveEvent(self, event)
105 |
106 | index = self.tabAt(event.pos())
107 | if index != self.hovered_index:
108 | self.hovered_index = index
109 |
110 | index = self.tabAt(event.pos())
111 | rect = self.get_close_button_rect(index)
112 | if rect.contains(event.pos()):
113 | self.close_button_hovered = index
114 | else:
115 | self.close_button_hovered = -1
116 |
117 | self.update()
118 |
119 | def paintEvent(self, event):
120 | QTabBar.paintEvent(self, event)
121 |
122 | """ # TODO: First step - draw the text! then tab controls
123 | painter = QPainter(self)
124 | painter.save()
125 | for i in range(self.count()):
126 | rect = self.tabRect(i)
127 | if event.rect().contains(rect):
128 | text = self.tabText(i)
129 | painter.drawText(rect.bottomLeft(), text)
130 | painter.restore()
131 | """
132 |
133 | index = self.currentIndex()
134 | self.draw_close_button(index)
135 | if self.hovered_index not in [index, -1]:
136 | self.draw_close_button(self.hovered_index)
137 |
138 | def draw_close_button(self, index):
139 | painter = QPainter(self)
140 | painter.save()
141 |
142 | opt = QStyleOption()
143 | opt.initFrom(self)
144 | opt.rect = self.get_close_button_rect(index)
145 | if self.close_button_pressed == index:
146 | opt.state |= QStyle.State_Sunken
147 | elif self.close_button_hovered == index:
148 | opt.state |= QStyle.State_Raised
149 | else:
150 | opt.state = QStyle.State_Enabled
151 |
152 | self.style().drawPrimitive(
153 | QStyle.PE_IndicatorTabClose,
154 | opt,
155 | painter,
156 | self
157 | )
158 | painter.restore()
159 |
160 | # def set_current(self, index):
161 | # print index
162 |
163 | def get_close_button_rect(self, index):
164 | rect = self.tabRect(index)
165 | size = self.tabSizeHint(index)
166 | h = size.height()/2
167 | return QRect(-25, h-8, 16, 16).translated(rect.topRight())
168 |
169 | def show_line_edit(self, index):
170 | self.line_edit.current_index = index
171 | rect = self.tabRect(index)
172 | self.line_edit.setGeometry(rect)
173 | text = self.tabText(index)
174 | self.line_edit.setText(text)
175 | self.line_edit.show()
176 | self.line_edit.selectAll()
177 | self.line_edit.setFocus(Qt.MouseFocusReason)
178 |
179 | def name_edited(self):
180 | self.setTabText(self.line_edit.current_index, self.line_edit.text())
181 | self.line_edit.hide()
182 |
183 |
184 |
185 | tab = Tabs()
186 | self = tab # for dev purposes
187 | for i in range(20):
188 | tab.addTab('test %i'%i)
189 | tab.setTabToolTip(i, 'test %i'%i)
190 |
191 | w = QWidget()
192 | w.setLayout(QVBoxLayout(w))
193 | w.layout().addWidget(tab)
194 | w.show()
--------------------------------------------------------------------------------
/scripts/prototypes/terminal_events.py:
--------------------------------------------------------------------------------
1 | """
2 | This module needs to satisfy the following requirements:
3 |
4 | - [ ] Redirect stdout to all Python Editor Terminal QPlainTextEdits
5 | - [ ] Preserve unread output in Queue objects, which are read when loading the Terminal(s)
6 | - [ ] Be reloadable without losing stdout connections
7 | (currently doesn't pick up on queue object because stdout is wiped on reload)
8 | - [ ] Not keep references to destroyed objects
9 |
10 |
11 | """
12 |
13 |
14 | import sys
15 | import os
16 | from Queue import Queue
17 |
18 | sys.dont_write_bytecode = True
19 | TESTS_DIR = os.path.dirname(__file__)
20 | PACKAGE_PATH = os.path.dirname(TESTS_DIR)
21 | sys.path.append(PACKAGE_PATH)
22 |
23 |
24 | from PythonEditor.ui.Qt import QtWidgets, QtCore, QtGui
25 |
26 |
27 | # ----- override nuke FnRedirect -----
28 | class VirtualModule(object):
29 | pass
30 |
31 | sys.outputRedirector = lambda x: None
32 | sys.stderrRedirector = lambda x: None
33 |
34 | class Loader(object):
35 | def load_module(self, name):
36 | print(name)
37 | try:
38 | from _fnpython import stderrRedirector, outputRedirector
39 | sys.outputRedirector = outputRedirector
40 | sys.stderrRedirector = stderrRedirector
41 | finally:
42 | # firmly block all imports of the module
43 | return VirtualModule()
44 |
45 |
46 | class Finder(object):
47 | _deletable = ''
48 | def find_module(self, name, path=''):
49 | if 'FnRedirect' in name:
50 | return Loader()
51 |
52 |
53 | sys.meta_path = [i for i in sys.meta_path
54 | if not hasattr(i, '_deletable')]
55 | sys.meta_path.append(Finder())
56 | # ----- end override section -----
57 |
58 |
59 | class Relay(QtCore.QObject):
60 | def customEvent(self, event):
61 | pass
62 | # if event.print_type == '':
63 | # sys.outputRedirector(event.text)
64 | # elif event.print_type == '':
65 | # sys.stderrRedirector(event.text)
66 |
67 |
68 | class PrintEvent(QtCore.QEvent):
69 | def __init__(self, text=None, queue=None, print_type=None):
70 | super(PrintEvent, self).__init__(QtCore.QEvent.User)
71 | self.text = text
72 | self.queue = queue
73 |
74 |
75 | class PySingleton(object):
76 | """
77 | Return a single instance of a class
78 | or create a new instance if none exists.
79 | """
80 | def __new__(cls, *args, **kwargs):
81 | if '_the_instance' not in cls.__dict__:
82 | cls._the_instance = object.__new__(cls)
83 | return cls._the_instance
84 |
85 |
86 | class Redirect(object):
87 | def __init__(self, stream, queue=Queue()):
88 | self.stream = stream
89 | self.queue = queue
90 | self.receivers = []
91 |
92 | for a in dir(stream):
93 | try:
94 | getattr(self, a)
95 | except AttributeError:
96 | attr = getattr(stream, a)
97 | setattr(self, a, attr)
98 |
99 | def write(self, text):
100 | # self.stream.write(text)
101 |
102 | if not self.receivers:
103 | self.queue.put(text)
104 |
105 | app = QtWidgets.QApplication.instance()
106 | for receiver in self.receivers:
107 | event = PrintEvent(text=text)
108 | app.postEvent(receiver, event, -2)
109 |
110 |
111 | class SysOut(Redirect, PySingleton):
112 | pass
113 | class SysErr(Redirect, PySingleton):
114 | pass
115 | class SysIn(Redirect, PySingleton):
116 | pass
117 |
118 | import time
119 |
120 | class Terminal(QtWidgets.QPlainTextEdit):
121 | def __init__(self):
122 | super(Terminal, self).__init__()
123 | self.setReadOnly(True)
124 | self.setObjectName('Terminal')
125 | self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
126 |
127 | self.queue = Queue()
128 | self.timer = QtCore.QTimer()
129 | self.timer.timeout.connect(self.check_queue)
130 | self.timer.setInterval(10)
131 | self.timer.start()
132 |
133 | self.interval = time.time()
134 |
135 | def get(self, text=None, queue=None, name=None):
136 | """
137 | The get method allows the terminal to pick up
138 | on output created between the stream object
139 | encapsulation and the terminal creation.
140 |
141 | This is as opposed to connecting directly to the
142 | insertPlainText method, e.g.
143 | sys.stdout.write = self.insertPlainText
144 |
145 | !Warning! Don't print anything in here, it
146 | will cause an infinite loop.
147 | """
148 |
149 | if name is not None:
150 | self.receive(name)
151 |
152 | if queue is not None:
153 | while not queue.empty():
154 | _text = queue.get()
155 | self.receive(_text)
156 |
157 | if text is not None:
158 | self.receive(text)
159 |
160 | def receive(self, text):
161 | self.moveCursor(QtGui.QTextCursor.End)
162 | self.insertPlainText(text)
163 |
164 | def customEvent(self, event):
165 | if (time.time()-self.interval) < 0.2:
166 | self.queue.put(event.text)
167 | else:
168 | self.receive(event.text)
169 | self.interval = time.time()
170 |
171 | @QtCore.Slot()
172 | def check_queue(self):
173 | chunk = ''
174 | n = 0
175 | while not self.queue.empty():
176 | chunk += self.queue.get()
177 | n += 1
178 | if n > 10:
179 | break
180 | if len(chunk) > 3000:
181 | break
182 | # self.get(queue=self.queue)
183 | self.receive(chunk)
184 | # self.timer.stop()
185 |
186 | def showEvent(self, event):
187 |
188 | sys.stdout.receivers.append(self)
189 | sys.stderr.receivers.append(self)
190 |
191 | for stream in sys.stdout, sys.stderr:
192 | self.get(queue=stream.queue)
193 |
194 | self.get(queue=self.queue)
195 | super(Terminal, self).showEvent(event)
196 |
197 | def hideEvent(self, event):
198 |
199 | for r in sys.stdout.receivers, sys.stderr.receivers:
200 | while self in r:
201 | r.remove(self)
202 |
203 | super(Terminal, self).hideEvent(event)
204 |
205 |
206 | # before we reset stdout and err, try to recover their queues
207 | out_queue = getattr(sys.stdout, 'queue', Queue())
208 | err_queue = getattr(sys.stderr, 'queue', Queue())
209 |
210 | # reset stdout, stderr, stdin:
211 | sys.stdout = sys.__stdout__
212 | sys.stderr = sys.__stderr__
213 | sys.stdin = sys.__stdin__
214 |
215 | # override stdout, stderr, stdin
216 | sys.stdout = SysOut(sys.__stdout__, queue=out_queue)
217 | sys.stderr = SysErr(sys.__stderr__, queue=err_queue)
218 | sys.stdin = SysIn(sys.__stdin__)
219 |
220 | # in case we decide to reload the module, we need to
221 | # re-add the functions to write to Nuke's Script Editor.
222 | try:
223 | from _fnpython import stderrRedirector, outputRedirector
224 | sys.outputRedirector = outputRedirector
225 | sys.stderrRedirector = stderrRedirector
226 | except ImportError:
227 | pass
228 |
229 |
--------------------------------------------------------------------------------
/PythonEditor/ui/features/nukepalette.py:
--------------------------------------------------------------------------------
1 | from PythonEditor.ui.Qt import QtCore, QtGui
2 |
3 | QPalette = QtGui.QPalette
4 | QBrush = QtGui.QBrush
5 | QColor = QtGui.QColor
6 |
7 |
8 | def getNukePalette():
9 | """
10 | #get macro for QApplication
11 | from PySide.QtGui import qApp
12 | #get palette
13 | nukePalette = qApp.palette()
14 | #apply palette
15 | myWindow.setPalette(nukePalette)
16 |
17 | else use the palette from below. Note that it is difficult to mix
18 | palettes and style sheets, it always gave me weird results.
19 | """
20 | palette = QPalette()
21 |
22 | brush = QBrush(QColor(255, 255, 255))
23 | brush.setStyle(QtCore.Qt.SolidPattern)
24 | palette.setBrush(QPalette.Active, QPalette.WindowText, brush)
25 |
26 | brush = QBrush(QColor(80, 80, 80))
27 | brush.setStyle(QtCore.Qt.SolidPattern)
28 | palette.setBrush(QPalette.Active, QPalette.Button, brush)
29 |
30 | brush = QBrush(QColor(75, 75, 75))
31 | brush.setStyle(QtCore.Qt.SolidPattern)
32 | palette.setBrush(QPalette.Active, QPalette.Light, brush)
33 |
34 | brush = QBrush(QColor(62, 62, 62))
35 | brush.setStyle(QtCore.Qt.SolidPattern)
36 | palette.setBrush(QPalette.Active, QPalette.Midlight, brush)
37 |
38 | brush = QBrush(QColor(25, 25, 25))
39 | brush.setStyle(QtCore.Qt.SolidPattern)
40 | palette.setBrush(QPalette.Active, QPalette.Dark, brush)
41 |
42 | brush = QBrush(QColor(33, 33, 33))
43 | brush.setStyle(QtCore.Qt.SolidPattern)
44 | palette.setBrush(QPalette.Active, QPalette.Mid, brush)
45 |
46 | brush = QBrush(QColor(245, 245, 245))
47 | brush.setStyle(QtCore.Qt.SolidPattern)
48 | palette.setBrush(QPalette.Active, QPalette.Text, brush)
49 |
50 | brush = QBrush(QColor(255, 255, 255))
51 | brush.setStyle(QtCore.Qt.SolidPattern)
52 | palette.setBrush(QPalette.Active, QPalette.BrightText, brush)
53 |
54 | brush = QBrush(QColor(255, 255, 255))
55 | brush.setStyle(QtCore.Qt.SolidPattern)
56 | palette.setBrush(QPalette.Active, QPalette.ButtonText, brush)
57 |
58 | brush = QBrush(QColor(58, 58, 58))
59 | brush.setStyle(QtCore.Qt.SolidPattern)
60 | palette.setBrush(QPalette.Active, QPalette.Base, brush)
61 |
62 | brush = QBrush(QColor(50, 50, 50))
63 | brush.setStyle(QtCore.Qt.SolidPattern)
64 | palette.setBrush(QPalette.Active, QPalette.Window, brush)
65 |
66 | brush = QBrush(QColor(0, 0, 0))
67 | brush.setStyle(QtCore.Qt.SolidPattern)
68 | palette.setBrush(QPalette.Active, QPalette.Shadow, brush)
69 |
70 | brush = QBrush(QColor(247, 147, 30))
71 | brush.setStyle(QtCore.Qt.SolidPattern)
72 | palette.setBrush(QPalette.Active, QPalette.Highlight, brush)
73 |
74 | brush = QBrush(QColor(25, 25, 25))
75 | brush.setStyle(QtCore.Qt.SolidPattern)
76 | palette.setBrush(QPalette.Active, QPalette.AlternateBase, brush)
77 |
78 | brush = QBrush(QColor(255, 255, 220))
79 | brush.setStyle(QtCore.Qt.SolidPattern)
80 | palette.setBrush(QPalette.Active, QPalette.ToolTipBase, brush)
81 |
82 | brush = QBrush(QColor(0, 0, 0))
83 | brush.setStyle(QtCore.Qt.SolidPattern)
84 | palette.setBrush(QPalette.Active, QPalette.ToolTipText, brush)
85 |
86 | brush = QBrush(QColor(255, 255, 255))
87 | brush.setStyle(QtCore.Qt.SolidPattern)
88 | palette.setBrush(QPalette.Inactive, QPalette.WindowText, brush)
89 |
90 | brush = QBrush(QColor(80, 80, 80))
91 | brush.setStyle(QtCore.Qt.SolidPattern)
92 | palette.setBrush(QPalette.Inactive, QPalette.Button, brush)
93 |
94 | brush = QBrush(QColor(75, 75, 75))
95 | brush.setStyle(QtCore.Qt.SolidPattern)
96 | palette.setBrush(QPalette.Inactive, QPalette.Light, brush)
97 |
98 | brush = QBrush(QColor(62, 62, 62))
99 | brush.setStyle(QtCore.Qt.SolidPattern)
100 | palette.setBrush(QPalette.Inactive, QPalette.Midlight, brush)
101 |
102 | brush = QBrush(QColor(25, 25, 25))
103 | brush.setStyle(QtCore.Qt.SolidPattern)
104 | palette.setBrush(QPalette.Inactive, QPalette.Dark, brush)
105 |
106 | brush = QBrush(QColor(33, 33, 33))
107 | brush.setStyle(QtCore.Qt.SolidPattern)
108 | palette.setBrush(QPalette.Inactive, QPalette.Mid, brush)
109 |
110 | brush = QBrush(QColor(245, 245, 245))
111 | brush.setStyle(QtCore.Qt.SolidPattern)
112 | palette.setBrush(QPalette.Inactive, QPalette.Text, brush)
113 |
114 | brush = QBrush(QColor(255, 255, 255))
115 | brush.setStyle(QtCore.Qt.SolidPattern)
116 | palette.setBrush(QPalette.Inactive, QPalette.BrightText, brush)
117 |
118 | brush = QBrush(QColor(255, 255, 255))
119 | brush.setStyle(QtCore.Qt.SolidPattern)
120 | palette.setBrush(QPalette.Inactive, QPalette.ButtonText, brush)
121 |
122 | brush = QBrush(QColor(58, 58, 58))
123 | brush.setStyle(QtCore.Qt.SolidPattern)
124 | palette.setBrush(QPalette.Inactive, QPalette.Base, brush)
125 |
126 | brush = QBrush(QColor(50, 50, 50))
127 | brush.setStyle(QtCore.Qt.SolidPattern)
128 | palette.setBrush(QPalette.Inactive, QPalette.Window, brush)
129 |
130 | brush = QBrush(QColor(0, 0, 0))
131 | brush.setStyle(QtCore.Qt.SolidPattern)
132 | palette.setBrush(QPalette.Inactive, QPalette.Shadow, brush)
133 |
134 | brush = QBrush(QColor(247, 147, 30))
135 | brush.setStyle(QtCore.Qt.SolidPattern)
136 | palette.setBrush(QPalette.Inactive, QPalette.Highlight, brush)
137 |
138 | brush = QBrush(QColor(25, 25, 25))
139 | brush.setStyle(QtCore.Qt.SolidPattern)
140 | palette.setBrush(QPalette.Inactive, QPalette.AlternateBase, brush)
141 |
142 | brush = QBrush(QColor(255, 255, 220))
143 | brush.setStyle(QtCore.Qt.SolidPattern)
144 | palette.setBrush(QPalette.Inactive, QPalette.ToolTipBase, brush)
145 |
146 | brush = QBrush(QColor(0, 0, 0))
147 | brush.setStyle(QtCore.Qt.SolidPattern)
148 | palette.setBrush(QPalette.Inactive, QPalette.ToolTipText, brush)
149 |
150 | brush = QBrush(QColor(25, 25, 25))
151 | brush.setStyle(QtCore.Qt.SolidPattern)
152 | palette.setBrush(QPalette.Disabled, QPalette.WindowText, brush)
153 |
154 | brush = QBrush(QColor(80, 80, 80))
155 | brush.setStyle(QtCore.Qt.SolidPattern)
156 | palette.setBrush(QPalette.Disabled, QPalette.Button, brush)
157 |
158 | brush = QBrush(QColor(75, 75, 75))
159 | brush.setStyle(QtCore.Qt.SolidPattern)
160 | palette.setBrush(QPalette.Disabled, QPalette.Light, brush)
161 |
162 | brush = QBrush(QColor(62, 62, 62))
163 | brush.setStyle(QtCore.Qt.SolidPattern)
164 | palette.setBrush(QPalette.Disabled, QPalette.Midlight, brush)
165 |
166 | brush = QBrush(QColor(25, 25, 25))
167 | brush.setStyle(QtCore.Qt.SolidPattern)
168 | palette.setBrush(QPalette.Disabled, QPalette.Dark, brush)
169 |
170 | brush = QBrush(QColor(33, 33, 33))
171 | brush.setStyle(QtCore.Qt.SolidPattern)
172 | palette.setBrush(QPalette.Disabled, QPalette.Mid, brush)
173 |
174 | brush = QBrush(QColor(25, 25, 25))
175 | brush.setStyle(QtCore.Qt.SolidPattern)
176 | palette.setBrush(QPalette.Disabled, QPalette.Text, brush)
177 |
178 | brush = QBrush(QColor(255, 255, 255))
179 | brush.setStyle(QtCore.Qt.SolidPattern)
180 | palette.setBrush(QPalette.Disabled, QPalette.BrightText, brush)
181 |
182 | brush = QBrush(QColor(25, 25, 25))
183 | brush.setStyle(QtCore.Qt.SolidPattern)
184 | palette.setBrush(QPalette.Disabled, QPalette.ButtonText, brush)
185 |
186 | brush = QBrush(QColor(50, 50, 50))
187 | brush.setStyle(QtCore.Qt.SolidPattern)
188 | palette.setBrush(QPalette.Disabled, QPalette.Base, brush)
189 |
190 | brush = QBrush(QColor(50, 50, 50))
191 | brush.setStyle(QtCore.Qt.SolidPattern)
192 | palette.setBrush(QPalette.Disabled, QPalette.Window, brush)
193 |
194 | brush = QBrush(QColor(0, 0, 0))
195 | brush.setStyle(QtCore.Qt.SolidPattern)
196 | palette.setBrush(QPalette.Disabled, QPalette.Shadow, brush)
197 |
198 | brush = QBrush(QColor(174, 174, 174))
199 | brush.setStyle(QtCore.Qt.SolidPattern)
200 | palette.setBrush(QPalette.Disabled, QPalette.Highlight, brush)
201 |
202 | brush = QBrush(QColor(50, 50, 50))
203 | brush.setStyle(QtCore.Qt.SolidPattern)
204 | palette.setBrush(QPalette.Disabled, QPalette.AlternateBase, brush)
205 |
206 | brush = QBrush(QColor(255, 255, 220))
207 | brush.setStyle(QtCore.Qt.SolidPattern)
208 | palette.setBrush(QPalette.Disabled, QPalette.ToolTipBase, brush)
209 |
210 | brush = QBrush(QColor(0, 0, 0))
211 | brush.setStyle(QtCore.Qt.SolidPattern)
212 | palette.setBrush(QPalette.Disabled, QPalette.ToolTipText, brush)
213 | return palette
214 |
--------------------------------------------------------------------------------
/scripts/prototypes/terminal_earlier.py:
--------------------------------------------------------------------------------
1 | """
2 | This module needs to satisfy the following requirements:
3 |
4 | - [ ] Redirect stdout to all Python Editor Terminal QPlainTextEdits
5 | - [ ] Preserve unread output in Queue objects, which are read when loading the Terminal(s)
6 | - [ ] Be reloadable without losing stdout connections
7 | (currently doesn't pick up on queue object because stdout is wiped on reload)
8 | - [ ] Not keep references to destroyed objects
9 |
10 |
11 | # would sys.displayhook be useful in here?
12 |
13 | """
14 |
15 |
16 | import sys
17 | import os
18 | from Queue import Queue
19 |
20 | sys.dont_write_bytecode = True
21 | TESTS_DIR = os.path.dirname(__file__)
22 | PACKAGE_PATH = os.path.dirname(TESTS_DIR)
23 | sys.path.append(PACKAGE_PATH)
24 |
25 |
26 | from PythonEditor.ui.Qt import QtWidgets, QtCore, QtGui
27 |
28 |
29 | # ----- override nuke FnRedirect -----
30 | class VirtualModule(object):
31 | pass
32 |
33 |
34 | class Loader(object):
35 | def load_module(self, name):
36 | print(name)
37 | try:
38 | from _fnpython import stderrRedirector, outputRedirector
39 | sys.outputRedirector = outputRedirector
40 | sys.stderrRedirector = stderrRedirector
41 | # sys.stdout.SERedirect = outputRedirector
42 | # sys.stderr.SERedirect = stderrRedirector
43 |
44 | finally:
45 | # firmly block all imports of the module
46 | return VirtualModule()
47 |
48 |
49 | class Finder(object):
50 | _deletable = ''
51 |
52 | def find_module(self, name, path=''):
53 | if 'FnRedirect' in name:
54 | return Loader()
55 |
56 |
57 | sys.meta_path = [i for i in sys.meta_path
58 | if not hasattr(i, '_deletable')]
59 | sys.meta_path.append(Finder())
60 | # ----- end override section -----
61 |
62 |
63 | class Signal(QtCore.QObject):
64 | s = QtCore.Signal(str)
65 | e = QtCore.Signal()
66 | receivers = []
67 | def customEvent(self, event):
68 | pass
69 | # from _fnpython import stderrRedirector, outputRedirector
70 | # try:
71 | # outputRedirector(event.text)
72 | # except:
73 | # pass
74 | for func in self.receivers:
75 | func(text=event.text)
76 | # self.e.emit()
77 | # self.s.emit(event.text)
78 |
79 | class PrintEvent(QtCore.QEvent):
80 | def __init__(self, text):
81 | self.text = text
82 | super(PrintEvent, self).__init__(QtCore.QEvent.User)
83 |
84 |
85 | class PySingleton(object):
86 | """
87 | Return a single instance of a class
88 | or create a new instance if none exists.
89 | """
90 | def __new__(cls, *args, **kwargs):
91 | if '_the_instance' not in cls.__dict__:
92 | cls._the_instance = object.__new__(cls)
93 | return cls._the_instance
94 |
95 |
96 | def post_out(text):
97 | app = QtWidgets.QApplication.instance()
98 | app.postEvent(sys.stdout.signal, PrintEvent(text))
99 |
100 | def post_err(text):
101 | app = QtWidgets.QApplication.instance()
102 | app.postEvent(sys.stderr.signal, PrintEvent(text))
103 |
104 |
105 | class Redirect(object):
106 | def __init__(self, stream, queue=Queue()):
107 | self.stream = stream
108 | self.signal = Signal()
109 | self.queue = queue
110 | self.SERedirect = lambda x: None
111 | # self.post_text = lambda x: None
112 | self.receivers = []
113 |
114 | for a in dir(stream):
115 | try:
116 | getattr(self, a)
117 | except AttributeError:
118 | attr = getattr(stream, a)
119 | setattr(self, a, attr)
120 |
121 | # def post_text(self, text):
122 | # app = QtWidgets.QApplication.instance()
123 | # app.postEvent(self.signal, PrintEvent(text))
124 |
125 | def write(self, text):
126 |
127 | app = QtWidgets.QApplication.instance()
128 | event = PrintEvent(text)#, self.SERedirect)
129 | app.postEvent(self.signal, event)
130 |
131 | self.stream.write(text)
132 | # queue = self.queue
133 | # queue.put(text)
134 | # self.post_text(text)
135 | # try:
136 | # self.post_text(text)
137 | # except Exception as e:
138 | # print 'this is why it did not work', e
139 | # self.stream.write('this is why it did not work:\n')
140 | # self.stream.write(e)
141 | # self.post_text = lambda x: None
142 | # self.signal.s.emit(text)
143 |
144 | # receivers = self.receivers
145 | # if not receivers:
146 | # queue.put(text)
147 | # else:
148 | # if queue.empty():
149 | # queue = None
150 | # # TODO: at this point, what if we called
151 | # # a relay object that would read the
152 | # # queues and emit signals to various
153 | # # listeners? instead of triggering the receivers
154 | # # one by one here.
155 | # for func in receivers:
156 | # func(text=text, queue=queue)
157 |
158 | # self.stream.write(text)
159 |
160 | # would be nice to have a way to delay this too
161 | # as it seems to cause a fair bit of slowdown in Nuke.
162 | # self.SERedirect(text)
163 |
164 |
165 | class SysOut(Redirect, PySingleton):
166 | pass
167 | class SysErr(Redirect, PySingleton):
168 | pass
169 | class SysIn(Redirect, PySingleton):
170 | pass
171 |
172 |
173 | class Terminal(QtWidgets.QPlainTextEdit):
174 | def __init__(self):
175 | super(Terminal, self).__init__()
176 | self.setReadOnly(True)
177 | self.setObjectName('Terminal')
178 | self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
179 |
180 | # sys.stdout.receivers.append(self.get)
181 | # self.get(queue=sys.stdout.queue)
182 | # sys.stderr.receivers.append(self.get)
183 | # self.get(queue=sys.stderr.queue)
184 |
185 | @QtCore.Slot(str)
186 | def get(self, text=None, queue=None):
187 | """
188 | The get method allows the terminal to pick up
189 | on output created between the stream object
190 | encapsulation and the terminal creation.
191 |
192 | This is as opposed to connecting directly to the
193 | insertPlainText method, e.g.
194 | sys.stdout.write = self.insertPlainText
195 |
196 | !Warning! Don't print anything in here, it
197 | will cause an infinite loop.
198 | """
199 | if queue is not None:
200 | while not queue.empty():
201 | _text = queue.get()
202 | self.receive(_text)
203 |
204 | if text is not None:
205 | self.receive(text)
206 |
207 | def receive(self, text):
208 | # textCursor = self.textCursor()
209 | self.moveCursor(QtGui.QTextCursor.End)
210 | self.insertPlainText(text)
211 |
212 | def showEvent(self, event):
213 | # sys.stdout.signal.s.connect(self.get)
214 | # sys.stderr.signal.s.connect(self.get)
215 | # print 'showing'
216 |
217 | for stream in sys.stdout.signal, sys.stderr.signal:
218 | stream.receivers.append(self.get)
219 | self.get(queue=stream.queue)
220 |
221 | super(Terminal, self).showEvent(event)
222 | # sys.stdout.receivers.append(self.get)
223 | # sys.stderr.receivers.append(self.get)
224 | # # self.get(queue=sys.stdout.queue)
225 | # # self.get(queue=sys.stderr.queue)
226 |
227 | def hideEvent(self, event):
228 | # sys.stdout.signal.s.disconnect(self.get)
229 | # sys.stderr.signal.s.disconnect(self.get)
230 | # print 'hiding'
231 | for stream in sys.stdout.signal, sys.stderr.signal:
232 | if self.get in stream.receivers:
233 | stream.receivers.remove(self.get)
234 |
235 | super(Terminal, self).hideEvent(event)
236 |
237 | def closeEvent(self, event):
238 | print('closing')
239 | super(Terminal, self).closeEvent(event)
240 |
241 |
242 | # before we reset stdout and err, try to recover their queues
243 | out_queue = getattr(sys.stdout, 'queue', Queue())
244 | err_queue = getattr(sys.stderr, 'queue', Queue())
245 |
246 | # reset stdout, stderr, stdin:
247 | sys.stdout = sys.__stdout__
248 | sys.stderr = sys.__stderr__
249 | sys.stdin = sys.__stdin__
250 |
251 | # override stdout, stderr, stdin
252 | sys.stdout = SysOut(sys.__stdout__, queue=out_queue)
253 | sys.stderr = SysErr(sys.__stderr__, queue=err_queue)
254 | sys.stdin = SysIn(sys.__stdin__)
255 |
256 | # in case we decide to reload the module, we need to
257 | # re-add the functions to write to Nuke's Script Editor.
258 | try:
259 | from _fnpython import stderrRedirector, outputRedirector
260 | sys.stdout.SERedirect = outputRedirector
261 | sys.stderr.SERedirect = stderrRedirector
262 | except ImportError:
263 | pass
264 |
265 |
--------------------------------------------------------------------------------
/scripts/prototypes/manager.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from PythonEditor.ui.Qt import QtWidgets, QtCore, QtGui
4 | from PythonEditor.ui import edittabs
5 | from PythonEditor.ui import browser
6 | from PythonEditor.ui import menubar
7 | from PythonEditor.ui.features import shortcuts
8 | from PythonEditor.ui.features import autosavexml
9 | from PythonEditor.ui.dialogs import preferences
10 | from PythonEditor.ui.dialogs import shortcuteditor
11 | # from PythonEditor import save
12 | from PythonEditor.utils.constants import NUKE_DIR
13 |
14 |
15 | def get_parent(widget, level=1):
16 | """
17 | Return a widget's nth parent widget.
18 | """
19 | parent = widget
20 | for p in range(level):
21 | parent = parent.parentWidget()
22 | return parent
23 |
24 |
25 | class Manager(QtWidgets.QWidget):
26 | """
27 | Layout that connects code editors to a file system
28 | that allows editing of multiple files and autosaves.
29 | """
30 | def __init__(self):
31 | super(Manager, self).__init__()
32 | self.currently_viewed_file = None
33 | self.build_layout()
34 |
35 | def build_layout(self):
36 | """
37 | Create the layout.
38 | """
39 | layout = QtWidgets.QVBoxLayout(self)
40 | layout.setContentsMargins(0, 0, 0, 0)
41 | # self.setup_menu()
42 | self.menubar = menubar.MenuBar(self)
43 |
44 | left_widget = QtWidgets.QWidget()
45 | left_layout = QtWidgets.QVBoxLayout(left_widget)
46 |
47 | path_edit = QtWidgets.QLineEdit()
48 | path_edit.textChanged.connect(self.update_tree)
49 | self.path_edit = path_edit
50 |
51 | splitter = QtWidgets.QSplitter(QtCore.Qt.Horizontal)
52 | self.splitter = splitter
53 |
54 | self.xpanded = False
55 | self.setLayout(layout)
56 | self.tool_button = QtWidgets.QToolButton()
57 | self.tool_button.setText('<')
58 | self.tool_button.clicked.connect(self.xpand)
59 | self.tool_button.setMaximumWidth(20)
60 |
61 | layout.addWidget(splitter)
62 |
63 | browse = browser.FileTree(NUKE_DIR)
64 | self.browser = browse
65 | left_layout.addWidget(self.path_edit)
66 | left_layout.addWidget(self.browser)
67 |
68 | self.tabs = edittabs.EditTabs()
69 |
70 | widgets = [left_widget,
71 | self.tool_button,
72 | self.tabs]
73 | for w in widgets:
74 | splitter.addWidget(w)
75 |
76 | splitter.setSizes([200, 10, 800])
77 |
78 | self.install_features()
79 |
80 | self.check_modified_tabs()
81 | if self.tabs.count() == 0:
82 | self.tabs.new_tab()
83 |
84 | def install_features(self):
85 | """
86 | Install features and connect required signals.
87 | """
88 | sch = shortcuts.ShortcutHandler(self.tabs)
89 | # sch.clear_output_signal.connect(self.terminal.clear)
90 | self.shortcuteditor = shortcuteditor.ShortcutEditor(sch)
91 | self.preferenceseditor = preferences.PreferencesEditor()
92 |
93 | self.filehandler = autosavexml.AutoSaveManager(self.tabs)
94 |
95 | self.browser.path_signal.connect(self.read)
96 |
97 | def check_modified_tabs(self):
98 | """
99 | On open, check to see which documents are
100 | not matching their files (if they have them)
101 | """
102 | indices = []
103 | for tab_index in range(self.tabs.count()):
104 | editor = self.tabs.widget(tab_index)
105 | if not editor.objectName() == 'Editor':
106 | continue
107 |
108 | if not hasattr(editor, 'path'):
109 | # document not yet saved
110 | indices.append(tab_index)
111 | continue
112 |
113 | if not os.path.isfile(editor.path):
114 | # file does not exist
115 | indices.append(tab_index)
116 | continue
117 |
118 | with open(editor.path, 'rt') as f:
119 | if f.read() != editor.toPlainText():
120 | indices.append(tab_index)
121 |
122 | for index in indices:
123 | self.update_icon(tab_index=index)
124 |
125 | def xpand(self):
126 | """
127 | Expand or contract the QSplitter
128 | to show or hide the file browser.
129 | """
130 | if self.xpanded:
131 | symbol = '<'
132 | sizes = [200, 10, 800] # should be current sizes
133 | else:
134 | symbol = '>'
135 | sizes = [0, 10, 800] # should be current sizes
136 |
137 | self.tool_button.setText(symbol)
138 | self.splitter.setSizes(sizes)
139 | self.xpanded = not self.xpanded
140 |
141 | @QtCore.Slot(str)
142 | def update_tree(self, path):
143 | """
144 | Update the file browser when the
145 | lineedit is updated.
146 | """
147 | model = self.browser.model()
148 | root_path = model.rootPath()
149 | if root_path in path:
150 | return
151 | path = os.path.dirname(path)
152 | if not os.path.isdir(path):
153 | return
154 | path = path+os.altsep
155 | print(path)
156 | self.browser.set_model(path)
157 |
158 | def find_file_tab(self, path):
159 | """
160 | Search currently opened tabs for an editor
161 | that matches the given file path.
162 | """
163 | for tab_index in range(self.tabs.count()):
164 | editor = self.tabs.widget(tab_index)
165 | if hasattr(editor, 'path') and editor.path == path:
166 | return tab_index, editor
167 |
168 | return None, None
169 |
170 | @QtCore.Slot(str)
171 | def read(self, path):
172 | """
173 | Read from text file and create associated editor
174 | if not present. This should replace last viewed file
175 | if that file has not been edited, to avoid cluttering.
176 | """
177 | self.path_edit.setText(path)
178 | if not os.path.isfile(path):
179 | return
180 |
181 | tab_index, editor = self.find_file_tab(path)
182 | already_open = (tab_index is not None)
183 | if not already_open:
184 | self.replace_viewed(path)
185 | else:
186 | self.tabs.setCurrentIndex(tab_index)
187 | # if no editor with path in tabs add new
188 | self.editor = self.tabs.currentWidget()
189 | self.editor.path = path
190 |
191 | with open(path, 'rt') as f:
192 | text = f.read()
193 | self.editor.setPlainText(text)
194 |
195 | doc = self.editor.document()
196 | doc.modificationChanged.connect(self.modification_handler)
197 | # self.editor.modificationChanged.connect(self.modification_handler)
198 |
199 | def replace_viewed(self, path):
200 | """
201 | Replaces the currently viewed document,
202 | if unedited, with a new document.
203 | """
204 | viewed = self.currently_viewed_file
205 | self.currently_viewed_file = path
206 |
207 | find_replaceable = (viewed is not None)
208 |
209 | # let's only replace files if they're the current tab
210 | editor = self.tabs.currentWidget()
211 | tab_index = self.tabs.currentIndex()
212 | if hasattr(editor, 'path'):
213 | if path == viewed:
214 | find_replaceable = True
215 |
216 | if find_replaceable:
217 | # tab_index, editor = self.find_file_tab(viewed)
218 |
219 | is_replaceable = (tab_index is not None)
220 | if is_replaceable:
221 | with open(viewed, 'rt') as f:
222 | file_text = f.read()
223 | editor_text = editor.toPlainText()
224 | if file_text != editor_text: # ndiff cool feature
225 | is_replaceable = False
226 |
227 | if is_replaceable:
228 | self.tabs.setCurrentIndex(tab_index)
229 | self.tabs.setTabText(tab_index, os.path.basename(path))
230 | return
231 |
232 | filename = os.path.basename(path)
233 | self.tabs.new_tab(tab_name=filename)
234 |
235 | @QtCore.Slot(bool)
236 | def modification_handler(self, changed):
237 | """
238 | Slot for editor document modificationChanged
239 | """
240 | print(changed, 'set tab italic!')
241 | size = 20, 20
242 | if changed:
243 | size = 10, 10
244 | self.update_icon(size=size)
245 |
246 | def update_icon(self, tab_index=None, size=(10, 10)):
247 | """
248 | Represent the document's save state
249 | by setting an icon on the tab.
250 | """
251 | if tab_index is None:
252 | tab_index = self.tabs.currentIndex()
253 | px = QtGui.QPixmap(*size)
254 | ico = QtGui.QIcon(px)
255 | self.tabs.setTabIcon(tab_index, ico)
256 | # editor = self.tabs.widget(tab)
257 | # editor.document().setModified(False)
258 |
259 | def showEvent(self, event):
260 | """
261 | Hack to get rid of margins automatically put in
262 | place by Nuke Dock Window.
263 | """
264 | try:
265 | for i in 2, 4:
266 | parent = get_parent(self, level=i)
267 | parent.layout().setContentsMargins(0, 0, 0, 0)
268 | except Exception:
269 | pass
270 |
271 | super(Manager, self).showEvent(event)
272 |
--------------------------------------------------------------------------------