├── .coveragerc
├── .github
├── ISSUE_TEMPLATE
│ ├── BUG_REPORT.md
│ └── FEATURE_REQUEST.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── release.yml
│ └── static-analysis-and-test.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── examples
├── add_to_app.py
└── output_capture_and_show.py
├── preditor
├── __init__.py
├── __main__.py
├── about_module.py
├── cli.py
├── config.py
├── contexts.py
├── cores
│ ├── __init__.py
│ └── core.py
├── dccs
│ └── maya
│ │ ├── PrEditor_maya.mod
│ │ └── plug-ins
│ │ └── PrEditor_maya.py
├── debug.py
├── delayable_engine
│ ├── __init__.py
│ └── delayables.py
├── enum.py
├── excepthooks.py
├── gui
│ ├── __init__.py
│ ├── app.py
│ ├── codehighlighter.py
│ ├── completer.py
│ ├── console.py
│ ├── dialog.py
│ ├── drag_tab_bar.py
│ ├── editor_chooser.py
│ ├── errordialog.py
│ ├── find_files.py
│ ├── fuzzy_search
│ │ ├── __init__.py
│ │ └── fuzzy_search.py
│ ├── group_tab_widget
│ │ ├── __init__.py
│ │ ├── grouped_tab_menu.py
│ │ ├── grouped_tab_models.py
│ │ ├── grouped_tab_widget.py
│ │ └── one_tab_widget.py
│ ├── level_buttons.py
│ ├── logger_window_handler.py
│ ├── logger_window_plugin.py
│ ├── loggerwindow.py
│ ├── newtabwidget.py
│ ├── set_text_editor_path_dialog.py
│ ├── status_label.py
│ ├── suggest_path_quotes_dialog.py
│ ├── ui
│ │ ├── editor_chooser.ui
│ │ ├── errordialog.ui
│ │ ├── find_files.ui
│ │ ├── loggerwindow.ui
│ │ ├── set_text_editor_path_dialog.ui
│ │ └── suggest_path_quotes_dialog.ui
│ ├── window.py
│ ├── workbox_mixin.py
│ ├── workbox_text_edit.py
│ └── workboxwidget.py
├── logging_config.py
├── osystem.py
├── plugins.py
├── prefs.py
├── resource
│ ├── environment_variables.html
│ ├── error_mail.html
│ ├── error_mail_inline.html
│ ├── img
│ │ ├── README.md
│ │ ├── arrow_forward.png
│ │ ├── check-bold.png
│ │ ├── chevron-down.png
│ │ ├── chevron-up.png
│ │ ├── close-thick.png
│ │ ├── comment-edit.png
│ │ ├── content-copy.png
│ │ ├── content-cut.png
│ │ ├── content-duplicate.png
│ │ ├── content-paste.png
│ │ ├── content-save.png
│ │ ├── debug_disabled.png
│ │ ├── eye-check.png
│ │ ├── file-plus.png
│ │ ├── file-remove.png
│ │ ├── format-align-left.png
│ │ ├── format-letter-case-lower.png
│ │ ├── format-letter-case-upper.png
│ │ ├── format-letter-case.svg
│ │ ├── information.png
│ │ ├── logging_critical.png
│ │ ├── logging_custom.png
│ │ ├── logging_debug.png
│ │ ├── logging_error.png
│ │ ├── logging_info.png
│ │ ├── logging_not_set.png
│ │ ├── logging_warning.png
│ │ ├── marker.png
│ │ ├── play.png
│ │ ├── playlist-play.png
│ │ ├── plus-minus-variant.png
│ │ ├── preditor.ico
│ │ ├── preditor.png
│ │ ├── preditor.psd
│ │ ├── preditor.svg
│ │ ├── regex.svg
│ │ ├── restart.svg
│ │ ├── skip-forward-outline.png
│ │ ├── skip-next-outline.png
│ │ ├── skip-next.png
│ │ ├── skip-previous.png
│ │ ├── subdirectory-arrow-right.png
│ │ ├── text-search-variant.png
│ │ └── warning-big.png
│ ├── lang
│ │ └── python.json
│ ├── settings.ini
│ └── stylesheet
│ │ ├── Bright.css
│ │ └── Dark.css
├── scintilla
│ ├── __init__.py
│ ├── delayables
│ │ ├── __init__.py
│ │ ├── smart_highlight.py
│ │ └── spell_check.py
│ ├── documenteditor.py
│ ├── finddialog.py
│ ├── lang
│ │ ├── __init__.py
│ │ ├── config
│ │ │ ├── bash.ini
│ │ │ ├── batch.ini
│ │ │ ├── cpp.ini
│ │ │ ├── css.ini
│ │ │ ├── eyeonscript.ini
│ │ │ ├── html.ini
│ │ │ ├── javascript.ini
│ │ │ ├── lua.ini
│ │ │ ├── maxscript.ini
│ │ │ ├── mel.ini
│ │ │ ├── mu.ini
│ │ │ ├── nsi.ini
│ │ │ ├── perl.ini
│ │ │ ├── puppet.ini
│ │ │ ├── python.ini
│ │ │ ├── ruby.ini
│ │ │ ├── sql.ini
│ │ │ ├── xml.ini
│ │ │ └── yaml.ini
│ │ └── language.py
│ ├── lexers
│ │ ├── __init__.py
│ │ ├── cpplexer.py
│ │ ├── javascriptlexer.py
│ │ ├── maxscriptlexer.py
│ │ ├── mellexer.py
│ │ ├── mulexer.py
│ │ └── pythonlexer.py
│ └── ui
│ │ └── finddialog.ui
├── settings.py
├── stream
│ ├── __init__.py
│ ├── director.py
│ └── manager.py
├── streamhandler_helper.py
├── utils
│ ├── __init__.py
│ ├── cute.py
│ ├── stylesheets.py
│ └── text_search.py
└── weakref.py
├── pyproject.toml
├── requirements.txt
├── tests
├── conftest.py
├── find_files
│ ├── re_greedy_False_0_True.md
│ ├── re_greedy_False_2_True.md
│ ├── re_greedy_True_2_True.md
│ ├── re_greedy_upper_True_2_True.md
│ ├── re_simple_False_0_True.md
│ ├── re_simple_False_2_True.md
│ ├── re_simple_False_3_True.md
│ ├── re_simple_True_2_True.md
│ ├── simple_False_0_False.md
│ ├── simple_False_1_False.md
│ ├── simple_False_2_False.md
│ ├── simple_False_3_False.md
│ ├── simple_True_2_False.md
│ ├── tab_text.txt
│ └── test_find_files.py
├── ide
│ └── test_delayable_engine.py
├── test_config.py
├── test_prefs.py
└── test_stream.py
└── tox.ini
/.coveragerc:
--------------------------------------------------------------------------------
1 | [report]
2 | show_missing = false
3 | skip_covered = true
4 | skip_empty = true
5 |
6 | [run]
7 | # Ensure all python modules in preditor have their coverage reported,
8 | # not just files that pytest touches.
9 | source = preditor
10 | omit =
11 | */site-packages/*
12 | tests/*
13 | # This file is automatically generated by setuptools_scm
14 | preditor/version.py
15 | parallel=True
16 | relative_files=True
17 | data_file=.coverage/.coverage
18 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/BUG_REPORT.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: File a bug report. (Please search exisiting issues before submitting. Also see CONTRIBUTING.md.)
4 | title: ""
5 | labels: bug
6 | assignees: ""
7 | ---
8 |
9 | ## Summary
10 |
11 |
15 |
16 | ## Expected Behavior
17 |
18 |
21 |
22 | ## Steps to Reproduce Behavior
23 |
24 |
28 |
29 | ### Solution
30 |
31 |
34 |
35 | ## Environment
36 |
37 | - Version:
38 | - OS and Python version:
39 |
40 | ## Additional Context
41 |
42 |
46 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | about: Suggest an idea for the project. (Please search exisiting issues before submitting. Also see CONTRIBUTING.md.)
4 | title: ""
5 | labels: enhancement
6 | assignees: ""
7 | ---
8 |
9 |
10 | ## Description
11 |
12 |
16 |
17 | ## Solution
18 |
19 |
22 |
23 | ## Additional Context
24 |
25 |
29 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Checklist
2 |
3 |
6 |
7 | - [ ] I have read the [CONTRIBUTING.md](../CONTRIBUTING.md) document
8 | - [ ] I formatted my changes with [black](https://github.com/psf/black)
9 | - [ ] I linted my changes with [flake8](https://github.com/PyCQA/flake8)
10 | - [ ] I have added documentation regarding my changes where necessary
11 | - [ ] Any pre-existing tests continue to pass
12 | - [ ] Additional tests were made covering my changes
13 |
14 | ## Types of Changes
15 |
16 |
19 |
20 | - [ ] Bugfix (change that fixes an issue)
21 | - [ ] New Feature (change that adds functionality)
22 | - [ ] Documentation Update (if none of the other choices apply)
23 |
24 | ## Proposed Changes
25 |
26 |
30 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: PyPi Release
2 |
3 | on:
4 | release:
5 | types: [created]
6 |
7 | jobs:
8 |
9 | build-and-publish:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout code
14 | uses: actions/checkout@v4
15 | with:
16 | fetch-depth: 0
17 |
18 | - name: Setup Python
19 | uses: actions/setup-python@v5
20 | with:
21 | python-version: "3.x"
22 |
23 | - name: Install dependencies
24 | run: |
25 | python -m pip install --upgrade pip
26 | python -m pip install --upgrade build setuptools wheel twine
27 |
28 | - name: Build wheel
29 | run: |
30 | python -m build --wheel --sdist
31 |
32 | # Upload the built pip packages so we can inspect them
33 | - name: Upload packages.
34 | uses: actions/upload-artifact@v4
35 | with:
36 | name: pip-packages
37 | path: |
38 | dist/preditor-*.whl
39 | dist/preditor-*.tar.gz
40 | # This is only used if there is a problem with the next step
41 | retention-days: 1
42 |
43 | - name: Publish to PyPI
44 | env:
45 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
46 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
47 | run: |
48 | twine upload --verbose dist/*
49 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *.pyo
3 | *.rej
4 | *.swp
5 | .DS_Store
6 |
7 | /.project
8 | /.pydevproject
9 | *.egg-info
10 | .eggs/
11 | /build/
12 | /dist/
13 | /venv/
14 | *.sublime-*
15 | pip-wheel-metadata
16 |
17 | # Ignore the majority of the docs folder
18 | /docs/*
19 | !/docs/
20 | /docs/source/*
21 | !/docs/source/
22 | !/docs/source/conf.py
23 | !/docs/source/index.rst
24 |
25 | # Used by the ci
26 | .cache/
27 | downloads/
28 | shared-venv/
29 | .tox/
30 |
31 | # /preditor
32 | /preditor/version.py
33 |
34 | # Coverage
35 | .coverage
36 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | ci:
3 | autoupdate_schedule: quarterly
4 | skip: [black, flake8]
5 |
6 | repos:
7 |
8 | - repo: https://github.com/psf/black
9 | rev: 22.12.0
10 | hooks:
11 | - id: black
12 |
13 | - repo: https://github.com/PyCQA/flake8
14 | rev: 5.0.4
15 | hooks:
16 | - id: flake8
17 | additional_dependencies:
18 | - flake8-bugbear==22.12.6
19 | - Flake8-pyproject
20 | - pep8-naming==0.13.3
21 |
22 | - repo: https://github.com/pre-commit/pre-commit-hooks
23 | rev: v4.4.0
24 | hooks:
25 | - id: check-json
26 | - id: check-toml
27 | - id: check-xml
28 | - id: check-yaml
29 | - id: debug-statements
30 | - id: end-of-file-fixer
31 | - id: requirements-txt-fixer
32 | - id: trailing-whitespace
33 | exclude: ^(tests/find_files/)
34 |
35 | - repo: https://github.com/pycqa/isort
36 | rev: 5.12.0
37 | hooks:
38 | - id: isort
39 | name: isort (python)
40 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | Those who participate and/or contribute to projects maintained by Blur Studio are expected to treat one another with respect.
4 |
5 | ## Our Pledge
6 |
7 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
8 |
9 | ## Our Standards
10 |
11 | Examples of behavior that contributes to creating a positive environment include:
12 |
13 | * Using welcoming and inclusive language
14 | * Being respectful of differing viewpoints and experiences
15 | * Gracefully accepting constructive criticism
16 | * Focusing on what is best for the community
17 | * Showing empathy towards other community members
18 |
19 | Examples of unacceptable behavior by participants include:
20 |
21 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
22 | * Trolling, insulting/derogatory comments, and personal or political attacks
23 | * Public or private harassment
24 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
25 | * Other conduct which could reasonably be considered inappropriate in a professional setting
26 |
27 | ## Our Responsibilities
28 |
29 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
30 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
31 |
32 | ## Scope
33 |
34 | This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
35 |
36 | ## Enforcement
37 |
38 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the open source team at opensource@blur.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
39 |
40 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
41 |
42 | ## Attribution
43 |
44 | This Code of Conduct is adapted from [Contributor Covenant](https://www.contributor-covenant.org).
45 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Blur Projects
2 |
3 | We at Blur are excited to contribute to the Visual Effects community by open sourcing our internal projects. We welcome others to integrate these projects into their pipelines and contribute to them as they deem fit via bug reports, feature suggestions, and pull requests.
4 |
5 |
6 |
7 | - [Code of Conduct](#code-of-conduct)
8 | - [How to Contribute](#how-to-contribute)
9 | - [Reporting Issues / Bugs](#reporting-issues--bugs)
10 | - [Suggesting a Feature](#suggesting-a-feature)
11 | - [Submitting a Pull Request](#submitting-a-pull-request)
12 | - [Coding Style & Formatting](#coding-style--formatting)
13 | - [Creating a Release](#creating-a-release)
14 |
15 |
16 |
17 | ## Code of Conduct
18 |
19 | Before contributing we recommend checking out our _[code of conduct]_. Thanks! :smile:
20 |
21 | ## How to Contribute
22 |
23 | ### Reporting Issues / Bugs
24 |
25 | - Double check that the bug has not already been reported by searching the project's GitHub [Issues].
26 | - If an issue already exists, you're welcome to add additional context that may not already be present in the original message via the comments.
27 | - Once you have verified no pre-existing bug report exists, [create a new issue].
28 | - Provide a title and concise description.
29 | - Include as much detailed information regarding the problem as possible.
30 | - Supply reproducible steps that demonstrate the behavior.
31 |
32 | ### Suggesting a Feature
33 |
34 | - Double check that a similar request has not already been made by searching the project's GitHub [Issues].
35 | - If no issue exists pertaining to your feature request, [create a new issue].
36 | - Provide a title and concise description.
37 | - Describe what functionality is missing and why it would be useful for yourself and others.
38 | - If relevant, include any screenshots, animated GIFs, or sketches that might further demonstrate the desired feature.
39 |
40 | ### Submitting a Pull Request
41 |
42 | 1. Fork the Project
43 | 2. Create your Branch (`git checkout -b my-amazing-feature`)
44 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
45 | - Be sure to follow our coding style and formatting conventions (below).
46 | 4. Push to the Branch (`git push origin my-amazing-feature`)
47 | 5. Open a Pull Request
48 | - List what you've done.
49 | - Link to any related or addressed issues or feature requests.
50 |
51 | ## Coding Style & Formatting
52 |
53 | In order to streamline reviews and reduce the barrier-to-entry for developers, we've adopted several standardized tools and workflows to maintain a consistent and reliable code appearance.
54 |
55 | A set of GitHub Action workflows are in place to perform the following style and formatting checks against every push to our project repositories. These checks must successfully pass before a pull request can be accepted.
56 |
57 | **Styling**
58 |
59 | Styling or linting is performed via [flake8] along with the plugins [flake8-bugbear], [Flake8-pyproject] & [pep8-naming]. A minor amount of configuration has been added to _[pyproject.toml]_ in order to provide better compatibility with our formatter black (see next section).
60 |
61 | **Formatting**
62 |
63 | Code formatting is completed by [black]. By relinquishing code appearance standards to Black we manage to greatly reduce semantic arguments/discussions that might distract from progress on a project.
64 |
65 |
66 | ## Creating a Release
67 |
68 | Releases are made manually by project managers and will automatically be uploaded to PyPI (via GitHib Action workflow) once completed.
69 |
70 | [flake8]: https://github.com/PyCQA/flake8
71 | [flake8-bugbear]: https://github.com/PyCQA/flake8-bugbear
72 | [Flake8-pyproject]: https://github.com/john-hen/Flake8-pyproject
73 | [pep8-naming]: https://github.com/PyCQA/pep8-naming
74 | [pyproject.toml]: https://github.com/blurstudio/hab/blob/master/pyproject.toml
75 | [black]: https://github.com/psf/black
76 | [Issues]: https://github.com/blurstudio/preditor/issues
77 | [create a new issue]: https://github.com/blurstudio/preditor/issues/new
78 | [code of conduct]: https://github.com/blurstudio/preditor/blob/master/CODE_OF_CONDUCT.md
79 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | graft preditor/resource
2 | graft preditor/dccs
3 | recursive-include preditor *.ui
4 |
--------------------------------------------------------------------------------
/examples/add_to_app.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from Qt.QtWidgets import QApplication, QMainWindow, QMenuBar, QTextEdit
4 |
5 | import preditor
6 |
7 |
8 | def existing_application():
9 | """Example of a pre-existing application you want to add PrEditor to."""
10 | main_gui = QMainWindow()
11 | main_gui.setWindowTitle('PrEditor Test Application')
12 | uiMenuBar = QMenuBar(main_gui)
13 | main_gui.setMenuBar(uiMenuBar)
14 | uiEdit = QTextEdit(main_gui)
15 | uiEdit.setPlaceholderText('Use menu to show PrEditor')
16 | main_gui.setCentralWidget(uiEdit)
17 | menu = uiMenuBar.addMenu('File')
18 | act = menu.addAction('Exit')
19 | act.triggered.connect(main_gui.close)
20 |
21 | return main_gui
22 |
23 |
24 | def raise_error():
25 | """Simulate a python exception being raised. You will be prompted to show
26 | PrEditor if its not currently visible. This can be disabled by setting
27 | `excepthook` to false when calling `preditor.configure`.
28 | """
29 | raise RuntimeError(
30 | "The user generated this error. If PrEditor is not already "
31 | "visible, the user is prompted to show it."
32 | )
33 |
34 |
35 | if __name__ == '__main__':
36 | # Configure PrEditor for this application, start capturing all text output
37 | # from stderr/stdout so once PrEditor is launched, it can show this text.
38 | # This does not initialize any QtGui/QtWidgets.
39 | preditor.configure(
40 | # This is the name used to store PrEditor preferences and workboxes
41 | # specific to this application.
42 | 'add_to_app',
43 | )
44 | import preditor.excepthooks
45 |
46 | preditor.excepthooks.PreditorExceptHook.install()
47 |
48 | # Create a Gui Application allowing the user to show PrEditor
49 | app = QApplication(sys.argv)
50 | main_gui = existing_application()
51 |
52 | # Get the menu from the window instance. This method assumes you don't have
53 | # the ability to directly add the menu items when building the application.
54 | for act in main_gui.menuBar().actions():
55 | if act.text() == "File":
56 | menu = act.menu()
57 | break
58 | else:
59 | raise RuntimeError("Unable to find the File menu.")
60 |
61 | menu.addSeparator()
62 | # Add the PrEditor menu items to the pre-existing GUI.
63 | # If the user presses "F2" or uses the menu item, show the GUI
64 | act = preditor.connect_preditor(main_gui)
65 | menu.addAction(act)
66 |
67 | # Simulate something raising an error in the application
68 | act = menu.addAction('Raise Error')
69 | act.triggered.connect(raise_error)
70 |
71 | main_gui.show()
72 | app.exec_()
73 |
--------------------------------------------------------------------------------
/examples/output_capture_and_show.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | import preditor
4 |
5 | print("1. logging.root.level: {}".format(logging.root.level))
6 | print("Printed before stream manager installed. Doesn't show up in PrEditor")
7 | preditor.configure('PrEditor')
8 | print("2. logging.root.level: {}".format(logging.root.level))
9 | print("Printed after stream manager installed. Shows up in PrEditor")
10 |
11 | preditor.launch()
12 | print("3. logging.root.level: {}".format(logging.root.level))
13 |
--------------------------------------------------------------------------------
/preditor/__main__.py:
--------------------------------------------------------------------------------
1 | """ Enables support for calling the preditor cli using `python -m preditor`
2 | """
3 | from __future__ import absolute_import
4 |
5 | import sys
6 |
7 | import preditor.cli
8 |
9 | if __name__ == '__main__':
10 | # prog_name prevents __main__.py from being shown as the command name in the help
11 | # text. We don't know the exact command the user passed so we provide a generic
12 | # `python -m preditor` command.
13 | sys.exit(preditor.cli.cli(prog_name="python -m preditor"))
14 |
--------------------------------------------------------------------------------
/preditor/about_module.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import abc
4 | import os
5 | import sys
6 | import textwrap
7 |
8 | from future.utils import with_metaclass
9 |
10 | import preditor
11 |
12 |
13 | class AboutModule(with_metaclass(abc.ABCMeta, object)):
14 | """Base class for the `preditor.plug.about_module` entry point. Create a
15 | subclass of this method and expose the class object to the entry point.
16 |
17 | Properties:
18 | instance: If provided, the instance of PrEditor to generate text for.
19 | """
20 |
21 | indent = " "
22 | """Use this to indent new lines for text"""
23 |
24 | def __init__(self, instance=None):
25 | self.instance = instance
26 |
27 | @classmethod
28 | def generate(cls, instance=None):
29 | """Generates the output text for all plugins.
30 |
31 | Outputs this for each plugin:
32 |
33 | {name}: {version}
34 | {indent}{text line 1}
35 | {indent}{text line ...}
36 |
37 | Name is the name of each entry point. If duplicates are found, a logging
38 | warning is generated. Each line of `self.text()` is indented
39 | """
40 | ret = []
41 | for name, plugin in preditor.plugins.about_module():
42 | version = "Unknown"
43 | if isinstance(plugin, str):
44 | text = plugin
45 | else:
46 | try:
47 | plug = plugin(instance=instance)
48 | if not plug.enabled():
49 | continue
50 | version = plug.version()
51 | text = plug.text()
52 | except Exception as error:
53 | text = "Error processing: {}".format(error)
54 | text = textwrap.indent(text, cls.indent)
55 |
56 | # Build the output string including the version information if provided.
57 | if version is not None:
58 | ret.append("{}: {}\n{}".format(name, version, text))
59 | else:
60 | ret.append("{}:\n{}".format(name, text))
61 |
62 | return '\n'.join(ret)
63 |
64 | def enabled(self):
65 | """The plugin can use this to disable reporting by generate."""
66 | return True
67 |
68 | @abc.abstractmethod
69 | def text(self):
70 | """Returns info about this plugin. This can have multiple lines, and
71 | each line will be indented to provide visual distinction between plugins.
72 | """
73 |
74 | @abc.abstractmethod
75 | def version(self):
76 | """Returns The version as a string to show next to name."""
77 |
78 |
79 | class AboutPreditor(AboutModule):
80 | """About module used to show info about PrEditor."""
81 |
82 | def text(self):
83 | """Return the path PrEditor was loaded from for quick debugging."""
84 | ret = []
85 | # Include the core_name of the current PrEditor gui instance if possible
86 | if self.instance:
87 | ret.append("Core Name: {}".format(self.instance.name))
88 | # THe path to the PrEditor package
89 | ret.append("Path: {}".format(os.path.dirname(preditor.__file__)))
90 | return "\n".join(ret)
91 |
92 | def version(self):
93 | return preditor.__version__
94 |
95 |
96 | class AboutQt(AboutModule):
97 | """Info about Qt modules being used."""
98 |
99 | def text(self):
100 | """Return the path PrEditor was loaded from for quick debugging."""
101 | from Qt import QtCore, __binding__, __version__
102 |
103 | ret = ['Qt.py: {}, binding: {}'.format(__version__, __binding__)]
104 |
105 | # Attempt to get a version for QtSiteConfig if defined
106 | try:
107 | import QtSiteConfig
108 |
109 | ret.append('QtSiteConfig: {}'.format(QtSiteConfig.__version__))
110 | except (ImportError, AttributeError):
111 | pass
112 |
113 | # Add info for all Qt5 bindings that have been imported
114 | if 'PyQt5.QtCore' in sys.modules:
115 | ret.append('PyQt5: {}'.format(sys.modules['PyQt5.QtCore'].PYQT_VERSION_STR))
116 | if 'PySide2.QtCore' in sys.modules:
117 | ret.append('PySide2: {}'.format(sys.modules['PySide2.QtCore'].qVersion()))
118 |
119 | # Add qt library paths for plugin debugging
120 | for i, path in enumerate(QtCore.QCoreApplication.libraryPaths()):
121 | if i == 0:
122 | ret.append('Library Paths: {}'.format(path))
123 | else:
124 | ret.append(' {}'.format(path))
125 |
126 | return "\n".join(ret)
127 |
128 | def version(self):
129 | from Qt import __qt_version__
130 |
131 | return __qt_version__
132 |
133 |
134 | class AboutPython(AboutModule):
135 | """Info about the current instance of python."""
136 |
137 | def text(self):
138 | """Return the path PrEditor was loaded from for quick debugging."""
139 | ret = sys.version
140 | # Windows doesn't add a newline before the compiler info, and it can end
141 | # up being a little long for QMessageBox's with short file paths. Add
142 | # the newline like is present on linux
143 | ret = ret.replace(") [", ")\n[")
144 | return ret
145 |
146 | def version(self):
147 | return '{}.{}.{}'.format(*sys.version_info[:3])
148 |
149 |
150 | class AboutExe(AboutModule):
151 | """The value of sys.executable, disabled if not set."""
152 |
153 | def enabled(self):
154 | return bool(sys.executable)
155 |
156 | def text(self):
157 | return sys.executable
158 |
159 | def version(self):
160 | """No version is returned for this class."""
161 | return None
162 |
--------------------------------------------------------------------------------
/preditor/contexts.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function
2 |
3 | import logging
4 |
5 | _LOGGER = logging.getLogger(__name__)
6 |
7 |
8 | class ErrorReport(object):
9 | """Allows you to provide additional debug info if a error happens in this context.
10 |
11 | PrEditor can send a error email when any python error is raised.
12 | Sometimes just a traceback does not provide enough information to debug the
13 | traceback. This class allows you to provide additional information to the error
14 | report only if it is generated. For example if your treegrunt environment does not
15 | have a email setup, or the current debug level is not set to Disabled.
16 |
17 | ErrorReport can be used as a with context, or as a function decorator.
18 |
19 | Examples:
20 | This example shows a class using both the with context and a decorated method.
21 |
22 | from preditor.contexts import ErrorReport
23 | class Test(object):
24 | def __init__(self):
25 | self.value = None
26 | def errorInfo(self):
27 | # The text returned by this function will be included in the error email
28 | return 'Info about the Test class: {}'.format(self.value)
29 | def doStuff(self):
30 | with ErrorReport(self.errorInfo, 'Test.doStuff'):
31 | self.value = 'doStuff'
32 | raise RuntimeError("BILL")
33 | @ErrorReport(errorInfo, 'Test.doMoreStuff')
34 | def doMoreStuff(self):
35 | self.value = 'doMoreStuff'
36 | raise RuntimeError("BOB")
37 |
38 | Using this class does not initialize the Python Logger, so you don't need to worry
39 | if your class is running headless and not use this class. However unless you set up
40 | your own error reporting system the callbacks will not be called and nothing will be
41 | reported.
42 |
43 | If you want to set up your own error reporting system you need to set
44 | `ErrorReport.enabled = True`. Then you will need to call ErrorReport.clearReports()
45 | any time excepthook is called. This prevents a buildup of all error reports any time
46 | a exception occurs. It should always be in place when you set enabled == True to
47 | prevent wasting memory. Calling ErrorReport.generateReport() will return the info
48 | you should include in your report. Calling generateReport is optional, but must be
49 | called before calling clearReports.
50 |
51 | Args:
52 | callback (function): If a exception happens this function is called and its
53 | returned value is added to the error email if sent. No arguments are passed
54 | to this function and it is expected to only return a string.
55 | title (str, optional): This short string is added to the title of the
56 | ErrorReport.
57 | Attributes:
58 | enabled (bool): If False(the default), then all callbacks are cleared even if
59 | there is a exception. This is used to prevent these functions from leaking
60 | memory if there isn't a excepthook calling clearReports.
61 | """
62 |
63 | __reports__ = []
64 | enabled = False
65 |
66 | def __init__(self, callback, title=''):
67 | self._callback = callback
68 | self._title = title
69 |
70 | def __call__(self, funct):
71 | def wrapper(wrappedSelf, *args, **kwargs):
72 | unbound = self._callback
73 | self._callback = self._callback.__get__(wrappedSelf)
74 | try:
75 | with self:
76 | return funct(wrappedSelf, *args, **kwargs)
77 | finally:
78 | self._callback = unbound
79 |
80 | return wrapper
81 |
82 | def __enter__(self):
83 | type(self).__reports__.append((self._title, self._callback))
84 |
85 | def __exit__(self, exc_type, exc_val, exc_tb):
86 | # If exc_type is None, then no exception was raised, so we should remove the
87 | # callback. If cls.enabled is False, then nothing has set itself up to call
88 | # clearReports. We need to remove the callback so it doesn't stay in memory.
89 | if exc_type is None or not type(self).enabled:
90 | type(self).__reports__.remove((self._title, self._callback))
91 |
92 | @classmethod
93 | def clearReports(cls):
94 | """Removes all of the currently stored callbacks.
95 |
96 | This should be called after all error reporting is finished, or if a error
97 | happened and there is nothing to report it. If you set cls.enabled to True,
98 | something in excepthook should call this to prevent keeping refrences to
99 | functions from staying in memory.
100 | """
101 | cls.__reports__ = []
102 |
103 | @classmethod
104 | def generateReport(cls, fmt='{result}'):
105 | """Executes and returns all of the currently stored callbacks.
106 | Args:
107 |
108 | ftm (str, Optional): The results of the callbacks will be inserted into this
109 | string using str.format into {results}.
110 | Returns:
111 | list: A list of tuples for all active ErrorReport classes. The tuples
112 | contain two strings; the title string, and result of the passed in
113 | callback function.
114 | """
115 | ret = []
116 | for title, callback in cls.__reports__:
117 | result = callback()
118 | ret.append((title, fmt.format(result=result)))
119 | return ret
120 |
--------------------------------------------------------------------------------
/preditor/cores/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/cores/__init__.py
--------------------------------------------------------------------------------
/preditor/cores/core.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function
2 |
3 | from Qt.QtCore import QObject
4 |
5 |
6 | class Core(QObject):
7 | """
8 | The Core class provides all the main shared functionality and signals that need to
9 | be distributed between different pacakges.
10 | """
11 |
12 | def __init__(self, objectName=None):
13 | super(Core, self).__init__()
14 | if objectName is None:
15 | objectName = 'PrEditor'
16 | self.setObjectName(objectName)
17 |
18 | # Paths in this variable will be removed in
19 | # preditor.osystem.subprocessEnvironment
20 | self._removeFromPATHEnv = set()
21 |
--------------------------------------------------------------------------------
/preditor/dccs/maya/PrEditor_maya.mod:
--------------------------------------------------------------------------------
1 | + PrEditor DEVELOPMENT .
2 | PYTHONPATH +:= ../..
3 |
--------------------------------------------------------------------------------
/preditor/dccs/maya/plug-ins/PrEditor_maya.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import maya.mel
4 | from maya import OpenMayaUI, cmds
5 |
6 | preditor_menu = None
7 |
8 |
9 | def headless():
10 | """If true, no Qt gui elements should be used because python is running a
11 | QCoreApplication."""
12 | return bool(cmds.about(batch=True))
13 |
14 | # TODO: This is the old method for detecting batch mode. Remove this once
15 | # the above about command is vetted as working.
16 | # basename = os.path.splitext(os.path.basename(sys.executable).lower())[0]
17 | # return basename in ('mayabatch', 'mayapy')
18 |
19 |
20 | def root_window():
21 | """Returns the main window of Maya as a Qt object to be used for parenting."""
22 | from Qt import QtCompat
23 |
24 | ptr = OpenMayaUI.MQtUtil.mainWindow()
25 | if ptr is not None:
26 | pointer = int(ptr)
27 | return QtCompat.wrapInstance(pointer)
28 |
29 |
30 | def launch(ignored):
31 | """Show the PrEditor GUI and bring it into focus if it was minimized."""
32 | import preditor
33 |
34 | widget = preditor.launch()
35 | return widget
36 |
37 |
38 | def initializePlugin(mobject): # noqa: N802
39 | """Initialize the script plug-in"""
40 | global preditor_menu
41 |
42 | # If running headless, there is no need to build a gui and create the python logger
43 | if not headless():
44 | from Qt.QtWidgets import QApplication
45 |
46 | import preditor
47 |
48 | maya_ver = cmds.about(version=True).split(" ")[0]
49 |
50 | # Capture all stderr/out after the plugin is loaded. This makes it so
51 | # if the PrEditor GUI is shown, it will include all of the output. Also
52 | # tells PrEditor how to parent itself to the main window and save prefs.
53 | preditor.configure(
54 | # Set the core_name so preferences are saved per-maya version.
55 | "Maya-{}".format(maya_ver),
56 | # Tell PrEditor how to find the maya root window for parenting
57 | parent_callback=root_window,
58 | # Tell it how to check if running in batch mode
59 | headless_callback=headless,
60 | )
61 |
62 | # Detect Maya shutting down and ensure PrEditor's prefs are saved
63 | if QApplication.instance():
64 | QApplication.instance().aboutToQuit.connect(preditor.shutdown)
65 |
66 | # Add a new PrEditor menu with an item that launches PrEditor
67 | gmainwindow = maya.mel.eval("$temp1=$gMainWindow")
68 | preditor_menu = cmds.menu(label="PrEditor", parent=gmainwindow, tearOff=True)
69 | cmds.menuItem(
70 | label="Show",
71 | command=launch,
72 | sourceType="python",
73 | image=preditor.resourcePath('img/preditor.png'),
74 | parent=preditor_menu,
75 | )
76 |
77 | # TODO: Alternatively figure out how to add the launcher menuItem to a
78 | # pre-existing maya menu like next to the "Script Editor" in
79 | # "Windows -> General Editors"
80 | # https://github.com/chadmv/cvwrap/blob/master/scripts/cvwrap/menu.py#L18
81 |
82 | # menu = 'mainWindowMenu'
83 | # # Make sure the menu widgets exist first.
84 | # maya.mel.eval('ChaDeformationsMenu MayaWindow|{0};'.format(menu))
85 | # items = cmds.menu(menu, q=True, ia=True)
86 | # # print(items)
87 | # for item in items:
88 | # menu_label = cmds.menuItem(item, q=True, label=True)
89 | # # print(menu_label)
90 | # if menu_label == "General Editors":
91 | # # cmds.menuItem(parent=item, divider=True, dividerLabel='PrEditor' )
92 | # cmds.menuItem(
93 | # label="PrEditor",
94 | # command=launch,
95 | # sourceType='python',
96 | # image=preditor.resourcePath('img/preditor.png'),
97 | # parent=item,
98 | # )
99 |
100 |
101 | def uninitializePlugin(mobject): # noqa: N802
102 | """Uninitialize the script plug-in"""
103 | import preditor
104 |
105 | # Remove the PrEditor Menu if it exists
106 | if preditor_menu and cmds.menu(preditor_menu, exists=True):
107 | cmds.deleteUI(preditor_menu, menu=True)
108 |
109 | # Close PrEditor making sure to save prefs
110 | preditor.core.shutdown()
111 |
--------------------------------------------------------------------------------
/preditor/debug.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function
2 |
3 | import datetime
4 | import inspect
5 | import logging
6 | import sys
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 |
11 | class FileLogger:
12 | def __init__(self, stdhandle, logfile, _print=True, clearLog=True):
13 | self._stdhandle = stdhandle
14 | self._logfile = logfile
15 | self._print = _print
16 | if clearLog:
17 | # clear the log file
18 | self.clear()
19 |
20 | def clear(self, stamp=False):
21 | """Removes the contents of the log file."""
22 | open(self._logfile, 'w').close()
23 | if stamp:
24 | msg = '--------- Date: {today} Version: {version} ---------'
25 | print(msg.format(today=datetime.datetime.today(), version=sys.version))
26 |
27 | def flush(self):
28 | self._stdhandle.flush()
29 |
30 | def write(self, msg):
31 | f = open(self._logfile, 'a')
32 | f.write(msg)
33 | f.close()
34 | if self._print:
35 | self._stdhandle.write(msg)
36 |
37 |
38 | def logToFile(path, stdout=True, stderr=True, useOldStd=True, clearLog=True):
39 | """Redirect all stdout and/or stderr output to a log file.
40 |
41 | Creates a FileLogger class for stdout and stderr and installs itself in python.
42 | All output will be logged to the file path. Prints the current datetime and
43 | sys.version info when stdout is True.
44 |
45 | Args:
46 | path (str): File path to log output to.
47 |
48 | stdout (bool): If True(default) override sys.stdout.
49 |
50 | stderr (bool): If True(default) override sys.stderr.
51 |
52 | useOldStd (bool): If True, messages will be written to the FileLogger
53 | and the previous sys.stdout/sys.stderr.
54 |
55 | clearLog (bool): If True(default) clear the log file when this command is
56 | called.
57 | """
58 | if stderr:
59 | sys.stderr = FileLogger(sys.stderr, path, useOldStd, clearLog=clearLog)
60 | if stdout:
61 | sys.stdout = FileLogger(sys.stdout, path, useOldStd, clearLog=False)
62 | if clearLog:
63 | sys.stdout.clear(stamp=True)
64 |
65 | from .streamhandler_helper import StreamHandlerHelper
66 |
67 | # Update any StreamHandler's that were setup using the old stdout/err
68 | if stdout:
69 | StreamHandlerHelper.replace_stream(sys.stdout._stdhandle, sys.stdout)
70 | if stderr:
71 | StreamHandlerHelper.replace_stream(sys.stderr._stdhandle, sys.stderr)
72 |
73 |
74 | def printCallingFunction(compact=False):
75 | """Prints and returns info about the calling function
76 |
77 | Args:
78 | compact (bool): If set to True, prints a more compact printout
79 |
80 | Returns:
81 | str: Info on the calling function.
82 | """
83 | import inspect
84 |
85 | current = inspect.currentframe().f_back
86 | try:
87 | parent = current.f_back
88 | except AttributeError:
89 | print('No Calling function found')
90 | return
91 | currentInfo = inspect.getframeinfo(current)
92 | parentInfo = inspect.getframeinfo(parent)
93 | if parentInfo[3] is not None:
94 | context = ', '.join(parentInfo[3]).strip('\t').rstrip()
95 | else:
96 | context = 'No context to return'
97 | if compact:
98 | output = '# %s Calling Function: %s Filename: %s Line: %i Context: %s' % (
99 | currentInfo[2],
100 | parentInfo[2],
101 | parentInfo[0],
102 | parentInfo[1],
103 | context,
104 | )
105 | else:
106 | output = ["Function: '%s' in file '%s'" % (currentInfo[2], currentInfo[0])]
107 | output.append(
108 | " Calling Function: '%s' in file '%s'" % (parentInfo[2], parentInfo[0])
109 | )
110 | output.append(" Line: '%i'" % parentInfo[1])
111 | output.append(" Context: '%s'" % context)
112 | output = '\n'.join(output)
113 | print(output)
114 | return output
115 |
116 |
117 | def mroDump(obj, nice=True, joinString='\n'):
118 | """Formats inspect.getmro into text.
119 |
120 | For the given class object or instance of a class, use inspect to return the Method
121 | Resolution Order.
122 |
123 | Args: obj (object): The object to return the mro of. This can be a class object or
124 | instance.1
125 |
126 | nice (bool): Returns the same module names as help(object) if True, otherwise
127 | repr(object).
128 |
129 | joinString (str, optional): The repr of each class is joined by this string.
130 |
131 | Returns:
132 | str: A string showing the Method Resolution Order of the given object.
133 | """
134 | import pydoc
135 |
136 | # getmro requires a class, turn instances into a class
137 | if not inspect.isclass(obj):
138 | obj = type(obj)
139 | classes = inspect.getmro(obj)
140 | if nice:
141 | ret = [pydoc.classname(x, obj.__module__) for x in (classes)]
142 | else:
143 | ret = [repr(x) for x in (classes)]
144 | return joinString.join(ret)
145 |
--------------------------------------------------------------------------------
/preditor/delayable_engine/delayables.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function
2 |
3 | from Qt.QtCore import QObject
4 |
5 |
6 | class Delayable(QObject):
7 | key = 'invalid'
8 | supports = ('ide', 'workbox')
9 |
10 | def __init__(self, engine):
11 | self.engine = engine
12 | super(Delayable, self).__init__()
13 |
14 | @classmethod
15 | def _all_subclasses(cls):
16 | return cls.__subclasses__() + [
17 | g for s in cls.__subclasses__() for g in s._all_subclasses()
18 | ]
19 |
20 | def add_document(self, document):
21 | pass
22 |
23 | def loop(self, document, *args):
24 | return
25 |
26 | def merge_args(self, args1, args2):
27 | return args2
28 |
29 | def remove_document(self, document):
30 | pass
31 |
32 |
33 | class RangeDelayable(Delayable):
34 | """Delayable designed to take a start and stop range as its first arguments."""
35 |
36 | def merge_args(self, args1, args2):
37 | """Uses the lowest start argument value. The end argument returns None if
38 | one of them is None otherwise the largest is returned. All other arguments
39 | of from args2 are used.
40 |
41 | Args:
42 | args1 (tuple): The old arguments.
43 | args2 (tuple): The new arguments.
44 |
45 | Returns:
46 | tuple: args2 with its first two arguments modified to the largest range.
47 | """
48 | start1 = args1[0]
49 | start2 = args2[0]
50 | start = min(start1, start2)
51 |
52 | end1 = args1[1]
53 | end2 = args2[1]
54 | if end1 is None or end2 is None:
55 | # Always prefer None for the end. It indicates that we want to
56 | # go to the end of the document.
57 | end = None
58 | else:
59 | end = max(end1, end2)
60 | return (start, end) + args2[2:]
61 |
62 |
63 | class SearchDelayable(Delayable):
64 | def loop(self, document, find_state):
65 | start, end = document.find_text(find_state)
66 | if find_state.wrapped:
67 | # once we have wrapped, disable wrap
68 | find_state.wrap = False
69 | if start != -1:
70 | self.text_found(document, start, end, find_state)
71 | return (find_state,)
72 |
73 | def search_from_position(self, document, find_state, position, *args):
74 | if position is None:
75 | position = document.positionFromLineIndex(*document.getCursorPosition())
76 | # Start searching from position, wrap past the end and stop where we started
77 | find_state.start_pos = position
78 | find_state.start_pos_original = position
79 | find_state.wrap = True
80 | find_state.wrapped = False
81 | self.engine.enqueue(document, self.key, find_state, *args)
82 |
83 | def text_found(self, document, start, end, find_state):
84 | """Called each time text is found."""
85 | raise NotImplementedError('SearchDelayable.text_found should be subclassed.')
86 |
--------------------------------------------------------------------------------
/preditor/excepthooks.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function
2 |
3 | import sys
4 | import traceback
5 |
6 | from Qt import QtCompat
7 |
8 | from . import config, plugins
9 | from .contexts import ErrorReport
10 |
11 |
12 | class PreditorExceptHook(object):
13 | """Replacement for `sys.excepthook` that adds error reporting features.
14 |
15 | This calls each callable in the `preditor.config.excepthooks` list any time
16 | `sys.excepthook` is called due to an raised exception.
17 |
18 | If `config.excepthook` is empty when installing this class, it will
19 | automatically add the `call_base_excepthook` and `ask_to_show_logger` methods.
20 | This enables showing the excepthook in parent streams and prompting the user
21 | to show PrEditor when an error happens. You can disable this by adding `None`
22 | to the list before this class is initialized.
23 | """
24 |
25 | def __init__(self, base_excepthook=None):
26 | self.base_excepthook = base_excepthook or sys.__excepthook__
27 |
28 | # Add the default excepthooks
29 | if not config.excepthooks and config.excepthooks != [None]:
30 | config.excepthooks.append(self.call_base_excepthook)
31 | config.excepthooks.append(self.ask_to_show_logger)
32 |
33 | def __call__(self, *exc_info):
34 | """Run when an exception is raised and calls all `config.excepthooks`."""
35 | for plugin in config.excepthooks:
36 | if plugin is None:
37 | continue
38 | plugin(*exc_info)
39 |
40 | # Clear any ErrorReports that were generated by this exception handling
41 | ErrorReport.clearReports()
42 |
43 | def call_base_excepthook(self, *exc_info):
44 | """Process `base_excepthook` supplied during object instantiation.
45 |
46 | This is useful for showing the exception in the original excepthook that
47 | PrEditor replaced when this class was installed.
48 |
49 | A newline is printed pre-traceback to ensure the first line of output
50 | is not printed in-line with the prompt. This also provides visual
51 | separation between tracebacks, when received consecutively.
52 | """
53 | print("")
54 | try:
55 | self.base_excepthook(*exc_info)
56 | except (TypeError, NameError):
57 | sys.__excepthook__(*exc_info)
58 |
59 | def ask_to_show_logger(self, *exc_info):
60 | """Show a dialog asking the user how to handle the error."""
61 | if config.error_dialog_class is True:
62 | # Default to the base ErrorDialog class
63 | from .gui.errordialog import ErrorDialog
64 |
65 | config.error_dialog_class = ErrorDialog
66 | elif isinstance(config.error_dialog_class, str):
67 | # If passed an EntryPoint string load the EntryPoint
68 | config.error_dialog_class = plugins.from_string(config.error_dialog_class)
69 |
70 | # Handle cases where we shouldn't ask to show the logger.
71 | if config.error_dialog_class is None:
72 | return
73 | elif not config.error_dialog_class.show_prompt(*exc_info):
74 | return
75 |
76 | from .gui.console import ConsolePrEdit
77 | from .gui.loggerwindow import LoggerWindow
78 |
79 | instance = LoggerWindow.instance(create=False)
80 |
81 | if instance:
82 | # logger reference deleted, fallback and print to console
83 | if not QtCompat.isValid(instance):
84 | print("[LoggerWindow] LoggerWindow object has been deleted.")
85 | # TODO: This seems incorrect, what should it be printing?
86 | print(traceback)
87 | return
88 |
89 | # logger is visible and check if it was minimized on windows
90 | if instance.isVisible() and not instance.isMinimized():
91 | if instance.uiAutoPromptACT.isChecked():
92 | instance.console().startInputLine()
93 | return
94 |
95 | # error already prompted by exception currently being handled
96 | if ConsolePrEdit._errorPrompted:
97 | return
98 |
99 | # Preemptively marking error as "prompted" (handled) to avoid errors
100 | # from being raised multiple times due to C++ and/or threading error
101 | # processing.
102 | try:
103 | ConsolePrEdit._errorPrompted = True
104 | errorDialog = config.error_dialog_class(config.root_window())
105 | errorDialog.setText(exc_info)
106 | errorDialog.exec_()
107 |
108 | # interrupted until dialog closed
109 | finally:
110 | ConsolePrEdit._errorPrompted = False
111 |
112 | @classmethod
113 | def install(cls, force=False):
114 | """
115 | Install PrEditor excepthook override, returning previously implemented
116 | excepthook function.
117 |
118 | Arguments:
119 | force (bool): force re-installation of excepthook override when
120 | already previously implemented.
121 |
122 | Returns:
123 | func: pre-override excepthook function
124 | """
125 | ErrorReport.enabled = True
126 | prev_excepthook = sys.excepthook
127 |
128 | if not isinstance(prev_excepthook, cls) or force:
129 | sys.excepthook = cls(prev_excepthook)
130 |
131 | return prev_excepthook
132 |
--------------------------------------------------------------------------------
/preditor/gui/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from functools import partial
4 |
5 | from Qt.QtCore import Property
6 | from Qt.QtWidgets import QStackedWidget
7 |
8 | from .dialog import Dialog # noqa: F401
9 | from .window import Window # noqa: F401
10 |
11 |
12 | def QtPropertyInit(name, default, callback=None, typ=None):
13 | """Initializes a default Property value with a usable getter and setter.
14 |
15 | You can optionally pass a function that will get called any time the property
16 | is set. If using the same callback for multiple properties, you may want to
17 | use the preditor.decorators.singleShot decorator to prevent your function getting
18 | called multiple times at once. This callback must accept the attribute name and
19 | value being set.
20 |
21 | Example:
22 | class TestClass(QWidget):
23 | def __init__(self, *args, **kwargs):
24 | super(TestClass, self).__init__(*args, **kwargs)
25 |
26 | stdoutColor = QtPropertyInit('_stdoutColor', QColor(0, 0, 255))
27 | pyForegroundColor = QtPropertyInit('_pyForegroundColor', QColor(0, 0, 255))
28 |
29 | Args:
30 | name(str): The name of internal attribute to store to and lookup from.
31 | default: The property's default value. This will also define the Property type
32 | if typ is not set.
33 | callback(callable): If provided this function is called when the property is
34 | set.
35 | typ (class, optional): If not None this value is used to specify the type of
36 | the Property. This is useful when you need to specify a property as python's
37 | object but pass a default value of a given class.
38 |
39 | Returns:
40 | Property
41 | """
42 |
43 | def _getattrDefault(default, self, attrName):
44 | try:
45 | value = getattr(self, attrName)
46 | except AttributeError:
47 | setattr(self, attrName, default)
48 | return default
49 | return value
50 |
51 | def _setattrCallback(callback, attrName, self, value):
52 | setattr(self, attrName, value)
53 | if callback:
54 | callback(self, attrName, value)
55 |
56 | ga = partial(_getattrDefault, default)
57 | sa = partial(_setattrCallback, callback, name)
58 | # Use the default value's class if typ is not provided.
59 | if typ is None:
60 | typ = default.__class__
61 | return Property(typ, fget=(lambda s: ga(s, name)), fset=(lambda s, v: sa(s, v)))
62 |
63 |
64 | def loadUi(filename, widget, uiname=''):
65 | """use's Qt's uic loader to load dynamic interafces onto the inputed widget
66 |
67 | Args:
68 | filename (str): The python filename. Its basename will be split off, and a
69 | ui folder will be added. The file ext will be changed to .ui
70 | widget (QWidget): The basewidget the ui file will be loaded onto.
71 | uiname (str, optional): Used instead of the basename. This is useful if
72 | filename is not the same as the ui file you want to load.
73 | """
74 | import os.path
75 |
76 | from Qt import QtCompat
77 |
78 | # first, inherit the palette of the parent
79 | if widget.parent():
80 | widget.setPalette(widget.parent().palette())
81 |
82 | if not uiname:
83 | uiname = os.path.basename(filename).split('.')[0]
84 |
85 | QtCompat.loadUi(os.path.split(filename)[0] + '/ui/%s.ui' % uiname, widget)
86 |
87 |
88 | def tab_widget_for_tab(tab_widget):
89 | """Returns the `QTabWidget` `tab_widget` is parented to or `None`."""
90 | tab_parent = tab_widget.parent()
91 | if not isinstance(tab_parent, QStackedWidget):
92 | return None
93 | return tab_parent.parent()
94 |
--------------------------------------------------------------------------------
/preditor/gui/editor_chooser.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from Qt.QtCore import Slot
4 | from Qt.QtGui import QIcon
5 | from Qt.QtWidgets import QWidget
6 |
7 | from .. import plugins, resourcePath
8 | from ..gui import loadUi
9 |
10 |
11 | class EditorChooser(QWidget):
12 | """A widget that lets the user choose from a a list of available editors."""
13 |
14 | def __init__(self, parent=None, editor_name=None):
15 | super(EditorChooser, self).__init__(parent=parent)
16 | loadUi(__file__, self)
17 | icon = QIcon(resourcePath('img/warning-big.png'))
18 | self.uiWarningIconLBL.setPixmap(icon.pixmap(icon.availableSizes()[0]))
19 | if editor_name:
20 | self.set_editor_name(editor_name)
21 |
22 | def editor_name(self):
23 | return self.uiWorkboxEditorDDL.currentText()
24 |
25 | def set_editor_name(self, name):
26 | index = self.uiWorkboxEditorDDL.findText(name)
27 | if index == -1:
28 | self.uiWorkboxEditorDDL.addItem(name)
29 | index = self.uiWorkboxEditorDDL.findText(name)
30 | self.uiWorkboxEditorDDL.setCurrentIndex(index)
31 |
32 | @Slot()
33 | def refresh(self):
34 | warning = "Choose an editor to enable Workboxs."
35 | editor_name = self.editor_name()
36 | if editor_name:
37 | _, editor = plugins.editor(editor_name)
38 | warning = editor._warning_text
39 | self.uiWarningIconLBL.setVisible(bool(warning))
40 | self.uiWarningTextLBL.setVisible(bool(warning))
41 | self.uiWarningTextLBL.setText(warning)
42 |
43 | def refresh_editors(self):
44 | current = self.editor_name()
45 | self.uiWorkboxEditorDDL.blockSignals(True)
46 | self.uiWorkboxEditorDDL.clear()
47 | for name, _ in sorted(set(plugins.editors())):
48 | self.uiWorkboxEditorDDL.addItem(name)
49 |
50 | self.uiWorkboxEditorDDL.setCurrentIndex(
51 | self.uiWorkboxEditorDDL.findText(current)
52 | )
53 | self.uiWorkboxEditorDDL.blockSignals(False)
54 |
55 | def showEvent(self, event): # noqa: N802
56 | super(EditorChooser, self).showEvent(event)
57 | self.refresh_editors()
58 |
--------------------------------------------------------------------------------
/preditor/gui/errordialog.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import os
4 | import traceback
5 |
6 | from Qt.QtCore import Qt
7 | from Qt.QtGui import QColor, QPixmap
8 |
9 | from .. import __file__ as pfile
10 | from . import Dialog, QtPropertyInit, loadUi
11 |
12 |
13 | class ErrorDialog(Dialog):
14 | # These Qt Properties can be customized using style sheets.
15 | errorMessageColor = QtPropertyInit('_errorMessageColor', QColor(Qt.GlobalColor.red))
16 |
17 | def __init__(self, parent):
18 | super(ErrorDialog, self).__init__(parent)
19 |
20 | loadUi(__file__, self)
21 |
22 | self.parent_ = parent
23 | self.setWindowTitle('Error Occurred')
24 | self.uiErrorLBL.setTextFormat(Qt.RichText)
25 | self.uiIconLBL.setPixmap(
26 | QPixmap(
27 | os.path.join(
28 | os.path.dirname(pfile),
29 | 'resource',
30 | 'img',
31 | 'warning-big.png',
32 | )
33 | ).scaledToHeight(64, Qt.SmoothTransformation)
34 | )
35 |
36 | self.uiLoggerBTN.clicked.connect(self.show_logger)
37 | self.uiIgnoreBTN.clicked.connect(self.close)
38 |
39 | def setText(self, exc_info):
40 | self.traceback_msg = "".join(traceback.format_exception(*exc_info))
41 | msg = (
42 | 'The following error has occurred:
'
43 | '
%(text)s'
44 | )
45 | self.uiErrorLBL.setText(
46 | msg
47 | % {
48 | 'text': self.traceback_msg.split('\n')[-2],
49 | 'color': self.errorMessageColor.name(),
50 | }
51 | )
52 |
53 | def show_logger(self):
54 | """Create/show the main PrEditor instance with the full traceback."""
55 | from .. import launch
56 |
57 | launch()
58 | self.close()
59 |
60 | @classmethod
61 | def show_prompt(cls, *exc_info):
62 | """Return False to this dialog should not be shown on an exception.
63 |
64 | This is useful for applications like Nuke which uses exceptions to signal
65 | traditionally non-exception worthy events, such as when a user cancels
66 | an Open File dialog window.
67 | """
68 | return True
69 |
--------------------------------------------------------------------------------
/preditor/gui/find_files.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function
2 |
3 | from Qt.QtCore import Qt, Slot
4 | from Qt.QtGui import QIcon, QKeySequence
5 | from Qt.QtWidgets import QApplication, QShortcut, QWidget
6 |
7 | from .. import resourcePath
8 | from ..utils.text_search import RegexTextSearch, SimpleTextSearch
9 | from . import loadUi
10 |
11 |
12 | class FindFiles(QWidget):
13 | def __init__(self, parent=None, managers=None, console=None):
14 | super(FindFiles, self).__init__(parent=parent)
15 | if managers is None:
16 | managers = []
17 | self.managers = managers
18 | self.console = console
19 | self.finder = None
20 | self.match_files_count = 0
21 |
22 | loadUi(__file__, self)
23 |
24 | # Set the icons
25 | self.uiCaseSensitiveBTN.setIcon(
26 | QIcon(resourcePath("img/format-letter-case.svg"))
27 | )
28 | self.uiCloseBTN.setIcon(QIcon(resourcePath('img/close-thick.png')))
29 | self.uiRegexBTN.setIcon(QIcon(resourcePath("img/regex.svg")))
30 |
31 | # Create shortcuts
32 | self.uiCloseSCT = QShortcut(
33 | QKeySequence(Qt.Key_Escape), self, context=Qt.WidgetWithChildrenShortcut
34 | )
35 |
36 | self.uiCloseSCT.activated.connect(self.hide)
37 |
38 | self.uiCaseSensitiveSCT = QShortcut(
39 | QKeySequence(Qt.AltModifier | Qt.Key_C),
40 | self,
41 | context=Qt.WidgetWithChildrenShortcut,
42 | )
43 | self.uiCaseSensitiveSCT.activated.connect(self.uiCaseSensitiveBTN.toggle)
44 |
45 | self.uiRegexSCT = QShortcut(
46 | QKeySequence(Qt.AltModifier | Qt.Key_R),
47 | self,
48 | context=Qt.WidgetWithChildrenShortcut,
49 | )
50 | self.uiRegexSCT.activated.connect(self.uiRegexBTN.toggle)
51 |
52 | def activate(self):
53 | """Called to make this widget ready for the user to interact with."""
54 | self.show()
55 | self.uiFindTXT.setFocus()
56 |
57 | @Slot()
58 | def find(self):
59 | find_text = self.uiFindTXT.text()
60 | context = self.uiContextSPN.value()
61 | # Create an instance of the TextSearch to use for this search
62 | if self.uiRegexBTN.isChecked():
63 | TextSearch = RegexTextSearch
64 | else:
65 | TextSearch = SimpleTextSearch
66 | self.finder = TextSearch(
67 | find_text, self.uiCaseSensitiveBTN.isChecked(), context=context
68 | )
69 | self.finder.callback_matching = self.insert_found_text
70 | self.finder.callback_non_matching = self.insert_text
71 |
72 | self.insert_text(self.finder.title())
73 |
74 | self.match_files_count = 0
75 | for manager in self.managers:
76 | for (
77 | editor,
78 | group_name,
79 | tab_name,
80 | group_index,
81 | tab_index,
82 | ) in manager.all_widgets():
83 | path = "/".join((group_name, tab_name))
84 | workbox_id = '{},{}'.format(group_index, tab_index)
85 | self.find_in_editor(editor, path, workbox_id)
86 |
87 | self.insert_text(
88 | '\n{} matches in {} workboxes\n'.format(
89 | self.finder.match_count, self.match_files_count
90 | )
91 | )
92 |
93 | def find_in_editor(self, editor, path, workbox_id):
94 | # Ensure the editor text is loaded and get its raw text
95 | editor.__show__()
96 | text = editor.__text__()
97 |
98 | # Use the finder to check for matches
99 | found = self.finder.search_text(text, path, workbox_id)
100 | if found:
101 | self.match_files_count += 1
102 |
103 | def insert_found_text(self, text, workbox_id, line_num, tool_tip):
104 | href = ', {}, {}'.format(workbox_id, line_num)
105 | cursor = self.console.textCursor()
106 | # Insert hyperlink
107 | fmt = cursor.charFormat()
108 | fmt.setAnchor(True)
109 | fmt.setAnchorHref(href)
110 | fmt.setFontUnderline(True)
111 | fmt.setToolTip(tool_tip)
112 | cursor.insertText(text, fmt)
113 | # Show the updated text output
114 | QApplication.instance().processEvents()
115 |
116 | def insert_text(self, text):
117 | cursor = self.console.textCursor()
118 | fmt = cursor.charFormat()
119 | fmt.setAnchor(False)
120 | fmt.setAnchorHref('')
121 | fmt.setFontUnderline(False)
122 | fmt.setToolTip('')
123 | cursor.insertText(text, fmt)
124 | # Show the updated text output
125 | QApplication.instance().processEvents()
126 |
--------------------------------------------------------------------------------
/preditor/gui/fuzzy_search/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/gui/fuzzy_search/__init__.py
--------------------------------------------------------------------------------
/preditor/gui/fuzzy_search/fuzzy_search.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from functools import partial
4 |
5 | from Qt.QtCore import QModelIndex, QPoint, Qt, Signal
6 | from Qt.QtWidgets import QFrame, QLineEdit, QListView, QShortcut, QVBoxLayout
7 |
8 | from ..group_tab_widget.grouped_tab_models import GroupTabFuzzyFilterProxyModel
9 |
10 |
11 | class FuzzySearch(QFrame):
12 | canceled = Signal("QModelIndex")
13 | """Passes the original QModelIndex for the tab that was selected when the
14 | widget was first shown. This lets you reset back to the orignal state."""
15 | highlighted = Signal("QModelIndex")
16 | """Emitted when the user navitages to the given index, but hasn't selected."""
17 | selected = Signal("QModelIndex")
18 | """Emitted when the user selects a item."""
19 |
20 | def __init__(self, model, parent=None, **kwargs):
21 | super(FuzzySearch, self).__init__(parent=parent, **kwargs)
22 | self.y_offset = 100
23 | self.setMinimumSize(400, 200)
24 | self.uiCloseSCT = QShortcut(
25 | Qt.Key_Escape, self, context=Qt.WidgetWithChildrenShortcut
26 | )
27 | self.uiCloseSCT.activated.connect(self._canceled)
28 |
29 | self.uiUpSCT = QShortcut(Qt.Key_Up, self, context=Qt.WidgetWithChildrenShortcut)
30 | self.uiUpSCT.activated.connect(partial(self.increment_selection, -1))
31 | self.uiDownSCT = QShortcut(
32 | Qt.Key_Down, self, context=Qt.WidgetWithChildrenShortcut
33 | )
34 | self.uiDownSCT.activated.connect(partial(self.increment_selection, 1))
35 |
36 | lyt = QVBoxLayout(self)
37 | self.uiLineEDIT = QLineEdit(parent=self)
38 | self.uiLineEDIT.textChanged.connect(self.update_completer)
39 | self.uiLineEDIT.returnPressed.connect(self.activated)
40 | lyt.addWidget(self.uiLineEDIT)
41 | self.uiResultsLIST = QListView(self)
42 | self.uiResultsLIST.activated.connect(self.activated)
43 | self.proxy_model = GroupTabFuzzyFilterProxyModel(self)
44 | self.proxy_model.setSourceModel(model)
45 | self.uiResultsLIST.setModel(self.proxy_model)
46 | lyt.addWidget(self.uiResultsLIST)
47 |
48 | self.original_model_index = model.original_model_index
49 |
50 | def activated(self):
51 | current = self.uiResultsLIST.currentIndex()
52 | self.selected.emit(current)
53 | self.hide()
54 |
55 | def increment_selection(self, direction):
56 | current = self.uiResultsLIST.currentIndex()
57 | col = 0
58 | row = 0
59 | if current.isValid():
60 | col = current.column()
61 | row = current.row() + direction
62 | new = self.uiResultsLIST.model().index(row, col)
63 | self.uiResultsLIST.setCurrentIndex(new)
64 | self.highlighted.emit(new)
65 |
66 | def update_completer(self, wildcard):
67 | if wildcard:
68 | if not self.uiResultsLIST.currentIndex().isValid():
69 | new = self.uiResultsLIST.model().index(0, 0)
70 | self.uiResultsLIST.setCurrentIndex(new)
71 | else:
72 | self.uiResultsLIST.clearSelection()
73 | self.uiResultsLIST.setCurrentIndex(QModelIndex())
74 | self.proxy_model.setFuzzySearch(wildcard)
75 | self.highlighted.emit(self.uiResultsLIST.currentIndex())
76 |
77 | def _canceled(self):
78 | # Restore the original tab as the user didn't choose the new tab
79 | self.canceled.emit(self.original_model_index)
80 | self.hide()
81 |
82 | def reposition(self):
83 | pgeo = self.parent().geometry()
84 | geo = self.geometry()
85 | center = QPoint(pgeo.width() // 2, 0)
86 | geo.moveCenter(center)
87 | geo.setY(self.y_offset)
88 | self.setGeometry(geo)
89 |
90 | def popup(self):
91 | self.show()
92 | self.reposition()
93 | self.uiLineEDIT.setFocus(Qt.PopupFocusReason)
94 |
--------------------------------------------------------------------------------
/preditor/gui/group_tab_widget/grouped_tab_menu.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from preditor.gui.level_buttons import LazyMenu
4 |
5 |
6 | class GroupTabMenu(LazyMenu):
7 | """A menu listing all tabs of GroupTabWidget and their child GroupedTabWidget
8 | tabs. When selecting one of the GroupedTabWidget tab, it will make that tab
9 | the current tab and give it focus.
10 | """
11 |
12 | def __init__(self, manager, parent=None):
13 | super(GroupTabMenu, self).__init__(parent=parent)
14 | self.manager = manager
15 | self.triggered.connect(self.focus_tab)
16 |
17 | def refresh(self):
18 | self.clear()
19 | for group in range(self.manager.count()):
20 | # Create a "header" for the group tabs
21 | self.addSeparator()
22 | act = self.addAction(self.manager.tabText(group))
23 | act.setEnabled(False)
24 | self.addSeparator()
25 |
26 | # Add all of this group tab's tabs
27 | tab_widget = self.manager.widget(group)
28 | for index in range(tab_widget.count()):
29 | act = self.addAction(' {}'.format(tab_widget.tabText(index)))
30 | act.setProperty('info', (group, index))
31 |
32 | def focus_tab(self, action):
33 | group, editor = action.property('info')
34 | widget = self.manager.set_current_groups_from_index(group, editor)
35 | widget.setFocus()
36 |
--------------------------------------------------------------------------------
/preditor/gui/group_tab_widget/grouped_tab_models.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import re
4 |
5 | from Qt.QtCore import QSortFilterProxyModel, Qt
6 | from Qt.QtGui import QStandardItem, QStandardItemModel
7 |
8 |
9 | class GroupTabItemModel(QStandardItemModel):
10 | GroupIndexRole = Qt.UserRole + 1
11 | TabIndexRole = GroupIndexRole + 1
12 |
13 | def __init__(self, manager, *args, **kwargs):
14 | super(GroupTabItemModel, self).__init__(*args, **kwargs)
15 | self.manager = manager
16 |
17 | def workbox_indexes_from_model_index(self, index):
18 | """Returns the group_index and tab_index for the provided QModelIndex"""
19 | return (
20 | index.data(self.GroupIndexRole),
21 | index.data(self.TabIndexRole),
22 | )
23 |
24 | def pathFromIndex(self, index):
25 | parts = [""]
26 | while index.isValid():
27 | parts.append(self.data(index, Qt.DisplayRole))
28 | index = index.parent()
29 | if len(parts) == 1:
30 | return ""
31 | return "/".join([x for x in parts[::-1] if x])
32 |
33 |
34 | class GroupTabTreeItemModel(GroupTabItemModel):
35 | def process(self):
36 | root = self.invisibleRootItem()
37 | current_group = self.manager.currentIndex()
38 | current_tab = self.manager.currentWidget().currentIndex()
39 |
40 | prev_group = -1
41 | all_widgets = self.manager.all_widgets()
42 | for _, group_name, tab_name, group_index, tab_index in all_widgets:
43 | if prev_group != group_index:
44 | group_item = QStandardItem(group_name)
45 | group_item.setData(group_index, self.GroupIndexRole)
46 | root.appendRow(group_item)
47 | prev_group = group_index
48 |
49 | tab_item = QStandardItem(tab_name)
50 | tab_item.setData(group_index, self.GroupIndexRole)
51 | tab_item.setData(tab_index, self.TabIndexRole)
52 | group_item.appendRow(tab_item)
53 | if group_index == current_group and tab_index == current_tab:
54 | self.original_model_index = self.indexFromItem(tab_item)
55 |
56 |
57 | class GroupTabListItemModel(GroupTabItemModel):
58 | def flags(self, index):
59 | return Qt.ItemIsEnabled | Qt.ItemIsSelectable
60 |
61 | def process(self):
62 | root = self.invisibleRootItem()
63 | current_group = self.manager.currentIndex()
64 | current_tab = self.manager.currentWidget().currentIndex()
65 |
66 | all_widgets = self.manager.all_widgets()
67 | for _, group_name, tab_name, group_index, tab_index in all_widgets:
68 | tab_item = QStandardItem('/'.join((group_name, tab_name)))
69 | tab_item.setData(group_index, self.GroupIndexRole)
70 | tab_item.setData(tab_index, self.TabIndexRole)
71 | root.appendRow(tab_item)
72 | if group_index == current_group and tab_index == current_tab:
73 | self.original_model_index = self.indexFromItem(tab_item)
74 |
75 |
76 | class GroupTabFuzzyFilterProxyModel(QSortFilterProxyModel):
77 | """Implements a fuzzy search filter proxy model."""
78 |
79 | def __init__(self, parent=None):
80 | super(GroupTabFuzzyFilterProxyModel, self).__init__(parent=parent)
81 | self._fuzzy_regex = None
82 |
83 | def setFuzzySearch(self, search):
84 | search = '.*'.join(search)
85 | # search = '.*{}.*'.format(search)
86 | self._fuzzy_regex = re.compile(search, re.I)
87 | self.invalidateFilter()
88 |
89 | def filterAcceptsRow(self, sourceRow, sourceParent):
90 | if self.filterKeyColumn() == 0 and self._fuzzy_regex:
91 |
92 | index = self.sourceModel().index(sourceRow, 0, sourceParent)
93 | data = self.sourceModel().data(index)
94 | ret = bool(self._fuzzy_regex.search(data))
95 | return ret
96 |
97 | return super(GroupTabFuzzyFilterProxyModel, self).filterAcceptsRow(
98 | sourceRow, sourceParent
99 | )
100 |
101 | def pathFromIndex(self, index):
102 | parts = [""]
103 | while index.isValid():
104 | parts.append(self.data(index, Qt.DisplayRole))
105 | index = index.parent()
106 | if len(parts) == 1:
107 | return ""
108 | return "/".join([x for x in parts[::-1] if x])
109 |
--------------------------------------------------------------------------------
/preditor/gui/group_tab_widget/grouped_tab_widget.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from Qt.QtCore import Qt
4 | from Qt.QtGui import QIcon
5 | from Qt.QtWidgets import QMessageBox, QToolButton
6 |
7 | from ... import resourcePath
8 | from ..drag_tab_bar import DragTabBar
9 | from ..workbox_text_edit import WorkboxTextEdit
10 | from .one_tab_widget import OneTabWidget
11 |
12 |
13 | class GroupedTabWidget(OneTabWidget):
14 | def __init__(self, editor_kwargs, editor_cls=None, core_name=None, *args, **kwargs):
15 | super(GroupedTabWidget, self).__init__(*args, **kwargs)
16 | DragTabBar.install_tab_widget(self, 'grouped_tab_widget')
17 | self.editor_kwargs = editor_kwargs
18 | if editor_cls is None:
19 | editor_cls = WorkboxTextEdit
20 | self.editor_cls = editor_cls
21 | self.core_name = core_name
22 | self.currentChanged.connect(self.tab_shown)
23 |
24 | self.uiCornerBTN = QToolButton(self)
25 | self.uiCornerBTN.setText('+')
26 | self.uiCornerBTN.setIcon(QIcon(resourcePath('img/file-plus.png')))
27 | self.uiCornerBTN.released.connect(lambda: self.add_new_editor())
28 | self.setCornerWidget(self.uiCornerBTN, Qt.TopRightCorner)
29 |
30 | def add_new_editor(self, title="Workbox"):
31 | editor, title = self.default_tab(title)
32 | index = self.addTab(editor, title)
33 | self.setCurrentIndex(index)
34 | return editor
35 |
36 | def addTab(self, *args, **kwargs): # noqa: N802
37 | ret = super(GroupedTabWidget, self).addTab(*args, **kwargs)
38 | self.update_closable_tabs()
39 | return ret
40 |
41 | def close_tab(self, index):
42 | if self.count() == 1:
43 | msg = "You have to leave at least one tab open."
44 | QMessageBox.critical(self, 'Tab can not be closed.', msg, QMessageBox.Ok)
45 | return
46 | ret = QMessageBox.question(
47 | self,
48 | 'Donate to the cause?',
49 | "Would you like to donate this tabs contents to the /dev/null fund "
50 | "for wayward code?",
51 | QMessageBox.Yes | QMessageBox.Cancel,
52 | )
53 | if ret == QMessageBox.Yes:
54 | # If the tab was saved to a temp file, remove it from disk
55 | editor = self.widget(index)
56 | editor.__remove_tempfile__()
57 |
58 | super(GroupedTabWidget, self).close_tab(index)
59 |
60 | def default_tab(self, title='Workbox'):
61 | kwargs = self.editor_kwargs if self.editor_kwargs else {}
62 | editor = self.editor_cls(parent=self, core_name=self.core_name, **kwargs)
63 | return editor, title
64 |
65 | def showEvent(self, event): # noqa: N802
66 | super(GroupedTabWidget, self).showEvent(event)
67 | self.tab_shown(self.currentIndex())
68 |
69 | def tab_shown(self, index):
70 | editor = self.widget(index)
71 | if editor and editor.isVisible():
72 | editor.__show__()
73 |
74 | if hasattr(self.window(), "setWorkboxFontBasedOnConsole"):
75 | self.window().setWorkboxFontBasedOnConsole()
76 |
77 | def update_closable_tabs(self):
78 | self.setTabsClosable(self.count() != 1)
79 |
--------------------------------------------------------------------------------
/preditor/gui/group_tab_widget/one_tab_widget.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from Qt.QtWidgets import QTabWidget
4 |
5 |
6 | class OneTabWidget(QTabWidget):
7 | """A QTabWidget that shows the close button only if there is more than one
8 | tab. If something removes the last tab, it will add a default tab if the
9 | default_tab method is implemented on a subclass. This is also used to create
10 | the first tab on showEvent.
11 |
12 | Subclasses can implement a `default_tab()` method. This should return the
13 | widget to add and the title of the tab to create if implemented.
14 | """
15 |
16 | def __init__(self, *args, **kwargs):
17 | super(OneTabWidget, self).__init__(*args, **kwargs)
18 | self.tabCloseRequested.connect(self.close_tab)
19 |
20 | def addTab(self, *args, **kwargs): # noqa: N802
21 | ret = super(OneTabWidget, self).addTab(*args, **kwargs)
22 | self.update_closable_tabs()
23 | return ret
24 |
25 | def close_tab(self, index):
26 | self.removeTab(index)
27 | self.update_closable_tabs()
28 |
29 | def index_for_text(self, text):
30 | """Return the index of the tab with this text. Returns -1 if not found"""
31 | for i in range(self.count()):
32 | if self.tabText(i) == text:
33 | return i
34 | return -1
35 |
36 | def insertTab(self, *args, **kwargs): # noqa: N802
37 | ret = super(OneTabWidget, self).insertTab(*args, **kwargs)
38 | self.update_closable_tabs()
39 | return ret
40 |
41 | def removeTab(self, index): # noqa: N802
42 | super(OneTabWidget, self).removeTab(index)
43 | if hasattr(self, 'default_tab') and not self.count():
44 | self.addTab(*self.default_tab())
45 | self.update_closable_tabs()
46 |
47 | def showEvent(self, event): # noqa: N802
48 | super(OneTabWidget, self).showEvent(event)
49 | # Force the creation of a default tab if defined
50 | if hasattr(self, 'default_tab') and not self.count():
51 | self.addTab(*self.default_tab())
52 |
53 | def update_closable_tabs(self):
54 | self.setTabsClosable(self.count() != 1)
55 |
--------------------------------------------------------------------------------
/preditor/gui/logger_window_handler.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import logging
4 |
5 | from .. import instance
6 |
7 |
8 | class LoggerWindowHandler(logging.Handler):
9 | """A logging handler that writes directly to the PrEditor instance.
10 |
11 | Args:
12 | error (bool, optional): Write the output as if it were written
13 | to sys.stderr and not sys.stdout. Ie in red text.
14 | formatter (str or logging.Formatter, optional): If specified,
15 | this is passed to setFormatter.
16 | """
17 |
18 | default_format = (
19 | '%(levelname)s %(module)s.%(funcName)s line:%(lineno)d - %(message)s'
20 | )
21 |
22 | def __init__(self, error=True, formatter=default_format):
23 | super(LoggerWindowHandler, self).__init__()
24 | self.error = error
25 | if formatter is not None:
26 | if not isinstance(formatter, logging.Formatter):
27 | formatter = logging.Formatter(formatter)
28 | self.setFormatter(formatter)
29 |
30 | def emit(self, record):
31 | _instance = instance(create=False)
32 | if _instance is None:
33 | # No gui has been created yet, so nothing to do
34 | return
35 | try:
36 | # If the python logger was closed and garbage collected,
37 | # there is nothing to do, simply exit the call
38 | console = _instance.console()
39 | if not console:
40 | return
41 |
42 | msg = self.format(record)
43 | msg = u'{}\n'.format(msg)
44 | console.write(msg, self.error)
45 | except (KeyboardInterrupt, SystemExit):
46 | raise
47 | except Exception:
48 | self.handleError(record)
49 |
--------------------------------------------------------------------------------
/preditor/gui/logger_window_plugin.py:
--------------------------------------------------------------------------------
1 | class LoggerWindowPlugin:
2 | """Base class for LoggerWindow plugins.
3 |
4 | These plugins are loaded using the `preditor.plug.loggerwindow` entry point.
5 | This entry point is loaded when `LoggerWindow` is initialized. For each entry
6 | point defined a single instance of the plugin is created per instance of
7 | a LoggerWindow.
8 |
9 | To save preferences override `record_prefs` and `restore_prefs` methods. These
10 | are used to save and load preferences any time the PrEditor save/loads prefs.
11 | """
12 |
13 | def __init__(self, parent):
14 | self.parent = parent
15 |
16 | def record_prefs(self, name):
17 | """Returns any prefs to save with the PrEditor's preferences.
18 |
19 | Returns:
20 | dict: A dictionary that will be saved using json or None.
21 | """
22 |
23 | def restore_prefs(self, name, prefs):
24 | """Restore the preferences saved from a previous launch.
25 |
26 | Args:
27 | name(str): The name specified by the `preditor.plug.loggerwindow`
28 | entry point.
29 | prefs(dict or None): The prefs returned by a previous call to
30 | `record_prefs()` from the last preference save. None is passed
31 | if no prefs were recorded.
32 | """
33 |
--------------------------------------------------------------------------------
/preditor/gui/newtabwidget.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from Qt.QtCore import QSize, Signal
4 | from Qt.QtWidgets import QPushButton, QTabBar, QTabWidget
5 |
6 | # This class is pulled from this example
7 | # http://stackoverflow.com/a/20098415
8 |
9 |
10 | class TabBarPlus(QTabBar):
11 | plusClicked = Signal()
12 |
13 | def __init__(self, parent=None):
14 | super(TabBarPlus, self).__init__(parent)
15 | # Plus Button
16 | self.uiPlusBTN = QPushButton("+")
17 | self.uiPlusBTN.setParent(self)
18 | self.uiPlusBTN.setObjectName('uiPlusBTN')
19 | self.uiPlusBTN.clicked.connect(self.plusClicked.emit)
20 | self.movePlusButton()
21 |
22 | def sizeHint(self):
23 | sizeHint = QTabBar.sizeHint(self)
24 | width = sizeHint.width()
25 | height = sizeHint.height()
26 | return QSize(width + 25, height)
27 |
28 | def resizeEvent(self, event):
29 | super(TabBarPlus, self).resizeEvent(event)
30 | self.movePlusButton()
31 |
32 | def tabLayoutChange(self):
33 | super(TabBarPlus, self).tabLayoutChange()
34 |
35 | self.movePlusButton()
36 |
37 | def movePlusButton(self):
38 | size = 0
39 | for i in range(self.count()):
40 | size += self.tabRect(i).width()
41 |
42 | h = self.geometry().top()
43 | w = self.width()
44 | if size > w: # Show just to the left of the scroll buttons
45 | self.uiPlusBTN.move(w - 54, h)
46 | else:
47 | self.uiPlusBTN.move(size, h)
48 | # Resize the button to fit the height of the tab bar
49 | hint = self.sizeHint().height()
50 | self.uiPlusBTN.setMaximumSize(hint, hint)
51 | self.uiPlusBTN.setMinimumSize(hint, hint)
52 |
53 |
54 | class NewTabWidget(QTabWidget):
55 | addTabClicked = Signal()
56 |
57 | def __init__(self, parent=None):
58 | super(NewTabWidget, self).__init__(parent)
59 |
60 | # Tab Bar
61 | self._tab = TabBarPlus()
62 | self.setTabBar(self._tab)
63 |
64 | # Properties
65 | self.setMovable(True)
66 | self.setTabsClosable(True)
67 |
68 | # Signals
69 | self._tab.plusClicked.connect(self.addTabClicked.emit)
70 |
--------------------------------------------------------------------------------
/preditor/gui/set_text_editor_path_dialog.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import os
4 |
5 | from Qt.QtWidgets import QDialog, QMessageBox
6 |
7 | from . import loadUi
8 |
9 |
10 | class SetTextEditorPathDialog(QDialog):
11 | """A dialog used to set the user's text editor executable path, as well as define a
12 | 'command template', which allows for the various ways text editor's may implement
13 | opening a file at a given line number via Command Prompt.
14 | """
15 |
16 | def __init__(self, parent=None):
17 | super(SetTextEditorPathDialog, self).__init__(parent)
18 | loadUi(__file__, self)
19 |
20 | # Retrieve existing data from LoggerWindow
21 | path = self.parent().textEditorPath
22 | cmdTempl = self.parent().textEditorCmdTempl
23 |
24 | # If the data exists, place in the UI, otherwise use UI defaults
25 | if path:
26 | self.uiTextEditorExecutablePathLE.setText(path)
27 | if cmdTempl:
28 | self.uiTextEditorCommandPatternLE.setText(cmdTempl)
29 |
30 | toolTip = (
31 | "Examples:\n"
32 | 'SublimeText: "{exePath}" "{modulePath}":{lineNum}\n'
33 | 'notepad++: "{exePath}" "{modulePath}" -n{lineNum}\n'
34 | 'vim: "{exePath}" + {lineNum} "{modulePath}'
35 | )
36 | self.uiTextEditorCommandPatternLE.setToolTip(toolTip)
37 |
38 | def accept(self):
39 | """Validate that the path exists and is executable.
40 | Can't really validate the command template, so instead we use try/except when
41 | issuing the command.
42 | """
43 | path = self.uiTextEditorExecutablePathLE.text()
44 | cmdTempl = self.uiTextEditorCommandPatternLE.text()
45 |
46 | path = path.strip("\"")
47 |
48 | # isExecutable is not very accurate, because on Windows, .jpg and .txt, etc
49 | # files are returned as executable. Perhaps on other systems, it's actually
50 | # relevant to whether the file is 'executable'.
51 | isExecutable = os.access(path, os.X_OK)
52 | if isExecutable:
53 | self.parent().textEditorPath = path
54 | self.parent().textEditorCmdTempl = cmdTempl
55 | super(SetTextEditorPathDialog, self).accept()
56 | else:
57 | msg = "That path doesn't exists or isn't an executable file."
58 | label = 'Incorrect Path'
59 | QMessageBox.warning(self.window(), label, msg, QMessageBox.Ok)
60 |
--------------------------------------------------------------------------------
/preditor/gui/status_label.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from collections import deque
4 | from functools import partial
5 |
6 | from Qt.QtCore import QPoint, QTimer
7 | from Qt.QtWidgets import QApplication, QInputDialog, QLabel, QMenu
8 |
9 |
10 | class StatusLabel(QLabel):
11 | """A label that shows text and an average of code execution times in popup menu."""
12 |
13 | def __init__(self, *args, limit=5, **kwargs):
14 | self.render_as_href = False
15 | super(StatusLabel, self).__init__(*args, **kwargs)
16 | self.times = deque(maxlen=limit)
17 |
18 | def clear(self):
19 | self.setText("")
20 |
21 | def clearTimes(self):
22 | """"""
23 | self.times.clear()
24 |
25 | def chooseLimit(self):
26 | limit, success = QInputDialog.getInt(
27 | self,
28 | "Choose Avg length",
29 | "Choose how many execution time history to keep.",
30 | value=self.limit(),
31 | min=1,
32 | max=100,
33 | )
34 | if limit:
35 | self.setLimit(limit)
36 |
37 | def copy_action_text(self, action):
38 | """Copy the text of the provided action into the clipboard."""
39 | QApplication.clipboard().setText(action.text())
40 |
41 | def mouseReleaseEvent(self, event):
42 | QTimer.singleShot(0, self.showMenu)
43 | super(StatusLabel, self).mouseReleaseEvent(event)
44 |
45 | def secondsText(self, seconds):
46 | """Generates text to show seconds of exec time."""
47 | return 'Exec: {:0.04f} Seconds'.format(seconds)
48 |
49 | def limit(self):
50 | return self.times.maxlen
51 |
52 | def setLimit(self, limit):
53 | self.times = deque(self.times, maxlen=limit)
54 |
55 | def setText(self, text):
56 | if self.render_as_href:
57 | text = '{}'.format(text)
58 | super(StatusLabel, self).setText(text)
59 |
60 | def showSeconds(self, seconds):
61 | self.times.append(seconds)
62 | self.setText(self.secondsText(seconds[0]))
63 |
64 | def showMenu(self):
65 | menu = QMenu(self)
66 | if self.times:
67 | # Show the time it took to run the last X code calls
68 | times = []
69 | for seconds in self.times:
70 | secs, cmd = seconds
71 | times.append(secs)
72 |
73 | # Add a simplified copy of the command that was run
74 | cmd = cmd.strip()
75 | cmds = cmd.split("\n")
76 | if len(cmds) > 1 or len(cmds[0]) > 50:
77 | cmd = "{} ...".format(cmds[0][:50])
78 | # Escape &'s so they dont' get turned into a shortcut'
79 | cmd = cmd.replace("&", "&&")
80 | act = menu.addAction("{}: {}".format(self.secondsText(secs), cmd))
81 | # Selecting this action should copy the time it took to run
82 | act.triggered.connect(partial(self.copy_action_text, act))
83 |
84 | menu.addSeparator()
85 | avg = sum(times) / len(times)
86 | act = menu.addAction("Average: {:0.04f}s".format(avg))
87 | act.triggered.connect(partial(self.copy_action_text, act))
88 |
89 | act = menu.addAction("Clear")
90 | act.triggered.connect(self.clearTimes)
91 |
92 | menu.addSeparator()
93 | act = menu.addAction("Set limit...")
94 | act.triggered.connect(self.chooseLimit)
95 |
96 | # Position the menu at the bottom of the widget
97 | height = self.geometry().height()
98 | pos = self.mapToGlobal(QPoint(0, height))
99 | menu.popup(pos)
100 |
--------------------------------------------------------------------------------
/preditor/gui/suggest_path_quotes_dialog.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from Qt.QtWidgets import QDialog
4 |
5 | from . import loadUi
6 |
7 |
8 | class SuggestPathQuotesDialog(QDialog):
9 | """A dialog to suggest to enclose paths in double-quotes in the cmdTempl which is
10 | used to launch an external text editor.
11 | """
12 |
13 | def __init__(self, parent, oldCmdTempl, newCmdTempl):
14 | super(SuggestPathQuotesDialog, self).__init__(parent)
15 | loadUi(__file__, self)
16 |
17 | self.parentWindow = self.parent().window()
18 |
19 | self.uiTextEditorOldCommandPatternLE.setText(oldCmdTempl)
20 | self.uiTextEditorNewCommandPatternLE.setText(newCmdTempl)
21 |
22 | toolTip = (
23 | "Examples:\n"
24 | 'SublimeText: "{exePath}" "{modulePath}":{lineNum}\n'
25 | 'notepad++: "{exePath}" "{modulePath}" -n{lineNum}\n'
26 | 'vim: "{exePath}" + {lineNum} "{modulePath}'
27 | )
28 | self.uiTextEditorNewCommandPatternLE.setToolTip(toolTip)
29 |
30 | def accept(self):
31 | """Set the parentWindow's textEditorCmdTempl property from the dialog, and
32 | optionally add dialog to parent's dont_ask_again list, and accept.
33 | """
34 |
35 | cmdTempl = self.uiTextEditorNewCommandPatternLE.text()
36 | self.parentWindow.textEditorCmdTempl = cmdTempl
37 |
38 | if self.uiDontAskAgainCHK.isChecked():
39 | if hasattr(self.parentWindow, "dont_ask_again"):
40 | self.parentWindow.dont_ask_again.append(self.objectName())
41 |
42 | super(SuggestPathQuotesDialog, self).accept()
43 |
44 | def reject(self):
45 | """Optionally add dialog to parentWindow's dont_ask_again list, and reject"""
46 | if self.uiDontAskAgainCHK.isChecked():
47 | if hasattr(self.parentWindow, "dont_ask_again"):
48 | self.parentWindow.dont_ask_again.append(self.objectName())
49 |
50 | super(SuggestPathQuotesDialog, self).reject()
51 |
--------------------------------------------------------------------------------
/preditor/gui/ui/editor_chooser.ui:
--------------------------------------------------------------------------------
1 |
2 |
These variables should be defined at a user or system level to configure blurdev.
8 |BDEV_DESIGNERPLUG_*: Used to add a collection of designer plugins to QDesigner. Should be set to "XMLPATH,MODULE_DIR". XMLPATH is the full path to a xml file listing plugins to load. MODULE_DIR is a path that needs added to sys.path so the modules in XMLPATH are importable. You can use environment variables in these strings, they will be expanded. This is used by QDesigner, nothing else.
9 |BDEV_OFFLINE: If set to 1, this indicates that blurdev is not running on the "Blur" network. This causes the [* Offline] section of blurdev/resource/settings.ini to override the [Default] and [*] sections. When blurdev is imported it adds all env vars defined in settings.ini for the current operating system and Default if they are not already defined in os.environ.
10 |BDEV_PATH_PREFS: This environment variable points to where per-computer user prefs are stored.
11 |BDEV_PATH_PREFS_SHARED: This environment variable points to where shared user prefs are stored. This is often on the network and includes the os's logged in username in the path. If BDEV_OFFLINE is set to 1 this may point to the BDEV_PATH_PREFS location.
12 | 13 |These variables should not be defined all the time.
15 |BDEV_DISABLE_AUTORUN: Set to "true" to disable the autorun.bat script used at blur. If this is not set when Maya shuts down, maya takes minutes to close. Maya uses several subprocess calls when closing and for some reason the doskey calls in the script take much longer than normal.
16 |BDEV_STYLESHEET: Used to override the stylesheet when initalizing blurdev.
17 |BDEV_TOOL_ENVIRONMENT: Forces blurdev to initialize with this treegrunt environment name. When saving prefs, this environment name change will not be saved. This is mostly used to ensure that launching a subprocess or farm job happens on the same treegrunt environment.
18 | 19 |TRAX_SPOOL_DIRECTORY_OVERRIDE: If defined, all messages will be stored the directory of this variable. Each message filename will have a "[mapping.mount()]_" prefix added giving you a hint to which server it would have ended up on. Defining this variable effectively disables all spool messages, and gives you a way to see what each spool message would look like. If your code depends on trax.api.spool.waitForCompletion, it will never complete.
22 |Any variable names containing a * are wildcards. This allows you to add as many instances of that environment variable type as needed.
25 | 26 | 27 | -------------------------------------------------------------------------------- /preditor/resource/error_mail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 47 | 48 | 49 |