├── 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 , put this before the <>
32 | # symbols for tags get colored.
33 | r"?\w+",
34 | QtCore.Qt.darkRed,
35 | QtCore.Qt.red
36 | ],
37 | [
38 | # XML symbols
39 | r"(?:/>|\?>|>|?|<\?xml\b)",
40 | #(?:/>|\?>|<(?:/|\?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 | ('[0m', ''),
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" + "]||true|false)",
34 | QtCore.Qt.darkMagenta, # Light theme
35 | QtGui.QColor("#CCC"), # Dark theme
36 | QtGui.QFont.Bold
37 | ],
38 | [ # Keyword actions/descriptors
39 | # add append prepend del delete custom uniform rel
40 | r"\b(?:a(?:dd|ppend)|prepend|del(?:ete)?|custom|uniform|varying|rel)\b",
41 | QtGui.QColor("#4b7029"),
42 | QtGui.QColor("#4b7029"),
43 | QtGui.QFont.Bold
44 | ],
45 | [ # Keywords
46 | # references payload defaultPrim doc subLayers specializes active assetInfo hidden kind inherits
47 | # instanceable customData variant variants variantSets config connect default dictionary displayUnit
48 | # nameChildren None offset permission prefixSubstitutions properties relocates reorder rootPrims scale
49 | # suffixSubstitutions symmetryArguments symmetryFunction timeSamples
50 | r"\b(?:references|payload|d(?:efaultPrim|oc)|s(?:ubLayers|pecializes)|a(?:ctive|ssetInfo)|hidden|kind|"
51 | r"in(?:herits|stanceable)|customData|variant(?:s|Sets)?|config|connect|default|dictionary|displayUnit|"
52 | r"nameChildren|None|offset|permission|prefixSubstitutions|properties|relocates|reorder|rootPrims|scale"
53 | r"|suffixSubstitutions|symmetryArguments|symmetryFunction|timeSamples)\b",
54 | QtGui.QColor("#649636"),
55 | QtGui.QColor("#649636"),
56 | QtGui.QFont.Bold
57 | ],
58 | [ # Datatypes (https://graphics.pixar.com/usd/docs/api/_usd__page__datatypes.html)
59 | # bool uchar int uint int64 uint64 int2 int3 int4 half half2 half3 half4 float float2 float3 float4
60 | # double double2 double3 double4 string token asset matrix matrix2d matrix3d matrix4d quatd quatf quath
61 | # color3d color3f color3h color4d color4f color4h normal3d normal3f normal3h point3d point3f point3h
62 | # vector3d vector3f vector3h frame4d texCoord2d texCoord2f texCoord2h texCoord3d texCoord3f texCoord3h
63 | r"\b(?:bool|uchar|u?int(?:64)?|int[234]|half[234]?|float[234]?|double[234]?|string|token|asset|"
64 | r"matrix[234]d|quat[dfh]|color[34][dfh]|normal3[dfh]|point3[dfh]|vector3[dfh]|frame4d|"
65 | r"texCoord[23][dfh])\b",
66 | QtGui.QColor("#678CB1"),
67 | QtGui.QColor("#678CB1"),
68 | QtGui.QFont.Bold,
69 | False,
70 | QtCore.Qt.CaseInsensitive
71 | ],
72 | [ # Schemas
73 | # TODO: Can this query USD to see what schemas are defined?
74 | # Xform Scope Shader Sphere Subdiv Camera Cube Curve Mesh Material PointInstancer Plane
75 | r"\b(?:Xform|S(?:cope|hader|phere|ubdiv)|C(?:amera|ube|urve)|M(?:esh|aterial)|"
76 | r"P(?:ointInstancer|lane))\b",
77 | QtGui.QColor("#997500"),
78 | QtGui.QColor("#997500"),
79 | QtGui.QFont.Bold
80 | ],
81 | [ # Specifiers
82 | # def over class variantSet
83 | r"\b(?:def|over|class|variantSet)\b",
84 | QtGui.QColor("#8080FF"),
85 | QtGui.QColor("#8080FF"),
86 | QtGui.QFont.Bold
87 | ],
88 | self.ruleNumber,
89 | self.ruleDoubleQuote,
90 | self.ruleSingleQuote,
91 | self.ruleLink,
92 | self.ruleComment
93 | ]
94 |
95 | def createRules(self):
96 | super(MasterUSDHighlighter, self).createRules()
97 |
98 | # Support triple quote strings. Doesn't deal with escaped quotes.
99 | self.multilineRules.append(createMultilineRule('"""', '"""', QtCore.Qt.darkGreen, QtGui.QColor(25, 255, 25)))
100 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | [](https://usdmanager.readthedocs.io/en/stable/?badge=stable)
4 | [](https://opensource.org/licenses/Apache-2.0)
5 |
6 | [Website](http://www.usdmanager.org)
7 |
8 | USD Manager is an open-source, python-based Qt tool for browsing, managing, and editing text-based files like USD,
9 | combining the best features from your favorite web browser and text editor into one application, with hooks to deeply
10 | integrate with other pipeline tools. It is developed and maintained by [DreamWorks Animation](http://www.dreamworksanimation.com)
11 | for use with USD and other hierarchical, text-based workflows, primarily geared towards feature film production. While
12 | originally designed around PyQt4, USD Manager uses the Qt.py compatibility library to allow working with PyQt4, PyQt5,
13 | PySide, or PySide2 for Qt bindings.
14 |
15 | 
16 |
17 | ### Development Repository
18 |
19 | This GitHub repository hosts the trunk of the USD Manager development. This implies that it is the newest public
20 | version with the latest features and bug fixes. However, it also means that it has not undergone a lot of testing and
21 | is generally less stable than the [production releases](https://github.com/dreamworksanimation/usdmanager/releases).
22 |
23 | ### License
24 |
25 | USD Manager is released under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0), which is
26 | a free, open-source, and detailed software license developed and maintained by the Apache Software Foundation.
27 |
28 | Contents
29 | ========
30 |
31 | - [Installing USD Manager](#installing-usd-manager)
32 | * [Requirements](#requirements)
33 | * [Install with setup.py](#install-with-setuppy)
34 | - [Using USD Manager](#using-usd-manager)
35 | * [Keyboard shortcuts](#keyboard-shortcuts)
36 | - [Development / Customization](#development---customization)
37 | - [Contributing](#contributing)
38 |
39 | Installing USD Manager
40 | ======================
41 |
42 | Requirements
43 | ------------
44 |
45 | usdmanager requires [Python](https://www.python.org/) 2 (for Python 3, see the
46 | [python3 branch](https://github.com/dreamworksanimation/usdmanager/tree/python3)),
47 | [Qt.py](https://github.com/mottosso/Qt.py) and [setuptools](https://github.com/pypa/setuptools)
48 | (can be handled by setup.py), and one of Qt.py's four supported Qt bindings, which will need to be installed separately.
49 |
50 | Additionally, an installation of [USD](https://graphics.pixar.com/usd) itself is recommended but not required for all use cases.
51 | Installing USD provides access to file path resolvers, non-ASCII USD formats, and plug-ins like usdview.
52 | All USD versions should be supported.
53 |
54 | Install with setup.py
55 | ---------------------
56 |
57 | For a site-wide install, try:
58 | ```
59 | python setup.py install
60 | ```
61 |
62 | For a personal install, try:
63 | ```
64 | python setup.py install --user
65 | ```
66 |
67 | Studios with significant python codebases or non-trivial installs may need to customize [setup.py](setup.py).
68 |
69 | Your PATH and PYTHONPATH will need to be set appropriately to launch usdmanager,
70 | and this will depend on your setup.py install settings.
71 |
72 | For more OS-specific installation notes, known issues, and common problems, see [Installing USD Manager](docs/installation.md).
73 |
74 | Using USD Manager
75 | =================
76 |
77 | Once you have installed usdmanager, you can launch from the command line:
78 |
79 | ```
80 | usdmanager
81 | ```
82 |
83 | You can also specify one or more files to open directly:
84 |
85 | ```
86 | usdmanager shot.usd
87 | ```
88 |
89 | For more documentation on usage, see [Using USD Manager](docs/usage.md)
90 |
91 | Keyboard shortcuts
92 | ------------------
93 |
94 | For a full list of keyboard shortcuts, see [Keyboard Shortcuts](docs/keyboardShortcuts.rst)
95 |
96 | Development / Customization
97 | ===========================
98 |
99 | Most customization of the app is through the [usdmanager/config.json](usdmanager/config.json) file.
100 |
101 | For a full list of all customization options, see [Development / Customization](docs/development.md)
102 |
103 | Contributing
104 | ============
105 |
106 | Developers who wish to contribute code to be considered for inclusion in the USD Manager distribution must first
107 | complete the [Contributor License Agreement](http://www.usdmanager.org/USDManagerContributorLicenseAgreement.pdf)
108 | and submit it to DreamWorks (directions in the CLA). We prefer code submissions in the form of pull requests to this
109 | repository.
110 |
111 | _Every commit must be signed off_. That is, every commit log message must include a "`Signed-off-by`" line (generated, for example, with
112 | "`git commit --signoff`"), indicating that the committer wrote the code and has the right to release it under the
113 | [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) license. See http://developercertificate.org/ for more
114 | information on this requirement.
115 |
116 | 1. Fork the repository on GitHub
117 | 2. Clone it locally
118 | 3. Build a local copy
119 | ```
120 | python setup.py install --user
121 | pip install -r docs/requirements.txt
122 | ```
123 | 4. Write code, following the [style guide](docs/contributing.md).
124 | 5. Test it
125 | 6. Update any manual documentation pages (like this one)
126 | 7. Test that the documentation builds without errors with:
127 | ```
128 | sphinx-build -b html docs/ docs/_build
129 | ```
130 | 8. Commit changes to the dev branch, signing off on them per the code signing instructions, then
131 | push the changes to your fork on GitHub
132 | 9. Make a pull request targeting the dev branch
133 |
134 | Pull requests should be rebased on the latest dev commit and squashed to as few logical commits as possible, preferably
135 | one. Each commit should pass tests without requiring further commits.
136 |
--------------------------------------------------------------------------------
/docs/development.md:
--------------------------------------------------------------------------------
1 | # Development / Customization
2 |
3 | Most customization of the app is through the [usdmanager/config.json](https://github.com/dreamworksanimation/usdmanager/blob/master/usdmanager/config.json) file.
4 |
5 | ## Contents
6 |
7 | - [File extensions](#file-extensions)
8 | - [Syntax highlighting](#syntax-highlighting)
9 | - [Plug-ins](#plug-ins)
10 | - [Icons](#icons)
11 |
12 | ## File extensions
13 |
14 | This app supports more than just USD files! It's well suited to display most text-based files, but you need to register
15 | additional file extensions to search for. Non-text files can also be launched via the program of your choice. For
16 | example, .exr files can be launched in your preferred image viewer, and .abc model files in a modeling playback tool
17 | like usdview. To register files, define a "defaultPrograms" dictionary in the app config file.
18 | The dictionary keys are file extensions (without the starting period). The value is either a blank string, which means
19 | files of this type will be opened in this app, or a string to a command to run with the file path appended. USD file
20 | types are already included, so you don't need to redefine these.
21 |
22 | Additional default app settings can be optionally overridden via the [app config file](https://github.com/dreamworksanimation/usdmanager/blob/master/usdmanager/config.json).
23 | Supported keys include:
24 | - **appURL _(str)_** - Documentation URL. Defaults to the public GitHub repository.
25 | - **defaultPrograms _({str: str})_** - File extension keys with the command to open the file type as the values.
26 | - **diffTool _(str)_** - Diff command. Defaults to xdiff.
27 | - **iconTheme _(str)_** - QtGui.QIcon theme name. Defaults to crystal_project. Can be overridden by iconThemes for a
28 | specific app theme.
29 | - **iconThemes _(dict)_** - QtGui.QIcon theme name to use per theme the app supports ("light" and "dark")
30 | - **textEditor _(str)_** - Text editor to use when opening files externally if $EDITOR environment variable is
31 | not set. Defaults to nedit.
32 | - **themeSearchPaths _([str])_** - Paths to prepend to QtGui.QIcon's theme search paths.
33 | - **usdview** - Command to launch usdview.
34 |
35 | Example app config JSON file:
36 | ```
37 | {
38 | "defaultPrograms": {
39 | "txt": "",
40 | "log": "",
41 | "exr": "r_view",
42 | "tif": "r_view",
43 | "tiff": "r_view",
44 | "abc": "usdview",
45 | "tx": "rez-run openimageio_arras -- iv"
46 | },
47 | "diffTool": "python /usr/bin/meld",
48 | "iconThemes": {
49 | "light": "crystal_project",
50 | "dark": "gnome"
51 | },
52 | "textEditor": "gedit",
53 | "themeSearchPaths": []
54 | }
55 | ```
56 |
57 | ## Language-specific parsing
58 |
59 | When default path parsing logic is not good enough, you can add unique parsing for file types by subclassing the
60 | [parser.AbstractExtParser](https://github.com/dreamworksanimation/usdmanager/blob/master/usdmanager/parser.py).
61 | Save your new class in a file in the parsers directory. Set the class's extensions tuple (e.g. (".html", ".xml")) for a
62 | simple list of file extensions to match, or override the acceptsFile method for more advanced control.
63 |
64 | Within each parser, you can define custom menu items that will be added to the bottom of the Commands menu whenever a
65 | parser is active. For example, the [USD parser](https://github.com/dreamworksanimation/usdmanager/blob/master/usdmanager/parsers/usd.py)
66 | adds an "Open in usdview..." action.
67 |
68 | ## Syntax highlighting
69 |
70 | To add syntax highlighting for additional languages, subclass the
71 | [highlighter.MasterHighlighter](https://github.com/dreamworksanimation/usdmanager/blob/master/usdmanager/highlighter.py) class and save your new class in a file in the highlighters
72 | directory. Set the class's extensions list variable to the languages this highlighter supports (e.g.
73 | [".html", ".xml"]). Already supported languages include [USD](https://github.com/dreamworksanimation/usdmanager/blob/master/usdmanager/highlighters/usd.py),
74 | [Lua](https://github.com/dreamworksanimation/usdmanager/blob/master/usdmanager/highlighters/lua.py), [Python](https://github.com/dreamworksanimation/usdmanager/blob/master/usdmanager/highlighters/python.py), and some basic
75 | [HTML/XML](https://github.com/dreamworksanimation/usdmanager/blob/master/usdmanager/highlighters/xml.py).
76 |
77 | ## Plug-ins
78 |
79 | Plug-ins can be added via the plugins directory. Create a new module in the [plugins](https://github.com/dreamworksanimation/usdmanager/blob/master/usdmanager/plugins) directory
80 | (e.g. my_plugin.py) and define a class that inherits from the Plugin class. This allows you to add your own menu items,
81 | customize the UI, etc.
82 |
83 | Please be careful if you access, override or define anything in the main window, as future release may break the
84 | functionality you added!
85 |
86 | Example plug-in file:
87 | ```
88 | from Qt.QtCore import Slot
89 | from Qt.QtGui import QIcon
90 | from Qt.QtWidgets import QMenu
91 |
92 | from . import Plugin, images_rc
93 |
94 |
95 | class CustomExample(Plugin):
96 | def __init__(self, parent):
97 | """ Initialize my custom plugin.
98 |
99 | :Parameters:
100 | parent : `UsdMngrWindow`
101 | Main window
102 | """
103 | super(CustomExample, self).__init__(parent)
104 |
105 | # Setup UI.
106 | menu = QMenu("My Menu", parent)
107 | parent.menubar.insertMenu(parent.helpMenu.menuAction(), menu)
108 |
109 | self.customAction = menu.addAction(QIcon(":plugins/custom_icon.png"), "Do something")
110 | self.customAction.triggered.connect(self.doSomething)
111 |
112 | @Slot(bool)
113 | def doSomething(self, checked=False):
114 | """ Do something.
115 |
116 | :Parameters:
117 | checked : `bool`
118 | For slot connection only.
119 | """
120 | print("Doing something")
121 | ```
122 |
123 | ## Icons
124 |
125 | Some icons in the app come from themes pre-installed on your system, ideally following the
126 | [freedesktop.org standards](https://standards.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html).
127 | The preferred icon set that usdmanager was originally developed with is Crystal Project Icons. These icons are licensed
128 | under LGPL and available via PyPI and GitHub here: https://github.com/mds-dwa/crystal-small. While not required for
129 | the application to work, if you would like these icons to get the most out of the application, please ensure crystal-small
130 | is installed via pip (already part of the default setup) or install them to a directory named crystal_project under one
131 | of the directories listed by `Qt.QtGui.QIcon.themeSearchPaths()` (e.g. /usr/share/icons/crystal_project).
132 |
133 | Additional icons for custom plug-ins can be placed in the plugins directory and then added to the
134 | [usdmanager/plugins/images.qrc](https://github.com/dreamworksanimation/usdmanager/blob/master/usdmanager/plugins/images.qrc) file. After adding a file to images.rc, run the
135 | following to update [usdmanager/plugins/images_rc.py](https://github.com/dreamworksanimation/usdmanager/blob/master/usdmanager/plugins/images_rc.py):
136 |
137 | ```
138 | pyrcc4 usdmanager/plugins/images.rc > 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 |
--------------------------------------------------------------------------------