├── 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 | [![build](https://github.com/plasmax/PythonEditor/actions/workflows/python-app.yml/badge.svg)](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 | --------------------------------------------------------------------------------