├── .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 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 543 10 | 118 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 19 | 20 | 21 | 22 | 23 | Editor Class: 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | Qt::RightToLeft 34 | 35 | 36 | Warning 37 | 38 | 39 | 40 | 41 | 42 | 43 | Choose an editor to enable Workboxs. 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 0 52 | 0 53 | 54 | 55 | 56 | <html><head/><body><p>New editors are added by pip packages. A list of known pip editors can be found at <a href="https://github.com/blurstudio/preditor"><span style=" text-decoration: underline; color:#0000ff;">https://github.com/blurstudio/preditor</span></a>.</p></body></html> 57 | 58 | 59 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 60 | 61 | 62 | true 63 | 64 | 65 | true 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | uiWorkboxEditorDDL 75 | currentIndexChanged(int) 76 | Form 77 | refresh() 78 | 79 | 80 | 506 81 | 23 82 | 83 | 84 | 535 85 | 32 86 | 87 | 88 | 89 | 90 | 91 | refresh() 92 | 93 | 94 | -------------------------------------------------------------------------------- /preditor/gui/ui/errordialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | uiPythonErrorMBOX 4 | 5 | 6 | 7 | 0 8 | 0 9 | 409 10 | 98 11 | 12 | 13 | 14 | Dialog 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | TextLabel 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | TextLabel 32 | 33 | 34 | 35 | 36 | 37 | 38 | Qt::Horizontal 39 | 40 | 41 | 42 | 40 43 | 20 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | Show Logger 58 | 59 | 60 | 61 | 62 | 63 | 64 | Ignore 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /preditor/gui/ui/find_files.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | uiFindFilesWGT 4 | 5 | 6 | 7 | 0 8 | 0 9 | 636 10 | 41 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 19 | 20 | Find: 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | Regex (Alt + R) 30 | 31 | 32 | Regex 33 | 34 | 35 | true 36 | 37 | 38 | 39 | 40 | 41 | 42 | Case Sensitive (Alt + C) 43 | 44 | 45 | Case Sensitive 46 | 47 | 48 | true 49 | 50 | 51 | 52 | 53 | 54 | 55 | # of lines of context to show 56 | 57 | 58 | QAbstractSpinBox::PlusMinus 59 | 60 | 61 | 2 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | Find 74 | 75 | 76 | 77 | 78 | 79 | 80 | x 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | uiFindBTN 90 | released() 91 | uiFindFilesWGT 92 | find() 93 | 94 | 95 | 601 96 | 31 97 | 98 | 99 | 421 100 | 29 101 | 102 | 103 | 104 | 105 | uiFindTXT 106 | returnPressed() 107 | uiFindFilesWGT 108 | find() 109 | 110 | 111 | 488 112 | 23 113 | 114 | 115 | 501 116 | 65 117 | 118 | 119 | 120 | 121 | uiCloseBTN 122 | released() 123 | uiFindFilesWGT 124 | hide() 125 | 126 | 127 | 620 128 | 19 129 | 130 | 131 | 676 132 | 24 133 | 134 | 135 | 136 | 137 | 138 | find() 139 | 140 | 141 | -------------------------------------------------------------------------------- /preditor/gui/workbox_text_edit.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import logging 4 | 5 | from Qt.QtGui import QFont, QFontMetrics, QTextCursor 6 | from Qt.QtWidgets import QTextEdit 7 | 8 | from .codehighlighter import CodeHighlighter 9 | from .workbox_mixin import WorkboxMixin 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class WorkboxTextEdit(WorkboxMixin, QTextEdit): 15 | """A very simple multi-line text editor without any bells and whistles. 16 | 17 | It's better than nothing, but not by much. 18 | """ 19 | 20 | _warning_text = ( 21 | "This is a bare bones workbox, if you have another option, it's probably" 22 | "a better option." 23 | ) 24 | 25 | def __init__( 26 | self, parent=None, console=None, delayable_engine='default', core_name=None 27 | ): 28 | super(WorkboxTextEdit, self).__init__(parent=parent, core_name=core_name) 29 | self._filename = None 30 | self.__set_console__(console) 31 | highlight = CodeHighlighter(self) 32 | highlight.setLanguage('Python') 33 | self.uiCodeHighlighter = highlight 34 | 35 | def __auto_complete_enabled__(self): 36 | pass 37 | 38 | def __set_auto_complete_enabled__(self, state): 39 | pass 40 | 41 | def __copy_indents_as_spaces__(self): 42 | """When copying code, should it convert leading tabs to spaces?""" 43 | return False 44 | 45 | def __set_copy_indents_as_spaces__(self, state): 46 | logger.info( 47 | "WorkboxTextEdit does not support converting indents to spaces on copy." 48 | ) 49 | 50 | def __cursor_position__(self): 51 | """Returns the line and index of the cursor.""" 52 | cursor = self.textCursor() 53 | sc = QTextCursor(self.document()) 54 | sc.setPosition(cursor.selectionStart()) 55 | return sc.blockNumber(), sc.positionInBlock() 56 | 57 | def __exec_all__(self): 58 | txt = self.__text__().rstrip() 59 | filename = self.__workbox_filename__() 60 | self.__console__().executeString(txt, filename=filename) 61 | 62 | def __font__(self): 63 | return self.font() 64 | 65 | def __set_font__(self, font): 66 | metrics = QFontMetrics(font) 67 | self.setTabStopDistance(metrics.width(" ") * 4) 68 | super(WorkboxTextEdit, self).setFont(font) 69 | 70 | def __goto_line__(self, line): 71 | cursor = QTextCursor(self.document().findBlockByLineNumber(line - 1)) 72 | self.setTextCursor(cursor) 73 | 74 | def __indentations_use_tabs__(self): 75 | return True 76 | 77 | def __set_indentations_use_tabs__(self, state): 78 | logger.info("WorkboxTextEdit does not support using spaces for tabs.") 79 | 80 | def __load__(self, filename): 81 | self._filename = filename 82 | txt = self.__open_file__(self._filename) 83 | self.__set_text__(txt) 84 | 85 | def __margins_font__(self): 86 | return QFont() 87 | 88 | def __set_margins_font__(self, font): 89 | pass 90 | 91 | def __tab_width__(self): 92 | # TODO: Implement custom tab widths 93 | return 4 94 | 95 | def __text__(self, line=None, start=None, end=None): 96 | return self.toPlainText() 97 | 98 | def __set_text__(self, text): 99 | super(WorkboxTextEdit, self).__set_text__(text) 100 | self.setPlainText(text) 101 | 102 | def __selected_text__(self, start_of_line=False, selectText=False): 103 | cursor = self.textCursor() 104 | 105 | # Get starting line number. Must set the cursor's position to the start of the 106 | # selection, otherwise we may instead get the ending line number. 107 | tempCursor = self.textCursor() 108 | tempCursor.setPosition(tempCursor.selectionStart()) 109 | line = tempCursor.block().firstLineNumber() 110 | 111 | # If no selection, return the current line 112 | if cursor.selection().isEmpty(): 113 | text = cursor.block().text() 114 | 115 | selectText = self.window().uiSelectTextACT.isChecked() or selectText 116 | if selectText: 117 | cursor.select(QTextCursor.LineUnderCursor) 118 | self.setTextCursor(cursor) 119 | 120 | return text, line 121 | 122 | # Otherwise return the selected text 123 | if start_of_line: 124 | sc = QTextCursor(self.document()) 125 | sc.setPosition(cursor.selectionStart()) 126 | sc.movePosition(cursor.StartOfLine, sc.MoveAnchor) 127 | sc.setPosition(cursor.selectionEnd(), sc.KeepAnchor) 128 | 129 | return sc.selection().toPlainText(), line 130 | 131 | return self.textCursor().selection().toPlainText(), line 132 | 133 | def keyPressEvent(self, event): 134 | if self.process_shortcut(event): 135 | return 136 | else: 137 | super(WorkboxTextEdit, self).keyPressEvent(event) 138 | -------------------------------------------------------------------------------- /preditor/logging_config.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import json 4 | import logging 5 | import logging.config 6 | import os 7 | 8 | from .prefs import prefs_path 9 | 10 | 11 | class LoggingConfig(object): 12 | def __init__(self, core_name, version=1): 13 | self._filename = None 14 | self.cfg = {'version': version} 15 | self.core_name = core_name 16 | 17 | def add_logger(self, name, logger): 18 | if not logger.level: 19 | # No need to record a logger that is inheriting its logging level 20 | return 21 | 22 | # Build the required dictionaries 23 | loggers = self.cfg.setdefault('loggers', {}) 24 | log = loggers.setdefault(name, {}) 25 | log['level'] = logger.level 26 | 27 | def build(self): 28 | self.add_logger("", logging.root) 29 | for name, logger in logging.root.manager.loggerDict.items(): 30 | if not isinstance(logger, logging.PlaceHolder): 31 | self.add_logger(name, logger) 32 | 33 | @property 34 | def filename(self): 35 | if self._filename: 36 | return self._filename 37 | 38 | self._filename = prefs_path('logging_prefs.json', core_name=self.core_name) 39 | return self._filename 40 | 41 | def load(self): 42 | if not os.path.exists(self.filename): 43 | return False 44 | 45 | with open(self.filename) as fle: 46 | self.cfg = json.load(fle) 47 | logging.config.dictConfig(self.cfg) 48 | return True 49 | 50 | def save(self): 51 | with open(self.filename, 'w') as fle: 52 | json.dump(self.cfg, fle, indent=4) 53 | -------------------------------------------------------------------------------- /preditor/plugins.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import logging 4 | 5 | from importlib_metadata import EntryPoint, entry_points 6 | 7 | _logger = logging.getLogger(__name__) 8 | 9 | 10 | class Plugins(object): 11 | def about_module(self): 12 | plugs = {} 13 | for ep in self.iterator("preditor.plug.about_module"): 14 | name = ep.name 15 | if name in plugs: 16 | _logger.warning( 17 | 'Duplicate "preditor.plug.about_module" plugin found with ' 18 | 'name "{}"'.format(name) 19 | ) 20 | else: 21 | plugs[name] = ep 22 | 23 | # Sort the plugins alphabetically 24 | for name in sorted(plugs.keys(), key=lambda i: i.lower()): 25 | ep = plugs[name] 26 | try: 27 | result = ep.load() 28 | except Exception as error: 29 | result = "Error processing: {}".format(error) 30 | 31 | yield name, result 32 | 33 | def add_logging_handler(self, logger, handler_cls, *args, **kwargs): 34 | """Add a logging handler to a logger if not already installed. 35 | 36 | Checks for an existing handler on logger for the specific class(does not 37 | use isinstance). If not then it will create an instance of the handler 38 | and add it to the logger. 39 | 40 | Args: 41 | logger (logging.RootLogger): The logger instance to add the handler. 42 | handler_cls (logging.Handler or str): If a string is passed it will 43 | use `self.logging_handlers` to get the class. If not found then 44 | exits with success marked as False. Other values are treated as 45 | the handler class to add to the logger. 46 | *args: Passed to the handler_cls if a new instance is created. 47 | **kargs: Passed to the handler_cls if a new instance is created. 48 | 49 | Returns: 50 | logging.Handler or None: The handler instance that was added, already 51 | has been added, or None if the handler name isn't a valid plugin. 52 | bool: True only if the handler_cls was not already added to this logger. 53 | """ 54 | if isinstance(handler_cls, str): 55 | handlers = dict(self.logging_handlers(handler_cls)) 56 | if not handlers: 57 | # No handler to add for this name 58 | return None, False 59 | handler_cls = handlers[handler_cls] 60 | 61 | # Attempt to find an existing handler instance and return it 62 | for h in logger.handlers: 63 | if type(h) is handler_cls: 64 | return h, False 65 | 66 | # No handler installed create and install it 67 | handler = handler_cls(*args, **kwargs) 68 | logger.addHandler(handler) 69 | return handler, True 70 | 71 | def editor(self, name): 72 | for plug_name, ep in self.editors(name): 73 | return plug_name, ep.load() 74 | return None, None 75 | 76 | def editors(self, name=None): 77 | for ep in self.iterator(group="preditor.plug.editors"): 78 | if name and ep.name != name: 79 | continue 80 | yield ep.name, ep 81 | 82 | def initialize(self, name=None): 83 | for ep in self.iterator(group="preditor.plug.initialize"): 84 | yield ep.load() 85 | 86 | def loggerwindow(self, name=None): 87 | """Returns instances of "preditor.plug.loggerwindow" plugins. 88 | 89 | These plugins are used by the LoggerWindow to extend its interface. For 90 | example it can be used to add a toolbar or update the menus. 91 | 92 | When using this plugin, make sure the returned class is a subclass of 93 | `preditor.gui.logger_window_plugin.LoggerWindowPlugin`. 94 | """ 95 | for ep in self.iterator(group="preditor.plug.loggerwindow"): 96 | if name and ep.name != name: 97 | continue 98 | yield ep.name, ep.load() 99 | 100 | def logging_handlers(self, name=None): 101 | for ep in self.iterator(group="preditor.plug.logging_handlers"): 102 | yield ep.name, ep.load() 103 | 104 | @classmethod 105 | def iterator(cls, group=None, name=None): 106 | """Iterates over the requested entry point yielding results.""" 107 | for ep in entry_points().select(group=group): 108 | yield ep 109 | 110 | @classmethod 111 | def from_string(cls, value, name="", group=""): 112 | """Resolve an EntryPoint string into its object. 113 | 114 | Example: 115 | cls = from_string("preditor.gui.errordialog:ErrorDialog") 116 | """ 117 | ep = EntryPoint(name=name, value=value, group=group) 118 | return ep.load() 119 | -------------------------------------------------------------------------------- /preditor/prefs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for handling user interface preferences 3 | 4 | """ 5 | from __future__ import absolute_import 6 | 7 | import os 8 | import sys 9 | 10 | # cache of all the preferences 11 | _cache = {} 12 | 13 | 14 | def backup(): 15 | """Saves a copy of the current preferences to a zip archive.""" 16 | import glob 17 | import shutil 18 | 19 | archive_base = "preditor_backup_" 20 | # Save all prefs not just the current core_name. 21 | prefs = prefs_path() 22 | # Note: Using parent dir of prefs so we can use shutil.make_archive without 23 | # backing up the previous backups. 24 | parent_dir = os.path.join(os.path.dirname(prefs), "_backups") 25 | 26 | # Get the next backup version number to use. 27 | filenames = glob.glob(os.path.join(parent_dir, "{}*.zip".format(archive_base))) 28 | version = 1 29 | if filenames: 30 | # Add one to the largest version that exists on disk. 31 | version = int(os.path.splitext(max(filenames))[0].split(archive_base)[-1]) 32 | version += 1 33 | 34 | # Build the file path to save the archive to. 35 | archive_base = os.path.join(parent_dir, archive_base + "{:04}".format(version)) 36 | 37 | # Save the preferences to the given archive name. 38 | zip_path = shutil.make_archive(archive_base, "zip", prefs) 39 | 40 | return zip_path 41 | 42 | 43 | def browse(core_name): 44 | from . import osystem 45 | 46 | path = prefs_path(core_name) 47 | osystem.explore(path) 48 | 49 | 50 | def existing(): 51 | """Returns a list of PrEditor preference path names that exist on disk.""" 52 | root = prefs_path() 53 | return sorted(next(os.walk(root))[1], key=lambda i: i.lower()) 54 | 55 | 56 | def prefs_path(filename=None, core_name=None): 57 | """The path PrEditor's preferences are saved as a json file. 58 | 59 | The enviroment variable `PREDITOR_PREF_PATH` is used if set, otherwise 60 | it is saved in one of the user folders. 61 | """ 62 | if "PREDITOR_PREF_PATH" in os.environ: 63 | ret = os.environ["PREDITOR_PREF_PATH"] 64 | else: 65 | if sys.platform == "win32": 66 | ret = "%appdata%/blur/preditor" 67 | else: 68 | ret = "$HOME/.blur/preditor" 69 | ret = os.path.normpath(os.path.expandvars(os.path.expanduser(ret))) 70 | if core_name: 71 | ret = os.path.join(ret, core_name) 72 | if filename: 73 | ret = os.path.join(ret, filename) 74 | return ret 75 | -------------------------------------------------------------------------------- /preditor/resource/environment_variables.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

