├── usdmanager ├── plugins │ ├── images_rc.py │ ├── images.qrc │ └── __init__.py ├── images │ ├── lock.png │ ├── logo.png │ ├── usd.png │ ├── findNext.png │ └── findPrev.png ├── config.json ├── images.qrc ├── version.py ├── parsers │ ├── __init__.py │ └── log.py ├── highlighters │ ├── __init__.py │ ├── xml.py │ ├── lua.py │ ├── python.py │ └── usd.py ├── file_dialog.py ├── find_dialog.py ├── file_status.py ├── constants.py ├── linenumbers.py ├── find_dialog.ui ├── highlighter.py ├── usdviewstyle.qss ├── parser.py ├── preferences_dialog.py └── include_panel.py ├── logo.png ├── docs ├── requirements.txt ├── _static │ ├── icon.png │ ├── logo.png │ ├── logo_512.png │ └── screenshot_island.png ├── index.rst ├── keyboardShortcuts.rst ├── contributing.md ├── installation.md ├── development.md ├── usage.md └── conf.py ├── readthedocs.yaml ├── scripts └── usdmanager ├── setup.py ├── .gitignore ├── README.md └── LICENSE /usdmanager/plugins/images_rc.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamworksanimation/usdmanager/HEAD/logo.png -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | recommonmark>=0.5.0 2 | Sphinx>=1.8.5 3 | sphinxcontrib-apidoc>=0.3.0 -------------------------------------------------------------------------------- /docs/_static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamworksanimation/usdmanager/HEAD/docs/_static/icon.png -------------------------------------------------------------------------------- /docs/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamworksanimation/usdmanager/HEAD/docs/_static/logo.png -------------------------------------------------------------------------------- /usdmanager/plugins/images.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/_static/logo_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamworksanimation/usdmanager/HEAD/docs/_static/logo_512.png -------------------------------------------------------------------------------- /usdmanager/images/lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamworksanimation/usdmanager/HEAD/usdmanager/images/lock.png -------------------------------------------------------------------------------- /usdmanager/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamworksanimation/usdmanager/HEAD/usdmanager/images/logo.png -------------------------------------------------------------------------------- /usdmanager/images/usd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamworksanimation/usdmanager/HEAD/usdmanager/images/usd.png -------------------------------------------------------------------------------- /usdmanager/images/findNext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamworksanimation/usdmanager/HEAD/usdmanager/images/findNext.png -------------------------------------------------------------------------------- /usdmanager/images/findPrev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamworksanimation/usdmanager/HEAD/usdmanager/images/findPrev.png -------------------------------------------------------------------------------- /docs/_static/screenshot_island.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamworksanimation/usdmanager/HEAD/docs/_static/screenshot_island.png -------------------------------------------------------------------------------- /usdmanager/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultPrograms": { 3 | }, 4 | "themeSearchPaths": [], 5 | "iconThemes": { 6 | "light": "crystal_project", 7 | "dark": "crystal_project" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /usdmanager/images.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | images/lock.png 4 | images/findNext.png 5 | images/findPrev.png 6 | images/logo.png 7 | images/usd.png 8 | 9 | 10 | -------------------------------------------------------------------------------- /readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Use of v1 over v2 is intentional for maximum rtd compatibility 3 | 4 | formats: [] # Don't build htmlzip, pdf or epub 5 | 6 | build: 7 | image: latest 8 | 9 | requirements_file: docs/requirements.txt 10 | 11 | python: 12 | version: 2.7 13 | setup_py_install: true 14 | pip_install: false -------------------------------------------------------------------------------- /usdmanager/version.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 DreamWorks Animation L.L.C. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | __version__ = '0.15.0' 17 | -------------------------------------------------------------------------------- /usdmanager/parsers/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 DreamWorks Animation L.L.C. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | from pkgutil import extend_path 17 | __path__ = extend_path(__path__, __name__) 18 | -------------------------------------------------------------------------------- /usdmanager/highlighters/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 DreamWorks Animation L.L.C. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | from pkgutil import extend_path 17 | __path__ = extend_path(__path__, __name__) 18 | -------------------------------------------------------------------------------- /scripts/usdmanager: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2018 DreamWorks Animation L.L.C. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | from usdmanager import run 18 | 19 | if __name__ == "__main__": 20 | run() 21 | -------------------------------------------------------------------------------- /usdmanager/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 DreamWorks Animation L.L.C. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | from Qt.QtCore import QObject 17 | 18 | 19 | class Plugin(QObject): 20 | """ Classes in modules in the plugins directory that inherit from Plugin will be automatically initialized when the 21 | main window loads. 22 | """ 23 | def __init__(self, parent, **kwargs): 24 | """ Initialize the plugin. 25 | 26 | :Parameters: 27 | parent : `UsdMngrWindow` 28 | Main window 29 | """ 30 | super(Plugin, self).__init__(parent, **kwargs) 31 | -------------------------------------------------------------------------------- /usdmanager/highlighters/xml.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 DreamWorks Animation L.L.C. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | from Qt import QtCore, QtGui 17 | 18 | from ..highlighter import MasterHighlighter 19 | 20 | 21 | class MasterXMLHighlighter(MasterHighlighter): 22 | """ XML syntax highlighter 23 | """ 24 | extensions = ["html", "xml"] 25 | comment = None 26 | multilineComment = ("") 27 | 28 | def getRules(self): 29 | """ XML syntax highlighting rules """ 30 | return [ 31 | [ # XML element. Since we can't do a look behind in Qt to check for < or 32 | # symbols for tags get colored. 33 | r"|\?>|>||\?>|<(?:/|\?xml)?)\\b)", 41 | QtCore.Qt.darkMagenta, 42 | QtCore.Qt.magenta, 43 | QtGui.QFont.Bold 44 | ], 45 | [ # XML attribute 46 | r"\b\w+(?==)", 47 | None, 48 | None, 49 | None, 50 | True # Italic 51 | ], 52 | self.ruleNumber, 53 | self.ruleDoubleQuote, 54 | self.ruleSingleQuote, 55 | ] 56 | -------------------------------------------------------------------------------- /usdmanager/file_dialog.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 DreamWorks Animation L.L.C. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | from Qt.QtCore import QDir 17 | from Qt.QtWidgets import QFileDialog 18 | 19 | from .constants import FILE_FILTER 20 | 21 | 22 | class FileDialog(QFileDialog): 23 | """ 24 | Override the QFileDialog to provide hooks for customization. 25 | """ 26 | def __init__(self, parent=None, caption="", directory="", filters=None, selectedFilter="", showHidden=False): 27 | """ Initialize the dialog. 28 | 29 | :Parameters: 30 | parent : `QtCore.QObject` 31 | Parent object 32 | caption : `str` 33 | Dialog title 34 | directory : `str` 35 | Starting directory 36 | filters : `list` | None 37 | List of `str` file filters. Defaults to constants.FILE_FILTER 38 | selectedFilter : `str` 39 | Selected file filter 40 | showHidden : `bool` 41 | Show hidden files 42 | """ 43 | super(FileDialog, self).__init__(parent, caption, directory, ';;'.join(filters or FILE_FILTER)) 44 | 45 | # The following line avoids this warning with Qt5: 46 | # "GtkDialog mapped without a transient parent. This is discouraged." 47 | self.setOption(QFileDialog.DontUseNativeDialog) 48 | 49 | if selectedFilter: 50 | self.selectNameFilter(selectedFilter) 51 | if showHidden: 52 | self.setFilter(self.filter() | QDir.Hidden) 53 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. USD Manager documentation master file, created by 2 | sphinx-quickstart on Tue Mar 12 10:59:49 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | USD Manager 7 | =========== 8 | 9 | .. image:: ./_static/logo_512.png 10 | :target: ./_static/logo_512.png 11 | :alt: USD Manager 12 | 13 | 14 | `Website `_ 15 | 16 | USD Manager is an open-source, python-based Qt tool for browsing, managing, and editing text-based files like USD, 17 | combining the best features from your favorite web browser and text editor into one application, with hooks to deeply 18 | integrate with other pipeline tools. It is developed and maintained by `DreamWorks Animation `_ 19 | for use with USD and other hierarchical, text-based workflows, primarily geared towards feature film production. While 20 | primarily designed around PyQt4, USD Manager uses the Qt.py compatibility library to allow working with PyQt4, PyQt5, 21 | PySide, or PySide2 for Qt bindings. 22 | 23 | Development Repository 24 | ^^^^^^^^^^^^^^^^^^^^^^ 25 | 26 | This GitHub repository hosts the trunk of the USD Manager development. This implies that it is the newest public 27 | version with the latest features and bug fixes. However, it also means that it has not undergone a lot of testing and 28 | is generally less stable than the `production releases `_. 29 | 30 | License 31 | ^^^^^^^ 32 | 33 | USD Manager is released under the `Apache 2.0`_ license, which is a free, open-source, and detailed software license 34 | developed and maintained by the Apache Software Foundation. 35 | 36 | Contents 37 | ======== 38 | 39 | User Documentation 40 | 41 | .. toctree:: 42 | :maxdepth: 2 43 | 44 | Installing USD Manager 45 | Using USD Manager 46 | Keyboard Shortcuts 47 | Development / Customization 48 | Contributing 49 | 50 | API Documentation 51 | 52 | .. toctree:: 53 | :maxdepth: 3 54 | 55 | api/usdmanager 56 | 57 | .. _Apache 2.0: https://www.apache.org/licenses/LICENSE-2.0 -------------------------------------------------------------------------------- /docs/keyboardShortcuts.rst: -------------------------------------------------------------------------------- 1 | 2 | Keyboard Shortcuts 3 | ================== 4 | 5 | Full list of all normal and hidden keyboard shortcuts for USD Manager 6 | 7 | Keyboard Shortcuts 8 | ------------------ 9 | 10 | .. list-table:: 11 | :header-rows: 1 12 | 13 | * - Command 14 | - Shortcut 15 | * - Select All 16 | - Ctrl+A 17 | * - Copy 18 | - Ctrl+C 19 | * - Edit File 20 | - Ctrl+E 21 | * - Find/Replace 22 | - Ctrl+Shift+F 23 | * - Find Next 24 | - Ctrl+G 25 | * - Find Previous 26 | - Ctrl+Shift+G 27 | * - File Browser 28 | - Ctrl+I 29 | * - File Info... 30 | - Ctrl+Shift+I 31 | * - Go To Line Number... 32 | - Ctrl+Shift+L 33 | * - New Window 34 | - Ctrl+N 35 | * - Open... 36 | - Ctrl+O 37 | * - Open with... 38 | - Ctrl+Shift+O 39 | * - Print... 40 | - Ctrl+P 41 | * - Quit 42 | - Ctrl+Q 43 | * - Reload 44 | - Ctrl+R 45 | * - Save 46 | - Ctrl+S 47 | * - Save As... 48 | - Ctrl+Shift+S 49 | * - New Tab 50 | - Ctrl+T 51 | * - Paste 52 | - Ctrl+V 53 | * - Close Tab 54 | - Ctrl+W 55 | * - Cut 56 | - Ctrl+X 57 | * - Undo 58 | - Ctrl+Z 59 | * - Redo 60 | - Ctrl+Shift+Z 61 | * - Unindent 62 | - Ctrl+Shift+0 63 | * - Uncomment 64 | - Ctrl+3 65 | * - Comment Out 66 | - Ctrl+Shift+3 67 | * - Indent 68 | - Ctrl+Shift+9 69 | * - Zoom In 70 | - Ctrl++ 71 | * - Zoom Out 72 | - Ctrl+- 73 | * - Normal Size 74 | - Ctrl+0 75 | * - Back 76 | - Alt+Left 77 | * - Forward 78 | - Alt+Right 79 | * - Stop 80 | - Esc 81 | * - Documentation 82 | - F1 83 | * - Full Screen 84 | - F11 85 | 86 | 87 | Hidden shortcuts 88 | ---------------- 89 | 90 | For ease of use, there are some extra shortcuts not shown in the menus themselves: 91 | 92 | .. list-table:: 93 | :header-rows: 1 94 | 95 | * - Command 96 | - Shortcut 97 | * - Back 98 | - Backspace 99 | * - Find 100 | - Ctrl+F 101 | * - Zoom In 102 | - Ctrl+= 103 | * - Next Tab 104 | - Ctrl+Tab 105 | * - Previous Tab 106 | - Ctrl+Shift+Tab 107 | * - Reload 108 | - F5 109 | * - Indent (if text is selected) 110 | - Tab 111 | * - Unindent (if text is selected) 112 | - Shift+Tab 113 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 DreamWorks Animation L.L.C. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | from __future__ import absolute_import, division, print_function 17 | 18 | from setuptools import setup, find_packages 19 | from glob import glob 20 | 21 | 22 | PACKAGE = "usdmanager" 23 | import sys 24 | if sys.version_info[0] < 3: 25 | execfile("{}/version.py".format(PACKAGE)) 26 | else: 27 | exec(open("{}/version.py".format(PACKAGE)).read()) 28 | VERSION = __version__ 29 | 30 | 31 | setup( 32 | name=PACKAGE, 33 | version=VERSION, 34 | description="Tool for browsing, editing, and managing USD and other text files.", 35 | author="DreamWorks Animation", 36 | author_email="usdmanager@dreamworks.com", 37 | maintainer="Mark Sandell, DreamWorks Animation", 38 | maintainer_email="mark.sandell@dreamworks.com", 39 | url="https://github.com/dreamworksanimation/usdmanager", 40 | long_description=open("README.md").read(), 41 | classifiers=[ 42 | # Get classifiers from: 43 | # https://pypi.python.org/pypi?%3Aaction=list_classifiers 44 | "Development Status :: 4 - Beta", 45 | "Natural Language :: English", 46 | "Operating System :: POSIX", 47 | "Programming Language :: Python", 48 | "Programming Language :: Python :: 2", 49 | "License :: OSI Approved :: Apache Software License", 50 | ], 51 | packages=find_packages(), 52 | # package_data will only find files that are located within python packages 53 | package_data={ 54 | "usdmanager": [ 55 | "highlighters/*.py", 56 | "parsers/*.py", 57 | "plugins/*.py", 58 | "*.json", 59 | "*.ui" 60 | ] 61 | }, 62 | # data_files will find all other files. It is a list of two member tuples. 63 | # The first item of the tuple is the desired destination folder 64 | # The second member of the tuple is a list of source files. 65 | # Given data_files=[("xml_data", ["xml_examples/xml1.xml"])], xml1.xml will 66 | # be copied to the "xml_data" folder of the destination package. 67 | # the xml_examples folder will not be copied or created. 68 | data_files=[("usdmanager", ["usdmanager/usdviewstyle.qss"])], 69 | scripts=glob("scripts/*"), 70 | install_requires=[ 71 | "crystal_small", # Default icons 72 | "Qt.py>=1.1", 73 | "setuptools", # For pkg_resources 74 | ], 75 | setup_requires=[ 76 | "setuptools>=2.2", 77 | ], 78 | tests_require=[], 79 | dependency_links=[], 80 | ) 81 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This document details the contributing requirements and coding practices that are used in the USD Manager codebase. 4 | 5 | ## Contents 6 | 7 | - [The Contributor License Agreement](#the-contributor-license-agreement) 8 | - [Code Signing](#code-signing) 9 | - [Pull Requests](#pull-requests) 10 | - [Process](#process) 11 | - [Style Guide](#style-guide) 12 | * [Naming Conventions](#naming-conventions) 13 | * [Formatting](#formatting) 14 | * [General](#general) 15 | 16 | ## The Contributor License Agreement 17 | 18 | Developers who wish to contribute code to be considered for inclusion in the USD Manager distribution must first 19 | complete the [Contributor License Agreement](http://www.usdmanager.org/USDManagerContributorLicenseAgreement.pdf) 20 | and submit it to DreamWorks (directions in the CLA). 21 | 22 | ## Code Signing 23 | 24 | _Every commit must be signed off_. That is, every commit log message must include a "`Signed-off-by`" line (generated, for example, with 25 | "`git commit --signoff`"), indicating that the committer wrote the code and has the right to release it under the 26 | [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) license. See http://developercertificate.org/ for more 27 | information on this requirement. 28 | 29 | ## Pull Requests 30 | 31 | Pull requests should be rebased on the latest dev commit and squashed to as few logical commits as possible, preferably 32 | one. Each commit should pass tests without requiring further commits. 33 | 34 | ## Process 35 | 36 | 1. Fork the repository on GitHub 37 | 2. Clone it locally 38 | 3. Build a local copy 39 | ``` 40 | python setup.py install --user 41 | pip install -r docs/requirements.txt 42 | ``` 43 | 4. Write code, following the [style guide](#style-guide). 44 | 5. Test it 45 | 6. Update any manual documentation pages (like this one) 46 | 7. Test that the documentation builds without errors with: 47 | ``` 48 | sphinx-build -b html docs/ docs/_build 49 | ``` 50 | 6. Commit changes to the dev branch, signing off on them per the "[code signing](#code-signing)" instructions, then 51 | push the changes to your fork on GitHub 52 | 7. Make a pull request targeting the dev branch 53 | 54 | ## Style Guide 55 | 56 | In general, Python's [PEP 8 style guide](https://www.python.org/dev/peps/pep-0008) should be followed, with the few exceptions or clarifications noted below. 57 | Contributed code should conform to these guidelines to maintain consistency and maintainability. 58 | If there is a rule that you would like clarified, changed, or added, 59 | please send a note to [usdmanager@dreamworks.com](mailto:usdmanager@dreamworks.com). 60 | 61 | ### Naming Conventions 62 | 63 | In general, follow Qt naming conventions: 64 | * Class names should be CapitalizedWords with an uppercase starting letter. 65 | * Variable, function, and method names should be mixedCase with a lowercase starting letter. 66 | * Global constants should be UPPER_CASE_WITH_UNDERSCORES; otherwise, names_with_underscores should be avoided. 67 | 68 | ### Formatting 69 | 70 | * Indentation is 4 spaces. Do not use tabs. 71 | * Line length generally should not exceed 120 characters, especially for comments, but this is not a strict requirement. 72 | * Use Unix-style carriage returns ("\n") rather than Windows/DOS ones ("\r\n"). 73 | 74 | ### General 75 | 76 | * For new files, be sure to use the right license boilerplate per our license policy. 77 | -------------------------------------------------------------------------------- /usdmanager/highlighters/lua.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 DreamWorks Animation L.L.C. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | """ 17 | Lua syntax highlighter 18 | """ 19 | from Qt import QtCore, QtGui 20 | 21 | from ..highlighter import MasterHighlighter 22 | 23 | 24 | class MasterLuaHighlighter(MasterHighlighter): 25 | """ Lua syntax highlighter. 26 | """ 27 | extensions = ["lua"] 28 | comment = "--" 29 | multilineComment = ("--[[", "]]") 30 | 31 | def getRules(self): 32 | return [ 33 | [ # Symbols 34 | "[(){}[\]]", 35 | QtCore.Qt.darkMagenta, 36 | QtCore.Qt.magenta, 37 | QtGui.QFont.Bold 38 | ], 39 | [ 40 | # Keywords 41 | r"\b(?:and|break|do|else|elseif|end|for|function|if|in|local|not|or|repeat|return|then|until|while)\b", 42 | QtGui.QColor("#4b7029"), 43 | QtGui.QColor("#4b7029"), 44 | QtGui.QFont.Bold 45 | ], 46 | [ 47 | # Built-in constants 48 | r"\b(?:true|false|nil|_G|_VERSION)\b", 49 | QtGui.QColor("#997500"), 50 | QtGui.QColor("#997500"), 51 | QtGui.QFont.Bold 52 | ], 53 | [ 54 | # Built-in functions 55 | r"\b(?:abs|acos|asin|assert|atan|atan2|byte|ceil|char|clock|close|collectgarbage|concat|config|" 56 | "coroutine|cos|cosh|cpath|create|date|debug|debug|deg|difftime|dofile|dump|error|execute|exit|exp|" 57 | "find|floor|flush|fmod|foreach|foreachi|format|frexp|gcinfo|getenv|getfenv|getfenv|gethook|getinfo|" 58 | "getlocal|getmetatable|getmetatable|getn|getregistry|getupvalue|gfind|gmatch|gsub|huge|input|insert|" 59 | "io|ipairs|ldexp|len|lines|load|loaded|loaders|loadfile|loadlib|loadstring|log|log10|lower|match|math|" 60 | "max|maxn|min|mod|modf|module|newproxy|next|open|os|output|package|pairs|path|pcall|pi|popen|pow|" 61 | "preload|print|rad|random|randomseed|rawequal|rawget|rawset|read|remove|remove|rename|rep|require|" 62 | "resume|reverse|running|seeall|select|setfenv|setfenv|sethook|setlocal|setlocale|setmetatable|" 63 | "setmetatable|setn|setupvalue|sin|sinh|sort|sqrt|status|stderr|stdin|stdout|string|sub|table|tan|tanh|" 64 | r"time|tmpfile|tmpname|tonumber|tostring|traceback|type|type|unpack|upper|wrap|write|xpcall|yield)\b", 65 | QtGui.QColor("#678CB1"), 66 | QtGui.QColor("#678CB1") 67 | ], 68 | [ 69 | # Standard libraries 70 | r"\b(?:coroutine|debug|io|math|os|package|string|table)\b", 71 | QtGui.QColor("#8080FF"), 72 | QtGui.QColor("#8080FF") 73 | ], 74 | [ # Operators 75 | '(?:[\-+*/%=!<>&|^~]|\.\.)', 76 | QtGui.QColor("#990000"), 77 | QtGui.QColor("#990000") 78 | ], 79 | self.ruleNumber, 80 | self.ruleDoubleQuote, 81 | self.ruleSingleQuote, 82 | self.ruleLink, 83 | self.ruleComment 84 | ] 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Linux template 3 | *~ 4 | 5 | # temporary files which can be created if a process still has a handle open of a deleted file 6 | .fuse_hidden* 7 | 8 | # KDE directory preferences 9 | .directory 10 | 11 | # Linux trash folder which might appear on any partition or disk 12 | .Trash-* 13 | 14 | # .nfs files are created when an open file is removed but is still being accessed 15 | .nfs* 16 | 17 | ### macOS template 18 | # General 19 | .DS_Store 20 | .AppleDouble 21 | .LSOverride 22 | 23 | # Icon must end with two \r 24 | Icon 25 | 26 | # Thumbnails 27 | ._* 28 | 29 | # Files that might appear in the root of a volume 30 | .DocumentRevisions-V100 31 | .fseventsd 32 | .Spotlight-V100 33 | .TemporaryItems 34 | .Trashes 35 | .VolumeIcon.icns 36 | .com.apple.timemachine.donotpresent 37 | 38 | # Directories potentially created on remote AFP share 39 | .AppleDB 40 | .AppleDesktop 41 | Network Trash Folder 42 | Temporary Items 43 | .apdisk 44 | 45 | ### Python template 46 | # Byte-compiled / optimized / DLL files 47 | __pycache__/ 48 | *.py[cod] 49 | *$py.class 50 | 51 | # C extensions 52 | *.so 53 | 54 | # Distribution / packaging 55 | .Python 56 | build/ 57 | develop-eggs/ 58 | dist/ 59 | downloads/ 60 | eggs/ 61 | .eggs/ 62 | lib/ 63 | lib64/ 64 | parts/ 65 | sdist/ 66 | var/ 67 | wheels/ 68 | *.egg-info/ 69 | .installed.cfg 70 | *.egg 71 | MANIFEST 72 | 73 | # PyInstaller 74 | # Usually these files are written by a python script from a template 75 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 76 | *.manifest 77 | *.spec 78 | 79 | # Installer logs 80 | pip-log.txt 81 | pip-delete-this-directory.txt 82 | 83 | # Unit test / coverage reports 84 | htmlcov/ 85 | .tox/ 86 | .coverage 87 | .coverage.* 88 | .cache 89 | nosetests.xml 90 | coverage.xml 91 | *.cover 92 | .hypothesis/ 93 | 94 | # Translations 95 | *.mo 96 | *.pot 97 | 98 | # Django stuff: 99 | *.log 100 | local_settings.py 101 | 102 | # Flask stuff: 103 | instance/ 104 | .webassets-cache 105 | 106 | # Scrapy stuff: 107 | .scrapy 108 | 109 | # Sphinx documentation 110 | docs/_build/ 111 | docs/api/ 112 | 113 | # PyBuilder 114 | target/ 115 | 116 | # Jupyter Notebook 117 | .ipynb_checkpoints 118 | 119 | # pyenv 120 | .python-version 121 | 122 | # celery beat schedule file 123 | celerybeat-schedule 124 | 125 | # SageMath parsed files 126 | *.sage.py 127 | 128 | # Environments 129 | .env 130 | .venv 131 | env/ 132 | venv/ 133 | ENV/ 134 | env.bak/ 135 | venv.bak/ 136 | 137 | # Spyder project settings 138 | .spyderproject 139 | .spyproject 140 | 141 | # Rope project settings 142 | .ropeproject 143 | 144 | # mkdocs documentation 145 | /site 146 | 147 | # mypy 148 | .mypy_cache/ 149 | 150 | ### Windows template 151 | # Windows thumbnail cache files 152 | Thumbs.db 153 | ehthumbs.db 154 | ehthumbs_vista.db 155 | 156 | # Dump file 157 | *.stackdump 158 | 159 | # Folder config file 160 | Desktop.ini 161 | 162 | # Recycle Bin used on file shares 163 | $RECYCLE.BIN/ 164 | 165 | # Windows Installer files 166 | *.cab 167 | *.msi 168 | *.msm 169 | *.msp 170 | 171 | # Windows shortcuts 172 | *.lnk 173 | 174 | ### JetBrains template 175 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 176 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 177 | .idea 178 | 179 | # CMake 180 | cmake-build-debug/ 181 | 182 | ## File-based project format: 183 | *.iws 184 | 185 | ## Plugin-specific files: 186 | 187 | # IntelliJ 188 | out/ 189 | 190 | # mpeltonen/sbt-idea plugin 191 | .idea_modules/ 192 | 193 | # JIRA plugin 194 | atlassian-ide-plugin.xml 195 | 196 | # Crashlytics plugin (for Android Studio and IntelliJ) 197 | com_crashlytics_export_strings.xml 198 | crashlytics.properties 199 | crashlytics-build.properties 200 | fabric.properties 201 | 202 | # VSCode 203 | .vscode/ 204 | -------------------------------------------------------------------------------- /usdmanager/highlighters/python.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 DreamWorks Animation L.L.C. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | from Qt import QtCore, QtGui 17 | 18 | from ..highlighter import createMultilineRule, MasterHighlighter 19 | 20 | 21 | class MasterPythonHighlighter(MasterHighlighter): 22 | """ Python syntax highlighter. 23 | """ 24 | extensions = ["py"] 25 | comment = "#" 26 | multilineComment = ('"""', '"""') 27 | 28 | def getRules(self): 29 | return [ 30 | [ # Symbols 31 | "[(){}\[\]]", 32 | QtCore.Qt.darkMagenta, 33 | QtCore.Qt.magenta, 34 | QtGui.QFont.Bold 35 | ], 36 | [ 37 | # Keywords 38 | r"\b(?:and|as|assert|break|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|" 39 | r"import|in|is|lambda|nonlocal|not|or|pass|raise|return|try|while|with|yield)\b", 40 | QtGui.QColor("#4b7029"), 41 | QtGui.QColor("#4b7029"), 42 | QtGui.QFont.Bold 43 | ], 44 | [ 45 | # Built-ins 46 | r"\b(?:ArithmeticError|AssertionError|AttributeError|BaseException|BufferError|BytesWarning|" 47 | "DeprecationWarning|EOFError|Ellipsis|EnvironmentError|Exception|False|FloatingPointError|" 48 | "FutureWarning|GeneratorExit|IOError|ImportError|ImportWarning|IndentationError|IndexError|KeyError|" 49 | "KeyboardInterrupt|LookupError|MemoryError|NameError|None|NotImplemented|NotImplementedError|OSError|" 50 | "OverflowError|PendingDeprecationWarning|ReferenceError|RuntimeError|RuntimeWarning|StandardError|" 51 | "StopIteration|SyntaxError|SyntaxWarning|SystemError|SystemExit|TabError|True|TypeError|" 52 | "UnboundLocalError|UnicodeDecodeError|UnicodeEncodeError|UnicodeError|UnicodeTranslateError|" 53 | "UnicodeWarning|UserWarning|ValueError|Warning|ZeroDivisionError|__debug__|__doc__|__import__|" 54 | "__name__|__package__|abs|all|any|apply|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|" 55 | "classmethod|cmp|coerce|compile|complex|copyright|credits|delattr|dict|dir|divmod|dreload|enumerate|" 56 | "eval|execfile|file|filter|float|format|frozenset|get_ipython|getattr|globals|hasattr|hash|help|hex|" 57 | "id|input|int|intern|isinstance|issubclass|iter|len|license|list|locals|long|map|max|memoryview|min|" 58 | "next|object|oct|open|ord|pow|print|property|range|raw_input|reduce|reload|repr|reversed|round|set|" 59 | r"setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\b", 60 | QtGui.QColor("#678CB1"), 61 | QtGui.QColor("#678CB1") 62 | ], 63 | [ # Operators 64 | '[\-+*/%=!<>&|^~]', 65 | QtGui.QColor("#990000"), 66 | QtGui.QColor("#990000") 67 | ], 68 | self.ruleNumber, 69 | self.ruleDoubleQuote, 70 | self.ruleSingleQuote, 71 | self.ruleLink, 72 | self.ruleComment 73 | ] 74 | 75 | def createRules(self): 76 | super(MasterPythonHighlighter, self).createRules() 77 | 78 | # Support single-quote triple quotes in additional the the double quote triple quotes. 79 | self.multilineRules.append(createMultilineRule("'''", "'''", QtCore.Qt.gray, italic=True)) 80 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installing USD Manager 2 | 3 | USD Manager has primarily been developed for and tested on Linux. While the basics should work on other platforms, they 4 | have not been as heavily tested. Notes to help with installation on specific operating systems can be added here. 5 | 6 | **_These steps provide an example only and may need to be modified based on your specific setup and needs._** 7 | 8 | ## Contents 9 | 10 | - [Prerequisites](#prerequisites) 11 | - [Install with setup.py](#install-with-setup-py) 12 | - [OS Specific Notes](#os-specific-notes) 13 | * [Linux](#linux) 14 | * [Mac (OSX)](#mac-osx) 15 | * [Windows](#windows) 16 | - [Common Problems](#common-problems) 17 | 18 | ## Prerequisites 19 | - Install Python 2 ([https://www.python.org/downloads/](https://www.python.org/downloads/)), or 3 if on the python3 branch. 20 | * **Windows:** Ensure the install location is part of your PATH variable (newer installs should have an option for this) 21 | - Install one of the recommended Python Qt bindings 22 | * **Python 2:** PyQt4 or PySide 23 | * **Python 3:** PyQt5 or PySide2, example: 24 | ``` 25 | pip install PySide2 26 | ``` 27 | 28 | ## Install with setup.py 29 | 30 | For a site-wide install, try: 31 | ``` 32 | python setup.py install 33 | ``` 34 | 35 | For a personal install, try: 36 | ``` 37 | python setup.py install --user 38 | ``` 39 | 40 | Studios with significant python codebases or non-trivial installs may need to customize setup.py 41 | 42 | Your PATH and PYTHONPATH will need to be set appropriately to launch usdmanager, 43 | and this will depend on your setup.py install settings. 44 | 45 | ## OS Specific Notes 46 | 47 | ### Linux 48 | 49 | #### Known Issues 50 | - Print server may not recognize network printers. 51 | 52 | ### Mac (OSX) 53 | 54 | #### Installation 55 | 1. Launch Terminal 56 | 2. ```cd``` to the downloaded usdmanager folder (you should see a setup.py file in here). 57 | 3. Customize usdmanager/config.json if needed. 58 | 4. Run ```python setup.py install``` (may need to prepend the command with ```sudo``` and/or add the ```--user``` flag) 59 | 5. Depending on where you installed it (e.g. /Users/username/Library/Python/3.7/bin), update your $PATH to include the relevant bin directory by editing /etc/paths or ~/.zshrc. 60 | 61 | #### Known Issues 62 | - Since this is not installed as an entirely self-contained package, the application name (and icon) will by Python, not USD Manager. 63 | 64 | ### Windows 65 | 66 | #### Installation 67 | 1. Launch Command Prompt 68 | 2. ```cd``` to the downloaded usdmanager folder (you should see a setup.py file in here). 69 | 3. Customize usdmanager/config.json if needed. 70 | 4. Run ```python setup.py install``` (may need the ```--user``` flag) 71 | 72 | If setup.py complains about missing setuptools, you can install it via pip. If you installed a new enough python version, pip should already be handled for you, but you may still need to add it to your PATH. pip should already live somewhere like this (C:\Python27\Scripts\pip.exe or C:\Users\username\AppData\Local\Microsoft\WindowsApps\pip.exe), but if needed, you can permanently add it to your environment with this (adjusting the path as needed): ```setx PATH "%PATH%;C:\Python27\Scripts"``` 73 | 74 | 1. Upgrade pip if needed 75 | 1. Launch Command Prompt in Administrator mode 76 | 2. Run ```pip install pip --upgrade``` (may need the ```--user``` flag) 77 | 2. Install setuptools if needed 78 | 1. Run ```pip install setuptools``` 79 | 3. Re-run the setup.py step above for usdmanager 80 | 4. If you don't modify your path, you should now be able to run something like this to launch the program: ```python C:\Python27\Scripts\usdmanager``` or from the install directory itself, e.g. ``` python .\build\scripts-3.8\usdmanager``` 81 | 82 | #### Known Issues 83 | - Since this is not installed as an entirely self-contained package, the application name (and icon) will by Python, not USD Manager. 84 | 85 | ## Common Problems 86 | - Can't open files in external text editor 87 | * In Preferences, update your default text editor 88 | * **Windows:** Try ```notepad```, ```notepad.exe```, or ```"C:\Windows\notepad.exe"``` (including the quotation marks on that last one) 89 | -------------------------------------------------------------------------------- /usdmanager/find_dialog.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 DreamWorks Animation L.L.C. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | """ Create the Find or Find/Replace dialog. 17 | """ 18 | from Qt.QtCore import Slot 19 | from Qt.QtWidgets import QDialog, QStatusBar 20 | from Qt.QtGui import QTextDocument 21 | 22 | from .utils import icon, loadUiWidget 23 | 24 | 25 | class FindDialog(QDialog): 26 | """ 27 | Find/Replace dialog 28 | """ 29 | def __init__(self, parent=None, **kwargs): 30 | """ Initialize the dialog. 31 | 32 | :Parameters: 33 | parent : `QtWidgets.QWidget` | None 34 | Parent widget 35 | """ 36 | super(FindDialog, self).__init__(parent, **kwargs) 37 | self.setupUi() 38 | self.connectSignals() 39 | 40 | def setupUi(self): 41 | """ Creates and lays out the widgets defined in the ui file. 42 | """ 43 | self.baseInstance = loadUiWidget('find_dialog.ui', self) 44 | self.statusBar = QStatusBar(self) 45 | self.verticalLayout.addWidget(self.statusBar) 46 | self.findBtn.setIcon(icon("edit-find")) 47 | self.replaceBtn.setIcon(icon("edit-find-replace")) 48 | 49 | def connectSignals(self): 50 | """ Connect signals to slots. 51 | """ 52 | self.findLineEdit.textChanged.connect(self.updateButtons) 53 | 54 | def searchFlags(self): 55 | """ Get find flags based on checked options. 56 | 57 | :Returns: 58 | Find flags 59 | :Rtype: 60 | `QTextDocument.FindFlags` 61 | """ 62 | flags = QTextDocument.FindFlags() 63 | if self.caseSensitiveCheck.isChecked(): 64 | flags |= QTextDocument.FindCaseSensitively 65 | if self.wholeWordsCheck.isChecked(): 66 | flags |= QTextDocument.FindWholeWords 67 | if self.searchBackwardsCheck.isChecked(): 68 | flags |= QTextDocument.FindBackward 69 | return flags 70 | 71 | @Slot(str) 72 | def updateButtons(self, text): 73 | """ 74 | Update enabled state of buttons as entered text changes. 75 | 76 | :Parameters: 77 | text : `str` 78 | Currently entered find text 79 | """ 80 | enabled = bool(text) 81 | self.findBtn.setEnabled(enabled) 82 | self.replaceBtn.setEnabled(enabled) 83 | self.replaceFindBtn.setEnabled(enabled) 84 | self.replaceAllBtn.setEnabled(enabled) 85 | self.replaceAllOpenBtn.setEnabled(enabled) 86 | if not enabled: 87 | self.statusBar.clearMessage() 88 | self.setStyleSheet("QLineEdit#findLineEdit{background:none}") 89 | 90 | @Slot(bool) 91 | def updateForEditMode(self, edit): 92 | """ 93 | Show/Hide text replacement options based on if we are editing or not. 94 | If editing, allow replacement of the found text. 95 | 96 | :Parameters: 97 | edit : `bool` 98 | If in edit mode or not 99 | """ 100 | self.replaceLabel.setVisible(edit) 101 | self.replaceLineEdit.setVisible(edit) 102 | self.replaceBtn.setVisible(edit) 103 | self.replaceFindBtn.setVisible(edit) 104 | self.replaceAllBtn.setVisible(edit) 105 | self.replaceAllOpenBtn.setVisible(edit) 106 | self.buttonBox.setVisible(edit) 107 | self.buttonBox2.setVisible(not edit) 108 | if edit: 109 | self.setWindowTitle("Find/Replace") 110 | self.setWindowIcon(icon("edit-find-replace")) 111 | else: 112 | self.setWindowTitle("Find") 113 | self.setWindowIcon(icon("edit-find")) 114 | -------------------------------------------------------------------------------- /usdmanager/parsers/log.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2020 DreamWorks Animation L.L.C. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | """ 17 | Log file parser 18 | """ 19 | import re 20 | from xml.sax.saxutils import escape 21 | 22 | from Qt.QtCore import QFileInfo, Slot 23 | 24 | from ..constants import TTY2HTML 25 | from ..parser import AbstractExtParser 26 | from ..utils import expandPath 27 | 28 | 29 | class LogParser(AbstractExtParser): 30 | """ Used for log files that may contain terminal color code characters. 31 | """ 32 | exts = ("log", "txt") 33 | 34 | @staticmethod 35 | def convertTeletype(t): 36 | """ Convert teletype codes to HTML styles. 37 | This method assumes you have already escaped any necessary HTML characters. 38 | 39 | :Parameters: 40 | t : `str` 41 | Original text 42 | :Returns: 43 | String with teletype codes converted to HTML styles. 44 | :Rtype: 45 | `str` 46 | """ 47 | for (code, style) in TTY2HTML: 48 | t = t.replace(code, style) 49 | return "{}".format(t) 50 | 51 | @Slot() 52 | def compile(self): 53 | super(LogParser, self).compile() 54 | # Optionally match ", line " followed by a number (often found in tracebacks). 55 | # This number is used for attaching the query string ?line= to the URL 56 | self.regex = re.compile(self.regex.pattern + r'(?:, line (\d+))?') 57 | 58 | def htmlFormat(self, text): 59 | if self.parent().preferences['teletype']: 60 | text = self.convertTeletype(text) 61 | return super(LogParser, self).htmlFormat(text) 62 | 63 | def parseMatch(self, match, linkPath, nativeAbsPath, fileInfo): 64 | """ Parse a RegEx match of a patch to another file. 65 | 66 | Override for specific language parsing. 67 | 68 | :Parameters: 69 | match 70 | RegEx match object 71 | linkPath : `str` 72 | Displayed file path matched by the RegEx 73 | nativeAbsPath : `str` 74 | OS-native absolute file path for the file being parsed 75 | fileInfo : `QFileInfo` 76 | File info object for the file being parsed 77 | :Returns: 78 | HTML link 79 | :Rtype: 80 | `str` 81 | :Raises ValueError: 82 | If path does not exist or cannot be resolved. 83 | """ 84 | # This group number must be carefully kept in sync based on the default RegEx from the parent class. 85 | queryStr = "?line=" + match.group(2) if match.group(2) is not None else "" 86 | 87 | if QFileInfo(linkPath).isAbsolute(): 88 | fullPath = QFileInfo(expandPath(linkPath, nativeAbsPath)).absoluteFilePath() 89 | else: 90 | # Relative path from the current file to the link. 91 | fullPath = fileInfo.dir().absoluteFilePath(expandPath(linkPath, nativeAbsPath)) 92 | 93 | # Make the HTML link. 94 | if self.exists[fullPath]: 95 | return '{}'.format(fullPath, queryStr, escape(linkPath)) 96 | elif '*' in linkPath or '' in linkPath or '.#.' in linkPath: 97 | # Create an orange link for files with wildcards in the path, 98 | # designating zero or more files may exist. 99 | return '{}'.format( 100 | fullPath, queryStr, escape(linkPath)) 101 | return '{}'.format( 102 | fullPath, queryStr, escape(linkPath)) 103 | -------------------------------------------------------------------------------- /usdmanager/file_status.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 DreamWorks Animation L.L.C. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | import logging 17 | 18 | from Qt.QtCore import QFileInfo, QUrl 19 | from Qt.QtGui import QIcon 20 | 21 | 22 | # Set up logging. 23 | logger = logging.getLogger(__name__) 24 | logging.basicConfig() 25 | 26 | 27 | class FileStatus(object): 28 | """ File status cache class allowing overriding with additional statuses for custom integration of things like a 29 | revision control system. 30 | """ 31 | FILE_NEW = 0 # New file, never saved. Ok to edit, save. 32 | FILE_NOT_WRITABLE = 1 # File not writable. Nothing allowed. 33 | FILE_WRITABLE = 2 # File writable. Ok to edit, save. 34 | FILE_TRUNCATED = 4 # File was truncated on read. Nothing allowed. 35 | 36 | def __init__(self, url=None, update=True, truncated=False): 37 | """ Initialize the FileStatus cache 38 | 39 | :Parameters: 40 | url : `QtCore.QUrl` 41 | File URL 42 | update : `bool` 43 | Immediately update file status or not, like checking if it's writable. 44 | truncated : `bool` 45 | If the file was truncated on read, and therefore should never be edited. 46 | """ 47 | self.url = url if url else QUrl() 48 | self.path = "" if self.url.isEmpty() else self.url.toLocalFile() 49 | self.status = self.FILE_NEW 50 | self.fileInfo = None 51 | if update: 52 | self.updateFileStatus(truncated) 53 | 54 | def updateFileStatus(self, truncated=False): 55 | """ Cache the status of a file. 56 | 57 | :Parameters: 58 | truncated : `bool` 59 | If the file was truncated on read, and therefore should never be edited. 60 | """ 61 | if self.path: 62 | if self.fileInfo is None: 63 | self.fileInfo = QFileInfo(self.path) 64 | self.fileInfo.setCaching(False) 65 | if truncated: 66 | self.status = self.FILE_TRUNCATED 67 | elif self.fileInfo.isWritable(): 68 | self.status = self.FILE_WRITABLE 69 | else: 70 | self.status = self.FILE_NOT_WRITABLE 71 | else: 72 | self.status = self.FILE_NEW 73 | 74 | @property 75 | def icon(self): 76 | """ Get an icon to display representing the file's status. 77 | 78 | :Returns: 79 | Icon (may be blank) 80 | :Rtype: 81 | `QIcon` 82 | """ 83 | if self.status == self.FILE_NOT_WRITABLE: 84 | return QIcon(":images/images/lock") 85 | return QIcon() 86 | 87 | @property 88 | def text(self): 89 | """ Get a status string to display for the file. 90 | 91 | :Returns: 92 | File status (may be an empty string) 93 | :Rtype: 94 | `str` 95 | """ 96 | if self.status == self.FILE_NEW: 97 | return "" 98 | elif self.status == self.FILE_NOT_WRITABLE: 99 | return "File not writable" 100 | elif self.status == self.FILE_WRITABLE: 101 | return "File writable" 102 | elif self.status == self.FILE_TRUNCATED: 103 | return "File too large to fully display" 104 | else: 105 | logger.error("Unexpected file status code: %s", self.status) 106 | return "" 107 | 108 | @property 109 | def writable(self): 110 | """ Get if the file is writable. 111 | 112 | :Returns: 113 | If the file is writable 114 | :Rtype: 115 | `bool` 116 | """ 117 | return self.status in (self.FILE_NEW, self.FILE_WRITABLE) 118 | -------------------------------------------------------------------------------- /usdmanager/constants.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 DreamWorks Animation L.L.C. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | """ 17 | Constant values 18 | """ 19 | # USD file extensions. 20 | # Expandable with custom file formats. 21 | # First in each tuple is preferred extension for that format (e.g. in Save dialog). 22 | USD_AMBIGUOUS_EXTS = ("usd",) # Can be ASCII or crate. 23 | USD_ASCII_EXTS = ("usda",) # Can ONLY be ASCII. 24 | USD_CRATE_EXTS = ("usdc",) # Can ONLY be Crate. 25 | USD_ZIP_EXTS = ("usdz",) 26 | USD_EXTS = USD_AMBIGUOUS_EXTS + USD_ASCII_EXTS + USD_CRATE_EXTS + USD_ZIP_EXTS 27 | 28 | 29 | # File filters for the File > Open... and File > Save As... dialogs. 30 | FILE_FILTER = ( 31 | "USD Files (*.{})".format(" *.".join(USD_EXTS)), 32 | "USD - ASCII (*.{})".format(" *.".join(USD_AMBIGUOUS_EXTS + USD_ASCII_EXTS)), 33 | "USD - Crate (*.{})".format(" *.".join(USD_AMBIGUOUS_EXTS + USD_CRATE_EXTS)), 34 | "USD - Zip (*.{})".format(" *.".join(USD_ZIP_EXTS)), 35 | "Text Files (*.html *.json *.log *.py *.txt *.xml *.yaml *.yml)", 36 | "All Files (*)" 37 | ) 38 | 39 | # Format of the currently active file. Also, the index in the file filter list for that type. 40 | # Used for things such as differentiating between file types when using the generic .usd extension. 41 | FILE_FORMAT_USD = 0 # Generic USD file (usda or usdc) 42 | FILE_FORMAT_USDA = 1 # ASCII USD file 43 | FILE_FORMAT_USDC = 2 # Binary USD crate file 44 | FILE_FORMAT_USDZ = 3 # Zip-compressed USD package 45 | FILE_FORMAT_TXT = 4 # Plain text file 46 | FILE_FORMAT_NONE = 5 # Generic file, presumably plain text 47 | 48 | # Default template for display files with links. 49 | # When dark theme is enabled, this is overridden in __init__.py. 50 | HTML_BODY = """ 51 | {}""" 56 | 57 | # Set a length limit on parsing for links and syntax highlighting on long lines. 999 chosen semi-arbitrarily to speed 58 | # up things like crate files with really long timeSamples lines that otherwise lock up the UI. 59 | # TODO: Potentially truncate the display of long lines, too, since it can slow down interactivity of the Qt UI. 60 | # Maybe make it a [...] link to display the full line again? 61 | LINE_CHAR_LIMIT = 999 62 | 63 | # Truncate loading files with more lines than this. 64 | # Display can slow down and/or become unusable with too many lines. 65 | # This number is less important than the total number of characters and can be overridden in Preferences. 66 | LINE_LIMIT = 50000 67 | 68 | # Truncate loading files with more total chars than this. 69 | # QString crashes at ~2.1 billion chars, but display slows down way before that. 70 | CHAR_LIMIT = 100000000 71 | 72 | # Number of recent files and tabs to remember. 73 | RECENT_FILES = 20 74 | RECENT_TABS = 10 75 | 76 | # Shell character escape codes that can be converted for HTML display. 77 | TTY2HTML = ( 78 | ('', ''), 79 | ('\x1b[40m', ''), 80 | ('\x1b[44m', ''), 81 | ('\x1b[46m', ''), 82 | ('\x1b[42m', ''), 83 | ('\x1b[45m', ''), 84 | ('\x1b[41m', ''), 85 | ('\x1b[47m', ''), 86 | ('\x1b[43m', ''), 87 | ('\x1b[30m', ''), 88 | ('\x1b[34m', ''), 89 | ('\x1b[36m', ''), 90 | ('\x1b[32m', ''), 91 | ('\x1b[35m', ''), 92 | ('\x1b[31m', ''), 93 | ('\x1b[37m', ''), 94 | ('\x1b[33m', ''), 95 | ('\x1b[7m', ''), 96 | ('\x1b[0m', ''), 97 | ('\x1b[4m', ''), 98 | ('\x1b[1m', '') 99 | ) 100 | -------------------------------------------------------------------------------- /usdmanager/highlighters/usd.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 DreamWorks Animation L.L.C. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | from Qt import QtCore, QtGui 17 | 18 | from ..highlighter import createMultilineRule, MasterHighlighter 19 | from ..constants import USD_EXTS 20 | 21 | 22 | class MasterUSDHighlighter(MasterHighlighter): 23 | """ USD syntax highlighter 24 | """ 25 | extensions = USD_EXTS 26 | comment = "#" 27 | multilineComment = ('/*', '*/') 28 | 29 | def getRules(self): 30 | return [ 31 | [ # Symbols and booleans 32 | # \u2026 is the horizontal ellipsis we insert in the middle of long arrays. 33 | "(?:[>(){}[\]=@" + u"\u2026" + "]| usdmanager/plugins/images_rc.py 139 | ``` 140 | 141 | If using pyrcc4, be sure to replace PyQt4 with Qt in the images_rc.py's import line. 142 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Using USD Manager 2 | 3 | Once you have installed usdmanager, you can launch from the command line: 4 | 5 | ``` 6 | usdmanager 7 | ``` 8 | 9 | You can also specify one or more files to open directly: 10 | 11 | ``` 12 | usdmanager shot.usd 13 | ``` 14 | 15 | ## Contents 16 | 17 | - [Browse Mode](#browse-mode) 18 | * [Browsing Standard Features](#browsing-standard-features) 19 | - [Edit Mode](#edit-mode) 20 | * [Editing Standard Features](#editing-standard-features) 21 | - [USD Crate](#usd-crate) 22 | - [Preferences](#preferences) 23 | * [Tabbed Browsing](#tabbed-browsing) 24 | * [Font](#font) 25 | * [Programs](#programs) 26 | - [Commands](#commands) 27 | * [File Info...](#file-info) 28 | * [Diff File...](#diff-file) 29 | * [Comment Out](#comment-out) 30 | * [Uncomment](#uncomment) 31 | * [Indent](#indent) 32 | * [Unindent](#unindent) 33 | * [Open with usdview...](#open-with-usdview) 34 | * [Open with text editor...](#open-with-text-editor) 35 | * [Open with...](#open-with) 36 | 37 | ## Browse Mode 38 | 39 | Browse mode is the standard mode that the application launches in, which displays text-based files like a typical web 40 | browser. Additionally, it attempts to parse the given file for links to other files, such as USD references and 41 | payloads, images, and models, looking for anything inside of 'single quotes,' "double quotes," and @at symbols@. 42 | These links can then be followed like links on a website. Links to files that exist are colored blue, wildcard links 43 | to zero or more files are colored yellow, and paths we think are links that cannot be resolved to a valid path are red. 44 | Binary USD Crate files are highlighted in purple instead of blue. 45 | 46 | ### Browsing Standard Features 47 | The browser boasts many standard features, including tabbed browsing with rearrangeable tabs, a navigational history 48 | per tab, a recent files list (File > Open Recent), and the ability to restore closed Tabs (History > Recently Closed 49 | Tabs). 50 | 51 | ## Edit Mode 52 | 53 | The program can switch back and forth between browsing (the default) and editing. Before switching to the editor, the 54 | file must be writable. If using files in a revision control system, this is where custom plug-ins can come in handy to 55 | allow you to check in and out files so that you have write permissions before switching to Edit mode. 56 | 57 | To switch between Browse mode and Edit mode, hit the Ctrl+E keyboard shortcut, click the Browser/Editor button above 58 | the address bar (to the right of the zoom buttons), or click File -> Browse Mode (or File -> Edit Mode). If you have 59 | modified the file without saving, you will be prompted to save your changes before continuing. 60 | 61 | ### Editing Standard Features 62 | The editor includes many standard features such as cut/copy/paste support, comment/uncomment macros, and find/replace 63 | functionality. 64 | 65 | Files that have been modified are marked as dirty with asterisk around the file name in tabs and the window title. 66 | Before saving a modified file, you can choose to diff your file (Commands -> Diff file...) if you want to see what you 67 | changed. The diffing tool can be modified per user in preferences (Edit > Preferences...) or with the "diffTool" key in 68 | the app config file. 69 | 70 | ## USD Crate 71 | 72 | Binary USD Crate files are supported within the app. You can view and edit them just like using usdedit, but under the 73 | hood, the app is converting back and forth between binary and ASCII formats as needed. Any edits to the file are saved 74 | in the original file format, so opening a binary .usd or .usdc file will save back out in binary. You can force a file 75 | to ASCII by saving with the .usda extension. Similarly, you can force a formerly ASCII-based file to the binary Crate 76 | format by saving with the .usdc extension. Currently, there is no UI to switch between ASCII and binary other than 77 | setting the file extension in the Save As dialog. 78 | 79 | ## Preferences 80 | 81 | Most user preferences can be accessed under the Edit > Preferences... menu option. Preferences in this dialog are saved 82 | for future sessions. 83 | 84 | ### Tabbed Browsing 85 | Like many web browsers, files can be viewed in multiple tabs. The "+" button on the upper-left of the browser portion 86 | adds a new tab, and the "x" closes the current tab. You can choose to always open files in new tabs under 87 | Edit > Preferences... On the General tab, select "Open files in new tabs." 88 | 89 | Alternatively, you can open a file in the current tab by left-clicking the link, and open a file in a new tab by 90 | Ctrl+left-clicking or middle-mouse-clicking the link. There is also a right-click menu item to open the link in a new 91 | tab. To navigate among tabs, you can simply click on the desired tab, or use "Ctrl+Tab" to move forward and 92 | "Ctrl+Shift+Tab" to move backwards. 93 | 94 | ### Font 95 | Font sizes can be adjusted with the "Zoom In," "Zoom Out," and "Normal Size" options under the "View" menu, or with the 96 | keyboard shortcuts: Ctrl++, Ctrl+-, and Ctrl+0. This size will be applied to all future tabs and is saved as a 97 | preference for your next session. You can also choose a default font for the displayed document in the Preferences 98 | dialog. 99 | 100 | ### Programs 101 | The extensions that usdmanager searches for when creating links to files can be adjusted under the Programs tab of the 102 | Preferences dialog. If an extension is not on this page, usdmanager will not know to look for it. Any file type that 103 | you wish to display in-app should be listed on the first line in a comma-separated list. File types that you wish to 104 | open in external programs such as an image viewer can be designated in the lower section. If you always want .jpg files 105 | to open in a fullscreen version of eog, for example, set "eog --fullscreen" for the program and ".jpg" for the 106 | extension. 107 | 108 | ## Commands 109 | 110 | Commands may be accessed through the "Commands" menu or by right-clicking inside the browser portion of the program. 111 | 112 | _Additional commands beyond the basics provided here can be added via the app config file and 113 | plug-in system. For details, see "Menu plug-ins" in the "Development / Customization" section._ 114 | 115 | ### File Info... 116 | View information about the current file, including the owner, size, permissions, and last modified time. 117 | 118 | ### Diff File... 119 | If you have made changes to the file without saving, you can use this command to compare the changes to the version 120 | currently saved on disk. The program saves a temporary version of your changes and launches the original and temp files 121 | via the diff tool of your choice (default: xdiff), which can be managed via the Preferences dialog and the app config 122 | file. 123 | 124 | ### Comment Out 125 | Comment out the selected lines with the appropriate symbol for the current file type. Supports USD, Lua, Python, HTML, 126 | and XML comments, defaulting to # for the comment string if the language is unknown. 127 | 128 | ### Uncomment 129 | Uncomment the selected lines. See "Comment Out" above for supported languages. 130 | 131 | ### Indent 132 | Indent the selected lines by one tab stop (4 spaces). 133 | 134 | ### Unindent 135 | Unindent the selected lines by one tab stop (4 spaces). 136 | 137 | ### Open with usdview... 138 | For USD files, launch the current file in usdview. 139 | 140 | ### Open with text editor... 141 | The command launches the current file in a text editor of your choice. By default, usdmanager uses $EDITOR, and nedit 142 | if that environment variable is not defined. You can set your preferred editor using the Preferences dialog under 143 | Edit > Preferences.... This preference will be saved for future sessions. 144 | 145 | ### Open with... 146 | If usdmanager does not open a file with the program you desire, you can use "Open with..." to enter a program (and any 147 | extra flags) of your choosing. The file is appended at the end of what you enter. To open a link in this manner, 148 | right-click the link and select "Open link with..." from the context menu. 149 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # USD Manager documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Mar 12 10:59:49 2019. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import os 16 | from recommonmark.parser import CommonMarkParser 17 | import sys 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | sys.path.insert(0, os.path.abspath('../')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | needs_sphinx = '1.8' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.autodoc', 34 | 'sphinx.ext.viewcode', 35 | 'sphinxcontrib.apidoc', 36 | ] 37 | 38 | autodoc_mock_imports = [ 39 | 'Qt', 40 | ] 41 | 42 | # Add any paths that contain templates here, relative to this directory. 43 | templates_path = ['_templates'] 44 | 45 | source_parsers = { 46 | '.md': CommonMarkParser, 47 | } 48 | 49 | # The suffix of source filenames. 50 | source_suffix = ['.rst', '.md'] 51 | 52 | # The encoding of source files. 53 | #source_encoding = 'utf-8-sig' 54 | 55 | # The master toctree document. 56 | master_doc = 'index' 57 | 58 | # General information about the project. 59 | project = u'USD Manager' 60 | copyright = u'2019, DreamWorks Animation' 61 | 62 | # The version info for the project you're documenting, acts as replacement for 63 | # |version| and |release|, also used in various other places throughout the 64 | # built documents. 65 | # 66 | execfile("../usdmanager/version.py") 67 | # The short X.Y version. 68 | version = __version__ 69 | # The full version, including alpha/beta/rc tags. 70 | release = __version__ 71 | 72 | apidoc_module_dir = '../usdmanager' 73 | apidoc_output_dir = 'api' 74 | apidoc_separate_modules = True 75 | apidoc_toc_file = False 76 | apidoc_extra_args = ['-P', '-f'] 77 | 78 | # The language for content autogenerated by Sphinx. Refer to documentation 79 | # for a list of supported languages. 80 | #language = None 81 | 82 | # There are two options for replacing |today|: either, you set today to some 83 | # non-false value, then it is used: 84 | #today = '' 85 | # Else, today_fmt is used as the format for a strftime call. 86 | #today_fmt = '%B %d, %Y' 87 | 88 | # List of patterns, relative to source directory, that match files and 89 | # directories to ignore when looking for source files. 90 | exclude_patterns = ['_build'] 91 | 92 | # The reST default role (used for this markup: `text`) to use for all 93 | # documents. 94 | #default_role = None 95 | 96 | # If true, '()' will be appended to :func: etc. cross-reference text. 97 | #add_function_parentheses = True 98 | 99 | # If true, the current module name will be prepended to all description 100 | # unit titles (such as .. function::). 101 | #add_module_names = True 102 | 103 | # If true, sectionauthor and moduleauthor directives will be shown in the 104 | # output. They are ignored by default. 105 | #show_authors = False 106 | 107 | # The name of the Pygments (syntax highlighting) style to use. 108 | pygments_style = 'sphinx' 109 | 110 | # A list of ignored prefixes for module index sorting. 111 | #modindex_common_prefix = [] 112 | 113 | # If true, keep warnings as "system message" paragraphs in the built documents. 114 | #keep_warnings = False 115 | 116 | 117 | # -- Options for HTML output ---------------------------------------------- 118 | 119 | # The theme to use for HTML and HTML Help pages. See the documentation for 120 | # a list of builtin themes. 121 | html_theme = 'default' 122 | 123 | # Theme options are theme-specific and customize the look and feel of a theme 124 | # further. For a list of options available for each theme, see the 125 | # documentation. 126 | #html_theme_options = {} 127 | 128 | # Add any paths that contain custom themes here, relative to this directory. 129 | #html_theme_path = [] 130 | 131 | # The name for this set of Sphinx documents. If None, it defaults to 132 | # " v documentation". 133 | #html_title = None 134 | 135 | # A shorter title for the navigation bar. Default is the same as html_title. 136 | #html_short_title = None 137 | 138 | # The name of an image file (relative to this directory) to place at the top 139 | # of the sidebar. 140 | #html_logo = None 141 | 142 | # The name of an image file (within the static path) to use as favicon of the 143 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 144 | # pixels large. 145 | #html_favicon = None 146 | 147 | # Add any paths that contain custom static files (such as style sheets) here, 148 | # relative to this directory. They are copied after the builtin static files, 149 | # so a file named "default.css" will overwrite the builtin "default.css". 150 | html_static_path = ['_static'] 151 | 152 | # Add any extra paths that contain custom files (such as robots.txt or 153 | # .htaccess) here, relative to this directory. These files are copied 154 | # directly to the root of the documentation. 155 | #html_extra_path = [] 156 | 157 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 158 | # using the given strftime format. 159 | #html_last_updated_fmt = '%b %d, %Y' 160 | 161 | # If true, SmartyPants will be used to convert quotes and dashes to 162 | # typographically correct entities. 163 | #html_use_smartypants = True 164 | 165 | # Custom sidebar templates, maps document names to template names. 166 | #html_sidebars = {} 167 | 168 | # Additional templates that should be rendered to pages, maps page names to 169 | # template names. 170 | #html_additional_pages = {} 171 | 172 | # If false, no module index is generated. 173 | #html_domain_indices = True 174 | 175 | # If false, no index is generated. 176 | #html_use_index = True 177 | 178 | # If true, the index is split into individual pages for each letter. 179 | #html_split_index = False 180 | 181 | # If true, links to the reST sources are added to the pages. 182 | #html_show_sourcelink = True 183 | 184 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 185 | #html_show_sphinx = True 186 | 187 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 188 | #html_show_copyright = True 189 | 190 | # If true, an OpenSearch description file will be output, and all pages will 191 | # contain a tag referring to it. The value of this option must be the 192 | # base URL from which the finished HTML is served. 193 | #html_use_opensearch = '' 194 | 195 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 196 | #html_file_suffix = None 197 | 198 | # Output file base name for HTML help builder. 199 | htmlhelp_basename = 'USDManagerdoc' 200 | 201 | 202 | # -- Options for LaTeX output --------------------------------------------- 203 | 204 | latex_elements = { 205 | # The paper size ('letterpaper' or 'a4paper'). 206 | #'papersize': 'letterpaper', 207 | 208 | # The font size ('10pt', '11pt' or '12pt'). 209 | #'pointsize': '10pt', 210 | 211 | # Additional stuff for the LaTeX preamble. 212 | #'preamble': '', 213 | } 214 | 215 | # Grouping the document tree into LaTeX files. List of tuples 216 | # (source start file, target name, title, 217 | # author, documentclass [howto, manual, or own class]). 218 | latex_documents = [ 219 | ('index', 'USDManager.tex', u'USD Manager Documentation', 220 | u'DreamWorks Animation', 'manual'), 221 | ] 222 | 223 | # The name of an image file (relative to this directory) to place at the top of 224 | # the title page. 225 | #latex_logo = None 226 | 227 | # For "manual" documents, if this is true, then toplevel headings are parts, 228 | # not chapters. 229 | #latex_use_parts = False 230 | 231 | # If true, show page references after internal links. 232 | #latex_show_pagerefs = False 233 | 234 | # If true, show URL addresses after external links. 235 | #latex_show_urls = False 236 | 237 | # Documents to append as an appendix to all manuals. 238 | #latex_appendices = [] 239 | 240 | # If false, no module index is generated. 241 | #latex_domain_indices = True 242 | 243 | 244 | # -- Options for manual page output --------------------------------------- 245 | 246 | # One entry per manual page. List of tuples 247 | # (source start file, name, description, authors, manual section). 248 | man_pages = [ 249 | ('index', 'usdmanager', u'USD Manager Documentation', 250 | [u'DreamWorks Animation'], 1) 251 | ] 252 | 253 | # If true, show URL addresses after external links. 254 | #man_show_urls = False 255 | 256 | 257 | # -- Options for Texinfo output ------------------------------------------- 258 | 259 | # Grouping the document tree into Texinfo files. List of tuples 260 | # (source start file, target name, title, author, 261 | # dir menu entry, description, category) 262 | texinfo_documents = [ 263 | ('index', 'USDManager', u'USD Manager Documentation', 264 | u'DreamWorks Animation', 'USDManager', 'One line description of project.', 265 | 'Miscellaneous'), 266 | ] 267 | 268 | # Documents to append as an appendix to all manuals. 269 | #texinfo_appendices = [] 270 | 271 | # If false, no module index is generated. 272 | #texinfo_domain_indices = True 273 | 274 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 275 | #texinfo_show_urls = 'footnote' 276 | 277 | # If true, do not generate a @detailmenu in the "Top" node's menu. 278 | #texinfo_no_detailmenu = False 279 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /usdmanager/linenumbers.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 DreamWorks Animation L.L.C. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | """ 17 | Line numbers widget for optimized display of line numbers on the left side of 18 | a text widget. 19 | """ 20 | from __future__ import division 21 | 22 | from Qt import QtCore 23 | from Qt.QtCore import QRect, QSize, Slot 24 | from Qt.QtGui import QColor, QFont, QPainter, QTextCharFormat, QTextFormat 25 | from Qt.QtWidgets import QTextEdit, QWidget 26 | 27 | # Shadow round for Python 3 compatibility 28 | from .utils import round as round 29 | 30 | 31 | class PlainTextLineNumbers(QWidget): 32 | """ Line number widget for `QPlainTextEdit` widgets. 33 | """ 34 | def __init__(self, parent): 35 | """ Initialize the line numbers widget. 36 | 37 | :Parameters: 38 | parent : `QPlainTextEdit` 39 | Text widget 40 | """ 41 | super(PlainTextLineNumbers, self).__init__(parent) 42 | self.textWidget = parent 43 | self._hiddenByUser = False 44 | self._highlightCurrentLine = True 45 | self._movePos = None 46 | 47 | # Monospaced font to keep width from shifting. 48 | font = QFont() 49 | font.setStyleHint(QFont.Courier) 50 | font.setFamily("Monospace") 51 | self.setFont(font) 52 | 53 | self.connectSignals() 54 | self.updateLineWidth() 55 | self.highlightCurrentLine() 56 | 57 | def blockCount(self): 58 | return self.textWidget.blockCount() 59 | 60 | def connectSignals(self): 61 | """ Connect signals from the text widget that affect line numbers. 62 | """ 63 | self.textWidget.blockCountChanged.connect(self.updateLineWidth) 64 | self.textWidget.updateRequest.connect(self.updateLineNumbers) 65 | self.textWidget.cursorPositionChanged.connect(self.highlightCurrentLine) 66 | 67 | @Slot() 68 | def highlightCurrentLine(self): 69 | """ Highlight the line the cursor is on. 70 | 71 | :Returns: 72 | If highlighting was enabled or not. 73 | :Rtype: 74 | `bool` 75 | """ 76 | if not self._highlightCurrentLine: 77 | return False 78 | 79 | extras = [x for x in self.textWidget.extraSelections() if x.format.property(QTextFormat.UserProperty) != "line"] 80 | selection = QTextEdit.ExtraSelection() 81 | lineColor = QColor(QtCore.Qt.darkGray).darker() if self.window().isDarkTheme() else \ 82 | QColor(QtCore.Qt.yellow).lighter(180) 83 | selection.format.setBackground(lineColor) 84 | selection.format.setProperty(QTextFormat.FullWidthSelection, True) 85 | selection.format.setProperty(QTextFormat.UserProperty, "line") 86 | selection.cursor = self.textWidget.textCursor() 87 | selection.cursor.clearSelection() 88 | # Put at the beginning of the list so we preserve any highlighting from Find's highlight all functionality. 89 | extras.insert(0, selection) 90 | ''' 91 | if self.window().buttonHighlightAll.isChecked() and self.window().findBar.text(): 92 | selection = QTextEdit.ExtraSelection() 93 | lineColor = QColor(QtCore.Qt.yellow) 94 | selection.format.setBackground(lineColor) 95 | selection.cursor = QtGui.QTextCursor(self.textWidget.document()) 96 | selection.find(self.window().findBar.text()) 97 | ''' 98 | self.textWidget.setExtraSelections(extras) 99 | return True 100 | 101 | def lineWidth(self, count=0): 102 | """ Calculate the width of the widget based on the block count. 103 | 104 | :Parameters: 105 | count : `int` 106 | Block count. Defaults to current block count. 107 | """ 108 | if self._hiddenByUser: 109 | return 0 110 | blocks = str(count or self.blockCount()) 111 | try: 112 | # horizontalAdvance added in Qt 5.11. 113 | return 6 + self.fontMetrics().horizontalAdvance(blocks) 114 | except AttributeError: 115 | # Obsolete in Qt 5. 116 | return 6 + self.fontMetrics().width(blocks) 117 | 118 | def mouseMoveEvent(self, event): 119 | """ Track mouse movement to select more lines if press is active. 120 | 121 | :Parameters: 122 | event : `QMouseEvent` 123 | Mouse move event 124 | """ 125 | if event.buttons() != QtCore.Qt.LeftButton: 126 | event.accept() 127 | return 128 | 129 | cursor = self.textWidget.textCursor() 130 | cursor2 = self.textWidget.cursorForPosition(event.pos()) 131 | new = cursor2.position() 132 | if new == self._movePos: 133 | event.accept() 134 | return 135 | 136 | cursor.setPosition(self._movePos) 137 | if new > self._movePos: 138 | cursor.movePosition(cursor.StartOfLine) 139 | cursor2.movePosition(cursor2.EndOfLine) 140 | else: 141 | cursor.movePosition(cursor.EndOfLine) 142 | cursor2.movePosition(cursor2.StartOfLine) 143 | cursor.setPosition(cursor2.position(), cursor.KeepAnchor) 144 | self.textWidget.setTextCursor(cursor) 145 | event.accept() 146 | 147 | def mousePressEvent(self, event): 148 | """ Select the line that was clicked. If moved while pressed, select 149 | multiple lines as the mouse moves. 150 | 151 | :Parameters: 152 | event : `QMouseEvent` 153 | Mouse press event 154 | """ 155 | if event.buttons() != QtCore.Qt.LeftButton: 156 | event.accept() 157 | return 158 | 159 | cursor = self.textWidget.cursorForPosition(event.pos()) 160 | cursor.select(cursor.LineUnderCursor) 161 | 162 | # Allow Shift-selecting lines from the previous selection to new position. 163 | if self.textWidget.textCursor().hasSelection() and event.modifiers() == QtCore.Qt.ShiftModifier: 164 | cursor2 = self.textWidget.textCursor() 165 | self._movePos = cursor2.position() 166 | start = min(cursor.selectionStart(), cursor2.selectionStart()) 167 | end = max(cursor.selectionEnd(), cursor2.selectionEnd()) 168 | cursor.setPosition(start) 169 | cursor.setPosition(end, cursor.KeepAnchor) 170 | else: 171 | self._movePos = cursor.position() 172 | 173 | self.textWidget.setTextCursor(cursor) 174 | event.accept() 175 | 176 | def onEditorResize(self): 177 | """ Adjust line numbers size if the text widget is resized. 178 | """ 179 | cr = self.textWidget.contentsRect() 180 | self.setGeometry(QRect(cr.left(), cr.top(), self.lineWidth(), cr.height())) 181 | 182 | def paintEvent(self, event): 183 | """ Draw the visible line numbers. 184 | """ 185 | painter = QPainter(self) 186 | painter.fillRect(event.rect(), QColor(QtCore.Qt.darkGray).darker(300) if self.window().isDarkTheme() \ 187 | else QtCore.Qt.lightGray) 188 | 189 | textWidget = self.textWidget 190 | currBlock = textWidget.document().findBlock(textWidget.textCursor().position()) 191 | 192 | block = textWidget.firstVisibleBlock() 193 | blockNumber = block.blockNumber() + 1 194 | geo = textWidget.blockBoundingGeometry(block).translated(textWidget.contentOffset()) 195 | top = round(geo.top()) 196 | bottom = round(geo.bottom()) 197 | width = self.width() - 3 # 3 is magic padding number 198 | height = round(geo.height()) 199 | flags = QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter 200 | font = painter.font() 201 | 202 | # Shrink the line numbers if we zoom out so numbers don't overlap, but don't increase the size, since we don't 203 | # (yet) account for that in this widget's width, leading to larger numbers cutting off the leading digits. 204 | size = max(1, min(width, height - 3)) 205 | if size < font.pointSize(): 206 | font.setPointSize(size) 207 | 208 | while block.isValid() and top <= event.rect().bottom(): 209 | if block.isVisible() and bottom >= event.rect().top(): 210 | # Make the line number for the selected line bold. 211 | font.setBold(block == currBlock) 212 | painter.setFont(font) 213 | painter.drawText(0, top, width, height, flags, str(blockNumber)) 214 | 215 | block = block.next() 216 | top = bottom 217 | bottom = top + round(textWidget.blockBoundingRect(block).height()) 218 | blockNumber += 1 219 | 220 | def setVisible(self, visible): 221 | super(PlainTextLineNumbers, self).setVisible(visible) 222 | self._hiddenByUser = not visible 223 | self.updateLineWidth() 224 | 225 | def sizeHint(self): 226 | return QSize(self.lineWidth(), self.textWidget.height()) 227 | 228 | @Slot(QRect, int) 229 | def updateLineNumbers(self, rect, dY): 230 | """ Scroll the line numbers or repaint the visible numbers. 231 | """ 232 | if dY: 233 | self.scroll(0, dY) 234 | else: 235 | self.update(0, rect.y(), self.width(), rect.height()) 236 | if rect.contains(self.textWidget.viewport().rect()): 237 | self.updateLineWidth() 238 | 239 | @Slot(int) 240 | def updateLineWidth(self, count=0): 241 | """ Adjust display of text widget to account for the widget of the line numbers. 242 | 243 | :Parameters: 244 | count : `int` 245 | Block count of document. 246 | """ 247 | self.textWidget.setViewportMargins(self.lineWidth(count), 0, 0, 0) 248 | 249 | 250 | class LineNumbers(PlainTextLineNumbers): 251 | """ Line number widget for `QTextBrowser` and `QTextEdit` widgets. 252 | Currently does not support `QPlainTextEdit` widgets. 253 | """ 254 | def blockCount(self): 255 | return self.textWidget.document().blockCount() 256 | 257 | def connectSignals(self): 258 | """ Connect relevant `QTextBrowser` or `QTextEdit` signals. 259 | """ 260 | self.doc = self.textWidget.document() 261 | self.textWidget.verticalScrollBar().valueChanged.connect(self.update) 262 | self.textWidget.currentCharFormatChanged.connect(self.resizeAndUpdate) 263 | self.textWidget.cursorPositionChanged.connect(self.highlightCurrentLine) 264 | self.doc.blockCountChanged.connect(self.updateLineWidth) 265 | 266 | @Slot() 267 | def highlightCurrentLine(self): 268 | """ Make sure the active line number is redrawn in bold by calling update. 269 | """ 270 | if super(LineNumbers, self).highlightCurrentLine(): 271 | self.update() 272 | 273 | @Slot(QTextCharFormat) 274 | def resizeAndUpdate(self, *args): 275 | """ Resize bar if needed. 276 | """ 277 | self.updateLineWidth() 278 | super(LineNumbers, self).update() 279 | 280 | def paintEvent(self, event): 281 | """ Draw line numbers. 282 | """ 283 | painter = QPainter(self) 284 | painter.fillRect(event.rect(), QColor(QtCore.Qt.darkGray).darker(300) if self.window().isDarkTheme() \ 285 | else QtCore.Qt.lightGray) 286 | 287 | textWidget = self.textWidget 288 | doc = textWidget.document() 289 | vScrollPos = textWidget.verticalScrollBar().value() 290 | pageBtm = vScrollPos + textWidget.viewport().height() 291 | currBlock = doc.findBlock(textWidget.textCursor().position()) 292 | 293 | width = self.width() - 3 # 3 is magic padding number 294 | height = textWidget.fontMetrics().height() 295 | flags = QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter 296 | font = painter.font() 297 | 298 | # Shrink the line numbers if we zoom out so numbers don't overlap, but don't increase the size, since we don't 299 | # (yet) account for that in this widget's width, leading to larger numbers cutting off the leading digits. 300 | size = max(1, min(width, height - 3)) 301 | if size < font.pointSize(): 302 | font.setPointSize(size) 303 | 304 | # Find roughly the current top-most visible block. 305 | block = doc.begin() 306 | layout = doc.documentLayout() 307 | lineHeight = layout.blockBoundingRect(block).height() 308 | 309 | block = doc.findBlockByNumber(int(vScrollPos / lineHeight)) 310 | currLine = block.blockNumber() 311 | 312 | while block.isValid(): 313 | currLine += 1 314 | 315 | # Check if the position of the block is outside the visible area. 316 | yPos = layout.blockBoundingRect(block).topLeft().y() 317 | if yPos > pageBtm: 318 | break 319 | 320 | # Make the line number for the selected line bold. 321 | font.setBold(block == currBlock) 322 | painter.setFont(font) 323 | painter.drawText(0, yPos - vScrollPos, width, height, flags, str(currLine)) 324 | 325 | # Go to the next block. 326 | block = block.next() 327 | -------------------------------------------------------------------------------- /usdmanager/find_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 443 10 | 242 11 | 12 | 13 | 14 | Find/Replace 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 0 24 | 25 | 26 | 27 | 28 | 9 29 | 30 | 31 | 32 | 33 | 34 | 35 | Search &backwards 36 | 37 | 38 | 39 | 40 | 41 | 42 | &Case sensitive 43 | 44 | 45 | 46 | 47 | 48 | 49 | &Whole words only 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | Replace 59 | 60 | 61 | 62 | 63 | 64 | 65 | Find 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 0 74 | 0 75 | 76 | 77 | 78 | Replace: 79 | 80 | 81 | replaceLineEdit 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 0 90 | 0 91 | 92 | 93 | 94 | Find: 95 | 96 | 97 | findLineEdit 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 0 106 | 0 107 | 108 | 109 | 110 | Options: 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | false 120 | 121 | 122 | 123 | 0 124 | 0 125 | 126 | 127 | 128 | 129 | 0 130 | 34 131 | 132 | 133 | 134 | 135 | 16777215 136 | 34 137 | 138 | 139 | 140 | &Find 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 20 150 | 20 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | false 159 | 160 | 161 | 162 | 0 163 | 34 164 | 165 | 166 | 167 | 168 | 16777215 169 | 34 170 | 171 | 172 | 173 | &Replace 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | false 186 | 187 | 188 | 189 | 0 190 | 34 191 | 192 | 193 | 194 | 195 | 16777215 196 | 34 197 | 198 | 199 | 200 | Replace && Fi&nd 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 0 209 | 0 210 | 211 | 212 | 213 | 214 | 0 215 | 34 216 | 217 | 218 | 219 | 220 | 16777215 221 | 34 222 | 223 | 224 | 225 | QDialogButtonBox::Close 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | false 237 | 238 | 239 | 240 | 0 241 | 34 242 | 243 | 244 | 245 | 246 | 16777215 247 | 34 248 | 249 | 250 | 251 | Replace &All 252 | 253 | 254 | 255 | 256 | 257 | 258 | false 259 | 260 | 261 | 262 | 0 263 | 34 264 | 265 | 266 | 267 | 268 | 16777215 269 | 34 270 | 271 | 272 | 273 | Replace All in &Open Files 274 | 275 | 276 | 277 | 278 | 279 | 280 | Qt::Horizontal 281 | 282 | 283 | 284 | 40 285 | 20 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 0 295 | 0 296 | 297 | 298 | 299 | 300 | 0 301 | 34 302 | 303 | 304 | 305 | 306 | 16777215 307 | 34 308 | 309 | 310 | 311 | Qt::Vertical 312 | 313 | 314 | QDialogButtonBox::Close 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | Qt::Vertical 326 | 327 | 328 | 329 | 20 330 | 40 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | findLineEdit 339 | replaceLineEdit 340 | caseSensitiveCheck 341 | searchBackwardsCheck 342 | wholeWordsCheck 343 | findBtn 344 | replaceBtn 345 | replaceFindBtn 346 | replaceAllBtn 347 | replaceAllOpenBtn 348 | buttonBox 349 | 350 | 351 | 352 | 353 | buttonBox 354 | accepted() 355 | Dialog 356 | accept() 357 | 358 | 359 | 398 360 | 221 361 | 362 | 363 | 157 364 | 204 365 | 366 | 367 | 368 | 369 | buttonBox 370 | rejected() 371 | Dialog 372 | reject() 373 | 374 | 375 | 398 376 | 221 377 | 378 | 379 | 286 380 | 204 381 | 382 | 383 | 384 | 385 | findLineEdit 386 | returnPressed() 387 | findBtn 388 | click() 389 | 390 | 391 | 222 392 | 42 393 | 394 | 395 | 338 396 | 42 397 | 398 | 399 | 400 | 401 | replaceLineEdit 402 | returnPressed() 403 | findBtn 404 | click() 405 | 406 | 407 | 243 408 | 109 409 | 410 | 411 | 308 412 | 45 413 | 414 | 415 | 416 | 417 | buttonBox2 418 | accepted() 419 | Dialog 420 | accept() 421 | 422 | 423 | 366 424 | 154 425 | 426 | 427 | 209 428 | 114 429 | 430 | 431 | 432 | 433 | buttonBox2 434 | rejected() 435 | Dialog 436 | reject() 437 | 438 | 439 | 366 440 | 154 441 | 442 | 443 | 209 444 | 114 445 | 446 | 447 | 448 | 449 | 450 | -------------------------------------------------------------------------------- /usdmanager/highlighter.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 DreamWorks Animation L.L.C. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | """ 17 | Custom syntax highlighters. 18 | """ 19 | 20 | import inspect 21 | import re 22 | 23 | from Qt import QtCore, QtGui 24 | 25 | from .constants import LINE_CHAR_LIMIT 26 | from .utils import findModules 27 | 28 | 29 | # Enabled when running in a theme with a dark background color. 30 | DARK_THEME = False 31 | 32 | 33 | def createRule(pattern, color=None, darkColor=None, weight=None, italic=False, cs=QtCore.Qt.CaseSensitive): 34 | """ Create a single-line syntax highlighting rule. 35 | 36 | :Parameters: 37 | pattern : `str` 38 | RegEx to match 39 | color : `QtGui.QColor` 40 | Color to highlight matches when in a light background theme 41 | darkColor : `QtGui.QColor` 42 | Color to highlight matches when in a dark background theme. 43 | Defaults to color if not given. 44 | weight : `int` | None 45 | Optional font weight for matches 46 | italic : `bool` 47 | Set the font to italic 48 | cs : `int` 49 | Case sensitivity for RegEx matching 50 | :Returns: 51 | Tuple of `QtCore.QRegExp` and `QtGui.QTextCharFormat` objects. 52 | :Rtype: 53 | tuple 54 | """ 55 | frmt = QtGui.QTextCharFormat() 56 | if DARK_THEME and darkColor is not None: 57 | frmt.setForeground(darkColor) 58 | elif color is not None: 59 | frmt.setForeground(color) 60 | if weight is not None: 61 | frmt.setFontWeight(weight) 62 | if italic: 63 | frmt.setFontItalic(True) 64 | return QtCore.QRegExp(pattern, cs), frmt 65 | 66 | 67 | def createMultilineRule(startPattern, endPattern, color=None, darkColor=None, weight=None, italic=False, cs=QtCore.Qt.CaseSensitive): 68 | """ Create a multiline syntax highlighting rule. 69 | 70 | :Parameters: 71 | startPattern : `str` 72 | RegEx to match for the start of the block of lines. 73 | endPattern : `str` 74 | RegEx to match for the end of the block of lines. 75 | color : `QtGui.QColor` 76 | Color to highlight matches 77 | darkColor : `QtGui.QColor` 78 | Color to highlight matches when in a dark background theme. 79 | weight : `int` | None 80 | Optional font weight for matches 81 | italic : `bool` 82 | Set the font to italic 83 | cs : `int` 84 | Case sensitivity for RegEx matching 85 | :Returns: 86 | Tuple of `QtCore.QRegExp` and `QtGui.QTextCharFormat` objects. 87 | :Rtype: 88 | tuple 89 | """ 90 | start, frmt = createRule(startPattern, color, darkColor, weight, italic, cs) 91 | end = QtCore.QRegExp(endPattern, cs) 92 | return start, end, frmt 93 | 94 | 95 | def findHighlighters(): 96 | """ Get the installed highlighter classes. 97 | 98 | :Returns: 99 | List of `MasterHighlighter` objects 100 | :Rtype: 101 | `list` 102 | """ 103 | # Find all available "MasterHighlighter" subclasses within the highlighters module. 104 | classes = [] 105 | for module in findModules("highlighters"): 106 | for _, cls in inspect.getmembers(module, lambda x: inspect.isclass(x) and issubclass(x, MasterHighlighter)): 107 | classes.append(cls) 108 | return classes 109 | 110 | 111 | class MasterHighlighter(QtCore.QObject): 112 | """ Master object containing shared highlighting rules. 113 | """ 114 | dirtied = QtCore.Signal() 115 | 116 | # List of file extensions (without the starting '.') to register this 117 | # highlighter for. The MasterHighlighter class is explicity set to [None] 118 | # as the default highlighter when a matching file extension is not found. 119 | extensions = [None] 120 | 121 | # Character(s) to start a single-line comment, or None for no comment support. 122 | comment = "#" 123 | 124 | # Tuple of start and end strings for a multiline comment (e.g. ("--[[", "]]") for Lua), 125 | # or None for no multiline comment support. 126 | multilineComment = None 127 | 128 | def __init__(self, parent, enableSyntaxHighlighting=False, programs=None): 129 | """ Initialize the master highlighter, used once per language and shared among tabs. 130 | 131 | :Parameters: 132 | parent : `QtCore.QObject` 133 | Can install to a `QTextEdit` or `QTextDocument` to apply highlighting. 134 | enableSyntaxHighlighting : `bool` 135 | Whether or not to enable syntax highlighting. 136 | programs : `dict` 137 | extension: program pairs of strings. This is used to contruct a syntax rule 138 | to undo syntax highlighting on links so that we see their original colors. 139 | """ 140 | super(MasterHighlighter, self).__init__(parent) 141 | 142 | # Highlighting rules. Rules farther down take priority. 143 | self.highlightingRules = [] 144 | self.multilineRules = [] 145 | self.rules = [] 146 | 147 | # Match everything for clearing syntax highlighting. 148 | self.blankRules = [createRule(".+")] 149 | self.enableSyntax = None 150 | self.findPhrase = None 151 | 152 | # Undo syntax highlighting on at least some of our links so the assigned colors show. 153 | self.ruleLink = createRule("*") 154 | self.highlightingRules.append(self.ruleLink) 155 | self.setLinkPattern(programs or {}, dirty=False) 156 | 157 | # Some general single-line rules that apply to many file formats. 158 | # Numeric literals 159 | self.ruleNumber = [ 160 | r'\b[+-]?(?:[0-9]+[lL]?|0[xX][0-9A-Fa-f]+[lL]?|[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?)\b', 161 | QtCore.Qt.darkBlue, 162 | QtCore.Qt.cyan 163 | ] 164 | # Double-quoted string, possibly containing escape sequences. 165 | self.ruleDoubleQuote = [ 166 | r'"[^"\\]*(?:\\.[^"\\]*)*"', 167 | QtCore.Qt.darkGreen, 168 | QtGui.QColor(25, 255, 25) 169 | ] 170 | # Single-quoted string, possibly containing escape sequences. 171 | self.ruleSingleQuote = [ 172 | r"'[^'\\]*(?:\\.[^'\\]*)*'", 173 | QtCore.Qt.darkGreen, 174 | QtGui.QColor(25, 255, 25) 175 | ] 176 | # Matches a comment from the starting point to the end of the line, 177 | # if not part of a single- or double-quoted string. 178 | if self.comment: 179 | self.ruleComment = [ 180 | "^(?:[^\"']|\"[^\"]*\"|'[^']*')*(" + re.escape(self.comment) + ".*)$", # TODO: This should probably be language-specific instead of assumed for all. 181 | QtCore.Qt.gray, 182 | QtCore.Qt.gray, 183 | None, # Not bold 184 | True # Italic 185 | ] 186 | 187 | # Create the rules specific to this syntax. 188 | self.createRules() 189 | 190 | # If createRules didn't place the link rule in a specific place, put it at the end. 191 | if self.ruleLink not in self.highlightingRules: 192 | self.highlightingRules.append(self.ruleLink) 193 | 194 | self.setSyntaxHighlighting(enableSyntaxHighlighting) 195 | 196 | def getRules(self): 197 | """ Syntax rules specific to this highlighter class. 198 | """ 199 | # Operators. 200 | return [ 201 | [ # Operators 202 | r'[\-+*/%=!<>&|^~]', 203 | QtCore.Qt.red, 204 | QtGui.QColor("#F33") 205 | ], 206 | self.ruleNumber, 207 | self.ruleDoubleQuote, 208 | self.ruleSingleQuote, 209 | self.ruleLink, # Undo syntax highlighting on at least some of our links so the assigned colors show. 210 | self.ruleComment 211 | ] 212 | 213 | def createRules(self): 214 | for r in self.getRules(): 215 | self.highlightingRules.append(createRule(*r) if type(r) is list else r) 216 | 217 | # Multi-line comment. 218 | if self.multilineComment: 219 | self.multilineRules.append(createMultilineRule( 220 | # Make sure the start of the comment isn't inside a single- or double-quoted string. 221 | # TODO: This should probably be language-specific instead of assumed for all. 222 | "^(?:[^\"']|\"[^\"]*\"|'[^']*')*(" + re.escape(self.multilineComment[0]) + ")", 223 | re.escape(self.multilineComment[1]), 224 | QtCore.Qt.gray, 225 | italic=True)) 226 | 227 | def dirty(self): 228 | """ Let highlighters that subscribe to this know a rule has changed. 229 | """ 230 | self.dirtied.emit() 231 | 232 | def setLinkPattern(self, programs, dirty=True): 233 | """ Set the rules to search for files based on file extensions, quotes, etc. 234 | 235 | :Parameters: 236 | programs : `dict` 237 | extension: program pairs of strings. 238 | dirty : `bool` 239 | If we should trigger a rehighlight or not. 240 | """ 241 | # This is slightly different than the main program's RegEx because Qt doesn't support all the same things. 242 | # TODO: Not allowing a backslash here might break Windows file paths if/when we try to support that. 243 | self.ruleLink[0].setPattern(r'(?:[^\'"@()\t\n\r\f\v\\]*\.)(?:' + '|'.join(programs.keys()) + r')(?=(?:[\'")@]|\\\"))') 244 | if dirty: 245 | self.dirty() 246 | 247 | def setSyntaxHighlighting(self, enable, force=True): 248 | """ Enable/Disable syntax highlighting. 249 | If enabling, dirties the state of this highlighter so highlighting runs again. 250 | 251 | :Parameters: 252 | enable : `bool` 253 | Whether or not to enable syntax highlighting. 254 | force : `bool` 255 | Force re-enabling syntax highlighting even if it was already enabled. 256 | Allows force rehighlighting even if nothing has really changed. 257 | """ 258 | if force or enable != self.enableSyntax: 259 | self.enableSyntax = enable 260 | self.rules = self.highlightingRules if enable else self.blankRules 261 | self.dirty() 262 | 263 | 264 | class Highlighter(QtGui.QSyntaxHighlighter): 265 | masterClass = MasterHighlighter 266 | 267 | def __init__(self, parent=None, master=None): 268 | """ Syntax highlighter for an individual document in the app. 269 | 270 | :Parameters: 271 | parent : `QtCore.QObject` 272 | Can install to a `QTextEdit` or `QTextDocument` to apply highlighting. 273 | master : `MasterHighlighter` | None 274 | Master object containing shared highlighting rules. 275 | """ 276 | super(Highlighter, self).__init__(parent) 277 | self.master = master or self.masterClass(self) 278 | self.findPhrase = None 279 | self.dirty = False 280 | 281 | # Connect this directly to self.rehighlight if we can ever manage to thread or speed that up. 282 | self.master.dirtied.connect(self.setDirty) 283 | 284 | def isDirty(self): 285 | return self.dirty 286 | 287 | def setDirty(self): 288 | self.dirty = True 289 | 290 | def highlightBlock(self, text): 291 | """ Override this method only if needed for a specific language. """ 292 | # Really long lines like timeSamples in Crate files don't play nicely with RegEx. 293 | # Skip them for now. 294 | if len(text) > LINE_CHAR_LIMIT: 295 | # TODO: Do we need to reset the block state or anything else here? 296 | return 297 | 298 | # Reduce name lookups for speed, since this is one of the slowest parts of the app. 299 | setFormat = self.setFormat 300 | currentBlockState = self.currentBlockState 301 | setCurrentBlockState = self.setCurrentBlockState 302 | previousBlockState = self.previousBlockState 303 | 304 | for pattern, frmt in self.master.rules: 305 | i = pattern.indexIn(text) 306 | while i >= 0: 307 | # If we have a grouped match, only highlight that first group and not the chars before it. 308 | pos1 = pattern.pos(1) 309 | if pos1 != -1: 310 | length = pattern.matchedLength() - (pos1 - i) 311 | i = pos1 312 | else: 313 | length = pattern.matchedLength() 314 | setFormat(i, length, frmt) 315 | i = pattern.indexIn(text, i + length) 316 | 317 | setCurrentBlockState(0) 318 | for state, (startExpr, endExpr, frmt) in enumerate(self.master.multilineRules, 1): 319 | if previousBlockState() == state: 320 | # We're already inside a match for this rule. See if there's an ending match. 321 | startIndex = 0 322 | add = 0 323 | else: 324 | # Look for the start of the expression. 325 | startIndex = startExpr.indexIn(text) 326 | # If we have a grouped match, only highlight that first group and not the chars before it. 327 | pos1 = startExpr.pos(1) 328 | if pos1 != -1: 329 | add = startExpr.matchedLength() - (pos1 - startIndex) 330 | startIndex = pos1 331 | else: 332 | add = startExpr.matchedLength() 333 | 334 | # If we're inside the match, look for the end expression. 335 | while startIndex >= 0: 336 | endIndex = endExpr.indexIn(text, startIndex + add) 337 | if endIndex >= add: 338 | # We found the end of the multiline rule. 339 | length = endIndex - startIndex + add + endExpr.matchedLength() 340 | # Since we're at the end of this rule, reset the state so other multiline rules can try to match. 341 | setCurrentBlockState(0) 342 | else: 343 | # Still inside the multiline rule. 344 | length = len(text) - startIndex + add 345 | setCurrentBlockState(state) 346 | 347 | # Highlight the portion of this line that's inside the multiline rule. 348 | # TODO: This doesn't actually ensure we hit the closing expression before highlighting. 349 | setFormat(startIndex, length, frmt) 350 | 351 | # Look for the next match. 352 | startIndex = startExpr.indexIn(text, startIndex + length) 353 | pos1 = startExpr.pos(1) 354 | if pos1 != -1: 355 | add = startExpr.matchedLength() - (pos1 - startIndex) 356 | startIndex = pos1 357 | else: 358 | add = startExpr.matchedLength() 359 | 360 | if currentBlockState() == state: 361 | break 362 | 363 | self.dirty = False 364 | -------------------------------------------------------------------------------- /usdmanager/usdviewstyle.qss: -------------------------------------------------------------------------------- 1 | /** 2 | * GENERAL CSS STYLE RULES 3 | * Copied with slight modifications from usdview 4 | * https://github.com/PixarAnimationStudios/USD/blob/release/pxr/usdImaging/usdviewq/usdviewstyle.qss 5 | */ 6 | 7 | /* *** QWidget *** 8 | * Base style for all QWidgets 9 | */ 10 | 11 | QWidget { 12 | color: rgb(227, 227, 227); 13 | 14 | /* brownish background color */ 15 | background: rgb(56, 56, 56); 16 | selection-background-color: rgb(189, 155, 84); 17 | } 18 | 19 | /* Default disabled font color for all widgets */ 20 | QWidget:disabled { 21 | color: rgb(122, 122, 122); 22 | } 23 | 24 | /* *** QStatusBar *** 25 | * Font color for status bar 26 | */ 27 | QStatusBar { 28 | color: rgb(222, 158, 46) 29 | } 30 | 31 | /* *** QGroupBox *** 32 | * Base style for QGroupBox 33 | */ 34 | QGroupBox { 35 | border: 1px solid rgb(26, 26, 26); /* thin black border */ 36 | border-radius: 5px; /* rounded */ 37 | margin-top: 1ex; /* leave space at the top for the title */ 38 | } 39 | 40 | /* Position to title of the QGroupBox */ 41 | QGroupBox::title { 42 | subcontrol-position: top center; 43 | subcontrol-origin: margin; /* vertical position */ 44 | padding: 0px 3px; /* cover the border around the title */ 45 | } 46 | 47 | /* *** QAbstractSpinBox *** 48 | * Base style for QAbstractSpinBox 49 | * This is the widget that allows users to select a value 50 | * and provides up/down arrows to adjust it. We configure QAbstractSpinBox 51 | * because we use both QDoubleSpinBox and QSpinBox 52 | */ 53 | QAbstractSpinBox { 54 | background: rgb(34, 34, 34); 55 | padding: 2px; /* make it a little bigger */ 56 | border-radius: 7px; /* make it very round like in presto */ 57 | border-top: 2px solid rgb(19,19,19); /* thick black border on top */ 58 | border-left: 2px solid rgb(19,19,19); /* and on the left */ 59 | } 60 | 61 | /* Common style for the up and down buttons */ 62 | QAbstractSpinBox::up-button, QAbstractSpinBox::down-button { 63 | background: rgb(42, 42, 42); 64 | margin-right: -1px; /* Move to the right a little */ 65 | } 66 | 67 | /* Darken the background when button pressed down */ 68 | QAbstractSpinBox::up-button:pressed, QAbstractSpinBox::down-button:pressed { 69 | background: rgb(34, 34, 34); 70 | } 71 | 72 | /* Round the outside of the button like in presto */ 73 | QAbstractSpinBox::up-button { 74 | margin-top: -3px; /* move higher to align */ 75 | border-top-right-radius: 7px; 76 | } 77 | 78 | /* Round the outside of the button like in presto */ 79 | QAbstractSpinBox::down-button { 80 | margin-bottom: -3px; /* move lower to align */ 81 | border-bottom-right-radius: 7px; 82 | } 83 | 84 | /* Adjust size and color of both arrows (inside buttons) */ 85 | QAbstractSpinBox::up-arrow, 86 | QAbstractSpinBox::down-arrow, 87 | QComboBox::down-arrow { 88 | width: 6px; 89 | height: 3px; 90 | background: rgb(227, 227, 227); 91 | } 92 | 93 | /* Set the disabled color for the arrows */ 94 | QAbstractSpinBox::up-arrow:disabled, 95 | QAbstractSpinBox::down-arrow:disabled, 96 | QComboBox::down-arrow:disabled { 97 | background: rgb(88, 88, 88); 98 | } 99 | 100 | /* Shape the up arrow */ 101 | QAbstractSpinBox::up-arrow { 102 | border-top-right-radius: 3px; /* round upper left and upper right */ 103 | border-top-left-radius: 3px; /* to form a triangle-ish shape */ 104 | border-bottom: 1px solid rgb(122, 122, 122); /* decorative */ 105 | } 106 | 107 | 108 | /* Shape the down arrow */ 109 | QAbstractSpinBox::down-arrow, 110 | QComboBox::down-arrow{ 111 | border-bottom-right-radius: 3px; /* round lower right and lower left */ 112 | border-bottom-left-radius: 3px; /* to form a triangle-ish shape */ 113 | border-top: 1px solid rgb(122, 122, 122); /* decorative */ 114 | } 115 | 116 | /* *** QTextEdit *** 117 | * base style for QTextEdit 118 | */ 119 | 120 | /* font color for QTextEdit, QLineEdit and QAbstractSpinBox */ 121 | QTextEdit, QPlainTextEdit, QAbstractSpinBox, QlineEdit{ 122 | color: rgb(227, 227, 227); 123 | } 124 | 125 | 126 | /* Border for QLineEdit as well as checkboxes and other widgets. */ 127 | QTextEdit, QPlainTextEdit, QLineEdit, QGraphicsView, Line{ 128 | border: 2px solid rgb(47, 47, 47); 129 | } 130 | 131 | QCheckBox::indicator { 132 | border: 1px solid rgb(26, 26, 26); 133 | } 134 | 135 | /* Normal background for QLineEdit and checkboxes */ 136 | QLineEdit, QCheckBox::indicator { 137 | background: rgb(58, 58, 58); 138 | border-radius: 3px; 139 | } 140 | 141 | /* Disabled font color and background for QLineEdits */ 142 | QLineEdit:disabled, QSlider::groove:horizontal:disabled { 143 | background: rgb(50, 50, 50); 144 | } 145 | 146 | /* Orange border for QLineEdit and QCheckBox when focused/hovering */ 147 | QLineEdit:focus { 148 | border: 2px solid rgb(163, 135, 78); 149 | } 150 | 151 | QCheckBox::indicator:hover { 152 | border: 1px solid rgb(163, 135, 78); 153 | } 154 | 155 | /* *** QCheckBox *** 156 | /* Make the checkbox orange when checked 157 | */ 158 | QCheckBox::indicator:checked { 159 | background: rgb(229, 162, 44); 160 | } 161 | 162 | /* Size of the checkbox */ 163 | QCheckBox::indicator { 164 | width : 12px; 165 | height: 12px; 166 | } 167 | 168 | /* *** QSplitter *** 169 | * Color the UI splitter lines 170 | */ 171 | QSplitter::handle { 172 | background-color: rgb(32, 32, 32); 173 | } 174 | 175 | /* Balance between making the splitters easier to find/grab, and 176 | * clean use of space. */ 177 | QSplitter::handle:horizontal { 178 | width: 4px; 179 | } 180 | 181 | QSplitter::handle:vertical { 182 | height: 4px; 183 | } 184 | 185 | /* Override the backround for labels, make them transparent */ 186 | QLabel { 187 | background: none; 188 | } 189 | 190 | /* *** QPushButton *** 191 | * Main Style for QPushButton 192 | */ 193 | QPushButton{ 194 | /* gradient background */ 195 | background-color: QLinearGradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 rgb(88, 88, 88), stop: 1 rgb(75, 75, 75)); 196 | 197 | /* thin dark round border */ 198 | border-width: 1px; 199 | border-color: rgb(42, 42, 42); 200 | border-style: solid; 201 | border-radius: 3; 202 | 203 | /* give the text enough space */ 204 | padding: 3px; 205 | padding-right: 10px; 206 | padding-left: 10px; 207 | } 208 | 209 | /* Darker gradient when the button is pressed down */ 210 | QPushButton:pressed, QToolButton:pressed { 211 | background-color: QLinearGradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 rgb(65, 65, 65), stop: 1 rgb(75, 75, 75)); 212 | } 213 | 214 | /* Greyed-out colors when the button is disabled */ 215 | QPushButton:disabled, QToolButton:disabled { 216 | background-color: QLinearGradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 rgb(66, 66, 66), stop: 1 rgb(56, 56, 56)); 217 | } 218 | 219 | /* *** QToolButton *** 220 | * Main Style for QToolButton 221 | */ 222 | QToolButton{ 223 | /* gradient background */ 224 | color: rgb(42, 42, 42); 225 | background-color: rgb(100, 100, 100); 226 | 227 | /* thin dark round border */ 228 | border-width: 1px; 229 | border-color: rgb(42, 42, 42); 230 | border-style: solid; 231 | border-radius: 8; 232 | 233 | padding: 0px 1px 0px 1px; 234 | } 235 | 236 | /* *** QTreeView, QTableView *** 237 | * Style the tree view and table view 238 | */ 239 | QTreeView::item, QTableView::item { 240 | /* this border serves to separate the columns 241 | * since the grid is often invised */ 242 | border-right: 1px solid rgb(41, 41, 41); 243 | 244 | padding-top: 1px; 245 | padding-bottom: 1px; 246 | } 247 | 248 | /* Selected items highlighted in orange */ 249 | .QTreeWidget::item:selected, 250 | QTreeView::branch:selected, 251 | QTableView::item:selected { 252 | background: rgb(189, 155, 84); 253 | } 254 | 255 | /* hover items a bit lighter */ 256 | .QTreeWidget::item:hover:!pressed:!selected, 257 | QTreeView::branch:hover:!pressed:!selected, 258 | QTableView::item:hover:!pressed:!selected { 259 | background: rgb(70, 70, 70); 260 | } 261 | 262 | .QTreeWidget::item:hover:!pressed:selected, 263 | QTreeView::branch:hover:!pressed:selected, 264 | QTableView::item:hover:!pressed:selected { 265 | /* background: rgb(132, 109, 59); */ 266 | background: rgb(227, 186, 101); 267 | } 268 | 269 | /* give the tables and trees an alternating dark/clear blue background */ 270 | QTableView, QTableWidget, QTreeWidget { 271 | background: rgb(55, 55, 55); 272 | alternate-background-color: rgb(59, 59, 59); 273 | } 274 | 275 | /* bump to the right to hide the extra line */ 276 | QTableWidget, QTreeWidget { 277 | margin-right: -1px; 278 | } 279 | 280 | /* *** QHeaderView *** 281 | * This style the headers for both QTreeView and QTableView 282 | */ 283 | QHeaderView::section { 284 | padding: 3px; 285 | border-right: 1px solid rgb(67, 67, 67); 286 | border-bottom: 1px solid rgb(42, 42, 42); 287 | 288 | border-top: none; 289 | border-left: none; 290 | 291 | /* clear blue color and darker background */ 292 | color: rgb(201, 199, 195); 293 | background: rgb(78, 80, 84); 294 | } 295 | 296 | /* *** QTabWidget *** 297 | * Style the tabs for the tab widget 298 | */ 299 | QTabWidget::tab-bar:top { 300 | left: 10px; /* move to the right by 5px */ 301 | top: 1px; 302 | } 303 | 304 | QTabWidget::tab-bar:left { 305 | right: 1px; 306 | } 307 | 308 | QTabWidget::top:pane { 309 | border: none; 310 | border-top: 1px solid rgb(42, 42, 42); 311 | } 312 | 313 | QTabBar { 314 | background: none; 315 | } 316 | 317 | /* Style the tab using the tab sub-control. Note that 318 | * it reads QTabBar _not_ QTabWidget */ 319 | QTabBar::tab:top { 320 | /* Gradient bg similar to pushbuttons */ 321 | background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 rgb(89, 89, 89), stop: 1.0 rgb(74, 74, 74)); 322 | 323 | /* Style the border */ 324 | border: 1px solid rgb(42, 42, 42); 325 | border-top-left-radius: 3px; 326 | border-top-right-radius: 3px; 327 | 328 | /* size properly */ 329 | min-width: 8ex; 330 | padding-left: 10px; 331 | padding-right: 10px; 332 | } 333 | 334 | QTabBar::tab:left { 335 | /* Gradient bg similar to pushbuttons */ 336 | background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 rgb(89, 89, 89), stop: 1.0 rgb(74, 74, 74)); 337 | 338 | /* Style the border */ 339 | border: 1px solid rgb(42, 42, 42); 340 | border-bottom-right-radius: 3px; 341 | border-top-right-radius: 3px; 342 | 343 | /* size properly */ 344 | min-height: 4ex; 345 | padding-top: 10px; 346 | padding-bottom: 10px; 347 | } 348 | 349 | /* Lighter background for the selected tab */ 350 | QTabBar::tab:selected, QTabBar::tab:hover { 351 | background: rgb(56, 56, 56); 352 | } 353 | 354 | /* make the seleted tab blend with the tab's container */ 355 | QTabBar::tab:top:selected { 356 | border-bottom: none; /* same as pane color */ 357 | } 358 | 359 | QTabBar::tab:left:selected { 360 | border-left: none; /* name as pane color */ 361 | } 362 | 363 | /* make non-selected tabs look smaller */ 364 | QTabBar::tab:top:!selected { 365 | margin-top: 2px; 366 | } 367 | 368 | QTabBar::tab:left:!selected { 369 | margin-right: 2px; 370 | } 371 | 372 | /* Set the disabled background color for slider handle and checkbox */ 373 | QCheckBox::indicator:checked:disabled { 374 | background: QLinearGradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 rgb(177, 161, 134), stop: 1 rgb(188, 165, 125)); 375 | } 376 | 377 | /* *** QScrollBar *** 378 | * Style the scroll bars menv30-style 379 | */ 380 | 381 | /* set up background and border (behind the handle) */ 382 | QScrollBar:horizontal, QScrollBar:vertical { 383 | background: rgb(42, 42, 42); 384 | border: 1px solid rgb(42, 42, 42); 385 | } 386 | 387 | /* Round the bottom corners behind a horizontal scrollbar */ 388 | QScrollBar:horizontal { 389 | border-bottom-right-radius: 12px; 390 | border-bottom-left-radius: 12px; 391 | } 392 | 393 | /* Round the right corners behind a vertical scrollbar */ 394 | QScrollBar:vertical { 395 | border-top-right-radius: 12px; 396 | border-bottom-right-radius: 12px; 397 | } 398 | 399 | /* set the color and border for the actual bar */ 400 | QScrollBar::handle:horizontal, QScrollBar::handle:vertical { 401 | background: rgb(90, 90, 90); 402 | border: 1px solid rgb(90, 90, 90); 403 | } 404 | 405 | /* Round the bottom corners for the horizontal scrollbar handle */ 406 | QScrollBar::handle:horizontal { 407 | border-bottom-right-radius: 12px; 408 | border-bottom-left-radius: 12px; 409 | border-top-color: rgb(126, 126, 126); 410 | min-width:45px; 411 | } 412 | 413 | /* Round the right corners for the vertical scrollbar handle */ 414 | QScrollBar::handle:vertical { 415 | border-top-right-radius: 12px; 416 | border-bottom-right-radius: 12px; 417 | border-left-color: rgb(126, 126, 126); 418 | min-height:45px; 419 | } 420 | 421 | /* Make the scroll bar arrows invisible */ 422 | QScrollBar:left-arrow:horizontal, QScrollBar::right-arrow:horizontal, 423 | QScrollBar:left-arrow:vertical, QScrollBar::right-arrow:vertical { 424 | background: transparent; 425 | } 426 | 427 | QScrollBar::add-line:horizontal, QScrollBar::add-line:vertical { 428 | background: transparent; 429 | } 430 | 431 | QScrollBar::sub-line:horizontal, QScrollBar::sub-line:vertical { 432 | background: transparent; 433 | } 434 | 435 | QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal, 436 | QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { 437 | background: none; 438 | } 439 | 440 | /* *** QMenuBar *** 441 | * Style the menu bars 442 | */ 443 | 444 | /* A bit bigger and brighter for main menu */ 445 | QMenuBar#menubar { 446 | background: rgb(80, 80, 80); 447 | border: 2px solid rgb(41, 41, 41); 448 | } 449 | 450 | /* Style the menu bar sections like presto, with rounded top corners */ 451 | QMenuBar::item { 452 | spacing: 6px; 453 | padding: 2px 5px; 454 | background: transparent; 455 | border-top-right-radius: 3px; 456 | border-top-left-radius: 3px; 457 | } 458 | 459 | QMenuBar::item:selected { /* when selected using mouse or keyboard */ 460 | background: rgb(59, 59, 59); 461 | } 462 | 463 | /* dark background when pressed */ 464 | QMenuBar::item:pressed { 465 | background: rgb(42, 42, 42); 466 | } 467 | 468 | /* *** QMenu *** 469 | * style the actual menu (when you click on a section in the menu bar) */ 470 | QMenu, 471 | QComboBox QAbstractItemView { 472 | /* dark border */ 473 | border: 2px solid rgb(19, 19, 19); 474 | } 475 | 476 | QMenu::item { 477 | /* Transparent menu item background because we want it to match 478 | * the menu's background color when not selected. 479 | */ 480 | background: none; 481 | } 482 | 483 | /* When user selects menu item using mouse or keyboard */ 484 | QMenu::item:selected { 485 | background: rgb(190, 156, 85); 486 | color: rgb(54, 54, 54); 487 | } 488 | 489 | /* Thin separator between menu sections */ 490 | QMenu::separator { 491 | height: 1px; 492 | background: rgb(42, 42, 42); 493 | } 494 | 495 | /* *** QComboBox *** 496 | * Style the drop-down menus 497 | * Note: The down arrow is style in the QSpinBox style 498 | */ 499 | QComboBox { 500 | color: rgb(227, 227, 227); /* Weird, if we dont specify, it's black */ 501 | height: 22px; 502 | background: rgb(41, 41, 41); 503 | border:none; 504 | border-radius: 5px; 505 | padding: 1px 0px 1px 3px; /*This makes text colour work*/ 506 | } 507 | 508 | QComboBox::drop-down { 509 | background: rgb(41, 41, 41); 510 | border:none; 511 | border-radius: 5px; 512 | } 513 | 514 | QToolTip { 515 | padding-left: 7px; 516 | padding-right: 7px; 517 | } 518 | 519 | /* End usdview styles. USD Manager specific changes below this. */ 520 | 521 | QToolBar { 522 | background-color: rgb(56, 56, 56); 523 | border-bottom: 1px solid rgb(35, 35, 35); /* Defining any border fixes an issue with background-color not working */ 524 | padding: 1px 1px 1px 2px; 525 | } 526 | 527 | QLineEdit#findBar { 528 | background-color:inherit; 529 | } 530 | 531 | AddressBar { 532 | background-color: rgb(41, 41, 41); 533 | } 534 | 535 | QStatusBar::item { 536 | border: 0px solid black 537 | } -------------------------------------------------------------------------------- /usdmanager/parser.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2020 DreamWorks Animation L.L.C. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | """ 17 | File parsers 18 | """ 19 | 20 | import logging 21 | import re 22 | import traceback 23 | from collections import defaultdict 24 | from xml.sax.saxutils import escape, unescape 25 | 26 | from Qt.QtCore import QFile, QFileInfo, QIODevice, QObject, QTextStream, Signal, Slot 27 | from Qt.QtGui import QIcon 28 | 29 | from .constants import LINE_CHAR_LIMIT, CHAR_LIMIT, FILE_FORMAT_NONE, HTML_BODY 30 | from .utils import expandPath 31 | 32 | 33 | # Set up logging. 34 | logger = logging.getLogger(__name__) 35 | logging.basicConfig() 36 | 37 | 38 | class PathCacheDict(defaultdict): 39 | """ Cache if file paths referenced more than once in a file exist, so we don't check on disk over and over. 40 | """ 41 | def __missing__(self, key): 42 | self[key] = QFile.exists(key) 43 | return self[key] 44 | 45 | 46 | class SaveFileError(Exception): 47 | """ Exception when saving files, where details can be used to provide the earlier traceback for the user in the 48 | error dialog's details section. 49 | """ 50 | def __init__(self, message, details=None): 51 | """ Initialize the exception. 52 | 53 | :Parameters: 54 | message : `str` 55 | Message 56 | details : `str` | None 57 | Optional traceback to accompany this message. 58 | """ 59 | super(SaveFileError, self).__init__(message) 60 | self.details = details 61 | 62 | 63 | class FileParser(QObject): 64 | """ Base class for RegEx-based file parsing. 65 | """ 66 | progress = Signal(int) 67 | status = Signal(str) 68 | 69 | # Override as needed. 70 | fileFormat = FILE_FORMAT_NONE 71 | lineCharLimit = LINE_CHAR_LIMIT 72 | 73 | # If the file format is binary or not (e.g. USD's crate format). 74 | binary = False 75 | 76 | # Optional icon to display in the tab bar when this file parser is used. 77 | icon = QIcon() 78 | 79 | # Group within the RegEx corresponding to the file path only. 80 | # Useful if you modify compile() but not linkParse(). 81 | RE_FILE_GROUP = 1 82 | 83 | def __init__(self, parent=None): 84 | """ Initialize the parser. 85 | 86 | :Parameters: 87 | parent : `QObject` 88 | Parent object (main window) 89 | """ 90 | super(FileParser, self).__init__(parent) 91 | 92 | # List of args to pass to addAction on the Commands menu. 93 | # Each item in the list represents a new menu item. 94 | self.plugins = [] 95 | 96 | self.regex = None 97 | self._stop = False 98 | self.cleanup() 99 | 100 | self.progress.connect(parent.setLoadingProgress) 101 | self.status.connect(parent.loadingProgressLabel.setText) 102 | parent.actionStop.triggered.connect(self.stopTriggered) 103 | parent.compileLinkRegEx.connect(self.compile) 104 | 105 | def acceptsFile(self, fileInfo, link): 106 | """ Determine if this parser can accept the incoming file. 107 | Note: Parsers check this in a non-deterministic order. Ensure multiple parsers don't accept the same file. 108 | 109 | Override in subclass to filter for files this parser can support. 110 | 111 | :Parameters: 112 | fileInfo : `QFileInfo` 113 | File info object 114 | link : `QtCore.QUrl` 115 | Full URL, potentially with query string 116 | :Returns: 117 | It the parser should be able to handle the file 118 | :Rtype: 119 | `bool` 120 | """ 121 | raise NotImplementedError 122 | 123 | def cleanup(self): 124 | """ Reset variables for a new file. 125 | 126 | Don't override. 127 | """ 128 | self.exists = PathCacheDict() 129 | self.html = "" 130 | self.text = [] 131 | self.truncated = False 132 | self.warning = None 133 | 134 | @Slot() 135 | def compile(self): 136 | """ Compile regular expression to find links based on the acceptable extensions stored in self.programs. 137 | 138 | Override for language-specific RegEx. 139 | 140 | NOTE: If this RegEx changes, the syntax highlighting rules may need to as well. 141 | """ 142 | exts = self.parent().programs.keys() 143 | self.regex = re.compile( 144 | r'(?:[\'"@]+)' # 1 or more single quote, double quote, or at symbol. 145 | r'(' # Group 1: Path. This is the main group we are looking for. Matches based on extension before the pipe, or variable after the pipe. 146 | r'[^\t\n\r\f\v\'"]*?' # 0 or more (greedy) non-whitespace characters (regular spaces are ok) and no quotes followed by a period, then 1 of the acceptable file extensions. 147 | r'\.(?:'+'|'.join(exts)+r')' # followed by a period, then 1 of the acceptable file extensions 148 | r'|\${[\w/${}:.-]+}' # One or more of these characters -- A-Za-z0-9_-/${}:. -- inside the variable curly brackets -- ${} 149 | r')' # end group 1 150 | r'(?:[\'"@]|\\\")' # 1 of: single quote, double quote, backslash followed by double quote, or at symbol. 151 | ) 152 | 153 | @staticmethod 154 | def generateTempFile(fileName, tmpDir=None): 155 | """ For file formats supporting ASCII and binary representations, generate a temporary ASCII file that the user can edit. 156 | 157 | :Parameters: 158 | fileName : `str` 159 | Binary file path 160 | tmpDir : `str` | None 161 | Temp directory to create the new file within 162 | :Returns: 163 | Temporary file name 164 | :Rtype: 165 | `str` 166 | """ 167 | raise NotImplementedError 168 | 169 | def parse(self, nativeAbsPath, fileInfo, link): 170 | """ Parse a file for links, generating a plain text version and HTML version of the file text. 171 | 172 | In general, don't override unless you need to add something before parsing really starts, and then just call 173 | super() for the rest of this method. 174 | 175 | :Parameters: 176 | nativeAbsPath : `str` 177 | OS-native absolute file path 178 | fileInfo : `QFileInfo` 179 | File info object 180 | link : `QUrl` 181 | Full file path URL 182 | """ 183 | self.cleanup() 184 | 185 | self.status.emit("Reading file") 186 | self.text = self.read(nativeAbsPath) 187 | 188 | # TODO: Figure out a better way to handle streaming text for large files like Crate geometry. 189 | # Large chunks of text (e.g. 2.2 billion characters) will cause Qt to segfault when creating a QString. 190 | length = len(self.text) 191 | if length > self.parent().preferences['lineLimit']: 192 | length = self.parent().preferences['lineLimit'] 193 | self.truncated = True 194 | self.text = self.text[:length] 195 | self.warning = "Extremely large file! Capping display at {:,d} lines. You can edit this cap in the "\ 196 | "Advanced tab of Preferences.".format(length) 197 | self.parent().loadingProgressBar.setMaximum(length) 198 | 199 | if self._stop: 200 | self.status.emit("Parsing text") 201 | logger.debug("Parsing text.") 202 | else: 203 | self.status.emit("Parsing text for links") 204 | logger.debug("Parsing text for links.") 205 | 206 | # Reduce name lookups for speed, since this is one of the slowest parts of the app. 207 | emit = self.progress.emit 208 | lineCharLimit = self.lineCharLimit 209 | finditer = self.regex.finditer 210 | re_file_group = self.RE_FILE_GROUP 211 | parseMatch = self.parseMatch 212 | 213 | html = "" 214 | # Escape HTML characters for proper display. 215 | # Do this before we add any actual HTML characters. 216 | lines = [escape(x) for x in self.text] 217 | for i, line in enumerate(lines): 218 | if self._stop: 219 | # If the user has requested to stop, load the rest of the document 220 | # without doing the expensive parsing for links. 221 | html += "".join(lines[i:]) 222 | break 223 | 224 | emit(i) 225 | if len(line) > lineCharLimit: 226 | html += self.parseLongLine(line) 227 | continue 228 | 229 | # Search for multiple, non-overlapping links on each line. 230 | offset = 0 231 | for m in finditer(line): 232 | # Since we had to escape all potential HTML-related characters before finding links, undo any replaced 233 | # by escape if part of the linkPath itself. URIs may have & as part of the path for query parameters. 234 | # We then have to re-escape the path before inserting it into HTML. 235 | linkPath = unescape(m.group(re_file_group)) 236 | start = m.start(re_file_group) 237 | end = m.end(re_file_group) 238 | try: 239 | href = parseMatch(m, linkPath, nativeAbsPath, fileInfo) 240 | except ValueError: 241 | # File doesn't exist or path cannot be resolved. 242 | # Color it red. 243 | href = '{}'.format(escape(linkPath)) 244 | # Calculate difference in length between new link and original text so that we know where 245 | # in the string to start the replacement when we have multiple matches in the same line. 246 | line = line[:start + offset] + href + line[end + offset:] 247 | offset += len(href) - end + start 248 | html += line 249 | 250 | logger.debug("Done parsing text for links.") 251 | if len(html) > CHAR_LIMIT: 252 | self.truncated = True 253 | html = html[:CHAR_LIMIT] 254 | self.warning = "Extremely large file! Capping display at {:,d} characters.".format(CHAR_LIMIT) 255 | 256 | # Wrap the final text in a proper HTML document. 257 | self.html = self.htmlFormat(html) 258 | 259 | def htmlFormat(self, text): 260 | """ Wrap the final text in a proper HTML document. 261 | 262 | Override to add additional HTML tags only to the HTML representation of this file. 263 | 264 | :Parameters: 265 | text : `str` 266 | :Returns: 267 | HTML text document 268 | :Rtype: 269 | `str` 270 | """ 271 | return HTML_BODY.format(text) 272 | 273 | def parseMatch(self, match, linkPath, nativeAbsPath, fileInfo): 274 | """ Parse a RegEx match of a patch to another file. 275 | 276 | Override for specific language parsing. 277 | 278 | :Parameters: 279 | match 280 | RegEx match object 281 | linkPath : `str` 282 | Displayed file path matched by the RegEx 283 | nativeAbsPath : `str` 284 | OS-native absolute file path for the file being parsed 285 | fileInfo : `QFileInfo` 286 | File info object for the file being parsed 287 | :Returns: 288 | HTML link 289 | :Rtype: 290 | `str` 291 | :Raises ValueError: 292 | If path does not exist or cannot be resolved. 293 | """ 294 | # linkPath = `str` displayed file path 295 | # fullPath = `str` absolute file path 296 | # Example: linkPath 297 | if QFileInfo(linkPath).isAbsolute(): 298 | fullPath = QFileInfo(expandPath(linkPath, nativeAbsPath)).absoluteFilePath() 299 | logger.debug("Parsed link is absolute (%s). Expanded to %s", linkPath, fullPath) 300 | else: 301 | # Relative path from the current file to the link. 302 | fullPath = fileInfo.dir().absoluteFilePath(expandPath(linkPath, nativeAbsPath)) 303 | logger.debug("Parsed link is relative (%s). Expanded to %s", linkPath, fullPath) 304 | 305 | # Make the HTML link. 306 | if self.exists[fullPath]: 307 | return '{}'.format(fullPath, escape(linkPath)) 308 | elif '*' in linkPath or '' in linkPath or '.#.' in linkPath: 309 | # Create an orange link for files with wildcards in the path, 310 | # designating zero or more files may exist. 311 | return '{}'.format( 312 | fullPath, escape(linkPath)) 313 | return '{}'.format(fullPath, escape(linkPath)) 314 | 315 | def parseLongLine(self, line): 316 | """ Process a long line. Link parsing is skipped by default for lines over a certain length. 317 | 318 | Override if desired, like truncating the display of a long array. 319 | 320 | :Parameters: 321 | line : `str` 322 | Line of text 323 | :Returns: 324 | Line of text 325 | :Rtype: 326 | `str` 327 | """ 328 | logger.debug("Skipping link parsing for long line") 329 | return line 330 | 331 | def read(self, path): 332 | """ 333 | :Parameters: 334 | path : `str` 335 | OS-native absolute file path 336 | :Returns: 337 | List of lines of text of file. 338 | Can be overridden by subclasses to handle things like crate conversion from binary to ASCII. 339 | :Rtype: 340 | [`str`] 341 | """ 342 | with open(path) as f: 343 | return f.readlines() 344 | 345 | def stop(self, stop=True): 346 | """ Request to stop parsing the active file for links. 347 | 348 | Don't override. 349 | 350 | :Parameters: 351 | stop : `bool` 352 | To stop or not 353 | """ 354 | self._stop = stop 355 | 356 | @Slot(bool) 357 | def stopTriggered(self, checked=False): 358 | """ Request to stop parsing the active file for links. 359 | 360 | Don't override. 361 | 362 | :Parameters: 363 | checked : `bool` 364 | For signal only 365 | """ 366 | self.stop() 367 | 368 | def write(self, qFile, filePath, tab, tmpDir): 369 | """ Write out a plain text file. 370 | 371 | :Parameters: 372 | qFile : `QtCore.QFile` 373 | Object representing the file to write to 374 | filePath : `str` 375 | File path to write to 376 | tab : `str` 377 | Tab being written 378 | tmpDir : `str` 379 | Temporary directory, if needed for any write operations. 380 | :Raises SaveFileError: 381 | If the file write fails. 382 | """ 383 | if not qFile.open(QIODevice.WriteOnly | QIODevice.Text): 384 | raise SaveFileError("The file could not be opened for saving!") 385 | 386 | try: 387 | out = QTextStream(qFile) 388 | _ = out << tab.textEditor.toPlainText() 389 | except Exception: 390 | raise SaveFileError("The file could not be saved.", traceback.format_exc()) 391 | finally: 392 | qFile.close() 393 | 394 | tab.parser = self 395 | tab.fileFormat = self.fileFormat 396 | 397 | 398 | class AbstractExtParser(FileParser): 399 | """ Determines which files are supported based on extension. 400 | Override exts in a subclass to add extensions. 401 | """ 402 | # Tuple of `str` file extensions (without the leading .) that this parser can support. Example: ("usda",) 403 | exts = () 404 | 405 | def acceptsFile(self, fileInfo, link): 406 | """ Accept files with the proper extension. 407 | 408 | :Parameters: 409 | fileInfo : `QFileInfo` 410 | File info object 411 | link : `QtCore.QUrl` 412 | Full URL, potentially with query string 413 | """ 414 | return fileInfo.suffix() in self.exts 415 | -------------------------------------------------------------------------------- /usdmanager/preferences_dialog.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 DreamWorks Animation L.L.C. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | """ Create a Preferences dialog. 18 | """ 19 | 20 | from Qt.QtCore import Slot, QRegExp 21 | from Qt.QtGui import QRegExpValidator 22 | from Qt.QtWidgets import QAbstractButton, QDialog, QDialogButtonBox, QFontDialog, QLineEdit, QMessageBox, QVBoxLayout 23 | 24 | from .constants import LINE_LIMIT 25 | from .utils import icon, loadUiWidget 26 | 27 | 28 | class PreferencesDialog(QDialog): 29 | """ 30 | Preferences dialog 31 | """ 32 | def __init__(self, parent, **kwargs): 33 | """ Initialize the dialog. 34 | 35 | :Parameters: 36 | parent : `UsdMngrWindow` 37 | Main window 38 | """ 39 | super(PreferencesDialog, self).__init__(parent, **kwargs) 40 | 41 | self.docFont = parent.tabWidget.font() 42 | self.fileAssociations = {} 43 | self.lineEditProgs = [] 44 | self.lineEditExts = [] 45 | 46 | self.setupUi() 47 | self.connectSignals() 48 | 49 | def setupUi(self): 50 | """ Creates and lays out the widgets defined in the ui file. 51 | """ 52 | self.baseInstance = loadUiWidget("preferences_dialog.ui", self) 53 | self.setWindowIcon(icon("preferences-system")) 54 | self.buttonFont.setIcon(icon("preferences-desktop-font")) 55 | self.buttonNewProg.setIcon(icon("list-add")) 56 | 57 | # ----- General tab ----- 58 | # Set initial preferences. 59 | parent = self.parent() 60 | self.checkBox_parseLinks.setChecked(parent.preferences['parseLinks']) 61 | self.checkBox_newTab.setChecked(parent.preferences['newTab']) 62 | self.checkBox_syntaxHighlighting.setChecked(parent.preferences['syntaxHighlighting']) 63 | self.checkBox_teletypeConversion.setChecked(parent.preferences['teletype']) 64 | self.checkBox_lineNumbers.setChecked(parent.preferences['lineNumbers']) 65 | self.checkBox_showAllMessages.setChecked(parent.preferences['showAllMessages']) 66 | self.checkBox_showHiddenFiles.setChecked(parent.preferences['showHiddenFiles']) 67 | self.checkBox_autoCompleteAddressBar.setChecked(parent.preferences['autoCompleteAddressBar']) 68 | self.useSpacesCheckBox.setChecked(parent.preferences['useSpaces']) 69 | self.useSpacesSpinBox.setValue(parent.preferences['tabSpaces']) 70 | self.lineEditTextEditor.setText(parent.preferences['textEditor']) 71 | self.lineEditDiffTool.setText(parent.preferences['diffTool']) 72 | self.themeWidget.setChecked(parent.preferences['theme'] == "dark") 73 | self.lineLimitSpinBox.setValue(parent.preferences['lineLimit']) 74 | self.checkBox_autoIndent.setChecked(parent.preferences['autoIndent']) 75 | self.updateFontLabel() 76 | 77 | # ----- Programs tab ----- 78 | self.progLayout = QVBoxLayout() 79 | self.extLayout = QVBoxLayout() 80 | 81 | # Extensions can only be: 82 | #self.progValidator = QRegExpValidator(QRegExp("[\w,. ]+"), self) 83 | self.extValidator = QRegExpValidator(QRegExp(r"(?:\.?\w*,?\s*)+"), self) 84 | self.lineEdit.setValidator(self.extValidator) 85 | 86 | # Create the fields for programs and extensions. 87 | self.populateProgsAndExts(parent.programs) 88 | 89 | def connectSignals(self): 90 | """ Connect signals to slots. 91 | """ 92 | self.buttonBox.clicked.connect(self.restoreDefaults) 93 | self.buttonNewProg.clicked.connect(self.newProgField) 94 | self.buttonBox.accepted.connect(self.validate) 95 | self.buttonFont.clicked.connect(self.selectFont) 96 | 97 | def deleteItems(self, layout): 98 | """ Delete all items in given layout. 99 | 100 | :Parameters: 101 | layout : `QLayout` 102 | Layout to delete items from 103 | """ 104 | if layout is not None: 105 | while layout.count(): 106 | item = layout.takeAt(0) 107 | widget = item.widget() 108 | if widget is not None: 109 | widget.deleteLater() 110 | else: 111 | self.deleteItems(item.layout()) 112 | 113 | def getPrefFont(self): 114 | """ Get the user preference for font. 115 | 116 | :Returns: 117 | Font selected for documents. 118 | :Rtype: 119 | `QFont` 120 | """ 121 | return self.docFont 122 | 123 | def getPrefLineNumbers(self): 124 | """ Get the user preference for displaying line numbers. 125 | 126 | :Returns: 127 | State of "Show line numbers" check box. 128 | :Rtype: 129 | `bool` 130 | """ 131 | return self.checkBox_lineNumbers.isChecked() 132 | 133 | def getPrefNewTab(self): 134 | """ Get the user preference for opening links in a new tab or not. 135 | 136 | :Returns: 137 | State of "Open links in new tabs" check box. 138 | :Rtype: 139 | `bool` 140 | """ 141 | return self.checkBox_newTab.isChecked() 142 | 143 | def getPrefParseLinks(self): 144 | """ Get the user preference to enable link parsing. 145 | 146 | :Returns: 147 | Search for links in the opened file. 148 | Disable this for huge files that freeze the app. 149 | 150 | :Rtype: 151 | `bool` 152 | """ 153 | return self.checkBox_parseLinks.isChecked() 154 | 155 | def getPrefPrograms(self): 156 | """ Get the user preference for file extensions and apps to open them with. 157 | 158 | :Returns: 159 | Dictionary of extension: program pairs of strings. 160 | :Rtype: 161 | `dict` 162 | """ 163 | return self.fileAssociations 164 | 165 | def getPrefShowAllMessages(self): 166 | """ Get the user preference to display all messages or just errors. 167 | 168 | :Returns: 169 | State of "Show success messages" check box. 170 | :Rtype: 171 | `bool` 172 | """ 173 | return self.checkBox_showAllMessages.isChecked() 174 | 175 | def getPrefShowHiddenFiles(self): 176 | """ Get the user preference for showing hidden files by default. 177 | 178 | :Returns: 179 | State of "Show hidden files" check box. 180 | :Rtype: 181 | `bool` 182 | """ 183 | return self.checkBox_showHiddenFiles.isChecked() 184 | 185 | def getPrefAutoCompleteAddressBar(self): 186 | """ Get the user preference for enabling address bar auto-completion. 187 | 188 | :Returns: 189 | State of "Auto complete paths in address bar" check box. 190 | :Rtype: 191 | `bool` 192 | """ 193 | return self.checkBox_autoCompleteAddressBar.isChecked() 194 | 195 | def getPrefLineLimit(self): 196 | """ Get the user preference for line limit before truncating files. 197 | 198 | :Returns: 199 | Number of lines to display before truncating a file. 200 | :Rtype: 201 | `int` 202 | """ 203 | return self.lineLimitSpinBox.value() 204 | 205 | def getPrefSyntaxHighlighting(self): 206 | """ Get the user preference to enable syntax highlighting. 207 | 208 | :Returns: 209 | State of "Enable syntax highlighting" check box. 210 | :Rtype: 211 | `bool` 212 | """ 213 | return self.checkBox_syntaxHighlighting.isChecked() 214 | 215 | def getPrefTeletypeConversion(self): 216 | """ Get the user preference to enable teletype character conversion. 217 | 218 | :Returns: 219 | State of "Display teletype colors" check box. 220 | :Rtype: 221 | `bool` 222 | """ 223 | return self.checkBox_teletypeConversion.isChecked() 224 | 225 | def getPrefTextEditor(self): 226 | """ Get the user-preferred text editor. 227 | 228 | :Returns: 229 | Text in Text editor QTextEdit. 230 | :Rtype: 231 | `str` 232 | """ 233 | return self.lineEditTextEditor.text() 234 | 235 | def getPrefTheme(self): 236 | """ Get the selected theme. 237 | 238 | We may eventually make this a combo box supporting multiple themes, 239 | so use the string name instead of just a boolean. 240 | 241 | :Returns: 242 | Selected theme name, or None if the default 243 | :Rtype: 244 | `str` | None 245 | """ 246 | return "dark" if self.themeWidget.isChecked() else None 247 | 248 | def getPrefUseSpaces(self): 249 | """ Get the user preference for spaces vs. tabs. 250 | 251 | :Returns: 252 | State of "Use spaces instead of tabs" check box. 253 | :Rtype: 254 | `bool` 255 | """ 256 | return self.useSpacesCheckBox.isChecked() 257 | 258 | def getPrefTabSpaces(self): 259 | """ Get the user preference for number of spaces equaling a tab. 260 | 261 | :Returns: 262 | Number of spaces to use instead of a tab. 263 | Only use this number of use spaces is also True. 264 | :Rtype: 265 | `int` 266 | """ 267 | return self.useSpacesSpinBox.value() 268 | 269 | def getPrefAutoIndent(self): 270 | """ Get the user preference for auto-indentation. 271 | 272 | :Returns: 273 | State of "Use auto indentation" check box. 274 | :Rtype: 275 | `bool` 276 | """ 277 | return self.checkBox_autoIndent.isChecked() 278 | 279 | def getPrefDiffTool(self): 280 | """ Get the user preference for diff tool. 281 | 282 | :Returns: 283 | Text in Diff tool QTextEdit. 284 | :Rtype: 285 | `str` 286 | """ 287 | return self.lineEditDiffTool.text() 288 | 289 | @Slot(bool) 290 | def newProgField(self, *args): 291 | """ Add a new line to the programs list. 292 | """ 293 | self.lineEditProgs.append(QLineEdit(self)) 294 | self.progLayout.addWidget(self.lineEditProgs[len(self.lineEditProgs)-1]) 295 | self.lineEditExts.append(QLineEdit(self)) 296 | self.extLayout.addWidget(self.lineEditExts[len(self.lineEditExts)-1]) 297 | 298 | def populateProgsAndExts(self, programs): 299 | """ Fill out the UI with the user preference for programs and extensions. 300 | 301 | :Parameters: 302 | programs : `dict` 303 | Dictionary of extension: program pairs of strings. 304 | """ 305 | self.lineEditProgs = [] 306 | self.lineEditExts = [] 307 | 308 | # Get unique programs. 309 | tmpSet = set() 310 | progs = [x for x in programs.values() if x not in tmpSet and not tmpSet.add(x)] 311 | del tmpSet 312 | progs.sort() 313 | 314 | # Get extensions per program. 315 | exts = [] 316 | for prog in progs: 317 | # Find each extension matching this program. 318 | progExts = ["."+x for x in programs if programs[x] == prog] 319 | progExts.sort() 320 | # Format in comma-separated list for display. 321 | exts.append(", ".join(progExts)) 322 | 323 | # Put the files that should open with this app in their own place. 324 | # Then remove them from these lists. 325 | index = progs.index("") 326 | progs.pop(index) 327 | self.lineEdit.setText(exts[index]) 328 | exts.pop(index) 329 | del index 330 | 331 | for i, prog in enumerate(progs): 332 | # Create and populate two QLineEdit objects per extension: program pair. 333 | self.lineEditProgs.append(QLineEdit(prog, self)) 334 | #self.lineEditProgs[i].setValidator(self.progValidator) 335 | self.progLayout.addWidget(self.lineEditProgs[i]) 336 | self.lineEditExts.append(QLineEdit(exts[i], self)) 337 | self.lineEditExts[i].setValidator(self.extValidator) 338 | self.extLayout.addWidget(self.lineEditExts[i]) 339 | self.progWidget.setLayout(self.progLayout) 340 | self.extWidget.setLayout(self.extLayout) 341 | 342 | @Slot(QAbstractButton) 343 | def restoreDefaults(self, btn): 344 | """ Restore the GUI to the program's default settings. 345 | Don't update the actual preferences (that happens if OK is pressed). 346 | """ 347 | if btn == self.buttonBox.button(QDialogButtonBox.RestoreDefaults): 348 | # Delete old QLineEdit objects. 349 | self.deleteItems(self.progLayout) 350 | self.deleteItems(self.extLayout) 351 | 352 | # Set other preferences in the GUI. 353 | default = self.parent().window().app.DEFAULTS 354 | self.checkBox_parseLinks.setChecked(default['parseLinks']) 355 | self.checkBox_newTab.setChecked(default['newTab']) 356 | self.checkBox_syntaxHighlighting.setChecked(default['syntaxHighlighting']) 357 | self.checkBox_teletypeConversion.setChecked(default['teletype']) 358 | self.checkBox_lineNumbers.setChecked(default['lineNumbers']) 359 | self.checkBox_showAllMessages.setChecked(default['showAllMessages']) 360 | self.checkBox_showHiddenFiles.setChecked(default['showHiddenFiles']) 361 | self.checkBox_autoCompleteAddressBar.setChecked(default['autoCompleteAddressBar']) 362 | self.lineEditTextEditor.setText(default['textEditor']) 363 | self.lineEditDiffTool.setText(default['diffTool']) 364 | self.useSpacesCheckBox.setChecked(default['useSpaces']) 365 | self.useSpacesSpinBox.setValue(default['tabSpaces']) 366 | self.themeWidget.setChecked(False) 367 | self.docFont = default['font'] 368 | self.updateFontLabel() 369 | self.lineLimitSpinBox.setValue(default['lineLimit']) 370 | self.checkBox_autoIndent.setChecked(default['autoIndent']) 371 | 372 | # Re-create file association fields with the default programs. 373 | self.populateProgsAndExts(self.parent().defaultPrograms) 374 | 375 | @Slot(bool) 376 | def selectFont(self, *args): 377 | """ Update the user's font preference. 378 | """ 379 | font, ok = QFontDialog.getFont(self.docFont, self, "Select Font") 380 | if ok: 381 | self.docFont = font 382 | self.updateFontLabel() 383 | 384 | def updateFontLabel(self): 385 | """ Update the UI font label to show the user's selected font. 386 | """ 387 | bold = "Bold " if self.docFont.bold() else "" 388 | italic = "Italic " if self.docFont.italic() else "" 389 | self.labelFont.setText("Document font: {}pt {}{}{}".format(self.docFont.pointSize(), bold, italic, 390 | self.docFont.family())) 391 | 392 | @Slot() 393 | def validate(self): 394 | """ Make sure everything has valid input. 395 | Make sure there are no duplicate extensions. 396 | Accepts or rejects accepted() signal accordingly. 397 | """ 398 | for lineEdit in self.lineEditExts: 399 | if lineEdit.hasAcceptableInput(): 400 | lineEdit.setStyleSheet("background-color:none") 401 | else: 402 | lineEdit.setStyleSheet("background-color:salmon") 403 | QMessageBox.warning(self, "Warning", "One or more extension is invalid.") 404 | return 405 | 406 | # Get file extensions for this app to handle. 407 | extText = self.lineEdit.text() 408 | # Strip out periods and spaces. 409 | extText = extText.replace(' ', '').replace('.', '') 410 | progList = [[x, ""] for x in extText.split(',') if x] 411 | 412 | for i in range(len(self.lineEditProgs)): 413 | extText = self.lineEditExts[i].text() 414 | progText = self.lineEditProgs[i].text() 415 | extText = extText.replace(' ', '').replace('.', '') 416 | for ext in extText.split(','): 417 | if ext: 418 | progList.append([ext, progText]) 419 | 420 | # Make sure there aren't any duplicate extensions. 421 | tmpSet = set() 422 | uniqueExt = [ext for ext, prog in progList if ext not in tmpSet and not tmpSet.add(ext)] 423 | if len(uniqueExt) == len(progList): 424 | self.fileAssociations = dict(progList) 425 | else: 426 | QMessageBox.warning(self, "Warning", "You have entered the same extension for two or more programs.") 427 | return 428 | 429 | # Accept if we made it this far. 430 | self.accept() 431 | -------------------------------------------------------------------------------- /usdmanager/include_panel.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 DreamWorks Animation L.L.C. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | """ Left-hand side file browser. 17 | """ 18 | import os 19 | 20 | from Qt import QtCore, QtWidgets 21 | from Qt.QtCore import Signal, Slot 22 | 23 | from .utils import expandPath, icon, overrideCursor 24 | 25 | 26 | class IncludePanel(QtWidgets.QWidget): 27 | """ 28 | File browsing panel for the left side of the main UI. 29 | """ 30 | openFile = Signal(str) 31 | 32 | def __init__(self, path="", filter="", selectedFilter="", parent=None): 33 | """ Initialize the panel. 34 | 35 | :Parameters: 36 | path : `str` 37 | default path to look in when creating or choosing the file. 38 | filter : `list` 39 | A list of strings denoting filename match filters. These strings 40 | are displayed in a user-selectable combobox. When selected, 41 | the file list is filtered by the pattern 42 | The format must follow: 43 | ["Descriptive text (pattern1 pattern2 ...)", ...] 44 | The glob matching pattern is in parens, and the entire string is 45 | displayed for the user. 46 | selectedFilter : `str` 47 | Set the current filename filter. Needs to be one of the entries 48 | specified in the "filter" parameter. 49 | parent : `QObject` 50 | Parent for this widget. 51 | """ 52 | super(IncludePanel, self).__init__(parent) 53 | 54 | # Setup UI. 55 | self.lookInCombo = QtWidgets.QComboBox(self) 56 | self.toParentButton = QtWidgets.QToolButton(self) 57 | self.buttonHome = QtWidgets.QToolButton(self) 58 | self.buttonOriginal = QtWidgets.QPushButton("Original", self) 59 | self.fileNameEdit = QtWidgets.QLineEdit(self) 60 | self.fileNameLabel = QtWidgets.QLabel("File:", self) 61 | self.fileTypeCombo = QtWidgets.QComboBox(self) 62 | self.fileTypeLabel = QtWidgets.QLabel("Type:", self) 63 | self.stackedWidget = QtWidgets.QStackedWidget(self) 64 | self.listView = QtWidgets.QListView(self) 65 | self.fileTypeLabelFiller = QtWidgets.QLabel(self) 66 | self.fileTypeComboFiller = QtWidgets.QLabel(self) 67 | self.buttonOpen = QtWidgets.QPushButton(icon("document-open"), "Open", self) 68 | self.buttonOpen.setEnabled(False) 69 | 70 | # Item settings. 71 | self.buttonHome.setIcon(icon("folder-home", self.style().standardIcon(QtWidgets.QStyle.SP_DirHomeIcon))) 72 | self.buttonHome.setToolTip("User's home directory") 73 | self.buttonHome.setAutoRaise(True) 74 | self.buttonOriginal.setToolTip("Original directory") 75 | self.lookInCombo.setMinimumSize(50, 0) 76 | self.toParentButton.setIcon(icon("folder-up", self.style().standardIcon(QtWidgets.QStyle.SP_FileDialogToParent))) 77 | self.toParentButton.setAutoRaise(True) 78 | self.toParentButton.setToolTip("Parent directory") 79 | self.listView.setDragEnabled(True) 80 | self.fileNameLabel.setToolTip("Selected file or directory") 81 | self.fileTypeLabel.setBuddy(self.fileTypeCombo) 82 | self.fileTypeLabel.setToolTip("File type filter") 83 | self.buttonOpen.setToolTip("Open selected file") 84 | 85 | # Focus policies. 86 | self.lookInCombo.setFocusPolicy(QtCore.Qt.NoFocus) 87 | self.toParentButton.setFocusPolicy(QtCore.Qt.NoFocus) 88 | self.buttonHome.setFocusPolicy(QtCore.Qt.NoFocus) 89 | self.buttonOriginal.setFocusPolicy(QtCore.Qt.NoFocus) 90 | self.buttonOpen.setFocusPolicy(QtCore.Qt.NoFocus) 91 | 92 | # Item size policies. 93 | self.lookInCombo.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Fixed) 94 | self.toParentButton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 95 | self.buttonHome.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 96 | self.buttonOriginal.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 97 | self.fileNameLabel.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) 98 | self.fileTypeCombo.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Fixed) 99 | self.fileTypeLabel.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) 100 | self.buttonOpen.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 101 | 102 | # Layouts. 103 | self.include1Layout = QtWidgets.QHBoxLayout() 104 | self.include1Layout.setContentsMargins(0, 0, 0, 0) 105 | self.include1Layout.setSpacing(5) 106 | self.include1Layout.addWidget(self.buttonHome) 107 | self.include1Layout.addWidget(self.lookInCombo) 108 | self.include1Layout.addWidget(self.toParentButton) 109 | 110 | self.include2Layout = QtWidgets.QHBoxLayout() 111 | self.include2Layout.setContentsMargins(0, 0, 0, 0) 112 | self.include2Layout.setSpacing(5) 113 | self.include2Layout.addWidget(self.stackedWidget) 114 | 115 | self.include4Layout = QtWidgets.QGridLayout() 116 | self.include4Layout.setContentsMargins(0, 0, 0, 0) 117 | self.include4Layout.setSpacing(5) 118 | self.include4Layout.addWidget(self.fileNameLabel, 0, 0, QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) 119 | self.include4Layout.addWidget(self.fileNameEdit, 0, 1) 120 | self.include4Layout.addWidget(self.fileTypeLabel, 1, 0, QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) 121 | self.include4Layout.addWidget(self.fileTypeCombo, 1, 1) 122 | self.include4Layout.addWidget(self.fileTypeLabelFiller, 2, 0) 123 | self.include4Layout.addWidget(self.fileTypeComboFiller, 2, 1) 124 | 125 | self.include5Layout = QtWidgets.QHBoxLayout() 126 | self.include5Layout.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) 127 | self.include5Layout.setContentsMargins(0, 0, 0, 0) 128 | self.include5Layout.setSpacing(5) 129 | self.include5Layout.addWidget(self.buttonOriginal) 130 | spacer = QtWidgets.QSpacerItem(5, 0, QtWidgets.QSizePolicy.MinimumExpanding) 131 | self.include5Layout.addSpacerItem(spacer) 132 | self.include5Layout.addWidget(self.buttonOpen) 133 | 134 | self.includeLayout = QtWidgets.QVBoxLayout() 135 | self.includeLayout.setContentsMargins(0, 0, 0, 0) 136 | self.includeLayout.setSpacing(5) 137 | self.includeLayout.addLayout(self.include1Layout) 138 | self.includeLayout.addLayout(self.include2Layout) 139 | self.includeLayout.addLayout(self.include4Layout) 140 | line1 = QtWidgets.QFrame() 141 | line1.setFrameStyle(QtWidgets.QFrame.HLine | QtWidgets.QFrame.Sunken) 142 | self.includeLayout.addWidget(line1) 143 | self.includeLayout.addLayout(self.include5Layout) 144 | 145 | self.setLayout(self.includeLayout) 146 | self.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) 147 | 148 | self.buttonHome.clicked.connect(self.onHome) 149 | self.buttonOriginal.clicked.connect(self.onOriginal) 150 | self.lookInCombo.activated[int].connect(self.onPathComboChanged) 151 | self.fileTypeCombo.activated[int].connect(self._useNameFilter) 152 | 153 | self.fileModel = QtWidgets.QFileSystemModel(parent) 154 | self.fileModel.setReadOnly(True) 155 | self.fileModel.setNameFilterDisables(False) 156 | self.fileModel.setResolveSymlinks(True) 157 | self.fileModel.rootPathChanged.connect(self.pathChanged) 158 | 159 | self.listView.setModel(self.fileModel) 160 | 161 | self.listView.activated[QtCore.QModelIndex].connect(self.enterDirectory) 162 | 163 | # Set selection mode and behavior. 164 | self.listView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) 165 | self.listView.setWrapping(False) 166 | self.listView.setResizeMode(QtWidgets.QListView.Adjust) 167 | 168 | selectionMode = QtWidgets.QAbstractItemView.SingleSelection 169 | self.listView.setSelectionMode(selectionMode) 170 | 171 | # Setup the completer. 172 | completer = QtWidgets.QCompleter(self.fileModel, self.fileNameEdit) 173 | completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) 174 | self.fileNameEdit.setCompleter(completer) 175 | self.fileNameEdit.textChanged.connect(self.autoCompleteFileName) 176 | self.fileNameEdit.returnPressed.connect(self.accept) 177 | 178 | pathFile = None 179 | if not path: 180 | self.__path = os.getcwd() 181 | elif os.path.isfile(path): 182 | self.__path, pathFile = os.path.split(path) 183 | else: 184 | self.__path = path 185 | 186 | self.setPath(self.__path) 187 | 188 | if filter: 189 | self.setNameFilters(filter) 190 | 191 | if selectedFilter: 192 | self.selectNameFilter(selectedFilter) 193 | 194 | self.listPage = QtWidgets.QWidget(self.stackedWidget) 195 | self.stackedWidget.addWidget(self.listPage) 196 | listLayout = QtWidgets.QGridLayout(self.listPage) 197 | #listLayout.setMargin(0) 198 | listLayout.setContentsMargins(0, 0, 0, 0) 199 | listLayout.addWidget(self.listView, 0, 0, 1, 1) 200 | 201 | self.fileTypeLabelFiller.hide() 202 | self.fileTypeComboFiller.hide() 203 | 204 | # Selections 205 | selections = self.listView.selectionModel() 206 | selections.selectionChanged.connect(self.fileSelectionChanged) 207 | 208 | if pathFile is not None: 209 | idx = self.fileModel.index(self.fileModel.rootPath() + QtCore.QDir.separator() + pathFile) 210 | self.select(idx) 211 | self.fileNameEdit.setText(pathFile) 212 | 213 | # Connect signals. 214 | self.toParentButton.clicked.connect(self.onUp) 215 | self.buttonOpen.clicked.connect(self.accept) 216 | 217 | self.listView.scheduleDelayedItemsLayout() 218 | self.stackedWidget.setCurrentWidget(self.listPage) 219 | self.fileNameEdit.setFocus() 220 | 221 | def setNameFilters(self, filters): 222 | self._nameFilters = filters 223 | 224 | self.fileTypeCombo.clear() 225 | if len(self._nameFilters) == 0: 226 | return 227 | for filter in self._nameFilters: 228 | self.fileTypeCombo.addItem(filter) 229 | self.selectNameFilter(filters[0]) 230 | 231 | def selectNameFilter(self, filter): 232 | i = self.fileTypeCombo.findText(filter) 233 | if i >= 0: 234 | self.fileTypeCombo.setCurrentIndex(i) 235 | self._useNameFilter(i) 236 | 237 | @Slot(int) 238 | def _useNameFilter(self, index): 239 | filter = self.fileTypeCombo.itemText(index) 240 | filter = [f.strip() for f in filter.split(" (", 1)[1][:-1].split(" ")] 241 | self.fileModel.setNameFilters(filter) 242 | 243 | def setDirectory(self, directory): 244 | with overrideCursor(): 245 | directory = str(directory) # it may be a ResolvedPath; convert to str 246 | if not (directory.endswith('/') or directory.endswith('\\')): 247 | directory += '/' 248 | self.fileNameEdit.completer().setCompletionPrefix(directory) 249 | root = self.fileModel.setRootPath(directory) 250 | self.listView.setRootIndex(root) 251 | self.fileNameEdit.setText('') 252 | self.fileNameEdit.clear() 253 | self.listView.selectionModel().clear() 254 | 255 | @Slot(str) 256 | def pathChanged(self, path): 257 | pass 258 | 259 | @Slot(QtCore.QModelIndex) 260 | def enterDirectory(self, index): 261 | fname = str(index.data(QtWidgets.QFileSystemModel.FileNameRole)) 262 | isDirectory = self.fileModel.isDir(index) 263 | if isDirectory: 264 | self.appendToPath(fname, isDirectory) 265 | else: 266 | self.accept() 267 | 268 | def showAll(self, checked): 269 | """ Show hidden files 270 | 271 | :Parameters: 272 | checked : `bool` 273 | If True, show hidden files 274 | """ 275 | dirFilters = self.fileModel.filter() 276 | if checked: 277 | dirFilters |= QtCore.QDir.Hidden 278 | else: 279 | dirFilters &= ~QtCore.QDir.Hidden 280 | self.fileModel.setFilter(dirFilters) 281 | 282 | @Slot(bool) 283 | def onUp(self, *args): 284 | path = os.path.abspath(self.path) 285 | if not os.path.isdir(path): 286 | path = os.path.dirname(path) 287 | dirName = os.path.dirname(path) 288 | self.setPath(dirName) 289 | 290 | @Slot(bool) 291 | def onHome(self, *args): 292 | self.setPath(QtCore.QDir.homePath()) 293 | self.setFileDisplay() 294 | 295 | @Slot(bool) 296 | def onOriginal(self, *args): 297 | self.setPath(self.__path) 298 | self.setFileDisplay() 299 | 300 | @Slot(int) 301 | def onPathComboChanged(self, index): 302 | self.setPath(str(self.lookInCombo.itemData(index))) 303 | 304 | def setPath(self, path): 305 | self.setDirectory(expandPath(path)) 306 | self.path = path 307 | self.lookInCombo.clear() 308 | p = path 309 | dirs = [] 310 | while True: 311 | p1, p2 = os.path.split(p) 312 | if not p2: 313 | break 314 | dirs.insert(0, (p2, p)) 315 | p = p1 316 | for d, dp in dirs: 317 | self.lookInCombo.addItem("%s%s" % (self.lookInCombo.count()*" ", d), dp) 318 | self.lookInCombo.setCurrentIndex(self.lookInCombo.count() - 1) 319 | 320 | def appendToPath(self, filename, isDirectory): 321 | """ 322 | :Parameters: 323 | filename : `str` 324 | isDirectory : `bool` 325 | """ 326 | self.path = os.path.join(self.path, filename) 327 | if isDirectory: 328 | self.setDirectory(expandPath(self.path)) 329 | self.lookInCombo.addItem("%s%s" % (self.lookInCombo.count()*" ", filename), self.path) 330 | self.lookInCombo.setCurrentIndex(self.lookInCombo.count() - 1) 331 | return self.path 332 | 333 | def getPath(self): 334 | return self.path 335 | 336 | def setFileDisplay(self): 337 | self.stackedWidget.setCurrentWidget(self.listPage) 338 | self.fileNameLabel.show() 339 | self.fileNameEdit.show() 340 | self.fileNameEdit.setFocus() 341 | self.fileTypeLabel.show() 342 | self.fileTypeCombo.show() 343 | self.fileTypeLabelFiller.hide() 344 | self.fileTypeComboFiller.hide() 345 | self.toParentButton.setEnabled(True) 346 | 347 | @Slot() 348 | @Slot(bool) 349 | def accept(self, *args): 350 | indexes = self.listView.selectionModel().selectedRows() 351 | if indexes: 352 | index = indexes[0] 353 | if self.fileModel.isDir(index): 354 | self.enterDirectory(index) 355 | return 356 | fname = str(index.data()) 357 | else: 358 | fname = self.fileNameEdit.text().strip() 359 | if not fname: 360 | return 361 | info = QtCore.QFileInfo(fname) 362 | if info.isDir(): 363 | self.setPath(info.absoluteFilePath()) 364 | return 365 | self.openFile.emit(os.path.join(self.getPath(), fname)) 366 | 367 | @Slot(str) 368 | def autoCompleteFileName(self, text): 369 | if not text.strip(): 370 | return 371 | if text.strip().startswith("/"): 372 | self.listView.selectionModel().clearSelection() 373 | return 374 | idx = self.fileModel.index(self.fileModel.rootPath() + QtCore.QDir.separator() + text) 375 | if self.fileNameEdit.hasFocus(): 376 | self.listView.selectionModel().clear() 377 | self.select(idx) 378 | 379 | def select(self, index): 380 | if index.isValid(): 381 | self.listView.selectionModel().select(index, 382 | QtCore.QItemSelectionModel.Select | QtCore.QItemSelectionModel.Rows) 383 | self.listView.scrollTo(index, self.listView.EnsureVisible) 384 | return index 385 | 386 | @Slot(QtCore.QItemSelection, QtCore.QItemSelection) 387 | def fileSelectionChanged(self, one, two): 388 | indexes = self.listView.selectionModel().selectedRows() 389 | if indexes: 390 | idx = indexes[0] 391 | self.fileNameEdit.setText(str(idx.data())) 392 | self.buttonOpen.setEnabled(True) 393 | else: 394 | self.buttonOpen.setEnabled(False) 395 | --------------------------------------------------------------------------------