blurdev

5 |
6 |

blurdev Site variables:

7 |

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 |

blurdev Temp variables:

14 |

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

20 |
21 |

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 |

Notes

23 |
24 |

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 |

{{ subject }}

50 |
51 |

Debug Information

52 | 53 | {% for label, section_dict in info_dict.items() %} 54 |

{{ label }}

55 | 60 | {% endfor %} 61 | 62 |
63 |
64 |

Trackback

65 | {{ highlight_code(traceback) }} 66 |
67 | 68 | {% if error_report %} 69 |
70 |

Error Reports

71 | 72 | {% for title, report in error_report %} 73 |

{{ title }}

74 | {{ highlight_code(report) }} 75 | {% endfor %} 76 |
77 | {% endif %} 78 | 79 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /preditor/resource/error_mail_inline.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

{{ subject }}

6 |
7 |

Debug Information

8 | 9 | {% for label, section_dict in info_dict.items() %} 10 |

{{ label }}

11 | 16 | {% endfor %} 17 | 18 |
19 |
20 |

Trackback

21 | {{ highlight_code(traceback) }} 22 |
23 | 24 | {% if error_report %} 25 |
26 |

Error Reports

27 | 28 | {% for title, report in error_report %} 29 |

{{ title }}

30 | {{ highlight_code(report) }} 31 | {% endfor %} 32 |
33 | {% endif %} 34 | 35 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /preditor/resource/img/README.md: -------------------------------------------------------------------------------- 1 | # Preditor icon build from these source images 2 | 3 | * Predator Icon: https://icon-icons.com/icon/predator/54149 4 | * Pencil Icon: https://icon-icons.com/icon/pencil/73779 5 | Converted to multi-resolution icon using: https://convertico.com/svg-to-ico/ 6 | 7 | Most other icons downloaded from https://materialdesignicons.com/. 8 | 9 | Svg icons are preferred as they are plain text files that play nicely with git. 10 | Please make sure to update the sources table when adding or updating images. 11 | 12 | # Sources for resources 13 | 14 | | File | Source | Notes | Author | 15 | |---|---|---|---| 16 | | ![](preditor/resource/img/format-letter-case.svg) [format-letter-case.svg](preditor/resource/img/format-letter-case.svg) | https://pictogrammers.com/library/mdi/icon/format-letter-case/ | | [Austin Andrews](https://pictogrammers.com/contributor/Templarian/) | 17 | | ![](preditor/resource/img/regex.svg) [regex.svg](preditor/resource/img/regex.svg) | https://pictogrammers.com/library/mdi/icon/regex/ | | [Doug C. Hardester](https://pictogrammers.com/contributor/r3volution11/) | 18 | -------------------------------------------------------------------------------- /preditor/resource/img/arrow_forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/arrow_forward.png -------------------------------------------------------------------------------- /preditor/resource/img/check-bold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/check-bold.png -------------------------------------------------------------------------------- /preditor/resource/img/chevron-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/chevron-down.png -------------------------------------------------------------------------------- /preditor/resource/img/chevron-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/chevron-up.png -------------------------------------------------------------------------------- /preditor/resource/img/close-thick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/close-thick.png -------------------------------------------------------------------------------- /preditor/resource/img/comment-edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/comment-edit.png -------------------------------------------------------------------------------- /preditor/resource/img/content-copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/content-copy.png -------------------------------------------------------------------------------- /preditor/resource/img/content-cut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/content-cut.png -------------------------------------------------------------------------------- /preditor/resource/img/content-duplicate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/content-duplicate.png -------------------------------------------------------------------------------- /preditor/resource/img/content-paste.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/content-paste.png -------------------------------------------------------------------------------- /preditor/resource/img/content-save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/content-save.png -------------------------------------------------------------------------------- /preditor/resource/img/debug_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/debug_disabled.png -------------------------------------------------------------------------------- /preditor/resource/img/eye-check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/eye-check.png -------------------------------------------------------------------------------- /preditor/resource/img/file-plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/file-plus.png -------------------------------------------------------------------------------- /preditor/resource/img/file-remove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/file-remove.png -------------------------------------------------------------------------------- /preditor/resource/img/format-align-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/format-align-left.png -------------------------------------------------------------------------------- /preditor/resource/img/format-letter-case-lower.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/format-letter-case-lower.png -------------------------------------------------------------------------------- /preditor/resource/img/format-letter-case-upper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/format-letter-case-upper.png -------------------------------------------------------------------------------- /preditor/resource/img/format-letter-case.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /preditor/resource/img/information.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/information.png -------------------------------------------------------------------------------- /preditor/resource/img/logging_critical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/logging_critical.png -------------------------------------------------------------------------------- /preditor/resource/img/logging_custom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/logging_custom.png -------------------------------------------------------------------------------- /preditor/resource/img/logging_debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/logging_debug.png -------------------------------------------------------------------------------- /preditor/resource/img/logging_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/logging_error.png -------------------------------------------------------------------------------- /preditor/resource/img/logging_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/logging_info.png -------------------------------------------------------------------------------- /preditor/resource/img/logging_not_set.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/logging_not_set.png -------------------------------------------------------------------------------- /preditor/resource/img/logging_warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/logging_warning.png -------------------------------------------------------------------------------- /preditor/resource/img/marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/marker.png -------------------------------------------------------------------------------- /preditor/resource/img/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/play.png -------------------------------------------------------------------------------- /preditor/resource/img/playlist-play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/playlist-play.png -------------------------------------------------------------------------------- /preditor/resource/img/plus-minus-variant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/plus-minus-variant.png -------------------------------------------------------------------------------- /preditor/resource/img/preditor.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/preditor.ico -------------------------------------------------------------------------------- /preditor/resource/img/preditor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/preditor.png -------------------------------------------------------------------------------- /preditor/resource/img/preditor.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/preditor.psd -------------------------------------------------------------------------------- /preditor/resource/img/preditor.svg: -------------------------------------------------------------------------------- 1 | 2 | Predator_icon-icons 3 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /preditor/resource/img/regex.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /preditor/resource/img/restart.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /preditor/resource/img/skip-forward-outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/skip-forward-outline.png -------------------------------------------------------------------------------- /preditor/resource/img/skip-next-outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/skip-next-outline.png -------------------------------------------------------------------------------- /preditor/resource/img/skip-next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/skip-next.png -------------------------------------------------------------------------------- /preditor/resource/img/skip-previous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/skip-previous.png -------------------------------------------------------------------------------- /preditor/resource/img/subdirectory-arrow-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/subdirectory-arrow-right.png -------------------------------------------------------------------------------- /preditor/resource/img/text-search-variant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/text-search-variant.png -------------------------------------------------------------------------------- /preditor/resource/img/warning-big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/resource/img/warning-big.png -------------------------------------------------------------------------------- /preditor/resource/lang/python.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "Python", 3 | "ext": ".py;.pys;.pyw", 4 | "comments": [ 5 | "#(?!Result: )[^\\n]*" 6 | ], 7 | "strings": [ 8 | "'", 9 | "\"" 10 | ], 11 | "keywords": [ 12 | "class", 13 | "def", 14 | "elif", 15 | "else", 16 | "except", 17 | "False", 18 | "for", 19 | "from", 20 | "if", 21 | "import", 22 | "in", 23 | "pass", 24 | "print", 25 | "self", 26 | "True", 27 | "try", 28 | "while" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /preditor/resource/settings.ini: -------------------------------------------------------------------------------- 1 | [Default] 2 | bdev_error_email = The Pipe 3 | 4 | [Windows] 5 | bdev_cmd_shell_execfile = "%(filepath)s" 6 | bdev_cmd_shell_debug = "%(filepath)s" 7 | bdev_cmd_shell_exec = "%(command)s" 8 | bdev_cmd_browse = open "%(filepath)s" 9 | bdev_path_temp = c:/temp 10 | bdev_path_prefs_shared = \\SNAKE\user\config\%(username)s\userprefs 11 | bdev_path_blur = c:/blur 12 | bdev_path_prefs = %appdata%\blur\userprefs 13 | 14 | [Windows Offline] 15 | bdev_path_prefs_shared = %APPDATA%/blur/userprefs 16 | 17 | [Linux] 18 | bdev_cmd_shell_exec = gnome-terminal -x bash -c "%(command)s && echo Press enter to close && read" 19 | bdev_cmd_shell_debug = gnome-terminal -x bash -c "%(filepath)s && echo Press enter to close && read" 20 | bdev_cmd_shell_execfile = bash --rcfile %(filepath)s 21 | bdev_path_temp = /tmp/ 22 | bdev_path_blur = $HOME/.blur 23 | bdev_path_prefs_shared = $BDEV_PATH_PREFS 24 | bdev_path_prefs = $BDEV_PATH_BLUR/userprefs 25 | bdev_cmd_browse = nemo %(filepath)s 26 | -------------------------------------------------------------------------------- /preditor/resource/stylesheet/Bright.css: -------------------------------------------------------------------------------- 1 | DocumentEditor { 2 | qproperty-pyMarginsForegroundColor: "black"; 3 | qproperty-pyMarginsBackgroundColor: rgb(224, 224, 224); 4 | qproperty-pySelectionBackgroundColor: rgb(192, 192, 192); 5 | qproperty-pySelectionForegroundColor: "black"; 6 | qproperty-pyCaretBackgroundColor: "white"; 7 | qproperty-pyCaretForegroundColor: "black"; 8 | qproperty-pyMatchedBraceBackgroundColor: rgb(100, 225, 100); 9 | qproperty-pyMatchedBraceForegroundColor: "black"; 10 | qproperty-pyUnmatchedBraceBackgroundColor: "white"; 11 | qproperty-pyUnmatchedBraceForegroundColor: "blue"; 12 | qproperty-pyEdgeColor: rgb(100, 45, 45); 13 | 14 | qproperty-pyIndentationGuidesBackgroundColor: "white"; 15 | qproperty-pyIndentationGuidesForegroundColor : "black"; 16 | qproperty-pyMarkerBackgroundColor: "white"; 17 | qproperty-pyMarkerForegroundColor: "black"; 18 | qproperty-foldMarginsBackgroundColor: rgb(224, 224, 224); 19 | qproperty-foldMarginsForegroundColor: "white"; 20 | qproperty-braceBadForeground: "black"; 21 | qproperty-braceBadBackground: rgb(250, 100, 100); 22 | 23 | qproperty-colorDefault: "black"; 24 | qproperty-colorComment: rgb(0, 127, 0); 25 | qproperty-colorNumber: rgb(0, 127, 127); 26 | qproperty-colorString: rgb(127, 0, 127); 27 | qproperty-colorKeyword: rgb(0, 0, 127); 28 | qproperty-colorTripleQuotedString: rgb(127, 0, 0); 29 | qproperty-colorMethod: rgb(0, 0, 255); 30 | qproperty-colorFunction: rgb(0, 127, 127); 31 | qproperty-colorOperator: "black"; 32 | qproperty-colorIdentifier: "black"; 33 | qproperty-colorCommentBlock: rgb(127, 127, 127); 34 | qproperty-colorUnclosedString: "black"; 35 | qproperty-colorSmartHighlight: rgb(64, 112, 144); 36 | qproperty-colorDecorator: rgb(128, 80, 0); 37 | qproperty-paperDefault: "white"; 38 | qproperty-paperComment: "white"; 39 | qproperty-paperNumber: "white"; 40 | qproperty-paperString: "white"; 41 | qproperty-paperKeyword: "white"; 42 | qproperty-paperTripleQuotedString: "white"; 43 | qproperty-paperMethod: "white"; 44 | qproperty-paperFunction: "white"; 45 | qproperty-paperOperator: "white"; 46 | qproperty-paperIdentifier: "white"; 47 | qproperty-paperCommentBlock: "white"; 48 | qproperty-paperUnclosedString: rgb(224, 192, 224); 49 | qproperty-paperSmartHighlight: rgba(155, 255, 155, 75); 50 | qproperty-paperDecorator: "white"; 51 | } 52 | 53 | ConsolePrEdit, ErrorDialog { 54 | qproperty-errorMessageColor: rgb(255, 0, 0); 55 | } 56 | 57 | ConsolePrEdit { 58 | color: rgb(0,0,0); 59 | background-color: rgb(255,255,255); 60 | qproperty-commentColor: rgb(0, 206, 52); 61 | qproperty-keywordColor: rgb(17, 154, 255); 62 | qproperty-resultColor: rgb(128, 128, 128); 63 | qproperty-stdoutColor: rgb(17, 154, 255); 64 | qproperty-stringColor: rgb(255, 128, 0); 65 | } 66 | -------------------------------------------------------------------------------- /preditor/scintilla/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | 4 | class FindState(object): 5 | """ 6 | Arguments: 7 | end_pos (int): The position in the document to stop searching. If None, then 8 | search to the end of the document. 9 | """ 10 | 11 | def __init__(self): 12 | self.expr = '' 13 | self.wrap = True 14 | self.wrapped = False 15 | self.forward = True 16 | self.flags = 0 17 | self.start_pos = 0 18 | self.start_pos_original = None 19 | self.end_pos = None 20 | 21 | 22 | from . import delayables # noqa: F401, E402 23 | -------------------------------------------------------------------------------- /preditor/scintilla/delayables/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function 2 | 3 | from . import smart_highlight, spell_check 4 | 5 | # Import the base classes used to make most Delayables 6 | # TODO: Make these imports a plugin based system of some sort. 7 | 8 | __all__ = [ 9 | "smart_highlight", 10 | "spell_check", 11 | ] 12 | -------------------------------------------------------------------------------- /preditor/scintilla/delayables/smart_highlight.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function 2 | 3 | from PyQt5.Qsci import QsciScintilla 4 | from Qt.QtCore import QSignalMapper 5 | from Qt.QtWidgets import QWidget 6 | 7 | from ...delayable_engine.delayables import SearchDelayable 8 | from .. import FindState 9 | 10 | 11 | class SmartHighlight(SearchDelayable): 12 | key = 'smart_highlight' 13 | indicator_number = 30 14 | indicator_style = QsciScintilla.StraightBoxIndicator 15 | border_alpha = 255 16 | 17 | def __init__(self, engine): 18 | super(SmartHighlight, self).__init__(engine) 19 | self.signal_mapper = QSignalMapper(self) 20 | # Respect style sheet changes 21 | # TODO: Correctly connect this signal 22 | # LoggerWindow.styleSheetChanged.connect(self.update_indicator_color) 23 | 24 | def add_document(self, document): 25 | document.indicatorDefine(self.indicator_style, self.indicator_number) 26 | document.SendScintilla( 27 | QsciScintilla.SCI_SETINDICATORCURRENT, self.indicator_number 28 | ) 29 | document.setIndicatorForegroundColor( 30 | document.paperSmartHighlight, self.indicator_number 31 | ) 32 | document.SendScintilla( 33 | QsciScintilla.SCI_INDICSETOUTLINEALPHA, 34 | self.indicator_number, 35 | self.border_alpha, 36 | ) 37 | 38 | self.signal_mapper.setMapping(document, document) 39 | self.signal_mapper.mapped[QWidget].connect(self.update_highlighter) 40 | document.selectionChanged.connect(self.signal_mapper.map) 41 | 42 | def clear_markings(self, document): 43 | """Remove markings made by this Delayable for the given document. 44 | 45 | Args: 46 | document (blurdev.scintilla.documenteditor.DocumentEditor): The document 47 | to clear spell check markings from. 48 | """ 49 | document.SendScintilla( 50 | QsciScintilla.SCI_SETINDICATORCURRENT, self.indicator_number 51 | ) 52 | document.SendScintilla( 53 | QsciScintilla.SCI_INDICATORCLEARRANGE, 0, len(document.text()) 54 | ) 55 | 56 | def loop(self, document, find_state, clear): 57 | # Clear the document if needed 58 | if clear: 59 | self.clear_markings(document) 60 | ret = super(SmartHighlight, self).loop(document, find_state) 61 | if ret: 62 | # clear should always be false when continuing 63 | return ret + (False,) 64 | 65 | def remove_document(self, document): 66 | self.clear_markings(document) 67 | document.selectionChanged.disconnect(self.signal_mapper.map) 68 | 69 | def text_found(self, document, start, end, find_state): 70 | if not document.is_word(start, end): 71 | # Don't highlight the word if its not a word on its own 72 | return 73 | document.SendScintilla( 74 | QsciScintilla.SCI_SETINDICATORCURRENT, self.indicator_number 75 | ) 76 | document.SendScintilla(QsciScintilla.SCI_INDICATORFILLRANGE, start, end - start) 77 | 78 | def update_highlighter(self, document): 79 | self.clear_markings(document) 80 | if document.selection_is_word(): 81 | find_state = FindState() 82 | find_state.expr = document.selectedText() 83 | self.search_from_position(document, find_state, None, False) 84 | 85 | def update_indicator_color(self): 86 | for document in self.engine.documents: 87 | document.setIndicatorForegroundColor( 88 | document.paperSmartHighlight, self.indicator_number 89 | ) 90 | document.SendScintilla( 91 | QsciScintilla.SCI_INDICSETOUTLINEALPHA, 92 | self.indicator_number, 93 | self.border_alpha, 94 | ) 95 | -------------------------------------------------------------------------------- /preditor/scintilla/finddialog.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from ..gui import Dialog, loadUi 4 | from .documenteditor import SearchOptions 5 | 6 | 7 | class FindDialog(Dialog): 8 | def __init__(self, parent): 9 | super(FindDialog, self).__init__(parent) 10 | 11 | loadUi(__file__, self) 12 | 13 | self.uiCaseSensitiveCHK.setChecked( 14 | parent.searchFlags() & SearchOptions.CaseSensitive 15 | ) 16 | self.uiFindWholeWordsCHK.setChecked( 17 | parent.searchFlags() & SearchOptions.WholeWords 18 | ) 19 | self.uiQRegExpCHK.setChecked(parent.searchFlags() & SearchOptions.QRegExp) 20 | self.uiSearchTXT.setPlainText(parent.searchText()) 21 | 22 | # update the signals 23 | self.uiCaseSensitiveCHK.clicked.connect(self.updateSearchTerms) 24 | self.uiFindWholeWordsCHK.clicked.connect(self.updateSearchTerms) 25 | self.uiQRegExpCHK.clicked.connect(self.updateSearchTerms) 26 | self.uiSearchTXT.textChanged.connect(self.updateSearchTerms) 27 | 28 | self.uiFindNextBTN.clicked.connect(parent.uiFindNextACT.triggered.emit) 29 | self.uiFindPrevBTN.clicked.connect(parent.uiFindPrevACT.triggered.emit) 30 | 31 | self.uiSearchTXT.installEventFilter(self) 32 | self.uiSearchTXT.setFocus() 33 | self.uiSearchTXT.selectAll() 34 | 35 | def eventFilter(self, object, event): 36 | from Qt.QtCore import QEvent, Qt 37 | 38 | if event.type() == QEvent.KeyPress: 39 | if ( 40 | event.key() in (Qt.Key_Enter, Qt.Key_Return) 41 | and not event.modifiers() == Qt.ShiftModifier 42 | ): 43 | self.parent().uiFindNextACT.triggered.emit(True) 44 | self.accept() 45 | return True 46 | return False 47 | 48 | def search(self, text): 49 | # show the dialog 50 | self.show() 51 | 52 | # set the search text 53 | self.uiSearchTXT.setPlainText(text) 54 | self.uiSearchTXT.setFocus() 55 | self.uiSearchTXT.selectAll() 56 | 57 | def updateSearchTerms(self): 58 | parent = self.parent() 59 | options = 0 60 | if self.uiCaseSensitiveCHK.isChecked(): 61 | options |= SearchOptions.CaseSensitive 62 | if self.uiFindWholeWordsCHK.isChecked(): 63 | options |= SearchOptions.WholeWords 64 | if self.uiQRegExpCHK.isChecked(): 65 | options |= SearchOptions.QRegExp 66 | 67 | parent.setSearchFlags(options) 68 | parent.setSearchText(self.uiSearchTXT.toPlainText()) 69 | -------------------------------------------------------------------------------- /preditor/scintilla/lang/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function 2 | 3 | import glob 4 | import os.path 5 | 6 | from ... import osystem, prefPath 7 | from .language import Language 8 | 9 | _plugins = {} 10 | 11 | 12 | def byName(name): 13 | return _plugins.get(str(name)) 14 | 15 | 16 | def byLexer(lexer): 17 | for plugin in _plugins.values(): 18 | if isinstance(lexer, plugin.lexerClass()): 19 | return plugin 20 | return None 21 | 22 | 23 | def byExtension(extension): 24 | for plugin in _plugins.values(): 25 | if extension in plugin.fileTypes(): 26 | return plugin 27 | return None 28 | 29 | 30 | def languages(): 31 | return sorted(_plugins.keys()) 32 | 33 | 34 | def filetypes(): 35 | keys = sorted(_plugins.keys()) 36 | 37 | output = [] 38 | output.append('All Files (*.*)') 39 | output.append('Text Files (*.txt)') 40 | 41 | for key in keys: 42 | output.append( 43 | '%s Files (%s)' % (key, '*' + ';*'.join(_plugins[key].fileTypes())) 44 | ) 45 | 46 | return ';;'.join(output) 47 | 48 | 49 | def loadPlugins(path, custom=False): 50 | path = osystem.expandvars(path) 51 | if not os.path.exists(path): 52 | return False 53 | 54 | files = glob.glob(os.path.join(path, '*.ini')) 55 | 56 | for file in files: 57 | plugin = Language.fromConfig(file) 58 | if plugin: 59 | plugin.setCustom(custom) 60 | _plugins[plugin.name()] = plugin 61 | else: 62 | print('[preditor.scintilla.lang Error] Could not import %s' % file) 63 | 64 | 65 | def refresh(): 66 | _plugins.clear() 67 | 68 | # load the installed plugins 69 | loadPlugins(os.path.dirname(__file__) + '/config') 70 | 71 | # load languags from the environment 72 | for key in os.environ.keys(): 73 | if key.startswith('BDEV_PATH_LANG_'): 74 | loadPlugins(os.environ[key]) 75 | 76 | # load the user plugins 77 | loadPlugins(prefPath('lang'), True) 78 | 79 | 80 | refresh() 81 | -------------------------------------------------------------------------------- /preditor/scintilla/lang/config/bash.ini: -------------------------------------------------------------------------------- 1 | [GLOBALS] 2 | name=Bash 3 | filetypes=.sh 4 | 5 | [LEXER] 6 | class=QsciLexerBash 7 | 8 | [COLOR_TYPES] 9 | misc=0 10 | comment=2 11 | string=5,6,13 12 | error=1 13 | operator=12,7 14 | keyword=4 15 | number=3,9 16 | -------------------------------------------------------------------------------- /preditor/scintilla/lang/config/batch.ini: -------------------------------------------------------------------------------- 1 | [GLOBALS] 2 | name=Batch 3 | filetypes=.bat 4 | linecomment="REM " 5 | 6 | [LEXER] 7 | class=QsciLexerBatch 8 | 9 | [COLOR_TYPES] 10 | comment=1 11 | keyword=5,4,2 12 | text=3 13 | operator=7 14 | attribute=6 15 | -------------------------------------------------------------------------------- /preditor/scintilla/lang/config/cpp.ini: -------------------------------------------------------------------------------- 1 | [GLOBALS] 2 | name=C++ 3 | filetypes=.cpp;.c;.h;.sip 4 | linecomment=// 5 | 6 | [LEXER] 7 | class=CppLexer 8 | module=preditor.scintilla.lexers.cpplexer 9 | 10 | [COLOR_TYPES] 11 | comment=1,3,17,18,2,15 12 | misc=0 13 | string=6,7,12,13 14 | method=19 15 | keyword=5,16,9 16 | number=4,8 17 | operator=10 18 | regex=14 19 | smartHighlight=16 20 | -------------------------------------------------------------------------------- /preditor/scintilla/lang/config/css.ini: -------------------------------------------------------------------------------- 1 | [GLOBALS] 2 | name=CSS 3 | filetypes=.css;.qss 4 | linecomment=; 5 | 6 | [LEXER] 7 | class=QsciLexerCSS 8 | 9 | [COLOR_TYPES] 10 | attribute=16,6,15,17,19 11 | comment=9 12 | keyword=11 13 | method=2,20,21,3,18 14 | misc=0 15 | error=7,4 16 | operator=5 17 | tag=1 18 | string=13,14 19 | text=8 20 | -------------------------------------------------------------------------------- /preditor/scintilla/lang/config/eyeonscript.ini: -------------------------------------------------------------------------------- 1 | [GLOBALS] 2 | name=Eyeonscript 3 | filetypes=.eyeonscript;.Fuse;.fuse;.scriptlib 4 | linecomment=-- 5 | 6 | [LEXER] 7 | class=QsciLexerLua 8 | 9 | [COLOR_TYPES] 10 | comment=1,2 11 | keyword=5 12 | method=13,12,9 13 | string=7,8,6,14,12 14 | operator=10 15 | number=4 16 | misc=0 17 | text=11 18 | -------------------------------------------------------------------------------- /preditor/scintilla/lang/config/html.ini: -------------------------------------------------------------------------------- 1 | [GLOBALS] 2 | name=HTML 3 | filetypes=.htm;.html 4 | 5 | [LEXER] 6 | class=QsciLexerHTML 7 | 8 | [COLOR_TYPES] 9 | method=99,100,115,114 10 | comment=92,43,44,42,107,58,59,57,82,20,9,124,125,72 11 | number=93,45,108,60,83,5,122,73 12 | misc=91,41,106,56,81,118,71 13 | keyword=96,46,47,111,61,62,84,121,74 14 | operator=101,50,116,65,127 15 | string=94,95,97,98,51,49,48,112,113,110,109,66,64,63,85,87,6,7,119,126,120,75,77 16 | regex=52,67 17 | attribute=3,123 18 | entity=10 19 | tag=1,11 20 | error=4,2 21 | text=14,19,17,0 22 | -------------------------------------------------------------------------------- /preditor/scintilla/lang/config/javascript.ini: -------------------------------------------------------------------------------- 1 | [GLOBALS] 2 | name=Javascript 3 | filetypes=.js;.jsx;.json 4 | linecomment=// 5 | 6 | [LEXER] 7 | class=JavaScriptLexer 8 | module=preditor.scintilla.lexers.javascriptlexer 9 | 10 | [DESCRIPTORS] 11 | function1=^(?P[ \t]*)(?P[a-zA-Z0-9_\.]+)[ \t]*(:|=)[ \t]*function 12 | class1=^(?P[ \t]*)(?P[a-zA-Z0-9_\.]+)[ \t]*(:|=)[ \t]*new[ \t]*Class 13 | 14 | [COLOR_TYPES] 15 | comment=1,3,17,18,2,15 16 | misc=0 17 | string=6,7,12,13 18 | method=19 19 | keyword=5,16,9 20 | text=11 21 | number=4,8 22 | operator=10 23 | regex=14 24 | smartHighlight=16 25 | -------------------------------------------------------------------------------- /preditor/scintilla/lang/config/lua.ini: -------------------------------------------------------------------------------- 1 | [GLOBALS] 2 | name=Lua 3 | filetypes=.lua 4 | 5 | [LEXER] 6 | class=QsciLexerLua 7 | 8 | [COLOR_TYPES] 9 | comment=1,2 10 | keyword=5 11 | method=13,12,9 12 | string=7,8,6,14,12 13 | operator=10 14 | number=4 15 | misc=0 16 | text=11 17 | -------------------------------------------------------------------------------- /preditor/scintilla/lang/config/maxscript.ini: -------------------------------------------------------------------------------- 1 | [GLOBALS] 2 | name=Maxscript 3 | filetypes=.ms;.mcr 4 | linecomment=-- 5 | 6 | [LEXER] 7 | class=MaxscriptLexer 8 | module=preditor.scintilla.lexers.maxscriptlexer 9 | 10 | [DESCRIPTORS] 11 | function1=(?P\s*)(?Pfunction|fn)\s*(?P[^=\n]+)= 12 | class1=(?P\s*)(?Pstruct|rollout)\s*(?P[^=($]+) 13 | 14 | [COLOR_TYPES] 15 | comment=1,2 16 | keyword=3 17 | number=5 18 | operator=4 19 | string=6 20 | smartHighlight=7 21 | -------------------------------------------------------------------------------- /preditor/scintilla/lang/config/mel.ini: -------------------------------------------------------------------------------- 1 | [GLOBALS] 2 | name=MEL 3 | filetypes=.mel;.ma 4 | linecomment=// 5 | 6 | [LEXER] 7 | class=MelLexer 8 | module=preditor.scintilla.lexers.mellexer 9 | 10 | [COLOR_TYPES] 11 | comment=1,3,17,18,2,15 12 | misc=0 13 | string=6,7,12,13 14 | keyword=5,16,9 15 | number=4,8 16 | operator=10 17 | regex=14 18 | smartHighlight=19 19 | -------------------------------------------------------------------------------- /preditor/scintilla/lang/config/mu.ini: -------------------------------------------------------------------------------- 1 | [GLOBALS] 2 | name=Mu 3 | filetypes=.mu 4 | linecomment=// 5 | 6 | [LEXER] 7 | class=MuLexer 8 | module=preditor.scintilla.lexers.mulexer 9 | 10 | [DESCRIPTORS] 11 | function1=^(?P[ \t]*)method:\s+(?P\w+\s*[^{]*) 12 | class1=^(?P[ \t]*)class:\s+(?P\w+\s*[^{]*) 13 | 14 | [COLOR_TYPES] 15 | comment=1,3,17,18,2,15 16 | misc=0 17 | string=6,7,12,13 18 | method=19 19 | operator=10 20 | keyword=5,16,9 21 | number=4,8 22 | regex=14 23 | -------------------------------------------------------------------------------- /preditor/scintilla/lang/config/nsi.ini: -------------------------------------------------------------------------------- 1 | [GLOBALS] 2 | name=NSIS 3 | filetypes=.nsi;.nsh 4 | linecomment=# 5 | 6 | [LEXER] 7 | class=QsciLexerPython 8 | 9 | [DESCRIPTORS] 10 | function1=^(?P[ \t]*)Function\s+(?P\w+\s*[^{]*) 11 | 12 | [COLOR_TYPES] 13 | comment=1,12 14 | keyword=5 15 | operator=15,10 16 | method=9,8 17 | number=2 18 | misc=0 19 | string=3,4,7,6,13 20 | -------------------------------------------------------------------------------- /preditor/scintilla/lang/config/perl.ini: -------------------------------------------------------------------------------- 1 | [GLOBALS] 2 | name=Perl 3 | filetypes=.perl 4 | linecomment=# 5 | 6 | [LEXER] 7 | class=QsciLexerPerl 8 | 9 | [COLOR_TYPES] 10 | comment=2 11 | misc=0 12 | method=40 13 | string=24,6,26,27,28,29,30,23,7 14 | error=1 15 | text=11 16 | keyword=5 17 | number=4,12 18 | operator=10 19 | regex=17 20 | -------------------------------------------------------------------------------- /preditor/scintilla/lang/config/puppet.ini: -------------------------------------------------------------------------------- 1 | [GLOBALS] 2 | name=Puppet 3 | filetypes=.pp 4 | linecomment=# 5 | 6 | [LEXER] 7 | class=QsciLexerPerl 8 | 9 | [COLOR_TYPES] 10 | comment=2 11 | misc=0 12 | method=40 13 | string=24,6,26,27,28,29,30,23,7 14 | error=1 15 | text=11 16 | keyword=5 17 | number=4,12 18 | operator=10 19 | regex=17 20 | -------------------------------------------------------------------------------- /preditor/scintilla/lang/config/python.ini: -------------------------------------------------------------------------------- 1 | [GLOBALS] 2 | name=Python 3 | filetypes=.py;.pyw;.pys;.b;.pytempl 4 | linecomment=# 5 | 6 | [LEXER] 7 | class=PythonLexer 8 | module=preditor.scintilla.lexers.pythonlexer 9 | 10 | [DESCRIPTORS] 11 | function1=^(?P[ \t]*)def\s+(?P\w+\s*[^:]*): 12 | class1=^(?P[ \t]*)class\s+(?P\w+\s*[^:]*): 13 | 14 | [COLOR_TYPES] 15 | comment=1 16 | commentBlock=12 17 | keyword=5 18 | operator=10 19 | decorator=15 20 | method=8 21 | function=9 22 | number=2 23 | misc=0 24 | string=3,4,7,6 25 | tripleQuotedString=6,7 26 | unclosedString=13 27 | identifier=11 28 | smartHighlight=14 29 | -------------------------------------------------------------------------------- /preditor/scintilla/lang/config/ruby.ini: -------------------------------------------------------------------------------- 1 | [GLOBALS] 2 | name=Ruby 3 | filetypes=.rb 4 | linecomment=# 5 | 6 | [LEXER] 7 | class=QsciLexerRuby 8 | 9 | [COLOR_TYPES] 10 | attribute=16,6,15,17,19 11 | comment=9 12 | keyword=11 13 | method=2,20,21,3,18 14 | misc=0 15 | error=7,4 16 | operator=5 17 | tag=1 18 | string=13,14 19 | text=8 20 | -------------------------------------------------------------------------------- /preditor/scintilla/lang/config/sql.ini: -------------------------------------------------------------------------------- 1 | [GLOBALS] 2 | name=SQL 3 | filetypes=.sql 4 | linecomment=--; 5 | 6 | [LEXER] 7 | class=QsciLexerSQL 8 | -------------------------------------------------------------------------------- /preditor/scintilla/lang/config/xml.ini: -------------------------------------------------------------------------------- 1 | [GLOBALS] 2 | name=XML 3 | filetypes=.xml;.ui;.sdk;.blurproj;.pref;.schema;.tglnk 4 | 5 | [LEXER] 6 | class=QsciLexerXML 7 | 8 | [COLOR_TYPES] 9 | method=99,100,115,114 10 | comment=92,43,44,42,107,58,59,57,82,20,9,124,125,72 11 | number=93,45,108,60,83,5,122,73 12 | misc=91,41,106,56,81,118,71 13 | keyword=96,46,47,111,61,62,84,121,74 14 | operator=101,50,116,65,127 15 | string=94,95,97,98,51,49,48,112,113,110,109,66,64,63,85,87,6,7,119,126,120,75,77 16 | regex=52,67 17 | attribute=3,123 18 | entity=10 19 | tag=1,11 20 | error=4,2 21 | text=14,19,17,0 22 | -------------------------------------------------------------------------------- /preditor/scintilla/lang/config/yaml.ini: -------------------------------------------------------------------------------- 1 | [GLOBALS] 2 | name=YAML 3 | filetypes=.yml;.yaml 4 | linecomment=# 5 | 6 | [LEXER] 7 | class=QsciLexerYAML 8 | 9 | [COLOR_TYPES] 10 | comment=1 11 | identifier=2 12 | keyword=3 13 | number=4 14 | reference=5 15 | documentDelimiter=6 16 | textBlockMarker=7 17 | syntaxErrorMarker=8 18 | operator=9 19 | -------------------------------------------------------------------------------- /preditor/scintilla/lexers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/scintilla/lexers/__init__.py -------------------------------------------------------------------------------- /preditor/scintilla/lexers/cpplexer.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from PyQt5.Qsci import QsciLexerCPP 4 | from Qt.QtGui import QColor 5 | 6 | 7 | class CppLexer(QsciLexerCPP): 8 | # Items in this list will be highlighted using the color for self.KeywordSet2 9 | highlightedKeywords = '' 10 | 11 | def defaultPaper(self, style): 12 | if style == self.CommentLine: 13 | # Set the highlight color for this lexer 14 | return QColor(155, 255, 155) 15 | return super(CppLexer, self).defaultPaper(style) 16 | 17 | def keywords(self, style): 18 | # Words to be highlighted 19 | if style == self.CommentLine and self.highlightedKeywords: 20 | return self.highlightedKeywords 21 | return super(CppLexer, self).keywords(style) 22 | -------------------------------------------------------------------------------- /preditor/scintilla/lexers/javascriptlexer.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from PyQt5.Qsci import QsciLexerJavaScript 4 | from Qt.QtGui import QColor 5 | 6 | 7 | class JavaScriptLexer(QsciLexerJavaScript): 8 | # Items in this list will be highlighted using the color for self.KeywordSet2 9 | highlightedKeywords = '' 10 | 11 | def defaultFont(self, index): 12 | # HACK: TODO: I should probably preserve the existing fonts 13 | return self.font(0) 14 | 15 | def defaultPaper(self, style): 16 | if style == self.KeywordSet2: 17 | # Set the highlight color for this lexer 18 | return QColor(155, 255, 155) 19 | return super(JavaScriptLexer, self).defaultPaper(style) 20 | 21 | def keywords(self, style): 22 | # Words to be highlighted 23 | if style == 2 and self.highlightedKeywords: 24 | return self.highlightedKeywords 25 | return super(JavaScriptLexer, self).keywords(style) 26 | -------------------------------------------------------------------------------- /preditor/scintilla/lexers/mulexer.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from PyQt5.Qsci import QsciLexerCPP 4 | from Qt.QtGui import QColor 5 | 6 | MU_KEYWORDS = """ 7 | method string Color use require module for_each let global function nil void 8 | """ 9 | 10 | 11 | class MuLexer(QsciLexerCPP): 12 | # Items in this list will be highlighted using the color for self.KeywordSet2 13 | highlightedKeywords = '' 14 | 15 | def defaultPaper(self, style): 16 | if style == self.CommentLine: 17 | # Set the highlight color for this lexer 18 | return QColor(155, 255, 155) 19 | return super(MuLexer, self).defaultPaper(style) 20 | 21 | def keywords(self, style): 22 | # Words to be highlighted 23 | if style == self.CommentLine and self.highlightedKeywords: 24 | return self.highlightedKeywords 25 | 26 | output = super(MuLexer, self).keywords(style) 27 | # for some reason, CPP lexer uses comment style for 28 | # its keywords 29 | if style == self.Comment: 30 | output += MU_KEYWORDS 31 | 32 | return output 33 | -------------------------------------------------------------------------------- /preditor/scintilla/lexers/pythonlexer.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from PyQt5.Qsci import QsciLexerPython 4 | from Qt.QtGui import QColor 5 | 6 | 7 | class PythonLexer(QsciLexerPython): 8 | # Items in this list will be highlighted using the color for 9 | # self.HighlightedIdentifier 10 | highlightedKeywords = '' 11 | 12 | def __init__(self, *args): 13 | super(PythonLexer, self).__init__(*args) 14 | 15 | # set the indentation warning 16 | self.setIndentationWarning(self.Inconsistent) 17 | 18 | def defaultPaper(self, style): 19 | if style == self.HighlightedIdentifier: 20 | # Set the highlight color for this lexer 21 | return QColor(155, 255, 155) 22 | return super(PythonLexer, self).defaultPaper(style) 23 | 24 | def keywords(self, keyset): 25 | # Words to be highlighted 26 | if keyset == 2 and self.highlightedKeywords: 27 | return self.highlightedKeywords 28 | ret = super(PythonLexer, self).keywords(keyset) 29 | if keyset == 1: 30 | ret += ( 31 | ' True False abs divmod input open staticmethod all enumerate int ord ' 32 | 'str any eval isinstance pow sum basestring execfile' 33 | ' issubclass print super bin file iter property tuple bool filter len ' 34 | 'range type bytearray float list raw_input unichr' 35 | ' callable format locals reduce unicode chr frozenset long reload vars ' 36 | 'classmethod getattr map repr xrange cmp globals max' 37 | ' reversed zip compile hasattr memoryview round complex hash min set ' 38 | 'apply delattr help next setattr buffer dict hex object' 39 | ' slice coerce dir id oct sorted intern __import__' 40 | ) 41 | return ret 42 | -------------------------------------------------------------------------------- /preditor/scintilla/ui/finddialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 436 10 | 170 11 | 12 | 13 | 14 | Find 15 | 16 | 17 | 18 | 19 | 20 | 21 | 118 22 | 0 23 | 24 | 25 | 26 | 27 | 118 28 | 16777215 29 | 30 | 31 | 32 | Find Next 33 | 34 | 35 | true 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 118 44 | 0 45 | 46 | 47 | 48 | 49 | 118 50 | 16777215 51 | 52 | 53 | 54 | Find Prev 55 | 56 | 57 | 58 | 59 | 60 | 61 | Qt::Vertical 62 | 63 | 64 | 65 | 115 66 | 62 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | Match Whole Word 75 | 76 | 77 | 78 | 79 | 80 | 81 | Case Sensitive 82 | 83 | 84 | 85 | 86 | 87 | 88 | Qt::Horizontal 89 | 90 | 91 | 92 | 32 93 | 20 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 118 103 | 0 104 | 105 | 106 | 107 | 108 | 118 109 | 16777215 110 | 111 | 112 | 113 | Close 114 | 115 | 116 | 117 | 118 | 119 | 120 | <html><head/><body><p>Use Qt's QRegExp engine to find using regular expressions instead of a plain text find.</p></body></html> 121 | 122 | 123 | QRegExp 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | uiSearchTXT 134 | uiFindNextBTN 135 | uiFindPrevBTN 136 | uiFindWholeWordsCHK 137 | uiCaseSensitiveCHK 138 | uiQRegExpCHK 139 | uiCloseBTN 140 | 141 | 142 | 143 | 144 | uiCloseBTN 145 | clicked() 146 | dialog 147 | reject() 148 | 149 | 150 | 322 151 | 154 152 | 153 | 154 | 284 155 | 172 156 | 157 | 158 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /preditor/settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import absolute_import, print_function 3 | 4 | import os 5 | import sys 6 | 7 | try: 8 | import configparser 9 | except Exception: 10 | import ConfigParser as configparser # noqa: N813 11 | 12 | # define the default environment variables 13 | OS_TYPE = '' 14 | if os.name == 'posix': 15 | OS_TYPE = 'Linux' 16 | elif os.name == 'nt': 17 | OS_TYPE = 'Windows' 18 | elif os.name == 'osx': 19 | OS_TYPE = 'MacOS' 20 | 21 | # The sections to add from settings.ini 22 | # The order matters. Add from most specific to least specific. 23 | # Example: Add Windows Offline, then Windows, then Default. 24 | # Environment variables that exist in os.environ will not be added. 25 | _SECTIONS_TO_ADD = [] 26 | if os.getenv('BDEV_OFFLINE') == '1': 27 | _SECTIONS_TO_ADD.append('{} Offline'.format(OS_TYPE)) 28 | _SECTIONS_TO_ADD += [OS_TYPE, 'Default'] 29 | 30 | _currentEnv = '' 31 | defaults = {} 32 | 33 | 34 | def environStr(value): 35 | if sys.version_info[0] > 2: 36 | # Python 3 requires a unicode value. aka str(), which these values already are 37 | return value 38 | # Python 2 requires str object, not unicode 39 | return value.encode('utf8') 40 | 41 | 42 | def addConfigSection(config_parser, section): 43 | """ 44 | Add a config section to os.environ for a section. 45 | 46 | Does not add options that already exist in os.environ. 47 | 48 | Args: 49 | config_parser (configparser.RawConfigParser): The parser to read from. 50 | Must already be read. 51 | section (str): The section name to add. 52 | """ 53 | 54 | for option in config_parser.options(section): 55 | if option.upper() not in os.environ: 56 | value = config_parser.get(section, option) 57 | if value == 'None': 58 | value = '' 59 | # In python2.7 on windows you can't pass unicode values to 60 | # subprocess.Popen's env argument. This is the reason we are calling str() 61 | os.environ[environStr(option.upper())] = environStr(value) 62 | 63 | 64 | # load the default environment from the settings INI 65 | config = configparser.RawConfigParser() 66 | config.read(os.path.join(os.path.dirname(__file__), 'resource', 'settings.ini')) 67 | for section in _SECTIONS_TO_ADD: 68 | addConfigSection(config, section) 69 | 70 | # store the blurdev path in the environment 71 | os.environ['BDEV_PATH'] = environStr(os.path.dirname(__file__)) 72 | -------------------------------------------------------------------------------- /preditor/stream/__init__.py: -------------------------------------------------------------------------------- 1 | """ A system for capturing stream output and later inserting the captured output into a 2 | gui, and allowing that GUI to directly capture any new output written to the streams. 3 | 4 | A use case for this is to attach a Manager's as early as possible to ``sys.stdout`` 5 | and ``sys.stderr`` process, before any GUI's are created. Then later you initialize a 6 | GUI python console that should show all python output that has already been written. 7 | Any future writes are delivered directly to the gui using a callback mechanism. 8 | 9 | Example:: 10 | 11 | # Startup script or plugin for DCC runs this as early as possible. 12 | from preditor.stream import install_to_std 13 | 14 | manager = install_to_std() 15 | # Startup script exits and DCC continues to load eventually creating the main gui. 16 | 17 | # From a menu a user chooses to show a custom console(A gui that replicates a 18 | # python interactive console inside the DCC). 19 | console = PythonConsole() 20 | 21 | # We will want to see all the previous writes made by python, so replay them 22 | # in the console so it can properly handle stdout and stderr writes. 23 | for msg, state in manager: 24 | console.write(msg, state) 25 | 26 | # Make it so any future writes are automatically added to the console. 27 | manager.add_callback(console.write) 28 | 29 | # Optionally, disable storing data in the buffer. buffer.write calls will now 30 | # directly write to console so there is no reason to duplicate the data to the 31 | # buffer. 32 | manager.append_writes = False 33 | """ 34 | from __future__ import absolute_import, print_function 35 | 36 | import sys 37 | 38 | STDERR = 1 39 | STDIN = 2 40 | STDOUT = 3 41 | 42 | from .director import Director # noqa: E402 43 | from .manager import Manager # noqa: E402 44 | 45 | """Set when :py:attr:``install_to_std`` is called. This stores the installed Manager 46 | so it can be accessed to install callbacks. 47 | """ 48 | active = None 49 | 50 | __all__ = [ 51 | "active", 52 | "Director", 53 | "install_to_std", 54 | "Manager", 55 | "STDERR", 56 | "STDIN", 57 | "STDOUT", 58 | ] 59 | 60 | 61 | def install_to_std(out=True, err=True): 62 | """Replaces ``sys.stdout`` and ``sys.stderr`` with :py:class:`Director`'s 63 | using the returned :py:class:`Manager`. This manager is stored as the ``active`` 64 | variable and can be accessed later. This can be called more than once, and it will 65 | simply return the already installed Manager. 66 | 67 | Args: 68 | out (bool, optional): Enables replacement of ``sys.stdout`` on first call. 69 | err (bool, optional): Enables replacement of ``sys.stderr`` on first call. 70 | """ 71 | global active 72 | 73 | if active is None: 74 | active = Manager() 75 | if out: 76 | sys.stdout = Director(active, STDOUT) 77 | if err: 78 | sys.stderr = Director(active, STDERR) 79 | 80 | return active 81 | -------------------------------------------------------------------------------- /preditor/stream/director.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function 2 | 3 | import io 4 | import sys 5 | 6 | from . import STDERR, STDOUT 7 | 8 | 9 | class Director(io.TextIOBase): 10 | """A file like object that stores the text written to it in a manager. 11 | This manager can be shared between multiple Directors to build a single 12 | continuous history of all writes. 13 | 14 | Args: 15 | manager (Manager): The manager that writes are stored in. 16 | state: The state passed to the manager. This is often ``preditor.stream.STDOUT`` 17 | or ``preditor.stream.STDERR``. 18 | old_stream: A second stream that will be written to every time this stream 19 | is written to. This allows this object to replace sys.stdout and still 20 | send that output to the original stdout, which is useful for not breaking 21 | DCC's script editors. Pass False to disable this feature. If you pass None 22 | and state is set to ``preditor.stream.STDOUT`` or ``preditor.stream.STDERR`` 23 | this will automatically be set to the current sys.stdout or sys.stderr. 24 | """ 25 | 26 | def __init__(self, manager, state, old_stream=None, *args, **kwargs): 27 | super(Director, self).__init__(*args, **kwargs) 28 | self.manager = manager 29 | self.state = state 30 | 31 | # Keep track of whether we wrapped a std stream 32 | # that way we don't .close() any streams that we don't control 33 | self.std_stream_wrapped = False 34 | 35 | if old_stream is False: 36 | old_stream = None 37 | elif old_stream is None: 38 | if state == STDOUT: 39 | # On Windows if we're in pythonw.exe, then sys.stdout is named "nul" 40 | # And it uses cp1252 encoding (which breaks with unicode) 41 | # So if we find this nul TextIOWrapper, it's safe to just skip it 42 | if getattr(sys.stdout, 'name', '') != 'nul': 43 | self.std_stream_wrapped = True 44 | old_stream = sys.stdout 45 | elif state == STDERR: 46 | if getattr(sys.stderr, 'name', '') != 'nul': 47 | self.std_stream_wrapped = True 48 | old_stream = sys.stderr 49 | 50 | self.old_stream = old_stream 51 | 52 | def close(self): 53 | if ( 54 | self.old_stream 55 | and not self.std_stream_wrapped 56 | and self.old_stream is not sys.__stdout__ 57 | and self.old_stream is not sys.__stderr__ 58 | ): 59 | self.old_stream.close() 60 | 61 | super(Director, self).close() 62 | 63 | def flush(self): 64 | if self.old_stream: 65 | self.old_stream.flush() 66 | 67 | super(Director, self).flush() 68 | 69 | def write(self, msg): 70 | self.manager.write(msg, self.state) 71 | 72 | if self.old_stream: 73 | self.old_stream.write(msg) 74 | -------------------------------------------------------------------------------- /preditor/stream/manager.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function 2 | 3 | import collections 4 | 5 | from ..weakref import WeakList 6 | 7 | 8 | class Manager(collections.deque): 9 | """Stores all of the data from the stdout/stderr writes. You can iterate over this 10 | object to see all of the (msg, state) calls that have been written to it up to the 11 | maxlen specified when constructing it. 12 | 13 | Args: 14 | maxlen (int, optional): The maximum number of raw writes to store. If this is 15 | exceeded, the oldest writes are discarded. 16 | 17 | Properties: 18 | store_writes (bool): Set this to False if you no longer want write calls to 19 | store on the manager. 20 | """ 21 | 22 | def __init__(self, maxlen=10000): 23 | super(Manager, self).__init__(maxlen=maxlen) 24 | self.callbacks = WeakList() 25 | self.store_writes = True 26 | 27 | def add_callback(self, callback, replay=False, disable_writes=False, clear=False): 28 | """Add a callable that will be called every time write is called. 29 | 30 | Args: 31 | callback (callable): A callable object that takes two arguments. It must 32 | take two arguments (msg, state). See write for more details. 33 | replay (bool, optional): If True, then iterate over all the stored writes 34 | and pass them to callback. This is useful for when you are initializing 35 | a gui and want to include all previous prints. 36 | disable_writes (bool, optional): Set store_writes to False if this is True. 37 | clear (bool, optional): Clear the stored history on this object. 38 | """ 39 | self.callbacks.append(callback) 40 | 41 | if replay: 42 | # Replay the existing prints into the console. 43 | for msg, state in self: 44 | callback(msg, state) 45 | 46 | if disable_writes: 47 | # Disable storing data in the buffer. buffer.write calls will now 48 | # directly write to console so there is no reason to duplicate the 49 | # data to the buffer. 50 | self.store_writes = False 51 | 52 | if clear: 53 | self.clear() 54 | 55 | def remove_callback(self, callback): 56 | self.callbacks.remove(callback) 57 | 58 | def get_value(self, fmt="[{state}:{msg}]"): 59 | return ''.join([fmt.format(msg=d[0], state=d[1]) for d in self]) 60 | 61 | def write(self, msg, state): 62 | """Adds the written text to the manager and passes it to any attached callbacks. 63 | 64 | Args: 65 | msg (str): The text to be written. 66 | state: A identifier for how the text is to be written. For example if this 67 | write is coming from sys.stderr this will likely be set to 68 | ``preditor.stream.STDERR``. 69 | """ 70 | if self.store_writes: 71 | self.append((msg, state)) 72 | 73 | for callback in self.callbacks: 74 | callback(msg, state) 75 | -------------------------------------------------------------------------------- /preditor/streamhandler_helper.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import logging 4 | import sys 5 | 6 | 7 | class StreamHandlerHelper(object): 8 | """A collection of functions for manipulating ``logging.StreamHandler`` objects.""" 9 | 10 | @classmethod 11 | def set_stream(cls, handler, stream): 12 | """For the given StreamHandler, set its stream. This works around 13 | python 2's lack of StreamHandler.setStream by replicating python 3. 14 | """ 15 | # TODO: once python 2 is no longer supported, replace any uses of this 16 | # function with `handler.setStream(stream)` 17 | if sys.version_info[0] > 2: 18 | handler.setStream(stream) 19 | else: 20 | # Copied from python 3's logging's setStream to work in python 2 21 | handler.acquire() 22 | try: 23 | handler.flush() 24 | handler.stream = stream 25 | finally: 26 | handler.release() 27 | 28 | @classmethod 29 | def replace_stream(cls, old, new, logger=None): 30 | """Replaces the stream of StreamHandlers by checking all 31 | `logging.StreamHandler`'s attached to the provided logger. If any of them are 32 | using old for their stream, update that stream to new. 33 | 34 | Args: 35 | old (stream): Only StreamHandlers using this stream will be updated to new. 36 | new (stream): A file stream object like `sys.stderr` that will replace old. 37 | logger (logging.Logger, optional): The logger to update streams for. If 38 | None, the root logger(`logging.getLogger()`) will be used. 39 | """ 40 | if logger is None: 41 | logger = logging.getLogger() 42 | 43 | for handler in logger.handlers: 44 | if isinstance(handler, logging.StreamHandler): 45 | if handler.stream == old: 46 | cls.set_stream(handler, new) 47 | -------------------------------------------------------------------------------- /preditor/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blurstudio/PrEditor/a8a08ed9f6652b846f0c7d95013d920dd0b841d9/preditor/utils/__init__.py -------------------------------------------------------------------------------- /preditor/utils/cute.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | __all__ = ["ensureWindowIsVisible"] 4 | from Qt.QtWidgets import QApplication 5 | 6 | 7 | def ensureWindowIsVisible(widget): 8 | """ 9 | Checks the widget's geometry against all of the system's screens. If it does 10 | not intersect it will reposition it to the top left corner of the highest 11 | numbered desktop. Returns a boolean indicating if it had to move the 12 | widget. 13 | """ 14 | desktop = QApplication.desktop() 15 | geo = widget.geometry() 16 | for screen in range(desktop.screenCount()): 17 | monGeo = desktop.screenGeometry(screen) 18 | if monGeo.intersects(geo): 19 | break 20 | else: 21 | geo.moveTo(monGeo.x() + 7, monGeo.y() + 30) 22 | # setting the geometry may trigger a second check if setGeometry is overridden 23 | disable = hasattr(widget, 'checkScreenGeo') and widget.checkScreenGeo 24 | if disable: 25 | widget.checkScreenGeo = False 26 | widget.setGeometry(geo) 27 | if disable: 28 | widget.checkScreenGeo = True 29 | return True 30 | return False 31 | -------------------------------------------------------------------------------- /preditor/utils/stylesheets.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import glob 4 | import os 5 | 6 | 7 | def read_stylesheet(stylesheet='', path=None): 8 | """Returns the contents of the requested stylesheet. 9 | 10 | Args: 11 | 12 | stylesheet (str): the name of the stylesheet. Attempt to load stylesheet.css 13 | shipped with preditor. Ignored if path is provided. 14 | 15 | path (str): Return the contents of this file path. 16 | 17 | Returns: 18 | str: The contents of stylesheet or blank if stylesheet was not found. 19 | valid: A stylesheet was found and loaded. 20 | """ 21 | if path is None: 22 | path = os.path.join( 23 | os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 24 | 'resource', 25 | 'stylesheet', 26 | '{}.css'.format(stylesheet), 27 | ) 28 | if os.path.isfile(path): 29 | with open(path) as f: 30 | return f.read(), True 31 | return '', False 32 | 33 | 34 | def stylesheets(subFolder=None): 35 | """Returns a list of installed stylesheet names. 36 | 37 | Args: 38 | subFolder (str or None, optional): Use this to access sub-folders of 39 | the stylesheet resource directory. 40 | 41 | Returns: 42 | list: A list .css file paths in the target directory. 43 | """ 44 | components = [ 45 | os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 46 | 'resource', 47 | 'stylesheet', 48 | ] 49 | if subFolder is not None: 50 | components.append(subFolder) 51 | cssdir = os.path.join(*components) 52 | cssfiles = sorted(glob.glob(os.path.join(cssdir, '*.css'))) 53 | # Only return the filename without the .css extension 54 | return [os.path.splitext(os.path.basename(fp))[0] for fp in cssfiles] 55 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools >= 44.1.1", 4 | "setuptools_scm[toml] >= 4", 5 | "wheel >= 0.36", 6 | ] 7 | build-backend = "setuptools.build_meta" 8 | 9 | [project] 10 | name = "PrEditor" 11 | description = "A python REPL and Editor and console based on Qt." 12 | authors = [{name = "Blur Studio", email = "opensource@blur.com"}] 13 | license = {text = "LGPL-3.0"} 14 | classifiers = [ 15 | "Development Status :: 5 - Production/Stable", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", 18 | "Operating System :: OS Independent", 19 | "Programming Language :: Python", 20 | "Programming Language :: Python :: 3", 21 | "Programming Language :: Python :: Implementation :: CPython", 22 | "Programming Language :: Python :: Implementation :: PyPy", 23 | ] 24 | requires-python = ">=3.7" 25 | dependencies = [ 26 | "Qt.py", 27 | "configparser>=4.0.2", 28 | "future>=0.18.2", 29 | "signalslot>=0.1.2", 30 | "importlib-metadata>=4.8.3", 31 | ] 32 | dynamic = ["version"] 33 | 34 | [project.readme] 35 | file = "README.md" 36 | content-type = "text/markdown" 37 | 38 | [project.urls] 39 | Homepage = "https://github.com/blurstudio/PrEditor" 40 | Source = "https://github.com/blurstudio/PrEditor" 41 | Tracker = "https://github.com/blurstudio/PrEditor/issues" 42 | 43 | [project.optional-dependencies] 44 | cli =[ 45 | "click>=7.1.2", 46 | "click-default-group", 47 | ] 48 | dev =[ 49 | "black", 50 | "build", 51 | "covdefaults", 52 | "coverage", 53 | "flake8", 54 | "flake8-bugbear", 55 | "Flake8-pyproject", 56 | "pep8-naming", 57 | "pytest", 58 | "tox", 59 | ] 60 | shortcut =[ 61 | "casement>=0.1.0;platform_system=='Windows'", 62 | ] 63 | 64 | [project.scripts] 65 | preditor = "preditor.cli:cli" 66 | 67 | [project.gui-scripts] 68 | preditorw = "preditor.cli:cli" 69 | 70 | [project.entry-points."preditor.plug.about_module"] 71 | PrEditor = "preditor.about_module:AboutPreditor" 72 | Qt = "preditor.about_module:AboutQt" 73 | Python = "preditor.about_module:AboutPython" 74 | Exe = "preditor.about_module:AboutExe" 75 | 76 | [project.entry-points."preditor.plug.editors"] 77 | TextEdit = "preditor.gui.workbox_text_edit:WorkboxTextEdit" 78 | QScintilla = "preditor.gui.workboxwidget:WorkboxWidget" 79 | 80 | [project.entry-points."preditor.plug.logging_handlers"] 81 | PrEditor = "preditor.gui.logger_window_handler:LoggerWindowHandler" 82 | 83 | 84 | [tool.setuptools] 85 | include-package-data = true 86 | platforms = ["any"] 87 | license-files = ["LICENSE"] 88 | 89 | [tool.setuptools.packages.find] 90 | exclude = ["tests"] 91 | namespaces = false 92 | 93 | [tool.setuptools_scm] 94 | write_to = "preditor/version.py" 95 | version_scheme = "release-branch-semver" 96 | 97 | [tool.flake8] 98 | select = ["B", "C", "E", "F", "N", "W", "B9"] 99 | extend-ignore = [ 100 | "E203", 101 | "E501", 102 | "E722", 103 | "N802", 104 | "N803", 105 | "N806", 106 | "N815", 107 | "N816", 108 | "W503", 109 | ] 110 | max-line-length = "80" 111 | exclude = [ 112 | "*.egg-info", 113 | "*.pyc", 114 | ".cache", 115 | ".eggs", 116 | ".git", 117 | ".tox", 118 | "__pycache__", 119 | "build", 120 | "dist", 121 | "docs", 122 | "shared-venv", 123 | ] 124 | 125 | 126 | [tool.black] 127 | skip-string-normalization = true 128 | 129 | [tool.isort] 130 | profile = "black" 131 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | build 2 | configparser>=4.0.2 3 | future>=0.18.2 4 | importlib-metadata>=4.8.3 5 | Qt.py 6 | signalslot>=0.1.2 7 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture(scope="session", autouse=True) 7 | def shared_prefs(tmp_path_factory): 8 | """All tests use this prefs dir instead of the user. 9 | 10 | Safety to prevent possible overwriting of a user's prefs. 11 | """ 12 | path = tmp_path_factory.mktemp("_shared_prefs") 13 | os.environ["PREDITOR_PREF_PATH"] = str(path) 14 | return path 15 | 16 | 17 | @pytest.fixture() 18 | def pref_root(tmp_path): 19 | """A per-test user prefs folder.""" 20 | path = tmp_path / "_prefs" 21 | path.mkdir() 22 | os.environ["PREDITOR_PREF_PATH"] = str(path) 23 | return path 24 | -------------------------------------------------------------------------------- /tests/find_files/re_greedy_False_0_True.md: -------------------------------------------------------------------------------- 1 | 2 | Find in workboxs: "search.+term" (regex) 3 | 4 | 5 | # File: [First Group/First Tab](, 1,2, 0 "Open First Group/First Tab") 6 | 1: [Search term](, 1,2, 1 "Open First Group/First Tab at line number 1") 7 | . 8 | 3: [search term](, 1,2, 3 "Open First Group/First Tab at line number 3") 9 | . 10 | 6: [search term](, 1,2, 6 "Open First Group/First Tab at line number 6") at the start of the line 11 | .. 12 | 11: [search is in this line but the actual search term](, 1,2, 11 "Open First Group/First Tab at line number 11") is in the middle 13 | .. 14 | 17: This line has underscores around _[search term_ and "search term](, 1,2, 17 "Open First Group/First Tab at line number 17")" has double quotes 15 | 18: This line has the [search term in multiple times... search term](, 1,2, 18 "Open First Group/First Tab at line number 18") 16 | 19: [Search term the search term with the search term](, 1,2, 19 "Open First Group/First Tab at line number 19") 17 | -------------------------------------------------------------------------------- /tests/find_files/re_greedy_False_2_True.md: -------------------------------------------------------------------------------- 1 | 2 | Find in workboxs: "search.+term" (regex) 3 | 4 | 5 | # File: [First Group/First Tab](, 1,2, 0 "Open First Group/First Tab") 6 | 1: [Search term](, 1,2, 1 "Open First Group/First Tab at line number 1") 7 | 2 line 2 8 | 3: [search term](, 1,2, 3 "Open First Group/First Tab at line number 3") 9 | 4 10 | 5 line 5 11 | 6: [search term](, 1,2, 6 "Open First Group/First Tab at line number 6") at the start of the line 12 | 7 line 7 13 | 8 line 8 14 | 9 line 9 15 | 10 line 10 16 | 11: [search is in this line but the actual search term](, 1,2, 11 "Open First Group/First Tab at line number 11") is in the middle 17 | 12 18 | 13 19 | .. 20 | 15 line 15 21 | 16 22 | 17: This line has underscores around _[search term_ and "search term](, 1,2, 17 "Open First Group/First Tab at line number 17")" has double quotes 23 | 18: This line has the [search term in multiple times... search term](, 1,2, 18 "Open First Group/First Tab at line number 18") 24 | 19: [Search term the search term with the search term](, 1,2, 19 "Open First Group/First Tab at line number 19") 25 | 20 The "newline at end of file" is not printed due to str.splitlines. 26 | -------------------------------------------------------------------------------- /tests/find_files/re_greedy_True_2_True.md: -------------------------------------------------------------------------------- 1 | 2 | Find in workboxs: "search.+term" (regex, case sensitive) 3 | 4 | 5 | # File: [First Group/First Tab](, 1,2, 0 "Open First Group/First Tab") 6 | 1 Search term 7 | 2 line 2 8 | 3: [search term](, 1,2, 3 "Open First Group/First Tab at line number 3") 9 | 4 10 | 5 line 5 11 | 6: [search term](, 1,2, 6 "Open First Group/First Tab at line number 6") at the start of the line 12 | 7 line 7 13 | 8 line 8 14 | 9 line 9 15 | 10 line 10 16 | 11: [search is in this line but the actual search term](, 1,2, 11 "Open First Group/First Tab at line number 11") is in the middle 17 | 12 18 | 13 19 | .. 20 | 15 line 15 21 | 16 22 | 17: This line has underscores around _[search term_ and "search term](, 1,2, 17 "Open First Group/First Tab at line number 17")" has double quotes 23 | 18: This line has the [search term in multiple times... search term](, 1,2, 18 "Open First Group/First Tab at line number 18") 24 | 19: Search term the [search term with the search term](, 1,2, 19 "Open First Group/First Tab at line number 19") 25 | 20 The "newline at end of file" is not printed due to str.splitlines. 26 | -------------------------------------------------------------------------------- /tests/find_files/re_greedy_upper_True_2_True.md: -------------------------------------------------------------------------------- 1 | 2 | Find in workboxs: "Search.+term" (regex, case sensitive) 3 | 4 | 5 | # File: [First Group/First Tab](, 1,2, 0 "Open First Group/First Tab") 6 | 1: [Search term](, 1,2, 1 "Open First Group/First Tab at line number 1") 7 | 2 line 2 8 | 3 search term 9 | .. 10 | 17 This line has underscores around _search term_ and "search term" has double quotes 11 | 18 This line has the search term in multiple times... search term 12 | 19: [Search term the search term with the search term](, 1,2, 19 "Open First Group/First Tab at line number 19") 13 | 20 The "newline at end of file" is not printed due to str.splitlines. 14 | -------------------------------------------------------------------------------- /tests/find_files/re_simple_False_0_True.md: -------------------------------------------------------------------------------- 1 | 2 | Find in workboxs: "search term" (regex) 3 | 4 | 5 | # File: [First Group/First Tab](, 1,2, 0 "Open First Group/First Tab") 6 | 1: [Search term](, 1,2, 1 "Open First Group/First Tab at line number 1") 7 | . 8 | 3: [search term](, 1,2, 3 "Open First Group/First Tab at line number 3") 9 | . 10 | 6: [search term](, 1,2, 6 "Open First Group/First Tab at line number 6") at the start of the line 11 | .. 12 | 11: search is in this line but the actual [search term](, 1,2, 11 "Open First Group/First Tab at line number 11") is in the middle 13 | .. 14 | 17: This line has underscores around _[search term](, 1,2, 17 "Open First Group/First Tab at line number 17")_ and "[search term](, 1,2, 17 "Open First Group/First Tab at line number 17")" has double quotes 15 | 18: This line has the [search term](, 1,2, 18 "Open First Group/First Tab at line number 18") in multiple times... [search term](, 1,2, 18 "Open First Group/First Tab at line number 18") 16 | 19: [Search term](, 1,2, 19 "Open First Group/First Tab at line number 19") the [search term](, 1,2, 19 "Open First Group/First Tab at line number 19") with the [search term](, 1,2, 19 "Open First Group/First Tab at line number 19") 17 | -------------------------------------------------------------------------------- /tests/find_files/re_simple_False_2_True.md: -------------------------------------------------------------------------------- 1 | 2 | Find in workboxs: "search term" (regex) 3 | 4 | 5 | # File: [First Group/First Tab](, 1,2, 0 "Open First Group/First Tab") 6 | 1: [Search term](, 1,2, 1 "Open First Group/First Tab at line number 1") 7 | 2 line 2 8 | 3: [search term](, 1,2, 3 "Open First Group/First Tab at line number 3") 9 | 4 10 | 5 line 5 11 | 6: [search term](, 1,2, 6 "Open First Group/First Tab at line number 6") at the start of the line 12 | 7 line 7 13 | 8 line 8 14 | 9 line 9 15 | 10 line 10 16 | 11: search is in this line but the actual [search term](, 1,2, 11 "Open First Group/First Tab at line number 11") is in the middle 17 | 12 18 | 13 19 | .. 20 | 15 line 15 21 | 16 22 | 17: This line has underscores around _[search term](, 1,2, 17 "Open First Group/First Tab at line number 17")_ and "[search term](, 1,2, 17 "Open First Group/First Tab at line number 17")" has double quotes 23 | 18: This line has the [search term](, 1,2, 18 "Open First Group/First Tab at line number 18") in multiple times... [search term](, 1,2, 18 "Open First Group/First Tab at line number 18") 24 | 19: [Search term](, 1,2, 19 "Open First Group/First Tab at line number 19") the [search term](, 1,2, 19 "Open First Group/First Tab at line number 19") with the [search term](, 1,2, 19 "Open First Group/First Tab at line number 19") 25 | 20 The "newline at end of file" is not printed due to str.splitlines. 26 | -------------------------------------------------------------------------------- /tests/find_files/re_simple_False_3_True.md: -------------------------------------------------------------------------------- 1 | 2 | Find in workboxs: "search term" (regex) 3 | 4 | 5 | # File: [First Group/First Tab](, 1,2, 0 "Open First Group/First Tab") 6 | 1: [Search term](, 1,2, 1 "Open First Group/First Tab at line number 1") 7 | 2 line 2 8 | 3: [search term](, 1,2, 3 "Open First Group/First Tab at line number 3") 9 | 4 10 | 5 line 5 11 | 6: [search term](, 1,2, 6 "Open First Group/First Tab at line number 6") at the start of the line 12 | 7 line 7 13 | 8 line 8 14 | 9 line 9 15 | 10 line 10 16 | 11: search is in this line but the actual [search term](, 1,2, 11 "Open First Group/First Tab at line number 11") is in the middle 17 | 12 18 | 13 19 | 14 line 14 20 | 15 line 15 21 | 16 22 | 17: This line has underscores around _[search term](, 1,2, 17 "Open First Group/First Tab at line number 17")_ and "[search term](, 1,2, 17 "Open First Group/First Tab at line number 17")" has double quotes 23 | 18: This line has the [search term](, 1,2, 18 "Open First Group/First Tab at line number 18") in multiple times... [search term](, 1,2, 18 "Open First Group/First Tab at line number 18") 24 | 19: [Search term](, 1,2, 19 "Open First Group/First Tab at line number 19") the [search term](, 1,2, 19 "Open First Group/First Tab at line number 19") with the [search term](, 1,2, 19 "Open First Group/First Tab at line number 19") 25 | 20 The "newline at end of file" is not printed due to str.splitlines. 26 | -------------------------------------------------------------------------------- /tests/find_files/re_simple_True_2_True.md: -------------------------------------------------------------------------------- 1 | 2 | Find in workboxs: "search term" (regex, case sensitive) 3 | 4 | 5 | # File: [First Group/First Tab](, 1,2, 0 "Open First Group/First Tab") 6 | 1 Search term 7 | 2 line 2 8 | 3: [search term](, 1,2, 3 "Open First Group/First Tab at line number 3") 9 | 4 10 | 5 line 5 11 | 6: [search term](, 1,2, 6 "Open First Group/First Tab at line number 6") at the start of the line 12 | 7 line 7 13 | 8 line 8 14 | 9 line 9 15 | 10 line 10 16 | 11: search is in this line but the actual [search term](, 1,2, 11 "Open First Group/First Tab at line number 11") is in the middle 17 | 12 18 | 13 19 | .. 20 | 15 line 15 21 | 16 22 | 17: This line has underscores around _[search term](, 1,2, 17 "Open First Group/First Tab at line number 17")_ and "[search term](, 1,2, 17 "Open First Group/First Tab at line number 17")" has double quotes 23 | 18: This line has the [search term](, 1,2, 18 "Open First Group/First Tab at line number 18") in multiple times... [search term](, 1,2, 18 "Open First Group/First Tab at line number 18") 24 | 19: Search term the [search term](, 1,2, 19 "Open First Group/First Tab at line number 19") with the [search term](, 1,2, 19 "Open First Group/First Tab at line number 19") 25 | 20 The "newline at end of file" is not printed due to str.splitlines. 26 | -------------------------------------------------------------------------------- /tests/find_files/simple_False_0_False.md: -------------------------------------------------------------------------------- 1 | 2 | Find in workboxs: "search term" 3 | 4 | 5 | # File: [First Group/First Tab](, 1,2, 0 "Open First Group/First Tab") 6 | 1: [Search term](, 1,2, 1 "Open First Group/First Tab at line number 1") 7 | . 8 | 3: [search term](, 1,2, 3 "Open First Group/First Tab at line number 3") 9 | . 10 | 6: [search term](, 1,2, 6 "Open First Group/First Tab at line number 6") at the start of the line 11 | .. 12 | 11: search is in this line but the actual [search term](, 1,2, 11 "Open First Group/First Tab at line number 11") is in the middle 13 | .. 14 | 17: This line has underscores around _[search term](, 1,2, 17 "Open First Group/First Tab at line number 17")_ and "[search term](, 1,2, 17 "Open First Group/First Tab at line number 17")" has double quotes 15 | 18: This line has the [search term](, 1,2, 18 "Open First Group/First Tab at line number 18") in multiple times... [search term](, 1,2, 18 "Open First Group/First Tab at line number 18") 16 | 19: [Search term](, 1,2, 19 "Open First Group/First Tab at line number 19") the [search term](, 1,2, 19 "Open First Group/First Tab at line number 19") with the [search term](, 1,2, 19 "Open First Group/First Tab at line number 19") 17 | -------------------------------------------------------------------------------- /tests/find_files/simple_False_1_False.md: -------------------------------------------------------------------------------- 1 | 2 | Find in workboxs: "search term" 3 | 4 | 5 | # File: [First Group/First Tab](, 1,2, 0 "Open First Group/First Tab") 6 | 1: [Search term](, 1,2, 1 "Open First Group/First Tab at line number 1") 7 | 2 line 2 8 | 3: [search term](, 1,2, 3 "Open First Group/First Tab at line number 3") 9 | 4 10 | 5 line 5 11 | 6: [search term](, 1,2, 6 "Open First Group/First Tab at line number 6") at the start of the line 12 | 7 line 7 13 | .. 14 | 10 line 10 15 | 11: search is in this line but the actual [search term](, 1,2, 11 "Open First Group/First Tab at line number 11") is in the middle 16 | 12 17 | .. 18 | 16 19 | 17: This line has underscores around _[search term](, 1,2, 17 "Open First Group/First Tab at line number 17")_ and "[search term](, 1,2, 17 "Open First Group/First Tab at line number 17")" has double quotes 20 | 18: This line has the [search term](, 1,2, 18 "Open First Group/First Tab at line number 18") in multiple times... [search term](, 1,2, 18 "Open First Group/First Tab at line number 18") 21 | 19: [Search term](, 1,2, 19 "Open First Group/First Tab at line number 19") the [search term](, 1,2, 19 "Open First Group/First Tab at line number 19") with the [search term](, 1,2, 19 "Open First Group/First Tab at line number 19") 22 | 20 The "newline at end of file" is not printed due to str.splitlines. 23 | -------------------------------------------------------------------------------- /tests/find_files/simple_False_2_False.md: -------------------------------------------------------------------------------- 1 | 2 | Find in workboxs: "search term" 3 | 4 | 5 | # File: [First Group/First Tab](, 1,2, 0 "Open First Group/First Tab") 6 | 1: [Search term](, 1,2, 1 "Open First Group/First Tab at line number 1") 7 | 2 line 2 8 | 3: [search term](, 1,2, 3 "Open First Group/First Tab at line number 3") 9 | 4 10 | 5 line 5 11 | 6: [search term](, 1,2, 6 "Open First Group/First Tab at line number 6") at the start of the line 12 | 7 line 7 13 | 8 line 8 14 | 9 line 9 15 | 10 line 10 16 | 11: search is in this line but the actual [search term](, 1,2, 11 "Open First Group/First Tab at line number 11") is in the middle 17 | 12 18 | 13 19 | .. 20 | 15 line 15 21 | 16 22 | 17: This line has underscores around _[search term](, 1,2, 17 "Open First Group/First Tab at line number 17")_ and "[search term](, 1,2, 17 "Open First Group/First Tab at line number 17")" has double quotes 23 | 18: This line has the [search term](, 1,2, 18 "Open First Group/First Tab at line number 18") in multiple times... [search term](, 1,2, 18 "Open First Group/First Tab at line number 18") 24 | 19: [Search term](, 1,2, 19 "Open First Group/First Tab at line number 19") the [search term](, 1,2, 19 "Open First Group/First Tab at line number 19") with the [search term](, 1,2, 19 "Open First Group/First Tab at line number 19") 25 | 20 The "newline at end of file" is not printed due to str.splitlines. 26 | -------------------------------------------------------------------------------- /tests/find_files/simple_False_3_False.md: -------------------------------------------------------------------------------- 1 | 2 | Find in workboxs: "search term" 3 | 4 | 5 | # File: [First Group/First Tab](, 1,2, 0 "Open First Group/First Tab") 6 | 1: [Search term](, 1,2, 1 "Open First Group/First Tab at line number 1") 7 | 2 line 2 8 | 3: [search term](, 1,2, 3 "Open First Group/First Tab at line number 3") 9 | 4 10 | 5 line 5 11 | 6: [search term](, 1,2, 6 "Open First Group/First Tab at line number 6") at the start of the line 12 | 7 line 7 13 | 8 line 8 14 | 9 line 9 15 | 10 line 10 16 | 11: search is in this line but the actual [search term](, 1,2, 11 "Open First Group/First Tab at line number 11") is in the middle 17 | 12 18 | 13 19 | 14 line 14 20 | 15 line 15 21 | 16 22 | 17: This line has underscores around _[search term](, 1,2, 17 "Open First Group/First Tab at line number 17")_ and "[search term](, 1,2, 17 "Open First Group/First Tab at line number 17")" has double quotes 23 | 18: This line has the [search term](, 1,2, 18 "Open First Group/First Tab at line number 18") in multiple times... [search term](, 1,2, 18 "Open First Group/First Tab at line number 18") 24 | 19: [Search term](, 1,2, 19 "Open First Group/First Tab at line number 19") the [search term](, 1,2, 19 "Open First Group/First Tab at line number 19") with the [search term](, 1,2, 19 "Open First Group/First Tab at line number 19") 25 | 20 The "newline at end of file" is not printed due to str.splitlines. 26 | -------------------------------------------------------------------------------- /tests/find_files/simple_True_2_False.md: -------------------------------------------------------------------------------- 1 | 2 | Find in workboxs: "search term" (case sensitive) 3 | 4 | 5 | # File: [First Group/First Tab](, 1,2, 0 "Open First Group/First Tab") 6 | 1 Search term 7 | 2 line 2 8 | 3: [search term](, 1,2, 3 "Open First Group/First Tab at line number 3") 9 | 4 10 | 5 line 5 11 | 6: [search term](, 1,2, 6 "Open First Group/First Tab at line number 6") at the start of the line 12 | 7 line 7 13 | 8 line 8 14 | 9 line 9 15 | 10 line 10 16 | 11: search is in this line but the actual [search term](, 1,2, 11 "Open First Group/First Tab at line number 11") is in the middle 17 | 12 18 | 13 19 | .. 20 | 15 line 15 21 | 16 22 | 17: This line has underscores around _[search term](, 1,2, 17 "Open First Group/First Tab at line number 17")_ and "[search term](, 1,2, 17 "Open First Group/First Tab at line number 17")" has double quotes 23 | 18: This line has the [search term](, 1,2, 18 "Open First Group/First Tab at line number 18") in multiple times... [search term](, 1,2, 18 "Open First Group/First Tab at line number 18") 24 | 19: Search term the [search term](, 1,2, 19 "Open First Group/First Tab at line number 19") with the [search term](, 1,2, 19 "Open First Group/First Tab at line number 19") 25 | 20 The "newline at end of file" is not printed due to str.splitlines. 26 | -------------------------------------------------------------------------------- /tests/find_files/tab_text.txt: -------------------------------------------------------------------------------- 1 | Search term 2 | line 2 3 | search term 4 | 5 | line 5 6 | search term at the start of the line 7 | line 7 8 | line 8 9 | line 9 10 | line 10 11 | search is in this line but the actual search term is in the middle 12 | 13 | 14 | line 14 15 | line 15 16 | 17 | This line has underscores around _search term_ and "search term" has double quotes 18 | This line has the search term in multiple times... search term 19 | Search term the search term with the search term 20 | The "newline at end of file" is not printed due to str.splitlines. 21 | -------------------------------------------------------------------------------- /tests/find_files/test_find_files.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from preditor.utils.text_search import RegexTextSearch, SimpleTextSearch 6 | 7 | 8 | def text_for_test(filename): 9 | dirname = os.path.dirname(__file__) 10 | filename = os.path.join(dirname, filename) 11 | with open(filename) as fle: 12 | return fle.read() 13 | 14 | 15 | @pytest.mark.parametrize( 16 | "check_type,search_text,is_cs,context,is_re", 17 | ( 18 | # Simple text search testing context and case 19 | ("simple", "search term", False, 0, False), 20 | ("simple", "search term", False, 1, False), 21 | ("simple", "search term", False, 2, False), 22 | ("simple", "search term", False, 3, False), 23 | ("simple", "search term", True, 2, False), 24 | # Regex search testing context and case 25 | ("re_simple", "search term", False, 0, True), 26 | ("re_simple", "search term", False, 2, True), 27 | ("re_simple", "search term", False, 3, True), 28 | ("re_simple", "search term", True, 2, True), 29 | # Complex regex with a greedy search term 30 | ("re_greedy", "search.+term", False, 0, True), 31 | ("re_greedy", "search.+term", False, 2, True), 32 | ("re_greedy", "search.+term", True, 2, True), 33 | ("re_greedy_upper", "Search.+term", True, 2, True), 34 | ), 35 | ) 36 | def test_find_files(capsys, check_type, search_text, is_cs, context, is_re): 37 | workbox_id = "1,2" 38 | path = 'First Group/First Tab' 39 | text = text_for_test("tab_text.txt") 40 | 41 | if is_re: 42 | TextSearch = RegexTextSearch 43 | else: 44 | TextSearch = SimpleTextSearch 45 | 46 | search = TextSearch(search_text, case_sensitive=is_cs, context=context) 47 | # Add the title to the printed output so title is tested when checking 48 | # `captured.out` later. 49 | print(search.title()) 50 | 51 | # Generate the search text and print it to `captured.out` so we can check 52 | search.search_text(text, path, workbox_id) 53 | 54 | captured = capsys.readouterr() 55 | check_filename = "{}_{}_{}_{}.md".format(check_type, is_cs, context, is_re) 56 | check = text_for_test(check_filename) 57 | 58 | # To update tests, print text and save over top of the md. Then verify 59 | # that it is actually rendered properly. You will need to add one trailing 60 | # space after dot lines, two spaces after blank lines, and ensue the end of 61 | # file newline is present. The default print callbacks use markdown links, 62 | # but don't really render valid markdown. If you want to render to html, 63 | # use regular markdown not github flavored. 64 | # print(check_filename) 65 | # print(captured.out) 66 | 67 | # print('*' * 50) 68 | # for line in check.rstrip().splitlines(keepends=True): 69 | # print([line]) 70 | # print('*' * 50) 71 | # for line in captured.out.splitlines(keepends=True): 72 | # print([line]) 73 | 74 | assert captured.out == check 75 | -------------------------------------------------------------------------------- /tests/test_prefs.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | import preditor.prefs 5 | 6 | 7 | def test_auto_fixture(tmp_path_factory): 8 | """Tests that the autouse fixture `shared_prefs` is applied by default.""" 9 | path = os.environ["PREDITOR_PREF_PATH"] 10 | assert isinstance(path, str) 11 | # The fixture always adds a digit to account for multiple uses per test. 12 | # This is a session scoped test so it should always be zero 13 | assert os.path.basename(path) == "_shared_prefs0" 14 | 15 | # Make sure its using the pytest dir not the user folder 16 | pytest_root = tmp_path_factory.getbasetemp() 17 | assert pytest_root in Path(path).parents 18 | 19 | # Verify that preditor actually uses the env var. 20 | prefs_path = preditor.prefs.prefs_path() 21 | assert prefs_path == path 22 | 23 | 24 | def test_pref_root(pref_root, tmp_path): 25 | """Test that if the `pref_root` fixture is used it generates a per-test pref.""" 26 | path = os.environ["PREDITOR_PREF_PATH"] 27 | assert isinstance(path, str) 28 | 29 | # The parent folder takes care of unique names so no the _prefs folder is 30 | # not modified in this context. 31 | assert os.path.basename(path) == "_prefs" 32 | 33 | # Make sure its using the test dir not the user folder 34 | assert os.path.normpath(path).startswith(str(tmp_path)) 35 | 36 | # Verify that preditor actually uses the env var. 37 | prefs_path = preditor.prefs.prefs_path() 38 | assert prefs_path == path 39 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = begin,py{37,38,39,310,311},end,black,flake8 3 | skip_missing_interpreters = True 4 | 5 | [testenv] 6 | changedir = {toxinidir} 7 | package = editable 8 | deps = 9 | -rrequirements.txt 10 | covdefaults 11 | coverage 12 | pytest 13 | PyQt5;python_version>="3.5" 14 | QScintilla>=2.11.4;python_version>="3.5" 15 | Qt.py 16 | commands = 17 | coverage run -m pytest {tty:--color=yes} {posargs:tests/} 18 | 19 | [testenv:begin] 20 | basepython = python3 21 | deps = 22 | coverage[toml] 23 | build 24 | commands = 25 | coverage erase 26 | 27 | [testenv:py{37,38,39,310,311}] 28 | depends = begin 29 | 30 | [testenv:end] 31 | basepython = python3 32 | depends = 33 | begin 34 | py{37,38,39,310,311} 35 | parallel_show_output = True 36 | deps = 37 | coverage 38 | commands = 39 | coverage combine 40 | coverage report 41 | 42 | [testenv:black] 43 | basepython = python3 44 | deps = 45 | black==22.12.0 46 | commands = 47 | python -m black . --check 48 | 49 | [testenv:flake8] 50 | basepython = python3 51 | deps = 52 | flake8-bugbear==22.12.6 53 | Flake8-pyproject 54 | flake8==5.0.4 55 | pep8-naming==0.13.3 56 | commands = 57 | python -m flake8 . 58 | --------------------------------------------------------------------------------